gsd-pi 2.67.0-dev.4fb8afe → 2.67.0-dev.5399650
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto.js +27 -0
- package/dist/resources/extensions/gsd/commands/handlers/core.js +38 -24
- package/dist/resources/extensions/gsd/commands/index.js +8 -1
- package/dist/resources/extensions/gsd/init-wizard.js +34 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +18 -3
- package/dist/resources/extensions/gsd/workflow-mcp.js +38 -7
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +4 -2
- package/packages/mcp-server/dist/cli.d.ts +9 -0
- package/packages/mcp-server/dist/cli.d.ts.map +1 -0
- package/packages/mcp-server/dist/cli.js +58 -0
- package/packages/mcp-server/dist/cli.js.map +1 -0
- package/packages/mcp-server/dist/index.d.ts +20 -0
- package/packages/mcp-server/dist/index.d.ts.map +1 -0
- package/packages/mcp-server/dist/index.js +14 -0
- package/packages/mcp-server/dist/index.js.map +1 -0
- package/packages/mcp-server/dist/readers/captures.d.ts +25 -0
- package/packages/mcp-server/dist/readers/captures.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/captures.js +67 -0
- package/packages/mcp-server/dist/readers/captures.js.map +1 -0
- package/packages/mcp-server/dist/readers/doctor-lite.d.ts +20 -0
- package/packages/mcp-server/dist/readers/doctor-lite.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/doctor-lite.js +173 -0
- package/packages/mcp-server/dist/readers/doctor-lite.js.map +1 -0
- package/packages/mcp-server/dist/readers/index.d.ts +14 -0
- package/packages/mcp-server/dist/readers/index.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/index.js +10 -0
- package/packages/mcp-server/dist/readers/index.js.map +1 -0
- package/packages/mcp-server/dist/readers/knowledge.d.ts +18 -0
- package/packages/mcp-server/dist/readers/knowledge.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/knowledge.js +82 -0
- package/packages/mcp-server/dist/readers/knowledge.js.map +1 -0
- package/packages/mcp-server/dist/readers/metrics.d.ts +32 -0
- package/packages/mcp-server/dist/readers/metrics.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/metrics.js +74 -0
- package/packages/mcp-server/dist/readers/metrics.js.map +1 -0
- package/packages/mcp-server/dist/readers/paths.d.ts +42 -0
- package/packages/mcp-server/dist/readers/paths.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/paths.js +199 -0
- package/packages/mcp-server/dist/readers/paths.js.map +1 -0
- package/packages/mcp-server/dist/readers/roadmap.d.ts +26 -0
- package/packages/mcp-server/dist/readers/roadmap.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/roadmap.js +194 -0
- package/packages/mcp-server/dist/readers/roadmap.js.map +1 -0
- package/packages/mcp-server/dist/readers/state.d.ts +43 -0
- package/packages/mcp-server/dist/readers/state.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/state.js +184 -0
- package/packages/mcp-server/dist/readers/state.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -0
- package/packages/mcp-server/dist/server.js +319 -0
- package/packages/mcp-server/dist/server.js.map +1 -0
- package/packages/mcp-server/dist/session-manager.d.ts +54 -0
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -0
- package/packages/mcp-server/dist/session-manager.js +284 -0
- package/packages/mcp-server/dist/session-manager.js.map +1 -0
- package/packages/mcp-server/dist/types.d.ts +61 -0
- package/packages/mcp-server/dist/types.d.ts.map +1 -0
- package/packages/mcp-server/dist/types.js +11 -0
- package/packages/mcp-server/dist/types.js.map +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts +9 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -0
- package/packages/mcp-server/dist/workflow-tools.js +526 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -0
- package/packages/mcp-server/tsconfig.json +1 -1
- package/packages/rpc-client/dist/index.d.ts +10 -0
- package/packages/rpc-client/dist/index.d.ts.map +1 -0
- package/packages/rpc-client/dist/index.js +9 -0
- package/packages/rpc-client/dist/index.js.map +1 -0
- package/packages/rpc-client/dist/jsonl.d.ts +17 -0
- package/packages/rpc-client/dist/jsonl.d.ts.map +1 -0
- package/packages/rpc-client/dist/jsonl.js +54 -0
- package/packages/rpc-client/dist/jsonl.js.map +1 -0
- package/packages/rpc-client/dist/rpc-client.d.ts +259 -0
- package/packages/rpc-client/dist/rpc-client.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-client.js +541 -0
- package/packages/rpc-client/dist/rpc-client.js.map +1 -0
- package/packages/rpc-client/dist/rpc-client.test.d.ts +2 -0
- package/packages/rpc-client/dist/rpc-client.test.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-client.test.js +477 -0
- package/packages/rpc-client/dist/rpc-client.test.js.map +1 -0
- package/packages/rpc-client/dist/rpc-types.d.ts +566 -0
- package/packages/rpc-client/dist/rpc-types.d.ts.map +1 -0
- package/packages/rpc-client/dist/rpc-types.js +12 -0
- package/packages/rpc-client/dist/rpc-types.js.map +1 -0
- package/scripts/ensure-workspace-builds.cjs +2 -0
- package/scripts/link-workspace-packages.cjs +21 -14
- package/src/resources/extensions/gsd/auto/session.ts +6 -0
- package/src/resources/extensions/gsd/auto.ts +29 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +52 -25
- package/src/resources/extensions/gsd/commands/index.ts +7 -1
- package/src/resources/extensions/gsd/init-wizard.ts +34 -0
- package/src/resources/extensions/gsd/tests/auto-project-root-env.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +101 -0
- package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +121 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +39 -1
- package/src/resources/extensions/gsd/workflow-logger.ts +19 -3
- package/src/resources/extensions/gsd/workflow-mcp.ts +41 -7
- /package/dist/web/standalone/.next/static/{IBTC_HlEpTBAa4HXMoV_A → 6_QPFhgX0DQnDhhquheRc}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{IBTC_HlEpTBAa4HXMoV_A → 6_QPFhgX0DQnDhhquheRc}/_ssgManifest.js +0 -0
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* link-workspace-packages.cjs
|
|
4
4
|
*
|
|
5
|
-
* Creates node_modules/@gsd/* symlinks pointing
|
|
5
|
+
* Creates node_modules/@gsd/* and node_modules/@gsd-build/* symlinks pointing
|
|
6
|
+
* to shipped packages/* directories.
|
|
6
7
|
*
|
|
7
8
|
* During development, npm workspaces creates these automatically. But in the
|
|
8
9
|
* published tarball, workspace packages are shipped under packages/ (via the
|
|
@@ -20,27 +21,33 @@ const { resolve, join } = require('path')
|
|
|
20
21
|
|
|
21
22
|
const root = resolve(__dirname, '..')
|
|
22
23
|
const packagesDir = join(root, 'packages')
|
|
23
|
-
const
|
|
24
|
+
const scopeDirs = {
|
|
25
|
+
'@gsd': join(root, 'node_modules', '@gsd'),
|
|
26
|
+
'@gsd-build': join(root, 'node_modules', '@gsd-build'),
|
|
27
|
+
}
|
|
24
28
|
|
|
25
|
-
// Map directory names to package names
|
|
29
|
+
// Map directory names to scoped package names
|
|
26
30
|
const packageMap = {
|
|
27
|
-
'native': 'native',
|
|
28
|
-
'pi-agent-core': 'pi-agent-core',
|
|
29
|
-
'pi-ai': 'pi-ai',
|
|
30
|
-
'pi-coding-agent': 'pi-coding-agent',
|
|
31
|
-
'pi-tui': 'pi-tui',
|
|
31
|
+
'native': { scope: '@gsd', name: 'native' },
|
|
32
|
+
'pi-agent-core': { scope: '@gsd', name: 'pi-agent-core' },
|
|
33
|
+
'pi-ai': { scope: '@gsd', name: 'pi-ai' },
|
|
34
|
+
'pi-coding-agent': { scope: '@gsd', name: 'pi-coding-agent' },
|
|
35
|
+
'pi-tui': { scope: '@gsd', name: 'pi-tui' },
|
|
36
|
+
'rpc-client': { scope: '@gsd-build', name: 'rpc-client' },
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
if (!existsSync(
|
|
36
|
-
|
|
39
|
+
for (const scopeDir of Object.values(scopeDirs)) {
|
|
40
|
+
if (!existsSync(scopeDir)) {
|
|
41
|
+
mkdirSync(scopeDir, { recursive: true })
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
let linked = 0
|
|
40
46
|
let copied = 0
|
|
41
|
-
for (const [dir,
|
|
47
|
+
for (const [dir, pkg] of Object.entries(packageMap)) {
|
|
42
48
|
const source = join(packagesDir, dir)
|
|
43
|
-
const
|
|
49
|
+
const scopeDir = scopeDirs[pkg.scope]
|
|
50
|
+
const target = join(scopeDir, pkg.name)
|
|
44
51
|
|
|
45
52
|
if (!existsSync(source)) continue
|
|
46
53
|
|
|
@@ -50,7 +57,7 @@ for (const [dir, name] of Object.entries(packageMap)) {
|
|
|
50
57
|
const stat = lstatSync(target)
|
|
51
58
|
if (stat.isSymbolicLink()) {
|
|
52
59
|
const linkTarget = readlinkSync(target)
|
|
53
|
-
if (resolve(join(
|
|
60
|
+
if (resolve(join(scopeDir, linkTarget)) === source || linkTarget === source) {
|
|
54
61
|
continue // Already correct
|
|
55
62
|
}
|
|
56
63
|
unlinkSync(target) // Wrong target, relink
|
|
@@ -84,6 +84,9 @@ export class AutoSession {
|
|
|
84
84
|
// ── Paths ────────────────────────────────────────────────────────────────
|
|
85
85
|
basePath = "";
|
|
86
86
|
originalBasePath = "";
|
|
87
|
+
previousProjectRootEnv: string | null = null;
|
|
88
|
+
hadProjectRootEnv = false;
|
|
89
|
+
projectRootEnvCaptured = false;
|
|
87
90
|
gitService: GitServiceImpl | null = null;
|
|
88
91
|
|
|
89
92
|
// ── Dispatch counters ────────────────────────────────────────────────────
|
|
@@ -192,6 +195,9 @@ export class AutoSession {
|
|
|
192
195
|
// Paths
|
|
193
196
|
this.basePath = "";
|
|
194
197
|
this.originalBasePath = "";
|
|
198
|
+
this.previousProjectRootEnv = null;
|
|
199
|
+
this.hadProjectRootEnv = false;
|
|
200
|
+
this.projectRootEnvCaptured = false;
|
|
195
201
|
this.gitService = null;
|
|
196
202
|
|
|
197
203
|
// Dispatch
|
|
@@ -241,6 +241,29 @@ const s = new AutoSession();
|
|
|
241
241
|
/** Throttle STATE.md rebuilds — at most once per 30 seconds */
|
|
242
242
|
const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
243
243
|
|
|
244
|
+
function captureProjectRootEnv(projectRoot: string): void {
|
|
245
|
+
if (!s.projectRootEnvCaptured) {
|
|
246
|
+
s.hadProjectRootEnv = Object.prototype.hasOwnProperty.call(process.env, "GSD_PROJECT_ROOT");
|
|
247
|
+
s.previousProjectRootEnv = process.env.GSD_PROJECT_ROOT ?? null;
|
|
248
|
+
s.projectRootEnvCaptured = true;
|
|
249
|
+
}
|
|
250
|
+
process.env.GSD_PROJECT_ROOT = projectRoot;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function restoreProjectRootEnv(): void {
|
|
254
|
+
if (!s.projectRootEnvCaptured) return;
|
|
255
|
+
|
|
256
|
+
if (s.hadProjectRootEnv && s.previousProjectRootEnv !== null) {
|
|
257
|
+
process.env.GSD_PROJECT_ROOT = s.previousProjectRootEnv;
|
|
258
|
+
} else {
|
|
259
|
+
delete process.env.GSD_PROJECT_ROOT;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
s.previousProjectRootEnv = null;
|
|
263
|
+
s.hadProjectRootEnv = false;
|
|
264
|
+
s.projectRootEnvCaptured = false;
|
|
265
|
+
}
|
|
266
|
+
|
|
244
267
|
export function shouldUseWorktreeIsolation(): boolean {
|
|
245
268
|
const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
246
269
|
if (prefs?.isolation === "worktree") return true;
|
|
@@ -542,6 +565,7 @@ function handleLostSessionLock(
|
|
|
542
565
|
s.active = false;
|
|
543
566
|
s.paused = false;
|
|
544
567
|
clearUnitTimeout();
|
|
568
|
+
restoreProjectRootEnv();
|
|
545
569
|
deregisterSigtermHandler();
|
|
546
570
|
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
547
571
|
const base = lockBase();
|
|
@@ -577,6 +601,7 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
|
|
|
577
601
|
s.currentUnit = null;
|
|
578
602
|
s.active = false;
|
|
579
603
|
clearUnitTimeout();
|
|
604
|
+
restoreProjectRootEnv();
|
|
580
605
|
|
|
581
606
|
// Clear crash lock and release session lock so the next `/gsd next` does
|
|
582
607
|
// not see a stale lock with the current PID and treat it as a "remote"
|
|
@@ -846,6 +871,7 @@ export async function stopAuto(
|
|
|
846
871
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
847
872
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
848
873
|
ctx?.ui.setFooter(undefined);
|
|
874
|
+
restoreProjectRootEnv();
|
|
849
875
|
|
|
850
876
|
// Reset all session state in one call
|
|
851
877
|
s.reset();
|
|
@@ -934,6 +960,7 @@ export async function pauseAuto(
|
|
|
934
960
|
|
|
935
961
|
s.active = false;
|
|
936
962
|
s.paused = true;
|
|
963
|
+
restoreProjectRootEnv();
|
|
937
964
|
s.pendingVerificationRetry = null;
|
|
938
965
|
s.verificationRetryCount.clear();
|
|
939
966
|
ctx?.ui.setStatus("gsd-auto", "paused");
|
|
@@ -1305,6 +1332,7 @@ export async function startAuto(
|
|
|
1305
1332
|
);
|
|
1306
1333
|
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
|
|
1307
1334
|
|
|
1335
|
+
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
|
1308
1336
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
1309
1337
|
cleanupAfterLoopExit(ctx);
|
|
1310
1338
|
return;
|
|
@@ -1329,6 +1357,7 @@ export async function startAuto(
|
|
|
1329
1357
|
);
|
|
1330
1358
|
if (!ready) return;
|
|
1331
1359
|
|
|
1360
|
+
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
|
1332
1361
|
try {
|
|
1333
1362
|
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
1334
1363
|
} catch (err) {
|
|
@@ -1569,4 +1598,3 @@ export {
|
|
|
1569
1598
|
buildLoopRemediationSteps,
|
|
1570
1599
|
} from "./auto-recovery.js";
|
|
1571
1600
|
export { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
|
|
1572
|
-
|
|
@@ -194,6 +194,56 @@ function sortModelsForSelection(models: Model<any>[], currentModel: Model<any> |
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
function buildProviderModelGroups(
|
|
198
|
+
models: Model<any>[],
|
|
199
|
+
currentModel: Model<any> | undefined,
|
|
200
|
+
): Map<string, Model<any>[]> {
|
|
201
|
+
const byProvider = new Map<string, Model<any>[]>();
|
|
202
|
+
|
|
203
|
+
for (const model of sortModelsForSelection(models, currentModel)) {
|
|
204
|
+
let group = byProvider.get(model.provider);
|
|
205
|
+
if (!group) {
|
|
206
|
+
group = [];
|
|
207
|
+
byProvider.set(model.provider, group);
|
|
208
|
+
}
|
|
209
|
+
group.push(model);
|
|
210
|
+
}
|
|
211
|
+
return byProvider;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function selectModelByProvider(
|
|
215
|
+
title: string,
|
|
216
|
+
models: Model<any>[],
|
|
217
|
+
ctx: ExtensionCommandContext,
|
|
218
|
+
currentModel: Model<any> | undefined,
|
|
219
|
+
): Promise<Model<any> | undefined> {
|
|
220
|
+
const byProvider = buildProviderModelGroups(models, currentModel);
|
|
221
|
+
const providerOptions = Array.from(byProvider.entries()).map(([provider, group]) =>
|
|
222
|
+
`${provider} (${group.length} model${group.length === 1 ? "" : "s"})`,
|
|
223
|
+
);
|
|
224
|
+
providerOptions.push("(cancel)");
|
|
225
|
+
|
|
226
|
+
const providerChoice = await ctx.ui.select(`${title} — choose provider:`, providerOptions);
|
|
227
|
+
if (!providerChoice || typeof providerChoice !== "string" || providerChoice === "(cancel)") return undefined;
|
|
228
|
+
|
|
229
|
+
const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
|
|
230
|
+
const providerModels = byProvider.get(providerName);
|
|
231
|
+
if (!providerModels || providerModels.length === 0) return undefined;
|
|
232
|
+
|
|
233
|
+
const optionToModel = new Map<string, Model<any>>();
|
|
234
|
+
const modelOptions = providerModels.map((model) => {
|
|
235
|
+
const isCurrent = currentModel && model.provider === currentModel.provider && model.id === currentModel.id;
|
|
236
|
+
const label = `${isCurrent ? "* " : ""}${model.id}`;
|
|
237
|
+
optionToModel.set(label, model);
|
|
238
|
+
return label;
|
|
239
|
+
});
|
|
240
|
+
modelOptions.push("(cancel)");
|
|
241
|
+
|
|
242
|
+
const modelChoice = await ctx.ui.select(`${title} — ${providerName}:`, modelOptions);
|
|
243
|
+
if (!modelChoice || typeof modelChoice !== "string" || modelChoice === "(cancel)") return undefined;
|
|
244
|
+
return optionToModel.get(modelChoice);
|
|
245
|
+
}
|
|
246
|
+
|
|
197
247
|
async function resolveRequestedModel(
|
|
198
248
|
query: string,
|
|
199
249
|
ctx: ExtensionCommandContext,
|
|
@@ -211,19 +261,7 @@ async function resolveRequestedModel(
|
|
|
211
261
|
|
|
212
262
|
if (partialMatches.length === 1) return partialMatches[0];
|
|
213
263
|
if (partialMatches.length === 0 || !ctx.hasUI) return undefined;
|
|
214
|
-
|
|
215
|
-
const sorted = sortModelsForSelection(partialMatches, ctx.model);
|
|
216
|
-
const optionToModel = new Map<string, Model<any>>();
|
|
217
|
-
const options = sorted.map((model) => {
|
|
218
|
-
const label = `${model.provider}/${model.id}`;
|
|
219
|
-
optionToModel.set(label, model);
|
|
220
|
-
return label;
|
|
221
|
-
});
|
|
222
|
-
options.push("(cancel)");
|
|
223
|
-
|
|
224
|
-
const choice = await ctx.ui.select(`Multiple models match "${query}" — choose one:`, options);
|
|
225
|
-
if (!choice || typeof choice !== "string" || choice === "(cancel)") return undefined;
|
|
226
|
-
return optionToModel.get(choice);
|
|
264
|
+
return selectModelByProvider(`Multiple models match "${query}"`, partialMatches, ctx, ctx.model);
|
|
227
265
|
}
|
|
228
266
|
|
|
229
267
|
async function handleModel(trimmedArgs: string, ctx: ExtensionCommandContext, pi: ExtensionAPI | undefined): Promise<void> {
|
|
@@ -247,18 +285,7 @@ async function handleModel(trimmedArgs: string, ctx: ExtensionCommandContext, pi
|
|
|
247
285
|
return;
|
|
248
286
|
}
|
|
249
287
|
|
|
250
|
-
|
|
251
|
-
const options = sortModelsForSelection(availableModels, ctx.model).map((model) => {
|
|
252
|
-
const isCurrent = ctx.model && model.provider === ctx.model.provider && model.id === ctx.model.id;
|
|
253
|
-
const label = `${isCurrent ? "* " : ""}${model.provider}/${model.id}`;
|
|
254
|
-
optionToModel.set(label, model);
|
|
255
|
-
return label;
|
|
256
|
-
});
|
|
257
|
-
options.push("(cancel)");
|
|
258
|
-
|
|
259
|
-
const choice = await ctx.ui.select("Select session model:", options);
|
|
260
|
-
if (!choice || typeof choice !== "string" || choice === "(cancel)") return;
|
|
261
|
-
targetModel = optionToModel.get(choice);
|
|
288
|
+
targetModel = await selectModelByProvider("Select session model:", availableModels, ctx, ctx.model);
|
|
262
289
|
} else {
|
|
263
290
|
targetModel = await resolveRequestedModel(trimmed, ctx);
|
|
264
291
|
}
|
|
@@ -8,7 +8,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
8
8
|
getArgumentCompletions: getGsdArgumentCompletions,
|
|
9
9
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
10
10
|
const { handleGSDCommand } = await import("./dispatcher.js");
|
|
11
|
-
await
|
|
11
|
+
const { setStderrLoggingEnabled } = await import("../workflow-logger.js");
|
|
12
|
+
const previousStderrSetting = setStderrLoggingEnabled(false);
|
|
13
|
+
try {
|
|
14
|
+
await handleGSDCommand(args, ctx, pi);
|
|
15
|
+
} finally {
|
|
16
|
+
setStderrLoggingEnabled(previousStderrSetting);
|
|
17
|
+
}
|
|
12
18
|
},
|
|
13
19
|
});
|
|
14
20
|
}
|
|
@@ -235,6 +235,20 @@ export async function showProjectInit(
|
|
|
235
235
|
// ── Step 9: Bootstrap .gsd/ ────────────────────────────────────────────────
|
|
236
236
|
bootstrapGsdDirectory(basePath, prefs, signals);
|
|
237
237
|
|
|
238
|
+
// Initialize SQLite database so GSD starts in full-capability mode (#3880).
|
|
239
|
+
// Without this, isDbAvailable() returns false and GSD enters degraded
|
|
240
|
+
// markdown-only mode until a tool handler happens to call ensureDbOpen().
|
|
241
|
+
let dbReady = false;
|
|
242
|
+
try {
|
|
243
|
+
const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
|
|
244
|
+
dbReady = await ensureDbOpen(basePath);
|
|
245
|
+
} catch {
|
|
246
|
+
// Swallowed — warning surfaced below
|
|
247
|
+
}
|
|
248
|
+
if (!dbReady) {
|
|
249
|
+
ctx.ui.notify("Warning: database initialization failed — GSD will run in degraded mode until the next /gsd invocation.", "warning");
|
|
250
|
+
}
|
|
251
|
+
|
|
238
252
|
// Ensure .gitignore
|
|
239
253
|
ensureGitignore(basePath);
|
|
240
254
|
untrackRuntimeFiles(basePath);
|
|
@@ -250,6 +264,25 @@ export async function showProjectInit(
|
|
|
250
264
|
// Non-fatal — codebase map generation failure should never block project init
|
|
251
265
|
}
|
|
252
266
|
|
|
267
|
+
// Write initial STATE.md so it exists before the first /gsd invocation.
|
|
268
|
+
// The explicit /gsd init path (ops.ts) returns without entering showSmartEntry(),
|
|
269
|
+
// which would otherwise generate STATE.md at guided-flow.ts:1358.
|
|
270
|
+
let stateReady = false;
|
|
271
|
+
try {
|
|
272
|
+
const { deriveState } = await import("./state.js");
|
|
273
|
+
const { buildStateMarkdown } = await import("./doctor.js");
|
|
274
|
+
const { saveFile } = await import("./files.js");
|
|
275
|
+
const { resolveGsdRootFile } = await import("./paths.js");
|
|
276
|
+
const state = await deriveState(basePath);
|
|
277
|
+
await saveFile(resolveGsdRootFile(basePath, "STATE"), buildStateMarkdown(state));
|
|
278
|
+
stateReady = true;
|
|
279
|
+
} catch {
|
|
280
|
+
// Swallowed — warning surfaced below
|
|
281
|
+
}
|
|
282
|
+
if (!stateReady) {
|
|
283
|
+
ctx.ui.notify("Warning: initial STATE.md generation failed — it will be created on the next /gsd invocation.", "warning");
|
|
284
|
+
}
|
|
285
|
+
|
|
253
286
|
ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
|
|
254
287
|
|
|
255
288
|
return { completed: true, bootstrapped: true };
|
|
@@ -433,6 +466,7 @@ function bootstrapGsdDirectory(
|
|
|
433
466
|
|
|
434
467
|
const gsd = gsdRoot(basePath);
|
|
435
468
|
mkdirSync(join(gsd, "milestones"), { recursive: true });
|
|
469
|
+
mkdirSync(join(gsd, "runtime"), { recursive: true });
|
|
436
470
|
|
|
437
471
|
// Write PREFERENCES.md from wizard answers
|
|
438
472
|
const preferencesContent = buildPreferencesFile(prefs);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const sourcePath = join(import.meta.dirname, "..", "auto.ts");
|
|
7
|
+
const source = readFileSync(sourcePath, "utf-8");
|
|
8
|
+
|
|
9
|
+
test("auto-mode captures GSD_PROJECT_ROOT before entering the dispatch loop", () => {
|
|
10
|
+
const captureDeclIdx = source.indexOf("function captureProjectRootEnv(projectRoot: string): void {");
|
|
11
|
+
assert.ok(captureDeclIdx > -1, "auto.ts should define captureProjectRootEnv()");
|
|
12
|
+
|
|
13
|
+
const resumeCallIdx = source.indexOf("captureProjectRootEnv(s.originalBasePath || s.basePath);");
|
|
14
|
+
assert.ok(resumeCallIdx > -1, "auto.ts should capture GSD_PROJECT_ROOT before resume autoLoop");
|
|
15
|
+
|
|
16
|
+
const firstAutoLoopIdx = source.indexOf("await autoLoop(ctx, pi, s, buildLoopDeps());");
|
|
17
|
+
assert.ok(firstAutoLoopIdx > -1, "auto.ts should invoke autoLoop()");
|
|
18
|
+
assert.ok(
|
|
19
|
+
resumeCallIdx < firstAutoLoopIdx,
|
|
20
|
+
"auto.ts must set GSD_PROJECT_ROOT before the first autoLoop() call",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("auto-mode restores GSD_PROJECT_ROOT when execution stops or pauses", () => {
|
|
25
|
+
assert.match(source, /function restoreProjectRootEnv\(\): void \{/);
|
|
26
|
+
assert.match(source, /cleanupAfterLoopExit\(ctx: ExtensionContext\): void \{[\s\S]*restoreProjectRootEnv\(\);/);
|
|
27
|
+
assert.match(source, /export async function pauseAuto\([\s\S]*restoreProjectRootEnv\(\);/);
|
|
28
|
+
assert.match(source, /\} finally \{[\s\S]*restoreProjectRootEnv\(\);[\s\S]*s\.reset\(\);/);
|
|
29
|
+
});
|
|
@@ -74,3 +74,104 @@ test("model command resolves and persists exact provider-qualified selection", a
|
|
|
74
74
|
assert.deepEqual(applied, selectedModel);
|
|
75
75
|
assert.match(notices[0]!.message, /openai\/gpt-5\.4/);
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
test("interactive model picker chooses provider first, then model", async () => {
|
|
79
|
+
const selectedModel = { provider: "openai", id: "gpt-5.4" };
|
|
80
|
+
let applied: typeof selectedModel | null = null;
|
|
81
|
+
const selects: Array<{ title: string; options: string[] }> = [];
|
|
82
|
+
const notices: Array<{ message: string; type?: string }> = [];
|
|
83
|
+
|
|
84
|
+
const ctx = {
|
|
85
|
+
hasUI: true,
|
|
86
|
+
model: { provider: "anthropic", id: "claude-sonnet-4-6" },
|
|
87
|
+
modelRegistry: {
|
|
88
|
+
getAvailable: () => [
|
|
89
|
+
{ provider: "openai", id: "gpt-5.4" },
|
|
90
|
+
{ provider: "anthropic", id: "claude-opus-4-6" },
|
|
91
|
+
{ provider: "openai", id: "gpt-5.3-mini" },
|
|
92
|
+
{ provider: "anthropic", id: "claude-sonnet-4-6" },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
ui: {
|
|
96
|
+
select: async (title: string, options: string[]) => {
|
|
97
|
+
selects.push({ title, options });
|
|
98
|
+
return selects.length === 1 ? "openai (2 models)" : "gpt-5.4";
|
|
99
|
+
},
|
|
100
|
+
notify: (message: string, type?: string) => {
|
|
101
|
+
notices.push({ message, type });
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
} as any;
|
|
105
|
+
|
|
106
|
+
const pi = {
|
|
107
|
+
setModel: async (model: typeof selectedModel) => {
|
|
108
|
+
applied = model;
|
|
109
|
+
return true;
|
|
110
|
+
},
|
|
111
|
+
} as any;
|
|
112
|
+
|
|
113
|
+
const handled = await handleCoreCommand("model", ctx, pi);
|
|
114
|
+
assert.equal(handled, true);
|
|
115
|
+
assert.deepEqual(selects, [
|
|
116
|
+
{
|
|
117
|
+
title: "Select session model: — choose provider:",
|
|
118
|
+
options: ["anthropic (2 models)", "openai (2 models)", "(cancel)"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
title: "Select session model: — openai:",
|
|
122
|
+
options: ["gpt-5.3-mini", "gpt-5.4", "(cancel)"],
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
assert.deepEqual(applied, selectedModel);
|
|
126
|
+
assert.match(notices[0]!.message, /openai\/gpt-5\.4/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("ambiguous typed model selection chooses provider first, then model", async () => {
|
|
130
|
+
const selectedModel = { provider: "github-copilot", id: "gpt-5" };
|
|
131
|
+
let applied: typeof selectedModel | null = null;
|
|
132
|
+
const selects: Array<{ title: string; options: string[] }> = [];
|
|
133
|
+
const notices: Array<{ message: string; type?: string }> = [];
|
|
134
|
+
|
|
135
|
+
const ctx = {
|
|
136
|
+
hasUI: true,
|
|
137
|
+
model: { provider: "anthropic", id: "claude-sonnet-4-6" },
|
|
138
|
+
modelRegistry: {
|
|
139
|
+
getAvailable: () => [
|
|
140
|
+
{ provider: "openai", id: "gpt-5" },
|
|
141
|
+
{ provider: "github-copilot", id: "gpt-5" },
|
|
142
|
+
{ provider: "openai", id: "gpt-5-mini" },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
ui: {
|
|
146
|
+
select: async (title: string, options: string[]) => {
|
|
147
|
+
selects.push({ title, options });
|
|
148
|
+
return selects.length === 1 ? "github-copilot (1 model)" : "gpt-5";
|
|
149
|
+
},
|
|
150
|
+
notify: (message: string, type?: string) => {
|
|
151
|
+
notices.push({ message, type });
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
} as any;
|
|
155
|
+
|
|
156
|
+
const pi = {
|
|
157
|
+
setModel: async (model: typeof selectedModel) => {
|
|
158
|
+
applied = model;
|
|
159
|
+
return true;
|
|
160
|
+
},
|
|
161
|
+
} as any;
|
|
162
|
+
|
|
163
|
+
const handled = await handleCoreCommand("model gpt", ctx, pi);
|
|
164
|
+
assert.equal(handled, true);
|
|
165
|
+
assert.deepEqual(selects, [
|
|
166
|
+
{
|
|
167
|
+
title: "Multiple models match \"gpt\" — choose provider:",
|
|
168
|
+
options: ["github-copilot (1 model)", "openai (2 models)", "(cancel)"],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
title: "Multiple models match \"gpt\" — github-copilot:",
|
|
172
|
+
options: ["gpt-5", "(cancel)"],
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
assert.deepEqual(applied, selectedModel);
|
|
176
|
+
assert.match(notices[0]!.message, /github-copilot\/gpt-5/);
|
|
177
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Init Wizard — Bootstrap completeness regression tests
|
|
3
|
+
*
|
|
4
|
+
* Regression test for #3880 — fresh install never creates gsd.db.
|
|
5
|
+
*
|
|
6
|
+
* The init wizard must create all artifacts needed for full-capability
|
|
7
|
+
* mode: gsd.db (via ensureDbOpen), runtime/ directory, and STATE.md
|
|
8
|
+
* (via deriveState + buildStateMarkdown). Without these, GSD enters
|
|
9
|
+
* degraded markdown-only mode on every fresh install.
|
|
10
|
+
*
|
|
11
|
+
* These are structural tests that verify the init-wizard.ts source
|
|
12
|
+
* contains the required calls in the correct order.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, test } from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
const wizardSrc = readFileSync(
|
|
25
|
+
join(__dirname, "..", "init-wizard.ts"),
|
|
26
|
+
"utf-8",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
describe("init-wizard bootstrap completeness (#3880)", () => {
|
|
30
|
+
// ── Gap 1: gsd.db must be created during init ─────────────────────────
|
|
31
|
+
|
|
32
|
+
test("bootstrapGsdDirectory is followed by ensureDbOpen", () => {
|
|
33
|
+
const bootstrapIdx = wizardSrc.indexOf("bootstrapGsdDirectory(basePath");
|
|
34
|
+
const ensureDbIdx = wizardSrc.indexOf("ensureDbOpen(basePath)");
|
|
35
|
+
assert.ok(bootstrapIdx > -1, "bootstrapGsdDirectory call should exist");
|
|
36
|
+
assert.ok(ensureDbIdx > -1, "ensureDbOpen(basePath) call should exist");
|
|
37
|
+
assert.ok(
|
|
38
|
+
ensureDbIdx > bootstrapIdx,
|
|
39
|
+
"ensureDbOpen must appear after bootstrapGsdDirectory so .gsd/ exists first",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("ensureDbOpen is imported from dynamic-tools", () => {
|
|
44
|
+
assert.match(
|
|
45
|
+
wizardSrc,
|
|
46
|
+
/import.*dynamic-tools/,
|
|
47
|
+
"init-wizard should import from dynamic-tools for ensureDbOpen",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ── Gap 2: runtime/ directory must be created during init ──────────────
|
|
52
|
+
|
|
53
|
+
test("bootstrapGsdDirectory creates runtime/ directory", () => {
|
|
54
|
+
// Find the bootstrapGsdDirectory function body
|
|
55
|
+
const fnStart = wizardSrc.indexOf("function bootstrapGsdDirectory(");
|
|
56
|
+
assert.ok(fnStart > -1, "bootstrapGsdDirectory function should exist");
|
|
57
|
+
|
|
58
|
+
// Find the next function definition to bound the search
|
|
59
|
+
const fnBody = wizardSrc.slice(fnStart, wizardSrc.indexOf("\nfunction ", fnStart + 1));
|
|
60
|
+
|
|
61
|
+
assert.match(
|
|
62
|
+
fnBody,
|
|
63
|
+
/mkdirSync\(.*"runtime"/,
|
|
64
|
+
'bootstrapGsdDirectory should create "runtime" directory',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Gap 3: STATE.md must be written during init ────────────────────────
|
|
69
|
+
|
|
70
|
+
test("showProjectInit generates STATE.md after bootstrap", () => {
|
|
71
|
+
const bootstrapIdx = wizardSrc.indexOf("bootstrapGsdDirectory(basePath");
|
|
72
|
+
const deriveIdx = wizardSrc.indexOf("deriveState(basePath)");
|
|
73
|
+
const stateIdx = wizardSrc.indexOf("buildStateMarkdown(state)");
|
|
74
|
+
const saveIdx = wizardSrc.indexOf('resolveGsdRootFile(basePath, "STATE")');
|
|
75
|
+
|
|
76
|
+
assert.ok(deriveIdx > -1, "deriveState call should exist in init-wizard");
|
|
77
|
+
assert.ok(stateIdx > -1, "buildStateMarkdown call should exist in init-wizard");
|
|
78
|
+
assert.ok(saveIdx > -1, "resolveGsdRootFile STATE call should exist in init-wizard");
|
|
79
|
+
assert.ok(
|
|
80
|
+
deriveIdx > bootstrapIdx,
|
|
81
|
+
"deriveState must appear after bootstrapGsdDirectory",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── Ordering: DB must be open before deriveState ───────────────────────
|
|
86
|
+
|
|
87
|
+
test("ensureDbOpen appears before deriveState", () => {
|
|
88
|
+
const ensureDbIdx = wizardSrc.indexOf("ensureDbOpen(basePath)");
|
|
89
|
+
const deriveIdx = wizardSrc.indexOf("deriveState(basePath)");
|
|
90
|
+
assert.ok(ensureDbIdx > -1, "ensureDbOpen should exist");
|
|
91
|
+
assert.ok(deriveIdx > -1, "deriveState should exist");
|
|
92
|
+
assert.ok(
|
|
93
|
+
ensureDbIdx < deriveIdx,
|
|
94
|
+
"ensureDbOpen must appear before deriveState so DB is ready for state derivation",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── Failure visibility: user must be warned on partial bootstrap ───────
|
|
99
|
+
|
|
100
|
+
test("ensureDbOpen failure surfaces a warning to the user", () => {
|
|
101
|
+
assert.match(
|
|
102
|
+
wizardSrc,
|
|
103
|
+
/if\s*\(\s*!dbReady\s*\)/,
|
|
104
|
+
"init-wizard should check dbReady and warn the user on failure",
|
|
105
|
+
);
|
|
106
|
+
// The warning must reference degraded mode so the user knows what happened
|
|
107
|
+
assert.match(
|
|
108
|
+
wizardSrc,
|
|
109
|
+
/degraded mode/,
|
|
110
|
+
"DB failure warning should mention degraded mode",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("STATE.md failure surfaces a warning to the user", () => {
|
|
115
|
+
assert.match(
|
|
116
|
+
wizardSrc,
|
|
117
|
+
/if\s*\(\s*!stateReady\s*\)/,
|
|
118
|
+
"init-wizard should check stateReady and warn the user on failure",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
summarizeLogs,
|
|
19
19
|
formatForNotification,
|
|
20
20
|
setLogBasePath,
|
|
21
|
+
setStderrLoggingEnabled,
|
|
21
22
|
_resetLogs,
|
|
22
23
|
} from "../workflow-logger.ts";
|
|
23
24
|
|
|
@@ -375,5 +376,20 @@ describe("workflow-logger", () => {
|
|
|
375
376
|
logError("tool", "failed", { cmd: "complete_task" });
|
|
376
377
|
assert.ok(written[0].includes('"cmd":"complete_task"'));
|
|
377
378
|
});
|
|
379
|
+
|
|
380
|
+
test("suppresses stderr when disabled", (t) => {
|
|
381
|
+
const written: string[] = [];
|
|
382
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
383
|
+
const previous = setStderrLoggingEnabled(false);
|
|
384
|
+
// @ts-ignore — patching for test
|
|
385
|
+
process.stderr.write = (chunk: string) => { written.push(chunk); return true; };
|
|
386
|
+
t.after(() => {
|
|
387
|
+
process.stderr.write = orig;
|
|
388
|
+
setStderrLoggingEnabled(previous);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
logWarning("engine", "hidden warning");
|
|
392
|
+
assert.deepEqual(written, []);
|
|
393
|
+
});
|
|
378
394
|
});
|
|
379
395
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -70,6 +71,43 @@ test("buildWorkflowMcpServers mirrors explicit launch config", () => {
|
|
|
70
71
|
});
|
|
71
72
|
});
|
|
72
73
|
|
|
74
|
+
test("detectWorkflowMcpLaunchConfig resolves the bundled server from GSD_PROJECT_ROOT", () => {
|
|
75
|
+
const repoRoot = mkdtempSync(join(tmpdir(), "gsd-workflow-root-"));
|
|
76
|
+
const worktreeRoot = mkdtempSync(join(tmpdir(), "gsd-workflow-worktree-"));
|
|
77
|
+
const cliPath = join(repoRoot, "packages", "mcp-server", "dist", "cli.js");
|
|
78
|
+
|
|
79
|
+
mkdirSync(join(repoRoot, "packages", "mcp-server", "dist"), { recursive: true });
|
|
80
|
+
writeFileSync(cliPath, "#!/usr/bin/env node\n", "utf-8");
|
|
81
|
+
|
|
82
|
+
const launch = detectWorkflowMcpLaunchConfig(worktreeRoot, {
|
|
83
|
+
GSD_PROJECT_ROOT: repoRoot,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
assert.deepEqual(launch, {
|
|
87
|
+
name: "gsd-workflow",
|
|
88
|
+
command: process.execPath,
|
|
89
|
+
args: [cliPath],
|
|
90
|
+
cwd: repoRoot,
|
|
91
|
+
env: {
|
|
92
|
+
GSD_PERSIST_WRITE_GATE_STATE: "1",
|
|
93
|
+
GSD_WORKFLOW_PROJECT_ROOT: repoRoot,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("detectWorkflowMcpLaunchConfig resolves the bundled server relative to the installed GSD package", () => {
|
|
99
|
+
const launch = detectWorkflowMcpLaunchConfig("/tmp/project", {
|
|
100
|
+
GSD_BIN_PATH: "/tmp/gsd-loader.js",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
assert.equal(launch?.command, process.execPath);
|
|
104
|
+
assert.equal(launch?.cwd, "/tmp/project");
|
|
105
|
+
assert.equal(launch?.env?.GSD_CLI_PATH, "/tmp/gsd-loader.js");
|
|
106
|
+
assert.equal(launch?.env?.GSD_WORKFLOW_PROJECT_ROOT, "/tmp/project");
|
|
107
|
+
assert.equal(typeof launch?.args?.[0], "string");
|
|
108
|
+
assert.match(launch?.args?.[0] ?? "", /packages[\/\\]mcp-server[\/\\]dist[\/\\]cli\.js$/);
|
|
109
|
+
});
|
|
110
|
+
|
|
73
111
|
test("usesWorkflowMcpTransport matches local externalCli providers", () => {
|
|
74
112
|
assert.equal(usesWorkflowMcpTransport("externalCli", "local://claude-code"), true);
|
|
75
113
|
assert.equal(usesWorkflowMcpTransport("externalCli", "https://api.example.com"), false);
|