gsd-pi 2.35.0-dev.30eec3f → 2.35.0-dev.640d5c7
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/cli.js +2 -7
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +1 -13
- package/dist/resources/extensions/async-jobs/await-tool.js +2 -0
- package/dist/resources/extensions/async-jobs/job-manager.js +6 -0
- package/dist/resources/extensions/bg-shell/output-formatter.js +19 -1
- package/dist/resources/extensions/bg-shell/process-manager.js +4 -0
- package/dist/resources/extensions/bg-shell/types.js +2 -0
- package/dist/resources/extensions/context7/index.js +0 -5
- package/dist/resources/extensions/get-secrets-from-user.js +30 -2
- package/dist/resources/extensions/google-search/index.js +0 -5
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -43
- package/dist/resources/extensions/gsd/auto-loop.js +1 -10
- package/dist/resources/extensions/gsd/auto-recovery.js +0 -35
- package/dist/resources/extensions/gsd/auto-start.js +2 -35
- package/dist/resources/extensions/gsd/auto.js +4 -59
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/doctor-environment.js +17 -26
- package/dist/resources/extensions/gsd/files.js +1 -9
- package/dist/resources/extensions/gsd/gitignore.js +7 -54
- package/dist/resources/extensions/gsd/guided-flow.js +1 -1
- package/dist/resources/extensions/gsd/health-widget.js +46 -97
- package/dist/resources/extensions/gsd/index.js +1 -10
- package/dist/resources/extensions/gsd/migrate-external.js +2 -55
- package/dist/resources/extensions/gsd/paths.js +7 -74
- package/dist/resources/extensions/gsd/post-unit-hooks.js +1 -4
- package/dist/resources/extensions/gsd/preferences-validation.js +1 -16
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
- package/dist/resources/extensions/gsd/session-lock.js +2 -26
- package/dist/resources/extensions/gsd/templates/plan.md +0 -8
- package/dist/resources/extensions/gsd/worktree-resolver.js +0 -12
- package/dist/resources/extensions/remote-questions/remote-command.js +22 -2
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +0 -30
- package/dist/resources/extensions/shared/wizard-ui.js +478 -0
- package/dist/resources/extensions/subagent/index.js +14 -6
- package/package.json +1 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +2 -13
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/resource-loader.ts +2 -13
- package/src/resources/extensions/async-jobs/await-tool.ts +2 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +7 -0
- package/src/resources/extensions/bg-shell/output-formatter.ts +17 -0
- package/src/resources/extensions/bg-shell/process-manager.ts +4 -0
- package/src/resources/extensions/bg-shell/types.ts +12 -0
- package/src/resources/extensions/context7/index.ts +0 -7
- package/src/resources/extensions/get-secrets-from-user.ts +35 -2
- package/src/resources/extensions/google-search/index.ts +0 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -49
- package/src/resources/extensions/gsd/auto-loop.ts +1 -11
- package/src/resources/extensions/gsd/auto-recovery.ts +0 -39
- package/src/resources/extensions/gsd/auto-start.ts +2 -42
- package/src/resources/extensions/gsd/auto.ts +3 -61
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/doctor-environment.ts +16 -26
- package/src/resources/extensions/gsd/files.ts +1 -10
- package/src/resources/extensions/gsd/gitignore.ts +7 -54
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/health-widget.ts +59 -103
- package/src/resources/extensions/gsd/index.ts +1 -10
- package/src/resources/extensions/gsd/migrate-external.ts +2 -47
- package/src/resources/extensions/gsd/paths.ts +7 -73
- package/src/resources/extensions/gsd/post-unit-hooks.ts +1 -5
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -16
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
- package/src/resources/extensions/gsd/session-lock.ts +2 -29
- package/src/resources/extensions/gsd/templates/plan.md +0 -8
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -12
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +0 -15
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -32
- package/src/resources/extensions/gsd/worktree-resolver.ts +0 -11
- package/src/resources/extensions/remote-questions/remote-command.ts +23 -2
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +0 -36
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/subagent/index.ts +12 -6
- package/dist/resources/extensions/gsd/health-widget-core.js +0 -96
- package/dist/resources/extensions/gsd/roadmap-mutations.js +0 -55
- package/src/resources/extensions/gsd/health-widget-core.ts +0 -129
- package/src/resources/extensions/gsd/roadmap-mutations.ts +0 -66
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +0 -214
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +0 -158
- package/src/resources/extensions/gsd/tests/paths.test.ts +0 -113
- package/src/resources/extensions/gsd/tests/test-utils.ts +0 -165
package/dist/cli.js
CHANGED
|
@@ -327,10 +327,7 @@ if (isPrintMode) {
|
|
|
327
327
|
markStartup('createAgentSession');
|
|
328
328
|
if (extensionsResult.errors.length > 0) {
|
|
329
329
|
for (const err of extensionsResult.errors) {
|
|
330
|
-
|
|
331
|
-
const isSuperseded = err.error.includes("supersedes");
|
|
332
|
-
const prefix = isSuperseded ? "Extension conflict" : "Extension load error";
|
|
333
|
-
process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
|
|
330
|
+
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
|
|
334
331
|
}
|
|
335
332
|
}
|
|
336
333
|
// Apply --model override if specified
|
|
@@ -459,9 +456,7 @@ const { session, extensionsResult } = await createAgentSession({
|
|
|
459
456
|
markStartup('createAgentSession');
|
|
460
457
|
if (extensionsResult.errors.length > 0) {
|
|
461
458
|
for (const err of extensionsResult.errors) {
|
|
462
|
-
|
|
463
|
-
const prefix = isSuperseded ? "Extension conflict" : "Extension load error";
|
|
464
|
-
process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
|
|
459
|
+
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
|
|
465
460
|
}
|
|
466
461
|
}
|
|
467
462
|
// Restore scoped models from settings on startup.
|
|
@@ -9,7 +9,7 @@ export declare function getNewerManagedResourceVersion(agentDir: string, current
|
|
|
9
9
|
* - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
|
|
10
10
|
* - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
|
|
11
11
|
* - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
|
|
12
|
-
* - GSD-WORKFLOW.md
|
|
12
|
+
* - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
|
|
13
13
|
*
|
|
14
14
|
* Skips the copy when the managed-resources.json version matches the current
|
|
15
15
|
* GSD version, avoiding ~128ms of synchronous cpSync on every startup.
|
package/dist/resource-loader.js
CHANGED
|
@@ -18,9 +18,6 @@ import { loadRegistry, readManifestFromEntryPath, isExtensionEnabled, ensureRegi
|
|
|
18
18
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
19
|
const distResources = join(packageRoot, 'dist', 'resources');
|
|
20
20
|
const srcResources = join(packageRoot, 'src', 'resources');
|
|
21
|
-
// Use dist/resources only if it has the full expected structure.
|
|
22
|
-
// A partial build (tsc without copy-resources) creates dist/resources/extensions/
|
|
23
|
-
// but not agents/ or skills/, causing initResources to sync from an incomplete source.
|
|
24
21
|
const resourcesDir = (existsSync(distResources) && existsSync(join(distResources, 'agents')))
|
|
25
22
|
? distResources
|
|
26
23
|
: srcResources;
|
|
@@ -223,7 +220,7 @@ function copyDirRecursive(src, dest) {
|
|
|
223
220
|
* - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
|
|
224
221
|
* - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
|
|
225
222
|
* - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
|
|
226
|
-
* - GSD-WORKFLOW.md
|
|
223
|
+
* - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
|
|
227
224
|
*
|
|
228
225
|
* Skips the copy when the managed-resources.json version matches the current
|
|
229
226
|
* GSD version, avoiding ~128ms of synchronous cpSync on every startup.
|
|
@@ -250,15 +247,6 @@ export function initResources(agentDir) {
|
|
|
250
247
|
syncResourceDir(bundledExtensionsDir, join(agentDir, 'extensions'));
|
|
251
248
|
syncResourceDir(join(resourcesDir, 'agents'), join(agentDir, 'agents'));
|
|
252
249
|
syncResourceDir(join(resourcesDir, 'skills'), join(agentDir, 'skills'));
|
|
253
|
-
// Sync GSD-WORKFLOW.md to agentDir as a fallback for when GSD_WORKFLOW_PATH
|
|
254
|
-
// env var is not set (e.g. fork/dev builds, alternative entry points).
|
|
255
|
-
const workflowSrc = join(resourcesDir, 'GSD-WORKFLOW.md');
|
|
256
|
-
if (existsSync(workflowSrc)) {
|
|
257
|
-
try {
|
|
258
|
-
copyFileSync(workflowSrc, join(agentDir, 'GSD-WORKFLOW.md'));
|
|
259
|
-
}
|
|
260
|
-
catch { /* non-fatal */ }
|
|
261
|
-
}
|
|
262
250
|
// Ensure all newly copied files are owner-writable so the next run can
|
|
263
251
|
// overwrite them (covers extensions, agents, and skills in one walk).
|
|
264
252
|
makeTreeWritable(agentDir);
|
|
@@ -52,12 +52,14 @@ export function createAwaitTool(getManager) {
|
|
|
52
52
|
const running = watched.filter((j) => j.status === "running");
|
|
53
53
|
if (running.length === 0) {
|
|
54
54
|
const result = formatResults(watched);
|
|
55
|
+
manager.acknowledgeDeliveries(watched.map((j) => j.id));
|
|
55
56
|
return { content: [{ type: "text", text: result }], details: undefined };
|
|
56
57
|
}
|
|
57
58
|
// Wait for at least one to complete
|
|
58
59
|
await Promise.race(running.map((j) => j.promise));
|
|
59
60
|
// Collect all completed results (more may have finished while waiting)
|
|
60
61
|
const completed = watched.filter((j) => j.status !== "running");
|
|
62
|
+
manager.acknowledgeDeliveries(completed.map((j) => j.id));
|
|
61
63
|
const stillRunning = watched.filter((j) => j.status === "running");
|
|
62
64
|
let result = formatResults(completed);
|
|
63
65
|
if (stillRunning.length > 0) {
|
|
@@ -101,6 +101,12 @@ export class AsyncJobManager {
|
|
|
101
101
|
getAllJobs() {
|
|
102
102
|
return [...this.jobs.values()];
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* No-op. Retained for API compatibility with await_job tool.
|
|
106
|
+
*/
|
|
107
|
+
acknowledgeDeliveries(_jobIds) {
|
|
108
|
+
// Delivery is fire-once; no retries to cancel.
|
|
109
|
+
}
|
|
104
110
|
/**
|
|
105
111
|
* Cleanup all timers and resources.
|
|
106
112
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Output analysis, digest generation, highlights extraction, and output retrieval.
|
|
3
3
|
*/
|
|
4
4
|
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, } from "@gsd/pi-coding-agent";
|
|
5
|
-
import { ERROR_PATTERN_UNION, WARNING_PATTERN_UNION, READINESS_PATTERN_UNION, BUILD_COMPLETE_PATTERN_UNION, TEST_RESULT_PATTERN_UNION, URL_PATTERN, PORT_PATTERN_SOURCE, } from "./types.js";
|
|
5
|
+
import { ERROR_PATTERN_UNION, WARNING_PATTERN_UNION, READINESS_PATTERN_UNION, BUILD_COMPLETE_PATTERN_UNION, TEST_RESULT_PATTERN_UNION, URL_PATTERN, PORT_PATTERN_SOURCE, LINE_DEDUP_MAX, } from "./types.js";
|
|
6
6
|
import { addEvent, pushAlert } from "./process-manager.js";
|
|
7
7
|
import { transitionToReady } from "./readiness-detector.js";
|
|
8
8
|
import { formatUptime, formatTimeAgo } from "./utilities.js";
|
|
@@ -78,6 +78,24 @@ export function analyzeLine(bg, line, stream) {
|
|
|
78
78
|
pushAlert(bg, "recovered — errors cleared");
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
// Dedup tracking — evict oldest entry when map exceeds LINE_DEDUP_MAX (LRU via Map insertion order)
|
|
82
|
+
bg.totalRawLines++;
|
|
83
|
+
const lineHash = line.trim().slice(0, 100);
|
|
84
|
+
const existing = bg.lineDedup.get(lineHash);
|
|
85
|
+
if (existing !== undefined) {
|
|
86
|
+
// Re-insert to update insertion order (move to tail = most recent)
|
|
87
|
+
bg.lineDedup.delete(lineHash);
|
|
88
|
+
bg.lineDedup.set(lineHash, existing + 1);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (bg.lineDedup.size >= LINE_DEDUP_MAX) {
|
|
92
|
+
// Evict oldest entry (Map iteration order = insertion order = LRU at head)
|
|
93
|
+
const oldest = bg.lineDedup.keys().next().value;
|
|
94
|
+
if (oldest !== undefined)
|
|
95
|
+
bg.lineDedup.delete(oldest);
|
|
96
|
+
}
|
|
97
|
+
bg.lineDedup.set(lineHash, 1);
|
|
98
|
+
}
|
|
81
99
|
}
|
|
82
100
|
// ── Digest Generation ──────────────────────────────────────────────────────
|
|
83
101
|
export function generateDigest(bg, mutate = false) {
|
|
@@ -135,8 +135,12 @@ export function startProcess(opts) {
|
|
|
135
135
|
group: opts.group || null,
|
|
136
136
|
lastErrorCount: 0,
|
|
137
137
|
lastWarningCount: 0,
|
|
138
|
+
commandHistory: [],
|
|
139
|
+
lineDedup: new Map(),
|
|
140
|
+
totalRawLines: 0,
|
|
138
141
|
stdoutLineCount: 0,
|
|
139
142
|
stderrLineCount: 0,
|
|
143
|
+
envKeys: Object.keys(opts.env || {}),
|
|
140
144
|
restartCount: 0,
|
|
141
145
|
startConfig: {
|
|
142
146
|
command,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export const MAX_BUFFER_LINES = 5000;
|
|
6
6
|
export const MAX_EVENTS = 200;
|
|
7
7
|
export const DEAD_PROCESS_TTL = 10 * 60 * 1000;
|
|
8
|
+
/** Maximum unique entries in the per-process lineDedup Map before LRU eviction. */
|
|
9
|
+
export const LINE_DEDUP_MAX = 500;
|
|
8
10
|
export const PORT_PROBE_TIMEOUT = 500;
|
|
9
11
|
export const READY_POLL_INTERVAL = 250;
|
|
10
12
|
export const DEFAULT_READY_TIMEOUT = 30000;
|
|
@@ -327,11 +327,6 @@ export default function (pi) {
|
|
|
327
327
|
return new Text(text, 0, 0);
|
|
328
328
|
},
|
|
329
329
|
});
|
|
330
|
-
// ── Session cleanup ─────────────────────────────────────────────────────
|
|
331
|
-
pi.on("session_shutdown", async () => {
|
|
332
|
-
searchCache.clear();
|
|
333
|
-
docCache.clear();
|
|
334
|
-
});
|
|
335
330
|
// ── Startup notification ─────────────────────────────────────────────────
|
|
336
331
|
pi.on("session_start", async (_event, ctx) => {
|
|
337
332
|
if (!getApiKey()) {
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
import { readFile, writeFile } from "node:fs/promises";
|
|
9
9
|
import { existsSync, statSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
-
import { Editor, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
|
|
11
|
+
import { CURSOR_MARKER, Editor, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
|
|
12
12
|
import { Type } from "@sinclair/typebox";
|
|
13
|
-
import { makeUI
|
|
13
|
+
import { makeUI } from "./shared/mod.js";
|
|
14
14
|
import { parseSecretsManifest, formatSecretsManifest } from "./gsd/files.js";
|
|
15
15
|
import { resolveMilestoneFile } from "./gsd/paths.js";
|
|
16
16
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -21,6 +21,34 @@ function maskPreview(value) {
|
|
|
21
21
|
return "*".repeat(value.length);
|
|
22
22
|
return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Replace editor visible text with masked characters while preserving ANSI cursor/sequencer codes.
|
|
26
|
+
*/
|
|
27
|
+
function maskEditorLine(line) {
|
|
28
|
+
// Keep border / metadata lines readable.
|
|
29
|
+
if (line.startsWith("─")) {
|
|
30
|
+
return line;
|
|
31
|
+
}
|
|
32
|
+
let output = "";
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < line.length) {
|
|
35
|
+
if (line.startsWith(CURSOR_MARKER, i)) {
|
|
36
|
+
output += CURSOR_MARKER;
|
|
37
|
+
i += CURSOR_MARKER.length;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
|
|
41
|
+
if (ansiMatch) {
|
|
42
|
+
output += ansiMatch[0];
|
|
43
|
+
i += ansiMatch[0].length;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const ch = line[i];
|
|
47
|
+
output += ch === " " ? " " : "*";
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
return output;
|
|
51
|
+
}
|
|
24
52
|
function shellEscapeSingle(value) {
|
|
25
53
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
26
54
|
}
|
|
@@ -326,11 +326,6 @@ export default function (pi) {
|
|
|
326
326
|
return new Text(text, 0, 0);
|
|
327
327
|
},
|
|
328
328
|
});
|
|
329
|
-
// ── Session cleanup ─────────────────────────────────────────────────────
|
|
330
|
-
pi.on("session_shutdown", async () => {
|
|
331
|
-
resultCache.clear();
|
|
332
|
-
client = null;
|
|
333
|
-
});
|
|
334
329
|
// ── Startup notification ─────────────────────────────────────────────────
|
|
335
330
|
pi.on("session_start", async (_event, ctx) => {
|
|
336
331
|
if (process.env.GEMINI_API_KEY)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* data structure that is inspectable, testable per-rule, and extensible
|
|
9
9
|
* without modifying orchestration code.
|
|
10
10
|
*/
|
|
11
|
-
import { loadFile, loadActiveOverrides
|
|
11
|
+
import { loadFile, loadActiveOverrides } from "./files.js";
|
|
12
12
|
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relSliceFile, buildMilestoneFileName, } from "./paths.js";
|
|
13
13
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
@@ -274,28 +274,6 @@ const DISPATCH_RULES = [
|
|
|
274
274
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
275
275
|
if (state.phase !== "validating-milestone")
|
|
276
276
|
return null;
|
|
277
|
-
// Safety guard (#1368): verify all roadmap slices have SUMMARY files before
|
|
278
|
-
// allowing milestone validation. If any slice lacks a summary, the milestone
|
|
279
|
-
// is not genuinely complete — something skipped earlier slices.
|
|
280
|
-
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
281
|
-
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
282
|
-
if (roadmapContent) {
|
|
283
|
-
const roadmap = parseRoadmap(roadmapContent);
|
|
284
|
-
const missingSlices = [];
|
|
285
|
-
for (const slice of roadmap.slices) {
|
|
286
|
-
const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
|
|
287
|
-
if (!summaryPath || !existsSync(summaryPath)) {
|
|
288
|
-
missingSlices.push(slice.id);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (missingSlices.length > 0) {
|
|
292
|
-
return {
|
|
293
|
-
action: "stop",
|
|
294
|
-
reason: `Cannot validate milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. These slices may have been skipped.`,
|
|
295
|
-
level: "error",
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
277
|
// Skip preference: write a minimal pass-through VALIDATION file
|
|
300
278
|
if (prefs?.phases?.skip_milestone_validation) {
|
|
301
279
|
const mDir = resolveMilestonePath(basePath, mid);
|
|
@@ -330,26 +308,6 @@ const DISPATCH_RULES = [
|
|
|
330
308
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
331
309
|
if (state.phase !== "completing-milestone")
|
|
332
310
|
return null;
|
|
333
|
-
// Safety guard (#1368): verify all roadmap slices have SUMMARY files.
|
|
334
|
-
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
335
|
-
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
336
|
-
if (roadmapContent) {
|
|
337
|
-
const roadmap = parseRoadmap(roadmapContent);
|
|
338
|
-
const missingSlices = [];
|
|
339
|
-
for (const slice of roadmap.slices) {
|
|
340
|
-
const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
|
|
341
|
-
if (!summaryPath || !existsSync(summaryPath)) {
|
|
342
|
-
missingSlices.push(slice.id);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
if (missingSlices.length > 0) {
|
|
346
|
-
return {
|
|
347
|
-
action: "stop",
|
|
348
|
-
reason: `Cannot complete milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. Run /gsd doctor to diagnose.`,
|
|
349
|
-
level: "error",
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
311
|
return {
|
|
354
312
|
action: "dispatch",
|
|
355
313
|
unitType: "complete-milestone",
|
|
@@ -161,15 +161,6 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
|
|
|
161
161
|
const unitPromise = new Promise((resolve) => {
|
|
162
162
|
s.pendingResolve = resolve;
|
|
163
163
|
});
|
|
164
|
-
// Ensure cwd matches basePath before dispatch (#1389).
|
|
165
|
-
// async_bash and background jobs can drift cwd away from the worktree.
|
|
166
|
-
// Realigning here prevents commits from landing on the wrong branch.
|
|
167
|
-
try {
|
|
168
|
-
if (process.cwd() !== s.basePath) {
|
|
169
|
-
process.chdir(s.basePath);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch { /* non-fatal — chdir may fail if dir was removed */ }
|
|
173
164
|
// ── Send the prompt ──
|
|
174
165
|
debugLog("runUnit", { phase: "send-message", unitType, unitId });
|
|
175
166
|
pi.sendMessage({ customType: "gsd-auto", content: prompt, display: s.verbose }, { triggerTurn: true });
|
|
@@ -507,7 +498,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
507
498
|
}
|
|
508
499
|
// Secrets re-check gate
|
|
509
500
|
try {
|
|
510
|
-
const manifestStatus = await deps.getManifestStatus(s.basePath, mid
|
|
501
|
+
const manifestStatus = await deps.getManifestStatus(s.basePath, mid);
|
|
511
502
|
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
512
503
|
const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
513
504
|
if (result &&
|
|
@@ -6,13 +6,11 @@
|
|
|
6
6
|
* Pure functions that receive all needed state as parameters — no module-level
|
|
7
7
|
* globals or AutoContext dependency.
|
|
8
8
|
*/
|
|
9
|
-
import { parseUnitId } from "./unit-id.js";
|
|
10
9
|
import { clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
11
10
|
import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
|
|
12
11
|
import { isValidationTerminal } from "./state.js";
|
|
13
12
|
import { nativeConflictFiles, nativeCommit, nativeCheckoutTheirs, nativeAddPaths, nativeMergeAbort, nativeResetHard, } from "./native-git-bridge.js";
|
|
14
13
|
import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
|
|
15
|
-
import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
|
|
16
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, } from "node:fs";
|
|
17
15
|
import { dirname, join } from "node:path";
|
|
18
16
|
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
@@ -434,39 +432,6 @@ export async function selfHealRuntimeRecords(base, ctx) {
|
|
|
434
432
|
const now = Date.now();
|
|
435
433
|
for (const record of records) {
|
|
436
434
|
const { unitType, unitId } = record;
|
|
437
|
-
// Case 0: complete-slice with SUMMARY + UAT but unchecked roadmap (#1350).
|
|
438
|
-
// If a complete-slice was interrupted after writing artifacts but before
|
|
439
|
-
// flipping the roadmap checkbox, the verification fails and the dispatch
|
|
440
|
-
// loop relaunches the same unit forever. Auto-fix the checkbox.
|
|
441
|
-
if (unitType === "complete-slice") {
|
|
442
|
-
const { milestone: mid, slice: sid } = parseUnitId(unitId);
|
|
443
|
-
if (mid && sid) {
|
|
444
|
-
const dir = resolveSlicePath(base, mid, sid);
|
|
445
|
-
if (dir) {
|
|
446
|
-
const summaryPath = join(dir, buildSliceFileName(sid, "SUMMARY"));
|
|
447
|
-
const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
|
|
448
|
-
if (existsSync(summaryPath) && existsSync(uatPath)) {
|
|
449
|
-
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
450
|
-
if (roadmapFile && existsSync(roadmapFile)) {
|
|
451
|
-
try {
|
|
452
|
-
const roadmapContent = readFileSync(roadmapFile, "utf-8");
|
|
453
|
-
const roadmap = parseRoadmap(roadmapContent);
|
|
454
|
-
const slice = (roadmap.slices ?? []).find(s => s.id === sid);
|
|
455
|
-
if (slice && !slice.done) {
|
|
456
|
-
// Auto-fix: flip the checkbox using shared utility
|
|
457
|
-
if (markSliceDoneInRoadmap(base, mid, sid)) {
|
|
458
|
-
ctx.ui.notify(`Self-heal: marked ${sid} done in roadmap (SUMMARY + UAT exist but checkbox was stale).`, "info");
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
catch {
|
|
463
|
-
// Roadmap parse failure — don't block self-heal
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
435
|
// Clear stale dispatched records (dispatched > 1h ago, process crashed)
|
|
471
436
|
const age = now - (record.startedAt ?? 0);
|
|
472
437
|
if (record.phase === "dispatched" && age > STALE_THRESHOLD_MS) {
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
import { deriveState } from "./state.js";
|
|
12
12
|
import { loadFile, getManifestStatus } from "./files.js";
|
|
13
13
|
import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode, } from "./preferences.js";
|
|
14
|
-
import { ensureGsdSymlink } from "./repo-identity.js";
|
|
15
|
-
import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
|
|
16
14
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
17
15
|
import { gsdRoot, resolveMilestoneFile } from "./paths.js";
|
|
18
16
|
import { invalidateAllCaches } from "./cache.js";
|
|
@@ -44,12 +42,6 @@ import { sep as pathSep } from "node:path";
|
|
|
44
42
|
* Returns false if the bootstrap aborted (e.g., guided flow returned,
|
|
45
43
|
* concurrent session detected). Returns true when ready to dispatch.
|
|
46
44
|
*/
|
|
47
|
-
/** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
|
|
48
|
-
* Prevents the recursive dialog loop described in #1348 where
|
|
49
|
-
* bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto
|
|
50
|
-
* cycles indefinitely when the discuss workflow doesn't produce a milestone. */
|
|
51
|
-
let _consecutiveCompleteBootstraps = 0;
|
|
52
|
-
const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
|
|
53
45
|
export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps) {
|
|
54
46
|
const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildResolver, } = deps;
|
|
55
47
|
const lockResult = acquireSessionLock(base);
|
|
@@ -68,19 +60,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
68
60
|
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
69
61
|
nativeInit(base, mainBranch);
|
|
70
62
|
}
|
|
71
|
-
//
|
|
72
|
-
// Migration MUST run before ensureGitignore to avoid adding ".gsd" to
|
|
73
|
-
// .gitignore when .gsd/ is git-tracked (data-loss bug #1364).
|
|
74
|
-
recoverFailedMigration(base);
|
|
75
|
-
const migration = migrateToExternalState(base);
|
|
76
|
-
if (migration.error) {
|
|
77
|
-
ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
|
|
78
|
-
}
|
|
79
|
-
// Ensure symlink exists (handles fresh projects and post-migration)
|
|
80
|
-
ensureGsdSymlink(base);
|
|
81
|
-
// Ensure .gitignore has baseline patterns.
|
|
82
|
-
// ensureGitignore checks for git-tracked .gsd/ files and skips the
|
|
83
|
-
// ".gsd" pattern if the project intentionally tracks .gsd/ in git.
|
|
63
|
+
// Ensure .gitignore has baseline patterns
|
|
84
64
|
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
85
65
|
const commitDocs = gitPrefs?.commit_docs;
|
|
86
66
|
const manageGitignore = gitPrefs?.manage_gitignore;
|
|
@@ -206,16 +186,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
206
186
|
if (!hasSurvivorBranch) {
|
|
207
187
|
// No active work — start a new milestone via discuss flow
|
|
208
188
|
if (!state.activeMilestone || state.phase === "complete") {
|
|
209
|
-
// Guard against recursive dialog loop (#1348):
|
|
210
|
-
// If we've entered this branch multiple times in quick succession,
|
|
211
|
-
// the discuss workflow isn't producing a milestone. Break the cycle.
|
|
212
|
-
_consecutiveCompleteBootstraps++;
|
|
213
|
-
if (_consecutiveCompleteBootstraps > MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS) {
|
|
214
|
-
_consecutiveCompleteBootstraps = 0;
|
|
215
|
-
ctx.ui.notify("All milestones are complete and the discussion didn't produce a new one. " +
|
|
216
|
-
"Run /gsd to start a new milestone manually.", "warning");
|
|
217
|
-
return releaseLockAndReturn();
|
|
218
|
-
}
|
|
219
189
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
220
190
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
221
191
|
invalidateAllCaches();
|
|
@@ -223,7 +193,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
223
193
|
if (postState.activeMilestone &&
|
|
224
194
|
postState.phase !== "complete" &&
|
|
225
195
|
postState.phase !== "pre-planning") {
|
|
226
|
-
_consecutiveCompleteBootstraps = 0; // Successfully advanced past "complete"
|
|
227
196
|
state = postState;
|
|
228
197
|
}
|
|
229
198
|
else if (postState.activeMilestone &&
|
|
@@ -268,8 +237,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
268
237
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
269
238
|
return releaseLockAndReturn();
|
|
270
239
|
}
|
|
271
|
-
// Successfully resolved an active milestone — reset the re-entry guard
|
|
272
|
-
_consecutiveCompleteBootstraps = 0;
|
|
273
240
|
// ── Initialize session state ──
|
|
274
241
|
s.active = true;
|
|
275
242
|
s.stepMode = requestedStepMode;
|
|
@@ -378,7 +345,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
378
345
|
// Secrets collection gate
|
|
379
346
|
const mid = state.activeMilestone.id;
|
|
380
347
|
try {
|
|
381
|
-
const manifestStatus = await getManifestStatus(base, mid
|
|
348
|
+
const manifestStatus = await getManifestStatus(base, mid);
|
|
382
349
|
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
383
350
|
const result = await collectSecretsFromManifest(base, mid, ctx);
|
|
384
351
|
if (result &&
|
|
@@ -36,7 +36,7 @@ import { clearSkillSnapshot } from "./skill-discovery.js";
|
|
|
36
36
|
import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
|
|
37
37
|
import { initMetrics, resetMetrics, getLedger, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
|
|
38
38
|
import { join } from "node:path";
|
|
39
|
-
import { readFileSync, existsSync, mkdirSync
|
|
39
|
+
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
40
40
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
41
41
|
import { autoCommitCurrentBranch, captureIntegrationBranch, detectWorktreeName, getCurrentBranch, getMainBranch, setActiveMilestoneId, } from "./worktree.js";
|
|
42
42
|
import { GitServiceImpl } from "./git-service.js";
|
|
@@ -325,13 +325,6 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
325
325
|
resetHookState();
|
|
326
326
|
if (s.basePath)
|
|
327
327
|
clearPersistedHookState(s.basePath);
|
|
328
|
-
// Remove paused-session metadata if present (#1383)
|
|
329
|
-
try {
|
|
330
|
-
const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
|
|
331
|
-
if (existsSync(pausedPath))
|
|
332
|
-
unlinkSync(pausedPath);
|
|
333
|
-
}
|
|
334
|
-
catch { /* non-fatal */ }
|
|
335
328
|
s.active = false;
|
|
336
329
|
s.paused = false;
|
|
337
330
|
s.stepMode = false;
|
|
@@ -376,28 +369,10 @@ export async function pauseAuto(ctx, _pi) {
|
|
|
376
369
|
return;
|
|
377
370
|
clearUnitTimeout();
|
|
378
371
|
s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
|
|
379
|
-
|
|
380
|
-
// The fresh-start bootstrap checks for this file and restores worktree context.
|
|
381
|
-
try {
|
|
382
|
-
const pausedMeta = {
|
|
383
|
-
milestoneId: s.currentMilestoneId,
|
|
384
|
-
worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
|
|
385
|
-
originalBasePath: s.originalBasePath,
|
|
386
|
-
stepMode: s.stepMode,
|
|
387
|
-
pausedAt: new Date().toISOString(),
|
|
388
|
-
sessionFile: s.pausedSessionFile,
|
|
389
|
-
};
|
|
390
|
-
const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
|
|
391
|
-
mkdirSync(runtimeDir, { recursive: true });
|
|
392
|
-
writeFileSync(join(runtimeDir, "paused-session.json"), JSON.stringify(pausedMeta, null, 2), "utf-8");
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
// Non-fatal — resume will still work via full bootstrap, just without worktree context
|
|
396
|
-
}
|
|
397
|
-
if (lockBase()) {
|
|
398
|
-
releaseSessionLock(lockBase());
|
|
372
|
+
if (lockBase())
|
|
399
373
|
clearLock(lockBase());
|
|
400
|
-
|
|
374
|
+
if (lockBase())
|
|
375
|
+
releaseSessionLock(lockBase());
|
|
401
376
|
deregisterSigtermHandler();
|
|
402
377
|
s.active = false;
|
|
403
378
|
s.paused = true;
|
|
@@ -545,30 +520,6 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
545
520
|
// Escape stale worktree cwd from a previous milestone (#608).
|
|
546
521
|
base = escapeStaleWorktree(base);
|
|
547
522
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
548
|
-
// Check persisted paused-session first (#1383) — survives /exit.
|
|
549
|
-
if (!s.paused) {
|
|
550
|
-
try {
|
|
551
|
-
const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
|
|
552
|
-
if (existsSync(pausedPath)) {
|
|
553
|
-
const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
|
|
554
|
-
if (meta.milestoneId) {
|
|
555
|
-
s.currentMilestoneId = meta.milestoneId;
|
|
556
|
-
s.originalBasePath = meta.originalBasePath || base;
|
|
557
|
-
s.stepMode = meta.stepMode ?? requestedStepMode;
|
|
558
|
-
s.paused = true;
|
|
559
|
-
// Clean up the persisted file — we're consuming it
|
|
560
|
-
try {
|
|
561
|
-
unlinkSync(pausedPath);
|
|
562
|
-
}
|
|
563
|
-
catch { /* non-fatal */ }
|
|
564
|
-
ctx.ui.notify(`Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`, "info");
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
// Malformed or missing — proceed with fresh bootstrap
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
523
|
if (s.paused) {
|
|
573
524
|
const resumeLock = acquireSessionLock(base);
|
|
574
525
|
if (!resumeLock.acquired) {
|
|
@@ -803,12 +754,6 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
|
|
|
803
754
|
}, hookHardTimeoutMs);
|
|
804
755
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
805
756
|
ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
|
|
806
|
-
// Ensure cwd matches basePath before hook dispatch (#1389)
|
|
807
|
-
try {
|
|
808
|
-
if (process.cwd() !== s.basePath)
|
|
809
|
-
process.chdir(s.basePath);
|
|
810
|
-
}
|
|
811
|
-
catch { }
|
|
812
757
|
debugLog("dispatchHookUnit", {
|
|
813
758
|
phase: "send-message",
|
|
814
759
|
promptLength: hookPrompt.length,
|
|
@@ -15,7 +15,7 @@ import { isAutoActive } from "./auto.js";
|
|
|
15
15
|
import { projectRoot } from "./commands.js";
|
|
16
16
|
import { loadPrompt } from "./prompt-loader.js";
|
|
17
17
|
export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
18
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
18
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
19
19
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
20
20
|
const prompt = loadPrompt("doctor-heal", {
|
|
21
21
|
doctorSummary: reportText,
|
|
@@ -144,7 +144,7 @@ export async function handleTriage(ctx, pi, basePath) {
|
|
|
144
144
|
currentPlan: currentPlan || "(no active slice plan)",
|
|
145
145
|
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
146
146
|
});
|
|
147
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
147
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
148
148
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
149
149
|
pi.sendMessage({
|
|
150
150
|
customType: "gsd-triage",
|
|
@@ -148,36 +148,27 @@ function checkPortConflicts(basePath) {
|
|
|
148
148
|
// Try to detect ports from package.json scripts
|
|
149
149
|
const portsToCheck = new Set();
|
|
150
150
|
const pkgPath = join(basePath, "package.json");
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
portsToCheck.add(port);
|
|
151
|
+
if (existsSync(pkgPath)) {
|
|
152
|
+
try {
|
|
153
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
154
|
+
const scripts = pkg.scripts ?? {};
|
|
155
|
+
const scriptText = Object.values(scripts).join(" ");
|
|
156
|
+
// Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
|
|
157
|
+
const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
|
|
158
|
+
for (const m of portMatches) {
|
|
159
|
+
const port = parseInt(m[1], 10);
|
|
160
|
+
if (port >= 1024 && port <= 65535)
|
|
161
|
+
portsToCheck.add(port);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// parse failed — use defaults
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
|
-
|
|
170
|
-
// parse failed — skip port checks rather than using defaults
|
|
171
|
-
return [];
|
|
172
|
-
}
|
|
173
|
-
// If no ports found in scripts, check common defaults.
|
|
174
|
-
// Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
|
|
168
|
+
// If no ports found in scripts, check common defaults
|
|
175
169
|
if (portsToCheck.size === 0) {
|
|
176
|
-
for (const p of DEFAULT_DEV_PORTS)
|
|
177
|
-
if (p === 5000 && process.platform === "darwin")
|
|
178
|
-
continue;
|
|
170
|
+
for (const p of DEFAULT_DEV_PORTS)
|
|
179
171
|
portsToCheck.add(p);
|
|
180
|
-
}
|
|
181
172
|
}
|
|
182
173
|
for (const port of portsToCheck) {
|
|
183
174
|
const result = tryExec(`lsof -i :${port} -sTCP:LISTEN -t`, basePath);
|