pi-supernova 0.6.0 → 0.7.1
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 +27 -3
- package/docs/CHANGELOG.md +114 -0
- package/docs/TOKEN_COSTS.md +13 -5
- package/index.js +120 -79
- package/package.json +1 -1
- package/src/adapters/bash.js +73 -0
- package/src/adapters/edit.js +249 -0
- package/src/adapters/errors.js +31 -0
- package/src/adapters/index.js +31 -0
- package/src/adapters/list.js +102 -0
- package/src/adapters/read.js +805 -0
- package/src/adapters/refs.js +41 -0
- package/src/adapters/write.js +96 -0
- package/src/bridge/catalog.js +28 -222
- package/src/bridge/host-bridge.js +113 -1668
- package/src/bridge/invoke.js +35 -0
- package/src/bridge/native-tools.js +1 -198
- package/src/context/evidence.js +140 -76
- package/src/context/fuzzy.js +42 -24
- package/src/context/ledger.js +43 -24
- package/src/context/outline.js +23 -18
- package/src/context/repo-index.js +206 -170
- package/src/context/search.js +157 -77
- package/src/context/snap.js +240 -136
- package/src/context/spans.js +2 -1
- package/src/context/surface.js +14 -5
- package/src/contract/bash.js +31 -0
- package/src/contract/edit.js +95 -0
- package/src/contract/read.js +220 -0
- package/src/fs/check.js +12 -8
- package/src/fs/diff.js +18 -7
- package/src/fs/json-read.js +66 -35
- package/src/fs/patch.js +94 -50
- package/src/fs/source-window.js +82 -0
- package/src/fs/text-ops.js +512 -0
- package/src/fs/vfs.js +205 -175
- package/src/fs/workspace.js +119 -108
- package/src/output/bottleneck.js +195 -116
- package/src/output/format.js +101 -67
- package/src/runtime/guest-deny-imports.js +34 -0
- package/src/runtime/guest-worker.js +296 -292
- package/src/runtime/parallel.js +97 -64
- package/src/runtime/program-batch.js +178 -69
- package/src/runtime/reference.js +15 -14
- package/src/runtime/runtime.js +327 -187
- package/src/shared/decode.js +58 -36
- package/src/ui/omp-frame.js +59 -42
- package/src/ui/render-measure.js +51 -29
- package/src/ui/render.js +241 -145
package/src/context/search.js
CHANGED
|
@@ -29,6 +29,47 @@ function pendingInScope(root, pendingPaths) {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function parseMatchRecord(line, truncated, isLast) {
|
|
33
|
+
if (!line) return { skip: true };
|
|
34
|
+
|
|
35
|
+
try { return { record: JSON.parse(line) }; }
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (truncated && isLast) return { stop: true };
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isRgMatch(record) {
|
|
43
|
+
return record.type === "match" && isString(record.data?.path?.text) && isString(record.data.lines?.text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function applyMatchRecord(record, root, overlayText, add) {
|
|
47
|
+
if (!isRgMatch(record)) return;
|
|
48
|
+
const file = path.resolve(root, record.data.path.text);
|
|
49
|
+
|
|
50
|
+
if (overlayText(file) === undefined) add(file, record.data.line_number, record.data.lines.text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ingestRgMatches(records, result, signal, root, overlayText, add) {
|
|
54
|
+
for (let i = 0; i < records.length; i++) {
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
const parsed = parseMatchRecord(records[i], result.outputTruncated, i === records.length - 1);
|
|
57
|
+
|
|
58
|
+
if (parsed.stop) break;
|
|
59
|
+
|
|
60
|
+
if (parsed.skip) continue;
|
|
61
|
+
applyMatchRecord(parsed.record, root, overlayText, add);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ingestPendingRefs(root, pendingPaths, overlayText, add) {
|
|
66
|
+
for (const file of pendingInScope(root, pendingPaths)) {
|
|
67
|
+
const text = overlayText(file);
|
|
68
|
+
|
|
69
|
+
if (text !== undefined && Buffer.byteLength(text, "utf8") <= 512 * 1024) text.split("\n").forEach((line, i) => add(file, i + 1, line));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
32
73
|
/** One bounded direct search for all changed names; no repository index or per-name spawn. */
|
|
33
74
|
export async function referencesForNames({ root, names, excludePath, overlayText, pendingPaths, signal, run = runCommand }) {
|
|
34
75
|
const references = new Map(names.map(name => [name, []]));
|
|
@@ -48,85 +89,90 @@ export async function referencesForNames({ root, names, excludePath, overlayText
|
|
|
48
89
|
{ cwd: root, signal, timeoutMs: 5000, maxOutputChars: 65536 });
|
|
49
90
|
|
|
50
91
|
if (result.exitCode !== 0 && result.exitCode !== 1) throw new Error(result.stderr.trim() || "reference search failed");
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for (let i = 0; i < records.length; i++) {
|
|
54
|
-
signal?.throwIfAborted();
|
|
55
|
-
|
|
56
|
-
if (!records[i]) continue;
|
|
57
|
-
let record;
|
|
58
|
-
|
|
59
|
-
try { record = JSON.parse(records[i]); }
|
|
60
|
-
catch (error) { if (result.outputTruncated && i === records.length - 1) break; throw error; }
|
|
92
|
+
ingestRgMatches(result.stdout.split("\n"), result, signal, root, overlayText, add);
|
|
93
|
+
ingestPendingRefs(root, pendingPaths, overlayText, add);
|
|
61
94
|
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
return { references, incomplete: result.outputTruncated === true };
|
|
96
|
+
}
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
function grepCaseSensitive(pattern, params) {
|
|
99
|
+
return params?.caseSensitive === true || (params?.caseSensitive !== false && smartCase(pattern));
|
|
100
|
+
}
|
|
67
101
|
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
function pushGrepFlags(args, pattern, params) {
|
|
103
|
+
if (!grepCaseSensitive(pattern, params)) args.push("--ignore-case");
|
|
70
104
|
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
if (params?.glob) args.push("--glob", String(params.glob));
|
|
106
|
+
const limit = params?.limit;
|
|
73
107
|
|
|
74
|
-
|
|
108
|
+
if (Number.isInteger(limit) && limit > 0) args.push("--max-count", String(Math.min(limit, 2000)));
|
|
75
109
|
}
|
|
76
110
|
|
|
77
111
|
export function rgGrepArgs(pattern, params, searchPath) {
|
|
78
112
|
const args = ["--line-number", "--no-heading", "--color", "never"];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!caseSensitive) args.push("--ignore-case");
|
|
82
|
-
|
|
83
|
-
if (params?.glob) args.push("--glob", String(params.glob));
|
|
84
|
-
if (Number.isInteger(params?.limit) && params.limit > 0) args.push("--max-count", String(Math.min(params.limit, 2000)));
|
|
113
|
+
pushGrepFlags(args, pattern, params);
|
|
85
114
|
args.push("--", pattern, searchPath);
|
|
86
115
|
|
|
87
116
|
return args;
|
|
88
117
|
}
|
|
89
118
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const stat = await fs.stat(searchDir).catch(() => null);
|
|
93
|
-
const pendingAbs = pendingInScope(searchDir, pendingPaths);
|
|
94
|
-
const pending = pendingAbs.map(file => relativeSlash(cwd, file));
|
|
119
|
+
function globMatcher(pattern) {
|
|
120
|
+
if (!pattern) return null;
|
|
95
121
|
|
|
96
|
-
|
|
122
|
+
try { return globToRegExp(pattern); }
|
|
123
|
+
catch { return /^$/; }
|
|
124
|
+
}
|
|
97
125
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
126
|
+
function isDirectList(stat, pendingLength) {
|
|
127
|
+
return !!(stat?.isFile() || (!stat?.isDirectory() && pendingLength));
|
|
128
|
+
}
|
|
102
129
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const rows = [...new Set([...(rel ? [rel] : []), ...pending])].filter(file => !matcher || matcher.test(file));
|
|
130
|
+
function listDirectRows(stat, searchDir, cwd, pending, matcher) {
|
|
131
|
+
const rel = stat?.isFile() ? relativeSlash(cwd, searchDir) : null;
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
return [...new Set([...(rel ? [rel] : []), ...pending])].filter(file => !matcher || matcher.test(file));
|
|
134
|
+
}
|
|
109
135
|
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const rows = [...new Set([...diskRows, ...pendingMerged])];
|
|
136
|
+
function mergeListStdout(stdout, cwd, pendingMerged) {
|
|
137
|
+
const diskRows = String(stdout || "").split("\n").filter(Boolean)
|
|
138
|
+
.map(row => relativeSlash(cwd, path.isAbsolute(row) ? row : path.resolve(cwd, row)));
|
|
139
|
+
const rows = [...new Set([...diskRows, ...pendingMerged])];
|
|
115
140
|
|
|
116
|
-
|
|
117
|
-
|
|
141
|
+
return rows.length ? rows.join("\n") + "\n" : "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function listDisk(searchDir, pattern, cwd, signal) {
|
|
118
145
|
const args = ["--files"];
|
|
119
146
|
|
|
120
147
|
if (pattern) args.push("-g", pattern);
|
|
121
148
|
const res = await runCommand(["rg", ...args, searchDir], { cwd, timeoutMs: 30_000, signal }).catch(() => null);
|
|
122
149
|
|
|
123
|
-
if (res && (res.exitCode === 0 || res.exitCode === 1)) return
|
|
150
|
+
if (res && (res.exitCode === 0 || res.exitCode === 1)) return { stdout: res.stdout, via: "rg", outputTruncated: res.outputTruncated === true };
|
|
124
151
|
const findArgs = [searchDir];
|
|
125
152
|
|
|
126
153
|
if (pattern) findArgs.push("-name", pattern);
|
|
127
154
|
const findRes = await runCommand(["find", ...findArgs], { cwd, timeoutMs: 30_000, signal });
|
|
128
155
|
|
|
129
|
-
return
|
|
156
|
+
return { stdout: findRes.stdout, via: "find", outputTruncated: findRes.outputTruncated === true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** rg --files, then find(1) when rg is unavailable; both accept an optional glob/name pattern. */
|
|
160
|
+
export async function listWithTools(searchDir, pattern, cwd, signal, pendingPaths = []) {
|
|
161
|
+
const stat = await fs.stat(searchDir).catch(() => null);
|
|
162
|
+
const pendingAbs = pendingInScope(searchDir, pendingPaths);
|
|
163
|
+
const pending = pendingAbs.map(file => relativeSlash(cwd, file));
|
|
164
|
+
const matcher = globMatcher(pattern);
|
|
165
|
+
|
|
166
|
+
if (isDirectList(stat, pending.length)) {
|
|
167
|
+
const rows = listDirectRows(stat, searchDir, cwd, pending, matcher);
|
|
168
|
+
|
|
169
|
+
return textResult(rows.length ? rows.join("\n") + "\n" : "", { via: pending.length ? "vfs" : "file" });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const pendingMerged = pendingAbs.filter((_, i) => !matcher || matcher.test(pending[i]));
|
|
173
|
+
const listed = await listDisk(searchDir, pattern, cwd, signal);
|
|
174
|
+
|
|
175
|
+
return textResult(mergeListStdout(listed.stdout, cwd, pendingMerged), { via: listed.via, outputTruncated: listed.outputTruncated });
|
|
130
176
|
}
|
|
131
177
|
|
|
132
178
|
const GLOB_CHARS = /[*?[\]{}]/;
|
|
@@ -153,6 +199,23 @@ export async function fuzzyFind(index, root, cwd, pattern, limit = 20, pendingPa
|
|
|
153
199
|
return rows.map((r) => r.path).join("\n") + "\n";
|
|
154
200
|
}
|
|
155
201
|
|
|
202
|
+
function applyGlob(files, params, cwd) {
|
|
203
|
+
if (!params?.glob) return files;
|
|
204
|
+
const matcher = globToRegExp(String(params.glob));
|
|
205
|
+
|
|
206
|
+
return files.filter((f) => matcher.test(relativeSlash(cwd, f)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function filesReadable(index, files, overlayText) {
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
const overlay = overlayText(file);
|
|
212
|
+
|
|
213
|
+
if (overlay === undefined && index.entry(file) === null) return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
156
219
|
/** fff-style grep: smart-case, definition lines first, fuzzy fallback when the literal has no hits. */
|
|
157
220
|
export async function grepIndexed(index, pattern, params, searchPath, cwd, overlayText = () => undefined, pendingPaths = []) {
|
|
158
221
|
const compiled = grepRegex(pattern, params);
|
|
@@ -162,17 +225,9 @@ export async function grepIndexed(index, pattern, params, searchPath, cwd, overl
|
|
|
162
225
|
let files = [...new Set([...await candidateFileList(index, searchPath), ...pendingInScope(searchPath, pendingPaths)])];
|
|
163
226
|
|
|
164
227
|
if (!index.canScan(files)) return null;
|
|
228
|
+
files = applyGlob(files, params, cwd);
|
|
165
229
|
|
|
166
|
-
if (
|
|
167
|
-
const matcher = globToRegExp(String(params.glob));
|
|
168
|
-
files = files.filter((f) => matcher.test(relativeSlash(cwd, f)));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
for (const file of files) {
|
|
172
|
-
const overlay = overlayText(file);
|
|
173
|
-
|
|
174
|
-
if (overlay === undefined && index.entry(file) === null) return null;
|
|
175
|
-
}
|
|
230
|
+
if (!filesReadable(index, files, overlayText)) return null;
|
|
176
231
|
const rows = index.grepRows(files, regex, cwd, overlayText);
|
|
177
232
|
const fallback = rows.length === 0 && /^[\w$.-]{4,}$/.test(pattern) ? fuzzyGrepRows(index, files, pattern, cwd, caseSensitive, overlayText) : rows;
|
|
178
233
|
|
|
@@ -193,35 +248,45 @@ function grepRegex(pattern, params) {
|
|
|
193
248
|
}
|
|
194
249
|
}
|
|
195
250
|
|
|
251
|
+
function overlaySearchEntry(index, filePath, overlayText) {
|
|
252
|
+
const pending = overlayText(filePath);
|
|
253
|
+
|
|
254
|
+
return pending === undefined
|
|
255
|
+
? index.entry(filePath)
|
|
256
|
+
: Buffer.byteLength(pending, "utf8") <= 512 * 1024 ? WorkspaceIndex.fromText(filePath, pending) : null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function fuzzyLineRow(pattern, rawLine, defName, rel, line, maxTypos, caseSensitive) {
|
|
260
|
+
const m = fuzzyMatch(pattern, rawLine, { maxTypos, caseSensitive });
|
|
261
|
+
|
|
262
|
+
if (!m || m.end - m.start > pattern.length + 2) return null;
|
|
263
|
+
|
|
264
|
+
return { rel, line, text: rawLine, def: defName !== "" && fuzzyMatch(pattern, defName, { maxTypos }) !== null };
|
|
265
|
+
}
|
|
266
|
+
|
|
196
267
|
/** Zero literal hits: retry each line fuzzily (1 typo, 2 for long names) within a tight span, so IsOffTheRecord finds is_off_the_record. */
|
|
197
268
|
function fuzzyGrepRows(index, files, pattern, cwd, caseSensitive, overlayText = () => undefined) {
|
|
198
269
|
const maxTypos = pattern.length >= 8 ? 2 : 1;
|
|
199
270
|
const rows = [];
|
|
200
271
|
|
|
201
272
|
for (const filePath of files) {
|
|
202
|
-
const
|
|
203
|
-
const e = pending === undefined
|
|
204
|
-
? index.entry(filePath)
|
|
205
|
-
: Buffer.byteLength(pending, "utf8") <= 512 * 1024 ? WorkspaceIndex.fromText(filePath, pending) : null;
|
|
273
|
+
const e = overlaySearchEntry(index, filePath, overlayText);
|
|
206
274
|
|
|
207
275
|
if (!e) continue;
|
|
208
276
|
const { raw, defNames } = WorkspaceIndex.linesOf(e);
|
|
209
277
|
const rel = relativeSlash(cwd, filePath);
|
|
210
278
|
|
|
211
279
|
for (let i = 0; i < raw.length && rows.length <= 400; i++) {
|
|
212
|
-
const
|
|
280
|
+
const row = fuzzyLineRow(pattern, raw[i], defNames[i], rel, i + 1, maxTypos, caseSensitive);
|
|
213
281
|
|
|
214
|
-
if (
|
|
215
|
-
rows.push({ rel, line: i + 1, text: raw[i], def: defNames[i] !== "" && fuzzyMatch(pattern, defNames[i], { maxTypos }) !== null });
|
|
282
|
+
if (row) rows.push(row);
|
|
216
283
|
}
|
|
217
284
|
}
|
|
218
285
|
|
|
219
286
|
return rows;
|
|
220
287
|
}
|
|
221
288
|
|
|
222
|
-
|
|
223
|
-
function formatGrepRows(rows, limit) {
|
|
224
|
-
if (rows.length === 0) return "";
|
|
289
|
+
function groupGrepRows(rows) {
|
|
225
290
|
const groups = new Map();
|
|
226
291
|
|
|
227
292
|
for (const r of rows) {
|
|
@@ -229,19 +294,34 @@ function formatGrepRows(rows, limit) {
|
|
|
229
294
|
groups.get(r.rel).push(r);
|
|
230
295
|
}
|
|
231
296
|
|
|
232
|
-
|
|
297
|
+
return [...groups.values()].sort((a, b) => Number(b.some((r) => r.def)) - Number(a.some((r) => r.def)));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function formatGroup(group, limit, shown) {
|
|
301
|
+
let out = group[0].rel + "\n";
|
|
302
|
+
group.sort((a, b) => Number(b.def) - Number(a.def) || a.line - b.line);
|
|
303
|
+
let n = shown;
|
|
304
|
+
|
|
305
|
+
for (const r of group) {
|
|
306
|
+
if (n++ >= limit) break;
|
|
307
|
+
out += " " + r.line + (r.def ? "*" : ":") + " " + r.text.trim() + "\n";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { out, shown: n };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** fff definition-first hinting: files that declare the name come first, declarations first within a file; one header per file. */
|
|
314
|
+
function formatGrepRows(rows, limit) {
|
|
315
|
+
if (rows.length === 0) return "";
|
|
316
|
+
const files = groupGrepRows(rows);
|
|
233
317
|
let out = "";
|
|
234
318
|
let shown = 0;
|
|
235
319
|
|
|
236
320
|
for (const group of files) {
|
|
237
321
|
if (shown >= limit) break;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
for (const r of group) {
|
|
242
|
-
if (shown++ >= limit) break;
|
|
243
|
-
out += " " + r.line + (r.def ? "*" : ":") + " " + r.text.trim() + "\n";
|
|
244
|
-
}
|
|
322
|
+
const next = formatGroup(group, limit, shown);
|
|
323
|
+
out += next.out;
|
|
324
|
+
shown = next.shown;
|
|
245
325
|
}
|
|
246
326
|
|
|
247
327
|
if (rows.length > limit) out += "… " + (rows.length - limit) + " more matches (pass limit or narrow the pattern)\n";
|