From fa55666498c4717f0af211a4a661478073050b41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:22:19 +0000 Subject: [PATCH 1/5] Initial plan From cd512d89090011d1a13259502f9fc140cfa933fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:30:21 +0000 Subject: [PATCH 2/5] feat: detect outdated Dev Proxy config files and offer Copilot upgrade Add version mismatch detection for Dev Proxy config files in the workspace. When the schema version in config files doesn't match the installed Dev Proxy version, show a warning notification offering to upgrade using Copilot Chat. - Add extractVersionFromSchemaUrl() to extract version from schema URLs - Add findOutdatedConfigFiles() to scan workspace for outdated configs - Add upgradeConfigs command that opens Copilot Chat with upgrade prompt - Add handleOutdatedConfigFilesNotification() for the warning UI - Add tests for version extraction and command registration Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- package.json | 6 +++ src/commands/index.ts | 4 ++ src/commands/upgrade-config.ts | 55 +++++++++++++++++++++++++++ src/constants.ts | 5 ++- src/extension.ts | 6 ++- src/notifications.ts | 42 +++++++++++++++++++++ src/test/config-detection.test.ts | 29 +++++++++++++- src/test/extension.test.ts | 6 +++ src/utils/config-detection.ts | 63 +++++++++++++++++++++++++++++++ src/utils/index.ts | 2 +- 10 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 src/commands/upgrade-config.ts diff --git a/package.json b/package.json index d17d028..02266d1 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,12 @@ "command": "dev-proxy-toolkit.reset-state", "title": "Reset State", "category": "Dev Proxy Toolkit" + }, + { + "command": "dev-proxy-toolkit.upgrade-configs", + "title": "Upgrade config files", + "category": "Dev Proxy Toolkit", + "enablement": "isDevProxyInstalled" } ], "mcpServerDefinitionProviders": [ diff --git a/src/commands/index.ts b/src/commands/index.ts index 35d8e9e..008a8fa 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -8,6 +8,7 @@ import { registerDiscoveryCommands } from './discovery'; import { registerDocCommands } from './docs'; import { Commands } from '../constants'; import { addExtensionToRecommendations } from '../utils'; +import { registerUpgradeConfigCommands } from './upgrade-config'; /** * Register all commands for the extension. @@ -58,6 +59,8 @@ export function registerCommands( }); }) ); + + registerUpgradeConfigCommands(context); } // Re-export individual modules for testing and direct access @@ -68,3 +71,4 @@ export { registerInstallCommands } from './install'; export { registerJwtCommands } from './jwt'; export { registerDiscoveryCommands } from './discovery'; export { registerDocCommands } from './docs'; +export { registerUpgradeConfigCommands } from './upgrade-config'; diff --git a/src/commands/upgrade-config.ts b/src/commands/upgrade-config.ts new file mode 100644 index 0000000..e5fd495 --- /dev/null +++ b/src/commands/upgrade-config.ts @@ -0,0 +1,55 @@ +import * as vscode from 'vscode'; +import { Commands } from '../constants'; +import { DevProxyInstall } from '../types'; + +/** + * Config upgrade commands. + */ + +export function registerUpgradeConfigCommands( + context: vscode.ExtensionContext, +): void { + context.subscriptions.push( + vscode.commands.registerCommand(Commands.upgradeConfigs, (fileUris?: vscode.Uri[]) => + upgradeConfigsWithCopilot(context, fileUris) + ) + ); +} + +async function upgradeConfigsWithCopilot( + context: vscode.ExtensionContext, + fileUris?: vscode.Uri[], +): Promise { + const devProxyInstall = context.globalState.get('devProxyInstall'); + if (!devProxyInstall?.isInstalled) { + return; + } + + const devProxyVersion = devProxyInstall.isBeta + ? devProxyInstall.version.split('-')[0] + : devProxyInstall.version; + + const fileList = fileUris?.length + ? fileUris.map(uri => `- ${vscode.workspace.asRelativePath(uri)}`).join('\n') + : 'all Dev Proxy config files in the workspace'; + + const prompt = [ + `Upgrade the following Dev Proxy configuration files to version v${devProxyVersion}:`, + '', + fileList, + '', + `Use the Dev Proxy MCP tools to get the latest schema information for v${devProxyVersion} and update each config file.`, + 'Update the $schema URLs and make any necessary configuration changes for the new version.', + ].join('\n'); + + try { + await vscode.commands.executeCommand('workbench.action.chat.open', { + query: prompt, + isPartialQuery: false, + }); + } catch { + vscode.window.showWarningMessage( + 'Could not open Copilot Chat. Please make sure GitHub Copilot is installed and enabled.' + ); + } +} diff --git a/src/constants.ts b/src/constants.ts index b97d9a4..f4ce5a4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,9 +40,12 @@ export const Commands = { // Language model commands addLanguageModelConfig: 'dev-proxy-toolkit.addLanguageModelConfig', - // Workspace commands +// Workspace commands addToRecommendations: 'dev-proxy-toolkit.add-to-recommendations', resetState: 'dev-proxy-toolkit.reset-state', + + // Config upgrade commands + upgradeConfigs: 'dev-proxy-toolkit.upgrade-configs', } as const; /** diff --git a/src/extension.ts b/src/extension.ts index e760d77..1cb62a3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { registerCommands } from './commands'; -import { handleStartNotification, processNotification } from './notifications'; +import { handleStartNotification, processNotification, handleOutdatedConfigFilesNotification } from './notifications'; import { registerDocumentListeners } from './documents'; import { registerCodeLens } from './code-lens'; import { createStatusBar, statusBarLoop, updateStatusBar } from './status-bar'; @@ -36,9 +36,11 @@ export const activate = async (context: vscode.ExtensionContext): Promise { const devProxyInstall = context.globalState.get('devProxyInstall'); @@ -50,3 +52,43 @@ export const handleStartNotification = (context: vscode.ExtensionContext) => { export const processNotification = (notification: (() => { message: string; show: () => Promise; }) | undefined) => { if (notification) { notification().show(); }; }; + +/** + * Check for outdated config files and notify the user. + * + * Scans the workspace for Dev Proxy config files whose schema version + * doesn't match the installed Dev Proxy version and offers to upgrade + * them using Copilot Chat. + */ +export async function handleOutdatedConfigFilesNotification( + context: vscode.ExtensionContext, +): Promise { + const devProxyInstall = context.globalState.get('devProxyInstall'); + if (!devProxyInstall?.isInstalled) { + return; + } + + const devProxyVersion = devProxyInstall.isBeta + ? devProxyInstall.version.split('-')[0] + : devProxyInstall.version; + + const outdatedFiles = await findOutdatedConfigFiles(devProxyVersion); + + if (outdatedFiles.length === 0) { + return; + } + + const fileCount = outdatedFiles.length; + const fileWord = fileCount === 1 ? 'file' : 'files'; + const message = `${fileCount} Dev Proxy config ${fileWord} found with a schema version that doesn't match the installed version (v${devProxyVersion}).`; + + const result = await vscode.window.showWarningMessage( + message, + 'Upgrade with Copilot', + 'Dismiss', + ); + + if (result === 'Upgrade with Copilot') { + await vscode.commands.executeCommand(Commands.upgradeConfigs, outdatedFiles); + } +} diff --git a/src/test/config-detection.test.ts b/src/test/config-detection.test.ts index 59fc0be..c01b7c2 100644 --- a/src/test/config-detection.test.ts +++ b/src/test/config-detection.test.ts @@ -1,10 +1,11 @@ /** * Config file detection tests. * Verifies isConfigFile correctly identifies Dev Proxy configuration files. + * Verifies extractVersionFromSchemaUrl correctly extracts versions from schema URLs. */ import * as assert from 'assert'; import * as vscode from 'vscode'; -import { isConfigFile, sleep } from '../utils'; +import { isConfigFile, extractVersionFromSchemaUrl, sleep } from '../utils'; import { getFixturePath, testDevProxyInstall, getExtensionContext } from './helpers'; suite('isConfigFile', () => { @@ -83,3 +84,29 @@ suite('isConfigFile', () => { assert.strictEqual(actual, expected); }); }); + +suite('extractVersionFromSchemaUrl', () => { + test('should extract version from standard schema URL', () => { + const url = 'https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.24.0/rc.schema.json'; + assert.strictEqual(extractVersionFromSchemaUrl(url), '0.24.0'); + }); + + test('should extract version from legacy schema URL', () => { + const url = 'https://raw.githubusercontent.com/microsoft/dev-proxy/main/schemas/v0.14.1/rc.schema.json'; + assert.strictEqual(extractVersionFromSchemaUrl(url), '0.14.1'); + }); + + test('should extract pre-release version from schema URL', () => { + const url = 'https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.24.0-beta.1/rc.schema.json'; + assert.strictEqual(extractVersionFromSchemaUrl(url), '0.24.0-beta.1'); + }); + + test('should return empty string for URL without version', () => { + const url = 'https://example.com/schema.json'; + assert.strictEqual(extractVersionFromSchemaUrl(url), ''); + }); + + test('should return empty string for empty string', () => { + assert.strictEqual(extractVersionFromSchemaUrl(''), ''); + }); +}); diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index b14c7ce..6ff11eb 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -26,4 +26,10 @@ suite('Commands', () => { const jwtCreateCommand = commands.find(cmd => cmd === 'dev-proxy-toolkit.jwt-create'); assert.ok(jwtCreateCommand, 'JWT create command should be registered'); }); + + test('Upgrade configs command should be registered', async () => { + const commands = await vscode.commands.getCommands(); + const upgradeConfigsCommand = commands.find(cmd => cmd === 'dev-proxy-toolkit.upgrade-configs'); + assert.ok(upgradeConfigsCommand, 'Upgrade configs command should be registered'); + }); }); diff --git a/src/utils/config-detection.ts b/src/utils/config-detection.ts index 1adf162..2edc3fd 100644 --- a/src/utils/config-detection.ts +++ b/src/utils/config-detection.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; import parse from 'json-to-ast'; /** @@ -87,3 +88,65 @@ export function isProxyFile(document: vscode.TextDocument): boolean { return false; } } + +/** + * Extract version from a Dev Proxy schema URL. + * + * Schema URLs follow the pattern: + * https://raw.githubusercontent.com/.../schemas/v{version}/{filename} + * + * @returns The version string (e.g., "0.24.0") or empty string if not found. + */ +export function extractVersionFromSchemaUrl(schemaUrl: string): string { + const match = schemaUrl.match(/\/v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)\//); + return match ? match[1] : ''; +} + +/** + * Find all Dev Proxy config files in the workspace that have an outdated schema version. + * + * Scans the workspace for JSON files containing a Dev Proxy `$schema` property + * and compares the schema version against the installed Dev Proxy version. + * + * @param devProxyVersion The installed Dev Proxy version (e.g., "0.24.0") + * @returns Array of URIs for config files with mismatched schema versions. + */ +export async function findOutdatedConfigFiles(devProxyVersion: string): Promise { + const outdatedFiles: vscode.Uri[] = []; + + const jsonFiles = await vscode.workspace.findFiles('**/*.{json,jsonc}', '**/node_modules/**'); + + for (const uri of jsonFiles) { + try { + const content = fs.readFileSync(uri.fsPath, 'utf-8'); + + // Quick check before parsing + if (!content.includes('dev-proxy') || !content.includes('schema')) { + continue; + } + + const documentNode = parse(content) as parse.ObjectNode; + const schemaNode = getASTNode(documentNode.children, 'Identifier', '$schema'); + + if (!schemaNode) { + continue; + } + + const schemaValue = (schemaNode.value as parse.LiteralNode).value as string; + + if (!schemaValue.includes('dev-proxy') || !schemaValue.endsWith('rc.schema.json')) { + continue; + } + + const schemaVersion = extractVersionFromSchemaUrl(schemaValue); + + if (schemaVersion && schemaVersion !== devProxyVersion) { + outdatedFiles.push(uri); + } + } catch { + // Skip files that can't be read or parsed + } + } + + return outdatedFiles; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index b0089c2..c6bd45e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -16,7 +16,7 @@ export { } from './ast'; // Config file detection -export { isConfigFile, isProxyFile } from './config-detection'; +export { isConfigFile, isProxyFile, extractVersionFromSchemaUrl, findOutdatedConfigFiles } from './config-detection'; // Shell execution utilities export { From 8cdab4be1fe51d6ae68e3cb282631a84844670f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:32:50 +0000 Subject: [PATCH 3/5] refactor: address code review feedback - Use async vscode.workspace.fs.readFile instead of sync fs.readFileSync - Add type guard for parse result before casting to ObjectNode - Extract getNormalizedVersion() as shared utility to reduce duplication - Add regex pattern documentation and named constant - Check for workbench.action.chat.open command availability before invoking Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- src/commands/upgrade-config.ts | 14 +++++++++++--- src/detect.ts | 10 ++++++++++ src/notifications.ts | 6 ++---- src/utils/config-detection.ts | 16 ++++++++++++---- src/utils/index.ts | 2 +- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/commands/upgrade-config.ts b/src/commands/upgrade-config.ts index e5fd495..f783e49 100644 --- a/src/commands/upgrade-config.ts +++ b/src/commands/upgrade-config.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { Commands } from '../constants'; import { DevProxyInstall } from '../types'; +import { getNormalizedVersion } from '../utils'; /** * Config upgrade commands. @@ -25,9 +26,7 @@ async function upgradeConfigsWithCopilot( return; } - const devProxyVersion = devProxyInstall.isBeta - ? devProxyInstall.version.split('-')[0] - : devProxyInstall.version; + const devProxyVersion = getNormalizedVersion(devProxyInstall); const fileList = fileUris?.length ? fileUris.map(uri => `- ${vscode.workspace.asRelativePath(uri)}`).join('\n') @@ -43,6 +42,15 @@ async function upgradeConfigsWithCopilot( ].join('\n'); try { + // workbench.action.chat.open requires GitHub Copilot Chat extension + const allCommands = await vscode.commands.getCommands(); + if (!allCommands.includes('workbench.action.chat.open')) { + vscode.window.showWarningMessage( + 'GitHub Copilot Chat is not available. Please install the GitHub Copilot extension to use this feature.' + ); + return; + } + await vscode.commands.executeCommand('workbench.action.chat.open', { query: prompt, isPartialQuery: false, diff --git a/src/detect.ts b/src/detect.ts index 05583dd..c47e8a5 100644 --- a/src/detect.ts +++ b/src/detect.ts @@ -103,3 +103,13 @@ export const getDevProxyExe = (versionPreference: VersionPreference) => { ? VersionExeName.Stable : VersionExeName.Beta; }; + +/** + * Get the normalized Dev Proxy version from an install object. + * Strips the beta pre-release suffix for version comparison. + */ +export const getNormalizedVersion = (devProxyInstall: DevProxyInstall): string => { + return devProxyInstall.isBeta + ? devProxyInstall.version.split('-')[0] + : devProxyInstall.version; +}; diff --git a/src/notifications.ts b/src/notifications.ts index de3cab0..364055c 100644 --- a/src/notifications.ts +++ b/src/notifications.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { DevProxyInstall } from './types'; import { Commands } from './constants'; -import { findOutdatedConfigFiles } from './utils'; +import { findOutdatedConfigFiles, getNormalizedVersion } from './utils'; export const handleStartNotification = (context: vscode.ExtensionContext) => { const devProxyInstall = context.globalState.get('devProxyInstall'); @@ -68,9 +68,7 @@ export async function handleOutdatedConfigFilesNotification( return; } - const devProxyVersion = devProxyInstall.isBeta - ? devProxyInstall.version.split('-')[0] - : devProxyInstall.version; + const devProxyVersion = getNormalizedVersion(devProxyInstall); const outdatedFiles = await findOutdatedConfigFiles(devProxyVersion); diff --git a/src/utils/config-detection.ts b/src/utils/config-detection.ts index 2edc3fd..7fa853c 100644 --- a/src/utils/config-detection.ts +++ b/src/utils/config-detection.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; import parse from 'json-to-ast'; /** @@ -98,7 +97,9 @@ export function isProxyFile(document: vscode.TextDocument): boolean { * @returns The version string (e.g., "0.24.0") or empty string if not found. */ export function extractVersionFromSchemaUrl(schemaUrl: string): string { - const match = schemaUrl.match(/\/v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)\//); + // Matches /vX.Y.Z/ or /vX.Y.Z-prerelease/ in schema URLs + const versionPattern = /\/v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)\//; + const match = schemaUrl.match(versionPattern); return match ? match[1] : ''; } @@ -118,14 +119,21 @@ export async function findOutdatedConfigFiles(devProxyVersion: string): Promise< for (const uri of jsonFiles) { try { - const content = fs.readFileSync(uri.fsPath, 'utf-8'); + const contentBytes = await vscode.workspace.fs.readFile(uri); + const content = Buffer.from(contentBytes).toString('utf-8'); // Quick check before parsing if (!content.includes('dev-proxy') || !content.includes('schema')) { continue; } - const documentNode = parse(content) as parse.ObjectNode; + const rootNode = parse(content); + + if (rootNode.type !== 'Object') { + continue; + } + + const documentNode = rootNode as parse.ObjectNode; const schemaNode = getASTNode(documentNode.children, 'Identifier', '$schema'); if (!schemaNode) { diff --git a/src/utils/index.ts b/src/utils/index.ts index c6bd45e..afc2b0b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -30,7 +30,7 @@ export { } from './shell'; // Re-export from detect for convenience -export { getDevProxyExe } from '../detect'; +export { getDevProxyExe, getNormalizedVersion } from '../detect'; // Workspace recommendations utilities export { From c447bb56ccd571540a48b42f058acf9606c63833 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:15:26 +0000 Subject: [PATCH 4/5] docs: add changelog and readme entries for config upgrade feature - CHANGELOG: Add notification and upgrade-configs command entries under [1.13.0] - README: Add Upgrade config files command to Commands table - README: Add Notifications section with outdated config files description Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- CHANGELOG.md | 2 ++ README.md | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f3930..13b8ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added: - 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 - Quick Fixes: Enable local language model fix now adds or updates `languageModel.enabled: true` for supported plugins only - Workspace: Added automatic prompt to recommend extension in `.vscode/extensions.json` when Dev Proxy config files are detected - Command: Added `Add to Workspace Recommendations` to manually add extension to workspace recommendations diff --git a/README.md b/README.md index 58d2f09..3bcab6a 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Control Dev Proxy directly from VS Code via the Command Palette (`Cmd+Shift+P` / | Generate JWT | Dev Proxy installed | | Add to Workspace Recommendations | Always | | Reset State | Always | +| Upgrade config files | Dev Proxy installed | ### Snippets @@ -217,6 +218,10 @@ The prompt offers three options: You can also manually add the extension to recommendations at any time using the `Add to Workspace Recommendations` command, or use `Reset State` to clear all extension state including prompt preferences. +### Notifications + +- **Outdated config files** - On activation, scans workspace for Dev Proxy config files with a schema version that doesn't match the installed Dev Proxy version. Offers a one-click upgrade using Copilot Chat and Dev Proxy MCP tools. + ## Configuration | Setting | Type | Default | Description | From 752d9ad0d0cf16d94275b07cdba893a56d32a781 Mon Sep 17 00:00:00 2001 From: Garry Trinder Date: Mon, 2 Mar 2026 16:59:47 +0000 Subject: [PATCH 5/5] fix: broaden outdated config detection to all Dev Proxy schema files --- src/utils/config-detection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/config-detection.ts b/src/utils/config-detection.ts index 7fa853c..105ee9e 100644 --- a/src/utils/config-detection.ts +++ b/src/utils/config-detection.ts @@ -142,7 +142,7 @@ export async function findOutdatedConfigFiles(devProxyVersion: string): Promise< const schemaValue = (schemaNode.value as parse.LiteralNode).value as string; - if (!schemaValue.includes('dev-proxy') || !schemaValue.endsWith('rc.schema.json')) { + if (!schemaValue.includes('dev-proxy') || !schemaValue.endsWith('.schema.json')) { continue; }