@vibgrate/cli 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +1 -1
- package/dist/{baseline-RV3GAW72.js → baseline-SI57J3VQ.js} +2 -2
- package/dist/{chunk-4N4BALQQ.js → chunk-N37F5EZQ.js} +960 -118
- package/dist/{chunk-POMRKRQN.js → chunk-T5R4TYW4.js} +1 -1
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +44 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ var Semaphore = class {
|
|
|
23
23
|
this.available--;
|
|
24
24
|
return Promise.resolve();
|
|
25
25
|
}
|
|
26
|
-
return new Promise((
|
|
26
|
+
return new Promise((resolve7) => this.queue.push(resolve7));
|
|
27
27
|
}
|
|
28
28
|
release() {
|
|
29
29
|
const next = this.queue.shift();
|
|
@@ -50,6 +50,140 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
50
50
|
"packages",
|
|
51
51
|
"TestResults"
|
|
52
52
|
]);
|
|
53
|
+
var TEXT_CACHE_MAX_BYTES = 1048576;
|
|
54
|
+
var FileCache = class _FileCache {
|
|
55
|
+
/** Directory walk results keyed by rootDir */
|
|
56
|
+
walkCache = /* @__PURE__ */ new Map();
|
|
57
|
+
/** File content keyed by absolute path (only files ≤ TEXT_CACHE_MAX_BYTES) */
|
|
58
|
+
textCache = /* @__PURE__ */ new Map();
|
|
59
|
+
/** Parsed JSON keyed by absolute path */
|
|
60
|
+
jsonCache = /* @__PURE__ */ new Map();
|
|
61
|
+
/** pathExists keyed by absolute path */
|
|
62
|
+
existsCache = /* @__PURE__ */ new Map();
|
|
63
|
+
// ── Directory walking ──
|
|
64
|
+
/**
|
|
65
|
+
* Walk the directory tree from `rootDir` once, skipping SKIP_DIRS plus
|
|
66
|
+
* common framework output dirs (.nuxt, .output, .svelte-kit).
|
|
67
|
+
*
|
|
68
|
+
* The result is memoised so every scanner filters the same array.
|
|
69
|
+
* Consumers that need additional filtering (e.g. SOURCE_EXTENSIONS,
|
|
70
|
+
* SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
|
|
71
|
+
*/
|
|
72
|
+
walkDir(rootDir) {
|
|
73
|
+
const cached = this.walkCache.get(rootDir);
|
|
74
|
+
if (cached) return cached;
|
|
75
|
+
const promise = this._doWalk(rootDir);
|
|
76
|
+
this.walkCache.set(rootDir, promise);
|
|
77
|
+
return promise;
|
|
78
|
+
}
|
|
79
|
+
/** Additional dirs skipped only by the cached walk (framework outputs) */
|
|
80
|
+
static EXTRA_SKIP = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
|
|
81
|
+
async _doWalk(rootDir) {
|
|
82
|
+
const results = [];
|
|
83
|
+
const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
|
|
84
|
+
const sem = new Semaphore(maxConcurrentReads);
|
|
85
|
+
const extraSkip = _FileCache.EXTRA_SKIP;
|
|
86
|
+
async function walk(dir) {
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const subWalks = [];
|
|
94
|
+
for (const e of entries) {
|
|
95
|
+
const absPath = path.join(dir, e.name);
|
|
96
|
+
const relPath = path.relative(rootDir, absPath);
|
|
97
|
+
if (e.isDirectory()) {
|
|
98
|
+
if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
|
|
99
|
+
results.push({ absPath, relPath, name: e.name, isFile: false, isDirectory: true });
|
|
100
|
+
subWalks.push(sem.run(() => walk(absPath)));
|
|
101
|
+
} else if (e.isFile()) {
|
|
102
|
+
results.push({ absPath, relPath, name: e.name, isFile: true, isDirectory: false });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await Promise.all(subWalks);
|
|
106
|
+
}
|
|
107
|
+
await sem.run(() => walk(rootDir));
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Find files matching a predicate from the cached walk.
|
|
112
|
+
* Returns absolute paths (same contract as the standalone `findFiles`).
|
|
113
|
+
*/
|
|
114
|
+
async findFiles(rootDir, predicate) {
|
|
115
|
+
const entries = await this.walkDir(rootDir);
|
|
116
|
+
return entries.filter((e) => e.isFile && predicate(e.name)).map((e) => e.absPath);
|
|
117
|
+
}
|
|
118
|
+
async findPackageJsonFiles(rootDir) {
|
|
119
|
+
return this.findFiles(rootDir, (name) => name === "package.json");
|
|
120
|
+
}
|
|
121
|
+
async findCsprojFiles(rootDir) {
|
|
122
|
+
return this.findFiles(rootDir, (name) => name.endsWith(".csproj"));
|
|
123
|
+
}
|
|
124
|
+
async findSolutionFiles(rootDir) {
|
|
125
|
+
return this.findFiles(rootDir, (name) => name.endsWith(".sln"));
|
|
126
|
+
}
|
|
127
|
+
// ── File content reading ──
|
|
128
|
+
/**
|
|
129
|
+
* Read a text file. Files ≤ 1 MB are cached so subsequent calls from
|
|
130
|
+
* different scanners return the same string. Files > 1 MB (lockfiles,
|
|
131
|
+
* large generated files) are read directly and never retained.
|
|
132
|
+
*/
|
|
133
|
+
readTextFile(filePath) {
|
|
134
|
+
const abs = path.resolve(filePath);
|
|
135
|
+
const cached = this.textCache.get(abs);
|
|
136
|
+
if (cached) return cached;
|
|
137
|
+
const promise = fs.readFile(abs, "utf8").then((content) => {
|
|
138
|
+
if (content.length > TEXT_CACHE_MAX_BYTES) {
|
|
139
|
+
this.textCache.delete(abs);
|
|
140
|
+
}
|
|
141
|
+
return content;
|
|
142
|
+
});
|
|
143
|
+
this.textCache.set(abs, promise);
|
|
144
|
+
return promise;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read and parse a JSON file. The parsed object is cached; the raw
|
|
148
|
+
* text is evicted immediately so we never hold both representations.
|
|
149
|
+
*/
|
|
150
|
+
readJsonFile(filePath) {
|
|
151
|
+
const abs = path.resolve(filePath);
|
|
152
|
+
const cached = this.jsonCache.get(abs);
|
|
153
|
+
if (cached) return cached;
|
|
154
|
+
const promise = this.readTextFile(abs).then((txt) => {
|
|
155
|
+
this.textCache.delete(abs);
|
|
156
|
+
return JSON.parse(txt);
|
|
157
|
+
});
|
|
158
|
+
this.jsonCache.set(abs, promise);
|
|
159
|
+
return promise;
|
|
160
|
+
}
|
|
161
|
+
// ── Existence checks ──
|
|
162
|
+
pathExists(p) {
|
|
163
|
+
const abs = path.resolve(p);
|
|
164
|
+
const cached = this.existsCache.get(abs);
|
|
165
|
+
if (cached) return cached;
|
|
166
|
+
const promise = fs.access(abs).then(() => true, () => false);
|
|
167
|
+
this.existsCache.set(abs, promise);
|
|
168
|
+
return promise;
|
|
169
|
+
}
|
|
170
|
+
// ── Lifecycle ──
|
|
171
|
+
/** Release all cached data. Call after the scan completes. */
|
|
172
|
+
clear() {
|
|
173
|
+
this.walkCache.clear();
|
|
174
|
+
this.textCache.clear();
|
|
175
|
+
this.jsonCache.clear();
|
|
176
|
+
this.existsCache.clear();
|
|
177
|
+
}
|
|
178
|
+
/** Number of file content entries currently held */
|
|
179
|
+
get textCacheSize() {
|
|
180
|
+
return this.textCache.size;
|
|
181
|
+
}
|
|
182
|
+
/** Number of parsed JSON entries currently held */
|
|
183
|
+
get jsonCacheSize() {
|
|
184
|
+
return this.jsonCache.size;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
53
187
|
async function findFiles(rootDir, predicate) {
|
|
54
188
|
const results = [];
|
|
55
189
|
const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
|
|
@@ -187,12 +321,12 @@ function eolScore(projects) {
|
|
|
187
321
|
}
|
|
188
322
|
function computeDriftScore(projects) {
|
|
189
323
|
const rs = runtimeScore(projects);
|
|
190
|
-
const
|
|
324
|
+
const fs6 = frameworkScore(projects);
|
|
191
325
|
const ds = dependencyScore(projects);
|
|
192
326
|
const es = eolScore(projects);
|
|
193
327
|
const components = [
|
|
194
328
|
{ score: rs, weight: 0.25 },
|
|
195
|
-
{ score:
|
|
329
|
+
{ score: fs6, weight: 0.25 },
|
|
196
330
|
{ score: ds, weight: 0.3 },
|
|
197
331
|
{ score: es, weight: 0.2 }
|
|
198
332
|
];
|
|
@@ -203,7 +337,7 @@ function computeDriftScore(projects) {
|
|
|
203
337
|
riskLevel: "low",
|
|
204
338
|
components: {
|
|
205
339
|
runtimeScore: Math.round(rs ?? 100),
|
|
206
|
-
frameworkScore: Math.round(
|
|
340
|
+
frameworkScore: Math.round(fs6 ?? 100),
|
|
207
341
|
dependencyScore: Math.round(ds ?? 100),
|
|
208
342
|
eolScore: Math.round(es ?? 100)
|
|
209
343
|
}
|
|
@@ -221,7 +355,7 @@ function computeDriftScore(projects) {
|
|
|
221
355
|
else riskLevel = "high";
|
|
222
356
|
const measured = [];
|
|
223
357
|
if (rs !== null) measured.push("runtime");
|
|
224
|
-
if (
|
|
358
|
+
if (fs6 !== null) measured.push("framework");
|
|
225
359
|
if (ds !== null) measured.push("dependency");
|
|
226
360
|
if (es !== null) measured.push("eol");
|
|
227
361
|
return {
|
|
@@ -229,7 +363,7 @@ function computeDriftScore(projects) {
|
|
|
229
363
|
riskLevel,
|
|
230
364
|
components: {
|
|
231
365
|
runtimeScore: Math.round(rs ?? 100),
|
|
232
|
-
frameworkScore: Math.round(
|
|
366
|
+
frameworkScore: Math.round(fs6 ?? 100),
|
|
233
367
|
dependencyScore: Math.round(ds ?? 100),
|
|
234
368
|
eolScore: Math.round(es ?? 100)
|
|
235
369
|
},
|
|
@@ -585,6 +719,122 @@ function formatExtended(ext) {
|
|
|
585
719
|
lines.push("");
|
|
586
720
|
}
|
|
587
721
|
}
|
|
722
|
+
if (ext.architecture) {
|
|
723
|
+
lines.push(...formatArchitectureDiagram(ext.architecture));
|
|
724
|
+
}
|
|
725
|
+
return lines;
|
|
726
|
+
}
|
|
727
|
+
var LAYER_LABELS = {
|
|
728
|
+
"presentation": "Presentation",
|
|
729
|
+
"routing": "Routing",
|
|
730
|
+
"middleware": "Middleware",
|
|
731
|
+
"services": "Services",
|
|
732
|
+
"domain": "Domain",
|
|
733
|
+
"data-access": "Data Access",
|
|
734
|
+
"infrastructure": "Infrastructure",
|
|
735
|
+
"config": "Config",
|
|
736
|
+
"shared": "Shared",
|
|
737
|
+
"testing": "Testing"
|
|
738
|
+
};
|
|
739
|
+
var LAYER_ICONS = {
|
|
740
|
+
"presentation": "\u{1F5A5}",
|
|
741
|
+
"routing": "\u{1F500}",
|
|
742
|
+
"middleware": "\u{1F517}",
|
|
743
|
+
"services": "\u2699",
|
|
744
|
+
"domain": "\u{1F48E}",
|
|
745
|
+
"data-access": "\u{1F5C4}",
|
|
746
|
+
"infrastructure": "\u{1F3D7}",
|
|
747
|
+
"config": "\u2699",
|
|
748
|
+
"shared": "\u{1F4E6}",
|
|
749
|
+
"testing": "\u{1F9EA}"
|
|
750
|
+
};
|
|
751
|
+
function formatArchitectureDiagram(arch) {
|
|
752
|
+
const lines = [];
|
|
753
|
+
lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
754
|
+
lines.push(chalk.bold.cyan("\u2551 Architecture Layers \u2551"));
|
|
755
|
+
lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
756
|
+
lines.push("");
|
|
757
|
+
const archetypeDisplay = arch.archetype === "unknown" ? "Unknown" : arch.archetype;
|
|
758
|
+
const confPct = Math.round(arch.archetypeConfidence * 100);
|
|
759
|
+
lines.push(chalk.bold(" Archetype: ") + chalk.cyan.bold(archetypeDisplay) + chalk.dim(` (${confPct}% confidence)`));
|
|
760
|
+
lines.push(chalk.dim(` ${arch.totalClassified} files classified \xB7 ${arch.unclassified} unclassified`));
|
|
761
|
+
lines.push("");
|
|
762
|
+
const boxWidth = 44;
|
|
763
|
+
const visibleLayers = arch.layers.filter((l) => l.fileCount > 0 || l.techStack.length > 0 || l.services.length > 0);
|
|
764
|
+
if (visibleLayers.length === 0) {
|
|
765
|
+
lines.push(chalk.dim(" No layers detected"));
|
|
766
|
+
lines.push("");
|
|
767
|
+
return lines;
|
|
768
|
+
}
|
|
769
|
+
for (let i = 0; i < visibleLayers.length; i++) {
|
|
770
|
+
const layer = visibleLayers[i];
|
|
771
|
+
const icon = LAYER_ICONS[layer.layer] ?? "\xB7";
|
|
772
|
+
const label = LAYER_LABELS[layer.layer] ?? layer.layer;
|
|
773
|
+
const scoreColor = layer.driftScore >= 70 ? chalk.green : layer.driftScore >= 40 ? chalk.yellow : chalk.red;
|
|
774
|
+
const riskBadgeStr = layer.riskLevel === "low" ? chalk.bgGreen.black(" LOW ") : layer.riskLevel === "moderate" ? chalk.bgYellow.black(" MOD ") : chalk.bgRed.white(" HIGH ");
|
|
775
|
+
if (i === 0) {
|
|
776
|
+
lines.push(chalk.cyan(` \u250C${"\u2500".repeat(boxWidth)}\u2510`));
|
|
777
|
+
}
|
|
778
|
+
const nameStr = `${icon} ${label}`;
|
|
779
|
+
const scoreStr = `${layer.driftScore}/100`;
|
|
780
|
+
const fileSuffix = `${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""}`;
|
|
781
|
+
const leftContent = ` ${nameStr}`;
|
|
782
|
+
const rightContent = `${fileSuffix} ${scoreStr} `;
|
|
783
|
+
const leftLen = nameStr.length + 2;
|
|
784
|
+
const rightLen = rightContent.length;
|
|
785
|
+
const padLen = Math.max(1, boxWidth - leftLen - rightLen);
|
|
786
|
+
lines.push(
|
|
787
|
+
chalk.cyan(" \u2502") + ` ${icon} ${chalk.bold(label)}` + " ".repeat(padLen) + chalk.dim(fileSuffix) + " " + scoreColor.bold(scoreStr) + " " + chalk.cyan("\u2502")
|
|
788
|
+
);
|
|
789
|
+
const barWidth = boxWidth - 8;
|
|
790
|
+
const filled = Math.round(layer.driftScore / 100 * barWidth);
|
|
791
|
+
const empty = barWidth - filled;
|
|
792
|
+
lines.push(
|
|
793
|
+
chalk.cyan(" \u2502") + " " + scoreColor("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + " " + chalk.cyan("\u2502")
|
|
794
|
+
);
|
|
795
|
+
if (layer.techStack.length > 0) {
|
|
796
|
+
const techNames = layer.techStack.slice(0, 6).map((t) => t.name);
|
|
797
|
+
const moreCount = layer.techStack.length > 6 ? ` +${layer.techStack.length - 6}` : "";
|
|
798
|
+
const techLine = `Tech: ${techNames.join(", ")}${moreCount}`;
|
|
799
|
+
const truncated = techLine.length > boxWidth - 6 ? techLine.slice(0, boxWidth - 9) + "..." : techLine;
|
|
800
|
+
const techPad = Math.max(0, boxWidth - truncated.length - 4);
|
|
801
|
+
lines.push(
|
|
802
|
+
chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(techPad) + chalk.cyan("\u2502")
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
if (layer.services.length > 0) {
|
|
806
|
+
const svcNames = layer.services.slice(0, 5).map((s) => s.name);
|
|
807
|
+
const moreCount = layer.services.length > 5 ? ` +${layer.services.length - 5}` : "";
|
|
808
|
+
const svcLine = `Services: ${svcNames.join(", ")}${moreCount}`;
|
|
809
|
+
const truncated = svcLine.length > boxWidth - 6 ? svcLine.slice(0, boxWidth - 9) + "..." : svcLine;
|
|
810
|
+
const svcPad = Math.max(0, boxWidth - truncated.length - 4);
|
|
811
|
+
lines.push(
|
|
812
|
+
chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(svcPad) + chalk.cyan("\u2502")
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const driftPkgs = layer.packages.filter((p) => p.majorsBehind !== null && p.majorsBehind > 0);
|
|
816
|
+
if (driftPkgs.length > 0) {
|
|
817
|
+
const worst = driftPkgs.sort((a, b) => (b.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
|
|
818
|
+
const shown = worst.slice(0, 3);
|
|
819
|
+
const pkgStrs = shown.map((p) => {
|
|
820
|
+
const color = (p.majorsBehind ?? 0) >= 2 ? chalk.red : chalk.yellow;
|
|
821
|
+
return color(`${p.name} -${p.majorsBehind}`);
|
|
822
|
+
});
|
|
823
|
+
const moreCount = worst.length > 3 ? chalk.dim(` +${worst.length - 3}`) : "";
|
|
824
|
+
const pkgLine = pkgStrs.join(chalk.dim(", ")) + moreCount;
|
|
825
|
+
const roughLen = shown.map((p) => `${p.name} -${p.majorsBehind}`).join(", ").length + (worst.length > 3 ? ` +${worst.length - 3}`.length : 0);
|
|
826
|
+
const pkgPad = Math.max(0, boxWidth - roughLen - 4);
|
|
827
|
+
lines.push(
|
|
828
|
+
chalk.cyan(" \u2502") + " " + pkgLine + " ".repeat(pkgPad) + chalk.cyan("\u2502")
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
if (i < visibleLayers.length - 1) {
|
|
832
|
+
lines.push(chalk.cyan(` \u251C${"\u2500".repeat(boxWidth)}\u2524`));
|
|
833
|
+
} else {
|
|
834
|
+
lines.push(chalk.cyan(` \u2514${"\u2500".repeat(boxWidth)}\u2518`));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
lines.push("");
|
|
588
838
|
return lines;
|
|
589
839
|
}
|
|
590
840
|
function generatePriorityActions(artifact) {
|
|
@@ -1050,7 +1300,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1050
1300
|
});
|
|
1051
1301
|
|
|
1052
1302
|
// src/commands/scan.ts
|
|
1053
|
-
import * as
|
|
1303
|
+
import * as path15 from "path";
|
|
1054
1304
|
import { Command as Command3 } from "commander";
|
|
1055
1305
|
import chalk5 from "chalk";
|
|
1056
1306
|
|
|
@@ -1070,7 +1320,7 @@ function maxStable(versions) {
|
|
|
1070
1320
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
1071
1321
|
}
|
|
1072
1322
|
async function npmViewJson(args, cwd) {
|
|
1073
|
-
return new Promise((
|
|
1323
|
+
return new Promise((resolve7, reject) => {
|
|
1074
1324
|
const child = spawn("npm", ["view", ...args, "--json"], {
|
|
1075
1325
|
cwd,
|
|
1076
1326
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1087,13 +1337,13 @@ async function npmViewJson(args, cwd) {
|
|
|
1087
1337
|
}
|
|
1088
1338
|
const trimmed = out.trim();
|
|
1089
1339
|
if (!trimmed) {
|
|
1090
|
-
|
|
1340
|
+
resolve7(null);
|
|
1091
1341
|
return;
|
|
1092
1342
|
}
|
|
1093
1343
|
try {
|
|
1094
|
-
|
|
1344
|
+
resolve7(JSON.parse(trimmed));
|
|
1095
1345
|
} catch {
|
|
1096
|
-
|
|
1346
|
+
resolve7(trimmed.replace(/^"|"$/g, ""));
|
|
1097
1347
|
}
|
|
1098
1348
|
});
|
|
1099
1349
|
});
|
|
@@ -1229,12 +1479,12 @@ var KNOWN_FRAMEWORKS = {
|
|
|
1229
1479
|
"storybook": "Storybook",
|
|
1230
1480
|
"@storybook/react": "Storybook"
|
|
1231
1481
|
};
|
|
1232
|
-
async function scanNodeProjects(rootDir, npmCache) {
|
|
1233
|
-
const packageJsonFiles = await findPackageJsonFiles(rootDir);
|
|
1482
|
+
async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
1483
|
+
const packageJsonFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
1234
1484
|
const results = [];
|
|
1235
1485
|
for (const pjPath of packageJsonFiles) {
|
|
1236
1486
|
try {
|
|
1237
|
-
const scan = await scanOnePackageJson(pjPath, rootDir, npmCache);
|
|
1487
|
+
const scan = await scanOnePackageJson(pjPath, rootDir, npmCache, cache);
|
|
1238
1488
|
results.push(scan);
|
|
1239
1489
|
} catch (e) {
|
|
1240
1490
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -1243,8 +1493,8 @@ async function scanNodeProjects(rootDir, npmCache) {
|
|
|
1243
1493
|
}
|
|
1244
1494
|
return results;
|
|
1245
1495
|
}
|
|
1246
|
-
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
1247
|
-
const pj = await readJsonFile(packageJsonPath);
|
|
1496
|
+
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
1497
|
+
const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
|
|
1248
1498
|
const absProjectPath = path4.dirname(packageJsonPath);
|
|
1249
1499
|
const projectPath = path4.relative(rootDir, absProjectPath) || ".";
|
|
1250
1500
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
@@ -1571,13 +1821,13 @@ function parseCsproj(xml, filePath) {
|
|
|
1571
1821
|
projectName: path5.basename(filePath, ".csproj")
|
|
1572
1822
|
};
|
|
1573
1823
|
}
|
|
1574
|
-
async function scanDotnetProjects(rootDir) {
|
|
1575
|
-
const csprojFiles = await findCsprojFiles(rootDir);
|
|
1576
|
-
const slnFiles = await findSolutionFiles(rootDir);
|
|
1824
|
+
async function scanDotnetProjects(rootDir, cache) {
|
|
1825
|
+
const csprojFiles = cache ? await cache.findCsprojFiles(rootDir) : await findCsprojFiles(rootDir);
|
|
1826
|
+
const slnFiles = cache ? await cache.findSolutionFiles(rootDir) : await findSolutionFiles(rootDir);
|
|
1577
1827
|
const slnCsprojPaths = /* @__PURE__ */ new Set();
|
|
1578
1828
|
for (const slnPath of slnFiles) {
|
|
1579
1829
|
try {
|
|
1580
|
-
const slnContent = await readTextFile(slnPath);
|
|
1830
|
+
const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
|
|
1581
1831
|
const slnDir = path5.dirname(slnPath);
|
|
1582
1832
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1583
1833
|
let match;
|
|
@@ -1594,7 +1844,7 @@ async function scanDotnetProjects(rootDir) {
|
|
|
1594
1844
|
const results = [];
|
|
1595
1845
|
for (const csprojPath of allCsprojFiles) {
|
|
1596
1846
|
try {
|
|
1597
|
-
const scan = await scanOneCsproj(csprojPath, rootDir);
|
|
1847
|
+
const scan = await scanOneCsproj(csprojPath, rootDir, cache);
|
|
1598
1848
|
results.push(scan);
|
|
1599
1849
|
} catch (e) {
|
|
1600
1850
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -1603,8 +1853,8 @@ async function scanDotnetProjects(rootDir) {
|
|
|
1603
1853
|
}
|
|
1604
1854
|
return results;
|
|
1605
1855
|
}
|
|
1606
|
-
async function scanOneCsproj(csprojPath, rootDir) {
|
|
1607
|
-
const xml = await readTextFile(csprojPath);
|
|
1856
|
+
async function scanOneCsproj(csprojPath, rootDir, cache) {
|
|
1857
|
+
const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
|
|
1608
1858
|
const data = parseCsproj(xml, csprojPath);
|
|
1609
1859
|
const primaryTfm = data.targetFrameworks[0];
|
|
1610
1860
|
let runtimeMajorsBehind;
|
|
@@ -2212,7 +2462,7 @@ var OS_PATTERNS = [
|
|
|
2212
2462
|
{ pattern: /\bbash\b|#!\/bin\/bash/i, label: "bash-scripts" },
|
|
2213
2463
|
{ pattern: /\\\\/g, label: "backslash-paths" }
|
|
2214
2464
|
];
|
|
2215
|
-
async function scanPlatformMatrix(rootDir) {
|
|
2465
|
+
async function scanPlatformMatrix(rootDir, cache) {
|
|
2216
2466
|
const result = {
|
|
2217
2467
|
dotnetTargetFrameworks: [],
|
|
2218
2468
|
nativeModules: [],
|
|
@@ -2220,12 +2470,12 @@ async function scanPlatformMatrix(rootDir) {
|
|
|
2220
2470
|
dockerBaseImages: [],
|
|
2221
2471
|
nodeVersionFiles: []
|
|
2222
2472
|
};
|
|
2223
|
-
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
2473
|
+
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
2224
2474
|
const allDeps = /* @__PURE__ */ new Set();
|
|
2225
2475
|
const osAssumptions = /* @__PURE__ */ new Set();
|
|
2226
2476
|
for (const pjPath of pkgFiles) {
|
|
2227
2477
|
try {
|
|
2228
|
-
const pj = await readJsonFile(pjPath);
|
|
2478
|
+
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
2229
2479
|
if (pj.engines?.node && !result.nodeEngines) result.nodeEngines = pj.engines.node;
|
|
2230
2480
|
if (pj.engines?.npm && !result.npmEngines) result.npmEngines = pj.engines.npm;
|
|
2231
2481
|
if (pj.engines?.pnpm && !result.pnpmEngines) {
|
|
@@ -2261,11 +2511,11 @@ async function scanPlatformMatrix(rootDir) {
|
|
|
2261
2511
|
}
|
|
2262
2512
|
result.nativeModules.sort();
|
|
2263
2513
|
result.osAssumptions = [...osAssumptions].sort();
|
|
2264
|
-
const csprojFiles = await findFiles(rootDir, (name) => name.endsWith(".csproj"));
|
|
2514
|
+
const csprojFiles = cache ? await cache.findCsprojFiles(rootDir) : await findFiles(rootDir, (name) => name.endsWith(".csproj"));
|
|
2265
2515
|
const tfms = /* @__PURE__ */ new Set();
|
|
2266
2516
|
for (const csprojPath of csprojFiles) {
|
|
2267
2517
|
try {
|
|
2268
|
-
const xml = await readTextFile(csprojPath);
|
|
2518
|
+
const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
|
|
2269
2519
|
const tfMatch = xml.match(/<TargetFramework>(.*?)<\/TargetFramework>/);
|
|
2270
2520
|
if (tfMatch?.[1]) tfms.add(tfMatch[1]);
|
|
2271
2521
|
const tfsMatch = xml.match(/<TargetFrameworks>(.*?)<\/TargetFrameworks>/);
|
|
@@ -2278,14 +2528,17 @@ async function scanPlatformMatrix(rootDir) {
|
|
|
2278
2528
|
}
|
|
2279
2529
|
}
|
|
2280
2530
|
result.dotnetTargetFrameworks = [...tfms].sort();
|
|
2281
|
-
const dockerfiles = await findFiles(
|
|
2531
|
+
const dockerfiles = cache ? await cache.findFiles(
|
|
2532
|
+
rootDir,
|
|
2533
|
+
(name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
|
|
2534
|
+
) : await findFiles(
|
|
2282
2535
|
rootDir,
|
|
2283
2536
|
(name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
|
|
2284
2537
|
);
|
|
2285
2538
|
const baseImages = /* @__PURE__ */ new Set();
|
|
2286
2539
|
for (const df of dockerfiles) {
|
|
2287
2540
|
try {
|
|
2288
|
-
const content = await readTextFile(df);
|
|
2541
|
+
const content = cache ? await cache.readTextFile(df) : await readTextFile(df);
|
|
2289
2542
|
for (const line of content.split("\n")) {
|
|
2290
2543
|
const trimmed = line.trim();
|
|
2291
2544
|
if (/^FROM\s+/i.test(trimmed)) {
|
|
@@ -2303,7 +2556,8 @@ async function scanPlatformMatrix(rootDir) {
|
|
|
2303
2556
|
}
|
|
2304
2557
|
result.dockerBaseImages = [...baseImages].sort();
|
|
2305
2558
|
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
2306
|
-
|
|
2559
|
+
const exists = cache ? await cache.pathExists(path8.join(rootDir, file)) : await pathExists(path8.join(rootDir, file));
|
|
2560
|
+
if (exists) {
|
|
2307
2561
|
result.nodeVersionFiles.push(file);
|
|
2308
2562
|
}
|
|
2309
2563
|
}
|
|
@@ -2429,7 +2683,7 @@ function parseYarnLock(content) {
|
|
|
2429
2683
|
}
|
|
2430
2684
|
return entries;
|
|
2431
2685
|
}
|
|
2432
|
-
async function scanDependencyGraph(rootDir) {
|
|
2686
|
+
async function scanDependencyGraph(rootDir, cache) {
|
|
2433
2687
|
const result = {
|
|
2434
2688
|
lockfileType: null,
|
|
2435
2689
|
totalUnique: 0,
|
|
@@ -2441,17 +2695,19 @@ async function scanDependencyGraph(rootDir) {
|
|
|
2441
2695
|
const pnpmLock = path9.join(rootDir, "pnpm-lock.yaml");
|
|
2442
2696
|
const npmLock = path9.join(rootDir, "package-lock.json");
|
|
2443
2697
|
const yarnLock = path9.join(rootDir, "yarn.lock");
|
|
2444
|
-
|
|
2698
|
+
const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
|
|
2699
|
+
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
2700
|
+
if (await _pathExists(pnpmLock)) {
|
|
2445
2701
|
result.lockfileType = "pnpm";
|
|
2446
|
-
const content = await
|
|
2702
|
+
const content = await _readTextFile(pnpmLock);
|
|
2447
2703
|
entries = parsePnpmLock(content);
|
|
2448
|
-
} else if (await
|
|
2704
|
+
} else if (await _pathExists(npmLock)) {
|
|
2449
2705
|
result.lockfileType = "npm";
|
|
2450
|
-
const content = await
|
|
2706
|
+
const content = await _readTextFile(npmLock);
|
|
2451
2707
|
entries = parseNpmLock(content);
|
|
2452
|
-
} else if (await
|
|
2708
|
+
} else if (await _pathExists(yarnLock)) {
|
|
2453
2709
|
result.lockfileType = "yarn";
|
|
2454
|
-
const content = await
|
|
2710
|
+
const content = await _readTextFile(yarnLock);
|
|
2455
2711
|
entries = parseYarnLock(content);
|
|
2456
2712
|
}
|
|
2457
2713
|
if (entries.length === 0) return result;
|
|
@@ -2479,12 +2735,12 @@ async function scanDependencyGraph(rootDir) {
|
|
|
2479
2735
|
duplicated.sort((a, b) => b.versions.length - a.versions.length || a.name.localeCompare(b.name));
|
|
2480
2736
|
result.duplicatedPackages = duplicated;
|
|
2481
2737
|
const lockedNames = new Set(versionMap.keys());
|
|
2482
|
-
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
2738
|
+
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
2483
2739
|
const phantoms = /* @__PURE__ */ new Set();
|
|
2484
2740
|
const phantomDetails = [];
|
|
2485
2741
|
for (const pjPath of pkgFiles) {
|
|
2486
2742
|
try {
|
|
2487
|
-
const pj = await readJsonFile(pjPath);
|
|
2743
|
+
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
2488
2744
|
const relPath = path9.relative(rootDir, pjPath);
|
|
2489
2745
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
2490
2746
|
const deps = pj[section];
|
|
@@ -2869,7 +3125,7 @@ var IAC_EXTENSIONS = {
|
|
|
2869
3125
|
".tf": "terraform",
|
|
2870
3126
|
".bicep": "bicep"
|
|
2871
3127
|
};
|
|
2872
|
-
async function scanBuildDeploy(rootDir) {
|
|
3128
|
+
async function scanBuildDeploy(rootDir, cache) {
|
|
2873
3129
|
const result = {
|
|
2874
3130
|
ci: [],
|
|
2875
3131
|
ciWorkflowCount: 0,
|
|
@@ -2879,26 +3135,37 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2879
3135
|
packageManagers: [],
|
|
2880
3136
|
monorepoTools: []
|
|
2881
3137
|
};
|
|
3138
|
+
const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
|
|
3139
|
+
const _findFiles = cache ? (dir, pred) => cache.findFiles(dir, pred) : findFiles;
|
|
3140
|
+
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
2882
3141
|
const ciSystems = /* @__PURE__ */ new Set();
|
|
2883
3142
|
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
2884
3143
|
const fullPath = path10.join(rootDir, file);
|
|
2885
|
-
if (await
|
|
3144
|
+
if (await _pathExists(fullPath)) {
|
|
2886
3145
|
ciSystems.add(system);
|
|
2887
3146
|
}
|
|
2888
3147
|
}
|
|
2889
3148
|
const ghWorkflowDir = path10.join(rootDir, ".github", "workflows");
|
|
2890
|
-
if (await
|
|
3149
|
+
if (await _pathExists(ghWorkflowDir)) {
|
|
2891
3150
|
try {
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3151
|
+
if (cache) {
|
|
3152
|
+
const entries = await cache.walkDir(rootDir);
|
|
3153
|
+
const ghPrefix = path10.relative(rootDir, ghWorkflowDir) + path10.sep;
|
|
3154
|
+
result.ciWorkflowCount = entries.filter(
|
|
3155
|
+
(e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
|
|
3156
|
+
).length;
|
|
3157
|
+
} else {
|
|
3158
|
+
const files = await _findFiles(
|
|
3159
|
+
ghWorkflowDir,
|
|
3160
|
+
(name) => name.endsWith(".yml") || name.endsWith(".yaml")
|
|
3161
|
+
);
|
|
3162
|
+
result.ciWorkflowCount = files.length;
|
|
3163
|
+
}
|
|
2897
3164
|
} catch {
|
|
2898
3165
|
}
|
|
2899
3166
|
}
|
|
2900
3167
|
result.ci = [...ciSystems].sort();
|
|
2901
|
-
const dockerfiles = await
|
|
3168
|
+
const dockerfiles = await _findFiles(
|
|
2902
3169
|
rootDir,
|
|
2903
3170
|
(name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
|
|
2904
3171
|
);
|
|
@@ -2906,7 +3173,7 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2906
3173
|
const baseImages = /* @__PURE__ */ new Set();
|
|
2907
3174
|
for (const df of dockerfiles) {
|
|
2908
3175
|
try {
|
|
2909
|
-
const content = await
|
|
3176
|
+
const content = await _readTextFile(df);
|
|
2910
3177
|
for (const line of content.split("\n")) {
|
|
2911
3178
|
const trimmed = line.trim();
|
|
2912
3179
|
if (/^FROM\s+/i.test(trimmed)) {
|
|
@@ -2926,24 +3193,24 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2926
3193
|
result.docker.baseImages = [...baseImages].sort();
|
|
2927
3194
|
const iacSystems = /* @__PURE__ */ new Set();
|
|
2928
3195
|
for (const [ext, system] of Object.entries(IAC_EXTENSIONS)) {
|
|
2929
|
-
const files = await
|
|
3196
|
+
const files = await _findFiles(rootDir, (name) => name.endsWith(ext));
|
|
2930
3197
|
if (files.length > 0) iacSystems.add(system);
|
|
2931
3198
|
}
|
|
2932
|
-
const cfnFiles = await
|
|
3199
|
+
const cfnFiles = await _findFiles(
|
|
2933
3200
|
rootDir,
|
|
2934
3201
|
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
2935
3202
|
);
|
|
2936
3203
|
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
2937
|
-
if (await
|
|
3204
|
+
if (await _pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
2938
3205
|
result.iac = [...iacSystems].sort();
|
|
2939
3206
|
const releaseTools = /* @__PURE__ */ new Set();
|
|
2940
3207
|
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
2941
|
-
if (await
|
|
3208
|
+
if (await _pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
|
|
2942
3209
|
}
|
|
2943
|
-
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
3210
|
+
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
2944
3211
|
for (const pjPath of pkgFiles) {
|
|
2945
3212
|
try {
|
|
2946
|
-
const pj = await readJsonFile(pjPath);
|
|
3213
|
+
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
2947
3214
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
2948
3215
|
const deps = pj[section];
|
|
2949
3216
|
if (!deps) continue;
|
|
@@ -2963,12 +3230,12 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2963
3230
|
};
|
|
2964
3231
|
const managers = /* @__PURE__ */ new Set();
|
|
2965
3232
|
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
2966
|
-
if (await
|
|
3233
|
+
if (await _pathExists(path10.join(rootDir, file))) managers.add(manager);
|
|
2967
3234
|
}
|
|
2968
3235
|
result.packageManagers = [...managers].sort();
|
|
2969
3236
|
const monoTools = /* @__PURE__ */ new Set();
|
|
2970
3237
|
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
2971
|
-
if (await
|
|
3238
|
+
if (await _pathExists(path10.join(rootDir, file))) monoTools.add(tool);
|
|
2972
3239
|
}
|
|
2973
3240
|
result.monorepoTools = [...monoTools].sort();
|
|
2974
3241
|
return result;
|
|
@@ -2976,7 +3243,7 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2976
3243
|
|
|
2977
3244
|
// src/scanners/ts-modernity.ts
|
|
2978
3245
|
import * as path11 from "path";
|
|
2979
|
-
async function scanTsModernity(rootDir) {
|
|
3246
|
+
async function scanTsModernity(rootDir, cache) {
|
|
2980
3247
|
const result = {
|
|
2981
3248
|
typescriptVersion: null,
|
|
2982
3249
|
strict: null,
|
|
@@ -2988,12 +3255,12 @@ async function scanTsModernity(rootDir) {
|
|
|
2988
3255
|
moduleType: null,
|
|
2989
3256
|
exportsField: false
|
|
2990
3257
|
};
|
|
2991
|
-
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
3258
|
+
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
2992
3259
|
let hasEsm = false;
|
|
2993
3260
|
let hasCjs = false;
|
|
2994
3261
|
for (const pjPath of pkgFiles) {
|
|
2995
3262
|
try {
|
|
2996
|
-
const pj = await readJsonFile(pjPath);
|
|
3263
|
+
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
2997
3264
|
if (!result.typescriptVersion) {
|
|
2998
3265
|
const tsVer = pj.devDependencies?.["typescript"] ?? pj.dependencies?.["typescript"];
|
|
2999
3266
|
if (tsVer) {
|
|
@@ -3014,8 +3281,9 @@ async function scanTsModernity(rootDir) {
|
|
|
3014
3281
|
else if (hasEsm) result.moduleType = "esm";
|
|
3015
3282
|
else if (hasCjs) result.moduleType = "cjs";
|
|
3016
3283
|
let tsConfigPath = path11.join(rootDir, "tsconfig.json");
|
|
3017
|
-
|
|
3018
|
-
|
|
3284
|
+
const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
|
|
3285
|
+
if (!tsConfigExists) {
|
|
3286
|
+
const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
3019
3287
|
if (tsConfigs.length > 0) {
|
|
3020
3288
|
tsConfigPath = tsConfigs[0];
|
|
3021
3289
|
} else {
|
|
@@ -3023,7 +3291,7 @@ async function scanTsModernity(rootDir) {
|
|
|
3023
3291
|
}
|
|
3024
3292
|
}
|
|
3025
3293
|
try {
|
|
3026
|
-
const raw = await readTextFile(tsConfigPath);
|
|
3294
|
+
const raw = cache ? await cache.readTextFile(tsConfigPath) : await readTextFile(tsConfigPath);
|
|
3027
3295
|
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
|
|
3028
3296
|
const tsConfig = JSON.parse(stripped);
|
|
3029
3297
|
const co = tsConfig?.compilerOptions;
|
|
@@ -3394,43 +3662,63 @@ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
3394
3662
|
".mp4",
|
|
3395
3663
|
".webm"
|
|
3396
3664
|
]);
|
|
3397
|
-
async function scanFileHotspots(rootDir) {
|
|
3665
|
+
async function scanFileHotspots(rootDir, cache) {
|
|
3398
3666
|
const extensionCounts = {};
|
|
3399
3667
|
const allFiles = [];
|
|
3400
3668
|
let maxDepth = 0;
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
const
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3669
|
+
if (cache) {
|
|
3670
|
+
const entries = await cache.walkDir(rootDir);
|
|
3671
|
+
for (const entry of entries) {
|
|
3672
|
+
if (!entry.isFile) continue;
|
|
3673
|
+
const ext = path12.extname(entry.name).toLowerCase();
|
|
3674
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
3675
|
+
const depth = entry.relPath.split(path12.sep).length - 1;
|
|
3676
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
3677
|
+
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
3678
|
+
try {
|
|
3679
|
+
const stat3 = await fs4.stat(entry.absPath);
|
|
3680
|
+
allFiles.push({
|
|
3681
|
+
path: entry.relPath,
|
|
3682
|
+
bytes: stat3.size
|
|
3683
|
+
});
|
|
3684
|
+
} catch {
|
|
3685
|
+
}
|
|
3413
3686
|
}
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
const
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3687
|
+
} else {
|
|
3688
|
+
async function walk(dir, depth) {
|
|
3689
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
3690
|
+
let entries;
|
|
3691
|
+
try {
|
|
3692
|
+
const dirents = await fs4.readdir(dir, { withFileTypes: true });
|
|
3693
|
+
entries = dirents.map((d) => ({
|
|
3694
|
+
name: d.name,
|
|
3695
|
+
isDirectory: d.isDirectory(),
|
|
3696
|
+
isFile: d.isFile()
|
|
3697
|
+
}));
|
|
3698
|
+
} catch {
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
for (const e of entries) {
|
|
3702
|
+
if (e.isDirectory) {
|
|
3703
|
+
if (SKIP_DIRS2.has(e.name)) continue;
|
|
3704
|
+
await walk(path12.join(dir, e.name), depth + 1);
|
|
3705
|
+
} else if (e.isFile) {
|
|
3706
|
+
const ext = path12.extname(e.name).toLowerCase();
|
|
3707
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
3708
|
+
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
3709
|
+
try {
|
|
3710
|
+
const stat3 = await fs4.stat(path12.join(dir, e.name));
|
|
3711
|
+
allFiles.push({
|
|
3712
|
+
path: path12.relative(rootDir, path12.join(dir, e.name)),
|
|
3713
|
+
bytes: stat3.size
|
|
3714
|
+
});
|
|
3715
|
+
} catch {
|
|
3716
|
+
}
|
|
3429
3717
|
}
|
|
3430
3718
|
}
|
|
3431
3719
|
}
|
|
3720
|
+
await walk(rootDir, 0);
|
|
3432
3721
|
}
|
|
3433
|
-
await walk(rootDir, 0);
|
|
3434
3722
|
allFiles.sort((a, b) => b.bytes - a.bytes);
|
|
3435
3723
|
const largestFiles = allFiles.slice(0, 20);
|
|
3436
3724
|
return {
|
|
@@ -3452,7 +3740,7 @@ var LOCKFILES = {
|
|
|
3452
3740
|
"bun.lockb": "bun",
|
|
3453
3741
|
"packages.lock.json": "nuget"
|
|
3454
3742
|
};
|
|
3455
|
-
async function scanSecurityPosture(rootDir) {
|
|
3743
|
+
async function scanSecurityPosture(rootDir, cache) {
|
|
3456
3744
|
const result = {
|
|
3457
3745
|
lockfilePresent: false,
|
|
3458
3746
|
multipleLockfileTypes: false,
|
|
@@ -3461,9 +3749,11 @@ async function scanSecurityPosture(rootDir) {
|
|
|
3461
3749
|
envFilesTracked: false,
|
|
3462
3750
|
lockfileTypes: []
|
|
3463
3751
|
};
|
|
3752
|
+
const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
|
|
3753
|
+
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
3464
3754
|
const foundLockfiles = [];
|
|
3465
3755
|
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
3466
|
-
if (await
|
|
3756
|
+
if (await _pathExists(path13.join(rootDir, file))) {
|
|
3467
3757
|
foundLockfiles.push(type);
|
|
3468
3758
|
}
|
|
3469
3759
|
}
|
|
@@ -3471,9 +3761,9 @@ async function scanSecurityPosture(rootDir) {
|
|
|
3471
3761
|
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
3472
3762
|
result.lockfileTypes = foundLockfiles.sort();
|
|
3473
3763
|
const gitignorePath = path13.join(rootDir, ".gitignore");
|
|
3474
|
-
if (await
|
|
3764
|
+
if (await _pathExists(gitignorePath)) {
|
|
3475
3765
|
try {
|
|
3476
|
-
const content = await
|
|
3766
|
+
const content = await _readTextFile(gitignorePath);
|
|
3477
3767
|
const lines = content.split("\n").map((l) => l.trim());
|
|
3478
3768
|
result.gitignoreCoversEnv = lines.some(
|
|
3479
3769
|
(line) => line === ".env" || line === ".env*" || line === ".env.*" || line === ".env.local" || line === "*.env"
|
|
@@ -3485,7 +3775,7 @@ async function scanSecurityPosture(rootDir) {
|
|
|
3485
3775
|
}
|
|
3486
3776
|
}
|
|
3487
3777
|
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
3488
|
-
if (await
|
|
3778
|
+
if (await _pathExists(path13.join(rootDir, envFile))) {
|
|
3489
3779
|
if (!result.gitignoreCoversEnv) {
|
|
3490
3780
|
result.envFilesTracked = true;
|
|
3491
3781
|
break;
|
|
@@ -3909,12 +4199,545 @@ function scanServiceDependencies(projects) {
|
|
|
3909
4199
|
return result;
|
|
3910
4200
|
}
|
|
3911
4201
|
|
|
4202
|
+
// src/scanners/architecture.ts
|
|
4203
|
+
import * as path14 from "path";
|
|
4204
|
+
import * as fs5 from "fs/promises";
|
|
4205
|
+
var ARCHETYPE_SIGNALS = [
|
|
4206
|
+
// Meta-frameworks (highest priority — they imply routing patterns)
|
|
4207
|
+
{ packages: ["next", "@next/core"], archetype: "nextjs", weight: 10 },
|
|
4208
|
+
{ packages: ["@remix-run/react", "@remix-run/node", "@remix-run/dev"], archetype: "remix", weight: 10 },
|
|
4209
|
+
{ packages: ["@sveltejs/kit"], archetype: "sveltekit", weight: 10 },
|
|
4210
|
+
{ packages: ["nuxt"], archetype: "nuxt", weight: 10 },
|
|
4211
|
+
// Backend frameworks
|
|
4212
|
+
{ packages: ["@nestjs/core", "@nestjs/common"], archetype: "nestjs", weight: 9 },
|
|
4213
|
+
{ packages: ["fastify"], archetype: "fastify", weight: 8 },
|
|
4214
|
+
{ packages: ["hono"], archetype: "hono", weight: 8 },
|
|
4215
|
+
{ packages: ["koa"], archetype: "koa", weight: 8 },
|
|
4216
|
+
{ packages: ["express"], archetype: "express", weight: 7 },
|
|
4217
|
+
// Serverless
|
|
4218
|
+
{ packages: ["serverless", "aws-lambda", "@aws-sdk/client-lambda", "middy", "@cloudflare/workers-types"], archetype: "serverless", weight: 6 },
|
|
4219
|
+
// CLI
|
|
4220
|
+
{ packages: ["commander", "yargs", "meow", "cac", "clipanion", "oclif"], archetype: "cli", weight: 5 }
|
|
4221
|
+
];
|
|
4222
|
+
function detectArchetype(projects) {
|
|
4223
|
+
const allPackages = /* @__PURE__ */ new Set();
|
|
4224
|
+
for (const p of projects) {
|
|
4225
|
+
for (const d of p.dependencies) {
|
|
4226
|
+
allPackages.add(d.package);
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
if (projects.length > 2) {
|
|
4230
|
+
return { archetype: "monorepo", confidence: 0.8 };
|
|
4231
|
+
}
|
|
4232
|
+
let bestArchetype = "unknown";
|
|
4233
|
+
let bestScore = 0;
|
|
4234
|
+
for (const signal of ARCHETYPE_SIGNALS) {
|
|
4235
|
+
const matched = signal.packages.filter((p) => allPackages.has(p)).length;
|
|
4236
|
+
if (matched > 0) {
|
|
4237
|
+
const score = matched * signal.weight;
|
|
4238
|
+
if (score > bestScore) {
|
|
4239
|
+
bestScore = score;
|
|
4240
|
+
bestArchetype = signal.archetype;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
if (bestArchetype === "unknown") {
|
|
4245
|
+
bestArchetype = "library";
|
|
4246
|
+
bestScore = 3;
|
|
4247
|
+
}
|
|
4248
|
+
const confidence = Math.min(bestScore / 15, 1);
|
|
4249
|
+
return { archetype: bestArchetype, confidence: Math.round(confidence * 100) / 100 };
|
|
4250
|
+
}
|
|
4251
|
+
var PATH_RULES = [
|
|
4252
|
+
// ── Testing (high precision) ──
|
|
4253
|
+
{ pattern: /\/__tests__\//, layer: "testing", confidence: 0.95, signal: "__tests__ directory" },
|
|
4254
|
+
{ pattern: /\.test\.[jt]sx?$/, layer: "testing", confidence: 0.95, signal: ".test.* file" },
|
|
4255
|
+
{ pattern: /\.spec\.[jt]sx?$/, layer: "testing", confidence: 0.95, signal: ".spec.* file" },
|
|
4256
|
+
{ pattern: /\/test\//, layer: "testing", confidence: 0.85, signal: "test/ directory" },
|
|
4257
|
+
{ pattern: /\/tests\//, layer: "testing", confidence: 0.85, signal: "tests/ directory" },
|
|
4258
|
+
{ pattern: /\/__mocks__\//, layer: "testing", confidence: 0.9, signal: "__mocks__ directory" },
|
|
4259
|
+
{ pattern: /\/fixtures\//, layer: "testing", confidence: 0.8, signal: "fixtures/ directory" },
|
|
4260
|
+
// ── Config/Infrastructure (high precision) ──
|
|
4261
|
+
{ pattern: /\/config\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "config.* file" },
|
|
4262
|
+
{ pattern: /\/config\//, layer: "config", confidence: 0.8, signal: "config/ directory" },
|
|
4263
|
+
{ pattern: /\.config\.[jt]sx?$/, layer: "config", confidence: 0.9, signal: ".config.* file" },
|
|
4264
|
+
{ pattern: /\/env\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "env.* file" },
|
|
4265
|
+
{ pattern: /\/bootstrap\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "bootstrap file" },
|
|
4266
|
+
{ pattern: /\/setup\.[jt]sx?$/, layer: "config", confidence: 0.8, signal: "setup file" },
|
|
4267
|
+
// ── Next.js (archetype-specific) ──
|
|
4268
|
+
{ pattern: /(^|\/)app\/.*\/route\.[jt]sx?$/, layer: "routing", confidence: 0.95, signal: "Next.js App Router route", archetypes: ["nextjs"] },
|
|
4269
|
+
{ pattern: /(^|\/)pages\/api\//, layer: "routing", confidence: 0.95, signal: "Next.js Pages API route", archetypes: ["nextjs"] },
|
|
4270
|
+
{ pattern: /(^|\/)app\/.*page\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Next.js page component", archetypes: ["nextjs"] },
|
|
4271
|
+
{ pattern: /(^|\/)app\/.*layout\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Next.js layout component", archetypes: ["nextjs"] },
|
|
4272
|
+
{ pattern: /(^|\/)app\/.*loading\.[jt]sx?$/, layer: "presentation", confidence: 0.85, signal: "Next.js loading component", archetypes: ["nextjs"] },
|
|
4273
|
+
{ pattern: /(^|\/)app\/.*error\.[jt]sx?$/, layer: "presentation", confidence: 0.85, signal: "Next.js error component", archetypes: ["nextjs"] },
|
|
4274
|
+
{ pattern: /(^|\/)middleware\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "Next.js middleware", archetypes: ["nextjs"] },
|
|
4275
|
+
// ── Remix (archetype-specific) ──
|
|
4276
|
+
{ pattern: /\/app\/routes\//, layer: "routing", confidence: 0.95, signal: "Remix route file", archetypes: ["remix"] },
|
|
4277
|
+
{ pattern: /\/app\/root\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Remix root", archetypes: ["remix"] },
|
|
4278
|
+
// ── SvelteKit (archetype-specific) ──
|
|
4279
|
+
{ pattern: /\/src\/routes\/.*\+server\.[jt]s$/, layer: "routing", confidence: 0.95, signal: "SvelteKit API route", archetypes: ["sveltekit"] },
|
|
4280
|
+
{ pattern: /\/src\/routes\/.*\+page\.svelte$/, layer: "presentation", confidence: 0.9, signal: "SvelteKit page", archetypes: ["sveltekit"] },
|
|
4281
|
+
{ pattern: /\/src\/routes\/.*\+layout\.svelte$/, layer: "presentation", confidence: 0.9, signal: "SvelteKit layout", archetypes: ["sveltekit"] },
|
|
4282
|
+
{ pattern: /\/src\/hooks\.server\.[jt]s$/, layer: "middleware", confidence: 0.9, signal: "SvelteKit server hooks", archetypes: ["sveltekit"] },
|
|
4283
|
+
// ── Nuxt (archetype-specific) ──
|
|
4284
|
+
{ pattern: /\/server\/api\//, layer: "routing", confidence: 0.95, signal: "Nuxt server API", archetypes: ["nuxt"] },
|
|
4285
|
+
{ pattern: /\/server\/routes\//, layer: "routing", confidence: 0.95, signal: "Nuxt server route", archetypes: ["nuxt"] },
|
|
4286
|
+
{ pattern: /\/server\/middleware\//, layer: "middleware", confidence: 0.95, signal: "Nuxt server middleware", archetypes: ["nuxt"] },
|
|
4287
|
+
{ pattern: /\/pages\//, layer: "presentation", confidence: 0.85, signal: "Nuxt pages directory", archetypes: ["nuxt"] },
|
|
4288
|
+
// ── NestJS (archetype-specific) ──
|
|
4289
|
+
{ pattern: /\.controller\.[jt]sx?$/, layer: "routing", confidence: 0.95, signal: "NestJS controller", archetypes: ["nestjs"] },
|
|
4290
|
+
{ pattern: /\.service\.[jt]sx?$/, layer: "services", confidence: 0.95, signal: "NestJS service", archetypes: ["nestjs"] },
|
|
4291
|
+
{ pattern: /\.module\.[jt]sx?$/, layer: "config", confidence: 0.9, signal: "NestJS module", archetypes: ["nestjs"] },
|
|
4292
|
+
{ pattern: /\.guard\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS guard", archetypes: ["nestjs"] },
|
|
4293
|
+
{ pattern: /\.interceptor\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS interceptor", archetypes: ["nestjs"] },
|
|
4294
|
+
{ pattern: /\.pipe\.[jt]sx?$/, layer: "middleware", confidence: 0.85, signal: "NestJS pipe", archetypes: ["nestjs"] },
|
|
4295
|
+
{ pattern: /\.middleware\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS middleware", archetypes: ["nestjs"] },
|
|
4296
|
+
{ pattern: /\.entity\.[jt]sx?$/, layer: "domain", confidence: 0.9, signal: "NestJS entity", archetypes: ["nestjs"] },
|
|
4297
|
+
{ pattern: /\.dto\.[jt]sx?$/, layer: "domain", confidence: 0.85, signal: "NestJS DTO", archetypes: ["nestjs"] },
|
|
4298
|
+
{ pattern: /\.repository\.[jt]sx?$/, layer: "data-access", confidence: 0.9, signal: "NestJS repository", archetypes: ["nestjs"] },
|
|
4299
|
+
// ── Generic routing patterns ──
|
|
4300
|
+
{ pattern: /\/routes\//, layer: "routing", confidence: 0.8, signal: "routes/ directory" },
|
|
4301
|
+
{ pattern: /\/router\//, layer: "routing", confidence: 0.8, signal: "router/ directory" },
|
|
4302
|
+
{ pattern: /\/controllers\//, layer: "routing", confidence: 0.8, signal: "controllers/ directory" },
|
|
4303
|
+
{ pattern: /\/handlers\//, layer: "routing", confidence: 0.75, signal: "handlers/ directory" },
|
|
4304
|
+
{ pattern: /\/api\//, layer: "routing", confidence: 0.7, signal: "api/ directory" },
|
|
4305
|
+
{ pattern: /\/endpoints\//, layer: "routing", confidence: 0.8, signal: "endpoints/ directory" },
|
|
4306
|
+
// ── Middleware ──
|
|
4307
|
+
{ pattern: /\/middleware\//, layer: "middleware", confidence: 0.85, signal: "middleware/ directory" },
|
|
4308
|
+
{ pattern: /\/middlewares\//, layer: "middleware", confidence: 0.85, signal: "middlewares/ directory" },
|
|
4309
|
+
{ pattern: /\/hooks\//, layer: "middleware", confidence: 0.6, signal: "hooks/ directory" },
|
|
4310
|
+
{ pattern: /\/plugins\//, layer: "middleware", confidence: 0.6, signal: "plugins/ directory" },
|
|
4311
|
+
{ pattern: /\/guards\//, layer: "middleware", confidence: 0.85, signal: "guards/ directory" },
|
|
4312
|
+
{ pattern: /\/interceptors\//, layer: "middleware", confidence: 0.85, signal: "interceptors/ directory" },
|
|
4313
|
+
// ── Services / application layer ──
|
|
4314
|
+
{ pattern: /\/services\//, layer: "services", confidence: 0.85, signal: "services/ directory" },
|
|
4315
|
+
{ pattern: /\/service\//, layer: "services", confidence: 0.8, signal: "service/ directory" },
|
|
4316
|
+
{ pattern: /\/usecases\//, layer: "services", confidence: 0.85, signal: "usecases/ directory" },
|
|
4317
|
+
{ pattern: /\/use-cases\//, layer: "services", confidence: 0.85, signal: "use-cases/ directory" },
|
|
4318
|
+
{ pattern: /\/application\//, layer: "services", confidence: 0.7, signal: "application/ directory" },
|
|
4319
|
+
{ pattern: /\/actions\//, layer: "services", confidence: 0.65, signal: "actions/ directory" },
|
|
4320
|
+
// ── Domain / models ──
|
|
4321
|
+
{ pattern: /\/domain\//, layer: "domain", confidence: 0.85, signal: "domain/ directory" },
|
|
4322
|
+
{ pattern: /\/models\//, layer: "domain", confidence: 0.8, signal: "models/ directory" },
|
|
4323
|
+
{ pattern: /\/entities\//, layer: "domain", confidence: 0.85, signal: "entities/ directory" },
|
|
4324
|
+
{ pattern: /\/types\//, layer: "domain", confidence: 0.7, signal: "types/ directory" },
|
|
4325
|
+
{ pattern: /\/schemas\//, layer: "domain", confidence: 0.7, signal: "schemas/ directory" },
|
|
4326
|
+
{ pattern: /\/validators\//, layer: "domain", confidence: 0.7, signal: "validators/ directory" },
|
|
4327
|
+
// ── Data access ──
|
|
4328
|
+
{ pattern: /\/repositories\//, layer: "data-access", confidence: 0.9, signal: "repositories/ directory" },
|
|
4329
|
+
{ pattern: /\/repository\//, layer: "data-access", confidence: 0.85, signal: "repository/ directory" },
|
|
4330
|
+
{ pattern: /\/dao\//, layer: "data-access", confidence: 0.9, signal: "dao/ directory" },
|
|
4331
|
+
{ pattern: /\/db\//, layer: "data-access", confidence: 0.8, signal: "db/ directory" },
|
|
4332
|
+
{ pattern: /\/database\//, layer: "data-access", confidence: 0.8, signal: "database/ directory" },
|
|
4333
|
+
{ pattern: /\/persistence\//, layer: "data-access", confidence: 0.85, signal: "persistence/ directory" },
|
|
4334
|
+
{ pattern: /\/migrations\//, layer: "data-access", confidence: 0.9, signal: "migrations/ directory" },
|
|
4335
|
+
{ pattern: /\/seeds\//, layer: "data-access", confidence: 0.85, signal: "seeds/ directory" },
|
|
4336
|
+
{ pattern: /\/prisma\//, layer: "data-access", confidence: 0.85, signal: "prisma/ directory" },
|
|
4337
|
+
{ pattern: /\/drizzle\//, layer: "data-access", confidence: 0.85, signal: "drizzle/ directory" },
|
|
4338
|
+
// ── Infrastructure ──
|
|
4339
|
+
{ pattern: /\/infra\//, layer: "infrastructure", confidence: 0.85, signal: "infra/ directory" },
|
|
4340
|
+
{ pattern: /\/infrastructure\//, layer: "infrastructure", confidence: 0.85, signal: "infrastructure/ directory" },
|
|
4341
|
+
{ pattern: /\/adapters\//, layer: "infrastructure", confidence: 0.8, signal: "adapters/ directory" },
|
|
4342
|
+
{ pattern: /\/clients\//, layer: "infrastructure", confidence: 0.75, signal: "clients/ directory" },
|
|
4343
|
+
{ pattern: /\/integrations\//, layer: "infrastructure", confidence: 0.8, signal: "integrations/ directory" },
|
|
4344
|
+
{ pattern: /\/external\//, layer: "infrastructure", confidence: 0.75, signal: "external/ directory" },
|
|
4345
|
+
{ pattern: /\/queue\//, layer: "infrastructure", confidence: 0.8, signal: "queue/ directory" },
|
|
4346
|
+
{ pattern: /\/jobs\//, layer: "infrastructure", confidence: 0.75, signal: "jobs/ directory" },
|
|
4347
|
+
{ pattern: /\/workers\//, layer: "infrastructure", confidence: 0.75, signal: "workers/ directory" },
|
|
4348
|
+
{ pattern: /\/cron\//, layer: "infrastructure", confidence: 0.8, signal: "cron/ directory" },
|
|
4349
|
+
// ── Presentation (UI layer) ──
|
|
4350
|
+
{ pattern: /\/components\//, layer: "presentation", confidence: 0.85, signal: "components/ directory" },
|
|
4351
|
+
{ pattern: /\/views\//, layer: "presentation", confidence: 0.85, signal: "views/ directory" },
|
|
4352
|
+
{ pattern: /\/pages\//, layer: "presentation", confidence: 0.8, signal: "pages/ directory" },
|
|
4353
|
+
{ pattern: /\/layouts\//, layer: "presentation", confidence: 0.85, signal: "layouts/ directory" },
|
|
4354
|
+
{ pattern: /\/templates\//, layer: "presentation", confidence: 0.8, signal: "templates/ directory" },
|
|
4355
|
+
{ pattern: /\/widgets\//, layer: "presentation", confidence: 0.8, signal: "widgets/ directory" },
|
|
4356
|
+
{ pattern: /\/ui\//, layer: "presentation", confidence: 0.75, signal: "ui/ directory" },
|
|
4357
|
+
// ── Shared / utils ──
|
|
4358
|
+
{ pattern: /\/utils\//, layer: "shared", confidence: 0.7, signal: "utils/ directory" },
|
|
4359
|
+
{ pattern: /\/helpers\//, layer: "shared", confidence: 0.7, signal: "helpers/ directory" },
|
|
4360
|
+
{ pattern: /\/lib\//, layer: "shared", confidence: 0.6, signal: "lib/ directory" },
|
|
4361
|
+
{ pattern: /\/common\//, layer: "shared", confidence: 0.65, signal: "common/ directory" },
|
|
4362
|
+
{ pattern: /\/shared\//, layer: "shared", confidence: 0.75, signal: "shared/ directory" },
|
|
4363
|
+
{ pattern: /\/constants\//, layer: "shared", confidence: 0.7, signal: "constants/ directory" },
|
|
4364
|
+
// ── CLI-specific (command layer → routing) ──
|
|
4365
|
+
{ pattern: /\/commands\//, layer: "routing", confidence: 0.8, signal: "commands/ directory", archetypes: ["cli"] },
|
|
4366
|
+
{ pattern: /\/formatters\//, layer: "presentation", confidence: 0.8, signal: "formatters/ directory", archetypes: ["cli"] },
|
|
4367
|
+
{ pattern: /\/scanners\//, layer: "services", confidence: 0.8, signal: "scanners/ directory", archetypes: ["cli"] },
|
|
4368
|
+
{ pattern: /\/scoring\//, layer: "domain", confidence: 0.8, signal: "scoring/ directory", archetypes: ["cli"] },
|
|
4369
|
+
// ── Serverless-specific ──
|
|
4370
|
+
{ pattern: /\/functions\//, layer: "routing", confidence: 0.8, signal: "functions/ directory", archetypes: ["serverless"] },
|
|
4371
|
+
{ pattern: /\/lambdas\//, layer: "routing", confidence: 0.85, signal: "lambdas/ directory", archetypes: ["serverless"] },
|
|
4372
|
+
{ pattern: /\/layers\//, layer: "shared", confidence: 0.7, signal: "Lambda layers/ directory", archetypes: ["serverless"] }
|
|
4373
|
+
];
|
|
4374
|
+
var SUFFIX_RULES = [
|
|
4375
|
+
{ suffix: ".controller", layer: "routing", confidence: 0.85, signal: "controller suffix" },
|
|
4376
|
+
{ suffix: ".route", layer: "routing", confidence: 0.85, signal: "route suffix" },
|
|
4377
|
+
{ suffix: ".router", layer: "routing", confidence: 0.85, signal: "router suffix" },
|
|
4378
|
+
{ suffix: ".handler", layer: "routing", confidence: 0.8, signal: "handler suffix" },
|
|
4379
|
+
{ suffix: ".middleware", layer: "middleware", confidence: 0.85, signal: "middleware suffix" },
|
|
4380
|
+
{ suffix: ".guard", layer: "middleware", confidence: 0.85, signal: "guard suffix" },
|
|
4381
|
+
{ suffix: ".interceptor", layer: "middleware", confidence: 0.85, signal: "interceptor suffix" },
|
|
4382
|
+
{ suffix: ".service", layer: "services", confidence: 0.85, signal: "service suffix" },
|
|
4383
|
+
{ suffix: ".usecase", layer: "services", confidence: 0.85, signal: "usecase suffix" },
|
|
4384
|
+
{ suffix: ".model", layer: "domain", confidence: 0.8, signal: "model suffix" },
|
|
4385
|
+
{ suffix: ".entity", layer: "domain", confidence: 0.85, signal: "entity suffix" },
|
|
4386
|
+
{ suffix: ".dto", layer: "domain", confidence: 0.8, signal: "DTO suffix" },
|
|
4387
|
+
{ suffix: ".schema", layer: "domain", confidence: 0.75, signal: "schema suffix" },
|
|
4388
|
+
{ suffix: ".validator", layer: "domain", confidence: 0.75, signal: "validator suffix" },
|
|
4389
|
+
{ suffix: ".repository", layer: "data-access", confidence: 0.9, signal: "repository suffix" },
|
|
4390
|
+
{ suffix: ".repo", layer: "data-access", confidence: 0.85, signal: "repo suffix" },
|
|
4391
|
+
{ suffix: ".dao", layer: "data-access", confidence: 0.9, signal: "dao suffix" },
|
|
4392
|
+
{ suffix: ".migration", layer: "data-access", confidence: 0.85, signal: "migration suffix" },
|
|
4393
|
+
{ suffix: ".adapter", layer: "infrastructure", confidence: 0.8, signal: "adapter suffix" },
|
|
4394
|
+
{ suffix: ".client", layer: "infrastructure", confidence: 0.75, signal: "client suffix" },
|
|
4395
|
+
{ suffix: ".provider", layer: "infrastructure", confidence: 0.7, signal: "provider suffix" },
|
|
4396
|
+
{ suffix: ".config", layer: "config", confidence: 0.8, signal: "config suffix" },
|
|
4397
|
+
{ suffix: ".component", layer: "presentation", confidence: 0.8, signal: "component suffix" },
|
|
4398
|
+
{ suffix: ".page", layer: "presentation", confidence: 0.85, signal: "page suffix" },
|
|
4399
|
+
{ suffix: ".view", layer: "presentation", confidence: 0.8, signal: "view suffix" },
|
|
4400
|
+
{ suffix: ".layout", layer: "presentation", confidence: 0.85, signal: "layout suffix" },
|
|
4401
|
+
{ suffix: ".util", layer: "shared", confidence: 0.7, signal: "util suffix" },
|
|
4402
|
+
{ suffix: ".helper", layer: "shared", confidence: 0.7, signal: "helper suffix" },
|
|
4403
|
+
{ suffix: ".constant", layer: "shared", confidence: 0.7, signal: "constant suffix" }
|
|
4404
|
+
];
|
|
4405
|
+
var PACKAGE_LAYER_MAP = {
|
|
4406
|
+
// Routing/controllers
|
|
4407
|
+
"express": "routing",
|
|
4408
|
+
"fastify": "routing",
|
|
4409
|
+
"@nestjs/core": "routing",
|
|
4410
|
+
"hono": "routing",
|
|
4411
|
+
"koa": "routing",
|
|
4412
|
+
"koa-router": "routing",
|
|
4413
|
+
"@hapi/hapi": "routing",
|
|
4414
|
+
"h3": "routing",
|
|
4415
|
+
// Middleware
|
|
4416
|
+
"cors": "middleware",
|
|
4417
|
+
"helmet": "middleware",
|
|
4418
|
+
"passport": "middleware",
|
|
4419
|
+
"express-rate-limit": "middleware",
|
|
4420
|
+
"cookie-parser": "middleware",
|
|
4421
|
+
"body-parser": "middleware",
|
|
4422
|
+
"multer": "middleware",
|
|
4423
|
+
"morgan": "middleware",
|
|
4424
|
+
"compression": "middleware",
|
|
4425
|
+
"express-session": "middleware",
|
|
4426
|
+
// Services / application
|
|
4427
|
+
"bullmq": "services",
|
|
4428
|
+
"bull": "services",
|
|
4429
|
+
"agenda": "services",
|
|
4430
|
+
"pg-boss": "services",
|
|
4431
|
+
"inngest": "services",
|
|
4432
|
+
// Domain / validation
|
|
4433
|
+
"zod": "domain",
|
|
4434
|
+
"joi": "domain",
|
|
4435
|
+
"yup": "domain",
|
|
4436
|
+
"class-validator": "domain",
|
|
4437
|
+
"class-transformer": "domain",
|
|
4438
|
+
"superstruct": "domain",
|
|
4439
|
+
"valibot": "domain",
|
|
4440
|
+
// Data access / ORM
|
|
4441
|
+
"prisma": "data-access",
|
|
4442
|
+
"@prisma/client": "data-access",
|
|
4443
|
+
"drizzle-orm": "data-access",
|
|
4444
|
+
"typeorm": "data-access",
|
|
4445
|
+
"sequelize": "data-access",
|
|
4446
|
+
"knex": "data-access",
|
|
4447
|
+
"pg": "data-access",
|
|
4448
|
+
"mysql2": "data-access",
|
|
4449
|
+
"mongodb": "data-access",
|
|
4450
|
+
"mongoose": "data-access",
|
|
4451
|
+
"ioredis": "data-access",
|
|
4452
|
+
"redis": "data-access",
|
|
4453
|
+
"better-sqlite3": "data-access",
|
|
4454
|
+
"kysely": "data-access",
|
|
4455
|
+
"@mikro-orm/core": "data-access",
|
|
4456
|
+
// Infrastructure
|
|
4457
|
+
"@aws-sdk/client-s3": "infrastructure",
|
|
4458
|
+
"@aws-sdk/client-sqs": "infrastructure",
|
|
4459
|
+
"@aws-sdk/client-sns": "infrastructure",
|
|
4460
|
+
"@aws-sdk/client-ses": "infrastructure",
|
|
4461
|
+
"@aws-sdk/client-lambda": "infrastructure",
|
|
4462
|
+
"@google-cloud/storage": "infrastructure",
|
|
4463
|
+
"@azure/storage-blob": "infrastructure",
|
|
4464
|
+
"nodemailer": "infrastructure",
|
|
4465
|
+
"@sendgrid/mail": "infrastructure",
|
|
4466
|
+
"stripe": "infrastructure",
|
|
4467
|
+
"kafkajs": "infrastructure",
|
|
4468
|
+
"amqplib": "infrastructure",
|
|
4469
|
+
// Presentation
|
|
4470
|
+
"react": "presentation",
|
|
4471
|
+
"react-dom": "presentation",
|
|
4472
|
+
"vue": "presentation",
|
|
4473
|
+
"@angular/core": "presentation",
|
|
4474
|
+
"svelte": "presentation",
|
|
4475
|
+
// Shared
|
|
4476
|
+
"lodash": "shared",
|
|
4477
|
+
"dayjs": "shared",
|
|
4478
|
+
"date-fns": "shared",
|
|
4479
|
+
"uuid": "shared",
|
|
4480
|
+
"nanoid": "shared",
|
|
4481
|
+
// Testing
|
|
4482
|
+
"vitest": "testing",
|
|
4483
|
+
"jest": "testing",
|
|
4484
|
+
"mocha": "testing",
|
|
4485
|
+
"@playwright/test": "testing",
|
|
4486
|
+
"cypress": "testing",
|
|
4487
|
+
"supertest": "testing",
|
|
4488
|
+
// Observability → infrastructure
|
|
4489
|
+
"@sentry/node": "infrastructure",
|
|
4490
|
+
"@opentelemetry/api": "infrastructure",
|
|
4491
|
+
"pino": "infrastructure",
|
|
4492
|
+
"winston": "infrastructure",
|
|
4493
|
+
"dd-trace": "infrastructure"
|
|
4494
|
+
};
|
|
4495
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs", ".svelte", ".vue"]);
|
|
4496
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", ".output", ".svelte-kit", "coverage", ".vibgrate"]);
|
|
4497
|
+
async function walkSourceFiles(rootDir, cache) {
|
|
4498
|
+
if (cache) {
|
|
4499
|
+
const entries = await cache.walkDir(rootDir);
|
|
4500
|
+
return entries.filter((e) => {
|
|
4501
|
+
if (!e.isFile) return false;
|
|
4502
|
+
const name = path14.basename(e.absPath);
|
|
4503
|
+
if (name.startsWith(".") && name !== ".") return false;
|
|
4504
|
+
const ext = path14.extname(name);
|
|
4505
|
+
return SOURCE_EXTENSIONS.has(ext);
|
|
4506
|
+
}).map((e) => e.relPath);
|
|
4507
|
+
}
|
|
4508
|
+
const files = [];
|
|
4509
|
+
async function walk(dir) {
|
|
4510
|
+
let entries;
|
|
4511
|
+
try {
|
|
4512
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
4513
|
+
} catch {
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
for (const entry of entries) {
|
|
4517
|
+
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
4518
|
+
const fullPath = path14.join(dir, entry.name);
|
|
4519
|
+
if (entry.isDirectory()) {
|
|
4520
|
+
if (!IGNORE_DIRS.has(entry.name)) {
|
|
4521
|
+
await walk(fullPath);
|
|
4522
|
+
}
|
|
4523
|
+
} else if (entry.isFile()) {
|
|
4524
|
+
const ext = path14.extname(entry.name);
|
|
4525
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
4526
|
+
files.push(path14.relative(rootDir, fullPath));
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
await walk(rootDir);
|
|
4532
|
+
return files;
|
|
4533
|
+
}
|
|
4534
|
+
function classifyFile(filePath, archetype) {
|
|
4535
|
+
const normalised = filePath.replace(/\\/g, "/");
|
|
4536
|
+
let bestMatch = null;
|
|
4537
|
+
for (const rule of PATH_RULES) {
|
|
4538
|
+
if (rule.archetypes && rule.archetypes.length > 0 && !rule.archetypes.includes(archetype)) {
|
|
4539
|
+
continue;
|
|
4540
|
+
}
|
|
4541
|
+
if (rule.pattern.test(normalised)) {
|
|
4542
|
+
const boost = rule.archetypes ? 0.05 : 0;
|
|
4543
|
+
const adjustedConfidence = Math.min(rule.confidence + boost, 1);
|
|
4544
|
+
if (!bestMatch || adjustedConfidence > bestMatch.confidence) {
|
|
4545
|
+
bestMatch = { layer: rule.layer, confidence: adjustedConfidence, signal: rule.signal };
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
if (!bestMatch || bestMatch.confidence < 0.7) {
|
|
4550
|
+
const baseName = path14.basename(filePath, path14.extname(filePath));
|
|
4551
|
+
const cleanBase = baseName.replace(/\.(test|spec)$/, "");
|
|
4552
|
+
for (const rule of SUFFIX_RULES) {
|
|
4553
|
+
if (cleanBase.endsWith(rule.suffix)) {
|
|
4554
|
+
if (!bestMatch || rule.confidence > bestMatch.confidence) {
|
|
4555
|
+
bestMatch = { layer: rule.layer, confidence: rule.confidence, signal: rule.signal };
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
if (bestMatch) {
|
|
4561
|
+
return {
|
|
4562
|
+
filePath,
|
|
4563
|
+
layer: bestMatch.layer,
|
|
4564
|
+
confidence: bestMatch.confidence,
|
|
4565
|
+
signals: [bestMatch.signal]
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
return null;
|
|
4569
|
+
}
|
|
4570
|
+
function computeLayerDrift(packages) {
|
|
4571
|
+
if (packages.length === 0) {
|
|
4572
|
+
return { score: 100, riskLevel: "low" };
|
|
4573
|
+
}
|
|
4574
|
+
let current = 0;
|
|
4575
|
+
let oneBehind = 0;
|
|
4576
|
+
let twoPlusBehind = 0;
|
|
4577
|
+
let unknown = 0;
|
|
4578
|
+
for (const pkg2 of packages) {
|
|
4579
|
+
if (pkg2.majorsBehind === null) {
|
|
4580
|
+
unknown++;
|
|
4581
|
+
} else if (pkg2.majorsBehind === 0) {
|
|
4582
|
+
current++;
|
|
4583
|
+
} else if (pkg2.majorsBehind === 1) {
|
|
4584
|
+
oneBehind++;
|
|
4585
|
+
} else {
|
|
4586
|
+
twoPlusBehind++;
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
const known = current + oneBehind + twoPlusBehind;
|
|
4590
|
+
if (known === 0) return { score: 100, riskLevel: "low" };
|
|
4591
|
+
const currentPct = current / known;
|
|
4592
|
+
const onePct = oneBehind / known;
|
|
4593
|
+
const twoPct = twoPlusBehind / known;
|
|
4594
|
+
const score = Math.round(Math.max(0, Math.min(100, currentPct * 100 - onePct * 10 - twoPct * 40)));
|
|
4595
|
+
const riskLevel = score >= 70 ? "low" : score >= 40 ? "moderate" : "high";
|
|
4596
|
+
return { score, riskLevel };
|
|
4597
|
+
}
|
|
4598
|
+
function mapToolingToLayers(tooling, services, depsByLayer) {
|
|
4599
|
+
const layerTooling = /* @__PURE__ */ new Map();
|
|
4600
|
+
const layerServices = /* @__PURE__ */ new Map();
|
|
4601
|
+
const pkgLayerLookup = /* @__PURE__ */ new Map();
|
|
4602
|
+
for (const [layer, packages] of depsByLayer) {
|
|
4603
|
+
for (const pkg2 of packages) {
|
|
4604
|
+
pkgLayerLookup.set(pkg2, layer);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
if (tooling) {
|
|
4608
|
+
for (const [, items] of Object.entries(tooling)) {
|
|
4609
|
+
for (const item of items) {
|
|
4610
|
+
const layer = pkgLayerLookup.get(item.package) ?? PACKAGE_LAYER_MAP[item.package] ?? "shared";
|
|
4611
|
+
if (!layerTooling.has(layer)) layerTooling.set(layer, []);
|
|
4612
|
+
const existing = layerTooling.get(layer);
|
|
4613
|
+
if (!existing.some((t) => t.package === item.package)) {
|
|
4614
|
+
existing.push(item);
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
if (services) {
|
|
4620
|
+
for (const [, items] of Object.entries(services)) {
|
|
4621
|
+
for (const item of items) {
|
|
4622
|
+
const layer = pkgLayerLookup.get(item.package) ?? PACKAGE_LAYER_MAP[item.package] ?? "infrastructure";
|
|
4623
|
+
if (!layerServices.has(layer)) layerServices.set(layer, []);
|
|
4624
|
+
const existing = layerServices.get(layer);
|
|
4625
|
+
if (!existing.some((s) => s.package === item.package)) {
|
|
4626
|
+
existing.push(item);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
return { layerTooling, layerServices };
|
|
4632
|
+
}
|
|
4633
|
+
async function scanArchitecture(rootDir, projects, tooling, services, cache) {
|
|
4634
|
+
const { archetype, confidence: archetypeConfidence } = detectArchetype(projects);
|
|
4635
|
+
const sourceFiles = await walkSourceFiles(rootDir, cache);
|
|
4636
|
+
const classifications = [];
|
|
4637
|
+
let unclassified = 0;
|
|
4638
|
+
for (const file of sourceFiles) {
|
|
4639
|
+
const classification = classifyFile(file, archetype);
|
|
4640
|
+
if (classification) {
|
|
4641
|
+
classifications.push(classification);
|
|
4642
|
+
} else {
|
|
4643
|
+
unclassified++;
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
const allDeps = /* @__PURE__ */ new Map();
|
|
4647
|
+
for (const p of projects) {
|
|
4648
|
+
for (const d of p.dependencies) {
|
|
4649
|
+
if (!allDeps.has(d.package)) {
|
|
4650
|
+
allDeps.set(d.package, d);
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
const depsByLayer = /* @__PURE__ */ new Map();
|
|
4655
|
+
for (const [pkg2] of allDeps) {
|
|
4656
|
+
const layer = PACKAGE_LAYER_MAP[pkg2];
|
|
4657
|
+
if (layer) {
|
|
4658
|
+
if (!depsByLayer.has(layer)) depsByLayer.set(layer, /* @__PURE__ */ new Set());
|
|
4659
|
+
depsByLayer.get(layer).add(pkg2);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
const { layerTooling, layerServices } = mapToolingToLayers(tooling, services, depsByLayer);
|
|
4663
|
+
const ALL_LAYERS = [
|
|
4664
|
+
"routing",
|
|
4665
|
+
"middleware",
|
|
4666
|
+
"services",
|
|
4667
|
+
"domain",
|
|
4668
|
+
"data-access",
|
|
4669
|
+
"infrastructure",
|
|
4670
|
+
"presentation",
|
|
4671
|
+
"config",
|
|
4672
|
+
"testing",
|
|
4673
|
+
"shared"
|
|
4674
|
+
];
|
|
4675
|
+
const layerFileCounts = /* @__PURE__ */ new Map();
|
|
4676
|
+
for (const c of classifications) {
|
|
4677
|
+
layerFileCounts.set(c.layer, (layerFileCounts.get(c.layer) ?? 0) + 1);
|
|
4678
|
+
}
|
|
4679
|
+
const layers = [];
|
|
4680
|
+
for (const layer of ALL_LAYERS) {
|
|
4681
|
+
const fileCount = layerFileCounts.get(layer) ?? 0;
|
|
4682
|
+
const layerPkgs = depsByLayer.get(layer) ?? /* @__PURE__ */ new Set();
|
|
4683
|
+
const tech = layerTooling.get(layer) ?? [];
|
|
4684
|
+
const svc = layerServices.get(layer) ?? [];
|
|
4685
|
+
if (fileCount === 0 && layerPkgs.size === 0 && tech.length === 0 && svc.length === 0) {
|
|
4686
|
+
continue;
|
|
4687
|
+
}
|
|
4688
|
+
const packages = [];
|
|
4689
|
+
for (const pkg2 of layerPkgs) {
|
|
4690
|
+
const dep = allDeps.get(pkg2);
|
|
4691
|
+
if (dep) {
|
|
4692
|
+
packages.push({
|
|
4693
|
+
name: dep.package,
|
|
4694
|
+
version: dep.resolvedVersion,
|
|
4695
|
+
latestStable: dep.latestStable,
|
|
4696
|
+
majorsBehind: dep.majorsBehind,
|
|
4697
|
+
drift: dep.drift
|
|
4698
|
+
});
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
const { score, riskLevel } = computeLayerDrift(packages);
|
|
4702
|
+
layers.push({
|
|
4703
|
+
layer,
|
|
4704
|
+
fileCount,
|
|
4705
|
+
driftScore: score,
|
|
4706
|
+
riskLevel,
|
|
4707
|
+
techStack: tech,
|
|
4708
|
+
services: svc,
|
|
4709
|
+
packages
|
|
4710
|
+
});
|
|
4711
|
+
}
|
|
4712
|
+
const LAYER_ORDER = {
|
|
4713
|
+
"presentation": 0,
|
|
4714
|
+
"routing": 1,
|
|
4715
|
+
"middleware": 2,
|
|
4716
|
+
"services": 3,
|
|
4717
|
+
"domain": 4,
|
|
4718
|
+
"data-access": 5,
|
|
4719
|
+
"infrastructure": 6,
|
|
4720
|
+
"config": 7,
|
|
4721
|
+
"shared": 8,
|
|
4722
|
+
"testing": 9
|
|
4723
|
+
};
|
|
4724
|
+
layers.sort((a, b) => (LAYER_ORDER[a.layer] ?? 99) - (LAYER_ORDER[b.layer] ?? 99));
|
|
4725
|
+
return {
|
|
4726
|
+
archetype,
|
|
4727
|
+
archetypeConfidence,
|
|
4728
|
+
layers,
|
|
4729
|
+
totalClassified: classifications.length,
|
|
4730
|
+
unclassified
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4733
|
+
|
|
3912
4734
|
// src/commands/scan.ts
|
|
3913
4735
|
async function runScan(rootDir, opts) {
|
|
3914
4736
|
const scanStart = Date.now();
|
|
3915
4737
|
const config = await loadConfig(rootDir);
|
|
3916
4738
|
const sem = new Semaphore(opts.concurrency);
|
|
3917
4739
|
const npmCache = new NpmCache(rootDir, sem);
|
|
4740
|
+
const fileCache = new FileCache();
|
|
3918
4741
|
const scanners = config.scanners;
|
|
3919
4742
|
let filesScanned = 0;
|
|
3920
4743
|
const progress = new ScanProgress(rootDir);
|
|
@@ -3933,7 +4756,8 @@ async function runScan(rootDir, opts) {
|
|
|
3933
4756
|
...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
3934
4757
|
...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
3935
4758
|
...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
3936
|
-
...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : []
|
|
4759
|
+
...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
|
|
4760
|
+
...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : []
|
|
3937
4761
|
] : [],
|
|
3938
4762
|
{ id: "drift", label: "Computing drift score" },
|
|
3939
4763
|
{ id: "findings", label: "Generating findings" }
|
|
@@ -3945,7 +4769,7 @@ async function runScan(rootDir, opts) {
|
|
|
3945
4769
|
const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
|
|
3946
4770
|
progress.completeStep("vcs", vcsDetail);
|
|
3947
4771
|
progress.startStep("node");
|
|
3948
|
-
const nodeProjects = await scanNodeProjects(rootDir, npmCache);
|
|
4772
|
+
const nodeProjects = await scanNodeProjects(rootDir, npmCache, fileCache);
|
|
3949
4773
|
for (const p of nodeProjects) {
|
|
3950
4774
|
progress.addDependencies(p.dependencies.length);
|
|
3951
4775
|
progress.addFrameworks(p.frameworks.length);
|
|
@@ -3954,7 +4778,7 @@ async function runScan(rootDir, opts) {
|
|
|
3954
4778
|
progress.addProjects(nodeProjects.length);
|
|
3955
4779
|
progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
|
|
3956
4780
|
progress.startStep("dotnet");
|
|
3957
|
-
const dotnetProjects = await scanDotnetProjects(rootDir);
|
|
4781
|
+
const dotnetProjects = await scanDotnetProjects(rootDir, fileCache);
|
|
3958
4782
|
for (const p of dotnetProjects) {
|
|
3959
4783
|
progress.addDependencies(p.dependencies.length);
|
|
3960
4784
|
progress.addFrameworks(p.frameworks.length);
|
|
@@ -3976,7 +4800,7 @@ async function runScan(rootDir, opts) {
|
|
|
3976
4800
|
if (scanners?.platformMatrix?.enabled !== false) {
|
|
3977
4801
|
progress.startStep("platform");
|
|
3978
4802
|
scannerTasks.push(
|
|
3979
|
-
scanPlatformMatrix(rootDir).then((result) => {
|
|
4803
|
+
scanPlatformMatrix(rootDir, fileCache).then((result) => {
|
|
3980
4804
|
extended.platformMatrix = result;
|
|
3981
4805
|
const nativeCount = result.nativeModules.length;
|
|
3982
4806
|
const dockerCount = result.dockerBaseImages.length;
|
|
@@ -4025,7 +4849,7 @@ async function runScan(rootDir, opts) {
|
|
|
4025
4849
|
if (scanners?.securityPosture?.enabled !== false) {
|
|
4026
4850
|
progress.startStep("security");
|
|
4027
4851
|
scannerTasks.push(
|
|
4028
|
-
scanSecurityPosture(rootDir).then((result) => {
|
|
4852
|
+
scanSecurityPosture(rootDir, fileCache).then((result) => {
|
|
4029
4853
|
extended.securityPosture = result;
|
|
4030
4854
|
const secDetail = result.lockfilePresent ? `lockfile \u2714${result.gitignoreCoversEnv ? " \xB7 .env \u2714" : " \xB7 .env \u2716"}` : "no lockfile";
|
|
4031
4855
|
progress.completeStep("security", secDetail);
|
|
@@ -4035,7 +4859,7 @@ async function runScan(rootDir, opts) {
|
|
|
4035
4859
|
if (scanners?.buildDeploy?.enabled !== false) {
|
|
4036
4860
|
progress.startStep("build");
|
|
4037
4861
|
scannerTasks.push(
|
|
4038
|
-
scanBuildDeploy(rootDir).then((result) => {
|
|
4862
|
+
scanBuildDeploy(rootDir, fileCache).then((result) => {
|
|
4039
4863
|
extended.buildDeploy = result;
|
|
4040
4864
|
const bdParts = [];
|
|
4041
4865
|
if (result.ci.length > 0) bdParts.push(result.ci.join(", "));
|
|
@@ -4047,7 +4871,7 @@ async function runScan(rootDir, opts) {
|
|
|
4047
4871
|
if (scanners?.tsModernity?.enabled !== false) {
|
|
4048
4872
|
progress.startStep("ts");
|
|
4049
4873
|
scannerTasks.push(
|
|
4050
|
-
scanTsModernity(rootDir).then((result) => {
|
|
4874
|
+
scanTsModernity(rootDir, fileCache).then((result) => {
|
|
4051
4875
|
extended.tsModernity = result;
|
|
4052
4876
|
const tsParts = [];
|
|
4053
4877
|
if (result.typescriptVersion) tsParts.push(`v${result.typescriptVersion}`);
|
|
@@ -4060,7 +4884,7 @@ async function runScan(rootDir, opts) {
|
|
|
4060
4884
|
if (scanners?.fileHotspots?.enabled !== false) {
|
|
4061
4885
|
progress.startStep("hotspots");
|
|
4062
4886
|
scannerTasks.push(
|
|
4063
|
-
scanFileHotspots(rootDir).then((result) => {
|
|
4887
|
+
scanFileHotspots(rootDir, fileCache).then((result) => {
|
|
4064
4888
|
extended.fileHotspots = result;
|
|
4065
4889
|
progress.completeStep("hotspots", `${result.totalFiles} files`, result.totalFiles);
|
|
4066
4890
|
})
|
|
@@ -4069,7 +4893,7 @@ async function runScan(rootDir, opts) {
|
|
|
4069
4893
|
if (scanners?.dependencyGraph?.enabled !== false) {
|
|
4070
4894
|
progress.startStep("depgraph");
|
|
4071
4895
|
scannerTasks.push(
|
|
4072
|
-
scanDependencyGraph(rootDir).then((result) => {
|
|
4896
|
+
scanDependencyGraph(rootDir, fileCache).then((result) => {
|
|
4073
4897
|
extended.dependencyGraph = result;
|
|
4074
4898
|
const dgDetail = result.lockfileType ? `${result.lockfileType} \xB7 ${result.totalUnique} unique` : "no lockfile";
|
|
4075
4899
|
progress.completeStep("depgraph", dgDetail, result.totalUnique);
|
|
@@ -4090,6 +4914,23 @@ async function runScan(rootDir, opts) {
|
|
|
4090
4914
|
);
|
|
4091
4915
|
}
|
|
4092
4916
|
await Promise.all(scannerTasks);
|
|
4917
|
+
if (scanners?.architecture?.enabled !== false) {
|
|
4918
|
+
progress.startStep("architecture");
|
|
4919
|
+
extended.architecture = await scanArchitecture(
|
|
4920
|
+
rootDir,
|
|
4921
|
+
allProjects,
|
|
4922
|
+
extended.toolingInventory,
|
|
4923
|
+
extended.serviceDependencies,
|
|
4924
|
+
fileCache
|
|
4925
|
+
);
|
|
4926
|
+
const arch = extended.architecture;
|
|
4927
|
+
const layerCount = arch.layers.filter((l) => l.fileCount > 0).length;
|
|
4928
|
+
progress.completeStep(
|
|
4929
|
+
"architecture",
|
|
4930
|
+
`${arch.archetype} \xB7 ${layerCount} layer${layerCount !== 1 ? "s" : ""} \xB7 ${arch.totalClassified} files`,
|
|
4931
|
+
layerCount
|
|
4932
|
+
);
|
|
4933
|
+
}
|
|
4093
4934
|
}
|
|
4094
4935
|
progress.startStep("drift");
|
|
4095
4936
|
const drift = computeDriftScore(allProjects);
|
|
@@ -4106,6 +4947,7 @@ async function runScan(rootDir, opts) {
|
|
|
4106
4947
|
if (noteCount > 0) findingParts.push(`${noteCount} note${noteCount !== 1 ? "s" : ""}`);
|
|
4107
4948
|
progress.completeStep("findings", findingParts.join(", ") || "none");
|
|
4108
4949
|
progress.finish();
|
|
4950
|
+
fileCache.clear();
|
|
4109
4951
|
if (allProjects.length === 0) {
|
|
4110
4952
|
console.log(chalk5.yellow("No projects found."));
|
|
4111
4953
|
}
|
|
@@ -4122,7 +4964,7 @@ async function runScan(rootDir, opts) {
|
|
|
4122
4964
|
schemaVersion: "1.0",
|
|
4123
4965
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4124
4966
|
vibgrateVersion: VERSION,
|
|
4125
|
-
rootPath:
|
|
4967
|
+
rootPath: path15.basename(rootDir),
|
|
4126
4968
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
4127
4969
|
projects: allProjects,
|
|
4128
4970
|
drift,
|
|
@@ -4132,7 +4974,7 @@ async function runScan(rootDir, opts) {
|
|
|
4132
4974
|
filesScanned
|
|
4133
4975
|
};
|
|
4134
4976
|
if (opts.baseline) {
|
|
4135
|
-
const baselinePath =
|
|
4977
|
+
const baselinePath = path15.resolve(opts.baseline);
|
|
4136
4978
|
if (await pathExists(baselinePath)) {
|
|
4137
4979
|
try {
|
|
4138
4980
|
const baseline = await readJsonFile(baselinePath);
|
|
@@ -4143,15 +4985,15 @@ async function runScan(rootDir, opts) {
|
|
|
4143
4985
|
}
|
|
4144
4986
|
}
|
|
4145
4987
|
}
|
|
4146
|
-
const vibgrateDir =
|
|
4988
|
+
const vibgrateDir = path15.join(rootDir, ".vibgrate");
|
|
4147
4989
|
await ensureDir(vibgrateDir);
|
|
4148
|
-
await writeJsonFile(
|
|
4990
|
+
await writeJsonFile(path15.join(vibgrateDir, "scan_result.json"), artifact);
|
|
4149
4991
|
for (const project of allProjects) {
|
|
4150
4992
|
if (project.drift && project.path) {
|
|
4151
|
-
const projectDir =
|
|
4152
|
-
const projectVibgrateDir =
|
|
4993
|
+
const projectDir = path15.resolve(rootDir, project.path);
|
|
4994
|
+
const projectVibgrateDir = path15.join(projectDir, ".vibgrate");
|
|
4153
4995
|
await ensureDir(projectVibgrateDir);
|
|
4154
|
-
await writeJsonFile(
|
|
4996
|
+
await writeJsonFile(path15.join(projectVibgrateDir, "project_score.json"), {
|
|
4155
4997
|
projectId: project.projectId,
|
|
4156
4998
|
name: project.name,
|
|
4157
4999
|
type: project.type,
|
|
@@ -4168,7 +5010,7 @@ async function runScan(rootDir, opts) {
|
|
|
4168
5010
|
if (opts.format === "json") {
|
|
4169
5011
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
4170
5012
|
if (opts.out) {
|
|
4171
|
-
await writeTextFile(
|
|
5013
|
+
await writeTextFile(path15.resolve(opts.out), jsonStr);
|
|
4172
5014
|
console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
4173
5015
|
} else {
|
|
4174
5016
|
console.log(jsonStr);
|
|
@@ -4177,7 +5019,7 @@ async function runScan(rootDir, opts) {
|
|
|
4177
5019
|
const sarif = formatSarif(artifact);
|
|
4178
5020
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
4179
5021
|
if (opts.out) {
|
|
4180
|
-
await writeTextFile(
|
|
5022
|
+
await writeTextFile(path15.resolve(opts.out), sarifStr);
|
|
4181
5023
|
console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
4182
5024
|
} else {
|
|
4183
5025
|
console.log(sarifStr);
|
|
@@ -4186,7 +5028,7 @@ async function runScan(rootDir, opts) {
|
|
|
4186
5028
|
const text = formatText(artifact);
|
|
4187
5029
|
console.log(text);
|
|
4188
5030
|
if (opts.out) {
|
|
4189
|
-
await writeTextFile(
|
|
5031
|
+
await writeTextFile(path15.resolve(opts.out), text);
|
|
4190
5032
|
}
|
|
4191
5033
|
}
|
|
4192
5034
|
return artifact;
|
|
@@ -4245,7 +5087,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
4245
5087
|
}
|
|
4246
5088
|
}
|
|
4247
5089
|
var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").action(async (targetPath, opts) => {
|
|
4248
|
-
const rootDir =
|
|
5090
|
+
const rootDir = path15.resolve(targetPath);
|
|
4249
5091
|
if (!await pathExists(rootDir)) {
|
|
4250
5092
|
console.error(chalk5.red(`Path does not exist: ${rootDir}`));
|
|
4251
5093
|
process.exit(1);
|