pi-ast-sgrep 1.3.2 → 2.0.0
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 +212 -18
- package/dist/code-mode.d.ts +89 -0
- package/dist/code-mode.js +432 -0
- package/dist/codemode/connector.d.ts +92 -0
- package/dist/codemode/connector.js +79 -0
- package/dist/codemode/dispatch.d.ts +83 -0
- package/dist/codemode/dispatch.js +320 -0
- package/dist/codemode/index.d.ts +18 -0
- package/dist/codemode/index.js +18 -0
- package/dist/codemode/native.d.ts +55 -0
- package/dist/codemode/native.js +119 -0
- package/dist/codemode/runner.d.ts +37 -0
- package/dist/codemode/runner.js +242 -0
- package/dist/codemode/sandbox-worker.d.ts +1 -0
- package/dist/codemode/sandbox-worker.js +204 -0
- package/dist/codemode/session-pool.d.ts +40 -0
- package/dist/codemode/session-pool.js +238 -0
- package/dist/codemode/types.d.ts +18 -0
- package/dist/codemode/types.js +21 -0
- package/dist/codemode/worker.d.ts +29 -0
- package/dist/codemode/worker.js +307 -0
- package/dist/index.d.ts +31 -3
- package/dist/index.js +393 -21
- package/dist/present.d.ts +67 -0
- package/dist/present.js +166 -0
- package/dist/runtime.d.ts +29 -2
- package/dist/runtime.js +436 -148
- package/native/.gitignore +3 -0
- package/native/README.md +17 -0
- package/package.json +26 -11
- package/skills/ast-sgrep/SKILL.md +0 -36
- package/skills/ast-sgrep/references/query-guide.md +0 -21
package/dist/runtime.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { constants, accessSync, existsSync, realpathSync } from "node:fs";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { constants, accessSync, existsSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
|
|
4
3
|
import { DatabaseSync } from "node:sqlite";
|
|
5
4
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
5
|
import { resolveBinary } from "ast-sgrep";
|
|
7
|
-
export const RUNTIME_VERSION = "
|
|
6
|
+
export const RUNTIME_VERSION = "2.0.0";
|
|
8
7
|
export const MACHINE_SCHEMA_VERSION = "1.0.0";
|
|
9
8
|
export const CONFIG_SCHEMA_VERSION = 1;
|
|
10
|
-
export const INDEX_FORMAT_VERSION =
|
|
9
|
+
export const INDEX_FORMAT_VERSION = 12;
|
|
11
10
|
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
12
11
|
export const DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
13
12
|
export const DEFAULT_REFRESH_INTERVAL_MS = 30_000;
|
|
13
|
+
const MAX_TARGETED_INDEX_PATHS = 1_024;
|
|
14
14
|
const RESOLVED_ROOT = Symbol("resolvedRoot");
|
|
15
15
|
export class RuntimeError extends Error {
|
|
16
16
|
code;
|
|
@@ -36,6 +36,11 @@ function sameSetting(current, legacy, currentName, legacyName) {
|
|
|
36
36
|
}
|
|
37
37
|
return current ?? legacy;
|
|
38
38
|
}
|
|
39
|
+
const LEGACY_NUMBER_FIELDS = [
|
|
40
|
+
["timeoutMs", "timeout"],
|
|
41
|
+
["maxOutputBytes", "maxOutput"],
|
|
42
|
+
["refreshIntervalMs", "refreshInterval"],
|
|
43
|
+
];
|
|
39
44
|
/** Convert schema 0/unversioned settings without mutating the rollback source. */
|
|
40
45
|
export function migrateConfig(input = {}) {
|
|
41
46
|
const value = { ...input };
|
|
@@ -47,33 +52,26 @@ export function migrateConfig(input = {}) {
|
|
|
47
52
|
return value;
|
|
48
53
|
const legacy = value;
|
|
49
54
|
const migrated = { ...legacy, schemaVersion: CONFIG_SCHEMA_VERSION };
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
migrated
|
|
57
|
-
|
|
58
|
-
migrated.refreshIntervalMs = refreshIntervalMs;
|
|
59
|
-
delete migrated.timeout;
|
|
60
|
-
delete migrated.maxOutput;
|
|
61
|
-
delete migrated.refreshInterval;
|
|
55
|
+
for (const [currentName, legacyName] of LEGACY_NUMBER_FIELDS) {
|
|
56
|
+
const next = sameSetting(value[currentName], legacy[legacyName], currentName, legacyName);
|
|
57
|
+
if (next !== undefined)
|
|
58
|
+
migrated[currentName] = next;
|
|
59
|
+
}
|
|
60
|
+
for (const [, legacyName] of LEGACY_NUMBER_FIELDS) {
|
|
61
|
+
delete migrated[legacyName];
|
|
62
|
+
}
|
|
62
63
|
return migrated;
|
|
63
64
|
}
|
|
64
65
|
/** Serialize current settings for a schema-0 rollback without mutating the current value. */
|
|
65
66
|
export function rollbackConfig(input) {
|
|
66
67
|
const current = migrateConfig(input);
|
|
67
68
|
const legacy = { ...current, schemaVersion: 0 };
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
delete legacy.timeoutMs;
|
|
75
|
-
delete legacy.maxOutputBytes;
|
|
76
|
-
delete legacy.refreshIntervalMs;
|
|
69
|
+
for (const [currentName, legacyName] of LEGACY_NUMBER_FIELDS) {
|
|
70
|
+
const value = current[currentName];
|
|
71
|
+
if (value !== undefined)
|
|
72
|
+
legacy[legacyName] = value;
|
|
73
|
+
delete legacy[currentName];
|
|
74
|
+
}
|
|
77
75
|
return legacy;
|
|
78
76
|
}
|
|
79
77
|
function envConfig(env = {}) {
|
|
@@ -112,9 +110,10 @@ export function resolveConfig(sources = {}) {
|
|
|
112
110
|
merged.schemaVersion = CONFIG_SCHEMA_VERSION;
|
|
113
111
|
return merged;
|
|
114
112
|
}
|
|
115
|
-
function
|
|
113
|
+
function pathContained(parent, child) {
|
|
116
114
|
const rel = relative(parent, child);
|
|
117
|
-
|
|
115
|
+
const first = rel.split(/[\\/]/u, 1)[0];
|
|
116
|
+
return rel === "" || (!isAbsolute(rel) && first !== "..");
|
|
118
117
|
}
|
|
119
118
|
export async function resolveRuntimeRoot(projectCwd, requestedRoot, allowOutsideProject = false) {
|
|
120
119
|
let project;
|
|
@@ -126,7 +125,7 @@ export async function resolveRuntimeRoot(projectCwd, requestedRoot, allowOutside
|
|
|
126
125
|
catch (cause) {
|
|
127
126
|
throw new RuntimeError("INVALID_ROOT", "Project or requested root does not exist", { projectCwd, requestedRoot, cause: cause instanceof Error ? cause.message : String(cause) });
|
|
128
127
|
}
|
|
129
|
-
if (!allowOutsideProject && !
|
|
128
|
+
if (!allowOutsideProject && !pathContained(project, candidate)) {
|
|
130
129
|
throw new RuntimeError("ROOT_OUTSIDE_PROJECT", "Requested root resolves outside the project", { project, requestedRoot, resolvedRoot: candidate });
|
|
131
130
|
}
|
|
132
131
|
return candidate;
|
|
@@ -134,7 +133,7 @@ export async function resolveRuntimeRoot(projectCwd, requestedRoot, allowOutside
|
|
|
134
133
|
function record(value) {
|
|
135
134
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
136
135
|
}
|
|
137
|
-
function indexHealth(status) {
|
|
136
|
+
function indexHealth(status, knownExisting = false) {
|
|
138
137
|
const index = record(status.index);
|
|
139
138
|
const state = typeof index?.status === "string" ? index.status :
|
|
140
139
|
typeof status.index_status === "string" ? status.index_status : undefined;
|
|
@@ -145,7 +144,7 @@ function indexHealth(status) {
|
|
|
145
144
|
if (state === "ready" || state === "current" || index?.exists === true || status.indexed === true)
|
|
146
145
|
return "ready";
|
|
147
146
|
if (typeof status.index_path === "string" && typeof status.file_count === "number") {
|
|
148
|
-
return status.file_count
|
|
147
|
+
return knownExisting || status.file_count > 0 ? "ready" : "missing";
|
|
149
148
|
}
|
|
150
149
|
throw new RuntimeError("INDEX_STATUS_UNKNOWN", "ast-sgrep status did not report index freshness", { index: status.index, index_status: status.index_status });
|
|
151
150
|
}
|
|
@@ -155,13 +154,63 @@ function incompatibleStatusFailure(cause) {
|
|
|
155
154
|
const text = `${cause.message} ${JSON.stringify(cause.details)}`;
|
|
156
155
|
return /incompatib|unsupported.{0,24}schema|schema.{0,24}(version|mismatch)/i.test(text);
|
|
157
156
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
/** Probe compatibility hook then status; map incompat operational failures to health. */
|
|
158
|
+
async function probeIndexHealth(runtime, rootContext, options) {
|
|
159
|
+
const hinted = await runtime.inspectIndexCompatibility?.(rootContext);
|
|
160
|
+
if (hinted === "missing" || hinted === "incompatible")
|
|
161
|
+
return hinted;
|
|
162
|
+
try {
|
|
163
|
+
const status = runtime.nativeCall
|
|
164
|
+
? await runtime.nativeCall("index_status", {}, rootContext, options)
|
|
165
|
+
: await runtime.run(["status", ".", "--json"], rootContext, options);
|
|
166
|
+
return indexHealth(status, hinted === "ready");
|
|
167
|
+
}
|
|
168
|
+
catch (cause) {
|
|
169
|
+
if (!incompatibleStatusFailure(cause))
|
|
170
|
+
throw cause;
|
|
171
|
+
return "incompatible";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function indexCompletion(response, requireWalkErrors) {
|
|
175
|
+
const stats = record(response.stats) ?? response;
|
|
176
|
+
const failed = stats.files_failed;
|
|
177
|
+
const walkErrors = stats.walk_errors;
|
|
178
|
+
if (!Number.isSafeInteger(failed) || failed < 0
|
|
179
|
+
|| (requireWalkErrors ? typeof walkErrors !== "boolean" : walkErrors !== undefined && typeof walkErrors !== "boolean")) {
|
|
180
|
+
throw new RuntimeError("INDEX_RESPONSE_INVALID", "ast-sgrep index response omitted valid completion status", { filesFailed: failed, walkErrors, requireWalkErrors });
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
failed: failed,
|
|
184
|
+
walkErrors: walkErrors === true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Run index_repo via native sticky pool or CLI argv. force=true → reindex. */
|
|
188
|
+
async function runIndex(runtime, force, rootContext, options) {
|
|
189
|
+
const response = runtime.nativeCall
|
|
190
|
+
? await runtime.nativeCall("index_repo", { force }, rootContext, options)
|
|
191
|
+
: await runtime.run([force ? "reindex" : "index", ".", "--json"], rootContext, options);
|
|
192
|
+
const { failed, walkErrors } = indexCompletion(response, true);
|
|
193
|
+
if (failed > 0 || walkErrors) {
|
|
194
|
+
throw new RuntimeError("INDEX_UPDATE_INCOMPLETE", "ast-sgrep did not complete the full index reconciliation", { failed, walkErrors, force });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Update known changed paths without walking the repository. */
|
|
198
|
+
async function runTargetedIndex(runtime, paths, rootContext, options) {
|
|
199
|
+
for (let offset = 0; offset < paths.length; offset += MAX_TARGETED_INDEX_PATHS) {
|
|
200
|
+
const chunk = paths.slice(offset, offset + MAX_TARGETED_INDEX_PATHS);
|
|
201
|
+
const response = runtime.nativeCall
|
|
202
|
+
? await runtime.nativeCall("index_repo", { paths: chunk }, rootContext, options)
|
|
203
|
+
: await runtime.run(["index", ".", "--json", ...chunk.flatMap((path) => ["--path", path])], rootContext, options);
|
|
204
|
+
const { failed } = indexCompletion(response, false);
|
|
205
|
+
if (failed > 0) {
|
|
206
|
+
throw new RuntimeError("INDEX_UPDATE_INCOMPLETE", `ast-sgrep failed to update ${failed} changed path${failed === 1 ? "" : "s"}`, { failed, pathCount: chunk.length });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
161
209
|
}
|
|
162
210
|
function canonicalizeAffectedPath(path) {
|
|
163
|
-
const
|
|
164
|
-
|
|
211
|
+
const absolute = resolve(path);
|
|
212
|
+
const unresolved = [basename(absolute)];
|
|
213
|
+
let existing = dirname(absolute);
|
|
165
214
|
for (;;) {
|
|
166
215
|
try {
|
|
167
216
|
return resolve(realpathSync(existing), ...unresolved.reverse());
|
|
@@ -176,96 +225,296 @@ function canonicalizeAffectedPath(path) {
|
|
|
176
225
|
}
|
|
177
226
|
}
|
|
178
227
|
}
|
|
228
|
+
function canonicalizeRootPath(path) {
|
|
229
|
+
try {
|
|
230
|
+
return realpathSync(resolve(path));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return canonicalizeAffectedPath(path);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function changesIgnoreRules(path) {
|
|
237
|
+
const name = basename(path);
|
|
238
|
+
return name === ".gitignore" || name === ".ignore" || name === ".asgrepignore";
|
|
239
|
+
}
|
|
240
|
+
function ignoredIndexWrite(root, path, indexPath) {
|
|
241
|
+
const defaultIndexDirectory = join(root, ".asgrep");
|
|
242
|
+
if (pathContained(defaultIndexDirectory, path))
|
|
243
|
+
return true;
|
|
244
|
+
const indexDirectory = dirname(indexPath);
|
|
245
|
+
if (dirname(path) !== indexDirectory)
|
|
246
|
+
return false;
|
|
247
|
+
const name = basename(path);
|
|
248
|
+
const sqliteArtifact = (database) => {
|
|
249
|
+
const suffix = name.slice(database.length);
|
|
250
|
+
return name.startsWith(database) && (suffix === ""
|
|
251
|
+
|| suffix === "-wal"
|
|
252
|
+
|| suffix === "-shm"
|
|
253
|
+
|| suffix === "-journal"
|
|
254
|
+
|| suffix === ".reindex.lock"
|
|
255
|
+
|| /^\.corrupt(?:\.\d+)?(?:-(?:wal|shm|journal))?$/u.test(suffix));
|
|
256
|
+
};
|
|
257
|
+
return sqliteArtifact(basename(indexPath))
|
|
258
|
+
|| sqliteArtifact("lexical.db")
|
|
259
|
+
|| name === "semantic.ivf"
|
|
260
|
+
|| (name.startsWith(".semantic.ivf.") && name.endsWith(".tmp"));
|
|
261
|
+
}
|
|
262
|
+
function existingDirectory(path) {
|
|
263
|
+
try {
|
|
264
|
+
return statSync(path).isDirectory();
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function markStatePathDirty(state, path) {
|
|
271
|
+
state.dirtyGeneration += 1;
|
|
272
|
+
if (changesIgnoreRules(path)) {
|
|
273
|
+
state.dirtyPaths.clear();
|
|
274
|
+
state.fullScanRequired = true;
|
|
275
|
+
}
|
|
276
|
+
else if (!state.fullScanRequired) {
|
|
277
|
+
if (!state.dirtyPaths.has(path) && state.dirtyPaths.size >= MAX_TARGETED_INDEX_PATHS) {
|
|
278
|
+
state.dirtyPaths.clear();
|
|
279
|
+
state.fullScanRequired = true;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
state.dirtyPaths.add(path);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function markStateFullScan(state) {
|
|
287
|
+
state.dirtyGeneration += 1;
|
|
288
|
+
state.dirtyPaths.clear();
|
|
289
|
+
state.fullScanRequired = true;
|
|
290
|
+
}
|
|
291
|
+
function cancelledRefreshWait() {
|
|
292
|
+
return new RuntimeError("CANCELLED", "ast-sgrep freshness wait was cancelled");
|
|
293
|
+
}
|
|
294
|
+
/** Stop one caller waiting without transferring cancellation ownership to shared work. */
|
|
295
|
+
function waitForRefresh(refresh, signal) {
|
|
296
|
+
if (!signal)
|
|
297
|
+
return refresh;
|
|
298
|
+
if (signal.aborted)
|
|
299
|
+
return Promise.reject(cancelledRefreshWait());
|
|
300
|
+
return new Promise((resolveWait, rejectWait) => {
|
|
301
|
+
const onAbort = () => {
|
|
302
|
+
signal.removeEventListener("abort", onAbort);
|
|
303
|
+
rejectWait(cancelledRefreshWait());
|
|
304
|
+
};
|
|
305
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
306
|
+
refresh.then(() => {
|
|
307
|
+
signal.removeEventListener("abort", onAbort);
|
|
308
|
+
resolveWait();
|
|
309
|
+
}, (cause) => {
|
|
310
|
+
signal.removeEventListener("abort", onAbort);
|
|
311
|
+
rejectWait(cause);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
179
315
|
export class FreshnessCoordinator {
|
|
180
316
|
#states = new Map();
|
|
181
|
-
#
|
|
317
|
+
#pending = new Map();
|
|
182
318
|
#interval;
|
|
183
319
|
#now;
|
|
320
|
+
#watchFactory;
|
|
184
321
|
constructor(options = {}) {
|
|
185
322
|
this.#interval = finitePositive(options.refreshIntervalMs, DEFAULT_REFRESH_INTERVAL_MS, "refreshIntervalMs");
|
|
186
323
|
this.#now = options.now ?? Date.now;
|
|
324
|
+
this.#watchFactory = options.watchFactory ?? watch;
|
|
187
325
|
}
|
|
188
326
|
markAffectedPath(path, cwd) {
|
|
189
327
|
const affected = canonicalizeAffectedPath(isAbsolute(path) ? path : resolve(canonicalizeAffectedPath(cwd), path));
|
|
190
|
-
|
|
328
|
+
let matched = false;
|
|
191
329
|
for (const [root, state] of this.#states) {
|
|
192
|
-
if (pathContained(root, affected))
|
|
193
|
-
|
|
330
|
+
if (!pathContained(root, affected))
|
|
331
|
+
continue;
|
|
332
|
+
markStatePathDirty(state, affected);
|
|
333
|
+
matched = true;
|
|
334
|
+
}
|
|
335
|
+
if (!matched) {
|
|
336
|
+
const pendingRoot = canonicalizeRootPath(cwd);
|
|
337
|
+
// Before root resolution, the caller's cwd is the only trustworthy
|
|
338
|
+
// confinement boundary. Do not retain unrelated/escaping paths forever.
|
|
339
|
+
if (!pathContained(pendingRoot, affected))
|
|
340
|
+
return;
|
|
341
|
+
let pending = this.#pending.get(pendingRoot);
|
|
342
|
+
if (!pending) {
|
|
343
|
+
pending = { paths: new Set(), fullScanRequired: false, consumedFullScanRoots: new Set() };
|
|
344
|
+
this.#pending.set(pendingRoot, pending);
|
|
345
|
+
}
|
|
346
|
+
if (changesIgnoreRules(affected)) {
|
|
347
|
+
pending.paths.clear();
|
|
348
|
+
pending.fullScanRequired = true;
|
|
349
|
+
}
|
|
350
|
+
else if (!pending.fullScanRequired) {
|
|
351
|
+
if (!pending.paths.has(affected) && pending.paths.size >= MAX_TARGETED_INDEX_PATHS) {
|
|
352
|
+
pending.paths.clear();
|
|
353
|
+
pending.fullScanRequired = true;
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
pending.paths.add(affected);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
194
359
|
}
|
|
195
360
|
}
|
|
196
361
|
markRootDirty(root) {
|
|
197
|
-
const canonical =
|
|
362
|
+
const canonical = canonicalizeRootPath(root);
|
|
198
363
|
const state = this.#states.get(canonical);
|
|
199
|
-
if (state)
|
|
200
|
-
state
|
|
201
|
-
|
|
202
|
-
|
|
364
|
+
if (state) {
|
|
365
|
+
markStateFullScan(state);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
this.#pending.set(canonical, {
|
|
369
|
+
paths: new Set(),
|
|
370
|
+
fullScanRequired: true,
|
|
371
|
+
consumedFullScanRoots: new Set(),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
203
374
|
}
|
|
204
375
|
async ensureFresh(runtime, context, options = {}) {
|
|
205
376
|
const root = await runtime.resolveRoot(context);
|
|
206
377
|
const rootContext = { cwd: root, [RESOLVED_ROOT]: true };
|
|
207
378
|
let state = this.#states.get(root);
|
|
208
379
|
if (!state) {
|
|
209
|
-
state = {
|
|
380
|
+
state = {
|
|
381
|
+
dirtyGeneration: 0,
|
|
382
|
+
cleanGeneration: 0,
|
|
383
|
+
dirtyPaths: new Set(),
|
|
384
|
+
fullScanRequired: false,
|
|
385
|
+
initialized: false,
|
|
386
|
+
lastRefreshAt: 0,
|
|
387
|
+
inFlight: undefined,
|
|
388
|
+
watcher: undefined,
|
|
389
|
+
};
|
|
210
390
|
this.#states.set(root, state);
|
|
211
391
|
}
|
|
212
|
-
|
|
213
|
-
|
|
392
|
+
if (runtime.watchExternalChanges && state.watcher === undefined) {
|
|
393
|
+
const indexPath = canonicalizeAffectedPath(runtime.resolveIndexPath?.(root) ?? join(root, ".asgrep", "index.db"));
|
|
394
|
+
this.#startWatcher(root, state, indexPath);
|
|
395
|
+
}
|
|
396
|
+
for (const [pendingRoot, pending] of this.#pending) {
|
|
397
|
+
if (!pathContained(pendingRoot, root) && !pathContained(root, pendingRoot))
|
|
398
|
+
continue;
|
|
399
|
+
if (pending.fullScanRequired) {
|
|
400
|
+
if (!pending.consumedFullScanRoots.has(root)) {
|
|
401
|
+
markStateFullScan(state);
|
|
402
|
+
pending.consumedFullScanRoots.add(root);
|
|
403
|
+
}
|
|
214
404
|
continue;
|
|
215
|
-
|
|
216
|
-
|
|
405
|
+
}
|
|
406
|
+
for (const path of pending.paths) {
|
|
407
|
+
if (!pathContained(root, path))
|
|
408
|
+
continue;
|
|
409
|
+
markStatePathDirty(state, path);
|
|
410
|
+
pending.paths.delete(path);
|
|
411
|
+
}
|
|
412
|
+
if (pending.paths.size === 0)
|
|
413
|
+
this.#pending.delete(pendingRoot);
|
|
217
414
|
}
|
|
218
415
|
if (state.inFlight) {
|
|
219
|
-
await state.inFlight;
|
|
416
|
+
await waitForRefresh(state.inFlight, options.signal);
|
|
220
417
|
return this.ensureFresh(runtime, rootContext, options);
|
|
221
418
|
}
|
|
222
419
|
const now = this.#now();
|
|
223
420
|
const elapsed = now - state.lastRefreshAt;
|
|
421
|
+
// Lease expiry: initialized and interval elapsed (or clock went backwards).
|
|
224
422
|
const expired = state.initialized && (elapsed < 0 || elapsed >= this.#interval);
|
|
225
423
|
if (state.initialized && state.cleanGeneration === state.dirtyGeneration && !expired)
|
|
226
424
|
return root;
|
|
227
425
|
const refreshGeneration = state.dirtyGeneration;
|
|
228
|
-
const
|
|
426
|
+
const refreshPaths = [...state.dirtyPaths];
|
|
427
|
+
const fullScanRequired = state.fullScanRequired;
|
|
428
|
+
// Correctness work belongs to the root, not to whichever request happened
|
|
429
|
+
// to start it. Individual callers may stop waiting, but cannot cancel the
|
|
430
|
+
// shared refresh while other callers depend on it.
|
|
431
|
+
const sharedOptions = {};
|
|
432
|
+
if (options.timeoutMs !== undefined)
|
|
433
|
+
sharedOptions.timeoutMs = options.timeoutMs;
|
|
434
|
+
if (options.env !== undefined)
|
|
435
|
+
sharedOptions.env = options.env;
|
|
229
436
|
const refresh = (async () => {
|
|
230
|
-
|
|
231
|
-
if (health !== "incompatible") {
|
|
232
|
-
try {
|
|
233
|
-
const status = await runtime.run(["status", ".", "--json"], rootContext, options);
|
|
234
|
-
health = indexHealth(status);
|
|
235
|
-
}
|
|
236
|
-
catch (cause) {
|
|
237
|
-
if (!incompatibleStatusFailure(cause))
|
|
238
|
-
throw cause;
|
|
239
|
-
health = "incompatible";
|
|
240
|
-
}
|
|
241
|
-
}
|
|
437
|
+
const health = await probeIndexHealth(runtime, rootContext, sharedOptions);
|
|
242
438
|
const dirty = refreshGeneration > state.cleanGeneration;
|
|
243
439
|
if (health === "incompatible") {
|
|
440
|
+
// Requisite variety: force rebuild path (hook or reindex).
|
|
244
441
|
if (runtime.rebuildIncompatibleIndex)
|
|
245
|
-
await runtime.rebuildIncompatibleIndex(rootContext,
|
|
442
|
+
await runtime.rebuildIncompatibleIndex(rootContext, sharedOptions);
|
|
246
443
|
else
|
|
247
|
-
await runtime
|
|
444
|
+
await runIndex(runtime, true, rootContext, sharedOptions);
|
|
248
445
|
}
|
|
249
|
-
else if (health === "missing" || !
|
|
250
|
-
await runtime
|
|
446
|
+
else if (health === "missing" || !state.initialized || expired || (dirty && (fullScanRequired || refreshPaths.length === 0))) {
|
|
447
|
+
await runIndex(runtime, false, rootContext, sharedOptions);
|
|
251
448
|
}
|
|
252
|
-
else if (
|
|
253
|
-
|
|
254
|
-
// so external create/modify/delete are reconciled without rebuild thrash (5du.9).
|
|
255
|
-
await runtime.run(["index", ".", "--json"], rootContext, options);
|
|
449
|
+
else if (dirty) {
|
|
450
|
+
await runTargetedIndex(runtime, refreshPaths, rootContext, sharedOptions);
|
|
256
451
|
}
|
|
257
452
|
state.initialized = true;
|
|
258
453
|
state.cleanGeneration = refreshGeneration;
|
|
454
|
+
if (state.dirtyGeneration === refreshGeneration) {
|
|
455
|
+
state.dirtyPaths.clear();
|
|
456
|
+
state.fullScanRequired = false;
|
|
457
|
+
}
|
|
259
458
|
state.lastRefreshAt = this.#now();
|
|
260
459
|
})();
|
|
261
|
-
|
|
460
|
+
let tracked;
|
|
461
|
+
tracked = refresh.finally(() => {
|
|
462
|
+
if (state.inFlight === tracked)
|
|
463
|
+
state.inFlight = undefined;
|
|
464
|
+
});
|
|
465
|
+
state.inFlight = tracked;
|
|
466
|
+
// If every waiter is cancelled, the root-owned refresh still needs a
|
|
467
|
+
// rejection handler while it finishes in the background.
|
|
468
|
+
void tracked.catch(() => undefined);
|
|
469
|
+
await waitForRefresh(tracked, options.signal);
|
|
470
|
+
if (state.cleanGeneration !== state.dirtyGeneration) {
|
|
471
|
+
return this.ensureFresh(runtime, rootContext, options);
|
|
472
|
+
}
|
|
473
|
+
return root;
|
|
474
|
+
}
|
|
475
|
+
shutdown() {
|
|
476
|
+
for (const state of this.#states.values())
|
|
477
|
+
state.watcher?.close();
|
|
478
|
+
this.#states.clear();
|
|
479
|
+
this.#pending.clear();
|
|
480
|
+
}
|
|
481
|
+
#startWatcher(root, state, indexPath) {
|
|
482
|
+
if (!existsSync(root)) {
|
|
483
|
+
state.watcher = null;
|
|
484
|
+
markStateFullScan(state);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
262
487
|
try {
|
|
263
|
-
|
|
264
|
-
|
|
488
|
+
const watcher = this.#watchFactory(root, { recursive: true, persistent: false, encoding: "utf8" }, (eventType, filename) => {
|
|
489
|
+
if (!filename) {
|
|
490
|
+
markStateFullScan(state);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const affected = canonicalizeAffectedPath(join(root, filename));
|
|
494
|
+
if (ignoredIndexWrite(root, affected, indexPath))
|
|
495
|
+
return;
|
|
496
|
+
if (eventType === "rename" || existingDirectory(affected)) {
|
|
497
|
+
markStateFullScan(state);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
markStatePathDirty(state, affected);
|
|
501
|
+
});
|
|
502
|
+
watcher.on("error", () => {
|
|
503
|
+
watcher.close();
|
|
504
|
+
// Watcher errors (including backend overflow) make event history
|
|
505
|
+
// unknowable. Scan once, then rely on the periodic correctness lease;
|
|
506
|
+
// retrying a permanently broken watcher on every request hot-loops.
|
|
507
|
+
if (state.watcher === watcher)
|
|
508
|
+
state.watcher = null;
|
|
509
|
+
markStateFullScan(state);
|
|
510
|
+
});
|
|
511
|
+
state.watcher = watcher;
|
|
265
512
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
513
|
+
catch {
|
|
514
|
+
// Do one correctness scan now, then rely on periodic scans instead of
|
|
515
|
+
// retrying (and rescanning) on every query on unsupported filesystems.
|
|
516
|
+
state.watcher = null;
|
|
517
|
+
markStateFullScan(state);
|
|
269
518
|
}
|
|
270
519
|
}
|
|
271
520
|
}
|
|
@@ -291,27 +540,63 @@ function getBinary(config, env, resolver) {
|
|
|
291
540
|
return binary;
|
|
292
541
|
}
|
|
293
542
|
function byteLength(value) { return Buffer.byteLength(value, "utf8"); }
|
|
543
|
+
/** Present-field version identity checks. Pass `requireIdentity` for version --json. */
|
|
544
|
+
function assertVersionTriple(envelope, requireIdentity = false) {
|
|
545
|
+
// Compound guards (same short-circuit as nested if): check only when required or field present.
|
|
546
|
+
if ((requireIdentity || envelope.version !== undefined) && envelope.version !== RUNTIME_VERSION) {
|
|
547
|
+
throw new RuntimeError("VERSION_MISMATCH", "ast-sgrep binary version does not match the extension", { expected: RUNTIME_VERSION, actual: envelope.version });
|
|
548
|
+
}
|
|
549
|
+
if ((requireIdentity || envelope.machine_schema_version !== undefined) && envelope.machine_schema_version !== MACHINE_SCHEMA_VERSION) {
|
|
550
|
+
throw new RuntimeError("PROTOCOL_MISMATCH", "ast-sgrep binary reports an incompatible machine protocol", { expected: MACHINE_SCHEMA_VERSION, actual: envelope.machine_schema_version });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Nonzero CLI exit: prefer structured failed envelope (OPERATIONAL_ERROR), else PROCESS_FAILED.
|
|
555
|
+
* Always throws — error-path extract so parseEnvelope keeps success-path protocol field checks.
|
|
556
|
+
*/
|
|
557
|
+
function throwNonzeroProcessFailure(result, code) {
|
|
558
|
+
try {
|
|
559
|
+
const value = record(JSON.parse(result.stdout));
|
|
560
|
+
// Wire-valid ok:false asgrep envelope → structured operational failure (not PROCESS_FAILED).
|
|
561
|
+
if (value && value.tool === "asgrep" && value.schema_version === MACHINE_SCHEMA_VERSION && value.ok === false) {
|
|
562
|
+
const failure = record(value.error);
|
|
563
|
+
const message = typeof failure?.message === "string" ? failure.message : "ast-sgrep reported an operational failure";
|
|
564
|
+
throw new RuntimeError("OPERATIONAL_ERROR", message, { command: value.command, error: failure, exitCode: code });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (cause) {
|
|
568
|
+
if (cause instanceof RuntimeError)
|
|
569
|
+
throw cause;
|
|
570
|
+
}
|
|
571
|
+
throw new RuntimeError("PROCESS_FAILED", `ast-sgrep exited with code ${code}`, {
|
|
572
|
+
exitCode: code,
|
|
573
|
+
signal: result.signal ?? undefined,
|
|
574
|
+
stderr: result.stderr.slice(0, 1024),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/** Map exec failures (abort / timeout / generic) to RuntimeError. Re-throws RuntimeError as-is. */
|
|
578
|
+
function rethrowExecFailure(cause, options, timeout) {
|
|
579
|
+
if (cause instanceof RuntimeError)
|
|
580
|
+
throw cause;
|
|
581
|
+
if (options.signal?.aborted || (cause instanceof Error && cause.name === "AbortError")) {
|
|
582
|
+
throw new RuntimeError("CANCELLED", "ast-sgrep execution was cancelled");
|
|
583
|
+
}
|
|
584
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
585
|
+
if (/timeout|timed out/i.test(message)) {
|
|
586
|
+
throw new RuntimeError("TIMEOUT", `ast-sgrep exceeded ${timeout}ms`, { timeoutMs: timeout });
|
|
587
|
+
}
|
|
588
|
+
throw new RuntimeError("EXEC_FAILED", "Unable to execute ast-sgrep", { cause: message });
|
|
589
|
+
}
|
|
294
590
|
function parseEnvelope(result, limit) {
|
|
295
591
|
const stdoutBytes = byteLength(result.stdout);
|
|
296
592
|
const stderrBytes = byteLength(result.stderr);
|
|
297
|
-
|
|
593
|
+
// Byte lengths are non-negative: sum > limit covers either-side overflow and combined cap.
|
|
594
|
+
if (stdoutBytes + stderrBytes > limit) {
|
|
298
595
|
throw new RuntimeError("OUTPUT_LIMIT", "ast-sgrep output exceeded the configured limit", { limit, stdoutBytes, stderrBytes });
|
|
299
596
|
}
|
|
300
597
|
const code = result.exitCode ?? result.code ?? 0;
|
|
301
598
|
if (code !== 0) {
|
|
302
|
-
|
|
303
|
-
const value = JSON.parse(result.stdout);
|
|
304
|
-
if (value && typeof value === "object" && value.tool === "asgrep" && value.schema_version === MACHINE_SCHEMA_VERSION && value.ok === false) {
|
|
305
|
-
const failure = record(value.error);
|
|
306
|
-
const message = typeof failure?.message === "string" ? failure.message : "ast-sgrep reported an operational failure";
|
|
307
|
-
throw new RuntimeError("OPERATIONAL_ERROR", message, { command: value.command, error: failure, exitCode: code });
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
catch (cause) {
|
|
311
|
-
if (cause instanceof RuntimeError)
|
|
312
|
-
throw cause;
|
|
313
|
-
}
|
|
314
|
-
throw new RuntimeError("PROCESS_FAILED", `ast-sgrep exited with code ${code}`, { exitCode: code, signal: result.signal ?? undefined, stderr: result.stderr.slice(0, 1024) });
|
|
599
|
+
throwNonzeroProcessFailure(result, code);
|
|
315
600
|
}
|
|
316
601
|
let value;
|
|
317
602
|
try {
|
|
@@ -320,9 +605,10 @@ function parseEnvelope(result, limit) {
|
|
|
320
605
|
catch (cause) {
|
|
321
606
|
throw new RuntimeError("MALFORMED_OUTPUT", "ast-sgrep returned malformed JSON", { cause: cause instanceof Error ? cause.message : String(cause) });
|
|
322
607
|
}
|
|
323
|
-
|
|
608
|
+
const envelope = record(value);
|
|
609
|
+
if (!envelope)
|
|
324
610
|
throw new RuntimeError("MALFORMED_OUTPUT", "ast-sgrep returned a non-object JSON payload");
|
|
325
|
-
|
|
611
|
+
// Protocol field varieties (Ashby Keep) — sequential wire-contract checks stay here.
|
|
326
612
|
if (envelope.tool !== "asgrep")
|
|
327
613
|
throw new RuntimeError("TOOL_MISMATCH", "Response is not from ast-sgrep", { actual: envelope.tool });
|
|
328
614
|
if (envelope.schema_version !== MACHINE_SCHEMA_VERSION)
|
|
@@ -330,14 +616,12 @@ function parseEnvelope(result, limit) {
|
|
|
330
616
|
if (typeof envelope.ok !== "boolean")
|
|
331
617
|
throw new RuntimeError("MALFORMED_OUTPUT", "ast-sgrep response is missing boolean ok");
|
|
332
618
|
if (!envelope.ok) {
|
|
619
|
+
// Preserve pre-extract failure shape: plain object check (arrays allowed as error bag).
|
|
333
620
|
const failure = envelope.error && typeof envelope.error === "object" ? envelope.error : undefined;
|
|
334
621
|
const message = typeof failure?.message === "string" ? failure.message : "ast-sgrep reported an operational failure";
|
|
335
622
|
throw new RuntimeError("OPERATIONAL_ERROR", message, { command: envelope.command, error: failure });
|
|
336
623
|
}
|
|
337
|
-
|
|
338
|
-
throw new RuntimeError("VERSION_MISMATCH", "ast-sgrep binary version does not match the extension", { expected: RUNTIME_VERSION, actual: envelope.version });
|
|
339
|
-
if (envelope.machine_schema_version !== undefined && envelope.machine_schema_version !== MACHINE_SCHEMA_VERSION)
|
|
340
|
-
throw new RuntimeError("PROTOCOL_MISMATCH", "ast-sgrep binary reports an incompatible machine protocol", { expected: MACHINE_SCHEMA_VERSION, actual: envelope.machine_schema_version });
|
|
624
|
+
assertVersionTriple(envelope);
|
|
341
625
|
return envelope;
|
|
342
626
|
}
|
|
343
627
|
function indexPathFor(root, env) {
|
|
@@ -347,6 +631,34 @@ function indexPathFor(root, env) {
|
|
|
347
631
|
const resolved = resolve(root, configured);
|
|
348
632
|
return extname(resolved) === ".db" ? resolved : join(resolved, "index.db");
|
|
349
633
|
}
|
|
634
|
+
function indexQuarantines(indexPath) {
|
|
635
|
+
const quarantinePrefix = `${basename(indexPath)}.corrupt`;
|
|
636
|
+
try {
|
|
637
|
+
return readdirSync(dirname(indexPath), { withFileTypes: true })
|
|
638
|
+
.filter((entry) => entry.isFile() && (entry.name === quarantinePrefix || entry.name.startsWith(`${quarantinePrefix}.`)))
|
|
639
|
+
.map((entry) => join(dirname(indexPath), entry.name))
|
|
640
|
+
.sort();
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/** Classify a rebuild failure and identify recovery copies made by this attempt. */
|
|
647
|
+
function throwIndexRebuildFailed(cause, indexPath, quarantinesBefore) {
|
|
648
|
+
const newQuarantines = indexQuarantines(indexPath).filter((path) => !quarantinesBefore.has(path));
|
|
649
|
+
const recoveryPaths = [
|
|
650
|
+
...newQuarantines,
|
|
651
|
+
...(existsSync(indexPath) ? [indexPath] : []),
|
|
652
|
+
];
|
|
653
|
+
throw new RuntimeError("INDEX_REBUILD_FAILED", "Incompatible index rebuild failed; the prior index remains recoverable", {
|
|
654
|
+
indexPath,
|
|
655
|
+
recoveryPath: recoveryPaths[0] ?? indexPath,
|
|
656
|
+
recoveryPaths,
|
|
657
|
+
priorIndexPreserved: recoveryPaths.length > 0,
|
|
658
|
+
expectedIndexFormat: INDEX_FORMAT_VERSION,
|
|
659
|
+
cause: cause instanceof Error ? cause.message : String(cause),
|
|
660
|
+
});
|
|
661
|
+
}
|
|
350
662
|
function inspectIndexFile(path) {
|
|
351
663
|
if (!existsSync(path))
|
|
352
664
|
return "missing";
|
|
@@ -375,6 +687,7 @@ function inspectIndexFile(path) {
|
|
|
375
687
|
}
|
|
376
688
|
export class AstSgrepRuntime {
|
|
377
689
|
pi;
|
|
690
|
+
watchExternalChanges = true;
|
|
378
691
|
config;
|
|
379
692
|
#resolver;
|
|
380
693
|
#environment;
|
|
@@ -389,6 +702,9 @@ export class AstSgrepRuntime {
|
|
|
389
702
|
? resolveRuntimeRoot(context.cwd)
|
|
390
703
|
: resolveRuntimeRoot(context.cwd, this.config.root, this.config.allowOutsideProject);
|
|
391
704
|
}
|
|
705
|
+
resolveIndexPath(root) {
|
|
706
|
+
return indexPathFor(root, { ...this.#environment, ...this.config.env });
|
|
707
|
+
}
|
|
392
708
|
async inspectIndexCompatibility(context) {
|
|
393
709
|
const root = await this.resolveRoot(context);
|
|
394
710
|
return inspectIndexFile(indexPathFor(root, { ...this.#environment, ...this.config.env }));
|
|
@@ -397,50 +713,23 @@ export class AstSgrepRuntime {
|
|
|
397
713
|
const root = await this.resolveRoot(context);
|
|
398
714
|
const env = { ...this.#environment, ...this.config.env, ...options.env };
|
|
399
715
|
const indexPath = indexPathFor(root, env);
|
|
400
|
-
const
|
|
401
|
-
await mkdir(parent, { recursive: true });
|
|
402
|
-
const temporaryDirectory = await mkdtemp(join(parent, ".rebuild-"));
|
|
403
|
-
const replacementPath = join(temporaryDirectory, "index.db");
|
|
404
|
-
const backupPath = `${indexPath}.backup-${randomUUID()}`;
|
|
405
|
-
let priorMoved = false;
|
|
716
|
+
const quarantinesBefore = new Set(indexQuarantines(indexPath));
|
|
406
717
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
try {
|
|
416
|
-
await rename(replacementPath, indexPath);
|
|
718
|
+
// Core reindex prepares files before opening one bulk transaction and
|
|
719
|
+
// commits rewrites plus stale-row pruning together. Keeping the same DB
|
|
720
|
+
// inode avoids stale warm NAPI sessions and removes rename crash windows.
|
|
721
|
+
const response = await this.run(["reindex", ".", "--json"], { cwd: root }, options);
|
|
722
|
+
const { failed, walkErrors } = indexCompletion(response, true);
|
|
723
|
+
if (failed > 0 || walkErrors) {
|
|
724
|
+
throw new RuntimeError("INDEX_UPDATE_INCOMPLETE", "ast-sgrep did not complete the incompatible-index rebuild", { failed, walkErrors, force: true });
|
|
417
725
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
await rename(backupPath, indexPath);
|
|
421
|
-
throw cause;
|
|
726
|
+
if (inspectIndexFile(indexPath) !== "ready") {
|
|
727
|
+
throw new RuntimeError("INDEX_REBUILD_INVALID", "Rebuilt index has an incompatible format", { expected: INDEX_FORMAT_VERSION });
|
|
422
728
|
}
|
|
423
|
-
if (priorMoved)
|
|
424
|
-
await rm(backupPath, { force: true });
|
|
425
729
|
return response;
|
|
426
730
|
}
|
|
427
731
|
catch (cause) {
|
|
428
|
-
|
|
429
|
-
let priorIndexPreserved = existsSync(indexPath);
|
|
430
|
-
if (priorMoved && !priorIndexPreserved && existsSync(backupPath)) {
|
|
431
|
-
recoveryPath = backupPath;
|
|
432
|
-
priorIndexPreserved = true;
|
|
433
|
-
}
|
|
434
|
-
throw new RuntimeError("INDEX_REBUILD_FAILED", "Incompatible index rebuild failed; the prior index remains recoverable", {
|
|
435
|
-
indexPath,
|
|
436
|
-
recoveryPath,
|
|
437
|
-
priorIndexPreserved,
|
|
438
|
-
expectedIndexFormat: INDEX_FORMAT_VERSION,
|
|
439
|
-
cause: cause instanceof Error ? cause.message : String(cause),
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
finally {
|
|
443
|
-
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
732
|
+
throwIndexRebuildFailed(cause, indexPath, quarantinesBefore);
|
|
444
733
|
}
|
|
445
734
|
}
|
|
446
735
|
async run(args, context, options = {}) {
|
|
@@ -460,22 +749,21 @@ export class AstSgrepRuntime {
|
|
|
460
749
|
return parseEnvelope(result, this.config.maxOutputBytes);
|
|
461
750
|
}
|
|
462
751
|
catch (cause) {
|
|
463
|
-
|
|
464
|
-
throw cause;
|
|
465
|
-
if (options.signal?.aborted || (cause instanceof Error && cause.name === "AbortError"))
|
|
466
|
-
throw new RuntimeError("CANCELLED", "ast-sgrep execution was cancelled");
|
|
467
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
468
|
-
if (/timeout|timed out/i.test(message))
|
|
469
|
-
throw new RuntimeError("TIMEOUT", `ast-sgrep exceeded ${timeout}ms`, { timeoutMs: timeout });
|
|
470
|
-
throw new RuntimeError("EXEC_FAILED", "Unable to execute ast-sgrep", { cause: message });
|
|
752
|
+
rethrowExecFailure(cause, options, timeout);
|
|
471
753
|
}
|
|
472
754
|
}
|
|
755
|
+
/** Absolute path to the native binary (for sticky serve / stdin batch spawn). */
|
|
756
|
+
resolveBinaryPath(options = {}) {
|
|
757
|
+
const env = { ...this.#environment, ...this.config.env, ...options.env, NO_COLOR: "1" };
|
|
758
|
+
return getBinary(this.config, env, this.#resolver);
|
|
759
|
+
}
|
|
760
|
+
/** Merged process env for native Code Mode workers. */
|
|
761
|
+
nativeEnv(options = {}) {
|
|
762
|
+
return { ...this.#environment, ...this.config.env, ...options.env, NO_COLOR: "1" };
|
|
763
|
+
}
|
|
473
764
|
async checkCompatibility(context, options = {}) {
|
|
474
765
|
const value = await this.run(["version", "--json"], context, options);
|
|
475
|
-
|
|
476
|
-
throw new RuntimeError("VERSION_MISMATCH", "ast-sgrep binary version does not match the extension", { expected: RUNTIME_VERSION, actual: value.version });
|
|
477
|
-
if (value.machine_schema_version !== MACHINE_SCHEMA_VERSION)
|
|
478
|
-
throw new RuntimeError("PROTOCOL_MISMATCH", "ast-sgrep binary reports an incompatible machine protocol", { expected: MACHINE_SCHEMA_VERSION, actual: value.machine_schema_version });
|
|
766
|
+
assertVersionTriple(value, true);
|
|
479
767
|
return value;
|
|
480
768
|
}
|
|
481
769
|
}
|