@opengsd/gsd-pi 1.9.0-dev.e10b60d4 → 1.10.0-dev.a5e42e99

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 (251) hide show
  1. package/README.md +10 -9
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +24 -4
  4. package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -0
  6. package/dist/resources/extensions/gsd/auto-start.js +30 -10
  7. package/dist/resources/extensions/gsd/auto-verification.js +25 -12
  8. package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
  9. package/dist/resources/extensions/gsd/auto.js +12 -1
  10. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
  11. package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  12. package/dist/resources/extensions/gsd/doctor-engine-checks.js +37 -6
  13. package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
  14. package/dist/resources/extensions/gsd/gsd-db.js +75 -31
  15. package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
  16. package/dist/resources/extensions/gsd/model-router.js +10 -5
  17. package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
  18. package/dist/resources/extensions/gsd/preferences-models.js +50 -10
  19. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  20. package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
  21. package/dist/resources/extensions/gsd/preferences.js +7 -0
  22. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  23. package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
  24. package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
  25. package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
  26. package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
  27. package/dist/resources/extensions/subagent/index.js +1 -1
  28. package/dist/startup-model-validation.d.ts +1 -1
  29. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  30. package/dist/web/standalone/.next/BUILD_ID +1 -1
  31. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  32. package/dist/web/standalone/.next/build-manifest.json +2 -2
  33. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  34. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.html +1 -1
  51. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  58. package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
  59. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  60. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  61. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  63. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  64. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  65. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  66. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  67. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  68. package/integrations/hermes/pyproject.toml +1 -1
  69. package/package.json +7 -6
  70. package/packages/cloud-mcp-gateway/package.json +2 -2
  71. package/packages/contracts/dist/rpc.d.ts +1 -1
  72. package/packages/contracts/dist/rpc.d.ts.map +1 -1
  73. package/packages/contracts/dist/rpc.js +1 -1
  74. package/packages/contracts/dist/rpc.js.map +1 -1
  75. package/packages/contracts/dist/rpc.test.js +1 -1
  76. package/packages/contracts/dist/rpc.test.js.map +1 -1
  77. package/packages/contracts/package.json +1 -1
  78. package/packages/daemon/package.json +4 -4
  79. package/packages/gsd-agent-core/package.json +5 -5
  80. package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
  81. package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
  82. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  83. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
  84. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  85. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  86. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
  87. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  88. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
  89. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
  90. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  91. package/packages/gsd-agent-modes/package.json +7 -7
  92. package/packages/gsd-cloud/README.md +21 -5
  93. package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
  94. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
  95. package/packages/gsd-cloud/dist/background-cli.test.js +325 -0
  96. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
  97. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  98. package/packages/gsd-cloud/dist/cli.js +146 -16
  99. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  100. package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
  101. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
  102. package/packages/gsd-cloud/dist/cloud-config.js +7 -1
  103. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
  104. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +7 -4
  105. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  106. package/packages/gsd-cloud/dist/cloud-runtime.js +115 -31
  107. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  108. package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
  109. package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
  110. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
  111. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
  112. package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
  113. package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
  114. package/packages/gsd-cloud/dist/runtime-process.d.ts +32 -0
  115. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
  116. package/packages/gsd-cloud/dist/runtime-process.js +640 -0
  117. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
  118. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
  119. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
  120. package/packages/gsd-cloud/dist/runtime-process.test.js +543 -0
  121. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
  122. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
  123. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
  124. package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
  125. package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
  126. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
  127. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
  128. package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
  129. package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
  130. package/packages/gsd-cloud/package.json +2 -2
  131. package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
  132. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
  133. package/packages/mcp-server/dist/cli-runner.js +119 -33
  134. package/packages/mcp-server/dist/cli-runner.js.map +1 -1
  135. package/packages/mcp-server/package.json +4 -4
  136. package/packages/native/package.json +1 -1
  137. package/packages/pi-agent-core/dist/types.d.ts +2 -2
  138. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  139. package/packages/pi-agent-core/dist/types.js.map +1 -1
  140. package/packages/pi-agent-core/package.json +1 -1
  141. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  142. package/packages/pi-ai/dist/models.generated.d.ts +130 -2
  143. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/models.generated.js +101 -9
  145. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  146. package/packages/pi-ai/dist/models.js +18 -6
  147. package/packages/pi-ai/dist/models.js.map +1 -1
  148. package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
  149. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  150. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
  151. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  152. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  153. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  154. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  155. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
  156. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  157. package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
  158. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  159. package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
  160. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  161. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  162. package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
  163. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  164. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  165. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  166. package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
  167. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  168. package/packages/pi-ai/dist/providers/simple-options.js +1 -1
  169. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  170. package/packages/pi-ai/dist/types.d.ts +9 -1
  171. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  172. package/packages/pi-ai/dist/types.js.map +1 -1
  173. package/packages/pi-ai/package.json +1 -1
  174. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  175. package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
  176. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
  178. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  179. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
  180. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  181. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/skills.d.ts +5 -0
  183. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/skills.js +54 -0
  185. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
  187. package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
  188. package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
  189. package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
  190. package/packages/pi-coding-agent/package.json +7 -7
  191. package/packages/pi-tui/package.json +2 -2
  192. package/packages/rpc-client/package.json +2 -2
  193. package/pkg/dist/theme/theme.d.ts +1 -1
  194. package/pkg/dist/theme/theme.d.ts.map +1 -1
  195. package/pkg/dist/theme/theme.js +1 -0
  196. package/pkg/dist/theme/theme.js.map +1 -1
  197. package/pkg/package.json +1 -1
  198. package/src/resources/extensions/gsd/auto/loop.ts +48 -4
  199. package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
  200. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
  201. package/src/resources/extensions/gsd/auto-start.ts +32 -9
  202. package/src/resources/extensions/gsd/auto-verification.ts +46 -12
  203. package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
  204. package/src/resources/extensions/gsd/auto.ts +13 -1
  205. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
  206. package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  207. package/src/resources/extensions/gsd/doctor-engine-checks.ts +42 -6
  208. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
  209. package/src/resources/extensions/gsd/gsd-db.ts +85 -33
  210. package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
  211. package/src/resources/extensions/gsd/model-router.ts +10 -5
  212. package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
  213. package/src/resources/extensions/gsd/preferences-models.ts +54 -10
  214. package/src/resources/extensions/gsd/preferences-types.ts +10 -2
  215. package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
  216. package/src/resources/extensions/gsd/preferences.ts +13 -0
  217. package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  218. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +156 -0
  219. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
  220. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
  221. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +147 -0
  222. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
  223. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
  224. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
  225. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
  226. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
  227. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  228. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
  229. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
  230. package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
  231. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
  232. package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
  233. package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
  234. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
  235. package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
  236. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
  237. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
  238. package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
  239. package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
  240. package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
  241. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  242. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
  243. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
  244. package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
  245. package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
  246. package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
  247. package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
  248. package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
  249. package/src/resources/extensions/subagent/index.ts +1 -1
  250. /package/dist/web/standalone/.next/static/{V9MAVyYVPboQUB9KTTAMi → xwUaOqui04jIIFVsIBLiL}/_buildManifest.js +0 -0
  251. /package/dist/web/standalone/.next/static/{V9MAVyYVPboQUB9KTTAMi → xwUaOqui04jIIFVsIBLiL}/_ssgManifest.js +0 -0
