gsd-pi 2.31.2 → 2.32.0-dev.3d7932c
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 +27 -20
- package/dist/cli.js +5 -5
- package/dist/resource-loader.js +13 -3
- package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +23 -27
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
- package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
- package/dist/resources/extensions/gsd/auto-post-unit.ts +32 -37
- package/dist/resources/extensions/gsd/auto-prompts.ts +84 -78
- package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
- package/dist/resources/extensions/gsd/auto-start.ts +16 -12
- package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
- package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
- package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
- package/dist/resources/extensions/gsd/auto-worktree.ts +5 -4
- package/dist/resources/extensions/gsd/auto.ts +82 -60
- package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
- package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
- package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
- package/dist/resources/extensions/gsd/doctor.ts +6 -0
- package/dist/resources/extensions/gsd/error-utils.ts +6 -0
- package/dist/resources/extensions/gsd/export.ts +2 -1
- package/dist/resources/extensions/gsd/git-service.ts +12 -2
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +18 -5
- package/dist/resources/extensions/gsd/key-manager.ts +2 -1
- package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
- package/dist/resources/extensions/gsd/metrics.ts +3 -3
- package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
- package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
- package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
- package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
- package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/dist/resources/extensions/gsd/progress-score.ts +273 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/dist/resources/extensions/gsd/quick.ts +61 -8
- package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
- package/dist/resources/extensions/gsd/session-lock.ts +12 -1
- package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/dist/resources/extensions/gsd/undo.ts +5 -7
- package/dist/resources/extensions/gsd/unit-id.ts +14 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
- package/dist/worktree-cli.d.ts +42 -6
- package/dist/worktree-cli.js +88 -48
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-constants.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +23 -27
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
- package/src/resources/extensions/gsd/auto-observability.ts +2 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +32 -37
- package/src/resources/extensions/gsd/auto-prompts.ts +84 -78
- package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
- package/src/resources/extensions/gsd/auto-start.ts +16 -12
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
- package/src/resources/extensions/gsd/auto-timers.ts +3 -2
- package/src/resources/extensions/gsd/auto-verification.ts +6 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +5 -4
- package/src/resources/extensions/gsd/auto.ts +82 -60
- package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
- package/src/resources/extensions/gsd/commands.ts +19 -0
- package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
- package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
- package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/src/resources/extensions/gsd/doctor-types.ts +14 -1
- package/src/resources/extensions/gsd/doctor.ts +6 -0
- package/src/resources/extensions/gsd/error-utils.ts +6 -0
- package/src/resources/extensions/gsd/export.ts +2 -1
- package/src/resources/extensions/gsd/git-service.ts +12 -2
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/src/resources/extensions/gsd/guided-flow.ts +3 -2
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +18 -5
- package/src/resources/extensions/gsd/key-manager.ts +2 -1
- package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
- package/src/resources/extensions/gsd/metrics.ts +3 -3
- package/src/resources/extensions/gsd/migrate-external.ts +21 -4
- package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
- package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/src/resources/extensions/gsd/progress-score.ts +273 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/src/resources/extensions/gsd/quick.ts +61 -8
- package/src/resources/extensions/gsd/repo-identity.ts +22 -1
- package/src/resources/extensions/gsd/session-lock.ts +12 -1
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/src/resources/extensions/gsd/undo.ts +5 -7
- package/src/resources/extensions/gsd/unit-id.ts +14 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/src/resources/extensions/gsd/worktree-command.ts +8 -7
|
@@ -17,11 +17,8 @@ If a `GSD Skill Preferences` block is present in system context, use it to decid
|
|
|
17
17
|
## UAT Instructions
|
|
18
18
|
|
|
19
19
|
**UAT file:** `{{uatPath}}`
|
|
20
|
-
**UAT type:** `{{uatType}}`
|
|
21
20
|
**Result file to write:** `{{uatResultPath}}`
|
|
22
21
|
|
|
23
|
-
### If UAT type is `artifact-driven`
|
|
24
|
-
|
|
25
22
|
You are the test runner. Execute every check defined in `{{uatPath}}` directly:
|
|
26
23
|
|
|
27
24
|
- Run shell commands with `bash`
|
|
@@ -46,7 +43,7 @@ Write `{{uatResultPath}}` with:
|
|
|
46
43
|
```markdown
|
|
47
44
|
---
|
|
48
45
|
sliceId: {{sliceId}}
|
|
49
|
-
uatType:
|
|
46
|
+
uatType: artifact-driven
|
|
50
47
|
verdict: PASS | FAIL | PARTIAL
|
|
51
48
|
date: <ISO 8601 timestamp>
|
|
52
49
|
---
|
|
@@ -68,44 +65,6 @@ date: <ISO 8601 timestamp>
|
|
|
68
65
|
<any additional context, errors encountered, or follow-up items>
|
|
69
66
|
```
|
|
70
67
|
|
|
71
|
-
### If UAT type is NOT `artifact-driven` (type is `{{uatType}}`)
|
|
72
|
-
|
|
73
|
-
This UAT type requires human execution or live-runtime observation that you cannot perform mechanically. Your role is to surface it clearly for review.
|
|
74
|
-
|
|
75
|
-
Write `{{uatResultPath}}` with:
|
|
76
|
-
|
|
77
|
-
```markdown
|
|
78
|
-
---
|
|
79
|
-
sliceId: {{sliceId}}
|
|
80
|
-
uatType: {{uatType}}
|
|
81
|
-
verdict: surfaced-for-human-review
|
|
82
|
-
date: <ISO 8601 timestamp>
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
# UAT Result — {{sliceId}}
|
|
86
|
-
|
|
87
|
-
## UAT Type
|
|
88
|
-
|
|
89
|
-
`{{uatType}}` — requires human execution or live-runtime verification.
|
|
90
|
-
|
|
91
|
-
## Status
|
|
92
|
-
|
|
93
|
-
Surfaced for human review. Auto-mode will pause after this unit so the UAT can be performed manually.
|
|
94
|
-
|
|
95
|
-
## UAT File
|
|
96
|
-
|
|
97
|
-
See `{{uatPath}}` for the full UAT specification and acceptance criteria.
|
|
98
|
-
|
|
99
|
-
## Instructions for Human Reviewer
|
|
100
|
-
|
|
101
|
-
Review `{{uatPath}}`, perform the described UAT steps, then update this file with:
|
|
102
|
-
- The actual verdict (PASS / FAIL / PARTIAL)
|
|
103
|
-
- Results for each check
|
|
104
|
-
- Date completed
|
|
105
|
-
|
|
106
|
-
Once updated, run `/gsd auto` to resume auto-mode.
|
|
107
|
-
```
|
|
108
|
-
|
|
109
68
|
---
|
|
110
69
|
|
|
111
70
|
**You MUST write `{{uatResultPath}}` before finishing.**
|
|
@@ -14,8 +14,8 @@ import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { loadPrompt } from "./prompt-loader.js";
|
|
16
16
|
import { gsdRoot } from "./paths.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { createGitService, runGit } from "./git-service.js";
|
|
18
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
19
19
|
|
|
20
20
|
// ─── Quick Task Helpers ───────────────────────────────────────────────────────
|
|
21
21
|
|
|
@@ -103,16 +103,16 @@ export async function handleQuick(
|
|
|
103
103
|
const date = new Date().toISOString().split("T")[0];
|
|
104
104
|
|
|
105
105
|
// Create git branch for the quick task (unless isolation: none)
|
|
106
|
-
const
|
|
107
|
-
const git = new GitServiceImpl(basePath, gitPrefs);
|
|
106
|
+
const git = createGitService(basePath);
|
|
108
107
|
const branchName = `gsd/quick/${taskNum}-${slug}`;
|
|
109
|
-
const skipBranch =
|
|
108
|
+
const skipBranch = git.prefs.isolation === "none";
|
|
110
109
|
|
|
111
110
|
let branchCreated = false;
|
|
111
|
+
let originalBranch: string | undefined;
|
|
112
112
|
if (!skipBranch) {
|
|
113
113
|
try {
|
|
114
|
-
|
|
115
|
-
if (
|
|
114
|
+
originalBranch = git.getCurrentBranch();
|
|
115
|
+
if (originalBranch !== branchName) {
|
|
116
116
|
// Auto-commit any dirty state before switching
|
|
117
117
|
try {
|
|
118
118
|
git.autoCommit("quick-task", `Q${taskNum}`, []);
|
|
@@ -123,7 +123,7 @@ export async function handleQuick(
|
|
|
123
123
|
}
|
|
124
124
|
} catch (err) {
|
|
125
125
|
// Branch creation failed — continue on current branch
|
|
126
|
-
const message =
|
|
126
|
+
const message = getErrorMessage(err);
|
|
127
127
|
ctx.ui.notify(`Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning");
|
|
128
128
|
}
|
|
129
129
|
}
|
|
@@ -156,4 +156,57 @@ export async function handleQuick(
|
|
|
156
156
|
},
|
|
157
157
|
{ triggerTurn: true },
|
|
158
158
|
);
|
|
159
|
+
|
|
160
|
+
// Schedule branch merge-back after the quick task agent session ends.
|
|
161
|
+
// Without this, auto-mode resumes on the quick-task branch (#1269).
|
|
162
|
+
if (branchCreated && originalBranch) {
|
|
163
|
+
_pendingQuickBranchReturn = {
|
|
164
|
+
basePath,
|
|
165
|
+
originalBranch,
|
|
166
|
+
quickBranch: branchName,
|
|
167
|
+
taskNum,
|
|
168
|
+
slug,
|
|
169
|
+
description,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Pending quick-task branch return — consumed by cleanupQuickBranch(). */
|
|
175
|
+
let _pendingQuickBranchReturn: {
|
|
176
|
+
basePath: string;
|
|
177
|
+
originalBranch: string;
|
|
178
|
+
quickBranch: string;
|
|
179
|
+
taskNum: number;
|
|
180
|
+
slug: string;
|
|
181
|
+
description: string;
|
|
182
|
+
} | null = null;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Merge the quick-task branch back to the original branch and switch.
|
|
186
|
+
* Called from the agent_end handler after a quick task completes.
|
|
187
|
+
* Returns true if a branch return was performed.
|
|
188
|
+
*/
|
|
189
|
+
export function cleanupQuickBranch(): boolean {
|
|
190
|
+
if (!_pendingQuickBranchReturn) return false;
|
|
191
|
+
const { basePath, originalBranch, quickBranch, taskNum, slug, description } = _pendingQuickBranchReturn;
|
|
192
|
+
_pendingQuickBranchReturn = null;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// Auto-commit any remaining work
|
|
196
|
+
try { runGit(basePath, ["add", "-A"]); } catch {}
|
|
197
|
+
try { runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${slug}`]); } catch {}
|
|
198
|
+
|
|
199
|
+
// Switch back and merge
|
|
200
|
+
runGit(basePath, ["checkout", originalBranch]);
|
|
201
|
+
try {
|
|
202
|
+
runGit(basePath, ["merge", "--squash", quickBranch]);
|
|
203
|
+
runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${description.slice(0, 72)}`]);
|
|
204
|
+
} catch { /* merge conflict or nothing — non-fatal */ }
|
|
205
|
+
|
|
206
|
+
// Clean up quick branch
|
|
207
|
+
try { runGit(basePath, ["branch", "-D", quickBranch]); } catch {}
|
|
208
|
+
return true;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
159
212
|
}
|
|
@@ -10,7 +10,7 @@ import { createHash } from "node:crypto";
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, symlinkSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
13
|
+
import { join, resolve, sep } from "node:path";
|
|
14
14
|
|
|
15
15
|
// ─── Repo Identity ──────────────────────────────────────────────────────────
|
|
16
16
|
|
|
@@ -37,6 +37,27 @@ function getRemoteUrl(basePath: string): string {
|
|
|
37
37
|
*/
|
|
38
38
|
function resolveGitRoot(basePath: string): string {
|
|
39
39
|
try {
|
|
40
|
+
// In a worktree, --show-toplevel returns the worktree path, not the main
|
|
41
|
+
// repo root. Use --git-common-dir to find the shared .git directory,
|
|
42
|
+
// then derive the main repo root from it (#1288).
|
|
43
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
44
|
+
cwd: basePath,
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
47
|
+
timeout: 5_000,
|
|
48
|
+
}).trim();
|
|
49
|
+
|
|
50
|
+
// If commonDir ends with .git/worktrees/<name>, the main repo is two
|
|
51
|
+
// levels up from the worktrees dir. If it's just .git, resolve normally.
|
|
52
|
+
if (commonDir.includes(`${sep}worktrees${sep}`) || commonDir.includes("/worktrees/")) {
|
|
53
|
+
// e.g., /path/to/project/.gsd/worktrees/M001/.git → /path/to/project
|
|
54
|
+
// or /path/to/project/.git/worktrees/M001 → /path/to/project
|
|
55
|
+
const gitDir = commonDir.replace(/[/\\]worktrees[/\\][^/\\]+$/, "");
|
|
56
|
+
const mainRoot = resolve(gitDir, "..");
|
|
57
|
+
return mainRoot;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Not in a worktree — use --show-toplevel as usual
|
|
40
61
|
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
41
62
|
cwd: basePath,
|
|
42
63
|
encoding: "utf-8",
|
|
@@ -154,12 +154,23 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|
|
154
154
|
// Retry acquisition after cleanup
|
|
155
155
|
const release = lockfile.lockSync(gsdDir, {
|
|
156
156
|
realpath: false,
|
|
157
|
-
stale:
|
|
157
|
+
stale: 1_800_000, // 30 minutes — match primary lock settings
|
|
158
158
|
update: 10_000,
|
|
159
|
+
onCompromised: () => {
|
|
160
|
+
_lockCompromised = true;
|
|
161
|
+
},
|
|
159
162
|
});
|
|
160
163
|
_releaseFunction = release;
|
|
161
164
|
_lockedPath = basePath;
|
|
162
165
|
_lockPid = process.pid;
|
|
166
|
+
|
|
167
|
+
// Safety net for retry path too
|
|
168
|
+
const retryLockDir = join(gsdDir + ".lock");
|
|
169
|
+
process.once("exit", () => {
|
|
170
|
+
try { if (_releaseFunction) { _releaseFunction(); _releaseFunction = null; } } catch {}
|
|
171
|
+
try { if (existsSync(retryLockDir)) rmSync(retryLockDir, { recursive: true, force: true }); } catch {}
|
|
172
|
+
});
|
|
173
|
+
|
|
163
174
|
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|
|
164
175
|
return { acquired: true };
|
|
165
176
|
} catch {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto-reentrancy-guard.test.ts — Tests for the unconditional reentrancy guard.
|
|
3
|
+
*
|
|
4
|
+
* Regression for #1272: auto-mode stuck-loop where gap watchdog or
|
|
5
|
+
* pendingAgentEndRetry could enter dispatchNextUnit concurrently during
|
|
6
|
+
* recursive skip chains because the reentrancy guard was bypassed when
|
|
7
|
+
* skipDepth > 0.
|
|
8
|
+
*
|
|
9
|
+
* The fix makes the guard unconditional (`if (s.dispatching)` without
|
|
10
|
+
* `&& s.skipDepth === 0`), and defers recursive re-dispatch via
|
|
11
|
+
* setImmediate/setTimeout so s.dispatching is released first.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
_getDispatching,
|
|
16
|
+
_setDispatching,
|
|
17
|
+
_getSkipDepth,
|
|
18
|
+
_setSkipDepth,
|
|
19
|
+
} from "../auto.ts";
|
|
20
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
21
|
+
|
|
22
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
23
|
+
|
|
24
|
+
async function main(): Promise<void> {
|
|
25
|
+
// ─── Test-only accessors work ───────────────────────────────────────────
|
|
26
|
+
console.log("\n=== reentrancy guard: test accessors round-trip ===");
|
|
27
|
+
{
|
|
28
|
+
_setDispatching(false);
|
|
29
|
+
assertEq(_getDispatching(), false, "dispatching starts false");
|
|
30
|
+
|
|
31
|
+
_setDispatching(true);
|
|
32
|
+
assertEq(_getDispatching(), true, "dispatching set to true");
|
|
33
|
+
|
|
34
|
+
_setDispatching(false);
|
|
35
|
+
assertEq(_getDispatching(), false, "dispatching reset to false");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── skipDepth accessors ────────────────────────────────────────────────
|
|
39
|
+
console.log("\n=== reentrancy guard: skipDepth accessors round-trip ===");
|
|
40
|
+
{
|
|
41
|
+
_setSkipDepth(0);
|
|
42
|
+
assertEq(_getSkipDepth(), 0, "skipDepth starts at 0");
|
|
43
|
+
|
|
44
|
+
_setSkipDepth(3);
|
|
45
|
+
assertEq(_getSkipDepth(), 3, "skipDepth set to 3");
|
|
46
|
+
|
|
47
|
+
_setSkipDepth(0);
|
|
48
|
+
assertEq(_getSkipDepth(), 0, "skipDepth reset to 0");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Guard blocks even when skipDepth > 0 (#1272 regression) ───────────
|
|
52
|
+
console.log("\n=== reentrancy guard: blocks when dispatching=true regardless of skipDepth ===");
|
|
53
|
+
{
|
|
54
|
+
// Simulate the scenario from #1272: dispatching=true + skipDepth>0
|
|
55
|
+
// The old guard (`if (s.dispatching && s.skipDepth === 0)`) would allow
|
|
56
|
+
// concurrent entry when skipDepth > 0. The fix makes the check
|
|
57
|
+
// unconditional on skipDepth.
|
|
58
|
+
_setDispatching(true);
|
|
59
|
+
_setSkipDepth(2);
|
|
60
|
+
|
|
61
|
+
// Verify dispatching is true — guard should block regardless of skipDepth
|
|
62
|
+
assertTrue(
|
|
63
|
+
_getDispatching() === true,
|
|
64
|
+
"dispatching flag is true during skip chain"
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// The actual reentrancy guard in dispatchNextUnit checks:
|
|
68
|
+
// if (s.dispatching) { return; }
|
|
69
|
+
// We verify the state that would trigger the guard:
|
|
70
|
+
const wouldBlock = _getDispatching(); // unconditional check
|
|
71
|
+
const wouldBlockOld = _getDispatching() && _getSkipDepth() === 0; // old check
|
|
72
|
+
|
|
73
|
+
assertTrue(wouldBlock === true, "new guard blocks when dispatching=true, skipDepth=2");
|
|
74
|
+
assertTrue(wouldBlockOld === false, "old guard WOULD NOT block when dispatching=true, skipDepth=2 (the bug)");
|
|
75
|
+
|
|
76
|
+
// Clean up
|
|
77
|
+
_setDispatching(false);
|
|
78
|
+
_setSkipDepth(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Guard allows entry when dispatching=false ──────────────────────────
|
|
82
|
+
console.log("\n=== reentrancy guard: allows entry when dispatching=false ===");
|
|
83
|
+
{
|
|
84
|
+
_setDispatching(false);
|
|
85
|
+
_setSkipDepth(0);
|
|
86
|
+
assertTrue(!_getDispatching(), "guard allows entry when dispatching=false, skipDepth=0");
|
|
87
|
+
|
|
88
|
+
_setDispatching(false);
|
|
89
|
+
_setSkipDepth(3);
|
|
90
|
+
assertTrue(!_getDispatching(), "guard allows entry when dispatching=false, skipDepth=3");
|
|
91
|
+
|
|
92
|
+
_setSkipDepth(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── skipDepth does not affect guard decision (the fix) ─────────────────
|
|
96
|
+
console.log("\n=== reentrancy guard: skipDepth is irrelevant to guard decision ===");
|
|
97
|
+
{
|
|
98
|
+
for (const depth of [0, 1, 2, 5]) {
|
|
99
|
+
_setDispatching(true);
|
|
100
|
+
_setSkipDepth(depth);
|
|
101
|
+
assertTrue(
|
|
102
|
+
_getDispatching() === true,
|
|
103
|
+
`guard blocks at skipDepth=${depth} when dispatching=true`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const depth of [0, 1, 2, 5]) {
|
|
108
|
+
_setDispatching(false);
|
|
109
|
+
_setSkipDepth(depth);
|
|
110
|
+
assertTrue(
|
|
111
|
+
_getDispatching() === false,
|
|
112
|
+
`guard allows at skipDepth=${depth} when dispatching=false`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Clean up
|
|
117
|
+
_setDispatching(false);
|
|
118
|
+
_setSkipDepth(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
report();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main().catch((err) => {
|
|
125
|
+
console.error(err);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
|
@@ -91,7 +91,7 @@ test("compression: buildPlanMilestonePrompt minimal drops project/requirements/d
|
|
|
91
91
|
// The plan-milestone builder should gate root file inlining on inlineLevel
|
|
92
92
|
assert.ok(
|
|
93
93
|
promptsSrc.includes('inlineLevel !== "minimal"') &&
|
|
94
|
-
promptsSrc.includes(
|
|
94
|
+
promptsSrc.includes("inlineProjectFromDb(base)"),
|
|
95
95
|
"plan-milestone should conditionally include project.md based on level",
|
|
96
96
|
);
|
|
97
97
|
});
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor-environment.test.ts — Tests for environment health checks (#1221).
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - Node version detection
|
|
6
|
+
* - Dependencies installed check
|
|
7
|
+
* - Env file detection
|
|
8
|
+
* - Port conflict detection
|
|
9
|
+
* - Disk space check
|
|
10
|
+
* - Docker detection
|
|
11
|
+
* - Project tool detection
|
|
12
|
+
* - Doctor issue conversion
|
|
13
|
+
* - Report formatting
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
runEnvironmentChecks,
|
|
22
|
+
runFullEnvironmentChecks,
|
|
23
|
+
environmentResultsToDoctorIssues,
|
|
24
|
+
formatEnvironmentReport,
|
|
25
|
+
checkEnvironmentHealth,
|
|
26
|
+
type EnvironmentCheckResult,
|
|
27
|
+
} from "../doctor-environment.ts";
|
|
28
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
29
|
+
|
|
30
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
31
|
+
|
|
32
|
+
function createProjectDir(files: Record<string, string> = {}): string {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-env-test-"));
|
|
34
|
+
for (const [name, content] of Object.entries(files)) {
|
|
35
|
+
const filePath = join(dir, name);
|
|
36
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
37
|
+
writeFileSync(filePath, content);
|
|
38
|
+
}
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main(): Promise<void> {
|
|
43
|
+
const cleanups: string[] = [];
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// ── Node Version Check ─────────────────────────────────────────────
|
|
47
|
+
console.log("\n=== env: no package.json returns empty ===");
|
|
48
|
+
{
|
|
49
|
+
const dir = createProjectDir();
|
|
50
|
+
cleanups.push(dir);
|
|
51
|
+
const results = runEnvironmentChecks(dir);
|
|
52
|
+
// No package.json → no node checks
|
|
53
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
54
|
+
assertEq(nodeCheck, undefined, "no node version check without package.json");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log("\n=== env: package.json without engines returns no node check ===");
|
|
58
|
+
{
|
|
59
|
+
const dir = createProjectDir({
|
|
60
|
+
"package.json": JSON.stringify({ name: "test", version: "1.0.0" }),
|
|
61
|
+
});
|
|
62
|
+
cleanups.push(dir);
|
|
63
|
+
const results = runEnvironmentChecks(dir);
|
|
64
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
65
|
+
assertEq(nodeCheck, undefined, "no node version check without engines field");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log("\n=== env: package.json with engines returns node check ===");
|
|
69
|
+
{
|
|
70
|
+
const dir = createProjectDir({
|
|
71
|
+
"package.json": JSON.stringify({
|
|
72
|
+
name: "test",
|
|
73
|
+
version: "1.0.0",
|
|
74
|
+
engines: { node: ">=18.0.0" },
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
cleanups.push(dir);
|
|
78
|
+
const results = runEnvironmentChecks(dir);
|
|
79
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
80
|
+
assertTrue(nodeCheck !== undefined, "node version check runs with engines field");
|
|
81
|
+
// Current node should be >= 18 in CI
|
|
82
|
+
assertEq(nodeCheck!.status, "ok", "node version meets requirement");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Dependencies Check ─────────────────────────────────────────────
|
|
86
|
+
console.log("\n=== env: missing node_modules detected ===");
|
|
87
|
+
{
|
|
88
|
+
const dir = createProjectDir({
|
|
89
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
90
|
+
});
|
|
91
|
+
cleanups.push(dir);
|
|
92
|
+
const results = runEnvironmentChecks(dir);
|
|
93
|
+
const depsCheck = results.find(r => r.name === "dependencies");
|
|
94
|
+
assertTrue(depsCheck !== undefined, "dependencies check runs");
|
|
95
|
+
assertEq(depsCheck!.status, "error", "missing node_modules is an error");
|
|
96
|
+
assertTrue(depsCheck!.message.includes("node_modules missing"), "reports missing node_modules");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log("\n=== env: existing node_modules detected ===");
|
|
100
|
+
{
|
|
101
|
+
const dir = createProjectDir({
|
|
102
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
103
|
+
});
|
|
104
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
105
|
+
cleanups.push(dir);
|
|
106
|
+
const results = runEnvironmentChecks(dir);
|
|
107
|
+
const depsCheck = results.find(r => r.name === "dependencies");
|
|
108
|
+
assertTrue(depsCheck !== undefined, "dependencies check runs");
|
|
109
|
+
assertEq(depsCheck!.status, "ok", "existing node_modules is ok");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Env File Check ─────────────────────────────────────────────────
|
|
113
|
+
console.log("\n=== env: .env.example without .env detected ===");
|
|
114
|
+
{
|
|
115
|
+
const dir = createProjectDir({
|
|
116
|
+
".env.example": "DB_URL=xxx\nAPI_KEY=xxx\n",
|
|
117
|
+
});
|
|
118
|
+
cleanups.push(dir);
|
|
119
|
+
const results = runEnvironmentChecks(dir);
|
|
120
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
121
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
122
|
+
assertEq(envCheck!.status, "warning", "missing .env is a warning");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("\n=== env: .env.example with .env is ok ===");
|
|
126
|
+
{
|
|
127
|
+
const dir = createProjectDir({
|
|
128
|
+
".env.example": "DB_URL=xxx\n",
|
|
129
|
+
".env": "DB_URL=postgres://localhost/test\n",
|
|
130
|
+
});
|
|
131
|
+
cleanups.push(dir);
|
|
132
|
+
const results = runEnvironmentChecks(dir);
|
|
133
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
134
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
135
|
+
assertEq(envCheck!.status, "ok", "present .env is ok");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log("\n=== env: .env.example with .env.local is ok ===");
|
|
139
|
+
{
|
|
140
|
+
const dir = createProjectDir({
|
|
141
|
+
".env.example": "DB_URL=xxx\n",
|
|
142
|
+
".env.local": "DB_URL=postgres://localhost/test\n",
|
|
143
|
+
});
|
|
144
|
+
cleanups.push(dir);
|
|
145
|
+
const results = runEnvironmentChecks(dir);
|
|
146
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
147
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
148
|
+
assertEq(envCheck!.status, "ok", ".env.local counts as present");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Disk Space Check ───────────────────────────────────────────────
|
|
152
|
+
console.log("\n=== env: disk space check returns result ===");
|
|
153
|
+
if (process.platform !== "win32") {
|
|
154
|
+
const dir = createProjectDir();
|
|
155
|
+
cleanups.push(dir);
|
|
156
|
+
const results = runEnvironmentChecks(dir);
|
|
157
|
+
const diskCheck = results.find(r => r.name === "disk_space");
|
|
158
|
+
assertTrue(diskCheck !== undefined, "disk space check runs on unix");
|
|
159
|
+
// Should be ok on dev machines with reasonable disk
|
|
160
|
+
assertTrue(diskCheck!.status === "ok" || diskCheck!.status === "warning", "disk check returns valid status");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Project Tools Check ────────────────────────────────────────────
|
|
164
|
+
console.log("\n=== env: detects missing python when pyproject.toml exists ===");
|
|
165
|
+
{
|
|
166
|
+
const dir = createProjectDir({
|
|
167
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
168
|
+
"pyproject.toml": "[build-system]\nrequires = ['setuptools']\n",
|
|
169
|
+
});
|
|
170
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
171
|
+
cleanups.push(dir);
|
|
172
|
+
const results = runEnvironmentChecks(dir);
|
|
173
|
+
const pythonCheck = results.find(r => r.name === "python");
|
|
174
|
+
// Python is likely installed on CI/dev machines, so just verify the check runs
|
|
175
|
+
// without error — the result depends on the system
|
|
176
|
+
assertTrue(true, "python check runs without error");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("\n=== env: detects Cargo.toml ===");
|
|
180
|
+
{
|
|
181
|
+
const dir = createProjectDir({
|
|
182
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
183
|
+
"Cargo.toml": "[package]\nname = 'test'\n",
|
|
184
|
+
});
|
|
185
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
186
|
+
cleanups.push(dir);
|
|
187
|
+
const results = runEnvironmentChecks(dir);
|
|
188
|
+
// Just verify it runs without error
|
|
189
|
+
assertTrue(true, "cargo check runs without error");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Docker Check ───────────────────────────────────────────────────
|
|
193
|
+
console.log("\n=== env: no docker check without Dockerfile ===");
|
|
194
|
+
{
|
|
195
|
+
const dir = createProjectDir({
|
|
196
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
197
|
+
});
|
|
198
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
199
|
+
cleanups.push(dir);
|
|
200
|
+
const results = runEnvironmentChecks(dir);
|
|
201
|
+
const dockerCheck = results.find(r => r.name === "docker");
|
|
202
|
+
assertEq(dockerCheck, undefined, "no docker check without Dockerfile");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log("\n=== env: docker check with Dockerfile ===");
|
|
206
|
+
{
|
|
207
|
+
const dir = createProjectDir({
|
|
208
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
209
|
+
"Dockerfile": "FROM node:22\n",
|
|
210
|
+
});
|
|
211
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
212
|
+
cleanups.push(dir);
|
|
213
|
+
const results = runEnvironmentChecks(dir);
|
|
214
|
+
const dockerCheck = results.find(r => r.name === "docker");
|
|
215
|
+
// Docker may or may not be installed on the test machine
|
|
216
|
+
assertTrue(dockerCheck !== undefined, "docker check runs when Dockerfile present");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Doctor Issue Conversion ────────────────────────────────────────
|
|
220
|
+
console.log("\n=== env: converts results to doctor issues ===");
|
|
221
|
+
{
|
|
222
|
+
const results: EnvironmentCheckResult[] = [
|
|
223
|
+
{ name: "node_version", status: "ok", message: "Node.js v22.0.0" },
|
|
224
|
+
{ name: "dependencies", status: "error", message: "node_modules missing" },
|
|
225
|
+
{ name: "env_file", status: "warning", message: ".env missing", detail: "Copy .env.example" },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const issues = environmentResultsToDoctorIssues(results);
|
|
229
|
+
assertEq(issues.length, 2, "only non-ok results converted");
|
|
230
|
+
assertEq(issues[0]!.severity, "error", "error severity preserved");
|
|
231
|
+
assertEq(issues[0]!.code, "env_dependencies", "code prefixed with env_");
|
|
232
|
+
assertEq(issues[1]!.severity, "warning", "warning severity preserved");
|
|
233
|
+
assertTrue(issues[1]!.message.includes("Copy .env.example"), "detail included in message");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── checkEnvironmentHealth integration ──────────────────────────────
|
|
237
|
+
console.log("\n=== env: checkEnvironmentHealth adds issues to array ===");
|
|
238
|
+
{
|
|
239
|
+
const dir = createProjectDir({
|
|
240
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
241
|
+
});
|
|
242
|
+
cleanups.push(dir);
|
|
243
|
+
|
|
244
|
+
const issues: any[] = [];
|
|
245
|
+
await checkEnvironmentHealth(dir, issues);
|
|
246
|
+
// Should have at least the missing node_modules issue
|
|
247
|
+
assertTrue(issues.some(i => i.code === "env_dependencies"), "environment issues added to array");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Report Formatting ──────────────────────────────────────────────
|
|
251
|
+
console.log("\n=== env: formatEnvironmentReport ===");
|
|
252
|
+
{
|
|
253
|
+
const results: EnvironmentCheckResult[] = [
|
|
254
|
+
{ name: "node_version", status: "ok", message: "Node.js v22.0.0" },
|
|
255
|
+
{ name: "dependencies", status: "error", message: "node_modules missing", detail: "Run npm install" },
|
|
256
|
+
{ name: "disk_space", status: "ok", message: "50.2GB free" },
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const report = formatEnvironmentReport(results);
|
|
260
|
+
assertTrue(report.includes("Environment Health:"), "has header");
|
|
261
|
+
assertTrue(report.includes("Node.js v22.0.0"), "includes ok result");
|
|
262
|
+
assertTrue(report.includes("node_modules missing"), "includes error result");
|
|
263
|
+
assertTrue(report.includes("Run npm install"), "includes detail for errors");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log("\n=== env: formatEnvironmentReport empty ===");
|
|
267
|
+
{
|
|
268
|
+
const report = formatEnvironmentReport([]);
|
|
269
|
+
assertEq(report, "No environment checks applicable.", "empty report message");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Full environment checks include git remote ─────────────────────
|
|
273
|
+
console.log("\n=== env: runFullEnvironmentChecks includes git remote ===");
|
|
274
|
+
{
|
|
275
|
+
// runFullEnvironmentChecks adds git remote check
|
|
276
|
+
// We can't easily test this without a real git repo, but verify it doesn't throw
|
|
277
|
+
const dir = createProjectDir();
|
|
278
|
+
cleanups.push(dir);
|
|
279
|
+
const results = runFullEnvironmentChecks(dir);
|
|
280
|
+
// No git repo → no remote check, but should not throw
|
|
281
|
+
assertTrue(true, "runFullEnvironmentChecks does not throw on non-git dir");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Port Detection from package.json ───────────────────────────────
|
|
285
|
+
console.log("\n=== env: port detection from scripts ===");
|
|
286
|
+
if (process.platform !== "win32") {
|
|
287
|
+
const dir = createProjectDir({
|
|
288
|
+
"package.json": JSON.stringify({
|
|
289
|
+
name: "test",
|
|
290
|
+
scripts: {
|
|
291
|
+
dev: "next dev --port 3456",
|
|
292
|
+
start: "node server.js",
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
});
|
|
296
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
297
|
+
cleanups.push(dir);
|
|
298
|
+
const results = runEnvironmentChecks(dir);
|
|
299
|
+
// Port 3456 is unlikely to be in use, so no conflicts expected
|
|
300
|
+
const portConflicts = results.filter(r => r.name === "port_conflict");
|
|
301
|
+
// Just verify it ran without error
|
|
302
|
+
assertTrue(true, "port check with script-detected ports runs without error");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
} finally {
|
|
306
|
+
for (const dir of cleanups) {
|
|
307
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
report();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
main();
|