diff --git a/CHANGELOG.md b/CHANGELOG.md index ec0e8b9..73b0bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added: - 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 +- Command: Added `Reset State` to clear all extension state ### Fixed: diff --git a/README.md b/README.md index eeea544..e6bbfbb 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Control Dev Proxy directly from VS Code via the Command Palette (`Cmd+Shift+P` / | Create configuration file | Dev Proxy installed | | Discover URLs to watch | Dev Proxy not running | | Generate JWT | Dev Proxy installed | +| Add to Workspace Recommendations | Always | +| Reset State | Always | ### Snippets @@ -201,6 +203,18 @@ Shows Dev Proxy status at a glance: - Running state (radio tower icon when active) - Error indicator if Dev Proxy is not installed +### Workspace Recommendations + +When you open a workspace containing `devproxyrc.json` or `devproxyrc.jsonc` files, the extension will prompt you to add it to your workspace's recommended extensions (`.vscode/extensions.json`). This helps teams ensure all contributors have the Dev Proxy Toolkit installed for a consistent development experience. + +The prompt offers three options: + +- **Yes** — adds the extension to workspace recommendations +- **No** — dismisses the prompt, it will appear again next session +- **Don't ask again** — permanently suppresses the prompt for this workspace + +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. + ## Configuration | Setting | Type | Default | Description | diff --git a/package.json b/package.json index 5cdef69..d17d028 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,16 @@ "title": "Generate JWT", "category": "Dev Proxy Toolkit", "enablement": "isDevProxyInstalled" + }, + { + "command": "dev-proxy-toolkit.add-to-recommendations", + "title": "Add to Workspace Recommendations", + "category": "Dev Proxy Toolkit" + }, + { + "command": "dev-proxy-toolkit.reset-state", + "title": "Reset State", + "category": "Dev Proxy Toolkit" } ], "mcpServerDefinitionProviders": [ diff --git a/src/commands/index.ts b/src/commands/index.ts index c7fd3c3..35d8e9e 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -6,6 +6,8 @@ import { registerInstallCommands } from './install'; import { registerJwtCommands } from './jwt'; import { registerDiscoveryCommands } from './discovery'; import { registerDocCommands } from './docs'; +import { Commands } from '../constants'; +import { addExtensionToRecommendations } from '../utils'; /** * Register all commands for the extension. @@ -18,6 +20,7 @@ import { registerDocCommands } from './docs'; * - jwt: create JWT tokens * - discovery: discover URLs to watch * - docs: open plugin documentation, add language model config + * - workspace: add to recommendations */ export function registerCommands( context: vscode.ExtensionContext, @@ -30,6 +33,31 @@ export function registerCommands( registerJwtCommands(context, configuration); registerDiscoveryCommands(context, configuration); registerDocCommands(context); + + context.subscriptions.push( + vscode.commands.registerCommand(Commands.addToRecommendations, async () => { + const success = await addExtensionToRecommendations(); + if (success) { + vscode.window.showInformationMessage('Dev Proxy Toolkit added to workspace recommendations.'); + } else { + vscode.window.showErrorMessage('Failed to add extension to workspace recommendations. Ensure a workspace folder is open.'); + } + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand(Commands.resetState, async () => { + const keys = context.globalState.keys(); + for (const key of keys) { + await context.globalState.update(key, undefined); + } + vscode.window.showInformationMessage('Dev Proxy Toolkit state has been reset. Reload the window to apply changes.', 'Reload').then(action => { + if (action === 'Reload') { + vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + }); + }) + ); } // Re-export individual modules for testing and direct access diff --git a/src/constants.ts b/src/constants.ts index 079c39f..4ae635a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -39,6 +39,10 @@ export const Commands = { // Language model commands addLanguageModelConfig: 'dev-proxy-toolkit.addLanguageModelConfig', + + // Workspace commands + addToRecommendations: 'dev-proxy-toolkit.add-to-recommendations', + resetState: 'dev-proxy-toolkit.reset-state', } as const; /** @@ -92,3 +96,11 @@ export const Urls = { schemaBase: 'https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas', diagnosticsDoc: 'https://learn.microsoft.com/microsoft-cloud/dev/dev-proxy/technical-reference/toolkit-diagnostics', } as const; + +/** + * Extension-related constants. + */ +export const Extension = { + id: 'garrytrinder.dev-proxy-toolkit', + extensionsJsonPath: '.vscode/extensions.json', +} as const; diff --git a/src/extension.ts b/src/extension.ts index de6d4a5..e760d77 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,6 +9,7 @@ import { updateGlobalState } from './state'; import { VersionPreference } from './enums'; import { registerMcpServer } from './mcp'; import { registerTaskProvider } from './task-provider'; +import { promptForWorkspaceRecommendation } from './utils'; // Global variable to track the interval let statusBarInterval: NodeJS.Timeout | undefined; @@ -35,6 +36,9 @@ export const activate = async (context: vscode.ExtensionContext): Promise { + let tempWorkspaceFolder: vscode.WorkspaceFolder; + let tempDir: string; + + setup(async () => { + const context = await getExtensionContext(); + await context.globalState.update('devProxyInstall', testDevProxyInstall); + + // Create a temporary directory for test files + tempDir = path.join(process.cwd(), '.test-workspace-' + Date.now()); + try { + await vscode.workspace.fs.createDirectory(vscode.Uri.file(tempDir)); + } catch { + // Directory might already exist + } + + tempWorkspaceFolder = { + uri: vscode.Uri.file(tempDir), + name: 'test-workspace', + index: 0, + }; + }); + + teardown(async () => { + // Clean up test files + try { + await vscode.workspace.fs.delete(vscode.Uri.file(tempDir), { recursive: true }); + } catch { + // Ignore errors + } + }); + + test('hasDevProxyConfig should return false when no config files exist', async () => { + const result = await hasDevProxyConfig(); + // In the actual workspace, we don't expect config files unless they're in test/examples + // This is a best-effort test + assert.ok(result !== undefined); + }); + + test('isExtensionRecommended should return false when extensions.json does not exist', async () => { + // This test requires a workspace folder, but we can't easily mock it + // Just ensure the function runs without error + const result = await isExtensionRecommended(); + assert.ok(result === false || result === true); + }); + + test('addExtensionToRecommendations should create extensions.json if it does not exist', async () => { + // This test requires manipulating workspace folders, which is difficult in tests + // We'll just ensure the function is callable + const result = await addExtensionToRecommendations(); + assert.ok(result === false || result === true); + }); + + test('Extension constant should have correct ID', () => { + assert.strictEqual(Extension.id, 'garrytrinder.dev-proxy-toolkit'); + }); + + test('Extension constant should have correct extensions.json path', () => { + assert.strictEqual(Extension.extensionsJsonPath, '.vscode/extensions.json'); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index 3affa5d..01d3f29 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -30,3 +30,11 @@ export { // Re-export from detect for convenience export { getDevProxyExe } from '../detect'; + +// Workspace recommendations utilities +export { + hasDevProxyConfig, + isExtensionRecommended, + addExtensionToRecommendations, + promptForWorkspaceRecommendation, +} from './workspace-recommendations'; diff --git a/src/utils/workspace-recommendations.ts b/src/utils/workspace-recommendations.ts new file mode 100644 index 0000000..152f4f8 --- /dev/null +++ b/src/utils/workspace-recommendations.ts @@ -0,0 +1,141 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { Extension } from '../constants'; + +/** + * Utilities for managing workspace extension recommendations. + */ + +/** + * Check if workspace contains Dev Proxy config files. + */ +export async function hasDevProxyConfig(): Promise { + const files = await vscode.workspace.findFiles( + '{devproxyrc.json,devproxyrc.jsonc}' + ); + return files.length > 0; +} + +/** + * Check if the Dev Proxy Toolkit extension is already in workspace recommendations. + */ +export async function isExtensionRecommended(): Promise { + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + return false; + } + + const workspaceFolder = vscode.workspace.workspaceFolders[0]; + const extensionsJsonPath = path.join(workspaceFolder.uri.fsPath, Extension.extensionsJsonPath); + + try { + const uri = vscode.Uri.file(extensionsJsonPath); + const document = await vscode.workspace.openTextDocument(uri); + const content = document.getText(); + const json = JSON.parse(content); + + if (json.recommendations && Array.isArray(json.recommendations)) { + return json.recommendations.includes(Extension.id); + } + } catch (error) { + // File doesn't exist or can't be parsed + return false; + } + + return false; +} + +/** + * Add the Dev Proxy Toolkit extension to workspace recommendations. + */ +export async function addExtensionToRecommendations(): Promise { + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + return false; + } + + const workspaceFolder = vscode.workspace.workspaceFolders[0]; + const vscodeFolderPath = path.join(workspaceFolder.uri.fsPath, '.vscode'); + const extensionsJsonPath = path.join(workspaceFolder.uri.fsPath, Extension.extensionsJsonPath); + + try { + let json: { recommendations?: string[] } = {}; + + // Try to read existing file + try { + const uri = vscode.Uri.file(extensionsJsonPath); + const document = await vscode.workspace.openTextDocument(uri); + json = JSON.parse(document.getText()); + } catch { + // File doesn't exist or can't be parsed, create new structure + json = { recommendations: [] }; + } + + // Ensure recommendations array exists + if (!json.recommendations) { + json.recommendations = []; + } + + // Add extension if not already present + if (!json.recommendations.includes(Extension.id)) { + json.recommendations.push(Extension.id); + } + + // Create .vscode directory if it doesn't exist + try { + await vscode.workspace.fs.createDirectory(vscode.Uri.file(vscodeFolderPath)); + } catch { + // Directory might already exist + } + + // Write the updated file + const uri = vscode.Uri.file(extensionsJsonPath); + const content = JSON.stringify(json, null, 2); + await vscode.workspace.fs.writeFile(uri, Buffer.from(content, 'utf8')); + + return true; + } catch (error) { + console.error('Error adding extension to recommendations:', error); + return false; + } +} + +/** + * Prompt user to add the extension to workspace recommendations. + */ +export async function promptForWorkspaceRecommendation(context: vscode.ExtensionContext): Promise { + // Check if we've already prompted for this workspace + const workspaceKey = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; + // Use a safe storage key by replacing path separators with underscores + const storageKey = `recommendation-prompted-${workspaceKey.replace(/[/\\:]/g, '_')}`; + + if (context.globalState.get(storageKey)) { + // Already prompted for this workspace + return; + } + + // Check if workspace has Dev Proxy config + const hasConfig = await hasDevProxyConfig(); + if (!hasConfig) { + return; + } + + // Check if extension is already recommended + const isRecommended = await isExtensionRecommended(); + if (isRecommended) { + return; + } + + // Show prompt + const message = 'This workspace contains Dev Proxy configuration files. Would you like to add the Dev Proxy Toolkit extension to workspace recommendations?'; + const result = await vscode.window.showInformationMessage(message, 'Yes', 'No', 'Don\'t ask again'); + + if (result === 'Yes') { + const success = await addExtensionToRecommendations(); + if (success) { + vscode.window.showInformationMessage('Dev Proxy Toolkit added to workspace recommendations.'); + } else { + vscode.window.showErrorMessage('Failed to add extension to workspace recommendations.'); + } + } else if (result === 'Don\'t ask again') { + await context.globalState.update(storageKey, true); + } +}