pi-cursor-sdk 0.1.39 → 0.1.41
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/CHANGELOG.md +25 -0
- package/README.md +13 -12
- package/docs/cursor-dogfood-checklist.md +7 -1
- package/docs/cursor-live-smoke-checklist.md +13 -13
- package/docs/cursor-model-ux-spec.md +8 -8
- package/docs/cursor-native-tool-replay.md +4 -4
- package/docs/cursor-native-tool-visual-audit.md +5 -5
- package/docs/cursor-testing-lessons.md +5 -5
- package/docs/cursor-tool-surfaces.md +4 -0
- package/docs/platform-smoke.md +22 -7
- package/package.json +8 -5
- package/platform-smoke.config.mjs +5 -0
- package/scripts/debug-provider-events.mjs +1 -0
- package/scripts/isolated-cursor-smoke.sh +7 -7
- package/scripts/lib/cursor-visual-manifest.d.mts +3 -0
- package/scripts/lib/cursor-visual-manifest.mjs +82 -0
- package/scripts/platform-smoke/artifacts.mjs +225 -2
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/doctor.mjs +53 -8
- package/scripts/platform-smoke/live-suite-runner.mjs +7 -6
- package/scripts/platform-smoke/platform-build-windows.ps1 +2 -2
- package/scripts/platform-smoke/scenarios.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/platform-smoke.mjs +75 -6
- package/scripts/steering-rpc-smoke.mjs +1 -1
- package/scripts/tmux-live-smoke.sh +1 -1
- package/scripts/visual-tui-smoke-self-test.mjs +229 -0
- package/scripts/visual-tui-smoke.mjs +46 -179
- package/shared/cursor-setting-sources.d.mts +1 -0
- package/shared/cursor-setting-sources.mjs +2 -1
- package/src/context.ts +25 -10
- package/src/cursor-active-tools.ts +7 -0
- package/src/cursor-native-tool-display-registration.ts +31 -21
- package/src/cursor-native-tool-display-state.ts +13 -4
- package/src/cursor-pi-tool-bridge-run.ts +6 -3
- package/src/cursor-pi-tool-bridge-types.ts +2 -2
- package/src/cursor-provider-errors.ts +2 -1
- package/src/cursor-provider-live-run-drain.ts +1 -1
- package/src/cursor-provider-turn-prepare.ts +1 -1
- package/src/cursor-provider-turn-send.ts +2 -0
- package/src/cursor-question-tool.ts +2 -1
- package/src/cursor-sdk-event-debug.ts +3 -1
- package/src/cursor-setting-sources.ts +2 -0
- package/src/cursor-skill-tool.ts +2 -1
- package/src/cursor-state.ts +2 -1
- package/src/cursor-tool-manifest.ts +2 -1
- package/src/cursor-usage-accounting.ts +5 -4
package/docs/platform-smoke.md
CHANGED
|
@@ -123,7 +123,7 @@ Runtime budget is part of the contract:
|
|
|
123
123
|
- `smoke:platform:doctor` never calls Cursor.
|
|
124
124
|
- `platform-build` runs once per target and is the only suite that performs the full local CI/build/typecheck/package gate.
|
|
125
125
|
- Live suites reuse the target checkout and prepared `node_modules` when run after `platform-build`; they do not repeat `npm ci` in a target-session release run.
|
|
126
|
-
- Live suites share one target-local packed-install prep directory per target-session release run. The first live suite runs `npm pack` and `npm install --no-save <tarball>` once, then each suite still performs its own `pi install -l <packed package path>`, `pi list`, fresh `--session-dir`, suite `PI_CODING_AGENT_DIR`, workspace fixture, JSONL, visual, bridge, and abort assertions.
|
|
126
|
+
- Live suites share one target-local packed-install prep directory per target-session release run. The first live suite runs `npm pack` and `npm install --no-save <tarball>` once, then each suite still performs its own `pi install --approve -l <packed package path>`, `pi list --approve`, fresh `--session-dir`, suite `PI_CODING_AGENT_DIR`, workspace fixture, JSONL, visual, bridge, and abort assertions.
|
|
127
127
|
- Visual coverage is batched into one native prompt, one bridge prompt, and one abort/cleanup prompt per target. Do not split these into one prompt per card.
|
|
128
128
|
- The gate is fail-fast by target to avoid burning Cursor calls after a platform has already failed.
|
|
129
129
|
|
|
@@ -186,6 +186,11 @@ export default {
|
|
|
186
186
|
packageName: "pi-cursor-sdk",
|
|
187
187
|
cursorModel: "cursor/composer-2-5",
|
|
188
188
|
artifactRoot: ".artifacts/platform-smoke",
|
|
189
|
+
artifactRetention: {
|
|
190
|
+
maxRunDirs: 18,
|
|
191
|
+
maxAgeDays: 14,
|
|
192
|
+
preserveRecentHours: 24,
|
|
193
|
+
},
|
|
189
194
|
requiredTargets: ["macos", "ubuntu", "windows-native"],
|
|
190
195
|
requiredSuites: [
|
|
191
196
|
"platform-build",
|
|
@@ -211,6 +216,8 @@ export default {
|
|
|
211
216
|
|
|
212
217
|
`windowsParallels` records this repo's default shared Windows template contract. Environment overrides may point at a temporary candidate template during infrastructure work, but release runs should use the shared `pi-extension-windows-template` / `crabbox-ready` baseline unless this document is updated.
|
|
213
218
|
|
|
219
|
+
`artifactRetention` bounds local host evidence growth under `artifactRoot`. `smoke:platform:run` prunes only top-level directories named `run-<timestamp>-<suffix>` before starting a new matrix; it leaves non-run/manual directories untouched and preserves directories newer than `preserveRecentHours` to avoid deleting evidence from active or very recent runs. Doctor is read-only and does not prune artifacts.
|
|
220
|
+
|
|
214
221
|
## Required local environment
|
|
215
222
|
|
|
216
223
|
The config owns reusable defaults. Environment variables are local-machine knobs and one-off overrides, not a second source of truth. The doctor fails if required auth or target readiness is missing.
|
|
@@ -261,12 +268,12 @@ Definitions:
|
|
|
261
268
|
- `piProjectRoot`: target-local pi project where platform-build proves packed install.
|
|
262
269
|
- `livePrepRoot`: target-local shared live-suite prep where the first live suite installs the packed tarball once for reuse by later live suites in the same target session.
|
|
263
270
|
|
|
264
|
-
Live suites run in a suite-local `testWorkspaceRoot`. The extension loaded by pi is the packed tarball package path from `livePrepRoot`, installed into that suite-local workspace with `pi install -l`; no live suite uses `pi -e .`.
|
|
271
|
+
Live suites run in a suite-local `testWorkspaceRoot`. The extension loaded by pi is the packed tarball package path from `livePrepRoot`, installed into that suite-local workspace with `pi install --approve -l`; no live suite uses `pi -e .`.
|
|
265
272
|
|
|
266
273
|
The runner must prove this by recording:
|
|
267
274
|
|
|
268
275
|
- packed tarball path;
|
|
269
|
-
- `pi list` output from the suite-local project after `pi install -l <packed package path>`;
|
|
276
|
+
- `pi list --approve` output from the suite-local project after `pi install --approve -l <packed package path>`;
|
|
270
277
|
- command line showing no `-e .`;
|
|
271
278
|
- live suite cwd as `testWorkspaceRoot`.
|
|
272
279
|
|
|
@@ -354,7 +361,7 @@ Doctor checks:
|
|
|
354
361
|
17. `tar` is available on macOS and native Windows.
|
|
355
362
|
18. `node-pty` self-test passes on every target.
|
|
356
363
|
19. Target pi tool probe proves the shell tool accepts platform-rendered commands on every target.
|
|
357
|
-
20. Host-side xterm/Playwright render self-test passes.
|
|
364
|
+
20. Host-side xterm/Playwright render self-test passes by rendering a minimal ANSI fixture through the repo xterm helper and launching Playwright Chromium to write a tiny PNG. If this fails, run `npm install` and `npx playwright install chromium` before live suites.
|
|
358
365
|
21. `CURSOR_API_KEY` is present.
|
|
359
366
|
22. Artifact root is writable.
|
|
360
367
|
23. `git status --short` is recorded.
|
|
@@ -386,8 +393,8 @@ Per target, `platform-build` must:
|
|
|
386
393
|
6. Run `npm pack`.
|
|
387
394
|
7. Create `testWorkspaceRoot` with deterministic fixture files copied from the repo.
|
|
388
395
|
8. Create `piProjectRoot`.
|
|
389
|
-
9. Install the packed tarball into `piProjectRoot` with `pi install -l <tarball>`.
|
|
390
|
-
10. Run `pi list` and assert the installed package points at the packed tarball/install, not `-e .`.
|
|
396
|
+
9. Install the packed tarball into `piProjectRoot` with `pi install --approve -l <tarball>`.
|
|
397
|
+
10. Run `pi list --approve` and assert the installed package points at the packed tarball/install, not `-e .`.
|
|
391
398
|
|
|
392
399
|
## Required suites
|
|
393
400
|
|
|
@@ -401,7 +408,7 @@ Purpose:
|
|
|
401
408
|
- fail before spending Cursor tokens;
|
|
402
409
|
- produce the packed extension used by later suites.
|
|
403
410
|
|
|
404
|
-
The host `smoke:platform:all` entrypoint enforces doctor first before running targets. Required artifacts include `node-version.txt`, `npm-version.txt`, stdout/stderr for `npm ci`, `npm run check:platform-smoke`, `npm test`, `npm run typecheck`, `npm pack`, packed npm install, `pi install`, and `pi list`, plus `packed-tarball.txt`, `summary.json`, `artifact-manifest.json`, `assertions.json`, and `failures.md` on failed assertions.
|
|
411
|
+
The host `smoke:platform:all` entrypoint enforces doctor first before running targets. Required artifacts include `node-version.txt`, `npm-version.txt`, stdout/stderr for `npm ci`, `npm run check:platform-smoke`, `npm test`, `npm run typecheck`, `npm pack`, packed npm install, `pi install --approve`, and `pi list --approve`, plus `packed-tarball.txt`, `summary.json`, `artifact-manifest.json`, `assertions.json`, and `failures.md` on failed assertions.
|
|
405
412
|
|
|
406
413
|
### `cursor-native-visual-matrix`
|
|
407
414
|
|
|
@@ -636,6 +643,14 @@ Every suite writes under:
|
|
|
636
643
|
.artifacts/platform-smoke/<run-id>/<target>/<suite>/
|
|
637
644
|
```
|
|
638
645
|
|
|
646
|
+
After each `smoke:platform run` invocation, the host writes an atomic latest artifact index for agents and humans:
|
|
647
|
+
|
|
648
|
+
```text
|
|
649
|
+
.artifacts/platform-smoke/latest.json
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
`latest.json` records the invocation timestamps, command selection, PID, run id(s), target/suite artifact directories, paths to suite summaries/assertions/failures when present, rendered terminal HTML/PNG paths, visual evidence, session JSONL, JSONL tool-result summaries, and capped Cursor SDK/provider debug artifact paths. The per-suite artifact directories remain the source of truth; `latest.json` is only a discoverability pointer.
|
|
653
|
+
|
|
639
654
|
Common required artifacts:
|
|
640
655
|
|
|
641
656
|
```text
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cursor-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"description": "pi provider extension backed by @cursor/sdk local agents",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"scripts/steering-rpc-smoke.mjs",
|
|
29
29
|
"scripts/tmux-live-smoke.sh",
|
|
30
30
|
"scripts/visual-tui-smoke.mjs",
|
|
31
|
+
"scripts/visual-tui-smoke-self-test.mjs",
|
|
31
32
|
"scripts/isolated-cursor-smoke.sh",
|
|
32
33
|
"scripts/fixtures/plan-strip-shim",
|
|
33
34
|
"scripts/validate-smoke-jsonl.mjs",
|
|
@@ -60,6 +61,8 @@
|
|
|
60
61
|
"scripts/lib/cursor-smoke-env.mjs",
|
|
61
62
|
"scripts/lib/cursor-smoke-env.d.mts",
|
|
62
63
|
"scripts/lib/cursor-smoke-shell.sh",
|
|
64
|
+
"scripts/lib/cursor-visual-manifest.mjs",
|
|
65
|
+
"scripts/lib/cursor-visual-manifest.d.mts",
|
|
63
66
|
"scripts/lib/cursor-visual-render.mjs",
|
|
64
67
|
"scripts/lib/cursor-visual-render.d.mts",
|
|
65
68
|
"scripts/lib/cursor-sdk-output-filter.mjs",
|
|
@@ -106,7 +109,7 @@
|
|
|
106
109
|
"smoke:platform:all": "npm run smoke:platform:doctor && node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native"
|
|
107
110
|
},
|
|
108
111
|
"dependencies": {
|
|
109
|
-
"@cursor/sdk": "1.0.
|
|
112
|
+
"@cursor/sdk": "1.0.18",
|
|
110
113
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
111
114
|
},
|
|
112
115
|
"peerDependencies": {
|
|
@@ -116,9 +119,9 @@
|
|
|
116
119
|
"typebox": "*"
|
|
117
120
|
},
|
|
118
121
|
"devDependencies": {
|
|
119
|
-
"@earendil-works/pi-ai": "0.
|
|
120
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
121
|
-
"@earendil-works/pi-tui": "0.
|
|
122
|
+
"@earendil-works/pi-ai": "0.79.1",
|
|
123
|
+
"@earendil-works/pi-coding-agent": "0.79.1",
|
|
124
|
+
"@earendil-works/pi-tui": "0.79.1",
|
|
122
125
|
"@xterm/xterm": "^6.0.0",
|
|
123
126
|
"node-pty": "^1.1.0",
|
|
124
127
|
"playwright": "^1.60.0",
|
|
@@ -5,6 +5,11 @@ export default {
|
|
|
5
5
|
packageName: "pi-cursor-sdk",
|
|
6
6
|
cursorModel: "cursor/composer-2-5",
|
|
7
7
|
artifactRoot: ".artifacts/platform-smoke",
|
|
8
|
+
artifactRetention: {
|
|
9
|
+
maxRunDirs: 18,
|
|
10
|
+
maxAgeDays: 14,
|
|
11
|
+
preserveRecentHours: 24,
|
|
12
|
+
},
|
|
8
13
|
requiredTargets: ["macos", "ubuntu", "windows-native"],
|
|
9
14
|
requiredSuites: [
|
|
10
15
|
"platform-build",
|
|
@@ -344,12 +344,12 @@ fi
|
|
|
344
344
|
log "npm install packed extension deps"
|
|
345
345
|
run_in_dir_capture_combined "npm install --omit=dev" 120 "$EXTRACT_DIR/package" "$ISOLATED/npm-install.log" "${TOOL_ENV[@]}" "$NPM_BIN" install --omit=dev
|
|
346
346
|
|
|
347
|
-
log "pi install -l (clean HOME)"
|
|
347
|
+
log "pi install --approve -l (clean HOME)"
|
|
348
348
|
cp "$REPO/README.md" "$PROJECT_DIR/README.md"
|
|
349
|
-
run_in_dir_capture_combined "pi install" 30 "$PROJECT_DIR" "$ISOLATED/pi-install.log" "${PI_DEFAULT_ENV[@]}" "$PI_BIN" install -l "$EXTRACT_DIR/package"
|
|
349
|
+
run_in_dir_capture_combined "pi install" 30 "$PROJECT_DIR" "$ISOLATED/pi-install.log" "${PI_DEFAULT_ENV[@]}" "$PI_BIN" install --approve -l "$EXTRACT_DIR/package"
|
|
350
350
|
|
|
351
351
|
PI_LIST_OUT="$ISOLATED/pi-list.txt"
|
|
352
|
-
run_in_dir_capture_combined "pi list" 15 "$PROJECT_DIR" "$PI_LIST_OUT" "${PI_DEFAULT_ENV[@]}" "$PI_BIN" list
|
|
352
|
+
run_in_dir_capture_combined "pi list" 15 "$PROJECT_DIR" "$PI_LIST_OUT" "${PI_DEFAULT_ENV[@]}" "$PI_BIN" list --approve
|
|
353
353
|
"$RG_BIN" -q "extract/package" "$PI_LIST_OUT" || fail "packed extension not installed"
|
|
354
354
|
|
|
355
355
|
PI_CURSOR_ENV=( "${PI_NONE_ENV[@]}" )
|
|
@@ -360,14 +360,14 @@ fi
|
|
|
360
360
|
log "check: list-models"
|
|
361
361
|
LIST_OUT="$ISOLATED/list-models.txt"
|
|
362
362
|
run_in_dir_capture_combined "list-models" 30 "$PROJECT_DIR" "$LIST_OUT" "${PI_CURSOR_ENV[@]}" \
|
|
363
|
-
"$PI_BIN" --cursor-no-fast --list-models cursor
|
|
363
|
+
"$PI_BIN" --approve --cursor-no-fast --list-models cursor
|
|
364
364
|
"$RG_BIN" -q "composer-2\\.5|composer-2-5" "$LIST_OUT" || fail "composer-2-5 not listed (see $LIST_OUT)"
|
|
365
365
|
|
|
366
366
|
log "check: basic provider prompt"
|
|
367
367
|
BASIC_DIR="$SESSION_ROOT/basic"
|
|
368
368
|
mkdir -p "$BASIC_DIR"
|
|
369
369
|
run_in_dir_capture_split "basic prompt" "$PI_LIVE_TIMEOUT" "$PROJECT_DIR" "$ISOLATED/basic.stdout.txt" "$ISOLATED/basic.stderr.txt" "${PI_CURSOR_ENV[@]}" \
|
|
370
|
-
"$PI_BIN" --cursor-no-fast --model cursor/composer-2-5 --session-dir "$BASIC_DIR" --no-tools -p 'Reply exactly: PI_CURSOR_ISOLATED_OK'
|
|
370
|
+
"$PI_BIN" --approve --cursor-no-fast --model cursor/composer-2-5 --session-dir "$BASIC_DIR" --no-tools -p 'Reply exactly: PI_CURSOR_ISOLATED_OK'
|
|
371
371
|
"$RG_BIN" -q "PI_CURSOR_ISOLATED_OK" "$ISOLATED/basic.stdout.txt" || fail "basic prompt missing PI_CURSOR_ISOLATED_OK"
|
|
372
372
|
validate_replay_jsonl "$BASIC_DIR"
|
|
373
373
|
|
|
@@ -375,14 +375,14 @@ log "check: native replay"
|
|
|
375
375
|
REPLAY_DIR="$SESSION_ROOT/native-replay"
|
|
376
376
|
mkdir -p "$REPLAY_DIR"
|
|
377
377
|
run_in_dir_capture_split "native replay" "$PI_LIVE_TIMEOUT" "$PROJECT_DIR" "$ISOLATED/replay.stdout.txt" "$ISOLATED/replay.stderr.txt" "${PI_CURSOR_ENV[@]}" PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
378
|
-
"$PI_BIN" --cursor-no-fast --model cursor/composer-2-5 --session-dir "$REPLAY_DIR" -p 'Read ./README.md briefly, then answer README_SEEN=yes if it mentions pi-cursor-sdk.'
|
|
378
|
+
"$PI_BIN" --approve --cursor-no-fast --model cursor/composer-2-5 --session-dir "$REPLAY_DIR" -p 'Read ./README.md briefly, then answer README_SEEN=yes if it mentions pi-cursor-sdk.'
|
|
379
379
|
validate_replay_jsonl "$REPLAY_DIR"
|
|
380
380
|
|
|
381
381
|
log "check: plan-strip shim (plan-mode execute reset)"
|
|
382
382
|
PLAN_DIR="$SESSION_ROOT/plan-strip"
|
|
383
383
|
mkdir -p "$PLAN_DIR"
|
|
384
384
|
run_in_dir_capture_split "plan-strip replay" "$PI_LIVE_TIMEOUT" "$PROJECT_DIR" "$ISOLATED/plan.stdout.txt" "$ISOLATED/plan.stderr.txt" "${PI_CURSOR_ENV[@]}" PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
385
|
-
"$PI_BIN" -e "$SHIM_DIR" --cursor-no-fast --model cursor/composer-2-5 --session-dir "$PLAN_DIR" -p 'After reset, read README.md and answer PLAN_STRIP_OK=yes.'
|
|
385
|
+
"$PI_BIN" --approve -e "$SHIM_DIR" --cursor-no-fast --model cursor/composer-2-5 --session-dir "$PLAN_DIR" -p 'After reset, read README.md and answer PLAN_STRIP_OK=yes.'
|
|
386
386
|
validate_replay_jsonl "$PLAN_DIR"
|
|
387
387
|
|
|
388
388
|
log "PASS isolated install smoke: $ISOLATED"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
|
|
5
|
+
function writeUtf8(path, text) {
|
|
6
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
7
|
+
writeFileSync(path, text, "utf8");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function redactedArgv(argv) {
|
|
11
|
+
const redacted = [];
|
|
12
|
+
let redactNext = false;
|
|
13
|
+
for (const arg of argv) {
|
|
14
|
+
if (redactNext) {
|
|
15
|
+
redacted.push("[redacted]");
|
|
16
|
+
redactNext = false;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg === "--prompt" || arg === "--prompt-file") {
|
|
20
|
+
redacted.push(arg);
|
|
21
|
+
redactNext = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (arg.startsWith("--prompt=") || arg.startsWith("--prompt-file=")) {
|
|
25
|
+
const [flag] = arg.split("=", 1);
|
|
26
|
+
redacted.push(`${flag}=[redacted]`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
redacted.push(arg);
|
|
30
|
+
}
|
|
31
|
+
return redacted;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function promptDigest(prompt) {
|
|
35
|
+
return createHash("sha256").update(prompt).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function manifestExistingPath(path) {
|
|
39
|
+
return path && existsSync(path) ? path : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function writeVisualManifest(path, options, artifacts, failure) {
|
|
43
|
+
const paths = {
|
|
44
|
+
ansi: manifestExistingPath(artifacts.ansiPath),
|
|
45
|
+
text: manifestExistingPath(artifacts.textPath),
|
|
46
|
+
html: manifestExistingPath(artifacts.htmlPath),
|
|
47
|
+
png: artifacts.pngWritten === true ? manifestExistingPath(artifacts.pngPath) : undefined,
|
|
48
|
+
jsonlPathFile: manifestExistingPath(artifacts.jsonlPathFile),
|
|
49
|
+
jsonl: manifestExistingPath(artifacts.jsonlPath),
|
|
50
|
+
};
|
|
51
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
52
|
+
if (value === undefined) delete paths[key];
|
|
53
|
+
}
|
|
54
|
+
writeUtf8(path, `${JSON.stringify({
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
kind: "visual-tui-smoke-manifest",
|
|
57
|
+
label: options.label,
|
|
58
|
+
safeLabel: options.safeLabel,
|
|
59
|
+
promptLength: options.prompt.length,
|
|
60
|
+
promptSha256: promptDigest(options.prompt),
|
|
61
|
+
width: options.width,
|
|
62
|
+
height: options.height,
|
|
63
|
+
model: options.model,
|
|
64
|
+
mode: options.mode,
|
|
65
|
+
cwd: options.cwd,
|
|
66
|
+
ext: options.ext,
|
|
67
|
+
outDir: options.outDir,
|
|
68
|
+
sessionDir: options.sessionDir,
|
|
69
|
+
sessionId: options.sessionId,
|
|
70
|
+
waitMs: options.waitMs,
|
|
71
|
+
startupMs: options.startupMs,
|
|
72
|
+
screenshot: options.screenshot,
|
|
73
|
+
paths,
|
|
74
|
+
command: {
|
|
75
|
+
argv: redactedArgv(process.argv.slice(2)),
|
|
76
|
+
cwd: process.cwd(),
|
|
77
|
+
pid: process.pid,
|
|
78
|
+
},
|
|
79
|
+
...(failure ? { failure } : {}),
|
|
80
|
+
writtenAt: new Date().toISOString(),
|
|
81
|
+
}, null, 2)}\n`);
|
|
82
|
+
}
|
|
@@ -2,8 +2,87 @@
|
|
|
2
2
|
* Artifact management — directory layout, manifest, redaction scanning, packaging.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { mkdirSync, writeFileSync, readFileSync, readdirSync,
|
|
6
|
-
import { resolve, relative, basename } from "node:path";
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync, renameSync } from "node:fs";
|
|
6
|
+
import { resolve, relative, basename, dirname } from "node:path";
|
|
7
|
+
|
|
8
|
+
const PLATFORM_SMOKE_RUN_DIR_PATTERN = /^run-(\d+)-[a-z0-9]+$/i;
|
|
9
|
+
const HOURS_TO_MS = 60 * 60 * 1000;
|
|
10
|
+
const DAYS_TO_MS = 24 * HOURS_TO_MS;
|
|
11
|
+
const LATEST_INDEX_NAME = "latest.json";
|
|
12
|
+
|
|
13
|
+
function finiteNonNegativeNumber(value) {
|
|
14
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function finiteNonNegativeInteger(value) {
|
|
18
|
+
return Number.isInteger(value) && value >= 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Prune old top-level platform-smoke run artifact directories. */
|
|
22
|
+
export function prunePlatformSmokeArtifacts(artifactRoot, retention = {}, options = {}) {
|
|
23
|
+
const root = resolve(process.cwd(), artifactRoot);
|
|
24
|
+
const maxRunDirs = finiteNonNegativeInteger(retention.maxRunDirs) ? retention.maxRunDirs : undefined;
|
|
25
|
+
const maxAgeDays = finiteNonNegativeNumber(retention.maxAgeDays) ? retention.maxAgeDays : undefined;
|
|
26
|
+
const preserveRecentHours = finiteNonNegativeNumber(retention.preserveRecentHours) ? retention.preserveRecentHours : 24;
|
|
27
|
+
const enabled = retention.enabled !== false && (maxRunDirs !== undefined || maxAgeDays !== undefined);
|
|
28
|
+
const result = { root, enabled, removed: [], kept: [], ignored: [] };
|
|
29
|
+
if (!enabled || !existsSync(root)) return result;
|
|
30
|
+
|
|
31
|
+
const nowMs = finiteNonNegativeNumber(options.nowMs) ? options.nowMs : Date.now();
|
|
32
|
+
const preserveRecentMs = preserveRecentHours * HOURS_TO_MS;
|
|
33
|
+
const maxAgeMs = maxAgeDays === undefined ? undefined : maxAgeDays * DAYS_TO_MS;
|
|
34
|
+
const runDirs = [];
|
|
35
|
+
|
|
36
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
37
|
+
if (!entry.isDirectory()) {
|
|
38
|
+
result.ignored.push(entry.name);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const match = PLATFORM_SMOKE_RUN_DIR_PATTERN.exec(entry.name);
|
|
42
|
+
if (!match) {
|
|
43
|
+
result.ignored.push(entry.name);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
runDirs.push({ name: entry.name, path: resolve(root, entry.name), timestampMs: Number(match[1]) });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const recentCutoffMs = nowMs - preserveRecentMs;
|
|
50
|
+
const protectedRecent = new Set(runDirs.filter((dir) => dir.timestampMs > recentCutoffMs).map((dir) => dir.name));
|
|
51
|
+
const removeNames = new Set();
|
|
52
|
+
|
|
53
|
+
if (maxAgeMs !== undefined) {
|
|
54
|
+
const staleCutoffMs = nowMs - maxAgeMs;
|
|
55
|
+
for (const dir of runDirs) {
|
|
56
|
+
if (dir.timestampMs < staleCutoffMs) removeNames.add(dir.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (maxRunDirs !== undefined && runDirs.length > maxRunDirs) {
|
|
61
|
+
const sortedNewestFirst = [...runDirs].sort((a, b) => b.timestampMs - a.timestampMs);
|
|
62
|
+
let remainingKeepSlots = maxRunDirs - protectedRecent.size;
|
|
63
|
+
for (const dir of sortedNewestFirst) {
|
|
64
|
+
if (protectedRecent.has(dir.name)) continue;
|
|
65
|
+
if (remainingKeepSlots > 0) {
|
|
66
|
+
remainingKeepSlots--;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
removeNames.add(dir.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const dir of runDirs) {
|
|
74
|
+
if (!removeNames.has(dir.name)) {
|
|
75
|
+
result.kept.push(dir.name);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
rmSync(dir.path, { recursive: true, force: true });
|
|
79
|
+
result.removed.push(dir.name);
|
|
80
|
+
}
|
|
81
|
+
result.kept.sort();
|
|
82
|
+
result.removed.sort();
|
|
83
|
+
result.ignored.sort();
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
7
86
|
|
|
8
87
|
/** Create a suite artifact directory. */
|
|
9
88
|
export function createSuiteDir(artifactRoot, runId, targetName, suiteName) {
|
|
@@ -100,6 +179,150 @@ export function writeSummary(dir, data) {
|
|
|
100
179
|
}, null, 2));
|
|
101
180
|
}
|
|
102
181
|
|
|
182
|
+
function readJsonFile(path) {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
185
|
+
} catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectFiles(root) {
|
|
191
|
+
const files = [];
|
|
192
|
+
function walk(dir) {
|
|
193
|
+
let entries;
|
|
194
|
+
try {
|
|
195
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
196
|
+
} catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
const path = resolve(dir, entry.name);
|
|
201
|
+
if (entry.isDirectory()) walk(path);
|
|
202
|
+
else if (entry.isFile()) files.push(path);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (existsSync(root)) walk(root);
|
|
206
|
+
files.sort();
|
|
207
|
+
return files;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function existingPath(path) {
|
|
211
|
+
return existsSync(path) ? path : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function providerDebugPathFields(debugRoot) {
|
|
215
|
+
if (!existsSync(debugRoot)) return {};
|
|
216
|
+
const providerDebugArtifacts = collectFiles(debugRoot);
|
|
217
|
+
const keyArtifacts = providerDebugArtifacts.filter((path) => /(?:^|[\\/])(?:session|summary|timeline|provider-events|bridge-events|wait-result)\.(?:json|jsonl)$/i.test(path));
|
|
218
|
+
const capped = keyArtifacts.slice(0, 40);
|
|
219
|
+
return {
|
|
220
|
+
providerDebugRoot: debugRoot,
|
|
221
|
+
providerDebugArtifactCount: providerDebugArtifacts.length,
|
|
222
|
+
providerDebugArtifacts: capped,
|
|
223
|
+
...(providerDebugArtifacts.length > capped.length ? { providerDebugArtifactsTruncated: true } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function pathFields(suiteDir) {
|
|
228
|
+
const artifactsDir = resolve(suiteDir, "artifacts");
|
|
229
|
+
const debugRoot = resolve(suiteDir, "cursor-sdk-events");
|
|
230
|
+
const paths = {
|
|
231
|
+
artifactManifest: existingPath(resolve(suiteDir, "artifact-manifest.json")),
|
|
232
|
+
summary: existingPath(resolve(suiteDir, "summary.json")),
|
|
233
|
+
assertions: existingPath(resolve(suiteDir, "assertions.json")),
|
|
234
|
+
failures: existingPath(resolve(suiteDir, "failures.md")),
|
|
235
|
+
terminalHtml: existingPath(resolve(artifactsDir, "terminal.html")),
|
|
236
|
+
terminalFullPng: existingPath(resolve(artifactsDir, "terminal.full.png")),
|
|
237
|
+
terminalFinalViewportPng: existingPath(resolve(artifactsDir, "terminal.final-viewport.png")),
|
|
238
|
+
visualEvidence: existingPath(resolve(artifactsDir, "visual-evidence.json")),
|
|
239
|
+
sessionJsonl: existingPath(resolve(artifactsDir, "session.jsonl")),
|
|
240
|
+
jsonlToolResults: existingPath(resolve(artifactsDir, "jsonl-tool-results.json")),
|
|
241
|
+
...providerDebugPathFields(debugRoot),
|
|
242
|
+
};
|
|
243
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
244
|
+
if (value === undefined) delete paths[key];
|
|
245
|
+
}
|
|
246
|
+
return paths;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function suiteIndexFromResult(result, artifactRoot) {
|
|
250
|
+
if (!result?.suiteDir) return undefined;
|
|
251
|
+
const suiteDir = resolve(result.suiteDir);
|
|
252
|
+
const summary = readJsonFile(resolve(suiteDir, "summary.json"));
|
|
253
|
+
const target = readJsonFile(resolve(suiteDir, "target.json"));
|
|
254
|
+
const suite = readJsonFile(resolve(suiteDir, "suite.json"));
|
|
255
|
+
const rel = relative(resolve(process.cwd(), artifactRoot), suiteDir).split(/[\\/]/);
|
|
256
|
+
return {
|
|
257
|
+
target: summary?.target ?? target?.targetName ?? rel.at(-2),
|
|
258
|
+
suite: summary?.suite ?? suite?.suiteName ?? rel.at(-1),
|
|
259
|
+
runId: target?.runId ?? rel.at(-3),
|
|
260
|
+
ok: result.ok === true,
|
|
261
|
+
artifactDir: suiteDir,
|
|
262
|
+
paths: pathFields(suiteDir),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function targetIndexesFromRun(targetName, result, artifactRoot) {
|
|
267
|
+
const suiteResults = Array.isArray(result?.results) ? result.results : [result];
|
|
268
|
+
const suites = suiteResults.map((suiteResult) => suiteIndexFromResult(suiteResult, artifactRoot)).filter(Boolean);
|
|
269
|
+
const runIds = [...new Set(suites.map((suite) => suite.runId).filter(Boolean))];
|
|
270
|
+
return {
|
|
271
|
+
target: targetName,
|
|
272
|
+
ok: result?.ok === true,
|
|
273
|
+
...(result?.error ? { error: redactSecrets(result.error) } : {}),
|
|
274
|
+
runId: runIds.length === 1 ? runIds[0] : undefined,
|
|
275
|
+
runIds,
|
|
276
|
+
suites,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Build a stable, agent-readable platform-smoke latest index from target run results. */
|
|
281
|
+
export function buildLatestPlatformSmokeIndex(config, runResults, metadata = {}) {
|
|
282
|
+
const artifactRoot = resolve(process.cwd(), config?.artifactRoot ?? ".artifacts/platform-smoke");
|
|
283
|
+
const targets = runResults.map(({ targetName, result }) => targetIndexesFromRun(targetName, result, artifactRoot));
|
|
284
|
+
const runIds = [...new Set(targets.flatMap((target) => target.runIds).filter(Boolean))].sort();
|
|
285
|
+
const newestRunId = runIds
|
|
286
|
+
.map((runId) => ({ runId, match: PLATFORM_SMOKE_RUN_DIR_PATTERN.exec(runId) }))
|
|
287
|
+
.filter((entry) => entry.match)
|
|
288
|
+
.sort((a, b) => Number(b.match[1]) - Number(a.match[1]))[0]?.runId ?? runIds.at(-1);
|
|
289
|
+
return {
|
|
290
|
+
schemaVersion: 1,
|
|
291
|
+
kind: "platform-smoke-latest",
|
|
292
|
+
runId: runIds.length === 1 ? runIds[0] : newestRunId,
|
|
293
|
+
runIds,
|
|
294
|
+
artifactRoot,
|
|
295
|
+
startedAt: metadata.startedAt,
|
|
296
|
+
finishedAt: metadata.finishedAt,
|
|
297
|
+
command: metadata.command,
|
|
298
|
+
pid: process.pid,
|
|
299
|
+
ok: targets.every((target) => target.ok),
|
|
300
|
+
targets,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Atomically write .artifacts/platform-smoke/latest.json. */
|
|
305
|
+
export function writeLatestPlatformSmokeIndex(config, runResults, metadata = {}) {
|
|
306
|
+
const index = buildLatestPlatformSmokeIndex(config, runResults, metadata);
|
|
307
|
+
mkdirSync(index.artifactRoot, { recursive: true });
|
|
308
|
+
const outPath = resolve(index.artifactRoot, LATEST_INDEX_NAME);
|
|
309
|
+
const tmpPath = resolve(dirname(outPath), `.${LATEST_INDEX_NAME}.${process.pid}.${Date.now()}.tmp`);
|
|
310
|
+
writeFileSync(tmpPath, `${JSON.stringify(index, null, 2)}\n`);
|
|
311
|
+
renameSync(tmpPath, outPath);
|
|
312
|
+
return { index, path: outPath };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Return concise existing evidence paths for a failed suite result. */
|
|
316
|
+
export function platformSmokeSuiteEvidence(result, artifactRoot) {
|
|
317
|
+
const suite = suiteIndexFromResult(result, artifactRoot ?? ".artifacts/platform-smoke");
|
|
318
|
+
if (!suite) return undefined;
|
|
319
|
+
return {
|
|
320
|
+
suite: suite.suite,
|
|
321
|
+
artifactDir: suite.artifactDir,
|
|
322
|
+
paths: suite.paths,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
103
326
|
/** Write command.txt recording the command that was executed. */
|
|
104
327
|
export function writeCommand(dir, cmd) {
|
|
105
328
|
writeFileSync(resolve(dir, "command.txt"), Array.isArray(cmd) ? cmd.join(" ") + "\n" : cmd + "\n");
|
|
@@ -18,7 +18,7 @@ const CARD_PATTERNS = [
|
|
|
18
18
|
{ id: "write", pattern: /^\s*\+.*beta\s*$/i },
|
|
19
19
|
{ id: "edit-diff", pattern: /^\s*\+.*gamma\s*$/i },
|
|
20
20
|
{ id: "shell-failure", pattern: /^\s*(?:native shell failure|Command exited with code 7)\s*$/i },
|
|
21
|
-
{ id: "bridge-read-success", pattern: /^\s*read
|
|
21
|
+
{ id: "bridge-read-success", pattern: /^\s*read (?:\.\/package\.json|.*[\\/]package\.json)\s*$/i },
|
|
22
22
|
{ id: "bridge-read-failure", pattern: /^\s*(?:read \.\/definitely-missing-platform-smoke-file\.txt|ENOENT: no such file)\s*/i },
|
|
23
23
|
{ id: "bridge-shell-success", pattern: /^\s*bridge visual smoke\s*$/i },
|
|
24
24
|
{ id: "footer-status", pattern: /\bcomposer-2-5\b|\bcomposer-2\.5\b/i },
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { execSync, execFileSync } from "node:child_process";
|
|
10
|
-
import { accessSync, constants, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
10
|
+
import { accessSync, constants, existsSync, mkdirSync, writeFileSync, unlinkSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { renderAll } from "./render-ansi.mjs";
|
|
12
13
|
|
|
13
14
|
let failures = 0;
|
|
14
15
|
|
|
@@ -126,6 +127,41 @@ function disposableWindowsSshProbe(cbox, config = {}) {
|
|
|
126
127
|
|
|
127
128
|
function hasBin(name) { return silent("which", [name]) !== null; }
|
|
128
129
|
|
|
130
|
+
async function runRenderProbe(artifactRoot) {
|
|
131
|
+
const probeDir = resolve(artifactRoot, `.doctor-render-${process.pid}-${Date.now()}`);
|
|
132
|
+
try {
|
|
133
|
+
mkdirSync(probeDir, { recursive: true });
|
|
134
|
+
const ansiPath = resolve(probeDir, "terminal.ansi");
|
|
135
|
+
writeFileSync(ansiPath, "\u001b[32mplatform smoke render probe\u001b[0m\n");
|
|
136
|
+
const rendered = await renderAll(ansiPath, probeDir, {
|
|
137
|
+
label: "doctor-render-probe",
|
|
138
|
+
model: "doctor",
|
|
139
|
+
mode: "doctor",
|
|
140
|
+
cwd: process.cwd(),
|
|
141
|
+
sessionId: "doctor-render-probe",
|
|
142
|
+
width: 80,
|
|
143
|
+
height: 10,
|
|
144
|
+
historyLines: 100,
|
|
145
|
+
});
|
|
146
|
+
const pngPath = resolve(probeDir, "terminal.full.png");
|
|
147
|
+
const pngOk = rendered.pngOk && existsSync(pngPath) && statSync(pngPath).size > 100;
|
|
148
|
+
if (!pngOk) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
message: `host-side xterm/Playwright render probe did not produce a PNG at ${pngPath}. Run: npx playwright install chromium`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, message: pngPath };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
message: `host-side xterm/Playwright render probe failed: ${error instanceof Error ? error.message : String(error)}. Run npm install, then: npx playwright install chromium`,
|
|
159
|
+
};
|
|
160
|
+
} finally {
|
|
161
|
+
rmSync(probeDir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
129
165
|
function findGitRoot(startPath) {
|
|
130
166
|
let dir = startPath;
|
|
131
167
|
for (let i = 0; i < 8; i++) {
|
|
@@ -137,7 +173,7 @@ function findGitRoot(startPath) {
|
|
|
137
173
|
return null;
|
|
138
174
|
}
|
|
139
175
|
|
|
140
|
-
function runChecks(config) {
|
|
176
|
+
async function runChecks(config) {
|
|
141
177
|
// ── Phase 1: environment variables ──
|
|
142
178
|
console.log("\n── Environment variables ──");
|
|
143
179
|
const requiredVars = [
|
|
@@ -396,7 +432,16 @@ function runChecks(config) {
|
|
|
396
432
|
fail(`cannot write to ${artRoot}: ${e.message}`);
|
|
397
433
|
}
|
|
398
434
|
|
|
399
|
-
// ── Phase 10:
|
|
435
|
+
// ── Phase 10: Host-side visual render probe ──
|
|
436
|
+
console.log("\n── Host-side visual render probe ──");
|
|
437
|
+
const renderProbe = await runRenderProbe(artRoot);
|
|
438
|
+
if (renderProbe.ok) {
|
|
439
|
+
ok("xterm/Playwright Chromium render probe wrote a PNG");
|
|
440
|
+
} else {
|
|
441
|
+
fail(renderProbe.message);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Phase 11: Git status ──
|
|
400
445
|
console.log("\n── Git status ──");
|
|
401
446
|
const branch = shell("git branch --show-current");
|
|
402
447
|
branch ? ok(`branch: ${branch}`) : warn("could not determine branch");
|
|
@@ -408,7 +453,7 @@ function runChecks(config) {
|
|
|
408
453
|
ok("clean worktree");
|
|
409
454
|
}
|
|
410
455
|
|
|
411
|
-
// ── Phase
|
|
456
|
+
// ── Phase 12: Forbidden files ──
|
|
412
457
|
console.log("\n── Forbidden files ──");
|
|
413
458
|
let anyForbidden = false;
|
|
414
459
|
for (const pat of [".env", "*.tgz"]) {
|
|
@@ -428,7 +473,7 @@ function runChecks(config) {
|
|
|
428
473
|
}
|
|
429
474
|
ok("no tracked .env.*");
|
|
430
475
|
|
|
431
|
-
// ── Phase
|
|
476
|
+
// ── Phase 13: Cursor auth ──
|
|
432
477
|
console.log("\n── Cursor auth ──");
|
|
433
478
|
const key = env("CURSOR_API_KEY");
|
|
434
479
|
if (key && key.length > 10) {
|
|
@@ -439,7 +484,7 @@ function runChecks(config) {
|
|
|
439
484
|
fail("CURSOR_API_KEY missing — live Cursor suites will not run");
|
|
440
485
|
}
|
|
441
486
|
|
|
442
|
-
// ── Phase
|
|
487
|
+
// ── Phase 14: node-pty self-test ──
|
|
443
488
|
console.log("\n── node-pty self-test ──");
|
|
444
489
|
const ptyPath = resolve(process.cwd(), "node_modules", "node-pty");
|
|
445
490
|
if (existsSync(ptyPath)) {
|
|
@@ -458,7 +503,7 @@ function runChecks(config) {
|
|
|
458
503
|
warn("node-pty not installed — live PTY suites will not run. Run: npm ci");
|
|
459
504
|
}
|
|
460
505
|
|
|
461
|
-
// ── Phase
|
|
506
|
+
// ── Phase 15: Summary ──
|
|
462
507
|
console.log(`\n=== Results: ${failures} failure(s) ===`);
|
|
463
508
|
if (failures > 0) {
|
|
464
509
|
console.log("Fix failures above before running live Cursor suites.");
|
|
@@ -470,5 +515,5 @@ function runChecks(config) {
|
|
|
470
515
|
}
|
|
471
516
|
|
|
472
517
|
export async function runDoctor(config) {
|
|
473
|
-
runChecks(config);
|
|
518
|
+
await runChecks(config);
|
|
474
519
|
}
|