@vibgrate/cli 2026.814.2 → 2026.817.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-CUYQKC6S.js +26 -0
- package/dist/{agent-5ANHRYEI.js.map → agent-CUYQKC6S.js.map} +1 -1
- package/dist/{approvals-T2KXPJLV.js → approvals-IPMUDEDV.js} +5 -5
- package/dist/{approvals-T2KXPJLV.js.map → approvals-IPMUDEDV.js.map} +1 -1
- package/dist/baseline-NZFGLMER.js +9 -0
- package/dist/{baseline-WVD6P5IO.js.map → baseline-NZFGLMER.js.map} +1 -1
- package/dist/{chunk-TFAXW7VM.js → chunk-2BURPMYA.js} +3 -3
- package/dist/{chunk-TFAXW7VM.js.map → chunk-2BURPMYA.js.map} +1 -1
- package/dist/{chunk-X76VPOYH.js → chunk-2GSMCOIC.js} +4 -4
- package/dist/{chunk-X76VPOYH.js.map → chunk-2GSMCOIC.js.map} +1 -1
- package/dist/{chunk-KGPX4EUN.js → chunk-2TJ5EY4I.js} +4 -4
- package/dist/{chunk-KGPX4EUN.js.map → chunk-2TJ5EY4I.js.map} +1 -1
- package/dist/{chunk-BH54SLGZ.js → chunk-3GZSDY35.js} +4 -4
- package/dist/{chunk-BH54SLGZ.js.map → chunk-3GZSDY35.js.map} +1 -1
- 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-7NMXE6KG.js → chunk-45AKSS4A.js} +4 -4
- package/dist/{chunk-7NMXE6KG.js.map → chunk-45AKSS4A.js.map} +1 -1
- package/dist/{chunk-FOW5IF73.js → chunk-4W5SYK2M.js} +11 -34
- package/dist/chunk-4W5SYK2M.js.map +1 -0
- package/dist/{chunk-UTNNGKKX.js → chunk-6NOGNOE4.js} +4 -4
- package/dist/chunk-6NOGNOE4.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-YNV5M3JW.js → chunk-A7EKFLAN.js} +8 -8
- package/dist/{chunk-YNV5M3JW.js.map → chunk-A7EKFLAN.js.map} +1 -1
- package/dist/{chunk-N6ZMZ5AI.js → chunk-ANYL7SUI.js} +349 -46
- package/dist/chunk-ANYL7SUI.js.map +1 -0
- package/dist/{chunk-75LJM5XD.js → chunk-ENFSSUNK.js} +7 -3
- package/dist/chunk-ENFSSUNK.js.map +1 -0
- package/dist/{chunk-3LSG7Z4I.js → chunk-EZ7UC5Y5.js} +25 -4
- package/dist/chunk-EZ7UC5Y5.js.map +1 -0
- package/dist/{chunk-WWGLVF7D.js → chunk-FD2HPGX2.js} +8 -8
- package/dist/{chunk-WWGLVF7D.js.map → chunk-FD2HPGX2.js.map} +1 -1
- 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-FMS6G67U.js +467 -0
- package/dist/chunk-FMS6G67U.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-JCN6MSXQ.js → chunk-JHLCKMKF.js} +16 -29
- package/dist/chunk-JHLCKMKF.js.map +1 -0
- package/dist/{chunk-IHLLWBZU.js → chunk-K267DHCF.js} +6 -6
- package/dist/{chunk-IHLLWBZU.js.map → chunk-K267DHCF.js.map} +1 -1
- package/dist/{chunk-WQLZSVNF.js → chunk-MIYXVY57.js} +7 -6
- package/dist/chunk-MIYXVY57.js.map +1 -0
- package/dist/chunk-MS6V5XKB.js +33 -0
- package/dist/chunk-MS6V5XKB.js.map +1 -0
- package/dist/{chunk-5GLQNMTI.js → chunk-NHB53JTV.js} +6 -6
- package/dist/{chunk-5GLQNMTI.js.map → chunk-NHB53JTV.js.map} +1 -1
- package/dist/{chunk-DSUGC7F5.js → chunk-NKRWKCME.js} +4 -4
- package/dist/{chunk-DSUGC7F5.js.map → chunk-NKRWKCME.js.map} +1 -1
- package/dist/{chunk-7QDHQLUS.js → chunk-NQUWC2IA.js} +13 -11
- package/dist/chunk-NQUWC2IA.js.map +1 -0
- package/dist/{chunk-NKFXXP6W.js → chunk-OWSXI3PJ.js} +8 -6
- package/dist/chunk-OWSXI3PJ.js.map +1 -0
- package/dist/chunk-OZ3IFXRD.js +22 -0
- package/dist/chunk-OZ3IFXRD.js.map +1 -0
- package/dist/chunk-PV76EO4G.js +6 -0
- package/dist/{chunk-2H2U3DC7.js.map → chunk-PV76EO4G.js.map} +1 -1
- package/dist/{chunk-2U37PFB2.js → chunk-QPXVTQ7O.js} +3 -3
- package/dist/{chunk-2U37PFB2.js.map → chunk-QPXVTQ7O.js.map} +1 -1
- package/dist/{chunk-TNROG5CO.js → chunk-RJCRWQNS.js} +1855 -175
- package/dist/chunk-RJCRWQNS.js.map +1 -0
- package/dist/{chunk-54RGT2T5.js → chunk-RQAKBRAV.js} +3 -3
- package/dist/{chunk-54RGT2T5.js.map → chunk-RQAKBRAV.js.map} +1 -1
- package/dist/{chunk-YQNTIBKS.js → chunk-UBEVY5UT.js} +6 -6
- package/dist/{chunk-YQNTIBKS.js.map → chunk-UBEVY5UT.js.map} +1 -1
- package/dist/{chunk-IHXNOY3R.js → chunk-YL4OIUA5.js} +12 -12
- package/dist/{chunk-IHXNOY3R.js.map → chunk-YL4OIUA5.js.map} +1 -1
- package/dist/{chunk-TVDYEHIM.js → chunk-ZSECAGSU.js} +3 -3
- package/dist/{chunk-TVDYEHIM.js.map → chunk-ZSECAGSU.js.map} +1 -1
- package/dist/cli.js +416 -717
- package/dist/cli.js.map +1 -1
- package/dist/daemon-J2HX4GY3.js +25 -0
- package/dist/daemon-J2HX4GY3.js.map +1 -0
- package/dist/embed-worker-main.js +4 -4
- package/dist/ensure-map-5C6KG66X.js +32 -0
- package/dist/{ensure-map-HQ7A5NDL.js.map → ensure-map-5C6KG66X.js.map} +1 -1
- package/dist/graph-backend-HH5IIQ55.js +14 -0
- package/dist/{graph-backend-W2TNTMY7.js.map → graph-backend-HH5IIQ55.js.map} +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.js +18 -16
- package/dist/index.js.map +1 -1
- package/dist/{interactive-EW2VJ3BZ.js → interactive-FXPAZAUJ.js} +33 -31
- package/dist/interactive-FXPAZAUJ.js.map +1 -0
- package/dist/load-OR2XVGAV.js +10 -0
- package/dist/{load-64RK5O6C.js.map → load-OR2XVGAV.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 +1 -1
- package/dist/{run-outcome-6QNQIHPV.js → run-outcome-M4546HLJ.js} +6 -6
- package/dist/{run-outcome-6QNQIHPV.js.map → run-outcome-M4546HLJ.js.map} +1 -1
- package/dist/runtime-session-6KKMFS6Q.js +21 -0
- package/dist/{runtime-session-RJHH3KMF.js.map → runtime-session-6KKMFS6Q.js.map} +1 -1
- package/dist/session-HCFJUSKN.js +16 -0
- package/dist/{session-4BGYASOH.js.map → session-HCFJUSKN.js.map} +1 -1
- package/dist/{stream-json-GY5N7WZT.js → stream-json-4DL65D6U.js} +16 -16
- package/dist/{stream-json-GY5N7WZT.js.map → stream-json-4DL65D6U.js.map} +1 -1
- package/dist/{types-DLH-Ritr.d.ts → types-8SYlXhzm.d.ts} +57 -6
- package/dist/version-UPIC3F4P.js +4 -0
- package/dist/{version-DPAMQW6M.js.map → version-UPIC3F4P.js.map} +1 -1
- package/dist/{vg-mcp-bridge-IGHJKKSL.js → vg-mcp-bridge-FZGX6DYO.js} +10 -10
- package/dist/{vg-mcp-bridge-IGHJKKSL.js.map → vg-mcp-bridge-FZGX6DYO.js.map} +1 -1
- package/dist/{vgd-ORPA5C2K.js → vgd-QR26FAZD.js} +9 -9
- package/dist/vgd-QR26FAZD.js.map +1 -0
- package/grammars/tree-sitter-hcl.wasm +0 -0
- package/package.json +3 -2
- package/dist/agent-5ANHRYEI.js +0 -26
- package/dist/baseline-WVD6P5IO.js +0 -9
- package/dist/chunk-2H2U3DC7.js +0 -6
- 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-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-TNROG5CO.js.map +0 -1
- package/dist/chunk-UTNNGKKX.js.map +0 -1
- package/dist/chunk-WQLZSVNF.js.map +0 -1
- package/dist/ensure-map-HQ7A5NDL.js +0 -30
- 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/runtime-session-RJHH3KMF.js +0 -21
- package/dist/session-4BGYASOH.js +0 -16
- package/dist/version-DPAMQW6M.js +0 -4
- package/dist/vgd-ORPA5C2K.js.map +0 -1
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { FREE_PACK, navigationToolsetConfig, deferredToolNames, HOT_TOOLS } from './chunk-
|
|
1
|
+
import { FREE_PACK, navigationToolsetConfig, deferredToolNames, HOT_TOOLS } from './chunk-FD2HPGX2.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-
|
|
4
|
+
import { indexFor } from './chunk-2TJ5EY4I.js';
|
|
5
|
+
import { writeGraphIndex, SCHEMA_VERSION, loadGraph } from './chunk-MIYXVY57.js';
|
|
6
|
+
import { init_cli_invocation, whichOnPath, isInstalledOwnBinary } from './chunk-EZ7UC5Y5.js';
|
|
7
|
+
import { langById, grammarSetVersion, langForExtension, grammarDirs, setGrammarsOverride, parseSource, ensureParserInit } from './chunk-6NOGNOE4.js';
|
|
7
8
|
import { redactSecrets } from './chunk-RXP66R2E.js';
|
|
8
|
-
import { loadCache, cacheDir } from './chunk-
|
|
9
|
-
import { serializeGraph, stableStringify, ensureVibgrateGitignore } from './chunk-
|
|
10
|
-
import { VERSION } from './chunk-
|
|
9
|
+
import { loadCache, cacheDir } from './chunk-QPXVTQ7O.js';
|
|
10
|
+
import { serializeGraph, stableStringify, ensureVibgrateGitignore } from './chunk-ENFSSUNK.js';
|
|
11
|
+
import { VERSION } from './chunk-PV76EO4G.js';
|
|
11
12
|
import { shortId, canonicalize, hashString, hashBytes } from './chunk-JFGYT6BI.js';
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
13
|
+
import * as fs13 from 'fs';
|
|
14
|
+
import * as path12 from 'path';
|
|
14
15
|
import ignore from 'ignore';
|
|
15
16
|
import * as v8 from 'v8';
|
|
16
17
|
import Graph from 'graphology';
|
|
@@ -22,7 +23,10 @@ import * as os from 'os';
|
|
|
22
23
|
import { fileURLToPath } from 'url';
|
|
23
24
|
import ts from 'typescript';
|
|
24
25
|
import * as crypto from 'crypto';
|
|
25
|
-
import
|
|
26
|
+
import YAML from 'yaml';
|
|
27
|
+
import { createRequire } from 'module';
|
|
28
|
+
import { Parser, Language } from 'web-tree-sitter';
|
|
29
|
+
import * as fs10 from 'fs/promises';
|
|
26
30
|
|
|
27
31
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
28
32
|
// Version control, IDE & tool metadata
|
|
@@ -187,19 +191,19 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
|
187
191
|
"flake.lock"
|
|
188
192
|
]);
|
|
189
193
|
function toPosix(p) {
|
|
190
|
-
return p.split(
|
|
194
|
+
return p.split(path12.sep).join("/");
|
|
191
195
|
}
|
|
192
196
|
function buildRootIgnore(root, exclude) {
|
|
193
197
|
const ig = ignore();
|
|
194
|
-
const gitignorePath =
|
|
195
|
-
if (
|
|
196
|
-
ig.add(
|
|
198
|
+
const gitignorePath = path12.join(root, ".gitignore");
|
|
199
|
+
if (fs13.existsSync(gitignorePath)) {
|
|
200
|
+
ig.add(fs13.readFileSync(gitignorePath, "utf8"));
|
|
197
201
|
}
|
|
198
202
|
if (exclude.length) ig.add(exclude);
|
|
199
203
|
return ig;
|
|
200
204
|
}
|
|
201
205
|
function discover(options) {
|
|
202
|
-
const root =
|
|
206
|
+
const root = path12.resolve(options.root);
|
|
203
207
|
const onlyLangs = (options.only ?? []).filter(Boolean);
|
|
204
208
|
const allowLang = (lang) => onlyLangs.length === 0 || onlyLangs.includes(lang.id);
|
|
205
209
|
for (const id of onlyLangs) {
|
|
@@ -208,27 +212,27 @@ function discover(options) {
|
|
|
208
212
|
}
|
|
209
213
|
}
|
|
210
214
|
const rootIg = buildRootIgnore(root, options.exclude ?? []);
|
|
211
|
-
const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) =>
|
|
215
|
+
const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path12.resolve(root, p)) : [root]).filter((p) => fs13.existsSync(p));
|
|
212
216
|
const found = /* @__PURE__ */ new Map();
|
|
213
217
|
const considerFile = (abs) => {
|
|
214
|
-
const rel2 = toPosix(
|
|
218
|
+
const rel2 = toPosix(path12.relative(root, abs));
|
|
215
219
|
if (rel2.startsWith("..")) return;
|
|
216
220
|
if (rel2 === "" || rootIg.ignores(rel2)) return;
|
|
217
|
-
if (SKIP_FILES.has(
|
|
218
|
-
const lang = langForExtension(
|
|
221
|
+
if (SKIP_FILES.has(path12.basename(abs).toLowerCase())) return;
|
|
222
|
+
const lang = langForExtension(path12.extname(abs));
|
|
219
223
|
if (!lang || !allowLang(lang)) return;
|
|
220
224
|
found.set(rel2, { rel: rel2, abs, lang });
|
|
221
225
|
};
|
|
222
226
|
const walk = (dir) => {
|
|
223
227
|
let entries;
|
|
224
228
|
try {
|
|
225
|
-
entries =
|
|
229
|
+
entries = fs13.readdirSync(dir, { withFileTypes: true });
|
|
226
230
|
} catch {
|
|
227
231
|
return;
|
|
228
232
|
}
|
|
229
233
|
for (const entry of entries) {
|
|
230
|
-
const abs =
|
|
231
|
-
const rel2 = toPosix(
|
|
234
|
+
const abs = path12.join(dir, entry.name);
|
|
235
|
+
const rel2 = toPosix(path12.relative(root, abs));
|
|
232
236
|
if (entry.isDirectory()) {
|
|
233
237
|
if (isSkippedDirName(entry.name)) continue;
|
|
234
238
|
if (rel2 && rootIg.ignores(`${rel2}/`)) continue;
|
|
@@ -239,7 +243,7 @@ function discover(options) {
|
|
|
239
243
|
}
|
|
240
244
|
};
|
|
241
245
|
for (const scope of scopeAbs) {
|
|
242
|
-
const stat =
|
|
246
|
+
const stat = fs13.statSync(scope);
|
|
243
247
|
if (stat.isDirectory()) walk(scope);
|
|
244
248
|
else if (stat.isFile()) considerFile(scope);
|
|
245
249
|
}
|
|
@@ -721,14 +725,14 @@ var DEFAULT_PATHS = [
|
|
|
721
725
|
];
|
|
722
726
|
function loadCoverage(root, explicit) {
|
|
723
727
|
const candidates = (explicit && explicit.length ? explicit : DEFAULT_PATHS).map(
|
|
724
|
-
(p) =>
|
|
728
|
+
(p) => path12.resolve(root, p)
|
|
725
729
|
);
|
|
726
|
-
const found = candidates.filter((p) =>
|
|
730
|
+
const found = candidates.filter((p) => fs13.existsSync(p));
|
|
727
731
|
if (found.length === 0) return null;
|
|
728
732
|
const map = /* @__PURE__ */ new Map();
|
|
729
733
|
for (const file of found) {
|
|
730
734
|
try {
|
|
731
|
-
const text =
|
|
735
|
+
const text = fs13.readFileSync(file, "utf8");
|
|
732
736
|
if (file.endsWith(".json")) mergeIstanbul(map, text, root);
|
|
733
737
|
else mergeLcov(map, text, root);
|
|
734
738
|
} catch {
|
|
@@ -737,8 +741,8 @@ function loadCoverage(root, explicit) {
|
|
|
737
741
|
return map.size ? map : null;
|
|
738
742
|
}
|
|
739
743
|
function rel(root, p) {
|
|
740
|
-
const abs =
|
|
741
|
-
return
|
|
744
|
+
const abs = path12.isAbsolute(p) ? p : path12.resolve(root, p);
|
|
745
|
+
return path12.relative(root, abs).split(path12.sep).join("/");
|
|
742
746
|
}
|
|
743
747
|
function bump(map, file, line, hits) {
|
|
744
748
|
let lh = map.get(file);
|
|
@@ -949,7 +953,7 @@ async function parseInline(files, options) {
|
|
|
949
953
|
onProgress?.(0, files.length);
|
|
950
954
|
for (const file of files) {
|
|
951
955
|
try {
|
|
952
|
-
const source =
|
|
956
|
+
const source = fs13.readFileSync(file.abs, "utf8");
|
|
953
957
|
out.push(await parseSource(file.rel, file.lang.id, source));
|
|
954
958
|
} catch (err) {
|
|
955
959
|
out.push(emptyParse(file, `parse failed: ${err.message}`));
|
|
@@ -1004,9 +1008,9 @@ function isWorkerOom(err) {
|
|
|
1004
1008
|
return e?.code === "ERR_WORKER_OUT_OF_MEMORY" || /out of memory/i.test(e?.message ?? "");
|
|
1005
1009
|
}
|
|
1006
1010
|
function resolveWorkerFile() {
|
|
1007
|
-
const here =
|
|
1008
|
-
const candidate =
|
|
1009
|
-
return
|
|
1011
|
+
const here = path12.dirname(fileURLToPath(import.meta.url));
|
|
1012
|
+
const candidate = path12.join(here, "parse-worker.js");
|
|
1013
|
+
return fs13.existsSync(candidate) ? candidate : null;
|
|
1010
1014
|
}
|
|
1011
1015
|
function chunk(items, buckets) {
|
|
1012
1016
|
const out = Array.from({ length: Math.min(buckets, items.length || 1) }, () => []);
|
|
@@ -1285,12 +1289,12 @@ function enclosing3(nodes, line) {
|
|
|
1285
1289
|
return best;
|
|
1286
1290
|
}
|
|
1287
1291
|
function normalize2(p) {
|
|
1288
|
-
return
|
|
1292
|
+
return path12.resolve(p).split(path12.sep).join("/");
|
|
1289
1293
|
}
|
|
1290
1294
|
var TSC_CACHE_VERSION = "vg-tsc-cache/1";
|
|
1291
1295
|
var AMBIENT_RE = /\bdeclare\s+(global|module)\b/;
|
|
1292
1296
|
function tscCachePath(root) {
|
|
1293
|
-
return
|
|
1297
|
+
return path12.join(cacheDir(root), "tsc-cache.json");
|
|
1294
1298
|
}
|
|
1295
1299
|
function loadTscCache(root, opts) {
|
|
1296
1300
|
const file = tscCachePath(root);
|
|
@@ -1300,9 +1304,9 @@ function loadTscCache(root, opts) {
|
|
|
1300
1304
|
ts: ts.version,
|
|
1301
1305
|
entries: {}
|
|
1302
1306
|
};
|
|
1303
|
-
if (!opts.disabled &&
|
|
1307
|
+
if (!opts.disabled && fs13.existsSync(file)) {
|
|
1304
1308
|
try {
|
|
1305
|
-
const loaded = JSON.parse(
|
|
1309
|
+
const loaded = JSON.parse(fs13.readFileSync(file, "utf8"));
|
|
1306
1310
|
if (loaded.version === TSC_CACHE_VERSION && loaded.toolVersion === opts.toolVersion && loaded.ts === ts.version && loaded.entries) {
|
|
1307
1311
|
data = loaded;
|
|
1308
1312
|
}
|
|
@@ -1333,18 +1337,18 @@ function loadTscCache(root, opts) {
|
|
|
1333
1337
|
},
|
|
1334
1338
|
save() {
|
|
1335
1339
|
if (opts.disabled) return;
|
|
1336
|
-
|
|
1337
|
-
|
|
1340
|
+
fs13.mkdirSync(cacheDir(root), { recursive: true });
|
|
1341
|
+
fs13.writeFileSync(file, stableStringify(data, 0));
|
|
1338
1342
|
}
|
|
1339
1343
|
};
|
|
1340
1344
|
}
|
|
1341
1345
|
function abs$(root, rel2) {
|
|
1342
|
-
return
|
|
1346
|
+
return path12.join(root, rel2);
|
|
1343
1347
|
}
|
|
1344
1348
|
function detectAmbient(rel2, abs) {
|
|
1345
1349
|
if (rel2.endsWith(".d.ts")) return true;
|
|
1346
1350
|
try {
|
|
1347
|
-
return AMBIENT_RE.test(
|
|
1351
|
+
return AMBIENT_RE.test(fs13.readFileSync(abs, "utf8"));
|
|
1348
1352
|
} catch {
|
|
1349
1353
|
return true;
|
|
1350
1354
|
}
|
|
@@ -1638,14 +1642,14 @@ var IMPORTANCE = {
|
|
|
1638
1642
|
"other-config": 0.08
|
|
1639
1643
|
};
|
|
1640
1644
|
function toPosix2(p) {
|
|
1641
|
-
return p.split(
|
|
1645
|
+
return p.split(path12.sep).join("/");
|
|
1642
1646
|
}
|
|
1643
1647
|
function buildRootIgnore2(root, exclude) {
|
|
1644
1648
|
const ig = ignore();
|
|
1645
|
-
const gitignorePath =
|
|
1646
|
-
if (
|
|
1649
|
+
const gitignorePath = path12.join(root, ".gitignore");
|
|
1650
|
+
if (fs13.existsSync(gitignorePath)) {
|
|
1647
1651
|
try {
|
|
1648
|
-
ig.add(
|
|
1652
|
+
ig.add(fs13.readFileSync(gitignorePath, "utf8"));
|
|
1649
1653
|
} catch {
|
|
1650
1654
|
}
|
|
1651
1655
|
}
|
|
@@ -1654,7 +1658,7 @@ function buildRootIgnore2(root, exclude) {
|
|
|
1654
1658
|
}
|
|
1655
1659
|
function classifyProjectContext(relOrName) {
|
|
1656
1660
|
const rel2 = relOrName.replace(/\\/g, "/");
|
|
1657
|
-
const base =
|
|
1661
|
+
const base = path12.basename(rel2);
|
|
1658
1662
|
const baseLower = base.toLowerCase();
|
|
1659
1663
|
if (/^\.env($|\.)/i.test(base) && !isEnvExampleName(baseLower)) return null;
|
|
1660
1664
|
if (SKIP_FILES.has(baseLower)) return null;
|
|
@@ -1667,7 +1671,7 @@ function classifyProjectContext(relOrName) {
|
|
|
1667
1671
|
for (const { re, category } of PATH_PATTERNS) {
|
|
1668
1672
|
if (re.test(rel2)) return category;
|
|
1669
1673
|
}
|
|
1670
|
-
const ext =
|
|
1674
|
+
const ext = path12.extname(baseLower);
|
|
1671
1675
|
if (DOC_EXTENSIONS.has(ext)) return ext === ".txt" ? "text" : "markdown";
|
|
1672
1676
|
if (API_EXTENSIONS.has(ext)) return "api-contract";
|
|
1673
1677
|
if ((ext === ".yaml" || ext === ".yml" || ext === ".json") && /openapi|swagger|asyncapi/i.test(base)) {
|
|
@@ -1685,24 +1689,24 @@ function isEnvExampleName(baseLower) {
|
|
|
1685
1689
|
return false;
|
|
1686
1690
|
}
|
|
1687
1691
|
function discoverDocs(options) {
|
|
1688
|
-
const root =
|
|
1692
|
+
const root = path12.resolve(options.root);
|
|
1689
1693
|
const maxFiles = options.maxFiles ?? DOC_MAX_FILES;
|
|
1690
1694
|
const rootIg = buildRootIgnore2(root, options.exclude ?? []);
|
|
1691
|
-
const scopeAbs = (options.paths?.length ? options.paths.map((p) =>
|
|
1692
|
-
(p) =>
|
|
1695
|
+
const scopeAbs = (options.paths?.length ? options.paths.map((p) => path12.resolve(root, p)) : [root]).filter(
|
|
1696
|
+
(p) => fs13.existsSync(p)
|
|
1693
1697
|
);
|
|
1694
1698
|
const found = /* @__PURE__ */ new Map();
|
|
1695
1699
|
const consider = (abs) => {
|
|
1696
1700
|
if (found.size >= maxFiles) return;
|
|
1697
|
-
const rel2 = toPosix2(
|
|
1701
|
+
const rel2 = toPosix2(path12.relative(root, abs));
|
|
1698
1702
|
if (!rel2 || rel2.startsWith("..")) return;
|
|
1699
1703
|
if (rootIg.ignores(rel2)) return;
|
|
1700
|
-
const base =
|
|
1704
|
+
const base = path12.basename(abs);
|
|
1701
1705
|
if (SKIP_FILES.has(base.toLowerCase())) return;
|
|
1702
1706
|
const category = classifyProjectContext(rel2);
|
|
1703
1707
|
if (!category) return;
|
|
1704
1708
|
try {
|
|
1705
|
-
const st =
|
|
1709
|
+
const st = fs13.statSync(abs);
|
|
1706
1710
|
if (!st.isFile() || st.size > DOC_FILE_MAX_BYTES) return;
|
|
1707
1711
|
if (st.size === 0) return;
|
|
1708
1712
|
} catch {
|
|
@@ -1714,14 +1718,14 @@ function discoverDocs(options) {
|
|
|
1714
1718
|
if (found.size >= maxFiles || depth > 12) return;
|
|
1715
1719
|
let entries;
|
|
1716
1720
|
try {
|
|
1717
|
-
entries =
|
|
1721
|
+
entries = fs13.readdirSync(dir, { withFileTypes: true });
|
|
1718
1722
|
} catch {
|
|
1719
1723
|
return;
|
|
1720
1724
|
}
|
|
1721
1725
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1722
1726
|
if (found.size >= maxFiles) return;
|
|
1723
|
-
const abs =
|
|
1724
|
-
const rel2 = toPosix2(
|
|
1727
|
+
const abs = path12.join(dir, entry.name);
|
|
1728
|
+
const rel2 = toPosix2(path12.relative(root, abs));
|
|
1725
1729
|
if (entry.isDirectory()) {
|
|
1726
1730
|
if (isSkippedDirName(entry.name)) continue;
|
|
1727
1731
|
if (rel2 && rootIg.ignores(`${rel2}/`) && !rel2.startsWith(".github")) continue;
|
|
@@ -1733,7 +1737,7 @@ function discoverDocs(options) {
|
|
|
1733
1737
|
};
|
|
1734
1738
|
for (const scope of scopeAbs) {
|
|
1735
1739
|
try {
|
|
1736
|
-
const st =
|
|
1740
|
+
const st = fs13.statSync(scope);
|
|
1737
1741
|
if (st.isDirectory()) walk(scope, 0);
|
|
1738
1742
|
else if (st.isFile()) consider(scope);
|
|
1739
1743
|
} catch {
|
|
@@ -1744,7 +1748,7 @@ function discoverDocs(options) {
|
|
|
1744
1748
|
function prepareDocBody(rel2, raw, category) {
|
|
1745
1749
|
const scrubbed = redactSecrets(raw);
|
|
1746
1750
|
if (scrubbed.length <= DOC_BODY_MAX_CHARS) return scrubbed;
|
|
1747
|
-
if (category === "manifest" &&
|
|
1751
|
+
if (category === "manifest" && path12.basename(rel2).toLowerCase() === "package.json") {
|
|
1748
1752
|
return summarizePackageJson(scrubbed);
|
|
1749
1753
|
}
|
|
1750
1754
|
if (category === "api-contract" && /\.(ya?ml|json)$/i.test(rel2)) {
|
|
@@ -1790,14 +1794,14 @@ function documentNodesFromDocs(docs) {
|
|
|
1790
1794
|
for (const d of docs) {
|
|
1791
1795
|
let raw = "";
|
|
1792
1796
|
try {
|
|
1793
|
-
raw =
|
|
1797
|
+
raw = fs13.readFileSync(d.abs, "utf8");
|
|
1794
1798
|
} catch {
|
|
1795
1799
|
continue;
|
|
1796
1800
|
}
|
|
1797
1801
|
const category = d.category ?? d.kind;
|
|
1798
1802
|
const scrubbed = prepareDocBody(d.rel, raw, category);
|
|
1799
1803
|
if (!scrubbed.trim()) continue;
|
|
1800
|
-
const name =
|
|
1804
|
+
const name = path12.basename(d.rel);
|
|
1801
1805
|
const qualifiedName = d.rel.replace(/\\/g, "/");
|
|
1802
1806
|
const id = nodeId({
|
|
1803
1807
|
kind: "document",
|
|
@@ -1826,12 +1830,1703 @@ function documentNodesFromDocs(docs) {
|
|
|
1826
1830
|
}
|
|
1827
1831
|
return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.id.localeCompare(b.id));
|
|
1828
1832
|
}
|
|
1833
|
+
|
|
1834
|
+
// src/engine/toolchain/util.ts
|
|
1835
|
+
var TOOLCHAIN_FILE_MAX_BYTES = 2 * 1024 * 1024;
|
|
1836
|
+
var TOOLCHAIN_NODES_PER_FILE_MAX = 2e3;
|
|
1837
|
+
var DOC_MAX_CHARS = 240;
|
|
1838
|
+
var LineIndex = class {
|
|
1839
|
+
/** Byte offset at which each line starts. */
|
|
1840
|
+
starts;
|
|
1841
|
+
constructor(source) {
|
|
1842
|
+
const starts = [0];
|
|
1843
|
+
for (let i = 0; i < source.length; i++) {
|
|
1844
|
+
if (source.charCodeAt(i) === 10) starts.push(i + 1);
|
|
1845
|
+
}
|
|
1846
|
+
this.starts = starts;
|
|
1847
|
+
}
|
|
1848
|
+
/** 1-based line number containing `offset`. */
|
|
1849
|
+
lineAt(offset) {
|
|
1850
|
+
if (offset <= 0) return 1;
|
|
1851
|
+
let lo = 0;
|
|
1852
|
+
let hi = this.starts.length - 1;
|
|
1853
|
+
while (lo < hi) {
|
|
1854
|
+
const mid = lo + hi + 1 >> 1;
|
|
1855
|
+
if (this.starts[mid] <= offset) lo = mid;
|
|
1856
|
+
else hi = mid - 1;
|
|
1857
|
+
}
|
|
1858
|
+
return lo + 1;
|
|
1859
|
+
}
|
|
1860
|
+
/** Span covering the byte range `[start, end)`. */
|
|
1861
|
+
span(start, end) {
|
|
1862
|
+
const s = this.lineAt(start);
|
|
1863
|
+
const e = this.lineAt(Math.max(start, end - 1));
|
|
1864
|
+
return { start: s, end: Math.max(s, e) };
|
|
1865
|
+
}
|
|
1866
|
+
get lineCount() {
|
|
1867
|
+
return this.starts.length;
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
function safeDoc(text) {
|
|
1871
|
+
if (!text) return void 0;
|
|
1872
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
1873
|
+
if (!collapsed) return void 0;
|
|
1874
|
+
const redacted = redactSecrets(collapsed);
|
|
1875
|
+
return redacted.length > DOC_MAX_CHARS ? `${redacted.slice(0, DOC_MAX_CHARS - 1)}\u2026` : redacted;
|
|
1876
|
+
}
|
|
1877
|
+
function parseImageRef(raw) {
|
|
1878
|
+
const value = raw.trim();
|
|
1879
|
+
if (!value || /\s/.test(value)) return null;
|
|
1880
|
+
if (value.includes("$") || value.includes("{{")) return null;
|
|
1881
|
+
const atIdx = value.lastIndexOf("@");
|
|
1882
|
+
let digest;
|
|
1883
|
+
let rest = value;
|
|
1884
|
+
if (atIdx > 0) {
|
|
1885
|
+
digest = value.slice(atIdx + 1);
|
|
1886
|
+
rest = value.slice(0, atIdx);
|
|
1887
|
+
}
|
|
1888
|
+
let tag;
|
|
1889
|
+
const colonIdx = rest.lastIndexOf(":");
|
|
1890
|
+
if (colonIdx > 0 && colonIdx > rest.lastIndexOf("/")) {
|
|
1891
|
+
tag = rest.slice(colonIdx + 1);
|
|
1892
|
+
rest = rest.slice(0, colonIdx);
|
|
1893
|
+
}
|
|
1894
|
+
if (!rest) return null;
|
|
1895
|
+
return { raw: value, repository: rest, tag, digest };
|
|
1896
|
+
}
|
|
1897
|
+
function imageIdentity(ref) {
|
|
1898
|
+
if (ref.digest) return `${ref.repository}@${ref.digest}`;
|
|
1899
|
+
return ref.tag ? `${ref.repository}:${ref.tag}` : ref.repository;
|
|
1900
|
+
}
|
|
1901
|
+
function toPosix3(rel2) {
|
|
1902
|
+
return rel2.replace(/\\/g, "/");
|
|
1903
|
+
}
|
|
1904
|
+
function resolveRelative(fromFileRel, target) {
|
|
1905
|
+
const base = toPosix3(fromFileRel).split("/").slice(0, -1);
|
|
1906
|
+
const parts = toPosix3(target).split("/");
|
|
1907
|
+
const out = [...base];
|
|
1908
|
+
for (const part of parts) {
|
|
1909
|
+
if (!part || part === ".") continue;
|
|
1910
|
+
if (part === "..") {
|
|
1911
|
+
if (!out.length) return null;
|
|
1912
|
+
out.pop();
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
out.push(part);
|
|
1916
|
+
}
|
|
1917
|
+
const joined = out.join("/");
|
|
1918
|
+
return joined || null;
|
|
1919
|
+
}
|
|
1920
|
+
function parseYamlStream(rel2, source) {
|
|
1921
|
+
const warnings = [];
|
|
1922
|
+
let parsed;
|
|
1923
|
+
try {
|
|
1924
|
+
parsed = YAML.parseAllDocuments(source, {
|
|
1925
|
+
// Parsing itself does not expand aliases — the bomb only detonates on
|
|
1926
|
+
// materialisation, so the alias budget is applied at `toJS` below.
|
|
1927
|
+
logLevel: "silent",
|
|
1928
|
+
uniqueKeys: false
|
|
1929
|
+
});
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
warnings.push(`${rel2}: YAML parse failed (${err.message})`);
|
|
1932
|
+
return { docs: [], warnings };
|
|
1933
|
+
}
|
|
1934
|
+
const docs = [];
|
|
1935
|
+
parsed.forEach((ast, index) => {
|
|
1936
|
+
if (ast.errors.length) {
|
|
1937
|
+
warnings.push(`${rel2}: YAML document ${index + 1} has errors \u2014 skipped`);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
let value;
|
|
1941
|
+
try {
|
|
1942
|
+
value = ast.toJS({ maxAliasCount: 100 });
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
warnings.push(`${rel2}: YAML document ${index + 1} could not be materialised (${err.message})`);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (value === null || value === void 0) return;
|
|
1948
|
+
docs.push({ value, ast, index });
|
|
1949
|
+
});
|
|
1950
|
+
return { docs, warnings };
|
|
1951
|
+
}
|
|
1952
|
+
function spanAt(doc, lines, path13) {
|
|
1953
|
+
const node = path13.length ? safeGetIn(doc.ast, path13) : doc.ast.contents;
|
|
1954
|
+
const range = nodeRange(node) ?? nodeRange(doc.ast.contents);
|
|
1955
|
+
if (!range) return { start: 1, end: 1 };
|
|
1956
|
+
return lines.span(range[0], range[2] ?? range[1]);
|
|
1957
|
+
}
|
|
1958
|
+
function safeGetIn(ast, path13) {
|
|
1959
|
+
try {
|
|
1960
|
+
return ast.getIn(path13, true);
|
|
1961
|
+
} catch {
|
|
1962
|
+
return void 0;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
function nodeRange(node) {
|
|
1966
|
+
const range = node?.range;
|
|
1967
|
+
return Array.isArray(range) ? range : null;
|
|
1968
|
+
}
|
|
1969
|
+
function getPath(value, path13) {
|
|
1970
|
+
let current = value;
|
|
1971
|
+
for (const key of path13) {
|
|
1972
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
1973
|
+
current = current[key];
|
|
1974
|
+
}
|
|
1975
|
+
return current;
|
|
1976
|
+
}
|
|
1977
|
+
function getString(value, path13) {
|
|
1978
|
+
const found = getPath(value, path13);
|
|
1979
|
+
if (typeof found === "string") return found;
|
|
1980
|
+
if (typeof found === "number" || typeof found === "boolean") return String(found);
|
|
1981
|
+
return void 0;
|
|
1982
|
+
}
|
|
1983
|
+
function getArray(value, path13) {
|
|
1984
|
+
const found = getPath(value, path13);
|
|
1985
|
+
return Array.isArray(found) ? found : [];
|
|
1986
|
+
}
|
|
1987
|
+
function getRecordEntries(value, path13) {
|
|
1988
|
+
const found = getPath(value, path13);
|
|
1989
|
+
if (!found || typeof found !== "object" || Array.isArray(found)) return [];
|
|
1990
|
+
return Object.entries(found).sort(([a], [b]) => a.localeCompare(b, "en"));
|
|
1991
|
+
}
|
|
1992
|
+
function looksTemplated(source) {
|
|
1993
|
+
return /\{\{-?\s*(if|range|with|end|define|template|include|toYaml|\.Values|\.Release|\.Chart)/.test(
|
|
1994
|
+
source
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/engine/toolchain/compose.ts
|
|
1999
|
+
var DEFAULT_DOCKERFILE = "Dockerfile";
|
|
2000
|
+
var composeExtractor = {
|
|
2001
|
+
format: "compose",
|
|
2002
|
+
matches(rel2, category) {
|
|
2003
|
+
if (category === "compose") return true;
|
|
2004
|
+
return /(^|\/)(docker-)?compose([.-][\w.-]+)?\.ya?ml$/i.test(rel2);
|
|
2005
|
+
},
|
|
2006
|
+
extract(rel2, source) {
|
|
2007
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
2008
|
+
const lines = new LineIndex(source);
|
|
2009
|
+
const nodes = [];
|
|
2010
|
+
const edges = [];
|
|
2011
|
+
const doc = docs[0];
|
|
2012
|
+
if (!doc) return { nodes, edges, warnings };
|
|
2013
|
+
const services = getRecordEntries(doc.value, ["services"]);
|
|
2014
|
+
const declared = new Set(services.map(([name]) => `service:${name}`));
|
|
2015
|
+
for (const [name, service] of services) {
|
|
2016
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2017
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
const address = `service:${name}`;
|
|
2021
|
+
const image = getString(service, ["image"]);
|
|
2022
|
+
const ports = getArray(service, ["ports"]).map(String);
|
|
2023
|
+
nodes.push({
|
|
2024
|
+
kind: "resource",
|
|
2025
|
+
name,
|
|
2026
|
+
qualifiedName: address,
|
|
2027
|
+
span: spanAt(doc, lines, ["services", name]),
|
|
2028
|
+
signature: "compose.service",
|
|
2029
|
+
doc: safeDoc(
|
|
2030
|
+
[image && `image ${image}`, ports.length && `ports ${ports.join(", ")}`].filter(Boolean).join(", ")
|
|
2031
|
+
),
|
|
2032
|
+
importance: 0.5
|
|
2033
|
+
});
|
|
2034
|
+
if (image) {
|
|
2035
|
+
const ref = parseImageRef(image);
|
|
2036
|
+
if (ref) {
|
|
2037
|
+
nodes.push({
|
|
2038
|
+
kind: "image",
|
|
2039
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
2040
|
+
qualifiedName: `image:${ref.raw}`,
|
|
2041
|
+
span: spanAt(doc, lines, ["services", name, "image"]),
|
|
2042
|
+
signature: "compose.image",
|
|
2043
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
2044
|
+
importance: 0.3
|
|
2045
|
+
});
|
|
2046
|
+
edges.push({ kind: "builds_from", from: address, to: `image:${ref.raw}`, confidence: 1 });
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
const dockerfileRel = composeDockerfilePath(rel2, service);
|
|
2050
|
+
if (dockerfileRel) {
|
|
2051
|
+
nodes.push({
|
|
2052
|
+
kind: "image",
|
|
2053
|
+
name: dockerfileRel.split("/").pop() ?? dockerfileRel,
|
|
2054
|
+
qualifiedName: `dockerfile:${dockerfileRel}`,
|
|
2055
|
+
span: spanAt(doc, lines, ["services", name, "build"]),
|
|
2056
|
+
signature: "compose.build",
|
|
2057
|
+
doc: safeDoc(`builds from ${dockerfileRel}`),
|
|
2058
|
+
importance: 0.4
|
|
2059
|
+
});
|
|
2060
|
+
edges.push({
|
|
2061
|
+
kind: "builds_from",
|
|
2062
|
+
from: address,
|
|
2063
|
+
to: `dockerfile:${dockerfileRel}`,
|
|
2064
|
+
confidence: 1
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
for (const target of composeDependsOn(service)) {
|
|
2068
|
+
const to = `service:${target}`;
|
|
2069
|
+
if (declared.has(to)) edges.push({ kind: "depends_on", from: address, to, confidence: 1 });
|
|
2070
|
+
}
|
|
2071
|
+
for (const port of ports) {
|
|
2072
|
+
edges.push({ kind: "exposes", from: address, to: `port:${port}`, confidence: 1 });
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const portTargets = new Set(
|
|
2076
|
+
edges.filter((e) => e.kind === "exposes").map((e) => e.to)
|
|
2077
|
+
);
|
|
2078
|
+
for (const target of [...portTargets].sort()) {
|
|
2079
|
+
nodes.push({
|
|
2080
|
+
kind: "property",
|
|
2081
|
+
name: target.slice("port:".length),
|
|
2082
|
+
qualifiedName: target,
|
|
2083
|
+
span: { start: 1, end: 1 },
|
|
2084
|
+
signature: "compose.port",
|
|
2085
|
+
importance: 0.1
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
return { nodes, edges, warnings };
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
function composeDockerfilePath(rel2, service) {
|
|
2092
|
+
const build = getPath(service, ["build"]);
|
|
2093
|
+
if (build === void 0 || build === null) return null;
|
|
2094
|
+
if (typeof build === "string") {
|
|
2095
|
+
return resolveRelative(rel2, `${build}/${DEFAULT_DOCKERFILE}`);
|
|
2096
|
+
}
|
|
2097
|
+
if (typeof build !== "object") return null;
|
|
2098
|
+
const context = getString(build, ["context"]) ?? ".";
|
|
2099
|
+
const dockerfile = getString(build, ["dockerfile"]) ?? DEFAULT_DOCKERFILE;
|
|
2100
|
+
return resolveRelative(rel2, `${context}/${dockerfile}`);
|
|
2101
|
+
}
|
|
2102
|
+
function composeDependsOn(service) {
|
|
2103
|
+
const raw = getPath(service, ["depends_on"]);
|
|
2104
|
+
if (Array.isArray(raw)) {
|
|
2105
|
+
return raw.filter((v) => typeof v === "string").sort();
|
|
2106
|
+
}
|
|
2107
|
+
if (raw && typeof raw === "object") {
|
|
2108
|
+
return Object.keys(raw).sort();
|
|
2109
|
+
}
|
|
2110
|
+
return [];
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/engine/toolchain/dockerfile.ts
|
|
2114
|
+
var INSTRUCTION = /^([A-Za-z][A-Za-z0-9_]*)\s+([\s\S]*)$/;
|
|
2115
|
+
function logicalLines(source) {
|
|
2116
|
+
const rawLines = source.split(/\r?\n/);
|
|
2117
|
+
let escapeChar = "\\";
|
|
2118
|
+
for (const line of rawLines) {
|
|
2119
|
+
const trimmed = line.trim();
|
|
2120
|
+
if (!trimmed) continue;
|
|
2121
|
+
if (!trimmed.startsWith("#")) break;
|
|
2122
|
+
const directive = /^#\s*escape\s*=\s*(\S)/i.exec(trimmed);
|
|
2123
|
+
if (directive) escapeChar = directive[1];
|
|
2124
|
+
}
|
|
2125
|
+
const out = [];
|
|
2126
|
+
let buffer = null;
|
|
2127
|
+
let startLine = 0;
|
|
2128
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
2129
|
+
const raw = rawLines[i];
|
|
2130
|
+
const trimmed = raw.trim();
|
|
2131
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2132
|
+
const continues = trimmed.endsWith(escapeChar);
|
|
2133
|
+
const fragment = continues ? trimmed.slice(0, -1).trimEnd() : trimmed;
|
|
2134
|
+
if (buffer === null) {
|
|
2135
|
+
buffer = fragment;
|
|
2136
|
+
startLine = i + 1;
|
|
2137
|
+
} else {
|
|
2138
|
+
buffer += ` ${fragment}`;
|
|
2139
|
+
}
|
|
2140
|
+
if (!continues) {
|
|
2141
|
+
const match = INSTRUCTION.exec(buffer);
|
|
2142
|
+
if (match) {
|
|
2143
|
+
out.push({
|
|
2144
|
+
instruction: match[1].toUpperCase(),
|
|
2145
|
+
args: match[2].trim(),
|
|
2146
|
+
startLine,
|
|
2147
|
+
endLine: i + 1
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
buffer = null;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (buffer !== null) {
|
|
2154
|
+
const match = INSTRUCTION.exec(buffer);
|
|
2155
|
+
if (match) {
|
|
2156
|
+
out.push({
|
|
2157
|
+
instruction: match[1].toUpperCase(),
|
|
2158
|
+
args: match[2].trim(),
|
|
2159
|
+
startLine,
|
|
2160
|
+
endLine: rawLines.length
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return out;
|
|
2165
|
+
}
|
|
2166
|
+
function parseFrom(args) {
|
|
2167
|
+
const tokens = args.split(/\s+/).filter(Boolean);
|
|
2168
|
+
const positional = [];
|
|
2169
|
+
for (const token of tokens) {
|
|
2170
|
+
if (token.startsWith("--")) continue;
|
|
2171
|
+
positional.push(token);
|
|
2172
|
+
}
|
|
2173
|
+
if (!positional.length) return null;
|
|
2174
|
+
const image = positional[0];
|
|
2175
|
+
const asIdx = positional.findIndex((t) => t.toUpperCase() === "AS");
|
|
2176
|
+
const stage = asIdx > 0 && positional[asIdx + 1] ? positional[asIdx + 1] : void 0;
|
|
2177
|
+
return { image, stage };
|
|
2178
|
+
}
|
|
2179
|
+
function parseCopyFrom(args) {
|
|
2180
|
+
const match = /--from=(\S+)/i.exec(args);
|
|
2181
|
+
return match ? match[1] : null;
|
|
2182
|
+
}
|
|
2183
|
+
function parseExpose(args) {
|
|
2184
|
+
return args.split(/\s+/).filter(Boolean).filter((token) => !token.startsWith("--"));
|
|
2185
|
+
}
|
|
2186
|
+
function copySourcePaths(args, hasFromFlag) {
|
|
2187
|
+
const tokens = args.split(/\s+/).filter(Boolean).filter((t) => !t.startsWith("--"));
|
|
2188
|
+
if (tokens.length < 2) return [];
|
|
2189
|
+
const sources = tokens.slice(0, -1);
|
|
2190
|
+
return sources.filter(
|
|
2191
|
+
(source) => source !== "." && !source.includes("*") && !source.includes("?") && !source.includes("[") && !source.includes("$") && !/^https?:\/\//i.test(source)
|
|
2192
|
+
).map((source) => source.replace(/^\.\//, "")).sort();
|
|
2193
|
+
}
|
|
2194
|
+
var dockerfileExtractor = {
|
|
2195
|
+
format: "dockerfile",
|
|
2196
|
+
matches(rel2, category) {
|
|
2197
|
+
if (category === "docker") return true;
|
|
2198
|
+
const base = toPosix3(rel2).split("/").pop()?.toLowerCase() ?? "";
|
|
2199
|
+
return base === "dockerfile" || base === "containerfile" || base.startsWith("dockerfile.");
|
|
2200
|
+
},
|
|
2201
|
+
extract(rel2, source) {
|
|
2202
|
+
const lines = new LineIndex(source);
|
|
2203
|
+
const nodes = [];
|
|
2204
|
+
const edges = [];
|
|
2205
|
+
const warnings = [];
|
|
2206
|
+
const posixRel = toPosix3(rel2);
|
|
2207
|
+
const fileAddress = `dockerfile:${posixRel}`;
|
|
2208
|
+
nodes.push({
|
|
2209
|
+
kind: "image",
|
|
2210
|
+
name: posixRel.split("/").pop() ?? posixRel,
|
|
2211
|
+
qualifiedName: fileAddress,
|
|
2212
|
+
span: { start: 1, end: Math.max(1, lines.lineCount) },
|
|
2213
|
+
signature: "dockerfile",
|
|
2214
|
+
importance: 0.5
|
|
2215
|
+
});
|
|
2216
|
+
const stages = /* @__PURE__ */ new Map();
|
|
2217
|
+
const copySources = /* @__PURE__ */ new Map();
|
|
2218
|
+
let stageIndex = 0;
|
|
2219
|
+
let currentStage = null;
|
|
2220
|
+
for (const line of logicalLines(source)) {
|
|
2221
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2222
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
switch (line.instruction) {
|
|
2226
|
+
case "FROM": {
|
|
2227
|
+
const from = parseFrom(line.args);
|
|
2228
|
+
if (!from) break;
|
|
2229
|
+
const stageName = from.stage ?? String(stageIndex);
|
|
2230
|
+
const stageAddress = `${fileAddress}#${stageName}`;
|
|
2231
|
+
stages.set(stageName.toLowerCase(), stageAddress);
|
|
2232
|
+
currentStage = stageAddress;
|
|
2233
|
+
stageIndex++;
|
|
2234
|
+
nodes.push({
|
|
2235
|
+
kind: "image",
|
|
2236
|
+
name: from.stage ?? `stage ${stageIndex - 1}`,
|
|
2237
|
+
qualifiedName: stageAddress,
|
|
2238
|
+
span: { start: line.startLine, end: line.endLine },
|
|
2239
|
+
signature: "dockerfile.stage",
|
|
2240
|
+
doc: safeDoc(`from ${from.image}`),
|
|
2241
|
+
importance: 0.4
|
|
2242
|
+
});
|
|
2243
|
+
edges.push({ kind: "contains", from: fileAddress, to: stageAddress, confidence: 1 });
|
|
2244
|
+
const baseStage = stages.get(from.image.toLowerCase());
|
|
2245
|
+
if (baseStage && baseStage !== stageAddress) {
|
|
2246
|
+
edges.push({
|
|
2247
|
+
kind: "builds_from",
|
|
2248
|
+
from: stageAddress,
|
|
2249
|
+
to: baseStage,
|
|
2250
|
+
confidence: 1
|
|
2251
|
+
});
|
|
2252
|
+
} else {
|
|
2253
|
+
const ref = parseImageRef(from.image);
|
|
2254
|
+
if (ref) {
|
|
2255
|
+
const imageAddress = `image:${ref.raw}`;
|
|
2256
|
+
nodes.push({
|
|
2257
|
+
kind: "image",
|
|
2258
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
2259
|
+
qualifiedName: imageAddress,
|
|
2260
|
+
span: { start: line.startLine, end: line.endLine },
|
|
2261
|
+
signature: "dockerfile.base",
|
|
2262
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
2263
|
+
importance: 0.3
|
|
2264
|
+
});
|
|
2265
|
+
edges.push({
|
|
2266
|
+
kind: "builds_from",
|
|
2267
|
+
from: stageAddress,
|
|
2268
|
+
to: imageAddress,
|
|
2269
|
+
confidence: 1
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
break;
|
|
2274
|
+
}
|
|
2275
|
+
case "COPY":
|
|
2276
|
+
case "ADD": {
|
|
2277
|
+
const fromRef = parseCopyFrom(line.args);
|
|
2278
|
+
if (fromRef) {
|
|
2279
|
+
if (!currentStage) break;
|
|
2280
|
+
const target = stages.get(fromRef.toLowerCase());
|
|
2281
|
+
if (target && target !== currentStage) {
|
|
2282
|
+
edges.push({ kind: "depends_on", from: currentStage, to: target, confidence: 1 });
|
|
2283
|
+
}
|
|
2284
|
+
break;
|
|
2285
|
+
}
|
|
2286
|
+
if (!currentStage) break;
|
|
2287
|
+
const forStage = copySources.get(currentStage) ?? /* @__PURE__ */ new Set();
|
|
2288
|
+
for (const source2 of copySourcePaths(line.args)) {
|
|
2289
|
+
const relativeToDockerfile = resolveRelative(posixRel, source2);
|
|
2290
|
+
if (relativeToDockerfile) forStage.add(relativeToDockerfile);
|
|
2291
|
+
forStage.add(toPosix3(source2).replace(/^\.\//, ""));
|
|
2292
|
+
}
|
|
2293
|
+
if (forStage.size) copySources.set(currentStage, forStage);
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
case "EXPOSE": {
|
|
2297
|
+
if (!currentStage) break;
|
|
2298
|
+
for (const port of parseExpose(line.args)) {
|
|
2299
|
+
const portAddress = `port:${port}`;
|
|
2300
|
+
nodes.push({
|
|
2301
|
+
kind: "property",
|
|
2302
|
+
name: port,
|
|
2303
|
+
qualifiedName: portAddress,
|
|
2304
|
+
span: { start: line.startLine, end: line.endLine },
|
|
2305
|
+
signature: "dockerfile.expose",
|
|
2306
|
+
importance: 0.1
|
|
2307
|
+
});
|
|
2308
|
+
edges.push({ kind: "exposes", from: currentStage, to: portAddress, confidence: 1 });
|
|
2309
|
+
}
|
|
2310
|
+
break;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
const copySourcesByStage = {};
|
|
2315
|
+
for (const stage of [...copySources.keys()].sort()) {
|
|
2316
|
+
copySourcesByStage[stage] = [...copySources.get(stage) ?? []].sort();
|
|
2317
|
+
}
|
|
2318
|
+
return { nodes, edges, warnings, linkHints: { copySourcesByStage } };
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
|
|
2322
|
+
// src/engine/toolchain/helm.ts
|
|
2323
|
+
function collectValuesImages(value, prefix, out) {
|
|
2324
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2325
|
+
const repository = getString(value, ["repository"]);
|
|
2326
|
+
const tag = getString(value, ["tag"]);
|
|
2327
|
+
const registry = getString(value, ["registry"]);
|
|
2328
|
+
if (repository) {
|
|
2329
|
+
const full = [registry, repository].filter(Boolean).join("/");
|
|
2330
|
+
const raw = tag ? `${full}:${tag}` : full;
|
|
2331
|
+
if (!out.has(raw)) out.set(raw, prefix);
|
|
2332
|
+
}
|
|
2333
|
+
const direct = getString(value, ["image"]);
|
|
2334
|
+
if (direct && parseImageRef(direct)) {
|
|
2335
|
+
if (!out.has(direct)) out.set(direct, [...prefix, "image"]);
|
|
2336
|
+
}
|
|
2337
|
+
for (const [key, child] of getRecordEntries(value, [])) {
|
|
2338
|
+
if (key === "repository" || key === "tag" || key === "registry") continue;
|
|
2339
|
+
collectValuesImages(child, [...prefix, key], out);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
var helmExtractor = {
|
|
2343
|
+
format: "helm",
|
|
2344
|
+
matches(rel2) {
|
|
2345
|
+
const base = toPosix3(rel2).split("/").pop()?.toLowerCase() ?? "";
|
|
2346
|
+
if (base === "chart.yaml" || base === "chart.yml") return true;
|
|
2347
|
+
return /(^|\/)values([.-][\w.-]+)?\.ya?ml$/i.test(rel2) && /(^|\/)charts?\//i.test(rel2);
|
|
2348
|
+
},
|
|
2349
|
+
extract(rel2, source) {
|
|
2350
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
2351
|
+
const lines = new LineIndex(source);
|
|
2352
|
+
const nodes = [];
|
|
2353
|
+
const edges = [];
|
|
2354
|
+
const doc = docs[0];
|
|
2355
|
+
if (!doc) return { nodes, edges, warnings };
|
|
2356
|
+
const base = toPosix3(rel2).split("/").pop()?.toLowerCase() ?? "";
|
|
2357
|
+
const isChartFile = base === "chart.yaml" || base === "chart.yml";
|
|
2358
|
+
if (isChartFile) {
|
|
2359
|
+
const name = getString(doc.value, ["name"]);
|
|
2360
|
+
if (!name) return { nodes, edges, warnings };
|
|
2361
|
+
const version = getString(doc.value, ["version"]);
|
|
2362
|
+
const appVersion = getString(doc.value, ["appVersion"]);
|
|
2363
|
+
const address = `chart:${name}`;
|
|
2364
|
+
nodes.push({
|
|
2365
|
+
kind: "chart",
|
|
2366
|
+
name,
|
|
2367
|
+
qualifiedName: address,
|
|
2368
|
+
span: spanAt(doc, lines, []),
|
|
2369
|
+
signature: "helm.chart",
|
|
2370
|
+
doc: safeDoc(
|
|
2371
|
+
[version && `version ${version}`, appVersion && `app ${appVersion}`].filter(Boolean).join(", ")
|
|
2372
|
+
),
|
|
2373
|
+
importance: 0.7
|
|
2374
|
+
});
|
|
2375
|
+
for (const dependency of getArray(doc.value, ["dependencies"])) {
|
|
2376
|
+
const depName = getString(dependency, ["name"]);
|
|
2377
|
+
if (!depName) continue;
|
|
2378
|
+
const depVersion = getString(dependency, ["version"]);
|
|
2379
|
+
const depAddress = `chart:${depName}`;
|
|
2380
|
+
nodes.push({
|
|
2381
|
+
kind: "chart",
|
|
2382
|
+
name: depName,
|
|
2383
|
+
qualifiedName: depAddress,
|
|
2384
|
+
span: spanAt(doc, lines, ["dependencies"]),
|
|
2385
|
+
signature: "helm.dependency",
|
|
2386
|
+
doc: safeDoc(depVersion ? `version ${depVersion}` : void 0),
|
|
2387
|
+
importance: 0.4
|
|
2388
|
+
});
|
|
2389
|
+
edges.push({ kind: "depends_on", from: address, to: depAddress, confidence: 1 });
|
|
2390
|
+
}
|
|
2391
|
+
return { nodes, edges, warnings };
|
|
2392
|
+
}
|
|
2393
|
+
const images = /* @__PURE__ */ new Map();
|
|
2394
|
+
collectValuesImages(doc.value, [], images);
|
|
2395
|
+
for (const raw of [...images.keys()].sort()) {
|
|
2396
|
+
const ref = parseImageRef(raw);
|
|
2397
|
+
if (!ref) continue;
|
|
2398
|
+
nodes.push({
|
|
2399
|
+
kind: "image",
|
|
2400
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
2401
|
+
qualifiedName: `image:${ref.raw}`,
|
|
2402
|
+
span: spanAt(doc, lines, images.get(raw) ?? []),
|
|
2403
|
+
signature: "helm.values.image",
|
|
2404
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
2405
|
+
importance: 0.3
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
return { nodes, edges, warnings };
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2412
|
+
// src/engine/toolchain/kubernetes.ts
|
|
2413
|
+
var WORKLOAD_TEMPLATE_PATHS = {
|
|
2414
|
+
Deployment: ["spec", "template"],
|
|
2415
|
+
StatefulSet: ["spec", "template"],
|
|
2416
|
+
DaemonSet: ["spec", "template"],
|
|
2417
|
+
ReplicaSet: ["spec", "template"],
|
|
2418
|
+
ReplicationController: ["spec", "template"],
|
|
2419
|
+
Job: ["spec", "template"],
|
|
2420
|
+
CronJob: ["spec", "jobTemplate", "spec", "template"],
|
|
2421
|
+
Pod: []
|
|
2422
|
+
};
|
|
2423
|
+
var CONTAINER_FIELDS = ["initContainers", "containers", "ephemeralContainers"];
|
|
2424
|
+
function isKubernetesDoc(value) {
|
|
2425
|
+
return typeof getString(value, ["apiVersion"]) === "string" && typeof getString(value, ["kind"]) === "string";
|
|
2426
|
+
}
|
|
2427
|
+
function objectAddress(value) {
|
|
2428
|
+
const kind = getString(value, ["kind"]);
|
|
2429
|
+
const name = getString(value, ["metadata", "name"]);
|
|
2430
|
+
if (!kind || !name) return null;
|
|
2431
|
+
const namespace = getString(value, ["metadata", "namespace"]) ?? "default";
|
|
2432
|
+
return `${namespace}/${kind}/${name}`;
|
|
2433
|
+
}
|
|
2434
|
+
function podSpecPath(kind) {
|
|
2435
|
+
const templatePath = WORKLOAD_TEMPLATE_PATHS[kind];
|
|
2436
|
+
if (!templatePath) return null;
|
|
2437
|
+
return [...templatePath, "spec"];
|
|
2438
|
+
}
|
|
2439
|
+
var kubernetesExtractor = {
|
|
2440
|
+
format: "kubernetes",
|
|
2441
|
+
matches(rel2, category, head) {
|
|
2442
|
+
if (!/\.ya?ml$/i.test(rel2)) return false;
|
|
2443
|
+
if (looksTemplated(head)) return false;
|
|
2444
|
+
if (/(^|\/)templates\//i.test(rel2)) return false;
|
|
2445
|
+
return /(^|\n)apiVersion:\s*\S/.test(head) && /(^|\n)kind:\s*\S/.test(head);
|
|
2446
|
+
},
|
|
2447
|
+
extract(rel2, source) {
|
|
2448
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
2449
|
+
const lines = new LineIndex(source);
|
|
2450
|
+
const nodes = [];
|
|
2451
|
+
const edges = [];
|
|
2452
|
+
const workloadLabels = /* @__PURE__ */ new Map();
|
|
2453
|
+
for (const doc of docs) {
|
|
2454
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2455
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2456
|
+
break;
|
|
2457
|
+
}
|
|
2458
|
+
if (!isKubernetesDoc(doc.value)) continue;
|
|
2459
|
+
const address = objectAddress(doc.value);
|
|
2460
|
+
if (!address) continue;
|
|
2461
|
+
const kind = getString(doc.value, ["kind"]);
|
|
2462
|
+
const name = getString(doc.value, ["metadata", "name"]);
|
|
2463
|
+
const podPath = podSpecPath(kind);
|
|
2464
|
+
const isWorkload = podPath !== null;
|
|
2465
|
+
nodes.push({
|
|
2466
|
+
kind: isWorkload ? "workload" : "resource",
|
|
2467
|
+
name,
|
|
2468
|
+
qualifiedName: address,
|
|
2469
|
+
span: spanAt(doc, lines, []),
|
|
2470
|
+
signature: `k8s.${kind}`,
|
|
2471
|
+
doc: safeDoc(describeObject(doc.value, kind)),
|
|
2472
|
+
importance: isWorkload ? 0.6 : 0.4
|
|
2473
|
+
});
|
|
2474
|
+
if (isWorkload) {
|
|
2475
|
+
const labels = readLabels(doc.value, [...WORKLOAD_TEMPLATE_PATHS[kind] ?? [], "metadata", "labels"]);
|
|
2476
|
+
if (Object.keys(labels).length) workloadLabels.set(address, labels);
|
|
2477
|
+
extractContainers(doc, lines, address, podPath, nodes, edges);
|
|
2478
|
+
}
|
|
2479
|
+
for (const reference of referencedConfig(doc.value, podPath)) {
|
|
2480
|
+
edges.push({ kind: "mounts", from: address, to: reference, confidence: 1 });
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
for (const doc of docs) {
|
|
2484
|
+
if (getString(doc.value, ["kind"]) !== "Service") continue;
|
|
2485
|
+
const from = objectAddress(doc.value);
|
|
2486
|
+
if (!from) continue;
|
|
2487
|
+
const selector = readLabels(doc.value, ["spec", "selector"]);
|
|
2488
|
+
if (!Object.keys(selector).length) continue;
|
|
2489
|
+
for (const [address, labels] of [...workloadLabels].sort(([a], [b]) => a.localeCompare(b, "en"))) {
|
|
2490
|
+
if (isSubset(selector, labels)) {
|
|
2491
|
+
edges.push({ kind: "exposes", from, to: address, confidence: 1 });
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return { nodes, edges, warnings };
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
function extractContainers(doc, lines, workloadAddress, podPath, nodes, edges) {
|
|
2499
|
+
const seenImages = /* @__PURE__ */ new Set();
|
|
2500
|
+
for (const field of CONTAINER_FIELDS) {
|
|
2501
|
+
const containers = getArray(doc.value, [...podPath, field]);
|
|
2502
|
+
containers.forEach((container, index) => {
|
|
2503
|
+
const image = getString(container, ["image"]);
|
|
2504
|
+
if (!image) return;
|
|
2505
|
+
const ref = parseImageRef(image);
|
|
2506
|
+
if (!ref) return;
|
|
2507
|
+
if (seenImages.has(ref.raw)) return;
|
|
2508
|
+
seenImages.add(ref.raw);
|
|
2509
|
+
nodes.push({
|
|
2510
|
+
kind: "image",
|
|
2511
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
2512
|
+
qualifiedName: `image:${ref.raw}`,
|
|
2513
|
+
span: spanAt(doc, lines, [...podPath, field, index, "image"]),
|
|
2514
|
+
signature: "k8s.image",
|
|
2515
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
2516
|
+
importance: 0.3
|
|
2517
|
+
});
|
|
2518
|
+
edges.push({
|
|
2519
|
+
kind: "builds_from",
|
|
2520
|
+
from: workloadAddress,
|
|
2521
|
+
to: `image:${ref.raw}`,
|
|
2522
|
+
confidence: 1
|
|
2523
|
+
});
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function referencedConfig(value, podPath) {
|
|
2528
|
+
if (!podPath) return [];
|
|
2529
|
+
const out = /* @__PURE__ */ new Set();
|
|
2530
|
+
for (const volume of getArray(value, [...podPath, "volumes"])) {
|
|
2531
|
+
const configMap = getString(volume, ["configMap", "name"]);
|
|
2532
|
+
if (configMap) out.add(`ConfigMap/${configMap}`);
|
|
2533
|
+
const secret = getString(volume, ["secret", "secretName"]);
|
|
2534
|
+
if (secret) out.add(`Secret/${secret}`);
|
|
2535
|
+
const claim = getString(volume, ["persistentVolumeClaim", "claimName"]);
|
|
2536
|
+
if (claim) out.add(`PersistentVolumeClaim/${claim}`);
|
|
2537
|
+
}
|
|
2538
|
+
for (const field of CONTAINER_FIELDS) {
|
|
2539
|
+
for (const container of getArray(value, [...podPath, field])) {
|
|
2540
|
+
for (const envFrom of getArray(container, ["envFrom"])) {
|
|
2541
|
+
const configMap = getString(envFrom, ["configMapRef", "name"]);
|
|
2542
|
+
if (configMap) out.add(`ConfigMap/${configMap}`);
|
|
2543
|
+
const secret = getString(envFrom, ["secretRef", "name"]);
|
|
2544
|
+
if (secret) out.add(`Secret/${secret}`);
|
|
2545
|
+
}
|
|
2546
|
+
for (const env of getArray(container, ["env"])) {
|
|
2547
|
+
const configMap = getString(env, ["valueFrom", "configMapKeyRef", "name"]);
|
|
2548
|
+
if (configMap) out.add(`ConfigMap/${configMap}`);
|
|
2549
|
+
const secret = getString(env, ["valueFrom", "secretKeyRef", "name"]);
|
|
2550
|
+
if (secret) out.add(`Secret/${secret}`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return [...out].sort();
|
|
2555
|
+
}
|
|
2556
|
+
function readLabels(value, path13) {
|
|
2557
|
+
const out = {};
|
|
2558
|
+
for (const [key, raw] of getRecordEntries(value, path13)) {
|
|
2559
|
+
if (typeof raw === "string") out[key] = raw;
|
|
2560
|
+
else if (typeof raw === "number" || typeof raw === "boolean") out[key] = String(raw);
|
|
2561
|
+
}
|
|
2562
|
+
return out;
|
|
2563
|
+
}
|
|
2564
|
+
function isSubset(selector, labels) {
|
|
2565
|
+
const keys = Object.keys(selector);
|
|
2566
|
+
if (!keys.length) return false;
|
|
2567
|
+
return keys.every((key) => labels[key] === selector[key]);
|
|
2568
|
+
}
|
|
2569
|
+
function describeObject(value, kind) {
|
|
2570
|
+
const parts = [];
|
|
2571
|
+
const replicas = getPath(value, ["spec", "replicas"]);
|
|
2572
|
+
if (typeof replicas === "number") parts.push(`${replicas} replica${replicas === 1 ? "" : "s"}`);
|
|
2573
|
+
const podPath = podSpecPath(kind);
|
|
2574
|
+
if (podPath) {
|
|
2575
|
+
const count = CONTAINER_FIELDS.reduce(
|
|
2576
|
+
(sum, field) => sum + getArray(value, [...podPath, field]).length,
|
|
2577
|
+
0
|
|
2578
|
+
);
|
|
2579
|
+
if (count) parts.push(`${count} container${count === 1 ? "" : "s"}`);
|
|
2580
|
+
}
|
|
2581
|
+
if (kind === "Service") {
|
|
2582
|
+
const type = getString(value, ["spec", "type"]);
|
|
2583
|
+
if (type) parts.push(type);
|
|
2584
|
+
const ports = getArray(value, ["spec", "ports"]).map((p) => getString(p, ["port"])).filter(Boolean);
|
|
2585
|
+
if (ports.length) parts.push(`port ${ports.join(", ")}`);
|
|
2586
|
+
}
|
|
2587
|
+
return parts.join(", ");
|
|
2588
|
+
}
|
|
2589
|
+
var require2 = createRequire(import.meta.url);
|
|
2590
|
+
var TOOLCHAIN_GRAMMARS = {
|
|
2591
|
+
hcl: { wasm: "tree-sitter-hcl", pkg: "@tree-sitter-grammars/tree-sitter-hcl" }
|
|
2592
|
+
};
|
|
2593
|
+
var languageCache = /* @__PURE__ */ new Map();
|
|
2594
|
+
function toolchainGrammarDirs(pkg) {
|
|
2595
|
+
const dirs = [...grammarDirs()];
|
|
2596
|
+
try {
|
|
2597
|
+
dirs.push(path12.dirname(require2.resolve(`${pkg}/package.json`)));
|
|
2598
|
+
} catch {
|
|
2599
|
+
}
|
|
2600
|
+
return dirs;
|
|
2601
|
+
}
|
|
2602
|
+
function resolveToolchainGrammar(id) {
|
|
2603
|
+
const spec = TOOLCHAIN_GRAMMARS[id];
|
|
2604
|
+
if (!spec) return null;
|
|
2605
|
+
for (const dir of toolchainGrammarDirs(spec.pkg)) {
|
|
2606
|
+
const candidate = path12.join(dir, `${spec.wasm}.wasm`);
|
|
2607
|
+
if (fs13.existsSync(candidate)) return candidate;
|
|
2608
|
+
}
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
async function loadToolchainLanguage(id) {
|
|
2612
|
+
const cached = languageCache.get(id);
|
|
2613
|
+
if (cached) return cached;
|
|
2614
|
+
const wasmPath = resolveToolchainGrammar(id);
|
|
2615
|
+
if (!wasmPath) return null;
|
|
2616
|
+
try {
|
|
2617
|
+
await ensureParserInit();
|
|
2618
|
+
const language = await Language.load(fs13.readFileSync(wasmPath));
|
|
2619
|
+
languageCache.set(id, language);
|
|
2620
|
+
return language;
|
|
2621
|
+
} catch {
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
async function toolchainParser(id) {
|
|
2626
|
+
const language = await loadToolchainLanguage(id);
|
|
2627
|
+
if (!language) return null;
|
|
2628
|
+
const parser = new Parser();
|
|
2629
|
+
parser.setLanguage(language);
|
|
2630
|
+
return parser;
|
|
2631
|
+
}
|
|
2632
|
+
function toolchainGrammarSetVersion() {
|
|
2633
|
+
const parts = [];
|
|
2634
|
+
for (const id of Object.keys(TOOLCHAIN_GRAMMARS).sort()) {
|
|
2635
|
+
const { pkg } = TOOLCHAIN_GRAMMARS[id];
|
|
2636
|
+
let version = "bundled";
|
|
2637
|
+
try {
|
|
2638
|
+
const json = require2.resolve(`${pkg}/package.json`);
|
|
2639
|
+
version = JSON.parse(fs13.readFileSync(json, "utf8")).version;
|
|
2640
|
+
} catch {
|
|
2641
|
+
}
|
|
2642
|
+
parts.push(`${pkg}@${version}`);
|
|
2643
|
+
}
|
|
2644
|
+
return parts.join(",");
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// src/engine/toolchain/terraform.ts
|
|
2648
|
+
var BUILTIN_REF_PREFIXES = /* @__PURE__ */ new Set([
|
|
2649
|
+
"count",
|
|
2650
|
+
"each",
|
|
2651
|
+
"self",
|
|
2652
|
+
"path",
|
|
2653
|
+
"terraform"
|
|
2654
|
+
]);
|
|
2655
|
+
var BLOCK_SPECS = {
|
|
2656
|
+
resource: {
|
|
2657
|
+
kind: "resource",
|
|
2658
|
+
labels: 2,
|
|
2659
|
+
address: ([type, name]) => `${type}.${name}`,
|
|
2660
|
+
signature: "terraform.resource",
|
|
2661
|
+
importance: 0.5
|
|
2662
|
+
},
|
|
2663
|
+
data: {
|
|
2664
|
+
kind: "resource",
|
|
2665
|
+
labels: 2,
|
|
2666
|
+
address: ([type, name]) => `data.${type}.${name}`,
|
|
2667
|
+
signature: "terraform.data",
|
|
2668
|
+
importance: 0.3
|
|
2669
|
+
},
|
|
2670
|
+
module: {
|
|
2671
|
+
kind: "module",
|
|
2672
|
+
labels: 1,
|
|
2673
|
+
address: ([name]) => `module.${name}`,
|
|
2674
|
+
signature: "terraform.module",
|
|
2675
|
+
importance: 0.6
|
|
2676
|
+
},
|
|
2677
|
+
variable: {
|
|
2678
|
+
kind: "property",
|
|
2679
|
+
labels: 1,
|
|
2680
|
+
address: ([name]) => `var.${name}`,
|
|
2681
|
+
signature: "terraform.variable",
|
|
2682
|
+
importance: 0.2
|
|
2683
|
+
},
|
|
2684
|
+
output: {
|
|
2685
|
+
kind: "property",
|
|
2686
|
+
labels: 1,
|
|
2687
|
+
address: ([name]) => `output.${name}`,
|
|
2688
|
+
signature: "terraform.output",
|
|
2689
|
+
importance: 0.3
|
|
2690
|
+
},
|
|
2691
|
+
provider: {
|
|
2692
|
+
kind: "package",
|
|
2693
|
+
labels: 1,
|
|
2694
|
+
address: ([name]) => `provider.${name}`,
|
|
2695
|
+
signature: "terraform.provider",
|
|
2696
|
+
importance: 0.4
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
function stringLitValue(node) {
|
|
2700
|
+
for (const child of node.namedChildren) {
|
|
2701
|
+
if (child?.type === "template_literal") return child.text;
|
|
2702
|
+
}
|
|
2703
|
+
return "";
|
|
2704
|
+
}
|
|
2705
|
+
function topLevelBlocks(root) {
|
|
2706
|
+
const out = [];
|
|
2707
|
+
for (const bodyCandidate of root.namedChildren) {
|
|
2708
|
+
if (!bodyCandidate) continue;
|
|
2709
|
+
const body = bodyCandidate.type === "body" ? bodyCandidate : null;
|
|
2710
|
+
if (!body) continue;
|
|
2711
|
+
for (const child of body.namedChildren) {
|
|
2712
|
+
if (child?.type === "block") out.push(child);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
return out;
|
|
2716
|
+
}
|
|
2717
|
+
function blockBody(block) {
|
|
2718
|
+
for (const child of block.namedChildren) {
|
|
2719
|
+
if (child?.type === "body") return child;
|
|
2720
|
+
}
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
function attributes(body) {
|
|
2724
|
+
const out = [];
|
|
2725
|
+
for (const child of body.namedChildren) {
|
|
2726
|
+
if (child?.type !== "attribute") continue;
|
|
2727
|
+
let name = "";
|
|
2728
|
+
let expr = null;
|
|
2729
|
+
for (const part of child.namedChildren) {
|
|
2730
|
+
if (!part) continue;
|
|
2731
|
+
if (!name && part.type === "identifier") name = part.text;
|
|
2732
|
+
else if (part.type === "expression") expr = part;
|
|
2733
|
+
}
|
|
2734
|
+
if (name) out.push({ name, expr, node: child });
|
|
2735
|
+
}
|
|
2736
|
+
return out;
|
|
2737
|
+
}
|
|
2738
|
+
function nestedBlocks(body) {
|
|
2739
|
+
const out = [];
|
|
2740
|
+
for (const child of body.namedChildren) {
|
|
2741
|
+
if (child?.type === "block") out.push(child);
|
|
2742
|
+
}
|
|
2743
|
+
return out;
|
|
2744
|
+
}
|
|
2745
|
+
function blockHead(block) {
|
|
2746
|
+
let type = "";
|
|
2747
|
+
const labels = [];
|
|
2748
|
+
for (const child of block.namedChildren) {
|
|
2749
|
+
if (!child) continue;
|
|
2750
|
+
if (!type && child.type === "identifier") type = child.text;
|
|
2751
|
+
else if (child.type === "string_lit") labels.push(stringLitValue(child));
|
|
2752
|
+
else if (child.type === "body") break;
|
|
2753
|
+
}
|
|
2754
|
+
return { type, labels };
|
|
2755
|
+
}
|
|
2756
|
+
function referencedAddresses(expr, out) {
|
|
2757
|
+
if (!expr) return;
|
|
2758
|
+
const stack = [expr];
|
|
2759
|
+
while (stack.length) {
|
|
2760
|
+
const node = stack.pop();
|
|
2761
|
+
if (!node) continue;
|
|
2762
|
+
if (node.type === "variable_expr") {
|
|
2763
|
+
const root = node.text;
|
|
2764
|
+
const parts = [root];
|
|
2765
|
+
let sibling = node.nextNamedSibling;
|
|
2766
|
+
while (sibling && sibling.type === "get_attr") {
|
|
2767
|
+
const id = sibling.namedChildren.find((c) => c?.type === "identifier");
|
|
2768
|
+
if (!id) break;
|
|
2769
|
+
parts.push(id.text);
|
|
2770
|
+
sibling = sibling.nextNamedSibling;
|
|
2771
|
+
}
|
|
2772
|
+
const address = terraformAddress(parts);
|
|
2773
|
+
if (address) out.add(address);
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
for (const child of node.namedChildren) {
|
|
2777
|
+
if (child) stack.push(child);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
function terraformAddress(parts) {
|
|
2782
|
+
const [head, ...rest] = parts;
|
|
2783
|
+
if (!head || BUILTIN_REF_PREFIXES.has(head)) return null;
|
|
2784
|
+
if (head === "var" || head === "local") return rest.length ? `${head}.${rest[0]}` : null;
|
|
2785
|
+
if (head === "module") return rest.length ? `module.${rest[0]}` : null;
|
|
2786
|
+
if (head === "data") return rest.length >= 2 ? `data.${rest[0]}.${rest[1]}` : null;
|
|
2787
|
+
return rest.length ? `${head}.${rest[0]}` : null;
|
|
2788
|
+
}
|
|
2789
|
+
function extractRequiredProviders(terraformBlock, lines, nodes, seen) {
|
|
2790
|
+
const body = blockBody(terraformBlock);
|
|
2791
|
+
if (!body) return;
|
|
2792
|
+
for (const nested of nestedBlocks(body)) {
|
|
2793
|
+
const { type } = blockHead(nested);
|
|
2794
|
+
if (type !== "required_providers") continue;
|
|
2795
|
+
const inner = blockBody(nested);
|
|
2796
|
+
if (!inner) continue;
|
|
2797
|
+
for (const attr of attributes(inner)) {
|
|
2798
|
+
let source;
|
|
2799
|
+
let version;
|
|
2800
|
+
const objectAttrs = attr.expr ? collectObjectAttributes(attr.expr) : [];
|
|
2801
|
+
if (objectAttrs.length) {
|
|
2802
|
+
for (const { key, value } of objectAttrs) {
|
|
2803
|
+
if (key === "source") source = value;
|
|
2804
|
+
else if (key === "version") version = value;
|
|
2805
|
+
}
|
|
2806
|
+
} else if (attr.expr) {
|
|
2807
|
+
const literal = findFirst(attr.expr, "string_lit");
|
|
2808
|
+
if (literal) {
|
|
2809
|
+
version = stringLitValue(literal);
|
|
2810
|
+
source = `hashicorp/${attr.name}`;
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
const address = `provider.${attr.name}`;
|
|
2814
|
+
if (seen.has(address)) continue;
|
|
2815
|
+
seen.add(address);
|
|
2816
|
+
nodes.push({
|
|
2817
|
+
kind: "package",
|
|
2818
|
+
name: attr.name,
|
|
2819
|
+
qualifiedName: address,
|
|
2820
|
+
span: lines.span(attr.node.startIndex, attr.node.endIndex),
|
|
2821
|
+
signature: "terraform.required_provider",
|
|
2822
|
+
doc: safeDoc(
|
|
2823
|
+
[source && `source ${source}`, version && `version ${version}`].filter(Boolean).join(", ")
|
|
2824
|
+
),
|
|
2825
|
+
importance: 0.4
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
function collectObjectAttributes(expr) {
|
|
2831
|
+
const out = [];
|
|
2832
|
+
const stack = [expr];
|
|
2833
|
+
while (stack.length) {
|
|
2834
|
+
const node = stack.pop();
|
|
2835
|
+
if (!node) continue;
|
|
2836
|
+
if (node.type === "object_elem") {
|
|
2837
|
+
const key = node.namedChildren.find((c) => c?.type === "expression");
|
|
2838
|
+
const rest = node.namedChildren.filter((c) => c?.type === "expression");
|
|
2839
|
+
const valueNode = rest.length > 1 ? rest[1] : null;
|
|
2840
|
+
const keyText = key ? plainText(key) : "";
|
|
2841
|
+
const valueText = valueNode ? plainText(valueNode) : "";
|
|
2842
|
+
if (keyText) out.push({ key: keyText, value: valueText });
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
for (const child of node.namedChildren) {
|
|
2846
|
+
if (child) stack.push(child);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
return out;
|
|
2850
|
+
}
|
|
2851
|
+
function plainText(expr) {
|
|
2852
|
+
const lit = findFirst(expr, "string_lit");
|
|
2853
|
+
if (lit) return stringLitValue(lit);
|
|
2854
|
+
return expr.text.trim();
|
|
2855
|
+
}
|
|
2856
|
+
function findFirst(node, type) {
|
|
2857
|
+
const stack = [node];
|
|
2858
|
+
while (stack.length) {
|
|
2859
|
+
const current = stack.pop();
|
|
2860
|
+
if (!current) continue;
|
|
2861
|
+
if (current.type === type) return current;
|
|
2862
|
+
for (const child of current.namedChildren) {
|
|
2863
|
+
if (child) stack.push(child);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return null;
|
|
2867
|
+
}
|
|
2868
|
+
var terraformExtractor = {
|
|
2869
|
+
format: "terraform",
|
|
2870
|
+
matches(rel2) {
|
|
2871
|
+
const lower = rel2.toLowerCase();
|
|
2872
|
+
return lower.endsWith(".tf") || lower.endsWith(".tofu");
|
|
2873
|
+
},
|
|
2874
|
+
async extract(rel2, source) {
|
|
2875
|
+
const parser = await toolchainParser("hcl");
|
|
2876
|
+
if (!parser) {
|
|
2877
|
+
return {
|
|
2878
|
+
nodes: [],
|
|
2879
|
+
edges: [],
|
|
2880
|
+
warnings: [`${rel2}: HCL grammar unavailable \u2014 no Terraform structure extracted`]
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
const tree = parser.parse(source);
|
|
2884
|
+
if (!tree) return { nodes: [], edges: [], warnings: [`${rel2}: HCL parse produced no tree`] };
|
|
2885
|
+
const lines = new LineIndex(source);
|
|
2886
|
+
const nodes = [];
|
|
2887
|
+
const edges = [];
|
|
2888
|
+
const warnings = [];
|
|
2889
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2890
|
+
const pendingEdges = [];
|
|
2891
|
+
if (tree.rootNode.hasError) {
|
|
2892
|
+
warnings.push(`${rel2}: HCL parse recovered from a syntax error \u2014 extraction may be partial`);
|
|
2893
|
+
}
|
|
2894
|
+
for (const block of topLevelBlocks(tree.rootNode)) {
|
|
2895
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
2896
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
2897
|
+
break;
|
|
2898
|
+
}
|
|
2899
|
+
const { type, labels } = blockHead(block);
|
|
2900
|
+
if (type === "terraform") {
|
|
2901
|
+
extractRequiredProviders(block, lines, nodes, declared);
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
const spec = BLOCK_SPECS[type];
|
|
2905
|
+
if (!spec || labels.length < spec.labels) continue;
|
|
2906
|
+
const address = spec.address(labels);
|
|
2907
|
+
if (declared.has(address)) continue;
|
|
2908
|
+
declared.add(address);
|
|
2909
|
+
const body = blockBody(block);
|
|
2910
|
+
const attrs = body ? attributes(body) : [];
|
|
2911
|
+
const summaryParts = [];
|
|
2912
|
+
for (const attr of attrs) {
|
|
2913
|
+
if (attr.name === "source" || attr.name === "version" || attr.name === "description") {
|
|
2914
|
+
const value = attr.expr ? plainText(attr.expr) : "";
|
|
2915
|
+
if (value) summaryParts.push(`${attr.name} ${value}`);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
nodes.push({
|
|
2919
|
+
kind: spec.kind,
|
|
2920
|
+
name: labels[labels.length - 1] ?? type,
|
|
2921
|
+
qualifiedName: address,
|
|
2922
|
+
span: lines.span(block.startIndex, block.endIndex),
|
|
2923
|
+
signature: spec.signature,
|
|
2924
|
+
doc: safeDoc(summaryParts.join(", ")),
|
|
2925
|
+
importance: spec.importance
|
|
2926
|
+
});
|
|
2927
|
+
if (!body) continue;
|
|
2928
|
+
const references = /* @__PURE__ */ new Set();
|
|
2929
|
+
for (const attr of attrs) {
|
|
2930
|
+
referencedAddresses(attr.expr, references);
|
|
2931
|
+
}
|
|
2932
|
+
const nestedStack = nestedBlocks(body);
|
|
2933
|
+
while (nestedStack.length) {
|
|
2934
|
+
const nested = nestedStack.pop();
|
|
2935
|
+
if (!nested) continue;
|
|
2936
|
+
const innerBody = blockBody(nested);
|
|
2937
|
+
if (!innerBody) continue;
|
|
2938
|
+
for (const attr of attributes(innerBody)) referencedAddresses(attr.expr, references);
|
|
2939
|
+
nestedStack.push(...nestedBlocks(innerBody));
|
|
2940
|
+
}
|
|
2941
|
+
references.delete(address);
|
|
2942
|
+
for (const target of [...references].sort()) {
|
|
2943
|
+
pendingEdges.push({ kind: "depends_on", from: address, to: target, confidence: 1 });
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
for (const edge of pendingEdges) {
|
|
2947
|
+
if (declared.has(edge.to)) edges.push(edge);
|
|
2948
|
+
}
|
|
2949
|
+
return { nodes, edges, warnings };
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
|
|
2953
|
+
// src/engine/toolchain/workflows.ts
|
|
2954
|
+
var MAX_STEPS_PER_JOB = 100;
|
|
2955
|
+
var GITLAB_RESERVED = /* @__PURE__ */ new Set([
|
|
2956
|
+
"stages",
|
|
2957
|
+
"variables",
|
|
2958
|
+
"default",
|
|
2959
|
+
"include",
|
|
2960
|
+
"workflow",
|
|
2961
|
+
"image",
|
|
2962
|
+
"services",
|
|
2963
|
+
"before_script",
|
|
2964
|
+
"after_script",
|
|
2965
|
+
"cache",
|
|
2966
|
+
"pages"
|
|
2967
|
+
]);
|
|
2968
|
+
function isGitHubWorkflowPath(rel2) {
|
|
2969
|
+
return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(toPosix3(rel2));
|
|
2970
|
+
}
|
|
2971
|
+
function isGitLabCiPath(rel2) {
|
|
2972
|
+
const base = toPosix3(rel2).split("/").pop()?.toLowerCase() ?? "";
|
|
2973
|
+
return base === ".gitlab-ci.yml" || base === ".gitlab-ci.yaml";
|
|
2974
|
+
}
|
|
2975
|
+
function imagesBuiltBy(script) {
|
|
2976
|
+
const out = /* @__PURE__ */ new Set();
|
|
2977
|
+
const buildTag = /\b(?:docker|podman)\s+(?:buildx\s+)?build\b[^\n]*?(?:-t|--tag)[=\s]+(\S+)/g;
|
|
2978
|
+
for (const match of script.matchAll(buildTag)) out.add(match[1]);
|
|
2979
|
+
const push2 = /\b(?:docker|podman)\s+push\s+(\S+)/g;
|
|
2980
|
+
for (const match of script.matchAll(push2)) out.add(match[1]);
|
|
2981
|
+
return [...out].filter((tag) => !tag.includes("${") && !tag.includes("{{")).sort();
|
|
2982
|
+
}
|
|
2983
|
+
function terraformAppliedDirs(script, workingDirectory) {
|
|
2984
|
+
const out = /* @__PURE__ */ new Set();
|
|
2985
|
+
const apply = /\b(?:terraform|tofu)\s+(?:-chdir=(\S+)\s+)?(?:apply|destroy)\b/g;
|
|
2986
|
+
for (const match of script.matchAll(apply)) {
|
|
2987
|
+
out.add(match[1] ?? workingDirectory ?? ".");
|
|
2988
|
+
}
|
|
2989
|
+
return [...out].sort();
|
|
2990
|
+
}
|
|
2991
|
+
function collectSecretReferences(value, out) {
|
|
2992
|
+
const stack = [value];
|
|
2993
|
+
while (stack.length) {
|
|
2994
|
+
const current = stack.pop();
|
|
2995
|
+
if (typeof current === "string") {
|
|
2996
|
+
for (const match of current.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
2997
|
+
out.add(`secret:${match[1]}`);
|
|
2998
|
+
}
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
3001
|
+
if (Array.isArray(current)) {
|
|
3002
|
+
stack.push(...current);
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
if (current && typeof current === "object") {
|
|
3006
|
+
stack.push(...Object.values(current));
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
var githubActionsExtractor = {
|
|
3011
|
+
format: "github-actions",
|
|
3012
|
+
matches(rel2) {
|
|
3013
|
+
return isGitHubWorkflowPath(rel2);
|
|
3014
|
+
},
|
|
3015
|
+
extract(rel2, source) {
|
|
3016
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
3017
|
+
const lines = new LineIndex(source);
|
|
3018
|
+
const nodes = [];
|
|
3019
|
+
const edges = [];
|
|
3020
|
+
const doc = docs[0];
|
|
3021
|
+
if (!doc) return { nodes, edges, warnings };
|
|
3022
|
+
const workflowName = getString(doc.value, ["name"]) ?? toPosix3(rel2).split("/").pop() ?? rel2;
|
|
3023
|
+
const workflowAddress = `workflow:${toPosix3(rel2)}`;
|
|
3024
|
+
nodes.push({
|
|
3025
|
+
kind: "job",
|
|
3026
|
+
name: workflowName,
|
|
3027
|
+
qualifiedName: workflowAddress,
|
|
3028
|
+
span: spanAt(doc, lines, []),
|
|
3029
|
+
signature: "gha.workflow",
|
|
3030
|
+
doc: safeDoc(`GitHub Actions workflow`),
|
|
3031
|
+
importance: 0.6
|
|
3032
|
+
});
|
|
3033
|
+
const onValue = getPath(doc.value, ["on"]) ?? getPath(doc.value, [true]);
|
|
3034
|
+
for (const event of triggerEvents(onValue)) {
|
|
3035
|
+
const eventAddress = `event:${event}`;
|
|
3036
|
+
nodes.push({
|
|
3037
|
+
kind: "property",
|
|
3038
|
+
name: event,
|
|
3039
|
+
qualifiedName: eventAddress,
|
|
3040
|
+
span: spanAt(doc, lines, ["on"]),
|
|
3041
|
+
signature: "gha.event",
|
|
3042
|
+
importance: 0.2
|
|
3043
|
+
});
|
|
3044
|
+
edges.push({ kind: "triggers", from: eventAddress, to: workflowAddress, confidence: 1 });
|
|
3045
|
+
}
|
|
3046
|
+
const jobs = getRecordEntries(doc.value, ["jobs"]);
|
|
3047
|
+
const jobAddresses = new Set(jobs.map(([id]) => `job:${id}`));
|
|
3048
|
+
for (const [jobId, job] of jobs) {
|
|
3049
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
3050
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
3051
|
+
break;
|
|
3052
|
+
}
|
|
3053
|
+
const jobAddress = `job:${jobId}`;
|
|
3054
|
+
const runsOn = getPath(job, ["runs-on"]);
|
|
3055
|
+
const steps = getArray(job, ["steps"]);
|
|
3056
|
+
nodes.push({
|
|
3057
|
+
kind: "job",
|
|
3058
|
+
name: getString(job, ["name"]) ?? jobId,
|
|
3059
|
+
qualifiedName: jobAddress,
|
|
3060
|
+
span: spanAt(doc, lines, ["jobs", jobId]),
|
|
3061
|
+
signature: "gha.job",
|
|
3062
|
+
doc: safeDoc(
|
|
3063
|
+
[
|
|
3064
|
+
typeof runsOn === "string" ? `runs-on ${runsOn}` : null,
|
|
3065
|
+
steps.length ? `${steps.length} step${steps.length === 1 ? "" : "s"}` : null
|
|
3066
|
+
].filter(Boolean).join(", ")
|
|
3067
|
+
),
|
|
3068
|
+
importance: 0.5
|
|
3069
|
+
});
|
|
3070
|
+
edges.push({ kind: "contains", from: workflowAddress, to: jobAddress, confidence: 1 });
|
|
3071
|
+
for (const need of asStringList(getPath(job, ["needs"]))) {
|
|
3072
|
+
const to = `job:${need}`;
|
|
3073
|
+
if (jobAddresses.has(to)) {
|
|
3074
|
+
edges.push({ kind: "depends_on", from: jobAddress, to, confidence: 1 });
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
steps.slice(0, MAX_STEPS_PER_JOB).forEach((step, index) => {
|
|
3078
|
+
const uses = getString(step, ["uses"]);
|
|
3079
|
+
const run = getString(step, ["run"]);
|
|
3080
|
+
const stepName = getString(step, ["name"]) ?? uses ?? (run ? run.split("\n")[0] : `step ${index + 1}`);
|
|
3081
|
+
const stepAddress = `${jobAddress}#${index}`;
|
|
3082
|
+
nodes.push({
|
|
3083
|
+
kind: "step",
|
|
3084
|
+
name: stepName.slice(0, 80),
|
|
3085
|
+
qualifiedName: stepAddress,
|
|
3086
|
+
span: spanAt(doc, lines, ["jobs", jobId, "steps", index]),
|
|
3087
|
+
signature: uses ? "gha.step.uses" : "gha.step.run",
|
|
3088
|
+
// `run:` bodies are shell scripts and are exactly where a token gets
|
|
3089
|
+
// pasted — safeDoc redacts, and we keep only the first line.
|
|
3090
|
+
doc: safeDoc(uses ? `uses ${uses}` : run?.split("\n")[0]),
|
|
3091
|
+
importance: 0.2
|
|
3092
|
+
});
|
|
3093
|
+
edges.push({ kind: "contains", from: jobAddress, to: stepAddress, confidence: 1 });
|
|
3094
|
+
if (uses) {
|
|
3095
|
+
const actionAddress = `action:${uses}`;
|
|
3096
|
+
nodes.push({
|
|
3097
|
+
kind: "external",
|
|
3098
|
+
name: uses.split("@")[0],
|
|
3099
|
+
qualifiedName: actionAddress,
|
|
3100
|
+
span: spanAt(doc, lines, ["jobs", jobId, "steps", index, "uses"]),
|
|
3101
|
+
signature: "gha.action",
|
|
3102
|
+
importance: 0.2
|
|
3103
|
+
});
|
|
3104
|
+
edges.push({ kind: "depends_on", from: stepAddress, to: actionAddress, confidence: 1 });
|
|
3105
|
+
}
|
|
3106
|
+
if (run) {
|
|
3107
|
+
const stepSpan = spanAt(doc, lines, ["jobs", jobId, "steps", index]);
|
|
3108
|
+
const workingDirectory = getString(step, ["working-directory"]) ?? getString(job, ["defaults", "run", "working-directory"]);
|
|
3109
|
+
emitScriptEffects(rel2, run, workingDirectory, jobAddress, stepSpan, nodes, edges);
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
3112
|
+
if (steps.length > MAX_STEPS_PER_JOB) {
|
|
3113
|
+
warnings.push(`${rel2}: job ${jobId} has ${steps.length} steps \u2014 enumerated the first ${MAX_STEPS_PER_JOB}`);
|
|
3114
|
+
}
|
|
3115
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
3116
|
+
collectSecretReferences(job, secrets);
|
|
3117
|
+
for (const secret of [...secrets].sort()) {
|
|
3118
|
+
nodes.push({
|
|
3119
|
+
kind: "property",
|
|
3120
|
+
name: secret.slice("secret:".length),
|
|
3121
|
+
qualifiedName: secret,
|
|
3122
|
+
span: spanAt(doc, lines, ["jobs", jobId]),
|
|
3123
|
+
signature: "gha.secret-ref",
|
|
3124
|
+
importance: 0.2
|
|
3125
|
+
});
|
|
3126
|
+
edges.push({ kind: "mounts", from: jobAddress, to: secret, confidence: 1 });
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return { nodes, edges, warnings };
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3132
|
+
var gitlabCiExtractor = {
|
|
3133
|
+
format: "gitlab-ci",
|
|
3134
|
+
matches(rel2) {
|
|
3135
|
+
return isGitLabCiPath(rel2);
|
|
3136
|
+
},
|
|
3137
|
+
extract(rel2, source) {
|
|
3138
|
+
const { docs, warnings } = parseYamlStream(rel2, source);
|
|
3139
|
+
const lines = new LineIndex(source);
|
|
3140
|
+
const nodes = [];
|
|
3141
|
+
const edges = [];
|
|
3142
|
+
const doc = docs[0];
|
|
3143
|
+
if (!doc) return { nodes, edges, warnings };
|
|
3144
|
+
const pipelineAddress = `workflow:${toPosix3(rel2)}`;
|
|
3145
|
+
nodes.push({
|
|
3146
|
+
kind: "job",
|
|
3147
|
+
name: "GitLab CI",
|
|
3148
|
+
qualifiedName: pipelineAddress,
|
|
3149
|
+
span: spanAt(doc, lines, []),
|
|
3150
|
+
signature: "gitlab.pipeline",
|
|
3151
|
+
importance: 0.6
|
|
3152
|
+
});
|
|
3153
|
+
const jobs = getRecordEntries(doc.value, []).filter(
|
|
3154
|
+
([key, value]) => !GITLAB_RESERVED.has(key) && !key.startsWith(".") && value !== null && typeof value === "object" && !Array.isArray(value)
|
|
3155
|
+
);
|
|
3156
|
+
const jobAddresses = new Set(jobs.map(([id]) => `job:${id}`));
|
|
3157
|
+
for (const [jobId, job] of jobs) {
|
|
3158
|
+
if (nodes.length >= TOOLCHAIN_NODES_PER_FILE_MAX) {
|
|
3159
|
+
warnings.push(`${rel2}: stopped at ${TOOLCHAIN_NODES_PER_FILE_MAX} nodes`);
|
|
3160
|
+
break;
|
|
3161
|
+
}
|
|
3162
|
+
const jobAddress = `job:${jobId}`;
|
|
3163
|
+
const stage = getString(job, ["stage"]);
|
|
3164
|
+
const image = getString(job, ["image"]) ?? getString(job, ["image", "name"]);
|
|
3165
|
+
nodes.push({
|
|
3166
|
+
kind: "job",
|
|
3167
|
+
name: jobId,
|
|
3168
|
+
qualifiedName: jobAddress,
|
|
3169
|
+
span: spanAt(doc, lines, [jobId]),
|
|
3170
|
+
signature: "gitlab.job",
|
|
3171
|
+
doc: safeDoc([stage && `stage ${stage}`, image && `image ${image}`].filter(Boolean).join(", ")),
|
|
3172
|
+
importance: 0.5
|
|
3173
|
+
});
|
|
3174
|
+
edges.push({ kind: "contains", from: pipelineAddress, to: jobAddress, confidence: 1 });
|
|
3175
|
+
for (const key of ["needs", "dependencies"]) {
|
|
3176
|
+
for (const need of asStringList(getPath(job, [key]))) {
|
|
3177
|
+
const to = `job:${need}`;
|
|
3178
|
+
if (jobAddresses.has(to)) {
|
|
3179
|
+
edges.push({ kind: "depends_on", from: jobAddress, to, confidence: 1 });
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
return { nodes, edges, warnings };
|
|
3185
|
+
}
|
|
3186
|
+
};
|
|
3187
|
+
function emitScriptEffects(rel2, script, workingDirectory, jobAddress, span, nodes, edges) {
|
|
3188
|
+
for (const tag of imagesBuiltBy(script)) {
|
|
3189
|
+
const ref = parseImageRef(tag);
|
|
3190
|
+
if (!ref) continue;
|
|
3191
|
+
const address = `image:${ref.raw}`;
|
|
3192
|
+
nodes.push({
|
|
3193
|
+
kind: "image",
|
|
3194
|
+
name: ref.repository.split("/").pop() ?? ref.repository,
|
|
3195
|
+
qualifiedName: address,
|
|
3196
|
+
span,
|
|
3197
|
+
signature: "ci.builds-image",
|
|
3198
|
+
doc: safeDoc(ref.tag ? `${ref.repository} tag ${ref.tag}` : ref.repository),
|
|
3199
|
+
importance: 0.4
|
|
3200
|
+
});
|
|
3201
|
+
edges.push({ kind: "provisions", from: jobAddress, to: address, confidence: 1 });
|
|
3202
|
+
}
|
|
3203
|
+
for (const dir of terraformAppliedDirs(script, workingDirectory)) {
|
|
3204
|
+
const resolved = normaliseWorkspacePath(dir);
|
|
3205
|
+
const address = `tfdir:${resolved}`;
|
|
3206
|
+
nodes.push({
|
|
3207
|
+
kind: "resource",
|
|
3208
|
+
name: resolved,
|
|
3209
|
+
qualifiedName: address,
|
|
3210
|
+
span,
|
|
3211
|
+
signature: "ci.terraform-apply",
|
|
3212
|
+
doc: safeDoc(`terraform apply in ${resolved}`),
|
|
3213
|
+
importance: 0.4
|
|
3214
|
+
});
|
|
3215
|
+
edges.push({ kind: "deploys", from: jobAddress, to: address, confidence: 1 });
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
function normaliseWorkspacePath(dir) {
|
|
3219
|
+
const out = [];
|
|
3220
|
+
for (const part of toPosix3(dir).split("/")) {
|
|
3221
|
+
if (!part || part === ".") continue;
|
|
3222
|
+
if (part === "..") {
|
|
3223
|
+
out.pop();
|
|
3224
|
+
continue;
|
|
3225
|
+
}
|
|
3226
|
+
out.push(part);
|
|
3227
|
+
}
|
|
3228
|
+
return out.join("/") || ".";
|
|
3229
|
+
}
|
|
3230
|
+
function triggerEvents(value) {
|
|
3231
|
+
if (typeof value === "string") return [value];
|
|
3232
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string").sort();
|
|
3233
|
+
if (value && typeof value === "object") return Object.keys(value).sort();
|
|
3234
|
+
return [];
|
|
3235
|
+
}
|
|
3236
|
+
function asStringList(value) {
|
|
3237
|
+
if (typeof value === "string") return [value];
|
|
3238
|
+
if (!Array.isArray(value)) return [];
|
|
3239
|
+
const out = [];
|
|
3240
|
+
for (const item of value) {
|
|
3241
|
+
if (typeof item === "string") out.push(item);
|
|
3242
|
+
else if (item && typeof item === "object") {
|
|
3243
|
+
const job = item.job;
|
|
3244
|
+
if (typeof job === "string") out.push(job);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return out.sort();
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// src/engine/toolchain/link.ts
|
|
3251
|
+
var LINK_CONFIDENCE = {
|
|
3252
|
+
/** Compose names the Dockerfile path outright. */
|
|
3253
|
+
composeBuild: 0.95,
|
|
3254
|
+
/** A CI job builds tag X; a workload runs tag X. Same string, both sides. */
|
|
3255
|
+
imageIdentity: 0.8,
|
|
3256
|
+
/** A CI job applies Terraform in a directory that contains these resources. */
|
|
3257
|
+
terraformDir: 0.75,
|
|
3258
|
+
/** A Dockerfile copies a path that exists in the repository. */
|
|
3259
|
+
dockerfileCopy: 0.6
|
|
3260
|
+
};
|
|
3261
|
+
function linkToolchain(input) {
|
|
3262
|
+
const { nodes, edges, fileNodes, stageCopySources } = input;
|
|
3263
|
+
const out = /* @__PURE__ */ new Map();
|
|
3264
|
+
const counts = {};
|
|
3265
|
+
const add3 = (kind, src, dst, confidence, join11) => {
|
|
3266
|
+
if (src === dst) return;
|
|
3267
|
+
const id = edgeId(kind, src, dst);
|
|
3268
|
+
if (out.has(id)) return;
|
|
3269
|
+
out.set(id, {
|
|
3270
|
+
id,
|
|
3271
|
+
kind,
|
|
3272
|
+
src,
|
|
3273
|
+
dst,
|
|
3274
|
+
resolution: "heuristic",
|
|
3275
|
+
confidence,
|
|
3276
|
+
epistemic: "name-matched"
|
|
3277
|
+
});
|
|
3278
|
+
counts[join11] = (counts[join11] ?? 0) + 1;
|
|
3279
|
+
};
|
|
3280
|
+
const declared = new Set(edges.map((e) => `${e.kind} ${e.src} ${e.dst}`));
|
|
3281
|
+
const dockerfileByPath = /* @__PURE__ */ new Map();
|
|
3282
|
+
const dockerfileRefs = [];
|
|
3283
|
+
const imageSites = /* @__PURE__ */ new Map();
|
|
3284
|
+
const terraformDirMarkers = [];
|
|
3285
|
+
const terraformNodesByDir = /* @__PURE__ */ new Map();
|
|
3286
|
+
const stagesByDockerfile = /* @__PURE__ */ new Map();
|
|
3287
|
+
for (const node of nodes) {
|
|
3288
|
+
const qn = node.qualifiedName;
|
|
3289
|
+
if (qn.startsWith("dockerfile:")) {
|
|
3290
|
+
const target = qn.slice("dockerfile:".length);
|
|
3291
|
+
if (target.includes("#")) {
|
|
3292
|
+
push(stagesByDockerfile, target.split("#")[0], node);
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
if (toPosix3(node.file) === target) dockerfileByPath.set(target, node);
|
|
3296
|
+
else dockerfileRefs.push(node);
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
if (qn.startsWith("image:")) {
|
|
3300
|
+
const ref = parseImageRef(qn.slice("image:".length));
|
|
3301
|
+
if (ref) push(imageSites, imageIdentity(ref), node);
|
|
3302
|
+
continue;
|
|
3303
|
+
}
|
|
3304
|
+
if (qn.startsWith("tfdir:")) {
|
|
3305
|
+
terraformDirMarkers.push(node);
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
if (node.lang === "terraform") push(terraformNodesByDir, dirOf(toPosix3(node.file)), node);
|
|
3309
|
+
}
|
|
3310
|
+
for (const ref of dockerfileRefs) {
|
|
3311
|
+
const target = dockerfileByPath.get(ref.qualifiedName.slice("dockerfile:".length));
|
|
3312
|
+
if (!target) continue;
|
|
3313
|
+
add3("builds_from", ref.id, target.id, LINK_CONFIDENCE.composeBuild, "compose\u2192dockerfile");
|
|
3314
|
+
}
|
|
3315
|
+
const producerOf = /* @__PURE__ */ new Map();
|
|
3316
|
+
const consumerOf = /* @__PURE__ */ new Map();
|
|
3317
|
+
for (const edge of edges) {
|
|
3318
|
+
if (edge.kind === "provisions") push(producerOf, edge.dst, edge.src);
|
|
3319
|
+
else if (edge.kind === "builds_from") push(consumerOf, edge.dst, edge.src);
|
|
3320
|
+
}
|
|
3321
|
+
for (const identity of [...imageSites.keys()].sort((a, b) => a.localeCompare(b, "en"))) {
|
|
3322
|
+
const sites = imageSites.get(identity) ?? [];
|
|
3323
|
+
if (sites.length < 2) continue;
|
|
3324
|
+
const producers = /* @__PURE__ */ new Set();
|
|
3325
|
+
const consumers = /* @__PURE__ */ new Set();
|
|
3326
|
+
for (const site of sites) {
|
|
3327
|
+
for (const id of producerOf.get(site.id) ?? []) producers.add(id);
|
|
3328
|
+
for (const id of consumerOf.get(site.id) ?? []) consumers.add(id);
|
|
3329
|
+
}
|
|
3330
|
+
for (const producer of [...producers].sort()) {
|
|
3331
|
+
for (const consumer of [...consumers].sort()) {
|
|
3332
|
+
if (declared.has(`deploys ${producer} ${consumer}`)) continue;
|
|
3333
|
+
add3("deploys", producer, consumer, LINK_CONFIDENCE.imageIdentity, "ci\u2192workload");
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
const ordered = [...sites].sort((a, b) => a.id.localeCompare(b.id, "en"));
|
|
3337
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
3338
|
+
add3("references", ordered[0].id, ordered[i].id, LINK_CONFIDENCE.imageIdentity, "image\u2194image");
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
const terraformDirs = [...terraformNodesByDir.keys()].sort((a, b) => a.localeCompare(b, "en"));
|
|
3342
|
+
for (const marker of [...terraformDirMarkers].sort((a, b) => a.id.localeCompare(b.id, "en"))) {
|
|
3343
|
+
const dir = marker.qualifiedName.slice("tfdir:".length);
|
|
3344
|
+
for (const resourceDir of terraformDirs) {
|
|
3345
|
+
if (resourceDir !== dir && !resourceDir.startsWith(`${dir}/`)) continue;
|
|
3346
|
+
for (const resource of terraformNodesByDir.get(resourceDir) ?? []) {
|
|
3347
|
+
if (resource.signature !== "terraform.resource" && resource.signature !== "terraform.module") {
|
|
3348
|
+
continue;
|
|
3349
|
+
}
|
|
3350
|
+
add3("deploys", marker.id, resource.id, LINK_CONFIDENCE.terraformDir, "ci\u2192terraform");
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
for (const file of [...stagesByDockerfile.keys()].sort((a, b) => a.localeCompare(b, "en"))) {
|
|
3355
|
+
const stages = [...stagesByDockerfile.get(file) ?? []].sort(
|
|
3356
|
+
(a, b) => a.qualifiedName.localeCompare(b.qualifiedName, "en")
|
|
3357
|
+
);
|
|
3358
|
+
for (const stage of stages) {
|
|
3359
|
+
const copied = stageCopySources.get(stage.qualifiedName);
|
|
3360
|
+
if (!copied?.length) continue;
|
|
3361
|
+
for (const path13 of copied) {
|
|
3362
|
+
const fileNodeId = fileNodes.get(toPosix3(path13));
|
|
3363
|
+
if (!fileNodeId) continue;
|
|
3364
|
+
add3("depends_on", stage.id, fileNodeId, LINK_CONFIDENCE.dockerfileCopy, "dockerfile\u2192source");
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return {
|
|
3369
|
+
edges: [...out.values()].sort(
|
|
3370
|
+
(a, b) => a.kind.localeCompare(b.kind, "en") || a.src.localeCompare(b.src, "en") || a.dst.localeCompare(b.dst, "en")
|
|
3371
|
+
),
|
|
3372
|
+
counts
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
function push(map, key, value) {
|
|
3376
|
+
const list = map.get(key);
|
|
3377
|
+
if (list) list.push(value);
|
|
3378
|
+
else map.set(key, [value]);
|
|
3379
|
+
}
|
|
3380
|
+
function dirOf(rel2) {
|
|
3381
|
+
const idx = rel2.lastIndexOf("/");
|
|
3382
|
+
return idx === -1 ? "." : rel2.slice(0, idx);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// src/engine/toolchain/index.ts
|
|
3386
|
+
var EXTRACTORS = [
|
|
3387
|
+
terraformExtractor,
|
|
3388
|
+
dockerfileExtractor,
|
|
3389
|
+
githubActionsExtractor,
|
|
3390
|
+
gitlabCiExtractor,
|
|
3391
|
+
helmExtractor,
|
|
3392
|
+
composeExtractor,
|
|
3393
|
+
kubernetesExtractor
|
|
3394
|
+
];
|
|
3395
|
+
var HEAD_BYTES = 4096;
|
|
3396
|
+
var EMPTY_RESULT = {
|
|
3397
|
+
nodes: [],
|
|
3398
|
+
edges: [],
|
|
3399
|
+
warnings: [],
|
|
3400
|
+
counts: {},
|
|
3401
|
+
linkCounts: {}
|
|
3402
|
+
};
|
|
3403
|
+
function extractorFor(doc, head) {
|
|
3404
|
+
for (const extractor of EXTRACTORS) {
|
|
3405
|
+
try {
|
|
3406
|
+
if (extractor.matches(toPosix3(doc.rel), doc.category ?? doc.kind, head)) return extractor;
|
|
3407
|
+
} catch {
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
return null;
|
|
3411
|
+
}
|
|
3412
|
+
async function extractToolchain(docs, options = {}) {
|
|
3413
|
+
if (!docs.length) return EMPTY_RESULT;
|
|
3414
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
3415
|
+
const edges = /* @__PURE__ */ new Map();
|
|
3416
|
+
const warnings = [];
|
|
3417
|
+
const counts = {};
|
|
3418
|
+
const stageCopySources = /* @__PURE__ */ new Map();
|
|
3419
|
+
const ordered = [...docs].sort((a, b) => toPosix3(a.rel).localeCompare(toPosix3(b.rel), "en"));
|
|
3420
|
+
for (const doc of ordered) {
|
|
3421
|
+
let source;
|
|
3422
|
+
let head;
|
|
3423
|
+
try {
|
|
3424
|
+
const stat = fs13.statSync(doc.abs);
|
|
3425
|
+
if (stat.size > TOOLCHAIN_FILE_MAX_BYTES) continue;
|
|
3426
|
+
source = fs13.readFileSync(doc.abs, "utf8");
|
|
3427
|
+
head = source.slice(0, HEAD_BYTES);
|
|
3428
|
+
} catch {
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
const extractor = extractorFor(doc, head);
|
|
3432
|
+
if (!extractor) continue;
|
|
3433
|
+
const rel2 = toPosix3(doc.rel);
|
|
3434
|
+
let extraction;
|
|
3435
|
+
try {
|
|
3436
|
+
extraction = await extractor.extract(rel2, source);
|
|
3437
|
+
} catch (err) {
|
|
3438
|
+
warnings.push(`${rel2}: ${extractor.format} extraction failed (${err.message})`);
|
|
3439
|
+
continue;
|
|
3440
|
+
}
|
|
3441
|
+
if (extraction.warnings?.length) warnings.push(...extraction.warnings);
|
|
3442
|
+
for (const [stage, paths] of Object.entries(extraction.linkHints?.copySourcesByStage ?? {})) {
|
|
3443
|
+
if (paths.length) stageCopySources.set(stage, paths);
|
|
3444
|
+
}
|
|
3445
|
+
if (!extraction.nodes.length) continue;
|
|
3446
|
+
counts[extractor.format] = (counts[extractor.format] ?? 0) + 1;
|
|
3447
|
+
const idByQualifiedName = /* @__PURE__ */ new Map();
|
|
3448
|
+
for (const draft of extraction.nodes) {
|
|
3449
|
+
const id = nodeId({
|
|
3450
|
+
kind: draft.kind,
|
|
3451
|
+
qualifiedName: draft.qualifiedName,
|
|
3452
|
+
file: rel2,
|
|
3453
|
+
signature: draft.signature
|
|
3454
|
+
});
|
|
3455
|
+
if (!idByQualifiedName.has(draft.qualifiedName)) {
|
|
3456
|
+
idByQualifiedName.set(draft.qualifiedName, id);
|
|
3457
|
+
}
|
|
3458
|
+
if (nodes.has(id)) continue;
|
|
3459
|
+
nodes.set(id, toGraphNode(id, draft, rel2, extractor.format));
|
|
3460
|
+
}
|
|
3461
|
+
for (const draft of extraction.edges) {
|
|
3462
|
+
const src = idByQualifiedName.get(draft.from);
|
|
3463
|
+
const dst = idByQualifiedName.get(draft.to);
|
|
3464
|
+
if (!src || !dst || src === dst) continue;
|
|
3465
|
+
const id = edgeId(draft.kind, src, dst);
|
|
3466
|
+
if (edges.has(id)) continue;
|
|
3467
|
+
edges.set(id, {
|
|
3468
|
+
id,
|
|
3469
|
+
kind: draft.kind,
|
|
3470
|
+
src,
|
|
3471
|
+
dst,
|
|
3472
|
+
// Structural extraction from a declaration in the file itself.
|
|
3473
|
+
resolution: "heuristic",
|
|
3474
|
+
confidence: draft.confidence ?? 1,
|
|
3475
|
+
epistemic: "declared"
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
const sortedNodes = [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id, "en"));
|
|
3480
|
+
const sortedEdges = [...edges.values()].sort(byEdgeOrder);
|
|
3481
|
+
const linked = linkToolchain({
|
|
3482
|
+
nodes: sortedNodes,
|
|
3483
|
+
edges: sortedEdges,
|
|
3484
|
+
fileNodes: options.fileNodes ?? /* @__PURE__ */ new Map(),
|
|
3485
|
+
stageCopySources
|
|
3486
|
+
});
|
|
3487
|
+
const declaredIds = new Set(sortedEdges.map((e) => e.id));
|
|
3488
|
+
const allEdges = [...sortedEdges, ...linked.edges.filter((e) => !declaredIds.has(e.id))].sort(
|
|
3489
|
+
byEdgeOrder
|
|
3490
|
+
);
|
|
3491
|
+
return {
|
|
3492
|
+
nodes: sortedNodes,
|
|
3493
|
+
edges: allEdges,
|
|
3494
|
+
warnings: [...new Set(warnings)].sort(),
|
|
3495
|
+
counts,
|
|
3496
|
+
linkCounts: linked.counts
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
function byEdgeOrder(a, b) {
|
|
3500
|
+
return a.kind.localeCompare(b.kind, "en") || a.src.localeCompare(b.src, "en") || a.dst.localeCompare(b.dst, "en");
|
|
3501
|
+
}
|
|
3502
|
+
function toGraphNode(id, draft, rel2, format) {
|
|
3503
|
+
return {
|
|
3504
|
+
id,
|
|
3505
|
+
kind: draft.kind,
|
|
3506
|
+
name: draft.name,
|
|
3507
|
+
qualifiedName: draft.qualifiedName,
|
|
3508
|
+
file: rel2,
|
|
3509
|
+
span: draft.span,
|
|
3510
|
+
// `lang` is the format tag, so `--only` and the report can filter on it
|
|
3511
|
+
// without colliding with a source language id.
|
|
3512
|
+
lang: format,
|
|
3513
|
+
signature: draft.signature,
|
|
3514
|
+
doc: safeDoc(draft.doc),
|
|
3515
|
+
importance: draft.importance ?? 0.3,
|
|
3516
|
+
centrality: { degree: 0, pagerank: 0, betweenness: 0, eigenvector: 0 },
|
|
3517
|
+
area: -1,
|
|
3518
|
+
isHub: false,
|
|
3519
|
+
// Infrastructure has no notion of unit-test coverage; `null` is the
|
|
3520
|
+
// schema's "not analyzable", which is the honest answer.
|
|
3521
|
+
tested: null
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
1829
3524
|
function shardTsProjects(root, files) {
|
|
1830
3525
|
if (files.length === 0) return [];
|
|
1831
3526
|
const cache = /* @__PURE__ */ new Map();
|
|
1832
3527
|
const byKey = /* @__PURE__ */ new Map();
|
|
1833
3528
|
for (const f of files) {
|
|
1834
|
-
const dir =
|
|
3529
|
+
const dir = path12.dirname(f.abs);
|
|
1835
3530
|
const key = nearestProjectKey(root, dir, cache);
|
|
1836
3531
|
const list = byKey.get(key);
|
|
1837
3532
|
if (list) list.push(f);
|
|
@@ -1846,22 +3541,22 @@ function nearestProjectKey(root, startDir, cache) {
|
|
|
1846
3541
|
const hit = cache.get(startDir);
|
|
1847
3542
|
if (hit !== void 0) return hit;
|
|
1848
3543
|
let dir = startDir;
|
|
1849
|
-
const rootAbs =
|
|
3544
|
+
const rootAbs = path12.resolve(root);
|
|
1850
3545
|
while (true) {
|
|
1851
|
-
if (
|
|
1852
|
-
const key =
|
|
3546
|
+
if (fs13.existsSync(path12.join(dir, "tsconfig.json")) || fs13.existsSync(path12.join(dir, "tsconfig.base.json")) || fs13.existsSync(path12.join(dir, "package.json"))) {
|
|
3547
|
+
const key = toPosix4(path12.relative(rootAbs, dir)) || ".";
|
|
1853
3548
|
cache.set(startDir, key);
|
|
1854
3549
|
return key;
|
|
1855
3550
|
}
|
|
1856
|
-
if (dir === rootAbs ||
|
|
3551
|
+
if (dir === rootAbs || path12.dirname(dir) === dir) {
|
|
1857
3552
|
cache.set(startDir, ".");
|
|
1858
3553
|
return ".";
|
|
1859
3554
|
}
|
|
1860
|
-
dir =
|
|
3555
|
+
dir = path12.dirname(dir);
|
|
1861
3556
|
}
|
|
1862
3557
|
}
|
|
1863
|
-
function
|
|
1864
|
-
return p.split(
|
|
3558
|
+
function toPosix4(p) {
|
|
3559
|
+
return p.split(path12.sep).join("/");
|
|
1865
3560
|
}
|
|
1866
3561
|
|
|
1867
3562
|
// src/engine/timing.ts
|
|
@@ -1953,9 +3648,9 @@ var emptyNode = (partial) => ({
|
|
|
1953
3648
|
tested: null
|
|
1954
3649
|
});
|
|
1955
3650
|
function extractManifests(root, opts = {}) {
|
|
1956
|
-
const absRoot =
|
|
3651
|
+
const absRoot = path12.resolve(root);
|
|
1957
3652
|
const ig = buildRootIgnore3(absRoot, opts.exclude ?? []);
|
|
1958
|
-
const scopes = (opts.paths?.length ? opts.paths : ["."]).map((p) =>
|
|
3653
|
+
const scopes = (opts.paths?.length ? opts.paths : ["."]).map((p) => path12.resolve(absRoot, p)).filter((p) => fs13.existsSync(p));
|
|
1959
3654
|
const found = /* @__PURE__ */ new Map();
|
|
1960
3655
|
for (const scope of scopes) {
|
|
1961
3656
|
walkManifests(absRoot, scope, ig, found);
|
|
@@ -1965,7 +3660,7 @@ function extractManifests(root, opts = {}) {
|
|
|
1965
3660
|
let deps = 0;
|
|
1966
3661
|
for (const rel2 of [...found.keys()].sort()) {
|
|
1967
3662
|
const abs = found.get(rel2);
|
|
1968
|
-
const base =
|
|
3663
|
+
const base = path12.posix.basename(rel2);
|
|
1969
3664
|
try {
|
|
1970
3665
|
if (base === "package.json") {
|
|
1971
3666
|
deps += ingestPackageJson(rel2, abs, nodes, edges);
|
|
@@ -1985,8 +3680,8 @@ function extractManifests(root, opts = {}) {
|
|
|
1985
3680
|
};
|
|
1986
3681
|
}
|
|
1987
3682
|
function ingestPackageJson(rel2, abs, nodes, edges) {
|
|
1988
|
-
const raw = JSON.parse(
|
|
1989
|
-
const pkgName = raw.name && String(raw.name).trim() ||
|
|
3683
|
+
const raw = JSON.parse(fs13.readFileSync(abs, "utf8"));
|
|
3684
|
+
const pkgName = raw.name && String(raw.name).trim() || path12.posix.dirname(rel2) || ".";
|
|
1990
3685
|
const localId = nodeId({ kind: "package", qualifiedName: pkgName, file: rel2 });
|
|
1991
3686
|
nodes.set(
|
|
1992
3687
|
localId,
|
|
@@ -2005,7 +3700,7 @@ function ingestPackageJson(rel2, abs, nodes, edges) {
|
|
|
2005
3700
|
emptyNode({
|
|
2006
3701
|
id: fileId,
|
|
2007
3702
|
kind: "file",
|
|
2008
|
-
name:
|
|
3703
|
+
name: path12.posix.basename(rel2),
|
|
2009
3704
|
qualifiedName: rel2,
|
|
2010
3705
|
file: rel2,
|
|
2011
3706
|
lang: "json"
|
|
@@ -2029,8 +3724,8 @@ function ingestPackageJson(rel2, abs, nodes, edges) {
|
|
|
2029
3724
|
return n;
|
|
2030
3725
|
}
|
|
2031
3726
|
function ingestGoMod(rel2, abs, nodes, edges) {
|
|
2032
|
-
const text =
|
|
2033
|
-
let moduleName =
|
|
3727
|
+
const text = fs13.readFileSync(abs, "utf8");
|
|
3728
|
+
let moduleName = path12.posix.dirname(rel2) || ".";
|
|
2034
3729
|
const moduleMatch = /^\s*module\s+(\S+)/m.exec(text);
|
|
2035
3730
|
if (moduleMatch) moduleName = moduleMatch[1];
|
|
2036
3731
|
const localId = nodeId({ kind: "package", qualifiedName: moduleName, file: rel2 });
|
|
@@ -2051,7 +3746,7 @@ function ingestGoMod(rel2, abs, nodes, edges) {
|
|
|
2051
3746
|
emptyNode({
|
|
2052
3747
|
id: fileId,
|
|
2053
3748
|
kind: "file",
|
|
2054
|
-
name:
|
|
3749
|
+
name: path12.posix.basename(rel2),
|
|
2055
3750
|
qualifiedName: rel2,
|
|
2056
3751
|
file: rel2,
|
|
2057
3752
|
lang: "go"
|
|
@@ -2112,10 +3807,10 @@ function addEdge(edges, kind, src, dst, confidence) {
|
|
|
2112
3807
|
}
|
|
2113
3808
|
function buildRootIgnore3(root, exclude) {
|
|
2114
3809
|
const ig = ignore();
|
|
2115
|
-
const gitignorePath =
|
|
2116
|
-
if (
|
|
3810
|
+
const gitignorePath = path12.join(root, ".gitignore");
|
|
3811
|
+
if (fs13.existsSync(gitignorePath)) {
|
|
2117
3812
|
try {
|
|
2118
|
-
ig.add(
|
|
3813
|
+
ig.add(fs13.readFileSync(gitignorePath, "utf8"));
|
|
2119
3814
|
} catch {
|
|
2120
3815
|
}
|
|
2121
3816
|
}
|
|
@@ -2125,13 +3820,13 @@ function buildRootIgnore3(root, exclude) {
|
|
|
2125
3820
|
function walkManifests(root, dir, ig, found) {
|
|
2126
3821
|
let entries;
|
|
2127
3822
|
try {
|
|
2128
|
-
entries =
|
|
3823
|
+
entries = fs13.readdirSync(dir, { withFileTypes: true });
|
|
2129
3824
|
} catch {
|
|
2130
3825
|
return;
|
|
2131
3826
|
}
|
|
2132
3827
|
for (const entry of entries) {
|
|
2133
|
-
const abs =
|
|
2134
|
-
const rel2 =
|
|
3828
|
+
const abs = path12.join(dir, entry.name);
|
|
3829
|
+
const rel2 = path12.relative(root, abs).split(path12.sep).join("/");
|
|
2135
3830
|
if (entry.isDirectory()) {
|
|
2136
3831
|
if (isSkippedDirName(entry.name)) continue;
|
|
2137
3832
|
if (rel2 && ig.ignores(`${rel2}/`)) continue;
|
|
@@ -2158,7 +3853,7 @@ async function hashFilesParallel(jobs, concurrency) {
|
|
|
2158
3853
|
if (i >= jobs.length) return;
|
|
2159
3854
|
const job = jobs[i];
|
|
2160
3855
|
try {
|
|
2161
|
-
const buf = await
|
|
3856
|
+
const buf = await fs10.readFile(job.abs);
|
|
2162
3857
|
out[i] = { rel: job.rel, hash: hashBytes(buf), ok: true };
|
|
2163
3858
|
} catch {
|
|
2164
3859
|
out[i] = { rel: job.rel, hash: "", ok: false };
|
|
@@ -2173,7 +3868,7 @@ async function hashFilesParallel(jobs, concurrency) {
|
|
|
2173
3868
|
async function buildGraph(options) {
|
|
2174
3869
|
const timer = new StageTimer();
|
|
2175
3870
|
timer.start("total");
|
|
2176
|
-
const root =
|
|
3871
|
+
const root = path12.resolve(options.root);
|
|
2177
3872
|
timer.start("discover");
|
|
2178
3873
|
const files = discover({
|
|
2179
3874
|
root,
|
|
@@ -2189,7 +3884,7 @@ async function buildGraph(options) {
|
|
|
2189
3884
|
);
|
|
2190
3885
|
}
|
|
2191
3886
|
checkMemoryBudget("discovery", limits.memoryBudgetMb);
|
|
2192
|
-
const grammars = grammarSetVersion()
|
|
3887
|
+
const grammars = `${grammarSetVersion()}+${toolchainGrammarSetVersion()}`;
|
|
2193
3888
|
const cache = loadCache(root, {
|
|
2194
3889
|
toolVersion: VERSION,
|
|
2195
3890
|
grammars,
|
|
@@ -2206,7 +3901,7 @@ async function buildGraph(options) {
|
|
|
2206
3901
|
for (const file of files) {
|
|
2207
3902
|
let stat;
|
|
2208
3903
|
try {
|
|
2209
|
-
stat =
|
|
3904
|
+
stat = fs13.statSync(file.abs);
|
|
2210
3905
|
} catch {
|
|
2211
3906
|
continue;
|
|
2212
3907
|
}
|
|
@@ -2412,8 +4107,8 @@ async function buildGraph(options) {
|
|
|
2412
4107
|
});
|
|
2413
4108
|
for (const d of docs) {
|
|
2414
4109
|
try {
|
|
2415
|
-
const st =
|
|
2416
|
-
const hash = hashBytes(
|
|
4110
|
+
const st = fs13.statSync(d.abs);
|
|
4111
|
+
const hash = hashBytes(fs13.readFileSync(d.abs));
|
|
2417
4112
|
hashes.set(d.rel, hash);
|
|
2418
4113
|
fileStats.push({ rel: d.rel, size: st.size, mtimeMs: st.mtimeMs, hash });
|
|
2419
4114
|
} catch {
|
|
@@ -2421,14 +4116,29 @@ async function buildGraph(options) {
|
|
|
2421
4116
|
}
|
|
2422
4117
|
const docNodes = documentNodesFromDocs(docs);
|
|
2423
4118
|
if (docNodes.length) nodes = [...nodes, ...docNodes];
|
|
4119
|
+
timer.start("toolchain");
|
|
4120
|
+
const fileNodesByPath = /* @__PURE__ */ new Map();
|
|
4121
|
+
for (const node of nodes) {
|
|
4122
|
+
if (node.kind === "file") fileNodesByPath.set(node.file.replace(/\\/g, "/"), node.id);
|
|
4123
|
+
}
|
|
4124
|
+
const toolchainResult = await extractToolchain(docs, { fileNodes: fileNodesByPath });
|
|
4125
|
+
if (toolchainResult.nodes.length) nodes = [...nodes, ...toolchainResult.nodes];
|
|
4126
|
+
timer.end("toolchain");
|
|
2424
4127
|
timer.start("analyze");
|
|
2425
|
-
const
|
|
4128
|
+
const graphEdges = toolchainResult.edges.length ? [...linked.edges, ...toolchainResult.edges] : linked.edges;
|
|
4129
|
+
const analysis = analyze(nodes, graphEdges, {
|
|
2426
4130
|
cluster: options.cluster,
|
|
2427
4131
|
tier: options.analysisTier
|
|
2428
4132
|
});
|
|
2429
4133
|
timer.end("analyze");
|
|
2430
4134
|
checkMemoryBudget("analysis", limits.memoryBudgetMb);
|
|
2431
|
-
const languages = [
|
|
4135
|
+
const languages = [
|
|
4136
|
+
.../* @__PURE__ */ new Set([
|
|
4137
|
+
...parses.map((p) => p.lang),
|
|
4138
|
+
...docNodes.map((n) => n.lang),
|
|
4139
|
+
...toolchainResult.nodes.map((n) => n.lang)
|
|
4140
|
+
])
|
|
4141
|
+
].sort();
|
|
2432
4142
|
const edgeKinds = [...new Set(analysis.edges.map((e) => e.kind))].sort();
|
|
2433
4143
|
const corpusHash = computeCorpusHash(parses, hashes);
|
|
2434
4144
|
const nodeKindById = new Map(analysis.nodes.map((n) => [n.id, n.kind]));
|
|
@@ -2454,7 +4164,7 @@ async function buildGraph(options) {
|
|
|
2454
4164
|
toolchain
|
|
2455
4165
|
},
|
|
2456
4166
|
meta: {
|
|
2457
|
-
root:
|
|
4167
|
+
root: path12.basename(root) === "" ? "." : ".",
|
|
2458
4168
|
languages,
|
|
2459
4169
|
analysisTier: analysis.tier,
|
|
2460
4170
|
counts: {
|
|
@@ -2513,14 +4223,14 @@ function toRepoRel(p) {
|
|
|
2513
4223
|
function loadScipIndex(root, explicit) {
|
|
2514
4224
|
const candidates = [
|
|
2515
4225
|
explicit,
|
|
2516
|
-
|
|
2517
|
-
|
|
4226
|
+
path12.join(root, "index.scip"),
|
|
4227
|
+
path12.join(root, ".vibgrate", "index.scip")
|
|
2518
4228
|
].filter((p) => Boolean(p));
|
|
2519
4229
|
for (const file of candidates) {
|
|
2520
|
-
const abs =
|
|
2521
|
-
if (!
|
|
4230
|
+
const abs = path12.isAbsolute(file) ? file : path12.resolve(root, file);
|
|
4231
|
+
if (!fs13.existsSync(abs)) continue;
|
|
2522
4232
|
try {
|
|
2523
|
-
const index = decodeScipIndex(new Uint8Array(
|
|
4233
|
+
const index = decodeScipIndex(new Uint8Array(fs13.readFileSync(abs)));
|
|
2524
4234
|
if (index.documents.length) {
|
|
2525
4235
|
return { index, tool: index.toolVersion ? `${index.toolName} ${index.toolVersion}` : index.toolName };
|
|
2526
4236
|
}
|
|
@@ -2612,53 +4322,23 @@ function diffHint(x, y) {
|
|
|
2612
4322
|
}
|
|
2613
4323
|
return "outputs differ in length";
|
|
2614
4324
|
}
|
|
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
4325
|
var SNAPSHOT_VERSION = "vg-freshness/1";
|
|
2646
4326
|
function snapshotPath(root) {
|
|
2647
|
-
return
|
|
4327
|
+
return path12.join(cacheDir(root), "freshness.json");
|
|
2648
4328
|
}
|
|
2649
4329
|
function writeSnapshot(root, corpusHash, fileStats, scope = {}) {
|
|
2650
4330
|
const files = {};
|
|
2651
4331
|
for (const f of fileStats) files[f.rel] = { size: f.size, mtimeMs: f.mtimeMs, hash: f.hash };
|
|
2652
4332
|
const snapshot = { version: SNAPSHOT_VERSION, corpusHash, scope: pruneScope(scope), files };
|
|
2653
4333
|
try {
|
|
2654
|
-
|
|
2655
|
-
|
|
4334
|
+
fs13.mkdirSync(cacheDir(root), { recursive: true });
|
|
4335
|
+
fs13.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
|
|
2656
4336
|
} catch {
|
|
2657
4337
|
}
|
|
2658
4338
|
}
|
|
2659
4339
|
function loadSnapshot(root) {
|
|
2660
4340
|
try {
|
|
2661
|
-
const loaded = JSON.parse(
|
|
4341
|
+
const loaded = JSON.parse(fs13.readFileSync(snapshotPath(root), "utf8"));
|
|
2662
4342
|
if (loaded.version === SNAPSHOT_VERSION && loaded.files && typeof loaded.corpusHash === "string") {
|
|
2663
4343
|
return { ...loaded, scope: loaded.scope ?? {} };
|
|
2664
4344
|
}
|
|
@@ -2701,7 +4381,7 @@ function probeFreshness(root) {
|
|
|
2701
4381
|
}
|
|
2702
4382
|
let stat;
|
|
2703
4383
|
try {
|
|
2704
|
-
stat =
|
|
4384
|
+
stat = fs13.statSync(file.abs);
|
|
2705
4385
|
} catch {
|
|
2706
4386
|
drift.removed.push(file.rel);
|
|
2707
4387
|
continue;
|
|
@@ -2719,7 +4399,7 @@ function probeFreshness(root) {
|
|
|
2719
4399
|
}
|
|
2720
4400
|
let hash = "";
|
|
2721
4401
|
try {
|
|
2722
|
-
hash = hashBytes(
|
|
4402
|
+
hash = hashBytes(fs13.readFileSync(file.abs));
|
|
2723
4403
|
} catch {
|
|
2724
4404
|
drift.removed.push(file.rel);
|
|
2725
4405
|
continue;
|
|
@@ -2736,7 +4416,7 @@ function probeFreshness(root) {
|
|
|
2736
4416
|
}
|
|
2737
4417
|
if (absorbed && !hasDrift(drift)) {
|
|
2738
4418
|
try {
|
|
2739
|
-
|
|
4419
|
+
fs13.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
|
|
2740
4420
|
} catch {
|
|
2741
4421
|
}
|
|
2742
4422
|
}
|
|
@@ -2981,12 +4661,12 @@ function detectAssistants(root, opts = {}) {
|
|
|
2981
4661
|
const which = opts.which ?? whichOnPath;
|
|
2982
4662
|
const found = [];
|
|
2983
4663
|
for (const assistant of ASSISTANTS) {
|
|
2984
|
-
const repoHit = (assistant.markers ?? []).find((m) =>
|
|
4664
|
+
const repoHit = (assistant.markers ?? []).find((m) => fs13.existsSync(path12.join(root, m)));
|
|
2985
4665
|
if (repoHit) {
|
|
2986
4666
|
found.push({ assistant, via: "repo", marker: repoHit });
|
|
2987
4667
|
continue;
|
|
2988
4668
|
}
|
|
2989
|
-
const homeHit = (assistant.homeMarkers ?? []).find((m) =>
|
|
4669
|
+
const homeHit = (assistant.homeMarkers ?? []).find((m) => fs13.existsSync(path12.join(home, m)));
|
|
2990
4670
|
if (homeHit) {
|
|
2991
4671
|
found.push({ assistant, via: "home", marker: homeHit });
|
|
2992
4672
|
continue;
|
|
@@ -3018,19 +4698,19 @@ function installAssistant(a, opts) {
|
|
|
3018
4698
|
const skipped = [];
|
|
3019
4699
|
let note;
|
|
3020
4700
|
if (a.skill) {
|
|
3021
|
-
writeFileEnsured(
|
|
4701
|
+
writeFileEnsured(path12.join(opts.root, a.skill), skillMarkdown(a.id));
|
|
3022
4702
|
wrote.push(a.skill);
|
|
3023
4703
|
}
|
|
3024
4704
|
if (a.mcp) {
|
|
3025
4705
|
const launch = opts.launch ?? detectServeLaunch();
|
|
3026
|
-
upsertMcp(
|
|
4706
|
+
upsertMcp(path12.join(opts.root, a.mcp.file), a.mcp, launch);
|
|
3027
4707
|
wrote.push(a.mcp.file);
|
|
3028
4708
|
note = launch.note;
|
|
3029
4709
|
} else {
|
|
3030
4710
|
skipped.push("mcp (host-specific setup)");
|
|
3031
4711
|
}
|
|
3032
4712
|
if (a.nudge && opts.hook !== false) {
|
|
3033
|
-
writeNudge(
|
|
4713
|
+
writeNudge(path12.join(opts.root, a.nudge.file), a.nudge, opts.smallRepo, a.id);
|
|
3034
4714
|
wrote.push(a.nudge.file);
|
|
3035
4715
|
} else if (a.nudge) {
|
|
3036
4716
|
skipped.push("nudge (--no-hook)");
|
|
@@ -3040,34 +4720,34 @@ function installAssistant(a, opts) {
|
|
|
3040
4720
|
function uninstallAssistant(a, root, purge) {
|
|
3041
4721
|
const removed = [];
|
|
3042
4722
|
if (a.mcp) {
|
|
3043
|
-
const file =
|
|
4723
|
+
const file = path12.join(root, a.mcp.file);
|
|
3044
4724
|
if (removeMcp(file, a.mcp)) removed.push(a.mcp.file);
|
|
3045
4725
|
}
|
|
3046
4726
|
if (a.nudge) {
|
|
3047
|
-
const file =
|
|
4727
|
+
const file = path12.join(root, a.nudge.file);
|
|
3048
4728
|
if (a.nudge.kind === "block") {
|
|
3049
4729
|
if (removeBlock(file)) removed.push(a.nudge.file);
|
|
3050
|
-
} else if (
|
|
3051
|
-
|
|
4730
|
+
} else if (fs13.existsSync(file)) {
|
|
4731
|
+
fs13.rmSync(file);
|
|
3052
4732
|
removed.push(a.nudge.file);
|
|
3053
4733
|
}
|
|
3054
4734
|
}
|
|
3055
4735
|
if (purge && a.skill) {
|
|
3056
|
-
const file =
|
|
3057
|
-
if (
|
|
3058
|
-
|
|
4736
|
+
const file = path12.join(root, a.skill);
|
|
4737
|
+
if (fs13.existsSync(file)) {
|
|
4738
|
+
fs13.rmSync(file);
|
|
3059
4739
|
removed.push(a.skill);
|
|
3060
4740
|
}
|
|
3061
4741
|
}
|
|
3062
4742
|
return removed;
|
|
3063
4743
|
}
|
|
3064
4744
|
function isAssistantInstalled(a, root) {
|
|
3065
|
-
if (a.skill &&
|
|
4745
|
+
if (a.skill && fs13.existsSync(path12.join(root, a.skill))) return true;
|
|
3066
4746
|
if (a.mcp) {
|
|
3067
|
-
const file =
|
|
4747
|
+
const file = path12.join(root, a.mcp.file);
|
|
3068
4748
|
if (a.mcp.format === "toml") {
|
|
3069
4749
|
if (hasTomlMcp(file, a.mcp)) return true;
|
|
3070
|
-
} else if (
|
|
4750
|
+
} else if (fs13.existsSync(file)) {
|
|
3071
4751
|
try {
|
|
3072
4752
|
const config = readJson(file);
|
|
3073
4753
|
const bag = config[a.mcp.key];
|
|
@@ -3077,8 +4757,8 @@ function isAssistantInstalled(a, root) {
|
|
|
3077
4757
|
}
|
|
3078
4758
|
}
|
|
3079
4759
|
if (a.nudge) {
|
|
3080
|
-
const file =
|
|
3081
|
-
if (!
|
|
4760
|
+
const file = path12.join(root, a.nudge.file);
|
|
4761
|
+
if (!fs13.existsSync(file)) return false;
|
|
3082
4762
|
const text = readTextSafe(file);
|
|
3083
4763
|
if (!text) return false;
|
|
3084
4764
|
if (a.nudge.kind === "block") return text.includes(NUDGE_BEGIN);
|
|
@@ -3087,19 +4767,19 @@ function isAssistantInstalled(a, root) {
|
|
|
3087
4767
|
return false;
|
|
3088
4768
|
}
|
|
3089
4769
|
function writeFileEnsured(file, content) {
|
|
3090
|
-
|
|
3091
|
-
|
|
4770
|
+
fs13.mkdirSync(path12.dirname(file), { recursive: true });
|
|
4771
|
+
fs13.writeFileSync(file, content);
|
|
3092
4772
|
}
|
|
3093
4773
|
function writeNavigationConfig(root) {
|
|
3094
4774
|
ensureVibgrateGitignore(root);
|
|
3095
|
-
const rel2 =
|
|
4775
|
+
const rel2 = path12.join(".vibgrate", "mcp-navigation.json");
|
|
3096
4776
|
const doc = {
|
|
3097
4777
|
_readme: "Deferred-loading config for agents embedding `vg serve` via the Claude API (defer_loading + tool-search). The hot navigation core stays in context; the rest load on demand \u2014 ~350-450 schema tokens/step vs ~1,881 for the full set. Hosts without defer_loading ignore this and serve the whole (already optimized) tool set. See docs/graph/VG-NAVIGATION-PROFILE.md.",
|
|
3098
4778
|
hot_core: [...HOT_TOOLS],
|
|
3099
4779
|
deferred: deferredToolNames(),
|
|
3100
4780
|
toolset: navigationToolsetConfig()
|
|
3101
4781
|
};
|
|
3102
|
-
writeFileEnsured(
|
|
4782
|
+
writeFileEnsured(path12.join(root, rel2), `${JSON.stringify(doc, null, 2)}
|
|
3103
4783
|
`);
|
|
3104
4784
|
return rel2;
|
|
3105
4785
|
}
|
|
@@ -3129,7 +4809,7 @@ function upsertTomlMcp(file, target, launch) {
|
|
|
3129
4809
|
command = ${tomlString(entry.command)}
|
|
3130
4810
|
args = [${entry.args.map(tomlString).join(", ")}]
|
|
3131
4811
|
`;
|
|
3132
|
-
const existing =
|
|
4812
|
+
const existing = fs13.existsSync(file) ? fs13.readFileSync(file, "utf8") : "";
|
|
3133
4813
|
const re = tomlTableRe(target.key);
|
|
3134
4814
|
const next = re.test(existing) ? existing.replace(re, (m) => m.startsWith("\n") ? `
|
|
3135
4815
|
${table}` : table) : existing.length ? `${existing.replace(/\s*$/, "")}
|
|
@@ -3138,7 +4818,7 @@ ${table}` : table;
|
|
|
3138
4818
|
writeFileEnsured(file, next);
|
|
3139
4819
|
}
|
|
3140
4820
|
function removeTomlMcp(file, target) {
|
|
3141
|
-
if (!
|
|
4821
|
+
if (!fs13.existsSync(file)) return false;
|
|
3142
4822
|
const existing = readTextSafe(file);
|
|
3143
4823
|
if (existing === null) return false;
|
|
3144
4824
|
const re = tomlTableRe(target.key);
|
|
@@ -3148,13 +4828,13 @@ function removeTomlMcp(file, target) {
|
|
|
3148
4828
|
return true;
|
|
3149
4829
|
}
|
|
3150
4830
|
function hasTomlMcp(file, target) {
|
|
3151
|
-
if (!
|
|
4831
|
+
if (!fs13.existsSync(file)) return false;
|
|
3152
4832
|
const text = readTextSafe(file);
|
|
3153
4833
|
return text !== null && tomlTableRe(target.key).test(text);
|
|
3154
4834
|
}
|
|
3155
4835
|
function removeMcp(file, target) {
|
|
3156
4836
|
if (target.format === "toml") return removeTomlMcp(file, target);
|
|
3157
|
-
if (!
|
|
4837
|
+
if (!fs13.existsSync(file)) return false;
|
|
3158
4838
|
const config = readJson(file);
|
|
3159
4839
|
const bag = config[target.key];
|
|
3160
4840
|
if (!bag || !(bag.vg !== void 0)) return false;
|
|
@@ -3173,8 +4853,8 @@ function refreshInstalledInstructions(root, smallRepo) {
|
|
|
3173
4853
|
};
|
|
3174
4854
|
for (const a of ASSISTANTS) {
|
|
3175
4855
|
if (a.skill) {
|
|
3176
|
-
const file =
|
|
3177
|
-
if (
|
|
4856
|
+
const file = path12.join(root, a.skill);
|
|
4857
|
+
if (fs13.existsSync(file)) {
|
|
3178
4858
|
const text = readTextSafe(file);
|
|
3179
4859
|
const from = text === null ? null : staleVersion(text, "# vg \u2014 the code map");
|
|
3180
4860
|
if (from !== null) {
|
|
@@ -3184,8 +4864,8 @@ function refreshInstalledInstructions(root, smallRepo) {
|
|
|
3184
4864
|
}
|
|
3185
4865
|
}
|
|
3186
4866
|
if (a.nudge) {
|
|
3187
|
-
const file =
|
|
3188
|
-
if (!
|
|
4867
|
+
const file = path12.join(root, a.nudge.file);
|
|
4868
|
+
if (!fs13.existsSync(file)) continue;
|
|
3189
4869
|
const text = readTextSafe(file);
|
|
3190
4870
|
if (text === null) continue;
|
|
3191
4871
|
if (a.nudge.kind === "block") {
|
|
@@ -3209,7 +4889,7 @@ function refreshInstalledInstructions(root, smallRepo) {
|
|
|
3209
4889
|
}
|
|
3210
4890
|
function readTextSafe(file) {
|
|
3211
4891
|
try {
|
|
3212
|
-
return
|
|
4892
|
+
return fs13.readFileSync(file, "utf8");
|
|
3213
4893
|
} catch {
|
|
3214
4894
|
return null;
|
|
3215
4895
|
}
|
|
@@ -3228,8 +4908,8 @@ ${stripMarkers(nudgeMarkdown(smallRepo, client))}` : stripMarkers(nudgeMarkdown(
|
|
|
3228
4908
|
upsertBlock(file, nudgeMarkdown(smallRepo, client));
|
|
3229
4909
|
}
|
|
3230
4910
|
function upsertBlock(file, block) {
|
|
3231
|
-
|
|
3232
|
-
let existing =
|
|
4911
|
+
fs13.mkdirSync(path12.dirname(file), { recursive: true });
|
|
4912
|
+
let existing = fs13.existsSync(file) ? fs13.readFileSync(file, "utf8") : "";
|
|
3233
4913
|
const re = new RegExp(`${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}`);
|
|
3234
4914
|
if (re.test(existing)) existing = existing.replace(re, block);
|
|
3235
4915
|
else existing = existing.length ? `${existing.replace(/\s*$/, "")}
|
|
@@ -3237,20 +4917,20 @@ function upsertBlock(file, block) {
|
|
|
3237
4917
|
${block}
|
|
3238
4918
|
` : `${block}
|
|
3239
4919
|
`;
|
|
3240
|
-
|
|
4920
|
+
fs13.writeFileSync(file, existing);
|
|
3241
4921
|
}
|
|
3242
4922
|
function removeBlock(file) {
|
|
3243
|
-
if (!
|
|
3244
|
-
const existing =
|
|
4923
|
+
if (!fs13.existsSync(file)) return false;
|
|
4924
|
+
const existing = fs13.readFileSync(file, "utf8");
|
|
3245
4925
|
const re = new RegExp(`\\n*${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}\\n*`);
|
|
3246
4926
|
if (!re.test(existing)) return false;
|
|
3247
|
-
|
|
4927
|
+
fs13.writeFileSync(file, existing.replace(re, "\n"));
|
|
3248
4928
|
return true;
|
|
3249
4929
|
}
|
|
3250
4930
|
function readJson(file) {
|
|
3251
|
-
if (!
|
|
4931
|
+
if (!fs13.existsSync(file)) return {};
|
|
3252
4932
|
try {
|
|
3253
|
-
const parsed = JSON.parse(
|
|
4933
|
+
const parsed = JSON.parse(fs13.readFileSync(file, "utf8"));
|
|
3254
4934
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3255
4935
|
return parsed;
|
|
3256
4936
|
}
|
|
@@ -3269,6 +4949,6 @@ function escapeRe(s) {
|
|
|
3269
4949
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3270
4950
|
}
|
|
3271
4951
|
|
|
3272
|
-
export { ASSISTANTS,
|
|
3273
|
-
//# sourceMappingURL=chunk-
|
|
3274
|
-
//# sourceMappingURL=chunk-
|
|
4952
|
+
export { ASSISTANTS, ResourceLimitError, SKIP_DIRS, SKIP_FILES, SMALL_REPO_FILES, TOOLCHAIN_GRAMMARS, UsageError, analyze, applyCoverage, assistantById, buildFacts, buildGraph, decodeScipIndex, detectAssistants, detectServeLaunch, discover, driftCount, epistemicBreakdown, groundGraph, hasDrift, installAssistant, isAssistantInstalled, isSkippedDirName, loadCoverage, loadSnapshot, probeFreshness, refreshInstalledInstructions, resolveLimits, resolveToolchainGrammar, scipEdges, uninstallAssistant, verifyDeterminism, versionMarker, writeNavigationConfig, writeSnapshot };
|
|
4953
|
+
//# sourceMappingURL=chunk-RJCRWQNS.js.map
|
|
4954
|
+
//# sourceMappingURL=chunk-RJCRWQNS.js.map
|