Skip to content

Commit 7aa3779

Browse files
Copilotgarrytrinder
andcommitted
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>
1 parent 9c23906 commit 7aa3779

10 files changed

Lines changed: 210 additions & 3 deletions

File tree

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@
8989
"title": "Generate JWT",
9090
"category": "Dev Proxy Toolkit",
9191
"enablement": "isDevProxyInstalled"
92+
},
93+
{
94+
"command": "dev-proxy-toolkit.upgrade-configs",
95+
"title": "Upgrade config files",
96+
"category": "Dev Proxy Toolkit",
97+
"enablement": "isDevProxyInstalled"
9298
}
9399
],
94100
"mcpServerDefinitionProviders": [

src/commands/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { registerInstallCommands } from './install';
66
import { registerJwtCommands } from './jwt';
77
import { registerDiscoveryCommands } from './discovery';
88
import { registerDocCommands } from './docs';
9+
import { registerUpgradeConfigCommands } from './upgrade-config';
910

1011
/**
1112
* Register all commands for the extension.
@@ -30,6 +31,7 @@ export function registerCommands(
3031
registerJwtCommands(context, configuration);
3132
registerDiscoveryCommands(context, configuration);
3233
registerDocCommands(context);
34+
registerUpgradeConfigCommands(context);
3335
}
3436

3537
// Re-export individual modules for testing and direct access
@@ -40,3 +42,4 @@ export { registerInstallCommands } from './install';
4042
export { registerJwtCommands } from './jwt';
4143
export { registerDiscoveryCommands } from './discovery';
4244
export { registerDocCommands } from './docs';
45+
export { registerUpgradeConfigCommands } from './upgrade-config';

src/commands/upgrade-config.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as vscode from 'vscode';
2+
import { Commands } from '../constants';
3+
import { DevProxyInstall } from '../types';
4+
5+
/**
6+
* Config upgrade commands.
7+
*/
8+
9+
export function registerUpgradeConfigCommands(
10+
context: vscode.ExtensionContext,
11+
): void {
12+
context.subscriptions.push(
13+
vscode.commands.registerCommand(Commands.upgradeConfigs, (fileUris?: vscode.Uri[]) =>
14+
upgradeConfigsWithCopilot(context, fileUris)
15+
)
16+
);
17+
}
18+
19+
async function upgradeConfigsWithCopilot(
20+
context: vscode.ExtensionContext,
21+
fileUris?: vscode.Uri[],
22+
): Promise<void> {
23+
const devProxyInstall = context.globalState.get<DevProxyInstall>('devProxyInstall');
24+
if (!devProxyInstall?.isInstalled) {
25+
return;
26+
}
27+
28+
const devProxyVersion = devProxyInstall.isBeta
29+
? devProxyInstall.version.split('-')[0]
30+
: devProxyInstall.version;
31+
32+
const fileList = fileUris?.length
33+
? fileUris.map(uri => `- ${vscode.workspace.asRelativePath(uri)}`).join('\n')
34+
: 'all Dev Proxy config files in the workspace';
35+
36+
const prompt = [
37+
`Upgrade the following Dev Proxy configuration files to version v${devProxyVersion}:`,
38+
'',
39+
fileList,
40+
'',
41+
`Use the Dev Proxy MCP tools to get the latest schema information for v${devProxyVersion} and update each config file.`,
42+
'Update the $schema URLs and make any necessary configuration changes for the new version.',
43+
].join('\n');
44+
45+
try {
46+
await vscode.commands.executeCommand('workbench.action.chat.open', {
47+
query: prompt,
48+
isPartialQuery: false,
49+
});
50+
} catch {
51+
vscode.window.showWarningMessage(
52+
'Could not open Copilot Chat. Please make sure GitHub Copilot is installed and enabled.'
53+
);
54+
}
55+
}

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export const Commands = {
3939

4040
// Language model commands
4141
addLanguageModelConfig: 'dev-proxy-toolkit.addLanguageModelConfig',
42+
43+
// Config upgrade commands
44+
upgradeConfigs: 'dev-proxy-toolkit.upgrade-configs',
4245
} as const;
4346

4447
/**

src/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { registerCommands } from './commands';
3-
import { handleStartNotification, processNotification } from './notifications';
3+
import { handleStartNotification, processNotification, handleOutdatedConfigFilesNotification } from './notifications';
44
import { registerDocumentListeners } from './documents';
55
import { registerCodeLens } from './code-lens';
66
import { createStatusBar, statusBarLoop, updateStatusBar } from './status-bar';
@@ -35,6 +35,8 @@ export const activate = async (context: vscode.ExtensionContext): Promise<vscode
3535
const notification = handleStartNotification(context);
3636
processNotification(notification);
3737

38+
handleOutdatedConfigFilesNotification(context);
39+
3840
updateStatusBar(context, statusBar);
3941

4042
// Store the interval reference for proper cleanup

src/notifications.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as vscode from 'vscode';
22
import { DevProxyInstall } from './types';
3+
import { Commands } from './constants';
4+
import { findOutdatedConfigFiles } from './utils';
35

46
export const handleStartNotification = (context: vscode.ExtensionContext) => {
57
const devProxyInstall = context.globalState.get<DevProxyInstall>('devProxyInstall');
@@ -50,3 +52,43 @@ export const handleStartNotification = (context: vscode.ExtensionContext) => {
5052
export const processNotification = (notification: (() => { message: string; show: () => Promise<void>; }) | undefined) => {
5153
if (notification) { notification().show(); };
5254
};
55+
56+
/**
57+
* Check for outdated config files and notify the user.
58+
*
59+
* Scans the workspace for Dev Proxy config files whose schema version
60+
* doesn't match the installed Dev Proxy version and offers to upgrade
61+
* them using Copilot Chat.
62+
*/
63+
export async function handleOutdatedConfigFilesNotification(
64+
context: vscode.ExtensionContext,
65+
): Promise<void> {
66+
const devProxyInstall = context.globalState.get<DevProxyInstall>('devProxyInstall');
67+
if (!devProxyInstall?.isInstalled) {
68+
return;
69+
}
70+
71+
const devProxyVersion = devProxyInstall.isBeta
72+
? devProxyInstall.version.split('-')[0]
73+
: devProxyInstall.version;
74+
75+
const outdatedFiles = await findOutdatedConfigFiles(devProxyVersion);
76+
77+
if (outdatedFiles.length === 0) {
78+
return;
79+
}
80+
81+
const fileCount = outdatedFiles.length;
82+
const fileWord = fileCount === 1 ? 'file' : 'files';
83+
const message = `${fileCount} Dev Proxy config ${fileWord} found with a schema version that doesn't match the installed version (v${devProxyVersion}).`;
84+
85+
const result = await vscode.window.showWarningMessage(
86+
message,
87+
'Upgrade with Copilot',
88+
'Dismiss',
89+
);
90+
91+
if (result === 'Upgrade with Copilot') {
92+
await vscode.commands.executeCommand(Commands.upgradeConfigs, outdatedFiles);
93+
}
94+
}

src/test/config-detection.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/**
22
* Config file detection tests.
33
* Verifies isConfigFile correctly identifies Dev Proxy configuration files.
4+
* Verifies extractVersionFromSchemaUrl correctly extracts versions from schema URLs.
45
*/
56
import * as assert from 'assert';
67
import * as vscode from 'vscode';
7-
import { isConfigFile, sleep } from '../utils';
8+
import { isConfigFile, extractVersionFromSchemaUrl, sleep } from '../utils';
89
import { getFixturePath, testDevProxyInstall, getExtensionContext } from './helpers';
910

1011
suite('isConfigFile', () => {
@@ -83,3 +84,29 @@ suite('isConfigFile', () => {
8384
assert.strictEqual(actual, expected);
8485
});
8586
});
87+
88+
suite('extractVersionFromSchemaUrl', () => {
89+
test('should extract version from standard schema URL', () => {
90+
const url = 'https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.24.0/rc.schema.json';
91+
assert.strictEqual(extractVersionFromSchemaUrl(url), '0.24.0');
92+
});
93+
94+
test('should extract version from legacy schema URL', () => {
95+
const url = 'https://raw.githubusercontent.com/microsoft/dev-proxy/main/schemas/v0.14.1/rc.schema.json';
96+
assert.strictEqual(extractVersionFromSchemaUrl(url), '0.14.1');
97+
});
98+
99+
test('should extract pre-release version from schema URL', () => {
100+
const url = 'https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.24.0-beta.1/rc.schema.json';
101+
assert.strictEqual(extractVersionFromSchemaUrl(url), '0.24.0-beta.1');
102+
});
103+
104+
test('should return empty string for URL without version', () => {
105+
const url = 'https://example.com/schema.json';
106+
assert.strictEqual(extractVersionFromSchemaUrl(url), '');
107+
});
108+
109+
test('should return empty string for empty string', () => {
110+
assert.strictEqual(extractVersionFromSchemaUrl(''), '');
111+
});
112+
});

src/test/extension.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,10 @@ suite('Commands', () => {
2626
const jwtCreateCommand = commands.find(cmd => cmd === 'dev-proxy-toolkit.jwt-create');
2727
assert.ok(jwtCreateCommand, 'JWT create command should be registered');
2828
});
29+
30+
test('Upgrade configs command should be registered', async () => {
31+
const commands = await vscode.commands.getCommands();
32+
const upgradeConfigsCommand = commands.find(cmd => cmd === 'dev-proxy-toolkit.upgrade-configs');
33+
assert.ok(upgradeConfigsCommand, 'Upgrade configs command should be registered');
34+
});
2935
});

src/utils/config-detection.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as vscode from 'vscode';
2+
import * as fs from 'fs';
23
import parse from 'json-to-ast';
34

45
/**
@@ -87,3 +88,65 @@ export function isProxyFile(document: vscode.TextDocument): boolean {
8788
return false;
8889
}
8990
}
91+
92+
/**
93+
* Extract version from a Dev Proxy schema URL.
94+
*
95+
* Schema URLs follow the pattern:
96+
* https://raw.githubusercontent.com/.../schemas/v{version}/{filename}
97+
*
98+
* @returns The version string (e.g., "0.24.0") or empty string if not found.
99+
*/
100+
export function extractVersionFromSchemaUrl(schemaUrl: string): string {
101+
const match = schemaUrl.match(/\/v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)\//);
102+
return match ? match[1] : '';
103+
}
104+
105+
/**
106+
* Find all Dev Proxy config files in the workspace that have an outdated schema version.
107+
*
108+
* Scans the workspace for JSON files containing a Dev Proxy `$schema` property
109+
* and compares the schema version against the installed Dev Proxy version.
110+
*
111+
* @param devProxyVersion The installed Dev Proxy version (e.g., "0.24.0")
112+
* @returns Array of URIs for config files with mismatched schema versions.
113+
*/
114+
export async function findOutdatedConfigFiles(devProxyVersion: string): Promise<vscode.Uri[]> {
115+
const outdatedFiles: vscode.Uri[] = [];
116+
117+
const jsonFiles = await vscode.workspace.findFiles('**/*.{json,jsonc}', '**/node_modules/**');
118+
119+
for (const uri of jsonFiles) {
120+
try {
121+
const content = fs.readFileSync(uri.fsPath, 'utf-8');
122+
123+
// Quick check before parsing
124+
if (!content.includes('dev-proxy') || !content.includes('schema')) {
125+
continue;
126+
}
127+
128+
const documentNode = parse(content) as parse.ObjectNode;
129+
const schemaNode = getASTNode(documentNode.children, 'Identifier', '$schema');
130+
131+
if (!schemaNode) {
132+
continue;
133+
}
134+
135+
const schemaValue = (schemaNode.value as parse.LiteralNode).value as string;
136+
137+
if (!schemaValue.includes('dev-proxy') || !schemaValue.endsWith('rc.schema.json')) {
138+
continue;
139+
}
140+
141+
const schemaVersion = extractVersionFromSchemaUrl(schemaValue);
142+
143+
if (schemaVersion && schemaVersion !== devProxyVersion) {
144+
outdatedFiles.push(uri);
145+
}
146+
} catch {
147+
// Skip files that can't be read or parsed
148+
}
149+
}
150+
151+
return outdatedFiles;
152+
}

src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export {
1616
} from './ast';
1717

1818
// Config file detection
19-
export { isConfigFile, isProxyFile } from './config-detection';
19+
export { isConfigFile, isProxyFile, extractVersionFromSchemaUrl, findOutdatedConfigFiles } from './config-detection';
2020

2121
// Shell execution utilities
2222
export {

0 commit comments

Comments
 (0)