@opengsd/gsd-pi 1.10.0 → 1.11.0

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 (218) 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-model-selection.js +1 -0
  4. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  5. package/dist/resources/extensions/gsd/auto-verification.js +25 -12
  6. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
  7. package/dist/resources/extensions/gsd/db/engine.js +8 -2
  8. package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
  9. package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
  10. package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  11. package/dist/resources/extensions/gsd/doctor-engine-checks.js +16 -4
  12. package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
  13. package/dist/resources/extensions/gsd/model-router.js +10 -5
  14. package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
  15. package/dist/resources/extensions/gsd/preferences-models.js +50 -10
  16. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  17. package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
  18. package/dist/resources/extensions/gsd/preferences.js +7 -0
  19. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  20. package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
  21. package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
  22. package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
  23. package/dist/resources/extensions/subagent/index.js +1 -1
  24. package/dist/startup-model-validation.d.ts +1 -1
  25. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.html +1 -1
  46. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  53. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  56. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  57. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  58. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  59. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  60. package/integrations/hermes/pyproject.toml +1 -1
  61. package/package.json +7 -6
  62. package/packages/cloud-mcp-gateway/package.json +2 -2
  63. package/packages/contracts/dist/rpc.d.ts +1 -1
  64. package/packages/contracts/dist/rpc.d.ts.map +1 -1
  65. package/packages/contracts/dist/rpc.js +1 -1
  66. package/packages/contracts/dist/rpc.js.map +1 -1
  67. package/packages/contracts/dist/rpc.test.js +1 -1
  68. package/packages/contracts/dist/rpc.test.js.map +1 -1
  69. package/packages/contracts/package.json +1 -1
  70. package/packages/daemon/package.json +4 -4
  71. package/packages/gsd-agent-core/package.json +5 -5
  72. package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
  73. package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
  74. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  75. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
  76. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  77. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  78. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
  79. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  80. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
  81. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
  82. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  83. package/packages/gsd-agent-modes/package.json +7 -7
  84. package/packages/gsd-cloud/README.md +12 -2
  85. package/packages/gsd-cloud/dist/background-cli.test.js +157 -80
  86. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -1
  87. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  88. package/packages/gsd-cloud/dist/cli.js +36 -12
  89. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  90. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +6 -1
  91. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  92. package/packages/gsd-cloud/dist/cloud-runtime.js +106 -24
  93. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  94. package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
  95. package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
  96. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
  97. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
  98. package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
  99. package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
  100. package/packages/gsd-cloud/dist/runtime-process.d.ts +5 -0
  101. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -1
  102. package/packages/gsd-cloud/dist/runtime-process.js +429 -61
  103. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -1
  104. package/packages/gsd-cloud/dist/runtime-process.test.js +493 -47
  105. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -1
  106. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
  107. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
  108. package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
  109. package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
  110. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
  111. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
  112. package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
  113. package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
  114. package/packages/gsd-cloud/package.json +2 -2
  115. package/packages/mcp-server/package.json +4 -4
  116. package/packages/native/package.json +1 -1
  117. package/packages/pi-agent-core/dist/types.d.ts +2 -2
  118. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  119. package/packages/pi-agent-core/dist/types.js.map +1 -1
  120. package/packages/pi-agent-core/package.json +1 -1
  121. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  122. package/packages/pi-ai/dist/models.generated.d.ts +113 -2
  123. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  124. package/packages/pi-ai/dist/models.generated.js +84 -9
  125. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  126. package/packages/pi-ai/dist/models.js +18 -6
  127. package/packages/pi-ai/dist/models.js.map +1 -1
  128. package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
  129. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  130. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
  131. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  132. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  133. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  134. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  135. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
  136. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  137. package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
  138. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  139. package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
  140. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  141. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  142. package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
  143. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  145. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  146. package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
  147. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  148. package/packages/pi-ai/dist/providers/simple-options.js +1 -1
  149. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  150. package/packages/pi-ai/dist/types.d.ts +9 -1
  151. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  152. package/packages/pi-ai/dist/types.js.map +1 -1
  153. package/packages/pi-ai/package.json +1 -1
  154. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
  156. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
  158. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
  160. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  162. package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
  163. package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
  164. package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
  165. package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
  166. package/packages/pi-coding-agent/package.json +7 -7
  167. package/packages/pi-tui/package.json +2 -2
  168. package/packages/rpc-client/package.json +2 -2
  169. package/pkg/dist/theme/theme.d.ts +1 -1
  170. package/pkg/dist/theme/theme.d.ts.map +1 -1
  171. package/pkg/dist/theme/theme.js +1 -0
  172. package/pkg/dist/theme/theme.js.map +1 -1
  173. package/pkg/package.json +1 -1
  174. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
  175. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  176. package/src/resources/extensions/gsd/auto-verification.ts +46 -12
  177. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
  178. package/src/resources/extensions/gsd/db/engine.ts +9 -1
  179. package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
  180. package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
  181. package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  182. package/src/resources/extensions/gsd/doctor-engine-checks.ts +15 -4
  183. package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
  184. package/src/resources/extensions/gsd/model-router.ts +10 -5
  185. package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
  186. package/src/resources/extensions/gsd/preferences-models.ts +54 -10
  187. package/src/resources/extensions/gsd/preferences-types.ts +10 -2
  188. package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
  189. package/src/resources/extensions/gsd/preferences.ts +13 -0
  190. package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  191. package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
  192. package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
  193. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +64 -0
  194. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
  195. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  197. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
  198. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
  199. package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
  200. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
  201. package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
  202. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
  203. package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
  204. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
  205. package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
  206. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
  207. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
  208. package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
  209. package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
  210. package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
  211. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
  212. package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
  213. package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
  214. package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
  215. package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
  216. package/src/resources/extensions/subagent/index.ts +1 -1
  217. /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
  218. /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  // Project/App: Open GSD
