pi-cursor-sdk 0.1.31 → 0.1.33

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 CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.33 - 2026-06-04
6
+
7
+ ### Fixed
8
+
9
+ - Prevent connect-node-only Cursor SDK network resets such as `ConnectError: [aborted] read ECONNRESET` from escaping as process-level uncaught exceptions during active Cursor turns, while keeping provenance-free generic ConnectRPC errors unsuppressed (#121).
10
+ - Suppress expected Cursor SDK abort `ConnectError` / `AbortError` shapes during abandoned live-run cancellation so idle-resume and interrupt cleanup paths keep pi alive for later prompts (#120).
11
+
12
+ ## 0.1.32 - 2026-06-02
13
+
14
+ ### Added
15
+
16
+ - Add a production typing-safety regression test that blocks broad TypeScript escape hatches such as `as unknown as`, `as any`, `as never`, explicit `any`, and production `@ts-ignore` / `@ts-expect-error` usage.
17
+
18
+ ### Changed
19
+
20
+ - Replace repeated native replay render-test `as never` casts with typed test render fixture helpers.
21
+ - Use the maintained Homebrew Crabbox binary on `PATH` for platform smoke with a `0.24.0` minimum version, keeping `PLATFORM_SMOKE_CRABBOX` as an explicit override only.
22
+
23
+ ### Fixed
24
+
25
+ - Harden local Cursor cache/config JSON parsing so model-list, context-window, and fast-default files are validated from `unknown` before trusted values are used.
26
+
5
27
  ## 0.1.31 - 2026-06-01
6
28
 
7
29
  ### Added
package/README.md CHANGED
@@ -2,7 +2,27 @@
2
2
 
3
3
  A pi provider extension that lets pi use Cursor models through the local `@cursor/sdk` agent runtime.
4
4
 
5
- Use this extension if you want Cursor's model catalog inside pi while keeping pi's native model picker, thinking controls where the SDK exposes them, session restore, context display, and default footer UX.
5
+ Use this extension if you primarily use Cursor models inside pi and want Cursor's local SDK agent loop preserved while pi adds native model selection, auth, thinking/context controls, session behavior, replay UI, and optional pi tool bridging.
6
+
7
+ ## Why use this instead of an OpenAI-compatible Cursor endpoint?
8
+
9
+ Use `pi-cursor-sdk` when you primarily want to use Cursor models **inside pi**.
10
+
11
+ This extension runs Cursor models through the local `@cursor/sdk` agent runtime and keeps Cursor's agent loop intact. pi integrates around that loop: model discovery, model selection, context-window variants, thinking controls where Cursor exposes them, fast/slow aliases, Cursor mode, session handling, native replay cards, and the optional pi tool bridge.
12
+
13
+ OpenAI-compatible Cursor proxies are useful when you want a generic `/v1/chat/completions` or `/v1/responses` endpoint for many clients such as curl, the OpenAI SDK, OpenCode, or other tools. That compatibility comes from translating Cursor behavior into OpenAI-shaped requests, responses, and tool calls.
14
+
15
+ For pi users, that translation is usually the wrong abstraction. `pi-cursor-sdk` is pi-specific on purpose: it lets Cursor remain Cursor while making it feel native in pi.
16
+
17
+ | If you want... | Prefer |
18
+ | --- | --- |
19
+ | First-class Cursor usage inside pi | `pi-cursor-sdk` |
20
+ | Cursor's local SDK agent loop preserved, not replaced by an OpenAI-shaped adapter | `pi-cursor-sdk` |
21
+ | pi model picker, `/login`, `/model`, sessions, context display, footer/status UX | `pi-cursor-sdk` |
22
+ | Cursor SDK local-agent tools, settings, MCP, and native replay surfaced in pi | `pi-cursor-sdk` |
23
+ | pi extension tools exposed to Cursor through a local MCP bridge | `pi-cursor-sdk` |
24
+ | A generic OpenAI-compatible localhost `/v1` API for non-pi clients | An OpenAI-compatible Cursor proxy |
25
+ | One Cursor-ish endpoint shared across several unrelated tools | An OpenAI-compatible Cursor proxy |
6
26
 
7
27
  ## Quick start
8
28
 
@@ -1,6 +1,6 @@
1
1
  # Cursor Testing Lessons
2
2
 
3
- > **Platform Smoke (new):** The required cross-platform release gate is `npm run smoke:platform:doctor && npm run smoke:platform:all`. See [docs/platform-smoke.md](./platform-smoke.md). For portable lessons other pi extension projects can adapt without sharing repo-specific state, see [Crabbox Platform Testing Lessons](./crabbox-platform-testing-lessons.md). The live smoke checklist remains useful for inner-loop development but is not the release gate.
3
+ > **Platform Smoke (new):** The required cross-platform release gate is `npm run smoke:platform:doctor && npm run smoke:platform:all`. See [docs/platform-smoke.md](./platform-smoke.md). For portable lessons other pi extension projects can adapt without sharing repo-specific state, see the generic Crabbox platform testing guide at `/Users/mitchfultz/Projects/crabbox/docs/pi-extension-platform-testing.md`. The live smoke checklist remains useful for inner-loop development but is not the release gate.
4
4
 
5
5
  ## Purpose
6
6
 
@@ -6,6 +6,8 @@ Branch introduced by: `feat/crabbox-platform-smoke`
6
6
 
7
7
  Oracle review incorporated: this gate resolves the packed-install workspace conflict, Cursor budget contradiction, Windows shell drift, artifact-on-failure gap, render-location ambiguity, provider-debug ambiguity, and registry-classification gap called out during review.
8
8
 
9
+ Crabbox best-practice baseline applied from `~/Projects/crabbox`: Crabbox owns lease, sync, run, evidence transport, and cleanup; this repo owns target policy, package setup, scenario meaning, assertions, artifacts, auth forwarding, redaction, and release criteria.
10
+
9
11
  ## Decision
10
12
 
11
13
  Crabbox is the required platform smoke runner for `pi-cursor-sdk` releases that touch Cursor provider/runtime behavior.
@@ -52,12 +54,12 @@ The runner uses one supported Crabbox build.
52
54
  Current baseline:
53
55
 
54
56
  ```text
55
- install: brew install crabbox
56
- version: 0.24.0
57
- binary: /opt/homebrew/bin/crabbox on Apple Silicon Homebrew installs
57
+ install: brew install openclaw/tap/crabbox
58
+ version: 0.26.0 or newer
59
+ binary: Homebrew `crabbox` on PATH (`/opt/homebrew/bin/crabbox` on Apple Silicon Homebrew installs)
58
60
  ```
59
61
 
60
- Keep this exact version or replace it with another exact released Crabbox version when updating the gate. `smoke:platform:doctor` verifies the configured Crabbox binary and fails on mismatch.
62
+ Use the Homebrew Crabbox binary on PATH for normal release gates. `PLATFORM_SMOKE_CRABBOX=/path/to/crabbox` is only an explicit override for testing a non-default binary. `smoke:platform:doctor` verifies the configured binary and fails when it is older than the configured minimum version.
61
63
 
62
64
  Required Crabbox providers:
63
65
 
@@ -75,6 +77,8 @@ scenario + target capability + artifact contract
75
77
 
76
78
  not a one-off shell script.
77
79
 
80
+ Crabbox is deliberately kept as the transport/lifecycle layer. It must not be treated as proof that the pi extension behavior passed; every suite still fails or passes from project-owned assertions and artifact manifests.
81
+
78
82
  High-level flow:
79
83
 
80
84
  ```text
@@ -145,19 +149,21 @@ scripts/platform-smoke/artifacts.mjs
145
149
  scripts/platform-smoke/card-detect.mjs
146
150
  scripts/platform-smoke/crabbox-runner.mjs
147
151
  scripts/platform-smoke/doctor.mjs
152
+ scripts/platform-smoke/jsonl-text.mjs
148
153
  scripts/platform-smoke/live-suite-runner.mjs
149
154
  scripts/platform-smoke/platform-build-windows.ps1
150
155
  scripts/platform-smoke/pty-capture.mjs
151
156
  scripts/platform-smoke/render-ansi.mjs
152
157
  scripts/platform-smoke/scenarios.mjs
153
158
  scripts/platform-smoke/targets.mjs
159
+ scripts/platform-smoke/visual-evidence.mjs
154
160
  ```
155
161
 
156
162
  Package scripts:
157
163
 
158
164
  ```json
159
165
  {
160
- "check:platform-smoke": "node --check <platform smoke scripts> && vitest run test/smoke-tooling.test.ts",
166
+ "check:platform-smoke": "node --check platform-smoke.config.mjs && node --check <platform smoke scripts> && vitest run test/smoke-tooling.test.ts",
161
167
  "smoke:platform": "node scripts/platform-smoke.mjs",
162
168
  "smoke:platform:doctor": "node scripts/platform-smoke.mjs doctor",
163
169
  "smoke:platform:macos": "node scripts/platform-smoke.mjs run --target macos",
@@ -167,7 +173,7 @@ Package scripts:
167
173
  }
168
174
  ```
169
175
 
170
- Add `.artifacts/`, `.crabbox/`, and `.platform-smoke-runs/` to `.gitignore`.
176
+ Add `.artifacts/`, `.crabbox/`, `.debug/`, and `.platform-smoke-runs/` to `.gitignore`.
171
177
 
172
178
  ## Configuration source
173
179
 
@@ -188,21 +194,29 @@ export default {
188
194
  "cursor-abort-cleanup",
189
195
  ],
190
196
  requiredCrabbox: {
191
- install: "brew install crabbox",
192
- version: "0.24.0",
197
+ install: "Homebrew package or PLATFORM_SMOKE_CRABBOX override",
198
+ minVersion: "0.26.0",
193
199
  },
194
200
  ubuntuContainerImage: "cimg/node:24.16",
195
201
  nodeValidationMajor: 24,
202
+ windowsParallels: {
203
+ sourceVm: "pi-extension-windows-template",
204
+ snapshot: "crabbox-ready",
205
+ workRoot: "C:\\crabbox\\pi-cursor-sdk",
206
+ },
196
207
  };
197
208
  ```
198
209
 
199
210
  `ubuntuContainerImage` defaults the local-container Ubuntu target to an Ubuntu 24.04 Node 24 image with a current glibc baseline for native test dependencies; Crabbox still bootstraps SSH/Git/rsync/curl as needed. `nodeValidationMajor: 24` is the release-smoke validation baseline. It does not change the package engine by itself. A separate compatibility lane can test Node 22.19 later; this required gate validates Node 24 on every target.
200
211
 
212
+ `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
+
201
214
  ## Required local environment
202
215
 
203
- The doctor fails if any required value is missing.
216
+ 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.
204
217
 
205
218
  ```bash
219
+ # Optional override; by default the gate uses Homebrew `crabbox` from PATH.
206
220
  PLATFORM_SMOKE_CRABBOX=/opt/homebrew/bin/crabbox
207
221
 
208
222
  PLATFORM_SMOKE_MAC_HOST=localhost
@@ -210,11 +224,13 @@ PLATFORM_SMOKE_MAC_USER="$USER"
210
224
  PLATFORM_SMOKE_MAC_WORK_ROOT="/Users/$USER/crabbox/pi-cursor-sdk"
211
225
  PLATFORM_SMOKE_UBUNTU_IMAGE="cimg/node:24.16"
212
226
 
213
- PLATFORM_SMOKE_WINDOWS_VM="Windows 11"
227
+ # Optional Parallels overrides; defaults come from platform-smoke.config.mjs.
228
+ PLATFORM_SMOKE_WINDOWS_VM="pi-extension-windows-template"
214
229
  PLATFORM_SMOKE_WINDOWS_SNAPSHOT="crabbox-ready"
215
230
  PLATFORM_SMOKE_WINDOWS_USER="<windows-ssh-user>"
216
231
  PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT="C:\\crabbox\\pi-cursor-sdk"
217
232
 
233
+ # Required for live suites; doctor fails before spending Cursor tokens if absent.
218
234
  CURSOR_API_KEY="..."
219
235
  ```
220
236
 
@@ -277,7 +293,13 @@ Required:
277
293
 
278
294
  ### Windows template VM
279
295
 
280
- The user's daily Windows VM is not the long-term test target. A dedicated template VM and snapshot are required.
296
+ The user's daily Windows VM is not the long-term test target. Use the shared pi-extension Parallels template unless this project documents a replacement with equal evidence:
297
+
298
+ ```text
299
+ source VM: pi-extension-windows-template
300
+ snapshot: crabbox-ready
301
+ work root: C:\\crabbox\\pi-cursor-sdk
302
+ ```
281
303
 
282
304
  Template requirements:
283
305
 
@@ -292,8 +314,9 @@ Template requirements:
292
314
  - `node-pty` self-test passes in native Windows.
293
315
  - Source VM is powered off.
294
316
  - Snapshot named `crabbox-ready` exists.
317
+ - The template contains reusable platform tools only; no repo checkout, `.pi` state, Cursor API key, browser auth, smoke artifacts, or temp files.
295
318
 
296
- Crabbox Parallels creates linked clones from the powered-off snapshot. The source template VM is never used directly for smoke runs.
319
+ Crabbox Parallels creates linked clones from the powered-off snapshot. The source template VM is never used directly for smoke runs. If a run has to install a missing global tool or browser on every Windows clone, treat that as template drift and refresh the shared template instead of making the per-run fallback normal.
297
320
 
298
321
  ### Windows native
299
322
 
@@ -312,13 +335,13 @@ tar --version
312
335
 
313
336
  Doctor checks:
314
337
 
315
- 1. Required env vars exist.
316
- 2. `PLATFORM_SMOKE_CRABBOX` exists and is executable.
338
+ 1. Required auth is present and optional target overrides resolve against config defaults.
339
+ 2. Homebrew `crabbox` is available on PATH, or `PLATFORM_SMOKE_CRABBOX` points at an executable override.
317
340
  3. Crabbox build matches the configured baseline.
318
341
  4. Crabbox provider registry includes `local-container`, `ssh`, and `parallels`.
319
- 5. `crabbox doctor --provider local-container --json` passes.
342
+ 5. `crabbox doctor --provider local-container --target linux --json` passes.
320
343
  6. Docker runtime is active.
321
- 7. macOS SSH localhost probe passes and sees Node, npm, Git, rsync, and tar.
344
+ 7. Crabbox macOS static SSH doctor with `--doctor-probe-ssh` passes, and the localhost SSH probe sees Node, npm, Git, rsync, and tar.
322
345
  8. `prlctl` exists.
323
346
  9. Windows source VM exists.
324
347
  10. Windows source snapshot exists.
@@ -357,7 +380,7 @@ Per target, `platform-build` must:
357
380
 
358
381
  1. Record `node --version` and assert the target Node major is at least `nodeValidationMajor`.
359
382
  2. Run `npm ci` in `extensionSourceRoot`.
360
- 3. Run `npm run check:platform-smoke` on the target so smoke harness syntax and invariant tests fail before live Cursor calls, with only the target-local release-tag guard bypassed because Crabbox worktrees may not have release tags.
383
+ 3. Run `npm run check:platform-smoke` on the target so config syntax, smoke harness syntax, invalid target/suite guards, and invariant tests fail before live Cursor calls.
361
384
  4. Run `npm test` on the target with the same target-local release-tag guard bypass.
362
385
  5. Run `npm run typecheck`.
363
386
  6. Run `npm pack`.
@@ -378,7 +401,7 @@ Purpose:
378
401
  - fail before spending Cursor tokens;
379
402
  - produce the packed extension used by later suites.
380
403
 
381
- The host `smoke:platform:all` entrypoint enforces doctor first and then the release-version reuse guard before running targets, using local git tags and `package.json`. 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.
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.
382
405
 
383
406
  ### `cursor-native-visual-matrix`
384
407
 
@@ -595,7 +618,7 @@ cursor-abort-cleanup: 1
595
618
 
596
619
  Maximum per target: `3` Cursor invocations.
597
620
 
598
- Maximum full gate: `12` Cursor invocations.
621
+ Maximum full gate: `9` Cursor invocations.
599
622
 
600
623
  The merge gate is `npm run smoke:platform:all`; that script runs doctor first and then the matrix to preserve this budget. No suite adds a new Cursor invocation without updating this plan and `platform-smoke.config.mjs`.
601
624
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
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",
@@ -69,7 +69,6 @@
69
69
  "docs/cursor-tool-surfaces.md",
70
70
  "docs/cursor-live-smoke-checklist.md",
71
71
  "docs/cursor-testing-lessons.md",
72
- "docs/crabbox-platform-testing-lessons.md",
73
72
  "docs/cursor-dogfood-checklist.md",
74
73
  "docs/cursor-native-tool-replay.md",
75
74
  "docs/cursor-native-tool-visual-audit.md",
@@ -98,7 +97,7 @@
98
97
  "debug:sdk-events": "node scripts/debug-sdk-events.mjs",
99
98
  "debug:provider-events": "node scripts/debug-provider-events.mjs",
100
99
  "debug:mcp-coldstart": "node scripts/probe-mcp-coldstart.mjs",
101
- "check:platform-smoke": "node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/card-detect.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/jsonl-text.mjs && node --check scripts/platform-smoke/live-suite-runner.mjs && node --check scripts/platform-smoke/pty-capture.mjs && node --check scripts/platform-smoke/render-ansi.mjs && node --check scripts/platform-smoke/scenarios.mjs && node --check scripts/platform-smoke/targets.mjs && node --check scripts/platform-smoke/visual-evidence.mjs && vitest run test/smoke-tooling.test.ts",
100
+ "check:platform-smoke": "node --check platform-smoke.config.mjs && node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/card-detect.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/jsonl-text.mjs && node --check scripts/platform-smoke/live-suite-runner.mjs && node --check scripts/platform-smoke/pty-capture.mjs && node --check scripts/platform-smoke/render-ansi.mjs && node --check scripts/platform-smoke/scenarios.mjs && node --check scripts/platform-smoke/targets.mjs && node --check scripts/platform-smoke/visual-evidence.mjs && vitest run test/smoke-tooling.test.ts",
102
101
  "smoke:platform": "node scripts/platform-smoke.mjs",
103
102
  "smoke:platform:doctor": "node scripts/platform-smoke.mjs doctor",
104
103
  "smoke:platform:macos": "node scripts/platform-smoke.mjs run --target macos",
@@ -13,9 +13,14 @@ export default {
13
13
  "cursor-abort-cleanup",
14
14
  ],
15
15
  requiredCrabbox: {
16
- install: "brew install crabbox",
17
- version: "0.24.0",
16
+ install: "Homebrew package or PLATFORM_SMOKE_CRABBOX override",
17
+ minVersion: "0.26.0",
18
18
  },
19
19
  ubuntuContainerImage: "cimg/node:24.16",
20
20
  nodeValidationMajor: 24,
21
+ windowsParallels: {
22
+ sourceVm: "pi-extension-windows-template",
23
+ snapshot: "crabbox-ready",
24
+ workRoot: "C:\\crabbox\\pi-cursor-sdk",
25
+ },
21
26
  };
