@opengsd/gsd-pi 1.2.0-dev.5457a158 → 1.2.0-dev.84c56d87
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/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +209 -88
- package/dist/resources/extensions/browser-tools/engine/selection.js +73 -5
- package/dist/resources/extensions/browser-tools/index.js +69 -12
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +3 -2
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +19 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +27 -9
- package/dist/resources/extensions/gsd/browser-evidence.js +8 -2
- package/dist/resources/extensions/gsd/mcp-filter.js +2 -19
- package/dist/resources/extensions/gsd/uat-policy.js +2 -1
- package/dist/resources/extensions/gsd/unit-registry.js +7 -20
- package/dist/resources/extensions/gsd/web-app-uat.js +45 -8
- package/dist/resources/extensions/search-the-web/native-search.js +5 -3
- package/dist/resources/extensions/shared/browser-contract.js +59 -0
- package/dist/resources/extensions/shared/gsd-browser-cli.js +72 -4
- package/dist/resources/skills/create-skill/references/executable-code.md +1 -1
- package/dist/resources/skills/create-skill/workflows/add-reference.md +8 -3
- package/dist/resources/skills/create-skill/workflows/add-script.md +4 -2
- package/dist/resources/skills/create-skill/workflows/add-template.md +3 -1
- package/dist/resources/skills/create-skill/workflows/add-workflow.md +8 -3
- package/dist/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
- package/dist/resources/skills/create-skill/workflows/verify-skill.md +9 -4
- package/dist/resources/skills/spike-wrap-up/SKILL.md +9 -9
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
- 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 +9 -9
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- 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/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/node_modules/postcss/lib/container.js +18 -26
- package/dist/web/standalone/node_modules/postcss/lib/css-syntax-error.js +14 -47
- package/dist/web/standalone/node_modules/postcss/lib/declaration.js +4 -4
- package/dist/web/standalone/node_modules/postcss/lib/fromJSON.js +3 -3
- package/dist/web/standalone/node_modules/postcss/lib/input.js +29 -54
- package/dist/web/standalone/node_modules/postcss/lib/lazy-result.js +37 -47
- package/dist/web/standalone/node_modules/postcss/lib/map-generator.js +9 -26
- package/dist/web/standalone/node_modules/postcss/lib/no-work-result.js +55 -57
- package/dist/web/standalone/node_modules/postcss/lib/node.js +31 -99
- package/dist/web/standalone/node_modules/postcss/lib/parse.js +1 -1
- package/dist/web/standalone/node_modules/postcss/lib/parser.js +9 -10
- package/dist/web/standalone/node_modules/postcss/lib/postcss.js +12 -12
- package/dist/web/standalone/node_modules/postcss/lib/previous-map.js +11 -30
- package/dist/web/standalone/node_modules/postcss/lib/processor.js +7 -7
- package/dist/web/standalone/node_modules/postcss/lib/result.js +5 -5
- package/dist/web/standalone/node_modules/postcss/lib/rule.js +6 -6
- package/dist/web/standalone/node_modules/postcss/lib/stringifier.js +28 -69
- package/dist/web/standalone/node_modules/postcss/lib/tokenize.js +2 -6
- package/dist/web/standalone/node_modules/postcss/package.json +48 -48
- package/dist/web/standalone/package.json +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +66 -178
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +116 -204
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +9 -0
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +265 -98
- package/src/resources/extensions/browser-tools/engine/selection.ts +90 -4
- package/src/resources/extensions/browser-tools/index.ts +71 -13
- package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +83 -13
- package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +136 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +3 -2
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +48 -4
- package/src/resources/extensions/gsd/browser-evidence.ts +18 -2
- package/src/resources/extensions/gsd/mcp-filter.ts +2 -23
- package/src/resources/extensions/gsd/tests/browser-automation-contract-fixture.ts +39 -0
- package/src/resources/extensions/gsd/tests/browser-contract.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/dispatch-run-uat-browser-tools.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +35 -1
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +7 -11
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +20 -58
- package/src/resources/extensions/gsd/tests/integration/gsd-integration-fixture.ts +80 -0
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/uat-policy.test.ts +24 -29
- package/src/resources/extensions/gsd/tests/web-app-uat.test.ts +44 -1
- package/src/resources/extensions/gsd/uat-policy.ts +2 -1
- package/src/resources/extensions/gsd/unit-registry.ts +7 -20
- package/src/resources/extensions/gsd/web-app-uat.ts +51 -8
- package/src/resources/extensions/search-the-web/native-search.ts +5 -3
- package/src/resources/extensions/shared/browser-contract.ts +66 -0
- package/src/resources/extensions/shared/gsd-browser-cli.ts +88 -4
- package/src/resources/skills/create-skill/references/executable-code.md +1 -1
- package/src/resources/skills/create-skill/workflows/add-reference.md +8 -3
- package/src/resources/skills/create-skill/workflows/add-script.md +4 -2
- package/src/resources/skills/create-skill/workflows/add-template.md +3 -1
- package/src/resources/skills/create-skill/workflows/add-workflow.md +8 -3
- package/src/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
- package/src/resources/skills/create-skill/workflows/verify-skill.md +9 -4
- package/src/resources/skills/spike-wrap-up/SKILL.md +9 -9
- /package/dist/web/standalone/.next/static/{2p9Rv9pQflAxCBbGVI2vb → AOpDeK_gJHU8OZjRo31gQ}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{2p9Rv9pQflAxCBbGVI2vb → AOpDeK_gJHU8OZjRo31gQ}/_ssgManifest.js +0 -0
|
@@ -58,12 +58,14 @@ import { applyUnitSkillVisibility, unitHasSkillManifest } from "../skill-scope.j
|
|
|
58
58
|
import { getGuidedUnitContext } from "../guided-unit-context.js";
|
|
59
59
|
import { registerPlanMilestoneSchemaRecovery } from "./plan-milestone-schema-recovery.js";
|
|
60
60
|
import { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, canonicalWorkflowToolName, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
|
|
61
|
+
import { hasBrowserContractPrefix } from "../../shared/browser-contract.js";
|
|
61
62
|
import { filterToolsForProvider } from "../model-router.js";
|
|
62
63
|
import { mcpToolMatchesBaseName } from "../mcp-tool-name.js";
|
|
63
64
|
import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
|
|
64
65
|
import { supportsSourceObservationsForUnit } from "../source-observations.js";
|
|
65
66
|
import { clearPendingAutoStart } from "../pending-auto-start.js";
|
|
66
67
|
import { resolveWorkflowToolBasePath } from "./dynamic-tools.js";
|
|
68
|
+
import { getRequiredWorkflowToolsForUnit } from "../unit-tool-contracts.js";
|
|
67
69
|
|
|
68
70
|
let approvalQuestionAbortInFlight = false;
|
|
69
71
|
|
|
@@ -180,7 +182,7 @@ function withPreservedShimTools(toolNames: readonly string[]): string[] {
|
|
|
180
182
|
|
|
181
183
|
/** True for the browser automation tools (browser_navigate, browser_click, ...). */
|
|
182
184
|
function isBrowserTool(toolName: string): boolean {
|
|
183
|
-
return canonicalToolName(toolName)
|
|
185
|
+
return hasBrowserContractPrefix(canonicalToolName(toolName));
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
/**
|
|
@@ -261,6 +263,7 @@ export function buildMinimalAutoGsdToolSet(
|
|
|
261
263
|
activeToolNames: readonly string[],
|
|
262
264
|
unitType: string | undefined,
|
|
263
265
|
registeredToolNames: readonly string[] = activeToolNames,
|
|
266
|
+
warnOnUnresolvedRequiredTools = registeredToolNames !== activeToolNames,
|
|
264
267
|
): string[] {
|
|
265
268
|
if (unitType === "run-uat") {
|
|
266
269
|
return buildRunUatGsdToolSet(activeToolNames, registeredToolNames);
|
|
@@ -276,7 +279,36 @@ export function buildMinimalAutoGsdToolSet(
|
|
|
276
279
|
[...activeToolNames, ...registeredToolNames],
|
|
277
280
|
[...MINIMAL_GSD_TOOL_NAMES, ...unitTools],
|
|
278
281
|
);
|
|
279
|
-
|
|
282
|
+
const result = withPreservedShimTools([...new Set([...preserved, ...scoped])]);
|
|
283
|
+
warnIfRequiredWorkflowToolsUnresolved(unitType, result, warnOnUnresolvedRequiredTools);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasResolvedWorkflowTool(
|
|
288
|
+
resolvedToolNames: readonly string[],
|
|
289
|
+
requiredToolName: string,
|
|
290
|
+
): boolean {
|
|
291
|
+
return resolvedToolNames.some(
|
|
292
|
+
(name) => name === requiredToolName || mcpToolMatchesBaseName(name, requiredToolName),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function warnIfRequiredWorkflowToolsUnresolved(
|
|
297
|
+
unitType: string | undefined,
|
|
298
|
+
scopedToolNames: readonly string[],
|
|
299
|
+
shouldWarn: boolean,
|
|
300
|
+
): void {
|
|
301
|
+
if (!unitType || !shouldWarn) return;
|
|
302
|
+
|
|
303
|
+
const unresolved = getRequiredWorkflowToolsForUnit(unitType).filter(
|
|
304
|
+
(toolName) => !hasResolvedWorkflowTool(scopedToolNames, toolName),
|
|
305
|
+
);
|
|
306
|
+
if (unresolved.length === 0) return;
|
|
307
|
+
|
|
308
|
+
safetyLogWarning(
|
|
309
|
+
"bootstrap",
|
|
310
|
+
`buildMinimalAutoGsdToolSet(${unitType}): required workflow tool(s) not in active/registered surface after scoping: ${unresolved.join(", ")}. Tool registration may have partially failed, provider filtering may have removed a required tool, or workflow MCP may be disconnected.`,
|
|
311
|
+
);
|
|
280
312
|
}
|
|
281
313
|
|
|
282
314
|
export function buildRunUatGsdToolSet(
|
|
@@ -329,6 +361,7 @@ export function buildRequestScopedGsdToolSet(
|
|
|
329
361
|
requestCustomMessages: readonly { customType?: string }[] | undefined,
|
|
330
362
|
registeredToolNames: readonly string[] = activeToolNames,
|
|
331
363
|
guidedUnitType?: string,
|
|
364
|
+
warnOnUnresolvedRequiredTools = registeredToolNames !== activeToolNames,
|
|
332
365
|
): string[] | undefined {
|
|
333
366
|
for (let index = (requestCustomMessages?.length ?? 0) - 1; index >= 0; index--) {
|
|
334
367
|
const currentCustomType = requestCustomMessages?.[index]?.customType;
|
|
@@ -339,7 +372,12 @@ export function buildRequestScopedGsdToolSet(
|
|
|
339
372
|
currentCustomType === "gsd-triage"
|
|
340
373
|
) {
|
|
341
374
|
if (guidedUnitType) {
|
|
342
|
-
return buildMinimalAutoGsdToolSet(
|
|
375
|
+
return buildMinimalAutoGsdToolSet(
|
|
376
|
+
activeToolNames,
|
|
377
|
+
guidedUnitType,
|
|
378
|
+
registeredToolNames,
|
|
379
|
+
warnOnUnresolvedRequiredTools,
|
|
380
|
+
);
|
|
343
381
|
}
|
|
344
382
|
return buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNames);
|
|
345
383
|
}
|
|
@@ -388,11 +426,13 @@ function applyMinimalGsdToolSurface(pi: ExtensionAPI): void {
|
|
|
388
426
|
const dash = getAutoRuntimeSnapshot();
|
|
389
427
|
if (dash.active && dash.currentUnit) {
|
|
390
428
|
const currentToolNames = pi.getActiveTools();
|
|
429
|
+
const hasRegisteredSurface = typeof pi.getAllTools === "function";
|
|
391
430
|
const registeredToolNames = resolveRegisteredToolNames(pi, currentToolNames);
|
|
392
431
|
const scopedToolNames = buildMinimalAutoGsdToolSet(
|
|
393
432
|
currentToolNames,
|
|
394
433
|
dash.currentUnit.type,
|
|
395
434
|
registeredToolNames,
|
|
435
|
+
hasRegisteredSurface,
|
|
396
436
|
);
|
|
397
437
|
recordAutoToolSurfaceSnapshot({
|
|
398
438
|
source: "runtime-scope",
|
|
@@ -414,9 +454,10 @@ export function scopeGsdWorkflowToolsForDispatch(
|
|
|
414
454
|
): ScopedGsdWorkflowState | null {
|
|
415
455
|
if (isFullGsdToolSurfaceRequested()) return null;
|
|
416
456
|
const current = pi.getActiveTools();
|
|
457
|
+
const hasRegisteredSurface = typeof pi.getAllTools === "function";
|
|
417
458
|
const registeredToolNames = resolveRegisteredToolNames(pi, current);
|
|
418
459
|
const scoped = unitType
|
|
419
|
-
? buildMinimalAutoGsdToolSet(current, unitType, registeredToolNames)
|
|
460
|
+
? buildMinimalAutoGsdToolSet(current, unitType, registeredToolNames, hasRegisteredSurface)
|
|
420
461
|
: buildMinimalGsdWorkflowToolSet(current, registeredToolNames);
|
|
421
462
|
recordAutoToolSurfaceSnapshot({
|
|
422
463
|
source: "dispatch-scope",
|
|
@@ -1580,6 +1621,7 @@ export function registerHooks(
|
|
|
1580
1621
|
return surfaceReduced ? { toolNames: providerCompatible } : undefined;
|
|
1581
1622
|
}
|
|
1582
1623
|
const registeredToolNames = resolveRegisteredToolNames(pi, event.activeToolNames);
|
|
1624
|
+
const hasRegisteredSurface = typeof pi.getAllTools === "function";
|
|
1583
1625
|
const compatibleRegisteredToolNames = filterToolsForProvider(
|
|
1584
1626
|
registeredToolNames,
|
|
1585
1627
|
event.selectedModelApi,
|
|
@@ -1594,6 +1636,7 @@ export function registerHooks(
|
|
|
1594
1636
|
event.requestCustomMessages,
|
|
1595
1637
|
requestRegisteredToolNames,
|
|
1596
1638
|
guidedUnit?.unitType,
|
|
1639
|
+
hasRegisteredSurface,
|
|
1597
1640
|
);
|
|
1598
1641
|
if (requestScoped) {
|
|
1599
1642
|
recordAutoToolSurfaceSnapshot({
|
|
@@ -1614,6 +1657,7 @@ export function registerHooks(
|
|
|
1614
1657
|
dash.currentUnit.type === "run-uat" ? aliasFilteredCompatible : providerCompatible,
|
|
1615
1658
|
dash.currentUnit.type,
|
|
1616
1659
|
registeredForUnit,
|
|
1660
|
+
hasRegisteredSurface,
|
|
1617
1661
|
);
|
|
1618
1662
|
recordAutoToolSurfaceSnapshot({
|
|
1619
1663
|
source: "provider-adjustment",
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Shared browser-observable UAT requirement and evidence detection.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import { BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES } from "../shared/browser-contract.js";
|
|
5
|
+
|
|
6
|
+
// Alternation fragment over the contract's evidence-signal names, e.g.
|
|
7
|
+
// `browser_(?:assert|batch|...)`. The names are `browser_`-prefixed
|
|
8
|
+
// identifiers (pinned by tests/browser-contract.test.ts), so no escaping is
|
|
9
|
+
// needed.
|
|
10
|
+
const BROWSER_TOOL_SIGNAL = `browser_(?:${
|
|
11
|
+
BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES.map((name) => name.slice("browser_".length)).join("|")
|
|
12
|
+
})`;
|
|
13
|
+
|
|
14
|
+
export const BROWSER_REQUIREMENT_RE = new RegExp(
|
|
15
|
+
String.raw`\b(?:file://|localhost|playwright|chrome|screenshot|snapshot|${BROWSER_TOOL_SIGNAL})\b|\b(?:open|launch|navigate|load|visit|serve|start)\b.{0,80}\b(?:browser|page|localhost|file://)\b|\bbrowser\s+(?:check|session|test|uat|tool|automation|interaction|flow)\b`,
|
|
16
|
+
"i",
|
|
17
|
+
);
|
|
5
18
|
export const NO_BROWSER_EVIDENCE_RE = /\b(?:no|without|not|wasn'?t|isn'?t)\s+(?:automated\s+)?(?:live\s+)?browser(?:\s+(?:session|test|uat))?|\bno\s+automated\s+browser\b|\bnot\s+conducted\b/i;
|
|
6
|
-
export const BROWSER_RUNTIME_RE =
|
|
19
|
+
export const BROWSER_RUNTIME_RE = new RegExp(
|
|
20
|
+
String.raw`\b(?:browser|playwright|chrome|camoufox|${BROWSER_TOOL_SIGNAL}|screenshot|snapshot|file://|localhost)\b`,
|
|
21
|
+
"i",
|
|
22
|
+
);
|
|
7
23
|
export const BROWSER_ACTION_RE = /\b(?:open(?:ed)?|navigate(?:d)?|click(?:ed)?|type(?:d)?|reload(?:ed)?|capture(?:d)?|screenshot|snapshot)\b/i;
|
|
8
24
|
export const BROWSER_ASSERTION_RE = /\b(?:assert(?:ed|ion)?|observed|confirmed|verified|expected|visible|text|count|label|strikethrough|localstorage|screenshot|snapshot|passed)\b/i;
|
|
9
25
|
const NON_REQUIREMENT_BROWSER_HEADING_RE = /^(?:not\s+proven|not\s+covered|out\s+of\s+scope|deferred|follow-?ups?|known\s+limitations|notes\s+for\s+tester)\b/i;
|
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
import type { ClaudeCodeMcpConfig } from "./preferences-types.js";
|
|
6
|
+
import { isGsdBrowserMcpServerConfig } from "../shared/gsd-browser-cli.js";
|
|
6
7
|
import { toMcpWildcardToolName } from "./mcp-tool-name.js";
|
|
7
8
|
import { resolveModelMcpConfig } from "./preferences-mcp.js";
|
|
8
9
|
|
|
@@ -83,34 +84,12 @@ function isWorkflowMcpServerConfig(config: unknown): boolean {
|
|
|
83
84
|
return args.some((arg) => arg.includes("gsd-mcp-server") || arg.includes("packages/mcp-server"));
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function isBrowserMcpServerConfig(config: unknown): boolean {
|
|
87
|
-
if (!isRecord(config)) return false;
|
|
88
|
-
const command = typeof config.command === "string" ? config.command : "";
|
|
89
|
-
if (command.includes("gsd-browser") || command.includes("@opengsd/gsd-browser")) {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const env = config.env;
|
|
94
|
-
if (isRecord(env)) {
|
|
95
|
-
if (
|
|
96
|
-
typeof env.GSD_BROWSER_CLI_PATH === "string"
|
|
97
|
-
|| typeof env.GSD_BROWSER_BIN_PATH === "string"
|
|
98
|
-
|| typeof env.GSD_BROWSER_MCP_COMMAND === "string"
|
|
99
|
-
) {
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const args = Array.isArray(config.args) ? config.args.filter((arg): arg is string => typeof arg === "string") : [];
|
|
105
|
-
return args.some((arg) => arg.includes("gsd-browser") || arg.includes("@opengsd/gsd-browser"));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
87
|
export function discoverWorkflowMcpServerName(projectDir: string): string | undefined {
|
|
109
88
|
return discoverMcpServers(projectDir).find((server) => isWorkflowMcpServerConfig(server.config))?.name;
|
|
110
89
|
}
|
|
111
90
|
|
|
112
91
|
export function discoverBrowserMcpServerName(projectDir: string): string | undefined {
|
|
113
|
-
return discoverMcpServers(projectDir).find((server) =>
|
|
92
|
+
return discoverMcpServers(projectDir).find((server) => isGsdBrowserMcpServerConfig(server.config))?.name;
|
|
114
93
|
}
|
|
115
94
|
|
|
116
95
|
export function discoverMcpServerNames(projectDir: string): string[] {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getUatBrowserToolSupportError,
|
|
5
|
+
hasUatBrowserToolSurface,
|
|
6
|
+
type UatType,
|
|
7
|
+
} from "../uat-policy.ts";
|
|
8
|
+
|
|
9
|
+
export const BROWSER_AUTOMATION_CONTRACT_TOOLS = {
|
|
10
|
+
piProvider: ["read", "browser_navigate"],
|
|
11
|
+
externalMcpClient: ["read", "mcp__gsd-browser__browser_navigate"],
|
|
12
|
+
externalMcpWildcard: ["read", "mcp__gsd-browser__*"],
|
|
13
|
+
otherBrowserMcp: ["read", "mcp__browser-uat__*"],
|
|
14
|
+
workflowOnly: ["read", "mcp__gsd-workflow__*"],
|
|
15
|
+
withoutBrowser: ["read", "gsd_uat_exec"],
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
export function assertBrowserAutomationContractAvailable(tools: readonly string[]): void {
|
|
19
|
+
assert.equal(hasUatBrowserToolSurface(tools), true, `${tools.join(", ")} should satisfy the Browser Automation Contract`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function assertBrowserAutomationContractMissing(tools: readonly string[] | undefined): void {
|
|
23
|
+
assert.equal(hasUatBrowserToolSurface(tools), false, `${tools?.join(", ") ?? "undefined"} should not satisfy the Browser Automation Contract`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function assertBrowserBackedUatCanDispatch(options: {
|
|
27
|
+
uatType: UatType;
|
|
28
|
+
activeTools: readonly string[] | undefined;
|
|
29
|
+
registeredTools?: readonly string[];
|
|
30
|
+
}): void {
|
|
31
|
+
assert.equal(
|
|
32
|
+
getUatBrowserToolSupportError({
|
|
33
|
+
...options,
|
|
34
|
+
milestoneId: "M001",
|
|
35
|
+
sliceId: "S01",
|
|
36
|
+
}),
|
|
37
|
+
null,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BROWSER_CONTRACT_TOOL_NAMES,
|
|
6
|
+
BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES,
|
|
7
|
+
hasBrowserContractPrefix,
|
|
8
|
+
isBrowserContractToolName,
|
|
9
|
+
} from "../../shared/browser-contract.ts";
|
|
10
|
+
import { isUatBrowserToolName } from "../uat-policy.ts";
|
|
11
|
+
import { BROWSER_REQUIREMENT_RE, BROWSER_RUNTIME_RE } from "../browser-evidence.ts";
|
|
12
|
+
|
|
13
|
+
// Note: RUN_UAT_BROWSER_TOOL_NAMES and MANAGED_GSD_BROWSER_TOOL_NAMES are
|
|
14
|
+
// reference-equal aliases of BROWSER_CONTRACT_TOOL_NAMES, and the managed
|
|
15
|
+
// adapter's spec table is Record-keyed by BrowserContractToolName — both
|
|
16
|
+
// derivations are pinned by the type system, not by runtime assertions here.
|
|
17
|
+
describe("Browser Automation Contract parity", () => {
|
|
18
|
+
it("every contract name satisfies the UAT browser-tool predicate, bare and MCP-prefixed", () => {
|
|
19
|
+
for (const name of BROWSER_CONTRACT_TOOL_NAMES) {
|
|
20
|
+
assert.equal(isUatBrowserToolName(name), true, name);
|
|
21
|
+
assert.equal(isUatBrowserToolName(`mcp__gsd-browser__${name}`), true, `mcp__gsd-browser__${name}`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("contract names are canonical browser_* names with no duplicates", () => {
|
|
26
|
+
assert.equal(new Set(BROWSER_CONTRACT_TOOL_NAMES).size, BROWSER_CONTRACT_TOOL_NAMES.length);
|
|
27
|
+
for (const name of BROWSER_CONTRACT_TOOL_NAMES) {
|
|
28
|
+
assert.equal(hasBrowserContractPrefix(name), true, name);
|
|
29
|
+
assert.equal(isBrowserContractToolName(name), true, name);
|
|
30
|
+
}
|
|
31
|
+
assert.equal(isBrowserContractToolName("browser_not_a_real_tool"), false);
|
|
32
|
+
assert.equal(hasBrowserContractPrefix("gsd_uat_exec"), false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("evidence-signal names stay a subset of the contract and drive the detection regexes", () => {
|
|
36
|
+
for (const name of BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES) {
|
|
37
|
+
assert.equal(isBrowserContractToolName(name), true, name);
|
|
38
|
+
// Identifier-shaped names keep the regex splice in browser-evidence.ts escape-free.
|
|
39
|
+
assert.match(name, /^browser_[a-z_]+$/);
|
|
40
|
+
assert.match(`Verified via ${name} call`, BROWSER_REQUIREMENT_RE);
|
|
41
|
+
assert.match(`Verified via ${name} call`, BROWSER_RUNTIME_RE);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
|
|
10
10
|
import { DISPATCH_RULES, type DispatchContext } from "../auto-dispatch.ts";
|
|
11
11
|
import type { GSDState } from "../types.ts";
|
|
12
|
+
import { BROWSER_AUTOMATION_CONTRACT_TOOLS } from "./browser-automation-contract-fixture.ts";
|
|
12
13
|
|
|
13
14
|
type DispatchRuleEntry = (typeof DISPATCH_RULES)[number];
|
|
14
15
|
|
|
@@ -80,7 +81,7 @@ test("run-uat browser preflight uses registered tools when the active surface is
|
|
|
80
81
|
assert.match(blocked?.action === "stop" ? blocked.reason : "", /run-uat tool surface has none/);
|
|
81
82
|
|
|
82
83
|
const dispatched = await runUatRule().match(makeContext(basePath, {
|
|
83
|
-
registeredTools: [
|
|
84
|
+
registeredTools: [...BROWSER_AUTOMATION_CONTRACT_TOOLS.piProvider],
|
|
84
85
|
}));
|
|
85
86
|
assert.equal(dispatched?.action, "dispatch");
|
|
86
87
|
assert.equal(dispatched?.action === "dispatch" ? dispatched.unitType : undefined, "run-uat");
|
|
@@ -117,7 +117,8 @@ describe("extension bootstrap isolation (#4168, #4172)", () => {
|
|
|
117
117
|
// registration is wrapped in its own try/catch so one failure does not
|
|
118
118
|
// prevent siblings from loading.
|
|
119
119
|
|
|
120
|
-
import { registerGsdExtension } from "../bootstrap/register-extension.ts";
|
|
120
|
+
import { CRITICAL_GSD_WORKFLOW_TOOL_NAMES, registerGsdExtension } from "../bootstrap/register-extension.ts";
|
|
121
|
+
import { drainLogs } from "../workflow-logger.ts";
|
|
121
122
|
|
|
122
123
|
describe("registerGsdExtension defensive registration", () => {
|
|
123
124
|
test("a failing shortcut registration does not prevent kill command registration", async () => {
|
|
@@ -161,4 +162,37 @@ describe("registerGsdExtension defensive registration", () => {
|
|
|
161
162
|
`registerGsdExtension must NOT register 'gsd' (it is registered separately by index.ts), got ${JSON.stringify(registered)}`,
|
|
162
163
|
);
|
|
163
164
|
});
|
|
165
|
+
|
|
166
|
+
test("critical workflow tool list includes lifecycle planning and completion tools", () => {
|
|
167
|
+
assert.ok(CRITICAL_GSD_WORKFLOW_TOOL_NAMES.includes("gsd_plan_slice"));
|
|
168
|
+
assert.ok(CRITICAL_GSD_WORKFLOW_TOOL_NAMES.includes("gsd_slice_complete"));
|
|
169
|
+
assert.ok(CRITICAL_GSD_WORKFLOW_TOOL_NAMES.includes("gsd_validate_milestone"));
|
|
170
|
+
assert.ok(CRITICAL_GSD_WORKFLOW_TOOL_NAMES.includes("gsd_complete_milestone"));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("partial db-tools registration fails visibly when critical tools are missing", () => {
|
|
174
|
+
drainLogs();
|
|
175
|
+
const registeredTools: string[] = [];
|
|
176
|
+
const pi = {
|
|
177
|
+
registerCommand: () => {},
|
|
178
|
+
registerTool: (tool: { name: string }) => {
|
|
179
|
+
if (tool.name === "gsd_plan_slice") {
|
|
180
|
+
throw new Error("simulated db-tools partial registration failure");
|
|
181
|
+
}
|
|
182
|
+
registeredTools.push(tool.name);
|
|
183
|
+
},
|
|
184
|
+
registerHook: () => {},
|
|
185
|
+
registerShortcut: () => {},
|
|
186
|
+
events: { on: () => {}, off: () => {}, emit: () => {} },
|
|
187
|
+
getAllTools: () => registeredTools.map((name) => ({ name })),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
assert.throws(
|
|
191
|
+
() => registerGsdExtension(pi as any),
|
|
192
|
+
/Critical GSD workflow tool registration failed; missing required tool\(s\): .*gsd_plan_slice/,
|
|
193
|
+
);
|
|
194
|
+
assert.ok(registeredTools.includes("gsd_plan_milestone"));
|
|
195
|
+
assert.ok(!registeredTools.includes("gsd_plan_slice"));
|
|
196
|
+
drainLogs();
|
|
197
|
+
});
|
|
164
198
|
});
|
package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
insertTask,
|
|
36
36
|
openDatabase,
|
|
37
37
|
} from "../../gsd-db.ts";
|
|
38
|
+
import { createGsdIntegrationProject } from "./gsd-integration-fixture.ts";
|
|
38
39
|
|
|
39
40
|
function run(cmd: string, cwd: string): string {
|
|
40
41
|
// Safe: all inputs are hardcoded test strings, not user input
|
|
@@ -42,17 +43,12 @@ function run(cmd: string, cwd: string): string {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function createTempRepo(): string {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
|
|
52
|
-
run("git add .", dir);
|
|
53
|
-
run("git commit -m init", dir);
|
|
54
|
-
run("git branch -M main", dir);
|
|
55
|
-
return dir;
|
|
46
|
+
return createGsdIntegrationProject({
|
|
47
|
+
prefix: "wt-ms-merge-test-",
|
|
48
|
+
initialFiles: {
|
|
49
|
+
".gsd/STATE.md": "# State\n",
|
|
50
|
+
},
|
|
51
|
+
}).root;
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
function createTempRepoWithExternalGsd(): { repo: string; externalState: string } {
|
|
@@ -11,6 +11,11 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { execSync } from "node:child_process";
|
|
14
|
+
import {
|
|
15
|
+
commitAll,
|
|
16
|
+
createGsdIntegrationProject,
|
|
17
|
+
writeGsdMilestoneContext,
|
|
18
|
+
} from "./gsd-integration-fixture.ts";
|
|
14
19
|
|
|
15
20
|
import {
|
|
16
21
|
createAutoWorktree,
|
|
@@ -32,17 +37,12 @@ function run(command: string, cwd: string): string {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function createTempRepo(): string {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
run("git add .", dir);
|
|
42
|
-
run("git commit -m init", dir);
|
|
43
|
-
// Ensure branch is called main
|
|
44
|
-
run("git branch -M main", dir);
|
|
45
|
-
return dir;
|
|
40
|
+
return createGsdIntegrationProject("auto-wt-test-").root;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function commitMilestoneContext(repo: string, milestoneId: string): void {
|
|
44
|
+
writeGsdMilestoneContext(repo, milestoneId);
|
|
45
|
+
commitAll(repo, "add milestone");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
describe("auto-worktree lifecycle", () => {
|
|
@@ -59,13 +59,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
59
59
|
|
|
60
60
|
test("create → detect → teardown", () => {
|
|
61
61
|
tempDir = createTempRepo();
|
|
62
|
-
|
|
63
|
-
// Create .gsd/milestones/M003 with a dummy file (simulates planning artifacts)
|
|
64
|
-
const msDir = join(tempDir, ".gsd", "milestones", "M003");
|
|
65
|
-
mkdirSync(msDir, { recursive: true });
|
|
66
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
67
|
-
run("git add .", tempDir);
|
|
68
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
62
|
+
commitMilestoneContext(tempDir, "M003");
|
|
69
63
|
|
|
70
64
|
// ─── createAutoWorktree ──────────────────────────────────────────
|
|
71
65
|
const wtPath = createAutoWorktree(tempDir, "M003");
|
|
@@ -112,11 +106,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
112
106
|
|
|
113
107
|
test("re-entry: create again, exit without teardown, re-enter", () => {
|
|
114
108
|
tempDir = createTempRepo();
|
|
115
|
-
|
|
116
|
-
mkdirSync(msDir, { recursive: true });
|
|
117
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
118
|
-
run("git add .", tempDir);
|
|
119
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
109
|
+
commitMilestoneContext(tempDir, "M003");
|
|
120
110
|
|
|
121
111
|
const wtPath2 = createAutoWorktree(tempDir, "M003");
|
|
122
112
|
assert.ok(existsSync(wtPath2), "worktree re-created");
|
|
@@ -247,11 +237,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
247
237
|
|
|
248
238
|
test("coexistence with manual worktree", async () => {
|
|
249
239
|
tempDir = createTempRepo();
|
|
250
|
-
|
|
251
|
-
mkdirSync(msDir, { recursive: true });
|
|
252
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
253
|
-
run("git add .", tempDir);
|
|
254
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
240
|
+
commitMilestoneContext(tempDir, "M003");
|
|
255
241
|
|
|
256
242
|
// Import createWorktree directly for manual worktree
|
|
257
243
|
const { createWorktree } = await import("../../worktree-manager.ts");
|
|
@@ -274,11 +260,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
274
260
|
|
|
275
261
|
test("split-brain prevention: originalBase cleared after teardown", () => {
|
|
276
262
|
tempDir = createTempRepo();
|
|
277
|
-
|
|
278
|
-
mkdirSync(msDir, { recursive: true });
|
|
279
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
280
|
-
run("git add .", tempDir);
|
|
281
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
263
|
+
commitMilestoneContext(tempDir, "M003");
|
|
282
264
|
|
|
283
265
|
createAutoWorktree(tempDir, "M003");
|
|
284
266
|
teardownAutoWorktree(tempDir, "M003");
|
|
@@ -288,11 +270,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
288
270
|
|
|
289
271
|
test("#1526: getMainBranch returns milestone/<MID> in auto-worktree", async () => {
|
|
290
272
|
tempDir = createTempRepo();
|
|
291
|
-
|
|
292
|
-
mkdirSync(msDir, { recursive: true });
|
|
293
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M005 Context\n");
|
|
294
|
-
run("git add .", tempDir);
|
|
295
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
273
|
+
commitMilestoneContext(tempDir, "M005");
|
|
296
274
|
|
|
297
275
|
const { GitServiceImpl } = await import("../../git-service.ts");
|
|
298
276
|
|
|
@@ -312,11 +290,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
312
290
|
|
|
313
291
|
test("#1713: stale worktree directory without .git file", async () => {
|
|
314
292
|
tempDir = createTempRepo();
|
|
315
|
-
|
|
316
|
-
mkdirSync(msDir, { recursive: true });
|
|
317
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M010 Context\n");
|
|
318
|
-
run("git add .", tempDir);
|
|
319
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
293
|
+
commitMilestoneContext(tempDir, "M010");
|
|
320
294
|
|
|
321
295
|
// Simulate a crash leaving a stale directory with no .git file.
|
|
322
296
|
const { worktreePath } = await import("../../worktree-manager.ts");
|
|
@@ -337,11 +311,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
337
311
|
|
|
338
312
|
test("#778: re-attach does not reconcile plan checkboxes into a worktree-local .gsd projection", async () => {
|
|
339
313
|
tempDir = createTempRepo();
|
|
340
|
-
|
|
341
|
-
mkdirSync(msDir, { recursive: true });
|
|
342
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
343
|
-
run("git add .", tempDir);
|
|
344
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
314
|
+
commitMilestoneContext(tempDir, "M003");
|
|
345
315
|
|
|
346
316
|
const planRelPath = join(".gsd", "milestones", "M004", "slices", "S01", "S01-PLAN.md");
|
|
347
317
|
const planDir = join(tempDir, ".gsd", "milestones", "M004", "slices", "S01");
|
|
@@ -401,11 +371,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
401
371
|
|
|
402
372
|
test("#2791: mcp.json is not copied into worktree on creation after copyPlanningArtifacts removal", () => {
|
|
403
373
|
tempDir = createTempRepo();
|
|
404
|
-
|
|
405
|
-
mkdirSync(msDir, { recursive: true });
|
|
406
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
407
|
-
run("git add .", tempDir);
|
|
408
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
374
|
+
commitMilestoneContext(tempDir, "M003");
|
|
409
375
|
|
|
410
376
|
// Create mcp.json in .gsd/ AFTER the commit (untracked, like real usage).
|
|
411
377
|
// Phase C removed copyPlanningArtifacts, so creation should not seed a
|
|
@@ -430,11 +396,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
430
396
|
|
|
431
397
|
test("#2791: mcp.json synced via syncGsdStateToWorktree (ROOT_STATE_FILES)", () => {
|
|
432
398
|
tempDir = createTempRepo();
|
|
433
|
-
|
|
434
|
-
mkdirSync(msDir, { recursive: true });
|
|
435
|
-
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
436
|
-
run("git add .", tempDir);
|
|
437
|
-
run("git commit -m \"add milestone\"", tempDir);
|
|
399
|
+
commitMilestoneContext(tempDir, "M003");
|
|
438
400
|
|
|
439
401
|
// Create worktree first (no mcp.json yet)
|
|
440
402
|
const wtPath = createAutoWorktree(tempDir, "M003");
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export type GsdIntegrationProject = {
|
|
7
|
+
root: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CreateGsdIntegrationProjectOptions = {
|
|
11
|
+
prefix?: string;
|
|
12
|
+
initialFiles?: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function projectRoot(project: GsdIntegrationProject | string): string {
|
|
16
|
+
return typeof project === "string" ? project : project.root;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function git(project: GsdIntegrationProject | string, ...args: string[]): string {
|
|
20
|
+
return execFileSync("git", args, {
|
|
21
|
+
cwd: projectRoot(project),
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
}).trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createGsdIntegrationProject(
|
|
28
|
+
options: CreateGsdIntegrationProjectOptions | string = {},
|
|
29
|
+
): GsdIntegrationProject {
|
|
30
|
+
const resolvedOptions = typeof options === "string" ? { prefix: options } : options;
|
|
31
|
+
const prefix = resolvedOptions.prefix ?? "gsd-integration-";
|
|
32
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
|
|
33
|
+
|
|
34
|
+
git(root, "init");
|
|
35
|
+
git(root, "config", "user.email", "test@test.com");
|
|
36
|
+
git(root, "config", "user.name", "Test");
|
|
37
|
+
git(root, "config", "core.autocrlf", "false");
|
|
38
|
+
|
|
39
|
+
writeProjectFile(root, "README.md", "# test\n");
|
|
40
|
+
for (const [relativePath, content] of Object.entries(resolvedOptions.initialFiles ?? {})) {
|
|
41
|
+
writeProjectFile(root, relativePath, content);
|
|
42
|
+
}
|
|
43
|
+
git(root, "add", ".");
|
|
44
|
+
git(root, "commit", "-m", "init");
|
|
45
|
+
git(root, "branch", "-M", "main");
|
|
46
|
+
|
|
47
|
+
return { root };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function writeProjectFile(
|
|
51
|
+
project: GsdIntegrationProject | string,
|
|
52
|
+
relativePath: string,
|
|
53
|
+
content: string,
|
|
54
|
+
): string {
|
|
55
|
+
const filePath = join(projectRoot(project), relativePath);
|
|
56
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
57
|
+
writeFileSync(filePath, content, "utf-8");
|
|
58
|
+
return filePath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function writeGsdMilestoneContext(
|
|
62
|
+
project: GsdIntegrationProject | string,
|
|
63
|
+
milestoneId: string,
|
|
64
|
+
content = `# ${milestoneId} Context\n`,
|
|
65
|
+
): string {
|
|
66
|
+
return writeProjectFile(
|
|
67
|
+
project,
|
|
68
|
+
join(".gsd", "milestones", milestoneId, "CONTEXT.md"),
|
|
69
|
+
content,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function commitAll(project: GsdIntegrationProject | string, message: string): void {
|
|
74
|
+
git(project, "add", ".");
|
|
75
|
+
git(project, "commit", "-m", message);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function cleanupGsdIntegrationProject(project: GsdIntegrationProject | string): void {
|
|
79
|
+
rmSync(projectRoot(project), { recursive: true, force: true });
|
|
80
|
+
}
|