2
2
  // File Purpose: Acceptance coverage for the detached gsd-cloud runtime lifecycle.
3
3
  import { spawn } from "node:child_process";
4
- import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync } from "node:fs";
4
+ import { existsSync, mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { createServer } from "node:http";
6
6
  import { tmpdir } from "node:os";
7
7
  import { dirname, join, resolve } from "node:path";
@@ -10,6 +10,8 @@ import { test } from "node:test";
10
10
  import assert from "node:assert/strict";
11
11
  import { WebSocketServer } from "ws";
12
12
  import { saveCloudConfig } from "./cloud-config.js";
13
+ import { runtimeArtifactPath } from "./runtime-artifacts.js";
14
+ import { startBackgroundRuntime, stopBackgroundRuntime } from "./runtime-process.js";
13
15
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
14
16
  const cliPath = join(packageRoot, "bin", "gsd-cloud.js");
15
17
  function runCli(args, cwd, timeoutMs = 12_000, extraEnv = {}) {
@@ -109,74 +111,101 @@ async function createTestGateway(deviceFlow = false) {
109
111
  },
110
112
  };
111
113
  }
112
- test("connect returns while a background runtime advertises the selected project", async () => {
114
+ test("connect returns while a background runtime advertises the selected project", async (t) => {
113
115
  const root = mkdtempSync(join(tmpdir(), "gsd-cloud-background-"));
114
116
  const projectDir = join(root, "project");
115
117
  const expectedProjectDir = join(realpathSync(root), "project");
116
118
  const configPath = join(root, "daemon.yaml");
117
119
  mkdirSync(join(projectDir, ".gsd"), { recursive: true });
118
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
+ });
119
126
  saveCloudConfig(configPath, {
120
127
  gateway_url: gateway.baseUrl,
121
128
  device_token: "test",
122
129
  runtime_id: "fixture-runtime",
123
130
  enabled: true,
124
131
  });
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 {
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 () => {
142
154
  await runCli(["disconnect", "--config", configPath], projectDir).catch(() => undefined);
143
155
  await gateway.close();
144
156
  rmSync(root, { recursive: true, force: true });
145
- }
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);
146
176
  });
