@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.
- package/README.md +10 -9
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -0
- package/dist/resources/extensions/gsd/auto-start.js +1 -1
- package/dist/resources/extensions/gsd/auto-verification.js +25 -12
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
- package/dist/resources/extensions/gsd/db/engine.js +8 -2
- package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
- package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +16 -4
- package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
- package/dist/resources/extensions/gsd/model-router.js +10 -5
- package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
- package/dist/resources/extensions/gsd/preferences-models.js +50 -10
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
- package/dist/resources/extensions/gsd/preferences.js +7 -0
- package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
- package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
- package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
- package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
- package/dist/resources/extensions/subagent/index.js +1 -1
- package/dist/startup-model-validation.d.ts +1 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +7 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/rpc.d.ts +1 -1
- package/packages/contracts/dist/rpc.d.ts.map +1 -1
- package/packages/contracts/dist/rpc.js +1 -1
- package/packages/contracts/dist/rpc.js.map +1 -1
- package/packages/contracts/dist/rpc.test.js +1 -1
- package/packages/contracts/dist/rpc.test.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
- package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +12 -2
- package/packages/gsd-cloud/dist/background-cli.test.js +157 -80
- package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -1
- package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cli.js +36 -12
- package/packages/gsd-cloud/dist/cli.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts +6 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.js +106 -24
- package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
- package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts +5 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.js +429 -61
- package/packages/gsd-cloud/dist/runtime-process.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.test.js +493 -47
- package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
- package/packages/gsd-cloud/package.json +2 -2
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +2 -2
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +113 -2
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +84 -9
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.js +18 -6
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +9 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
- package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/theme/theme.d.ts +1 -1
- package/pkg/dist/theme/theme.d.ts.map +1 -1
- package/pkg/dist/theme/theme.js +1 -0
- package/pkg/dist/theme/theme.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
- package/src/resources/extensions/gsd/auto-start.ts +1 -1
- package/src/resources/extensions/gsd/auto-verification.ts +46 -12
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
- package/src/resources/extensions/gsd/db/engine.ts +9 -1
- package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
- package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +15 -4
- package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
- package/src/resources/extensions/gsd/model-router.ts +10 -5
- package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
- package/src/resources/extensions/gsd/preferences-models.ts +54 -10
- package/src/resources/extensions/gsd/preferences-types.ts +10 -2
- package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
- package/src/resources/extensions/gsd/preferences.ts +13 -0
- package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
- package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
- package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
- package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
- package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
- package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
- package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
- package/src/resources/extensions/subagent/index.ts +1 -1
- /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
- /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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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("
|
|
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
|
-
|
|
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":"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","sourcesContent":["// Project/App: Open GSD\n// File Purpose: Acceptance coverage for the detached gsd-cloud runtime lifecycle.\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync } from \"node:fs\";\nimport { createServer } from \"node:http\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { test } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { WebSocketServer } from \"ws\";\nimport { saveCloudConfig } from \"./cloud-config.js\";\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst cliPath = join(packageRoot, \"bin\", \"gsd-cloud.js\");\n\ninterface CliResult {\n code: number | null;\n stdout: string;\n stderr: string;\n}\n\ninterface TestGateway {\n baseUrl: string;\n hello: Promise<Record<string, unknown>>;\n close: () => Promise<void>;\n}\n\nfunction runCli(\n args: string[],\n cwd: string,\n timeoutMs = 12_000,\n extraEnv: Record<string, string | undefined> = {},\n): Promise<CliResult> {\n return new Promise((resolveResult, reject) => {\n const child = spawn(process.execPath, [cliPath, ...args], {\n cwd,\n env: { ...process.env, GSD_CLOUD_PROJECTS: undefined, ...extraEnv },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n let stdout = \"\";\n let stderr = \"\";\n child.stdout.setEncoding(\"utf8\");\n child.stderr.setEncoding(\"utf8\");\n child.stdout.on(\"data\", (chunk: string) => { stdout += chunk; });\n child.stderr.on(\"data\", (chunk: string) => { stderr += chunk; });\n\n const timeout = setTimeout(() => {\n child.kill(\"SIGTERM\");\n reject(new Error(`gsd-cloud did not return within ${timeoutMs}ms`));\n }, timeoutMs);\n child.once(\"error\", (error) => {\n clearTimeout(timeout);\n reject(error);\n });\n child.once(\"exit\", (code) => {\n clearTimeout(timeout);\n resolveResult({ code, stdout, stderr });\n });\n });\n}\n\ninterface ForegroundCli {\n child: ChildProcess;\n exited: Promise<number | null>;\n}\n\n// Spawn a long-lived CLI process (e.g. `--foreground`) without waiting for exit,\n// so the test can inspect its state and then stop it.\nfunction spawnForegroundCli(args: string[], cwd: string): ForegroundCli {\n const child = spawn(process.execPath, [cliPath, ...args], {\n cwd,\n env: { ...process.env, GSD_CLOUD_PROJECTS: undefined },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n const exited = new Promise<number | null>((resolveExit) => {\n child.once(\"exit\", (code) => resolveExit(code));\n });\n return { child, exited };\n}\n\nasync function createTestGateway(deviceFlow = false): Promise<TestGateway> {\n let baseUrl = \"\";\n const server = createServer((req, res) => {\n if (!deviceFlow) {\n res.writeHead(404).end();\n return;\n }\n res.setHeader(\"content-type\", \"application/json\");\n if (req.url === \"/api/device/code\") {\n res.end(JSON.stringify({\n userCode: \"TEST-CODE\",\n deviceCode: \"test-device\",\n verificationUriComplete: `${baseUrl}/approve-device?code=TEST-CODE`,\n expiresIn: 30,\n }));\n return;\n }\n if (req.url === \"/api/device/token\") {\n res.end(JSON.stringify({\n status: \"approved\",\n token: \"test\",\n runtimeId: \"fixture-runtime\",\n gateway_url: baseUrl,\n }));\n return;\n }\n res.writeHead(404).end();\n });\n const wss = new WebSocketServer({ server });\n await new Promise<void>((resolveListen, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", resolveListen);\n });\n const address = server.address();\n assert.ok(address && typeof address === \"object\");\n baseUrl = `http://127.0.0.1:${address.port}`;\n\n let helloTimeout: ReturnType<typeof setTimeout>;\n const hello = new Promise<Record<string, unknown>>((resolveHello, reject) => {\n helloTimeout = setTimeout(() => reject(new Error(\"background runtime did not advertise its project\")), 8_000);\n wss.once(\"connection\", (socket) => {\n socket.once(\"message\", (data) => {\n clearTimeout(helloTimeout);\n resolveHello(JSON.parse(data.toString(\"utf8\")) as Record<string, unknown>);\n });\n });\n });\n\n return {\n baseUrl,\n hello,\n close: async () => {\n clearTimeout(helloTimeout);\n for (const client of wss.clients) client.terminate();\n await new Promise<void>((resolveClose) => wss.close(() => server.close(() => resolveClose())));\n },\n };\n}\n\ntest(\"connect returns while a background runtime advertises the selected project\", async () => {\n const root = mkdtempSync(join(tmpdir(), \"gsd-cloud-background-\"));\n const projectDir = join(root, \"project\");\n const expectedProjectDir = join(realpathSync(root), \"project\");\n const configPath = join(root, \"daemon.yaml\");\n mkdirSync(join(projectDir, \".gsd\"), { recursive: true });\n\n const gateway = await createTestGateway();\n\n saveCloudConfig(configPath, {\n gateway_url: gateway.baseUrl,\n device_token: \"test\",\n runtime_id: \"fixture-runtime\",\n enabled: true,\n });\n\n try {\n const connect = await runCli([\"connect\", \"--config\", configPath], projectDir);\n assert.equal(connect.code, 0, connect.stderr);\n assert.match(connect.stdout, /connected in the background/i);\n\n const message = await gateway.hello;\n assert.equal(message.type, \"hello\");\n const projects = message.projects as Array<{ path: string }>;\n assert.equal(projects.length, 1);\n assert.equal(projects[0]?.path, expectedProjectDir);\n\n const status = await runCli([\"status\", \"--config\", configPath], projectDir);\n assert.equal(status.code, 0, status.stderr);\n const statusBody = JSON.parse(status.stdout) as {\n background?: { running?: boolean; pid?: number | null };\n projects?: string[];\n };\n assert.equal(statusBody.background?.running, true);\n assert.equal(typeof statusBody.background?.pid, \"number\");\n assert.deepEqual(statusBody.projects, [expectedProjectDir]);\n } finally {\n await runCli([\"disconnect\", \"--config\", configPath], projectDir).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n }\n});\n\ntest(\"login returns after approval and keeps the selected project connected\", async () => {\n const root = mkdtempSync(join(tmpdir(), \"gsd-cloud-login-background-\"));\n const projectDir = join(root, \"project\");\n const expectedProjectDir = join(realpathSync(root), \"project\");\n const configPath = join(root, \"daemon.yaml\");\n mkdirSync(join(projectDir, \".gsd\"), { recursive: true });\n const gateway = await createTestGateway(true);\n\n try {\n const login = await runCli([\n \"login\",\n \"--gateway\", gateway.baseUrl,\n \"--config\", configPath,\n ], projectDir, 12_000);\n assert.equal(login.code, 0, login.stderr);\n assert.match(login.stdout, /Authorization approved!/);\n assert.match(login.stdout, /connected in the background/i);\n\n const message = await gateway.hello;\n const projects = message.projects as Array<{ path: string }>;\n assert.deepEqual(projects.map((project) => project.path), [expectedProjectDir]);\n\n const disconnect = await runCli([\"disconnect\", \"--config\", configPath], projectDir);\n assert.equal(disconnect.code, 0, disconnect.stderr);\n const status = await runCli([\"status\", \"--config\", configPath], projectDir);\n const statusBody = JSON.parse(status.stdout) as {\n configured?: boolean;\n background?: { running?: boolean };\n };\n assert.equal(statusBody.configured, false);\n assert.equal(statusBody.background?.running, false);\n } finally {\n await runCli([\"disconnect\", \"--config\", configPath], projectDir).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n }\n});\n\ntest(\"advertised project paths are canonicalized through symlinks\", async () => {\n const root = realpathSync(mkdtempSync(join(tmpdir(), \"gsd-cloud-symlink-\")));\n const realProject = join(root, \"real-project\");\n mkdirSync(join(realProject, \".gsd\"), { recursive: true });\n const linkProject = join(root, \"link-project\");\n symlinkSync(realProject, linkProject);\n const configPath = join(root, \"daemon.yaml\");\n const gateway = await createTestGateway();\n\n saveCloudConfig(configPath, {\n gateway_url: gateway.baseUrl,\n device_token: \"test\",\n runtime_id: \"fixture-runtime\",\n enabled: true,\n });\n\n try {\n // Point the runtime at the symlink; the advertised path must be the real dir.\n const connect = await runCli(\n [\"connect\", \"--config\", configPath],\n realProject,\n 5_000,\n { GSD_CLOUD_PROJECTS: linkProject },\n );\n assert.equal(connect.code, 0, connect.stderr);\n\n const message = await gateway.hello;\n const projects = message.projects as Array<{ path: string }>;\n assert.equal(projects.length, 1);\n assert.equal(projects[0]?.path, realProject);\n\n const status = await runCli([\"status\", \"--config\", configPath], realProject);\n const statusBody = JSON.parse(status.stdout) as { projects?: string[] };\n assert.deepEqual(statusBody.projects, [realProject]);\n } finally {\n await runCli([\"disconnect\", \"--config\", configPath], realProject).catch(() => undefined);\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n }\n});\n\ntest(\"a foreground runtime registers its PID so disconnect can stop it\", async () => {\n const root = realpathSync(mkdtempSync(join(tmpdir(), \"gsd-cloud-foreground-\")));\n const projectDir = join(root, \"project\");\n const configPath = join(root, \"daemon.yaml\");\n mkdirSync(join(projectDir, \".gsd\"), { recursive: true });\n const gateway = await createTestGateway();\n\n saveCloudConfig(configPath, {\n gateway_url: gateway.baseUrl,\n device_token: \"test\",\n runtime_id: \"fixture-runtime\",\n enabled: true,\n });\n\n const foreground = spawnForegroundCli([\"connect\", \"--foreground\", \"--config\", configPath], projectDir);\n try {\n // Once the foreground runtime advertises its project it is fully connected.\n await gateway.hello;\n\n // The foreground session must record its own PID in the shared runtime state\n // so status can see it and a later disconnect can find and stop it.\n const status = await runCli([\"status\", \"--config\", configPath], projectDir);\n const statusBody = JSON.parse(status.stdout) as {\n background?: { running?: boolean; pid?: number | null };\n };\n assert.equal(statusBody.background?.running, true);\n assert.equal(statusBody.background?.pid, foreground.child.pid);\n\n const disconnect = await runCli([\"disconnect\", \"--config\", configPath], projectDir);\n assert.equal(disconnect.code, 0, disconnect.stderr);\n\n // disconnect must have terminated the foreground process.\n const exitCode = await foreground.exited;\n assert.equal(exitCode, 0);\n\n const afterStatus = await runCli([\"status\", \"--config\", configPath], projectDir);\n const afterBody = JSON.parse(afterStatus.stdout) as { background?: { running?: boolean } };\n assert.equal(afterBody.background?.running, false);\n } finally {\n foreground.child.kill(\"SIGKILL\");\n await gateway.close();\n rmSync(root, { recursive: true, force: true });\n }\n});\n"]}
|
|
1
|
+
{"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":"
|
|
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
|
|
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
|
|
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
|
|
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:
|