@vibgrate/cli 2026.4.30 → 2026.6.5
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/baseline-FPO7HS7Y.js +9 -0
- package/dist/chunk-42GW43JE.js +97 -0
- package/dist/chunk-74ZJFYEM.js +1936 -0
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1674 -301
- package/dist/index.d.ts +1 -617
- package/dist/index.js +8 -6
- package/dist/semver-JBJZTHUX.js +5 -0
- package/package.json +8 -7
- package/dist/baseline-QZZXBT74.js +0 -10
- package/dist/chunk-2VJCLUTR.js +0 -31
- package/dist/chunk-JQHUH6A3.js +0 -684
- package/dist/chunk-XCIPC2J7.js +0 -12756
- package/dist/fs-D24ONFXR.js +0 -32
package/dist/cli.js
CHANGED
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
baselineCommand
|
|
4
|
-
} from "./chunk-2VJCLUTR.js";
|
|
5
|
-
import {
|
|
6
|
-
VERSION,
|
|
7
|
-
computeHmac,
|
|
8
|
-
dsnCommand,
|
|
9
|
-
formatMarkdown,
|
|
10
|
-
formatText,
|
|
11
|
-
parseDsn,
|
|
12
|
-
pushCommand,
|
|
13
|
-
scanCommand,
|
|
14
|
-
writeDefaultConfig
|
|
15
|
-
} from "./chunk-XCIPC2J7.js";
|
|
16
2
|
import {
|
|
17
3
|
Semaphore,
|
|
4
|
+
baselineCommand,
|
|
18
5
|
ensureDir,
|
|
19
6
|
pathExists,
|
|
20
7
|
readJsonFile,
|
|
21
8
|
writeTextFile
|
|
22
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-42GW43JE.js";
|
|
10
|
+
import {
|
|
11
|
+
require_semver
|
|
12
|
+
} from "./chunk-74ZJFYEM.js";
|
|
13
|
+
import {
|
|
14
|
+
__toESM
|
|
15
|
+
} from "./chunk-JSBRDJBE.js";
|
|
23
16
|
|
|
24
17
|
// src/cli.ts
|
|
25
|
-
import { Command as
|
|
26
|
-
import
|
|
18
|
+
import { Command as Command11 } from "commander";
|
|
19
|
+
import chalk12 from "chalk";
|
|
27
20
|
|
|
28
21
|
// src/commands/init.ts
|
|
29
22
|
import * as path from "path";
|
|
30
23
|
import { Command } from "commander";
|
|
31
24
|
import chalk from "chalk";
|
|
25
|
+
import { writeDefaultConfig } from "@vibgrate/core";
|
|
32
26
|
var initCommand = new Command("init").description("Initialize vibgrate in a project").argument("[path]", "Path to initialize", ".").option("--baseline", "Create initial baseline after init").option("--yes", "Skip confirmation prompts").action(async (targetPath, opts) => {
|
|
33
27
|
const rootDir = path.resolve(targetPath);
|
|
34
28
|
const vibgrateDir = path.join(rootDir, ".vibgrate");
|
|
@@ -42,7 +36,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
42
36
|
console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
|
|
43
37
|
}
|
|
44
38
|
if (opts.baseline) {
|
|
45
|
-
const { runBaseline } = await import("./baseline-
|
|
39
|
+
const { runBaseline } = await import("./baseline-FPO7HS7Y.js");
|
|
46
40
|
await runBaseline(rootDir);
|
|
47
41
|
}
|
|
48
42
|
console.log("");
|
|
@@ -52,15 +46,1044 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
52
46
|
console.log("");
|
|
53
47
|
});
|
|
54
48
|
|
|
55
|
-
// src/commands/
|
|
49
|
+
// src/commands/scan.ts
|
|
50
|
+
import * as path3 from "path";
|
|
51
|
+
import { Command as Command3 } from "commander";
|
|
52
|
+
import chalk3 from "chalk";
|
|
53
|
+
import {
|
|
54
|
+
runScan,
|
|
55
|
+
pathExists as pathExists2,
|
|
56
|
+
parseDsn,
|
|
57
|
+
prepareCompressedUpload,
|
|
58
|
+
fetchScanPreflight,
|
|
59
|
+
computeRepoFingerprint,
|
|
60
|
+
detectVcs,
|
|
61
|
+
resolveRepositoryName
|
|
62
|
+
} from "@vibgrate/core";
|
|
63
|
+
|
|
64
|
+
// src/version.ts
|
|
65
|
+
import { createRequire } from "module";
|
|
66
|
+
var require2 = createRequire(import.meta.url);
|
|
67
|
+
var pkg = require2("../package.json");
|
|
68
|
+
var VERSION = pkg.version;
|
|
69
|
+
|
|
70
|
+
// src/commands/dsn.ts
|
|
71
|
+
import * as crypto from "crypto";
|
|
56
72
|
import * as path2 from "path";
|
|
57
73
|
import { Command as Command2 } from "commander";
|
|
58
74
|
import chalk2 from "chalk";
|
|
59
|
-
var
|
|
60
|
-
|
|
75
|
+
var REGION_HOSTS = {
|
|
76
|
+
us: "us.ingest.vibgrate.com",
|
|
77
|
+
eu: "eu.ingest.vibgrate.com"
|
|
78
|
+
};
|
|
79
|
+
function resolveIngestHost(region, ingest) {
|
|
80
|
+
if (ingest) {
|
|
81
|
+
try {
|
|
82
|
+
return new URL(ingest).host;
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error(`Invalid ingest URL: ${ingest}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const r = (region ?? "us").toLowerCase();
|
|
88
|
+
const host = REGION_HOSTS[r];
|
|
89
|
+
if (!host) {
|
|
90
|
+
throw new Error(`Unknown region "${r}". Supported: ${Object.keys(REGION_HOSTS).join(", ")}`);
|
|
91
|
+
}
|
|
92
|
+
return host;
|
|
93
|
+
}
|
|
94
|
+
async function provisionDsn(keyId, secret, workspaceId, ingestHost) {
|
|
95
|
+
const url = `https://${ingestHost}/v1/provision`;
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"Connection": "close"
|
|
102
|
+
// Prevent keep-alive delays on exit
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({ keyId, secret, workspaceId })
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const result = await response.json();
|
|
108
|
+
return { success: false, error: result.error || `HTTP ${response.status}` };
|
|
109
|
+
}
|
|
110
|
+
return { success: true };
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
var dsnCommand = new Command2("dsn").description("Manage DSN tokens");
|
|
116
|
+
dsnCommand.command("create").description("Create a new DSN token").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", "Data residency region (us, eu)", "us").requiredOption("--workspace <id>", 'Workspace ID (use "new" to auto-generate)').option("--write <path>", "Write DSN to file").action(async (opts) => {
|
|
117
|
+
const keyId = crypto.randomBytes(12).toString("hex");
|
|
118
|
+
const secret = crypto.randomBytes(32).toString("hex");
|
|
119
|
+
let ingestHost;
|
|
120
|
+
try {
|
|
121
|
+
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error(chalk2.red(e instanceof Error ? e.message : String(e)));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
let workspaceId = opts.workspace;
|
|
127
|
+
const isNewWorkspace = opts.workspace.toLowerCase() === "new";
|
|
128
|
+
if (isNewWorkspace) {
|
|
129
|
+
workspaceId = crypto.randomBytes(8).toString("hex");
|
|
130
|
+
console.log(chalk2.dim(`Provisioning new workspace ${workspaceId}...`));
|
|
131
|
+
}
|
|
132
|
+
if (isNewWorkspace) {
|
|
133
|
+
const result = await provisionDsn(keyId, secret, workspaceId, ingestHost);
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
console.error(chalk2.red(`Failed to provision DSN: ${result.error}`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const dsn = `vibgrate+https://${keyId}:${secret}@${ingestHost}/${workspaceId}`;
|
|
140
|
+
console.log(chalk2.green("\u2714") + " DSN created");
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log(chalk2.bold("DSN:"));
|
|
143
|
+
console.log(` ${dsn}`);
|
|
144
|
+
console.log("");
|
|
145
|
+
console.log(chalk2.bold("Key ID:"));
|
|
146
|
+
console.log(` ${keyId}`);
|
|
147
|
+
if (isNewWorkspace) {
|
|
148
|
+
console.log("");
|
|
149
|
+
console.log(chalk2.bold("Workspace ID:"));
|
|
150
|
+
console.log(` ${workspaceId}`);
|
|
151
|
+
}
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(chalk2.dim("Set this as VIBGRATE_DSN in your CI environment."));
|
|
154
|
+
if (!isNewWorkspace) {
|
|
155
|
+
console.log(chalk2.dim("The secret must be registered on your Vibgrate ingest API."));
|
|
156
|
+
}
|
|
157
|
+
if (opts.write) {
|
|
158
|
+
const writePath = path2.resolve(opts.write);
|
|
159
|
+
await writeTextFile(writePath, dsn + "\n");
|
|
160
|
+
console.log("");
|
|
161
|
+
console.log(chalk2.green("\u2714") + ` DSN written to ${opts.write}`);
|
|
162
|
+
console.log(chalk2.yellow("\u26A0") + " Add this file to .gitignore!");
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// src/utils/ingest-id-output.ts
|
|
167
|
+
function emitIngestIdLine(ingestId, options) {
|
|
168
|
+
console.log(`VIBGRATE_INGEST_ID=${ingestId}`);
|
|
169
|
+
if (options?.unchanged) {
|
|
170
|
+
console.log("VIBGRATE_INGEST_UNCHANGED=1");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/commands/scan.ts
|
|
175
|
+
async function autoPush(artifact, rootDir, opts) {
|
|
176
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
177
|
+
if (!dsn) {
|
|
178
|
+
console.error(chalk3.red("No DSN provided for push."));
|
|
179
|
+
console.error(chalk3.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
|
|
180
|
+
if (opts.strict) process.exit(1);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const parsed = parseDsn(dsn);
|
|
184
|
+
if (!parsed) {
|
|
185
|
+
console.error(chalk3.red("Invalid DSN format."));
|
|
186
|
+
if (opts.strict) process.exit(1);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const { body, contentEncoding } = await prepareCompressedUpload(artifact);
|
|
190
|
+
const timestamp = String(Date.now());
|
|
191
|
+
let host = parsed.host;
|
|
192
|
+
if (opts.region) {
|
|
193
|
+
try {
|
|
194
|
+
host = resolveIngestHost(opts.region);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
|
|
197
|
+
if (opts.strict) process.exit(1);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
202
|
+
const originalSize = JSON.stringify(artifact).length;
|
|
203
|
+
const compressedSize = body.length;
|
|
204
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
205
|
+
console.log(chalk3.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
206
|
+
try {
|
|
207
|
+
const response = await fetch(url, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
"Content-Encoding": contentEncoding,
|
|
212
|
+
"X-Vibgrate-Timestamp": timestamp,
|
|
213
|
+
"Authorization": `VibgrateDSN ${parsed.keyId}:${parsed.secret}`,
|
|
214
|
+
"Connection": "close"
|
|
215
|
+
// Prevent keep-alive delays on exit
|
|
216
|
+
},
|
|
217
|
+
body
|
|
218
|
+
});
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const text = await response.text();
|
|
221
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
222
|
+
}
|
|
223
|
+
const result = await response.json();
|
|
224
|
+
if (result.unchanged) {
|
|
225
|
+
console.log(
|
|
226
|
+
chalk3.green("\u2714") + ` Repository unchanged since ${result.lastScannedAt ?? "last scan"} \u2014 skipped upload (no credit used).`
|
|
227
|
+
);
|
|
228
|
+
if (result.previousIngestId) {
|
|
229
|
+
emitIngestIdLine(result.previousIngestId, { unchanged: true });
|
|
230
|
+
const dashUrl = `https://dash.vibgrate.com/${parsed.workspaceId}/scan/${result.previousIngestId}`;
|
|
231
|
+
console.log(chalk3.dim(` Previous report: ${dashUrl}`));
|
|
232
|
+
} else if (opts.strict) {
|
|
233
|
+
console.error(chalk3.red("Repository unchanged but no previous ingest id returned."));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
console.log(chalk3.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
239
|
+
if (result.ingestId) {
|
|
240
|
+
emitIngestIdLine(result.ingestId);
|
|
241
|
+
const dashUrl = `https://dash.vibgrate.com/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
242
|
+
const CLEAR_LINE = process.platform === "win32" ? "\x1B[0G\x1B[2K" : "";
|
|
243
|
+
console.log("");
|
|
244
|
+
console.log(CLEAR_LINE + chalk3.dim(" Processing continues in the background. Results available shortly."));
|
|
245
|
+
console.log("");
|
|
246
|
+
console.log(CLEAR_LINE + chalk3.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
247
|
+
console.log(CLEAR_LINE + chalk3.bold(" \u{1F4CA} View Scan Report"));
|
|
248
|
+
console.log(CLEAR_LINE + " " + chalk3.underline.cyan(dashUrl));
|
|
249
|
+
console.log(CLEAR_LINE + chalk3.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
250
|
+
console.log("");
|
|
251
|
+
console.log("");
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {
|
|
254
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
255
|
+
console.error(chalk3.red(`Upload failed: ${msg}`));
|
|
256
|
+
if (opts.strict) process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function parseNonNegativeNumber(value, label) {
|
|
260
|
+
if (value === void 0) return void 0;
|
|
261
|
+
const parsed = Number(value);
|
|
262
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
263
|
+
throw new Error(`${label} must be a non-negative number.`);
|
|
264
|
+
}
|
|
265
|
+
return parsed;
|
|
266
|
+
}
|
|
267
|
+
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|md)", "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").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
|
|
268
|
+
const rootDir = path3.resolve(targetPath);
|
|
269
|
+
if (!await pathExists2(rootDir)) {
|
|
270
|
+
console.error(chalk3.red(`Path does not exist: ${rootDir}`));
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
|
|
274
|
+
const willPush = !opts.offline && (opts.push || hasDsn);
|
|
275
|
+
if (willPush && hasDsn) {
|
|
276
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
277
|
+
const parsed = parseDsn(dsn);
|
|
278
|
+
if (parsed) {
|
|
279
|
+
const ingestHost = opts.region ? resolveIngestHost(opts.region) : parsed.host;
|
|
280
|
+
const vcs = await detectVcs(rootDir);
|
|
281
|
+
const fingerprint = await computeRepoFingerprint(rootDir, vcs);
|
|
282
|
+
const repositoryName = await resolveRepositoryName(rootDir);
|
|
283
|
+
try {
|
|
284
|
+
const preflight = await fetchScanPreflight(parsed, ingestHost, {
|
|
285
|
+
repositoryName,
|
|
286
|
+
vcsSha: fingerprint.vcsSha
|
|
287
|
+
});
|
|
288
|
+
if (preflight.status === "error" || !preflight.scans.allowed) {
|
|
289
|
+
console.error(chalk3.red(preflight.error ?? "Scan ingestion not allowed for this workspace."));
|
|
290
|
+
console.error(
|
|
291
|
+
chalk3.dim(
|
|
292
|
+
`Credits: ${preflight.scans.used}/${preflight.scans.limit} (${preflight.plan.label} plan)`
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
if (opts.strict) process.exit(1);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
console.log(
|
|
299
|
+
chalk3.dim(
|
|
300
|
+
`Plan: ${preflight.plan.label} \u2014 scan credits ${preflight.scans.used}/${preflight.scans.limit} this month`
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
if (preflight.repository?.unchanged) {
|
|
304
|
+
console.log(
|
|
305
|
+
chalk3.green("\u2714") + ` Repository unchanged at ${preflight.repository.lastVcsSha?.slice(0, 7) ?? "same revision"} \u2014 skipping scan.`
|
|
306
|
+
);
|
|
307
|
+
if (preflight.repository.lastIngestId) {
|
|
308
|
+
emitIngestIdLine(preflight.repository.lastIngestId, { unchanged: true });
|
|
309
|
+
const dashUrl = `https://dash.vibgrate.com/${parsed.workspaceId}/scan/${preflight.repository.lastIngestId}`;
|
|
310
|
+
console.log(chalk3.dim(` Latest report: ${dashUrl}`));
|
|
311
|
+
} else if (opts.strict) {
|
|
312
|
+
console.error(chalk3.red("Repository unchanged but no previous ingest id available."));
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
319
|
+
console.error(chalk3.yellow(`Preflight check failed: ${msg}`));
|
|
320
|
+
if (opts.strict) process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const scanOpts = {
|
|
325
|
+
vibgrateVersion: VERSION,
|
|
326
|
+
out: opts.out,
|
|
327
|
+
format: opts.format || "text",
|
|
328
|
+
failOn: opts.failOn,
|
|
329
|
+
baseline: opts.baseline,
|
|
330
|
+
changedOnly: opts.changedOnly,
|
|
331
|
+
concurrency: parseInt(opts.concurrency, 10) || 8,
|
|
332
|
+
push: opts.push,
|
|
333
|
+
dsn: opts.dsn,
|
|
334
|
+
region: opts.region,
|
|
335
|
+
strict: opts.strict,
|
|
336
|
+
uiPurpose: opts.uiPurpose,
|
|
337
|
+
noLocalArtifacts: opts.noLocalArtifacts,
|
|
338
|
+
maxPrivacy: opts.maxPrivacy,
|
|
339
|
+
offline: opts.offline,
|
|
340
|
+
packageManifest: opts.packageManifest,
|
|
341
|
+
driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
|
|
342
|
+
driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening"),
|
|
343
|
+
projectScanTimeout: opts.projectScanTimeout ? parseInt(opts.projectScanTimeout, 10) || void 0 : void 0
|
|
344
|
+
};
|
|
345
|
+
const artifact = await runScan(rootDir, scanOpts);
|
|
346
|
+
if (opts.failOn) {
|
|
347
|
+
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
348
|
+
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
349
|
+
if (opts.failOn === "error" && hasErrors) {
|
|
350
|
+
console.error(chalk3.red(`
|
|
351
|
+
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
352
|
+
process.exit(2);
|
|
353
|
+
}
|
|
354
|
+
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
355
|
+
console.error(chalk3.red(`
|
|
356
|
+
Failing: findings detected at warn level or above.`));
|
|
357
|
+
process.exit(2);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
361
|
+
console.error(chalk3.red(`
|
|
362
|
+
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
363
|
+
process.exit(2);
|
|
364
|
+
}
|
|
365
|
+
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
366
|
+
if (artifact.delta === void 0) {
|
|
367
|
+
console.error(chalk3.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
368
|
+
process.exit(2);
|
|
369
|
+
}
|
|
370
|
+
if (artifact.delta > 0) {
|
|
371
|
+
const baselineScore = artifact.drift.score - artifact.delta;
|
|
372
|
+
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
373
|
+
const worseningPercent = artifact.delta / denominator * 100;
|
|
374
|
+
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
375
|
+
console.error(chalk3.red(`
|
|
376
|
+
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
377
|
+
process.exit(2);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (willPush) {
|
|
382
|
+
await autoPush(artifact, rootDir, scanOpts);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// src/commands/report.ts
|
|
387
|
+
import * as path4 from "path";
|
|
388
|
+
import { Command as Command4 } from "commander";
|
|
389
|
+
import chalk5 from "chalk";
|
|
390
|
+
|
|
391
|
+
// src/formatters/text.ts
|
|
392
|
+
import chalk4 from "chalk";
|
|
393
|
+
function formatText(artifact) {
|
|
394
|
+
const lines = [];
|
|
395
|
+
lines.push("");
|
|
396
|
+
lines.push(chalk4.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk4.greenBright("\u279C"));
|
|
397
|
+
lines.push(chalk4.cyan(" \u256D\u2524") + chalk4.greenBright("\u25C9 \u25C9") + chalk4.cyan("\u251C\u256E") + " " + chalk4.bold.white("V I B G R A T E"));
|
|
398
|
+
lines.push(chalk4.cyan(" \u2570\u2524") + chalk4.dim("\u2500\u2500\u2500") + chalk4.cyan("\u251C\u256F") + " " + chalk4.dim(`Drift Intelligence Engine v${VERSION}`));
|
|
399
|
+
lines.push(chalk4.cyan(" \u2570\u2500\u2500\u2500\u256F"));
|
|
400
|
+
lines.push("");
|
|
401
|
+
lines.push(chalk4.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"));
|
|
402
|
+
lines.push(chalk4.bold.cyan("\u2551 Vibgrate Drift Report \u2551"));
|
|
403
|
+
lines.push(chalk4.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"));
|
|
404
|
+
lines.push("");
|
|
405
|
+
for (const project of artifact.projects) {
|
|
406
|
+
lines.push(chalk4.bold(` \u2500\u2500 ${project.name} `) + chalk4.dim(`(${project.type}) ${project.path}`));
|
|
407
|
+
if (project.runtime) {
|
|
408
|
+
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? chalk4.yellow(` (${project.runtimeMajorsBehind} major${project.runtimeMajorsBehind > 1 ? "s" : ""} behind)`) : chalk4.green(" (current)");
|
|
409
|
+
lines.push(` Runtime: ${project.runtime}${behindStr}`);
|
|
410
|
+
}
|
|
411
|
+
if (project.targetFramework) {
|
|
412
|
+
lines.push(` Target: ${project.targetFramework}`);
|
|
413
|
+
}
|
|
414
|
+
if (project.frameworks.length > 0) {
|
|
415
|
+
lines.push(" Frameworks:");
|
|
416
|
+
for (const fw of project.frameworks) {
|
|
417
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? chalk4.green("current") : chalk4.yellow(`${fw.majorsBehind} behind`) : chalk4.dim("unknown");
|
|
418
|
+
lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const b = project.dependencyAgeBuckets;
|
|
422
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
423
|
+
if (total > 0) {
|
|
424
|
+
lines.push(" Dependencies:");
|
|
425
|
+
lines.push(` ${chalk4.green(`${b.current} current`)} ${chalk4.yellow(`${b.oneBehind} 1-behind`)} ${chalk4.red(`${b.twoPlusBehind} 2+ behind`)} ${chalk4.dim(`${b.unknown} unknown`)}`);
|
|
426
|
+
}
|
|
427
|
+
lines.push("");
|
|
428
|
+
}
|
|
429
|
+
if (artifact.delta !== void 0) {
|
|
430
|
+
const deltaStr = artifact.delta > 0 ? chalk4.green(`+${artifact.delta}`) : artifact.delta < 0 ? chalk4.red(`${artifact.delta}`) : chalk4.dim("0");
|
|
431
|
+
lines.push(chalk4.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
|
|
432
|
+
lines.push("");
|
|
433
|
+
}
|
|
434
|
+
if (artifact.extended) {
|
|
435
|
+
lines.push(...formatExtended(artifact.extended));
|
|
436
|
+
}
|
|
437
|
+
if (artifact.findings.length > 0) {
|
|
438
|
+
const errors = artifact.findings.filter((f) => f.level === "error");
|
|
439
|
+
const warnings = artifact.findings.filter((f) => f.level === "warning");
|
|
440
|
+
const notes = artifact.findings.filter((f) => f.level === "note");
|
|
441
|
+
const summary = [
|
|
442
|
+
errors.length > 0 ? chalk4.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
|
|
443
|
+
warnings.length > 0 ? chalk4.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
|
|
444
|
+
notes.length > 0 ? chalk4.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
|
|
445
|
+
].filter(Boolean).join(chalk4.dim(", "));
|
|
446
|
+
lines.push(chalk4.bold.underline(` Findings`) + chalk4.dim(` (${summary})`));
|
|
447
|
+
for (const f of artifact.findings) {
|
|
448
|
+
const icon = f.level === "error" ? chalk4.red("\u2716") : f.level === "warning" ? chalk4.yellow("\u26A0") : chalk4.blue("\u2139");
|
|
449
|
+
lines.push(` ${icon} ${f.message}`);
|
|
450
|
+
lines.push(chalk4.dim(` ${f.ruleId} in ${f.location}`));
|
|
451
|
+
}
|
|
452
|
+
lines.push("");
|
|
453
|
+
}
|
|
454
|
+
const actions = generatePriorityActions(artifact);
|
|
455
|
+
if (actions.length > 0) {
|
|
456
|
+
lines.push(chalk4.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"));
|
|
457
|
+
lines.push(chalk4.bold.cyan("\u2551 Top Priority Actions \u2551"));
|
|
458
|
+
lines.push(chalk4.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"));
|
|
459
|
+
lines.push("");
|
|
460
|
+
for (let i = 0; i < actions.length; i++) {
|
|
461
|
+
const a = actions[i];
|
|
462
|
+
const num = chalk4.bold.cyan(` ${i + 1}.`);
|
|
463
|
+
lines.push(`${num} ${chalk4.bold(a.title)}`);
|
|
464
|
+
lines.push(chalk4.dim(` ${a.explanation}`));
|
|
465
|
+
if (a.impact) lines.push(` Impact: ${chalk4.green(a.impact)}`);
|
|
466
|
+
lines.push("");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (artifact.extended?.architecture) {
|
|
470
|
+
lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
|
|
471
|
+
}
|
|
472
|
+
if (artifact.solutions && artifact.solutions.length > 0) {
|
|
473
|
+
lines.push(chalk4.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"));
|
|
474
|
+
lines.push(chalk4.bold.cyan("\u2551 Solution Drift Summary \u2551"));
|
|
475
|
+
lines.push(chalk4.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"));
|
|
476
|
+
lines.push("");
|
|
477
|
+
for (const solution of artifact.solutions) {
|
|
478
|
+
const solScore = solution.drift?.score;
|
|
479
|
+
const color = typeof solScore === "number" ? solScore >= 70 ? chalk4.green : solScore >= 40 ? chalk4.yellow : chalk4.red : chalk4.dim;
|
|
480
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk4.dim("n/a")}`);
|
|
481
|
+
}
|
|
482
|
+
lines.push("");
|
|
483
|
+
}
|
|
484
|
+
const scoreColor = artifact.drift.score >= 70 ? chalk4.green : artifact.drift.score >= 40 ? chalk4.yellow : chalk4.red;
|
|
485
|
+
lines.push(chalk4.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"));
|
|
486
|
+
lines.push(chalk4.bold.cyan("\u2551 Drift Score Summary \u2551"));
|
|
487
|
+
lines.push(chalk4.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"));
|
|
488
|
+
lines.push("");
|
|
489
|
+
lines.push(chalk4.bold(" Drift Score: ") + scoreColor.bold(`${artifact.drift.score}/100`));
|
|
490
|
+
lines.push(chalk4.bold(" Risk Level: ") + riskBadge(artifact.drift.riskLevel));
|
|
491
|
+
lines.push(chalk4.bold(" Projects: ") + `${artifact.projects.length}`);
|
|
492
|
+
if (artifact.vcs) {
|
|
493
|
+
const vcsParts = [artifact.vcs.type];
|
|
494
|
+
if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
|
|
495
|
+
if (artifact.vcs.shortSha) vcsParts.push(chalk4.dim(artifact.vcs.shortSha));
|
|
496
|
+
lines.push(chalk4.bold(" VCS: ") + vcsParts.join(" "));
|
|
497
|
+
}
|
|
498
|
+
lines.push("");
|
|
499
|
+
const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
|
|
500
|
+
lines.push(" " + chalk4.bold.underline("Score Breakdown"));
|
|
501
|
+
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk4.dim("n/a")}`);
|
|
502
|
+
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk4.dim("n/a")}`);
|
|
503
|
+
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk4.dim("n/a")}`);
|
|
504
|
+
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk4.dim("n/a")}`);
|
|
505
|
+
lines.push("");
|
|
506
|
+
const scannedParts = [`Scanned at ${artifact.timestamp}`];
|
|
507
|
+
if (artifact.durationMs !== void 0) {
|
|
508
|
+
const secs = (artifact.durationMs / 1e3).toFixed(1);
|
|
509
|
+
scannedParts.push(`${secs}s`);
|
|
510
|
+
}
|
|
511
|
+
if (artifact.filesScanned !== void 0) {
|
|
512
|
+
scannedParts.push(`${artifact.filesScanned} file${artifact.filesScanned !== 1 ? "s" : ""} scanned`);
|
|
513
|
+
}
|
|
514
|
+
if (artifact.treeSummary) {
|
|
515
|
+
scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
|
|
516
|
+
scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
517
|
+
}
|
|
518
|
+
lines.push(chalk4.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
519
|
+
lines.push("");
|
|
520
|
+
return lines.join("\n");
|
|
521
|
+
}
|
|
522
|
+
function riskBadge(level) {
|
|
523
|
+
switch (level) {
|
|
524
|
+
case "low":
|
|
525
|
+
return chalk4.bgGreen.black(" LOW ");
|
|
526
|
+
case "moderate":
|
|
527
|
+
return chalk4.bgYellow.black(" MODERATE ");
|
|
528
|
+
case "high":
|
|
529
|
+
return chalk4.bgRed.white(" HIGH ");
|
|
530
|
+
default:
|
|
531
|
+
return level;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function scoreBar(score) {
|
|
535
|
+
const width = 20;
|
|
536
|
+
const filled = Math.round(score / 100 * width);
|
|
537
|
+
const empty = width - filled;
|
|
538
|
+
const color = score >= 70 ? chalk4.green : score >= 40 ? chalk4.yellow : chalk4.red;
|
|
539
|
+
return color("\u2588".repeat(filled)) + chalk4.dim("\u2591".repeat(empty)) + ` ${Math.round(score)}`;
|
|
540
|
+
}
|
|
541
|
+
var CATEGORY_LABELS = {
|
|
542
|
+
frontend: "Frontend",
|
|
543
|
+
metaFrameworks: "Meta-frameworks",
|
|
544
|
+
bundlers: "Bundlers",
|
|
545
|
+
css: "CSS / UI",
|
|
546
|
+
backend: "Backend",
|
|
547
|
+
orm: "ORM / Database",
|
|
548
|
+
testing: "Testing",
|
|
549
|
+
lintFormat: "Lint & Format",
|
|
550
|
+
apiMessaging: "API & Messaging",
|
|
551
|
+
observability: "Observability",
|
|
552
|
+
payment: "Payment",
|
|
553
|
+
auth: "Auth",
|
|
554
|
+
email: "Email",
|
|
555
|
+
cloud: "Cloud",
|
|
556
|
+
databases: "Databases",
|
|
557
|
+
messaging: "Messaging",
|
|
558
|
+
crm: "CRM & Comms",
|
|
559
|
+
storage: "Storage",
|
|
560
|
+
search: "Search & AI"
|
|
561
|
+
};
|
|
562
|
+
function formatExtended(ext) {
|
|
563
|
+
const lines = [];
|
|
564
|
+
if (ext.toolingInventory) {
|
|
565
|
+
const inv = ext.toolingInventory;
|
|
566
|
+
const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
|
|
567
|
+
if (categories.length > 0) {
|
|
568
|
+
lines.push(chalk4.bold.underline(" Tech Stack"));
|
|
569
|
+
for (const [cat, items] of categories) {
|
|
570
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
571
|
+
const names = items.map((i) => chalk4.white(i.name)).join(chalk4.dim(", "));
|
|
572
|
+
lines.push(` ${chalk4.cyan(label)}: ${names}`);
|
|
573
|
+
}
|
|
574
|
+
lines.push("");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (ext.serviceDependencies) {
|
|
578
|
+
const svc = ext.serviceDependencies;
|
|
579
|
+
const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
|
|
580
|
+
if (categories.length > 0) {
|
|
581
|
+
lines.push(chalk4.bold.underline(" Services & Integrations"));
|
|
582
|
+
for (const [cat, items] of categories) {
|
|
583
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
584
|
+
const names = items.map((i) => {
|
|
585
|
+
const ver = i.version ? chalk4.dim(` ${i.version}`) : "";
|
|
586
|
+
return chalk4.white(i.name) + ver;
|
|
587
|
+
}).join(chalk4.dim(", "));
|
|
588
|
+
lines.push(` ${chalk4.cyan(label)}: ${names}`);
|
|
589
|
+
}
|
|
590
|
+
lines.push("");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (ext.breakingChangeExposure) {
|
|
594
|
+
const bc = ext.breakingChangeExposure;
|
|
595
|
+
if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
|
|
596
|
+
lines.push(chalk4.bold.underline(" Breaking Change Exposure"));
|
|
597
|
+
const exposureColor = bc.exposureScore >= 40 ? chalk4.red : bc.exposureScore >= 20 ? chalk4.yellow : chalk4.green;
|
|
598
|
+
lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
|
|
599
|
+
if (bc.deprecatedPackages.length > 0) {
|
|
600
|
+
lines.push(` ${chalk4.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk4.dim(p)).join(", ")}`);
|
|
601
|
+
}
|
|
602
|
+
if (bc.legacyPolyfills.length > 0) {
|
|
603
|
+
lines.push(` ${chalk4.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk4.dim(p)).join(", ")}`);
|
|
604
|
+
}
|
|
605
|
+
if (bc.peerConflictsDetected) {
|
|
606
|
+
lines.push(` ${chalk4.red("\u26A0")} Peer dependency conflicts detected`);
|
|
607
|
+
}
|
|
608
|
+
lines.push(` Recommendation: ${chalk4.bold(bc.overallRecommendation)}`);
|
|
609
|
+
const projectsWithPlans = bc.projectIntelligence.filter((p) => p.packages.length > 0).slice(0, 3);
|
|
610
|
+
if (projectsWithPlans.length > 0) {
|
|
611
|
+
lines.push(" Major Upgrade Intelligence:");
|
|
612
|
+
for (const p of projectsWithPlans) {
|
|
613
|
+
lines.push(` - ${p.project} (${p.recommendation})`);
|
|
614
|
+
for (const pkg2 of p.packages.slice(0, 2)) {
|
|
615
|
+
lines.push(` \xB7 ${pkg2.package} ${pkg2.currentVersion ?? "?"} \u2192 ${pkg2.targetVersion ?? "?"} | touched ~${pkg2.usage.touchedPercent}% | ${pkg2.automatable}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
lines.push("");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
623
|
+
const ts = ext.tsModernity;
|
|
624
|
+
lines.push(chalk4.bold.underline(" TypeScript"));
|
|
625
|
+
const parts = [];
|
|
626
|
+
parts.push(`v${ts.typescriptVersion}`);
|
|
627
|
+
if (ts.strict === true) parts.push(chalk4.green("strict \u2714"));
|
|
628
|
+
else if (ts.strict === false) parts.push(chalk4.yellow("strict \u2716"));
|
|
629
|
+
if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
|
|
630
|
+
if (ts.target) parts.push(`target: ${ts.target}`);
|
|
631
|
+
lines.push(` ${parts.join(chalk4.dim(" \xB7 "))}`);
|
|
632
|
+
lines.push("");
|
|
633
|
+
}
|
|
634
|
+
if (ext.buildDeploy) {
|
|
635
|
+
const bd = ext.buildDeploy;
|
|
636
|
+
const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
|
|
637
|
+
if (hasSomething) {
|
|
638
|
+
lines.push(chalk4.bold.underline(" Build & Deploy"));
|
|
639
|
+
if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
|
|
640
|
+
if (bd.docker.dockerfileCount > 0) {
|
|
641
|
+
lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
|
|
642
|
+
}
|
|
643
|
+
if (bd.packageManagers.length > 0) lines.push(` Package Managers: ${bd.packageManagers.join(", ")}`);
|
|
644
|
+
if (bd.monorepoTools.length > 0) lines.push(` Monorepo: ${bd.monorepoTools.join(", ")}`);
|
|
645
|
+
if (bd.iac.length > 0) lines.push(` IaC: ${bd.iac.join(", ")}`);
|
|
646
|
+
lines.push("");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (ext.uiPurpose) {
|
|
650
|
+
const up = ext.uiPurpose;
|
|
651
|
+
lines.push(chalk4.bold.underline(" Product Purpose Signals"));
|
|
652
|
+
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk4.dim("unknown")}`);
|
|
653
|
+
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk4.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
|
|
654
|
+
const top = up.topEvidence.slice(0, 8);
|
|
655
|
+
if (top.length > 0) {
|
|
656
|
+
lines.push(" Top Signals:");
|
|
657
|
+
for (const item of top) {
|
|
658
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk4.dim(`(${item.file})`)}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (up.unknownSignals.length > 0) {
|
|
662
|
+
lines.push(" Unknowns:");
|
|
663
|
+
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
664
|
+
lines.push(` - ${chalk4.yellow(u)}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
lines.push("");
|
|
668
|
+
}
|
|
669
|
+
if (ext.securityPosture) {
|
|
670
|
+
const sec = ext.securityPosture;
|
|
671
|
+
lines.push(chalk4.bold.underline(" Security Posture"));
|
|
672
|
+
const checks = [];
|
|
673
|
+
checks.push(sec.lockfilePresent ? chalk4.green("Lockfile \u2714") : chalk4.red("Lockfile \u2716"));
|
|
674
|
+
checks.push(sec.gitignoreCoversEnv ? chalk4.green(".env \u2714") : chalk4.red(".env \u2716"));
|
|
675
|
+
checks.push(sec.gitignoreCoversNodeModules ? chalk4.green("node_modules \u2714") : chalk4.yellow("node_modules \u2716"));
|
|
676
|
+
if (sec.multipleLockfileTypes) checks.push(chalk4.yellow("Multiple lockfiles \u26A0"));
|
|
677
|
+
if (sec.envFilesTracked) checks.push(chalk4.red("Env files tracked \u2716"));
|
|
678
|
+
lines.push(` ${checks.join(chalk4.dim(" \xB7 "))}`);
|
|
679
|
+
lines.push("");
|
|
680
|
+
}
|
|
681
|
+
if (ext.platformMatrix) {
|
|
682
|
+
const pm = ext.platformMatrix;
|
|
683
|
+
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
684
|
+
lines.push(chalk4.bold.underline(" Platform"));
|
|
685
|
+
if (pm.nativeModules.length > 0) {
|
|
686
|
+
lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk4.dim(m)).join(", ")}`);
|
|
687
|
+
}
|
|
688
|
+
if (pm.osAssumptions.length > 0) {
|
|
689
|
+
lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
|
|
690
|
+
}
|
|
691
|
+
lines.push("");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (ext.codeQuality) {
|
|
695
|
+
const cq = ext.codeQuality;
|
|
696
|
+
lines.push(chalk4.bold.underline(" Code Quality"));
|
|
697
|
+
lines.push(` Files: ${chalk4.white(`${cq.filesAnalyzed}`)} \xB7 Functions: ${chalk4.white(`${cq.functionsAnalyzed}`)} \xB7 Avg complexity: ${chalk4.white(`${cq.avgCyclomaticComplexity}`)} \xB7 Avg length: ${chalk4.white(`${cq.avgFunctionLength}`)} lines`);
|
|
698
|
+
lines.push(` Max nesting: ${cq.maxNestingDepth} \xB7 Circular deps: ${cq.circularDependencies} \xB7 Dead code: ${cq.deadCodePercent}%`);
|
|
699
|
+
if (cq.godFiles.length > 0) {
|
|
700
|
+
const preview = cq.godFiles.slice(0, 3).map((f) => `${f.path} (${f.lines} lines)`).join(", ");
|
|
701
|
+
lines.push(` ${chalk4.yellow("God files")}: ${preview}`);
|
|
702
|
+
}
|
|
703
|
+
lines.push("");
|
|
704
|
+
}
|
|
705
|
+
if (ext.dependencyGraph) {
|
|
706
|
+
const dg = ext.dependencyGraph;
|
|
707
|
+
if (dg.lockfileType) {
|
|
708
|
+
lines.push(chalk4.bold.underline(" Dependency Graph"));
|
|
709
|
+
lines.push(` ${dg.lockfileType}: ${chalk4.white(`${dg.totalUnique}`)} unique, ${chalk4.white(`${dg.totalInstalled}`)} installed`);
|
|
710
|
+
if (dg.duplicatedPackages.length > 0) {
|
|
711
|
+
lines.push(` ${chalk4.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
|
|
712
|
+
}
|
|
713
|
+
if (dg.phantomDependencies.length > 0) {
|
|
714
|
+
lines.push(` ${chalk4.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
|
|
715
|
+
}
|
|
716
|
+
lines.push("");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return lines;
|
|
720
|
+
}
|
|
721
|
+
function formatArchitectureDiagram(arch) {
|
|
722
|
+
const lines = [];
|
|
723
|
+
lines.push(chalk4.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"));
|
|
724
|
+
lines.push(chalk4.bold.cyan("\u2551 Architecture Layers \u2551"));
|
|
725
|
+
lines.push(chalk4.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"));
|
|
726
|
+
lines.push("");
|
|
727
|
+
lines.push(chalk4.bold(" Archetype: ") + `${arch.archetype}` + chalk4.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
|
|
728
|
+
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk4.dim(` (${arch.unclassified} unclassified)`) : ""));
|
|
729
|
+
lines.push("");
|
|
730
|
+
if (arch.layers.length > 0) {
|
|
731
|
+
for (const layer of arch.layers) {
|
|
732
|
+
const risk = layer.riskLevel === "none" ? chalk4.dim("none") : layer.riskLevel === "low" ? chalk4.green("low") : layer.riskLevel === "moderate" ? chalk4.yellow("moderate") : chalk4.red("high");
|
|
733
|
+
lines.push(` ${chalk4.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
|
|
734
|
+
}
|
|
735
|
+
lines.push("");
|
|
736
|
+
}
|
|
737
|
+
return lines;
|
|
738
|
+
}
|
|
739
|
+
function generatePriorityActions(artifact) {
|
|
740
|
+
const actions = [];
|
|
741
|
+
const eolProjects = artifact.projects.filter(
|
|
742
|
+
(p) => p.runtimeMajorsBehind !== void 0 && p.runtimeMajorsBehind >= 3
|
|
743
|
+
);
|
|
744
|
+
if (eolProjects.length > 0) {
|
|
745
|
+
const names = eolProjects.map((p) => p.name).join(", ");
|
|
746
|
+
let detail = `End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`;
|
|
747
|
+
const fileLines = [];
|
|
748
|
+
for (const p of eolProjects) {
|
|
749
|
+
fileLines.push(`
|
|
750
|
+
./${p.path}`);
|
|
751
|
+
fileLines.push(`
|
|
752
|
+
${p.runtime} \u2192 ${p.runtimeLatest} (${p.runtimeMajorsBehind} major${p.runtimeMajorsBehind > 1 ? "s" : ""} behind)`);
|
|
753
|
+
}
|
|
754
|
+
detail += fileLines.join("");
|
|
755
|
+
actions.push({
|
|
756
|
+
title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
|
|
757
|
+
explanation: detail,
|
|
758
|
+
impact: `+${Math.min(eolProjects.length * 10, 30)} points (runtime & EOL scores)`,
|
|
759
|
+
severity: 100
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
const severeFrameworks = [];
|
|
763
|
+
for (const p of artifact.projects) {
|
|
764
|
+
for (const fw of p.frameworks) {
|
|
765
|
+
if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
|
|
766
|
+
severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name, projectPath: p.path });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (severeFrameworks.length > 0) {
|
|
771
|
+
const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
|
|
772
|
+
const others = severeFrameworks.length > 1 ? ` (+${severeFrameworks.length - 1} more)` : "";
|
|
773
|
+
let detail = `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`;
|
|
774
|
+
const fileLines = [];
|
|
775
|
+
let shown = 0;
|
|
776
|
+
for (const sf of severeFrameworks) {
|
|
777
|
+
if (shown >= 8) break;
|
|
778
|
+
fileLines.push(`
|
|
779
|
+
./${sf.projectPath}`);
|
|
780
|
+
fileLines.push(`
|
|
781
|
+
${sf.name}: ${sf.fw} (${sf.behind} major${sf.behind > 1 ? "s" : ""} behind)`);
|
|
782
|
+
shown++;
|
|
783
|
+
}
|
|
784
|
+
const remaining = severeFrameworks.length - shown;
|
|
785
|
+
detail += fileLines.join("");
|
|
786
|
+
if (remaining > 0) detail += `
|
|
787
|
+
... and ${remaining} more`;
|
|
788
|
+
actions.push({
|
|
789
|
+
title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
|
|
790
|
+
explanation: detail,
|
|
791
|
+
impact: `+5\u201315 points`,
|
|
792
|
+
severity: 90
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
for (const p of artifact.projects) {
|
|
796
|
+
const b = p.dependencyAgeBuckets;
|
|
797
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind;
|
|
798
|
+
if (total === 0) continue;
|
|
799
|
+
const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
|
|
800
|
+
if (twoPlusPct >= 40) {
|
|
801
|
+
let detail = `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`;
|
|
802
|
+
const worstDeps = p.dependencies.filter((d) => d.majorsBehind !== null && d.majorsBehind >= 2).sort((a, b2) => (b2.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
|
|
803
|
+
if (worstDeps.length > 0) {
|
|
804
|
+
const depLines = [];
|
|
805
|
+
let shown = 0;
|
|
806
|
+
depLines.push(`
|
|
807
|
+
./${p.path}`);
|
|
808
|
+
for (const dep of worstDeps) {
|
|
809
|
+
if (shown >= 8) break;
|
|
810
|
+
const current = dep.resolvedVersion ?? dep.currentSpec;
|
|
811
|
+
const latest = dep.latestStable ?? "?";
|
|
812
|
+
depLines.push(`
|
|
813
|
+
${dep.package}: ${current} \u2192 ${latest} (${dep.majorsBehind} major${dep.majorsBehind > 1 ? "s" : ""} behind)`);
|
|
814
|
+
shown++;
|
|
815
|
+
}
|
|
816
|
+
const remaining = worstDeps.length - shown;
|
|
817
|
+
detail += depLines.join("");
|
|
818
|
+
if (remaining > 0) detail += `
|
|
819
|
+
... and ${remaining} more`;
|
|
820
|
+
}
|
|
821
|
+
actions.push({
|
|
822
|
+
title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
|
|
823
|
+
explanation: detail,
|
|
824
|
+
impact: `+5\u201310 points`,
|
|
825
|
+
severity: 80 + twoPlusPct / 10
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const twoMajorFrameworks = [];
|
|
830
|
+
for (const p of artifact.projects) {
|
|
831
|
+
for (const fw of p.frameworks) {
|
|
832
|
+
if (fw.majorsBehind === 2) {
|
|
833
|
+
twoMajorFrameworks.push({ name: fw.name, project: p.name, projectPath: p.path, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const uniqueTwo = [...new Map(twoMajorFrameworks.map((f) => [f.name, f])).values()];
|
|
838
|
+
if (uniqueTwo.length > 0) {
|
|
839
|
+
const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
|
|
840
|
+
const moreCount = uniqueTwo.length > 3 ? ` +${uniqueTwo.length - 3} more` : "";
|
|
841
|
+
let detail = `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`;
|
|
842
|
+
const fileLines = [];
|
|
843
|
+
let shown = 0;
|
|
844
|
+
for (const tf of twoMajorFrameworks) {
|
|
845
|
+
if (shown >= 8) break;
|
|
846
|
+
fileLines.push(`
|
|
847
|
+
./${tf.projectPath}`);
|
|
848
|
+
fileLines.push(`
|
|
849
|
+
${tf.name}: ${tf.fw}`);
|
|
850
|
+
shown++;
|
|
851
|
+
}
|
|
852
|
+
const remaining = twoMajorFrameworks.length - shown;
|
|
853
|
+
detail += fileLines.join("");
|
|
854
|
+
if (remaining > 0) detail += `
|
|
855
|
+
... and ${remaining} more`;
|
|
856
|
+
actions.push({
|
|
857
|
+
title: `Plan major framework upgrades: ${list}${moreCount}`,
|
|
858
|
+
explanation: detail,
|
|
859
|
+
impact: `+5\u201310 points`,
|
|
860
|
+
severity: 60
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
if (artifact.extended?.breakingChangeExposure) {
|
|
864
|
+
const bc = artifact.extended.breakingChangeExposure;
|
|
865
|
+
const total = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
|
|
866
|
+
if (total > 0) {
|
|
867
|
+
const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
|
|
868
|
+
const moreCount = total > 5 ? ` +${total - 5} more` : "";
|
|
869
|
+
let detail = `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`;
|
|
870
|
+
const allPkgNames = /* @__PURE__ */ new Set([...bc.deprecatedPackages, ...bc.legacyPolyfills]);
|
|
871
|
+
const fileLines = [];
|
|
872
|
+
let shown = 0;
|
|
873
|
+
for (const p of artifact.projects) {
|
|
874
|
+
const matches = p.dependencies.filter((d) => allPkgNames.has(d.package));
|
|
875
|
+
if (matches.length === 0) continue;
|
|
876
|
+
if (shown >= 10) break;
|
|
877
|
+
fileLines.push(`
|
|
878
|
+
./${p.path}`);
|
|
879
|
+
for (const dep of matches) {
|
|
880
|
+
if (shown >= 10) break;
|
|
881
|
+
const ver = dep.resolvedVersion ?? dep.currentSpec;
|
|
882
|
+
const label = bc.deprecatedPackages.includes(dep.package) ? "deprecated" : "polyfill";
|
|
883
|
+
fileLines.push(`
|
|
884
|
+
${dep.package}: ${ver} (${label})`);
|
|
885
|
+
shown++;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const remaining = total - shown;
|
|
889
|
+
detail += fileLines.join("");
|
|
890
|
+
if (remaining > 0) detail += `
|
|
891
|
+
... and ${remaining} more`;
|
|
892
|
+
actions.push({
|
|
893
|
+
title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
|
|
894
|
+
explanation: detail,
|
|
895
|
+
severity: 55
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (artifact.extended?.dependencyGraph) {
|
|
900
|
+
const dg = artifact.extended.dependencyGraph;
|
|
901
|
+
const phantomCount = dg.phantomDependencies.length;
|
|
902
|
+
if (phantomCount >= 10) {
|
|
903
|
+
let detail = `Packages used in code but not declared in package.json. These rely on transitive installs and can break unpredictably when other packages update.`;
|
|
904
|
+
const details = dg.phantomDependencyDetails;
|
|
905
|
+
if (details && details.length > 0) {
|
|
906
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
907
|
+
for (const d of details) {
|
|
908
|
+
if (!byPath.has(d.sourcePath)) byPath.set(d.sourcePath, []);
|
|
909
|
+
byPath.get(d.sourcePath).push({ package: d.package, spec: d.spec });
|
|
910
|
+
}
|
|
911
|
+
const pathLines = [];
|
|
912
|
+
let shown = 0;
|
|
913
|
+
for (const [srcPath, pkgs] of byPath) {
|
|
914
|
+
if (shown >= 10) break;
|
|
915
|
+
pathLines.push(`
|
|
916
|
+
./${srcPath}`);
|
|
917
|
+
for (const pkg2 of pkgs) {
|
|
918
|
+
if (shown >= 10) break;
|
|
919
|
+
pathLines.push(`
|
|
920
|
+
${pkg2.package}: ${pkg2.spec}`);
|
|
921
|
+
shown++;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const remaining = phantomCount - shown;
|
|
925
|
+
detail += pathLines.join("");
|
|
926
|
+
if (remaining > 0) detail += `
|
|
927
|
+
... and ${remaining} more`;
|
|
928
|
+
}
|
|
929
|
+
actions.push({
|
|
930
|
+
title: `Fix ${phantomCount} phantom dependencies`,
|
|
931
|
+
explanation: detail,
|
|
932
|
+
severity: 45
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (artifact.extended?.securityPosture) {
|
|
937
|
+
const sec = artifact.extended.securityPosture;
|
|
938
|
+
if (sec.envFilesTracked || !sec.lockfilePresent) {
|
|
939
|
+
const issues = [];
|
|
940
|
+
if (sec.envFilesTracked) issues.push(".env files are tracked in git");
|
|
941
|
+
if (!sec.lockfilePresent) issues.push("no lockfile found");
|
|
942
|
+
let detail;
|
|
943
|
+
if (sec.envFilesTracked) {
|
|
944
|
+
detail = "Environment files may contain secrets. Add them to .gitignore and rotate any exposed credentials immediately.";
|
|
945
|
+
detail += "\n ./.gitignore";
|
|
946
|
+
detail += "\n Add: .env, .env.*, .env.local";
|
|
947
|
+
} else {
|
|
948
|
+
detail = "Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.";
|
|
949
|
+
detail += "\n ./";
|
|
950
|
+
detail += `
|
|
951
|
+
Missing: ${sec.lockfileTypes.length > 0 ? sec.lockfileTypes.join(", ") + " (multiple types detected)" : "package-lock.json, pnpm-lock.yaml, or yarn.lock"}`;
|
|
952
|
+
}
|
|
953
|
+
actions.push({
|
|
954
|
+
title: `Fix security posture: ${issues.join(", ")}`,
|
|
955
|
+
explanation: detail,
|
|
956
|
+
severity: 95
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (artifact.extended?.dependencyGraph) {
|
|
961
|
+
const dupes = artifact.extended.dependencyGraph.duplicatedPackages;
|
|
962
|
+
const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
|
|
963
|
+
if (highImpactDupes.length >= 3) {
|
|
964
|
+
const names = highImpactDupes.slice(0, 4).map((d) => `${d.name} (${d.versions.length}v)`).join(", ");
|
|
965
|
+
let detail = `${highImpactDupes.length} packages have 3+ versions installed. Run \`npm dedupe\` to reduce bundle size and install time.`;
|
|
966
|
+
const dupeLines = [];
|
|
967
|
+
let shown = 0;
|
|
968
|
+
for (const d of highImpactDupes) {
|
|
969
|
+
if (shown >= 8) break;
|
|
970
|
+
dupeLines.push(`
|
|
971
|
+
${d.name}: ${d.versions.join(", ")} (${d.consumers} consumer${d.consumers !== 1 ? "s" : ""})`);
|
|
972
|
+
shown++;
|
|
973
|
+
}
|
|
974
|
+
const remaining = highImpactDupes.length - shown;
|
|
975
|
+
detail += dupeLines.join("");
|
|
976
|
+
if (remaining > 0) detail += `
|
|
977
|
+
... and ${remaining} more`;
|
|
978
|
+
actions.push({
|
|
979
|
+
title: `Deduplicate heavily-versioned packages`,
|
|
980
|
+
explanation: detail,
|
|
981
|
+
severity: 35
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
actions.sort((a, b) => b.severity - a.severity);
|
|
986
|
+
return actions.slice(0, 5);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/formatters/markdown.ts
|
|
990
|
+
function formatMarkdown(artifact) {
|
|
991
|
+
const lines = [];
|
|
992
|
+
lines.push("# Vibgrate Drift Report");
|
|
993
|
+
lines.push("");
|
|
994
|
+
lines.push(`| Metric | Value |`);
|
|
995
|
+
lines.push(`|--------|-------|`);
|
|
996
|
+
lines.push(`| **Drift Score** | ${artifact.drift.score}/100 |`);
|
|
997
|
+
lines.push(`| **Risk Level** | ${artifact.drift.riskLevel.toUpperCase()} |`);
|
|
998
|
+
lines.push(`| **Projects** | ${artifact.projects.length} |`);
|
|
999
|
+
const scannedMeta = [artifact.timestamp];
|
|
1000
|
+
if (artifact.durationMs !== void 0) scannedMeta.push(`${(artifact.durationMs / 1e3).toFixed(1)}s`);
|
|
1001
|
+
if (artifact.filesScanned !== void 0) scannedMeta.push(`${artifact.filesScanned} files`);
|
|
1002
|
+
if (artifact.treeSummary) scannedMeta.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files \xB7 ${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
1003
|
+
lines.push(`| **Scanned** | ${scannedMeta.join(" \xB7 ")} |`);
|
|
1004
|
+
if (artifact.vcs) {
|
|
1005
|
+
lines.push(`| **VCS** | ${artifact.vcs.type} |`);
|
|
1006
|
+
if (artifact.vcs.branch) lines.push(`| **Branch** | ${artifact.vcs.branch} |`);
|
|
1007
|
+
if (artifact.vcs.sha) lines.push(`| **Commit** | \`${artifact.vcs.shortSha}\` |`);
|
|
1008
|
+
}
|
|
1009
|
+
lines.push("");
|
|
1010
|
+
lines.push("## Score Breakdown");
|
|
1011
|
+
lines.push("");
|
|
1012
|
+
lines.push(`| Component | Score |`);
|
|
1013
|
+
lines.push(`|-----------|-------|`);
|
|
1014
|
+
lines.push(`| Runtime | ${artifact.drift.components.runtimeScore} |`);
|
|
1015
|
+
lines.push(`| Frameworks | ${artifact.drift.components.frameworkScore} |`);
|
|
1016
|
+
lines.push(`| Dependencies | ${artifact.drift.components.dependencyScore} |`);
|
|
1017
|
+
lines.push(`| EOL Risk | ${artifact.drift.components.eolScore} |`);
|
|
1018
|
+
lines.push("");
|
|
1019
|
+
lines.push("## Projects");
|
|
1020
|
+
lines.push("");
|
|
1021
|
+
for (const project of artifact.projects) {
|
|
1022
|
+
lines.push(`### ${project.name} (${project.type})`);
|
|
1023
|
+
lines.push("");
|
|
1024
|
+
if (project.runtime) {
|
|
1025
|
+
const lag = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? ` \u2014 ${project.runtimeMajorsBehind} major(s) behind` : " \u2014 current";
|
|
1026
|
+
lines.push(`- **Runtime:** ${project.runtime}${lag}`);
|
|
1027
|
+
}
|
|
1028
|
+
if (project.frameworks.length > 0) {
|
|
1029
|
+
lines.push("- **Frameworks:**");
|
|
1030
|
+
for (const fw of project.frameworks) {
|
|
1031
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? "current" : `${fw.majorsBehind} behind` : "unknown";
|
|
1032
|
+
lines.push(` - ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const b = project.dependencyAgeBuckets;
|
|
1036
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
1037
|
+
if (total > 0) {
|
|
1038
|
+
lines.push(`- **Dependencies:** ${b.current} current, ${b.oneBehind} 1-behind, ${b.twoPlusBehind} 2+ behind, ${b.unknown} unknown`);
|
|
1039
|
+
}
|
|
1040
|
+
lines.push("");
|
|
1041
|
+
}
|
|
1042
|
+
if (artifact.extended?.uiPurpose) {
|
|
1043
|
+
const up = artifact.extended.uiPurpose;
|
|
1044
|
+
lines.push("## Product Purpose Signals");
|
|
1045
|
+
lines.push("");
|
|
1046
|
+
lines.push(`- **Frameworks:** ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : "unknown"}`);
|
|
1047
|
+
lines.push(`- **Evidence Items:** ${up.topEvidence.length}${up.capped ? ` (capped from ${up.evidenceCount})` : ""}`);
|
|
1048
|
+
if (up.topEvidence.length > 0) {
|
|
1049
|
+
lines.push("- **Top Evidence:**");
|
|
1050
|
+
for (const item of up.topEvidence.slice(0, 10)) {
|
|
1051
|
+
lines.push(` - [${item.kind}] ${item.value} (${item.file})`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (up.unknownSignals.length > 0) {
|
|
1055
|
+
lines.push("- **Unknowns:**");
|
|
1056
|
+
for (const u of up.unknownSignals.slice(0, 5)) {
|
|
1057
|
+
lines.push(` - ${u}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
lines.push("");
|
|
1061
|
+
}
|
|
1062
|
+
if (artifact.findings.length > 0) {
|
|
1063
|
+
lines.push("## Findings");
|
|
1064
|
+
lines.push("");
|
|
1065
|
+
lines.push(`| Level | Rule | Message | Location |`);
|
|
1066
|
+
lines.push(`|-------|------|---------|----------|`);
|
|
1067
|
+
for (const f of artifact.findings) {
|
|
1068
|
+
const emoji = f.level === "error" ? "\u{1F534}" : f.level === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
1069
|
+
lines.push(`| ${emoji} ${f.level} | ${f.ruleId} | ${f.message} | ${f.location} |`);
|
|
1070
|
+
}
|
|
1071
|
+
lines.push("");
|
|
1072
|
+
}
|
|
1073
|
+
if (artifact.delta !== void 0) {
|
|
1074
|
+
const dir = artifact.delta > 0 ? "\u{1F4C8}" : artifact.delta < 0 ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
1075
|
+
lines.push(`## Drift Delta: ${dir} ${artifact.delta > 0 ? "+" : ""}${artifact.delta} vs baseline`);
|
|
1076
|
+
lines.push("");
|
|
1077
|
+
}
|
|
1078
|
+
return lines.join("\n");
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/commands/report.ts
|
|
1082
|
+
var reportCommand = new Command4("report").description("Generate a drift report from a scan artifact").option("--in <file>", "Input artifact file", ".vibgrate/scan_result.json").option("--format <format>", "Output format (md|text|json)", "text").action(async (opts) => {
|
|
1083
|
+
const artifactPath = path4.resolve(opts.in);
|
|
61
1084
|
if (!await pathExists(artifactPath)) {
|
|
62
|
-
console.error(
|
|
63
|
-
console.error(
|
|
1085
|
+
console.error(chalk5.red(`Artifact not found: ${artifactPath}`));
|
|
1086
|
+
console.error(chalk5.dim('Run "vibgrate scan" first to generate a scan artifact.'));
|
|
64
1087
|
process.exit(1);
|
|
65
1088
|
}
|
|
66
1089
|
const artifact = await readJsonFile(artifactPath);
|
|
@@ -78,20 +1101,370 @@ var reportCommand = new Command2("report").description("Generate a drift report
|
|
|
78
1101
|
}
|
|
79
1102
|
});
|
|
80
1103
|
|
|
1104
|
+
// src/commands/push.ts
|
|
1105
|
+
import * as crypto2 from "crypto";
|
|
1106
|
+
import * as path5 from "path";
|
|
1107
|
+
import { Command as Command5 } from "commander";
|
|
1108
|
+
import chalk6 from "chalk";
|
|
1109
|
+
|
|
1110
|
+
// src/utils/compact-artifact.ts
|
|
1111
|
+
import * as zlib from "zlib";
|
|
1112
|
+
import { promisify } from "util";
|
|
1113
|
+
|
|
1114
|
+
// src/utils/compact-evidence.ts
|
|
1115
|
+
var CATEGORY_PATTERNS = [
|
|
1116
|
+
{ category: "pricing", pattern: /price|pricing|billing|subscri|trial|credit|plan|tier|upgrade|premium|pro|enterprise/i },
|
|
1117
|
+
{ category: "auth", pattern: /sign[- ]?in|sign[- ]?up|log[- ]?in|log[- ]?out|auth|sso|oauth|password|register|invite|onboard/i },
|
|
1118
|
+
{ category: "dashboard", pattern: /dashboard|overview|home|main|summary|stats/i },
|
|
1119
|
+
{ category: "settings", pattern: /setting|config|preference|option|profile|account/i },
|
|
1120
|
+
{ category: "users", pattern: /user|member|team|role|permission|access|admin|owner/i },
|
|
1121
|
+
{ category: "integrations", pattern: /integrat|connect|webhook|api[- ]?key|sync|import|export/i },
|
|
1122
|
+
{ category: "reports", pattern: /report|analy|metric|chart|graph|insight|track/i },
|
|
1123
|
+
{ category: "workflows", pattern: /workflow|automat|schedule|trigger|action|job|task|pipeline/i },
|
|
1124
|
+
{ category: "projects", pattern: /project|workspace|organization|folder|repo/i },
|
|
1125
|
+
{ category: "navigation", pattern: /menu|nav|sidebar|header|footer|breadcrumb/i }
|
|
1126
|
+
];
|
|
1127
|
+
function compactUiPurpose(result, maxSamplesPerCategory = 3) {
|
|
1128
|
+
const evidence = result.topEvidence;
|
|
1129
|
+
const dependencies = evidence.filter((e) => e.kind === "dependency").map((e) => e.value).slice(0, 10);
|
|
1130
|
+
const routes = dedupeRoutes(
|
|
1131
|
+
evidence.filter((e) => e.kind === "route").map((e) => e.value)
|
|
1132
|
+
).slice(0, 15);
|
|
1133
|
+
const textEvidence = evidence.filter(
|
|
1134
|
+
(e) => e.kind !== "dependency" && e.kind !== "route" && e.kind !== "feature_flag"
|
|
1135
|
+
);
|
|
1136
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1137
|
+
const categoryCounts = {};
|
|
1138
|
+
for (const item of textEvidence) {
|
|
1139
|
+
const category = categorize(item.value);
|
|
1140
|
+
if (!byCategory.has(category)) {
|
|
1141
|
+
byCategory.set(category, /* @__PURE__ */ new Set());
|
|
1142
|
+
}
|
|
1143
|
+
const normalized = normalizeValue(item.value);
|
|
1144
|
+
if (normalized.length >= 3) {
|
|
1145
|
+
byCategory.get(category).add(normalized);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const samples = [];
|
|
1149
|
+
for (const [category, values] of byCategory) {
|
|
1150
|
+
const deduped = dedupeStrings([...values]);
|
|
1151
|
+
categoryCounts[category] = deduped.length;
|
|
1152
|
+
for (const value of deduped.slice(0, maxSamplesPerCategory)) {
|
|
1153
|
+
samples.push({ kind: "text", value, category });
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const featureFlags = evidence.filter((e) => e.kind === "feature_flag");
|
|
1157
|
+
if (featureFlags.length > 0) {
|
|
1158
|
+
categoryCounts["feature_flags"] = featureFlags.length;
|
|
1159
|
+
samples.push({ kind: "feature_flag", value: "feature flags detected", category: "feature_flags" });
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
samples,
|
|
1163
|
+
categoryCounts,
|
|
1164
|
+
originalCount: evidence.length,
|
|
1165
|
+
dependencies,
|
|
1166
|
+
routes,
|
|
1167
|
+
detectedFrameworks: result.detectedFrameworks
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function categorize(value) {
|
|
1171
|
+
for (const { category, pattern } of CATEGORY_PATTERNS) {
|
|
1172
|
+
if (pattern.test(value)) return category;
|
|
1173
|
+
}
|
|
1174
|
+
return "general";
|
|
1175
|
+
}
|
|
1176
|
+
function normalizeValue(value) {
|
|
1177
|
+
return value.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
1178
|
+
}
|
|
1179
|
+
function dedupeStrings(values) {
|
|
1180
|
+
const sorted = values.sort((a, b) => b.length - a.length);
|
|
1181
|
+
const kept = [];
|
|
1182
|
+
for (const value of sorted) {
|
|
1183
|
+
const isDupe = kept.some((k) => {
|
|
1184
|
+
const stem = value.slice(0, 6);
|
|
1185
|
+
return k.startsWith(stem) || k.includes(value) || value.includes(k);
|
|
1186
|
+
});
|
|
1187
|
+
if (!isDupe) {
|
|
1188
|
+
kept.push(value);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return kept;
|
|
1192
|
+
}
|
|
1193
|
+
function dedupeRoutes(routes) {
|
|
1194
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1195
|
+
const result = [];
|
|
1196
|
+
for (const route of routes) {
|
|
1197
|
+
const normalized = route.replace(/:[a-z_]+/gi, ":param").replace(/\[\[*\.*\.*[a-z_]+\]*\]/gi, ":param").replace(/\/+$/, "").toLowerCase();
|
|
1198
|
+
if (!seen.has(normalized)) {
|
|
1199
|
+
seen.add(normalized);
|
|
1200
|
+
result.push(route);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return result;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/utils/compact-artifact.ts
|
|
1207
|
+
var gzip2 = promisify(zlib.gzip);
|
|
1208
|
+
var MAX_ITEMS = 50;
|
|
1209
|
+
function extractName(entry) {
|
|
1210
|
+
const match = entry.match(/^(.+?)\s*\(/);
|
|
1211
|
+
return match ? match[1].trim() : entry.trim();
|
|
1212
|
+
}
|
|
1213
|
+
function compactDataStores(result) {
|
|
1214
|
+
return {
|
|
1215
|
+
databaseTechnologies: result.databaseTechnologies.slice(0, 10),
|
|
1216
|
+
connectionStrings: [],
|
|
1217
|
+
// Don't include connection strings in upload
|
|
1218
|
+
connectionPoolSettings: result.connectionPoolSettings.slice(0, MAX_ITEMS),
|
|
1219
|
+
replicationSettings: result.replicationSettings.slice(0, 20),
|
|
1220
|
+
readReplicaSettings: result.readReplicaSettings.slice(0, 20),
|
|
1221
|
+
failoverSettings: result.failoverSettings.slice(0, 20),
|
|
1222
|
+
collationAndEncoding: result.collationAndEncoding.slice(0, 20),
|
|
1223
|
+
queryTimeoutDefaults: result.queryTimeoutDefaults.slice(0, 20),
|
|
1224
|
+
manualIndexes: result.manualIndexes.map(extractName).slice(0, MAX_ITEMS),
|
|
1225
|
+
tables: result.tables.map(extractName).slice(0, MAX_ITEMS),
|
|
1226
|
+
views: result.views.map(extractName).slice(0, MAX_ITEMS),
|
|
1227
|
+
storedProcedures: result.storedProcedures.map(extractName).slice(0, MAX_ITEMS),
|
|
1228
|
+
triggers: result.triggers.map(extractName).slice(0, MAX_ITEMS),
|
|
1229
|
+
rowLevelSecurityPolicies: result.rowLevelSecurityPolicies.slice(0, 20),
|
|
1230
|
+
otherServices: result.otherServices.slice(0, 20)
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function compactApiSurface(result) {
|
|
1234
|
+
const seenProviders = /* @__PURE__ */ new Set();
|
|
1235
|
+
const uniqueIntegrations = result.integrations.filter((i) => {
|
|
1236
|
+
const domain = i.provider.split(":")[0];
|
|
1237
|
+
if (seenProviders.has(domain)) return false;
|
|
1238
|
+
seenProviders.add(domain);
|
|
1239
|
+
return true;
|
|
1240
|
+
}).slice(0, MAX_ITEMS).map((i) => ({
|
|
1241
|
+
provider: i.provider,
|
|
1242
|
+
endpoint: "",
|
|
1243
|
+
// Don't include full endpoints
|
|
1244
|
+
version: i.version,
|
|
1245
|
+
parameters: [],
|
|
1246
|
+
// Don't include params
|
|
1247
|
+
configOptions: [],
|
|
1248
|
+
authHints: [],
|
|
1249
|
+
files: []
|
|
1250
|
+
// Don't include file paths
|
|
1251
|
+
}));
|
|
1252
|
+
return {
|
|
1253
|
+
integrations: uniqueIntegrations,
|
|
1254
|
+
openApiSpecifications: result.openApiSpecifications.slice(0, 10),
|
|
1255
|
+
webhookUrls: result.webhookUrls.slice(0, 20),
|
|
1256
|
+
callbackEndpoints: result.callbackEndpoints.slice(0, 20),
|
|
1257
|
+
apiVersionPins: result.apiVersionPins.slice(0, 20),
|
|
1258
|
+
tokenExpirationPolicies: result.tokenExpirationPolicies.slice(0, 20),
|
|
1259
|
+
rateLimitOverrides: result.rateLimitOverrides.slice(0, 20),
|
|
1260
|
+
customHeaders: result.customHeaders.slice(0, 20),
|
|
1261
|
+
corsPolicies: result.corsPolicies.slice(0, 20),
|
|
1262
|
+
oauthScopes: result.oauthScopes.slice(0, 20),
|
|
1263
|
+
apiTokens: []
|
|
1264
|
+
// Don't include token references
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
function compactAssetBranding(result) {
|
|
1268
|
+
return {
|
|
1269
|
+
faviconFiles: result.faviconFiles.slice(0, 1),
|
|
1270
|
+
productLogos: []
|
|
1271
|
+
// Don't include logos
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function prepareArtifactForUpload(artifact) {
|
|
1275
|
+
const compacted = { ...artifact };
|
|
1276
|
+
if (compacted.extended) {
|
|
1277
|
+
const ext = { ...compacted.extended };
|
|
1278
|
+
if (ext.dataStores) {
|
|
1279
|
+
ext.dataStores = compactDataStores(ext.dataStores);
|
|
1280
|
+
}
|
|
1281
|
+
if (ext.apiSurface) {
|
|
1282
|
+
ext.apiSurface = compactApiSurface(ext.apiSurface);
|
|
1283
|
+
}
|
|
1284
|
+
if (ext.assetBranding) {
|
|
1285
|
+
ext.assetBranding = compactAssetBranding(ext.assetBranding);
|
|
1286
|
+
}
|
|
1287
|
+
if (ext.uiPurpose) {
|
|
1288
|
+
const compactedUi = compactUiPurpose(ext.uiPurpose);
|
|
1289
|
+
ext.uiPurpose = {
|
|
1290
|
+
enabled: ext.uiPurpose.enabled,
|
|
1291
|
+
detectedFrameworks: compactedUi.detectedFrameworks,
|
|
1292
|
+
evidenceCount: compactedUi.originalCount,
|
|
1293
|
+
capped: ext.uiPurpose.capped,
|
|
1294
|
+
topEvidence: [],
|
|
1295
|
+
// Clear full evidence
|
|
1296
|
+
unknownSignals: [],
|
|
1297
|
+
// Add compacted data under extended properties
|
|
1298
|
+
...{ compacted: compactedUi }
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
if (ext.runtimeConfiguration) {
|
|
1302
|
+
ext.runtimeConfiguration = {
|
|
1303
|
+
...ext.runtimeConfiguration,
|
|
1304
|
+
environmentVariables: ext.runtimeConfiguration.environmentVariables.slice(0, 100),
|
|
1305
|
+
hiddenConfigFiles: ext.runtimeConfiguration.hiddenConfigFiles.slice(0, MAX_ITEMS),
|
|
1306
|
+
startupArguments: ext.runtimeConfiguration.startupArguments.slice(0, 100)
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
if (ext.operationalResilience) {
|
|
1310
|
+
const ops = ext.operationalResilience;
|
|
1311
|
+
ext.operationalResilience = {
|
|
1312
|
+
implicitTimeouts: ops.implicitTimeouts.slice(0, 30),
|
|
1313
|
+
defaultPaginationSize: ops.defaultPaginationSize.slice(0, 30),
|
|
1314
|
+
implicitRetryLogic: ops.implicitRetryLogic.slice(0, 30),
|
|
1315
|
+
defaultLocale: ops.defaultLocale.slice(0, 20),
|
|
1316
|
+
defaultCurrency: ops.defaultCurrency.slice(0, 20),
|
|
1317
|
+
implicitTimezone: ops.implicitTimezone.slice(0, 20),
|
|
1318
|
+
defaultCharacterEncoding: ops.defaultCharacterEncoding.slice(0, 20),
|
|
1319
|
+
sessionStores: ops.sessionStores.slice(0, 20),
|
|
1320
|
+
distributedLocks: ops.distributedLocks.slice(0, 20),
|
|
1321
|
+
jobSchedulers: ops.jobSchedulers.slice(0, 30),
|
|
1322
|
+
idempotencyKeys: ops.idempotencyKeys.slice(0, 20),
|
|
1323
|
+
rateLimitingCounters: ops.rateLimitingCounters.slice(0, 20),
|
|
1324
|
+
circuitBreakerState: ops.circuitBreakerState.slice(0, 20),
|
|
1325
|
+
abTestToggles: ops.abTestToggles.slice(0, 20),
|
|
1326
|
+
regionalEnablementRules: ops.regionalEnablementRules.slice(0, 20),
|
|
1327
|
+
betaAccessGroups: ops.betaAccessGroups.slice(0, 20),
|
|
1328
|
+
licensingEnforcementLogic: ops.licensingEnforcementLogic.slice(0, 20),
|
|
1329
|
+
killSwitches: ops.killSwitches.slice(0, 20),
|
|
1330
|
+
connectorRetryLogic: ops.connectorRetryLogic.slice(0, 20),
|
|
1331
|
+
apiPollingIntervals: ops.apiPollingIntervals.slice(0, 20),
|
|
1332
|
+
fieldMappings: ops.fieldMappings.slice(0, 20),
|
|
1333
|
+
schemaRegistryRules: ops.schemaRegistryRules.slice(0, 20),
|
|
1334
|
+
deadLetterQueueBehavior: ops.deadLetterQueueBehavior.slice(0, 20),
|
|
1335
|
+
dataMaskingRules: ops.dataMaskingRules.slice(0, 20),
|
|
1336
|
+
transformationLogic: ops.transformationLogic.slice(0, 20),
|
|
1337
|
+
timezoneHandling: ops.timezoneHandling.slice(0, 20),
|
|
1338
|
+
encryptionSettings: ops.encryptionSettings.slice(0, 30),
|
|
1339
|
+
hardcodedSecretSignals: ops.hardcodedSecretSignals.slice(0, 20)
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
if (ext.dependencyGraph) {
|
|
1343
|
+
ext.dependencyGraph = {
|
|
1344
|
+
...ext.dependencyGraph,
|
|
1345
|
+
phantomDependencies: ext.dependencyGraph.phantomDependencies.slice(0, MAX_ITEMS),
|
|
1346
|
+
phantomDependencyDetails: ext.dependencyGraph.phantomDependencyDetails?.slice(0, MAX_ITEMS),
|
|
1347
|
+
duplicatedPackages: ext.dependencyGraph.duplicatedPackages.slice(0, MAX_ITEMS)
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
compacted.extended = ext;
|
|
1351
|
+
}
|
|
1352
|
+
return compacted;
|
|
1353
|
+
}
|
|
1354
|
+
async function compressArtifact(artifact) {
|
|
1355
|
+
const json = JSON.stringify(artifact);
|
|
1356
|
+
return gzip2(json, { level: 9 });
|
|
1357
|
+
}
|
|
1358
|
+
async function prepareCompressedUpload2(artifact) {
|
|
1359
|
+
const compacted = prepareArtifactForUpload(artifact);
|
|
1360
|
+
const compressed = await compressArtifact(compacted);
|
|
1361
|
+
return { body: compressed, contentEncoding: "gzip" };
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/commands/push.ts
|
|
1365
|
+
function parseDsn2(dsn) {
|
|
1366
|
+
const cleaned = dsn.replace(/[\x00-\x1F\x7F\uFEFF\u200B-\u200D\u2060]/g, "").trim();
|
|
1367
|
+
const match = cleaned.match(/^vibgrate\+(https?):?\/\/([^:]+):([^@]+)@([^/]+)\/(.+)$/);
|
|
1368
|
+
if (!match) return null;
|
|
1369
|
+
return {
|
|
1370
|
+
scheme: match[1],
|
|
1371
|
+
keyId: match[2],
|
|
1372
|
+
secret: match[3],
|
|
1373
|
+
host: match[4],
|
|
1374
|
+
workspaceId: match[5]
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function computeHmac(body, secret) {
|
|
1378
|
+
return crypto2.createHmac("sha256", secret).update(body).digest("base64");
|
|
1379
|
+
}
|
|
1380
|
+
var pushCommand = new Command5("push").description("Push scan results to Vibgrate API").option("--dsn <dsn>", "DSN token (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region (us, eu)").option("--file <file>", "Scan artifact file", ".vibgrate/scan_result.json").option("--strict", "Fail on upload errors").action(async (opts) => {
|
|
1381
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
1382
|
+
if (!dsn) {
|
|
1383
|
+
console.error(chalk6.red("No DSN provided."));
|
|
1384
|
+
console.error(chalk6.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
|
|
1385
|
+
if (opts.strict) process.exit(1);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const parsed = parseDsn2(dsn);
|
|
1389
|
+
if (!parsed) {
|
|
1390
|
+
console.error(chalk6.red("Invalid DSN format."));
|
|
1391
|
+
console.error(chalk6.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
|
|
1392
|
+
if (opts.strict) process.exit(1);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
const filePath = path5.resolve(opts.file);
|
|
1396
|
+
if (!await pathExists(filePath)) {
|
|
1397
|
+
console.error(chalk6.red(`Scan artifact not found: ${filePath}`));
|
|
1398
|
+
console.error(chalk6.dim('Run "vibgrate scan" first.'));
|
|
1399
|
+
if (opts.strict) process.exit(1);
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const artifact = await readJsonFile(filePath);
|
|
1403
|
+
const { body, contentEncoding } = await prepareCompressedUpload2(artifact);
|
|
1404
|
+
const timestamp = String(Date.now());
|
|
1405
|
+
let host = parsed.host;
|
|
1406
|
+
if (opts.region) {
|
|
1407
|
+
try {
|
|
1408
|
+
host = resolveIngestHost(opts.region);
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
console.error(chalk6.red(e instanceof Error ? e.message : String(e)));
|
|
1411
|
+
if (opts.strict) process.exit(1);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
1416
|
+
const originalSize = JSON.stringify(artifact).length;
|
|
1417
|
+
const compressedSize = body.length;
|
|
1418
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
1419
|
+
console.log(chalk6.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
1420
|
+
try {
|
|
1421
|
+
const response = await fetch(url, {
|
|
1422
|
+
method: "POST",
|
|
1423
|
+
headers: {
|
|
1424
|
+
"Content-Type": "application/json",
|
|
1425
|
+
"Content-Encoding": contentEncoding,
|
|
1426
|
+
"X-Vibgrate-Timestamp": timestamp,
|
|
1427
|
+
"Authorization": `VibgrateDSN ${parsed.keyId}:${parsed.secret}`,
|
|
1428
|
+
"Connection": "close"
|
|
1429
|
+
// Prevent keep-alive delays on exit
|
|
1430
|
+
},
|
|
1431
|
+
body
|
|
1432
|
+
});
|
|
1433
|
+
if (!response.ok) {
|
|
1434
|
+
const text = await response.text();
|
|
1435
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1436
|
+
}
|
|
1437
|
+
const result = await response.json();
|
|
1438
|
+
console.log(chalk6.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
1439
|
+
console.log();
|
|
1440
|
+
console.log(chalk6.dim("Processing continues in the background. Results available shortly."));
|
|
1441
|
+
console.log();
|
|
1442
|
+
if (result.ingestId) {
|
|
1443
|
+
const dashHost = host.includes("eu.") ? "dash.vibgrate.eu" : "dash.vibgrate.com";
|
|
1444
|
+
const reportUrl = `https://${dashHost}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
1445
|
+
console.log(chalk6.dim("View report: ") + chalk6.underline(reportUrl));
|
|
1446
|
+
}
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1449
|
+
console.error(chalk6.red(`Upload failed: ${msg}`));
|
|
1450
|
+
if (opts.strict) process.exit(1);
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
|
|
81
1454
|
// src/commands/update.ts
|
|
82
1455
|
import { execSync } from "child_process";
|
|
83
|
-
import * as
|
|
84
|
-
import { Command as
|
|
85
|
-
import
|
|
1456
|
+
import * as path7 from "path";
|
|
1457
|
+
import { Command as Command6 } from "commander";
|
|
1458
|
+
import chalk7 from "chalk";
|
|
86
1459
|
|
|
87
1460
|
// src/utils/update-check.ts
|
|
1461
|
+
var import_semver = __toESM(require_semver(), 1);
|
|
88
1462
|
import * as fs from "fs/promises";
|
|
89
|
-
import * as
|
|
1463
|
+
import * as path6 from "path";
|
|
90
1464
|
import * as os from "os";
|
|
91
|
-
import semver from "semver";
|
|
92
1465
|
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
93
|
-
var CACHE_DIR =
|
|
94
|
-
var CACHE_FILE =
|
|
1466
|
+
var CACHE_DIR = path6.join(os.homedir(), ".vibgrate");
|
|
1467
|
+
var CACHE_FILE = path6.join(CACHE_DIR, "update-check.json");
|
|
95
1468
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
|
|
96
1469
|
async function checkForUpdate() {
|
|
97
1470
|
try {
|
|
@@ -100,7 +1473,7 @@ async function checkForUpdate() {
|
|
|
100
1473
|
return {
|
|
101
1474
|
current: VERSION,
|
|
102
1475
|
latest: cached.latest,
|
|
103
|
-
updateAvailable:
|
|
1476
|
+
updateAvailable: import_semver.default.gt(cached.latest, VERSION)
|
|
104
1477
|
};
|
|
105
1478
|
}
|
|
106
1479
|
const controller = new AbortController();
|
|
@@ -118,12 +1491,12 @@ async function checkForUpdate() {
|
|
|
118
1491
|
if (!response.ok) return null;
|
|
119
1492
|
const data = await response.json();
|
|
120
1493
|
const latest = data.version;
|
|
121
|
-
if (!latest || !
|
|
1494
|
+
if (!latest || !import_semver.default.valid(latest)) return null;
|
|
122
1495
|
await writeCache({ latest, checkedAt: Date.now() });
|
|
123
1496
|
return {
|
|
124
1497
|
current: VERSION,
|
|
125
1498
|
latest,
|
|
126
|
-
updateAvailable:
|
|
1499
|
+
updateAvailable: import_semver.default.gt(latest, VERSION)
|
|
127
1500
|
};
|
|
128
1501
|
} catch {
|
|
129
1502
|
return null;
|
|
@@ -146,7 +1519,7 @@ async function fetchLatestVersion() {
|
|
|
146
1519
|
if (!response.ok) return null;
|
|
147
1520
|
const data = await response.json();
|
|
148
1521
|
const latest = data.version;
|
|
149
|
-
if (!latest || !
|
|
1522
|
+
if (!latest || !import_semver.default.valid(latest)) return null;
|
|
150
1523
|
await writeCache({ latest, checkedAt: Date.now() });
|
|
151
1524
|
return latest;
|
|
152
1525
|
} catch {
|
|
@@ -184,8 +1557,8 @@ function detectGlobalInstall() {
|
|
|
184
1557
|
}
|
|
185
1558
|
return null;
|
|
186
1559
|
}
|
|
187
|
-
function getGlobalUpdateCommand(pm,
|
|
188
|
-
const spec = `${
|
|
1560
|
+
function getGlobalUpdateCommand(pm, pkg2, version) {
|
|
1561
|
+
const spec = `${pkg2}@${version}`;
|
|
189
1562
|
switch (pm) {
|
|
190
1563
|
case "pnpm":
|
|
191
1564
|
return `pnpm add -g ${spec}`;
|
|
@@ -199,13 +1572,13 @@ function getGlobalUpdateCommand(pm, pkg, version) {
|
|
|
199
1572
|
}
|
|
200
1573
|
}
|
|
201
1574
|
async function detectPackageManager(cwd) {
|
|
202
|
-
if (await pathExists(
|
|
203
|
-
if (await pathExists(
|
|
204
|
-
if (await pathExists(
|
|
1575
|
+
if (await pathExists(path7.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1576
|
+
if (await pathExists(path7.join(cwd, "bun.lockb"))) return "bun";
|
|
1577
|
+
if (await pathExists(path7.join(cwd, "yarn.lock"))) return "yarn";
|
|
205
1578
|
return "npm";
|
|
206
1579
|
}
|
|
207
|
-
function getInstallCommand(pm,
|
|
208
|
-
const spec = `${
|
|
1580
|
+
function getInstallCommand(pm, pkg2, version, isDev) {
|
|
1581
|
+
const spec = `${pkg2}@${version}`;
|
|
209
1582
|
switch (pm) {
|
|
210
1583
|
case "pnpm":
|
|
211
1584
|
return isDev ? `pnpm add -D ${spec}` : `pnpm add ${spec}`;
|
|
@@ -220,30 +1593,30 @@ function getInstallCommand(pm, pkg, version, isDev) {
|
|
|
220
1593
|
}
|
|
221
1594
|
async function isDevDependency(cwd) {
|
|
222
1595
|
try {
|
|
223
|
-
const pkgPath =
|
|
1596
|
+
const pkgPath = path7.join(cwd, "package.json");
|
|
224
1597
|
const raw = await (await import("fs/promises")).readFile(pkgPath, "utf-8");
|
|
225
|
-
const
|
|
226
|
-
return Boolean(
|
|
1598
|
+
const pkg2 = JSON.parse(raw);
|
|
1599
|
+
return Boolean(pkg2.devDependencies?.["@vibgrate/cli"]);
|
|
227
1600
|
} catch {
|
|
228
1601
|
return true;
|
|
229
1602
|
}
|
|
230
1603
|
}
|
|
231
|
-
var updateCommand = new
|
|
232
|
-
console.log(
|
|
233
|
-
console.log(
|
|
1604
|
+
var updateCommand = new Command6("update").description("Update vibgrate to the latest version").option("--check", "Only check for updates, do not install").option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)").option("--global", "Update global installation").action(async (opts) => {
|
|
1605
|
+
console.log(chalk7.dim(`Current version: ${VERSION}`));
|
|
1606
|
+
console.log(chalk7.dim("Checking npm registry..."));
|
|
234
1607
|
const latest = await fetchLatestVersion();
|
|
235
1608
|
if (!latest) {
|
|
236
|
-
console.error(
|
|
1609
|
+
console.error(chalk7.red("Could not reach the npm registry. Check your network connection."));
|
|
237
1610
|
process.exit(1);
|
|
238
1611
|
}
|
|
239
|
-
const semver2 = await import("semver");
|
|
1612
|
+
const semver2 = await import("./semver-JBJZTHUX.js");
|
|
240
1613
|
if (!semver2.gt(latest, VERSION)) {
|
|
241
|
-
console.log(
|
|
1614
|
+
console.log(chalk7.green("\u2714") + ` You are on the latest version (${VERSION}).`);
|
|
242
1615
|
return;
|
|
243
1616
|
}
|
|
244
|
-
console.log(
|
|
1617
|
+
console.log(chalk7.yellow(`Update available: ${VERSION} \u2192 ${latest}`));
|
|
245
1618
|
if (opts.check) {
|
|
246
|
-
console.log(
|
|
1619
|
+
console.log(chalk7.dim('Run "vibgrate update" to install.'));
|
|
247
1620
|
return;
|
|
248
1621
|
}
|
|
249
1622
|
const cwd = process.cwd();
|
|
@@ -253,26 +1626,26 @@ var updateCommand = new Command3("update").description("Update vibgrate to the l
|
|
|
253
1626
|
let cmd;
|
|
254
1627
|
if (isGlobal) {
|
|
255
1628
|
cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
|
|
256
|
-
console.log(
|
|
1629
|
+
console.log(chalk7.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
257
1630
|
} else {
|
|
258
1631
|
const isDev = await isDevDependency(cwd);
|
|
259
1632
|
cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev);
|
|
260
|
-
console.log(
|
|
1633
|
+
console.log(chalk7.dim(`Using ${pm}: ${cmd}`));
|
|
261
1634
|
}
|
|
262
1635
|
try {
|
|
263
1636
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
264
|
-
console.log(
|
|
1637
|
+
console.log(chalk7.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
|
|
265
1638
|
} catch {
|
|
266
|
-
console.error(
|
|
1639
|
+
console.error(chalk7.red(`Update failed. Run manually: ${cmd}`));
|
|
267
1640
|
process.exit(1);
|
|
268
1641
|
}
|
|
269
1642
|
});
|
|
270
1643
|
|
|
271
1644
|
// src/commands/sbom.ts
|
|
272
|
-
import * as
|
|
1645
|
+
import * as path8 from "path";
|
|
273
1646
|
import { randomUUID } from "crypto";
|
|
274
|
-
import { Command as
|
|
275
|
-
import
|
|
1647
|
+
import { Command as Command7 } from "commander";
|
|
1648
|
+
import chalk8 from "chalk";
|
|
276
1649
|
function flattenDependencies(artifact) {
|
|
277
1650
|
const rows = [];
|
|
278
1651
|
for (const project of artifact.projects) {
|
|
@@ -410,50 +1783,50 @@ function formatDeltaText(base, current) {
|
|
|
410
1783
|
return lines.join("\n");
|
|
411
1784
|
}
|
|
412
1785
|
async function readArtifactOrExit(filePath) {
|
|
413
|
-
const absolutePath =
|
|
1786
|
+
const absolutePath = path8.resolve(filePath);
|
|
414
1787
|
if (!await pathExists(absolutePath)) {
|
|
415
|
-
console.error(
|
|
1788
|
+
console.error(chalk8.red(`Artifact not found: ${absolutePath}`));
|
|
416
1789
|
process.exit(1);
|
|
417
1790
|
}
|
|
418
1791
|
return readJsonFile(absolutePath);
|
|
419
1792
|
}
|
|
420
|
-
var exportCommand = new
|
|
1793
|
+
var exportCommand = new Command7("export").description("Export scan artifact as SBOM").option("--in <file>", "Input artifact file", ".vibgrate/scan_result.json").option("--out <file>", "Output SBOM file").option("--format <format>", "SBOM format (cyclonedx|spdx)", "cyclonedx").action(async (opts) => {
|
|
421
1794
|
const artifact = await readArtifactOrExit(opts.in);
|
|
422
1795
|
const format = opts.format.toLowerCase();
|
|
423
1796
|
if (format !== "cyclonedx" && format !== "spdx") {
|
|
424
|
-
console.error(
|
|
1797
|
+
console.error(chalk8.red("Invalid SBOM format. Use cyclonedx or spdx."));
|
|
425
1798
|
process.exit(1);
|
|
426
1799
|
}
|
|
427
1800
|
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
428
1801
|
const body = JSON.stringify(sbom, null, 2);
|
|
429
1802
|
if (opts.out) {
|
|
430
|
-
await writeTextFile(
|
|
431
|
-
console.log(
|
|
1803
|
+
await writeTextFile(path8.resolve(opts.out), body);
|
|
1804
|
+
console.log(chalk8.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
432
1805
|
} else {
|
|
433
1806
|
console.log(body);
|
|
434
1807
|
}
|
|
435
1808
|
});
|
|
436
|
-
var deltaCommand = new
|
|
1809
|
+
var deltaCommand = new Command7("delta").description("Show SBOM delta between two scan artifacts").requiredOption("--from <file>", "Baseline scan artifact path").requiredOption("--to <file>", "Current scan artifact path").option("--out <file>", "Write report to file").action(async (opts) => {
|
|
437
1810
|
const base = await readArtifactOrExit(opts.from);
|
|
438
1811
|
const current = await readArtifactOrExit(opts.to);
|
|
439
1812
|
const report = formatDeltaText(base, current);
|
|
440
1813
|
if (opts.out) {
|
|
441
|
-
await writeTextFile(
|
|
442
|
-
console.log(
|
|
1814
|
+
await writeTextFile(path8.resolve(opts.out), report);
|
|
1815
|
+
console.log(chalk8.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
443
1816
|
} else {
|
|
444
1817
|
console.log(report);
|
|
445
1818
|
}
|
|
446
1819
|
});
|
|
447
|
-
var sbomCommand = new
|
|
1820
|
+
var sbomCommand = new Command7("sbom").description("SBOM export and delta reports for dependency drift tracking").addCommand(exportCommand).addCommand(deltaCommand);
|
|
448
1821
|
|
|
449
1822
|
// src/commands/extract.ts
|
|
450
|
-
import * as
|
|
1823
|
+
import * as path9 from "path";
|
|
451
1824
|
import * as os2 from "os";
|
|
452
1825
|
import * as fs2 from "fs/promises";
|
|
453
1826
|
import { existsSync } from "fs";
|
|
454
1827
|
import { spawn, spawnSync } from "child_process";
|
|
455
|
-
import { Command as
|
|
456
|
-
import
|
|
1828
|
+
import { Command as Command8 } from "commander";
|
|
1829
|
+
import chalk9 from "chalk";
|
|
457
1830
|
var EXIT_SUCCESS = 0;
|
|
458
1831
|
var EXIT_SCHEMA_FAILURE = 1;
|
|
459
1832
|
var EXIT_PARSE_FAILURE = 2;
|
|
@@ -543,8 +1916,8 @@ async function detectLanguages(rootDir, includeTests) {
|
|
|
543
1916
|
}
|
|
544
1917
|
for (const entry of entries) {
|
|
545
1918
|
if (entry.name.startsWith(".")) continue;
|
|
546
|
-
const absPath =
|
|
547
|
-
const relPath =
|
|
1919
|
+
const absPath = path9.join(dir, entry.name);
|
|
1920
|
+
const relPath = path9.join(relBase, entry.name);
|
|
548
1921
|
if (entry.isDirectory()) {
|
|
549
1922
|
if (!SKIP_DIRS.has(entry.name)) {
|
|
550
1923
|
await walk(absPath, relPath);
|
|
@@ -553,7 +1926,7 @@ async function detectLanguages(rootDir, includeTests) {
|
|
|
553
1926
|
}
|
|
554
1927
|
if (!entry.isFile()) continue;
|
|
555
1928
|
if (!includeTests && isTestPath(relPath)) continue;
|
|
556
|
-
const ext =
|
|
1929
|
+
const ext = path9.extname(entry.name).toLowerCase();
|
|
557
1930
|
for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
558
1931
|
if (exts.has(ext)) {
|
|
559
1932
|
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
@@ -629,10 +2002,10 @@ function splitHcsOutput(lines) {
|
|
|
629
2002
|
return { preamble, facts };
|
|
630
2003
|
}
|
|
631
2004
|
function resolveHcsWorkerBin() {
|
|
632
|
-
const base = import.meta.dirname ??
|
|
633
|
-
const bundledPath =
|
|
2005
|
+
const base = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
|
|
2006
|
+
const bundledPath = path9.resolve(base, "..", "dist", "hcs-worker.js");
|
|
634
2007
|
if (existsSync(bundledPath)) return bundledPath;
|
|
635
|
-
const siblingPath =
|
|
2008
|
+
const siblingPath = path9.resolve(base, "hcs-worker.js");
|
|
636
2009
|
if (existsSync(siblingPath)) return siblingPath;
|
|
637
2010
|
try {
|
|
638
2011
|
const resolved = import.meta.resolve("@vibgrate/hcs-node-worker");
|
|
@@ -640,9 +2013,9 @@ function resolveHcsWorkerBin() {
|
|
|
640
2013
|
if (existsSync(resolvedPath)) return resolvedPath;
|
|
641
2014
|
} catch {
|
|
642
2015
|
}
|
|
643
|
-
const monorepoPath =
|
|
2016
|
+
const monorepoPath = path9.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "dist", "main.js");
|
|
644
2017
|
if (existsSync(monorepoPath)) return monorepoPath;
|
|
645
|
-
const devPath =
|
|
2018
|
+
const devPath = path9.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "src", "main.ts");
|
|
646
2019
|
if (existsSync(devPath)) return devPath;
|
|
647
2020
|
throw new Error(
|
|
648
2021
|
'Cannot locate HCS worker. Run "pnpm build" in the CLI package to bundle the worker into dist/.'
|
|
@@ -745,7 +2118,7 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
745
2118
|
args.push("--cobol-copybook-paths", opts.copybookPaths);
|
|
746
2119
|
}
|
|
747
2120
|
}
|
|
748
|
-
return new Promise((
|
|
2121
|
+
return new Promise((resolve9) => {
|
|
749
2122
|
const facts = [];
|
|
750
2123
|
const errors = [];
|
|
751
2124
|
let stdoutBuf = "";
|
|
@@ -782,7 +2155,7 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
782
2155
|
continue;
|
|
783
2156
|
}
|
|
784
2157
|
if (opts.verbose) {
|
|
785
|
-
process.stderr.write(
|
|
2158
|
+
process.stderr.write(chalk9.dim(`[${language}] ${trimmed}
|
|
786
2159
|
`));
|
|
787
2160
|
}
|
|
788
2161
|
if (trimmed.startsWith("[error]")) {
|
|
@@ -796,15 +2169,15 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
796
2169
|
facts.push(stdoutBuf.trim());
|
|
797
2170
|
}
|
|
798
2171
|
if (killed) {
|
|
799
|
-
|
|
2172
|
+
resolve9({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
800
2173
|
} else {
|
|
801
|
-
|
|
2174
|
+
resolve9({ language, facts, errors, exitCode: code ?? 0 });
|
|
802
2175
|
}
|
|
803
2176
|
});
|
|
804
2177
|
child.on("error", (err) => {
|
|
805
2178
|
clearTimeout(timer);
|
|
806
2179
|
errors.push(`Failed to spawn worker: ${err.message}`);
|
|
807
|
-
|
|
2180
|
+
resolve9({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
808
2181
|
});
|
|
809
2182
|
});
|
|
810
2183
|
}
|
|
@@ -824,7 +2197,7 @@ function resolveDotnetPublishedWorker(workersDir) {
|
|
|
824
2197
|
};
|
|
825
2198
|
const candidates = candidatesByPlatform[process.platform] ?? [];
|
|
826
2199
|
for (const filename of candidates) {
|
|
827
|
-
const fullPath =
|
|
2200
|
+
const fullPath = path9.join(workersDir, filename);
|
|
828
2201
|
if (existsSync(fullPath)) {
|
|
829
2202
|
return fullPath;
|
|
830
2203
|
}
|
|
@@ -832,30 +2205,30 @@ function resolveDotnetPublishedWorker(workersDir) {
|
|
|
832
2205
|
return null;
|
|
833
2206
|
}
|
|
834
2207
|
function resolveNativeWorker(language, projectDir) {
|
|
835
|
-
const base = import.meta.dirname ??
|
|
836
|
-
const hcsFromBundle =
|
|
837
|
-
const hcsFromSrc =
|
|
2208
|
+
const base = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
|
|
2209
|
+
const hcsFromBundle = path9.resolve(base, "..", "..", "vibgrate-hcs");
|
|
2210
|
+
const hcsFromSrc = path9.resolve(base, "..", "..", "..", "vibgrate-hcs");
|
|
838
2211
|
const hcsRoot = existsSync(hcsFromBundle) ? hcsFromBundle : hcsFromSrc;
|
|
839
|
-
const workersDir =
|
|
2212
|
+
const workersDir = path9.resolve(base, "workers");
|
|
840
2213
|
switch (language) {
|
|
841
2214
|
case "go": {
|
|
842
|
-
const bin =
|
|
2215
|
+
const bin = path9.join(workersDir, process.platform === "win32" ? "vibgrate-hcs-go.exe" : "vibgrate-hcs-go");
|
|
843
2216
|
if (existsSync(bin)) {
|
|
844
2217
|
return { cmd: bin, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
845
2218
|
}
|
|
846
|
-
const src =
|
|
847
|
-
if (existsSync(
|
|
2219
|
+
const src = path9.join(hcsRoot, "go");
|
|
2220
|
+
if (existsSync(path9.join(src, "main.go"))) {
|
|
848
2221
|
return { cmd: "go", args: ["run", ".", "--project", projectDir, "--output", "ndjson"], cwd: src };
|
|
849
2222
|
}
|
|
850
2223
|
return null;
|
|
851
2224
|
}
|
|
852
2225
|
case "python": {
|
|
853
|
-
const bin =
|
|
2226
|
+
const bin = path9.join(workersDir, "vibgrate-hcs-python");
|
|
854
2227
|
if (existsSync(bin)) {
|
|
855
2228
|
return { cmd: bin, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
856
2229
|
}
|
|
857
|
-
const src =
|
|
858
|
-
if (existsSync(
|
|
2230
|
+
const src = path9.join(hcsRoot, "python");
|
|
2231
|
+
if (existsSync(path9.join(src, "pyproject.toml"))) {
|
|
859
2232
|
return {
|
|
860
2233
|
cmd: "python3",
|
|
861
2234
|
args: ["-m", "vibgrate_hcs_python.main", "--project", projectDir, "--output", "ndjson"],
|
|
@@ -865,13 +2238,13 @@ function resolveNativeWorker(language, projectDir) {
|
|
|
865
2238
|
return null;
|
|
866
2239
|
}
|
|
867
2240
|
case "java": {
|
|
868
|
-
const jar =
|
|
2241
|
+
const jar = path9.join(workersDir, "vibgrate-hcs-jvm.jar");
|
|
869
2242
|
if (existsSync(jar)) {
|
|
870
2243
|
return { cmd: "java", args: ["-jar", jar, "--project", projectDir, "--output", "ndjson"] };
|
|
871
2244
|
}
|
|
872
|
-
const src =
|
|
873
|
-
if (existsSync(
|
|
874
|
-
const gradlew =
|
|
2245
|
+
const src = path9.join(hcsRoot, "jvm");
|
|
2246
|
+
if (existsSync(path9.join(src, "build.gradle.kts"))) {
|
|
2247
|
+
const gradlew = path9.join(src, process.platform === "win32" ? "gradlew.bat" : "gradlew");
|
|
875
2248
|
const launcher = existsSync(gradlew) ? gradlew : "gradle";
|
|
876
2249
|
return {
|
|
877
2250
|
cmd: launcher,
|
|
@@ -887,11 +2260,11 @@ function resolveNativeWorker(language, projectDir) {
|
|
|
887
2260
|
if (publishedWorker) {
|
|
888
2261
|
return { cmd: publishedWorker, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
889
2262
|
}
|
|
890
|
-
const dll =
|
|
2263
|
+
const dll = path9.join(workersDir, "VibgrateHcsWorker.dll");
|
|
891
2264
|
if (existsSync(dll)) {
|
|
892
2265
|
return { cmd: "dotnet", args: [dll, "--project", projectDir, "--output", "ndjson"] };
|
|
893
2266
|
}
|
|
894
|
-
const csproj =
|
|
2267
|
+
const csproj = path9.join(hcsRoot, "dotnet", "src", "VibgrateHcsWorker", "VibgrateHcsWorker.csproj");
|
|
895
2268
|
if (existsSync(csproj)) {
|
|
896
2269
|
return {
|
|
897
2270
|
cmd: "dotnet",
|
|
@@ -917,19 +2290,19 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
917
2290
|
exitCode: EXIT_PARSE_FAILURE
|
|
918
2291
|
};
|
|
919
2292
|
}
|
|
920
|
-
return new Promise((
|
|
2293
|
+
return new Promise((resolve9) => {
|
|
921
2294
|
const facts = [];
|
|
922
2295
|
const errors = [];
|
|
923
2296
|
let stdoutBuf = "";
|
|
924
2297
|
let killed = false;
|
|
925
2298
|
if (opts.verbose) {
|
|
926
|
-
process.stderr.write(
|
|
2299
|
+
process.stderr.write(chalk9.dim(`[${language}] Spawning: ${spec.cmd} ${spec.args.join(" ")}
|
|
927
2300
|
`));
|
|
928
2301
|
}
|
|
929
2302
|
const runtimeReq = NATIVE_RUNTIME_REQUIREMENTS[language];
|
|
930
2303
|
const usesPathCommand = runtimeReq && spec.cmd === runtimeReq.command;
|
|
931
2304
|
if (usesPathCommand && !commandExistsOnPath(spec.cmd)) {
|
|
932
|
-
|
|
2305
|
+
resolve9({
|
|
933
2306
|
language,
|
|
934
2307
|
facts: [],
|
|
935
2308
|
errors: [
|
|
@@ -964,7 +2337,7 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
964
2337
|
const trimmed = line.trim();
|
|
965
2338
|
if (!trimmed) continue;
|
|
966
2339
|
if (opts.verbose) {
|
|
967
|
-
process.stderr.write(
|
|
2340
|
+
process.stderr.write(chalk9.dim(`[${language}] ${trimmed}
|
|
968
2341
|
`));
|
|
969
2342
|
}
|
|
970
2343
|
if (trimmed.startsWith("[error]")) {
|
|
@@ -976,9 +2349,9 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
976
2349
|
clearTimeout(timer);
|
|
977
2350
|
if (stdoutBuf.trim()) facts.push(stdoutBuf.trim());
|
|
978
2351
|
if (killed) {
|
|
979
|
-
|
|
2352
|
+
resolve9({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
980
2353
|
} else {
|
|
981
|
-
|
|
2354
|
+
resolve9({ language, facts, errors, exitCode: code ?? 0 });
|
|
982
2355
|
}
|
|
983
2356
|
});
|
|
984
2357
|
child.on("error", (err) => {
|
|
@@ -987,21 +2360,21 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
987
2360
|
errors.push(
|
|
988
2361
|
isNotFound ? `[error] '${spec.cmd}' not found. ${buildNativeInstallHint(language)}` : `[error] Failed to spawn native worker: ${err.message}`
|
|
989
2362
|
);
|
|
990
|
-
|
|
2363
|
+
resolve9({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
991
2364
|
});
|
|
992
2365
|
});
|
|
993
2366
|
}
|
|
994
2367
|
async function pushFacts(facts, dsn, verbose) {
|
|
995
|
-
const parsed =
|
|
2368
|
+
const parsed = parseDsn2(dsn);
|
|
996
2369
|
if (!parsed) {
|
|
997
|
-
process.stderr.write(
|
|
998
|
-
process.stderr.write(
|
|
2370
|
+
process.stderr.write(chalk9.red("Invalid DSN format.\n"));
|
|
2371
|
+
process.stderr.write(chalk9.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
999
2372
|
return false;
|
|
1000
2373
|
}
|
|
1001
2374
|
const body = facts.join("\n") + "\n";
|
|
1002
2375
|
const url = `${parsed.scheme}://${parsed.host}/v1/ingest/hcs`;
|
|
1003
2376
|
if (verbose) {
|
|
1004
|
-
process.stderr.write(
|
|
2377
|
+
process.stderr.write(chalk9.dim(`Pushing ${facts.length} facts to ${parsed.host}...
|
|
1005
2378
|
`));
|
|
1006
2379
|
}
|
|
1007
2380
|
try {
|
|
@@ -1019,17 +2392,17 @@ async function pushFacts(facts, dsn, verbose) {
|
|
|
1019
2392
|
});
|
|
1020
2393
|
if (!response.ok) {
|
|
1021
2394
|
const text = await response.text().catch(() => "");
|
|
1022
|
-
process.stderr.write(
|
|
2395
|
+
process.stderr.write(chalk9.red(`Push failed: HTTP ${response.status} ${text}
|
|
1023
2396
|
`));
|
|
1024
2397
|
return false;
|
|
1025
2398
|
}
|
|
1026
2399
|
if (verbose) {
|
|
1027
|
-
process.stderr.write(
|
|
2400
|
+
process.stderr.write(chalk9.green(`\u2714 Pushed ${facts.length} facts successfully
|
|
1028
2401
|
`));
|
|
1029
2402
|
}
|
|
1030
2403
|
return true;
|
|
1031
2404
|
} catch (err) {
|
|
1032
|
-
process.stderr.write(
|
|
2405
|
+
process.stderr.write(chalk9.red(`Push failed: ${err instanceof Error ? err.message : String(err)}
|
|
1033
2406
|
`));
|
|
1034
2407
|
return false;
|
|
1035
2408
|
}
|
|
@@ -1096,21 +2469,21 @@ var ProgressTracker = class {
|
|
|
1096
2469
|
const parts = [];
|
|
1097
2470
|
for (const [lang, st] of this.langStatus) {
|
|
1098
2471
|
if (st.done) {
|
|
1099
|
-
parts.push(
|
|
2472
|
+
parts.push(chalk9.green(`${lang} \u2713`));
|
|
1100
2473
|
} else if (st.phase === "waiting") {
|
|
1101
|
-
parts.push(
|
|
2474
|
+
parts.push(chalk9.dim(`${lang} \xB7`));
|
|
1102
2475
|
} else if (st.phase === "discovering") {
|
|
1103
|
-
parts.push(
|
|
2476
|
+
parts.push(chalk9.yellow(`${lang} \u2026`));
|
|
1104
2477
|
} else if (st.phase === "scanning" && st.fileCount > 0) {
|
|
1105
2478
|
const pct = Math.round(st.fileIndex / st.fileCount * 100);
|
|
1106
|
-
parts.push(
|
|
2479
|
+
parts.push(chalk9.cyan(`${lang} ${st.fileIndex}/${st.fileCount} ${pct}%`));
|
|
1107
2480
|
} else if (st.phase === "extracting") {
|
|
1108
|
-
parts.push(
|
|
2481
|
+
parts.push(chalk9.cyan(`${lang} extracting`));
|
|
1109
2482
|
} else {
|
|
1110
|
-
parts.push(
|
|
2483
|
+
parts.push(chalk9.dim(`${lang} ${st.phase}`));
|
|
1111
2484
|
}
|
|
1112
2485
|
}
|
|
1113
|
-
const line = ` ${parts.join(" ")} ${
|
|
2486
|
+
const line = ` ${parts.join(" ")} ${chalk9.dim(`${this.totalFacts} facts`)}`;
|
|
1114
2487
|
const padding = Math.max(0, this.lastLineLen - line.length);
|
|
1115
2488
|
process.stderr.write(`\r${line}${" ".repeat(padding)}`);
|
|
1116
2489
|
this.lastLineLen = line.length;
|
|
@@ -1122,10 +2495,10 @@ var ProgressTracker = class {
|
|
|
1122
2495
|
}
|
|
1123
2496
|
}
|
|
1124
2497
|
};
|
|
1125
|
-
var extractCommand = new
|
|
1126
|
-
const rootDir =
|
|
2498
|
+
var extractCommand = new Command8("extract").description("Analyze source code and emit validated HCS facts (NDJSON)").argument("[path]", "Path to source directory", ".").option("-o, --out <file>", "Write NDJSON to file (default: stdout)").option("--language <langs>", "Comma-separated languages to analyze (default: auto-detect)").option("--include-tests", "Include test files in analysis").option("--push", "Stream validated facts to dashboard API").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--concurrency <n>", "Number of parallel file workers").option("--timeout-mins <mins>", "Total analysis timeout in minutes", "60").option("--feedback <file>", "Load NDJSON diff artifact for refinement").option("--verbose", "Print worker stderr and summary statistics").action(async (targetPath, opts) => {
|
|
2499
|
+
const rootDir = path9.resolve(targetPath);
|
|
1127
2500
|
if (!await pathExists(rootDir)) {
|
|
1128
|
-
process.stderr.write(
|
|
2501
|
+
process.stderr.write(chalk9.red(`Path does not exist: ${rootDir}
|
|
1129
2502
|
`));
|
|
1130
2503
|
process.exit(EXIT_USAGE_ERROR);
|
|
1131
2504
|
}
|
|
@@ -1133,47 +2506,47 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1133
2506
|
try {
|
|
1134
2507
|
stat3 = await fs2.stat(rootDir);
|
|
1135
2508
|
} catch {
|
|
1136
|
-
process.stderr.write(
|
|
2509
|
+
process.stderr.write(chalk9.red(`Cannot read path: ${rootDir}
|
|
1137
2510
|
`));
|
|
1138
2511
|
process.exit(EXIT_USAGE_ERROR);
|
|
1139
2512
|
}
|
|
1140
2513
|
if (!stat3.isDirectory()) {
|
|
1141
|
-
process.stderr.write(
|
|
2514
|
+
process.stderr.write(chalk9.red(`Path must be a directory: ${rootDir}
|
|
1142
2515
|
`));
|
|
1143
2516
|
process.exit(EXIT_USAGE_ERROR);
|
|
1144
2517
|
}
|
|
1145
2518
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
1146
2519
|
if (opts.push && !dsn) {
|
|
1147
|
-
process.stderr.write(
|
|
2520
|
+
process.stderr.write(chalk9.red("--push requires --dsn or VIBGRATE_DSN environment variable\n"));
|
|
1148
2521
|
process.exit(EXIT_USAGE_ERROR);
|
|
1149
2522
|
}
|
|
1150
|
-
if (dsn && !
|
|
1151
|
-
process.stderr.write(
|
|
1152
|
-
process.stderr.write(
|
|
2523
|
+
if (dsn && !parseDsn2(dsn)) {
|
|
2524
|
+
process.stderr.write(chalk9.red("Invalid DSN format.\n"));
|
|
2525
|
+
process.stderr.write(chalk9.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
1153
2526
|
process.exit(EXIT_USAGE_ERROR);
|
|
1154
2527
|
}
|
|
1155
2528
|
const concurrency = opts.concurrency ? parseInt(opts.concurrency, 10) : os2.cpus().length;
|
|
1156
2529
|
if (isNaN(concurrency) || concurrency < 1) {
|
|
1157
|
-
process.stderr.write(
|
|
2530
|
+
process.stderr.write(chalk9.red("--concurrency must be >= 1\n"));
|
|
1158
2531
|
process.exit(EXIT_USAGE_ERROR);
|
|
1159
2532
|
}
|
|
1160
2533
|
const timeoutMins = parseInt(opts.timeoutMins, 10);
|
|
1161
2534
|
if (isNaN(timeoutMins) || timeoutMins < 1) {
|
|
1162
|
-
process.stderr.write(
|
|
2535
|
+
process.stderr.write(chalk9.red("--timeout-mins must be >= 1\n"));
|
|
1163
2536
|
process.exit(EXIT_USAGE_ERROR);
|
|
1164
2537
|
}
|
|
1165
2538
|
const timeoutMs = timeoutMins * 60 * 1e3;
|
|
1166
2539
|
if (opts.out) {
|
|
1167
|
-
const outDir =
|
|
2540
|
+
const outDir = path9.dirname(path9.resolve(opts.out));
|
|
1168
2541
|
if (!await pathExists(outDir)) {
|
|
1169
|
-
process.stderr.write(
|
|
2542
|
+
process.stderr.write(chalk9.red(`Output directory does not exist: ${outDir}
|
|
1170
2543
|
`));
|
|
1171
2544
|
process.exit(EXIT_USAGE_ERROR);
|
|
1172
2545
|
}
|
|
1173
2546
|
try {
|
|
1174
|
-
const outStat = await fs2.stat(
|
|
2547
|
+
const outStat = await fs2.stat(path9.resolve(opts.out));
|
|
1175
2548
|
if (outStat.isDirectory()) {
|
|
1176
|
-
process.stderr.write(
|
|
2549
|
+
process.stderr.write(chalk9.red(`--out cannot be a directory: ${opts.out}
|
|
1177
2550
|
`));
|
|
1178
2551
|
process.exit(EXIT_USAGE_ERROR);
|
|
1179
2552
|
}
|
|
@@ -1182,16 +2555,16 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1182
2555
|
}
|
|
1183
2556
|
let feedbackLines;
|
|
1184
2557
|
if (opts.feedback) {
|
|
1185
|
-
const feedbackPath =
|
|
2558
|
+
const feedbackPath = path9.resolve(opts.feedback);
|
|
1186
2559
|
if (!await pathExists(feedbackPath)) {
|
|
1187
|
-
process.stderr.write(
|
|
2560
|
+
process.stderr.write(chalk9.red(`Feedback file not found: ${feedbackPath}
|
|
1188
2561
|
`));
|
|
1189
2562
|
process.exit(EXIT_USAGE_ERROR);
|
|
1190
2563
|
}
|
|
1191
2564
|
try {
|
|
1192
2565
|
feedbackLines = await loadFeedback(feedbackPath);
|
|
1193
2566
|
} catch (err) {
|
|
1194
|
-
process.stderr.write(
|
|
2567
|
+
process.stderr.write(chalk9.red(`Invalid feedback file: ${err instanceof Error ? err.message : String(err)}
|
|
1195
2568
|
`));
|
|
1196
2569
|
process.exit(EXIT_USAGE_ERROR);
|
|
1197
2570
|
}
|
|
@@ -1201,9 +2574,9 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1201
2574
|
targetLanguages = opts.language.split(",").map((l) => normalizeLanguageToken(l)).filter(Boolean);
|
|
1202
2575
|
for (const lang of targetLanguages) {
|
|
1203
2576
|
if (!SUPPORTED_LANGUAGES.has(lang)) {
|
|
1204
|
-
process.stderr.write(
|
|
2577
|
+
process.stderr.write(chalk9.red(`Unknown language: "${lang}"
|
|
1205
2578
|
`));
|
|
1206
|
-
process.stderr.write(
|
|
2579
|
+
process.stderr.write(chalk9.dim(`Supported: ${[...SUPPORTED_LANGUAGES].sort().join(", ")}
|
|
1207
2580
|
`));
|
|
1208
2581
|
process.exit(EXIT_USAGE_ERROR);
|
|
1209
2582
|
}
|
|
@@ -1211,14 +2584,14 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1211
2584
|
} else {
|
|
1212
2585
|
const detected = await detectLanguages(rootDir, opts.includeTests ?? false);
|
|
1213
2586
|
if (detected.length === 0) {
|
|
1214
|
-
process.stderr.write(
|
|
2587
|
+
process.stderr.write(chalk9.yellow("No supported source files detected.\n"));
|
|
1215
2588
|
process.exit(EXIT_SUCCESS);
|
|
1216
2589
|
}
|
|
1217
2590
|
targetLanguages = detected.map((d) => d.language);
|
|
1218
2591
|
if (opts.verbose) {
|
|
1219
|
-
process.stderr.write(
|
|
2592
|
+
process.stderr.write(chalk9.dim("Detected languages:\n"));
|
|
1220
2593
|
for (const d of detected) {
|
|
1221
|
-
process.stderr.write(
|
|
2594
|
+
process.stderr.write(chalk9.dim(` ${d.language}: ${d.fileCount} files
|
|
1222
2595
|
`));
|
|
1223
2596
|
}
|
|
1224
2597
|
}
|
|
@@ -1226,20 +2599,20 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1226
2599
|
const runnableLanguages = targetLanguages.filter((l) => ALL_WORKER_LANGS.has(l));
|
|
1227
2600
|
const unknownWorkerLangs = targetLanguages.filter((l) => !ALL_WORKER_LANGS.has(l));
|
|
1228
2601
|
if (unknownWorkerLangs.length > 0 && opts.verbose) {
|
|
1229
|
-
process.stderr.write(
|
|
2602
|
+
process.stderr.write(chalk9.dim(
|
|
1230
2603
|
`No worker registered for: ${unknownWorkerLangs.join(", ")} \u2014 skipping.
|
|
1231
2604
|
`
|
|
1232
2605
|
));
|
|
1233
2606
|
}
|
|
1234
2607
|
if (runnableLanguages.length === 0) {
|
|
1235
|
-
process.stderr.write(
|
|
2608
|
+
process.stderr.write(chalk9.yellow("No languages with available HCS workers found.\n"));
|
|
1236
2609
|
process.exit(EXIT_SUCCESS);
|
|
1237
2610
|
}
|
|
1238
2611
|
const startTime = Date.now();
|
|
1239
2612
|
const globalDeadline = startTime + timeoutMs;
|
|
1240
2613
|
process.stderr.write(
|
|
1241
|
-
|
|
1242
|
-
`) +
|
|
2614
|
+
chalk9.bold(`Extracting HCS facts from ${rootDir}
|
|
2615
|
+
`) + chalk9.dim(`Languages: ${runnableLanguages.join(", ")} Concurrency: ${concurrency} Timeout: ${timeoutMins}m
|
|
1243
2616
|
`)
|
|
1244
2617
|
);
|
|
1245
2618
|
const progress = new ProgressTracker(runnableLanguages);
|
|
@@ -1279,7 +2652,7 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1279
2652
|
hasSchemaFailure = true;
|
|
1280
2653
|
allErrors.push(`[${language}] Schema validation: ${validation.error}`);
|
|
1281
2654
|
if (opts.verbose) {
|
|
1282
|
-
process.stderr.write(
|
|
2655
|
+
process.stderr.write(chalk9.red(`[${language}] Invalid fact: ${validation.error}
|
|
1283
2656
|
`));
|
|
1284
2657
|
}
|
|
1285
2658
|
}
|
|
@@ -1307,9 +2680,9 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1307
2680
|
const allLines = [...allPreamble, ...allFacts];
|
|
1308
2681
|
const ndjsonOutput = allLines.join("\n") + (allLines.length > 0 ? "\n" : "");
|
|
1309
2682
|
if (opts.out) {
|
|
1310
|
-
const outPath =
|
|
2683
|
+
const outPath = path9.resolve(opts.out);
|
|
1311
2684
|
await fs2.writeFile(outPath, ndjsonOutput, "utf-8");
|
|
1312
|
-
process.stderr.write(
|
|
2685
|
+
process.stderr.write(chalk9.green(`\u2714 Wrote ${allFacts.length} facts to ${outPath}
|
|
1313
2686
|
`));
|
|
1314
2687
|
} else {
|
|
1315
2688
|
process.stdout.write(ndjsonOutput);
|
|
@@ -1321,48 +2694,48 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1321
2694
|
}
|
|
1322
2695
|
}
|
|
1323
2696
|
if (opts.verbose) {
|
|
1324
|
-
process.stderr.write(
|
|
1325
|
-
process.stderr.write(
|
|
2697
|
+
process.stderr.write(chalk9.dim("\n\u2500\u2500 Summary \u2500\u2500\n"));
|
|
2698
|
+
process.stderr.write(chalk9.dim(` Facts emitted : ${allFacts.length}
|
|
1326
2699
|
`));
|
|
1327
|
-
process.stderr.write(
|
|
2700
|
+
process.stderr.write(chalk9.dim(` Languages : ${runnableLanguages.join(", ")}
|
|
1328
2701
|
`));
|
|
1329
|
-
process.stderr.write(
|
|
2702
|
+
process.stderr.write(chalk9.dim(` Elapsed : ${elapsed}s
|
|
1330
2703
|
`));
|
|
1331
2704
|
if (allErrors.length > 0) {
|
|
1332
|
-
process.stderr.write(
|
|
2705
|
+
process.stderr.write(chalk9.dim(` Errors : ${allErrors.length}
|
|
1333
2706
|
`));
|
|
1334
2707
|
for (const err of allErrors.slice(0, 10)) {
|
|
1335
|
-
process.stderr.write(
|
|
2708
|
+
process.stderr.write(chalk9.dim(` ${err}
|
|
1336
2709
|
`));
|
|
1337
2710
|
}
|
|
1338
2711
|
if (allErrors.length > 10) {
|
|
1339
|
-
process.stderr.write(
|
|
2712
|
+
process.stderr.write(chalk9.dim(` ... and ${allErrors.length - 10} more
|
|
1340
2713
|
`));
|
|
1341
2714
|
}
|
|
1342
2715
|
}
|
|
1343
|
-
process.stderr.write(
|
|
2716
|
+
process.stderr.write(chalk9.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n"));
|
|
1344
2717
|
}
|
|
1345
2718
|
if (hasTimeout) {
|
|
1346
|
-
process.stderr.write(
|
|
2719
|
+
process.stderr.write(chalk9.red(`Timeout exceeded (${timeoutMins} minutes)
|
|
1347
2720
|
`));
|
|
1348
2721
|
process.exit(EXIT_TIMEOUT);
|
|
1349
2722
|
}
|
|
1350
2723
|
if (hasSchemaFailure) {
|
|
1351
|
-
process.stderr.write(
|
|
2724
|
+
process.stderr.write(chalk9.red("Fact schema validation failures detected\n"));
|
|
1352
2725
|
process.exit(EXIT_SCHEMA_FAILURE);
|
|
1353
2726
|
}
|
|
1354
2727
|
if (hasParseFailure) {
|
|
1355
|
-
process.stderr.write(
|
|
2728
|
+
process.stderr.write(chalk9.red("Parsing failures detected\n"));
|
|
1356
2729
|
process.exit(EXIT_PARSE_FAILURE);
|
|
1357
2730
|
}
|
|
1358
2731
|
});
|
|
1359
2732
|
|
|
1360
2733
|
// src/commands/diff.ts
|
|
1361
|
-
import * as
|
|
2734
|
+
import * as path10 from "path";
|
|
1362
2735
|
import * as fs3 from "fs/promises";
|
|
1363
|
-
import * as
|
|
1364
|
-
import { Command as
|
|
1365
|
-
import
|
|
2736
|
+
import * as crypto3 from "crypto";
|
|
2737
|
+
import { Command as Command9 } from "commander";
|
|
2738
|
+
import chalk10 from "chalk";
|
|
1366
2739
|
var EXIT_INVALID_PATH = 2;
|
|
1367
2740
|
var EXIT_USAGE_ERROR2 = 5;
|
|
1368
2741
|
var VALID_FORMATS = /* @__PURE__ */ new Set(["ndjson", "json", "patch"]);
|
|
@@ -1384,8 +2757,8 @@ async function discoverFiles(root, includeGlobs, excludeGlobs) {
|
|
|
1384
2757
|
}
|
|
1385
2758
|
for (const entry of entries) {
|
|
1386
2759
|
if (entry.name.startsWith(".") && SKIP_DIRS2.has(entry.name)) continue;
|
|
1387
|
-
const absPath =
|
|
1388
|
-
const relPath =
|
|
2760
|
+
const absPath = path10.join(dir, entry.name);
|
|
2761
|
+
const relPath = path10.join(relBase, entry.name);
|
|
1389
2762
|
if (entry.isDirectory()) {
|
|
1390
2763
|
if (!SKIP_DIRS2.has(entry.name)) {
|
|
1391
2764
|
await walk(absPath, relPath);
|
|
@@ -1556,7 +2929,7 @@ function lcsRatio(a, b) {
|
|
|
1556
2929
|
return 2 * lcsLen / (n + m);
|
|
1557
2930
|
}
|
|
1558
2931
|
function generateDeltaId(kind, filePath) {
|
|
1559
|
-
const hash =
|
|
2932
|
+
const hash = crypto3.createHash("sha256").update(`${kind}:${filePath}`).digest("hex").substring(0, 16);
|
|
1560
2933
|
return `hcs:delta:${hash}`;
|
|
1561
2934
|
}
|
|
1562
2935
|
function formatNdjson(deltas) {
|
|
@@ -1568,8 +2941,8 @@ function formatJson(deltas) {
|
|
|
1568
2941
|
function formatPatch(deltas, originalPath, generatedPath) {
|
|
1569
2942
|
const lines = [];
|
|
1570
2943
|
for (const delta of deltas) {
|
|
1571
|
-
const aFile = delta.originalPath ?
|
|
1572
|
-
const bFile = delta.generatedPath ?
|
|
2944
|
+
const aFile = delta.originalPath ? path10.join("a", delta.originalPath) : "/dev/null";
|
|
2945
|
+
const bFile = delta.generatedPath ? path10.join("b", delta.generatedPath) : "/dev/null";
|
|
1573
2946
|
if (delta.changeKind === "renamed" && delta.originalPath && delta.generatedPath) {
|
|
1574
2947
|
lines.push(`diff --vibgrate ${aFile} ${bFile}`);
|
|
1575
2948
|
lines.push(`similarity index ${Math.round((delta.similarity ?? 0) * 100)}%`);
|
|
@@ -1590,12 +2963,12 @@ function formatPatch(deltas, originalPath, generatedPath) {
|
|
|
1590
2963
|
}
|
|
1591
2964
|
return lines.join("\n");
|
|
1592
2965
|
}
|
|
1593
|
-
var diffCommand = new
|
|
1594
|
-
const origDir =
|
|
1595
|
-
const genDir =
|
|
2966
|
+
var diffCommand = new Command9("diff").description("Compare two directory trees and emit structured delta facts").argument("<original_path>", "Original source directory").argument("<generated_path>", "Generated source directory").option("-o, --out <file>", "Output file path", "vibgrate.diff.ndjson").option("--format <fmt>", "Output format (ndjson|json|patch)", "ndjson").option("--ignore-whitespace", "Ignore whitespace-only changes").option("--context <n>", "Context lines for patch output", "3").option("--include <globs>", "Only include matching files (comma-separated)").option("--exclude <globs>", "Exclude matching files (comma-separated)").option("--stats", "Include insertions/deletions summary per file").option("--verbose", "Print file match decisions and rename detection").action(async (originalPath, generatedPath, opts) => {
|
|
2967
|
+
const origDir = path10.resolve(originalPath);
|
|
2968
|
+
const genDir = path10.resolve(generatedPath);
|
|
1596
2969
|
for (const [label, dir] of [["original_path", origDir], ["generated_path", genDir]]) {
|
|
1597
2970
|
if (!await pathExists(dir)) {
|
|
1598
|
-
process.stderr.write(
|
|
2971
|
+
process.stderr.write(chalk10.red(`${label} does not exist: ${dir}
|
|
1599
2972
|
`));
|
|
1600
2973
|
process.exit(EXIT_INVALID_PATH);
|
|
1601
2974
|
}
|
|
@@ -1603,25 +2976,25 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1603
2976
|
try {
|
|
1604
2977
|
stat3 = await fs3.stat(dir);
|
|
1605
2978
|
} catch {
|
|
1606
|
-
process.stderr.write(
|
|
2979
|
+
process.stderr.write(chalk10.red(`Cannot read ${label}: ${dir}
|
|
1607
2980
|
`));
|
|
1608
2981
|
process.exit(EXIT_INVALID_PATH);
|
|
1609
2982
|
}
|
|
1610
2983
|
if (!stat3.isDirectory()) {
|
|
1611
|
-
process.stderr.write(
|
|
2984
|
+
process.stderr.write(chalk10.red(`${label} must be a directory: ${dir}
|
|
1612
2985
|
`));
|
|
1613
2986
|
process.exit(EXIT_INVALID_PATH);
|
|
1614
2987
|
}
|
|
1615
2988
|
}
|
|
1616
2989
|
const format = opts.format.toLowerCase();
|
|
1617
2990
|
if (!VALID_FORMATS.has(format)) {
|
|
1618
|
-
process.stderr.write(
|
|
2991
|
+
process.stderr.write(chalk10.red(`Invalid format: "${opts.format}". Allowed: ndjson, json, patch
|
|
1619
2992
|
`));
|
|
1620
2993
|
process.exit(EXIT_USAGE_ERROR2);
|
|
1621
2994
|
}
|
|
1622
2995
|
const contextLines = parseInt(opts.context, 10);
|
|
1623
2996
|
if (isNaN(contextLines) || contextLines < 0) {
|
|
1624
|
-
process.stderr.write(
|
|
2997
|
+
process.stderr.write(chalk10.red("--context must be >= 0\n"));
|
|
1625
2998
|
process.exit(EXIT_USAGE_ERROR2);
|
|
1626
2999
|
}
|
|
1627
3000
|
const includeGlobs = opts.include ? opts.include.split(",").map((g) => g.trim()).filter(Boolean) : void 0;
|
|
@@ -1633,9 +3006,9 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1633
3006
|
const origMap = new Map(origFiles.map((f) => [f.relPath, f]));
|
|
1634
3007
|
const genMap = new Map(genFiles.map((f) => [f.relPath, f]));
|
|
1635
3008
|
if (opts.verbose) {
|
|
1636
|
-
process.stderr.write(
|
|
3009
|
+
process.stderr.write(chalk10.dim(`Original: ${origFiles.length} files
|
|
1637
3010
|
`));
|
|
1638
|
-
process.stderr.write(
|
|
3011
|
+
process.stderr.write(chalk10.dim(`Generated: ${genFiles.length} files
|
|
1639
3012
|
`));
|
|
1640
3013
|
}
|
|
1641
3014
|
const deltas = [];
|
|
@@ -1669,7 +3042,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1669
3042
|
}
|
|
1670
3043
|
deltas.push(delta);
|
|
1671
3044
|
if (opts.verbose) {
|
|
1672
|
-
process.stderr.write(
|
|
3045
|
+
process.stderr.write(chalk10.dim(` modified: ${relPath} (+${diffResult.insertions} -${diffResult.deletions})
|
|
1673
3046
|
`));
|
|
1674
3047
|
}
|
|
1675
3048
|
}
|
|
@@ -1700,7 +3073,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1700
3073
|
const pairs = [];
|
|
1701
3074
|
for (const [origPath, origContent] of origContents) {
|
|
1702
3075
|
for (const [genPath, genContent] of genContents) {
|
|
1703
|
-
if (
|
|
3076
|
+
if (path10.extname(origPath) !== path10.extname(genPath)) continue;
|
|
1704
3077
|
const similarity = computeSimilarity(origContent, genContent);
|
|
1705
3078
|
if (similarity >= RENAME_THRESHOLD) {
|
|
1706
3079
|
pairs.push({ origPath, genPath, similarity });
|
|
@@ -1733,7 +3106,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1733
3106
|
}
|
|
1734
3107
|
deltas.push(delta);
|
|
1735
3108
|
if (opts.verbose) {
|
|
1736
|
-
process.stderr.write(
|
|
3109
|
+
process.stderr.write(chalk10.dim(
|
|
1737
3110
|
` renamed: ${pair.origPath} \u2192 ${pair.genPath} (${(pair.similarity * 100).toFixed(0)}% similar)
|
|
1738
3111
|
`
|
|
1739
3112
|
));
|
|
@@ -1758,7 +3131,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1758
3131
|
}
|
|
1759
3132
|
deltas.push(delta);
|
|
1760
3133
|
if (opts.verbose) {
|
|
1761
|
-
process.stderr.write(
|
|
3134
|
+
process.stderr.write(chalk10.dim(` removed: ${file.relPath}
|
|
1762
3135
|
`));
|
|
1763
3136
|
}
|
|
1764
3137
|
}
|
|
@@ -1780,7 +3153,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1780
3153
|
}
|
|
1781
3154
|
deltas.push(delta);
|
|
1782
3155
|
if (opts.verbose) {
|
|
1783
|
-
process.stderr.write(
|
|
3156
|
+
process.stderr.write(chalk10.dim(` added: ${file.relPath}
|
|
1784
3157
|
`));
|
|
1785
3158
|
}
|
|
1786
3159
|
}
|
|
@@ -1801,16 +3174,16 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1801
3174
|
output = formatPatch(deltas, originalPath, generatedPath);
|
|
1802
3175
|
break;
|
|
1803
3176
|
}
|
|
1804
|
-
const outPath =
|
|
3177
|
+
const outPath = path10.resolve(opts.out);
|
|
1805
3178
|
await fs3.writeFile(outPath, output, "utf-8");
|
|
1806
3179
|
const added = deltas.filter((d) => d.changeKind === "added").length;
|
|
1807
3180
|
const removed = deltas.filter((d) => d.changeKind === "removed").length;
|
|
1808
3181
|
const modified = deltas.filter((d) => d.changeKind === "modified").length;
|
|
1809
3182
|
const renamed = deltas.filter((d) => d.changeKind === "renamed").length;
|
|
1810
3183
|
process.stderr.write(
|
|
1811
|
-
|
|
3184
|
+
chalk10.green(`\u2714 `) + `${deltas.length} changes: ` + chalk10.green(`+${added} added`) + ", " + chalk10.red(`-${removed} removed`) + ", " + chalk10.yellow(`~${modified} modified`) + ", " + chalk10.blue(`\u2192${renamed} renamed`) + "\n"
|
|
1812
3185
|
);
|
|
1813
|
-
process.stderr.write(
|
|
3186
|
+
process.stderr.write(chalk10.dim(` Written to ${outPath}
|
|
1814
3187
|
`));
|
|
1815
3188
|
if (opts.verbose && opts.stats) {
|
|
1816
3189
|
let totalIns = 0;
|
|
@@ -1821,193 +3194,193 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1821
3194
|
totalDel += d.stats.deletions;
|
|
1822
3195
|
}
|
|
1823
3196
|
}
|
|
1824
|
-
process.stderr.write(
|
|
3197
|
+
process.stderr.write(chalk10.dim(` Total: +${totalIns} insertions, -${totalDel} deletions
|
|
1825
3198
|
`));
|
|
1826
3199
|
}
|
|
1827
3200
|
});
|
|
1828
3201
|
|
|
1829
3202
|
// src/commands/help.ts
|
|
1830
|
-
import { Command as
|
|
1831
|
-
import
|
|
3203
|
+
import { Command as Command10 } from "commander";
|
|
3204
|
+
import chalk11 from "chalk";
|
|
1832
3205
|
var HELP_URL = "https://vibgrate.com/help";
|
|
1833
3206
|
function printFooter() {
|
|
1834
3207
|
console.log("");
|
|
1835
|
-
console.log(
|
|
3208
|
+
console.log(chalk11.dim(`See ${HELP_URL} for more guidance`));
|
|
1836
3209
|
}
|
|
1837
3210
|
var detailedHelp = {
|
|
1838
3211
|
scan: () => {
|
|
1839
3212
|
console.log("");
|
|
1840
|
-
console.log(
|
|
3213
|
+
console.log(chalk11.bold.underline("vibgrate scan") + chalk11.dim(" \u2014 Scan a project for upgrade drift"));
|
|
1841
3214
|
console.log("");
|
|
1842
|
-
console.log(
|
|
3215
|
+
console.log(chalk11.bold("Usage:"));
|
|
1843
3216
|
console.log(" vibgrate scan [path] [options]");
|
|
1844
3217
|
console.log("");
|
|
1845
|
-
console.log(
|
|
1846
|
-
console.log(` ${
|
|
3218
|
+
console.log(chalk11.bold("Arguments:"));
|
|
3219
|
+
console.log(` ${chalk11.cyan("[path]")} Path to scan (default: current directory)`);
|
|
1847
3220
|
console.log("");
|
|
1848
|
-
console.log(
|
|
1849
|
-
console.log(` ${
|
|
1850
|
-
console.log(` ${
|
|
3221
|
+
console.log(chalk11.bold("Output options:"));
|
|
3222
|
+
console.log(` ${chalk11.cyan("--format <format>")} Output format: ${chalk11.white("text")} | json | sarif | md (default: text)`);
|
|
3223
|
+
console.log(` ${chalk11.cyan("--out <file>")} Write output to a file instead of stdout`);
|
|
1851
3224
|
console.log("");
|
|
1852
|
-
console.log(
|
|
1853
|
-
console.log(` ${
|
|
1854
|
-
console.log(` ${
|
|
1855
|
-
console.log(` ${
|
|
1856
|
-
console.log(` ${
|
|
3225
|
+
console.log(chalk11.bold("Baseline & gating:"));
|
|
3226
|
+
console.log(` ${chalk11.cyan("--baseline <file>")} Compare results against a saved baseline`);
|
|
3227
|
+
console.log(` ${chalk11.cyan("--drift-budget <score>")} Fail if drift score exceeds this value (0\u2013100)`);
|
|
3228
|
+
console.log(` ${chalk11.cyan("--drift-worsening <percent>")} Fail if drift worsens by more than % since baseline`);
|
|
3229
|
+
console.log(` ${chalk11.cyan("--fail-on <level>")} Fail exit code on warn or error findings`);
|
|
1857
3230
|
console.log("");
|
|
1858
|
-
console.log(
|
|
1859
|
-
console.log(` ${
|
|
1860
|
-
console.log(` ${
|
|
3231
|
+
console.log(chalk11.bold("Performance:"));
|
|
3232
|
+
console.log(` ${chalk11.cyan("--concurrency <n>")} Max concurrent registry calls (default: 8)`);
|
|
3233
|
+
console.log(` ${chalk11.cyan("--changed-only")} Only scan files changed since last git commit`);
|
|
1861
3234
|
console.log("");
|
|
1862
|
-
console.log(
|
|
1863
|
-
console.log(` ${
|
|
1864
|
-
console.log(` ${
|
|
1865
|
-
console.log(` ${
|
|
1866
|
-
console.log(` ${
|
|
3235
|
+
console.log(chalk11.bold("Privacy & offline:"));
|
|
3236
|
+
console.log(` ${chalk11.cyan("--offline")} Run without any network calls; skip result upload`);
|
|
3237
|
+
console.log(` ${chalk11.cyan("--package-manifest <file>")} Use a local package-version manifest (JSON or ZIP) for offline mode`);
|
|
3238
|
+
console.log(` ${chalk11.cyan("--no-local-artifacts")} Do not write .vibgrate JSON artifacts to disk`);
|
|
3239
|
+
console.log(` ${chalk11.cyan("--max-privacy")} Strongest privacy mode: minimal scanners + no local artifacts`);
|
|
1867
3240
|
console.log("");
|
|
1868
|
-
console.log(
|
|
1869
|
-
console.log(` ${
|
|
1870
|
-
console.log(` ${
|
|
3241
|
+
console.log(chalk11.bold("Tooling:"));
|
|
3242
|
+
console.log(` ${chalk11.cyan("--install-tools")} Auto-install missing security scanners via Homebrew`);
|
|
3243
|
+
console.log(` ${chalk11.cyan("--ui-purpose")} Enable UI purpose evidence extraction (slower)`);
|
|
1871
3244
|
console.log("");
|
|
1872
|
-
console.log(
|
|
1873
|
-
console.log(` ${
|
|
1874
|
-
console.log(` ${
|
|
1875
|
-
console.log(` ${
|
|
1876
|
-
console.log(` ${
|
|
3245
|
+
console.log(chalk11.bold("Uploading results:"));
|
|
3246
|
+
console.log(` ${chalk11.cyan("--push")} Auto-push results to Vibgrate API after scan`);
|
|
3247
|
+
console.log(` ${chalk11.cyan("--dsn <dsn>")} DSN token for push (or set VIBGRATE_DSN env var)`);
|
|
3248
|
+
console.log(` ${chalk11.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
3249
|
+
console.log(` ${chalk11.cyan("--strict")} Fail if the upload to Vibgrate API fails`);
|
|
1877
3250
|
console.log("");
|
|
1878
|
-
console.log(
|
|
1879
|
-
console.log(` ${
|
|
3251
|
+
console.log(chalk11.bold("Examples:"));
|
|
3252
|
+
console.log(` ${chalk11.dim("# Scan the current directory and display a text report")}`);
|
|
1880
3253
|
console.log(" vibgrate scan .");
|
|
1881
3254
|
console.log("");
|
|
1882
|
-
console.log(` ${
|
|
3255
|
+
console.log(` ${chalk11.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`);
|
|
1883
3256
|
console.log(" vibgrate scan . --drift-budget 40 --format sarif --out drift.sarif");
|
|
1884
3257
|
console.log("");
|
|
1885
|
-
console.log(` ${
|
|
3258
|
+
console.log(` ${chalk11.dim("# Scan and automatically upload results via a DSN")}`);
|
|
1886
3259
|
console.log(" vibgrate scan . --push --dsn $VIBGRATE_DSN");
|
|
1887
3260
|
console.log("");
|
|
1888
|
-
console.log(` ${
|
|
3261
|
+
console.log(` ${chalk11.dim("# Offline scan using a pre-downloaded package manifest")}`);
|
|
1889
3262
|
console.log(" vibgrate scan . --offline --package-manifest ./manifest.zip");
|
|
1890
3263
|
},
|
|
1891
3264
|
init: () => {
|
|
1892
3265
|
console.log("");
|
|
1893
|
-
console.log(
|
|
3266
|
+
console.log(chalk11.bold.underline("vibgrate init") + chalk11.dim(" \u2014 Initialise vibgrate in a project directory"));
|
|
1894
3267
|
console.log("");
|
|
1895
|
-
console.log(
|
|
3268
|
+
console.log(chalk11.bold("Usage:"));
|
|
1896
3269
|
console.log(" vibgrate init [path] [options]");
|
|
1897
3270
|
console.log("");
|
|
1898
|
-
console.log(
|
|
1899
|
-
console.log(` ${
|
|
3271
|
+
console.log(chalk11.bold("Arguments:"));
|
|
3272
|
+
console.log(` ${chalk11.cyan("[path]")} Directory to initialise (default: current directory)`);
|
|
1900
3273
|
console.log("");
|
|
1901
|
-
console.log(
|
|
1902
|
-
console.log(` ${
|
|
1903
|
-
console.log(` ${
|
|
3274
|
+
console.log(chalk11.bold("Options:"));
|
|
3275
|
+
console.log(` ${chalk11.cyan("--baseline")} Create an initial drift baseline after init`);
|
|
3276
|
+
console.log(` ${chalk11.cyan("--yes")} Skip all confirmation prompts`);
|
|
1904
3277
|
console.log("");
|
|
1905
|
-
console.log(
|
|
3278
|
+
console.log(chalk11.bold("What it does:"));
|
|
1906
3279
|
console.log(" \u2022 Creates a .vibgrate/ directory");
|
|
1907
3280
|
console.log(" \u2022 Writes a vibgrate.config.ts starter config");
|
|
1908
3281
|
console.log(" \u2022 Optionally runs an initial baseline scan (--baseline)");
|
|
1909
3282
|
console.log("");
|
|
1910
|
-
console.log(
|
|
3283
|
+
console.log(chalk11.bold("Examples:"));
|
|
1911
3284
|
console.log(" vibgrate init");
|
|
1912
3285
|
console.log(" vibgrate init ./my-project --baseline");
|
|
1913
3286
|
},
|
|
1914
3287
|
baseline: () => {
|
|
1915
3288
|
console.log("");
|
|
1916
|
-
console.log(
|
|
3289
|
+
console.log(chalk11.bold.underline("vibgrate baseline") + chalk11.dim(" \u2014 Save a drift baseline snapshot"));
|
|
1917
3290
|
console.log("");
|
|
1918
|
-
console.log(
|
|
3291
|
+
console.log(chalk11.bold("Usage:"));
|
|
1919
3292
|
console.log(" vibgrate baseline [path]");
|
|
1920
3293
|
console.log("");
|
|
1921
|
-
console.log(
|
|
1922
|
-
console.log(` ${
|
|
3294
|
+
console.log(chalk11.bold("Arguments:"));
|
|
3295
|
+
console.log(` ${chalk11.cyan("[path]")} Path to baseline (default: current directory)`);
|
|
1923
3296
|
console.log("");
|
|
1924
|
-
console.log(
|
|
3297
|
+
console.log(chalk11.bold("What it does:"));
|
|
1925
3298
|
console.log(" Runs a full scan and saves the result as .vibgrate/baseline.json.");
|
|
1926
3299
|
console.log(" Future scans can compare against this file using --baseline.");
|
|
1927
3300
|
console.log("");
|
|
1928
|
-
console.log(
|
|
3301
|
+
console.log(chalk11.bold("Examples:"));
|
|
1929
3302
|
console.log(" vibgrate baseline .");
|
|
1930
3303
|
console.log(" vibgrate scan . --baseline .vibgrate/baseline.json --drift-worsening 10");
|
|
1931
3304
|
},
|
|
1932
3305
|
report: () => {
|
|
1933
3306
|
console.log("");
|
|
1934
|
-
console.log(
|
|
3307
|
+
console.log(chalk11.bold.underline("vibgrate report") + chalk11.dim(" \u2014 Generate a report from a saved scan artifact"));
|
|
1935
3308
|
console.log("");
|
|
1936
|
-
console.log(
|
|
3309
|
+
console.log(chalk11.bold("Usage:"));
|
|
1937
3310
|
console.log(" vibgrate report [options]");
|
|
1938
3311
|
console.log("");
|
|
1939
|
-
console.log(
|
|
1940
|
-
console.log(` ${
|
|
1941
|
-
console.log(` ${
|
|
3312
|
+
console.log(chalk11.bold("Options:"));
|
|
3313
|
+
console.log(` ${chalk11.cyan("--in <file>")} Input artifact file (default: .vibgrate/scan_result.json)`);
|
|
3314
|
+
console.log(` ${chalk11.cyan("--format <format>")} Output format: ${chalk11.white("text")} | md | json (default: text)`);
|
|
1942
3315
|
console.log("");
|
|
1943
|
-
console.log(
|
|
3316
|
+
console.log(chalk11.bold("Examples:"));
|
|
1944
3317
|
console.log(" vibgrate report");
|
|
1945
3318
|
console.log(" vibgrate report --format md > DRIFT-REPORT.md");
|
|
1946
3319
|
console.log(" vibgrate report --in ./ci/scan_result.json --format json");
|
|
1947
3320
|
},
|
|
1948
3321
|
sbom: () => {
|
|
1949
3322
|
console.log("");
|
|
1950
|
-
console.log(
|
|
3323
|
+
console.log(chalk11.bold.underline("vibgrate sbom") + chalk11.dim(" \u2014 Export a Software Bill of Materials from a scan artifact"));
|
|
1951
3324
|
console.log("");
|
|
1952
|
-
console.log(
|
|
3325
|
+
console.log(chalk11.bold("Usage:"));
|
|
1953
3326
|
console.log(" vibgrate sbom [options]");
|
|
1954
3327
|
console.log("");
|
|
1955
|
-
console.log(
|
|
1956
|
-
console.log(` ${
|
|
1957
|
-
console.log(` ${
|
|
1958
|
-
console.log(` ${
|
|
3328
|
+
console.log(chalk11.bold("Options:"));
|
|
3329
|
+
console.log(` ${chalk11.cyan("--in <file>")} Input artifact (default: .vibgrate/scan_result.json)`);
|
|
3330
|
+
console.log(` ${chalk11.cyan("--format <format>")} SBOM format: ${chalk11.white("cyclonedx")} | spdx (default: cyclonedx)`);
|
|
3331
|
+
console.log(` ${chalk11.cyan("--out <file>")} Write SBOM to file instead of stdout`);
|
|
1959
3332
|
console.log("");
|
|
1960
|
-
console.log(
|
|
3333
|
+
console.log(chalk11.bold("Examples:"));
|
|
1961
3334
|
console.log(" vibgrate sbom --format cyclonedx --out sbom.json");
|
|
1962
3335
|
console.log(" vibgrate sbom --format spdx --out sbom.spdx.json");
|
|
1963
3336
|
},
|
|
1964
3337
|
push: () => {
|
|
1965
3338
|
console.log("");
|
|
1966
|
-
console.log(
|
|
3339
|
+
console.log(chalk11.bold.underline("vibgrate push") + chalk11.dim(" \u2014 Upload a scan artifact to the Vibgrate API"));
|
|
1967
3340
|
console.log("");
|
|
1968
|
-
console.log(
|
|
3341
|
+
console.log(chalk11.bold("Usage:"));
|
|
1969
3342
|
console.log(" vibgrate push [options]");
|
|
1970
3343
|
console.log("");
|
|
1971
|
-
console.log(
|
|
1972
|
-
console.log(` ${
|
|
1973
|
-
console.log(` ${
|
|
1974
|
-
console.log(` ${
|
|
1975
|
-
console.log(` ${
|
|
3344
|
+
console.log(chalk11.bold("Options:"));
|
|
3345
|
+
console.log(` ${chalk11.cyan("--dsn <dsn>")} DSN token (or set VIBGRATE_DSN env var)`);
|
|
3346
|
+
console.log(` ${chalk11.cyan("--file <file>")} Artifact to upload (default: .vibgrate/scan_result.json)`);
|
|
3347
|
+
console.log(` ${chalk11.cyan("--region <region>")} Override data residency region: us | eu`);
|
|
3348
|
+
console.log(` ${chalk11.cyan("--strict")} Fail with non-zero exit code on upload error`);
|
|
1976
3349
|
console.log("");
|
|
1977
|
-
console.log(
|
|
3350
|
+
console.log(chalk11.bold("Examples:"));
|
|
1978
3351
|
console.log(" vibgrate push --dsn $VIBGRATE_DSN");
|
|
1979
3352
|
console.log(" vibgrate push --file ./ci/scan_result.json --strict");
|
|
1980
3353
|
},
|
|
1981
3354
|
dsn: () => {
|
|
1982
3355
|
console.log("");
|
|
1983
|
-
console.log(
|
|
3356
|
+
console.log(chalk11.bold.underline("vibgrate dsn") + chalk11.dim(" \u2014 Manage DSN tokens for API authentication"));
|
|
1984
3357
|
console.log("");
|
|
1985
|
-
console.log(
|
|
1986
|
-
console.log(` ${
|
|
3358
|
+
console.log(chalk11.bold("Subcommands:"));
|
|
3359
|
+
console.log(` ${chalk11.cyan("vibgrate dsn create")} Generate a new DSN token`);
|
|
1987
3360
|
console.log("");
|
|
1988
|
-
console.log(
|
|
1989
|
-
console.log(` ${
|
|
1990
|
-
console.log(` ${
|
|
1991
|
-
console.log(` ${
|
|
1992
|
-
console.log(` ${
|
|
3361
|
+
console.log(chalk11.bold("dsn create options:"));
|
|
3362
|
+
console.log(` ${chalk11.cyan("--workspace <id>")} Workspace ID or "new" to auto-generate ${chalk11.red("(required)")}`);
|
|
3363
|
+
console.log(` ${chalk11.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
3364
|
+
console.log(` ${chalk11.cyan("--ingest <url>")} Override ingest API URL`);
|
|
3365
|
+
console.log(` ${chalk11.cyan("--write <path>")} Write the DSN to a file (add to .gitignore!)`);
|
|
1993
3366
|
console.log("");
|
|
1994
|
-
console.log(
|
|
3367
|
+
console.log(chalk11.bold("Examples:"));
|
|
1995
3368
|
console.log(" vibgrate dsn create --workspace new");
|
|
1996
3369
|
console.log(" vibgrate dsn create --workspace abc123");
|
|
1997
3370
|
console.log(" vibgrate dsn create --workspace new --region eu --write .vibgrate/.dsn");
|
|
1998
3371
|
},
|
|
1999
3372
|
update: () => {
|
|
2000
3373
|
console.log("");
|
|
2001
|
-
console.log(
|
|
3374
|
+
console.log(chalk11.bold.underline("vibgrate update") + chalk11.dim(" \u2014 Update the vibgrate CLI to the latest version"));
|
|
2002
3375
|
console.log("");
|
|
2003
|
-
console.log(
|
|
3376
|
+
console.log(chalk11.bold("Usage:"));
|
|
2004
3377
|
console.log(" vibgrate update [options]");
|
|
2005
3378
|
console.log("");
|
|
2006
|
-
console.log(
|
|
2007
|
-
console.log(` ${
|
|
2008
|
-
console.log(` ${
|
|
3379
|
+
console.log(chalk11.bold("Options:"));
|
|
3380
|
+
console.log(` ${chalk11.cyan("--check")} Check for a newer version without installing`);
|
|
3381
|
+
console.log(` ${chalk11.cyan("--pm <manager>")} Force a package manager: npm | pnpm | yarn | bun`);
|
|
2009
3382
|
console.log("");
|
|
2010
|
-
console.log(
|
|
3383
|
+
console.log(chalk11.bold("Examples:"));
|
|
2011
3384
|
console.log(" vibgrate update");
|
|
2012
3385
|
console.log(" vibgrate update --check");
|
|
2013
3386
|
console.log(" vibgrate update --pm pnpm");
|
|
@@ -2015,40 +3388,40 @@ var detailedHelp = {
|
|
|
2015
3388
|
};
|
|
2016
3389
|
function printSummaryHelp() {
|
|
2017
3390
|
console.log("");
|
|
2018
|
-
console.log(
|
|
3391
|
+
console.log(chalk11.bold("vibgrate") + chalk11.dim(" \u2014 Continuous Drift Intelligence"));
|
|
2019
3392
|
console.log("");
|
|
2020
|
-
console.log(
|
|
3393
|
+
console.log(chalk11.bold("Usage:"));
|
|
2021
3394
|
console.log(" vibgrate <command> [options]");
|
|
2022
3395
|
console.log(" vibgrate help [command] Show detailed help for a command");
|
|
2023
3396
|
console.log("");
|
|
2024
|
-
console.log(
|
|
2025
|
-
console.log(` ${
|
|
3397
|
+
console.log(chalk11.bold("Getting started:"));
|
|
3398
|
+
console.log(` ${chalk11.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`);
|
|
2026
3399
|
console.log("");
|
|
2027
|
-
console.log(
|
|
2028
|
-
console.log(` ${
|
|
2029
|
-
console.log(` ${
|
|
3400
|
+
console.log(chalk11.bold("Core scanning:"));
|
|
3401
|
+
console.log(` ${chalk11.cyan("scan")} Scan a project for upgrade drift and generate a report`);
|
|
3402
|
+
console.log(` ${chalk11.cyan("baseline")} Save a baseline snapshot to compare future scans against`);
|
|
2030
3403
|
console.log("");
|
|
2031
|
-
console.log(
|
|
2032
|
-
console.log(` ${
|
|
2033
|
-
console.log(` ${
|
|
3404
|
+
console.log(chalk11.bold("Reporting & export:"));
|
|
3405
|
+
console.log(` ${chalk11.cyan("report")} Re-generate a report from a previously saved scan artifact`);
|
|
3406
|
+
console.log(` ${chalk11.cyan("sbom")} Export a Software Bill of Materials (CycloneDX or SPDX)`);
|
|
2034
3407
|
console.log("");
|
|
2035
|
-
console.log(
|
|
2036
|
-
console.log(` ${
|
|
2037
|
-
console.log(` ${
|
|
3408
|
+
console.log(chalk11.bold("CI/CD integration:"));
|
|
3409
|
+
console.log(` ${chalk11.cyan("push")} Upload a scan artifact to the Vibgrate API`);
|
|
3410
|
+
console.log(` ${chalk11.cyan("dsn")} Create and manage DSN tokens for API authentication`);
|
|
2038
3411
|
console.log("");
|
|
2039
|
-
console.log(
|
|
2040
|
-
console.log(` ${
|
|
3412
|
+
console.log(chalk11.bold("Maintenance:"));
|
|
3413
|
+
console.log(` ${chalk11.cyan("update")} Update the vibgrate CLI to the latest version`);
|
|
2041
3414
|
console.log("");
|
|
2042
|
-
console.log(
|
|
3415
|
+
console.log(chalk11.dim("Run") + ` ${chalk11.cyan("vibgrate help <command>")} ` + chalk11.dim("for detailed options, e.g.") + ` ${chalk11.cyan("vibgrate help scan")}`);
|
|
2043
3416
|
}
|
|
2044
|
-
var helpCommand = new
|
|
3417
|
+
var helpCommand = new Command10("help").description("Show help for vibgrate commands").argument("[command]", "Command to show detailed help for").helpOption(false).action((cmd) => {
|
|
2045
3418
|
const name = cmd?.toLowerCase().trim();
|
|
2046
3419
|
if (name && detailedHelp[name]) {
|
|
2047
3420
|
detailedHelp[name]();
|
|
2048
3421
|
} else if (name) {
|
|
2049
3422
|
console.log("");
|
|
2050
|
-
console.log(
|
|
2051
|
-
console.log(
|
|
3423
|
+
console.log(chalk11.red(`Unknown command: ${name}`));
|
|
3424
|
+
console.log(chalk11.dim(`Available commands: ${Object.keys(detailedHelp).join(", ")}`));
|
|
2052
3425
|
printSummaryHelp();
|
|
2053
3426
|
} else {
|
|
2054
3427
|
printSummaryHelp();
|
|
@@ -2057,7 +3430,7 @@ var helpCommand = new Command7("help").description("Show help for vibgrate comma
|
|
|
2057
3430
|
});
|
|
2058
3431
|
|
|
2059
3432
|
// src/cli.ts
|
|
2060
|
-
var program = new
|
|
3433
|
+
var program = new Command11();
|
|
2061
3434
|
program.name("vibgrate").description("Continuous Drift Intelligence").version(VERSION).addHelpText("after", "\nSee https://vibgrate.com/help for more guidance");
|
|
2062
3435
|
program.addCommand(helpCommand);
|
|
2063
3436
|
program.addCommand(initCommand);
|
|
@@ -2074,8 +3447,8 @@ function notifyIfUpdateAvailable() {
|
|
|
2074
3447
|
void checkForUpdate().then((update) => {
|
|
2075
3448
|
if (!update?.updateAvailable) return;
|
|
2076
3449
|
console.error("");
|
|
2077
|
-
console.error(
|
|
2078
|
-
console.error(
|
|
3450
|
+
console.error(chalk12.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
|
|
3451
|
+
console.error(chalk12.dim(' Run "vibgrate update" to install the latest version.'));
|
|
2079
3452
|
console.error("");
|
|
2080
3453
|
}).catch(() => {
|
|
2081
3454
|
});
|