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
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Roadmap Mutations — shared utilities for modifying roadmap checkbox state.
|
|
3
|
-
*
|
|
4
|
-
* Extracts the duplicated "flip slice checkbox" pattern that existed in
|
|
5
|
-
* doctor.ts, mechanical-completion.ts, and auto-recovery.ts.
|
|
6
|
-
*/
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
import { atomicWriteSync } from "./atomic-write.js";
|
|
9
|
-
import { resolveMilestoneFile } from "./paths.js";
|
|
10
|
-
import { clearParseCache } from "./files.js";
|
|
11
|
-
/**
|
|
12
|
-
* Mark a slice as done ([x]) in the milestone roadmap.
|
|
13
|
-
* Idempotent — no-op if already checked or if the slice isn't found.
|
|
14
|
-
*
|
|
15
|
-
* @returns true if the roadmap was modified, false if no change was needed
|
|
16
|
-
*/
|
|
17
|
-
export function markSliceDoneInRoadmap(basePath, mid, sid) {
|
|
18
|
-
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
19
|
-
if (!roadmapFile)
|
|
20
|
-
return false;
|
|
21
|
-
let content;
|
|
22
|
-
try {
|
|
23
|
-
content = readFileSync(roadmapFile, "utf-8");
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${sid}:`, "m"), `$1[x] **${sid}:`);
|
|
29
|
-
if (updated === content)
|
|
30
|
-
return false;
|
|
31
|
-
atomicWriteSync(roadmapFile, updated);
|
|
32
|
-
clearParseCache();
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Mark a task as done ([x]) in the slice plan.
|
|
37
|
-
* Idempotent — no-op if already checked or if the task isn't found.
|
|
38
|
-
*
|
|
39
|
-
* @returns true if the plan was modified, false if no change was needed
|
|
40
|
-
*/
|
|
41
|
-
export function markTaskDoneInPlan(basePath, planPath, tid) {
|
|
42
|
-
let content;
|
|
43
|
-
try {
|
|
44
|
-
content = readFileSync(planPath, "utf-8");
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${tid}:`, "m"), `$1[x] **${tid}:`);
|
|
50
|
-
if (updated === content)
|
|
51
|
-
return false;
|
|
52
|
-
atomicWriteSync(planPath, updated);
|
|
53
|
-
clearParseCache();
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure GSD health widget logic.
|
|
3
|
-
*
|
|
4
|
-
* Separates project-state detection and line rendering from the widget's
|
|
5
|
-
* runtime integrations so the regressions can be tested directly.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
-
import { gsdRoot } from "./paths.js";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import type { GSDState, Phase } from "./types.js";
|
|
12
|
-
|
|
13
|
-
export type HealthWidgetProjectState = "none" | "initialized" | "active";
|
|
14
|
-
|
|
15
|
-
export interface HealthWidgetData {
|
|
16
|
-
projectState: HealthWidgetProjectState;
|
|
17
|
-
budgetCeiling: number | undefined;
|
|
18
|
-
budgetSpent: number;
|
|
19
|
-
providerIssue: string | null;
|
|
20
|
-
environmentErrorCount: number;
|
|
21
|
-
environmentWarningCount: number;
|
|
22
|
-
lastRefreshed: number;
|
|
23
|
-
executionPhase?: Phase;
|
|
24
|
-
executionStatus?: string;
|
|
25
|
-
executionTarget?: string;
|
|
26
|
-
nextAction?: string;
|
|
27
|
-
blocker?: string | null;
|
|
28
|
-
activeMilestoneId?: string;
|
|
29
|
-
activeSliceId?: string;
|
|
30
|
-
activeTaskId?: string;
|
|
31
|
-
progress?: GSDState["progress"];
|
|
32
|
-
eta?: string | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function detectHealthWidgetProjectState(basePath: string): HealthWidgetProjectState {
|
|
36
|
-
const root = gsdRoot(basePath);
|
|
37
|
-
if (!existsSync(root)) return "none";
|
|
38
|
-
|
|
39
|
-
// Lightweight milestone count — avoids the full detectProjectState() scan
|
|
40
|
-
// (CI markers, Makefile targets, etc.) that is unnecessary on the 60s refresh.
|
|
41
|
-
try {
|
|
42
|
-
const milestonesDir = join(root, "milestones");
|
|
43
|
-
if (existsSync(milestonesDir)) {
|
|
44
|
-
const entries = readdirSync(milestonesDir, { withFileTypes: true });
|
|
45
|
-
if (entries.some(e => e.isDirectory())) return "active";
|
|
46
|
-
}
|
|
47
|
-
} catch { /* non-fatal */ }
|
|
48
|
-
|
|
49
|
-
return "initialized";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function formatCost(n: number): string {
|
|
53
|
-
return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function formatProgress(progress?: GSDState["progress"]): string | null {
|
|
57
|
-
if (!progress) return null;
|
|
58
|
-
|
|
59
|
-
const parts: string[] = [];
|
|
60
|
-
parts.push(`M ${progress.milestones.done}/${progress.milestones.total}`);
|
|
61
|
-
if (progress.slices) parts.push(`S ${progress.slices.done}/${progress.slices.total}`);
|
|
62
|
-
if (progress.tasks) parts.push(`T ${progress.tasks.done}/${progress.tasks.total}`);
|
|
63
|
-
return parts.length > 0 ? `Progress: ${parts.join(" · ")}` : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function formatEnvironmentSummary(errorCount: number, warningCount: number): string | null {
|
|
67
|
-
if (errorCount <= 0 && warningCount <= 0) return null;
|
|
68
|
-
|
|
69
|
-
const parts: string[] = [];
|
|
70
|
-
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
71
|
-
if (warningCount > 0) parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
72
|
-
return `Env: ${parts.join(", ")}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function formatBudgetSummary(data: HealthWidgetData): string | null {
|
|
76
|
-
if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
|
|
77
|
-
const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
|
|
78
|
-
return `Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`;
|
|
79
|
-
}
|
|
80
|
-
if (data.budgetSpent > 0) {
|
|
81
|
-
return `Spent: ${formatCost(data.budgetSpent)}`;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildExecutionHeadline(data: HealthWidgetData): string {
|
|
87
|
-
const status = data.executionStatus ?? "Active project";
|
|
88
|
-
const target = data.executionTarget ?? data.blocker ?? "loading status…";
|
|
89
|
-
return ` GSD ${status}${target ? ` - ${target}` : ""}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Build compact health lines for the widget.
|
|
94
|
-
* Returns a string array suitable for setWidget().
|
|
95
|
-
*/
|
|
96
|
-
export function buildHealthLines(data: HealthWidgetData): string[] {
|
|
97
|
-
if (data.projectState === "none") {
|
|
98
|
-
return [" GSD No project loaded — run /gsd to start"];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (data.projectState === "initialized") {
|
|
102
|
-
return [" GSD Project initialized — run /gsd to continue setup"];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const lines = [buildExecutionHeadline(data)];
|
|
106
|
-
const details: string[] = [];
|
|
107
|
-
|
|
108
|
-
const progress = formatProgress(data.progress);
|
|
109
|
-
if (progress) details.push(progress);
|
|
110
|
-
|
|
111
|
-
if (data.providerIssue) details.push(data.providerIssue);
|
|
112
|
-
|
|
113
|
-
const environment = formatEnvironmentSummary(
|
|
114
|
-
data.environmentErrorCount,
|
|
115
|
-
data.environmentWarningCount,
|
|
116
|
-
);
|
|
117
|
-
if (environment) details.push(environment);
|
|
118
|
-
|
|
119
|
-
const budget = formatBudgetSummary(data);
|
|
120
|
-
if (budget) details.push(budget);
|
|
121
|
-
|
|
122
|
-
if (data.eta) details.push(data.eta);
|
|
123
|
-
|
|
124
|
-
if (details.length > 0) {
|
|
125
|
-
lines.push(` ${details.join(" │ ")}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return lines;
|
|
129
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Roadmap Mutations — shared utilities for modifying roadmap checkbox state.
|
|
3
|
-
*
|
|
4
|
-
* Extracts the duplicated "flip slice checkbox" pattern that existed in
|
|
5
|
-
* doctor.ts, mechanical-completion.ts, and auto-recovery.ts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
9
|
-
import { atomicWriteSync } from "./atomic-write.js";
|
|
10
|
-
import { resolveMilestoneFile } from "./paths.js";
|
|
11
|
-
import { clearParseCache } from "./files.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Mark a slice as done ([x]) in the milestone roadmap.
|
|
15
|
-
* Idempotent — no-op if already checked or if the slice isn't found.
|
|
16
|
-
*
|
|
17
|
-
* @returns true if the roadmap was modified, false if no change was needed
|
|
18
|
-
*/
|
|
19
|
-
export function markSliceDoneInRoadmap(basePath: string, mid: string, sid: string): boolean {
|
|
20
|
-
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
21
|
-
if (!roadmapFile) return false;
|
|
22
|
-
|
|
23
|
-
let content: string;
|
|
24
|
-
try {
|
|
25
|
-
content = readFileSync(roadmapFile, "utf-8");
|
|
26
|
-
} catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const updated = content.replace(
|
|
31
|
-
new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${sid}:`, "m"),
|
|
32
|
-
`$1[x] **${sid}:`,
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
if (updated === content) return false;
|
|
36
|
-
|
|
37
|
-
atomicWriteSync(roadmapFile, updated);
|
|
38
|
-
clearParseCache();
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Mark a task as done ([x]) in the slice plan.
|
|
44
|
-
* Idempotent — no-op if already checked or if the task isn't found.
|
|
45
|
-
*
|
|
46
|
-
* @returns true if the plan was modified, false if no change was needed
|
|
47
|
-
*/
|
|
48
|
-
export function markTaskDoneInPlan(basePath: string, planPath: string, tid: string): boolean {
|
|
49
|
-
let content: string;
|
|
50
|
-
try {
|
|
51
|
-
content = readFileSync(planPath, "utf-8");
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const updated = content.replace(
|
|
57
|
-
new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${tid}:`, "m"),
|
|
58
|
-
`$1[x] **${tid}:`,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
if (updated === content) return false;
|
|
62
|
-
|
|
63
|
-
atomicWriteSync(planPath, updated);
|
|
64
|
-
clearParseCache();
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* gitignore-tracked-gsd.test.ts — Regression tests for #1364.
|
|
3
|
-
*
|
|
4
|
-
* Verifies that ensureGitignore() does NOT add ".gsd" to .gitignore
|
|
5
|
-
* when .gsd/ contains git-tracked files, and that migrateToExternalState()
|
|
6
|
-
* aborts migration for tracked .gsd/ directories.
|
|
7
|
-
*
|
|
8
|
-
* Uses real temporary git repos — no mocks.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { execFileSync } from "node:child_process";
|
|
14
|
-
import {
|
|
15
|
-
existsSync,
|
|
16
|
-
mkdirSync,
|
|
17
|
-
mkdtempSync,
|
|
18
|
-
readFileSync,
|
|
19
|
-
rmSync,
|
|
20
|
-
writeFileSync,
|
|
21
|
-
} from "node:fs";
|
|
22
|
-
import { join } from "node:path";
|
|
23
|
-
import { tmpdir } from "node:os";
|
|
24
|
-
|
|
25
|
-
import { ensureGitignore, hasGitTrackedGsdFiles } from "../gitignore.ts";
|
|
26
|
-
import { migrateToExternalState } from "../migrate-external.ts";
|
|
27
|
-
|
|
28
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
function git(dir: string, ...args: string[]): string {
|
|
31
|
-
return execFileSync("git", args, { cwd: dir, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function makeTempRepo(): string {
|
|
35
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-gitignore-test-"));
|
|
36
|
-
git(dir, "init");
|
|
37
|
-
git(dir, "config", "user.email", "test@test.com");
|
|
38
|
-
git(dir, "config", "user.name", "Test");
|
|
39
|
-
writeFileSync(join(dir, "README.md"), "# init\n");
|
|
40
|
-
git(dir, "add", "-A");
|
|
41
|
-
git(dir, "commit", "-m", "init");
|
|
42
|
-
git(dir, "branch", "-M", "main");
|
|
43
|
-
return dir;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function cleanup(dir: string): void {
|
|
47
|
-
try {
|
|
48
|
-
rmSync(dir, { recursive: true, force: true });
|
|
49
|
-
} catch {
|
|
50
|
-
// ignore
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ─── hasGitTrackedGsdFiles ───────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
test("hasGitTrackedGsdFiles returns false when .gsd/ does not exist", () => {
|
|
57
|
-
const dir = makeTempRepo();
|
|
58
|
-
try {
|
|
59
|
-
assert.equal(hasGitTrackedGsdFiles(dir), false);
|
|
60
|
-
} finally {
|
|
61
|
-
cleanup(dir);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("hasGitTrackedGsdFiles returns true when .gsd/ has tracked files", () => {
|
|
66
|
-
const dir = makeTempRepo();
|
|
67
|
-
try {
|
|
68
|
-
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
69
|
-
writeFileSync(join(dir, ".gsd", "PROJECT.md"), "# Test Project\n");
|
|
70
|
-
git(dir, "add", ".gsd/PROJECT.md");
|
|
71
|
-
git(dir, "commit", "-m", "add gsd");
|
|
72
|
-
assert.equal(hasGitTrackedGsdFiles(dir), true);
|
|
73
|
-
} finally {
|
|
74
|
-
cleanup(dir);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("hasGitTrackedGsdFiles returns false when .gsd/ exists but is untracked", () => {
|
|
79
|
-
const dir = makeTempRepo();
|
|
80
|
-
try {
|
|
81
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
82
|
-
writeFileSync(join(dir, ".gsd", "STATE.md"), "state\n");
|
|
83
|
-
// Not git-added — should return false
|
|
84
|
-
assert.equal(hasGitTrackedGsdFiles(dir), false);
|
|
85
|
-
} finally {
|
|
86
|
-
cleanup(dir);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// ─── ensureGitignore — tracked .gsd/ protection ─────────────────────
|
|
91
|
-
|
|
92
|
-
test("ensureGitignore does NOT add .gsd when .gsd/ has tracked files (#1364)", () => {
|
|
93
|
-
const dir = makeTempRepo();
|
|
94
|
-
try {
|
|
95
|
-
// Set up .gsd/ with tracked files
|
|
96
|
-
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
97
|
-
writeFileSync(join(dir, ".gsd", "PROJECT.md"), "# Test Project\n");
|
|
98
|
-
writeFileSync(join(dir, ".gsd", "DECISIONS.md"), "# Decisions\n");
|
|
99
|
-
git(dir, "add", ".gsd/");
|
|
100
|
-
git(dir, "commit", "-m", "track gsd state");
|
|
101
|
-
|
|
102
|
-
// Run ensureGitignore
|
|
103
|
-
ensureGitignore(dir);
|
|
104
|
-
|
|
105
|
-
// Verify .gsd is NOT in .gitignore
|
|
106
|
-
const gitignore = readFileSync(join(dir, ".gitignore"), "utf-8");
|
|
107
|
-
const lines = gitignore.split("\n").map((l) => l.trim());
|
|
108
|
-
assert.ok(
|
|
109
|
-
!lines.includes(".gsd"),
|
|
110
|
-
`Expected .gsd NOT to appear in .gitignore, but it does:\n${gitignore}`,
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
// Other baseline patterns should still be present
|
|
114
|
-
assert.ok(lines.includes(".DS_Store"), "Expected .DS_Store in .gitignore");
|
|
115
|
-
assert.ok(lines.includes("node_modules/"), "Expected node_modules/ in .gitignore");
|
|
116
|
-
} finally {
|
|
117
|
-
cleanup(dir);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("ensureGitignore adds .gsd when .gsd/ has NO tracked files", () => {
|
|
122
|
-
const dir = makeTempRepo();
|
|
123
|
-
try {
|
|
124
|
-
// Run ensureGitignore (no .gsd/ at all)
|
|
125
|
-
ensureGitignore(dir);
|
|
126
|
-
|
|
127
|
-
// Verify .gsd IS in .gitignore
|
|
128
|
-
const gitignore = readFileSync(join(dir, ".gitignore"), "utf-8");
|
|
129
|
-
const lines = gitignore.split("\n").map((l) => l.trim());
|
|
130
|
-
assert.ok(
|
|
131
|
-
lines.includes(".gsd"),
|
|
132
|
-
`Expected .gsd in .gitignore, but it's missing:\n${gitignore}`,
|
|
133
|
-
);
|
|
134
|
-
} finally {
|
|
135
|
-
cleanup(dir);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("ensureGitignore respects manageGitignore: false", () => {
|
|
140
|
-
const dir = makeTempRepo();
|
|
141
|
-
try {
|
|
142
|
-
const result = ensureGitignore(dir, { manageGitignore: false });
|
|
143
|
-
assert.equal(result, false);
|
|
144
|
-
assert.ok(!existsSync(join(dir, ".gitignore")), "Should not create .gitignore");
|
|
145
|
-
} finally {
|
|
146
|
-
cleanup(dir);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// ─── ensureGitignore — verify no tracked files become invisible ─────
|
|
151
|
-
|
|
152
|
-
test("ensureGitignore with tracked .gsd/ does not cause git to see files as deleted", () => {
|
|
153
|
-
const dir = makeTempRepo();
|
|
154
|
-
try {
|
|
155
|
-
// Create tracked .gsd/ files
|
|
156
|
-
mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true });
|
|
157
|
-
writeFileSync(join(dir, ".gsd", "PROJECT.md"), "# Project\n");
|
|
158
|
-
writeFileSync(
|
|
159
|
-
join(dir, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
|
|
160
|
-
"# M001\n",
|
|
161
|
-
);
|
|
162
|
-
git(dir, "add", ".gsd/");
|
|
163
|
-
git(dir, "commit", "-m", "track gsd state");
|
|
164
|
-
|
|
165
|
-
// Run ensureGitignore
|
|
166
|
-
ensureGitignore(dir);
|
|
167
|
-
|
|
168
|
-
// git status should show NO deleted files under .gsd/
|
|
169
|
-
const status = git(dir, "status", "--porcelain", ".gsd/");
|
|
170
|
-
|
|
171
|
-
// Filter for deletions (lines starting with " D" or "D ")
|
|
172
|
-
const deletions = status
|
|
173
|
-
.split("\n")
|
|
174
|
-
.filter((l) => l.match(/^\s*D\s/) || l.match(/^D\s/));
|
|
175
|
-
|
|
176
|
-
assert.equal(
|
|
177
|
-
deletions.length,
|
|
178
|
-
0,
|
|
179
|
-
`Expected no deleted .gsd/ files, but found:\n${deletions.join("\n")}`,
|
|
180
|
-
);
|
|
181
|
-
} finally {
|
|
182
|
-
cleanup(dir);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// ─── migrateToExternalState — tracked .gsd/ protection ──────────────
|
|
187
|
-
|
|
188
|
-
test("migrateToExternalState aborts when .gsd/ has tracked files (#1364)", () => {
|
|
189
|
-
const dir = makeTempRepo();
|
|
190
|
-
try {
|
|
191
|
-
// Create tracked .gsd/ files
|
|
192
|
-
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
193
|
-
writeFileSync(join(dir, ".gsd", "PROJECT.md"), "# Project\n");
|
|
194
|
-
git(dir, "add", ".gsd/");
|
|
195
|
-
git(dir, "commit", "-m", "track gsd state");
|
|
196
|
-
|
|
197
|
-
// Attempt migration — should abort without moving anything
|
|
198
|
-
const result = migrateToExternalState(dir);
|
|
199
|
-
|
|
200
|
-
assert.equal(result.migrated, false, "Should NOT migrate tracked .gsd/");
|
|
201
|
-
assert.equal(result.error, undefined, "Should not report an error — just skip");
|
|
202
|
-
|
|
203
|
-
// .gsd/ should still be a real directory, not a symlink
|
|
204
|
-
assert.ok(existsSync(join(dir, ".gsd", "PROJECT.md")), ".gsd/PROJECT.md should still exist");
|
|
205
|
-
|
|
206
|
-
// No .gsd.migrating should exist
|
|
207
|
-
assert.ok(
|
|
208
|
-
!existsSync(join(dir, ".gsd.migrating")),
|
|
209
|
-
".gsd.migrating should not exist",
|
|
210
|
-
);
|
|
211
|
-
} finally {
|
|
212
|
-
cleanup(dir);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import {
|
|
7
|
-
buildHealthLines,
|
|
8
|
-
detectHealthWidgetProjectState,
|
|
9
|
-
type HealthWidgetData,
|
|
10
|
-
} from "../health-widget-core.ts";
|
|
11
|
-
|
|
12
|
-
function makeTempDir(prefix: string): string {
|
|
13
|
-
const dir = join(
|
|
14
|
-
tmpdir(),
|
|
15
|
-
`gsd-health-widget-test-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
16
|
-
);
|
|
17
|
-
mkdirSync(dir, { recursive: true });
|
|
18
|
-
return dir;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function cleanup(dir: string): void {
|
|
22
|
-
try {
|
|
23
|
-
rmSync(dir, { recursive: true, force: true });
|
|
24
|
-
} catch {
|
|
25
|
-
// best-effort
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function activeData(overrides: Partial<HealthWidgetData> = {}): HealthWidgetData {
|
|
30
|
-
return {
|
|
31
|
-
projectState: "active",
|
|
32
|
-
budgetCeiling: undefined,
|
|
33
|
-
budgetSpent: 0,
|
|
34
|
-
providerIssue: null,
|
|
35
|
-
environmentErrorCount: 0,
|
|
36
|
-
environmentWarningCount: 0,
|
|
37
|
-
lastRefreshed: Date.now(),
|
|
38
|
-
...overrides,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
test("detectHealthWidgetProjectState: no .gsd returns none", () => {
|
|
43
|
-
const dir = makeTempDir("none");
|
|
44
|
-
try {
|
|
45
|
-
assert.equal(detectHealthWidgetProjectState(dir), "none");
|
|
46
|
-
} finally {
|
|
47
|
-
cleanup(dir);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("detectHealthWidgetProjectState: bootstrapped .gsd without milestones returns initialized", () => {
|
|
52
|
-
const dir = makeTempDir("initialized");
|
|
53
|
-
try {
|
|
54
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
55
|
-
assert.equal(detectHealthWidgetProjectState(dir), "initialized");
|
|
56
|
-
} finally {
|
|
57
|
-
cleanup(dir);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("detectHealthWidgetProjectState: milestone without metrics returns active", () => {
|
|
62
|
-
const dir = makeTempDir("active");
|
|
63
|
-
try {
|
|
64
|
-
mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true });
|
|
65
|
-
assert.equal(detectHealthWidgetProjectState(dir), "active");
|
|
66
|
-
} finally {
|
|
67
|
-
cleanup(dir);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("buildHealthLines: none state shows onboarding copy", () => {
|
|
72
|
-
assert.deepEqual(buildHealthLines(activeData({ projectState: "none" })), [
|
|
73
|
-
" GSD No project loaded — run /gsd to start",
|
|
74
|
-
]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("buildHealthLines: initialized state shows continue setup copy", () => {
|
|
78
|
-
assert.deepEqual(buildHealthLines(activeData({ projectState: "initialized" })), [
|
|
79
|
-
" GSD Project initialized — run /gsd to continue setup",
|
|
80
|
-
]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("buildHealthLines: active state leads with execution summary", () => {
|
|
84
|
-
const lines = buildHealthLines(activeData({
|
|
85
|
-
executionStatus: "Executing",
|
|
86
|
-
executionTarget: "Plan S01",
|
|
87
|
-
progress: {
|
|
88
|
-
milestones: { done: 0, total: 1 },
|
|
89
|
-
slices: { done: 0, total: 3 },
|
|
90
|
-
tasks: { done: 0, total: 5 },
|
|
91
|
-
},
|
|
92
|
-
}));
|
|
93
|
-
|
|
94
|
-
assert.equal(lines.length, 2);
|
|
95
|
-
assert.equal(lines[0], " GSD Executing - Plan S01");
|
|
96
|
-
assert.match(lines[1]!, /Progress: M 0\/1 · S 0\/3 · T 0\/5/);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("buildHealthLines: active state keeps issues secondary", () => {
|
|
100
|
-
const lines = buildHealthLines(activeData({
|
|
101
|
-
executionStatus: "Planning",
|
|
102
|
-
executionTarget: "Execute T03",
|
|
103
|
-
providerIssue: "✗ Anthropic (Claude) key missing",
|
|
104
|
-
environmentWarningCount: 1,
|
|
105
|
-
budgetSpent: 0.42,
|
|
106
|
-
}));
|
|
107
|
-
|
|
108
|
-
assert.equal(lines.length, 2);
|
|
109
|
-
assert.equal(lines[0], " GSD Planning - Execute T03");
|
|
110
|
-
assert.match(lines[1]!, /✗ Anthropic \(Claude\) key missing/);
|
|
111
|
-
assert.match(lines[1]!, /Env: 1 warning/);
|
|
112
|
-
assert.match(lines[1]!, /Spent: 42\.0¢/);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("buildHealthLines: blocked state explains wait reason", () => {
|
|
116
|
-
const lines = buildHealthLines(activeData({
|
|
117
|
-
executionStatus: "Blocked",
|
|
118
|
-
executionTarget: "waiting on unmet deps: M001",
|
|
119
|
-
blocker: "M002 is waiting on unmet deps: M001",
|
|
120
|
-
}));
|
|
121
|
-
|
|
122
|
-
assert.equal(lines[0], " GSD Blocked - waiting on unmet deps: M001");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("buildHealthLines: paused state can omit secondary line", () => {
|
|
126
|
-
const lines = buildHealthLines(activeData({
|
|
127
|
-
executionStatus: "Paused",
|
|
128
|
-
executionTarget: "waiting to resume",
|
|
129
|
-
}));
|
|
130
|
-
|
|
131
|
-
assert.deepEqual(lines, [" GSD Paused - waiting to resume"]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("buildHealthLines: active state with budget ceiling shows percent summary", () => {
|
|
135
|
-
const lines = buildHealthLines(activeData({
|
|
136
|
-
executionStatus: "Executing",
|
|
137
|
-
executionTarget: "Plan S01",
|
|
138
|
-
budgetSpent: 2.5,
|
|
139
|
-
budgetCeiling: 10,
|
|
140
|
-
}));
|
|
141
|
-
assert.equal(lines.length, 2);
|
|
142
|
-
assert.match(lines[1]!, /Budget: \$2\.50\/\$10\.00 \(25%\)/);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("detectHealthWidgetProjectState: metrics file alone does not imply project", () => {
|
|
146
|
-
const dir = makeTempDir("metrics-only");
|
|
147
|
-
try {
|
|
148
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
149
|
-
writeFileSync(
|
|
150
|
-
join(dir, ".gsd", "metrics.json"),
|
|
151
|
-
JSON.stringify({ version: 1, projectStartedAt: Date.now(), units: [] }),
|
|
152
|
-
"utf-8",
|
|
153
|
-
);
|
|
154
|
-
assert.equal(detectHealthWidgetProjectState(dir), "initialized");
|
|
155
|
-
} finally {
|
|
156
|
-
cleanup(dir);
|
|
157
|
-
}
|
|
158
|
-
});
|