From fa7b4581f4009f240b3be0124184278fab55e429 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 5 Apr 2026 18:55:07 -0500 Subject: [PATCH 1/2] Add Compose Pull commands for compose files and groups (#414) --- package.json | 138 +++++++++++++++++- package.nls.json | 11 ++ src/commands/compose/compose.ts | 16 +- src/commands/compose/getComposeSubsetList.ts | 16 +- src/commands/containers/composeGroup.ts | 37 +++++ src/commands/registerCommands.ts | 7 +- src/commands/selectCommandTemplate.ts | 10 +- .../commands/selectCommandTemplate.test.ts | 54 ++++++- src/utils/migration/settingsMap.ts | 2 + 9 files changed, 273 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 507c5ebd..44071be1 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,10 @@ "command": "vscode-containers.containers.composeGroup.stop", "when": "config.containers.containers.groupBy == 'Compose Project Name'" }, + { + "command": "vscode-containers.containers.composeGroup.pull", + "when": "config.containers.containers.groupBy == 'Compose Project Name'" + }, { "command": "vscode-containers.containers.composeGroup.restart", "when": "never" @@ -101,6 +105,10 @@ "command": "vscode-containers.compose.down", "when": "isWorkspaceTrusted" }, + { + "command": "vscode-containers.compose.pull", + "when": "isWorkspaceTrusted" + }, { "command": "vscode-containers.compose.restart", "when": "isWorkspaceTrusted" @@ -117,6 +125,10 @@ "command": "vscode-containers.compose.down.subset", "when": "isWorkspaceTrusted" }, + { + "command": "vscode-containers.compose.pull.subset", + "when": "isWorkspaceTrusted" + }, { "command": "vscode-containers.configure", "when": "isWorkspaceTrusted" @@ -177,6 +189,11 @@ "command": "vscode-containers.compose.down", "group": "containers" }, + { + "when": "isWorkspaceTrusted && editorLangId == dockercompose", + "command": "vscode-containers.compose.pull", + "group": "containers" + }, { "when": "isWorkspaceTrusted && editorLangId == dockercompose", "command": "vscode-containers.compose.restart", @@ -197,6 +214,11 @@ "command": "vscode-containers.compose.down.subset", "group": "containers" }, + { + "when": "isWorkspaceTrusted && editorLangId == dockercompose", + "command": "vscode-containers.compose.pull.subset", + "group": "containers" + }, { "when": "isWorkspaceTrusted && editorLangId == dockerfile", "command": "vscode-containers.images.build", @@ -214,6 +236,11 @@ "command": "vscode-containers.compose.down", "group": "containers" }, + { + "when": "isWorkspaceTrusted && resourceLangId == dockercompose", + "command": "vscode-containers.compose.pull", + "group": "containers" + }, { "when": "isWorkspaceTrusted && resourceLangId == dockercompose", "command": "vscode-containers.compose.restart", @@ -234,6 +261,11 @@ "command": "vscode-containers.compose.down.subset", "group": "containers" }, + { + "when": "isWorkspaceTrusted && resourceLangId == dockercompose", + "command": "vscode-containers.compose.pull.subset", + "group": "containers" + }, { "when": "isWorkspaceTrusted && resourceLangId == dockerfile", "command": "vscode-containers.images.build", @@ -453,6 +485,11 @@ "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", "group": "composeGroup_1_general@3" }, + { + "command": "vscode-containers.containers.composeGroup.pull", + "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", + "group": "composeGroup_1_general@4" + }, { "command": "vscode-containers.containers.composeGroup.restart", "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", @@ -1970,6 +2007,86 @@ "advanced" ] }, + "containers.commands.composePull": { + "oneOf": [ + { + "type": "array", + "items": { + "properties": { + "template": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.template%" + }, + "label": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.label%" + }, + "match": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.match%" + } + }, + "required": [ + "label", + "template" + ] + } + }, + { + "type": "string" + } + ], + "default": [ + { + "label": "Compose Pull", + "template": "${composeCommand} ${configurationFile} pull" + } + ], + "description": "%vscode-containers.config.template.composePull.description%", + "tags": [ + "advanced" + ] + }, + "containers.commands.composePullSubset": { + "oneOf": [ + { + "type": "array", + "items": { + "properties": { + "template": { + "type": "string", + "description": "%vscode-containers.config.template.composePullSubset.template%" + }, + "label": { + "type": "string", + "description": "%vscode-containers.config.template.composePullSubset.label%" + }, + "match": { + "type": "string", + "description": "%vscode-containers.config.template.composePullSubset.match%" + } + }, + "required": [ + "label", + "template" + ] + } + }, + { + "type": "string" + } + ], + "default": [ + { + "label": "Compose Pull", + "template": "${composeCommand} ${profileList} ${configurationFile} pull ${serviceList}" + } + ], + "description": "%vscode-containers.config.template.composePullSubset.description%", + "tags": [ + "advanced" + ] + }, "containers.containers.groupBy": { "type": "string", "default": "Compose Project Name", @@ -2598,6 +2715,16 @@ "title": "%vscode-containers.commands.compose.down.subset%", "category": "%vscode-containers.commands.category.containersGeneric%" }, + { + "command": "vscode-containers.compose.pull", + "title": "%vscode-containers.commands.compose.pull%", + "category": "%vscode-containers.commands.category.containersGeneric%" + }, + { + "command": "vscode-containers.compose.pull.subset", + "title": "%vscode-containers.commands.compose.pull.subset%", + "category": "%vscode-containers.commands.category.containersGeneric%" + }, { "command": "vscode-containers.configure", "title": "%vscode-containers.commands.configure%", @@ -2713,6 +2840,11 @@ "title": "%vscode-containers.commands.containers.composeGroup.stop%", "category": "%vscode-containers.commands.category.containers%" }, + { + "command": "vscode-containers.containers.composeGroup.pull", + "title": "%vscode-containers.commands.containers.composeGroup.pull%", + "category": "%vscode-containers.commands.category.containers%" + }, { "command": "vscode-containers.containers.composeGroup.restart", "title": "%vscode-containers.commands.containers.composeGroup.restart%", @@ -3238,7 +3370,9 @@ "onCommand:vscode-containers.images.run", "onCommand:vscode-containers.compose.up", "onCommand:vscode-containers.compose.up.subset", - "onCommand:vscode-containers.compose.down.subset" + "onCommand:vscode-containers.compose.down.subset", + "onCommand:vscode-containers.compose.pull", + "onCommand:vscode-containers.compose.pull.subset" ], "description": "%vscode-containers.walkthrough.containersStart.runContainer.description%", "media": { @@ -3330,6 +3464,8 @@ "containers.commands.composeUp", "containers.commands.composeUpSubset", "containers.commands.composeDown", + "containers.commands.composePull", + "containers.commands.composePullSubset", "containers.environment", "containers.scaffolding.templatePath", "containers.containerCommand", diff --git a/package.nls.json b/package.nls.json index a53452e0..1fbde450 100644 --- a/package.nls.json +++ b/package.nls.json @@ -163,6 +163,14 @@ "vscode-containers.config.template.composeDown.label": "The label displayed to the user.", "vscode-containers.config.template.composeDown.match": "The regular expression for choosing the right template. Checked against compose YAML files, folder name, etc.", "vscode-containers.config.template.composeDown.description": "Command templates for compose down commands.", + "vscode-containers.config.template.composePull.template": "The command template.", + "vscode-containers.config.template.composePull.label": "The label displayed to the user.", + "vscode-containers.config.template.composePull.match": "The regular expression for choosing the right template. Checked against compose YAML files, folder name, etc.", + "vscode-containers.config.template.composePull.description": "Command templates for compose pull commands.", + "vscode-containers.config.template.composePullSubset.template": "The command template.", + "vscode-containers.config.template.composePullSubset.label": "The label displayed to the user.", + "vscode-containers.config.template.composePullSubset.match": "The regular expression for choosing the right template. Checked against compose YAML files, folder name, etc.", + "vscode-containers.config.template.composePullSubset.description": "Command templates for compose pull (subset) commands.", "vscode-containers.config.containers.containers.groupBy": "The property to use to group containers in Containers view: ContainerId, ContainerName, CreatedTime, FullTag, ImageId, Networks, Ports, Registry, RegistryAndPath, Repository, RepositoryName, RepositoryNameShort, RepositoryNameAndTag, State, Status, Tag, or None", "vscode-containers.config.containers.containers.groupByLabel": "The items will be grouped by the value of this container label (e.g. `com.microsoft.created-by`)", "vscode-containers.config.containers.containers.description": "Any secondary properties to display for a container (an array). Possible elements include: ContainerId, ContainerName, CreatedTime, FullTag, ImageId, Networks, Ports, Registry, RegistryAndPath, Repository, RepositoryName, RepositoryNameShort, RepositoryNameAndTag, State, Status, and Tag", @@ -214,6 +222,8 @@ "vscode-containers.commands.compose.up": "Compose Up", "vscode-containers.commands.compose.up.subset": "Compose Up - Select Services", "vscode-containers.commands.compose.down.subset": "Compose Down - Select Services", + "vscode-containers.commands.compose.pull": "Compose Pull", + "vscode-containers.commands.compose.pull.subset": "Compose Pull - Select Services", "vscode-containers.commands.configure": "Add Docker Files to Workspace...", "vscode-containers.commands.configureCompose": "Add Compose Files to Workspace...", "vscode-containers.commands.containers.attachShell": "Attach Shell", @@ -237,6 +247,7 @@ "vscode-containers.commands.containers.composeGroup.logs": "Compose Logs", "vscode-containers.commands.containers.composeGroup.start": "Compose Start", "vscode-containers.commands.containers.composeGroup.stop": "Compose Stop", + "vscode-containers.commands.containers.composeGroup.pull": "Compose Pull", "vscode-containers.commands.containers.composeGroup.restart": "Compose Restart", "vscode-containers.commands.containers.composeGroup.down": "Compose Down", "vscode-containers.commands.debugging.initializeForDebugging": "Initialize for container debugging", diff --git a/src/commands/compose/compose.ts b/src/commands/compose/compose.ts index 0338c2a7..331c9321 100644 --- a/src/commands/compose/compose.ts +++ b/src/commands/compose/compose.ts @@ -14,7 +14,9 @@ import { quickPickWorkspaceFolder } from '../../utils/quickPickWorkspaceFolder'; import { selectComposeCommand } from '../selectCommandTemplate'; import { getComposeProfileList, getComposeProfilesOrServices, getComposeServiceList, getDefaultCommandComposeProfilesOrServices } from './getComposeSubsetList'; -async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSubset' | 'downSubset')[], message: string, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[], preselectedServices?: string[], preselectedProfiles?: string[]): Promise { +type ComposeCommand = 'up' | 'down' | 'upSubset' | 'downSubset' | 'pull' | 'pullSubset'; + +async function compose(context: IActionContext, commands: ComposeCommand[], message: string, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[], preselectedServices?: string[], preselectedProfiles?: string[]): Promise { if (!vscode.workspace.isTrusted) { throw new UserCancelledError('enforceTrust'); } @@ -67,9 +69,9 @@ async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSu if (!terminalCommand.args?.length) { // Add the service list if needed terminalCommand.command = await addServicesOrProfilesIfNeeded(context, folder, terminalCommand.command, preselectedServices, preselectedProfiles); - } else if (command === 'upSubset' || command === 'downSubset') { + } else if (command === 'upSubset' || command === 'downSubset' || command === 'pullSubset') { // If there are arguments, it means we're using a default command (based on the logic in selectCommandTemplate.ts) - // So, we only want to add profile/service list for the upSubset or downSubset command + // So, we only want to add profile/service list for the subset commands terminalCommand = await addDefaultCommandServicesOrProfilesIfNeeded(context, folder, terminalCommand, preselectedServices, preselectedProfiles); } @@ -102,6 +104,14 @@ export async function composeDownSubset(context: IActionContext, dockerComposeFi return await compose(context, ['downSubset'], vscode.l10n.t('Choose compose file to take down'), dockerComposeFileUri, selectedComposeFileUris, preselectedServices, preselectedProfiles); } +export async function composePull(context: IActionContext, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[]): Promise { + return await compose(context, ['pull'], vscode.l10n.t('Choose compose file to pull images for'), dockerComposeFileUri, selectedComposeFileUris); +} + +export async function composePullSubset(context: IActionContext, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[], preselectedServices?: string[], preselectedProfiles?: string[]): Promise { + return await compose(context, ['pullSubset'], vscode.l10n.t('Choose compose file to pull selected services from'), dockerComposeFileUri, selectedComposeFileUris, preselectedServices, preselectedProfiles); +} + export async function composeRestart(context: IActionContext, dockerComposeFileUri?: vscode.Uri, selectedComposeFileUris?: vscode.Uri[]): Promise { return await compose(context, ['down', 'up'], vscode.l10n.t('Choose compose file to restart'), dockerComposeFileUri, selectedComposeFileUris); } diff --git a/src/commands/compose/getComposeSubsetList.ts b/src/commands/compose/getComposeSubsetList.ts index 8ba17ddc..0b96b75c 100644 --- a/src/commands/compose/getComposeSubsetList.ts +++ b/src/commands/compose/getComposeSubsetList.ts @@ -11,8 +11,8 @@ import { ext } from '../../extensionVariables'; import { runWithDefaults } from '../../runtimes/runners/runWithDefaults'; import { execAsync } from '../../utils/execAsync'; -// Matches an `up` or `down` and everything after it--so that it can be replaced with `config --services`, to get a service list using all of the files originally part of the compose command -const composeCommandReplaceRegex = /(\b(up|down)\b).*$/i; +// Matches an `up`, `down`, or `pull` and everything after it--so that it can be replaced with `config --services`, to get a service list using all of the files originally part of the compose command +const composeCommandReplaceRegex = /(\b(up|down|pull)\b).*$/i; type SubsetType = 'services' | 'profiles'; @@ -21,7 +21,7 @@ export async function getDefaultCommandComposeProfilesOrServices(context: IActio const profiles = await getDefaultCommandServiceSubsets(workspaceFolder, composeCommand, 'profiles'); if (preselectedServices?.length && preselectedProfiles?.length) { - throw new Error(vscode.l10n.t('Cannot specify both services and profiles to start/stop. Please choose one or the other.')); + throw new Error(vscode.l10n.t('Cannot specify both services and profiles. Please choose one or the other.')); } // If there are any profiles, we need to ask the user whether they want profiles or services, since they are mutually exclusive to use @@ -43,7 +43,7 @@ export async function getDefaultCommandComposeProfilesOrServices(context: IActio } ]; - useProfiles = 'profiles' === (await context.ui.showQuickPick(profilesOrServices, { placeHolder: vscode.l10n.t('Do you want to start/stop services or profiles?') })).data; + useProfiles = 'profiles' === (await context.ui.showQuickPick(profilesOrServices, { placeHolder: vscode.l10n.t('Do you want to select services or profiles?') })).data; } return { @@ -57,7 +57,7 @@ export async function getComposeProfilesOrServices(context: IActionContext, work const profiles = await getServiceSubsets(workspaceFolder, composeCommand, 'profiles'); if (preselectedServices?.length && preselectedProfiles?.length) { - throw new Error(vscode.l10n.t('Cannot specify both services and profiles to start/stop. Please choose one or the other.')); + throw new Error(vscode.l10n.t('Cannot specify both services and profiles. Please choose one or the other.')); } // If there any profiles, we need to ask the user whether they want profiles or services, since they are mutually exclusive to use @@ -79,7 +79,7 @@ export async function getComposeProfilesOrServices(context: IActionContext, work } ]; - useProfiles = 'profiles' === (await context.ui.showQuickPick(profilesOrServices, { placeHolder: vscode.l10n.t('Do you want to start/stop services or profiles?') })).data; + useProfiles = 'profiles' === (await context.ui.showQuickPick(profilesOrServices, { placeHolder: vscode.l10n.t('Do you want to select services or profiles?') })).data; } return { @@ -168,8 +168,8 @@ export async function getComposeServiceList(context: IActionContext, workspaceFo async function pickSubsets(context: IActionContext, type: SubsetType, allChoices: string[], previousChoices: string[]): Promise { const label = type === 'profiles' ? - vscode.l10n.t('Choose profiles to start/stop') : - vscode.l10n.t('Choose services to start/stop'); + vscode.l10n.t('Choose profiles') : + vscode.l10n.t('Choose services'); const pickChoices: IAzureQuickPickItem[] = allChoices.map(s => ({ label: s, diff --git a/src/commands/containers/composeGroup.ts b/src/commands/containers/composeGroup.ts index 6aa86382..4180e312 100644 --- a/src/commands/containers/composeGroup.ts +++ b/src/commands/containers/composeGroup.ts @@ -5,9 +5,11 @@ import { IActionContext } from '@microsoft/vscode-azext-utils'; import { CommonOrchestratorCommandOptions, IContainerOrchestratorClient, LogsCommandOptions, VoidCommandResponse } from '@microsoft/vscode-container-client'; +import { CommandLineArgs, quoted } from '@microsoft/vscode-processutils'; import * as path from 'path'; import { l10n } from 'vscode'; import { ext } from '../../extensionVariables'; +import { isComposeV2ableOrchestratorClient } from '../../runtimes/clients/AutoConfigurableDockerComposeClient'; import { TaskCommandRunnerFactory } from '../../runtimes/runners/TaskCommandRunnerFactory'; import { ContainerGroupTreeItem } from '../../tree/containers/ContainerGroupTreeItem'; import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; @@ -33,6 +35,10 @@ export async function composeGroupDown(context: IActionContext, node: ContainerG return composeGroup(context, (client, options) => client.down(options), node); } +export async function composeGroupPull(context: IActionContext, node: ContainerGroupTreeItem): Promise { + return composeGroup(context, (client, options) => Promise.resolve(getComposeGroupPullCommand(client, options)), node); +} + type AdditionalOptions = Omit; async function composeGroup( @@ -75,6 +81,37 @@ async function composeGroup( await taskCRF.getCommandRunner()(composeCommandCallback(client, options)); } +function getComposeGroupPullCommand(client: IContainerOrchestratorClient, options: CommonOrchestratorCommandOptions): VoidCommandResponse { + const args: CommandLineArgs = []; + + if (isComposeV2ableOrchestratorClient(client) && client.composeV2) { + args.push('compose'); + } + + for (const composeFile of options.files ?? []) { + args.push('-f', quoted(composeFile)); + } + + if (options.projectName) { + args.push('--project-name', quoted(options.projectName)); + } + + if (options.environmentFile) { + args.push('--env-file', quoted(options.environmentFile)); + } + + for (const profile of options.profiles ?? []) { + args.push('--profile', quoted(profile)); + } + + args.push('pull'); + + return { + command: client.commandName, + args, + }; +} + function getComposeWorkingDirectory(node: ContainerGroupTreeItem): string | undefined { // Find a container with the `com.docker.compose.project.working_dir` label, which gives the working directory in which to execute the compose command const container = (node.ChildTreeItems as ContainerTreeItem[]).find(c => c.labels?.['com.docker.compose.project.working_dir']); diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index cea77c8f..b09a76b7 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -10,11 +10,11 @@ import { scaffold } from "../scaffolding/scaffold"; import { scaffoldCompose } from "../scaffolding/scaffoldCompose"; import { scaffoldDebugConfig } from "../scaffolding/scaffoldDebugConfig"; import { chooseContainerRuntime } from "./chooseContainerRuntime"; -import { composeDown, composeDownSubset, composeRestart, composeUp, composeUpSubset } from "./compose/compose"; +import { composeDown, composeDownSubset, composePull, composePullSubset, composeRestart, composeUp, composeUpSubset } from "./compose/compose"; import { askCopilot } from "./containers/askCopilot"; import { attachShellContainer } from "./containers/attachShellContainer"; import { browseContainer } from "./containers/browseContainer"; -import { composeGroupDown, composeGroupLogs, composeGroupRestart, composeGroupStart, composeGroupStop } from "./containers/composeGroup"; +import { composeGroupDown, composeGroupLogs, composeGroupPull, composeGroupRestart, composeGroupStart, composeGroupStop } from "./containers/composeGroup"; import { configureContainersExplorer } from "./containers/configureContainersExplorer"; import { downloadContainerFile } from "./containers/files/downloadContainerFile"; import { openContainerFile } from "./containers/files/openContainerFile"; @@ -120,6 +120,8 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-containers.debugging.initializeForDebugging', scaffoldDebugConfig); registerWorkspaceCommand('vscode-containers.compose.down', composeDown); + registerWorkspaceCommand('vscode-containers.compose.pull', composePull); + registerWorkspaceCommand('vscode-containers.compose.pull.subset', composePullSubset); registerWorkspaceCommand('vscode-containers.compose.restart', composeRestart); registerWorkspaceCommand('vscode-containers.compose.up', composeUp); registerWorkspaceCommand('vscode-containers.compose.up.subset', composeUpSubset); @@ -146,6 +148,7 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-containers.containers.composeGroup.logs', composeGroupLogs); registerWorkspaceCommand('vscode-containers.containers.composeGroup.start', composeGroupStart); registerWorkspaceCommand('vscode-containers.containers.composeGroup.stop', composeGroupStop); + registerWorkspaceCommand('vscode-containers.containers.composeGroup.pull', composeGroupPull); registerWorkspaceCommand('vscode-containers.containers.composeGroup.restart', composeGroupRestart); registerWorkspaceCommand('vscode-containers.containers.composeGroup.down', composeGroupDown); diff --git a/src/commands/selectCommandTemplate.ts b/src/commands/selectCommandTemplate.ts index df2c308d..2b17917f 100644 --- a/src/commands/selectCommandTemplate.ts +++ b/src/commands/selectCommandTemplate.ts @@ -12,7 +12,7 @@ import { ext } from '../extensionVariables'; import { isComposeV2ableOrchestratorClient } from '../runtimes/clients/AutoConfigurableDockerComposeClient'; import { resolveVariables } from '../utils/resolveVariables'; -type TemplateCommand = 'build' | 'run' | 'runInteractive' | 'attach' | 'logs' | 'composeUp' | 'composeDown' | 'composeUpSubset' | 'composeDownSubset'; +type TemplateCommand = 'build' | 'run' | 'runInteractive' | 'attach' | 'logs' | 'composeUp' | 'composeDown' | 'composeUpSubset' | 'composeDownSubset' | 'composePull' | 'composePullSubset'; type TemplatePicker = (items: IAzureQuickPickItem[], options: IAzureQuickPickOptions) => Promise>; @@ -78,7 +78,7 @@ export async function selectLogsCommand(context: IActionContext, containerName: ); } -export async function selectComposeCommand(context: IActionContext, folder: vscode.WorkspaceFolder, composeCommand: 'up' | 'down' | 'upSubset' | 'downSubset', configurationFile?: string, detached?: boolean, build?: boolean): Promise { +export async function selectComposeCommand(context: IActionContext, folder: vscode.WorkspaceFolder, composeCommand: 'up' | 'down' | 'upSubset' | 'downSubset' | 'pull' | 'pullSubset', configurationFile?: string, detached?: boolean, build?: boolean): Promise { let template: TemplateCommand; switch (composeCommand) { @@ -91,6 +91,12 @@ export async function selectComposeCommand(context: IActionContext, folder: vsco case 'downSubset': template = 'composeDownSubset'; break; + case 'pull': + template = 'composePull'; + break; + case 'pullSubset': + template = 'composePullSubset'; + break; case 'upSubset': default: template = 'composeUpSubset'; diff --git a/src/test/commands/selectCommandTemplate.test.ts b/src/test/commands/selectCommandTemplate.test.ts index 82671c1d..ebde75e5 100644 --- a/src/test/commands/selectCommandTemplate.test.ts +++ b/src/test/commands/selectCommandTemplate.test.ts @@ -8,6 +8,7 @@ import assert from 'assert'; import { CommandTemplate, selectCommandTemplate } from '../../commands/selectCommandTemplate'; const DefaultPickIndex = 0; +type TemplateCommand = Parameters[1]; suite("(unit) selectCommandTemplate", () => { test("One constrained from settings (match)", async () => { @@ -333,13 +334,61 @@ suite("(unit) selectCommandTemplate", () => { assert.equal(result.context.telemetry.properties.isDefaultCommand, 'true', 'Wrong value for isDefaultCommand'); assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'false', 'Wrong value for isCommandRegexMatched'); }); + + test('Compose pull templates are selectable', async () => { + const result = await runWithCommandSetting( + [ + { + label: 'compose pull', + template: 'compose pull test', + }, + ], + [ + { + label: 'fallback', + template: 'fallback', + }, + ], + [], + ['test'], + 'composePull' + ); + + assert.equal(result.command, 'compose pull test', 'Incorrect command selected'); + assert.deepEqual(result.args, [], 'Incorrect args selected'); + }); + + test('Compose pull subset templates are selectable', async () => { + const result = await runWithCommandSetting( + [ + { + label: 'compose pull subset', + template: 'compose pull test-service', + }, + ], + [ + { + label: 'fallback', + template: 'fallback', + }, + ], + [], + ['test'], + 'composePullSubset' + ); + + assert.equal(result.command, 'compose pull test-service', 'Incorrect command selected'); + assert.deepEqual(result.args, [], 'Incorrect args selected'); + }); }); async function runWithCommandSetting( userTemplates: CommandTemplate[] | string, overriddenDefaultTemplates: CommandTemplate[], pickInputs: number[], - matchContext: string[]): Promise<{ command: string, context: IActionContext }> { + matchContext: string[], + templateCommand: TemplateCommand = 'build'): + Promise<{ command: string, args: string[], context: IActionContext }> { const tempContext: IActionContext = { telemetry: { properties: {}, measurements: {}, }, @@ -361,7 +410,7 @@ async function runWithCommandSetting( return { globalValue: userTemplates, defaultValue: overriddenDefaultTemplates }; }; - const cmdResult = await selectCommandTemplate(tempContext, 'build', matchContext, undefined, {}, picker, settingsGetter); + const cmdResult = await selectCommandTemplate(tempContext, templateCommand, matchContext, undefined, {}, picker, settingsGetter); if (pickInputs.length !== 0) { // selectCommandTemplate never asked for an input we have (fail) @@ -370,6 +419,7 @@ async function runWithCommandSetting( return { command: cmdResult.command, + args: cmdResult.args?.map(a => typeof a === 'string' ? a : a.value) ?? [], context: tempContext, }; } diff --git a/src/utils/migration/settingsMap.ts b/src/utils/migration/settingsMap.ts index d922c5e3..bd6ae5e6 100644 --- a/src/utils/migration/settingsMap.ts +++ b/src/utils/migration/settingsMap.ts @@ -15,6 +15,8 @@ export const settingsMap: Record = { "docker.commands.composeUp": "containers.commands.composeUp", "docker.commands.composeUpSubset": "containers.commands.composeUpSubset", "docker.commands.composeDown": "containers.commands.composeDown", + "docker.commands.composePull": "containers.commands.composePull", + "docker.commands.composePullSubset": "containers.commands.composePullSubset", "docker.containers.groupBy": "containers.containers.groupBy", "docker.containers.groupByLabel": "containers.containers.groupByLabel", "docker.containers.description": "containers.containers.description", From 09a3f3f0be9e2c757572e777d2b861d0dfb29fda Mon Sep 17 00:00:00 2001 From: Kevin Sailema <108644636+KevinSailema@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:18:54 -0500 Subject: [PATCH 2/2] Update src/commands/compose/getComposeSubsetList.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/commands/compose/getComposeSubsetList.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/compose/getComposeSubsetList.ts b/src/commands/compose/getComposeSubsetList.ts index 0b96b75c..84c91e1f 100644 --- a/src/commands/compose/getComposeSubsetList.ts +++ b/src/commands/compose/getComposeSubsetList.ts @@ -11,8 +11,9 @@ import { ext } from '../../extensionVariables'; import { runWithDefaults } from '../../runtimes/runners/runWithDefaults'; import { execAsync } from '../../utils/execAsync'; -// Matches an `up`, `down`, or `pull` and everything after it--so that it can be replaced with `config --services`, to get a service list using all of the files originally part of the compose command -const composeCommandReplaceRegex = /(\b(up|down|pull)\b).*$/i; +// Matches an `up`, `down`, or `pull` subcommand token and everything after it--so that it can be replaced with `config --services`, to get a service list using all of the files originally part of the compose command. +// The token must appear at the start of the command or after whitespace so paths/quoted arguments containing words like `pull` are not mistaken for the compose subcommand. +const composeCommandReplaceRegex = /(?