@@ -0,0 +1,325 @@
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 { existsSync, mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } 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
+ import { runtimeArtifactPath } from "./runtime-artifacts.js";
14
+ import { startBackgroundRuntime, stopBackgroundRuntime } from "./runtime-process.js";
15
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
+ const cliPath = join(packageRoot, "bin", "gsd-cloud.js");
17
+ function runCli(args, cwd, timeoutMs = 12_000, extraEnv = {}) {
18
+ return new Promise((resolveResult, reject) => {
19
+ const child = spawn(process.execPath, [cliPath, ...args], {
20
+ cwd,
21
+ env: { ...process.env, GSD_CLOUD_PROJECTS: undefined, ...extraEnv },
22
+ stdio: ["ignore", "pipe", "pipe"],
23
+ });
24
+ let stdout = "";
25
+ let stderr = "";
26
+ child.stdout.setEncoding("utf8");
27
+ child.stderr.setEncoding("utf8");
28
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
29
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
30
+ const timeout = setTimeout(() => {
31
+ child.kill("SIGTERM");
32
+ reject(new Error(`gsd-cloud did not return within ${timeoutMs}ms`));
33
+ }, timeoutMs);
34
+ child.once("error", (error) => {
35
+ clearTimeout(timeout);
36
+ reject(error);
37
+ });
38
+ child.once("exit", (code) => {
39
+ clearTimeout(timeout);
40
+ resolveResult({ code, stdout, stderr });
41
+ });
42
+ });
43
+ }
44
+ // Spawn a long-lived CLI process (e.g. `--foreground`) without waiting for exit,
45
+ // so the test can inspect its state and then stop it.
46
+ function spawnForegroundCli(args, cwd) {
47
+ const child = spawn(process.execPath, [cliPath, ...args], {
48
+ cwd,
49
+ env: { ...process.env, GSD_CLOUD_PROJECTS: undefined },
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ });
52
+ const exited = new Promise((resolveExit) => {
53
+ child.once("exit", (code) => resolveExit(code));
54
+ });
55
+ return { child, exited };
56
+ }
57
+ async function createTestGateway(deviceFlow = false) {
58
+ let baseUrl = "";
59
+ const server = createServer((req, res) => {
60
+ if (!deviceFlow) {
61
+ res.writeHead(404).end();
62
+ return;
63
+ }
64
+ res.setHeader("content-type", "application/json");
65
+ if (req.url === "/api/device/code") {
66
+ res.end(JSON.stringify({
67
+ userCode: "TEST-CODE",
68
+ deviceCode: "test-device",
69
+ verificationUriComplete: `${baseUrl}/approve-device?code=TEST-CODE`,
70
+ expiresIn: 30,
71
+ }));
72
+ return;
73
+ }
74
+ if (req.url === "/api/device/token") {
75
+ res.end(JSON.stringify({
76
+ status: "approved",
77
+ token: "test",
78
+ runtimeId: "fixture-runtime",
79
+ gateway_url: baseUrl,
80
+ }));
81
+ return;
82
+ }
83
+ res.writeHead(404).end();
84
+ });
85
+ const wss = new WebSocketServer({ server });
86
+ await new Promise((resolveListen, reject) => {
87
+ server.once("error", reject);
88
+ server.listen(0, "127.0.0.1", resolveListen);
89
+ });
90
+ const address = server.address();
91
+ assert.ok(address && typeof address === "object");
92
+ baseUrl = `http://127.0.0.1:${address.port}`;
93
+ let helloTimeout;
94
+ const hello = new Promise((resolveHello, reject) => {
95
+ helloTimeout = setTimeout(() => reject(new Error("background runtime did not advertise its project")), 8_000);
96
+ wss.once("connection", (socket) => {
97
+ socket.once("message", (data) => {
98
+ clearTimeout(helloTimeout);
99
+ resolveHello(JSON.parse(data.toString("utf8")));
100
+ });
101
+ });
102
+ });
103
+ return {
104
+ baseUrl,
105
+ hello,
106
+ close: async () => {
107
+ clearTimeout(helloTimeout);
108
+ for (const client of wss.clients)
109
+ client.terminate();
110
+ await new Promise((resolveClose) => wss.close(() => server.close(() => resolveClose())));
111
+ },
112
+ };
113
+ }
114
+ test("connect returns while a background runtime advertises the selected project", async (t) => {
115
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-background-"));
116
+ const projectDir = join(root, "project");
117
+ const expectedProjectDir = join(realpathSync(root), "project");
118
+ const configPath = join(root, "daemon.yaml");
119
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
120
+ const gateway = await createTestGateway();
121
+ t.after(async () => {
122
+ await runCli(["disconnect", "--config", configPath], projectDir).catch(() => undefined);
123
+ await gateway.close();
124
+ rmSync(root, { recursive: true, force: true });
125
+ });
126
+ saveCloudConfig(configPath, {
127
+ gateway_url: gateway.baseUrl,
128
+ device_token: "test",
129
+ runtime_id: "fixture-runtime",
130
+ enabled: true,
131
+ });
132
+ const connect = await runCli(["connect", "--config", configPath], projectDir);
133
+ assert.equal(connect.code, 0, connect.stderr);
134
+ assert.match(connect.stdout, /connected in the background/i);
135
+ const message = await gateway.hello;
136
+ assert.equal(message.type, "hello");
137
+ const projects = message.projects;
138
+ assert.equal(projects.length, 1);
139
+ assert.equal(projects[0]?.path, expectedProjectDir);
140
+ const status = await runCli(["status", "--config", configPath], projectDir);
141
+ assert.equal(status.code, 0, status.stderr);
142
+ const statusBody = JSON.parse(status.stdout);
143
+ assert.equal(statusBody.background?.running, true);
144
+ assert.equal(typeof statusBody.background?.pid, "number");
145
+ assert.deepEqual(statusBody.projects, [expectedProjectDir]);
146
+ });
147
+ test("stop terminates the background runtime without removing pairing", async (t) => {
148
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-stop-command-"));
149
+ const projectDir = join(root, "project");
150
+ const configPath = join(root, "daemon.yaml");
151
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
152
+ const gateway = await createTestGateway();
153
+ t.after(async () => {
154
+ await runCli(["disconnect", "--config", configPath], projectDir).catch(() => undefined);
155
+ await gateway.close();
156
+ rmSync(root, { recursive: true, force: true });
157
+ });
158
+ saveCloudConfig(configPath, {
159
+ gateway_url: gateway.baseUrl,
160
+ device_token: "test",
161
+ runtime_id: "fixture-runtime",
162
+ enabled: true,
163
+ });
164
+ const connect = await runCli(["connect", "--config", configPath], projectDir);
165
+ assert.equal(connect.code, 0, connect.stderr);
166
+ await gateway.hello;
167
+ const stop = await runCli(["stop", "--config", configPath], projectDir);
168
+ assert.equal(stop.code, 0, stop.stderr);
169
+ assert.match(stop.stdout, /background runtime stopped/i);
170
+ const status = await runCli(["status", "--config", configPath], projectDir);
171
+ const body = JSON.parse(status.stdout);
172
+ assert.equal(body.configured, true);
173
+ assert.equal(body.runtime_id, "fixture-runtime");
174
+ assert.equal(body.background?.running, false);
175
+ assert.notEqual(body.telemetry, null);
176
+ });
177
+ test("login returns after approval and keeps the selected project connected", async (t) => {
178
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-login-background-"));
179
+ const projectDir = join(root, "project");
180
+ const expectedProjectDir = join(realpathSync(root), "project");
181
+ const configPath = join(root, "daemon.yaml");
182
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
183
+ const gateway = await createTestGateway(true);
184
+ t.after(async () => {
185
+ await runCli(["disconnect", "--config", configPath], projectDir).catch(() => undefined);
186
+ await gateway.close();
187
+ rmSync(root, { recursive: true, force: true });
188
+ });
189
+ const login = await runCli([
190
+ "login",
191
+ "--gateway", gateway.baseUrl,
192
+ "--config", configPath,
193
+ ], projectDir, 12_000);
194
+ assert.equal(login.code, 0, login.stderr);
195
+ assert.match(login.stdout, /Authorization approved!/);
196
+ assert.match(login.stdout, /connected in the background/i);
197
+ const message = await gateway.hello;
198
+ const projects = message.projects;
199
+ assert.deepEqual(projects.map((project) => project.path), [expectedProjectDir]);
200
+ const disconnect = await runCli(["disconnect", "--config", configPath], projectDir);
201
+ assert.equal(disconnect.code, 0, disconnect.stderr);
202
+ const status = await runCli(["status", "--config", configPath], projectDir);
203
+ const statusBody = JSON.parse(status.stdout);
204
+ assert.equal(statusBody.configured, false);
205
+ assert.equal(statusBody.background?.running, false);
206
+ assert.equal(statusBody.telemetry, null);
207
+ });
208
+ test("advertised project paths are canonicalized through symlinks", async (t) => {
209
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cloud-symlink-")));
210
+ const realProject = join(root, "real-project");
211
+ mkdirSync(join(realProject, ".gsd"), { recursive: true });
212
+ const linkProject = join(root, "link-project");
213
+ symlinkSync(realProject, linkProject);
214
+ const configPath = join(root, "daemon.yaml");
215
+ const gateway = await createTestGateway();
216
+ t.after(async () => {
217
+ await runCli(["disconnect", "--config", configPath], realProject).catch(() => undefined);
218
+ await gateway.close();
219
+ rmSync(root, { recursive: true, force: true });
220
+ });
221
+ saveCloudConfig(configPath, {
222
+ gateway_url: gateway.baseUrl,
223
+ device_token: "test",
224
+ runtime_id: "fixture-runtime",
225
+ enabled: true,
226
+ });
227
+ // Point the runtime at the symlink; the advertised path must be the real dir.
228
+ const connect = await runCli(["connect", "--config", configPath], realProject, 5_000, { GSD_CLOUD_PROJECTS: linkProject });
229
+ assert.equal(connect.code, 0, connect.stderr);
230
+ const message = await gateway.hello;
231
+ const projects = message.projects;
232
+ assert.equal(projects.length, 1);
233
+ assert.equal(projects[0]?.path, realProject);
234
+ const status = await runCli(["status", "--config", configPath], realProject);
235
+ const statusBody = JSON.parse(status.stdout);
236
+ assert.deepEqual(statusBody.projects, [realProject]);
237
+ });
238
+ test("a foreground runtime registers its PID so disconnect can stop it", async (t) => {
239
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cloud-foreground-")));
240
+ const projectDir = join(root, "project");
241
+ const configPath = join(root, "daemon.yaml");
242
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
243
+ const gateway = await createTestGateway();
244
+ saveCloudConfig(configPath, {
245
+ gateway_url: gateway.baseUrl,
246
+ device_token: "test",
247
+ runtime_id: "fixture-runtime",
248
+ enabled: true,
249
+ });
250
+ const foreground = spawnForegroundCli(["connect", "--foreground", "--config", configPath], projectDir);
251
+ t.after(async () => {
252
+ foreground.child.kill("SIGKILL");
253
+ await gateway.close();
254
+ rmSync(root, { recursive: true, force: true });
255
+ });
256
+ // Once the foreground runtime advertises its project it is fully connected.
257
+ await gateway.hello;
258
+ // The foreground session must record its own PID in the shared runtime state
259
+ // so status can see it and a later disconnect can find and stop it.
260
+ const status = await runCli(["status", "--config", configPath], projectDir);
261
+ const statusBody = JSON.parse(status.stdout);
262
+ assert.equal(statusBody.background?.running, true);
263
+ assert.equal(statusBody.background?.pid, foreground.child.pid);
264
+ const disconnect = await runCli(["disconnect", "--config", configPath], projectDir);
265
+ assert.equal(disconnect.code, 0, disconnect.stderr);
266
+ // disconnect must have terminated the foreground process.
267
+ const exitCode = await foreground.exited;
268
+ assert.equal(exitCode, 0);
269
+ const afterStatus = await runCli(["status", "--config", configPath], projectDir);
270
+ const afterBody = JSON.parse(afterStatus.stdout);
271
+ assert.equal(afterBody.background?.running, false);
272
+ });
273
+ test("foreground connect waits for an in-progress background start", { timeout: 10_000 }, async (t) => {
274
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cloud-foreground-lock-")));
275
+ const projectDir = join(root, "project");
276
+ const configPath = join(root, "daemon.yaml");
277
+ const delayedRuntimePath = join(root, "delayed-runtime.mjs");
278
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
279
+ writeFileSync(delayedRuntimePath, [
280
+ 'process.on("SIGTERM", () => process.exit(0));',
281
+ 'setTimeout(() => process.send?.({ type: "ready" }), 3_000);',
282
+ 'setInterval(() => undefined, 1_000);',
283
+ ].join("\n"));
284
+ const gateway = await createTestGateway();
285
+ saveCloudConfig(configPath, {
286
+ gateway_url: gateway.baseUrl,
287
+ device_token: "test",
288
+ runtime_id: "fixture-runtime",
289
+ enabled: true,
290
+ });
291
+ const backgroundStart = startBackgroundRuntime({
292
+ binaryPath: delayedRuntimePath,
293
+ configPath,
294
+ projectDirs: [projectDir],
295
+ });
296
+ await waitForCondition(() => existsSync(runtimeArtifactPath(configPath, "start.lock")));
297
+ await waitForCondition(() => existsSync(runtimeArtifactPath(configPath, "state")));
298
+ const foreground = spawnForegroundCli(["connect", "--foreground", "--config", configPath], projectDir);
299
+ t.after(async () => {
300
+ foreground.child.kill("SIGKILL");
301
+ await stopBackgroundRuntime(configPath).catch(() => undefined);
302
+ await gateway.close();
303
+ rmSync(root, { recursive: true, force: true });
304
+ });
305
+ const connectedBeforeBackgroundStartFinished = await Promise.race([
306
+ gateway.hello.then(() => true),
307
+ new Promise((resolveDelay) => setTimeout(() => resolveDelay(false), 500)),
308
+ ]);
309
+ assert.equal(connectedBeforeBackgroundStartFinished, false);
310
+ const background = await backgroundStart;
311
+ assert.equal(background.running, true);
312
+ await gateway.hello;
313
+ const disconnect = await runCli(["disconnect", "--config", configPath], projectDir);
314
+ assert.equal(disconnect.code, 0, disconnect.stderr);
315
+ assert.equal(await foreground.exited, 0);
316
+ });
317
+ async function waitForCondition(condition, timeoutMs = 2_000) {
318
+ const deadline = Date.now() + timeoutMs;
319
+ while (!condition()) {
320
+ if (Date.now() >= deadline)
321
+ throw new Error("timed out waiting for test condition");
322
+ await new Promise((resolve) => setTimeout(resolve, 25));
323
+ }
324
+ }
325
+ //# sourceMappingURL=background-cli.test.js.map
@@ -0,0 +1 @@
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 { existsSync, mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } 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\";\nimport { runtimeArtifactPath } from \"./runtime-artifacts.js\";\nimport { startBackgroundRuntime, stopBackgroundRuntime } from \"./runtime-process.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 (t) => {\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 t.after(async () => {\n await runCli([\"disconnect\", \"--config\", configPath], projectDir).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n });\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 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});\n\ntest(\"stop terminates the background runtime without removing pairing\", async (t) => {\n const root = mkdtempSync(join(tmpdir(), \"gsd-cloud-stop-command-\"));\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 t.after(async () => {\n await runCli([\"disconnect\", \"--config\", configPath], projectDir).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n });\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 connect = await runCli([\"connect\", \"--config\", configPath], projectDir);\n assert.equal(connect.code, 0, connect.stderr);\n await gateway.hello;\n\n const stop = await runCli([\"stop\", \"--config\", configPath], projectDir);\n assert.equal(stop.code, 0, stop.stderr);\n assert.match(stop.stdout, /background runtime stopped/i);\n\n const status = await runCli([\"status\", \"--config\", configPath], projectDir);\n const body = JSON.parse(status.stdout) as {\n configured?: boolean;\n runtime_id?: string;\n background?: { running?: boolean };\n telemetry?: unknown;\n };\n assert.equal(body.configured, true);\n assert.equal(body.runtime_id, \"fixture-runtime\");\n assert.equal(body.background?.running, false);\n assert.notEqual(body.telemetry, null);\n});\n\ntest(\"login returns after approval and keeps the selected project connected\", async (t) => {\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 t.after(async () => {\n await runCli([\"disconnect\", \"--config\", configPath], projectDir).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n });\n\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 telemetry?: unknown;\n };\n assert.equal(statusBody.configured, false);\n assert.equal(statusBody.background?.running, false);\n assert.equal(statusBody.telemetry, null);\n});\n\ntest(\"advertised project paths are canonicalized through symlinks\", async (t) => {\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 t.after(async () => {\n await runCli([\"disconnect\", \"--config\", configPath], realProject).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n });\n\n saveCloudConfig(configPath, {\n gateway_url: gateway.baseUrl,\n device_token: \"test\",\n runtime_id: \"fixture-runtime\",\n enabled: true,\n });\n\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});\n\ntest(\"a foreground runtime registers its PID so disconnect can stop it\", async (t) => {\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 t.after(async () => {\n foreground.child.kill(\"SIGKILL\");\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n });\n\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});\n\ntest(\"foreground connect waits for an in-progress background start\", { timeout: 10_000 }, async (t) => {\n const root = realpathSync(mkdtempSync(join(tmpdir(), \"gsd-cloud-foreground-lock-\")));\n const projectDir = join(root, \"project\");\n const configPath = join(root, \"daemon.yaml\");\n const delayedRuntimePath = join(root, \"delayed-runtime.mjs\");\n mkdirSync(join(projectDir, \".gsd\"), { recursive: true });\n writeFileSync(delayedRuntimePath, [\n 'process.on(\"SIGTERM\", () => process.exit(0));',\n 'setTimeout(() => process.send?.({ type: \"ready\" }), 3_000);',\n 'setInterval(() => undefined, 1_000);',\n ].join(\"\\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 const backgroundStart = startBackgroundRuntime({\n binaryPath: delayedRuntimePath,\n configPath,\n projectDirs: [projectDir],\n });\n await waitForCondition(() => existsSync(runtimeArtifactPath(configPath, \"start.lock\")));\n await waitForCondition(() => existsSync(runtimeArtifactPath(configPath, \"state\")));\n const foreground = spawnForegroundCli([\"connect\", \"--foreground\", \"--config\", configPath], projectDir);\n t.after(async () => {\n foreground.child.kill(\"SIGKILL\");\n await stopBackgroundRuntime(configPath).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n });\n\n const connectedBeforeBackgroundStartFinished = await Promise.race([\n gateway.hello.then(() => true),\n new Promise<false>((resolveDelay) => setTimeout(() => resolveDelay(false), 500)),\n ]);\n assert.equal(connectedBeforeBackgroundStartFinished, false);\n const background = await backgroundStart;\n assert.equal(background.running, true);\n await gateway.hello;\n\n const disconnect = await runCli([\"disconnect\", \"--config\", configPath], projectDir);\n assert.equal(disconnect.code, 0, disconnect.stderr);\n assert.equal(await foreground.exited, 0);\n});\n\nasync function waitForCondition(condition: () => boolean, timeoutMs = 2_000): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n while (!condition()) {\n if (Date.now() >= deadline) throw new Error(\"timed out waiting for test condition\");\n await new Promise((resolve) => setTimeout(resolve, 25));\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAqBA,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4FhB;AAkCD,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA8BtD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAgCA,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsIhB;AAmID,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAiCtD"}
@@ -5,12 +5,16 @@
5
5
  // no dependency on any Daemon or Orchestrator class; the only cloud behaviour is
