pi-cursor-sdk 0.1.28 → 0.1.30
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 +29 -0
- package/README.md +39 -36
- package/docs/crabbox-platform-testing-lessons.md +508 -0
- package/docs/cursor-dogfood-checklist.md +4 -3
- package/docs/cursor-live-smoke-checklist.md +22 -20
- package/docs/cursor-model-ux-spec.md +13 -13
- package/docs/cursor-native-tool-replay.md +11 -11
- package/docs/cursor-native-tool-visual-audit.md +9 -7
- package/docs/cursor-testing-lessons.md +20 -15
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +994 -0
- package/package.json +32 -3
- package/platform-smoke.config.mjs +21 -0
- package/scripts/debug-provider-events.mjs +10 -3
- package/scripts/debug-sdk-events.mjs +10 -2
- package/scripts/isolated-cursor-smoke.sh +4 -4
- package/scripts/lib/cursor-visual-render.mjs +1 -0
- package/scripts/platform-smoke/artifacts.mjs +124 -0
- package/scripts/platform-smoke/assertions.mjs +101 -0
- package/scripts/platform-smoke/card-detect.mjs +96 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +215 -0
- package/scripts/platform-smoke/doctor.mjs +446 -0
- package/scripts/platform-smoke/jsonl-text.mjs +31 -0
- package/scripts/platform-smoke/live-suite-runner.mjs +677 -0
- package/scripts/platform-smoke/platform-build-windows.ps1 +187 -0
- package/scripts/platform-smoke/pty-capture.mjs +131 -0
- package/scripts/platform-smoke/render-ansi.mjs +65 -0
- package/scripts/platform-smoke/scenarios.mjs +186 -0
- package/scripts/platform-smoke/targets.mjs +900 -0
- package/scripts/platform-smoke/visual-evidence.mjs +139 -0
- package/scripts/platform-smoke.mjs +193 -0
- package/scripts/probe-mcp-coldstart.mjs +8 -1
- package/scripts/steering-rpc-smoke.mjs +1 -1
- package/scripts/tmux-live-smoke.sh +3 -3
- package/scripts/visual-tui-smoke.mjs +1 -1
- package/src/context.ts +2 -4
- package/src/cursor-pi-tool-bridge-abort.ts +1 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +12 -1
- package/src/cursor-pi-tool-bridge.ts +46 -1
- package/src/cursor-provider-turn-lifecycle-emitter.ts +65 -8
- package/src/cursor-provider-turn-tool-ledger.ts +2 -3
- package/src/cursor-run-final-text.ts +11 -1
- package/src/cursor-skill-tool.ts +273 -0
- package/src/cursor-state.ts +38 -19
- package/src/cursor-tool-lifecycle.ts +1 -1
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-transcript-utils.ts +7 -3
- package/src/index.ts +3 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$PackageName = "pi-cursor-sdk",
|
|
3
|
+
[int]$NodeValidationMajor = 24
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = "Continue"
|
|
7
|
+
|
|
8
|
+
function Exit-CodeFromLastCommand {
|
|
9
|
+
if ($null -ne $global:LASTEXITCODE) { return [int]$global:LASTEXITCODE }
|
|
10
|
+
if ($?) { return 0 }
|
|
11
|
+
return 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function Write-SectionFile {
|
|
15
|
+
param([string]$Name, [string]$Path)
|
|
16
|
+
Write-Output "--- $Name START ---"
|
|
17
|
+
if (Test-Path -LiteralPath $Path) {
|
|
18
|
+
Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue
|
|
19
|
+
}
|
|
20
|
+
Write-Output "--- $Name END ---"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Write-Output "Starting platform-build in $(Get-Location) at $(Get-Date -Format o)"
|
|
24
|
+
$SourceRoot = (Get-Location).Path
|
|
25
|
+
$RunRoot = Join-Path $SourceRoot (Join-Path ".platform-smoke-runs" ("platform-build-" + (Get-Date -Format "yyyyMMddTHHmmssZ") + "-" + $PID))
|
|
26
|
+
$PackDir = Join-Path $RunRoot "pack"
|
|
27
|
+
$TestWorkspace = Join-Path $RunRoot "test-workspace"
|
|
28
|
+
$PiProject = Join-Path $RunRoot "pi-project"
|
|
29
|
+
New-Item -ItemType Directory -Force -Path $PackDir, $TestWorkspace, $PiProject | Out-Null
|
|
30
|
+
Write-Output "PLATFORM_RUN_ROOT=$RunRoot"
|
|
31
|
+
Write-Output "PLATFORM_TEST_WORKSPACE=$TestWorkspace"
|
|
32
|
+
Write-Output "PLATFORM_PI_PROJECT=$PiProject"
|
|
33
|
+
|
|
34
|
+
$NodeVersion = (& node.exe --version).Trim()
|
|
35
|
+
$NpmVersion = (& npm.cmd --version).Trim()
|
|
36
|
+
$NodeMajor = [int](($NodeVersion -replace '^v', '').Split('.')[0])
|
|
37
|
+
Set-Content -LiteralPath (Join-Path $PackDir "node-version.txt") -Value $NodeVersion
|
|
38
|
+
Set-Content -LiteralPath (Join-Path $PackDir "npm-version.txt") -Value $NpmVersion
|
|
39
|
+
if ($NodeMajor -ge $NodeValidationMajor) { $NODE_VERSION_EXIT = 0 } else { $NODE_VERSION_EXIT = 1 }
|
|
40
|
+
Write-Output "PLATFORM_NODE_VERSION=$NodeVersion"
|
|
41
|
+
Write-Output "PLATFORM_NPM_VERSION=$NpmVersion"
|
|
42
|
+
Write-Output "PLATFORM_NODE_VERSION_EXIT=$NODE_VERSION_EXIT"
|
|
43
|
+
Write-SectionFile "NODE_VERSION_STDOUT" (Join-Path $PackDir "node-version.txt")
|
|
44
|
+
Write-SectionFile "NPM_VERSION_STDOUT" (Join-Path $PackDir "npm-version.txt")
|
|
45
|
+
|
|
46
|
+
$NpmCiOut = Join-Path $PackDir "npm-ci.stdout.txt"
|
|
47
|
+
$NpmCiErr = Join-Path $PackDir "npm-ci.stderr.txt"
|
|
48
|
+
$CheckPlatformSmokeOut = Join-Path $PackDir "check-platform-smoke.stdout.txt"
|
|
49
|
+
$CheckPlatformSmokeErr = Join-Path $PackDir "check-platform-smoke.stderr.txt"
|
|
50
|
+
$NpmTestOut = Join-Path $PackDir "npm-test.stdout.txt"
|
|
51
|
+
$NpmTestErr = Join-Path $PackDir "npm-test.stderr.txt"
|
|
52
|
+
$TypecheckOut = Join-Path $PackDir "typecheck.stdout.txt"
|
|
53
|
+
$TypecheckErr = Join-Path $PackDir "typecheck.stderr.txt"
|
|
54
|
+
$NpmPackOut = Join-Path $PackDir "npm-pack.stdout.txt"
|
|
55
|
+
$NpmPackErr = Join-Path $PackDir "npm-pack.stderr.txt"
|
|
56
|
+
|
|
57
|
+
Write-Output "=== npm ci ==="
|
|
58
|
+
& npm.cmd ci 1> $NpmCiOut 2> $NpmCiErr
|
|
59
|
+
$CI_EXIT = Exit-CodeFromLastCommand
|
|
60
|
+
Write-Output "PLATFORM_NPM_CI_EXIT=$CI_EXIT"
|
|
61
|
+
Write-SectionFile "NPM_CI_STDOUT" $NpmCiOut
|
|
62
|
+
Write-SectionFile "NPM_CI_STDERR" $NpmCiErr
|
|
63
|
+
|
|
64
|
+
Write-Output "=== check:platform-smoke ==="
|
|
65
|
+
$env:PI_CURSOR_SKIP_RELEASE_VERSION_GUARD = "1"
|
|
66
|
+
& npm.cmd run check:platform-smoke 1> $CheckPlatformSmokeOut 2> $CheckPlatformSmokeErr
|
|
67
|
+
$CHECK_PLATFORM_SMOKE_EXIT = Exit-CodeFromLastCommand
|
|
68
|
+
Remove-Item Env:\PI_CURSOR_SKIP_RELEASE_VERSION_GUARD -ErrorAction SilentlyContinue
|
|
69
|
+
Write-Output "PLATFORM_CHECK_PLATFORM_SMOKE_EXIT=$CHECK_PLATFORM_SMOKE_EXIT"
|
|
70
|
+
Write-SectionFile "CHECK_PLATFORM_SMOKE_STDOUT" $CheckPlatformSmokeOut
|
|
71
|
+
Write-SectionFile "CHECK_PLATFORM_SMOKE_STDERR" $CheckPlatformSmokeErr
|
|
72
|
+
|
|
73
|
+
Write-Output "=== npm test ==="
|
|
74
|
+
$env:PI_CURSOR_SKIP_RELEASE_VERSION_GUARD = "1"
|
|
75
|
+
& npm.cmd test 1> $NpmTestOut 2> $NpmTestErr
|
|
76
|
+
$TEST_EXIT = Exit-CodeFromLastCommand
|
|
77
|
+
Remove-Item Env:\PI_CURSOR_SKIP_RELEASE_VERSION_GUARD -ErrorAction SilentlyContinue
|
|
78
|
+
Write-Output "PLATFORM_NPM_TEST_EXIT=$TEST_EXIT"
|
|
79
|
+
Write-SectionFile "NPM_TEST_STDOUT" $NpmTestOut
|
|
80
|
+
Write-SectionFile "NPM_TEST_STDERR" $NpmTestErr
|
|
81
|
+
|
|
82
|
+
Write-Output "=== typecheck ==="
|
|
83
|
+
& npm.cmd run typecheck 1> $TypecheckOut 2> $TypecheckErr
|
|
84
|
+
$TC_EXIT = Exit-CodeFromLastCommand
|
|
85
|
+
Write-Output "PLATFORM_TYPECHECK_EXIT=$TC_EXIT"
|
|
86
|
+
Write-SectionFile "TYPECHECK_STDOUT" $TypecheckOut
|
|
87
|
+
Write-SectionFile "TYPECHECK_STDERR" $TypecheckErr
|
|
88
|
+
|
|
89
|
+
Write-Output "=== npm pack ==="
|
|
90
|
+
& npm.cmd pack --silent 1> $NpmPackOut 2> $NpmPackErr
|
|
91
|
+
$PACK_EXIT = Exit-CodeFromLastCommand
|
|
92
|
+
Write-Output "PLATFORM_NPM_PACK_EXIT=$PACK_EXIT"
|
|
93
|
+
Write-SectionFile "NPM_PACK_STDOUT" $NpmPackOut
|
|
94
|
+
Write-SectionFile "NPM_PACK_STDERR" $NpmPackErr
|
|
95
|
+
$PackTarball = (Get-Content -LiteralPath $NpmPackOut -ErrorAction SilentlyContinue | Select-Object -First 1)
|
|
96
|
+
if ($PackTarball) { $PackTarball = $PackTarball.Trim() }
|
|
97
|
+
Write-Output "PLATFORM_NPM_PACK_EXIT=$PACK_EXIT"
|
|
98
|
+
if ($PackTarball -and (Test-Path -LiteralPath $PackTarball)) {
|
|
99
|
+
Move-Item -LiteralPath $PackTarball -Destination (Join-Path $PackDir $PackTarball) -Force
|
|
100
|
+
}
|
|
101
|
+
Write-Output "PLATFORM_PACKED_TARBALL=$PackTarball"
|
|
102
|
+
Set-Content -Path (Join-Path $PackDir "packed-tarball.txt") -Value $PackTarball
|
|
103
|
+
|
|
104
|
+
Write-Output "=== fixture workspace ==="
|
|
105
|
+
Copy-Item -LiteralPath package.json, README.md -Destination $TestWorkspace -ErrorAction SilentlyContinue
|
|
106
|
+
Copy-Item -LiteralPath src -Destination $TestWorkspace -Recurse -ErrorAction SilentlyContinue
|
|
107
|
+
if ((Test-Path -LiteralPath (Join-Path $TestWorkspace "package.json")) -and (Test-Path -LiteralPath (Join-Path $TestWorkspace "README.md")) -and (Test-Path -LiteralPath (Join-Path $TestWorkspace "src"))) {
|
|
108
|
+
$FIXTURE_EXIT = 0
|
|
109
|
+
} else {
|
|
110
|
+
$FIXTURE_EXIT = 1
|
|
111
|
+
}
|
|
112
|
+
Write-Output "PLATFORM_FIXTURE_EXIT=$FIXTURE_EXIT"
|
|
113
|
+
|
|
114
|
+
$PiCli = Join-Path (Get-Location) "node_modules\.bin\pi.cmd"
|
|
115
|
+
if (-not (Test-Path -LiteralPath $PiCli)) { $PiCli = Join-Path (Get-Location) "node_modules\.bin\pi" }
|
|
116
|
+
if (-not (Test-Path -LiteralPath $PiCli)) {
|
|
117
|
+
$Command = Get-Command pi -ErrorAction SilentlyContinue
|
|
118
|
+
$PiCli = $Command.Source
|
|
119
|
+
}
|
|
120
|
+
Write-Output "PLATFORM_PI_CLI=$PiCli"
|
|
121
|
+
|
|
122
|
+
$PackedNodeInstallOut = Join-Path $PackDir "packed-node-install.stdout.txt"
|
|
123
|
+
$PackedNodeInstallErr = Join-Path $PackDir "packed-node-install.stderr.txt"
|
|
124
|
+
$PiInstallOut = Join-Path $PackDir "pi-install.stdout.txt"
|
|
125
|
+
$PiInstallErr = Join-Path $PackDir "pi-install.stderr.txt"
|
|
126
|
+
$PiListOut = Join-Path $PackDir "pi-list.stdout.txt"
|
|
127
|
+
$PiListErr = Join-Path $PackDir "pi-list.stderr.txt"
|
|
128
|
+
|
|
129
|
+
Write-Output "=== pi install packed tarball ==="
|
|
130
|
+
$TarballPath = Join-Path $PackDir $PackTarball
|
|
131
|
+
if ($PackTarball -and $PiCli -and (Test-Path -LiteralPath $TarballPath)) {
|
|
132
|
+
Push-Location $PiProject
|
|
133
|
+
& npm.cmd init -y 1> $PackedNodeInstallOut 2> $PackedNodeInstallErr
|
|
134
|
+
$NPM_INIT_EXIT = Exit-CodeFromLastCommand
|
|
135
|
+
if ($NPM_INIT_EXIT -eq 0) {
|
|
136
|
+
& npm.cmd install --no-save $TarballPath 1>> $PackedNodeInstallOut 2>> $PackedNodeInstallErr
|
|
137
|
+
$PACKED_NODE_INSTALL_EXIT = Exit-CodeFromLastCommand
|
|
138
|
+
} else {
|
|
139
|
+
$PACKED_NODE_INSTALL_EXIT = $NPM_INIT_EXIT
|
|
140
|
+
}
|
|
141
|
+
if ($PACKED_NODE_INSTALL_EXIT -eq 0) {
|
|
142
|
+
$PreviousPiOffline = $env:PI_OFFLINE
|
|
143
|
+
$env:PI_OFFLINE = "1"
|
|
144
|
+
& $PiCli install -l (Join-Path ".\node_modules" $PackageName) 1> $PiInstallOut 2> $PiInstallErr
|
|
145
|
+
$PI_INSTALL_EXIT = Exit-CodeFromLastCommand
|
|
146
|
+
if ($null -eq $PreviousPiOffline) { Remove-Item Env:\PI_OFFLINE -ErrorAction SilentlyContinue } else { $env:PI_OFFLINE = $PreviousPiOffline }
|
|
147
|
+
} else {
|
|
148
|
+
Set-Content -LiteralPath $PiInstallErr -Value "packed npm install failed"
|
|
149
|
+
$PI_INSTALL_EXIT = 1
|
|
150
|
+
}
|
|
151
|
+
Pop-Location
|
|
152
|
+
} else {
|
|
153
|
+
Set-Content -LiteralPath $PackedNodeInstallErr -Value "missing pi cli or tarball"
|
|
154
|
+
Set-Content -LiteralPath $PiInstallErr -Value "missing pi cli or tarball"
|
|
155
|
+
$PACKED_NODE_INSTALL_EXIT = 1
|
|
156
|
+
$PI_INSTALL_EXIT = 1
|
|
157
|
+
}
|
|
158
|
+
Write-Output "PLATFORM_PACKED_NODE_INSTALL_EXIT=$PACKED_NODE_INSTALL_EXIT"
|
|
159
|
+
Write-SectionFile "PACKED_NODE_INSTALL_STDOUT" $PackedNodeInstallOut
|
|
160
|
+
Write-SectionFile "PACKED_NODE_INSTALL_STDERR" $PackedNodeInstallErr
|
|
161
|
+
Write-Output "PLATFORM_PI_INSTALL_EXIT=$PI_INSTALL_EXIT"
|
|
162
|
+
Write-SectionFile "PI_INSTALL_STDOUT" $PiInstallOut
|
|
163
|
+
Write-SectionFile "PI_INSTALL_STDERR" $PiInstallErr
|
|
164
|
+
|
|
165
|
+
Write-Output "=== pi list ==="
|
|
166
|
+
if ($PiCli) {
|
|
167
|
+
Push-Location $PiProject
|
|
168
|
+
$PreviousPiOffline = $env:PI_OFFLINE
|
|
169
|
+
$env:PI_OFFLINE = "1"
|
|
170
|
+
& $PiCli list 1> $PiListOut 2> $PiListErr
|
|
171
|
+
$PI_LIST_EXIT = Exit-CodeFromLastCommand
|
|
172
|
+
if ($null -eq $PreviousPiOffline) { Remove-Item Env:\PI_OFFLINE -ErrorAction SilentlyContinue } else { $env:PI_OFFLINE = $PreviousPiOffline }
|
|
173
|
+
Pop-Location
|
|
174
|
+
} else {
|
|
175
|
+
Set-Content -LiteralPath $PiListErr -Value "missing pi cli"
|
|
176
|
+
$PI_LIST_EXIT = 1
|
|
177
|
+
}
|
|
178
|
+
Write-Output "PLATFORM_PI_LIST_EXIT=$PI_LIST_EXIT"
|
|
179
|
+
Write-SectionFile "PI_LIST_STDOUT" $PiListOut
|
|
180
|
+
Write-SectionFile "PI_LIST_STDERR" $PiListErr
|
|
181
|
+
|
|
182
|
+
Write-Output "node=$NODE_VERSION_EXIT ci=$CI_EXIT checkPlatformSmoke=$CHECK_PLATFORM_SMOKE_EXIT test=$TEST_EXIT typecheck=$TC_EXIT pack=$PACK_EXIT fixture=$FIXTURE_EXIT packedNodeInstall=$PACKED_NODE_INSTALL_EXIT install=$PI_INSTALL_EXIT list=$PI_LIST_EXIT"
|
|
183
|
+
if ($NODE_VERSION_EXIT -ne 0 -or $CI_EXIT -ne 0 -or $CHECK_PLATFORM_SMOKE_EXIT -ne 0 -or $TEST_EXIT -ne 0 -or $TC_EXIT -ne 0 -or $PACK_EXIT -ne 0 -or $FIXTURE_EXIT -ne 0 -or $PACKED_NODE_INSTALL_EXIT -ne 0 -or $PI_INSTALL_EXIT -ne 0 -or $PI_LIST_EXIT -ne 0) {
|
|
184
|
+
Write-Output "PLATFORM_BUILD_FAILED: node=$NODE_VERSION_EXIT ci=$CI_EXIT checkPlatformSmoke=$CHECK_PLATFORM_SMOKE_EXIT test=$TEST_EXIT typecheck=$TC_EXIT pack=$PACK_EXIT fixture=$FIXTURE_EXIT packedNodeInstall=$PACKED_NODE_INSTALL_EXIT install=$PI_INSTALL_EXIT list=$PI_LIST_EXIT"
|
|
185
|
+
exit 1
|
|
186
|
+
}
|
|
187
|
+
Write-Output "PLATFORM_BUILD_OK"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY capture harness — records the full ANSI stream from a terminal session.
|
|
3
|
+
*
|
|
4
|
+
* Captures: pty.events.jsonl, terminal.ansi, terminal.txt, exit code, signal.
|
|
5
|
+
* Terminal dimensions: 150 columns × 45 rows.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { writeFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
const COLS = 150;
|
|
13
|
+
const ROWS = 45;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Capture a command inside a PTY and write artifacts to `dir`.
|
|
17
|
+
*
|
|
18
|
+
* Returns { code, signal, ansiPath, txtPath, eventsPath }.
|
|
19
|
+
*
|
|
20
|
+
* Uses node-pty if available, otherwise falls back to plain child_process.
|
|
21
|
+
*/
|
|
22
|
+
export async function capturePTY(dir, command, opts = {}) {
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
const eventsPath = resolve(dir, "pty.events.jsonl");
|
|
26
|
+
const ansiPath = resolve(dir, "terminal.ansi");
|
|
27
|
+
const txtPath = resolve(dir, "terminal.txt");
|
|
28
|
+
|
|
29
|
+
let ansiBuffer = "";
|
|
30
|
+
let txtBuffer = "";
|
|
31
|
+
|
|
32
|
+
function appendEvent(event) {
|
|
33
|
+
appendFileSync(eventsPath, JSON.stringify(event) + "\n");
|
|
34
|
+
}
|
|
35
|
+
function appendOutput(data) {
|
|
36
|
+
const text = typeof data === "string" ? data : data.toString();
|
|
37
|
+
ansiBuffer += text;
|
|
38
|
+
// Strip ANSI for plain text
|
|
39
|
+
txtBuffer += stripANSI(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try node-pty first
|
|
43
|
+
let pty;
|
|
44
|
+
try {
|
|
45
|
+
const ptyModule = await import("node-pty");
|
|
46
|
+
pty = ptyModule;
|
|
47
|
+
} catch {
|
|
48
|
+
console.log(" node-pty not available — using plain child_process spawn (no PTY sizing)");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const startTime = Date.now();
|
|
52
|
+
appendEvent({ type: "start", time: startTime, cols: COLS, rows: ROWS, command });
|
|
53
|
+
|
|
54
|
+
if (pty) {
|
|
55
|
+
// Use real PTY — always spawn through a shell for portability
|
|
56
|
+
const shellCmd = process.platform === "win32" ? "powershell.exe" : "/bin/bash";
|
|
57
|
+
const shellArgs = process.platform === "win32"
|
|
58
|
+
? ["-NoProfile", "-Command", command.join(" ")]
|
|
59
|
+
: ["-lc", command.join(" ")];
|
|
60
|
+
|
|
61
|
+
const ptyProcess = pty.spawn(shellCmd, shellArgs, {
|
|
62
|
+
name: "xterm-256color",
|
|
63
|
+
cols: COLS,
|
|
64
|
+
rows: ROWS,
|
|
65
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
66
|
+
env: { ...process.env, ...opts.env },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
ptyProcess.onData((data) => {
|
|
70
|
+
appendOutput(data);
|
|
71
|
+
appendEvent({ type: "output", time: Date.now() - startTime, bytes: data.length });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const { code, signal } = await new Promise((resolvePromise) => {
|
|
75
|
+
ptyProcess.onExit((e) => {
|
|
76
|
+
appendEvent({ type: "exit", time: Date.now() - startTime, code: e.exitCode, signal: e.signal });
|
|
77
|
+
resolvePromise({ code: e.exitCode, signal: e.signal });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
writeFileSync(ansiPath, ansiBuffer);
|
|
82
|
+
writeFileSync(txtPath, txtBuffer);
|
|
83
|
+
|
|
84
|
+
return { code: code ?? (signal ? 1 : 0), signal, ansiPath, txtPath, eventsPath };
|
|
85
|
+
} else {
|
|
86
|
+
// Fallback: plain child_process
|
|
87
|
+
const child = spawn(command[0], command.slice(1), {
|
|
88
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
89
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
90
|
+
env: { ...process.env, ...opts.env, COLUMNS: String(COLS), LINES: String(ROWS) },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
child.stdout.on("data", (d) => {
|
|
94
|
+
appendOutput(d.toString());
|
|
95
|
+
appendEvent({ type: "output", time: Date.now() - startTime, bytes: d.length });
|
|
96
|
+
});
|
|
97
|
+
child.stderr.on("data", (d) => {
|
|
98
|
+
appendOutput(d.toString());
|
|
99
|
+
appendEvent({ type: "stderr", time: Date.now() - startTime, bytes: d.length });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const { code, signal } = await new Promise((resolvePromise) => {
|
|
103
|
+
child.on("close", (c, s) => {
|
|
104
|
+
appendEvent({ type: "exit", time: Date.now() - startTime, code: c, signal: s });
|
|
105
|
+
resolvePromise({ code: c, signal: s });
|
|
106
|
+
});
|
|
107
|
+
child.on("error", (err) => {
|
|
108
|
+
appendEvent({ type: "error", time: Date.now() - startTime, error: err.message });
|
|
109
|
+
resolvePromise({ code: 1, signal: null });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
writeFileSync(ansiPath, ansiBuffer);
|
|
114
|
+
writeFileSync(txtPath, txtBuffer);
|
|
115
|
+
|
|
116
|
+
return { code: code ?? (signal ? 1 : 0), signal, ansiPath, txtPath, eventsPath };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Strip ANSI escape sequences from a string. */
|
|
121
|
+
function stripANSI(str) {
|
|
122
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Self-test: can we create a simple PTY? */
|
|
126
|
+
export async function ptySelfTest() {
|
|
127
|
+
const dir = resolve(process.cwd(), ".artifacts", "pty-self-test");
|
|
128
|
+
mkdirSync(dir, { recursive: true });
|
|
129
|
+
const result = await capturePTY(dir, ["echo", "pty-self-test-ok"]);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side ANSI renderer — converts terminal.ansi into HTML and PNG screenshots.
|
|
3
|
+
*
|
|
4
|
+
* This wraps the existing visual smoke renderer so platform smoke uses the same
|
|
5
|
+
* xterm.js/Playwright path as the maintainer visual TUI smoke.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { buildTerminalHtml, writeTerminalScreenshot } from "../lib/cursor-visual-render.mjs";
|
|
12
|
+
|
|
13
|
+
const COLS = 150;
|
|
14
|
+
const ROWS = 45;
|
|
15
|
+
const HISTORY_LINES = 3_000;
|
|
16
|
+
|
|
17
|
+
function stripANSI(text) {
|
|
18
|
+
return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Render terminal.ansi to terminal.html using the shared xterm.js renderer. */
|
|
22
|
+
export async function renderHTML(ansiPath, htmlPath, options = {}) {
|
|
23
|
+
const ansi = readFileSync(ansiPath, "utf8");
|
|
24
|
+
const plain = options.plain ?? stripANSI(ansi);
|
|
25
|
+
const html = buildTerminalHtml({
|
|
26
|
+
ansi,
|
|
27
|
+
plain,
|
|
28
|
+
options: {
|
|
29
|
+
label: options.label ?? "platform-smoke",
|
|
30
|
+
model: options.model ?? "cursor/composer-2-5",
|
|
31
|
+
mode: options.mode ?? "agent",
|
|
32
|
+
cwd: options.cwd ?? process.cwd(),
|
|
33
|
+
sessionId: options.sessionId ?? "platform-smoke",
|
|
34
|
+
width: options.width ?? COLS,
|
|
35
|
+
height: options.height ?? ROWS,
|
|
36
|
+
historyLines: options.historyLines ?? HISTORY_LINES,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
writeFileSync(htmlPath, html);
|
|
40
|
+
return htmlPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Render terminal.html to terminal.full.png and terminal.final-viewport.png using Playwright. */
|
|
44
|
+
export async function renderPNG(htmlPath, fullPNGPath, viewportPNGPath, options = {}) {
|
|
45
|
+
try {
|
|
46
|
+
await writeTerminalScreenshot(htmlPath, fullPNGPath, options.width ?? COLS, options.height ?? ROWS);
|
|
47
|
+
// The shared renderer captures the terminal element. Keep a second artifact name for the platform contract.
|
|
48
|
+
writeFileSync(viewportPNGPath, readFileSync(fullPNGPath));
|
|
49
|
+
return { fullPNGPath, viewportPNGPath };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.log(` PNG render skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Full render pipeline: ANSI → HTML → PNG. */
|
|
57
|
+
export async function renderAll(ansiPath, dir, options = {}) {
|
|
58
|
+
mkdirSync(dir, { recursive: true });
|
|
59
|
+
const htmlPath = resolve(dir, "terminal.html");
|
|
60
|
+
const fullPNGPath = resolve(dir, "terminal.full.png");
|
|
61
|
+
const viewportPNGPath = resolve(dir, "terminal.final-viewport.png");
|
|
62
|
+
await renderHTML(ansiPath, htmlPath, options);
|
|
63
|
+
const pngResult = await renderPNG(htmlPath, fullPNGPath, viewportPNGPath, options);
|
|
64
|
+
return { htmlPath, fullPNGPath, viewportPNGPath, pngOk: pngResult !== null };
|
|
65
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario definitions for all required platform smoke suites.
|
|
3
|
+
*
|
|
4
|
+
* Each scenario defines the prompt template, environment, command rendering,
|
|
5
|
+
* required artifacts, and assertion contracts.
|
|
6
|
+
*
|
|
7
|
+
* Platform rendering: posix (macOS/Ubuntu) or powershell (Windows native).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const SCENARIOS = {
|
|
11
|
+
"platform-build": {
|
|
12
|
+
description: "Build, test, typecheck, pack, and install the extension without Cursor calls.",
|
|
13
|
+
cursorCalls: 0,
|
|
14
|
+
commands: {
|
|
15
|
+
npmCi: { posix: "npm ci", powershell: "npm ci" },
|
|
16
|
+
npmTest: { posix: "npm test", powershell: "npm test" },
|
|
17
|
+
npmTypecheck: { posix: "npm run typecheck", powershell: "npm run typecheck" },
|
|
18
|
+
npmPack: { posix: "npm pack", powershell: "npm pack" },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
"cursor-native-visual-matrix": {
|
|
23
|
+
description: "Prove provider reality, native tool replay, card rendering, JSONL correctness.",
|
|
24
|
+
cursorCalls: 1,
|
|
25
|
+
env: {
|
|
26
|
+
PI_CURSOR_SETTING_SOURCES: "none",
|
|
27
|
+
PI_CURSOR_NATIVE_TOOL_DISPLAY: "1",
|
|
28
|
+
PI_CURSOR_REGISTER_NATIVE_TOOLS: "1",
|
|
29
|
+
PI_CURSOR_PI_TOOL_BRIDGE: "0",
|
|
30
|
+
PI_CURSOR_EXPOSE_BUILTIN_TOOLS: "0",
|
|
31
|
+
PI_CURSOR_SDK_EVENT_DEBUG: "1",
|
|
32
|
+
},
|
|
33
|
+
commands: {
|
|
34
|
+
shellSmoke: {
|
|
35
|
+
posix: "printf 'cursor visual smoke\\n'",
|
|
36
|
+
powershell: "Write-Output 'cursor visual smoke'",
|
|
37
|
+
},
|
|
38
|
+
shellFailure: {
|
|
39
|
+
posix: "sh -c 'echo native shell failure >&2; exit 7'",
|
|
40
|
+
powershell: "Write-Error 'native shell failure'; exit 7",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
promptTemplate: `Native visual matrix.
|
|
44
|
+
|
|
45
|
+
Use Cursor-native tools only. Do not use pi__ tools.
|
|
46
|
+
|
|
47
|
+
Steps:
|
|
48
|
+
1. read ./package.json and remember the package name.
|
|
49
|
+
2. grep ./README.md for "pi-cursor-sdk".
|
|
50
|
+
3. find README.md from repo root.
|
|
51
|
+
4. find src/cursor-provider.ts from repo root.
|
|
52
|
+
5. run shell: {{shellSmoke}}
|
|
53
|
+
6. write .debug/platform-smoke/native.txt with alpha and beta.
|
|
54
|
+
7. edit beta to gamma in that file.
|
|
55
|
+
8. run shell and preserve the failure: {{shellFailure}}
|
|
56
|
+
9. answer exactly:
|
|
57
|
+
NATIVE_MATRIX_OK package=<name> grep=<yes/no> find=<yes/no> list=<yes/no> shell=<yes/no> shell_fail=<yes/no> write=<yes/no> edit=<yes/no>`,
|
|
58
|
+
finalMarker: "NATIVE_MATRIX_OK package=pi-cursor-sdk",
|
|
59
|
+
requiredCards: [
|
|
60
|
+
"read", "grep", "find", "shell-success", "write", "edit-diff", "shell-failure", "footer-status",
|
|
61
|
+
],
|
|
62
|
+
requiredJSONLTools: [
|
|
63
|
+
{ name: "read" },
|
|
64
|
+
{ name: "grep" },
|
|
65
|
+
{ name: "find" },
|
|
66
|
+
{ name: "bash" },
|
|
67
|
+
{ name: "cursor" },
|
|
68
|
+
{ name: "edit" },
|
|
69
|
+
],
|
|
70
|
+
requiredJSONLResults: [
|
|
71
|
+
{ id: "native-read-package", toolName: "read", isError: false, contains: "pi-cursor-sdk" },
|
|
72
|
+
{ id: "native-grep-readme", toolName: "grep", isError: false, contains: "README.md" },
|
|
73
|
+
{ id: "native-find-readme", toolName: "find", isError: false, contains: "README.md" },
|
|
74
|
+
{ id: "native-list-src", toolName: "find", isError: false, contains: "cursor-provider.ts" },
|
|
75
|
+
{ id: "native-shell-output", toolName: "bash", isError: false, contains: "cursor visual smoke" },
|
|
76
|
+
{ id: "native-write-diff", toolName: "cursor", sourceToolName: "edit", isError: false, contains: "+beta" },
|
|
77
|
+
{ id: "native-edit-diff", toolName: "cursor", sourceToolName: "edit", isError: false, contains: "+gamma" },
|
|
78
|
+
{ id: "native-shell-failure", toolName: "bash", isError: true, contains: "native shell failure" },
|
|
79
|
+
],
|
|
80
|
+
visualEvidence: [
|
|
81
|
+
{ id: "native-read-package", pattern: "^\\s*read (\\./)?package\\.json", jsonlResultId: "native-read-package" },
|
|
82
|
+
{ id: "native-shell-success", pattern: "^\\s*cursor visual smoke\\s*$", jsonlResultId: "native-shell-output" },
|
|
83
|
+
{ id: "native-write-diff", pattern: "^\\s*\\+.*beta", jsonlResultId: "native-write-diff" },
|
|
84
|
+
{ id: "native-edit-diff", pattern: "^\\s*\\+.*gamma", jsonlResultId: "native-edit-diff" },
|
|
85
|
+
{ id: "native-shell-failure", pattern: "^\\s*native shell failure\\s*$|^\\s*Command exited with code 7\\s*$", jsonlResultId: "native-shell-failure" },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
"cursor-bridge-visual-matrix": {
|
|
90
|
+
description: "Prove pi bridge routing, bridge tool cards, diagnostics, real pi tool names.",
|
|
91
|
+
cursorCalls: 1,
|
|
92
|
+
env: {
|
|
93
|
+
PI_CURSOR_SETTING_SOURCES: "none",
|
|
94
|
+
PI_CURSOR_NATIVE_TOOL_DISPLAY: "1",
|
|
95
|
+
PI_CURSOR_REGISTER_NATIVE_TOOLS: "1",
|
|
96
|
+
PI_CURSOR_PI_TOOL_BRIDGE: "1",
|
|
97
|
+
PI_CURSOR_EXPOSE_BUILTIN_TOOLS: "1",
|
|
98
|
+
PI_CURSOR_PI_TOOL_BRIDGE_DEBUG: "1",
|
|
99
|
+
PI_CURSOR_SDK_EVENT_DEBUG: "1",
|
|
100
|
+
},
|
|
101
|
+
commands: {
|
|
102
|
+
shellSmoke: {
|
|
103
|
+
posix: "node -e \"console.log('bridge visual smoke')\"",
|
|
104
|
+
powershell: "node -e \"console.log('bridge visual smoke')\"",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
promptTemplate: `Bridge visual matrix.
|
|
108
|
+
|
|
109
|
+
Use pi bridge tools only. Use exact pi__ names.
|
|
110
|
+
|
|
111
|
+
You must make exactly three pi bridge tool calls before your final answer: pi__bash, pi__read, then pi__read. Do not answer until all three calls complete.
|
|
112
|
+
|
|
113
|
+
Steps:
|
|
114
|
+
1. call pi__bash with command: {{shellSmoke}}
|
|
115
|
+
2. call pi__read on ./package.json.
|
|
116
|
+
3. call pi__read on ./definitely-missing-platform-smoke-file.txt.
|
|
117
|
+
4. answer exactly:
|
|
118
|
+
BRIDGE_MATRIX_OK bash_ok=<yes/no> read_ok=<yes/no> read_missing_error=<yes/no>`,
|
|
119
|
+
finalMarker: "BRIDGE_MATRIX_OK bash_ok=yes",
|
|
120
|
+
requiredCards: [
|
|
121
|
+
"bridge-read-success", "bridge-read-failure",
|
|
122
|
+
"bridge-shell-success", "footer-status",
|
|
123
|
+
],
|
|
124
|
+
requiredJSONLTools: [
|
|
125
|
+
{ name: "read" },
|
|
126
|
+
{ name: "bash" },
|
|
127
|
+
],
|
|
128
|
+
requiredJSONLResults: [
|
|
129
|
+
{ id: "bridge-read-success", toolName: "read", isError: false, contains: "pi-cursor-sdk" },
|
|
130
|
+
{ id: "bridge-read-failure", toolName: "read", isError: true, contains: "definitely-missing-platform-smoke-file.txt" },
|
|
131
|
+
{ id: "bridge-shell-success", toolName: "bash", isError: false, contains: "bridge visual smoke" },
|
|
132
|
+
],
|
|
133
|
+
visualEvidence: [
|
|
134
|
+
{ id: "bridge-read-success", pattern: "^\\s*read \\./package\\.json", jsonlResultId: "bridge-read-success" },
|
|
135
|
+
{ id: "bridge-read-failure", pattern: "^\\s*read \\./definitely-missing-platform-smoke-file\\.txt|ENOENT: no such file", jsonlResultId: "bridge-read-failure" },
|
|
136
|
+
{ id: "bridge-shell-success", pattern: "^\\s*bridge visual smoke\\s*$", jsonlResultId: "bridge-shell-success" },
|
|
137
|
+
],
|
|
138
|
+
requiredBridgeDiagnostics: true,
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
"cursor-abort-cleanup": {
|
|
142
|
+
description: "Prove long-running bridge cancellation with no orphan processes.",
|
|
143
|
+
cursorCalls: 1,
|
|
144
|
+
env: {
|
|
145
|
+
PI_CURSOR_SETTING_SOURCES: "none",
|
|
146
|
+
PI_CURSOR_NATIVE_TOOL_DISPLAY: "1",
|
|
147
|
+
PI_CURSOR_REGISTER_NATIVE_TOOLS: "1",
|
|
148
|
+
PI_CURSOR_PI_TOOL_BRIDGE: "1",
|
|
149
|
+
PI_CURSOR_EXPOSE_BUILTIN_TOOLS: "1",
|
|
150
|
+
PI_CURSOR_PI_TOOL_BRIDGE_DEBUG: "1",
|
|
151
|
+
PI_CURSOR_SDK_EVENT_DEBUG: "1",
|
|
152
|
+
},
|
|
153
|
+
commands: {
|
|
154
|
+
longRunning: {
|
|
155
|
+
posix: "node -e \"const fs=require('fs');fs.mkdirSync('.debug/platform-smoke',{recursive:true});fs.writeFileSync('.debug/platform-smoke/abort-started.txt',String(process.pid));setTimeout(() => console.log(process.env.PLATFORM_ABORT_MARKER), 30000)\"",
|
|
156
|
+
powershell: "node -e \"const fs=require('fs');fs.mkdirSync('.debug/platform-smoke',{recursive:true});fs.writeFileSync('.debug/platform-smoke/abort-started.txt',String(process.pid));setTimeout(() => console.log(process.env.PLATFORM_ABORT_MARKER), 30000)\"",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
promptTemplate: `Abort cleanup matrix.
|
|
160
|
+
|
|
161
|
+
Call pi__bash with command:
|
|
162
|
+
{{longRunning}}
|
|
163
|
+
|
|
164
|
+
Do not answer until the tool completes.`,
|
|
165
|
+
finalMarker: null, // Should NOT succeed — harness interrupts
|
|
166
|
+
visualEvidence: [
|
|
167
|
+
{ id: "abort-long-running-shell", pattern: "^\\s*Elapsed [1-9](?:\\.\\d)?s\\s*$" },
|
|
168
|
+
],
|
|
169
|
+
requiredBridgeDiagnostics: "abort",
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export function getScenario(name) {
|
|
174
|
+
return SCENARIOS[name] ?? null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function renderPrompt(scenario, platform) {
|
|
178
|
+
let prompt = scenario.promptTemplate;
|
|
179
|
+
if (scenario.commands) {
|
|
180
|
+
for (const [key, cmdMap] of Object.entries(scenario.commands)) {
|
|
181
|
+
const cmd = platform === "powershell" ? (cmdMap.powershell ?? cmdMap.posix) : (cmdMap.posix ?? cmdMap.powershell);
|
|
182
|
+
prompt = prompt.replaceAll(`{{${key}}}`, cmd ?? "");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return prompt;
|
|
186
|
+
}
|