@@ -98,10 +98,11 @@ export function buildTargetBaseArgs(targetName, config = {}) {
98
98
  ];
99
99
  }
100
100
  case "windows-native": {
101
- const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || "pi-extension-windows-template";
102
- const snap = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || "crabbox-ready";
103
- const user = env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
104
- const workRoot = env("PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || "C:\\crabbox\\pi-cursor-sdk";
101
+ const windows = config.windowsParallels ?? {};
102
+ const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || windows.sourceVm || "pi-extension-windows-template";
103
+ const snap = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || windows.snapshot || "crabbox-ready";
104
+ const user = env("PLATFORM_SMOKE_WINDOWS_USER") || windows.user || env("USER");
105
+ const workRoot = env("PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || windows.workRoot || "C:\\crabbox\\pi-cursor-sdk";
105
106
  return [
106
107
  "--provider", "parallels",
107
108
  "--target", "windows",
@@ -45,17 +45,32 @@ function shell(cmd, opts = {}) {
45
45
  catch { return null; }
46
46
  }
47
47
 
48
+ function commandPath(command) {
49
+ return shell(`command -v ${command}`);
50
+ }
51
+
48
52
  function parseLeaseId(output) {
49
53
  return output.match(/\bleased\s+(\S+)/)?.[1]
50
54
  ?? output.match(/\blease=(\S+)/)?.[1]
51
55
  ?? null;
52
56
  }
53
57
 
54
- function windowsCrabboxBaseArgs() {
55
- const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || "pi-extension-windows-template";
56
- const snap = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || "crabbox-ready";
57
- const user = env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
58
- const workRoot = env("PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || "C:\\crabbox\\pi-cursor-sdk";
58
+ function windowsParallelsDefaults(config = {}) {
59
+ const windows = config?.windowsParallels ?? {};
60
+ return {
61
+ vm: windows.sourceVm || "pi-extension-windows-template",
62
+ snapshot: windows.snapshot || "crabbox-ready",
63
+ user: windows.user || env("USER"),
64
+ workRoot: windows.workRoot || "C:\\crabbox\\pi-cursor-sdk",
65
+ };
66
+ }
67
+
68
+ function windowsCrabboxBaseArgs(config = {}) {
69
+ const defaults = windowsParallelsDefaults(config);
70
+ const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || defaults.vm;
71
+ const snap = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || defaults.snapshot;
72
+ const user = env("PLATFORM_SMOKE_WINDOWS_USER") || defaults.user;
73
+ const workRoot = env("PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || defaults.workRoot;
59
74
  return [
60
75
  "--provider", "parallels",
61
76
  "--target", "windows",
@@ -87,9 +102,9 @@ function crabbox(cbox, args, timeout = 300_000) {
87
102
  }
88
103
  }
89
104
 
90
- function disposableWindowsSshProbe(cbox) {
105
+ function disposableWindowsSshProbe(cbox, config = {}) {
91
106
  const slug = "pi-cursor-sdk-doctor-windows";
92
- const baseArgs = windowsCrabboxBaseArgs();
107
+ const baseArgs = windowsCrabboxBaseArgs(config);
93
108
  const warm = crabbox(cbox, ["warmup", ...baseArgs, "--slug", slug, "--keep", "--reclaim"], 300_000);
94
109
  const leaseId = parseLeaseId(warm.stdout) ?? parseLeaseId(warm.stderr) ?? slug;
95
110
  try {
@@ -126,37 +141,50 @@ function runChecks(config) {
126
141
  // ── Phase 1: environment variables ──
127
142
  console.log("\n── Environment variables ──");
128
143
  const requiredVars = [
129
- "PLATFORM_SMOKE_CRABBOX",
130
144
  "CURSOR_API_KEY",
131
- "PLATFORM_SMOKE_WINDOWS_VM",
132
- "PLATFORM_SMOKE_WINDOWS_SNAPSHOT",
133
- "PLATFORM_SMOKE_WINDOWS_USER",
134
- "PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT",
135
145
  ];
136
146
  const optionalVars = [
147
+ "PLATFORM_SMOKE_CRABBOX",
137
148
  "PLATFORM_SMOKE_MAC_HOST",
138
149
  "PLATFORM_SMOKE_MAC_USER",
139
150
  "PLATFORM_SMOKE_MAC_WORK_ROOT",
140
151
  "PLATFORM_SMOKE_UBUNTU_IMAGE",
152
+ "PLATFORM_SMOKE_WINDOWS_VM",
153
+ "PLATFORM_SMOKE_WINDOWS_SNAPSHOT",
154
+ "PLATFORM_SMOKE_WINDOWS_USER",
155
+ "PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT",
141
156
  ];
142
157
  for (const name of requiredVars) {
143
158
  const v = env(name);
144
159
  v ? ok(`${name} = ${name === "CURSOR_API_KEY" ? "(present, redacted)" : (v.length > 50 ? v.slice(0, 50) + "..." : v)}`)
145
160
  : fail(`${name} missing`);
146
161
  }
162
+ const windowsDefaults = windowsParallelsDefaults(config);
163
+ const optionalDefaults = {
164
+ PLATFORM_SMOKE_WINDOWS_VM: windowsDefaults.vm,
165
+ PLATFORM_SMOKE_WINDOWS_SNAPSHOT: windowsDefaults.snapshot,
166
+ PLATFORM_SMOKE_WINDOWS_USER: windowsDefaults.user,
167
+ PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT: windowsDefaults.workRoot,
168
+ };
147
169
  for (const name of optionalVars) {
148
170
  const v = env(name);
149
- ok(`${name} = ${v || "(default)"}`);
171
+ const fallback = optionalDefaults[name] ? `(default: ${optionalDefaults[name]})` : "(default)";
172
+ ok(`${name} = ${v || fallback}`);
150
173
  }
151
174
 
152
175
  // ── Phase 2: Crabbox binary ──
153
176
  console.log("\n── Crabbox binary ──");
154
- const cbox = env("PLATFORM_SMOKE_CRABBOX");
155
- if (!cbox) {
156
- fail("PLATFORM_SMOKE_CRABBOX not set");
177
+ const cbox = env("PLATFORM_SMOKE_CRABBOX") || "crabbox";
178
+ const cboxPath = env("PLATFORM_SMOKE_CRABBOX") || commandPath("crabbox");
179
+ if (!cboxPath) {
180
+ fail(`crabbox not found on PATH; install with ${config.requiredCrabbox?.install ?? "Homebrew"} or set PLATFORM_SMOKE_CRABBOX`);
157
181
  } else {
158
- try { accessSync(cbox, constants.X_OK); ok(`binary: ${cbox}`); }
159
- catch { fail(`${cbox} not executable`); }
182
+ if (env("PLATFORM_SMOKE_CRABBOX")) {
183
+ try { accessSync(cboxPath, constants.X_OK); ok(`binary: ${cboxPath} (env override)`); }
184
+ catch { fail(`${cboxPath} not executable`); }
185
+ } else {
186
+ ok(`binary: ${cboxPath} (PATH)`);
187
+ }
160
188
  const ver = silent(cbox, ["--version"]);
161
189
  const actualVersion = ver?.split("\n")[0]?.trim();
162
190
  if (actualVersion) ok(`version: ${actualVersion}`);
@@ -174,9 +202,9 @@ function runChecks(config) {
174
202
  }
175
203
  const requiredCommit = config.requiredCrabbox?.commit;
176
204
  if (!requiredVersion && !minimumVersion && requiredCommit) {
177
- const gitRoot = findGitRoot(dirname(cbox));
205
+ const gitRoot = findGitRoot(dirname(cboxPath));
178
206
  const actualCommit = gitRoot ? silent("git", ["-C", gitRoot, "rev-parse", "HEAD"]) : null;
179
- if (!actualCommit) fail(`could not verify Crabbox source commit for ${cbox}`);
207
+ if (!actualCommit) fail(`could not verify Crabbox source commit for ${cboxPath}`);
180
208
  else if (actualCommit !== requiredCommit) fail(`Crabbox commit mismatch: expected ${requiredCommit}, got ${actualCommit}`);
181
209
  else ok(`commit: ${actualCommit}`);
182
210
  }
@@ -184,7 +212,7 @@ function runChecks(config) {
184
212
 
185
213
  // ── Phase 3: Crabbox providers ──
186
214
  console.log("\n── Crabbox providers ──");
187
- if (cbox) {
215
+ if (cboxPath) {
188
216
  const providerList = silent(cbox, ["providers"]);
189
217
  if (providerList) {
190
218
  for (const provider of ["ssh", "local-container", "parallels"]) {
@@ -196,7 +224,7 @@ function runChecks(config) {
196
224
  fail("crabbox providers failed");
197
225
  }
198
226
  const ubuntuImage = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config?.ubuntuContainerImage || "cimg/node:24.16";
199
- const lcDoc = silent(cbox, ["doctor", "--provider", "local-container", "--local-container-image", ubuntuImage, "--json"]);
227
+ const lcDoc = silent(cbox, ["doctor", "--provider", "local-container", "--target", "linux", "--local-container-image", ubuntuImage, "--json"]);
200
228
  if (lcDoc) {
201
229
  try {
202
230
  const d = JSON.parse(lcDoc);
@@ -214,7 +242,7 @@ function runChecks(config) {
214
242
  "doctor", "--provider", "ssh", "--target", "macos",
215
243
  "--static-host", sshHost, "--static-user", sshUser,
216
244
  "--static-port", "22", "--static-work-root", sshRoot,
217
- "--json",
245
+ "--doctor-probe-ssh", "--json",
218
246
  ]);
219
247
  if (sshDoc) {
220
248
  try {
@@ -256,7 +284,7 @@ function runChecks(config) {
256
284
  fail("prlctl not found");
257
285
  } else {
258
286
  ok("prlctl found");
259
- const vmName = env("PLATFORM_SMOKE_WINDOWS_VM") || "pi-extension-windows-template";
287
+ const vmName = env("PLATFORM_SMOKE_WINDOWS_VM") || windowsParallelsDefaults(config).vm;
260
288
  const list = shell("prlctl list -a --no-header 2>/dev/null");
261
289
  if (list) {
262
290
  const vms = list.split("\n").filter(Boolean);
@@ -270,7 +298,7 @@ function runChecks(config) {
270
298
  fail(`VM "${vmName}" state: ${status} — source VM must be stopped for linked clones`);
271
299
  }
272
300
 
273
- const snapName = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || "crabbox-ready";
301
+ const snapName = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || windowsParallelsDefaults(config).snapshot;
274
302
  const snapsJson = shell(`prlctl snapshot-list "${vmName}" -j 2>/dev/null`);
275
303
  let snapshotFound = false;
276
304
  let snapshotPowerOff = false;
@@ -314,7 +342,7 @@ function runChecks(config) {
314
342
  } else {
315
343
  ok(`template "${vmName}" has no IP; verifying Windows SSH/tools through a disposable Crabbox clone`);
316
344
  if (cbox && snapshotFound && snapshotPowerOff) {
317
- const probe = disposableWindowsSshProbe(cbox);
345
+ const probe = disposableWindowsSshProbe(cbox, config);
318
346
  probe.ok ? ok(`disposable Windows clone SSH/tool probe OK: ${probe.message}`) : fail(probe.message);
319
347
  } else {
320
348
  fail(`Windows SSH probe could not run because "${vmName}" has no IP and no verified snapshot was available`);
@@ -62,19 +62,15 @@ Write-SectionFile "NPM_CI_STDOUT" $NpmCiOut
62
62
  Write-SectionFile "NPM_CI_STDERR" $NpmCiErr
63
63
 
64
64
  Write-Output "=== check:platform-smoke ==="
65
- $env:PI_CURSOR_SKIP_RELEASE_VERSION_GUARD = "1"
66
65
  & npm.cmd run check:platform-smoke 1> $CheckPlatformSmokeOut 2> $CheckPlatformSmokeErr
67
66
  $CHECK_PLATFORM_SMOKE_EXIT = Exit-CodeFromLastCommand
68
- Remove-Item Env:\PI_CURSOR_SKIP_RELEASE_VERSION_GUARD -ErrorAction SilentlyContinue
69
67
  Write-Output "PLATFORM_CHECK_PLATFORM_SMOKE_EXIT=$CHECK_PLATFORM_SMOKE_EXIT"
70
68
  Write-SectionFile "CHECK_PLATFORM_SMOKE_STDOUT" $CheckPlatformSmokeOut
71
69
  Write-SectionFile "CHECK_PLATFORM_SMOKE_STDERR" $CheckPlatformSmokeErr
72
70
 
73
71
  Write-Output "=== npm test ==="
74
- $env:PI_CURSOR_SKIP_RELEASE_VERSION_GUARD = "1"
75
72
  & npm.cmd test 1> $NpmTestOut 2> $NpmTestErr
76
73
  $TEST_EXIT = Exit-CodeFromLastCommand
77
- Remove-Item Env:\PI_CURSOR_SKIP_RELEASE_VERSION_GUARD -ErrorAction SilentlyContinue
78
74
  Write-Output "PLATFORM_NPM_TEST_EXIT=$TEST_EXIT"
79
75
  Write-SectionFile "NPM_TEST_STDOUT" $NpmTestOut
80
76
  Write-SectionFile "NPM_TEST_STDERR" $NpmTestErr
@@ -422,14 +422,14 @@ export function buildPlatformBuildCommand(targetName, packageName = "pi-cursor-s
422
422
  lines.push(...posixSection("NPM_CI_STDERR", 'cat "$PACK_DIR/npm-ci.stderr.txt" 2>/dev/null || true'));
423
423
  lines.push("");
424
424
  lines.push('echo "=== check:platform-smoke ==="');
425
- lines.push('PI_CURSOR_SKIP_RELEASE_VERSION_GUARD=1 npm run check:platform-smoke >"$PACK_DIR/check-platform-smoke.stdout.txt" 2>"$PACK_DIR/check-platform-smoke.stderr.txt"');
425
+ lines.push('npm run check:platform-smoke >"$PACK_DIR/check-platform-smoke.stdout.txt" 2>"$PACK_DIR/check-platform-smoke.stderr.txt"');
426
426
  lines.push("CHECK_PLATFORM_SMOKE_EXIT=$?");
427
427
  lines.push('echo "PLATFORM_CHECK_PLATFORM_SMOKE_EXIT=$CHECK_PLATFORM_SMOKE_EXIT"');
428
428
  lines.push(...posixSection("CHECK_PLATFORM_SMOKE_STDOUT", 'cat "$PACK_DIR/check-platform-smoke.stdout.txt" 2>/dev/null || true'));
429
429
  lines.push(...posixSection("CHECK_PLATFORM_SMOKE_STDERR", 'cat "$PACK_DIR/check-platform-smoke.stderr.txt" 2>/dev/null || true'));
430
430
  lines.push("");
431
431
  lines.push('echo "=== npm test ==="');
432
- lines.push('PI_CURSOR_SKIP_RELEASE_VERSION_GUARD=1 npm test >"$PACK_DIR/npm-test.stdout.txt" 2>"$PACK_DIR/npm-test.stderr.txt"');
432
+ lines.push('npm test >"$PACK_DIR/npm-test.stdout.txt" 2>"$PACK_DIR/npm-test.stderr.txt"');
433
433
  lines.push("TEST_EXIT=$?");
434
434
  lines.push('echo "PLATFORM_NPM_TEST_EXIT=$TEST_EXIT"');
435
435
  lines.push(...posixSection("NPM_TEST_STDOUT", 'cat "$PACK_DIR/npm-test.stdout.txt" 2>/dev/null || true'));
@@ -3,8 +3,7 @@
3
3
  import { createRequire } from "node:module";
4
4
  import { resolve, dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { accessSync, constants, readFileSync } from "node:fs";
7
- import { spawnSync } from "node:child_process";
6
+ import { accessSync, constants } from "node:fs";
8
7
 
9
8
  // ── helpers ────────────────────────────────────────────────────────────────
10
9
  const __filename = fileURLToPath(import.meta.url);
@@ -47,11 +46,11 @@ Environment:
47
46
  PLATFORM_SMOKE_MAC_HOST macOS SSH host (default: localhost)
48
47
  PLATFORM_SMOKE_MAC_USER macOS SSH user (default: \$USER)
49
48
  PLATFORM_SMOKE_MAC_WORK_ROOT macOS work root
50
- PLATFORM_SMOKE_WINDOWS_VM Parallels source VM name
51
- PLATFORM_SMOKE_WINDOWS_SNAPSHOT Snapshot name
52
- PLATFORM_SMOKE_WINDOWS_USER Windows SSH user
53
- PLATFORM_SMOKE_UBUNTU_IMAGE Ubuntu container image
54
- PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT Windows native work root
49
+ PLATFORM_SMOKE_UBUNTU_IMAGE Ubuntu container image
50
+ PLATFORM_SMOKE_WINDOWS_VM Parallels source VM override (default from config)
51
+ PLATFORM_SMOKE_WINDOWS_SNAPSHOT Snapshot override (default from config)
52
+ PLATFORM_SMOKE_WINDOWS_USER Windows SSH user override (default: \$USER)
53
+ PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT Windows native work root override (default from config)
55
54
  `);
56
55
  }
57
56
 
@@ -90,17 +89,17 @@ function parseArgs(argv) {
90
89
  return args;
91
90
  }
92
91
 
93
- function assertHostReleaseVersionGuard() {
94
- const packageJson = JSON.parse(readFileSync(resolve(repoRoot, "package.json"), "utf8"));
95
- const result = spawnSync("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"], {
96
- cwd: repoRoot,
97
- encoding: "utf8",
98
- });
99
- if (result.status !== 0) throw new Error(`failed to inspect release tags: ${result.stderr || result.error?.message || "unknown git error"}`);
100
- const latestTag = result.stdout.split(/\r?\n/).find((tag) => tag.length > 0);
101
- if (!latestTag) throw new Error("no local release tags found; cannot enforce package version reuse guard");
102
- const latestVersion = latestTag.replace(/^v/, "");
103
- if (packageJson.version === latestVersion) throw new Error(`package version ${packageJson.version} reuses latest release tag ${latestTag}`);
92
+ function validateSelections(targets, suites) {
93
+ const allowedTargets = new Set(config.requiredTargets ?? []);
94
+ const allowedSuites = new Set(config.requiredSuites ?? []);
95
+ const badTargets = targets.filter((target) => !allowedTargets.has(target));
96
+ const badSuites = suites.filter((suite) => !allowedSuites.has(suite));
97
+ if (badTargets.length > 0) {
98
+ throw new Error(`unknown target(s): ${badTargets.join(", ")}; allowed: ${[...allowedTargets].join(", ")}`);
99
+ }
100
+ if (badSuites.length > 0) {
101
+ throw new Error(`unknown suite(s): ${badSuites.join(", ")}; allowed: ${[...allowedSuites].join(", ")}`);
102
+ }
104
103
  }
105
104
 
106
105
  // ── commands ───────────────────────────────────────────────────────────────
@@ -158,7 +157,6 @@ async function main() {
158
157
  }
159
158
 
160
159
  if (args.command === "run") {
161
- assertHostReleaseVersionGuard();
162
160
  const targets = args.target
163
161
  ? args.target.split(",").map((s) => s.trim()).filter(Boolean)
164
162
  : config.requiredTargets;
@@ -167,6 +165,13 @@ async function main() {
167
165
  ? [args.suite]
168
166
  : config.requiredSuites;
169
167
 
168
+ try {
169
+ validateSelections(targets, suites);
170
+ } catch (err) {
171
+ console.error(err.message);
172
+ process.exit(2);
173
+ }
174
+
170
175
  const targetRuns = targets.map(async (targetName) => {
171
176
  console.log(`\n=== Target: ${targetName} ===`);
172
177
  const result = args.suite