From e129d97bbb1dc2ec46680fdaeee80fa1963378fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:19:10 +0000 Subject: [PATCH 1/5] Initial plan From bb4beb0ef5a5d99a8bb360afaf6288766e554403 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:23:54 +0000 Subject: [PATCH 2/5] Add Start with Options interactive command for Dev Proxy Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- CHANGELOG.md | 1 + README.md | 1 + package.json | 12 ++ src/commands/proxy.ts | 308 +++++++++++++++++++++++++++++++++++++- src/constants.ts | 1 + src/test/commands.test.ts | 7 + 6 files changed, 329 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5642421..f0fea05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added: +- Command: Added `Start with Options...` to launch Dev Proxy with interactive prompts for CLI settings - Install: Added automated install and upgrade support for Linux using official setup scripts - Notification: Detect outdated Dev Proxy config files in workspace and show warning when schema versions don't match installed version - Command: `dev-proxy-toolkit.upgrade-configs` - Upgrade config files with Copilot Chat using Dev Proxy MCP tools diff --git a/README.md b/README.md index c1fa26f..f72ad5c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Control Dev Proxy directly from VS Code via the Command Palette (`Cmd+Shift+P` / | Command | When Available | |---------|----------------| | Start | Dev Proxy not running | +| Start with Options... | Dev Proxy not running | | Stop | Dev Proxy running | | Restart | Dev Proxy running | | Raise mock request | Dev Proxy running | diff --git a/package.json b/package.json index 02266d1..54f28f4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,13 @@ "icon": "$(debug-start)", "enablement": "!isDevProxyRunning" }, + { + "command": "dev-proxy-toolkit.start-with-options", + "title": "Start with Options...", + "category": "Dev Proxy Toolkit", + "icon": "$(debug-alt)", + "enablement": "!isDevProxyRunning" + }, { "command": "dev-proxy-toolkit.stop", "title": "Stop", @@ -120,6 +127,11 @@ "group": "navigation@1", "when": "!activeEditorIsDirty && isDevProxyConfigFile && !isDevProxyRunning" }, + { + "command": "dev-proxy-toolkit.start-with-options", + "group": "navigation@1", + "when": "!activeEditorIsDirty && isDevProxyConfigFile && !isDevProxyRunning" + }, { "command": "dev-proxy-toolkit.stop", "group": "navigation@2", diff --git a/src/commands/proxy.ts b/src/commands/proxy.ts index 3c207d8..9884431 100644 --- a/src/commands/proxy.ts +++ b/src/commands/proxy.ts @@ -8,7 +8,7 @@ import { VersionPreference } from '../enums'; import * as logger from '../logger'; /** - * Proxy lifecycle commands: start, stop, restart. + * Proxy lifecycle commands: start, start with options, stop, restart. * * These commands control the Dev Proxy process. */ @@ -25,6 +25,12 @@ export function registerProxyCommands( vscode.commands.registerCommand(Commands.start, () => startDevProxy(configuration, devProxyExe)) ); + context.subscriptions.push( + vscode.commands.registerCommand(Commands.startWithOptions, () => + startDevProxyWithOptions(devProxyExe) + ) + ); + context.subscriptions.push( vscode.commands.registerCommand(Commands.stop, () => stopDevProxy(apiClient, devProxyExe, configuration) @@ -52,6 +58,306 @@ async function startDevProxy( terminalService.sendCommand(terminal, command); } +async function startDevProxyWithOptions(devProxyExe: string): Promise { + const args: string[] = []; + + // Config file + const configFiles = await vscode.workspace.findFiles( + '**/devproxyrc.{json,jsonc}', + '**/node_modules/**' + ); + const configItems: vscode.QuickPickItem[] = [ + { label: '$(remove) None', description: 'Use default configuration' }, + ...configFiles.map(f => ({ + label: vscode.workspace.asRelativePath(f), + description: f.fsPath, + })), + ]; + + const selectedConfig = await vscode.window.showQuickPick(configItems, { + title: 'Start with Options (1/13): Config file', + placeHolder: 'Select a config file', + }); + if (selectedConfig === undefined) { + return; + } + if (selectedConfig.description && selectedConfig.description !== 'Use default configuration') { + args.push('--config-file', `"${selectedConfig.description}"`); + } + + // Port + const port = await vscode.window.showInputBox({ + title: 'Start with Options (2/13): Port', + prompt: 'Enter the proxy port number', + value: '8000', + validateInput: validatePortNumber, + }); + if (port === undefined) { + return; + } + if (port && port !== '8000') { + args.push('--port', port); + } + + // API port + const apiPort = await vscode.window.showInputBox({ + title: 'Start with Options (3/13): API port', + prompt: 'Enter the API port number', + value: '8897', + validateInput: validatePortNumber, + }); + if (apiPort === undefined) { + return; + } + if (apiPort && apiPort !== '8897') { + args.push('--api-port', apiPort); + } + + // IP address + const ipAddress = await vscode.window.showInputBox({ + title: 'Start with Options (4/13): IP address', + prompt: 'Enter the IP address to listen on', + value: '127.0.0.1', + validateInput: validateIpAddress, + }); + if (ipAddress === undefined) { + return; + } + if (ipAddress && ipAddress !== '127.0.0.1') { + args.push('--ip-address', ipAddress); + } + + // As system proxy + const asSystemProxy = await vscode.window.showQuickPick( + [ + { label: 'Yes', description: 'Register as system proxy (default)' }, + { label: 'No', description: 'Do not register as system proxy' }, + ], + { + title: 'Start with Options (5/13): As system proxy', + placeHolder: 'Register Dev Proxy as a system proxy?', + } + ); + if (asSystemProxy === undefined) { + return; + } + if (asSystemProxy.label === 'No') { + args.push('--as-system-proxy', 'false'); + } + + // Install cert + const installCert = await vscode.window.showQuickPick( + [ + { label: 'Yes', description: 'Install certificate (default)' }, + { label: 'No', description: 'Do not install certificate' }, + ], + { + title: 'Start with Options (6/13): Install certificate', + placeHolder: 'Install the root certificate?', + } + ); + if (installCert === undefined) { + return; + } + if (installCert.label === 'No') { + args.push('--install-cert', 'false'); + } + + // Log level + const logLevel = await vscode.window.showQuickPick( + ['trace', 'debug', 'information', 'warning', 'error'], + { + title: 'Start with Options (7/13): Log level', + placeHolder: 'Select a log level', + } + ); + if (logLevel === undefined) { + return; + } + if (logLevel !== 'information') { + args.push('--log-level', logLevel); + } + + // Failure rate + const failureRate = await vscode.window.showInputBox({ + title: 'Start with Options (8/13): Failure rate', + prompt: 'Enter the failure rate (0-100)', + value: '50', + validateInput: validateFailureRate, + }); + if (failureRate === undefined) { + return; + } + if (failureRate && failureRate !== '50') { + args.push('--failure-rate', failureRate); + } + + // URLs to watch + const urlsToWatch = await vscode.window.showInputBox({ + title: 'Start with Options (9/13): URLs to watch', + prompt: 'Enter URLs to watch (space separated). Leave empty to use config file values.', + placeHolder: 'https://api.example.com/* https://graph.microsoft.com/v1.0/*', + value: '', + }); + if (urlsToWatch === undefined) { + return; + } + if (urlsToWatch.trim()) { + args.push('--urls-to-watch', urlsToWatch.trim()); + } + + // Record + const record = await vscode.window.showQuickPick( + [ + { label: 'No', description: 'Do not record (default)' }, + { label: 'Yes', description: 'Start recording immediately' }, + ], + { + title: 'Start with Options (10/13): Record', + placeHolder: 'Start recording on launch?', + } + ); + if (record === undefined) { + return; + } + if (record.label === 'Yes') { + args.push('--record'); + } + + // No first run + const noFirstRun = await vscode.window.showQuickPick( + [ + { label: 'No', description: 'Show first run experience (default)' }, + { label: 'Yes', description: 'Skip first run experience' }, + ], + { + title: 'Start with Options (11/13): No first run', + placeHolder: 'Skip the first run experience?', + } + ); + if (noFirstRun === undefined) { + return; + } + if (noFirstRun.label === 'Yes') { + args.push('--no-first-run'); + } + + // Timeout + const timeout = await vscode.window.showInputBox({ + title: 'Start with Options (12/13): Timeout', + prompt: 'Enter timeout in seconds. Leave empty for no timeout.', + value: '', + validateInput: validateTimeout, + }); + if (timeout === undefined) { + return; + } + if (timeout.trim()) { + args.push('--timeout', timeout.trim()); + } + + // Watch PIDs + const watchPids = await vscode.window.showInputBox({ + title: 'Start with Options (13/13): Watch PIDs and process names', + prompt: 'Enter process IDs to watch (space separated). Leave empty to skip.', + placeHolder: '1234 5678', + value: '', + validateInput: validateWatchPids, + }); + if (watchPids === undefined) { + return; + } + if (watchPids.trim()) { + args.push('--watch-pids', watchPids.trim()); + } + + // Watch process names + const watchProcessNames = await vscode.window.showInputBox({ + prompt: 'Enter process names to watch (space separated). Leave empty to skip.', + placeHolder: 'msedge chrome', + value: '', + validateInput: validateProcessNames, + }); + if (watchProcessNames === undefined) { + return; + } + if (watchProcessNames.trim()) { + args.push('--watch-process-names', watchProcessNames.trim()); + } + + const command = [devProxyExe, ...args].join(' '); + const terminalService = TerminalService.fromConfiguration(); + const terminal = terminalService.getOrCreateTerminal(); + terminalService.sendCommand(terminal, command); +} + +function validatePortNumber(value: string): string | undefined { + if (!value) { + return undefined; + } + const num = Number(value); + if (!Number.isInteger(num) || num < 1 || num > 65535) { + return 'Port must be an integer between 1 and 65535'; + } + return undefined; +} + +function validateIpAddress(value: string): string | undefined { + if (!value) { + return undefined; + } + if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(value)) { + return 'Enter a valid IPv4 address (e.g. 127.0.0.1)'; + } + return undefined; +} + +function validateFailureRate(value: string): string | undefined { + if (!value) { + return undefined; + } + const num = Number(value); + if (!Number.isInteger(num) || num < 0 || num > 100) { + return 'Failure rate must be an integer between 0 and 100'; + } + return undefined; +} + +function validateTimeout(value: string): string | undefined { + if (!value) { + return undefined; + } + const num = Number(value); + if (!Number.isInteger(num) || num < 1) { + return 'Timeout must be a positive integer'; + } + return undefined; +} + +function validateWatchPids(value: string): string | undefined { + if (!value) { + return undefined; + } + const parts = value.trim().split(/\s+/); + for (const part of parts) { + const num = Number(part); + if (!Number.isInteger(num) || num < 1) { + return 'PIDs must be positive integers separated by spaces'; + } + } + return undefined; +} + +function validateProcessNames(value: string): string | undefined { + if (!value) { + return undefined; + } + if (!/^[a-zA-Z0-9\s]+$/.test(value)) { + return 'Process names can only contain alphanumeric characters and spaces'; + } + return undefined; +} + async function stopDevProxy( apiClient: DevProxyApiClient, devProxyExe: string, diff --git a/src/constants.ts b/src/constants.ts index f4ce5a4..764ee7f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,6 +10,7 @@ export const Commands = { // Proxy lifecycle commands start: 'dev-proxy-toolkit.start', + startWithOptions: 'dev-proxy-toolkit.start-with-options', stop: 'dev-proxy-toolkit.stop', restart: 'dev-proxy-toolkit.restart', diff --git a/src/test/commands.test.ts b/src/test/commands.test.ts index 622dafc..220dc0d 100644 --- a/src/test/commands.test.ts +++ b/src/test/commands.test.ts @@ -21,6 +21,13 @@ suite('Command Registration', () => { ); }); + test('start-with-options command should be registered', () => { + assert.ok( + registeredCommands.includes(Commands.startWithOptions), + `Command ${Commands.startWithOptions} should be registered` + ); + }); + test('stop command should be registered', () => { assert.ok( registeredCommands.includes(Commands.stop), From c7d3f5e94337fe8f43059f6f693764992611a7f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:25:16 +0000 Subject: [PATCH 3/5] Address code review: fix title, IP validation, menu ordering Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- package.json | 2 +- src/commands/proxy.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 54f28f4..cd1936d 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ }, { "command": "dev-proxy-toolkit.start-with-options", - "group": "navigation@1", + "group": "navigation@2", "when": "!activeEditorIsDirty && isDevProxyConfigFile && !isDevProxyRunning" }, { diff --git a/src/commands/proxy.ts b/src/commands/proxy.ts index 9884431..6eea333 100644 --- a/src/commands/proxy.ts +++ b/src/commands/proxy.ts @@ -258,7 +258,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { // Watch PIDs const watchPids = await vscode.window.showInputBox({ - title: 'Start with Options (13/13): Watch PIDs and process names', + title: 'Start with Options (13/13): Watch PIDs', prompt: 'Enter process IDs to watch (space separated). Leave empty to skip.', placeHolder: '1234 5678', value: '', @@ -306,9 +306,16 @@ function validateIpAddress(value: string): string | undefined { if (!value) { return undefined; } - if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(value)) { + const parts = value.split('.'); + if (parts.length !== 4) { return 'Enter a valid IPv4 address (e.g. 127.0.0.1)'; } + for (const part of parts) { + const num = Number(part); + if (!/^\d{1,3}$/.test(part) || num < 0 || num > 255) { + return 'Enter a valid IPv4 address (e.g. 127.0.0.1)'; + } + } return undefined; } From 5e248472b1ce25e4e8405c22b00939e35106803c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:18:39 +0000 Subject: [PATCH 4/5] Remove config picker, use active config file like existing start command Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- src/commands/proxy.ts | 49 +++++++++++++------------------------------ 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/src/commands/proxy.ts b/src/commands/proxy.ts index 6eea333..ae6dca5 100644 --- a/src/commands/proxy.ts +++ b/src/commands/proxy.ts @@ -61,33 +61,14 @@ async function startDevProxy( async function startDevProxyWithOptions(devProxyExe: string): Promise { const args: string[] = []; - // Config file - const configFiles = await vscode.workspace.findFiles( - '**/devproxyrc.{json,jsonc}', - '**/node_modules/**' - ); - const configItems: vscode.QuickPickItem[] = [ - { label: '$(remove) None', description: 'Use default configuration' }, - ...configFiles.map(f => ({ - label: vscode.workspace.asRelativePath(f), - description: f.fsPath, - })), - ]; - - const selectedConfig = await vscode.window.showQuickPick(configItems, { - title: 'Start with Options (1/13): Config file', - placeHolder: 'Select a config file', - }); - if (selectedConfig === undefined) { - return; - } - if (selectedConfig.description && selectedConfig.description !== 'Use default configuration') { - args.push('--config-file', `"${selectedConfig.description}"`); + const configFilePath = getActiveConfigFilePath(); + if (configFilePath) { + args.push('--config-file', `"${configFilePath}"`); } // Port const port = await vscode.window.showInputBox({ - title: 'Start with Options (2/13): Port', + title: 'Start with Options (1/12): Port', prompt: 'Enter the proxy port number', value: '8000', validateInput: validatePortNumber, @@ -101,7 +82,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { // API port const apiPort = await vscode.window.showInputBox({ - title: 'Start with Options (3/13): API port', + title: 'Start with Options (2/12): API port', prompt: 'Enter the API port number', value: '8897', validateInput: validatePortNumber, @@ -115,7 +96,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { // IP address const ipAddress = await vscode.window.showInputBox({ - title: 'Start with Options (4/13): IP address', + title: 'Start with Options (3/12): IP address', prompt: 'Enter the IP address to listen on', value: '127.0.0.1', validateInput: validateIpAddress, @@ -134,7 +115,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { { label: 'No', description: 'Do not register as system proxy' }, ], { - title: 'Start with Options (5/13): As system proxy', + title: 'Start with Options (4/12): As system proxy', placeHolder: 'Register Dev Proxy as a system proxy?', } ); @@ -152,7 +133,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { { label: 'No', description: 'Do not install certificate' }, ], { - title: 'Start with Options (6/13): Install certificate', + title: 'Start with Options (5/12): Install certificate', placeHolder: 'Install the root certificate?', } ); @@ -167,7 +148,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { const logLevel = await vscode.window.showQuickPick( ['trace', 'debug', 'information', 'warning', 'error'], { - title: 'Start with Options (7/13): Log level', + title: 'Start with Options (6/12): Log level', placeHolder: 'Select a log level', } ); @@ -180,7 +161,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { // Failure rate const failureRate = await vscode.window.showInputBox({ - title: 'Start with Options (8/13): Failure rate', + title: 'Start with Options (7/12): Failure rate', prompt: 'Enter the failure rate (0-100)', value: '50', validateInput: validateFailureRate, @@ -194,7 +175,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { // URLs to watch const urlsToWatch = await vscode.window.showInputBox({ - title: 'Start with Options (9/13): URLs to watch', + title: 'Start with Options (8/12): URLs to watch', prompt: 'Enter URLs to watch (space separated). Leave empty to use config file values.', placeHolder: 'https://api.example.com/* https://graph.microsoft.com/v1.0/*', value: '', @@ -213,7 +194,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { { label: 'Yes', description: 'Start recording immediately' }, ], { - title: 'Start with Options (10/13): Record', + title: 'Start with Options (9/12): Record', placeHolder: 'Start recording on launch?', } ); @@ -231,7 +212,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { { label: 'Yes', description: 'Skip first run experience' }, ], { - title: 'Start with Options (11/13): No first run', + title: 'Start with Options (10/12): No first run', placeHolder: 'Skip the first run experience?', } ); @@ -244,7 +225,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { // Timeout const timeout = await vscode.window.showInputBox({ - title: 'Start with Options (12/13): Timeout', + title: 'Start with Options (11/12): Timeout', prompt: 'Enter timeout in seconds. Leave empty for no timeout.', value: '', validateInput: validateTimeout, @@ -258,7 +239,7 @@ async function startDevProxyWithOptions(devProxyExe: string): Promise { // Watch PIDs const watchPids = await vscode.window.showInputBox({ - title: 'Start with Options (13/13): Watch PIDs', + title: 'Start with Options (12/12): Watch PIDs', prompt: 'Enter process IDs to watch (space separated). Leave empty to skip.', placeHolder: '1234 5678', value: '', From 4cb572d317340e6070aac3cec59f1fe51aa20126 Mon Sep 17 00:00:00 2001 From: Garry Trinder Date: Tue, 3 Mar 2026 13:34:02 +0000 Subject: [PATCH 5/5] refactor: replace sequential wizard with iterative QuickPick for start with options Replace 12 sequential input prompts with a single iterative QuickPick menu. Users see all options with defaults, edit only what they need, and launch when ready. Add config file picker with smart defaults: - Active editor config file takes priority - Falls back to workspace devproxyrc.json - Otherwise uses install folder default - Picker shows all workspace config files for override --- src/commands/proxy.ts | 483 +++++++++++++++++++++++++++--------------- 1 file changed, 307 insertions(+), 176 deletions(-) diff --git a/src/commands/proxy.ts b/src/commands/proxy.ts index ae6dca5..7c643d5 100644 --- a/src/commands/proxy.ts +++ b/src/commands/proxy.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as path from 'path'; import { Commands, ContextKeys } from '../constants'; import { DevProxyApiClient } from '../services/api-client'; import { TerminalService } from '../services/terminal'; @@ -58,218 +59,348 @@ async function startDevProxy( terminalService.sendCommand(terminal, command); } +interface StartOption { + key: string; + label: string; + value: string; + defaultValue: string; + flag: string; + editor: 'input' | 'pick' | 'config'; + prompt?: string; + placeholder?: string; + validate?: (value: string) => string | undefined; + choices?: string[]; +} + +const startOptionDefaults: Omit[] = [ + { + key: 'configFile', + label: 'Config file', + defaultValue: '', + flag: '--config-file', + editor: 'config', + }, + { + key: 'port', + label: 'Port', + defaultValue: '8000', + flag: '--port', + editor: 'input', + prompt: 'Enter the proxy port number', + validate: validatePortNumber, + }, + { + key: 'apiPort', + label: 'API port', + defaultValue: '8897', + flag: '--api-port', + editor: 'input', + prompt: 'Enter the API port number', + validate: validatePortNumber, + }, + { + key: 'ipAddress', + label: 'IP address', + defaultValue: '127.0.0.1', + flag: '--ip-address', + editor: 'input', + prompt: 'Enter the IP address to listen on', + validate: validateIpAddress, + }, + { + key: 'asSystemProxy', + label: 'As system proxy', + defaultValue: 'Yes', + flag: '--as-system-proxy', + editor: 'pick', + choices: ['Yes', 'No'], + }, + { + key: 'installCert', + label: 'Install certificate', + defaultValue: 'Yes', + flag: '--install-cert', + editor: 'pick', + choices: ['Yes', 'No'], + }, + { + key: 'logLevel', + label: 'Log level', + defaultValue: 'information', + flag: '--log-level', + editor: 'pick', + choices: ['trace', 'debug', 'information', 'warning', 'error'], + }, + { + key: 'failureRate', + label: 'Failure rate', + defaultValue: '50', + flag: '--failure-rate', + editor: 'input', + prompt: 'Enter the failure rate (0-100)', + validate: validateFailureRate, + }, + { + key: 'urlsToWatch', + label: 'URLs to watch', + defaultValue: '', + flag: '--urls-to-watch', + editor: 'input', + prompt: 'Enter URLs to watch (space separated). Leave empty to use config file values.', + placeholder: 'https://api.example.com/* https://graph.microsoft.com/v1.0/*', + }, + { + key: 'record', + label: 'Record', + defaultValue: 'No', + flag: '--record', + editor: 'pick', + choices: ['No', 'Yes'], + }, + { + key: 'noFirstRun', + label: 'No first run', + defaultValue: 'No', + flag: '--no-first-run', + editor: 'pick', + choices: ['No', 'Yes'], + }, + { + key: 'timeout', + label: 'Timeout', + defaultValue: '', + flag: '--timeout', + editor: 'input', + prompt: 'Enter timeout in seconds. Leave empty for no timeout.', + validate: validateTimeout, + }, + { + key: 'watchPids', + label: 'Watch PIDs', + defaultValue: '', + flag: '--watch-pids', + editor: 'input', + prompt: 'Enter process IDs to watch (space separated). Leave empty to skip.', + placeholder: '1234 5678', + validate: validateWatchPids, + }, + { + key: 'watchProcessNames', + label: 'Watch process names', + defaultValue: '', + flag: '--watch-process-names', + editor: 'input', + prompt: 'Enter process names to watch (space separated). Leave empty to skip.', + placeholder: 'msedge chrome', + validate: validateProcessNames, + }, +]; + +const startItemLabel = '$(play) Start Dev Proxy'; + async function startDevProxyWithOptions(devProxyExe: string): Promise { - const args: string[] = []; + const configDefault = await resolveDefaultConfigFile(); + const options: StartOption[] = startOptionDefaults.map(o => ({ + ...o, + value: o.key === 'configFile' ? configDefault : o.defaultValue, + defaultValue: o.key === 'configFile' ? configDefault : o.defaultValue, + })); + + while (true) { + const items: vscode.QuickPickItem[] = [ + { + label: startItemLabel, + description: 'Launch with the options below', + alwaysShow: true, + }, + { label: '', kind: vscode.QuickPickItemKind.Separator }, + ...options.map(o => ({ + label: o.label, + description: formatOptionValue(o), + })), + ]; + + const picked = await vscode.window.showQuickPick(items, { + title: 'Start with Options', + placeHolder: 'Select an option to change, or start Dev Proxy', + }); + + if (picked === undefined) { + return; + } - const configFilePath = getActiveConfigFilePath(); - if (configFilePath) { - args.push('--config-file', `"${configFilePath}"`); + if (picked.label === startItemLabel) { + break; + } + + const option = options.find(o => o.label === picked.label); + if (!option) { + continue; + } + + const newValue = await editOption(option); + if (newValue !== undefined) { + option.value = newValue; + } } - // Port - const port = await vscode.window.showInputBox({ - title: 'Start with Options (1/12): Port', - prompt: 'Enter the proxy port number', - value: '8000', - validateInput: validatePortNumber, - }); - if (port === undefined) { - return; + const args = buildArgs(options); + + const command = [devProxyExe, ...args].join(' '); + const terminalService = TerminalService.fromConfiguration(); + const terminal = terminalService.getOrCreateTerminal(); + terminalService.sendCommand(terminal, command); +} + +function formatOptionValue(option: StartOption): string { + if (option.key === 'configFile') { + if (!option.value) { + return 'devproxyrc.json (install folder)'; + } + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders) { + const relativePath = vscode.workspace.asRelativePath(option.value); + return relativePath + (option.value === option.defaultValue ? ' (default)' : ''); + } + return path.basename(option.value) + (option.value === option.defaultValue ? ' (default)' : ''); } - if (port && port !== '8000') { - args.push('--port', port); + if (!option.value) { + return '(not set)'; } + if (option.value === option.defaultValue) { + return `${option.value} (default)`; + } + return option.value; +} - // API port - const apiPort = await vscode.window.showInputBox({ - title: 'Start with Options (2/12): API port', - prompt: 'Enter the API port number', - value: '8897', - validateInput: validatePortNumber, - }); - if (apiPort === undefined) { - return; +async function editOption(option: StartOption): Promise { + if (option.editor === 'config') { + return editConfigFile(option.value); } - if (apiPort && apiPort !== '8897') { - args.push('--api-port', apiPort); + + if (option.editor === 'pick' && option.choices) { + const picked = await vscode.window.showQuickPick(option.choices, { + title: option.label, + placeHolder: `Select a value for ${option.label}`, + }); + return picked; } - // IP address - const ipAddress = await vscode.window.showInputBox({ - title: 'Start with Options (3/12): IP address', - prompt: 'Enter the IP address to listen on', - value: '127.0.0.1', - validateInput: validateIpAddress, + return vscode.window.showInputBox({ + title: option.label, + prompt: option.prompt, + value: option.value, + placeHolder: option.placeholder, + validateInput: option.validate, }); - if (ipAddress === undefined) { - return; - } - if (ipAddress && ipAddress !== '127.0.0.1') { - args.push('--ip-address', ipAddress); - } +} - // As system proxy - const asSystemProxy = await vscode.window.showQuickPick( - [ - { label: 'Yes', description: 'Register as system proxy (default)' }, - { label: 'No', description: 'Do not register as system proxy' }, - ], - { - title: 'Start with Options (4/12): As system proxy', - placeHolder: 'Register Dev Proxy as a system proxy?', +function buildArgs(options: StartOption[]): string[] { + const args: string[] = []; + + for (const option of options) { + // Config file: include if set (even if it's the default — user chose it explicitly) + if (option.key === 'configFile') { + if (option.value) { + args.push(option.flag, `"${option.value}"`); + } + continue; } - ); - if (asSystemProxy === undefined) { - return; - } - if (asSystemProxy.label === 'No') { - args.push('--as-system-proxy', 'false'); - } - // Install cert - const installCert = await vscode.window.showQuickPick( - [ - { label: 'Yes', description: 'Install certificate (default)' }, - { label: 'No', description: 'Do not install certificate' }, - ], - { - title: 'Start with Options (5/12): Install certificate', - placeHolder: 'Install the root certificate?', + if (option.value === option.defaultValue) { + continue; } - ); - if (installCert === undefined) { - return; - } - if (installCert.label === 'No') { - args.push('--install-cert', 'false'); - } - // Log level - const logLevel = await vscode.window.showQuickPick( - ['trace', 'debug', 'information', 'warning', 'error'], - { - title: 'Start with Options (6/12): Log level', - placeHolder: 'Select a log level', + // Boolean-style flags + if (option.key === 'asSystemProxy' && option.value === 'No') { + args.push(option.flag, 'false'); + } else if (option.key === 'installCert' && option.value === 'No') { + args.push(option.flag, 'false'); + } else if (option.key === 'record' && option.value === 'Yes') { + args.push(option.flag); + } else if (option.key === 'noFirstRun' && option.value === 'Yes') { + args.push(option.flag); + } else if (option.value.trim()) { + args.push(option.flag, option.value.trim()); } - ); - if (logLevel === undefined) { - return; - } - if (logLevel !== 'information') { - args.push('--log-level', logLevel); } - // Failure rate - const failureRate = await vscode.window.showInputBox({ - title: 'Start with Options (7/12): Failure rate', - prompt: 'Enter the failure rate (0-100)', - value: '50', - validateInput: validateFailureRate, - }); - if (failureRate === undefined) { - return; - } - if (failureRate && failureRate !== '50') { - args.push('--failure-rate', failureRate); - } + return args; +} - // URLs to watch - const urlsToWatch = await vscode.window.showInputBox({ - title: 'Start with Options (8/12): URLs to watch', - prompt: 'Enter URLs to watch (space separated). Leave empty to use config file values.', - placeHolder: 'https://api.example.com/* https://graph.microsoft.com/v1.0/*', - value: '', - }); - if (urlsToWatch === undefined) { - return; +async function resolveDefaultConfigFile(): Promise { + // 1. Active editor has a config file open → use that + const activeConfig = getActiveConfigFilePath(); + if (activeConfig) { + return activeConfig; } - if (urlsToWatch.trim()) { - args.push('--urls-to-watch', urlsToWatch.trim()); + + // 2. devproxyrc.json exists in the workspace → use that + const workspaceFiles = await vscode.workspace.findFiles('**/devproxyrc.json', '**/node_modules/**', 1); + if (workspaceFiles.length > 0) { + return workspaceFiles[0].fsPath; } - // Record - const record = await vscode.window.showQuickPick( - [ - { label: 'No', description: 'Do not record (default)' }, - { label: 'Yes', description: 'Start recording immediately' }, - ], - { - title: 'Start with Options (9/12): Record', - placeHolder: 'Start recording on launch?', + // 3. Otherwise empty — Dev Proxy will use its install folder default + return ''; +} + +async function findWorkspaceConfigFiles(): Promise { + const jsonFiles = await vscode.workspace.findFiles('**/*.{json,jsonc}', '**/node_modules/**'); + const configFiles: vscode.Uri[] = []; + + for (const uri of jsonFiles) { + try { + const doc = await vscode.workspace.openTextDocument(uri); + if (isConfigFile(doc)) { + configFiles.push(uri); + } + } catch { + // Skip files that can't be opened } - ); - if (record === undefined) { - return; - } - if (record.label === 'Yes') { - args.push('--record'); } - // No first run - const noFirstRun = await vscode.window.showQuickPick( - [ - { label: 'No', description: 'Show first run experience (default)' }, - { label: 'Yes', description: 'Skip first run experience' }, - ], + return configFiles; +} + +async function editConfigFile(currentValue: string): Promise { + const configFiles = await findWorkspaceConfigFiles(); + + const items: vscode.QuickPickItem[] = [ { - title: 'Start with Options (10/12): No first run', - placeHolder: 'Skip the first run experience?', + label: '$(home) Use install folder default', + description: 'devproxyrc.json', + detail: 'Use the default config file from the Dev Proxy install folder', + }, + ]; + + if (configFiles.length > 0) { + items.push({ label: '', kind: vscode.QuickPickItemKind.Separator }); + + for (const uri of configFiles) { + const relativePath = vscode.workspace.asRelativePath(uri); + items.push({ + label: relativePath, + description: uri.fsPath === currentValue ? '(current)' : undefined, + }); } - ); - if (noFirstRun === undefined) { - return; - } - if (noFirstRun.label === 'Yes') { - args.push('--no-first-run'); } - // Timeout - const timeout = await vscode.window.showInputBox({ - title: 'Start with Options (11/12): Timeout', - prompt: 'Enter timeout in seconds. Leave empty for no timeout.', - value: '', - validateInput: validateTimeout, + const picked = await vscode.window.showQuickPick(items, { + title: 'Config file', + placeHolder: 'Select a config file', }); - if (timeout === undefined) { - return; - } - if (timeout.trim()) { - args.push('--timeout', timeout.trim()); - } - // Watch PIDs - const watchPids = await vscode.window.showInputBox({ - title: 'Start with Options (12/12): Watch PIDs', - prompt: 'Enter process IDs to watch (space separated). Leave empty to skip.', - placeHolder: '1234 5678', - value: '', - validateInput: validateWatchPids, - }); - if (watchPids === undefined) { - return; - } - if (watchPids.trim()) { - args.push('--watch-pids', watchPids.trim()); + if (picked === undefined) { + return undefined; } - // Watch process names - const watchProcessNames = await vscode.window.showInputBox({ - prompt: 'Enter process names to watch (space separated). Leave empty to skip.', - placeHolder: 'msedge chrome', - value: '', - validateInput: validateProcessNames, - }); - if (watchProcessNames === undefined) { - return; - } - if (watchProcessNames.trim()) { - args.push('--watch-process-names', watchProcessNames.trim()); + if (picked.label === '$(home) Use install folder default') { + return ''; } - const command = [devProxyExe, ...args].join(' '); - const terminalService = TerminalService.fromConfiguration(); - const terminal = terminalService.getOrCreateTerminal(); - terminalService.sendCommand(terminal, command); + const match = configFiles.find(uri => vscode.workspace.asRelativePath(uri) === picked.label); + return match?.fsPath ?? currentValue; } function validatePortNumber(value: string): string | undefined {