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