6
6
  // the WS relay client driving the local GSD runtime through the Executor seam.
7
7
  import { parseArgs } from "node:util";
8
+ import { realpathSync } from "node:fs";
9
+ import { delimiter, resolve } from "node:path";
8
10
  import { resolveConfigPath, loadConfig } from "./config.js";
9
11
  import { Logger } from "./logger.js";
10
12
  import { clearCloudConfig, exchangePairingCode, redactedCloudStatus, saveCloudConfig, } from "./cloud-config.js";
11
13
  import { runDeviceFlow } from "./device-flow.js";
12
14
  import { CloudRuntime } from "./cloud-runtime.js";
13
15
  import { selectExecutor } from "./executors/index.js";
16
+ import { acquireRuntimeStartLock, backgroundRuntimeStatus, clearRuntimeState, startBackgroundRuntime, stopBackgroundRuntime, writeRuntimeState, } from "./runtime-process.js";
17
+ import { clearRuntimeTelemetry, readRuntimeTelemetry, RuntimeTelemetryStore } from "./runtime-telemetry.js";
14
18
  export async function handleCloudCommand(argv, opts) {
15
19
  if (argv[0] === "--help" || argv[0] === "-h") {
16
20
  process.stdout.write(formatUsage(opts.binaryName));
@@ -25,6 +29,7 @@ export async function handleCloudCommand(argv, opts) {
25
29
  code: { type: "string" },
26
30
  "runtime-name": { type: "string" },
27
31
  verbose: { type: "boolean", short: "v", default: false },
32
+ foreground: { type: "boolean", default: false },
28
33
  help: { type: "boolean", short: "h", default: false },
29
34
  },
30
35
  strict: true,
@@ -35,12 +40,27 @@ export async function handleCloudCommand(argv, opts) {
35
40
  }
36
41
  const configPath = resolveConfigPath(values.config);
37
42
  if (command === "status") {
38
- process.stdout.write(`${JSON.stringify(redactedCloudStatus(loadConfig(configPath)), null, 2)}\n`);
43
+ const config = loadConfig(configPath);
44
+ process.stdout.write(`${JSON.stringify({
45
+ ...redactedCloudStatus(config),
46
+ projects: config.projects.scan_roots,
47
+ background: backgroundRuntimeStatus(configPath),
48
+ telemetry: readRuntimeTelemetry(configPath),
49
+ }, null, 2)}\n`);
39
50
  return;
40
51
  }
41
52
  if (command === "disconnect") {
53
+ await stopBackgroundRuntime(configPath);
42
54
  clearCloudConfig(configPath);
43
- process.stdout.write(`${opts.binaryName}: cloud runtime disconnected locally.\n`);
55
+ clearRuntimeTelemetry(configPath);
56
+ process.stdout.write(`${opts.binaryName}: background runtime stopped and cloud credentials removed.\n`);
57
+ return;
58
+ }
59
+ if (command === "stop") {
60
+ const stopped = await stopBackgroundRuntime(configPath);
61
+ process.stdout.write(stopped
62
+ ? `${opts.binaryName}: background runtime stopped.\n`
63
+ : `${opts.binaryName}: background runtime is not running.\n`);
44
64
  return;
45
65
  }
46
66
  if (command === "login") {
@@ -54,15 +74,20 @@ export async function handleCloudCommand(argv, opts) {
54
74
  runtimeName,
55
75
  binaryName: opts.binaryName,
56
76
  });
77
+ const projectDirs = selectedProjectDirs([]);
57
78
  const config = saveCloudConfig(configPath, {
58
79
  gateway_url: gatewayUrl,
59
80
  device_token: deviceToken,
60
81
  runtime_id: runtimeId,
61
82
  ...(runtimeName ? { runtime_name: runtimeName } : {}),
62
83
  enabled: true,
63
- });
84
+ }, projectDirs);
64
85
  process.stdout.write(`${opts.binaryName}: cloud runtime ${runtimeId} paired — connecting...\n`);
