@opengsd/gsd-pi 1.9.0-dev.d2070876 → 1.9.0-dev.e10b60d4

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.
Files changed (117) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/loop.js +4 -24
  3. package/dist/resources/extensions/gsd/auto/run-unit.js +1 -19
  4. package/dist/resources/extensions/gsd/auto-start.js +9 -29
  5. package/dist/resources/extensions/gsd/auto-worktree-sync.js +3 -7
  6. package/dist/resources/extensions/gsd/auto.js +1 -12
  7. package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -21
  8. package/dist/resources/extensions/gsd/flat-phase-migration.js +0 -12
  9. package/dist/resources/extensions/gsd/gsd-db.js +31 -75
  10. package/dist/resources/extensions/gsd/worktree-state-projection.js +18 -20
  11. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  12. package/dist/web/standalone/.next/BUILD_ID +1 -1
  13. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  14. package/dist/web/standalone/.next/build-manifest.json +2 -2
  15. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  16. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.html +1 -1
  33. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  40. package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
  41. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  42. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  43. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  44. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  45. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  46. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  47. package/package.json +1 -1
  48. package/packages/cloud-mcp-gateway/package.json +2 -2
  49. package/packages/contracts/package.json +1 -1
  50. package/packages/daemon/package.json +4 -4
  51. package/packages/gsd-agent-core/package.json +5 -5
  52. package/packages/gsd-agent-modes/package.json +7 -7
  53. package/packages/gsd-cloud/README.md +3 -9
  54. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  55. package/packages/gsd-cloud/dist/cli.js +15 -121
  56. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  57. package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
  58. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
  59. package/packages/gsd-cloud/dist/cloud-config.js +1 -7
  60. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
  61. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +3 -1
  62. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  63. package/packages/gsd-cloud/dist/cloud-runtime.js +7 -9
  64. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  65. package/packages/gsd-cloud/package.json +2 -2
  66. package/packages/mcp-server/dist/cli-runner.d.ts +0 -3
  67. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
  68. package/packages/mcp-server/dist/cli-runner.js +33 -119
  69. package/packages/mcp-server/dist/cli-runner.js.map +1 -1
  70. package/packages/mcp-server/package.json +4 -4
  71. package/packages/native/package.json +1 -1
  72. package/packages/pi-agent-core/package.json +1 -1
  73. package/packages/pi-ai/dist/models.generated.d.ts +0 -17
  74. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  75. package/packages/pi-ai/dist/models.generated.js +0 -17
  76. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  77. package/packages/pi-ai/package.json +1 -1
  78. package/packages/pi-coding-agent/dist/core/skills.d.ts +0 -5
  79. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/skills.js +0 -54
  81. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  82. package/packages/pi-coding-agent/package.json +7 -7
  83. package/packages/pi-tui/package.json +2 -2
  84. package/packages/rpc-client/package.json +2 -2
  85. package/pkg/package.json +1 -1
  86. package/src/resources/extensions/gsd/auto/loop.ts +4 -48
  87. package/src/resources/extensions/gsd/auto/run-unit.ts +1 -20
  88. package/src/resources/extensions/gsd/auto-start.ts +8 -31
  89. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -10
  90. package/src/resources/extensions/gsd/auto.ts +1 -13
  91. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -27
  92. package/src/resources/extensions/gsd/flat-phase-migration.ts +0 -14
  93. package/src/resources/extensions/gsd/gsd-db.ts +33 -85
  94. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +0 -156
  95. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +1 -21
  96. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +1 -74
  97. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +0 -83
  98. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +9 -8
  99. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +0 -32
  100. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +0 -13
  101. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  102. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -60
  103. package/src/resources/extensions/gsd/worktree-state-projection.ts +24 -21
  104. package/packages/gsd-cloud/dist/background-cli.test.d.ts +0 -2
  105. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +0 -1
  106. package/packages/gsd-cloud/dist/background-cli.test.js +0 -248
  107. package/packages/gsd-cloud/dist/background-cli.test.js.map +0 -1
  108. package/packages/gsd-cloud/dist/runtime-process.d.ts +0 -27
  109. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +0 -1
  110. package/packages/gsd-cloud/dist/runtime-process.js +0 -272
  111. package/packages/gsd-cloud/dist/runtime-process.js.map +0 -1
  112. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +0 -2
  113. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +0 -1
  114. package/packages/gsd-cloud/dist/runtime-process.test.js +0 -97
  115. package/packages/gsd-cloud/dist/runtime-process.test.js.map +0 -1
  116. /package/dist/web/standalone/.next/static/{sggX-h41OjePo-FjdArP5 → V9MAVyYVPboQUB9KTTAMi}/_buildManifest.js +0 -0
  117. /package/dist/web/standalone/.next/static/{sggX-h41OjePo-FjdArP5 → V9MAVyYVPboQUB9KTTAMi}/_ssgManifest.js +0 -0
