@opengsd/gsd-pi 1.6.0 → 1.7.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 +6 -10
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +19 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +20 -0
- package/dist/resources/extensions/gsd/auto.js +12 -0
- package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +68 -2
- package/dist/resources/extensions/gsd/reopen-reason.js +98 -0
- package/dist/resources/extensions/gsd/session-status-io.js +31 -0
- package/dist/resources/extensions/gsd/tools/reopen-task.js +15 -0
- package/dist/resources/extensions/shared/gsd-browser-cli.js +22 -5
- 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 +11 -11
- 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 +11 -11
- 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 +6 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/cloud-cli.d.ts +1 -1
- package/packages/daemon/dist/cloud-cli.d.ts.map +1 -1
- package/packages/daemon/dist/cloud-cli.js +42 -2
- package/packages/daemon/dist/cloud-cli.js.map +1 -1
- package/packages/daemon/dist/cloud-config.d.ts +1 -0
- package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
- package/packages/daemon/dist/cloud-config.js +1 -1
- package/packages/daemon/dist/cloud-config.js.map +1 -1
- package/packages/daemon/dist/device-flow.d.ts +24 -0
- package/packages/daemon/dist/device-flow.d.ts.map +1 -0
- package/packages/daemon/dist/device-flow.js +128 -0
- package/packages/daemon/dist/device-flow.js.map +1 -0
- package/packages/daemon/dist/device-flow.test.d.ts +2 -0
- package/packages/daemon/dist/device-flow.test.d.ts.map +1 -0
- package/packages/daemon/dist/device-flow.test.js +172 -0
- package/packages/daemon/dist/device-flow.test.js.map +1 -0
- package/packages/daemon/package.json +5 -5
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +34 -0
- package/packages/gsd-cloud/bin/gsd-cloud.js +27 -0
- package/packages/gsd-cloud/package.json +38 -0
- 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/package.json +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/browser-tools/tests/gsd-browser-launch-config.test.mjs +19 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +22 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -0
- package/src/resources/extensions/gsd/auto.ts +12 -0
- package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +77 -1
- package/src/resources/extensions/gsd/reopen-reason.ts +109 -0
- package/src/resources/extensions/gsd/session-status-io.ts +34 -0
- package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/reopen-reason.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/worker-resume-wait.test.ts +73 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +15 -0
- package/src/resources/extensions/shared/gsd-browser-cli.ts +35 -7
- /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → enWQFRY-aLTUq3wmxG2Qz}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → enWQFRY-aLTUq3wmxG2Qz}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// GSD — reopen-reason injection tests (#1272)
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdtempSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
openDatabase,
|
|
12
|
+
closeDatabase,
|
|
13
|
+
insertMilestone,
|
|
14
|
+
insertSlice,
|
|
15
|
+
insertTask,
|
|
16
|
+
} from '../gsd-db.ts';
|
|
17
|
+
import { handleReopenTask } from '../tools/reopen-task.ts';
|
|
18
|
+
import {
|
|
19
|
+
reopenReasonArtifactPath,
|
|
20
|
+
writeReopenReason,
|
|
21
|
+
claimReopenReasonForInjection,
|
|
22
|
+
} from '../reopen-reason.ts';
|
|
23
|
+
|
|
24
|
+
function makeTmpBase(): string {
|
|
25
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-reopen-reason-'));
|
|
26
|
+
mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks'), { recursive: true });
|
|
27
|
+
return base;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanup(base: string): void {
|
|
31
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
32
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function seedCompleteTask(): void {
|
|
36
|
+
insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
|
|
37
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'in_progress' });
|
|
38
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Task One', status: 'complete' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('writeReopenReason then claim: injects the reason, then is one-shot', () => {
|
|
42
|
+
const base = makeTmpBase();
|
|
43
|
+
try {
|
|
44
|
+
const reason = 'T01 shifted the descendants count to 12. Fix: update the assertion to 12.';
|
|
45
|
+
writeReopenReason(base, 'M001', 'S01', 'T01', reason);
|
|
46
|
+
|
|
47
|
+
const path = reopenReasonArtifactPath(base, 'M001', 'S01', 'T01')!;
|
|
48
|
+
assert.ok(existsSync(path), 'artifact should be written');
|
|
49
|
+
|
|
50
|
+
const claimed = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
|
|
51
|
+
assert.ok(claimed, 'first claim should return an injection block');
|
|
52
|
+
assert.match(claimed!.injectionBlock, /Reopened — Reason/);
|
|
53
|
+
assert.match(claimed!.injectionBlock, /descendants count to 12/);
|
|
54
|
+
assert.ok(!existsSync(path), 'artifact should be deleted after claim (one-shot)');
|
|
55
|
+
|
|
56
|
+
const second = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
|
|
57
|
+
assert.equal(second, null, 'second claim should return null');
|
|
58
|
+
} finally {
|
|
59
|
+
cleanup(base);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('claimReopenReasonForInjection: returns null when nothing pending', () => {
|
|
64
|
+
const base = makeTmpBase();
|
|
65
|
+
try {
|
|
66
|
+
assert.equal(claimReopenReasonForInjection(base, 'M001', 'S01', 'T01'), null);
|
|
67
|
+
} finally {
|
|
68
|
+
cleanup(base);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('writeReopenReason: empty/whitespace reason is a no-op', () => {
|
|
73
|
+
const base = makeTmpBase();
|
|
74
|
+
try {
|
|
75
|
+
writeReopenReason(base, 'M001', 'S01', 'T01', ' ');
|
|
76
|
+
const path = reopenReasonArtifactPath(base, 'M001', 'S01', 'T01')!;
|
|
77
|
+
assert.ok(!existsSync(path), 'no artifact should be written for an empty reason');
|
|
78
|
+
} finally {
|
|
79
|
+
cleanup(base);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('handleReopenTask: persists a claimable reopen reason when reason is provided', async () => {
|
|
84
|
+
const base = makeTmpBase();
|
|
85
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
86
|
+
try {
|
|
87
|
+
seedCompleteTask();
|
|
88
|
+
|
|
89
|
+
const result = await handleReopenTask({
|
|
90
|
+
milestoneId: 'M001',
|
|
91
|
+
sliceId: 'S01',
|
|
92
|
+
taskId: 'T01',
|
|
93
|
+
reason: 'Full suite caught NavNodeTest regression — update count assertion to 12.',
|
|
94
|
+
}, base);
|
|
95
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
96
|
+
|
|
97
|
+
const claimed = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
|
|
98
|
+
assert.ok(claimed, 'reopen reason should be claimable after handleReopenTask');
|
|
99
|
+
assert.match(claimed!.injectionBlock, /NavNodeTest regression/);
|
|
100
|
+
} finally {
|
|
101
|
+
cleanup(base);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('handleReopenTask: no reason provided leaves nothing to claim', async () => {
|
|
106
|
+
const base = makeTmpBase();
|
|
107
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
108
|
+
try {
|
|
109
|
+
seedCompleteTask();
|
|
110
|
+
|
|
111
|
+
await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: 'T01' }, base);
|
|
112
|
+
|
|
113
|
+
assert.equal(claimReopenReasonForInjection(base, 'M001', 'S01', 'T01'), null);
|
|
114
|
+
} finally {
|
|
115
|
+
cleanup(base);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests: awaitWorkerResume — headless worker pause survives without a resumer.
|
|
3
|
+
*
|
|
4
|
+
* Regression for #1273: a worker `pause` used to call a terminal pauseAuto()
|
|
5
|
+
* that exited the process with BLOCKED (10). On the headless path there is no
|
|
6
|
+
* resumer to send the matching `resume`, so the worker was stranded after its
|
|
7
|
+
* first unit. awaitWorkerResume lets the worker wait for the coordinator to lift
|
|
8
|
+
* the pause and, when no resumer responds, report "timeout" so the caller can
|
|
9
|
+
* degrade to in-process serialization instead of halting forever.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { describe, it } from "node:test";
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
|
|
18
|
+
import { awaitWorkerResume, sendSignal } from "../session-status-io.ts";
|
|
19
|
+
|
|
20
|
+
function makeBase(): { base: string; cleanup: () => void } {
|
|
21
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-worker-resume-"));
|
|
22
|
+
mkdirSync(join(base, ".gsd", "parallel"), { recursive: true });
|
|
23
|
+
return { base, cleanup: () => rmSync(base, { recursive: true, force: true }) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("awaitWorkerResume", () => {
|
|
27
|
+
it("returns 'resume' when the coordinator sends resume", async () => {
|
|
28
|
+
const { base, cleanup } = makeBase();
|
|
29
|
+
try {
|
|
30
|
+
sendSignal(base, "M001", "resume");
|
|
31
|
+
const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 500, pollMs: 20 });
|
|
32
|
+
assert.equal(outcome, "resume");
|
|
33
|
+
} finally {
|
|
34
|
+
cleanup();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns 'stop' when the coordinator sends stop", async () => {
|
|
39
|
+
const { base, cleanup } = makeBase();
|
|
40
|
+
try {
|
|
41
|
+
sendSignal(base, "M001", "stop");
|
|
42
|
+
const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 500, pollMs: 20 });
|
|
43
|
+
assert.equal(outcome, "stop");
|
|
44
|
+
} finally {
|
|
45
|
+
cleanup();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns 'timeout' when no resumer lifts the pause (headless, #1273)", async () => {
|
|
50
|
+
const { base, cleanup } = makeBase();
|
|
51
|
+
try {
|
|
52
|
+
const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 100, pollMs: 20 });
|
|
53
|
+
assert.equal(outcome, "timeout");
|
|
54
|
+
} finally {
|
|
55
|
+
cleanup();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("ignores a repeated pause and still resumes when resume arrives", async () => {
|
|
60
|
+
const { base, cleanup } = makeBase();
|
|
61
|
+
try {
|
|
62
|
+
// A duplicate pause must not be treated as a resume, nor reset the wait.
|
|
63
|
+
sendSignal(base, "M001", "pause");
|
|
64
|
+
const waiting = awaitWorkerResume(base, "M001", { timeoutMs: 1000, pollMs: 20 });
|
|
65
|
+
// Deliver the resume shortly after the pause is consumed and ignored.
|
|
66
|
+
setTimeout(() => sendSignal(base, "M001", "resume"), 60);
|
|
67
|
+
const outcome = await waiting;
|
|
68
|
+
assert.equal(outcome, "resume");
|
|
69
|
+
} finally {
|
|
70
|
+
cleanup();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -23,6 +23,7 @@ import { flushWorkflowProjections } from "../projection-flush.js";
|
|
|
23
23
|
import { writeManifest } from "../workflow-manifest.js";
|
|
24
24
|
import { appendEvent } from "../workflow-events.js";
|
|
25
25
|
import { logWarning } from "../workflow-logger.js";
|
|
26
|
+
import { writeReopenReason } from "../reopen-reason.js";
|
|
26
27
|
import { existsSync, unlinkSync } from "node:fs";
|
|
27
28
|
import { join, basename, dirname } from "node:path";
|
|
28
29
|
import { resolveTasksDir, resolveSlicePath, clearPathCache } from "../paths.js";
|
|
@@ -125,6 +126,20 @@ export async function handleReopenTask(
|
|
|
125
126
|
}
|
|
126
127
|
clearPathCache();
|
|
127
128
|
|
|
129
|
+
// ── Persist the reopen reason for the next execute-task dispatch (#1272) ──
|
|
130
|
+
// Without this, the re-dispatched executor receives the original task plan
|
|
131
|
+
// and verify command with zero signal that it was reopened — it re-runs the
|
|
132
|
+
// (still-green) scoped verify and re-completes the task, never touching the
|
|
133
|
+
// regression the gate actually caught. buildExecuteTaskPrompt claims this
|
|
134
|
+
// artifact and prepends the diagnosis to the next dispatch.
|
|
135
|
+
if (params.reason && params.reason.trim() !== "") {
|
|
136
|
+
try {
|
|
137
|
+
writeReopenReason(basePath, params.milestoneId, params.sliceId, params.taskId, params.reason);
|
|
138
|
+
} catch (reasonErr) {
|
|
139
|
+
logWarning("tool", `reopen-task reason persistence warning: ${(reasonErr as Error).message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
// ── Post-mutation hook ───────────────────────────────────────────────────
|
|
129
144
|
try {
|
|
130
145
|
await flushWorkflowProjections(basePath, { milestoneId: params.milestoneId });
|
|
@@ -355,14 +355,16 @@ export function resolveGsdBrowserMcpLaunchConfig(
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
|
-
* CLI invocation that
|
|
359
|
-
* session and identity flags as
|
|
360
|
-
*
|
|
358
|
+
* CLI invocation that runs a `daemon <action>` command against the gsd-browser
|
|
359
|
+
* session daemon with the same session and identity flags as
|
|
360
|
+
* {@link resolveGsdBrowserMcpLaunchConfig}, so start (warm-up) and stop
|
|
361
|
+
* (teardown) address the exact same managed daemon.
|
|
361
362
|
*/
|
|
362
|
-
|
|
363
|
+
function resolveGsdBrowserDaemonInvocation(
|
|
364
|
+
action: "start" | "stop",
|
|
363
365
|
projectRoot: string,
|
|
364
|
-
env: NodeJS.ProcessEnv
|
|
365
|
-
options: GsdBrowserMcpLaunchOptions
|
|
366
|
+
env: NodeJS.ProcessEnv,
|
|
367
|
+
options: GsdBrowserMcpLaunchOptions,
|
|
366
368
|
): Pick<GsdBrowserMcpLaunchConfig, "command" | "args" | "cwd" | "env"> {
|
|
367
369
|
const launch = resolveGsdBrowserMcpLaunchConfig(projectRoot, env, options);
|
|
368
370
|
const mcpIndex = launch.args.indexOf("mcp");
|
|
@@ -374,8 +376,34 @@ export function resolveGsdBrowserDaemonStartInvocation(
|
|
|
374
376
|
const sessionFlags = launch.args.slice(mcpIndex + 1);
|
|
375
377
|
return {
|
|
376
378
|
command: launch.command,
|
|
377
|
-
args: [...prefix, "daemon",
|
|
379
|
+
args: [...prefix, "daemon", action, ...sessionFlags],
|
|
378
380
|
cwd: launch.cwd,
|
|
379
381
|
...(launch.env ? { env: launch.env } : {}),
|
|
380
382
|
};
|
|
381
383
|
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* CLI invocation that starts the gsd-browser session daemon with the same
|
|
387
|
+
* session and identity flags as {@link resolveGsdBrowserMcpLaunchConfig}, so
|
|
388
|
+
* browser UAT can warm Chrome/CDP before the first MCP navigation.
|
|
389
|
+
*/
|
|
390
|
+
export function resolveGsdBrowserDaemonStartInvocation(
|
|
391
|
+
projectRoot: string,
|
|
392
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
393
|
+
options: GsdBrowserMcpLaunchOptions = {},
|
|
394
|
+
): Pick<GsdBrowserMcpLaunchConfig, "command" | "args" | "cwd" | "env"> {
|
|
395
|
+
return resolveGsdBrowserDaemonInvocation("start", projectRoot, env, options);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* CLI invocation that stops the gsd-browser session daemon warmed for browser
|
|
400
|
+
* UAT, so the Chrome process is torn down instead of lingering after the
|
|
401
|
+
* triggering task/session ends.
|
|
402
|
+
*/
|
|
403
|
+
export function resolveGsdBrowserDaemonStopInvocation(
|
|
404
|
+
projectRoot: string,
|
|
405
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
406
|
+
options: GsdBrowserMcpLaunchOptions = {},
|
|
407
|
+
): Pick<GsdBrowserMcpLaunchConfig, "command" | "args" | "cwd" | "env"> {
|
|
408
|
+
return resolveGsdBrowserDaemonInvocation("stop", projectRoot, env, options);
|
|
409
|
+
}
|
|
File without changes
|
|
File without changes
|