65
- await runCloudRuntime(config, opts.binaryName, values.verbose);
86
+ if (values.foreground) {
87
+ await runForegroundCloudRuntime(config, configPath, opts.binaryName, values.verbose, projectDirs);
88
+ return;
89
+ }
90
+ await startAndReportBackgroundRuntime(configPath, projectDirs, opts.binaryName, values.verbose);
66
91
  return;
67
92
  }
68
93
  if (command === "pair") {
@@ -86,21 +111,53 @@ export async function handleCloudCommand(argv, opts) {
86
111
  return;
87
112
  }
88
113
  if (command === "connect") {
114
+ let config = loadConfig(configPath);
115
+ if (!config.cloud?.device_token || !config.cloud.runtime_id) {
116
+ throw new Error("cloud runtime is not paired; run `login` first");
117
+ }
118
+ const projectDirs = selectedProjectDirs(config.projects.scan_roots);
119
+ config = saveCloudConfig(configPath, config.cloud, projectDirs);
120
+ if (values.foreground) {
121
+ await runForegroundCloudRuntime(config, configPath, opts.binaryName, values.verbose, projectDirs);
122
+ return;
123
+ }
124
+ await startAndReportBackgroundRuntime(configPath, projectDirs, opts.binaryName, values.verbose);
125
+ return;
126
+ }
127
+ if (command === "_run") {
89
128
  const config = loadConfig(configPath);
90
129
  if (!config.cloud?.device_token || !config.cloud.runtime_id) {
91
130
  throw new Error("cloud runtime is not paired; run `login` first");
92
131
  }
93
- await runCloudRuntime(config, opts.binaryName, values.verbose);
132
+ const projectDirs = selectedProjectDirs(config.projects.scan_roots);
133
+ await runCloudRuntime(config, configPath, opts.binaryName, values.verbose, projectDirs, {
134
+ onConnected: () => {
135
+ process.send?.({ type: "ready" });
136
+ },
137
+ });
94
138
  return;
95
139
  }
96
140
  throw new Error(`Unknown cloud runtime command: ${command}`);
97
141
  }