@@ -1428,19 +1428,6 @@ process.exit(result.status ?? 0);
1428
1428
  rmSync(repo, { recursive: true, force: true });
1429
1429
  });
1430
1430
 
1431
- test('Integration branch: write does not create legacy milestones scaffold', () => {
1432
- const repo = initBranchTestRepo();
1433
- mkdirSync(join(repo, ".gsd", "phases"), { recursive: true });
1434
-
1435
- writeIntegrationBranch(repo, "M001", "f-123-new-thing");
1436
-
1437
- assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), "f-123-new-thing");
1438
- assert.ok(existsSync(join(repo, ".gsd", "M001-META.json")), "metadata is written at the flat .gsd root");
1439
- assert.ok(!existsSync(join(repo, ".gsd", "milestones")), "legacy milestones/ root is not created");
1440
-
1441
- rmSync(repo, { recursive: true, force: true });
1442
- });
1443
-
1444
1431
  // ─── writeIntegrationBranch: updates when branch changes (#300) ──────
1445
1432
 
1446
1433
  test('Integration branch: updates on branch change', () => {
@@ -117,8 +117,8 @@ test("projectRootToWorktree projects flat-phase discuss artifacts before dispatc
117
117
  new WorktreeStateProjection().projectRootToWorktree(scope);
118
118
 
119
119
  assert.ok(
120
- !existsSync(join(worktree, ".gsd", "milestones")),
121
- "stale metadata-only legacy milestones/ scaffold is not projected into the worktree",
120
+ existsSync(join(worktree, ".gsd", "milestones", "M001", "M001-META.json")),
121
+ "milestone metadata is projected into the worktree",
122
122
  );
123
123
  assert.ok(
124
124
  existsSync(join(worktree, ".gsd", "phases", "01-foundation", "01-CONTEXT.md")),
@@ -246,66 +246,6 @@ describe('worktree-sync-milestones', async () => {
246
246
  }
247
247
  }
248
248
 
249
- // ─── 7b. flat-phase sync skips empty legacy milestones root ───────────
250
- console.log('\n=== 7b. flat-phase sync skips empty legacy milestones root ===');
251
- {
252
- const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-flat-main-'));
253
- const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-flat-wt-'));
254
-
255
- try {
256
- const phaseDir = join(mainBase, '.gsd', 'phases', '01-foundation');
257
- mkdirSync(phaseDir, { recursive: true });
258
- mkdirSync(join(mainBase, '.gsd', 'milestones'), { recursive: true });
259
- mkdirSync(join(wtBase, '.gsd'), { recursive: true });
260
- writeFileSync(join(phaseDir, '01-CONTEXT.md'), '# Foundation\n');
261
-
262
- syncGsdStateToWorktree(mainBase, wtBase);
263
-
264
- assert.ok(
265
- existsSync(join(wtBase, '.gsd', 'phases', '01-foundation', '01-CONTEXT.md')),
266
- 'flat-phase artifact is synced to worktree',
267
- );
268
- assert.ok(
269
- !existsSync(join(wtBase, '.gsd', 'milestones')),
270
- 'empty legacy milestones/ root is not recreated in worktree',
271
- );
272
- } finally {
273
- rmSync(mainBase, { recursive: true, force: true });
274
- rmSync(wtBase, { recursive: true, force: true });
275
- }
276
- }
277
-
278
- // ─── 7c. flat-phase sync skips metadata-only legacy milestone dirs ────
279
- console.log('\n=== 7c. flat-phase sync skips metadata-only legacy milestone dirs ===');
280
- {
281
- const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-flat-meta-main-'));
282
- const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-flat-meta-wt-'));
283
-
284
- try {
285
- const phaseDir = join(mainBase, '.gsd', 'phases', '01-foundation');
286
- const metaDir = join(mainBase, '.gsd', 'milestones', 'M001');
287
- mkdirSync(phaseDir, { recursive: true });
288
- mkdirSync(metaDir, { recursive: true });
289
- mkdirSync(join(wtBase, '.gsd'), { recursive: true });
290
- writeFileSync(join(phaseDir, '01-CONTEXT.md'), '# Foundation\n');
291
- writeFileSync(join(metaDir, 'M001-META.json'), '{"integrationBranch":"main"}\n');
292
-
293
- syncGsdStateToWorktree(mainBase, wtBase);
294
-
295
- assert.ok(
296
- existsSync(join(wtBase, '.gsd', 'phases', '01-foundation', '01-CONTEXT.md')),
297
- 'flat-phase artifact is synced to worktree',
298
- );
299
- assert.ok(
300
- !existsSync(join(wtBase, '.gsd', 'milestones')),
301
- 'metadata-only legacy milestones/ scaffold is not recreated in worktree',
302
- );
303
- } finally {
304
- rmSync(mainBase, { recursive: true, force: true });
305
- rmSync(wtBase, { recursive: true, force: true });
306
- }
307
- }
308
-
309
249
  // ─── 8. syncWorktreeStateBack does not copy task projections ───────────
310
250
  console.log('\n=== 8. syncWorktreeStateBack leaves task projections in worktree ===');
311
251
  {
@@ -32,7 +32,7 @@ import {
32
32
  import { join } from "node:path";
33
33
 
34
34
  import { reconcileWorktreeDb } from "./gsd-db.js";
35
- import { dirIsContentBearingLegacyMilestone, resolveGsdPathContract } from "./paths.js";
35
+ import { resolveGsdPathContract } from "./paths.js";
36
36
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
37
37
  import type { MilestoneScope } from "./workspace.js";
38
38
  import { logError, logWarning } from "./workflow-logger.js";
@@ -161,7 +161,6 @@ function syncOtherMilestoneArtifacts(
161
161
  // additive safeCopyRecursive; skip it here to avoid redundant work.
162
162
  if (milestoneEntry.name === currentMilestoneId) continue;
163
163
  const srcMilestoneDir = join(srcMilestonesDir, milestoneEntry.name);
164
- if (!dirIsContentBearingLegacyMilestone(srcMilestoneDir)) continue;
165
164
  const dstMilestoneDir = join(dstMilestonesDir, milestoneEntry.name);
166
165
 
167
166
  // Additively project the entire milestone subtree (force:false), not just
@@ -272,32 +271,36 @@ export function _projectRootToWorktreeImpl(
272
271
  // Without this, worktree-local files (e.g. VALIDATION.md written
273
272
  // by validate-milestone) get clobbered by stale project root copies,
274
273
  // causing an infinite re-validation loop (#1886).
275
- const prMilestoneDir = join(prGsd, "milestones", milestoneId);
276
- const wtMilestoneDir = join(wtGsd, "milestones", milestoneId);
277
- if (dirIsContentBearingLegacyMilestone(prMilestoneDir)) {
278
- safeCopyRecursive(prMilestoneDir, wtMilestoneDir, { force: false });
279
-
280
- // Force-sync ASSESSMENT files that have a verdict from project root (#2821).
281
- // The additive-only copy above preserves worktree-local files, but
282
- // ASSESSMENT files are special: after run-uat writes a verdict and post-unit
283
- // syncs it to the project root, the worktree may retain a stale copy (e.g.
284
- // verdict:fail while the project root has verdict:pass from a retry). On
285
- // session resume the DB is rebuilt from disk, and if the stale ASSESSMENT
286
- // persists, checkNeedsRunUat finds no passing verdict → re-dispatches
287
- // run-uat indefinitely (stuck-loop ×9).
288
- forceOverwriteValidationWithVerdict(prMilestoneDir, wtMilestoneDir, milestoneId);
289
- forceOverwriteAssessmentsWithVerdict(prMilestoneDir, wtMilestoneDir);
290
- }
274
+ safeCopyRecursive(
275
+ join(prGsd, "milestones", milestoneId),
276
+ join(wtGsd, "milestones", milestoneId),
277
+ { force: false },
278
+ );
291
279
 
292
- // Additively project the full subtree of every OTHER content-bearing legacy
293
- // milestone so worktree-bound units can read prior-milestone context artifacts
294
- // without recreating empty/meta-only milestones/ scaffolds in flat-phase projects.
280
+ // Additively project the full subtree of every OTHER milestone so
281
+ // worktree-bound units can read prior-milestone context artifacts and so
282
+ // completed milestones retain their per-slice/per-task SUMMARY.md and
283
+ // UAT.md on the worktree filesystem (otherwise the stale-render detector
284
+ // flags them as "complete in DB but missing on disk").
295
285
  syncOtherMilestoneArtifacts(
296
286
  join(prGsd, "milestones"),
297
287
  join(wtGsd, "milestones"),
298
288
  milestoneId,
299
289
  );
300
290
 
291
+ // Force-sync ASSESSMENT files that have a verdict from project root (#2821).
292
+ // The additive-only copy above preserves worktree-local files, but
293
+ // ASSESSMENT files are special: after run-uat writes a verdict and post-unit
294
+ // syncs it to the project root, the worktree may retain a stale copy (e.g.
295
+ // verdict:fail while the project root has verdict:pass from a retry). On
296
+ // session resume the DB is rebuilt from disk, and if the stale ASSESSMENT
297
+ // persists, checkNeedsRunUat finds no passing verdict → re-dispatches
298
+ // run-uat indefinitely (stuck-loop ×9).
299
+ const prMilestoneDir = join(prGsd, "milestones", milestoneId);
300
+ const wtMilestoneDir = join(wtGsd, "milestones", milestoneId);
301
+ forceOverwriteValidationWithVerdict(prMilestoneDir, wtMilestoneDir, milestoneId);
302
+ forceOverwriteAssessmentsWithVerdict(prMilestoneDir, wtMilestoneDir);
303
+
301
304
  // Forward-sync completed-units.json from project root to worktree.
302
305
  // Project root is authoritative for completion state after crash recovery;
303
306
  // without this, the worktree re-dispatches already-completed units (#1886).
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=background-cli.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"background-cli.test.d.ts","sourceRoot":"","sources":["../src/background-cli.test.ts"],"names":[],"mappings":""}
@@ -1,248 +0,0 @@
1
- // Project/App: Open GSD
2
- // File Purpose: Acceptance coverage for the detached gsd-cloud runtime lifecycle.
3
- import { spawn } from "node:child_process";
4
- import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync } from "node:fs";
5
- import { createServer } from "node:http";
6
- import { tmpdir } from "node:os";
7
- import { dirname, join, resolve } from "node:path";
8
- import { fileURLToPath } from "node:url";
9
- import { test } from "node:test";
10
- import assert from "node:assert/strict";
11
- import { WebSocketServer } from "ws";
12
- import { saveCloudConfig } from "./cloud-config.js";
13
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
14
- const cliPath = join(packageRoot, "bin", "gsd-cloud.js");
15
- function runCli(args, cwd, timeoutMs = 12_000, extraEnv = {}) {
16
- return new Promise((resolveResult, reject) => {
17
- const child = spawn(process.execPath, [cliPath, ...args], {
18
- cwd,
19
- env: { ...process.env, GSD_CLOUD_PROJECTS: undefined, ...extraEnv },
20
- stdio: ["ignore", "pipe", "pipe"],
21
- });
22
- let stdout = "";
23
- let stderr = "";
24
- child.stdout.setEncoding("utf8");
25
- child.stderr.setEncoding("utf8");
26
- child.stdout.on("data", (chunk) => { stdout += chunk; });
27
- child.stderr.on("data", (chunk) => { stderr += chunk; });
28
- const timeout = setTimeout(() => {
29
- child.kill("SIGTERM");
30
- reject(new Error(`gsd-cloud did not return within ${timeoutMs}ms`));
31
- }, timeoutMs);
32
- child.once("error", (error) => {
33
- clearTimeout(timeout);
34
- reject(error);
35
- });
36
- child.once("exit", (code) => {
37
- clearTimeout(timeout);
38
- resolveResult({ code, stdout, stderr });
39
- });
40
- });
41
- }
42
- // Spawn a long-lived CLI process (e.g. `--foreground`) without waiting for exit,
43
- // so the test can inspect its state and then stop it.
44
- function spawnForegroundCli(args, cwd) {
45
- const child = spawn(process.execPath, [cliPath, ...args], {
46
- cwd,
47
- env: { ...process.env, GSD_CLOUD_PROJECTS: undefined },
48
- stdio: ["ignore", "pipe", "pipe"],
49
- });
50
- const exited = new Promise((resolveExit) => {
51
- child.once("exit", (code) => resolveExit(code));
52
- });
53
- return { child, exited };
54
- }
55
- async function createTestGateway(deviceFlow = false) {
56
- let baseUrl = "";
57
- const server = createServer((req, res) => {
58
- if (!deviceFlow) {
59
- res.writeHead(404).end();
60
- return;
61
- }
62
- res.setHeader("content-type", "application/json");
63
- if (req.url === "/api/device/code") {
64
- res.end(JSON.stringify({
65
- userCode: "TEST-CODE",
66
- deviceCode: "test-device",
67
- verificationUriComplete: `${baseUrl}/approve-device?code=TEST-CODE`,
68
- expiresIn: 30,
69
- }));
70
- return;
71
- }
72
- if (req.url === "/api/device/token") {
73
- res.end(JSON.stringify({
74
- status: "approved",
75
- token: "test",
76
- runtimeId: "fixture-runtime",
77
- gateway_url: baseUrl,
78
- }));
79
- return;
80
- }
81
- res.writeHead(404).end();
82
- });
83
- const wss = new WebSocketServer({ server });
84
- await new Promise((resolveListen, reject) => {
85
- server.once("error", reject);
86
- server.listen(0, "127.0.0.1", resolveListen);
87
- });
88
- const address = server.address();
89
- assert.ok(address && typeof address === "object");
90
- baseUrl = `http://127.0.0.1:${address.port}`;
91
- let helloTimeout;
92
- const hello = new Promise((resolveHello, reject) => {
93
- helloTimeout = setTimeout(() => reject(new Error("background runtime did not advertise its project")), 8_000);
94
- wss.once("connection", (socket) => {
95
- socket.once("message", (data) => {
96
- clearTimeout(helloTimeout);
97
- resolveHello(JSON.parse(data.toString("utf8")));
98
- });
99
- });
100
- });
101
- return {
102
- baseUrl,
103
- hello,
104
- close: async () => {
105
- clearTimeout(helloTimeout);
106
- for (const client of wss.clients)
107
- client.terminate();
108
- await new Promise((resolveClose) => wss.close(() => server.close(() => resolveClose())));
109
- },
110
- };
111
- }
112
- test("connect returns while a background runtime advertises the selected project", async () => {
113
- const root = mkdtempSync(join(tmpdir(), "gsd-cloud-background-"));
114
- const projectDir = join(root, "project");
115
- const expectedProjectDir = join(realpathSync(root), "project");
116
- const configPath = join(root, "daemon.yaml");
117
- mkdirSync(join(projectDir, ".gsd"), { recursive: true });
118
- const gateway = await createTestGateway();
119
- saveCloudConfig(configPath, {
120
- gateway_url: gateway.baseUrl,
121
- device_token: "test",
122
- runtime_id: "fixture-runtime",
123
- enabled: true,
124
- });
125
- try {
126
- const connect = await runCli(["connect", "--config", configPath], projectDir);
127
- assert.equal(connect.code, 0, connect.stderr);
128
- assert.match(connect.stdout, /connected in the background/i);
129
- const message = await gateway.hello;
130
- assert.equal(message.type, "hello");
131
- const projects = message.projects;
132
- assert.equal(projects.length, 1);
133
- assert.equal(projects[0]?.path, expectedProjectDir);
134
- const status = await runCli(["status", "--config", configPath], projectDir);
135
- assert.equal(status.code, 0, status.stderr);
136
- const statusBody = JSON.parse(status.stdout);
137
- assert.equal(statusBody.background?.running, true);
138
- assert.equal(typeof statusBody.background?.pid, "number");
139
- assert.deepEqual(statusBody.projects, [expectedProjectDir]);
140
- }
141
- finally {
142
- await runCli(["disconnect", "--config", configPath], projectDir).catch(() => undefined);
143
- await gateway.close();
144
- rmSync(root, { recursive: true, force: true });
145
- }
146
- });
147
- test("login returns after approval and keeps the selected project connected", async () => {
148
- const root = mkdtempSync(join(tmpdir(), "gsd-cloud-login-background-"));
149
- const projectDir = join(root, "project");
150
- const expectedProjectDir = join(realpathSync(root), "project");
151
- const configPath = join(root, "daemon.yaml");
152
- mkdirSync(join(projectDir, ".gsd"), { recursive: true });
153
- const gateway = await createTestGateway(true);
154
- try {
155
- const login = await runCli([
156
- "login",
157
- "--gateway", gateway.baseUrl,
158
- "--config", configPath,
159
- ], projectDir, 12_000);
160
- assert.equal(login.code, 0, login.stderr);
161
- assert.match(login.stdout, /Authorization approved!/);
162
- assert.match(login.stdout, /connected in the background/i);
163
- const message = await gateway.hello;
164
- const projects = message.projects;
165
- assert.deepEqual(projects.map((project) => project.path), [expectedProjectDir]);
166
- const disconnect = await runCli(["disconnect", "--config", configPath], projectDir);
167
- assert.equal(disconnect.code, 0, disconnect.stderr);
168
- const status = await runCli(["status", "--config", configPath], projectDir);
169
- const statusBody = JSON.parse(status.stdout);
170
- assert.equal(statusBody.configured, false);
171
- assert.equal(statusBody.background?.running, false);
172
- }
173
- finally {
174
- await runCli(["disconnect", "--config", configPath], projectDir).catch(() => undefined);
175
- await gateway.close();
176
- rmSync(root, { recursive: true, force: true });
177
- }
178
- });
179
- test("advertised project paths are canonicalized through symlinks", async () => {
180
- const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cloud-symlink-")));
181
- const realProject = join(root, "real-project");
182
- mkdirSync(join(realProject, ".gsd"), { recursive: true });
183
- const linkProject = join(root, "link-project");
184
- symlinkSync(realProject, linkProject);
185
- const configPath = join(root, "daemon.yaml");
186
- const gateway = await createTestGateway();
187
- saveCloudConfig(configPath, {
188
- gateway_url: gateway.baseUrl,
189
- device_token: "test",
190
- runtime_id: "fixture-runtime",
191
- enabled: true,
192
- });
193
- try {
194
- // Point the runtime at the symlink; the advertised path must be the real dir.
195
- const connect = await runCli(["connect", "--config", configPath], realProject, 5_000, { GSD_CLOUD_PROJECTS: linkProject });
196
- assert.equal(connect.code, 0, connect.stderr);
197
- const message = await gateway.hello;
198
- const projects = message.projects;
199
- assert.equal(projects.length, 1);
200
- assert.equal(projects[0]?.path, realProject);
201
- const status = await runCli(["status", "--config", configPath], realProject);
202
- const statusBody = JSON.parse(status.stdout);
203
- assert.deepEqual(statusBody.projects, [realProject]);
204
- }
205
- finally {
206
- await runCli(["disconnect", "--config", configPath], realProject).catch(() => undefined);
207
- await gateway.close();
208
- rmSync(root, { recursive: true, force: true });
209
- }
210
- });
211
- test("a foreground runtime registers its PID so disconnect can stop it", async () => {
212
- const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cloud-foreground-")));
213
- const projectDir = join(root, "project");
214
- const configPath = join(root, "daemon.yaml");
215
- mkdirSync(join(projectDir, ".gsd"), { recursive: true });
216
- const gateway = await createTestGateway();
217
- saveCloudConfig(configPath, {
218
- gateway_url: gateway.baseUrl,
219
- device_token: "test",
220
- runtime_id: "fixture-runtime",
221
- enabled: true,
222
- });
223
- const foreground = spawnForegroundCli(["connect", "--foreground", "--config", configPath], projectDir);
224
- try {
225
- // Once the foreground runtime advertises its project it is fully connected.
226
- await gateway.hello;
227
- // The foreground session must record its own PID in the shared runtime state
228
- // so status can see it and a later disconnect can find and stop it.
229
- const status = await runCli(["status", "--config", configPath], projectDir);
230
- const statusBody = JSON.parse(status.stdout);
231
- assert.equal(statusBody.background?.running, true);
232
- assert.equal(statusBody.background?.pid, foreground.child.pid);
233
- const disconnect = await runCli(["disconnect", "--config", configPath], projectDir);
234
- assert.equal(disconnect.code, 0, disconnect.stderr);
235
- // disconnect must have terminated the foreground process.
236
- const exitCode = await foreground.exited;
237
- assert.equal(exitCode, 0);
238
- const afterStatus = await runCli(["status", "--config", configPath], projectDir);
239
- const afterBody = JSON.parse(afterStatus.stdout);
240
- assert.equal(afterBody.background?.running, false);
241
- }
242
- finally {
243
- foreground.child.kill("SIGKILL");
244
- await gateway.close();
245
- rmSync(root, { recursive: true, force: true });
246
- }
247
- });
248
- //# sourceMappingURL=background-cli.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"background-cli.test.js","sourceRoot":"","sources":["../src/background-cli.test.ts"],"names":[],"mappings":"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","sourcesContent":["// Project/App: Open GSD\n// File Purpose: Acceptance coverage for the detached gsd-cloud runtime lifecycle.\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync } from \"node:fs\";\nimport { createServer } from \"node:http\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { test } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { WebSocketServer } from \"ws\";\nimport { saveCloudConfig } from \"./cloud-config.js\";\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst cliPath = join(packageRoot, \"bin\", \"gsd-cloud.js\");\n\ninterface CliResult {\n code: number | null;\n stdout: string;\n stderr: string;\n}\n\ninterface TestGateway {\n baseUrl: string;\n hello: Promise<Record<string, unknown>>;\n close: () => Promise<void>;\n}\n\nfunction runCli(\n args: string[],\n cwd: string,\n timeoutMs = 12_000,\n extraEnv: Record<string, string | undefined> = {},\n): Promise<CliResult> {\n return new Promise((resolveResult, reject) => {\n const child = spawn(process.execPath, [cliPath, ...args], {\n cwd,\n env: { ...process.env, GSD_CLOUD_PROJECTS: undefined, ...extraEnv },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n let stdout = \"\";\n let stderr = \"\";\n child.stdout.setEncoding(\"utf8\");\n child.stderr.setEncoding(\"utf8\");\n child.stdout.on(\"data\", (chunk: string) => { stdout += chunk; });\n child.stderr.on(\"data\", (chunk: string) => { stderr += chunk; });\n\n const timeout = setTimeout(() => {\n child.kill(\"SIGTERM\");\n reject(new Error(`gsd-cloud did not return within ${timeoutMs}ms`));\n }, timeoutMs);\n child.once(\"error\", (error) => {\n clearTimeout(timeout);\n reject(error);\n });\n child.once(\"exit\", (code) => {\n clearTimeout(timeout);\n resolveResult({ code, stdout, stderr });\n });\n });\n}\n\ninterface ForegroundCli {\n child: ChildProcess;\n exited: Promise<number | null>;\n}\n\n// Spawn a long-lived CLI process (e.g. `--foreground`) without waiting for exit,\n// so the test can inspect its state and then stop it.\nfunction spawnForegroundCli(args: string[], cwd: string): ForegroundCli {\n const child = spawn(process.execPath, [cliPath, ...args], {\n cwd,\n env: { ...process.env, GSD_CLOUD_PROJECTS: undefined },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n const exited = new Promise<number | null>((resolveExit) => {\n child.once(\"exit\", (code) => resolveExit(code));\n });\n return { child, exited };\n}\n\nasync function createTestGateway(deviceFlow = false): Promise<TestGateway> {\n let baseUrl = \"\";\n const server = createServer((req, res) => {\n if (!deviceFlow) {\n res.writeHead(404).end();\n return;\n }\n res.setHeader(\"content-type\", \"application/json\");\n if (req.url === \"/api/device/code\") {\n res.end(JSON.stringify({\n userCode: \"TEST-CODE\",\n deviceCode: \"test-device\",\n verificationUriComplete: `${baseUrl}/approve-device?code=TEST-CODE`,\n expiresIn: 30,\n }));\n return;\n }\n if (req.url === \"/api/device/token\") {\n res.end(JSON.stringify({\n status: \"approved\",\n token: \"test\",\n runtimeId: \"fixture-runtime\",\n gateway_url: baseUrl,\n }));\n return;\n }\n res.writeHead(404).end();\n });\n const wss = new WebSocketServer({ server });\n await new Promise<void>((resolveListen, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", resolveListen);\n });\n const address = server.address();\n assert.ok(address && typeof address === \"object\");\n baseUrl = `http://127.0.0.1:${address.port}`;\n\n let helloTimeout: ReturnType<typeof setTimeout>;\n const hello = new Promise<Record<string, unknown>>((resolveHello, reject) => {\n helloTimeout = setTimeout(() => reject(new Error(\"background runtime did not advertise its project\")), 8_000);\n wss.once(\"connection\", (socket) => {\n socket.once(\"message\", (data) => {\n clearTimeout(helloTimeout);\n resolveHello(JSON.parse(data.toString(\"utf8\")) as Record<string, unknown>);\n });\n });\n });\n\n return {\n baseUrl,\n hello,\n close: async () => {\n clearTimeout(helloTimeout);\n for (const client of wss.clients) client.terminate();\n await new Promise<void>((resolveClose) => wss.close(() => server.close(() => resolveClose())));\n },\n };\n}\n\ntest(\"connect returns while a background runtime advertises the selected project\", async () => {\n const root = mkdtempSync(join(tmpdir(), \"gsd-cloud-background-\"));\n const projectDir = join(root, \"project\");\n const expectedProjectDir = join(realpathSync(root), \"project\");\n const configPath = join(root, \"daemon.yaml\");\n mkdirSync(join(projectDir, \".gsd\"), { recursive: true });\n\n const gateway = await createTestGateway();\n\n saveCloudConfig(configPath, {\n gateway_url: gateway.baseUrl,\n device_token: \"test\",\n runtime_id: \"fixture-runtime\",\n enabled: true,\n });\n\n try {\n const connect = await runCli([\"connect\", \"--config\", configPath], projectDir);\n assert.equal(connect.code, 0, connect.stderr);\n assert.match(connect.stdout, /connected in the background/i);\n\n const message = await gateway.hello;\n assert.equal(message.type, \"hello\");\n const projects = message.projects as Array<{ path: string }>;\n assert.equal(projects.length, 1);\n assert.equal(projects[0]?.path, expectedProjectDir);\n\n const status = await runCli([\"status\", \"--config\", configPath], projectDir);\n assert.equal(status.code, 0, status.stderr);\n const statusBody = JSON.parse(status.stdout) as {\n background?: { running?: boolean; pid?: number | null };\n projects?: string[];\n };\n assert.equal(statusBody.background?.running, true);\n assert.equal(typeof statusBody.background?.pid, \"number\");\n assert.deepEqual(statusBody.projects, [expectedProjectDir]);\n } finally {\n await runCli([\"disconnect\", \"--config\", configPath], projectDir).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n }\n});\n\ntest(\"login returns after approval and keeps the selected project connected\", async () => {\n const root = mkdtempSync(join(tmpdir(), \"gsd-cloud-login-background-\"));\n const projectDir = join(root, \"project\");\n const expectedProjectDir = join(realpathSync(root), \"project\");\n const configPath = join(root, \"daemon.yaml\");\n mkdirSync(join(projectDir, \".gsd\"), { recursive: true });\n const gateway = await createTestGateway(true);\n\n try {\n const login = await runCli([\n \"login\",\n \"--gateway\", gateway.baseUrl,\n \"--config\", configPath,\n ], projectDir, 12_000);\n assert.equal(login.code, 0, login.stderr);\n assert.match(login.stdout, /Authorization approved!/);\n assert.match(login.stdout, /connected in the background/i);\n\n const message = await gateway.hello;\n const projects = message.projects as Array<{ path: string }>;\n assert.deepEqual(projects.map((project) => project.path), [expectedProjectDir]);\n\n const disconnect = await runCli([\"disconnect\", \"--config\", configPath], projectDir);\n assert.equal(disconnect.code, 0, disconnect.stderr);\n const status = await runCli([\"status\", \"--config\", configPath], projectDir);\n const statusBody = JSON.parse(status.stdout) as {\n configured?: boolean;\n background?: { running?: boolean };\n };\n assert.equal(statusBody.configured, false);\n assert.equal(statusBody.background?.running, false);\n } finally {\n await runCli([\"disconnect\", \"--config\", configPath], projectDir).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n }\n});\n\ntest(\"advertised project paths are canonicalized through symlinks\", async () => {\n const root = realpathSync(mkdtempSync(join(tmpdir(), \"gsd-cloud-symlink-\")));\n const realProject = join(root, \"real-project\");\n mkdirSync(join(realProject, \".gsd\"), { recursive: true });\n const linkProject = join(root, \"link-project\");\n symlinkSync(realProject, linkProject);\n const configPath = join(root, \"daemon.yaml\");\n const gateway = await createTestGateway();\n\n saveCloudConfig(configPath, {\n gateway_url: gateway.baseUrl,\n device_token: \"test\",\n runtime_id: \"fixture-runtime\",\n enabled: true,\n });\n\n try {\n // Point the runtime at the symlink; the advertised path must be the real dir.\n const connect = await runCli(\n [\"connect\", \"--config\", configPath],\n realProject,\n 5_000,\n { GSD_CLOUD_PROJECTS: linkProject },\n );\n assert.equal(connect.code, 0, connect.stderr);\n\n const message = await gateway.hello;\n const projects = message.projects as Array<{ path: string }>;\n assert.equal(projects.length, 1);\n assert.equal(projects[0]?.path, realProject);\n\n const status = await runCli([\"status\", \"--config\", configPath], realProject);\n const statusBody = JSON.parse(status.stdout) as { projects?: string[] };\n assert.deepEqual(statusBody.projects, [realProject]);\n } finally {\n await runCli([\"disconnect\", \"--config\", configPath], realProject).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n }\n});\n\ntest(\"a foreground runtime registers its PID so disconnect can stop it\", async () => {\n const root = realpathSync(mkdtempSync(join(tmpdir(), \"gsd-cloud-foreground-\")));\n const projectDir = join(root, \"project\");\n const configPath = join(root, \"daemon.yaml\");\n mkdirSync(join(projectDir, \".gsd\"), { recursive: true });\n const gateway = await createTestGateway();\n\n saveCloudConfig(configPath, {\n gateway_url: gateway.baseUrl,\n device_token: \"test\",\n runtime_id: \"fixture-runtime\",\n enabled: true,\n });\n\n const foreground = spawnForegroundCli([\"connect\", \"--foreground\", \"--config\", configPath], projectDir);\n try {\n // Once the foreground runtime advertises its project it is fully connected.\n await gateway.hello;\n\n // The foreground session must record its own PID in the shared runtime state\n // so status can see it and a later disconnect can find and stop it.\n const status = await runCli([\"status\", \"--config\", configPath], projectDir);\n const statusBody = JSON.parse(status.stdout) as {\n background?: { running?: boolean; pid?: number | null };\n };\n assert.equal(statusBody.background?.running, true);\n assert.equal(statusBody.background?.pid, foreground.child.pid);\n\n const disconnect = await runCli([\"disconnect\", \"--config\", configPath], projectDir);\n assert.equal(disconnect.code, 0, disconnect.stderr);\n\n // disconnect must have terminated the foreground process.\n const exitCode = await foreground.exited;\n assert.equal(exitCode, 0);\n\n const afterStatus = await runCli([\"status\", \"--config\", configPath], projectDir);\n const afterBody = JSON.parse(afterStatus.stdout) as { background?: { running?: boolean } };\n assert.equal(afterBody.background?.running, false);\n } finally {\n foreground.child.kill(\"SIGKILL\");\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n }\n});\n"]}
@@ -1,27 +0,0 @@
1
- export interface RuntimeProcessStatus {
2
- running: boolean;
3
- pid: number | null;
4
- projects: string[];
5
- log_file: string;
6
- }
7
- interface StartRuntimeOptions {
8
- binaryPath: string;
9
- configPath: string;
10
- projectDirs: string[];
11
- readyTimeoutMs?: number;
12
- verbose?: boolean;
13
- }
14
- export declare const BACKGROUND_RUNTIME_READY_TIMEOUT_MS: number;
15
- export declare function startBackgroundRuntime(opts: StartRuntimeOptions): Promise<RuntimeProcessStatus>;
16
- /**
17
- * Record a running runtime so a later launch can find and stop it. Used both by
18
- * the detached launcher (child PID) and by a `--foreground` session (its own
19
- * PID), so the two modes share one source of truth and never coexist on the
20
- * same device token.
21
- */
22
- export declare function writeRuntimeState(configPath: string, pid: number, projects: string[]): void;
23
- export declare function clearRuntimeState(configPath: string): void;
24
- export declare function stopBackgroundRuntime(configPath: string): Promise<boolean>;
25
- export declare function backgroundRuntimeStatus(configPath: string): RuntimeProcessStatus;
26
- export {};
27
- //# sourceMappingURL=runtime-process.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime-process.d.ts","sourceRoot":"","sources":["../src/runtime-process.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,mBAAmB;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAOD,eAAO,MAAM,mCAAmC,QACoB,CAAC;AAErE,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAoDrG;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAI3F;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAE1D;AAED,wBAAsB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMhF;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,CAoBhF"}