@vibgrate/cli 2026.814.2 → 2026.817.2
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/DOCS.md +61 -0
- package/README.md +1 -0
- package/dist/agent-UQC7QN77.js +27 -0
- package/dist/{agent-5ANHRYEI.js.map → agent-UQC7QN77.js.map} +1 -1
- package/dist/{approvals-T2KXPJLV.js → approvals-ASPJQUZK.js} +6 -5
- package/dist/{approvals-T2KXPJLV.js.map → approvals-ASPJQUZK.js.map} +1 -1
- package/dist/baseline-FIV6PR2M.js +9 -0
- package/dist/{baseline-WVD6P5IO.js.map → baseline-FIV6PR2M.js.map} +1 -1
- package/dist/{chunk-FOW5IF73.js → chunk-2FEB5KJS.js} +11 -34
- package/dist/chunk-2FEB5KJS.js.map +1 -0
- package/dist/{chunk-WQLZSVNF.js → chunk-3FMRFECC.js} +7 -6
- package/dist/chunk-3FMRFECC.js.map +1 -0
- package/dist/{chunk-7QDHQLUS.js → chunk-3H3SVJUY.js} +13 -11
- package/dist/chunk-3H3SVJUY.js.map +1 -0
- package/dist/{chunk-REIGWTTH.js → chunk-3HNPVXUK.js} +3 -3
- package/dist/{chunk-REIGWTTH.js.map → chunk-3HNPVXUK.js.map} +1 -1
- package/dist/{chunk-YQNTIBKS.js → chunk-3NJUOIZQ.js} +6 -6
- package/dist/{chunk-YQNTIBKS.js.map → chunk-3NJUOIZQ.js.map} +1 -1
- package/dist/{chunk-BH54SLGZ.js → chunk-5OL2TSHK.js} +4 -4
- package/dist/{chunk-BH54SLGZ.js.map → chunk-5OL2TSHK.js.map} +1 -1
- package/dist/{chunk-TFAXW7VM.js → chunk-6K3XK7C5.js} +69 -976
- package/dist/chunk-6K3XK7C5.js.map +1 -0
- package/dist/{chunk-N3XFY3PX.js → chunk-74UBGF4P.js} +3 -3
- package/dist/{chunk-N3XFY3PX.js.map → chunk-74UBGF4P.js.map} +1 -1
- package/dist/{chunk-X76VPOYH.js → chunk-AXZZ5CCG.js} +4 -4
- package/dist/{chunk-X76VPOYH.js.map → chunk-AXZZ5CCG.js.map} +1 -1
- package/dist/{chunk-5GLQNMTI.js → chunk-C3LMWZKF.js} +6 -6
- package/dist/{chunk-5GLQNMTI.js.map → chunk-C3LMWZKF.js.map} +1 -1
- package/dist/{chunk-WWGLVF7D.js → chunk-DVW3SVCW.js} +9 -8
- package/dist/chunk-DVW3SVCW.js.map +1 -0
- package/dist/{chunk-TNROG5CO.js → chunk-E2FAQREZ.js} +1739 -116
- package/dist/chunk-E2FAQREZ.js.map +1 -0
- package/dist/{chunk-7NMXE6KG.js → chunk-EXV4QSAD.js} +4 -4
- package/dist/{chunk-7NMXE6KG.js.map → chunk-EXV4QSAD.js.map} +1 -1
- package/dist/{chunk-3LSG7Z4I.js → chunk-EZ7UC5Y5.js} +25 -4
- package/dist/chunk-EZ7UC5Y5.js.map +1 -0
- package/dist/{chunk-OWSFZQOP.js → chunk-FJG5AXKZ.js} +3 -3
- package/dist/{chunk-OWSFZQOP.js.map → chunk-FJG5AXKZ.js.map} +1 -1
- package/dist/{chunk-NKFXXP6W.js → chunk-GOGMS3ST.js} +8 -6
- package/dist/chunk-GOGMS3ST.js.map +1 -0
- package/dist/{chunk-WHJMS7XC.js → chunk-I5LP55B6.js} +3 -3
- package/dist/{chunk-WHJMS7XC.js.map → chunk-I5LP55B6.js.map} +1 -1
- package/dist/{chunk-IHXNOY3R.js → chunk-K6DXETMC.js} +12 -12
- package/dist/{chunk-IHXNOY3R.js.map → chunk-K6DXETMC.js.map} +1 -1
- package/dist/{chunk-DSUGC7F5.js → chunk-KJT2R6WW.js} +4 -4
- package/dist/{chunk-DSUGC7F5.js.map → chunk-KJT2R6WW.js.map} +1 -1
- package/dist/chunk-KNGAZDGF.js +467 -0
- package/dist/chunk-KNGAZDGF.js.map +1 -0
- package/dist/{chunk-JCN6MSXQ.js → chunk-LHKQGVRF.js} +17 -29
- package/dist/chunk-LHKQGVRF.js.map +1 -0
- package/dist/{chunk-YNV5M3JW.js → chunk-M7IO3DXH.js} +9 -8
- package/dist/chunk-M7IO3DXH.js.map +1 -0
- package/dist/chunk-MS6V5XKB.js +33 -0
- package/dist/chunk-MS6V5XKB.js.map +1 -0
- package/dist/{chunk-2U37PFB2.js → chunk-MVPRPSXU.js} +7 -4
- package/dist/chunk-MVPRPSXU.js.map +1 -0
- package/dist/{chunk-DT4KIVRI.js → chunk-NG4KIJW6.js} +34 -2
- package/dist/chunk-NG4KIJW6.js.map +1 -0
- package/dist/{chunk-IHLLWBZU.js → chunk-O2DL2Q55.js} +6 -6
- package/dist/{chunk-IHLLWBZU.js.map → chunk-O2DL2Q55.js.map} +1 -1
- package/dist/chunk-O3LABX6I.js +43 -0
- package/dist/chunk-O3LABX6I.js.map +1 -0
- package/dist/chunk-OZ3IFXRD.js +22 -0
- package/dist/chunk-OZ3IFXRD.js.map +1 -0
- package/dist/chunk-PRG2ULPK.js +6 -0
- package/dist/{chunk-2H2U3DC7.js.map → chunk-PRG2ULPK.js.map} +1 -1
- package/dist/{chunk-TVDYEHIM.js → chunk-Q3M6K3HY.js} +3 -3
- package/dist/{chunk-TVDYEHIM.js.map → chunk-Q3M6K3HY.js.map} +1 -1
- package/dist/{chunk-KGPX4EUN.js → chunk-Q4G2BWE3.js} +4 -4
- package/dist/{chunk-KGPX4EUN.js.map → chunk-Q4G2BWE3.js.map} +1 -1
- package/dist/chunk-T7RSQ5WD.js +227 -0
- package/dist/chunk-T7RSQ5WD.js.map +1 -0
- package/dist/{chunk-54RGT2T5.js → chunk-TVDMNIUT.js} +3 -3
- package/dist/{chunk-54RGT2T5.js.map → chunk-TVDMNIUT.js.map} +1 -1
- package/dist/{chunk-N6ZMZ5AI.js → chunk-WAKP7SK7.js} +4922 -1011
- package/dist/chunk-WAKP7SK7.js.map +1 -0
- package/dist/{chunk-75LJM5XD.js → chunk-WVMOGIPC.js} +9 -43
- package/dist/chunk-WVMOGIPC.js.map +1 -0
- package/dist/{chunk-UTNNGKKX.js → chunk-XLXJC7H3.js} +142 -14
- package/dist/chunk-XLXJC7H3.js.map +1 -0
- package/dist/cli.js +1727 -1127
- package/dist/cli.js.map +1 -1
- package/dist/daemon-FPTRHO5N.js +26 -0
- package/dist/daemon-FPTRHO5N.js.map +1 -0
- package/dist/embed-worker-main.js +5 -4
- package/dist/embed-worker-main.js.map +1 -1
- package/dist/ensure-map-342G2IV3.js +33 -0
- package/dist/{ensure-map-HQ7A5NDL.js.map → ensure-map-342G2IV3.js.map} +1 -1
- package/dist/{fs-NTQUFCXV.js → fs-TLPOWC2D.js} +3 -3
- package/dist/fs-TLPOWC2D.js.map +1 -0
- package/dist/graph-backend-RNWQBT3P.js +15 -0
- package/dist/{graph-backend-W2TNTMY7.js.map → graph-backend-RNWQBT3P.js.map} +1 -1
- package/dist/index.d.ts +28 -6
- package/dist/index.js +21 -18
- package/dist/index.js.map +1 -1
- package/dist/{interactive-EW2VJ3BZ.js → interactive-SR7TMF6M.js} +36 -33
- package/dist/interactive-SR7TMF6M.js.map +1 -0
- package/dist/load-AYFECIQB.js +11 -0
- package/dist/{load-64RK5O6C.js.map → load-AYFECIQB.js.map} +1 -1
- package/dist/local-runtime-6SNWKSA7.js +6 -0
- package/dist/{local-runtime-2ED33KK6.js.map → local-runtime-6SNWKSA7.js.map} +1 -1
- package/dist/model-manager-SXHZ5VD4.js +10 -0
- package/dist/{model-manager-GJ2YSTSK.js.map → model-manager-SXHZ5VD4.js.map} +1 -1
- package/dist/parse-worker.d.ts +1 -1
- package/dist/parse-worker.js +5 -2
- package/dist/parse-worker.js.map +1 -1
- package/dist/{run-outcome-6QNQIHPV.js → run-outcome-YQQJG3LW.js} +7 -6
- package/dist/run-outcome-YQQJG3LW.js.map +1 -0
- package/dist/runtime-session-CNJ4DAV5.js +22 -0
- package/dist/{runtime-session-RJHH3KMF.js.map → runtime-session-CNJ4DAV5.js.map} +1 -1
- package/dist/session-H34HUV2Y.js +17 -0
- package/dist/{session-4BGYASOH.js.map → session-H34HUV2Y.js.map} +1 -1
- package/dist/{stream-json-GY5N7WZT.js → stream-json-55YDNAFX.js} +18 -17
- package/dist/stream-json-55YDNAFX.js.map +1 -0
- package/dist/{types-DLH-Ritr.d.ts → types-DHqH0XIE.d.ts} +68 -6
- package/dist/version-WSE3QOPK.js +4 -0
- package/dist/{version-DPAMQW6M.js.map → version-WSE3QOPK.js.map} +1 -1
- package/dist/{vg-mcp-bridge-IGHJKKSL.js → vg-mcp-bridge-2CNLHNEL.js} +13 -12
- package/dist/vg-mcp-bridge-2CNLHNEL.js.map +1 -0
- package/dist/{vgd-ORPA5C2K.js → vgd-URUZQORN.js} +10 -9
- package/dist/vgd-URUZQORN.js.map +1 -0
- package/grammars/tree-sitter-hcl.wasm +0 -0
- package/package.json +9 -8
- package/dist/agent-5ANHRYEI.js +0 -26
- package/dist/baseline-WVD6P5IO.js +0 -9
- package/dist/chunk-2H2U3DC7.js +0 -6
- package/dist/chunk-2U37PFB2.js.map +0 -1
- package/dist/chunk-3LSG7Z4I.js.map +0 -1
- package/dist/chunk-75LJM5XD.js.map +0 -1
- package/dist/chunk-7QDHQLUS.js.map +0 -1
- package/dist/chunk-DT4KIVRI.js.map +0 -1
- package/dist/chunk-FOW5IF73.js.map +0 -1
- package/dist/chunk-JCN6MSXQ.js.map +0 -1
- package/dist/chunk-N6ZMZ5AI.js.map +0 -1
- package/dist/chunk-NKFXXP6W.js.map +0 -1
- package/dist/chunk-RXP66R2E.js +0 -39
- package/dist/chunk-RXP66R2E.js.map +0 -1
- package/dist/chunk-TFAXW7VM.js.map +0 -1
- package/dist/chunk-TNROG5CO.js.map +0 -1
- package/dist/chunk-UTNNGKKX.js.map +0 -1
- package/dist/chunk-WQLZSVNF.js.map +0 -1
- package/dist/chunk-WWGLVF7D.js.map +0 -1
- package/dist/chunk-YNV5M3JW.js.map +0 -1
- package/dist/ensure-map-HQ7A5NDL.js +0 -30
- package/dist/fs-NTQUFCXV.js.map +0 -1
- package/dist/graph-backend-W2TNTMY7.js +0 -14
- package/dist/interactive-EW2VJ3BZ.js.map +0 -1
- package/dist/load-64RK5O6C.js +0 -10
- package/dist/local-runtime-2ED33KK6.js +0 -6
- package/dist/model-manager-GJ2YSTSK.js +0 -10
- package/dist/run-outcome-6QNQIHPV.js.map +0 -1
- package/dist/runtime-session-RJHH3KMF.js +0 -21
- package/dist/session-4BGYASOH.js +0 -16
- package/dist/stream-json-GY5N7WZT.js.map +0 -1
- package/dist/version-DPAMQW6M.js +0 -4
- package/dist/vg-mcp-bridge-IGHJKKSL.js.map +0 -1
- package/dist/vgd-ORPA5C2K.js.map +0 -1
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { FREE_PACK, navigationToolsetConfig, deferredToolNames, HOT_TOOLS } from './chunk-
|
|
1
|
+
import { FREE_PACK, navigationToolsetConfig, deferredToolNames, HOT_TOOLS } from './chunk-DVW3SVCW.js';
|
|
2
|
+
import { CliError, ExitCode } from './chunk-MS6V5XKB.js';
|
|
2
3
|
import { edgeId, buildModuleResolver, resolve, applyStaticTestLinkage, nodeId } from './chunk-VIWZ4QT7.js';
|
|
3
|
-
import { indexFor } from './chunk-
|
|
4
|
-
import { writeGraphIndex, SCHEMA_VERSION, loadGraph } from './chunk-
|
|
5
|
-
import { init_cli_invocation, whichOnPath, isInstalledOwnBinary } from './chunk-
|
|
6
|
-
import { langById, grammarSetVersion, langForExtension, setGrammarsOverride, parseSource } from './chunk-
|
|
7
|
-
import { redactSecrets } from './chunk-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
4
|
+
import { indexFor } from './chunk-Q4G2BWE3.js';
|
|
5
|
+
import { writeGraphIndex, SCHEMA_VERSION, loadGraph } from './chunk-3FMRFECC.js';
|
|
6
|
+
import { init_cli_invocation, whichOnPath, isInstalledOwnBinary } from './chunk-EZ7UC5Y5.js';
|
|
7
|
+
import { langById, grammarSetVersion, toolchainGrammarSetVersion, fileRolesFromParses, langForExtension, setGrammarsOverride, parseSource, toolchainParser } from './chunk-XLXJC7H3.js';
|
|
8
|
+
import { redactSecrets } from './chunk-T7RSQ5WD.js';
|
|
9
|
+
import { ensureVibgrateGitignore } from './chunk-WVMOGIPC.js';
|
|
10
|
+
import { loadCache, cacheDir } from './chunk-MVPRPSXU.js';
|
|
11
|
+
import { serializeGraph, stableStringify } from './chunk-O3LABX6I.js';
|
|
12
|
+
import { VERSION } from './chunk-PRG2ULPK.js';
|
|
11
13
|
import { shortId, canonicalize, hashString, hashBytes } from './chunk-JFGYT6BI.js';
|
|
12
|
-
import * as
|
|
14
|
+
import * as fs12 from 'fs';
|
|
13
15
|
import * as path11 from 'path';
|
|
14
16
|
import ignore from 'ignore';
|
|
15
17
|
import * as v8 from 'v8';
|
|
@@ -22,7 +24,8 @@ import * as os from 'os';
|
|
|
22
24
|
import { fileURLToPath } from 'url';
|
|
23
25
|
import ts from 'typescript';
|
|
24
26
|
import * as crypto from 'crypto';
|
|
25
|
-
import
|
|
27
|
+
import YAML from 'yaml';
|
|
28
|
+
import * as fs9 from 'fs/promises';
|
|
26
29
|
|
|
27
30
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
28
31
|
// Version control, IDE & tool metadata
|
|
@@ -192,8 +195,8 @@ function toPosix(p) {
|
|
|
192
195
|
function buildRootIgnore(root, exclude) {
|
|
193
196
|
const ig = ignore();
|
|
194
197
|
const gitignorePath = path11.join(root, ".gitignore");
|
|
195
|
-
if (
|
|
196
|
-
ig.add(
|
|
198
|
+
if (fs12.existsSync(gitignorePath)) {
|
|
199
|
+
ig.add(fs12.readFileSync(gitignorePath, "utf8"));
|
|
197
200
|
}
|
|
198
201
|
if (exclude.length) ig.add(exclude);
|
|
199
202
|
return ig;
|
|
@@ -208,7 +211,7 @@ function discover(options) {
|
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
const rootIg = buildRootIgnore(root, options.exclude ?? []);
|
|
211
|
-
const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path11.resolve(root, p)) : [root]).filter((p) =>
|
|
214
|
+
const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path11.resolve(root, p)) : [root]).filter((p) => fs12.existsSync(p));
|
|
212
215
|
const found = /* @__PURE__ */ new Map();
|
|
213
216
|
const considerFile = (abs) => {
|
|
214
217
|
const rel2 = toPosix(path11.relative(root, abs));
|
|
@@ -222,7 +225,7 @@ function discover(options) {
|
|
|
222
225
|
const walk = (dir) => {
|
|
223
226
|
let entries;
|
|
224
227
|
try {
|
|
225
|
-
entries =
|
|
228
|
+
entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
226
229
|
} catch {
|
|
227
230
|
return;
|
|
228
231
|
}
|
|
@@ -239,7 +242,7 @@ function discover(options) {
|
|
|
239
242
|
}
|
|
240
243
|
};
|
|
241
244
|
for (const scope of scopeAbs) {
|
|
242
|
-
const stat =
|
|
245
|
+
const stat = fs12.statSync(scope);
|
|
243
246
|
if (stat.isDirectory()) walk(scope);
|
|
244
247
|
else if (stat.isFile()) considerFile(scope);
|
|
245
248
|
}
|
|
@@ -723,12 +726,12 @@ function loadCoverage(root, explicit) {
|
|
|
723
726
|
const candidates = (explicit && explicit.length ? explicit : DEFAULT_PATHS).map(
|
|
724
727
|
(p) => path11.resolve(root, p)
|
|
725
728
|
);
|
|
726
|
-
const found = candidates.filter((p) =>
|
|
729
|
+
const found = candidates.filter((p) => fs12.existsSync(p));
|
|
727
730
|
if (found.length === 0) return null;
|
|
728
731
|
const map = /* @__PURE__ */ new Map();
|
|
729
732
|
for (const file of found) {
|
|
730
733
|
try {
|
|
731
|
-
const text =
|
|
734
|
+
const text = fs12.readFileSync(file, "utf8");
|
|
732
735
|
if (file.endsWith(".json")) mergeIstanbul(map, text, root);
|
|
733
736
|
else mergeLcov(map, text, root);
|
|
734
737
|
} catch {
|
|
@@ -949,7 +952,7 @@ async function parseInline(files, options) {
|
|
|
949
952
|
onProgress?.(0, files.length);
|
|
950
953
|
for (const file of files) {
|
|
951
954
|
try {
|
|
952
|
-
const source =
|
|
955
|
+
const source = fs12.readFileSync(file.abs, "utf8");
|
|
953
956
|
out.push(await parseSource(file.rel, file.lang.id, source));
|
|
954
957
|
} catch (err) {
|
|
955
958
|
out.push(emptyParse(file, `parse failed: ${err.message}`));
|
|
@@ -1006,7 +1009,7 @@ function isWorkerOom(err) {
|
|
|
1006
1009
|
function resolveWorkerFile() {
|
|
1007
1010
|
const here = path11.dirname(fileURLToPath(import.meta.url));
|
|
1008
1011
|
const candidate = path11.join(here, "parse-worker.js");
|
|
1009
|
-
return
|
|
1012
|
+
return fs12.existsSync(candidate) ? candidate : null;
|
|
1010
1013
|
}
|
|
1011
1014
|
function chunk(items, buckets) {
|
|
1012
1015
|
const out = Array.from({ length: Math.min(buckets, items.length || 1) }, () => []);
|
|
@@ -1300,9 +1303,9 @@ function loadTscCache(root, opts) {
|
|
|
1300
1303
|
ts: ts.version,
|
|
1301
1304
|
entries: {}
|
|
1302
1305
|
};
|
|
1303
|
-
if (!opts.disabled &&
|
|
1306
|
+
if (!opts.disabled && fs12.existsSync(file)) {
|
|
1304
1307
|
try {
|
|
1305
|
-
const loaded = JSON.parse(
|
|
1308
|
+
const loaded = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
1306
1309
|
if (loaded.version === TSC_CACHE_VERSION && loaded.toolVersion === opts.toolVersion && loaded.ts === ts.version && loaded.entries) {
|
|
1307
1310
|
data = loaded;
|
|
1308
1311
|
}
|
|
@@ -1333,8 +1336,8 @@ function loadTscCache(root, opts) {
|
|
|
1333
1336
|
},
|
|
1334
1337
|
save() {
|
|
1335
1338
|
if (opts.disabled) return;
|
|
1336
|
-
|
|
1337
|
-
|
|
1339
|
+
fs12.mkdirSync(cacheDir(root), { recursive: true });
|
|
1340
|
+
fs12.writeFileSync(file, stableStringify(data, 0));
|
|
1338
1341
|
}
|
|
1339
1342
|
};
|
|
1340
1343
|
}
|
|
@@ -1344,7 +1347,7 @@ function abs$(root, rel2) {
|
|
|
1344
1347
|
function detectAmbient(rel2, abs) {
|
|
1345
1348
|
if (rel2.endsWith(".d.ts")) return true;
|
|
1346
1349
|
try {
|
|
1347
|
-
return AMBIENT_RE.test(
|
|
1350
|
+
return AMBIENT_RE.test(fs12.readFileSync(abs, "utf8"));
|
|
1348
1351
|
} catch {
|
|
1349
1352
|
return true;
|
|
1350
1353
|
}
|
|
@@ -1643,9 +1646,9 @@ function toPosix2(p) {
|
|
|
1643
1646
|
function buildRootIgnore2(root, exclude) {
|
|
1644
1647
|
const ig = ignore();
|
|
1645
1648
|
const gitignorePath = path11.join(root, ".gitignore");
|
|
1646
|
-
if (
|
|
1649
|
+
if (fs12.existsSync(gitignorePath)) {
|
|
1647
1650
|
try {
|
|
1648
|
-
ig.add(
|
|
1651
|
+
ig.add(fs12.readFileSync(gitignorePath, "utf8"));
|
|
1649
1652
|
} catch {
|
|
1650
1653
|
}
|
|
1651
1654
|
}
|
|
@@ -1689,7 +1692,7 @@ function discoverDocs(options) {
|
|
|
1689
1692
|
const maxFiles = options.maxFiles ?? DOC_MAX_FILES;
|
|
1690
1693
|
const rootIg = buildRootIgnore2(root, options.exclude ?? []);
|
|
1691
1694
|
const scopeAbs = (options.paths?.length ? options.paths.map((p) => path11.resolve(root, p)) : [root]).filter(
|
|
1692
|
-
(p) =>
|
|
1695
|
+
(p) => fs12.existsSync(p)
|
|
1693
1696
|
);
|
|
1694
1697
|
const found = /* @__PURE__ */ new Map();
|
|
1695
1698
|
const consider = (abs) => {
|
|
@@ -1702,7 +1705,7 @@ function discoverDocs(options) {
|
|
|
1702
1705
|
const category = classifyProjectContext(rel2);
|
|
1703
1706
|
if (!category) return;
|
|
1704
1707
|
try {
|
|
1705
|
-
const st =
|
|
1708
|
+
const st = fs12.statSync(abs);
|
|
1706
1709
|
if (!st.isFile() || st.size > DOC_FILE_MAX_BYTES) return;
|
|
1707
1710
|
if (st.size === 0) return;
|
|
1708
1711
|
} catch {
|
|
@@ -1714,7 +1717,7 @@ function discoverDocs(options) {
|
|
|
1714
1717
|
if (found.size >= maxFiles || depth > 12) return;
|
|
1715
1718
|
let entries;
|
|
1716
1719
|
try {
|
|
1717
|
-
entries =
|
|
1720
|
+
entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
1718
1721
|
} catch {
|
|
1719
1722
|
return;
|
|
1720
1723
|
}
|
|
@@ -1733,7 +1736,7 @@ function discoverDocs(options) {
|
|
|
1733
1736
|
};
|
|
1734
1737
|
for (const scope of scopeAbs) {
|
|
1735
1738
|
try {
|
|
1736
|
-
const st =
|
|
1739
|
+
const st = fs12.statSync(scope);
|
|
1737
1740
|
if (st.isDirectory()) walk(scope, 0);
|
|
1738
1741
|
else if (st.isFile()) consider(scope);
|
|
1739
1742
|
} catch {
|
|
@@ -1790,7 +1793,7 @@ function documentNodesFromDocs(docs) {
|
|
|
1790
1793
|
for (const d of docs) {
|
|
1791
1794
|
let raw = "";
|
|
1792
1795
|
try {
|
|
1793
|
-
raw =
|
|
1796
|
+
raw = fs12.readFileSync(d.abs, "utf8");
|
|
1794
1797
|
} catch {
|
|
1795
1798
|
continue;
|
|
1796
1799
|
}
|
|
@@ -1826,6 +1829,1640 @@ function documentNodesFromDocs(docs) {
|
|
|
1826
1829
|
}
|
|
1827
1830
|
return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.id.localeCompare(b.id));
|
|
1828
1831
|
}
|
|
1832
|
+
|
|
1833
|
+
// src/engine/toolchain/util.ts
|
|
1834
|
+
var TOOLCHAIN_FILE_MAX_BYTES = 2 * 1024 * 1024;
|
|
1835
|
+
var TOOLCHAIN_NODES_PER_FILE_MAX = 2e3;
|
|
1836
|
+
var DOC_MAX_CHARS = 240;
|
|
1837
|
+
var LineIndex = class {
|
|
1838
|
+
/** Byte offset at which each line starts. */
|
|
1839
|
+
starts;
|
|
1840
|
+
constructor(source) {
|
|
1841
|
+
const starts = [0];
|
|
1842
|
+
for (let i = 0; i < source.length; i++) {
|
|
1843
|
+
if (source.charCodeAt(i) === 10) starts.push(i + 1);
|
|
1844
|
+
}
|
|
1845
|
+
this.starts = starts;
|
|
1846
|
+
}
|
|
1847
|
+
/** 1-based line number containing `offset`. */
|
|
1848
|
+
lineAt(offset) {
|
|
1849
|
+
if (offset <= 0) return 1;
|
|
1850
|
+
let lo = 0;
|
|
1851
|
+
let hi = this.starts.length - 1;
|
|
1852
|
+
while (lo < hi) {
|
|
1853
|
+
const mid = lo + hi + 1 >> 1;
|
|
1854
|
+
if (this.starts[mid] <= offset) lo = mid;
|
|
1855
|
+
else hi = mid - 1;
|
|
1856
|
+
}
|
|
1857
|
+
return lo + 1;
|
|
1858
|
+
}
|
|
1859
|
+
/** Span covering the byte range `[start, end)`. */
|
|
1860
|
+
span(start, end) {
|
|
1861
|
+
const s = this.lineAt(start);
|
|
1862
|
+
const e = this.lineAt(Math.max(start, end - 1));
|
|
1863
|
+
return { start: s, end: Math.max(s, e) };
|
|
1864
|
+
}
|
|
1865
|
+
get lineCount() {
|
|
1866
|
+
return this.starts.length;
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
function safeDoc(text) {
|
|
1870
|
+
if (!text) return void 0;
|
|
1871
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
1872
|
+
if (!collapsed) return void 0;
|
|
1873
|
+
const redacted = redactSecrets(collapsed);
|
|
1874
|
+
return redacted.length > DOC_MAX_CHARS ? `${redacted.slice(0, DOC_MAX_CHARS - 1)}\u2026` : redacted;
|
|
1875
|
+
}
|
|
1876
|
+
function parseImageRef(raw) {
|
|
1877
|
+
const value = raw.trim();
|
|
1878
|
+
if (!value || /\s/.test(value)) return null;
|
|
1879
|
+
if (value.includes("$") || value.includes("{{")) return null;
|
|
1880
|
+
const atIdx = value.lastIndexOf("@");
|
|
1881
|
+
let digest;
|
|
1882
|
+
let rest = value;
|
|
1883
|
+
if (atIdx > 0) {
|
|
1884
|
+
digest = value.slice(atIdx + 1);
|
|
1885
|
+
rest = value.slice(0, atIdx);
|
|
1886
|
+
}
|
|
1887
|
+
let tag;
|
|
1888
|
+
const colonIdx = rest.lastIndexOf(":");
|
|
1889
|
+
if (colonIdx > 0 && colonIdx > rest.lastIndexOf("/")) {
|
|
1890
|
+
tag = rest.slice(colonIdx + 1);
|
|
1891
|
+
rest = rest.slice(0, colonIdx);
|
|
1892
|
+
}
|
|
1893
|
+
if (!rest) return null;
|
|
1894
|
+
return { raw: value, repository: rest, tag, digest };
|
|
1895
|
+
}
|
|
1896
|
+
function imageIdentity(ref) {
|
|
1897
|
+
if (ref.digest) return `${ref.repository}@${ref.digest}`;
|
|
1898
|
+
return ref.tag ? `${ref.repository}:${ref.tag}` : ref.repository;
|
|
1899
|
+
}
|
|
1900
|
+
function toPosix3(rel2) {
|
|
1901
|
+
return rel2.replace(/\\/g, "/");
|
|
1902
|
+
}
|
|
1903
|
+
function resolveRelative(fromFileRel, target) {
|
|
1904
|
+
const base = toPosix3(fromFileRel).split("/").slice(0, -1);
|
|
1905
|
+
const parts = toPosix3(target).split("/");
|
|
1906
|
+
const out = [...base];
|
|
1907
|
+
for (const part of parts) {
|
|
1908
|
+
if (!part || part === ".") continue;
|
|
1909
|
+
if (part === "..") {
|
|
1910
|
+
if (!out.length) return null;
|
|
1911
|
+
out.pop();
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
out.push(part);
|
|
1915
|
+
}
|
|
1916
|
+
const joined = out.join("/");
|
|
1917
|
+
return joined || null;
|
|
1918
|
+
}
|
|
1919
|
+
function parseYamlStream(rel2, source) {
|
|
1920
|
+
const warnings = [];
|
|
1921
|
+
let parsed;
|
|
1922
|
+
try {
|
|
1923
|
+
parsed = YAML.parseAllDocuments(source, {
|
|
1924
|
+
// Parsing itself does not expand aliases — the bomb only detonates on
|
|
1925
|
+
// materialisation, so the alias budget is applied at `toJS` below.
|
|
1926
|
+
logLevel: "silent",
|
|
1927
|
+
uniqueKeys: false
|
|
1928
|
+
});
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
warnings.push(`${rel2}: YAML parse failed (${err.message})`);
|
|
1931
|
+
return { docs: [], warnings };
|
|
1932
|
+
}
|
|
1933
|
+
const docs = [];
|
|
1934
|
+
parsed.forEach((ast, index) => {
|
|
1935
|
+
if (ast.errors.length) {
|
|
1936
|
+
warnings.push(`${rel2}: YAML document ${index + 1} has errors \u2014 skipped`);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
let value;
|
|
1940
|
+
try {
|
|
1941
|
+
value = ast.toJS({ maxAliasCount: 100 });
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
warnings.push(`${rel2}: YAML document ${index + 1} could not be materialised (${err.message})`);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
if (value === null || value === void 0) return;
|
|
1947
|
+
docs.push({ value, ast, index });
|
|
1948
|
+
});
|
|
1949
|
+
return { docs, warnings };
|
|
1950
|
+
}
|
|
1951
|
+
function spanAt(doc, lines, path12) {
|
|
1952
|
+
const node = path12.length ? safeGetIn(doc.ast, path12) : doc.ast.contents;
|
|
1953
|
+
const range = nodeRange(node) ?? nodeRange(doc.ast.contents);
|
|
1954
|
+
if (!range) return { start: 1, end: 1 };
|
|
1955
|
+
return lines.span(range[0], range[2] ?? range[1]);
|
|
1956
|
+
}
|
|
1957
|
+
function safeGetIn(ast, path12) {
|
|
1958
|
+
try {
|
|
1959
|
+
return ast.getIn(path12, true);
|
|
1960
|
+
} catch {
|
|
1961
|
+
return void 0;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function nodeRange(node) {
|
|
1965
|
+
const range = node?.range;
|
|
1966
|
+
return Array.isArray(range) ? range : null;
|
|
1967
|
+
}
|
|
1968
|
+
function getPath(value, path12) {
|
|
1969
|
+
let current = value;
|
|
1970
|
+
for (const key of path12) {
|
|
1971
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
1972
|
+
current = current[key];
|
|
1973
|
+
}
|
|
1974
|
+
return current;
|
|
1975
|
+
}
|
|
1976
|
+
function getString(value, path12) {
|
|
1977
|
+
const found = getPath(value, path12);
|
|
1978
|
+
if (typeof found === "string") return found;
|
|
1979
|
+
if (typeof found === "number" || typeof found === "boolean") return String(found);
|
|
1980
|
+
return void 0;
|
|
1981
|
+
}
|
|
1982
|
+
function getArray(value, path12) {
|
|
1983
|
+
const found = getPath(value, path12);
|
|
1984
|
+
return Array.isArray(found) ? found : [];
|
|
1985
|
+
}
|
|
1986
|
+
function getRecordEntries(value, path12) {
|
|
1987
|
+
const found = getPath(value, path12);
|
|
1988
|
+
if (!found || typeof found !== "object" || Array.isArray(found)) return [];
|
|
1989
|
+
return Object.entries(found).sort(([a], [b]) => a.localeCompare(b, "en"));
|
|
1990
|
+
}
|
|
1991
|
+
function looksTemplated(source) {
|
|
1992
|
+
return /\{\{-?\s*(if|range|with|end|define|template|include|toYaml|\.Values|\.Release|\.Chart)/.test(
|
|
1993
|
+
source
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/engine/toolchain/compose.ts
|
|
1998
|
+
var DEFAULT_DOCKERFILE = "Dockerfile";
|
|
1999
|
+
var composeExtractor = {
|
|
2000
|
+
format: "compose",
|
|
2001
|
+
matches(rel2, category) {
|
|
2002
|
+
if (category === "compose") return true;
|
|
2003
|
+
return /(^|\/)(docker-)?compose([.-][\w.-]+)?\.ya?ml$/i.test(rel2);
|
|
2004
|
+
},
|
|
2005
|
+
extract(rel2, source) {
|
|
2006
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
2007
|
+
const lines = new LineIndex(source);
|
|
2008
|
+
const nodes = [];
|
|
2009
|
+
const edges = [];
|
|
2010
|
+
const doc = docs[0];
|
|
2011
|
+
if (!doc) return { nodes, edges, warnings };
|
|
2012
|
+
const services = getRecordEntries(doc.value, ["services"]);
|
|
2013
|
+
const declared = new Set(services.map(([name]) => `service:${name}`));
|
|
2014
|
+
for (const [name, service] of services) {
|
|
2015
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2016
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2017
|
+
break;
|
|
2018
|
+
}
|
|
2019
|
+
const address = `service:${name}`;
|
|
2020
|
+
const image = getString(service, ["image"]);
|
|
2021
|
+
const ports = getArray(service, ["ports"]).map(String);
|
|
2022
|
+
nodes.push({
|
|
2023
|
+
kind: "resource",
|
|
2024
|
+
name,
|
|
2025
|
+
qualifiedName: address,
|
|
2026
|
+
span: spanAt(doc, lines, ["services", name]),
|
|
2027
|
+
signature: "compose.service",
|
|
2028
|
+
doc: safeDoc(
|
|
2029
|
+
[image && `image ${image}`, ports.length && `ports ${ports.join(", ")}`].filter(Boolean).join(", ")
|
|
2030
|
+
),
|
|
2031
|
+
importance: 0.5
|
|
2032
|
+
});
|
|
2033
|
+
if (image) {
|
|
2034
|
+
const ref = parseImageRef(image);
|
|
2035
|
+
if (ref) {
|
|
2036
|
+
nodes.push({
|
|
2037
|
+
kind: "image",
|
|
2038
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
2039
|
+
qualifiedName: `image:${ref.raw}`,
|
|
2040
|
+
span: spanAt(doc, lines, ["services", name, "image"]),
|
|
2041
|
+
signature: "compose.image",
|
|
2042
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
2043
|
+
importance: 0.3
|
|
2044
|
+
});
|
|
2045
|
+
edges.push({ kind: "builds_from", from: address, to: `image:${ref.raw}`, confidence: 1 });
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const dockerfileRel = composeDockerfilePath(rel2, service);
|
|
2049
|
+
if (dockerfileRel) {
|
|
2050
|
+
nodes.push({
|
|
2051
|
+
kind: "image",
|
|
2052
|
+
name: dockerfileRel.split("/").pop() ?? dockerfileRel,
|
|
2053
|
+
qualifiedName: `dockerfile:${dockerfileRel}`,
|
|
2054
|
+
span: spanAt(doc, lines, ["services", name, "build"]),
|
|
2055
|
+
signature: "compose.build",
|
|
2056
|
+
doc: safeDoc(`builds from ${dockerfileRel}`),
|
|
2057
|
+
importance: 0.4
|
|
2058
|
+
});
|
|
2059
|
+
edges.push({
|
|
2060
|
+
kind: "builds_from",
|
|
2061
|
+
from: address,
|
|
2062
|
+
to: `dockerfile:${dockerfileRel}`,
|
|
2063
|
+
confidence: 1
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
for (const target of composeDependsOn(service)) {
|
|
2067
|
+
const to = `service:${target}`;
|
|
2068
|
+
if (declared.has(to)) edges.push({ kind: "depends_on", from: address, to, confidence: 1 });
|
|
2069
|
+
}
|
|
2070
|
+
for (const port of ports) {
|
|
2071
|
+
edges.push({ kind: "exposes", from: address, to: `port:${port}`, confidence: 1 });
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
const portTargets = new Set(
|
|
2075
|
+
edges.filter((e) => e.kind === "exposes").map((e) => e.to)
|
|
2076
|
+
);
|
|
2077
|
+
for (const target of [...portTargets].sort()) {
|
|
2078
|
+
nodes.push({
|
|
2079
|
+
kind: "property",
|
|
2080
|
+
name: target.slice("port:".length),
|
|
2081
|
+
qualifiedName: target,
|
|
2082
|
+
span: { start: 1, end: 1 },
|
|
2083
|
+
signature: "compose.port",
|
|
2084
|
+
importance: 0.1
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
return { nodes, edges, warnings };
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
function composeDockerfilePath(rel2, service) {
|
|
2091
|
+
const build = getPath(service, ["build"]);
|
|
2092
|
+
if (build === void 0 || build === null) return null;
|
|
2093
|
+
if (typeof build === "string") {
|
|
2094
|
+
return resolveRelative(rel2, `${build}/${DEFAULT_DOCKERFILE}`);
|
|
2095
|
+
}
|
|
2096
|
+
if (typeof build !== "object") return null;
|
|
2097
|
+
const context = getString(build, ["context"]) ?? ".";
|
|
2098
|
+
const dockerfile = getString(build, ["dockerfile"]) ?? DEFAULT_DOCKERFILE;
|
|
2099
|
+
return resolveRelative(rel2, `${context}/${dockerfile}`);
|
|
2100
|
+
}
|
|
2101
|
+
function composeDependsOn(service) {
|
|
2102
|
+
const raw = getPath(service, ["depends_on"]);
|
|
2103
|
+
if (Array.isArray(raw)) {
|
|
2104
|
+
return raw.filter((v) => typeof v === "string").sort();
|
|
2105
|
+
}
|
|
2106
|
+
if (raw && typeof raw === "object") {
|
|
2107
|
+
return Object.keys(raw).sort();
|
|
2108
|
+
}
|
|
2109
|
+
return [];
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/engine/toolchain/dockerfile.ts
|
|
2113
|
+
var INSTRUCTION = /^([A-Za-z][A-Za-z0-9_]*)\s+([\s\S]*)$/;
|
|
2114
|
+
function logicalLines(source) {
|
|
2115
|
+
const rawLines = source.split(/\r?\n/);
|
|
2116
|
+
let escapeChar = "\\";
|
|
2117
|
+
for (const line of rawLines) {
|
|
2118
|
+
const trimmed = line.trim();
|
|
2119
|
+
if (!trimmed) continue;
|
|
2120
|
+
if (!trimmed.startsWith("#")) break;
|
|
2121
|
+
const directive = /^#\s*escape\s*=\s*(\S)/i.exec(trimmed);
|
|
2122
|
+
if (directive) escapeChar = directive[1];
|
|
2123
|
+
}
|
|
2124
|
+
const out = [];
|
|
2125
|
+
let buffer = null;
|
|
2126
|
+
let startLine = 0;
|
|
2127
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
2128
|
+
const raw = rawLines[i];
|
|
2129
|
+
const trimmed = raw.trim();
|
|
2130
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2131
|
+
const continues = trimmed.endsWith(escapeChar);
|
|
2132
|
+
const fragment = continues ? trimmed.slice(0, -1).trimEnd() : trimmed;
|
|
2133
|
+
if (buffer === null) {
|
|
2134
|
+
buffer = fragment;
|
|
2135
|
+
startLine = i + 1;
|
|
2136
|
+
} else {
|
|
2137
|
+
buffer += ` ${fragment}`;
|
|
2138
|
+
}
|
|
2139
|
+
if (!continues) {
|
|
2140
|
+
const match = INSTRUCTION.exec(buffer);
|
|
2141
|
+
if (match) {
|
|
2142
|
+
out.push({
|
|
2143
|
+
instruction: match[1].toUpperCase(),
|
|
2144
|
+
args: match[2].trim(),
|
|
2145
|
+
startLine,
|
|
2146
|
+
endLine: i + 1
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
buffer = null;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
if (buffer !== null) {
|
|
2153
|
+
const match = INSTRUCTION.exec(buffer);
|
|
2154
|
+
if (match) {
|
|
2155
|
+
out.push({
|
|
2156
|
+
instruction: match[1].toUpperCase(),
|
|
2157
|
+
args: match[2].trim(),
|
|
2158
|
+
startLine,
|
|
2159
|
+
endLine: rawLines.length
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return out;
|
|
2164
|
+
}
|
|
2165
|
+
function parseFrom(args) {
|
|
2166
|
+
const tokens = args.split(/\s+/).filter(Boolean);
|
|
2167
|
+
const positional = [];
|
|
2168
|
+
for (const token of tokens) {
|
|
2169
|
+
if (token.startsWith("--")) continue;
|
|
2170
|
+
positional.push(token);
|
|
2171
|
+
}
|
|
2172
|
+
if (!positional.length) return null;
|
|
2173
|
+
const image = positional[0];
|
|
2174
|
+
const asIdx = positional.findIndex((t) => t.toUpperCase() === "AS");
|
|
2175
|
+
const stage = asIdx > 0 && positional[asIdx + 1] ? positional[asIdx + 1] : void 0;
|
|
2176
|
+
return { image, stage };
|
|
2177
|
+
}
|
|
2178
|
+
function parseCopyFrom(args) {
|
|
2179
|
+
const match = /--from=(\S+)/i.exec(args);
|
|
2180
|
+
return match ? match[1] : null;
|
|
2181
|
+
}
|
|
2182
|
+
function parseExpose(args) {
|
|
2183
|
+
return args.split(/\s+/).filter(Boolean).filter((token) => !token.startsWith("--"));
|
|
2184
|
+
}
|
|
2185
|
+
function copySourcePaths(args, hasFromFlag) {
|
|
2186
|
+
const tokens = args.split(/\s+/).filter(Boolean).filter((t) => !t.startsWith("--"));
|
|
2187
|
+
if (tokens.length < 2) return [];
|
|
2188
|
+
const sources = tokens.slice(0, -1);
|
|
2189
|
+
return sources.filter(
|
|
2190
|
+
(source) => source !== "." && !source.includes("*") && !source.includes("?") && !source.includes("[") && !source.includes("$") && !/^https?:\/\//i.test(source)
|
|
2191
|
+
).map((source) => source.replace(/^\.\//, "")).sort();
|
|
2192
|
+
}
|
|
2193
|
+
var dockerfileExtractor = {
|
|
2194
|
+
format: "dockerfile",
|
|
2195
|
+
matches(rel2, category) {
|
|
2196
|
+
if (category === "docker") return true;
|
|
2197
|
+
const base = toPosix3(rel2).split("/").pop()?.toLowerCase() ?? "";
|
|
2198
|
+
return base === "dockerfile" || base === "containerfile" || base.startsWith("dockerfile.");
|
|
2199
|
+
},
|
|
2200
|
+
extract(rel2, source) {
|
|
2201
|
+
const lines = new LineIndex(source);
|
|
2202
|
+
const nodes = [];
|
|
2203
|
+
const edges = [];
|
|
2204
|
+
const warnings = [];
|
|
2205
|
+
const posixRel = toPosix3(rel2);
|
|
2206
|
+
const fileAddress = `dockerfile:${posixRel}`;
|
|
2207
|
+
nodes.push({
|
|
2208
|
+
kind: "image",
|
|
2209
|
+
name: posixRel.split("/").pop() ?? posixRel,
|
|
2210
|
+
qualifiedName: fileAddress,
|
|
2211
|
+
span: { start: 1, end: Math.max(1, lines.lineCount) },
|
|
2212
|
+
signature: "dockerfile",
|
|
2213
|
+
importance: 0.5
|
|
2214
|
+
});
|
|
2215
|
+
const stages = /* @__PURE__ */ new Map();
|
|
2216
|
+
const copySources = /* @__PURE__ */ new Map();
|
|
2217
|
+
let stageIndex = 0;
|
|
2218
|
+
let currentStage = null;
|
|
2219
|
+
for (const line of logicalLines(source)) {
|
|
2220
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2221
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2222
|
+
break;
|
|
2223
|
+
}
|
|
2224
|
+
switch (line.instruction) {
|
|
2225
|
+
case "FROM": {
|
|
2226
|
+
const from = parseFrom(line.args);
|
|
2227
|
+
if (!from) break;
|
|
2228
|
+
const stageName = from.stage ?? String(stageIndex);
|
|
2229
|
+
const stageAddress = `${fileAddress}#${stageName}`;
|
|
2230
|
+
stages.set(stageName.toLowerCase(), stageAddress);
|
|
2231
|
+
currentStage = stageAddress;
|
|
2232
|
+
stageIndex++;
|
|
2233
|
+
nodes.push({
|
|
2234
|
+
kind: "image",
|
|
2235
|
+
name: from.stage ?? `stage ${stageIndex - 1}`,
|
|
2236
|
+
qualifiedName: stageAddress,
|
|
2237
|
+
span: { start: line.startLine, end: line.endLine },
|
|
2238
|
+
signature: "dockerfile.stage",
|
|
2239
|
+
doc: safeDoc(`from ${from.image}`),
|
|
2240
|
+
importance: 0.4
|
|
2241
|
+
});
|
|
2242
|
+
edges.push({ kind: "contains", from: fileAddress, to: stageAddress, confidence: 1 });
|
|
2243
|
+
const baseStage = stages.get(from.image.toLowerCase());
|
|
2244
|
+
if (baseStage && baseStage !== stageAddress) {
|
|
2245
|
+
edges.push({
|
|
2246
|
+
kind: "builds_from",
|
|
2247
|
+
from: stageAddress,
|
|
2248
|
+
to: baseStage,
|
|
2249
|
+
confidence: 1
|
|
2250
|
+
});
|
|
2251
|
+
} else {
|
|
2252
|
+
const ref = parseImageRef(from.image);
|
|
2253
|
+
if (ref) {
|
|
2254
|
+
const imageAddress = `image:${ref.raw}`;
|
|
2255
|
+
nodes.push({
|
|
2256
|
+
kind: "image",
|
|
2257
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
2258
|
+
qualifiedName: imageAddress,
|
|
2259
|
+
span: { start: line.startLine, end: line.endLine },
|
|
2260
|
+
signature: "dockerfile.base",
|
|
2261
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
2262
|
+
importance: 0.3
|
|
2263
|
+
});
|
|
2264
|
+
edges.push({
|
|
2265
|
+
kind: "builds_from",
|
|
2266
|
+
from: stageAddress,
|
|
2267
|
+
to: imageAddress,
|
|
2268
|
+
confidence: 1
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
break;
|
|
2273
|
+
}
|
|
2274
|
+
case "COPY":
|
|
2275
|
+
case "ADD": {
|
|
2276
|
+
const fromRef = parseCopyFrom(line.args);
|
|
2277
|
+
if (fromRef) {
|
|
2278
|
+
if (!currentStage) break;
|
|
2279
|
+
const target = stages.get(fromRef.toLowerCase());
|
|
2280
|
+
if (target && target !== currentStage) {
|
|
2281
|
+
edges.push({ kind: "depends_on", from: currentStage, to: target, confidence: 1 });
|
|
2282
|
+
}
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
if (!currentStage) break;
|
|
2286
|
+
const forStage = copySources.get(currentStage) ?? /* @__PURE__ */ new Set();
|
|
2287
|
+
for (const source2 of copySourcePaths(line.args)) {
|
|
2288
|
+
const relativeToDockerfile = resolveRelative(posixRel, source2);
|
|
2289
|
+
if (relativeToDockerfile) forStage.add(relativeToDockerfile);
|
|
2290
|
+
forStage.add(toPosix3(source2).replace(/^\.\//, ""));
|
|
2291
|
+
}
|
|
2292
|
+
if (forStage.size) copySources.set(currentStage, forStage);
|
|
2293
|
+
break;
|
|
2294
|
+
}
|
|
2295
|
+
case "EXPOSE": {
|
|
2296
|
+
if (!currentStage) break;
|
|
2297
|
+
for (const port of parseExpose(line.args)) {
|
|
2298
|
+
const portAddress = `port:${port}`;
|
|
2299
|
+
nodes.push({
|
|
2300
|
+
kind: "property",
|
|
2301
|
+
name: port,
|
|
2302
|
+
qualifiedName: portAddress,
|
|
2303
|
+
span: { start: line.startLine, end: line.endLine },
|
|
2304
|
+
signature: "dockerfile.expose",
|
|
2305
|
+
importance: 0.1
|
|
2306
|
+
});
|
|
2307
|
+
edges.push({ kind: "exposes", from: currentStage, to: portAddress, confidence: 1 });
|
|
2308
|
+
}
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
const copySourcesByStage = {};
|
|
2314
|
+
for (const stage of [...copySources.keys()].sort()) {
|
|
2315
|
+
copySourcesByStage[stage] = [...copySources.get(stage) ?? []].sort();
|
|
2316
|
+
}
|
|
2317
|
+
return { nodes, edges, warnings, linkHints: { copySourcesByStage } };
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
// src/engine/toolchain/helm.ts
|
|
2322
|
+
function collectValuesImages(value, prefix, out) {
|
|
2323
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2324
|
+
const repository = getString(value, ["repository"]);
|
|
2325
|
+
const tag = getString(value, ["tag"]);
|
|
2326
|
+
const registry = getString(value, ["registry"]);
|
|
2327
|
+
if (repository) {
|
|
2328
|
+
const full = [registry, repository].filter(Boolean).join("/");
|
|
2329
|
+
const raw = tag ? `${full}:${tag}` : full;
|
|
2330
|
+
if (!out.has(raw)) out.set(raw, prefix);
|
|
2331
|
+
}
|
|
2332
|
+
const direct = getString(value, ["image"]);
|
|
2333
|
+
if (direct && parseImageRef(direct)) {
|
|
2334
|
+
if (!out.has(direct)) out.set(direct, [...prefix, "image"]);
|
|
2335
|
+
}
|
|
2336
|
+
for (const [key, child] of getRecordEntries(value, [])) {
|
|
2337
|
+
if (key === "repository" || key === "tag" || key === "registry") continue;
|
|
2338
|
+
collectValuesImages(child, [...prefix, key], out);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
var helmExtractor = {
|
|
2342
|
+
format: "helm",
|
|
2343
|
+
matches(rel2) {
|
|
2344
|
+
const base = toPosix3(rel2).split("/").pop()?.toLowerCase() ?? "";
|
|
2345
|
+
if (base === "chart.yaml" || base === "chart.yml") return true;
|
|
2346
|
+
return /(^|\/)values([.-][\w.-]+)?\.ya?ml$/i.test(rel2) && /(^|\/)charts?\//i.test(rel2);
|
|
2347
|
+
},
|
|
2348
|
+
extract(rel2, source) {
|
|
2349
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
2350
|
+
const lines = new LineIndex(source);
|
|
2351
|
+
const nodes = [];
|
|
2352
|
+
const edges = [];
|
|
2353
|
+
const doc = docs[0];
|
|
2354
|
+
if (!doc) return { nodes, edges, warnings };
|
|
2355
|
+
const base = toPosix3(rel2).split("/").pop()?.toLowerCase() ?? "";
|
|
2356
|
+
const isChartFile = base === "chart.yaml" || base === "chart.yml";
|
|
2357
|
+
if (isChartFile) {
|
|
2358
|
+
const name = getString(doc.value, ["name"]);
|
|
2359
|
+
if (!name) return { nodes, edges, warnings };
|
|
2360
|
+
const version = getString(doc.value, ["version"]);
|
|
2361
|
+
const appVersion = getString(doc.value, ["appVersion"]);
|
|
2362
|
+
const address = `chart:${name}`;
|
|
2363
|
+
nodes.push({
|
|
2364
|
+
kind: "chart",
|
|
2365
|
+
name,
|
|
2366
|
+
qualifiedName: address,
|
|
2367
|
+
span: spanAt(doc, lines, []),
|
|
2368
|
+
signature: "helm.chart",
|
|
2369
|
+
doc: safeDoc(
|
|
2370
|
+
[version && `version ${version}`, appVersion && `app ${appVersion}`].filter(Boolean).join(", ")
|
|
2371
|
+
),
|
|
2372
|
+
importance: 0.7
|
|
2373
|
+
});
|
|
2374
|
+
for (const dependency of getArray(doc.value, ["dependencies"])) {
|
|
2375
|
+
const depName = getString(dependency, ["name"]);
|
|
2376
|
+
if (!depName) continue;
|
|
2377
|
+
const depVersion = getString(dependency, ["version"]);
|
|
2378
|
+
const depAddress = `chart:${depName}`;
|
|
2379
|
+
nodes.push({
|
|
2380
|
+
kind: "chart",
|
|
2381
|
+
name: depName,
|
|
2382
|
+
qualifiedName: depAddress,
|
|
2383
|
+
span: spanAt(doc, lines, ["dependencies"]),
|
|
2384
|
+
signature: "helm.dependency",
|
|
2385
|
+
doc: safeDoc(depVersion ? `version ${depVersion}` : void 0),
|
|
2386
|
+
importance: 0.4
|
|
2387
|
+
});
|
|
2388
|
+
edges.push({ kind: "depends_on", from: address, to: depAddress, confidence: 1 });
|
|
2389
|
+
}
|
|
2390
|
+
return { nodes, edges, warnings };
|
|
2391
|
+
}
|
|
2392
|
+
const images = /* @__PURE__ */ new Map();
|
|
2393
|
+
collectValuesImages(doc.value, [], images);
|
|
2394
|
+
for (const raw of [...images.keys()].sort()) {
|
|
2395
|
+
const ref = parseImageRef(raw);
|
|
2396
|
+
if (!ref) continue;
|
|
2397
|
+
nodes.push({
|
|
2398
|
+
kind: "image",
|
|
2399
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
2400
|
+
qualifiedName: `image:${ref.raw}`,
|
|
2401
|
+
span: spanAt(doc, lines, images.get(raw) ?? []),
|
|
2402
|
+
signature: "helm.values.image",
|
|
2403
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
2404
|
+
importance: 0.3
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
return { nodes, edges, warnings };
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
|
|
2411
|
+
// src/engine/toolchain/kubernetes.ts
|
|
2412
|
+
var WORKLOAD_TEMPLATE_PATHS = {
|
|
2413
|
+
Deployment: ["spec", "template"],
|
|
2414
|
+
StatefulSet: ["spec", "template"],
|
|
2415
|
+
DaemonSet: ["spec", "template"],
|
|
2416
|
+
ReplicaSet: ["spec", "template"],
|
|
2417
|
+
ReplicationController: ["spec", "template"],
|
|
2418
|
+
Job: ["spec", "template"],
|
|
2419
|
+
CronJob: ["spec", "jobTemplate", "spec", "template"],
|
|
2420
|
+
Pod: []
|
|
2421
|
+
};
|
|
2422
|
+
var CONTAINER_FIELDS = ["initContainers", "containers", "ephemeralContainers"];
|
|
2423
|
+
function isKubernetesDoc(value) {
|
|
2424
|
+
return typeof getString(value, ["apiVersion"]) === "string" && typeof getString(value, ["kind"]) === "string";
|
|
2425
|
+
}
|
|
2426
|
+
function objectAddress(value) {
|
|
2427
|
+
const kind = getString(value, ["kind"]);
|
|
2428
|
+
const name = getString(value, ["metadata", "name"]);
|
|
2429
|
+
if (!kind || !name) return null;
|
|
2430
|
+
const namespace = getString(value, ["metadata", "namespace"]) ?? "default";
|
|
2431
|
+
return `${namespace}/${kind}/${name}`;
|
|
2432
|
+
}
|
|
2433
|
+
function podSpecPath(kind) {
|
|
2434
|
+
const templatePath = WORKLOAD_TEMPLATE_PATHS[kind];
|
|
2435
|
+
if (!templatePath) return null;
|
|
2436
|
+
return [...templatePath, "spec"];
|
|
2437
|
+
}
|
|
2438
|
+
var kubernetesExtractor = {
|
|
2439
|
+
format: "kubernetes",
|
|
2440
|
+
matches(rel2, category, head) {
|
|
2441
|
+
if (!/\.ya?ml$/i.test(rel2)) return false;
|
|
2442
|
+
if (looksTemplated(head)) return false;
|
|
2443
|
+
if (/(^|\/)templates\//i.test(rel2)) return false;
|
|
2444
|
+
return /(^|\n)apiVersion:\s*\S/.test(head) && /(^|\n)kind:\s*\S/.test(head);
|
|
2445
|
+
},
|
|
2446
|
+
extract(rel2, source) {
|
|
2447
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
2448
|
+
const lines = new LineIndex(source);
|
|
2449
|
+
const nodes = [];
|
|
2450
|
+
const edges = [];
|
|
2451
|
+
const workloadLabels = /* @__PURE__ */ new Map();
|
|
2452
|
+
for (const doc of docs) {
|
|
2453
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2454
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2455
|
+
break;
|
|
2456
|
+
}
|
|
2457
|
+
if (!isKubernetesDoc(doc.value)) continue;
|
|
2458
|
+
const address = objectAddress(doc.value);
|
|
2459
|
+
if (!address) continue;
|
|
2460
|
+
const kind = getString(doc.value, ["kind"]);
|
|
2461
|
+
const name = getString(doc.value, ["metadata", "name"]);
|
|
2462
|
+
const podPath = podSpecPath(kind);
|
|
2463
|
+
const isWorkload = podPath !== null;
|
|
2464
|
+
nodes.push({
|
|
2465
|
+
kind: isWorkload ? "workload" : "resource",
|
|
2466
|
+
name,
|
|
2467
|
+
qualifiedName: address,
|
|
2468
|
+
span: spanAt(doc, lines, []),
|
|
2469
|
+
signature: `k8s.${kind}`,
|
|
2470
|
+
doc: safeDoc(describeObject(doc.value, kind)),
|
|
2471
|
+
importance: isWorkload ? 0.6 : 0.4
|
|
2472
|
+
});
|
|
2473
|
+
if (isWorkload) {
|
|
2474
|
+
const labels = readLabels(doc.value, [...WORKLOAD_TEMPLATE_PATHS[kind] ?? [], "metadata", "labels"]);
|
|
2475
|
+
if (Object.keys(labels).length) workloadLabels.set(address, labels);
|
|
2476
|
+
extractContainers(doc, lines, address, podPath, nodes, edges);
|
|
2477
|
+
}
|
|
2478
|
+
for (const reference of referencedConfig(doc.value, podPath)) {
|
|
2479
|
+
edges.push({ kind: "mounts", from: address, to: reference, confidence: 1 });
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
for (const doc of docs) {
|
|
2483
|
+
if (getString(doc.value, ["kind"]) !== "Service") continue;
|
|
2484
|
+
const from = objectAddress(doc.value);
|
|
2485
|
+
if (!from) continue;
|
|
2486
|
+
const selector = readLabels(doc.value, ["spec", "selector"]);
|
|
2487
|
+
if (!Object.keys(selector).length) continue;
|
|
2488
|
+
for (const [address, labels] of [...workloadLabels].sort(([a], [b]) => a.localeCompare(b, "en"))) {
|
|
2489
|
+
if (isSubset(selector, labels)) {
|
|
2490
|
+
edges.push({ kind: "exposes", from, to: address, confidence: 1 });
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return { nodes, edges, warnings };
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
function extractContainers(doc, lines, workloadAddress, podPath, nodes, edges) {
|
|
2498
|
+
const seenImages = /* @__PURE__ */ new Set();
|
|
2499
|
+
for (const field of CONTAINER_FIELDS) {
|
|
2500
|
+
const containers = getArray(doc.value, [...podPath, field]);
|
|
2501
|
+
containers.forEach((container, index) => {
|
|
2502
|
+
const image = getString(container, ["image"]);
|
|
2503
|
+
if (!image) return;
|
|
2504
|
+
const ref = parseImageRef(image);
|
|
2505
|
+
if (!ref) return;
|
|
2506
|
+
if (seenImages.has(ref.raw)) return;
|
|
2507
|
+
seenImages.add(ref.raw);
|
|
2508
|
+
nodes.push({
|
|
2509
|
+
kind: "image",
|
|
2510
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
2511
|
+
qualifiedName: `image:${ref.raw}`,
|
|
2512
|
+
span: spanAt(doc, lines, [...podPath, field, index, "image"]),
|
|
2513
|
+
signature: "k8s.image",
|
|
2514
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
2515
|
+
importance: 0.3
|
|
2516
|
+
});
|
|
2517
|
+
edges.push({
|
|
2518
|
+
kind: "builds_from",
|
|
2519
|
+
from: workloadAddress,
|
|
2520
|
+
to: `image:${ref.raw}`,
|
|
2521
|
+
confidence: 1
|
|
2522
|
+
});
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function referencedConfig(value, podPath) {
|
|
2527
|
+
if (!podPath) return [];
|
|
2528
|
+
const out = /* @__PURE__ */ new Set();
|
|
2529
|
+
for (const volume of getArray(value, [...podPath, "volumes"])) {
|
|
2530
|
+
const configMap = getString(volume, ["configMap", "name"]);
|
|
2531
|
+
if (configMap) out.add(`ConfigMap/${configMap}`);
|
|
2532
|
+
const secret = getString(volume, ["secret", "secretName"]);
|
|
2533
|
+
if (secret) out.add(`Secret/${secret}`);
|
|
2534
|
+
const claim = getString(volume, ["persistentVolumeClaim", "claimName"]);
|
|
2535
|
+
if (claim) out.add(`PersistentVolumeClaim/${claim}`);
|
|
2536
|
+
}
|
|
2537
|
+
for (const field of CONTAINER_FIELDS) {
|
|
2538
|
+
for (const container of getArray(value, [...podPath, field])) {
|
|
2539
|
+
for (const envFrom of getArray(container, ["envFrom"])) {
|
|
2540
|
+
const configMap = getString(envFrom, ["configMapRef", "name"]);
|
|
2541
|
+
if (configMap) out.add(`ConfigMap/${configMap}`);
|
|
2542
|
+
const secret = getString(envFrom, ["secretRef", "name"]);
|
|
2543
|
+
if (secret) out.add(`Secret/${secret}`);
|
|
2544
|
+
}
|
|
2545
|
+
for (const env of getArray(container, ["env"])) {
|
|
2546
|
+
const configMap = getString(env, ["valueFrom", "configMapKeyRef", "name"]);
|
|
2547
|
+
if (configMap) out.add(`ConfigMap/${configMap}`);
|
|
2548
|
+
const secret = getString(env, ["valueFrom", "secretKeyRef", "name"]);
|
|
2549
|
+
if (secret) out.add(`Secret/${secret}`);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return [...out].sort();
|
|
2554
|
+
}
|
|
2555
|
+
function readLabels(value, path12) {
|
|
2556
|
+
const out = {};
|
|
2557
|
+
for (const [key, raw] of getRecordEntries(value, path12)) {
|
|
2558
|
+
if (typeof raw === "string") out[key] = raw;
|
|
2559
|
+
else if (typeof raw === "number" || typeof raw === "boolean") out[key] = String(raw);
|
|
2560
|
+
}
|
|
2561
|
+
return out;
|
|
2562
|
+
}
|
|
2563
|
+
function isSubset(selector, labels) {
|
|
2564
|
+
const keys = Object.keys(selector);
|
|
2565
|
+
if (!keys.length) return false;
|
|
2566
|
+
return keys.every((key) => labels[key] === selector[key]);
|
|
2567
|
+
}
|
|
2568
|
+
function describeObject(value, kind) {
|
|
2569
|
+
const parts = [];
|
|
2570
|
+
const replicas = getPath(value, ["spec", "replicas"]);
|
|
2571
|
+
if (typeof replicas === "number") parts.push(`${replicas} replica${replicas === 1 ? "" : "s"}`);
|
|
2572
|
+
const podPath = podSpecPath(kind);
|
|
2573
|
+
if (podPath) {
|
|
2574
|
+
const count = CONTAINER_FIELDS.reduce(
|
|
2575
|
+
(sum, field) => sum + getArray(value, [...podPath, field]).length,
|
|
2576
|
+
0
|
|
2577
|
+
);
|
|
2578
|
+
if (count) parts.push(`${count} container${count === 1 ? "" : "s"}`);
|
|
2579
|
+
}
|
|
2580
|
+
if (kind === "Service") {
|
|
2581
|
+
const type = getString(value, ["spec", "type"]);
|
|
2582
|
+
if (type) parts.push(type);
|
|
2583
|
+
const ports = getArray(value, ["spec", "ports"]).map((p) => getString(p, ["port"])).filter(Boolean);
|
|
2584
|
+
if (ports.length) parts.push(`port ${ports.join(", ")}`);
|
|
2585
|
+
}
|
|
2586
|
+
return parts.join(", ");
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/engine/toolchain/terraform.ts
|
|
2590
|
+
var BUILTIN_REF_PREFIXES = /* @__PURE__ */ new Set([
|
|
2591
|
+
"count",
|
|
2592
|
+
"each",
|
|
2593
|
+
"self",
|
|
2594
|
+
"path",
|
|
2595
|
+
"terraform"
|
|
2596
|
+
]);
|
|
2597
|
+
var BLOCK_SPECS = {
|
|
2598
|
+
resource: {
|
|
2599
|
+
kind: "resource",
|
|
2600
|
+
labels: 2,
|
|
2601
|
+
address: ([type, name]) => `${type}.${name}`,
|
|
2602
|
+
signature: "terraform.resource",
|
|
2603
|
+
importance: 0.5
|
|
2604
|
+
},
|
|
2605
|
+
data: {
|
|
2606
|
+
kind: "resource",
|
|
2607
|
+
labels: 2,
|
|
2608
|
+
address: ([type, name]) => `data.${type}.${name}`,
|
|
2609
|
+
signature: "terraform.data",
|
|
2610
|
+
importance: 0.3
|
|
2611
|
+
},
|
|
2612
|
+
module: {
|
|
2613
|
+
kind: "module",
|
|
2614
|
+
labels: 1,
|
|
2615
|
+
address: ([name]) => `module.${name}`,
|
|
2616
|
+
signature: "terraform.module",
|
|
2617
|
+
importance: 0.6
|
|
2618
|
+
},
|
|
2619
|
+
variable: {
|
|
2620
|
+
kind: "property",
|
|
2621
|
+
labels: 1,
|
|
2622
|
+
address: ([name]) => `var.${name}`,
|
|
2623
|
+
signature: "terraform.variable",
|
|
2624
|
+
importance: 0.2
|
|
2625
|
+
},
|
|
2626
|
+
output: {
|
|
2627
|
+
kind: "property",
|
|
2628
|
+
labels: 1,
|
|
2629
|
+
address: ([name]) => `output.${name}`,
|
|
2630
|
+
signature: "terraform.output",
|
|
2631
|
+
importance: 0.3
|
|
2632
|
+
},
|
|
2633
|
+
provider: {
|
|
2634
|
+
kind: "package",
|
|
2635
|
+
labels: 1,
|
|
2636
|
+
address: ([name]) => `provider.${name}`,
|
|
2637
|
+
signature: "terraform.provider",
|
|
2638
|
+
importance: 0.4
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2641
|
+
function stringLitValue(node) {
|
|
2642
|
+
for (const child of node.namedChildren) {
|
|
2643
|
+
if (child?.type === "template_literal") return child.text;
|
|
2644
|
+
}
|
|
2645
|
+
return "";
|
|
2646
|
+
}
|
|
2647
|
+
function topLevelBlocks(root) {
|
|
2648
|
+
const out = [];
|
|
2649
|
+
for (const bodyCandidate of root.namedChildren) {
|
|
2650
|
+
if (!bodyCandidate) continue;
|
|
2651
|
+
const body = bodyCandidate.type === "body" ? bodyCandidate : null;
|
|
2652
|
+
if (!body) continue;
|
|
2653
|
+
for (const child of body.namedChildren) {
|
|
2654
|
+
if (child?.type === "block") out.push(child);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
return out;
|
|
2658
|
+
}
|
|
2659
|
+
function blockBody(block) {
|
|
2660
|
+
for (const child of block.namedChildren) {
|
|
2661
|
+
if (child?.type === "body") return child;
|
|
2662
|
+
}
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
function attributes(body) {
|
|
2666
|
+
const out = [];
|
|
2667
|
+
for (const child of body.namedChildren) {
|
|
2668
|
+
if (child?.type !== "attribute") continue;
|
|
2669
|
+
let name = "";
|
|
2670
|
+
let expr = null;
|
|
2671
|
+
for (const part of child.namedChildren) {
|
|
2672
|
+
if (!part) continue;
|
|
2673
|
+
if (!name && part.type === "identifier") name = part.text;
|
|
2674
|
+
else if (part.type === "expression") expr = part;
|
|
2675
|
+
}
|
|
2676
|
+
if (name) out.push({ name, expr, node: child });
|
|
2677
|
+
}
|
|
2678
|
+
return out;
|
|
2679
|
+
}
|
|
2680
|
+
function nestedBlocks(body) {
|
|
2681
|
+
const out = [];
|
|
2682
|
+
for (const child of body.namedChildren) {
|
|
2683
|
+
if (child?.type === "block") out.push(child);
|
|
2684
|
+
}
|
|
2685
|
+
return out;
|
|
2686
|
+
}
|
|
2687
|
+
function blockHead(block) {
|
|
2688
|
+
let type = "";
|
|
2689
|
+
const labels = [];
|
|
2690
|
+
for (const child of block.namedChildren) {
|
|
2691
|
+
if (!child) continue;
|
|
2692
|
+
if (!type && child.type === "identifier") type = child.text;
|
|
2693
|
+
else if (child.type === "string_lit") labels.push(stringLitValue(child));
|
|
2694
|
+
else if (child.type === "body") break;
|
|
2695
|
+
}
|
|
2696
|
+
return { type, labels };
|
|
2697
|
+
}
|
|
2698
|
+
function referencedAddresses(expr, out) {
|
|
2699
|
+
if (!expr) return;
|
|
2700
|
+
const stack = [expr];
|
|
2701
|
+
while (stack.length) {
|
|
2702
|
+
const node = stack.pop();
|
|
2703
|
+
if (!node) continue;
|
|
2704
|
+
if (node.type === "variable_expr") {
|
|
2705
|
+
const root = node.text;
|
|
2706
|
+
const parts = [root];
|
|
2707
|
+
let sibling = node.nextNamedSibling;
|
|
2708
|
+
while (sibling && sibling.type === "get_attr") {
|
|
2709
|
+
const id = sibling.namedChildren.find((c) => c?.type === "identifier");
|
|
2710
|
+
if (!id) break;
|
|
2711
|
+
parts.push(id.text);
|
|
2712
|
+
sibling = sibling.nextNamedSibling;
|
|
2713
|
+
}
|
|
2714
|
+
const address = terraformAddress(parts);
|
|
2715
|
+
if (address) out.add(address);
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
for (const child of node.namedChildren) {
|
|
2719
|
+
if (child) stack.push(child);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
function terraformAddress(parts) {
|
|
2724
|
+
const [head, ...rest] = parts;
|
|
2725
|
+
if (!head || BUILTIN_REF_PREFIXES.has(head)) return null;
|
|
2726
|
+
if (head === "var" || head === "local") return rest.length ? `${head}.${rest[0]}` : null;
|
|
2727
|
+
if (head === "module") return rest.length ? `module.${rest[0]}` : null;
|
|
2728
|
+
if (head === "data") return rest.length >= 2 ? `data.${rest[0]}.${rest[1]}` : null;
|
|
2729
|
+
return rest.length ? `${head}.${rest[0]}` : null;
|
|
2730
|
+
}
|
|
2731
|
+
function extractRequiredProviders(terraformBlock, lines, nodes, seen) {
|
|
2732
|
+
const body = blockBody(terraformBlock);
|
|
2733
|
+
if (!body) return;
|
|
2734
|
+
for (const nested of nestedBlocks(body)) {
|
|
2735
|
+
const { type } = blockHead(nested);
|
|
2736
|
+
if (type !== "required_providers") continue;
|
|
2737
|
+
const inner = blockBody(nested);
|
|
2738
|
+
if (!inner) continue;
|
|
2739
|
+
for (const attr of attributes(inner)) {
|
|
2740
|
+
let source;
|
|
2741
|
+
let version;
|
|
2742
|
+
const objectAttrs = attr.expr ? collectObjectAttributes(attr.expr) : [];
|
|
2743
|
+
if (objectAttrs.length) {
|
|
2744
|
+
for (const { key, value } of objectAttrs) {
|
|
2745
|
+
if (key === "source") source = value;
|
|
2746
|
+
else if (key === "version") version = value;
|
|
2747
|
+
}
|
|
2748
|
+
} else if (attr.expr) {
|
|
2749
|
+
const literal = findFirst(attr.expr, "string_lit");
|
|
2750
|
+
if (literal) {
|
|
2751
|
+
version = stringLitValue(literal);
|
|
2752
|
+
source = `hashicorp/${attr.name}`;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
const address = `provider.${attr.name}`;
|
|
2756
|
+
if (seen.has(address)) continue;
|
|
2757
|
+
seen.add(address);
|
|
2758
|
+
nodes.push({
|
|
2759
|
+
kind: "package",
|
|
2760
|
+
name: attr.name,
|
|
2761
|
+
qualifiedName: address,
|
|
2762
|
+
span: lines.span(attr.node.startIndex, attr.node.endIndex),
|
|
2763
|
+
signature: "terraform.required_provider",
|
|
2764
|
+
doc: safeDoc(
|
|
2765
|
+
[source && `source ${source}`, version && `version ${version}`].filter(Boolean).join(", ")
|
|
2766
|
+
),
|
|
2767
|
+
importance: 0.4
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
function collectObjectAttributes(expr) {
|
|
2773
|
+
const out = [];
|
|
2774
|
+
const stack = [expr];
|
|
2775
|
+
while (stack.length) {
|
|
2776
|
+
const node = stack.pop();
|
|
2777
|
+
if (!node) continue;
|
|
2778
|
+
if (node.type === "object_elem") {
|
|
2779
|
+
const key = node.namedChildren.find((c) => c?.type === "expression");
|
|
2780
|
+
const rest = node.namedChildren.filter((c) => c?.type === "expression");
|
|
2781
|
+
const valueNode = rest.length > 1 ? rest[1] : null;
|
|
2782
|
+
const keyText = key ? plainText(key) : "";
|
|
2783
|
+
const valueText = valueNode ? plainText(valueNode) : "";
|
|
2784
|
+
if (keyText) out.push({ key: keyText, value: valueText });
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
for (const child of node.namedChildren) {
|
|
2788
|
+
if (child) stack.push(child);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
return out;
|
|
2792
|
+
}
|
|
2793
|
+
function plainText(expr) {
|
|
2794
|
+
const lit = findFirst(expr, "string_lit");
|
|
2795
|
+
if (lit) return stringLitValue(lit);
|
|
2796
|
+
return expr.text.trim();
|
|
2797
|
+
}
|
|
2798
|
+
function findFirst(node, type) {
|
|
2799
|
+
const stack = [node];
|
|
2800
|
+
while (stack.length) {
|
|
2801
|
+
const current = stack.pop();
|
|
2802
|
+
if (!current) continue;
|
|
2803
|
+
if (current.type === type) return current;
|
|
2804
|
+
for (const child of current.namedChildren) {
|
|
2805
|
+
if (child) stack.push(child);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
return null;
|
|
2809
|
+
}
|
|
2810
|
+
var terraformExtractor = {
|
|
2811
|
+
format: "terraform",
|
|
2812
|
+
matches(rel2) {
|
|
2813
|
+
const lower = rel2.toLowerCase();
|
|
2814
|
+
return lower.endsWith(".tf") || lower.endsWith(".tofu");
|
|
2815
|
+
},
|
|
2816
|
+
async extract(rel2, source) {
|
|
2817
|
+
const parser = await toolchainParser("hcl");
|
|
2818
|
+
if (!parser) {
|
|
2819
|
+
return {
|
|
2820
|
+
nodes: [],
|
|
2821
|
+
edges: [],
|
|
2822
|
+
warnings: [`${rel2}: HCL grammar unavailable \u2014 no Terraform structure extracted`]
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
const tree = parser.parse(source);
|
|
2826
|
+
if (!tree) return { nodes: [], edges: [], warnings: [`${rel2}: HCL parse produced no tree`] };
|
|
2827
|
+
const lines = new LineIndex(source);
|
|
2828
|
+
const nodes = [];
|
|
2829
|
+
const edges = [];
|
|
2830
|
+
const warnings = [];
|
|
2831
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2832
|
+
const pendingEdges = [];
|
|
2833
|
+
if (tree.rootNode.hasError) {
|
|
2834
|
+
warnings.push(`${rel2}: HCL parse recovered from a syntax error \u2014 extraction may be partial`);
|
|
2835
|
+
}
|
|
2836
|
+
for (const block of topLevelBlocks(tree.rootNode)) {
|
|
2837
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2838
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2839
|
+
break;
|
|
2840
|
+
}
|
|
2841
|
+
const { type, labels } = blockHead(block);
|
|
2842
|
+
if (type === "terraform") {
|
|
2843
|
+
extractRequiredProviders(block, lines, nodes, declared);
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
const spec = BLOCK_SPECS[type];
|
|
2847
|
+
if (!spec || labels.length < spec.labels) continue;
|
|
2848
|
+
const address = spec.address(labels);
|
|
2849
|
+
if (declared.has(address)) continue;
|
|
2850
|
+
declared.add(address);
|
|
2851
|
+
const body = blockBody(block);
|
|
2852
|
+
const attrs = body ? attributes(body) : [];
|
|
2853
|
+
const summaryParts = [];
|
|
2854
|
+
for (const attr of attrs) {
|
|
2855
|
+
if (attr.name === "source" || attr.name === "version" || attr.name === "description") {
|
|
2856
|
+
const value = attr.expr ? plainText(attr.expr) : "";
|
|
2857
|
+
if (value) summaryParts.push(`${attr.name} ${value}`);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
nodes.push({
|
|
2861
|
+
kind: spec.kind,
|
|
2862
|
+
name: labels[labels.length - 1] ?? type,
|
|
2863
|
+
qualifiedName: address,
|
|
2864
|
+
span: lines.span(block.startIndex, block.endIndex),
|
|
2865
|
+
signature: spec.signature,
|
|
2866
|
+
doc: safeDoc(summaryParts.join(", ")),
|
|
2867
|
+
importance: spec.importance
|
|
2868
|
+
});
|
|
2869
|
+
if (!body) continue;
|
|
2870
|
+
const references = /* @__PURE__ */ new Set();
|
|
2871
|
+
for (const attr of attrs) {
|
|
2872
|
+
referencedAddresses(attr.expr, references);
|
|
2873
|
+
}
|
|
2874
|
+
const nestedStack = nestedBlocks(body);
|
|
2875
|
+
while (nestedStack.length) {
|
|
2876
|
+
const nested = nestedStack.pop();
|
|
2877
|
+
if (!nested) continue;
|
|
2878
|
+
const innerBody = blockBody(nested);
|
|
2879
|
+
if (!innerBody) continue;
|
|
2880
|
+
for (const attr of attributes(innerBody)) referencedAddresses(attr.expr, references);
|
|
2881
|
+
nestedStack.push(...nestedBlocks(innerBody));
|
|
2882
|
+
}
|
|
2883
|
+
references.delete(address);
|
|
2884
|
+
for (const target of [...references].sort()) {
|
|
2885
|
+
pendingEdges.push({ kind: "depends_on", from: address, to: target, confidence: 1 });
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
for (const edge of pendingEdges) {
|
|
2889
|
+
if (declared.has(edge.to)) edges.push(edge);
|
|
2890
|
+
}
|
|
2891
|
+
return { nodes, edges, warnings };
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
|
|
2895
|
+
// src/engine/toolchain/workflows.ts
|
|
2896
|
+
var MAX_STEPS_PER_JOB = 100;
|
|
2897
|
+
var GITLAB_RESERVED = /* @__PURE__ */ new Set([
|
|
2898
|
+
"stages",
|
|
2899
|
+
"variables",
|
|
2900
|
+
"default",
|
|
2901
|
+
"include",
|
|
2902
|
+
"workflow",
|
|
2903
|
+
"image",
|
|
2904
|
+
"services",
|
|
2905
|
+
"before_script",
|
|
2906
|
+
"after_script",
|
|
2907
|
+
"cache",
|
|
2908
|
+
"pages"
|
|
2909
|
+
]);
|
|
2910
|
+
function isGitHubWorkflowPath(rel2) {
|
|
2911
|
+
return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(toPosix3(rel2));
|
|
2912
|
+
}
|
|
2913
|
+
function isGitLabCiPath(rel2) {
|
|
2914
|
+
const base = toPosix3(rel2).split("/").pop()?.toLowerCase() ?? "";
|
|
2915
|
+
return base === ".gitlab-ci.yml" || base === ".gitlab-ci.yaml";
|
|
2916
|
+
}
|
|
2917
|
+
function imagesBuiltBy(script) {
|
|
2918
|
+
const out = /* @__PURE__ */ new Set();
|
|
2919
|
+
const buildTag = /\b(?:docker|podman)\s+(?:buildx\s+)?build\b[^\n]*?(?:-t|--tag)[=\s]+(\S+)/g;
|
|
2920
|
+
for (const match of script.matchAll(buildTag)) out.add(match[1]);
|
|
2921
|
+
const push2 = /\b(?:docker|podman)\s+push\s+(\S+)/g;
|
|
2922
|
+
for (const match of script.matchAll(push2)) out.add(match[1]);
|
|
2923
|
+
return [...out].filter((tag) => !tag.includes("${") && !tag.includes("{{")).sort();
|
|
2924
|
+
}
|
|
2925
|
+
function terraformAppliedDirs(script, workingDirectory) {
|
|
2926
|
+
const out = /* @__PURE__ */ new Set();
|
|
2927
|
+
const apply = /\b(?:terraform|tofu)\s+(?:-chdir=(\S+)\s+)?(?:apply|destroy)\b/g;
|
|
2928
|
+
for (const match of script.matchAll(apply)) {
|
|
2929
|
+
out.add(match[1] ?? workingDirectory ?? ".");
|
|
2930
|
+
}
|
|
2931
|
+
return [...out].sort();
|
|
2932
|
+
}
|
|
2933
|
+
function collectSecretReferences(value, out) {
|
|
2934
|
+
const stack = [value];
|
|
2935
|
+
while (stack.length) {
|
|
2936
|
+
const current = stack.pop();
|
|
2937
|
+
if (typeof current === "string") {
|
|
2938
|
+
for (const match of current.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
2939
|
+
out.add(`secret:${match[1]}`);
|
|
2940
|
+
}
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
if (Array.isArray(current)) {
|
|
2944
|
+
stack.push(...current);
|
|
2945
|
+
continue;
|
|
2946
|
+
}
|
|
2947
|
+
if (current && typeof current === "object") {
|
|
2948
|
+
stack.push(...Object.values(current));
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
var githubActionsExtractor = {
|
|
2953
|
+
format: "github-actions",
|
|
2954
|
+
matches(rel2) {
|
|
2955
|
+
return isGitHubWorkflowPath(rel2);
|
|
2956
|
+
},
|
|
2957
|
+
extract(rel2, source) {
|
|
2958
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
2959
|
+
const lines = new LineIndex(source);
|
|
2960
|
+
const nodes = [];
|
|
2961
|
+
const edges = [];
|
|
2962
|
+
const doc = docs[0];
|
|
2963
|
+
if (!doc) return { nodes, edges, warnings };
|
|
2964
|
+
const workflowName = getString(doc.value, ["name"]) ?? toPosix3(rel2).split("/").pop() ?? rel2;
|
|
2965
|
+
const workflowAddress = `workflow:${toPosix3(rel2)}`;
|
|
2966
|
+
nodes.push({
|
|
2967
|
+
kind: "job",
|
|
2968
|
+
name: workflowName,
|
|
2969
|
+
qualifiedName: workflowAddress,
|
|
2970
|
+
span: spanAt(doc, lines, []),
|
|
2971
|
+
signature: "gha.workflow",
|
|
2972
|
+
doc: safeDoc(`GitHub Actions workflow`),
|
|
2973
|
+
importance: 0.6
|
|
2974
|
+
});
|
|
2975
|
+
const onValue = getPath(doc.value, ["on"]) ?? getPath(doc.value, [true]);
|
|
2976
|
+
for (const event of triggerEvents(onValue)) {
|
|
2977
|
+
const eventAddress = `event:${event}`;
|
|
2978
|
+
nodes.push({
|
|
2979
|
+
kind: "property",
|
|
2980
|
+
name: event,
|
|
2981
|
+
qualifiedName: eventAddress,
|
|
2982
|
+
span: spanAt(doc, lines, ["on"]),
|
|
2983
|
+
signature: "gha.event",
|
|
2984
|
+
importance: 0.2
|
|
2985
|
+
});
|
|
2986
|
+
edges.push({ kind: "triggers", from: eventAddress, to: workflowAddress, confidence: 1 });
|
|
2987
|
+
}
|
|
2988
|
+
const jobs = getRecordEntries(doc.value, ["jobs"]);
|
|
2989
|
+
const jobAddresses = new Set(jobs.map(([id]) => `job:${id}`));
|
|
2990
|
+
for (const [jobId, job] of jobs) {
|
|
2991
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2992
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2993
|
+
break;
|
|
2994
|
+
}
|
|
2995
|
+
const jobAddress = `job:${jobId}`;
|
|
2996
|
+
const runsOn = getPath(job, ["runs-on"]);
|
|
2997
|
+
const steps = getArray(job, ["steps"]);
|
|
2998
|
+
nodes.push({
|
|
2999
|
+
kind: "job",
|
|
3000
|
+
name: getString(job, ["name"]) ?? jobId,
|
|
3001
|
+
qualifiedName: jobAddress,
|
|
3002
|
+
span: spanAt(doc, lines, ["jobs", jobId]),
|
|
3003
|
+
signature: "gha.job",
|
|
3004
|
+
doc: safeDoc(
|
|
3005
|
+
[
|
|
3006
|
+
typeof runsOn === "string" ? `runs-on ${runsOn}` : null,
|
|
3007
|
+
steps.length ? `${steps.length} step${steps.length === 1 ? "" : "s"}` : null
|
|
3008
|
+
].filter(Boolean).join(", ")
|
|
3009
|
+
),
|
|
3010
|
+
importance: 0.5
|
|
3011
|
+
});
|
|
3012
|
+
edges.push({ kind: "contains", from: workflowAddress, to: jobAddress, confidence: 1 });
|
|
3013
|
+
for (const need of asStringList(getPath(job, ["needs"]))) {
|
|
3014
|
+
const to = `job:${need}`;
|
|
3015
|
+
if (jobAddresses.has(to)) {
|
|
3016
|
+
edges.push({ kind: "depends_on", from: jobAddress, to, confidence: 1 });
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
steps.slice(0, MAX_STEPS_PER_JOB).forEach((step, index) => {
|
|
3020
|
+
const uses = getString(step, ["uses"]);
|
|
3021
|
+
const run = getString(step, ["run"]);
|
|
3022
|
+
const stepName = getString(step, ["name"]) ?? uses ?? (run ? run.split("\n")[0] : `step ${index + 1}`);
|
|
3023
|
+
const stepAddress = `${jobAddress}#${index}`;
|
|
3024
|
+
nodes.push({
|
|
3025
|
+
kind: "step",
|
|
3026
|
+
name: stepName.slice(0, 80),
|
|
3027
|
+
qualifiedName: stepAddress,
|
|
3028
|
+
span: spanAt(doc, lines, ["jobs", jobId, "steps", index]),
|
|
3029
|
+
signature: uses ? "gha.step.uses" : "gha.step.run",
|
|
3030
|
+
// `run:` bodies are shell scripts and are exactly where a token gets
|
|
3031
|
+
// pasted — safeDoc redacts, and we keep only the first line.
|
|
3032
|
+
doc: safeDoc(uses ? `uses ${uses}` : run?.split("\n")[0]),
|
|
3033
|
+
importance: 0.2
|
|
3034
|
+
});
|
|
3035
|
+
edges.push({ kind: "contains", from: jobAddress, to: stepAddress, confidence: 1 });
|
|
3036
|
+
if (uses) {
|
|
3037
|
+
const actionAddress = `action:${uses}`;
|
|
3038
|
+
nodes.push({
|
|
3039
|
+
kind: "external",
|
|
3040
|
+
name: uses.split("@")[0],
|
|
3041
|
+
qualifiedName: actionAddress,
|
|
3042
|
+
span: spanAt(doc, lines, ["jobs", jobId, "steps", index, "uses"]),
|
|
3043
|
+
signature: "gha.action",
|
|
3044
|
+
importance: 0.2
|
|
3045
|
+
});
|
|
3046
|
+
edges.push({ kind: "depends_on", from: stepAddress, to: actionAddress, confidence: 1 });
|
|
3047
|
+
}
|
|
3048
|
+
if (run) {
|
|
3049
|
+
const stepSpan = spanAt(doc, lines, ["jobs", jobId, "steps", index]);
|
|
3050
|
+
const workingDirectory = getString(step, ["working-directory"]) ?? getString(job, ["defaults", "run", "working-directory"]);
|
|
3051
|
+
emitScriptEffects(rel2, run, workingDirectory, jobAddress, stepSpan, nodes, edges);
|
|
3052
|
+
}
|
|
3053
|
+
});
|
|
3054
|
+
if (steps.length > MAX_STEPS_PER_JOB) {
|
|
3055
|
+
warnings.push(`${rel2}: job ${jobId} has ${steps.length} steps \u2014 enumerated the first ${MAX_STEPS_PER_JOB}`);
|
|
3056
|
+
}
|
|
3057
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
3058
|
+
collectSecretReferences(job, secrets);
|
|
3059
|
+
for (const secret of [...secrets].sort()) {
|
|
3060
|
+
nodes.push({
|
|
3061
|
+
kind: "property",
|
|
3062
|
+
name: secret.slice("secret:".length),
|
|
3063
|
+
qualifiedName: secret,
|
|
3064
|
+
span: spanAt(doc, lines, ["jobs", jobId]),
|
|
3065
|
+
signature: "gha.secret-ref",
|
|
3066
|
+
importance: 0.2
|
|
3067
|
+
});
|
|
3068
|
+
edges.push({ kind: "mounts", from: jobAddress, to: secret, confidence: 1 });
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
return { nodes, edges, warnings };
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
var gitlabCiExtractor = {
|
|
3075
|
+
format: "gitlab-ci",
|
|
3076
|
+
matches(rel2) {
|
|
3077
|
+
return isGitLabCiPath(rel2);
|
|
3078
|
+
},
|
|
3079
|
+
extract(rel2, source) {
|
|
3080
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
3081
|
+
const lines = new LineIndex(source);
|
|
3082
|
+
const nodes = [];
|
|
3083
|
+
const edges = [];
|
|
3084
|
+
const doc = docs[0];
|
|
3085
|
+
if (!doc) return { nodes, edges, warnings };
|
|
3086
|
+
const pipelineAddress = `workflow:${toPosix3(rel2)}`;
|
|
3087
|
+
nodes.push({
|
|
3088
|
+
kind: "job",
|
|
3089
|
+
name: "GitLab CI",
|
|
3090
|
+
qualifiedName: pipelineAddress,
|
|
3091
|
+
span: spanAt(doc, lines, []),
|
|
3092
|
+
signature: "gitlab.pipeline",
|
|
3093
|
+
importance: 0.6
|
|
3094
|
+
});
|
|
3095
|
+
const jobs = getRecordEntries(doc.value, []).filter(
|
|
3096
|
+
([key, value]) => !GITLAB_RESERVED.has(key) && !key.startsWith(".") && value !== null && typeof value === "object" && !Array.isArray(value)
|
|
3097
|
+
);
|
|
3098
|
+
const jobAddresses = new Set(jobs.map(([id]) => `job:${id}`));
|
|
3099
|
+
for (const [jobId, job] of jobs) {
|
|
3100
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
3101
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
const jobAddress = `job:${jobId}`;
|
|
3105
|
+
const stage = getString(job, ["stage"]);
|
|
3106
|
+
const image = getString(job, ["image"]) ?? getString(job, ["image", "name"]);
|
|
3107
|
+
nodes.push({
|
|
3108
|
+
kind: "job",
|
|
3109
|
+
name: jobId,
|
|
3110
|
+
qualifiedName: jobAddress,
|
|
3111
|
+
span: spanAt(doc, lines, [jobId]),
|
|
3112
|
+
signature: "gitlab.job",
|
|
3113
|
+
doc: safeDoc([stage && `stage ${stage}`, image && `image ${image}`].filter(Boolean).join(", ")),
|
|
3114
|
+
importance: 0.5
|
|
3115
|
+
});
|
|
3116
|
+
edges.push({ kind: "contains", from: pipelineAddress, to: jobAddress, confidence: 1 });
|
|
3117
|
+
for (const key of ["needs", "dependencies"]) {
|
|
3118
|
+
for (const need of asStringList(getPath(job, [key]))) {
|
|
3119
|
+
const to = `job:${need}`;
|
|
3120
|
+
if (jobAddresses.has(to)) {
|
|
3121
|
+
edges.push({ kind: "depends_on", from: jobAddress, to, confidence: 1 });
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
return { nodes, edges, warnings };
|
|
3127
|
+
}
|
|
3128
|
+
};
|
|
3129
|
+
function emitScriptEffects(rel2, script, workingDirectory, jobAddress, span, nodes, edges) {
|
|
3130
|
+
for (const tag of imagesBuiltBy(script)) {
|
|
3131
|
+
const ref = parseImageRef(tag);
|
|
3132
|
+
if (!ref) continue;
|
|
3133
|
+
const address = `image:${ref.raw}`;
|
|
3134
|
+
nodes.push({
|
|
3135
|
+
kind: "image",
|
|
3136
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
3137
|
+
qualifiedName: address,
|
|
3138
|
+
span,
|
|
3139
|
+
signature: "ci.builds-image",
|
|
3140
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
3141
|
+
importance: 0.4
|
|
3142
|
+
});
|
|
3143
|
+
edges.push({ kind: "provisions", from: jobAddress, to: address, confidence: 1 });
|
|
3144
|
+
}
|
|
3145
|
+
for (const dir of terraformAppliedDirs(script, workingDirectory)) {
|
|
3146
|
+
const resolved = normaliseWorkspacePath(dir);
|
|
3147
|
+
const address = `tfdir:${resolved}`;
|
|
3148
|
+
nodes.push({
|
|
3149
|
+
kind: "resource",
|
|
3150
|
+
name: resolved,
|
|
3151
|
+
qualifiedName: address,
|
|
3152
|
+
span,
|
|
3153
|
+
signature: "ci.terraform-apply",
|
|
3154
|
+
doc: safeDoc(`terraform apply in ${resolved}`),
|
|
3155
|
+
importance: 0.4
|
|
3156
|
+
});
|
|
3157
|
+
edges.push({ kind: "deploys", from: jobAddress, to: address, confidence: 1 });
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
function normaliseWorkspacePath(dir) {
|
|
3161
|
+
const out = [];
|
|
3162
|
+
for (const part of toPosix3(dir).split("/")) {
|
|
3163
|
+
if (!part || part === ".") continue;
|
|
3164
|
+
if (part === "..") {
|
|
3165
|
+
out.pop();
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
out.push(part);
|
|
3169
|
+
}
|
|
3170
|
+
return out.join("/") || ".";
|
|
3171
|
+
}
|
|
3172
|
+
function triggerEvents(value) {
|
|
3173
|
+
if (typeof value === "string") return [value];
|
|
3174
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string").sort();
|
|
3175
|
+
if (value && typeof value === "object") return Object.keys(value).sort();
|
|
3176
|
+
return [];
|
|
3177
|
+
}
|
|
3178
|
+
function asStringList(value) {
|
|
3179
|
+
if (typeof value === "string") return [value];
|
|
3180
|
+
if (!Array.isArray(value)) return [];
|
|
3181
|
+
const out = [];
|
|
3182
|
+
for (const item of value) {
|
|
3183
|
+
if (typeof item === "string") out.push(item);
|
|
3184
|
+
else if (item && typeof item === "object") {
|
|
3185
|
+
const job = item.job;
|
|
3186
|
+
if (typeof job === "string") out.push(job);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
return out.sort();
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// src/engine/toolchain/link.ts
|
|
3193
|
+
var LINK_CONFIDENCE = {
|
|
3194
|
+
/** Compose names the Dockerfile path outright. */
|
|
3195
|
+
composeBuild: 0.95,
|
|
3196
|
+
/** A CI job builds tag X; a workload runs tag X. Same string, both sides. */
|
|
3197
|
+
imageIdentity: 0.8,
|
|
3198
|
+
/** A CI job applies Terraform in a directory that contains these resources. */
|
|
3199
|
+
terraformDir: 0.75,
|
|
3200
|
+
/** A Dockerfile copies a path that exists in the repository. */
|
|
3201
|
+
dockerfileCopy: 0.6
|
|
3202
|
+
};
|
|
3203
|
+
function linkToolchain(input) {
|
|
3204
|
+
const { nodes, edges, fileNodes, stageCopySources } = input;
|
|
3205
|
+
const out = /* @__PURE__ */ new Map();
|
|
3206
|
+
const counts = {};
|
|
3207
|
+
const add3 = (kind, src, dst, confidence, join10) => {
|
|
3208
|
+
if (src === dst) return;
|
|
3209
|
+
const id = edgeId(kind, src, dst);
|
|
3210
|
+
if (out.has(id)) return;
|
|
3211
|
+
out.set(id, {
|
|
3212
|
+
id,
|
|
3213
|
+
kind,
|
|
3214
|
+
src,
|
|
3215
|
+
dst,
|
|
3216
|
+
resolution: "heuristic",
|
|
3217
|
+
confidence,
|
|
3218
|
+
epistemic: "name-matched"
|
|
3219
|
+
});
|
|
3220
|
+
counts[join10] = (counts[join10] ?? 0) + 1;
|
|
3221
|
+
};
|
|
3222
|
+
const declared = new Set(edges.map((e) => `${e.kind} ${e.src} ${e.dst}`));
|
|
3223
|
+
const dockerfileByPath = /* @__PURE__ */ new Map();
|
|
3224
|
+
const dockerfileRefs = [];
|
|
3225
|
+
const imageSites = /* @__PURE__ */ new Map();
|
|
3226
|
+
const terraformDirMarkers = [];
|
|
3227
|
+
const terraformNodesByDir = /* @__PURE__ */ new Map();
|
|
3228
|
+
const stagesByDockerfile = /* @__PURE__ */ new Map();
|
|
3229
|
+
for (const node of nodes) {
|
|
3230
|
+
const qn = node.qualifiedName;
|
|
3231
|
+
if (qn.startsWith("dockerfile:")) {
|
|
3232
|
+
const target = qn.slice("dockerfile:".length);
|
|
3233
|
+
if (target.includes("#")) {
|
|
3234
|
+
push(stagesByDockerfile, target.split("#")[0], node);
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
if (toPosix3(node.file) === target) dockerfileByPath.set(target, node);
|
|
3238
|
+
else dockerfileRefs.push(node);
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
if (qn.startsWith("image:")) {
|
|
3242
|
+
const ref = parseImageRef(qn.slice("image:".length));
|
|
3243
|
+
if (ref) push(imageSites, imageIdentity(ref), node);
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
if (qn.startsWith("tfdir:")) {
|
|
3247
|
+
terraformDirMarkers.push(node);
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
if (node.lang === "terraform") push(terraformNodesByDir, dirOf(toPosix3(node.file)), node);
|
|
3251
|
+
}
|
|
3252
|
+
for (const ref of dockerfileRefs) {
|
|
3253
|
+
const target = dockerfileByPath.get(ref.qualifiedName.slice("dockerfile:".length));
|
|
3254
|
+
if (!target) continue;
|
|
3255
|
+
add3("builds_from", ref.id, target.id, LINK_CONFIDENCE.composeBuild, "compose\u2192dockerfile");
|
|
3256
|
+
}
|
|
3257
|
+
const producerOf = /* @__PURE__ */ new Map();
|
|
3258
|
+
const consumerOf = /* @__PURE__ */ new Map();
|
|
3259
|
+
for (const edge of edges) {
|
|
3260
|
+
if (edge.kind === "provisions") push(producerOf, edge.dst, edge.src);
|
|
3261
|
+
else if (edge.kind === "builds_from") push(consumerOf, edge.dst, edge.src);
|
|
3262
|
+
}
|
|
3263
|
+
for (const identity of [...imageSites.keys()].sort((a, b) => a.localeCompare(b, "en"))) {
|
|
3264
|
+
const sites = imageSites.get(identity) ?? [];
|
|
3265
|
+
if (sites.length < 2) continue;
|
|
3266
|
+
const producers = /* @__PURE__ */ new Set();
|
|
3267
|
+
const consumers = /* @__PURE__ */ new Set();
|
|
3268
|
+
for (const site of sites) {
|
|
3269
|
+
for (const id of producerOf.get(site.id) ?? []) producers.add(id);
|
|
3270
|
+
for (const id of consumerOf.get(site.id) ?? []) consumers.add(id);
|
|
3271
|
+
}
|
|
3272
|
+
for (const producer of [...producers].sort()) {
|
|
3273
|
+
for (const consumer of [...consumers].sort()) {
|
|
3274
|
+
if (declared.has(`deploys ${producer} ${consumer}`)) continue;
|
|
3275
|
+
add3("deploys", producer, consumer, LINK_CONFIDENCE.imageIdentity, "ci\u2192workload");
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
const ordered = [...sites].sort((a, b) => a.id.localeCompare(b.id, "en"));
|
|
3279
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
3280
|
+
add3("references", ordered[0].id, ordered[i].id, LINK_CONFIDENCE.imageIdentity, "image\u2194image");
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
const terraformDirs = [...terraformNodesByDir.keys()].sort((a, b) => a.localeCompare(b, "en"));
|
|
3284
|
+
for (const marker of [...terraformDirMarkers].sort((a, b) => a.id.localeCompare(b.id, "en"))) {
|
|
3285
|
+
const dir = marker.qualifiedName.slice("tfdir:".length);
|
|
3286
|
+
for (const resourceDir of terraformDirs) {
|
|
3287
|
+
if (resourceDir !== dir && !resourceDir.startsWith(`${dir}/`)) continue;
|
|
3288
|
+
for (const resource of terraformNodesByDir.get(resourceDir) ?? []) {
|
|
3289
|
+
if (resource.signature !== "terraform.resource" && resource.signature !== "terraform.module") {
|
|
3290
|
+
continue;
|
|
3291
|
+
}
|
|
3292
|
+
add3("deploys", marker.id, resource.id, LINK_CONFIDENCE.terraformDir, "ci\u2192terraform");
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
for (const file of [...stagesByDockerfile.keys()].sort((a, b) => a.localeCompare(b, "en"))) {
|
|
3297
|
+
const stages = [...stagesByDockerfile.get(file) ?? []].sort(
|
|
3298
|
+
(a, b) => a.qualifiedName.localeCompare(b.qualifiedName, "en")
|
|
3299
|
+
);
|
|
3300
|
+
for (const stage of stages) {
|
|
3301
|
+
const copied = stageCopySources.get(stage.qualifiedName);
|
|
3302
|
+
if (!copied?.length) continue;
|
|
3303
|
+
for (const path12 of copied) {
|
|
3304
|
+
const fileNodeId = fileNodes.get(toPosix3(path12));
|
|
3305
|
+
if (!fileNodeId) continue;
|
|
3306
|
+
add3("depends_on", stage.id, fileNodeId, LINK_CONFIDENCE.dockerfileCopy, "dockerfile\u2192source");
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
return {
|
|
3311
|
+
edges: [...out.values()].sort(
|
|
3312
|
+
(a, b) => a.kind.localeCompare(b.kind, "en") || a.src.localeCompare(b.src, "en") || a.dst.localeCompare(b.dst, "en")
|
|
3313
|
+
),
|
|
3314
|
+
counts
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
function push(map, key, value) {
|
|
3318
|
+
const list = map.get(key);
|
|
3319
|
+
if (list) list.push(value);
|
|
3320
|
+
else map.set(key, [value]);
|
|
3321
|
+
}
|
|
3322
|
+
function dirOf(rel2) {
|
|
3323
|
+
const idx = rel2.lastIndexOf("/");
|
|
3324
|
+
return idx === -1 ? "." : rel2.slice(0, idx);
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
// src/engine/toolchain/index.ts
|
|
3328
|
+
var EXTRACTORS = [
|
|
3329
|
+
terraformExtractor,
|
|
3330
|
+
dockerfileExtractor,
|
|
3331
|
+
githubActionsExtractor,
|
|
3332
|
+
gitlabCiExtractor,
|
|
3333
|
+
helmExtractor,
|
|
3334
|
+
composeExtractor,
|
|
3335
|
+
kubernetesExtractor
|
|
3336
|
+
];
|
|
3337
|
+
var HEAD_BYTES = 4096;
|
|
3338
|
+
var EMPTY_RESULT = {
|
|
3339
|
+
nodes: [],
|
|
3340
|
+
edges: [],
|
|
3341
|
+
warnings: [],
|
|
3342
|
+
counts: {},
|
|
3343
|
+
linkCounts: {}
|
|
3344
|
+
};
|
|
3345
|
+
function extractorFor(doc, head) {
|
|
3346
|
+
for (const extractor of EXTRACTORS) {
|
|
3347
|
+
try {
|
|
3348
|
+
if (extractor.matches(toPosix3(doc.rel), doc.category ?? doc.kind, head)) return extractor;
|
|
3349
|
+
} catch {
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
return null;
|
|
3353
|
+
}
|
|
3354
|
+
async function extractToolchain(docs, options = {}) {
|
|
3355
|
+
if (!docs.length) return EMPTY_RESULT;
|
|
3356
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
3357
|
+
const edges = /* @__PURE__ */ new Map();
|
|
3358
|
+
const warnings = [];
|
|
3359
|
+
const counts = {};
|
|
3360
|
+
const stageCopySources = /* @__PURE__ */ new Map();
|
|
3361
|
+
const ordered = [...docs].sort((a, b) => toPosix3(a.rel).localeCompare(toPosix3(b.rel), "en"));
|
|
3362
|
+
for (const doc of ordered) {
|
|
3363
|
+
let source;
|
|
3364
|
+
let head;
|
|
3365
|
+
try {
|
|
3366
|
+
const stat = fs12.statSync(doc.abs);
|
|
3367
|
+
if (stat.size > TOOLCHAIN_FILE_MAX_BYTES) continue;
|
|
3368
|
+
source = fs12.readFileSync(doc.abs, "utf8");
|
|
3369
|
+
head = source.slice(0, HEAD_BYTES);
|
|
3370
|
+
} catch {
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
const extractor = extractorFor(doc, head);
|
|
3374
|
+
if (!extractor) continue;
|
|
3375
|
+
const rel2 = toPosix3(doc.rel);
|
|
3376
|
+
let extraction;
|
|
3377
|
+
try {
|
|
3378
|
+
extraction = await extractor.extract(rel2, source);
|
|
3379
|
+
} catch (err) {
|
|
3380
|
+
warnings.push(`${rel2}: ${extractor.format} extraction failed (${err.message})`);
|
|
3381
|
+
continue;
|
|
3382
|
+
}
|
|
3383
|
+
if (extraction.warnings?.length) warnings.push(...extraction.warnings);
|
|
3384
|
+
for (const [stage, paths] of Object.entries(extraction.linkHints?.copySourcesByStage ?? {})) {
|
|
3385
|
+
if (paths.length) stageCopySources.set(stage, paths);
|
|
3386
|
+
}
|
|
3387
|
+
if (!extraction.nodes.length) continue;
|
|
3388
|
+
counts[extractor.format] = (counts[extractor.format] ?? 0) + 1;
|
|
3389
|
+
const idByQualifiedName = /* @__PURE__ */ new Map();
|
|
3390
|
+
for (const draft of extraction.nodes) {
|
|
3391
|
+
const id = nodeId({
|
|
3392
|
+
kind: draft.kind,
|
|
3393
|
+
qualifiedName: draft.qualifiedName,
|
|
3394
|
+
file: rel2,
|
|
3395
|
+
signature: draft.signature
|
|
3396
|
+
});
|
|
3397
|
+
if (!idByQualifiedName.has(draft.qualifiedName)) {
|
|
3398
|
+
idByQualifiedName.set(draft.qualifiedName, id);
|
|
3399
|
+
}
|
|
3400
|
+
if (nodes.has(id)) continue;
|
|
3401
|
+
nodes.set(id, toGraphNode(id, draft, rel2, extractor.format));
|
|
3402
|
+
}
|
|
3403
|
+
for (const draft of extraction.edges) {
|
|
3404
|
+
const src = idByQualifiedName.get(draft.from);
|
|
3405
|
+
const dst = idByQualifiedName.get(draft.to);
|
|
3406
|
+
if (!src || !dst || src === dst) continue;
|
|
3407
|
+
const id = edgeId(draft.kind, src, dst);
|
|
3408
|
+
if (edges.has(id)) continue;
|
|
3409
|
+
edges.set(id, {
|
|
3410
|
+
id,
|
|
3411
|
+
kind: draft.kind,
|
|
3412
|
+
src,
|
|
3413
|
+
dst,
|
|
3414
|
+
// Structural extraction from a declaration in the file itself.
|
|
3415
|
+
resolution: "heuristic",
|
|
3416
|
+
confidence: draft.confidence ?? 1,
|
|
3417
|
+
epistemic: "declared"
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
const sortedNodes = [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id, "en"));
|
|
3422
|
+
const sortedEdges = [...edges.values()].sort(byEdgeOrder);
|
|
3423
|
+
const linked = linkToolchain({
|
|
3424
|
+
nodes: sortedNodes,
|
|
3425
|
+
edges: sortedEdges,
|
|
3426
|
+
fileNodes: options.fileNodes ?? /* @__PURE__ */ new Map(),
|
|
3427
|
+
stageCopySources
|
|
3428
|
+
});
|
|
3429
|
+
const declaredIds = new Set(sortedEdges.map((e) => e.id));
|
|
3430
|
+
const allEdges = [...sortedEdges, ...linked.edges.filter((e) => !declaredIds.has(e.id))].sort(
|
|
3431
|
+
byEdgeOrder
|
|
3432
|
+
);
|
|
3433
|
+
return {
|
|
3434
|
+
nodes: sortedNodes,
|
|
3435
|
+
edges: allEdges,
|
|
3436
|
+
warnings: [...new Set(warnings)].sort(),
|
|
3437
|
+
counts,
|
|
3438
|
+
linkCounts: linked.counts
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
function byEdgeOrder(a, b) {
|
|
3442
|
+
return a.kind.localeCompare(b.kind, "en") || a.src.localeCompare(b.src, "en") || a.dst.localeCompare(b.dst, "en");
|
|
3443
|
+
}
|
|
3444
|
+
function toGraphNode(id, draft, rel2, format) {
|
|
3445
|
+
return {
|
|
3446
|
+
id,
|
|
3447
|
+
kind: draft.kind,
|
|
3448
|
+
name: draft.name,
|
|
3449
|
+
qualifiedName: draft.qualifiedName,
|
|
3450
|
+
file: rel2,
|
|
3451
|
+
span: draft.span,
|
|
3452
|
+
// `lang` is the format tag, so `--only` and the report can filter on it
|
|
3453
|
+
// without colliding with a source language id.
|
|
3454
|
+
lang: format,
|
|
3455
|
+
signature: draft.signature,
|
|
3456
|
+
doc: safeDoc(draft.doc),
|
|
3457
|
+
importance: draft.importance ?? 0.3,
|
|
3458
|
+
centrality: { degree: 0, pagerank: 0, betweenness: 0, eigenvector: 0 },
|
|
3459
|
+
area: -1,
|
|
3460
|
+
isHub: false,
|
|
3461
|
+
// Infrastructure has no notion of unit-test coverage; `null` is the
|
|
3462
|
+
// schema's "not analyzable", which is the honest answer.
|
|
3463
|
+
tested: null
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
1829
3466
|
function shardTsProjects(root, files) {
|
|
1830
3467
|
if (files.length === 0) return [];
|
|
1831
3468
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -1848,8 +3485,8 @@ function nearestProjectKey(root, startDir, cache) {
|
|
|
1848
3485
|
let dir = startDir;
|
|
1849
3486
|
const rootAbs = path11.resolve(root);
|
|
1850
3487
|
while (true) {
|
|
1851
|
-
if (
|
|
1852
|
-
const key =
|
|
3488
|
+
if (fs12.existsSync(path11.join(dir, "tsconfig.json")) || fs12.existsSync(path11.join(dir, "tsconfig.base.json")) || fs12.existsSync(path11.join(dir, "package.json"))) {
|
|
3489
|
+
const key = toPosix4(path11.relative(rootAbs, dir)) || ".";
|
|
1853
3490
|
cache.set(startDir, key);
|
|
1854
3491
|
return key;
|
|
1855
3492
|
}
|
|
@@ -1860,7 +3497,7 @@ function nearestProjectKey(root, startDir, cache) {
|
|
|
1860
3497
|
dir = path11.dirname(dir);
|
|
1861
3498
|
}
|
|
1862
3499
|
}
|
|
1863
|
-
function
|
|
3500
|
+
function toPosix4(p) {
|
|
1864
3501
|
return p.split(path11.sep).join("/");
|
|
1865
3502
|
}
|
|
1866
3503
|
|
|
@@ -1955,7 +3592,7 @@ var emptyNode = (partial) => ({
|
|
|
1955
3592
|
function extractManifests(root, opts = {}) {
|
|
1956
3593
|
const absRoot = path11.resolve(root);
|
|
1957
3594
|
const ig = buildRootIgnore3(absRoot, opts.exclude ?? []);
|
|
1958
|
-
const scopes = (opts.paths?.length ? opts.paths : ["."]).map((p) => path11.resolve(absRoot, p)).filter((p) =>
|
|
3595
|
+
const scopes = (opts.paths?.length ? opts.paths : ["."]).map((p) => path11.resolve(absRoot, p)).filter((p) => fs12.existsSync(p));
|
|
1959
3596
|
const found = /* @__PURE__ */ new Map();
|
|
1960
3597
|
for (const scope of scopes) {
|
|
1961
3598
|
walkManifests(absRoot, scope, ig, found);
|
|
@@ -1985,7 +3622,7 @@ function extractManifests(root, opts = {}) {
|
|
|
1985
3622
|
};
|
|
1986
3623
|
}
|
|
1987
3624
|
function ingestPackageJson(rel2, abs, nodes, edges) {
|
|
1988
|
-
const raw = JSON.parse(
|
|
3625
|
+
const raw = JSON.parse(fs12.readFileSync(abs, "utf8"));
|
|
1989
3626
|
const pkgName = raw.name && String(raw.name).trim() || path11.posix.dirname(rel2) || ".";
|
|
1990
3627
|
const localId = nodeId({ kind: "package", qualifiedName: pkgName, file: rel2 });
|
|
1991
3628
|
nodes.set(
|
|
@@ -2029,7 +3666,7 @@ function ingestPackageJson(rel2, abs, nodes, edges) {
|
|
|
2029
3666
|
return n;
|
|
2030
3667
|
}
|
|
2031
3668
|
function ingestGoMod(rel2, abs, nodes, edges) {
|
|
2032
|
-
const text =
|
|
3669
|
+
const text = fs12.readFileSync(abs, "utf8");
|
|
2033
3670
|
let moduleName = path11.posix.dirname(rel2) || ".";
|
|
2034
3671
|
const moduleMatch = /^\s*module\s+(\S+)/m.exec(text);
|
|
2035
3672
|
if (moduleMatch) moduleName = moduleMatch[1];
|
|
@@ -2113,9 +3750,9 @@ function addEdge(edges, kind, src, dst, confidence) {
|
|
|
2113
3750
|
function buildRootIgnore3(root, exclude) {
|
|
2114
3751
|
const ig = ignore();
|
|
2115
3752
|
const gitignorePath = path11.join(root, ".gitignore");
|
|
2116
|
-
if (
|
|
3753
|
+
if (fs12.existsSync(gitignorePath)) {
|
|
2117
3754
|
try {
|
|
2118
|
-
ig.add(
|
|
3755
|
+
ig.add(fs12.readFileSync(gitignorePath, "utf8"));
|
|
2119
3756
|
} catch {
|
|
2120
3757
|
}
|
|
2121
3758
|
}
|
|
@@ -2125,7 +3762,7 @@ function buildRootIgnore3(root, exclude) {
|
|
|
2125
3762
|
function walkManifests(root, dir, ig, found) {
|
|
2126
3763
|
let entries;
|
|
2127
3764
|
try {
|
|
2128
|
-
entries =
|
|
3765
|
+
entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
2129
3766
|
} catch {
|
|
2130
3767
|
return;
|
|
2131
3768
|
}
|
|
@@ -2158,7 +3795,7 @@ async function hashFilesParallel(jobs, concurrency) {
|
|
|
2158
3795
|
if (i >= jobs.length) return;
|
|
2159
3796
|
const job = jobs[i];
|
|
2160
3797
|
try {
|
|
2161
|
-
const buf = await
|
|
3798
|
+
const buf = await fs9.readFile(job.abs);
|
|
2162
3799
|
out[i] = { rel: job.rel, hash: hashBytes(buf), ok: true };
|
|
2163
3800
|
} catch {
|
|
2164
3801
|
out[i] = { rel: job.rel, hash: "", ok: false };
|
|
@@ -2189,7 +3826,7 @@ async function buildGraph(options) {
|
|
|
2189
3826
|
);
|
|
2190
3827
|
}
|
|
2191
3828
|
checkMemoryBudget("discovery", limits.memoryBudgetMb);
|
|
2192
|
-
const grammars = grammarSetVersion()
|
|
3829
|
+
const grammars = `${grammarSetVersion()}+${toolchainGrammarSetVersion()}`;
|
|
2193
3830
|
const cache = loadCache(root, {
|
|
2194
3831
|
toolVersion: VERSION,
|
|
2195
3832
|
grammars,
|
|
@@ -2206,7 +3843,7 @@ async function buildGraph(options) {
|
|
|
2206
3843
|
for (const file of files) {
|
|
2207
3844
|
let stat;
|
|
2208
3845
|
try {
|
|
2209
|
-
stat =
|
|
3846
|
+
stat = fs12.statSync(file.abs);
|
|
2210
3847
|
} catch {
|
|
2211
3848
|
continue;
|
|
2212
3849
|
}
|
|
@@ -2412,8 +4049,8 @@ async function buildGraph(options) {
|
|
|
2412
4049
|
});
|
|
2413
4050
|
for (const d of docs) {
|
|
2414
4051
|
try {
|
|
2415
|
-
const st =
|
|
2416
|
-
const hash = hashBytes(
|
|
4052
|
+
const st = fs12.statSync(d.abs);
|
|
4053
|
+
const hash = hashBytes(fs12.readFileSync(d.abs));
|
|
2417
4054
|
hashes.set(d.rel, hash);
|
|
2418
4055
|
fileStats.push({ rel: d.rel, size: st.size, mtimeMs: st.mtimeMs, hash });
|
|
2419
4056
|
} catch {
|
|
@@ -2421,14 +4058,29 @@ async function buildGraph(options) {
|
|
|
2421
4058
|
}
|
|
2422
4059
|
const docNodes = documentNodesFromDocs(docs);
|
|
2423
4060
|
if (docNodes.length) nodes = [...nodes, ...docNodes];
|
|
4061
|
+
timer.start("toolchain");
|
|
4062
|
+
const fileNodesByPath = /* @__PURE__ */ new Map();
|
|
4063
|
+
for (const node of nodes) {
|
|
4064
|
+
if (node.kind === "file") fileNodesByPath.set(node.file.replace(/\\/g, "/"), node.id);
|
|
4065
|
+
}
|
|
4066
|
+
const toolchainResult = await extractToolchain(docs, { fileNodes: fileNodesByPath });
|
|
4067
|
+
if (toolchainResult.nodes.length) nodes = [...nodes, ...toolchainResult.nodes];
|
|
4068
|
+
timer.end("toolchain");
|
|
2424
4069
|
timer.start("analyze");
|
|
2425
|
-
const
|
|
4070
|
+
const graphEdges = toolchainResult.edges.length ? [...linked.edges, ...toolchainResult.edges] : linked.edges;
|
|
4071
|
+
const analysis = analyze(nodes, graphEdges, {
|
|
2426
4072
|
cluster: options.cluster,
|
|
2427
4073
|
tier: options.analysisTier
|
|
2428
4074
|
});
|
|
2429
4075
|
timer.end("analyze");
|
|
2430
4076
|
checkMemoryBudget("analysis", limits.memoryBudgetMb);
|
|
2431
|
-
const languages = [
|
|
4077
|
+
const languages = [
|
|
4078
|
+
.../* @__PURE__ */ new Set([
|
|
4079
|
+
...parses.map((p) => p.lang),
|
|
4080
|
+
...docNodes.map((n) => n.lang),
|
|
4081
|
+
...toolchainResult.nodes.map((n) => n.lang)
|
|
4082
|
+
])
|
|
4083
|
+
].sort();
|
|
2432
4084
|
const edgeKinds = [...new Set(analysis.edges.map((e) => e.kind))].sort();
|
|
2433
4085
|
const corpusHash = computeCorpusHash(parses, hashes);
|
|
2434
4086
|
const nodeKindById = new Map(analysis.nodes.map((n) => [n.id, n.kind]));
|
|
@@ -2504,7 +4156,8 @@ async function buildGraph(options) {
|
|
|
2504
4156
|
tsc: tscStats,
|
|
2505
4157
|
scip: scipStats,
|
|
2506
4158
|
index: indexResult,
|
|
2507
|
-
warnings
|
|
4159
|
+
warnings,
|
|
4160
|
+
fileRoles: fileRolesFromParses(parses)
|
|
2508
4161
|
};
|
|
2509
4162
|
}
|
|
2510
4163
|
function toRepoRel(p) {
|
|
@@ -2518,9 +4171,9 @@ function loadScipIndex(root, explicit) {
|
|
|
2518
4171
|
].filter((p) => Boolean(p));
|
|
2519
4172
|
for (const file of candidates) {
|
|
2520
4173
|
const abs = path11.isAbsolute(file) ? file : path11.resolve(root, file);
|
|
2521
|
-
if (!
|
|
4174
|
+
if (!fs12.existsSync(abs)) continue;
|
|
2522
4175
|
try {
|
|
2523
|
-
const index = decodeScipIndex(new Uint8Array(
|
|
4176
|
+
const index = decodeScipIndex(new Uint8Array(fs12.readFileSync(abs)));
|
|
2524
4177
|
if (index.documents.length) {
|
|
2525
4178
|
return { index, tool: index.toolVersion ? `${index.toolName} ${index.toolVersion}` : index.toolName };
|
|
2526
4179
|
}
|
|
@@ -2612,36 +4265,6 @@ function diffHint(x, y) {
|
|
|
2612
4265
|
}
|
|
2613
4266
|
return "outputs differ in length";
|
|
2614
4267
|
}
|
|
2615
|
-
|
|
2616
|
-
// src/util/exit.ts
|
|
2617
|
-
var ExitCode = {
|
|
2618
|
-
OK: 0,
|
|
2619
|
-
ERROR: 1,
|
|
2620
|
-
GATE_FAILED: 2,
|
|
2621
|
-
NOT_FOUND: 3,
|
|
2622
|
-
NON_DETERMINISTIC: 4,
|
|
2623
|
-
USAGE_ERROR: 5,
|
|
2624
|
-
/**
|
|
2625
|
-
* A required optional module (engine) is not installed and could not be
|
|
2626
|
-
* fetched. Deliberately distinct from GATE_FAILED so a CI gate can never
|
|
2627
|
-
* read "engine missing" as "gate passed" or "gate failed".
|
|
2628
|
-
*/
|
|
2629
|
-
ENGINE_UNAVAILABLE: 6
|
|
2630
|
-
};
|
|
2631
|
-
var CliError = class extends Error {
|
|
2632
|
-
code;
|
|
2633
|
-
constructor(message, code = ExitCode.ERROR) {
|
|
2634
|
-
super(message);
|
|
2635
|
-
this.name = "CliError";
|
|
2636
|
-
this.code = code;
|
|
2637
|
-
}
|
|
2638
|
-
};
|
|
2639
|
-
function usageError(message) {
|
|
2640
|
-
return new CliError(message, ExitCode.USAGE_ERROR);
|
|
2641
|
-
}
|
|
2642
|
-
function engineUnavailable(message) {
|
|
2643
|
-
return new CliError(message, ExitCode.ENGINE_UNAVAILABLE);
|
|
2644
|
-
}
|
|
2645
4268
|
var SNAPSHOT_VERSION = "vg-freshness/1";
|
|
2646
4269
|
function snapshotPath(root) {
|
|
2647
4270
|
return path11.join(cacheDir(root), "freshness.json");
|
|
@@ -2651,14 +4274,14 @@ function writeSnapshot(root, corpusHash, fileStats, scope = {}) {
|
|
|
2651
4274
|
for (const f of fileStats) files[f.rel] = { size: f.size, mtimeMs: f.mtimeMs, hash: f.hash };
|
|
2652
4275
|
const snapshot = { version: SNAPSHOT_VERSION, corpusHash, scope: pruneScope(scope), files };
|
|
2653
4276
|
try {
|
|
2654
|
-
|
|
2655
|
-
|
|
4277
|
+
fs12.mkdirSync(cacheDir(root), { recursive: true });
|
|
4278
|
+
fs12.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
|
|
2656
4279
|
} catch {
|
|
2657
4280
|
}
|
|
2658
4281
|
}
|
|
2659
4282
|
function loadSnapshot(root) {
|
|
2660
4283
|
try {
|
|
2661
|
-
const loaded = JSON.parse(
|
|
4284
|
+
const loaded = JSON.parse(fs12.readFileSync(snapshotPath(root), "utf8"));
|
|
2662
4285
|
if (loaded.version === SNAPSHOT_VERSION && loaded.files && typeof loaded.corpusHash === "string") {
|
|
2663
4286
|
return { ...loaded, scope: loaded.scope ?? {} };
|
|
2664
4287
|
}
|
|
@@ -2701,7 +4324,7 @@ function probeFreshness(root) {
|
|
|
2701
4324
|
}
|
|
2702
4325
|
let stat;
|
|
2703
4326
|
try {
|
|
2704
|
-
stat =
|
|
4327
|
+
stat = fs12.statSync(file.abs);
|
|
2705
4328
|
} catch {
|
|
2706
4329
|
drift.removed.push(file.rel);
|
|
2707
4330
|
continue;
|
|
@@ -2719,7 +4342,7 @@ function probeFreshness(root) {
|
|
|
2719
4342
|
}
|
|
2720
4343
|
let hash = "";
|
|
2721
4344
|
try {
|
|
2722
|
-
hash = hashBytes(
|
|
4345
|
+
hash = hashBytes(fs12.readFileSync(file.abs));
|
|
2723
4346
|
} catch {
|
|
2724
4347
|
drift.removed.push(file.rel);
|
|
2725
4348
|
continue;
|
|
@@ -2736,7 +4359,7 @@ function probeFreshness(root) {
|
|
|
2736
4359
|
}
|
|
2737
4360
|
if (absorbed && !hasDrift(drift)) {
|
|
2738
4361
|
try {
|
|
2739
|
-
|
|
4362
|
+
fs12.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
|
|
2740
4363
|
} catch {
|
|
2741
4364
|
}
|
|
2742
4365
|
}
|
|
@@ -2981,12 +4604,12 @@ function detectAssistants(root, opts = {}) {
|
|
|
2981
4604
|
const which = opts.which ?? whichOnPath;
|
|
2982
4605
|
const found = [];
|
|
2983
4606
|
for (const assistant of ASSISTANTS) {
|
|
2984
|
-
const repoHit = (assistant.markers ?? []).find((m) =>
|
|
4607
|
+
const repoHit = (assistant.markers ?? []).find((m) => fs12.existsSync(path11.join(root, m)));
|
|
2985
4608
|
if (repoHit) {
|
|
2986
4609
|
found.push({ assistant, via: "repo", marker: repoHit });
|
|
2987
4610
|
continue;
|
|
2988
4611
|
}
|
|
2989
|
-
const homeHit = (assistant.homeMarkers ?? []).find((m) =>
|
|
4612
|
+
const homeHit = (assistant.homeMarkers ?? []).find((m) => fs12.existsSync(path11.join(home, m)));
|
|
2990
4613
|
if (homeHit) {
|
|
2991
4614
|
found.push({ assistant, via: "home", marker: homeHit });
|
|
2992
4615
|
continue;
|
|
@@ -3047,27 +4670,27 @@ function uninstallAssistant(a, root, purge) {
|
|
|
3047
4670
|
const file = path11.join(root, a.nudge.file);
|
|
3048
4671
|
if (a.nudge.kind === "block") {
|
|
3049
4672
|
if (removeBlock(file)) removed.push(a.nudge.file);
|
|
3050
|
-
} else if (
|
|
3051
|
-
|
|
4673
|
+
} else if (fs12.existsSync(file)) {
|
|
4674
|
+
fs12.rmSync(file);
|
|
3052
4675
|
removed.push(a.nudge.file);
|
|
3053
4676
|
}
|
|
3054
4677
|
}
|
|
3055
4678
|
if (purge && a.skill) {
|
|
3056
4679
|
const file = path11.join(root, a.skill);
|
|
3057
|
-
if (
|
|
3058
|
-
|
|
4680
|
+
if (fs12.existsSync(file)) {
|
|
4681
|
+
fs12.rmSync(file);
|
|
3059
4682
|
removed.push(a.skill);
|
|
3060
4683
|
}
|
|
3061
4684
|
}
|
|
3062
4685
|
return removed;
|
|
3063
4686
|
}
|
|
3064
4687
|
function isAssistantInstalled(a, root) {
|
|
3065
|
-
if (a.skill &&
|
|
4688
|
+
if (a.skill && fs12.existsSync(path11.join(root, a.skill))) return true;
|
|
3066
4689
|
if (a.mcp) {
|
|
3067
4690
|
const file = path11.join(root, a.mcp.file);
|
|
3068
4691
|
if (a.mcp.format === "toml") {
|
|
3069
4692
|
if (hasTomlMcp(file, a.mcp)) return true;
|
|
3070
|
-
} else if (
|
|
4693
|
+
} else if (fs12.existsSync(file)) {
|
|
3071
4694
|
try {
|
|
3072
4695
|
const config = readJson(file);
|
|
3073
4696
|
const bag = config[a.mcp.key];
|
|
@@ -3078,7 +4701,7 @@ function isAssistantInstalled(a, root) {
|
|
|
3078
4701
|
}
|
|
3079
4702
|
if (a.nudge) {
|
|
3080
4703
|
const file = path11.join(root, a.nudge.file);
|
|
3081
|
-
if (!
|
|
4704
|
+
if (!fs12.existsSync(file)) return false;
|
|
3082
4705
|
const text = readTextSafe(file);
|
|
3083
4706
|
if (!text) return false;
|
|
3084
4707
|
if (a.nudge.kind === "block") return text.includes(NUDGE_BEGIN);
|
|
@@ -3087,8 +4710,8 @@ function isAssistantInstalled(a, root) {
|
|
|
3087
4710
|
return false;
|
|
3088
4711
|
}
|
|
3089
4712
|
function writeFileEnsured(file, content) {
|
|
3090
|
-
|
|
3091
|
-
|
|
4713
|
+
fs12.mkdirSync(path11.dirname(file), { recursive: true });
|
|
4714
|
+
fs12.writeFileSync(file, content);
|
|
3092
4715
|
}
|
|
3093
4716
|
function writeNavigationConfig(root) {
|
|
3094
4717
|
ensureVibgrateGitignore(root);
|
|
@@ -3129,7 +4752,7 @@ function upsertTomlMcp(file, target, launch) {
|
|
|
3129
4752
|
command = ${tomlString(entry.command)}
|
|
3130
4753
|
args = [${entry.args.map(tomlString).join(", ")}]
|
|
3131
4754
|
`;
|
|
3132
|
-
const existing =
|
|
4755
|
+
const existing = fs12.existsSync(file) ? fs12.readFileSync(file, "utf8") : "";
|
|
3133
4756
|
const re = tomlTableRe(target.key);
|
|
3134
4757
|
const next = re.test(existing) ? existing.replace(re, (m) => m.startsWith("\n") ? `
|
|
3135
4758
|
${table}` : table) : existing.length ? `${existing.replace(/\s*$/, "")}
|
|
@@ -3138,7 +4761,7 @@ ${table}` : table;
|
|
|
3138
4761
|
writeFileEnsured(file, next);
|
|
3139
4762
|
}
|
|
3140
4763
|
function removeTomlMcp(file, target) {
|
|
3141
|
-
if (!
|
|
4764
|
+
if (!fs12.existsSync(file)) return false;
|
|
3142
4765
|
const existing = readTextSafe(file);
|
|
3143
4766
|
if (existing === null) return false;
|
|
3144
4767
|
const re = tomlTableRe(target.key);
|
|
@@ -3148,13 +4771,13 @@ function removeTomlMcp(file, target) {
|
|
|
3148
4771
|
return true;
|
|
3149
4772
|
}
|
|
3150
4773
|
function hasTomlMcp(file, target) {
|
|
3151
|
-
if (!
|
|
4774
|
+
if (!fs12.existsSync(file)) return false;
|
|
3152
4775
|
const text = readTextSafe(file);
|
|
3153
4776
|
return text !== null && tomlTableRe(target.key).test(text);
|
|
3154
4777
|
}
|
|
3155
4778
|
function removeMcp(file, target) {
|
|
3156
4779
|
if (target.format === "toml") return removeTomlMcp(file, target);
|
|
3157
|
-
if (!
|
|
4780
|
+
if (!fs12.existsSync(file)) return false;
|
|
3158
4781
|
const config = readJson(file);
|
|
3159
4782
|
const bag = config[target.key];
|
|
3160
4783
|
if (!bag || !(bag.vg !== void 0)) return false;
|
|
@@ -3174,7 +4797,7 @@ function refreshInstalledInstructions(root, smallRepo) {
|
|
|
3174
4797
|
for (const a of ASSISTANTS) {
|
|
3175
4798
|
if (a.skill) {
|
|
3176
4799
|
const file = path11.join(root, a.skill);
|
|
3177
|
-
if (
|
|
4800
|
+
if (fs12.existsSync(file)) {
|
|
3178
4801
|
const text = readTextSafe(file);
|
|
3179
4802
|
const from = text === null ? null : staleVersion(text, "# vg \u2014 the code map");
|
|
3180
4803
|
if (from !== null) {
|
|
@@ -3185,7 +4808,7 @@ function refreshInstalledInstructions(root, smallRepo) {
|
|
|
3185
4808
|
}
|
|
3186
4809
|
if (a.nudge) {
|
|
3187
4810
|
const file = path11.join(root, a.nudge.file);
|
|
3188
|
-
if (!
|
|
4811
|
+
if (!fs12.existsSync(file)) continue;
|
|
3189
4812
|
const text = readTextSafe(file);
|
|
3190
4813
|
if (text === null) continue;
|
|
3191
4814
|
if (a.nudge.kind === "block") {
|
|
@@ -3209,7 +4832,7 @@ function refreshInstalledInstructions(root, smallRepo) {
|
|
|
3209
4832
|
}
|
|
3210
4833
|
function readTextSafe(file) {
|
|
3211
4834
|
try {
|
|
3212
|
-
return
|
|
4835
|
+
return fs12.readFileSync(file, "utf8");
|
|
3213
4836
|
} catch {
|
|
3214
4837
|
return null;
|
|
3215
4838
|
}
|
|
@@ -3228,8 +4851,8 @@ ${stripMarkers(nudgeMarkdown(smallRepo, client))}` : stripMarkers(nudgeMarkdown(
|
|
|
3228
4851
|
upsertBlock(file, nudgeMarkdown(smallRepo, client));
|
|
3229
4852
|
}
|
|
3230
4853
|
function upsertBlock(file, block) {
|
|
3231
|
-
|
|
3232
|
-
let existing =
|
|
4854
|
+
fs12.mkdirSync(path11.dirname(file), { recursive: true });
|
|
4855
|
+
let existing = fs12.existsSync(file) ? fs12.readFileSync(file, "utf8") : "";
|
|
3233
4856
|
const re = new RegExp(`${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}`);
|
|
3234
4857
|
if (re.test(existing)) existing = existing.replace(re, block);
|
|
3235
4858
|
else existing = existing.length ? `${existing.replace(/\s*$/, "")}
|
|
@@ -3237,20 +4860,20 @@ function upsertBlock(file, block) {
|
|
|
3237
4860
|
${block}
|
|
3238
4861
|
` : `${block}
|
|
3239
4862
|
`;
|
|
3240
|
-
|
|
4863
|
+
fs12.writeFileSync(file, existing);
|
|
3241
4864
|
}
|
|
3242
4865
|
function removeBlock(file) {
|
|
3243
|
-
if (!
|
|
3244
|
-
const existing =
|
|
4866
|
+
if (!fs12.existsSync(file)) return false;
|
|
4867
|
+
const existing = fs12.readFileSync(file, "utf8");
|
|
3245
4868
|
const re = new RegExp(`\\n*${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}\\n*`);
|
|
3246
4869
|
if (!re.test(existing)) return false;
|
|
3247
|
-
|
|
4870
|
+
fs12.writeFileSync(file, existing.replace(re, "\n"));
|
|
3248
4871
|
return true;
|
|
3249
4872
|
}
|
|
3250
4873
|
function readJson(file) {
|
|
3251
|
-
if (!
|
|
4874
|
+
if (!fs12.existsSync(file)) return {};
|
|
3252
4875
|
try {
|
|
3253
|
-
const parsed = JSON.parse(
|
|
4876
|
+
const parsed = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
3254
4877
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3255
4878
|
return parsed;
|
|
3256
4879
|
}
|
|
@@ -3269,6 +4892,6 @@ function escapeRe(s) {
|
|
|
3269
4892
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3270
4893
|
}
|
|
3271
4894
|
|
|
3272
|
-
export { ASSISTANTS,
|
|
3273
|
-
//# sourceMappingURL=chunk-
|
|
3274
|
-
//# sourceMappingURL=chunk-
|
|
4895
|
+
export { ASSISTANTS, ResourceLimitError, SKIP_DIRS, SKIP_FILES, SMALL_REPO_FILES, UsageError, analyze, applyCoverage, assistantById, buildFacts, buildGraph, decodeScipIndex, detectAssistants, detectServeLaunch, discover, driftCount, epistemicBreakdown, groundGraph, hasDrift, installAssistant, isAssistantInstalled, isSkippedDirName, loadCoverage, loadSnapshot, probeFreshness, refreshInstalledInstructions, resolveLimits, scipEdges, uninstallAssistant, verifyDeterminism, versionMarker, writeNavigationConfig, writeSnapshot };
|
|
4896
|
+
//# sourceMappingURL=chunk-E2FAQREZ.js.map
|
|
4897
|
+
//# sourceMappingURL=chunk-E2FAQREZ.js.map
|