142
+ async function runForegroundCloudRuntime(config, configPath, binaryName, verbose, projectDirs) {
143
+ const releaseStartLock = await acquireRuntimeStartLock(configPath);
144
+ try {
145
+ await stopBackgroundRuntime(configPath);
146
+ await runCloudRuntime(config, configPath, binaryName, verbose, projectDirs, {
147
+ registerConfigPath: configPath,
148
+ onConnected: releaseStartLock,
149
+ });
150
+ }
151
+ finally {
152
+ releaseStartLock();
153
+ }
154
+ }
98
155
  /**
99
156
  * Start the WS relay and block until the process is signalled. This is the whole
100
157
  * "daemon" for the standalone agent: one CloudRuntime + one Executor, no Discord,
101
158
  * no scanner, no session-manager class.
102
159
  */
103
- async function runCloudRuntime(config, binaryName, verbose) {
160
+ async function runCloudRuntime(config, configPath, binaryName, verbose, projectDirs, opts = {}) {
104
161
  if (!config.cloud)
105
162
  throw new Error("cloud runtime is not configured");
106
163
  if (config.cloud.enabled === false) {
@@ -111,12 +168,34 @@ async function runCloudRuntime(config, binaryName, verbose) {
111
168
  level: config.log.level,
112
169
  verbose,
113
170
  });
114
- const executor = selectExecutor(logger);
115
- const runtime = new CloudRuntime(config.cloud, executor, logger);
116
- await runtime.start();
171
+ const executor = selectExecutor(logger, { projectDirs });
172
+ const telemetry = new RuntimeTelemetryStore(configPath, {
173
+ gatewayUrl: config.cloud.gateway_url,
174
+ runtimeId: config.cloud.runtime_id,
175
+ runtimeName: config.cloud.runtime_name,
176
+ });
177
+ const runtime = new CloudRuntime(config.cloud, executor, logger, telemetry);
178
+ // A foreground session owns the runtime itself, so it records its own PID in
179
+ // the shared state file. That lets a later `connect`/`disconnect` find and
180
+ // stop it, just like a detached background runtime. Background `_run` children
181
+ // do not register here; their launcher records the child PID instead.
182
+ if (opts.registerConfigPath) {
183
+ writeRuntimeState(opts.registerConfigPath, process.pid, projectDirs);
184
+ }
185
+ try {
186
+ await runtime.start();
187
+ }
188
+ catch (error) {
189
+ if (opts.registerConfigPath)
190
+ clearRuntimeState(opts.registerConfigPath);
191
+ throw error;
192
+ }
193
+ opts.onConnected?.();
117
194
  process.stdout.write(`${binaryName}: connected to ${config.cloud.gateway_url}. Press Ctrl+C to stop.\n`);
118
195
  await new Promise((resolve) => {
119
196
  const shutdown = () => {
197
+ if (opts.registerConfigPath)
198
+ clearRuntimeState(opts.registerConfigPath);
120
199
  runtime.stop();
121
200
  void Promise.resolve(executor.close?.()).finally(() => {
122
201
  void logger.close().finally(() => resolve());
@@ -126,21 +205,71 @@ async function runCloudRuntime(config, binaryName, verbose) {
126
205
  process.once("SIGTERM", shutdown);
127
206
  });
128
207
  }
208
+ async function startAndReportBackgroundRuntime(configPath, projectDirs, binaryName, verbose = false) {
209
+ const binaryPath = process.argv[1];
210
+ if (!binaryPath)
211
+ throw new Error("could not resolve the gsd-cloud executable path");
212
+ const status = await startBackgroundRuntime({ binaryPath, configPath, projectDirs, verbose });
213
+ process.stdout.write(`${binaryName}: connected in the background (PID ${status.pid}).\n`);
214
+ for (const project of projectDirs)
215
+ process.stdout.write(`${binaryName}: project ${project}\n`);
216
+ process.stdout.write(`${binaryName}: logs ${status.log_file}\n`);
217
+ }
218
+ function selectedProjectDirs(savedProjectDirs) {
219
+ const configured = process.env["GSD_CLOUD_PROJECTS"];
220
+ if (configured?.trim()) {
221
+ const configuredPaths = uniqueResolvedPaths(configured.split(delimiter));
222
+ if (configuredPaths.length > 0)
223
+ return configuredPaths;
224
+ }
225
+ if (savedProjectDirs.length > 0) {
226
+ const savedPaths = uniqueResolvedPaths(savedProjectDirs);
227
+ if (savedPaths.length > 0)
228
+ return savedPaths;
229
+ }
230
+ return [canonicalizePath(process.cwd())];
231
+ }
232
+ function uniqueResolvedPaths(paths) {
233
+ const resolved = paths
234
+ .map((path) => path.trim())
235
+ .filter(Boolean)
236
+ .map((path) => canonicalizePath(path));
237
+ return [...new Set(resolved)];
238
+ }
239
+ /**
240
+ * Resolve a project path to its canonical, symlink-free absolute form. The
241
+ * executor advertises project paths via `resolve()` alone, so canonicalizing
242
+ * here keeps the saved `scan_roots` and the advertised hello paths identical to
243
+ * the real directory (e.g. when the cwd or GSD_CLOUD_PROJECTS points through a
244
+ * symlink). Falls back to the plain resolved path when the target does not yet
245
+ * exist on disk.
246
+ */
247
+ function canonicalizePath(path) {
248
+ const resolved = resolve(path);
249
+ try {
250
+ return realpathSync(resolved);
251
+ }
252
+ catch {
253
+ return resolved;
254
+ }
255
+ }
129
256
  export function formatUsage(binaryName) {
130
- return `Usage: ${binaryName} login [--gateway <url>] [--runtime-name <name>] [--config <path>]
257
+ return `Usage: ${binaryName} login [--gateway <url>] [--runtime-name <name>] [--config <path>] [--foreground]
131
258
  ${binaryName} status [--config <path>]
132
259
  ${binaryName} pair --gateway <url> --code <code> [--runtime-name <name>] [--config <path>]
133
- ${binaryName} connect [--config <path>] [--verbose]
260
+ ${binaryName} connect [--config <path>] [--verbose] [--foreground]
261
+ ${binaryName} stop [--config <path>]
134
262
  ${binaryName} disconnect [--config <path>]
135
263
 
136
264
  Commands:
137
- login (Recommended) Browser-based pairing — opens an approval URL in the
138
- terminal, polls for authorization, then auto-connects. Defaults to
139
- the public GSD Cloud gateway.
265
+ login (Recommended) Browser-based pairing — opens an approval URL, then
266
+ connects the current project in the background. Defaults to the
267
+ public GSD Cloud gateway.
140
268
  status Show current cloud runtime configuration and connection status.
141
269
  pair Exchange a pairing code for a device token (headless/CI environments).
142
- connect Connect using a previously paired device token.
143
- disconnect Remove cloud runtime configuration from the local config file.
270
+ connect Start a background connection using saved credentials and projects.
271
+ stop Stop the background runtime without removing saved credentials.
272
+ disconnect Stop the background runtime and remove local cloud credentials.
144
273
 
145
274
  Options:
146
275
  --config <path> Path to YAML config file (default: ~/.gsd/daemon.yaml)
@@ -148,6 +277,7 @@ Options:
148
277
  --code <code> Pairing code from the gateway (pair only)
149
278
  --runtime-name <name> Friendly name for this local GSD runtime
150
279
  --verbose Print log entries to stderr in addition to the log file
280
+ --foreground Keep login/connect attached to this terminal (debugging)
151
281
  --help Show this help message and exit
152
282
 
153
283
  Environment: