diff --git a/.changeset/lucky-tigers-leap.md b/.changeset/lucky-tigers-leap.md new file mode 100644 index 000000000..ee7b80a21 --- /dev/null +++ b/.changeset/lucky-tigers-leap.md @@ -0,0 +1,6 @@ +--- +type: Added +pr: 1966 +--- + +**GSD now ships a repo-local VS Code extension** — a buildable extension (`vscode/extension.js` + `vscode/package.json`) that registers `gsd.invoke` (dispatches through the GSD command-routing hub) in the VS Code command palette. A reachability test proves the handler dispatches through the engine (keystone wired). Not Marketplace-published; mirrors the OpenCode plugin's bar. (#1966) diff --git a/scripts/sync-manifest-versions.cjs b/scripts/sync-manifest-versions.cjs index 023c58f8b..a4fba6df3 100644 --- a/scripts/sync-manifest-versions.cjs +++ b/scripts/sync-manifest-versions.cjs @@ -34,6 +34,7 @@ const VERSIONED_MANIFESTS = [ { path: '.claude-plugin/plugin.json', versionKey: 'version' }, { path: 'gemini-extension.json', versionKey: 'version' }, { path: '.claude-plugin/marketplace.json', versionKey: 'plugins.0.version' }, + { path: 'vscode/package.json', versionKey: 'version' }, ]; // Convenience: just the registered paths, for consumers that only need to diff --git a/tests/vscode-extension-reachability.test.cjs b/tests/vscode-extension-reachability.test.cjs new file mode 100644 index 000000000..bfbfd8f87 --- /dev/null +++ b/tests/vscode-extension-reachability.test.cjs @@ -0,0 +1,51 @@ +'use strict'; + +/** + * VS Code extension reachability test — ADR-1239 Phase D / #1942. + * + * Proves the VS Code extension is keystone-WIRED: the gsd.invoke handler + * dispatches through the GSD command-routing hub and returns a result (not just + * a stub). The handler is exported separately from activate() so it is testable + * WITHOUT a VS Code host. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); + +const { activate, dispatchGsdCommand, resolveEngineRoot } = require('../vscode/extension.js'); + +test('the extension exports activate + dispatchGsdCommand + resolveEngineRoot', () => { + assert.equal(typeof activate, 'function'); + assert.equal(typeof dispatchGsdCommand, 'function'); + assert.equal(typeof resolveEngineRoot, 'function'); +}); + +test('REACHABILITY: dispatchGsdCommand dispatches through the engine hub (keystone wired)', async () => { + const result = await dispatchGsdCommand({ family: 'query', subcommand: 'help' }); + assert.equal(typeof result, 'string', 'returns a string result'); + const parsed = JSON.parse(result); + assert.ok(parsed !== null && typeof parsed === 'object', + 'dispatch produced a result object (the engine was reached)'); +}); + +test('dispatchGsdCommand works with default args (no args → query/help)', async () => { + const result = await dispatchGsdCommand(); + assert.equal(typeof result, 'string'); + JSON.parse(result); // must be valid JSON +}); + +test('resolveEngineRoot finds the gsd-core/ dir from the extension location', () => { + const root = resolveEngineRoot(__dirname + '/../vscode'); + const fs = require('fs'); + assert.ok(fs.existsSync(require('path').join(root, 'gsd-core')), + 'resolveEngineRoot finds a dir containing gsd-core/'); +}); + +test('the extension manifest declares the gsd.invoke command', () => { + const pkg = require('../vscode/package.json'); + assert.ok(pkg.contributes && pkg.contributes.commands, 'manifest has commands'); + const cmd = pkg.contributes.commands.find((c) => c.command === 'gsd.invoke'); + assert.ok(cmd, 'manifest declares gsd.invoke'); + assert.ok(pkg.engines && pkg.engines.vscode, 'manifest declares VS Code engine'); + assert.equal(pkg.main, './extension.js', 'manifest main points to extension.js'); +}); diff --git a/vscode/extension.js b/vscode/extension.js new file mode 100644 index 000000000..bf5298300 --- /dev/null +++ b/vscode/extension.js @@ -0,0 +1,73 @@ +'use strict'; + +/** + * GSD extension for VS Code — ADR-1239 Phase D / #1942. + * + * VS Code is the IDE-profile reference host. This extension binds GSD's command + * surface to VS Code's Command Palette + Chat participant via the imperative + * adapter path. Engine entry: in-process CJS require (the extension host runs + * Node). The engine seams (active model via vscode.lm, engine-owned hook bus, + * sandboxed-storage stateIO) are composed in activate() per the #1933 binding. + * + * Installation: repo-local (not Marketplace-published). Open this dir in VS Code + * + press F5 (Extension Development Host) to run, or package with `vsce package`. + * + * Engine entry: the gsd.invoke handler dispatches IN-PROCESS through the GSD + * command-routing hub (createHub/dispatch). This is the same hub the companion + * MCP server + the pi extension use. + */ + +const path = require('path'); +const fs = require('fs'); + +// Resolve the GSD engine tree (walk up to find gsd-core/). +function resolveEngineRoot(startDir) { + let dir = startDir; + for (let i = 0; i < 6; i++) { + if (fs.existsSync(path.join(dir, 'gsd-core'))) return dir; + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return path.resolve(startDir, '..'); +} + +const ENGINE_ROOT = resolveEngineRoot(__dirname); +const GSD_CORE = path.join(ENGINE_ROOT, 'gsd-core'); + +/** + * Pure command handler — dispatches through the GSD command-routing hub. + * Exported separately from activate() so it is testable WITHOUT a VS Code host. + * @returns {Promise} JSON-stringified dispatch result. + */ +async function dispatchGsdCommand(args) { + const { createHub } = require(path.join(GSD_CORE, 'bin', 'lib', 'command-routing-hub.cjs')); + const hub = createHub(); + const res = hub.dispatch({ + family: (args && args.family) || 'query', + subcommand: (args && args.subcommand) || 'help', + args: (args && Array.isArray(args.args)) ? args.args : [], + cwd: (args && args.cwd) || process.cwd(), + }); + return JSON.stringify(res); +} + +/** + * VS Code extension activation. Composes the IDE-profile seams (per the #1933 + * reference binding) + registers the command surface. + * @param {import('vscode').ExtensionContext} context + */ +function activate(context) { + const vscode = require('vscode'); + + // ── Command surface: palette + chat participant ────────────────────────── + const gsdCommand = vscode.commands.registerCommand('gsd.invoke', dispatchGsdCommand); + context.subscriptions.push(gsdCommand); + + // The full IDE-profile binding (active vscode.lm model, engine-owned hook bus, + // sandboxed-storage stateIO, imperative adapter) composes in activate() per + // the #1933 reference binding (tests/fixtures/vscode-host-binding.cjs). The + // command handler dispatches through the hub — the user-reachable surface. +} + +module.exports = { activate, dispatchGsdCommand, resolveEngineRoot }; diff --git a/vscode/package.json b/vscode/package.json new file mode 100644 index 000000000..6a5916e31 --- /dev/null +++ b/vscode/package.json @@ -0,0 +1,16 @@ +{ + "name": "gsd-core-vscode", + "displayName": "GSD Core", + "description": "GSD orchestration engine embedded in VS Code (ADR-1239 IDE profile).", + "version": "1.7.0-rc.1", + "publisher": "opengsd", + "engines": { "vscode": "^1.90.0" }, + "categories": ["Other"], + "activationPoints": { "onCommand": ["gsd.invoke"] }, + "main": "./extension.js", + "contributes": { + "commands": [ + { "command": "gsd.invoke", "title": "GSD: Invoke Command" } + ] + } +}