@herbertgao/pi-extensions 2026.8.7 → 2026.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +17 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +90 -0
- package/node_modules/pi-lens/README.md +106 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
- package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
- package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/index.js +1224 -393
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
- package/node_modules/pi-lens/package.json +18 -5
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +6 -5
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/extract.ts +12 -2
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +19 -4
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/package.json +9 -9
|
@@ -349,9 +349,16 @@ captureLspStatusRepaint, getRuntime) {
|
|
|
349
349
|
}
|
|
350
350
|
// ── delta mode ────────────────────────────────────────────────────────────────
|
|
351
351
|
function formatProjectDeltaDiagnostic(diagnostic, stale) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
352
|
+
// #1944: a demoted row loses the authority marker along with its
|
|
353
|
+
// coordinate. It kept the 🔴 while its line was replaced by the stale
|
|
354
|
+
// marker, which is the same "changed the channel, not the body" defect the
|
|
355
|
+
// turn-end advisory carried — `formatFullMode` (this file, the `d.stale`
|
|
356
|
+
// arm of its marker) already drops the marker for exactly this reason.
|
|
357
|
+
const marker = stale
|
|
358
|
+
? "○"
|
|
359
|
+
: diagnostic.semantic === "blocking" || diagnostic.severity === "error"
|
|
360
|
+
? "🔴"
|
|
361
|
+
: "ℹ";
|
|
355
362
|
const rule = diagnostic.rule ?? diagnostic.code ?? diagnostic.runner;
|
|
356
363
|
const where = stale ? STALE_LINE_MARKER : `L${diagnostic.line ?? "?"}`;
|
|
357
364
|
return ` ${marker} ${where} ${rule} ${diagnostic.message}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
|
|
6
6
|
"repository": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"build": "tsc --project tsconfig.build.json",
|
|
18
18
|
"build:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npx --yes -p typescript@7.0.2 tsc --project tsconfig.dist.json --noCheck && npm run bundle:dist",
|
|
19
19
|
"bundle:dist": "node scripts/bundle-dist.mjs",
|
|
20
|
-
"prepare": "npm run build:dist && node scripts/download-grammars.js --core --dest grammars",
|
|
20
|
+
"prepare": "npm run build:dist && node scripts/download-grammars.js --core --dest grammars && node scripts/setup-git-hooks.mjs && node scripts/warm-loader-cache.mjs",
|
|
21
21
|
"lint": "tsc --project tsconfig.json",
|
|
22
22
|
"watch": "tsc --watch",
|
|
23
23
|
"test": "node scripts/with-test-lock.mjs -- vitest run",
|
|
@@ -80,6 +80,9 @@
|
|
|
80
80
|
"scripts/grammars.lock.json",
|
|
81
81
|
"scripts/analyze-pi-lens-logs.mjs",
|
|
82
82
|
"scripts/install-selftest.mjs",
|
|
83
|
+
"scripts/lib/host-provided-deps.mjs",
|
|
84
|
+
"scripts/lib/warm-loader-cache.mjs",
|
|
85
|
+
"scripts/warm-loader-cache.mjs",
|
|
83
86
|
"scripts/rpc-load-check.mjs",
|
|
84
87
|
"README.md",
|
|
85
88
|
"CHANGELOG.md",
|
|
@@ -89,30 +92,40 @@
|
|
|
89
92
|
"dependencies": {
|
|
90
93
|
"@ast-grep/cli": "^0.45.0",
|
|
91
94
|
"@ast-grep/napi": "^0.45.0",
|
|
92
|
-
"@earendil-works/pi-tui": "^0.84.1",
|
|
93
95
|
"js-yaml": "^5.2.2",
|
|
94
96
|
"minimatch": "^10.2.6",
|
|
95
97
|
"pidusage": "^4.0.1",
|
|
96
|
-
"typebox": "^1.0.0",
|
|
97
98
|
"vscode-jsonrpc": "^9.0.0"
|
|
98
99
|
},
|
|
99
100
|
"peerDependencies": {
|
|
100
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
101
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
102
|
+
"@earendil-works/pi-tui": "^0.84.1",
|
|
103
|
+
"typebox": "^1.0.0"
|
|
101
104
|
},
|
|
102
105
|
"peerDependenciesMeta": {
|
|
103
106
|
"@earendil-works/pi-coding-agent": {
|
|
104
107
|
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@earendil-works/pi-tui": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"typebox": {
|
|
113
|
+
"optional": true
|
|
105
114
|
}
|
|
106
115
|
},
|
|
107
116
|
"optionalDependencies": {
|
|
117
|
+
"jiti": "^2.7.0",
|
|
108
118
|
"web-tree-sitter": "0.25.10"
|
|
109
119
|
},
|
|
110
120
|
"devDependencies": {
|
|
111
121
|
"@biomejs/biome": "^2.4.10",
|
|
112
122
|
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
123
|
+
"@earendil-works/pi-tui": "^0.84.1",
|
|
113
124
|
"@types/node": "^26.0.0",
|
|
114
125
|
"@types/pidusage": "^2.0.5",
|
|
115
126
|
"@vitest/coverage-v8": "^4.1.5",
|
|
127
|
+
"husky": "^9.1.7",
|
|
128
|
+
"typebox": "^1.0.0",
|
|
116
129
|
"typescript": "^7.0.2",
|
|
117
130
|
"typescript-language-server": "^5.1.3",
|
|
118
131
|
"vitest": "^4.1.1"
|
|
@@ -19,6 +19,20 @@
|
|
|
19
19
|
* build-script-provided assets (ast-grep CLI binary + tree-sitter grammars)
|
|
20
20
|
* that pnpm/bun skip by default. It runs no model and needs no credentials.
|
|
21
21
|
*
|
|
22
|
+
* WHAT IT NO LONGER COVERS (#1926)
|
|
23
|
+
* pi supplies `typebox` and `@earendil-works/pi-tui` from its own runtime, so
|
|
24
|
+
* they are optional peers and no install vendors them. This probe is not pi, so
|
|
25
|
+
* those specifiers cannot resolve here, and `dist/index.js` throws on the FIRST
|
|
26
|
+
* one it imports. A host-provided miss is therefore recorded as EXPECTED rather
|
|
27
|
+
* than failed. The cost is real and stated plainly: evaluation stops there, so
|
|
28
|
+
* the entry probe no longer proves the whole bundled graph resolves, which is
|
|
29
|
+
* what #285 and #335 asked it to prove. Any OTHER unresolved specifier still
|
|
30
|
+
* fails hard, and the per-module probes below (file-utils, complexity-client,
|
|
31
|
+
* bootstrap) still cover that graph directly. The surviving POSITIVE proof that
|
|
32
|
+
* the entry loads in full is the install-smoke `pi-load` job: it installs pi,
|
|
33
|
+
* installs pi-lens through pi, and confirms over RPC that pi-lens registered
|
|
34
|
+
* its commands.
|
|
35
|
+
*
|
|
22
36
|
* USAGE
|
|
23
37
|
* bun scripts/install-selftest.mjs # faithful: pi's runtime is bun
|
|
24
38
|
* node scripts/install-selftest.mjs # also works
|
|
@@ -28,10 +42,11 @@
|
|
|
28
42
|
*/
|
|
29
43
|
|
|
30
44
|
import { execFileSync } from "node:child_process";
|
|
45
|
+
import * as fs from "node:fs";
|
|
31
46
|
import { createRequire } from "node:module";
|
|
32
|
-
import { fileURLToPath } from "node:url";
|
|
33
47
|
import * as path from "node:path";
|
|
34
|
-
import
|
|
48
|
+
import { fileURLToPath } from "node:url";
|
|
49
|
+
import { HOST_PROVIDED_PACKAGES } from "./lib/host-provided-deps.mjs";
|
|
35
50
|
|
|
36
51
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
37
52
|
const pkgRoot = path.resolve(here, "..");
|
|
@@ -42,6 +57,26 @@ const results = [];
|
|
|
42
57
|
const record = (name, kind, ok, detail = "") =>
|
|
43
58
|
results.push({ name, kind, ok, detail });
|
|
44
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Which host-provided package, if any, a resolution error is about. Returns
|
|
62
|
+
* undefined for every other error, so an unrelated missing package still fails.
|
|
63
|
+
*/
|
|
64
|
+
function hostProvidedMiss(err) {
|
|
65
|
+
const text = `${err?.code || ""} ${err?.message || err}`;
|
|
66
|
+
if (
|
|
67
|
+
!/ERR_MODULE_NOT_FOUND|Cannot find (package|module)|ResolveMessage/.test(
|
|
68
|
+
text,
|
|
69
|
+
)
|
|
70
|
+
) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return HOST_PROVIDED_PACKAGES.find((name) =>
|
|
74
|
+
new RegExp(
|
|
75
|
+
`['"\`]${name.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&")}['"\`]`,
|
|
76
|
+
).test(text),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
45
80
|
/** Import a module by URL and record whether its (eager) dep graph resolved. */
|
|
46
81
|
async function probeImport(name, relPath) {
|
|
47
82
|
const url = new URL(
|
|
@@ -51,7 +86,24 @@ async function probeImport(name, relPath) {
|
|
|
51
86
|
await import(url.href);
|
|
52
87
|
record(name, "resolve", true);
|
|
53
88
|
} catch (err) {
|
|
54
|
-
|
|
89
|
+
// #1926: pi supplies these; outside pi they cannot resolve, and the entry
|
|
90
|
+
// throws on the first one. Expected, not a regression. Anything else is.
|
|
91
|
+
const host = hostProvidedMiss(err);
|
|
92
|
+
if (host) {
|
|
93
|
+
record(
|
|
94
|
+
name,
|
|
95
|
+
"expected",
|
|
96
|
+
true,
|
|
97
|
+
`stopped at host-provided '${host}' — pi supplies it; graph beyond this point unproven here (#1926)`,
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
record(
|
|
102
|
+
name,
|
|
103
|
+
"resolve",
|
|
104
|
+
false,
|
|
105
|
+
`${err?.code || ""} ${err?.message || err}`.trim(),
|
|
106
|
+
);
|
|
55
107
|
}
|
|
56
108
|
}
|
|
57
109
|
|
|
@@ -61,27 +113,53 @@ function probeResolve(spec) {
|
|
|
61
113
|
require.resolve(spec);
|
|
62
114
|
record(spec, "resolve", true);
|
|
63
115
|
} catch (err) {
|
|
64
|
-
record(
|
|
116
|
+
record(
|
|
117
|
+
spec,
|
|
118
|
+
"resolve",
|
|
119
|
+
false,
|
|
120
|
+
`${err?.code || ""} ${err?.message || err}`.trim(),
|
|
121
|
+
);
|
|
65
122
|
}
|
|
66
123
|
}
|
|
67
124
|
|
|
68
125
|
// --- 1. The documented failure-point modules (eager bare imports) ----------
|
|
69
126
|
await probeImport("dist/index.js (entry)", "dist/index.js");
|
|
70
|
-
await probeImport(
|
|
71
|
-
|
|
72
|
-
|
|
127
|
+
await probeImport(
|
|
128
|
+
"clients/file-utils.js (→ minimatch)",
|
|
129
|
+
"dist/clients/file-utils.js",
|
|
130
|
+
);
|
|
131
|
+
await probeImport(
|
|
132
|
+
"clients/complexity-client.js (→ tree-sitter)",
|
|
133
|
+
"dist/clients/complexity-client.js",
|
|
134
|
+
);
|
|
135
|
+
await probeImport(
|
|
136
|
+
"clients/bootstrap.js (→ all analyzers)",
|
|
137
|
+
"dist/clients/bootstrap.js",
|
|
138
|
+
);
|
|
73
139
|
|
|
74
140
|
// --- 2. Direct bare-specifier resolution -----------------------------------
|
|
75
|
-
|
|
141
|
+
// Host-provided packages are subtracted, not listed as exceptions: pi resolves
|
|
142
|
+
// them from its own runtime, so nothing installs them and probing one here is
|
|
143
|
+
// guaranteed to fail (#1926). The subtraction reads the same list the bundler
|
|
144
|
+
// and the packaging tests use, so it cannot drift.
|
|
145
|
+
const BARE_SPECIFIERS = [
|
|
76
146
|
"minimatch",
|
|
77
147
|
"typebox",
|
|
78
148
|
"js-yaml",
|
|
79
149
|
"vscode-jsonrpc",
|
|
80
150
|
"web-tree-sitter",
|
|
81
151
|
"@ast-grep/napi",
|
|
82
|
-
])
|
|
152
|
+
].filter((spec) => !HOST_PROVIDED_PACKAGES.includes(spec));
|
|
153
|
+
|
|
154
|
+
for (const spec of BARE_SPECIFIERS) {
|
|
83
155
|
probeResolve(spec);
|
|
84
156
|
}
|
|
157
|
+
record(
|
|
158
|
+
"host-provided specifiers skipped",
|
|
159
|
+
"expected",
|
|
160
|
+
true,
|
|
161
|
+
`${HOST_PROVIDED_PACKAGES.join(", ")} — pi supplies these (#1926)`,
|
|
162
|
+
);
|
|
85
163
|
|
|
86
164
|
// --- 3. Spawned-binary tools (the universal net) ----------------------------
|
|
87
165
|
// For every tool that ships its binary in a per-platform npm package
|
|
@@ -121,11 +199,21 @@ try {
|
|
|
121
199
|
});
|
|
122
200
|
record(label, "tool", true, bin);
|
|
123
201
|
} catch (err) {
|
|
124
|
-
record(
|
|
202
|
+
record(
|
|
203
|
+
label,
|
|
204
|
+
"tool",
|
|
205
|
+
false,
|
|
206
|
+
`${bin} failed to run: ${err?.message || err}`,
|
|
207
|
+
);
|
|
125
208
|
}
|
|
126
209
|
}
|
|
127
210
|
} catch (err) {
|
|
128
|
-
record(
|
|
211
|
+
record(
|
|
212
|
+
"spawned-tool probe",
|
|
213
|
+
"tool",
|
|
214
|
+
false,
|
|
215
|
+
`installer load failed: ${err?.message || err}`,
|
|
216
|
+
);
|
|
129
217
|
}
|
|
130
218
|
|
|
131
219
|
// tree-sitter grammars — download-grammars.js postinstall writes them into
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the packages `scripts/bundle-dist.mjs` keeps out of
|
|
3
|
+
* the dist bundle, split by the REASON they stay external. #1926.
|
|
4
|
+
*
|
|
5
|
+
* HOST_PROVIDED_PACKAGES
|
|
6
|
+
* pi ships these inside its own runtime (`@earendil-works/pi-coding-agent`'s
|
|
7
|
+
* node_modules). They must be declared as OPTIONAL peer dependencies plus dev
|
|
8
|
+
* dependencies, never as runtime `dependencies`, because a runtime dependency
|
|
9
|
+
* makes `npm install --omit=dev` — the command pi runs for a `git:` install —
|
|
10
|
+
* vendor a private second copy into the extension's own node_modules. Node
|
|
11
|
+
* then resolves the bare specifier to that copy and evaluates a whole extra
|
|
12
|
+
* module graph at import. That is #1926: vendoring `typebox` and
|
|
13
|
+
* `@earendil-works/pi-tui` cost 720ms of the 838ms git-install import, and
|
|
14
|
+
* removing the copies took the PI_TIMING module import from 941ms to ~180ms.
|
|
15
|
+
*
|
|
16
|
+
* The three entries are NOT interchangeable at runtime. pi resolves the bare
|
|
17
|
+
* specifiers `typebox` and `@earendil-works/pi-tui` for an extension, so
|
|
18
|
+
* VALUE imports of those work with no copy on disk — verified by hiding both
|
|
19
|
+
* from the dogfood install and re-running under PI_TIMING (#1926), and by
|
|
20
|
+
* `@tintinweb/pi-subagents`, which declares pi-tui as a peer and ships no
|
|
21
|
+
* copy. `@earendil-works/pi-coding-agent` is different: it is not resolvable
|
|
22
|
+
* from an extension at all, so pi-lens imports it TYPE-ONLY and inlines the
|
|
23
|
+
* runtime helpers it needs. `tests/host-sdk-type-only.test.ts` enforces that
|
|
24
|
+
* separate rule (#1334 S6); do not read this list as permission to
|
|
25
|
+
* value-import the host SDK.
|
|
26
|
+
*
|
|
27
|
+
* LAZY_NATIVE_PACKAGES
|
|
28
|
+
* Native addon / wasm, dynamic-imported by absolute file:// URL at call time.
|
|
29
|
+
* They stay external because esbuild cannot inline a .node or .wasm, and they
|
|
30
|
+
* cost nothing at load because nothing imports them statically.
|
|
31
|
+
*
|
|
32
|
+
* `tests/packaging.test.ts` pins both invariants against package.json and
|
|
33
|
+
* against the built dist entry, so this list cannot drift from what ships.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Host-provided packages the extension VALUE-imports. pi resolves these bare
|
|
38
|
+
* specifiers at runtime, so the compiled entry cannot load without them.
|
|
39
|
+
* Outside pi — a bare `node dist/index.js` in a CI smoke check — nothing
|
|
40
|
+
* supplies them, so a job that wants a FULL load has to install them first, the
|
|
41
|
+
* way pi does. `scripts/supply-host-provided-deps.mjs` does that.
|
|
42
|
+
*/
|
|
43
|
+
export const HOST_PROVIDED_RUNTIME_PACKAGES = Object.freeze([
|
|
44
|
+
"typebox",
|
|
45
|
+
"@earendil-works/pi-tui",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Host-provided packages the extension imports TYPE-ONLY. pi does not resolve
|
|
50
|
+
* these for an extension at all, which is why every runtime helper pi-lens
|
|
51
|
+
* needs from the host SDK is inlined instead; `tests/host-sdk-type-only.test.ts`
|
|
52
|
+
* enforces that (#1334 S6). Never install one of these to make a load succeed:
|
|
53
|
+
* the transitive tree has nested paths that exceed Windows MAX_PATH.
|
|
54
|
+
*/
|
|
55
|
+
export const HOST_PROVIDED_TYPE_ONLY_PACKAGES = Object.freeze([
|
|
56
|
+
"@earendil-works/pi-coding-agent",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** Everything pi supplies, so pi-lens must never declare it a dependency. */
|
|
60
|
+
export const HOST_PROVIDED_PACKAGES = Object.freeze([
|
|
61
|
+
...HOST_PROVIDED_RUNTIME_PACKAGES,
|
|
62
|
+
...HOST_PROVIDED_TYPE_ONLY_PACKAGES,
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/** Native/wasm packages dynamic-imported by absolute path at call time. */
|
|
66
|
+
export const LAZY_NATIVE_PACKAGES = Object.freeze([
|
|
67
|
+
"@ast-grep/napi",
|
|
68
|
+
"web-tree-sitter",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/** Everything esbuild must NOT inline into dist/index.js. */
|
|
72
|
+
export const BUNDLE_EXTERNALS = Object.freeze([
|
|
73
|
+
...HOST_PROVIDED_PACKAGES,
|
|
74
|
+
...LAZY_NATIVE_PACKAGES,
|
|
75
|
+
]);
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for `scripts/warm-loader-cache.mjs` (#1926).
|
|
3
|
+
*
|
|
4
|
+
* pi loads an extension through jiti (`createJiti(..., { moduleCache: false,
|
|
5
|
+
* alias: … })` in `@earendil-works/pi-coding-agent`'s
|
|
6
|
+
* `dist/core/extensions/loader.js`). jiti transforms the entry with Babel and
|
|
7
|
+
* stores the result in a filesystem cache. pi-lens's `dist/index.js` is a ~4MB
|
|
8
|
+
* esbuild bundle, so that transform costs seconds: the first session after a
|
|
9
|
+
* `git:` install or update measured 4847ms of `module import`, against a 138ms
|
|
10
|
+
* steady state once the cache is warm (#1926 field report).
|
|
11
|
+
*
|
|
12
|
+
* The fix is to pay that transform at install time instead of in the first
|
|
13
|
+
* interactive session. `prepare` already builds `dist/`, so the last step of
|
|
14
|
+
* the chain runs the same transform through the same jiti, writing the same
|
|
15
|
+
* cache file pi will later read.
|
|
16
|
+
*
|
|
17
|
+
* WHY THE WARM PRODUCES A HIT FOR THE REAL LOADER
|
|
18
|
+
*
|
|
19
|
+
* jiti keys a cache entry on the transformed file, not on the jiti instance
|
|
20
|
+
* (see `getCache` in `jiti/dist/jiti.cjs`):
|
|
21
|
+
*
|
|
22
|
+
* <basename(dirname(file))>-<basename(file) up to the first dot>
|
|
23
|
+
* + "+map" if sourceMaps + ".i" if interopDefault
|
|
24
|
+
* + "." + md5(file).slice(0, 8)
|
|
25
|
+
* + ".mjs" for an async import, ".cjs" otherwise
|
|
26
|
+
*
|
|
27
|
+
* and it validates the stored body against a trailing
|
|
28
|
+
* ` /* v<TRANSFORM_VERSION>-<md5(source).slice(0,16)> *\/` marker. So the warm
|
|
29
|
+
* has to agree with pi on three things only: the absolute path of the entry,
|
|
30
|
+
* the cache directory, and the transform itself. Everything else — the alias
|
|
31
|
+
* map, `moduleCache`, which package the jiti instance was created from — is
|
|
32
|
+
* outside the key AND outside the transform. Measured, not assumed: running
|
|
33
|
+
* this warm over the dogfood install's `dist/index.js` produced a cache file
|
|
34
|
+
* byte-identical to the one pi had written, both with pi's alias map and with
|
|
35
|
+
* no alias map at all.
|
|
36
|
+
*
|
|
37
|
+
* The marker is also the safety net. If pi ships a jiti whose transform version
|
|
38
|
+
* or Babel output differs, pi recomputes the marker, does not match, and simply
|
|
39
|
+
* re-transforms. A mismatched warm costs the install a few seconds and buys
|
|
40
|
+
* nothing; it can never hand pi the wrong code.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { createHash, getFips } from "node:crypto";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mirror of jiti's `utils_hash`: md5 truncated to `length` hex characters, or
|
|
47
|
+
* sha256 where FIPS mode forbids md5. jiti makes the same substitution, so the
|
|
48
|
+
* two agree on every machine.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @param {number} [length]
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function jitiHash(text, length = 8) {
|
|
55
|
+
let fips = false;
|
|
56
|
+
try {
|
|
57
|
+
fips = Boolean(getFips?.());
|
|
58
|
+
} catch {
|
|
59
|
+
fips = false;
|
|
60
|
+
}
|
|
61
|
+
return createHash(fips ? "sha256" : "md5")
|
|
62
|
+
.update(text)
|
|
63
|
+
.digest("hex")
|
|
64
|
+
.slice(0, length);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The cache file jiti writes for an async import of `entry`.
|
|
69
|
+
*
|
|
70
|
+
* Mirror of jiti's `getCache` naming:
|
|
71
|
+
* `<basename(dirname(file))>-<basename up to the first dot>.<hash(file)>.mjs`.
|
|
72
|
+
* jiti hashes the POSIX-normalised absolute path, so the separators are
|
|
73
|
+
* converted here. The drive-letter case is left exactly as the caller resolved
|
|
74
|
+
* it, because jiti does not touch it either. Verified against pi's own cache
|
|
75
|
+
* directory: the md5 of the dogfood install's forward-slashed entry path,
|
|
76
|
+
* truncated to 8 characters, is the `db18768f` in the
|
|
77
|
+
* `dist-index.db18768f.mjs` pi had already written.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} entry absolute path to the extension entry
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function expectedCacheFileName(entry) {
|
|
83
|
+
const posix = entry.split("\\").join("/");
|
|
84
|
+
const segments = posix.split("/");
|
|
85
|
+
const base = segments.pop() ?? posix;
|
|
86
|
+
const parent = segments.pop() ?? "";
|
|
87
|
+
const dot = base.indexOf(".");
|
|
88
|
+
const stem = dot <= 0 ? base : base.slice(0, dot);
|
|
89
|
+
return `${parent}-${stem}.${jitiHash(posix)}.mjs`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Did the warm actually leave a cache entry pi can use?
|
|
94
|
+
*
|
|
95
|
+
* Elapsed time is not evidence. jiti can return from an import having written
|
|
96
|
+
* nothing, and every such path exits 0:
|
|
97
|
+
*
|
|
98
|
+
* - the cache directory is not writable. jiti's `prepareCacheDir` catches
|
|
99
|
+
* that, sets `fsCache` to false, and transforms in memory from then on.
|
|
100
|
+
* - the entry imported natively and nothing needed transforming. For an
|
|
101
|
+
* async-imported ESM `.js` file jiti's `evalModule` attempts a native
|
|
102
|
+
* import UNCONDITIONALLY, and only falls back to the transform when that
|
|
103
|
+
* import rejects. So the cache exists at all because native import fails in
|
|
104
|
+
* a real `--omit=dev` install, where the host-provided specifiers do not
|
|
105
|
+
* resolve. That is the same reason pi's own load transforms the bundle,
|
|
106
|
+
* which is why the warm and pi agree — but it is a property of the
|
|
107
|
+
* environment, not of this script.
|
|
108
|
+
* - a cache entry exists but does not match the source. jiti validates the
|
|
109
|
+
* body against a trailing version-and-source-hash marker, so a stale or
|
|
110
|
+
* truncated file is a miss for pi and the session pays the transform.
|
|
111
|
+
*
|
|
112
|
+
* Checking the file is what turns "the import returned" into "the warm's own
|
|
113
|
+
* jiti cached this entry, with these versions". It is not a promise about pi:
|
|
114
|
+
* pi-side drift shows up as a version delta between this record and pi's, which
|
|
115
|
+
* is a thing to read from the log, not something an install can detect.
|
|
116
|
+
* An absent entry always warrants a look, never a shrug.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} args
|
|
119
|
+
* @param {string} args.cacheDir
|
|
120
|
+
* @param {string} args.fileName
|
|
121
|
+
* @param {string} args.source contents of the entry, for the marker check
|
|
122
|
+
* @param {object} args.fsDeps
|
|
123
|
+
* @param {(p: string) => boolean} args.fsDeps.existsSync
|
|
124
|
+
* @param {(p: string) => string} args.fsDeps.readFileSync
|
|
125
|
+
* @param {(p: string) => boolean} args.fsDeps.isWritable
|
|
126
|
+
* @returns {{ok: boolean, reason: string | null, transformVersion: string | null}}
|
|
127
|
+
*/
|
|
128
|
+
export function verifyCacheEntry({ cacheDir, fileName, source, fsDeps }) {
|
|
129
|
+
const file = `${cacheDir}/${fileName}`;
|
|
130
|
+
if (!fsDeps.existsSync(file)) {
|
|
131
|
+
if (!fsDeps.isWritable(cacheDir)) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: `cache directory is not writable: ${cacheDir}`,
|
|
135
|
+
transformVersion: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Deliberately generic. Several routes end here — a native import that
|
|
139
|
+
// needed no transform, a transform that threw before the write — and this
|
|
140
|
+
// check cannot tell them apart, so it does not name one.
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
reason: `no cache entry was written: ${fileName}`,
|
|
144
|
+
transformVersion: null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
let body;
|
|
148
|
+
try {
|
|
149
|
+
body = fsDeps.readFileSync(file);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
reason: `cache entry unreadable: ${message}`,
|
|
155
|
+
transformVersion: null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const marker = /\/\* v([^-\s]+)-([0-9a-f]+) \*\/\s*$/.exec(body);
|
|
159
|
+
if (!marker) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
reason: "cache entry has no jiti version marker — pi will re-transform",
|
|
163
|
+
transformVersion: null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (marker[2] !== jitiHash(source, marker[2].length)) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
reason:
|
|
170
|
+
"cache entry does not match the built entry — pi will re-transform",
|
|
171
|
+
transformVersion: marker[1],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, reason: null, transformVersion: marker[1] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Mirror of jiti's `prepareCacheDir` fallback branch.
|
|
179
|
+
*
|
|
180
|
+
* jiti picks `<dir of the file that created the jiti instance>/node_modules/
|
|
181
|
+
* .cache/jiti` when that `node_modules` exists, and `<tmpdir>/jiti` otherwise.
|
|
182
|
+
* pi creates its instance from `dist/core/extensions/loader.js` inside the
|
|
183
|
+
* installed `@earendil-works/pi-coding-agent`, which has no `node_modules`
|
|
184
|
+
* sibling, so pi always lands on the tmpdir branch. The warm therefore passes
|
|
185
|
+
* `<tmpdir>/jiti` EXPLICITLY rather than letting jiti derive a directory from
|
|
186
|
+
* this repo's layout: `scripts/` has no `node_modules` sibling today, but a
|
|
187
|
+
* script moved one level up would silently start filling a private cache that
|
|
188
|
+
* pi never reads.
|
|
189
|
+
*
|
|
190
|
+
* The TMPDIR dance below is jiti's, kept verbatim so the two agree on machines
|
|
191
|
+
* where TMPDIR is set to the current directory.
|
|
192
|
+
*
|
|
193
|
+
* @param {object} deps
|
|
194
|
+
* @param {() => string} deps.tmpdir
|
|
195
|
+
* @param {Record<string, string | undefined>} deps.env
|
|
196
|
+
* @param {() => string} deps.cwd
|
|
197
|
+
* @param {(a: string, b: string) => string} deps.join
|
|
198
|
+
* @returns {string}
|
|
199
|
+
*/
|
|
200
|
+
export function resolveJitiCacheDir({ tmpdir, env, cwd, join }) {
|
|
201
|
+
let dir = tmpdir();
|
|
202
|
+
if (env.TMPDIR && dir === cwd() && !env.JITI_RESPECT_TMPDIR_ENV) {
|
|
203
|
+
const saved = env.TMPDIR;
|
|
204
|
+
delete env.TMPDIR;
|
|
205
|
+
dir = tmpdir();
|
|
206
|
+
env.TMPDIR = saved;
|
|
207
|
+
}
|
|
208
|
+
return join(dir, "jiti");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Alias every host-provided specifier to a path that cannot exist.
|
|
213
|
+
*
|
|
214
|
+
* The warm only needs the TRANSFORM of `dist/index.js`; jiti writes the cache
|
|
215
|
+
* entry before it evaluates the module. Evaluation then walks the bundle's
|
|
216
|
+
* external imports, none of which resolve outside pi. Pointing the
|
|
217
|
+
* host-provided ones at a stub makes that walk stop at the first one instead of
|
|
218
|
+
* resolving unrelated packages first, so the warm does the least work it can
|
|
219
|
+
* and always ends the same way. Derived from the shared host-provided list so
|
|
220
|
+
* it cannot drift from what `bundle-dist.mjs` keeps external.
|
|
221
|
+
*
|
|
222
|
+
* @param {readonly string[]} hostProvidedPackages
|
|
223
|
+
* @returns {Record<string, string>}
|
|
224
|
+
*/
|
|
225
|
+
export function buildStubAliases(hostProvidedPackages) {
|
|
226
|
+
/** @type {Record<string, string>} */
|
|
227
|
+
const alias = {};
|
|
228
|
+
for (const name of hostProvidedPackages) {
|
|
229
|
+
alias[name] = STUB_TARGET;
|
|
230
|
+
}
|
|
231
|
+
return alias;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A path no filesystem resolves, so aliased imports fail immediately. */
|
|
235
|
+
export const STUB_TARGET = "/__pi-lens-warm-cache-stub__";
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Decide whether the warm can run, and say why not when it cannot.
|
|
239
|
+
*
|
|
240
|
+
* Every reason is a skip, never a failure: `prepare` also builds `dist/` and
|
|
241
|
+
* downloads grammars, and those steps MUST fail loudly. Cache warming is an
|
|
242
|
+
* optimisation, so it runs last and reports rather than throws — the same
|
|
243
|
+
* posture as `scripts/setup-git-hooks.mjs` (#1804).
|
|
244
|
+
*
|
|
245
|
+
* @param {object} state
|
|
246
|
+
* @param {Record<string, string | undefined>} state.env
|
|
247
|
+
* @param {boolean} state.distEntryExists
|
|
248
|
+
* @param {boolean} state.jitiResolvable
|
|
249
|
+
* @returns {string | null} skip reason, or null to proceed
|
|
250
|
+
*/
|
|
251
|
+
export function warmSkipReason({ env, distEntryExists, jitiResolvable }) {
|
|
252
|
+
const optOut = env.PI_LENS_SKIP_WARM_CACHE;
|
|
253
|
+
if (typeof optOut === "string" && optOut.length > 0) {
|
|
254
|
+
return "PI_LENS_SKIP_WARM_CACHE is set";
|
|
255
|
+
}
|
|
256
|
+
if (!distEntryExists) {
|
|
257
|
+
return "dist/index.js is missing — nothing to warm";
|
|
258
|
+
}
|
|
259
|
+
if (!jitiResolvable) {
|
|
260
|
+
// jiti is an optionalDependency, so `--omit=optional` (or a failed
|
|
261
|
+
// optional install) legitimately leaves it absent.
|
|
262
|
+
return "jiti is not installed — install ran without optional dependencies";
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Keep the install log bounded: one line per install, newest last.
|
|
269
|
+
*
|
|
270
|
+
* @param {string[]} existingLines
|
|
271
|
+
* @param {string} line
|
|
272
|
+
* @param {number} [max]
|
|
273
|
+
* @returns {string[]}
|
|
274
|
+
*/
|
|
275
|
+
export function appendBounded(
|
|
276
|
+
existingLines,
|
|
277
|
+
line,
|
|
278
|
+
max = INSTALL_LOG_MAX_LINES,
|
|
279
|
+
) {
|
|
280
|
+
const lines = [...existingLines.filter((l) => l.trim().length > 0), line];
|
|
281
|
+
return lines.slice(Math.max(0, lines.length - max));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Cap for `~/.pi-lens/install.log`. */
|
|
285
|
+
export const INSTALL_LOG_MAX_LINES = 100;
|