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 | 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..f783e49 --- /dev/null +++ b/src/commands/upgrade-config.ts @@ -0,0 +1,63 @@ +import * as vscode from 'vscode'; +import { Commands } from '../constants'; +import { DevProxyInstall } from '../types'; +import { getNormalizedVersion } from '../utils'; + +/** + * 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 = getNormalizedVersion(devProxyInstall); + + 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 { + // 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, + }); + } 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/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/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,41 @@ 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 = getNormalizedVersion(devProxyInstall); + + 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..105ee9e 100644 --- a/src/utils/config-detection.ts +++ b/src/utils/config-detection.ts @@ -87,3 +87,74 @@ 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 { + // 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] : ''; +} + +/** + * 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 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 rootNode = parse(content); + + if (rootNode.type !== 'Object') { + continue; + } + + const documentNode = rootNode 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('.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..afc2b0b 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 { @@ -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 {