147
- test("login returns after approval and keeps the selected project connected", async () => {
177
+ test("login returns after approval and keeps the selected project connected", async (t) => {
148
178
  const root = mkdtempSync(join(tmpdir(), "gsd-cloud-login-background-"));
149
179
  const projectDir = join(root, "project");
150
180
  const expectedProjectDir = join(realpathSync(root), "project");
151
181
  const configPath = join(root, "daemon.yaml");
152
182
  mkdirSync(join(projectDir, ".gsd"), { recursive: true });
153
183
  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 {
184
+ t.after(async () => {
174
185
  await runCli(["disconnect", "--config", configPath], projectDir).catch(() => undefined);
175
186
  await gateway.close();
176
187
  rmSync(root, { recursive: true, force: true });
177
- }
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);
178
207
  });
179
- test("advertised project paths are canonicalized through symlinks", async () => {
208
+ test("advertised project paths are canonicalized through symlinks", async (t) => {
180
209
  const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cloud-symlink-")));
181
210
  const realProject = join(root, "real-project");
182
211
  mkdirSync(join(realProject, ".gsd"), { recursive: true });
@@ -184,35 +213,74 @@ test("advertised project paths are canonicalized through symlinks", async () =>
184
213
  symlinkSync(realProject, linkProject);
185
214
  const configPath = join(root, "daemon.yaml");
186
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();
187
244
  saveCloudConfig(configPath, {
188
245
  gateway_url: gateway.baseUrl,
189
246
  device_token: "test",
190
247
  runtime_id: "fixture-runtime",
191
248
  enabled: true,
192
249
  });
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);
250
+ const foreground = spawnForegroundCli(["connect", "--foreground", "--config", configPath], projectDir);
251
+ t.after(async () => {
252
+ foreground.child.kill("SIGKILL");
207
253
  await gateway.close();
208
254
  rmSync(root, { recursive: true, force: true });
209
- }
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);
210
272
  });
211
- test("a foreground runtime registers its PID so disconnect can stop it", async () => {
212
- const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cloud-foreground-")));
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-")));
213
275
  const projectDir = join(root, "project");
214
276
  const configPath = join(root, "daemon.yaml");
277
+ const delayedRuntimePath = join(root, "delayed-runtime.mjs");
215
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"));
216
284
  const gateway = await createTestGateway();
217
285
  saveCloudConfig(configPath, {
218
286
  gateway_url: gateway.baseUrl,
@@ -220,29 +288,38 @@ test("a foreground runtime registers its PID so disconnect can stop it", async (
220
288
  runtime_id: "fixture-runtime",
221
289
  enabled: true,
222
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")));
223
298
  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 {
299
+ t.after(async () => {
243
300
  foreground.child.kill("SIGKILL");
301
+ await stopBackgroundRuntime(configPath).catch(() => undefined);
244
302
  await gateway.close();
245
303
  rmSync(root, { recursive: true, force: true });
246
- }
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);
247
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
+ }
248
325
  //# sourceMappingURL=background-cli.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"background-cli.test.js","sourceRoot":"","sources":["../src/background-cli.test.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,kFAAkF;AAClF,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AAC3E,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC;AAczD,SAAS,MAAM,CACb,IAAc,EACd,GAAW,EACX,SAAS,GAAG,MAAM,EAClB,WAA+C,EAAE;IAEjD,OAAO,IAAI,OAAO,CAAC,CAAC,aAAa,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,EAAE;YACxD,GAAG;YACH,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE;YACnE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAC;QACH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACjE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAEjE,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACtB,MAAM,CAAC,IAAI,KAAK,CAAC,mCAAmC,SAAS,IAAI,CAAC,CAAC,CAAC;QACtE,CAAC,EAAE,SAAS,CAAC,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC5B,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YAC1B,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,aAAa,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAOD,iFAAiF;AACjF,sDAAsD;AACtD,SAAS,kBAAkB,CAAC,IAAc,EAAE,GAAW;IACrD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,EAAE;QACxD,GAAG;QACH,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,SAAS,EAAE;QACtD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;KAClC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,OAAO,CAAgB,CAAC,WAAW,EAAE,EAAE;QACxD,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,UAAU,GAAG,KAAK;IACjD,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACvC,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QACD,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QAClD,IAAI,GAAG,CAAC,GAAG,KAAK,kBAAkB,EAAE,CAAC;YACnC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;gBACrB,QAAQ,EAAE,WAAW;gBACrB,UAAU,EAAE,aAAa;gBACzB,uBAAuB,EAAE,GAAG,OAAO,gCAAgC;gBACnE,SAAS,EAAE,EAAE;aACd,CAAC,CAAC,CAAC;YACJ,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,GAAG,KAAK,mBAAmB,EAAE,CAAC;YACpC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;gBACrB,MAAM,EAAE,UAAU;gBAClB,KAAK,EAAE,MAAM;gBACb,SAAS,EAAE,iBAAiB;gBAC5B,WAAW,EAAE,OAAO;aACrB,CAAC,CAAC,CAAC;YACJ,OAAO;QACT,CAAC;QACD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5C,MAAM,IAAI,OAAO,CAAO,CAAC,aAAa,EAAE,MAAM,EAAE,EAAE;QAChD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IACjC,MAAM,CAAC,EAAE,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC;IAClD,OAAO,GAAG,oBAAoB,OAAO,CAAC,IAAI,EAAE,CAAC;IAE7C,IAAI,YAA2C,CAAC;IAChD,MAAM,KAAK,GAAG,IAAI,OAAO,CAA0B,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE;QAC1E,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC9G,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE;YAChC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,YAAY,CAAC,YAAY,CAAC,CAAC;gBAC3B,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAA4B,CAAC,CAAC;YAC7E,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,KAAK;QACL,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,YAAY,CAAC,YAAY,CAAC,CAAC;YAC3B,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO;gBAAE,MAAM,CAAC,SAAS,EAAE,CAAC;YACrD,MAAM,IAAI,OAAO,CAAO,CAAC,YAAY,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC;QACjG,CAAC;KACF,CAAC;AACJ,CAAC;AAED,IAAI,CAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;IAC5F,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,uBAAuB,CAAC,CAAC,CAAC;IAClE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,kBAAkB,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;IAC/D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAC7C,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzD,MAAM,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAE1C,eAAe,CAAC,UAAU,EAAE;QAC1B,WAAW,EAAE,OAAO,CAAC,OAAO;QAC5B,YAAY,EAAE,MAAM;QACpB,UAAU,EAAE,iBAAiB;QAC7B,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;QAC9E,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,8BAA8B,CAAC,CAAC;QAE7D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAmC,CAAC;QAC7D,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;QAEpD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;QAC5E,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAG1C,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,UAAU,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC1D,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC9D,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,CAAC,YAAY,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACxF,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;IACvF,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,6BAA6B,CAAC,CAAC,CAAC;IACxE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,kBAAkB,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;IAC/D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAC7C,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAE9C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC;YACzB,OAAO;YACP,WAAW,EAAE,OAAO,CAAC,OAAO;YAC5B,UAAU,EAAE,UAAU;SACvB,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;QACvB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;QACtD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,8BAA8B,CAAC,CAAC;QAE3D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;QACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAmC,CAAC;QAC7D,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC;QAEhF,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,CAAC,YAAY,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;QACpF,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;QAC5E,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAG1C,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IACtD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,CAAC,YAAY,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACxF,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;IAC7E,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC;IAC7E,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IAC/C,SAAS,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IAC/C,WAAW,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAE1C,eAAe,CAAC,UAAU,EAAE;QAC1B,WAAW,EAAE,OAAO,CAAC,OAAO;QAC5B,YAAY,EAAE,MAAM;QACpB,UAAU,EAAE,iBAAiB;QAC7B,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,8EAA8E;QAC9E,MAAM,OAAO,GAAG,MAAM,MAAM,CAC1B,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC,EACnC,WAAW,EACX,KAAK,EACL,EAAE,kBAAkB,EAAE,WAAW,EAAE,CACpC,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAE9C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;QACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAmC,CAAC;QAC7D,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;QAE7C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC;QAC7E,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAA4B,CAAC;QACxE,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IACvD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,CAAC,YAAY,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACzF,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;IAClF,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAC;IAChF,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAC7C,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAE1C,eAAe,CAAC,UAAU,EAAE;QAC1B,WAAW,EAAE,OAAO,CAAC,OAAO;QAC5B,YAAY,EAAE,MAAM;QACpB,UAAU,EAAE,iBAAiB;QAC7B,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,kBAAkB,CAAC,CAAC,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;IACvG,IAAI,CAAC;QACH,4EAA4E;QAC5E,MAAM,OAAO,CAAC,KAAK,CAAC;QAEpB,6EAA6E;QAC7E,oEAAoE;QACpE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;QAC5E,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAE1C,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,EAAE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAE/D,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,CAAC,YAAY,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;QACpF,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAEpD,0DAA0D;QAC1D,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAE1B,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;QACjF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAA2C,CAAC;QAC3F,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IACrD,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACjC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;AACH,CAAC,CAAC,CAAC","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
+ {"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":"AA8BA,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkIhB;AA0GD,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+BtD"}
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"}
@@ -13,7 +13,8 @@ import { clearCloudConfig, exchangePairingCode, redactedCloudStatus, saveCloudCo
13
13
  import { runDeviceFlow } from "./device-flow.js";
14
14
  import { CloudRuntime } from "./cloud-runtime.js";
15
15
  import { selectExecutor } from "./executors/index.js";
16
- import { backgroundRuntimeStatus, clearRuntimeState, startBackgroundRuntime, stopBackgroundRuntime, writeRuntimeState, } from "./runtime-process.js";
16
+ import { acquireRuntimeStartLock, backgroundRuntimeStatus, clearRuntimeState, startBackgroundRuntime, stopBackgroundRuntime, writeRuntimeState, } from "./runtime-process.js";
17
+ import { clearRuntimeTelemetry, readRuntimeTelemetry, RuntimeTelemetryStore } from "./runtime-telemetry.js";
17
18
  export async function handleCloudCommand(argv, opts) {
18
19
  if (argv[0] === "--help" || argv[0] === "-h") {
19
20
  process.stdout.write(formatUsage(opts.binaryName));
@@ -44,15 +45,24 @@ export async function handleCloudCommand(argv, opts) {
44
45
  ...redactedCloudStatus(config),
45
46
  projects: config.projects.scan_roots,
46
47
  background: backgroundRuntimeStatus(configPath),
48
+ telemetry: readRuntimeTelemetry(configPath),
47
49
  }, null, 2)}\n`);
48
50
  return;
49
51
  }
50
52
  if (command === "disconnect") {
51
53
  await stopBackgroundRuntime(configPath);
52
54
  clearCloudConfig(configPath);
55
+ clearRuntimeTelemetry(configPath);
53
56
  process.stdout.write(`${opts.binaryName}: background runtime stopped and cloud credentials removed.\n`);
54
57
  return;
55
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`);
64
+ return;
65
+ }
56
66
  if (command === "login") {
57
67
  if (!values.gateway) {
58
68
  throw new Error("login requires --gateway");
@@ -74,10 +84,7 @@ export async function handleCloudCommand(argv, opts) {
74
84
  }, projectDirs);
75
85
  process.stdout.write(`${opts.binaryName}: cloud runtime ${runtimeId} paired — connecting...\n`);
76
86
  if (values.foreground) {
77
- await stopBackgroundRuntime(configPath);
78
- await runCloudRuntime(config, opts.binaryName, values.verbose, projectDirs, {
79
- registerConfigPath: configPath,
80
- });
87
+ await runForegroundCloudRuntime(config, configPath, opts.binaryName, values.verbose, projectDirs);
81
88
  return;
82
89
  }
83
90
  await startAndReportBackgroundRuntime(configPath, projectDirs, opts.binaryName, values.verbose);
@@ -111,10 +118,7 @@ export async function handleCloudCommand(argv, opts) {
111
118
  const projectDirs = selectedProjectDirs(config.projects.scan_roots);
112
119
  config = saveCloudConfig(configPath, config.cloud, projectDirs);
113
120
  if (values.foreground) {
114
- await stopBackgroundRuntime(configPath);
115
- await runCloudRuntime(config, opts.binaryName, values.verbose, projectDirs, {
116
- registerConfigPath: configPath,
117
- });
121
+ await runForegroundCloudRuntime(config, configPath, opts.binaryName, values.verbose, projectDirs);
118
122
  return;
119
123
  }
120
124
  await startAndReportBackgroundRuntime(configPath, projectDirs, opts.binaryName, values.verbose);
@@ -126,7 +130,7 @@ export async function handleCloudCommand(argv, opts) {
126
130
  throw new Error("cloud runtime is not paired; run `login` first");
127
131
  }
128
132
  const projectDirs = selectedProjectDirs(config.projects.scan_roots);
129
- await runCloudRuntime(config, opts.binaryName, values.verbose, projectDirs, {
133
+ await runCloudRuntime(config, configPath, opts.binaryName, values.verbose, projectDirs, {
130
134
  onConnected: () => {
131
135
  process.send?.({ type: "ready" });
132
136
  },
@@ -135,12 +139,25 @@ export async function handleCloudCommand(argv, opts) {
135
139
  }
136
140
  throw new Error(`Unknown cloud runtime command: ${command}`);
137
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
+ }
138
155
  /**
139
156
  * Start the WS relay and block until the process is signalled. This is the whole
140
157
  * "daemon" for the standalone agent: one CloudRuntime + one Executor, no Discord,
141
158
  * no scanner, no session-manager class.
142
159
  */
143
- async function runCloudRuntime(config, binaryName, verbose, projectDirs, opts = {}) {
160
+ async function runCloudRuntime(config, configPath, binaryName, verbose, projectDirs, opts = {}) {
144
161
  if (!config.cloud)
145
162
  throw new Error("cloud runtime is not configured");
146
163
  if (config.cloud.enabled === false) {
@@ -152,7 +169,12 @@ async function runCloudRuntime(config, binaryName, verbose, projectDirs, opts =
152
169
  verbose,
153
170
  });
154
171
  const executor = selectExecutor(logger, { projectDirs });
155
- const runtime = new CloudRuntime(config.cloud, executor, logger);
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);
156
178
  // A foreground session owns the runtime itself, so it records its own PID in
157
179
  // the shared state file. That lets a later `connect`/`disconnect` find and
158
180
  // stop it, just like a detached background runtime. Background `_run` children
@@ -236,6 +258,7 @@ export function formatUsage(binaryName) {
236
258
  ${binaryName} status [--config <path>]
237
259
  ${binaryName} pair --gateway <url> --code <code> [--runtime-name <name>] [--config <path>]
238
260
  ${binaryName} connect [--config <path>] [--verbose] [--foreground]
261
+ ${binaryName} stop [--config <path>]
239
262
  ${binaryName} disconnect [--config <path>]
240
263
 
241
264
  Commands:
@@ -245,6 +268,7 @@ Commands:
245
268
  status Show current cloud runtime configuration and connection status.
246
269
  pair Exchange a pairing code for a device token (headless/CI environments).
247
270
  connect Start a background connection using saved credentials and projects.
271
+ stop Stop the background runtime without removing saved credentials.
248
272
  disconnect Stop the background runtime and remove local cloud credentials.
249
273
 
250
274
  Options: