Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 |
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
4 changes: 4 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -58,6 +59,8 @@ export function registerCommands(
});
})
);

registerUpgradeConfigCommands(context);
}

// Re-export individual modules for testing and direct access
Expand All @@ -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';
63 changes: 63 additions & 0 deletions src/commands/upgrade-config.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const devProxyInstall = context.globalState.get<DevProxyInstall>('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.'
);
}
}
5 changes: 4 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
10 changes: 10 additions & 0 deletions src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
6 changes: 4 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -36,9 +36,11 @@ export const activate = async (context: vscode.ExtensionContext): Promise<vscode
const notification = handleStartNotification(context);
processNotification(notification);

// Prompt for workspace recommendations
// Prompt for workspace recommendations
promptForWorkspaceRecommendation(context);

handleOutdatedConfigFilesNotification(context);

updateStatusBar(context, statusBar);

// Store the interval reference for proper cleanup
Expand Down
40 changes: 40 additions & 0 deletions src/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as vscode from 'vscode';
import { DevProxyInstall } from './types';
import { Commands } from './constants';
import { findOutdatedConfigFiles, getNormalizedVersion } from './utils';

export const handleStartNotification = (context: vscode.ExtensionContext) => {
const devProxyInstall = context.globalState.get<DevProxyInstall>('devProxyInstall');
Expand Down Expand Up @@ -50,3 +52,41 @@ export const handleStartNotification = (context: vscode.ExtensionContext) => {
export const processNotification = (notification: (() => { message: string; show: () => Promise<void>; }) | 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<void> {
const devProxyInstall = context.globalState.get<DevProxyInstall>('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);
}
}
29 changes: 28 additions & 1 deletion src/test/config-detection.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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(''), '');
});
});
6 changes: 6 additions & 0 deletions src/test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
71 changes: 71 additions & 0 deletions src/utils/config-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.Uri[]> {
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;
}
4 changes: 2 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down