diff --git a/.gitignore b/.gitignore index 4994a6e12..bdd7549ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,20 @@ -node_modules +# Dependency directories +node_modules/ + +# Logs yarn-error.log + +# IntelliJ +.idea + +# VS Code +.vscode/* + +# Generated files +*.theia + + +# MacOS .DS_Store .theia .idea diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-api-provider.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-api-provider.ts index f387acdd6..b00e4a9c6 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-api-provider.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-api-provider.ts @@ -16,6 +16,7 @@ import { CheWorkspaceMainImpl } from './che-workspace-main'; import { CheFactoryMainImpl } from './che-factory-main'; import { CheVariablesMainImpl } from './che-variables-main'; import { CheTaskMainImpl } from './che-task-main'; +import { CheSshMainImpl } from './che-ssh-main'; @injectable() export class CheApiProvider implements MainPluginApiProvider { @@ -25,6 +26,7 @@ export class CheApiProvider implements MainPluginApiProvider { rpc.set(PLUGIN_RPC_CONTEXT.CHE_FACTORY_MAIN, new CheFactoryMainImpl(container)); rpc.set(PLUGIN_RPC_CONTEXT.CHE_VARIABLES_MAIN, new CheVariablesMainImpl(container, rpc)); rpc.set(PLUGIN_RPC_CONTEXT.CHE_TASK_MAIN, new CheTaskMainImpl(container, rpc)); + rpc.set(PLUGIN_RPC_CONTEXT.CHE_SSH_MAIN, new CheSshMainImpl(container)); } } diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-ssh-main.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-ssh-main.ts new file mode 100644 index 000000000..ad1f43152 --- /dev/null +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-ssh-main.ts @@ -0,0 +1,43 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import { che as cheApi } from '@eclipse-che/api'; +import { interfaces } from 'inversify'; +import { CheApiService, CheSshMain } from '../common/che-protocol'; + +export class CheSshMainImpl implements CheSshMain { + + private readonly cheApiService: CheApiService; + + constructor(container: interfaces.Container) { + this.cheApiService = container.get(CheApiService); + } + + async $generate(service: string, name: string): Promise { + return this.cheApiService.generateSshKey(service, name); + } + + async $create(sshKeyPair: cheApi.ssh.SshPair): Promise { + return this.cheApiService.createSshKey(sshKeyPair); + } + + async $get(service: string, name: string): Promise { + return this.cheApiService.getSshKey(service, name); + } + + async $getAll(service: string): Promise { + return this.cheApiService.getAllSshKey(service); + } + + async $deleteKey(service: string, name: string): Promise { + return this.cheApiService.deleteSshKey(service, name); + } + +} diff --git a/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts b/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts index 85a4b6d95..7122e3ae3 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts @@ -42,6 +42,17 @@ export interface CheFactoryMain { $getFactoryById(factoryId: string): Promise; } +export interface CheSsh { +} + +export interface CheSshMain { + $generate(service: string, name: string): Promise; + $create(sshKeyPair: cheApi.ssh.SshPair): Promise; + $get(service: string, name: string): Promise; + $getAll(service: string): Promise; + $deleteKey(service: string, name: string): Promise; +} + /** * Variables plugin API */ @@ -336,6 +347,9 @@ export const PLUGIN_RPC_CONTEXT = { CHE_VARIABLES_MAIN: >createProxyIdentifier('CheVariablesMain'), CHE_TASK: >createProxyIdentifier('CheTask'), CHE_TASK_MAIN: >createProxyIdentifier('CheTaskMain'), + + CHE_SSH: >createProxyIdentifier('CheSsh'), + CHE_SSH_MAIN: >createProxyIdentifier('CheSshMain'), }; // Theia RPC protocol @@ -362,6 +376,11 @@ export interface CheApiService { deleteUserPreferences(): Promise; deleteUserPreferences(list: string[] | undefined): Promise; + generateSshKey(service: string, name: string): Promise; + createSshKey(sshKeyPair: cheApi.ssh.SshPair): Promise; + getSshKey(service: string, name: string): Promise; + deleteSshKey(service: string, name: string): Promise; + getAllSshKey(service: string): Promise; } export const CHE_TASK_SERVICE_PATH = '/che-task-service'; diff --git a/extensions/eclipse-che-theia-plugin-ext/src/node/che-api-service.ts b/extensions/eclipse-che-theia-plugin-ext/src/node/che-api-service.ts index 96916dadd..9fbe8dc0d 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/node/che-api-service.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/node/che-api-service.ts @@ -109,6 +109,74 @@ export class CheApiServiceImpl implements CheApiService { } } + async generateSshKey(service: string, name: string): Promise { + try { + const client = await this.getCheApiClient(); + if (client) { + return client.generateSshKey(service, name); + } + + throw new Error(`Unable to generate SSH Key for ${service}:${name}`); + } catch (e) { + console.error(e); + throw new Error(e); + } + } + + async createSshKey(sshKeyPair: cheApi.ssh.SshPair): Promise { + try { + const client = await this.getCheApiClient(); + if (client) { + return client.createSshKey(sshKeyPair); + } + + throw new Error('Unable to create SSH Key'); + } catch (e) { + console.error(e); + throw new Error(e); + } + } + + async getSshKey(service: string, name: string): Promise { + try { + const client = await this.getCheApiClient(); + if (client) { + return await client.getSshKey(service, name); + } + + throw new Error(`Unable to get SSH Key for ${service}:${name}`); + } catch (e) { + console.error(e); + throw new Error(e); + } + } + + async getAllSshKey(service: string): Promise { + try { + const client = await this.getCheApiClient(); + if (client) { + return client.getAllSshKey(service); + } + throw new Error(`Unable to get SSH Keys for ${service}`); + } catch (e) { + console.error(e); + throw new Error(e); + } + } + + async deleteSshKey(service: string, name: string): Promise { + try { + const client = await this.getCheApiClient(); + if (client) { + return client.deleteSshKey(service, name); + } + throw new Error(`Unable to delete SSH Key for ${service}:${name}`); + } catch (e) { + console.error(e); + throw new Error(e); + } + } + private async getCheApiClient(): Promise { const cheApiInternalVar = process.env.CHE_API_INTERNAL; const cheMachineToken = process.env.CHE_MACHINE_TOKEN; diff --git a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-api.ts b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-api.ts index b06f5e757..d9d6154c6 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-api.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-api.ts @@ -17,6 +17,7 @@ import { CheVariablesImpl } from './che-variables'; import { PLUGIN_RPC_CONTEXT } from '../common/che-protocol'; import { CheFactoryImpl } from './che-factory'; import { CheTaskImpl } from './che-task-impl'; +import { CheSshImpl } from './che-ssh'; export interface CheApiFactory { (plugin: Plugin): typeof che; @@ -27,6 +28,7 @@ export function createAPIFactory(rpc: RPCProtocol): CheApiFactory { const cheFactoryImpl = rpc.set(PLUGIN_RPC_CONTEXT.CHE_FACTORY, new CheFactoryImpl(rpc)); const cheVariablesImpl = rpc.set(PLUGIN_RPC_CONTEXT.CHE_VARIABLES, new CheVariablesImpl(rpc)); const cheTaskImpl = rpc.set(PLUGIN_RPC_CONTEXT.CHE_TASK, new CheTaskImpl(rpc)); + const cheSshImpl = rpc.set(PLUGIN_RPC_CONTEXT.CHE_SSH, new CheSshImpl(rpc)); return function (plugin: Plugin): typeof che { const workspace: typeof che.workspace = { @@ -86,6 +88,25 @@ export function createAPIFactory(rpc: RPCProtocol): CheApiFactory { } }; + const ssh: typeof che.ssh = { + deleteKey(service: string, name: string): Promise { + return cheSshImpl.delete(service, name); + }, + generate(service: string, name: string): Promise { + return cheSshImpl.generate(service, name); + + }, + create(sshKeyPair: cheApi.ssh.SshPair): Promise { + return cheSshImpl.create(sshKeyPair); + }, + getAll(service: string): Promise { + return cheSshImpl.getAll(service); + }, + get(service: string, name: string): Promise { + return cheSshImpl.get(service, name); + } + }; + const task: typeof che.task = { registerTaskRunner(type: string, runner: che.TaskRunner): Promise { return cheTaskImpl.registerTaskRunner(type, runner); @@ -99,7 +120,8 @@ export function createAPIFactory(rpc: RPCProtocol): CheApiFactory { workspace, factory, variables, - task + task, + ssh }; }; diff --git a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-ssh.ts b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-ssh.ts new file mode 100644 index 000000000..a7ae26df9 --- /dev/null +++ b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-ssh.ts @@ -0,0 +1,78 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import { RPCProtocol } from '@theia/plugin-ext/lib/api/rpc-protocol'; +import { PLUGIN_RPC_CONTEXT, CheSshMain, CheSsh } from '../common/che-protocol'; +import { che as cheApi } from '@eclipse-che/api'; + +export class CheSshImpl implements CheSsh { + + private readonly sshMain: CheSshMain; + + constructor(rpc: RPCProtocol) { + this.sshMain = rpc.getProxy(PLUGIN_RPC_CONTEXT.CHE_SSH_MAIN); + } + + /** + * @inheritDoc + */ + async generate(service: string, name: string): Promise { + try { + return this.sshMain.$generate(service, name); + } catch (e) { + throw new Error(e); + } + } + + /** + * @inheritDoc + */ + async create(sshKeyPair: cheApi.ssh.SshPair): Promise { + try { + return this.sshMain.$create(sshKeyPair); + } catch (e) { + throw new Error(e); + } + } + + /** + * @inheritDoc + */ + async getAll(service: string): Promise { + try { + return this.sshMain.$getAll(service); + } catch (e) { + throw new Error(e); + } + } + + /** + * @inheritDoc + */ + async get(service: string, name: string): Promise { + try { + return this.sshMain.$get(service, name); + } catch (e) { + throw new Error(e); + } + } + + /** + * @inheritDoc + */ + async delete(service: string, name: string): Promise { + try { + return this.sshMain.$deleteKey(service, name); + } catch (e) { + throw new Error(e); + } + } + +} diff --git a/extensions/eclipse-che-theia-plugin-ext/webpack.config.js b/extensions/eclipse-che-theia-plugin-ext/webpack.config.js index 6aaaf31aa..55483049e 100644 --- a/extensions/eclipse-che-theia-plugin-ext/webpack.config.js +++ b/extensions/eclipse-che-theia-plugin-ext/webpack.config.js @@ -22,6 +22,9 @@ module.exports = { use: [ { loader: 'ts-loader', + options: { + transpileOnly: true + } } ], exclude: /node_modules/ diff --git a/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts b/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts index c350dde70..394785fab 100644 --- a/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts +++ b/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts @@ -39,6 +39,18 @@ declare module '@eclipse-che/plugin' { export function getById(id: string): PromiseLike; } + export namespace ssh { + export function generate(service: string, name: string): Promise; + + export function create(sshKeyPair: cheApi.ssh.SshPair): Promise; + + export function get(service: string, name: string): Promise; + + export function getAll(service: string): Promise; + + export function deleteKey(service: string, name: string): Promise; + } + /** * Namespace for variables substitution functionality. */ diff --git a/plugins/ssh-plugin/.gitignore b/plugins/ssh-plugin/.gitignore new file mode 100644 index 000000000..c66630b3c --- /dev/null +++ b/plugins/ssh-plugin/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +lib/ diff --git a/plugins/ssh-plugin/README.md b/plugins/ssh-plugin/README.md new file mode 100644 index 000000000..6508974ed --- /dev/null +++ b/plugins/ssh-plugin/README.md @@ -0,0 +1,15 @@ +# Theia - SSH Plug-in + +The SSH Plug-in allows to manage the SSH public/private key pairs stored in Che workspace. + +![Theia](https://user-images.githubusercontent.com/7668752/46194687-ca8b0d80-c30a-11e8-9eb1-fe8e6232486c.png) + +Adds the following commands (shortcut `F1`): +- SSH: generate key pair... +- SSH: create key pair... +- SSH: view public key... +- SSH: delete key pair... + +## License + +[EPL-2.0](http://www.eclipse.org/legal/epl-2.0) diff --git a/plugins/ssh-plugin/package.json b/plugins/ssh-plugin/package.json new file mode 100644 index 000000000..78347dd50 --- /dev/null +++ b/plugins/ssh-plugin/package.json @@ -0,0 +1,47 @@ +{ + "name": "@eclipse-che/theia-ssh-plugin", + "keywords": [ + "theia-plugin" + ], + "version": "0.0.1", + "license": "EPL-2.0", + "contributors": [ + { + "name": "Artem Zatsarynnyi", + "email": "azatsary@redhat.com" + }, + { + "name": "Igor Vinokur", + "email": "ivinokur@redhat.com" + }, + { + "name": "Vitalii Parfonov", + "email": "vparfono@redhat.com" + } + ], + "files": [ + "src" + ], + "devDependencies": { + "@eclipse-che/api": "latest", + "@eclipse-che/plugin": "0.0.1", + "@theia/plugin": "next", + "@theia/plugin-packager": "latest" + }, + "scripts": { + "prepare": "yarn run clean && yarn run build", + "clean": "rimraf lib", + "format": "tsfmt -r --useTsfmt ../../configs/tsfmt.json", + "watch": "tsc -watch", + "compile": "tsc", + "lint": "tslint -c ../../configs/tslint.json --project tsconfig.json", + "lint:fix": "tslint -c ../../configs/tslint.json --fix --project .", + "build": "concurrently -n \"format,lint,compile\" -c \"red,green,blue\" \"yarn format\" \"yarn lint\" \"yarn compile\" && theia:plugin pack" + }, + "engines": { + "theiaPlugin": "next" + }, + "theiaPlugin": { + "backend": "lib/ssh-plugin-backend.js" + } +} diff --git a/plugins/ssh-plugin/src/node/ssh-key-manager.ts b/plugins/ssh-plugin/src/node/ssh-key-manager.ts new file mode 100644 index 000000000..3085c38c6 --- /dev/null +++ b/plugins/ssh-plugin/src/node/ssh-key-manager.ts @@ -0,0 +1,122 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import { che as cheApi } from '@eclipse-che/api'; +import * as che from '@eclipse-che/plugin'; + +/** + * Simple SSH key pairs manager that performs basic operations like create, + * get, delete, etc. There is no restriction on the way the keys are obtained - + * remotely (via REST or JSON-RPC, ) or locally (e.g. dynamically generated + * and/or in-memory stored), so the implementation of the interface defines + * the mechanism that is used. + */ +export interface SshKeyManager { + + /** + * Generate an SSH key pair for specified service and name + * + * @param {string} service the name of the service that is associated with + * the SSH key pair + * @param {string} name the identifier of the key pair + * + * @returns {Promise} + */ + generate(service: string, name: string): Promise; + + /** + * Create a specified SSH key pair + * + * @param {SshKeyPair} sshKeyPair the SSH key pair that is to be created + * + * @returns {Promise} + */ + create(sshKeyPair: cheApi.ssh.SshPair): Promise; + + /** + * Get all SSH key pairs associated with specified service + * + * @param {string} service the name of the service that is associated with + * the SSH key pair + * + * @returns {Promise} + */ + getAll(service: string): Promise; + + /** + * Get an SSH key pair associated with specified service and name + * + * @param {string} service the name of the service that is associated with + * the SSH key pair + * @param {string} name the identifier of the key pair + * + * @returns {Promise} + */ + get(service: string, name: string): Promise; + + /** + * Delete an SSH key pair with a specified service and name + * + * @param {string} service the name of the service that is associated with + * the SSH key pair + * @param {string} name the identifier of the key pair + * + * @returns {Promise} + */ + delete(service: string, name: string): Promise; +} + +export interface CheService { + name: string, + displayName: string, + description: string +} + +/** + * A remote SSH key paris manager that uses {@link SshKeyServiceClient} for + * all SHH key related operations. + */ +export class RemoteSshKeyManager implements SshKeyManager { + + /** + * @inheritDoc + */ + generate(service: string, name: string): Promise { + return che.ssh.generate(service, name); + } + + /** + * @inheritDoc + */ + create(sshKeyPair: cheApi.ssh.SshPair): Promise { + return che.ssh.create(sshKeyPair); + } + + /** + * @inheritDoc + */ + getAll(service: string): Promise { + return che.ssh.getAll(service); + } + + /** + * @inheritDoc + */ + get(service: string, name: string): Promise { + return che.ssh.get(service, name); + } + + /** + * @inheritDoc + */ + delete(service: string, name: string): Promise { + return che.ssh.deleteKey(service, name); + } +} diff --git a/plugins/ssh-plugin/src/ssh-plugin-backend.ts b/plugins/ssh-plugin/src/ssh-plugin-backend.ts new file mode 100644 index 000000000..75a7e4b9a --- /dev/null +++ b/plugins/ssh-plugin/src/ssh-plugin-backend.ts @@ -0,0 +1,143 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import * as theia from '@theia/plugin'; +import { RemoteSshKeyManager, SshKeyManager, CheService } from './node/ssh-key-manager'; +import { che as cheApi } from '@eclipse-che/api'; + +export async function start() { + const sshKeyManager = new RemoteSshKeyManager(); + const GENERATE: theia.Command = { + id: 'ssh:generate', + label: 'SSH: generate key pair...' + }; + const CREATE: theia.Command = { + id: 'ssh:create', + label: 'SSH: create key pair...' + }; + const DELETE: theia.Command = { + id: 'ssh:delete', + label: 'SSH: delete key pair...' + }; + const VIEW: theia.Command = { + id: 'ssh:view', + label: 'SSH: view public key...' + }; + + theia.commands.registerCommand(GENERATE, () => { + generateKeyPair(sshKeyManager); + }); + theia.commands.registerCommand(CREATE, () => { + createKeyPair(sshKeyManager); + }); + theia.commands.registerCommand(DELETE, () => { + deleteKeyPair(sshKeyManager); + }); + theia.commands.registerCommand(VIEW, () => { + viewPublicKey(sshKeyManager); + }); +} + +const getSshServiceName = async function (): Promise { + /** + * Known Che services which can use the SSH key pairs. + */ + const services: CheService[] = [ + { + name: 'vcs', + displayName: 'VCS', + description: 'SSH keys used by Che VCS plugins' + }, + { + name: 'machine', + displayName: 'Workspace Containers', + description: 'SSH keys injected into all Workspace Containers' + } + ]; + + const option: theia.QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + canPickMany: false, + placeHolder: 'Select object:' + }; + const sshServiceValue = await theia.window.showQuickPick(services.map(service => + ({ + label: service.displayName, + description: service.description, + detail: service.name, + name: service.name + })), option); + if (sshServiceValue) { + return Promise.resolve(sshServiceValue.label); + } else { + return Promise.resolve(''); + } +}; + +const generateKeyPair = async function (sshkeyManager: SshKeyManager): Promise { + const keyName = await theia.window.showInputBox({ placeHolder: 'Please provide a key pair name' }); + const key = await sshkeyManager.generate(await getSshServiceName(), keyName ? keyName : ''); + const viewAction = 'View'; + const action = await theia.window.showInformationMessage('Do you want to view the generated private key?', viewAction); + if (action === viewAction && key.privateKey) { + theia.workspace.openTextDocument({ content: key.privateKey }); + } +}; + +const createKeyPair = async function (sshkeyManager: SshKeyManager): Promise { + const keyName = await theia.window.showInputBox({ placeHolder: 'Please provide a key pair name' }); + const publicKey = await theia.window.showInputBox({ placeHolder: 'Enter public key' }); + + await sshkeyManager + .create({ name: keyName ? keyName : '', service: await getSshServiceName(), publicKey: publicKey }) + .then(() => { + theia.window.showInformationMessage('Key "' + `${keyName}` + '" successfully created'); + }) + .catch(error => { + theia.window.showErrorMessage(error); + }); +}; + +const deleteKeyPair = async function (sshkeyManager: SshKeyManager): Promise { + const sshServiceName = await getSshServiceName(); + const keys: cheApi.ssh.SshPair[] = await sshkeyManager.getAll(sshServiceName); + const keyResp = await theia.window.showQuickPick(keys.map(key => + ({ label: key.name ? key.name : '' })), {}); + const keyName = keyResp ? keyResp.label : ''; + await sshkeyManager + .delete(sshServiceName, keyName) + .then(() => { + theia.window.showInformationMessage('Key "' + `${keyName}` + '" successfully deleted'); + }) + .catch(error => { + theia.window.showErrorMessage(error); + }); +}; + +const viewPublicKey = async function (sshkeyManager: SshKeyManager): Promise { + const sshServiceName = await getSshServiceName(); + const keys: cheApi.ssh.SshPair[] = await sshkeyManager.getAll(sshServiceName); + const keyResp = await theia.window.showQuickPick(keys.map(key => + ({ label: key.name ? key.name : '' })), {}); + const keyName = keyResp ? keyResp.label : ''; + await sshkeyManager + .get(sshServiceName, keyName) + .then(key => { + theia.workspace.openTextDocument({ content: key.publicKey }); + }) + .catch(error => { + theia.window.showErrorMessage(error); + }); +}; + +export function stop() { + +} diff --git a/plugins/ssh-plugin/tsconfig.json b/plugins/ssh-plugin/tsconfig.json new file mode 100644 index 000000000..c5a4f3c5a --- /dev/null +++ b/plugins/ssh-plugin/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "target": "es5", + "lib": [ + "es6", + "webworker" + ], + "sourceMap": true, + "rootDir": "src", + "outDir": "lib", + "types": [ + "node", "jest" + ] + }, + "include": [ + "src" + ] +}