├── .tool-versions ├── Makefile ├── .gitignore ├── images ├── icon.png └── explorer.png ├── resources ├── service-exit.png ├── service-up-healthy.png ├── service-up-unhealthy.png ├── dark │ └── refresh.svg └── light │ └── refresh.svg ├── .azure-pipelines ├── steps │ ├── test.yml │ ├── build-prepare.yml │ ├── build.yml │ └── test-linux.yml └── main.yml ├── .vscodeignore ├── src ├── types │ └── environment.d.ts ├── containers │ ├── enums.ts │ ├── models.ts │ └── views.ts ├── telemetry │ ├── nullClient.ts │ └── appInsightsClient.ts ├── configurators │ ├── workspaceConfigurator.ts │ └── models.ts ├── enums.ts ├── test │ ├── suite │ │ ├── extension.test.ts │ │ └── index.ts │ └── runTest.ts ├── executors │ ├── exceptions.ts │ ├── commandExecutor.ts │ ├── dockerExecutor.ts │ └── composeExecutor.ts ├── explorers │ ├── views.ts │ └── providers.ts ├── services │ ├── views.ts │ └── models.ts ├── compose │ └── views.ts ├── projects │ ├── views.ts │ └── models.ts └── extension.ts ├── .bumpversion.cfg ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── tsconfig.json ├── .eslintrc.js ├── LICENSE ├── CHANGELOG.md ├── README.md ├── vsc-extension-quickstart.md └── package.json /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.2.0 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | publish: 2 | vsce publish 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | .vsix 5 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p1c2u/vscode-docker-compose/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p1c2u/vscode-docker-compose/HEAD/images/explorer.png -------------------------------------------------------------------------------- /resources/service-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p1c2u/vscode-docker-compose/HEAD/resources/service-exit.png -------------------------------------------------------------------------------- /.azure-pipelines/steps/test.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: 'node ./out/test/runTest.js' 3 | displayName: 'Test: Run' 4 | -------------------------------------------------------------------------------- /resources/service-up-healthy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p1c2u/vscode-docker-compose/HEAD/resources/service-up-healthy.png -------------------------------------------------------------------------------- /resources/service-up-unhealthy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p1c2u/vscode-docker-compose/HEAD/resources/service-up-unhealthy.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | .gitignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | -------------------------------------------------------------------------------- /.azure-pipelines/steps/build-prepare.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: NodeTool@0 3 | displayName: 'Use Node 8.x' 4 | inputs: 5 | versionSpec: 8.11.4 6 | -------------------------------------------------------------------------------- /src/types/environment.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | namespace NodeJS { 5 | interface ProcessEnv { 6 | COMPOSE_PROJECT_NAME?: string; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/containers/enums.ts: -------------------------------------------------------------------------------- 1 | export enum ContainerState { 2 | Paused = "Paused", 3 | restarting = "Restarting", 4 | Ghost = "Ghost", 5 | Up = "Up", 6 | Exit = "Exit", 7 | } 8 | -------------------------------------------------------------------------------- /.azure-pipelines/steps/build.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: Npm@1 3 | displayName: 'Source: Install dependencies' 4 | 5 | - task: Npm@1 6 | displayName: 'Source: Build' 7 | inputs: 8 | command: custom 9 | customCommand: run compile -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.5.1 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+) 6 | serialize = 7 | {major}.{minor}.{patch} 8 | 9 | [bumpversion:file:package.json] 10 | -------------------------------------------------------------------------------- /.azure-pipelines/steps/test-linux.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | set -e 4 | /usr/bin/Xvfb :10 -ac >> /tmp/Xvfb.out 2>&1 & 5 | disown -ar 6 | displayName: 'Start xvfb' 7 | 8 | - script: 'node ./out/test/runTest.js' 9 | displayName: 'Test: Run' 10 | env: 11 | DISPLAY: :10 -------------------------------------------------------------------------------- /src/telemetry/nullClient.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export class NullClient { 4 | 5 | constructor() { 6 | // do nothing. 7 | } 8 | 9 | public sendEvent(eventName: string, properties?: { [key: string]: string; }): void { 10 | // do nothing. 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/configurators/workspaceConfigurator.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import * as vscode from "vscode"; 3 | 4 | export class WorkspaceConfigurator { 5 | 6 | public static getConfiguration(): vscode.WorkspaceConfiguration { 7 | return vscode.workspace.getConfiguration("docker-compose"); 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | } 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2018", 5 | "outDir": "out", 6 | "lib": [ 7 | "es7" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "alwaysStrict": true, 12 | "noFallthroughCasesInSwitch": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/enums.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export enum ResourceType { 4 | Message = "docker-compose:message", 5 | Projects = "docker-compose:projects", 6 | Project = "docker-compose:project", 7 | Services = "docker-compose:services", 8 | Service = "docker-compose:service", 9 | Container = "docker-compose:container", 10 | RunningContainer = "docker-compose:running-container", 11 | ExitedContainer = "docker-compose:exited-container" 12 | } 13 | -------------------------------------------------------------------------------- /src/telemetry/appInsightsClient.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import appInsights = require("applicationinsights"); 3 | 4 | export class AppInsightsClient { 5 | 6 | private client; 7 | 8 | constructor(hash: string) { 9 | this.client = new appInsights.TelemetryClient(hash); 10 | } 11 | 12 | public sendEvent(eventName: string, properties?: { [key: string]: string; }): void { 13 | this.client.trackEvent({name: eventName, properties: properties}); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /.azure-pipelines/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: Windows 3 | pool: 4 | vmImage: 'windows-2019' 5 | 6 | steps: 7 | - template: steps/build.yml 8 | - template: steps/test.yml 9 | 10 | - job: macOS 11 | pool: 12 | vmImage: 'macOS-13' 13 | 14 | steps: 15 | - template: steps/build.yml 16 | - template: steps/test.yml 17 | 18 | - job: Linux 19 | pool: 20 | vmImage: 'ubuntu-20.04' 21 | 22 | steps: 23 | - template: steps/build.yml 24 | - template: steps/test-linux.yml 25 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual([1, 2, 3].indexOf(5), -1); 13 | assert.strictEqual([1, 2, 3].indexOf(0), -1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, "always"], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/configurators/models.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export interface IDockerComposeConfig { 4 | enabled: boolean; 5 | projectName: string; 6 | autoRefreshInterval: number; 7 | showDockerCompose: boolean; 8 | enableTelemetry: boolean; 9 | shell: string; 10 | files: string[]; 11 | } 12 | 13 | export const emptyConfig: IDockerComposeConfig = { 14 | enabled: true, 15 | projectName: vscode.workspace.rootPath, 16 | autoRefreshInterval: 10000, 17 | showDockerCompose: true, 18 | enableTelemetry: false, 19 | shell: "/bin/sh", 20 | files: ["docker-compose.yml"] 21 | }; 22 | -------------------------------------------------------------------------------- /src/executors/exceptions.ts: -------------------------------------------------------------------------------- 1 | export class ExecutorError extends Error { 2 | constructor(public message: string, protected output: string) { 3 | super(); 4 | } 5 | } 6 | 7 | export class DockerExecutorError extends ExecutorError { 8 | 9 | } 10 | 11 | export class DockerUnhandledError extends DockerExecutorError { 12 | 13 | } 14 | 15 | export class ComposeExecutorError extends ExecutorError { 16 | 17 | } 18 | 19 | export class ComposeCommandNotFound extends ComposeExecutorError { 20 | 21 | } 22 | 23 | export class ComposeFileNotFound extends ComposeExecutorError { 24 | 25 | } 26 | 27 | export class ComposeUnhandledError extends ComposeExecutorError { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error(err); 19 | console.error('Failed to run tests'); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | main(); 25 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd' 9 | }); 10 | 11 | const testsRoot = path.resolve(__dirname, '..'); 12 | 13 | return new Promise((c, e) => { 14 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 15 | if (err) { 16 | return e(err); 17 | } 18 | 19 | // Add files to the test suite 20 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 21 | 22 | try { 23 | // Run the mocha test 24 | mocha.run(failures => { 25 | if (failures > 0) { 26 | e(new Error(`${failures} tests failed.`)); 27 | } else { 28 | c(); 29 | } 30 | }); 31 | } catch (err) { 32 | console.error(err); 33 | e(err); 34 | } 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /resources/dark/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | }, 19 | { 20 | "type": "typescript", 21 | "tsconfig": "tsconfig.json", 22 | "problemMatcher": [ 23 | "$tsc" 24 | ] 25 | }, 26 | { 27 | "type": "npm", 28 | "script": "clean", 29 | "path": ".vscode-test/VSCode-linux-x64/resources/app/extensions/json-language-features/server/", 30 | "problemMatcher": [] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 A 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/explorers/views.ts: -------------------------------------------------------------------------------- 1 | import { Command, Disposable, EventEmitter, ExtensionContext, TreeItem, TreeView } from 'vscode'; 2 | 3 | export abstract class ExplorerNode extends Disposable { 4 | 5 | protected children: ExplorerNode[] | undefined; 6 | protected disposable: Disposable | undefined; 7 | 8 | constructor( 9 | public readonly context: ExtensionContext 10 | ) { 11 | super(() => this.dispose()); 12 | } 13 | 14 | dispose() { 15 | if (this.disposable !== undefined) { 16 | this.disposable.dispose(); 17 | this.disposable = undefined; 18 | } 19 | 20 | this.resetChildren(); 21 | } 22 | 23 | abstract getChildren(): ExplorerNode[] | Promise; 24 | abstract getTreeItem(): TreeItem | Promise; 25 | abstract handleError(err: Error): ExplorerNode[] | Promise; 26 | 27 | getCommand(): Command | undefined { 28 | return undefined; 29 | } 30 | 31 | resetChildren() { 32 | if (this.children !== undefined) { 33 | this.children.forEach(c => c.dispose()); 34 | this.children = undefined; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.1 2 | - Docker Compose 2.21 format support 3 | 4 | ## 0.5.0 5 | - projects treeview 6 | - explorer view optimizations 7 | 8 | ## 0.4.2 9 | - containers view retouch 10 | 11 | ## 0.4.1 12 | - Sync vscode engine with types 13 | 14 | ## 0.4.0 15 | - Make docker compose great again 16 | 17 | ## 0.3.5 18 | - Keep environment variables on command execution 19 | - Project name normalisation fix 20 | - Custom container names fix 21 | 22 | ## 0.3.4 23 | - Container healthy icon 24 | 25 | ## 0.3.3 26 | - Windows folders path fix 27 | 28 | ## 0.3.2 29 | - Containers loop fix 30 | - New service icons 31 | 32 | ## 0.3.1 33 | - Find docker compose ps separator fix 34 | 35 | ## 0.3.0 36 | - Better executor exceptions handling 37 | - Refresh nodes on child process exit event 38 | - Container logs command 39 | 40 | ## 0.2.3 41 | - Activity bar 42 | - Activation events fix 43 | 44 | ## 0.2.2 45 | - Commands rename fix 46 | 47 | ## 0.2.1 48 | - List services error handing fix 49 | - Up service attached 50 | 51 | ## 0.2.0 52 | - Tree view explorer 53 | - Multi-root support 54 | - Service's containers support 55 | - Refresh performance increased 56 | 57 | ## 0.1.1 58 | - Attach and shell commands added 59 | 60 | ## 0.1.0 61 | - Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Compose 2 | 3 | Docker Compose Extension for Visual Studio Code 4 | 5 | ## Features 6 | 7 | * Projexts view to manage Compose projects. 8 | * Services view to manage Compose services and containers. 9 | * Multi-folder workspace support. 10 | 11 | ## Requirements 12 | 13 | * [Docker](https://www.docker.com/) 14 | * [Docker Compose](https://docs.docker.com/compose/) 15 | 16 | ## Usage 17 | 18 | * Docker Compose Explorer 19 | 20 | ![explorer](images/explorer.png) 21 | 22 | ## Extension Settings 23 | 24 | The extension contributes the following settings: 25 | 26 | * `docker-compose.autoRefreshInterval`: Interval (in milliseconds) to auto-refresh containers list. Set 0 to disable auto-refresh. (Default is `10000`) 27 | * `docker-compose.projectNames`: Override Docker Compose project name for each workspace root (Default is basename of the workspace directory). 28 | * `docker-compose.showExplorer`: Show Docker Compose explorer view. (Default is `True`) 29 | * `docker-compose.files`: Docker Compose files. You can define array of files. (Default is `["docker-compose.yml"]`). 30 | * `docker-compose.shell`: Specify shell to use inside Docker Container. (Default is `"/bin/sh"`). 31 | 32 | ## Known Issues 33 | 34 | The extension is in experimental stage. Feel free to report bugs. 35 | -------------------------------------------------------------------------------- /src/containers/models.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from "child_process"; 2 | import { ContainerState } from "../containers/enums"; 3 | import { DockerExecutor } from "../executors/dockerExecutor"; 4 | 5 | export class Container { 6 | 7 | constructor( 8 | private readonly executor: DockerExecutor, 9 | public readonly name: string, 10 | public readonly command: string, 11 | public readonly status: string, 12 | public readonly ports: string[] 13 | ) { 14 | } 15 | 16 | get state(): ContainerState { 17 | return this.status.startsWith('Up') ? ContainerState.Up : ContainerState.Exit; 18 | } 19 | 20 | get healthy(): boolean | null { 21 | return this.status.includes('(healthy)') ? true : this.status.includes('(unhealthy)') ? false : null; 22 | } 23 | 24 | public attach(): void { 25 | this.executor.attach(this.name); 26 | } 27 | 28 | public logs(): string { 29 | return this.executor.logs(this.name); 30 | } 31 | 32 | public start(): ChildProcess { 33 | return this.executor.start(this.name); 34 | } 35 | 36 | public stop(): ChildProcess { 37 | return this.executor.stop(this.name); 38 | } 39 | 40 | public kill(): ChildProcess { 41 | return this.executor.kill(this.name); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/services/views.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from "vscode"; 2 | import { ResourceType } from "../enums"; 3 | import { Service } from "../services/models"; 4 | import { ContainerNode } from "../containers/views"; 5 | import { ComposeNode } from "../compose/views"; 6 | 7 | export class ServiceNode extends ComposeNode { 8 | 9 | // iconPath = { 10 | // light: path.join(__filename, '..', '..', '..', 'resources', 'light'), 11 | // dark: path.join(__filename, '..', '..', '..', 'resources', 'dark') 12 | // }; 13 | 14 | constructor( 15 | context: ExtensionContext, 16 | public readonly service: Service 17 | ) { 18 | super(context); 19 | } 20 | 21 | async getChildren(): Promise { 22 | this.resetChildren(); 23 | 24 | const containers = await this.service.getContainers(true); 25 | 26 | const context = this.context; 27 | this.children = containers 28 | .map((container) => new ContainerNode(context, container)); 29 | return this.children; 30 | } 31 | 32 | async getTreeItem(): Promise { 33 | const item = new TreeItem(this.service.name, TreeItemCollapsibleState.Expanded); 34 | // item.iconPath = this.iconPath; 35 | item.contextValue = ResourceType.Service; 36 | return item; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "name": "Launch Extension", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceFolder}" 12 | ], 13 | "outFiles": [ 14 | "${workspaceFolder}/out/**/*.js" 15 | ], 16 | "preLaunchTask": "npm: vscode:prepublish", 17 | "smartStep": true 18 | }, 19 | { 20 | "name": "Extension", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "runtimeExecutable": "${execPath}", 24 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 25 | "stopOnEntry": false, 26 | "sourceMaps": true, 27 | "outFiles": [ "${workspaceRoot}/out/**/*.js" ], 28 | "preLaunchTask": "npm: watch" 29 | }, 30 | { 31 | "name": "Extension Tests", 32 | "type": "extensionHost", 33 | "request": "launch", 34 | "runtimeExecutable": "${execPath}", 35 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test"], 36 | "stopOnEntry": false, 37 | "sourceMaps": true, 38 | "outFiles": [ "${workspaceRoot}/out/test/**/*.js" ], 39 | "preLaunchTask": "npm: test" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/executors/commandExecutor.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { exec, execSync, ChildProcess } from "child_process"; 3 | import * as vscode from "vscode"; 4 | 5 | export class CommandExecutor { 6 | 7 | private terminals: { [id: string]: vscode.Terminal } = {}; 8 | private _cwd: string; 9 | private _env: NodeJS.ProcessEnv; 10 | 11 | constructor(cwd: string = null, env: NodeJS.ProcessEnv = process.env) { 12 | this._cwd = cwd; 13 | this._env = env; 14 | 15 | if ('onDidCloseTerminal' in vscode.window) { 16 | (vscode.window).onDidCloseTerminal((terminal) => { 17 | this.onDidCloseTerminal(terminal); 18 | }); 19 | } 20 | } 21 | 22 | protected getBaseCommand() { 23 | return ''; 24 | } 25 | 26 | public runInTerminal(subCommand: string, addNewLine = true, terminal = "Docker Compose"): void { 27 | const baseCommand = this.getBaseCommand(); 28 | const command = `${baseCommand} ${subCommand}`; 29 | if (this.terminals[terminal] === undefined ) { 30 | this.terminals[terminal] = vscode.window.createTerminal(terminal); 31 | this.terminals[terminal].sendText(command, addNewLine); 32 | } 33 | this.terminals[terminal].show(); 34 | } 35 | 36 | public exec(command: string): ChildProcess { 37 | return exec(command, {env: this._env, cwd: this._cwd, encoding: "utf8" }); 38 | } 39 | 40 | public execSync(command: string) { 41 | return execSync(command, {env: this._env, cwd: this._cwd, encoding: "utf8" }); 42 | } 43 | 44 | public onDidCloseTerminal(closedTerminal: vscode.Terminal): void { 45 | delete this.terminals[closedTerminal.name]; 46 | } 47 | 48 | public execute(subCommand: string): ChildProcess { 49 | const baseCommand = this.getBaseCommand(); 50 | const command = `${baseCommand} ${subCommand}`; 51 | return this.exec(command); 52 | } 53 | 54 | public executeSync(subCommand: string) { 55 | const baseCommand = this.getBaseCommand(); 56 | const command = `${baseCommand} ${subCommand}`; 57 | return this.execSync(command); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | * This folder contains all of the files necessary for your extension. 5 | * `package.json` - this is the manifest file in which you declare your extension and command. 6 | The sample plugin registers a command and defines its title and command name. With this information 7 | VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | The file exports one function, `activate`, which is called the very first time your extension is 10 | activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 11 | We pass the function containing the implementation of the command as the second parameter to 12 | `registerCommand`. 13 | 14 | ## Get up and running straight away 15 | * Press `F5` to open a new window with your extension loaded. 16 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 17 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 18 | * Find output from your extension in the debug console. 19 | 20 | ## Make changes 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | * You can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`. 26 | 27 | ## Run tests 28 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests`. 29 | * Press `F5` to run the tests in a new window with your extension loaded. 30 | * See the output of the test result in the debug console. 31 | * Make changes to `test/extension.test.ts` or create new test files inside the `test` folder. 32 | * By convention, the test runner will only consider files matching the name pattern `**.test.ts`. 33 | * You can create folders inside the `test` folder to structure your tests any way you want. 34 | -------------------------------------------------------------------------------- /src/compose/views.ts: -------------------------------------------------------------------------------- 1 | import { window, ExtensionContext, TreeItem, TreeItemCollapsibleState, ThemeIcon, MarkdownString, ThemeColor } from 'vscode'; 2 | import { ResourceType } from "../enums"; 3 | import { ComposeExecutorError, ComposeCommandNotFound, ComposeFileNotFound, DockerExecutorError } from "../executors/exceptions"; 4 | import { ExplorerNode } from '../explorers/views'; 5 | 6 | export class MessageNode extends ExplorerNode { 7 | 8 | constructor( 9 | public readonly context: ExtensionContext, 10 | private readonly message: string, 11 | private readonly iconId: string | undefined = null, 12 | private readonly iconColorId: string | undefined = null, 13 | private readonly tooltip: string | MarkdownString | undefined = null 14 | ) { 15 | super(context); 16 | } 17 | 18 | getChildren(): ComposeNode[] | Promise { 19 | return []; 20 | } 21 | 22 | getTreeItem(): TreeItem | Promise { 23 | const item = new TreeItem(this.message, TreeItemCollapsibleState.None); 24 | item.contextValue = ResourceType.Message; 25 | if (this.iconId !== undefined) 26 | if (this.iconColorId !== undefined) 27 | item.iconPath = new ThemeIcon(this.iconId, new ThemeColor(this.iconColorId)); 28 | else 29 | item.iconPath = new ThemeIcon(this.iconId); 30 | if (this.tooltip !== undefined) 31 | item.tooltip = this.tooltip; 32 | return item; 33 | } 34 | 35 | handleError(err: Error): ComposeNode[] | Promise { 36 | return []; 37 | } 38 | 39 | } 40 | 41 | export abstract class ComposeNode extends ExplorerNode { 42 | 43 | protected children: ComposeNode[] | undefined; 44 | 45 | handleError(err: Error): MessageNode[] { 46 | let message = 'unexpected error'; 47 | if (err instanceof DockerExecutorError) { 48 | message = 'Failed to execute docker command'; 49 | } else if (err instanceof ComposeFileNotFound) { 50 | message = 'No docker compose file(s)'; 51 | } else if (err instanceof ComposeCommandNotFound) { 52 | message = 'Command docker compose not found'; 53 | } else if (err instanceof ComposeExecutorError) { 54 | message = 'Failed to execute docker compose command'; 55 | } else { 56 | window.showErrorMessage("Docker-Compose Extension Error: " + err.message); 57 | } 58 | return [new MessageNode(this.context, message, 'error', 'problemsErrorIcon.foreground', err.message)]; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/projects/views.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState, ExtensionContext, ThemeIcon } from 'vscode'; 2 | import { ResourceType } from "../enums"; 3 | import { Project, Workspace } from "../projects/models"; 4 | import { ServiceNode } from "../services/views"; 5 | import { ComposeNode } from '../compose/views'; 6 | import { ExplorerNode } from '../explorers/views'; 7 | 8 | export class ProjectNode extends ComposeNode { 9 | 10 | constructor( 11 | context: ExtensionContext, 12 | public readonly project: Project, 13 | private readonly collapsbleState: TreeItemCollapsibleState = TreeItemCollapsibleState.Expanded 14 | ) { 15 | super(context); 16 | } 17 | 18 | async getChildren(): Promise { 19 | if (this.collapsbleState === TreeItemCollapsibleState.None) 20 | return []; 21 | 22 | this.resetChildren(); 23 | 24 | const services = await this.project.getServices(false); 25 | 26 | this.children = services 27 | .map(service => new ServiceNode(this.context, service)); 28 | return this.children; 29 | } 30 | 31 | getTreeItem(): TreeItem { 32 | const item = new TreeItem(this.project.name, this.collapsbleState); 33 | item.contextValue = ResourceType.Project; 34 | item.iconPath = new ThemeIcon("multiple-windows"); 35 | item.tooltip = this.project.cwd; 36 | item.command = { 37 | title: 'Select Node', 38 | command: 'docker-compose.project.select', 39 | arguments: [this] 40 | }; 41 | return item; 42 | } 43 | 44 | } 45 | 46 | export class ProjectsNode extends ComposeNode { 47 | 48 | constructor( 49 | context: ExtensionContext, 50 | private readonly workspace: Workspace, 51 | private readonly projectNodeCollapsbleState: TreeItemCollapsibleState = TreeItemCollapsibleState.Expanded 52 | ) { 53 | super(context); 54 | } 55 | 56 | async getChildren(): Promise { 57 | this.resetChildren(); 58 | 59 | try { 60 | this.workspace.validate(); 61 | } catch (err) { 62 | return this.handleError(err); 63 | } 64 | this.children = this.workspace.getProjects(true) 65 | .map(project => new ProjectNode(this.context, project, this.projectNodeCollapsbleState)); 66 | return this.children; 67 | } 68 | 69 | getTreeItem(): TreeItem { 70 | const item = new TreeItem(`Projects`, TreeItemCollapsibleState.Expanded); 71 | item.contextValue = ResourceType.Projects; 72 | return item; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/executors/dockerExecutor.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from "child_process"; 2 | import { CommandExecutor } from "./commandExecutor"; 3 | import { DockerExecutorError, DockerUnhandledError } from "./exceptions"; 4 | 5 | export interface IDockerPsOptions { 6 | projectName?: string; 7 | projectDir?: string; 8 | containerName?: string; 9 | } 10 | 11 | export class DockerExecutor extends CommandExecutor { 12 | 13 | private _shell: string; 14 | 15 | constructor(shell = "/bin/sh", cwd: string = null) { 16 | super(cwd); 17 | this._shell = shell; 18 | } 19 | 20 | getBaseCommand(): string { 21 | return 'docker'; 22 | } 23 | 24 | private getShellCommand(): string { 25 | return this._shell; 26 | } 27 | 28 | public getVersion(): string { 29 | const dockerCommand = `version`; 30 | return this.executeSync(dockerCommand); 31 | } 32 | 33 | public getPs(options?: IDockerPsOptions): string { 34 | let dockerCommand = `ps -a --format '{{.Label "com.docker.compose.service"}}'`; 35 | if (options !== undefined) { 36 | if (options.projectName !== undefined) 37 | dockerCommand += ` --filter label=com.docker.compose.project=${options.projectName}`; 38 | if (options.projectDir !== undefined) 39 | dockerCommand += ` --filter label=com.docker.compose.project.working_dir=${options.projectDir}`; 40 | if (options.containerName !== undefined) 41 | dockerCommand += ` --filter name=${options.containerName}`; 42 | } 43 | return this.executeSync(dockerCommand); 44 | } 45 | 46 | public attach(name: string): void { 47 | const dockerCommand = `attach ${name}`; 48 | this.runInTerminal(dockerCommand, true, name); 49 | } 50 | 51 | public logs(name: string): string { 52 | const dockerCommand = `logs ${name}`; 53 | return this.executeSync(dockerCommand); 54 | } 55 | 56 | public start(name: string): ChildProcess { 57 | const dockerCommand = `start ${name}`; 58 | return this.execute(dockerCommand); 59 | } 60 | 61 | public stop(name: string): ChildProcess { 62 | const dockerCommand = `stop ${name}`; 63 | return this.execute(dockerCommand); 64 | } 65 | 66 | public kill(name: string): ChildProcess { 67 | const dockerCommand = `kill ${name}`; 68 | return this.execute(dockerCommand); 69 | } 70 | 71 | public executeSync(dockerCommand: string) { 72 | try { 73 | return super.executeSync(dockerCommand); 74 | } 75 | catch (err) { 76 | // 1 - Catchall for general errors 77 | if (err.status === 1) 78 | throw new DockerExecutorError(err.message, err.output); 79 | else 80 | throw new DockerUnhandledError(err.message, err.output); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/containers/views.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState, ExtensionContext, MarkdownString, ThemeIcon, ThemeColor, Color } from "vscode"; 2 | import { ResourceType } from "../enums"; 3 | import { ContainerState } from "../containers/enums"; 4 | import { Container } from "../containers/models"; 5 | import { ComposeNode } from "../compose/views"; 6 | 7 | export class ContainerNode extends ComposeNode { 8 | 9 | // iconPath = { 10 | // light: path.join(__filename, '..', '..', '..', 'resources', 'light'), 11 | // dark: path.join(__filename, '..', '..', '..', 'resources', 'dark') 12 | // }; 13 | 14 | constructor( 15 | context: ExtensionContext, 16 | public readonly container: Container 17 | ) { 18 | super(context); 19 | } 20 | 21 | async getChildren(): Promise { 22 | return []; 23 | } 24 | 25 | async getTreeItem(): Promise { 26 | const item = new TreeItem(this.container.name, TreeItemCollapsibleState.None); 27 | 28 | item.tooltip = new MarkdownString(`### ${this.container.name}`); 29 | item.tooltip.supportHtml = true; 30 | item.tooltip.isTrusted = true; 31 | 32 | // custom iconPath 33 | // var iconPath = this.context.asAbsolutePath('resources/service-exit.png'); 34 | // if (this.container.state == ContainerState.Up) { 35 | // item.contextValue = ResourceType.RunningContainer; 36 | 37 | // var iconPath = this.context.asAbsolutePath('resources/service-up-unhealthy.png'); 38 | // if (this.container.healthy) 39 | // var iconPath = this.context.asAbsolutePath('resources/service-up-healthy.png'); 40 | // } 41 | // item.iconPath = { 42 | // dark: iconPath, 43 | // light: iconPath 44 | // }; 45 | 46 | let iconId: string; 47 | let iconColorId: string; 48 | let tooltipColor: string; 49 | if (this.container.state == ContainerState.Up) { 50 | item.contextValue = ResourceType.RunningContainer; 51 | 52 | if (this.container.healthy) { 53 | iconId = "vm-running"; 54 | iconColorId = "debugIcon.startForeground"; 55 | tooltipColor = "#99ff99"; 56 | } else { 57 | iconId = "vm-active"; 58 | iconColorId = "problemsWarningIcon.foreground"; 59 | tooltipColor = "#ffc600"; 60 | } 61 | } else { 62 | item.contextValue = ResourceType.ExitedContainer; 63 | 64 | iconId = "vm"; 65 | iconColorId = "problemsErrorIcon.foreground"; 66 | tooltipColor = "#ff9999"; 67 | } 68 | const iconColor = new ThemeColor(iconColorId); 69 | item.iconPath = new ThemeIcon(iconId, iconColor); 70 | item.tooltip.appendMarkdown(`\n${this.container.status}`); 71 | 72 | return item; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/services/models.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { ChildProcess } from "child_process"; 3 | import { Container } from "../containers/models"; 4 | import { Project } from "../projects/models"; 5 | import { ComposeExecutor } from "../executors/composeExecutor"; 6 | import { DockerExecutor } from "../executors/dockerExecutor"; 7 | 8 | export class Service { 9 | 10 | private _containers: Container[] | undefined; 11 | 12 | constructor( 13 | public project: Project, 14 | public readonly name: string, 15 | private dockerExecutor: DockerExecutor, 16 | private composeExecutor: ComposeExecutor 17 | ) { 18 | this._containers = undefined; 19 | } 20 | 21 | async getContainers(force = false): Promise { 22 | if (this._containers === undefined || force) { 23 | await this.refreshContainers(); 24 | } 25 | return this._containers; 26 | } 27 | 28 | async _getContainers(force = false): Promise { 29 | const containers = await this.project.getContainers(force); 30 | const projectPattern = this.project.name + '-'; 31 | const servicePattern = projectPattern + this.name + '-'; 32 | return containers.filter((container) => { 33 | // standard container name 34 | if (container.name.startsWith(projectPattern)) { 35 | return container.name.includes(servicePattern); 36 | // custom container name 37 | } else { 38 | const name = this.getContainerServiceName(container.name); 39 | return name == this.name; 40 | } 41 | }); 42 | } 43 | 44 | public getContainerServiceName(name: string) { 45 | const options = {projectName: this.project.name, containerName: name, ProjectDir: this.project.cwd}; 46 | const resultString = this.dockerExecutor.getPs(options); 47 | const linesString = resultString.split(/[\r\n]+/g).filter((item) => item); 48 | return linesString[0]; 49 | } 50 | 51 | async refreshContainers(): Promise { 52 | this._containers = await this._getContainers(true); 53 | } 54 | 55 | public shell(): void { 56 | this.composeExecutor.shell(this.name); 57 | } 58 | 59 | public up(): ChildProcess { 60 | return this.composeExecutor.up(this.name); 61 | } 62 | 63 | public down(): ChildProcess { 64 | return this.composeExecutor.down(this.name); 65 | } 66 | 67 | public start(): ChildProcess { 68 | return this.composeExecutor.start(this.name); 69 | } 70 | 71 | public stop(): ChildProcess { 72 | return this.composeExecutor.stop(this.name); 73 | } 74 | 75 | public restart(): ChildProcess { 76 | return this.composeExecutor.restart(this.name); 77 | } 78 | 79 | public build(): ChildProcess { 80 | return this.composeExecutor.build(this.name); 81 | } 82 | 83 | public kill(): ChildProcess { 84 | return this.composeExecutor.kill(this.name); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/executors/composeExecutor.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from "child_process"; 2 | import { CommandExecutor } from "./commandExecutor"; 3 | import { ComposeFileNotFound, ComposeCommandNotFound, ComposeExecutorError, ComposeUnhandledError } from "./exceptions"; 4 | 5 | export interface IComposePsOptions { 6 | format: string; 7 | } 8 | 9 | export interface IComposePsResultPublisher { 10 | Protocol: string; 11 | PublishedPort: number; 12 | TargetPort: number; 13 | URL: string; 14 | } 15 | 16 | export interface IComposePsResult { 17 | Command: string; 18 | Created: number; 19 | ExitCode: number; 20 | Health: string; 21 | ID: string; 22 | Image: string; 23 | Name: string; 24 | Project: string; 25 | Service: string; 26 | State: string; 27 | Status: string; 28 | Publishers: IComposePsResultPublisher[]; 29 | } 30 | 31 | export class ComposeExecutor extends CommandExecutor { 32 | 33 | private _files: string[]; 34 | private _shell: string; 35 | 36 | constructor(files: string[] = [], shell = "/bin/sh", cwd: string = null) { 37 | super(cwd, process.env); 38 | this._files = files; 39 | this._shell = shell; 40 | } 41 | 42 | getBaseCommand(): string { 43 | return this._files.reduce((myString, files) => myString + ' -f ' + files, 'docker compose'); 44 | } 45 | 46 | private getShellCommand(): string { 47 | return this._shell; 48 | } 49 | 50 | public getVersion(): string { 51 | const composeCommand = `version`; 52 | return this.executeSync(composeCommand); 53 | } 54 | 55 | public getConnfigServices(): string { 56 | const configServicesCommand = `config --services`; 57 | return this.executeSync(configServicesCommand); 58 | } 59 | 60 | public getPs(): string { 61 | const composeCommand = `ps`; 62 | return this.executeSync(composeCommand); 63 | } 64 | 65 | public getPs2(options?: IComposePsOptions): IComposePsResult[] { 66 | const dockerCommand = `ps -a --format json`; 67 | const result = this.executeSync(dockerCommand); 68 | try { 69 | // Docker Compose up to 2.20 format 70 | return JSON.parse(result); 71 | } catch(e:unknown) { 72 | if (e instanceof SyntaxError) { 73 | // Docker Compose 2.21+ format 74 | return result.trim().split("\n").map(entry => JSON.parse(entry)); 75 | } 76 | throw e; 77 | } 78 | } 79 | 80 | public shell(serviceName: string): void { 81 | const shellCommand = this.getShellCommand(); 82 | const composeCommand = `exec ${serviceName} ${shellCommand}`; 83 | const terminalName = `${serviceName} shell`; 84 | this.runInTerminal(composeCommand, true, terminalName); 85 | } 86 | 87 | public up(serviceName?: string): ChildProcess { 88 | const composeCommand = serviceName === undefined ? `up --no-recreate` : `up --no-recreate ${serviceName}`; 89 | return this.execute(composeCommand); 90 | } 91 | 92 | public down(serviceName?: string): ChildProcess { 93 | const composeCommand = serviceName === undefined ? `down` : `down ${serviceName}`; 94 | return this.execute(composeCommand); 95 | } 96 | 97 | public start(serviceName?: string): ChildProcess { 98 | const composeCommand = serviceName === undefined ? `start` : `start ${serviceName}`; 99 | return this.execute(composeCommand); 100 | } 101 | 102 | public stop(serviceName?: string): ChildProcess { 103 | const composeCommand = serviceName === undefined ? `stop` : `stop ${serviceName}`; 104 | return this.execute(composeCommand); 105 | } 106 | 107 | public restart(serviceName: string): ChildProcess { 108 | const composeCommand = `restart ${serviceName}`; 109 | return this.execute(composeCommand); 110 | } 111 | 112 | public build(serviceName: string): ChildProcess { 113 | const composeCommand = `build --no-cache ${serviceName}`; 114 | return this.execute(composeCommand); 115 | } 116 | 117 | public kill(serviceName: string): ChildProcess { 118 | const composeCommand = `kill ${serviceName}`; 119 | return this.execute(composeCommand); 120 | } 121 | 122 | public executeSync(composeCommand: string) { 123 | try { 124 | return super.executeSync(composeCommand); 125 | } 126 | catch (err) { 127 | // 1 - Catchall for general errors 128 | if (err.status === 1) 129 | throw new ComposeExecutorError(err.message, err.output); 130 | // 14 - docker compose configuration file not found 131 | else if (err.status === 14) 132 | throw new ComposeFileNotFound(err.message, err.output); 133 | // 127 - docker compose command not found 134 | else if (err.status === 127) 135 | throw new ComposeCommandNotFound(err.message, err.output); 136 | else 137 | throw new ComposeUnhandledError(err.message, err.output); 138 | } 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /src/projects/models.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { ChildProcess } from "child_process"; 3 | import * as vscode from 'vscode'; 4 | import { Container } from "../containers/models"; 5 | import { Service } from "../services/models"; 6 | import { DockerExecutor } from "../executors/dockerExecutor"; 7 | import { ComposeExecutor } from "../executors/composeExecutor"; 8 | import { ps } from "docker-compose/dist/v2"; 9 | 10 | export class Project { 11 | 12 | private _id: number; 13 | 14 | private _files: string[]; 15 | private _shell: string; 16 | 17 | private _services: Service[] | undefined; 18 | private _containers: Container[] | undefined; 19 | private dockerExecutor: DockerExecutor; 20 | private composeExecutor: ComposeExecutor; 21 | 22 | constructor( 23 | public readonly name: string, 24 | public readonly cwd: string = null, 25 | files: string[] = [], 26 | shell = "/bin/sh" 27 | ) { 28 | this._id = Math.random(); 29 | this._files = files; 30 | this._shell = shell; 31 | 32 | this._services = undefined; 33 | this._containers = undefined; 34 | 35 | this.dockerExecutor = new DockerExecutor(this._shell, this.cwd); 36 | this.composeExecutor = new ComposeExecutor(this._files, this._shell, this.cwd); 37 | } 38 | 39 | async getServices(force = false): Promise { 40 | if (this._services === undefined || force) { 41 | await this.refreshServices(); 42 | } 43 | return this._services; 44 | } 45 | 46 | async refreshServices(): Promise { 47 | this._services = await this._getServices(); 48 | } 49 | 50 | async _getServices(): Promise { 51 | const servicesString = this.composeExecutor.getConnfigServices(); 52 | const linesString = servicesString.split(/[\r\n]+/g).filter(item => item); 53 | return linesString.map(serviceString => { 54 | return new Service(this, serviceString, this.dockerExecutor, this.composeExecutor); 55 | }); 56 | } 57 | 58 | async getContainers(force = false): Promise { 59 | if (this._containers === undefined || force) { 60 | await this.refreshContainers(); 61 | } 62 | return this._containers; 63 | } 64 | 65 | async refreshContainers(): Promise { 66 | this._containers = await this._getContainers(); 67 | } 68 | 69 | async _getContainers2(): Promise { 70 | const config = ["docker-compose.yml", "docker-compose.yaml"]; 71 | const result = await ps({cwd: this.cwd, log: true, config: config, commandOptions: ["--all"]}); 72 | return result.data.services.map((service) => { 73 | const ports = service.ports.map((port) => port.exposed.port.toString()); 74 | return new Container( 75 | this.dockerExecutor, 76 | service.name, 77 | service.command, 78 | service.state, 79 | ports 80 | ); 81 | }); 82 | } 83 | 84 | async _getContainers(): Promise { 85 | const result = this.composeExecutor.getPs2(); 86 | return result.map((container) => { 87 | return new Container( 88 | this.dockerExecutor, 89 | container.Name, 90 | container.Command, 91 | container.Status, 92 | [] 93 | ); 94 | }); 95 | } 96 | 97 | public filterServiceContainers(serviceName: string, containers: Container[]): Container[] { 98 | const pattern = this.name + '_' + serviceName + '_'; 99 | return containers.filter((container) => { 100 | return container.name.includes(pattern); 101 | }); 102 | } 103 | 104 | public start(): ChildProcess { 105 | return this.composeExecutor.start(); 106 | } 107 | 108 | public stop(): ChildProcess { 109 | return this.composeExecutor.stop(); 110 | } 111 | 112 | public up(): ChildProcess { 113 | return this.composeExecutor.up(); 114 | } 115 | 116 | public down(): ChildProcess { 117 | return this.composeExecutor.down(); 118 | } 119 | 120 | } 121 | 122 | 123 | export class Workspace { 124 | 125 | private _projects: Project[] | undefined; 126 | 127 | constructor( 128 | public readonly workspaceFolders: readonly vscode.WorkspaceFolder[], 129 | public readonly projectNames: string[], 130 | public readonly files: string[] = [], 131 | public readonly shell: string = "/bin/sh" 132 | ) { 133 | this._projects = undefined; 134 | } 135 | 136 | public validate() { 137 | const dockerExecutor = new DockerExecutor(this.shell); 138 | dockerExecutor.getVersion(); 139 | const composeExecutor = new ComposeExecutor(this.files, this.shell); 140 | composeExecutor.getVersion(); 141 | } 142 | 143 | public getProjects(force = false): Project[] { 144 | if (this._projects === undefined || force) 145 | this.refreshProjects(); 146 | return this._projects; 147 | } 148 | 149 | public refreshProjects(): void { 150 | this._projects = this._getProjects(); 151 | } 152 | 153 | private _getProjects(): Project[] { 154 | return this.workspaceFolders.map((folder) => { 155 | // project name from mapping or use workspace dir name 156 | const name = this.projectNames[folder.index] || folder.name.replace(/[^-_a-z0-9]/gi, ''); 157 | return new Project(name, folder.uri.fsPath, this.files, this.shell); 158 | }); 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import { AppInsightsClient } from "./telemetry/appInsightsClient"; 4 | import { NullClient } from "./telemetry/nullClient"; 5 | import { WorkspaceConfigurator } from "./configurators/workspaceConfigurator"; 6 | import { DockerComposeProjectsProvider, DockerComposeServicesProvider } from "./explorers/providers"; 7 | import { ContainerNode } from "./containers/views"; 8 | import { ProjectNode } from "./projects/views"; 9 | import { ServiceNode } from "./services/views"; 10 | import { Workspace } from './projects/models'; 11 | 12 | export function activate(context: vscode.ExtensionContext) { 13 | const configuration = WorkspaceConfigurator.getConfiguration(); 14 | 15 | const interval = configuration.get("autoRefreshInterval"); 16 | const files = configuration.get("files"); 17 | const shell = configuration.get("shell"); 18 | const projectNames = configuration.get("projectNames"); 19 | const isTelemetryEnabled = configuration.get("enableTelemetry"); 20 | const telemetryClient = isTelemetryEnabled ? new AppInsightsClient("1234-1234-1234-1234") : new NullClient(); 21 | 22 | const workspace = new Workspace( 23 | vscode.workspace && vscode.workspace.workspaceFolders, 24 | projectNames, 25 | files, 26 | shell 27 | ); 28 | 29 | const projectsProvider = new DockerComposeProjectsProvider(context, workspace); 30 | const servicesProvider = new DockerComposeServicesProvider(context, workspace); 31 | 32 | servicesProvider.setAutoRefresh(interval); 33 | 34 | vscode.window.registerTreeDataProvider("dockerComposeServices", servicesProvider); 35 | 36 | vscode.window.createTreeView('dockerComposeProjects', { treeDataProvider: projectsProvider, canSelectMany: true}); 37 | 38 | telemetryClient.sendEvent("loadExtension"); 39 | 40 | const refreshExplorer = vscode.commands.registerCommand("docker-compose.explorer.refresh", () => { 41 | servicesProvider.refresh(); 42 | telemetryClient.sendEvent("refreshExplorer"); 43 | }); 44 | 45 | const shellService = vscode.commands.registerCommand("docker-compose.service.shell", (node: ServiceNode) => { 46 | servicesProvider.shellService(node); 47 | telemetryClient.sendEvent("shellService"); 48 | }); 49 | 50 | const selectProject = vscode.commands.registerCommand("docker-compose.project.select", (node: ProjectNode) => { 51 | // projectsProvider.selectProject(node); 52 | servicesProvider.selectProject(node); 53 | telemetryClient.sendEvent("selectProject"); 54 | }); 55 | 56 | const startProject = vscode.commands.registerCommand("docker-compose.project.start", (node: ProjectNode) => { 57 | servicesProvider.startProject(node); 58 | telemetryClient.sendEvent("startProject"); 59 | }); 60 | 61 | const stopProject = vscode.commands.registerCommand("docker-compose.project.stop", (node: ProjectNode) => { 62 | servicesProvider.stopProject(node); 63 | telemetryClient.sendEvent("stopProject"); 64 | }); 65 | 66 | const upProject = vscode.commands.registerCommand("docker-compose.project.up", (node: ProjectNode) => { 67 | servicesProvider.upProject(node); 68 | telemetryClient.sendEvent("upProject"); 69 | }); 70 | 71 | const downProject = vscode.commands.registerCommand("docker-compose.project.down", (node: ProjectNode) => { 72 | servicesProvider.downProject(node); 73 | telemetryClient.sendEvent("downProject"); 74 | }); 75 | 76 | const upService = vscode.commands.registerCommand("docker-compose.service.up", (node: ServiceNode) => { 77 | servicesProvider.upService(node); 78 | telemetryClient.sendEvent("upService"); 79 | }); 80 | 81 | const downService = vscode.commands.registerCommand("docker-compose.service.down", (node: ServiceNode) => { 82 | servicesProvider.downService(node); 83 | telemetryClient.sendEvent("downService"); 84 | }); 85 | 86 | const startService = vscode.commands.registerCommand("docker-compose.service.start", (node: ServiceNode) => { 87 | servicesProvider.startService(node); 88 | telemetryClient.sendEvent("startService"); 89 | }); 90 | 91 | const stopService = vscode.commands.registerCommand("docker-compose.service.stop", (node: ServiceNode) => { 92 | servicesProvider.stopService(node); 93 | telemetryClient.sendEvent("stopService"); 94 | }); 95 | 96 | const restartService = vscode.commands.registerCommand("docker-compose.service.restart", (node: ServiceNode) => { 97 | servicesProvider.restartService(node); 98 | telemetryClient.sendEvent("restartService"); 99 | }); 100 | 101 | const buildService = vscode.commands.registerCommand("docker-compose.service.build", (node: ServiceNode) => { 102 | servicesProvider.buildService(node); 103 | telemetryClient.sendEvent("buildService"); 104 | }); 105 | 106 | const killService = vscode.commands.registerCommand("docker-compose.service.kill", (node: ServiceNode) => { 107 | servicesProvider.killService(node); 108 | telemetryClient.sendEvent("killService"); 109 | }); 110 | 111 | const attachContainer = vscode.commands.registerCommand("docker-compose.container.attach", (node: ContainerNode) => { 112 | servicesProvider.attachContainer(node); 113 | telemetryClient.sendEvent("attachContainer"); 114 | }); 115 | 116 | const logsContainer = vscode.commands.registerCommand("docker-compose.container.logs", (node: ContainerNode) => { 117 | servicesProvider.logsContainer(node); 118 | telemetryClient.sendEvent("logsContainer"); 119 | }); 120 | 121 | const startContainer = vscode.commands.registerCommand("docker-compose.container.start", (node: ContainerNode) => { 122 | servicesProvider.startContainer(node); 123 | telemetryClient.sendEvent("startContainer"); 124 | }); 125 | 126 | const stopContainer = vscode.commands.registerCommand("docker-compose.container.stop", (node: ContainerNode) => { 127 | servicesProvider.stopContainer(node); 128 | telemetryClient.sendEvent("stopContainer"); 129 | }); 130 | 131 | const killContainer = vscode.commands.registerCommand("docker-compose.container.kill", (node: ContainerNode) => { 132 | servicesProvider.killContainer(node); 133 | telemetryClient.sendEvent("killContainer"); 134 | }); 135 | 136 | context.subscriptions.push(selectProject); 137 | context.subscriptions.push(upProject); 138 | context.subscriptions.push(downProject); 139 | context.subscriptions.push(startProject); 140 | context.subscriptions.push(stopProject); 141 | context.subscriptions.push(shellService); 142 | context.subscriptions.push(upService); 143 | context.subscriptions.push(downService); 144 | context.subscriptions.push(startService); 145 | context.subscriptions.push(stopService); 146 | context.subscriptions.push(restartService); 147 | context.subscriptions.push(buildService); 148 | context.subscriptions.push(killService); 149 | context.subscriptions.push(attachContainer); 150 | context.subscriptions.push(logsContainer); 151 | context.subscriptions.push(startContainer); 152 | context.subscriptions.push(stopContainer); 153 | context.subscriptions.push(killContainer); 154 | } 155 | -------------------------------------------------------------------------------- /src/explorers/providers.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from "child_process"; 2 | import * as vscode from 'vscode'; 3 | import { TreeItem, TreeDataProvider, EventEmitter, Event, workspace, window, ExtensionContext, Uri, TextDocument, Position } from "vscode"; 4 | import { Workspace } from "../projects/models"; 5 | import { ContainerNode } from "../containers/views"; 6 | import { ExplorerNode } from "../explorers/views"; 7 | import { ProjectNode, ProjectsNode } from "../projects/views"; 8 | import { ServiceNode } from "../services/views"; 9 | 10 | export class AutoRefreshTreeDataProvider { 11 | 12 | private autoRefreshEnabled: boolean; 13 | private debounceTimer: NodeJS.Timer; 14 | 15 | constructor(protected context: ExtensionContext) { 16 | this.autoRefreshEnabled = true; 17 | } 18 | 19 | protected _onDidChangeAutoRefresh = new EventEmitter(); 20 | public get onDidChangeAutoRefresh(): Event { 21 | return this._onDidChangeAutoRefresh.event; 22 | } 23 | 24 | protected _onDidChangeTreeData = new EventEmitter(); 25 | public get onDidChangeTreeData(): Event { 26 | return this._onDidChangeTreeData.event; 27 | } 28 | 29 | public setAutoRefresh(interval: number): void { 30 | if (interval > 0) { 31 | clearTimeout(this.debounceTimer); 32 | this.debounceTimer = setInterval(() => { 33 | if (this.autoRefreshEnabled) 34 | this.refresh(); 35 | }, interval); 36 | } 37 | } 38 | 39 | async refresh(root?: T): Promise { 40 | this._onDidChangeTreeData.fire(root); 41 | } 42 | 43 | public disableAutoRefresh() { 44 | this.autoRefreshEnabled = false; 45 | } 46 | 47 | public enableAutoRefresh() { 48 | this.autoRefreshEnabled = true; 49 | } 50 | 51 | } 52 | 53 | export class DockerComposeProjectsProvider implements TreeDataProvider { 54 | 55 | private _root?: ExplorerNode; 56 | private _loading: Promise | undefined; 57 | 58 | constructor( 59 | protected context: ExtensionContext, 60 | private workspace: Workspace, 61 | ) { 62 | } 63 | 64 | private _initRoot(): ExplorerNode { 65 | return new ProjectsNode(this.context, this.workspace, vscode.TreeItemCollapsibleState.None); 66 | } 67 | 68 | protected _onDidChangeTreeData = new EventEmitter(); 69 | public get onDidChangeTreeData(): Event { 70 | return this._onDidChangeTreeData.event; 71 | } 72 | 73 | async refresh(root?: ExplorerNode): Promise { 74 | this._onDidChangeTreeData.fire(root); 75 | } 76 | 77 | protected getRefreshCallable(node: ExplorerNode) { 78 | return this.refresh.bind(node); 79 | } 80 | 81 | public getRoot(): ExplorerNode { 82 | if (this._root === undefined) 83 | this._root = this._initRoot(); 84 | return this._root; 85 | } 86 | 87 | async getChildren(node?:ExplorerNode): Promise { 88 | if (this._loading !== undefined) { 89 | await this._loading; 90 | this._loading = undefined; 91 | } 92 | 93 | if (node === undefined) node = this.getRoot(); 94 | 95 | try { 96 | return await node.getChildren(); 97 | } catch (err) { 98 | return node.handleError(err); 99 | } 100 | } 101 | 102 | async getTreeItem(node: ExplorerNode): Promise { 103 | return node.getTreeItem(); 104 | } 105 | 106 | protected _onDidChangeSelect = new EventEmitter(); 107 | public get onDidChangeSelect(): Event { 108 | return this._onDidChangeSelect.event; 109 | } 110 | 111 | public async startProject(node: ProjectNode): Promise { 112 | return node.project.start(); 113 | } 114 | 115 | public async stopProject(node: ProjectNode): Promise { 116 | return node.project.stop(); 117 | } 118 | 119 | public async upProject(node: ProjectNode): Promise { 120 | const child_process = node.project.up(); 121 | child_process.on('close', this.getRefreshCallable(node)); 122 | return child_process; 123 | } 124 | 125 | public async downProject(node: ProjectNode): Promise { 126 | const child_process = node.project.down(); 127 | child_process.on('close', this.getRefreshCallable(node)); 128 | return child_process; 129 | } 130 | 131 | } 132 | 133 | export class DockerComposeServicesProvider extends AutoRefreshTreeDataProvider implements TreeDataProvider { 134 | 135 | private _root?: ExplorerNode; 136 | private _loading: Promise | undefined; 137 | 138 | constructor( 139 | context: ExtensionContext, 140 | private workspace: Workspace, 141 | ) { 142 | super(context); 143 | } 144 | 145 | private _initRoot(): ExplorerNode { 146 | return new ProjectsNode(this.context, this.workspace); 147 | } 148 | 149 | protected getRefreshCallable(node: ExplorerNode) { 150 | return this.refresh.bind(node); 151 | } 152 | 153 | public getRoot(): ExplorerNode { 154 | if (this._root === undefined) 155 | this._root = this._initRoot(); 156 | return this._root; 157 | } 158 | 159 | async setRoot(node: ProjectNode): Promise { 160 | this._root = new ProjectNode(node.context, node.project, vscode.TreeItemCollapsibleState.Expanded); 161 | } 162 | 163 | async getChildren(node?:ExplorerNode): Promise { 164 | if (this._loading !== undefined) { 165 | await this._loading; 166 | this._loading = undefined; 167 | } 168 | 169 | if (node === undefined) node = this.getRoot(); 170 | 171 | try { 172 | return await node.getChildren(); 173 | } catch (err) { 174 | return node.handleError(err); 175 | } 176 | } 177 | 178 | async getTreeItem(node: ExplorerNode): Promise { 179 | return node.getTreeItem(); 180 | } 181 | 182 | public async selectProject(node: ProjectNode): Promise { 183 | await this.setRoot(node); 184 | await this.refresh(); 185 | } 186 | 187 | public async startProject(node: ProjectNode): Promise { 188 | return node.project.start(); 189 | } 190 | 191 | public async stopProject(node: ProjectNode): Promise { 192 | return node.project.stop(); 193 | } 194 | 195 | public async upProject(node: ProjectNode): Promise { 196 | const child_process = node.project.up(); 197 | child_process.on('close', this.getRefreshCallable(node)); 198 | return child_process; 199 | } 200 | 201 | public async downProject(node: ProjectNode): Promise { 202 | const child_process = node.project.down(); 203 | child_process.on('close', this.getRefreshCallable(node)); 204 | return child_process; 205 | } 206 | 207 | public async shellService(node: ServiceNode): Promise { 208 | node.service.shell(); 209 | } 210 | 211 | public async upService(node: ServiceNode): Promise { 212 | const child_process = node.service.up(); 213 | child_process.on('close', this.getRefreshCallable(node)); 214 | return child_process; 215 | } 216 | 217 | public async downService(node: ServiceNode): Promise { 218 | const child_process = node.service.down(); 219 | child_process.on('close', this.getRefreshCallable(node)); 220 | return child_process; 221 | } 222 | 223 | public async buildService(node: ServiceNode): Promise { 224 | const child_process = node.service.build(); 225 | child_process.on('close', this.getRefreshCallable(node)); 226 | return child_process; 227 | } 228 | 229 | public async startService(node: ServiceNode): Promise { 230 | const child_process = node.service.start(); 231 | child_process.on('close', this.getRefreshCallable(node)); 232 | return child_process; 233 | } 234 | 235 | public async stopService(node: ServiceNode): Promise { 236 | const child_process = node.service.stop(); 237 | child_process.on('close', this.getRefreshCallable(node)); 238 | return child_process; 239 | } 240 | 241 | public async restartService(node: ServiceNode): Promise { 242 | const child_process = node.service.restart(); 243 | child_process.on('close', this.getRefreshCallable(node)); 244 | return child_process; 245 | } 246 | 247 | public async killService(node: ServiceNode): Promise { 248 | const child_process = node.service.kill(); 249 | child_process.on('close', this.getRefreshCallable(node)); 250 | return child_process; 251 | } 252 | 253 | public async attachContainer(node: ContainerNode): Promise { 254 | node.container.attach(); 255 | } 256 | 257 | public async logsContainer(node:ContainerNode): Promise { 258 | const setting: Uri = Uri.parse("untitled:" + node.container.name + ".logs"); 259 | const content = node.container.logs(); 260 | vscode.workspace.openTextDocument(setting).then((doc: TextDocument) => { 261 | window.showTextDocument(doc, 1, false).then(editor => { 262 | editor.edit(edit => { 263 | edit.insert(new Position(0, 0), content); 264 | }); 265 | }); 266 | }); 267 | } 268 | 269 | public async startContainer(node: ContainerNode): Promise { 270 | const child_process = node.container.start(); 271 | child_process.on('close', this.getRefreshCallable(node)); 272 | return child_process; 273 | } 274 | 275 | public async stopContainer(node: ContainerNode): Promise { 276 | const child_process = node.container.stop(); 277 | child_process.on('close', this.getRefreshCallable(node)); 278 | return child_process; 279 | } 280 | 281 | public async killContainer(node: ContainerNode): Promise { 282 | const child_process = node.container.kill(); 283 | child_process.on('close', this.getRefreshCallable(node)); 284 | return child_process; 285 | } 286 | 287 | } 288 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-compose", 3 | "displayName": "Docker Compose", 4 | "description": "Manage Docker Compose services", 5 | "icon": "images/icon.png", 6 | "version": "0.5.1", 7 | "publisher": "p1c2u", 8 | "license": "MIT", 9 | "engines": { 10 | "vscode": "^1.74.0" 11 | }, 12 | "categories": [ 13 | "Other" 14 | ], 15 | "keywords": [ 16 | "docker", 17 | "compose", 18 | "container", 19 | "image" 20 | ], 21 | "bugs": { 22 | "url": "https://github.com/p1c2u/vscode-docker-compose/issues", 23 | "email": "maciag.artur@gmail.com" 24 | }, 25 | "homepage": "https://github.com/p1c2u/vscode-docker-compose", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/p1c2u/vscode-docker-compose.git" 29 | }, 30 | "activationEvents": [ 31 | "workspaceContains:**/docker-compose.yml", 32 | "workspaceContains:**/docker-compose.yaml", 33 | "onDebugInitialConfigurations" 34 | ], 35 | "main": "./out/extension", 36 | "contributes": { 37 | "views": { 38 | "dockerComposeView": [ 39 | { 40 | "id": "dockerComposeProjects", 41 | "name": "Projects", 42 | "when": "config.docker-compose.showExplorer == true" 43 | }, 44 | { 45 | "id": "dockerComposeServices", 46 | "name": "Services", 47 | "when": "config.docker-compose.showExplorer == true" 48 | } 49 | ] 50 | }, 51 | "viewsContainers": { 52 | "activitybar": [ 53 | { 54 | "id": "dockerComposeView", 55 | "title": "Docker Compose", 56 | "icon": "images/compose.svg" 57 | } 58 | ] 59 | }, 60 | "commands": [ 61 | { 62 | "command": "docker-compose.explorer.refresh", 63 | "title": "Refresh explorer", 64 | "icon": { 65 | "light": "resources/light/refresh.svg", 66 | "dark": "resources/dark/refresh.svg" 67 | }, 68 | "description": "Refresh Docker Compose explorer", 69 | "category": "Docker Compose" 70 | }, 71 | { 72 | "command": "docker-compose.project.select", 73 | "title": "Select", 74 | "description": "Docker Compose project select", 75 | "category": "Docker Compose" 76 | }, 77 | { 78 | "command": "docker-compose.project.start", 79 | "title": "Start", 80 | "description": "Docker Compose project start", 81 | "category": "Docker Compose" 82 | }, 83 | { 84 | "command": "docker-compose.project.stop", 85 | "title": "Stop", 86 | "description": "Docker Compose project stop", 87 | "category": "Docker Compose" 88 | }, 89 | { 90 | "command": "docker-compose.project.up", 91 | "title": "Create and Start", 92 | "description": "Docker Compose project up", 93 | "category": "Docker Compose" 94 | }, 95 | { 96 | "command": "docker-compose.project.down", 97 | "title": "Stop and Remove", 98 | "description": "Docker Compose project down", 99 | "category": "Docker Compose" 100 | }, 101 | { 102 | "command": "docker-compose.service.shell", 103 | "title": "Open shell", 104 | "description": "Docker Compose service shell", 105 | "category": "Docker Compose" 106 | }, 107 | { 108 | "command": "docker-compose.service.up", 109 | "title": "Create and Start", 110 | "description": "Docker Compose service up", 111 | "category": "Docker Compose" 112 | }, 113 | { 114 | "command": "docker-compose.service.down", 115 | "title": "Stop and Remove", 116 | "description": "Docker Compose service down", 117 | "category": "Docker Compose" 118 | }, 119 | { 120 | "command": "docker-compose.service.start", 121 | "title": "Start", 122 | "description": "Docker Compose service start", 123 | "category": "Docker Compose" 124 | }, 125 | { 126 | "command": "docker-compose.service.stop", 127 | "title": "Stop", 128 | "description": "Docker Compose service stop", 129 | "category": "Docker Compose" 130 | }, 131 | { 132 | "command": "docker-compose.service.restart", 133 | "title": "Restart", 134 | "description": "Docker Compose service restart", 135 | "category": "Docker Compose" 136 | }, 137 | { 138 | "command": "docker-compose.service.build", 139 | "title": "Build", 140 | "description": "Docker Compose service build", 141 | "category": "Docker Compose" 142 | }, 143 | { 144 | "command": "docker-compose.service.kill", 145 | "title": "Kill", 146 | "description": "Docker Compose service kill", 147 | "category": "Docker Compose" 148 | }, 149 | { 150 | "command": "docker-compose.container.attach", 151 | "title": "Attach", 152 | "description": "Docker Compose container attach", 153 | "category": "Docker Compose" 154 | }, 155 | { 156 | "command": "docker-compose.container.logs", 157 | "title": "Open logs", 158 | "description": "Docker Compose container logs", 159 | "category": "Docker Compose" 160 | }, 161 | { 162 | "command": "docker-compose.container.start", 163 | "title": "Start", 164 | "description": "Docker Compose container start", 165 | "category": "Docker Compose" 166 | }, 167 | { 168 | "command": "docker-compose.container.stop", 169 | "title": "Stop", 170 | "description": "Docker Compose container stop", 171 | "category": "Docker Compose" 172 | }, 173 | { 174 | "command": "docker-compose.container.kill", 175 | "title": "Kill", 176 | "description": "Docker Compose container kill", 177 | "category": "Docker Compose" 178 | } 179 | ], 180 | "menus": { 181 | "view/title": [ 182 | { 183 | "command": "docker-compose.explorer.refresh", 184 | "when": "view == dockerComposeServices", 185 | "group": "navigation" 186 | } 187 | ], 188 | "view/item/context": [ 189 | { 190 | "command": "docker-compose.project.up", 191 | "when": "view == dockerComposeServices && viewItem == docker-compose:project", 192 | "group": "docker-compose:project-2-general@0" 193 | }, 194 | { 195 | "command": "docker-compose.project.start", 196 | "when": "view == dockerComposeServices && viewItem == docker-compose:project", 197 | "group": "docker-compose:project-2-general@1" 198 | }, 199 | { 200 | "command": "docker-compose.project.stop", 201 | "when": "view == dockerComposeServices && viewItem == docker-compose:project", 202 | "group": "docker-compose:project-2-general@2" 203 | }, 204 | { 205 | "command": "docker-compose.project.down", 206 | "when": "view == dockerComposeServices && viewItem == docker-compose:project", 207 | "group": "docker-compose:project-2-general@3" 208 | }, 209 | { 210 | "command": "docker-compose.service.shell", 211 | "when": "view == dockerComposeServices && viewItem == docker-compose:service", 212 | "group": "docker-compose:service-1-top@1" 213 | }, 214 | { 215 | "command": "docker-compose.service.up", 216 | "when": "view == dockerComposeServices && viewItem == docker-compose:service", 217 | "group": "docker-compose:service-2-general@2" 218 | }, 219 | { 220 | "command": "docker-compose.service.down", 221 | "when": "view == dockerComposeServices && viewItem == docker-compose:service", 222 | "group": "docker-compose:service-2-general@8" 223 | }, 224 | { 225 | "command": "docker-compose.service.start", 226 | "when": "view == dockerComposeServices && viewItem == docker-compose:service", 227 | "group": "docker-compose:service-2-general@3" 228 | }, 229 | { 230 | "command": "docker-compose.service.restart", 231 | "when": "view == dockerComposeServices && viewItem == docker-compose:service", 232 | "group": "docker-compose:service-2-general@4" 233 | }, 234 | { 235 | "command": "docker-compose.service.stop", 236 | "when": "view == dockerComposeServices && viewItem == docker-compose:service", 237 | "group": "docker-compose:service-2-general@5" 238 | }, 239 | { 240 | "command": "docker-compose.service.build", 241 | "when": "view == dockerComposeServices && viewItem == docker-compose:service", 242 | "group": "docker-compose:service-2-general@6" 243 | }, 244 | { 245 | "command": "docker-compose.service.kill", 246 | "when": "view == dockerComposeServices && viewItem == docker-compose:service", 247 | "group": "docker-compose:service-2-general@7" 248 | }, 249 | { 250 | "command": "docker-compose.container.logs", 251 | "when": "view == dockerComposeServices && viewItem == docker-compose:running-container", 252 | "group": "docker-compose:container-1-top@0" 253 | }, 254 | { 255 | "command": "docker-compose.container.attach", 256 | "when": "view == dockerComposeServices && viewItem == docker-compose:running-container", 257 | "group": "docker-compose:container-1-top@1" 258 | }, 259 | { 260 | "command": "docker-compose.container.start", 261 | "when": "view == dockerComposeServices && viewItem == docker-compose:exited-container", 262 | "group": "docker-compose:container-2-general@0" 263 | }, 264 | { 265 | "command": "docker-compose.container.stop", 266 | "when": "view == dockerComposeServices && viewItem == docker-compose:running-container", 267 | "group": "docker-compose:container-2-general@1" 268 | }, 269 | { 270 | "command": "docker-compose.container.kill", 271 | "when": "view == dockerComposeServices && viewItem == docker-compose:running-container", 272 | "group": "docker-compose:container-2-general@2" 273 | } 274 | ] 275 | }, 276 | "configuration": { 277 | "type": "object", 278 | "title": "Docker Compose configuration", 279 | "properties": { 280 | "docker-compose.projectNames": { 281 | "type": "array", 282 | "items": { 283 | "type": "string" 284 | }, 285 | "default": [], 286 | "description": "Override Docker Compose project name for each workspace root." 287 | }, 288 | "docker-compose.autoRefreshInterval": { 289 | "type": "integer", 290 | "default": 90000, 291 | "description": "Docker Compose auto refresh interval." 292 | }, 293 | "docker-compose.showExplorer": { 294 | "type": "boolean", 295 | "default": true, 296 | "description": "Show Docker Compose explorer." 297 | }, 298 | "docker-compose.enableTelemetry": { 299 | "type": "boolean", 300 | "default": false, 301 | "description": "Enable telemetry" 302 | }, 303 | "docker-compose.shell": { 304 | "type": "string", 305 | "default": "/bin/sh", 306 | "description": "Specify shell to use inside Docker Container." 307 | }, 308 | "docker-compose.files": { 309 | "type": "array", 310 | "items": { 311 | "type": "string" 312 | }, 313 | "default": [], 314 | "description": "Specify Docker Compose files." 315 | } 316 | } 317 | } 318 | }, 319 | "scripts": { 320 | "vscode:prepublish": "npm run compile", 321 | "compile": "tsc -p ./", 322 | "watch": "tsc -watch -p ./", 323 | "pretest": "npm run compile && npm run lint", 324 | "lint": "eslint src --ext ts", 325 | "test": "node ./out/test/runTest.js" 326 | }, 327 | "devDependencies": { 328 | "@types/glob": "^7.1.1", 329 | "@types/mocha": "^10.0.1", 330 | "@types/node": "^20.2.0", 331 | "@types/vscode": "^1.74.0", 332 | "@typescript-eslint/eslint-plugin": "^5.42.0", 333 | "@typescript-eslint/parser": "^5.42.0", 334 | "@vscode/test-electron": "^1.6.1", 335 | "eslint": "^8.26.0", 336 | "typescript": "^5.0.4" 337 | }, 338 | "dependencies": { 339 | "applicationinsights": "^1.0.5", 340 | "docker-compose": "^0.24.1", 341 | "yaml-config-loader": "^2.0.1" 342 | } 343 | } 344 | --------------------------------------------------------------------------------