pi-lens 4.4.2 → 4.4.4
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 +306 -0
- package/README.md +13 -1
- package/dist/{chunk-VITUCP5A.js → chunk-24AKHFGP.js} +9 -7
- package/dist/{chunk-JSQFTSC6.js → chunk-2FN63LPV.js} +11 -11
- package/dist/{chunk-CF24OIYI.js → chunk-2KXTHF3M.js} +20 -20
- package/dist/{chunk-ESBQHFMJ.js → chunk-3DA2JDOL.js} +543 -349
- package/dist/{chunk-RWWYJ623.js → chunk-3KAZU3VR.js} +1 -1
- package/dist/chunk-3PTHXJXR.js +28 -0
- package/dist/chunk-3QED2P4V.js +31 -0
- package/dist/{chunk-N2H3ZPZY.js → chunk-3RKP3FMS.js} +1 -0
- package/dist/{chunk-XVF2VVXP.js → chunk-5HSRT26B.js} +2 -2
- package/dist/{chunk-XKIZ2HNN.js → chunk-5IUSNGOC.js} +18 -16
- package/dist/{chunk-EJIUXPBX.js → chunk-5OFV52AM.js} +1 -1
- package/dist/{chunk-AW7R2GWG.js → chunk-5VSF4UUQ.js} +12 -7
- package/dist/{chunk-NW7BOILN.js → chunk-5WRPDP2A.js} +18 -18
- package/dist/{chunk-7ULNCDCO.js → chunk-6DLWKFEX.js} +35 -34
- package/dist/{chunk-WOA4LN2R.js → chunk-7D6L4MTX.js} +2 -2
- package/dist/{chunk-GAEGEPOC.js → chunk-7HXE67BN.js} +2 -2
- package/dist/chunk-AGSS7277.js +84 -0
- package/dist/{chunk-R3PCY3YS.js → chunk-AMERCH6V.js} +13 -13
- package/dist/{chunk-WU567RSC.js → chunk-ATKJFDQN.js} +14 -10
- package/dist/{chunk-VSSBEUFT.js → chunk-AWSCPCN2.js} +16 -16
- package/dist/{chunk-VCHXCQFG.js → chunk-BWD5EPKO.js} +3 -3
- package/dist/{chunk-ODBNXWT4.js → chunk-CT4MN5IU.js} +1271 -262
- package/dist/{chunk-34WSVGEM.js → chunk-CUHSHSB6.js} +83 -5
- package/dist/{chunk-CHOW74WE.js → chunk-CYRKQRPM.js} +7 -10
- package/dist/{chunk-Q7DUK4LX.js → chunk-DEL2U6CM.js} +5 -5
- package/dist/{chunk-6C7JWPCP.js → chunk-DGUKIYSI.js} +26 -23
- package/dist/{chunk-PHDMFAKM.js → chunk-DUO2TU77.js} +2 -2
- package/dist/{chunk-MRXKYPCS.js → chunk-EBC36LNN.js} +2 -2
- package/dist/chunk-ED36RF6Y.js +17 -0
- package/dist/{chunk-76MWC2K6.js → chunk-EUXZEFTV.js} +90 -97
- package/dist/{chunk-QOZXJ4NA.js → chunk-FM4RDMKT.js} +11 -8
- package/dist/{chunk-66VMUL7Q.js → chunk-GAQ7OQG6.js} +31 -28
- package/dist/{chunk-Q6ZWTCLY.js → chunk-GL5LUMZE.js} +23 -24
- package/dist/{chunk-BKE7E7TC.js → chunk-GMX5OS7B.js} +9 -9
- package/dist/{chunk-ZHFZWTZV.js → chunk-GSLNCYGG.js} +11 -11
- package/dist/{chunk-ARJ6PLFH.js → chunk-GSM5UU7A.js} +26 -34
- package/dist/{chunk-724UL2TJ.js → chunk-GXFV6MYU.js} +3 -3
- package/dist/chunk-HAD7QRQI.js +30 -0
- package/dist/{chunk-QOBUHWHX.js → chunk-HRNHZA3N.js} +6 -6
- package/dist/{chunk-VWCXW2E7.js → chunk-HWCCCQNX.js} +1 -1
- package/dist/{chunk-7DYBLXYA.js → chunk-HYGZEMX6.js} +3 -3
- package/dist/chunk-IJYGIDF4.js +186 -0
- package/dist/{chunk-4W4VLAPJ.js → chunk-IQWN5JCP.js} +11 -11
- package/dist/{chunk-GZYBXISR.js → chunk-IUE7SSKV.js} +9 -9
- package/dist/{chunk-IBGIOBEE.js → chunk-IWDLU6K3.js} +68 -5
- package/dist/{chunk-XCJN5IJU.js → chunk-IYNJ6YPZ.js} +4 -4
- package/dist/{chunk-JAU72XNY.js → chunk-J64T55EI.js} +3 -3
- package/dist/{chunk-2BBLFX75.js → chunk-J6MPGQTS.js} +5 -5
- package/dist/chunk-JORED7EX.js +44 -0
- package/dist/{chunk-6SDEP3AJ.js → chunk-JUKMK5Y4.js} +8 -8
- package/dist/{chunk-IBRALRBR.js → chunk-JW3YWBVB.js} +15 -14
- package/dist/{chunk-L3BLXZG7.js → chunk-KAPJINKN.js} +30 -69
- package/dist/{chunk-2B73ZPOK.js → chunk-KN35K52N.js} +3 -3
- package/dist/{chunk-C34ZXZ7O.js → chunk-KUA4OJ5Y.js} +24 -11
- package/dist/chunk-KYUB6QDC.js +72 -0
- package/dist/{chunk-67WMNWEE.js → chunk-LLJNQKQW.js} +12 -10
- package/dist/chunk-LZSEQW6A.js +57 -0
- package/dist/{chunk-ZANLILYD.js → chunk-M2IFVFWU.js} +2 -2
- package/dist/chunk-M2UD3LPU.js +88 -0
- package/dist/{chunk-WOKKRCJ5.js → chunk-MBKQHGA7.js} +15 -14
- package/dist/{chunk-P7266HLM.js → chunk-MPNJLASA.js} +2 -2
- package/dist/{chunk-6GFRNVYR.js → chunk-MZGQNL3Y.js} +10 -10
- package/dist/{chunk-QZVW3AAK.js → chunk-N5FLEO7W.js} +33 -31
- package/dist/{chunk-3JNKMP3B.js → chunk-O4NWQT6I.js} +4 -4
- package/dist/{chunk-PFNSLWU5.js → chunk-OHKJ7PQF.js} +33 -31
- package/dist/{chunk-VRZP76YM.js → chunk-P63GXC37.js} +4 -1
- package/dist/{chunk-XTMJ7XDT.js → chunk-POMLXPMY.js} +1 -1
- package/dist/{chunk-QQNQ7SJW.js → chunk-QBFBRV2B.js} +10 -4
- package/dist/{chunk-3UUPUE3O.js → chunk-R3T7CLQ2.js} +84 -60
- package/dist/{chunk-23WOO7TX.js → chunk-R7YOCSNV.js} +10 -10
- package/dist/{chunk-2CD5RS6M.js → chunk-RKBXZN3A.js} +23 -2
- package/dist/{chunk-OG5UUPN7.js → chunk-RKKOYPRA.js} +14 -14
- package/dist/{chunk-52FUZI6K.js → chunk-RMLC67LF.js} +1 -1
- package/dist/{chunk-XMSXXAHW.js → chunk-ROYSX2S5.js} +2 -2
- package/dist/{chunk-PV6LR5P5.js → chunk-RVK72CZX.js} +9605 -14776
- package/dist/{chunk-FLKWCIZH.js → chunk-S2HEXIWI.js} +5 -5
- package/dist/{chunk-UZ3TUTRV.js → chunk-S67KYG37.js} +1 -1
- package/dist/{chunk-JWA5I3SR.js → chunk-SD4I7ZO4.js} +16 -18
- package/dist/{chunk-Z527N6YQ.js → chunk-SKAMFC7A.js} +12 -12
- package/dist/{chunk-MQLUAEOC.js → chunk-SRYEJNES.js} +2 -2
- package/dist/{chunk-BRGDSBJE.js → chunk-SYE6T2EB.js} +12 -12
- package/dist/{chunk-GXW6K4YJ.js → chunk-T57LYLZT.js} +1 -1
- package/dist/{chunk-ZXYDQ3TZ.js → chunk-T7FEBKXN.js} +48 -15
- package/dist/{chunk-IXXDFY73.js → chunk-TIQ2PKDW.js} +3 -3
- package/dist/{chunk-QILIYSJF.js → chunk-TLX72JTG.js} +1 -1
- package/dist/{chunk-TYM6OUIT.js → chunk-TOY6QKOH.js} +12 -12
- package/dist/{chunk-IZSHAIJ7.js → chunk-TRETHTQO.js} +12 -12
- package/dist/{chunk-3VPVG5SU.js → chunk-U44DJXQP.js} +10 -6
- package/dist/{chunk-PNQDPDRC.js → chunk-UA7ZXYDR.js} +1 -1
- package/dist/{chunk-OXXKQCSV.js → chunk-UBOQLVT2.js} +4 -4
- package/dist/{chunk-QGQ5CCCG.js → chunk-UMS2MFPZ.js} +2 -2
- package/dist/{chunk-QHJ6DQ2R.js → chunk-US7ECLD7.js} +55 -13
- package/dist/{chunk-EETY4LF7.js → chunk-UZ2XCLYS.js} +8 -8
- package/dist/chunk-V35JG4JB.js +2667 -0
- package/dist/{chunk-N3X6Q4YT.js → chunk-VOOAPROD.js} +3 -3
- package/dist/{chunk-SAEFPSB6.js → chunk-W66O5E25.js} +21 -11
- package/dist/{chunk-KBG47GHP.js → chunk-WMYE35PX.js} +15 -11
- package/dist/{chunk-5NGR67U5.js → chunk-XBJVAEEO.js} +32 -5
- package/dist/chunk-XHJBS436.js +4217 -0
- package/dist/{chunk-GBPP5P7X.js → chunk-XTBDCR27.js} +4 -4
- package/dist/{chunk-4UNXBNE2.js → chunk-XTFDJI3T.js} +11 -11
- package/dist/{chunk-UXGETRP4.js → chunk-XYURS6EN.js} +13 -12
- package/dist/{chunk-RPLINGVB.js → chunk-Y6GZHWSJ.js} +6 -6
- package/dist/{chunk-2ZGIZPDG.js → chunk-YCWYO6TQ.js} +41 -38
- package/dist/{chunk-66SXUJCA.js → chunk-YH5MVY2U.js} +10 -8
- package/dist/{chunk-AOHZU2BM.js → chunk-YNE3RP52.js} +1 -1
- package/dist/{chunk-GOYRYZ2R.js → chunk-ZHH37KZL.js} +72 -20
- package/dist/{chunk-ZGDGW2V3.js → chunk-ZIVJBOO3.js} +2 -2
- package/dist/{chunk-GHB6AYCP.js → chunk-ZVUL7QYY.js} +29 -28
- package/dist/index.js +11559 -6213
- package/dist/mcp/analyze-cli.js +10 -9
- package/dist/mcp/cli.js +41 -38
- package/dist/mcp/server.js +1142 -495
- package/dist/mcp/worker.js +61 -57
- package/dist/probes/installer.js +19 -19
- package/dist/workers/project-snapshot-persist-worker.js +28 -9
- package/dist/workers/review-graph-persist-worker.js +1 -1
- package/docs/configuration.md +11 -0
- package/docs/engineering-principles.md +32 -0
- package/docs/lsp-capability-matrix.md +1 -1
- package/docs/lsp-idle-eviction.md +15 -26
- package/docs/pi-lens-fixer.md +117 -13
- package/docs/pi-lens-merge-policy.md +109 -11
- package/docs/pi-lens-monitor.md +8 -2
- package/docs/pi-lens-retro.md +4 -1
- package/docs/pi-lens-reviewer.md +114 -3
- package/docs/pi-lens-subagent.md +27 -14
- package/docs/release-qa-baseline.md +3 -1
- package/docs/servercapabilities.md +9 -19
- package/package.json +7 -5
- package/scripts/download-grammars.js +40 -7
- package/scripts/lib/latency-log-phases.mjs +95 -0
- package/scripts/rpc-load-check.mjs +570 -81
- package/dist/chunk-3WA4HP4I.js +0 -55
- package/dist/chunk-466Z26MG.js +0 -30
- package/dist/chunk-5JM2524W.js +0 -84
- package/dist/chunk-CO2G5PHH.js +0 -84
- package/dist/chunk-LOP6GRPH.js +0 -31
- package/dist/chunk-UOPVJQ4N.js +0 -103
- package/dist/chunk-XPZPXCBV.js +0 -28
- /package/dist/{chunk-FC4RP4I2.js → chunk-6HWVI6MI.js} +0 -0
|
@@ -1,103 +1,592 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* RPC
|
|
3
|
+
* RPC session smoke — drive pi-lens the way an RPC child runs it, headless and
|
|
4
|
+
* model-free, and red on ANY failure signal from start to the end of the first
|
|
5
|
+
* turn.
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Recurrence this guard names: #4400 (pi exited at session start under
|
|
8
|
+
* `pi --mode rpc` when the snapshot persist worker failed; nobody had run pi as
|
|
9
|
+
* an RPC child with a failing worker) and #4314 (the 4.4.2 tarball failed the
|
|
10
|
+
* LSP warm in every session; this check passed because a failed warm is a log
|
|
11
|
+
* line, not an `extension_error`). Both shipped past a check that only waited
|
|
12
|
+
* for `get_commands`. The issue is #4401.
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
* `pi
|
|
13
|
-
*
|
|
14
|
+
* What one run does (about 5 s warm, 12 s with a cold language-server install):
|
|
15
|
+
* 1. spawn `pi --mode rpc` with a PRIVATE agent dir and PI_LENS_HOME, a fake
|
|
16
|
+
* OpenAI-compatible provider on 127.0.0.1 (no credentials), and pi's own
|
|
17
|
+
* `defaultProjectTrust: "always"`, so the project is trusted and the LSP
|
|
18
|
+
* spawn and warm really run;
|
|
19
|
+
* 2. `get_commands`, and assert pi-lens's `lens-*` commands registered;
|
|
20
|
+
* 3. wait for the session-start warm-up (`warmup_total` in latency.log);
|
|
21
|
+
* 4. send a real `prompt` (text-only reply: no tool call runs), wait for
|
|
22
|
+
* `agent_end` and a clean `stop`;
|
|
23
|
+
* 5. read pi-lens's own logs under the pinned PI_LENS_HOME.
|
|
24
|
+
* It FAILS (exit 1) on any of: the pi process exiting, an `extension_error`
|
|
25
|
+
* event, a pi-lens notice at `warning` or `error` severity
|
|
26
|
+
* (`extension_ui_request` / `notify`), ANY `extension.log` line at `error` or
|
|
27
|
+
* `warn`, a latency.log phase named `*_failed` / `*_error`, a degraded
|
|
28
|
+
* snapshot-worker fallback, or a session that did not log
|
|
29
|
+
* `project trust = trusted`. Exit 2 = a stage timed out. On failure it prints
|
|
30
|
+
* the offending records in full and the log tails (the runner's scratch dies
|
|
31
|
+
* with the job), and retries once when a language-server `auto-install`
|
|
32
|
+
* failed (a registry blip).
|
|
33
|
+
*
|
|
34
|
+
* `--fail-worker` forces the project-snapshot persist worker to answer its
|
|
35
|
+
* first request with `{ id, generation, error }` and no `stagePath` (the #4400
|
|
36
|
+
* wire shape; the seam is PI_LENS_TEST_SNAPSHOT_WORKER_FAIL in
|
|
37
|
+
* clients/project-snapshot-persist-worker.ts, and it forces only that worker,
|
|
38
|
+
* not the review-graph one). Pi must survive, finish the turn
|
|
39
|
+
* and log the main-thread fallback for that exact reason; that one degraded
|
|
40
|
+
* fallback is the only failure signal the variant tolerates.
|
|
41
|
+
*
|
|
42
|
+
* `startRpcSession` is the one RPC driver: scripts/release-qa.mjs and
|
|
43
|
+
* scripts/compat-smoke-behavioral.mjs start pi through it too.
|
|
44
|
+
*
|
|
45
|
+
* Used by .github/workflows/install-smoke.yml (pi-load: the installed package;
|
|
46
|
+
* host-range-smoke: the packed tarball, plus the failing-worker variant).
|
|
14
47
|
*
|
|
15
48
|
* Usage: node scripts/rpc-load-check.mjs [path-to-pi-bin]
|
|
49
|
+
* [--extension <path-to-pi-lens>] [--fail-worker]
|
|
50
|
+
* no --extension: pi-lens comes from the ambient agent dir's `packages`
|
|
51
|
+
* (`pi install`), which this script links into its private agent dir.
|
|
16
52
|
*/
|
|
17
53
|
import { spawn } from "node:child_process";
|
|
54
|
+
import * as fs from "node:fs";
|
|
55
|
+
import * as http from "node:http";
|
|
56
|
+
import * as os from "node:os";
|
|
57
|
+
import * as path from "node:path";
|
|
58
|
+
import { pathToFileURL } from "node:url";
|
|
59
|
+
import {
|
|
60
|
+
parseNdjsonEntries,
|
|
61
|
+
phaseWasLogged,
|
|
62
|
+
} from "./lib/latency-log-phases.mjs";
|
|
18
63
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Where the TypeScript language server stands, from the two logs: started
|
|
66
|
+
* (`lsp_server_spawned` for `typescript` in latency.log), failed (a terminal
|
|
67
|
+
* `lsp spawn typescript: ...` line in sessionstart.log), or undefined while it
|
|
68
|
+
* is still spawning. The smoke project is TypeScript, so a started server is
|
|
69
|
+
* the positive evidence that the LSP leg ran (#4401 review: a refusal that
|
|
70
|
+
* logs after a fixed read never reached the check).
|
|
71
|
+
*/
|
|
72
|
+
export function tsServerOutcome(latencyRecords, sessionstartText) {
|
|
73
|
+
const spawned = latencyRecords.find(
|
|
74
|
+
(r) =>
|
|
75
|
+
r.phase === "lsp_server_spawned" && r.metadata?.serverId === "typescript",
|
|
76
|
+
);
|
|
77
|
+
if (spawned) return { started: true };
|
|
78
|
+
const line = String(sessionstartText)
|
|
79
|
+
.split("\n")
|
|
80
|
+
.find((l) =>
|
|
81
|
+
/lsp spawn typescript: (unavailable|failed|refused|aborted|permanently disabled)/.test(
|
|
82
|
+
l,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
return line ? { started: false, line: line.trim() } : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Every executable `name` on PATH, in order (a runner-image tool can shadow the install). */
|
|
89
|
+
function commandsOnPath(name) {
|
|
90
|
+
const found = [];
|
|
91
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
92
|
+
if (!dir) continue;
|
|
93
|
+
const file = path.join(dir, name);
|
|
94
|
+
try {
|
|
95
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
96
|
+
found.push(file);
|
|
97
|
+
} catch {
|
|
98
|
+
// not in this directory
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return found;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The env var the failing-worker variant sets; the worker echoes it back. */
|
|
105
|
+
export const FORCE_WORKER_FAIL_ENV = "PI_LENS_TEST_SNAPSHOT_WORKER_FAIL";
|
|
106
|
+
/** The worker's forced error starts with this; the smoke keys its tolerance on it. */
|
|
107
|
+
export const FORCED_ERROR_MARKER = "forced snapshot worker failure";
|
|
108
|
+
/** Per-stage wait; the env override exists for the spawn test's timeout cases. */
|
|
109
|
+
const STAGE_TIMEOUT_MS =
|
|
110
|
+
Number(process.env.RPC_LOAD_CHECK_STAGE_TIMEOUT_MS) || 30000;
|
|
111
|
+
/** Notice severities (`notifyType`) that are failures; `info` is not. */
|
|
112
|
+
const FAILING_NOTIFY_TYPES = new Set(["warning", "error"]);
|
|
113
|
+
/** A latency.log phase named like a failure. */
|
|
114
|
+
const FAILING_PHASE = /_(failed|error)$/;
|
|
115
|
+
|
|
116
|
+
/** An `extension_error` in either shape pi has used. */
|
|
117
|
+
export function isExtensionError(message) {
|
|
118
|
+
return (
|
|
119
|
+
message?.type === "extension_error" ||
|
|
120
|
+
(message?.type === "event" && message.event === "extension_error")
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Failure carried by one RPC stdout message, or undefined. Shapes (pi docs
|
|
126
|
+
* json.md / rpc-extension-ui.md): `{type:"extension_error",...}` (older hosts
|
|
127
|
+
* wrap it as `{type:"event",event:"extension_error"}`), and a notice is
|
|
128
|
+
* `{type:"extension_ui_request",method:"notify",message,notifyType}` with
|
|
129
|
+
* `notifyType` defaulting to `info`.
|
|
130
|
+
*/
|
|
131
|
+
export function rpcMessageFailure(message) {
|
|
132
|
+
if (!message || typeof message !== "object") return undefined;
|
|
133
|
+
if (isExtensionError(message)) {
|
|
134
|
+
return `extension_error: ${String(message.error ?? "").slice(0, 300)} (${String(message.event ?? "")})`;
|
|
135
|
+
}
|
|
136
|
+
if (message.type === "extension_ui_request" && message.method === "notify") {
|
|
137
|
+
const severity = message.notifyType ?? "info";
|
|
138
|
+
if (FAILING_NOTIFY_TYPES.has(severity)) {
|
|
139
|
+
return `${severity} notice: ${String(message.message ?? "").slice(0, 300)}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Failure carried by one record of pi-lens's own logs, or undefined.
|
|
147
|
+
* `file` is `extension.log` (NDJSON, `level`) or `latency.log` (NDJSON,
|
|
148
|
+
* `type:"phase"`). `allowFallbackReason` is the one degraded worker fallback
|
|
149
|
+
* the failing-worker variant expects.
|
|
150
|
+
*/
|
|
151
|
+
export function logRecordFailure(file, record, allowFallbackReason) {
|
|
152
|
+
if (!record || typeof record !== "object") return undefined;
|
|
153
|
+
if (file === "extension.log") {
|
|
154
|
+
if (record.level !== "error" && record.level !== "warn") return undefined;
|
|
155
|
+
return `extension.log ${record.level} [${record.subsystem}]: ${String(record.message ?? "").slice(0, 300)}`;
|
|
156
|
+
}
|
|
157
|
+
if (file === "latency.log" && record.type === "phase") {
|
|
158
|
+
const phase = String(record.phase ?? "");
|
|
159
|
+
if (phase === "project_snapshot_worker_fallback") {
|
|
160
|
+
if (record.metadata?.degraded !== true) return undefined;
|
|
161
|
+
const reason = String(record.metadata?.reason ?? "");
|
|
162
|
+
if (allowFallbackReason && reason.includes(allowFallbackReason)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return `latency.log degraded snapshot worker fallback: ${reason.slice(0, 300)}`;
|
|
166
|
+
}
|
|
167
|
+
if (FAILING_PHASE.test(phase)) {
|
|
168
|
+
return `latency.log phase ${phase}: ${JSON.stringify(record.metadata ?? {}).slice(0, 300)}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readNdjson(file) {
|
|
49
175
|
try {
|
|
50
|
-
|
|
176
|
+
return parseNdjsonEntries(fs.readFileSync(file, "utf8"));
|
|
51
177
|
} catch {
|
|
52
|
-
return;
|
|
178
|
+
return [];
|
|
53
179
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A minimal OpenAI-compatible streaming completion: one `ok` reply, no tools. */
|
|
183
|
+
function startFakeProvider() {
|
|
184
|
+
const state = { requests: 0 };
|
|
185
|
+
const server = http.createServer((req, res) => {
|
|
186
|
+
req.resume();
|
|
187
|
+
req.on("end", () => {
|
|
188
|
+
state.requests++;
|
|
189
|
+
res.writeHead(200, { "content-type": "text/event-stream" });
|
|
190
|
+
const chunk = (delta, finish) =>
|
|
191
|
+
`data: ${JSON.stringify({ id: "c1", object: "chat.completion.chunk", created: 1, model: "fake-model", choices: [{ index: 0, delta, finish_reason: finish ?? null }] })}\n\n`;
|
|
192
|
+
res.write(chunk({ role: "assistant", content: "ok" }));
|
|
193
|
+
res.write(chunk({}, "stop"));
|
|
194
|
+
res.write(
|
|
195
|
+
`data: ${JSON.stringify({ id: "c1", object: "chat.completion.chunk", created: 1, model: "fake-model", choices: [], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } })}\n\n`,
|
|
196
|
+
);
|
|
197
|
+
res.end("data: [DONE]\n\n");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
server.listen(0, "127.0.0.1", () =>
|
|
202
|
+
resolve({ server, state, port: server.address().port }),
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Private agent dir: the fake provider, plus the ambient packages when no --extension. */
|
|
208
|
+
function writeAgentDir(agentDir, port, linkAmbientPackages) {
|
|
209
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
210
|
+
fs.writeFileSync(
|
|
211
|
+
path.join(agentDir, "models.json"),
|
|
212
|
+
JSON.stringify({
|
|
213
|
+
providers: {
|
|
214
|
+
fake: {
|
|
215
|
+
baseUrl: `http://127.0.0.1:${port}/v1`,
|
|
216
|
+
api: "openai-completions",
|
|
217
|
+
apiKey: "fake",
|
|
218
|
+
models: [{ id: "fake-model" }],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
// pi's own setting: an RPC child has no UI to answer the trust prompt, and an
|
|
224
|
+
// untrusted project refuses the LSP spawn (the leg this smoke exists to run).
|
|
225
|
+
const settings = {
|
|
226
|
+
defaultProvider: "fake",
|
|
227
|
+
defaultModel: "fake-model",
|
|
228
|
+
defaultProjectTrust: "always",
|
|
229
|
+
};
|
|
230
|
+
if (linkAmbientPackages) {
|
|
231
|
+
const ambient =
|
|
232
|
+
process.env.PI_CODING_AGENT_DIR ||
|
|
233
|
+
path.join(os.homedir(), ".pi", "agent");
|
|
234
|
+
try {
|
|
235
|
+
const ambientSettings = JSON.parse(
|
|
236
|
+
fs.readFileSync(path.join(ambient, "settings.json"), "utf8"),
|
|
237
|
+
);
|
|
238
|
+
for (const key of ["packages", "npmCommand"]) {
|
|
239
|
+
if (ambientSettings[key] !== undefined)
|
|
240
|
+
settings[key] = ambientSettings[key];
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// no ambient settings: the command assertion below reports "did it load?"
|
|
244
|
+
}
|
|
245
|
+
for (const dir of ["npm", "git"]) {
|
|
246
|
+
try {
|
|
247
|
+
fs.symlinkSync(
|
|
248
|
+
path.join(ambient, dir),
|
|
249
|
+
path.join(agentDir, dir),
|
|
250
|
+
"dir",
|
|
251
|
+
);
|
|
252
|
+
} catch {
|
|
253
|
+
// no such ambient dir
|
|
254
|
+
}
|
|
255
|
+
}
|
|
60
256
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
257
|
+
fs.writeFileSync(
|
|
258
|
+
path.join(agentDir, "settings.json"),
|
|
259
|
+
JSON.stringify(settings),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
class StageTimeout extends Error {}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Spawn `pi` and frame its stdout as JSONL. The one RPC driver: this script's
|
|
267
|
+
* session smoke, release-qa's `get_commands` probe and the compat smoke's
|
|
268
|
+
* runs all start pi here, so the framing, the `extension_error` reading and
|
|
269
|
+
* the exit handling exist once (#4401; the two copies it replaced stopped at
|
|
270
|
+
* the first `get_commands` answer).
|
|
271
|
+
*
|
|
272
|
+
* `failures` holds every failure signal read off the wire (see
|
|
273
|
+
* `rpcMessageFailure`); `extensionErrors` only the `extension_error` events.
|
|
274
|
+
*/
|
|
275
|
+
export function startRpcSession(
|
|
276
|
+
piBin,
|
|
277
|
+
{ args, cwd, env, shell = false, stderr = "pipe" },
|
|
278
|
+
) {
|
|
279
|
+
const child = spawn(piBin, args, {
|
|
280
|
+
cwd,
|
|
281
|
+
env,
|
|
282
|
+
shell,
|
|
283
|
+
stdio: ["pipe", "pipe", stderr],
|
|
284
|
+
});
|
|
285
|
+
const session = {
|
|
286
|
+
child,
|
|
287
|
+
failures: [],
|
|
288
|
+
extensionErrors: [],
|
|
289
|
+
stderrTail: [],
|
|
290
|
+
exited: undefined,
|
|
291
|
+
commandsResponse: undefined,
|
|
292
|
+
agentEnd: undefined,
|
|
293
|
+
/** Called with each parsed stdout message. */
|
|
294
|
+
onMessage: (fn) => messageListeners.push(fn),
|
|
295
|
+
/** Called once with the exit code. */
|
|
296
|
+
onExit: (fn) => exitListeners.push(fn),
|
|
297
|
+
send(message) {
|
|
298
|
+
try {
|
|
299
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
300
|
+
} catch {
|
|
301
|
+
// an exited pi is reported through `exited`
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
kill() {
|
|
305
|
+
try {
|
|
306
|
+
child.kill("SIGKILL");
|
|
307
|
+
} catch {}
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const messageListeners = [];
|
|
311
|
+
const exitListeners = [];
|
|
312
|
+
let buf = "";
|
|
313
|
+
child.stderr?.on("data", (d) => {
|
|
314
|
+
session.stderrTail.push(d.toString());
|
|
315
|
+
if (session.stderrTail.length > 200) session.stderrTail.shift();
|
|
316
|
+
});
|
|
317
|
+
child.stdin.on("error", () => {
|
|
318
|
+
// a write to an exited pi; `exited` reports it
|
|
319
|
+
});
|
|
320
|
+
child.on("exit", (code, signal) => {
|
|
321
|
+
session.exited = { code, signal };
|
|
322
|
+
for (const fn of exitListeners) fn(code, signal);
|
|
323
|
+
});
|
|
324
|
+
child.stdout.on("data", (d) => {
|
|
325
|
+
// Strict JSONL: split on LF only (per pi RPC framing rules).
|
|
326
|
+
buf += d.toString();
|
|
327
|
+
let i = buf.indexOf("\n");
|
|
328
|
+
while (i >= 0) {
|
|
329
|
+
const line = buf.slice(0, i).replace(/\r$/, "");
|
|
330
|
+
buf = buf.slice(i + 1);
|
|
331
|
+
i = buf.indexOf("\n");
|
|
332
|
+
if (!line.trim()) continue;
|
|
333
|
+
let m;
|
|
334
|
+
try {
|
|
335
|
+
m = JSON.parse(line);
|
|
336
|
+
} catch {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const failure = rpcMessageFailure(m);
|
|
340
|
+
if (failure) session.failures.push(failure);
|
|
341
|
+
if (isExtensionError(m)) session.extensionErrors.push(m);
|
|
342
|
+
if (m.type === "response" && m.command === "get_commands")
|
|
343
|
+
session.commandsResponse = m;
|
|
344
|
+
if (m.type === "agent_end") session.agentEnd = m;
|
|
345
|
+
for (const fn of messageListeners) fn(m);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
return session;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const tailOf = (text, lines) => text.split("\n").filter(Boolean).slice(-lines);
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* One session smoke. Resolves `{ code, installFailed }` and prints its verdict;
|
|
355
|
+
* code 0 pass, 1 failure signals, 2 a stage timed out.
|
|
356
|
+
*/
|
|
357
|
+
export async function run(piBin, { extension, failWorker }) {
|
|
358
|
+
const scratch = fs.mkdtempSync(
|
|
359
|
+
path.join(os.tmpdir(), "pi-lens-rpc-session-smoke-"),
|
|
360
|
+
);
|
|
361
|
+
const agentDir = path.join(scratch, "agent");
|
|
362
|
+
const lensHome = path.join(scratch, "lens-home");
|
|
363
|
+
fs.mkdirSync(lensHome, { recursive: true });
|
|
364
|
+
const provider = await startFakeProvider();
|
|
365
|
+
writeAgentDir(agentDir, provider.port, !extension);
|
|
366
|
+
|
|
367
|
+
const env = {
|
|
368
|
+
...process.env,
|
|
369
|
+
PI_CODING_AGENT_DIR: agentDir,
|
|
370
|
+
PI_LENS_HOME: lensHome,
|
|
371
|
+
PI_OFFLINE: "1",
|
|
372
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
373
|
+
};
|
|
374
|
+
// pi-lens writes no logs under a test runner; this smoke needs them.
|
|
375
|
+
delete env.VITEST;
|
|
376
|
+
if (env.NODE_ENV === "test") delete env.NODE_ENV;
|
|
377
|
+
if (failWorker) env[FORCE_WORKER_FAIL_ENV] = "no-stage-path";
|
|
378
|
+
else delete env[FORCE_WORKER_FAIL_ENV];
|
|
379
|
+
|
|
380
|
+
const args = ["--mode", "rpc", "--no-session"];
|
|
381
|
+
if (extension) args.push("-e", path.resolve(extension));
|
|
382
|
+
// Printed on every run: a PATH tsc changes which TypeScript compiler pi-lens finds.
|
|
383
|
+
console.log(`tsc on PATH: ${commandsOnPath("tsc").join(", ") || "none"}`);
|
|
384
|
+
const pi = startRpcSession(piBin, { args, env });
|
|
385
|
+
const failures = pi.failures;
|
|
386
|
+
const offending = [];
|
|
387
|
+
|
|
388
|
+
const stage = { name: "start" };
|
|
389
|
+
const waitFor = async (what, ms, probe) => {
|
|
390
|
+
stage.name = what;
|
|
391
|
+
const deadline = Date.now() + ms;
|
|
392
|
+
for (;;) {
|
|
393
|
+
if (pi.exited) return undefined;
|
|
394
|
+
const value = probe();
|
|
395
|
+
if (value) return value;
|
|
396
|
+
if (Date.now() > deadline)
|
|
397
|
+
throw new StageTimeout(`TIMEOUT waiting for ${what} (${ms}ms)`);
|
|
398
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
const latency = () => readNdjson(path.join(lensHome, "latency.log"));
|
|
402
|
+
const readLog = (name) => {
|
|
403
|
+
try {
|
|
404
|
+
return fs.readFileSync(path.join(lensHome, name), "utf8");
|
|
405
|
+
} catch {
|
|
406
|
+
return "";
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const forcedFallback = () =>
|
|
410
|
+
latency().find(
|
|
411
|
+
(r) =>
|
|
412
|
+
r.phase === "project_snapshot_worker_fallback" &&
|
|
413
|
+
String(r.metadata?.reason ?? "").includes(FORCED_ERROR_MARKER),
|
|
67
414
|
);
|
|
68
|
-
|
|
69
|
-
|
|
415
|
+
|
|
416
|
+
let timedOut;
|
|
417
|
+
try {
|
|
418
|
+
// Give pi a moment to open stdin before the first command.
|
|
419
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
420
|
+
pi.send({ id: "cmds", type: "get_commands" });
|
|
421
|
+
await waitFor(
|
|
422
|
+
"get_commands response",
|
|
423
|
+
STAGE_TIMEOUT_MS,
|
|
424
|
+
() => pi.commandsResponse,
|
|
70
425
|
);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
finish(
|
|
78
|
-
0,
|
|
79
|
-
`PASS: pi-lens loaded — ${lens.length} lens-* commands registered`,
|
|
426
|
+
if (!pi.exited) {
|
|
427
|
+
const cmds = pi.commandsResponse?.data?.commands || [];
|
|
428
|
+
const lens = cmds.filter(
|
|
429
|
+
(c) =>
|
|
430
|
+
String(c.name || "").startsWith("lens-") ||
|
|
431
|
+
String(c.path || "").includes("pi-lens"),
|
|
80
432
|
);
|
|
433
|
+
console.log(
|
|
434
|
+
`total commands: ${cmds.length}; pi-lens commands: ${lens.map((c) => c.name).join(", ") || "(none)"}`,
|
|
435
|
+
);
|
|
436
|
+
if (!lens.length)
|
|
437
|
+
failures.push("pi-lens registered no commands (did it load?)");
|
|
438
|
+
}
|
|
439
|
+
await waitFor(
|
|
440
|
+
"session-start warm-up (warmup_total in latency.log)",
|
|
441
|
+
STAGE_TIMEOUT_MS,
|
|
442
|
+
() => phaseWasLogged(latency(), "warmup_total"),
|
|
443
|
+
);
|
|
444
|
+
pi.send({ id: "turn", type: "prompt", message: "hello" });
|
|
445
|
+
await waitFor(
|
|
446
|
+
"first turn (agent_end)",
|
|
447
|
+
STAGE_TIMEOUT_MS,
|
|
448
|
+
() => pi.agentEnd,
|
|
449
|
+
);
|
|
450
|
+
if (pi.agentEnd && !pi.exited) {
|
|
451
|
+
const last = [...(pi.agentEnd.messages || [])]
|
|
452
|
+
.reverse()
|
|
453
|
+
.find((m) => m.role === "assistant");
|
|
454
|
+
if (!last || last.stopReason !== "stop")
|
|
455
|
+
failures.push(
|
|
456
|
+
`first turn did not finish cleanly: stopReason=${last?.stopReason ?? "(no assistant message)"}`,
|
|
457
|
+
);
|
|
458
|
+
if (provider.state.requests < 1)
|
|
459
|
+
failures.push("first turn never reached the fake provider");
|
|
460
|
+
}
|
|
461
|
+
// The LSP spawn is asynchronous: its refusal logs about a second after
|
|
462
|
+
// the turn ends (the PATH-compiler probe), so wait for a terminal state.
|
|
463
|
+
const tsServer = await waitFor(
|
|
464
|
+
"the TypeScript language server to start or fail (lsp_server_spawned in latency.log)",
|
|
465
|
+
STAGE_TIMEOUT_MS,
|
|
466
|
+
() => tsServerOutcome(latency(), readLog("sessionstart.log")),
|
|
467
|
+
);
|
|
468
|
+
if (tsServer && !tsServer.started)
|
|
469
|
+
failures.push(
|
|
470
|
+
`the TypeScript language server did not start: ${tsServer.line}`,
|
|
471
|
+
);
|
|
472
|
+
if (failWorker) {
|
|
473
|
+
await waitFor(
|
|
474
|
+
"the forced worker failure's main-thread fallback",
|
|
475
|
+
STAGE_TIMEOUT_MS / 2,
|
|
476
|
+
() => forcedFallback(),
|
|
477
|
+
);
|
|
478
|
+
} else {
|
|
479
|
+
// Post-turn work (the snapshot save after the turn) must settle too.
|
|
480
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
481
|
+
}
|
|
482
|
+
} catch (err) {
|
|
483
|
+
if (err instanceof StageTimeout) timedOut = err.message;
|
|
484
|
+
else throw err;
|
|
81
485
|
}
|
|
82
|
-
}
|
|
83
486
|
|
|
84
|
-
pi.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
487
|
+
if (pi.exited) {
|
|
488
|
+
failures.push(
|
|
489
|
+
`pi exited during "${stage.name}" (code ${pi.exited.code}, signal ${pi.exited.signal})`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
// The LSP spawn and the warm only run in a trusted project; an untrusted
|
|
493
|
+
// one would leave that leg dark and green.
|
|
494
|
+
const trustLine = tailOf(readLog("sessionstart.log"), 1e9).find((l) =>
|
|
495
|
+
l.includes("project trust = "),
|
|
496
|
+
);
|
|
497
|
+
if (!trustLine?.includes("project trust = trusted")) {
|
|
498
|
+
failures.push(
|
|
499
|
+
`smoke project is not trusted: the LSP spawn and warm are not exercised (${trustLine?.trim() ?? "no 'project trust' line in sessionstart.log"})`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
for (const r of readNdjson(path.join(lensHome, "extension.log"))) {
|
|
503
|
+
const f = logRecordFailure("extension.log", r);
|
|
504
|
+
if (f) {
|
|
505
|
+
failures.push(f);
|
|
506
|
+
offending.push(["extension.log", r]);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const r of latency()) {
|
|
510
|
+
const f = logRecordFailure(
|
|
511
|
+
"latency.log",
|
|
512
|
+
r,
|
|
513
|
+
failWorker ? FORCED_ERROR_MARKER : undefined,
|
|
514
|
+
);
|
|
515
|
+
if (f) {
|
|
516
|
+
failures.push(f);
|
|
517
|
+
offending.push(["latency.log", r]);
|
|
518
|
+
}
|
|
92
519
|
}
|
|
93
|
-
});
|
|
94
|
-
pi.on("exit", (c) => {
|
|
95
|
-
if (!done) finish(extErrors.length ? 1 : 2, `pi exited early (code ${c})`);
|
|
96
|
-
});
|
|
97
520
|
|
|
98
|
-
//
|
|
99
|
-
|
|
521
|
+
// Graceful stop (stdin EOF runs pi's own teardown, which ends any language
|
|
522
|
+
// server), then SIGKILL if it does not leave in 5 s.
|
|
100
523
|
try {
|
|
101
|
-
pi.stdin.
|
|
524
|
+
pi.child.stdin.end();
|
|
102
525
|
} catch {}
|
|
103
|
-
|
|
526
|
+
for (let waited = 0; !pi.exited && waited < 5000; waited += 50)
|
|
527
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
528
|
+
pi.kill();
|
|
529
|
+
provider.server.close();
|
|
530
|
+
const unique = [...new Set(failures)];
|
|
531
|
+
// A registry blip fails the language-server install, and the failure then
|
|
532
|
+
// shows up as a cascade (a deno launch failure); name it and let main retry.
|
|
533
|
+
const installFailures = readLog("sessionstart.log")
|
|
534
|
+
.split("\n")
|
|
535
|
+
.filter((l) =>
|
|
536
|
+
/auto-install[^\n]*(fail|ECONN|ENOTFOUND|ETIMEDOUT|EAI_AGAIN)/i.test(l),
|
|
537
|
+
);
|
|
538
|
+
if (unique.length || timedOut) {
|
|
539
|
+
for (const f of unique) console.log(`FAIL: ${f}`);
|
|
540
|
+
if (timedOut) console.log(timedOut);
|
|
541
|
+
// The runner's scratch dies with the job: print what a reader needs.
|
|
542
|
+
for (const [file, record] of offending)
|
|
543
|
+
console.log(`offending record (${file}): ${JSON.stringify(record)}`);
|
|
544
|
+
for (const l of installFailures) console.log(`install failure: ${l}`);
|
|
545
|
+
for (const name of ["sessionstart.log", "extension.log"]) {
|
|
546
|
+
console.log(`--- ${name} (last 40 lines)`);
|
|
547
|
+
console.log(tailOf(readLog(name), 40).join("\n"));
|
|
548
|
+
}
|
|
549
|
+
if (pi.stderrTail.length)
|
|
550
|
+
console.log(`pi stderr (tail):\n${pi.stderrTail.join("").slice(-2000)}`);
|
|
551
|
+
console.log(`scratch kept for forensics: ${scratch}`);
|
|
552
|
+
return {
|
|
553
|
+
code: unique.length ? 1 : 2,
|
|
554
|
+
installFailed: installFailures.length > 0,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
558
|
+
console.log(
|
|
559
|
+
`PASS: pi-lens loaded and survived start, warm-up and the first turn${failWorker ? " with the snapshot worker forced to fail" : ""}`,
|
|
560
|
+
);
|
|
561
|
+
return { code: 0, installFailed: false };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function main(argv) {
|
|
565
|
+
const opts = { extension: undefined, failWorker: false };
|
|
566
|
+
const positional = [];
|
|
567
|
+
for (let i = 0; i < argv.length; i++) {
|
|
568
|
+
if (argv[i] === "--extension") opts.extension = argv[++i];
|
|
569
|
+
else if (argv[i] === "--fail-worker") opts.failWorker = true;
|
|
570
|
+
else positional.push(argv[i]);
|
|
571
|
+
}
|
|
572
|
+
const piBin = positional[0] || "pi";
|
|
573
|
+
let result = await run(piBin, opts);
|
|
574
|
+
if (result.code === 1 && result.installFailed) {
|
|
575
|
+
console.log("auto-install failed in the run above: retrying once");
|
|
576
|
+
result = await run(piBin, opts);
|
|
577
|
+
}
|
|
578
|
+
return result.code;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (
|
|
582
|
+
process.argv[1] &&
|
|
583
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
584
|
+
) {
|
|
585
|
+
main(process.argv.slice(2)).then(
|
|
586
|
+
(code) => process.exit(code),
|
|
587
|
+
(err) => {
|
|
588
|
+
console.log(`ERROR: ${err?.stack || err}`);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
},
|
|
591
|
+
);
|
|
592
|
+
}
|