@vibgrate/cli 2026.704.3 → 2026.708.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +90 -4
- package/README.md +1 -1
- package/dist/baseline-RRO4NNNF.js +6 -0
- package/dist/{baseline-64EUPWUH.js.map → baseline-RRO4NNNF.js.map} +1 -1
- package/dist/{chunk-JL7FIC6W.js → chunk-2J7DPIAL.js} +380 -214
- package/dist/chunk-2J7DPIAL.js.map +1 -0
- package/dist/{chunk-WHQRV66O.js → chunk-FMCI3MXH.js} +3 -3
- package/dist/{chunk-WHQRV66O.js.map → chunk-FMCI3MXH.js.map} +1 -1
- package/dist/{chunk-YN5OGRWU.js → chunk-SRWMZBLG.js} +54 -21
- package/dist/chunk-SRWMZBLG.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +1576 -324
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +20 -4
- package/dist/index.js +2 -2
- package/dist/parse-worker.d.ts +1 -1
- package/dist/{types-C6ZyQpa9.d.ts → types-CXnp1uGj.d.ts} +33 -1
- package/package.json +3 -3
- package/dist/baseline-64EUPWUH.js +0 -6
- package/dist/chunk-JL7FIC6W.js.map +0 -1
- package/dist/chunk-YN5OGRWU.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,267 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, dsnCommand, pushCommand,
|
|
3
|
-
import { resolvedGrammarFiles, grammarSetVersion } from './chunk-X5YT263H.js';
|
|
4
|
-
import { loadAdvancedScanHook, baselineCommand } from './chunk-
|
|
5
|
-
import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, prepareCompressedUpload, titleBox, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, brandProgressBar } from './chunk-
|
|
2
|
+
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, countPending, resolveOne, GraphIndex, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocs, selectForBudget, formatForExt, verifyDeterminism, renderHtml, isModelReady } from './chunk-2J7DPIAL.js';
|
|
3
|
+
import { resolvedGrammarFiles, grammarSetVersion, canonicalize } from './chunk-X5YT263H.js';
|
|
4
|
+
import { loadAdvancedScanHook, baselineCommand } from './chunk-FMCI3MXH.js';
|
|
5
|
+
import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-SRWMZBLG.js';
|
|
6
6
|
import './chunk-RXP66R2E.js';
|
|
7
7
|
import { pathExists as pathExists$1, parseExcludePatterns } from './chunk-GI6W53LM.js';
|
|
8
|
-
import * as
|
|
9
|
-
import
|
|
10
|
-
import * as
|
|
11
|
-
import
|
|
8
|
+
import * as fs5 from 'fs';
|
|
9
|
+
import fs5__default, { realpathSync } from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import path__default from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { Command, CommanderError, Option } from 'commander';
|
|
14
|
-
import { execSync, spawn, execFileSync } from 'child_process';
|
|
15
|
-
import
|
|
16
|
-
import
|
|
14
|
+
import { execSync, spawn, spawnSync, execFileSync, execFile } from 'child_process';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
17
|
+
import chalk7 from 'chalk';
|
|
17
18
|
import * as os from 'os';
|
|
19
|
+
import * as readline from 'readline';
|
|
20
|
+
import * as fs14 from 'fs/promises';
|
|
18
21
|
import semver from 'semver';
|
|
19
|
-
import { randomUUID } from 'crypto';
|
|
20
22
|
|
|
23
|
+
var DSSE_PAYLOAD_TYPE = "application/vnd.in-toto+json";
|
|
24
|
+
var IN_TOTO_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
25
|
+
var VG_PREDICATE_TYPE = "https://vibgrate.com/attestation/code-graph/v1";
|
|
26
|
+
function sha256Hex(input) {
|
|
27
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
function graphSubjectDigest(graph) {
|
|
30
|
+
const { generatedAt: _omit, ...rest } = graph;
|
|
31
|
+
return sha256Hex(canonicalize(rest));
|
|
32
|
+
}
|
|
33
|
+
function keyId(publicKey) {
|
|
34
|
+
return sha256Hex(publicKey.export({ type: "spki", format: "der" })).slice(0, 16);
|
|
35
|
+
}
|
|
36
|
+
function buildStatement(graph, input = {}) {
|
|
37
|
+
const graphDigest = graphSubjectDigest(graph);
|
|
38
|
+
const predicate = {
|
|
39
|
+
tool: { name: "vg", version: VERSION },
|
|
40
|
+
toolchain: graph.provenance.toolchain,
|
|
41
|
+
corpusHash: graph.provenance.corpusHash,
|
|
42
|
+
graphDigest,
|
|
43
|
+
counts: graph.meta.counts
|
|
44
|
+
};
|
|
45
|
+
if (input.commit) predicate.commit = input.commit;
|
|
46
|
+
if (input.sbomDigest) predicate.sbomDigest = input.sbomDigest;
|
|
47
|
+
if (input.timestamp) predicate.timestamp = input.timestamp;
|
|
48
|
+
return {
|
|
49
|
+
_type: IN_TOTO_STATEMENT_TYPE,
|
|
50
|
+
subject: [{ name: "graph.json", digest: { sha256: graphDigest } }],
|
|
51
|
+
predicateType: VG_PREDICATE_TYPE,
|
|
52
|
+
predicate
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function dssePae(payloadType, body) {
|
|
56
|
+
const typeBytes = Buffer.from(payloadType, "utf8");
|
|
57
|
+
const header = Buffer.from(
|
|
58
|
+
`DSSEv1 ${typeBytes.length} ${payloadType} ${body.length} `,
|
|
59
|
+
"utf8"
|
|
60
|
+
);
|
|
61
|
+
return Buffer.concat([header, Buffer.from(body)]);
|
|
62
|
+
}
|
|
63
|
+
function signStatement(statement, privateKey) {
|
|
64
|
+
const body = Buffer.from(JSON.stringify(statement), "utf8");
|
|
65
|
+
const pae = dssePae(DSSE_PAYLOAD_TYPE, body);
|
|
66
|
+
const sig = crypto.sign(null, pae, privateKey);
|
|
67
|
+
const publicKey = crypto.createPublicKey(privateKey.export({ type: "pkcs8", format: "pem" }).toString());
|
|
68
|
+
return {
|
|
69
|
+
payloadType: DSSE_PAYLOAD_TYPE,
|
|
70
|
+
payload: body.toString("base64"),
|
|
71
|
+
signatures: [
|
|
72
|
+
{
|
|
73
|
+
keyid: keyId(publicKey),
|
|
74
|
+
sig: sig.toString("base64"),
|
|
75
|
+
publicKey: publicKey.export({ type: "spki", format: "pem" }).toString()
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function emptyStatement() {
|
|
81
|
+
return {
|
|
82
|
+
_type: "",
|
|
83
|
+
subject: [],
|
|
84
|
+
predicateType: "",
|
|
85
|
+
predicate: {
|
|
86
|
+
tool: { name: "vg", version: "" },
|
|
87
|
+
corpusHash: "",
|
|
88
|
+
graphDigest: "",
|
|
89
|
+
counts: { nodes: 0, edges: 0, areas: 0, tests: 0, untested: 0 }
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function failedResult(reason) {
|
|
94
|
+
return {
|
|
95
|
+
status: "failed",
|
|
96
|
+
signatureValid: false,
|
|
97
|
+
signerPinned: false,
|
|
98
|
+
keyid: "",
|
|
99
|
+
reason,
|
|
100
|
+
statement: emptyStatement()
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function verifyEnvelope(env, opts = {}) {
|
|
104
|
+
let body;
|
|
105
|
+
let statement;
|
|
106
|
+
try {
|
|
107
|
+
body = Buffer.from(env?.payload ?? "", "base64");
|
|
108
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
109
|
+
if (!parsed || typeof parsed !== "object" || !parsed.predicate) {
|
|
110
|
+
throw new Error("missing predicate");
|
|
111
|
+
}
|
|
112
|
+
statement = parsed;
|
|
113
|
+
} catch {
|
|
114
|
+
return failedResult("malformed attestation payload (not a valid in-toto statement)");
|
|
115
|
+
}
|
|
116
|
+
const pae = dssePae(env.payloadType, body);
|
|
117
|
+
const sig0 = env.signatures?.[0];
|
|
118
|
+
const keyid = sig0?.keyid ?? "";
|
|
119
|
+
const sig = sig0?.sig ? Buffer.from(sig0.sig, "base64") : Buffer.alloc(0);
|
|
120
|
+
let signatureValid = false;
|
|
121
|
+
let signerPinned = false;
|
|
122
|
+
try {
|
|
123
|
+
if (sig0 && sig.length) {
|
|
124
|
+
if (opts.publicKeyPem) {
|
|
125
|
+
const pinnedKey = crypto.createPublicKey(opts.publicKeyPem);
|
|
126
|
+
signatureValid = crypto.verify(null, pae, pinnedKey, sig);
|
|
127
|
+
signerPinned = signatureValid;
|
|
128
|
+
} else if (sig0.publicKey) {
|
|
129
|
+
const embeddedKey = crypto.createPublicKey(sig0.publicKey);
|
|
130
|
+
signatureValid = crypto.verify(null, pae, embeddedKey, sig);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
signatureValid = false;
|
|
135
|
+
signerPinned = false;
|
|
136
|
+
}
|
|
137
|
+
const digestMatches = opts.graph ? graphSubjectDigest(opts.graph) === statement.predicate.graphDigest : void 0;
|
|
138
|
+
const dirty = statement.predicate.commit?.dirty;
|
|
139
|
+
let status;
|
|
140
|
+
let reason;
|
|
141
|
+
if (!signatureValid) {
|
|
142
|
+
status = "failed";
|
|
143
|
+
reason = sig0 ? "signature verification failed" : "no signature in envelope";
|
|
144
|
+
} else if (digestMatches === false) {
|
|
145
|
+
status = "failed";
|
|
146
|
+
reason = "graph.json no longer matches the attested digest (content changed since signing)";
|
|
147
|
+
} else if (signerPinned && !dirty) {
|
|
148
|
+
status = "verified";
|
|
149
|
+
reason = digestMatches === true ? "signature valid, signer trusted, graph digest matches" : "signature valid, signer trusted";
|
|
150
|
+
} else {
|
|
151
|
+
status = "signature-valid";
|
|
152
|
+
reason = dirty ? "signature valid but the attested tree was dirty (uncommitted changes)" : "signature valid but signer not pinned \u2014 pass --pub to establish trust";
|
|
153
|
+
}
|
|
154
|
+
return { status, signatureValid, signerPinned, digestMatches, dirty, keyid, reason, statement };
|
|
155
|
+
}
|
|
156
|
+
function generateKeypair() {
|
|
157
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
158
|
+
return {
|
|
159
|
+
privatePem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
160
|
+
publicPem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
161
|
+
keyid: keyId(publicKey)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function serializeEnvelope(env) {
|
|
165
|
+
return `${JSON.stringify(env)}
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
function parseEnvelope(text) {
|
|
169
|
+
const line = text.trim().split("\n")[0];
|
|
170
|
+
return JSON.parse(line);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/commands/attest-actions.ts
|
|
174
|
+
var DEFAULT_ATTESTATION = "attestation.intoto.jsonl";
|
|
175
|
+
var DEFAULT_KEY = "attest-key.pem";
|
|
176
|
+
async function signGraphAttestation(root, graph, opts) {
|
|
177
|
+
const notices = [];
|
|
178
|
+
const explicitKey = opts.key ?? process.env.VG_ATTEST_KEY;
|
|
179
|
+
const keyPath = explicitKey ? path.resolve(explicitKey) : path.join(root, ".vibgrate", DEFAULT_KEY);
|
|
180
|
+
let keyGeneratedAt;
|
|
181
|
+
if (!fs5.existsSync(keyPath)) {
|
|
182
|
+
if (explicitKey) {
|
|
183
|
+
throw new CliError(`signing key not found: ${explicitKey}`, ExitCode.USAGE_ERROR);
|
|
184
|
+
}
|
|
185
|
+
const kp = generateKeypair();
|
|
186
|
+
fs5.mkdirSync(path.dirname(keyPath), { recursive: true });
|
|
187
|
+
fs5.writeFileSync(keyPath, kp.privatePem, { mode: 384 });
|
|
188
|
+
fs5.writeFileSync(`${keyPath}.pub`, kp.publicPem);
|
|
189
|
+
keyGeneratedAt = keyPath;
|
|
190
|
+
notices.push(
|
|
191
|
+
`minted a new Ed25519 signing key at ${rel(root, keyPath)} (keyid ${kp.keyid}) \u2014 keep it, add it to .gitignore, and reuse it to re-sign reproducibly`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
let privateKey;
|
|
195
|
+
try {
|
|
196
|
+
privateKey = crypto.createPrivateKey(fs5.readFileSync(keyPath, "utf8"));
|
|
197
|
+
} catch {
|
|
198
|
+
throw new CliError(`could not read an Ed25519 private key from ${rel(root, keyPath)}`, ExitCode.USAGE_ERROR);
|
|
199
|
+
}
|
|
200
|
+
if (privateKey.asymmetricKeyType !== "ed25519") {
|
|
201
|
+
throw new CliError(
|
|
202
|
+
`attest requires an Ed25519 key, but ${rel(root, keyPath)} is ${privateKey.asymmetricKeyType ?? "unknown"}`,
|
|
203
|
+
ExitCode.USAGE_ERROR
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const commit = await gitCommitInfo(root);
|
|
207
|
+
const statement = buildStatement(graph, { commit });
|
|
208
|
+
const envelope = signStatement(statement, privateKey);
|
|
209
|
+
const outPath = opts.attestation ? path.resolve(opts.attestation) : path.join(root, ".vibgrate", DEFAULT_ATTESTATION);
|
|
210
|
+
fs5.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
211
|
+
fs5.writeFileSync(outPath, serializeEnvelope(envelope));
|
|
212
|
+
return {
|
|
213
|
+
summary: {
|
|
214
|
+
keyid: envelope.signatures[0]?.keyid ?? "",
|
|
215
|
+
graphDigest: statement.predicate.graphDigest,
|
|
216
|
+
fingerprint: statement.predicate.toolchain?.fingerprint,
|
|
217
|
+
commit,
|
|
218
|
+
out: rel(root, outPath),
|
|
219
|
+
keyGeneratedAt: keyGeneratedAt ? rel(root, keyGeneratedAt) : void 0
|
|
220
|
+
},
|
|
221
|
+
notices
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function verifyGraphAttestation(root, opts) {
|
|
225
|
+
const bundlePath = opts.attestation ? path.resolve(opts.attestation) : path.join(root, ".vibgrate", DEFAULT_ATTESTATION);
|
|
226
|
+
if (!fs5.existsSync(bundlePath)) {
|
|
227
|
+
if (opts.attestation || opts.pub) {
|
|
228
|
+
throw new CliError(
|
|
229
|
+
`no attestation at ${rel(root, bundlePath)} \u2014 sign one with \`vg build --attest\``,
|
|
230
|
+
ExitCode.NOT_FOUND
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return { bundlePath, missing: true };
|
|
234
|
+
}
|
|
235
|
+
const envelope = parseEnvelope(fs5.readFileSync(bundlePath, "utf8"));
|
|
236
|
+
const publicKeyPem = opts.pub ? fs5.readFileSync(path.resolve(opts.pub), "utf8") : void 0;
|
|
237
|
+
const graph = loadGraph(root) ?? void 0;
|
|
238
|
+
return { bundlePath, missing: false, result: verifyEnvelope(envelope, { publicKeyPem, graph }) };
|
|
239
|
+
}
|
|
240
|
+
async function gitCommitInfo(root) {
|
|
241
|
+
const sha = await resolveHead(root);
|
|
242
|
+
if (!sha) return void 0;
|
|
243
|
+
const dirty = await workingTreeDirty(root);
|
|
244
|
+
const branch = await gitBranch(root);
|
|
245
|
+
return { sha, shortSha: sha.slice(0, 7), branch, dirty: dirty === true ? true : dirty === false ? false : void 0 };
|
|
246
|
+
}
|
|
247
|
+
function gitBranch(root) {
|
|
248
|
+
return new Promise((resolve16) => {
|
|
249
|
+
execFile(
|
|
250
|
+
"git",
|
|
251
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
252
|
+
{ cwd: root, timeout: 5e3, windowsHide: true, encoding: "utf8" },
|
|
253
|
+
(err, stdout) => {
|
|
254
|
+
if (err) return resolve16(void 0);
|
|
255
|
+
const name = String(stdout).trim();
|
|
256
|
+
resolve16(name && name !== "HEAD" ? name : void 0);
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function rel(root, p) {
|
|
262
|
+
const r = path.relative(root, p);
|
|
263
|
+
return r.startsWith("..") ? p : r;
|
|
264
|
+
}
|
|
21
265
|
function info(message = "") {
|
|
22
266
|
process.stderr.write(`${message}
|
|
23
267
|
`);
|
|
@@ -30,9 +274,9 @@ function json(value) {
|
|
|
30
274
|
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
31
275
|
`);
|
|
32
276
|
}
|
|
33
|
-
var c =
|
|
277
|
+
var c = chalk7;
|
|
34
278
|
function disableColor() {
|
|
35
|
-
|
|
279
|
+
chalk7.level = 0;
|
|
36
280
|
}
|
|
37
281
|
|
|
38
282
|
// src/util/logo.ts
|
|
@@ -120,15 +364,19 @@ function readGlobal(cmd) {
|
|
|
120
364
|
|
|
121
365
|
// src/commands/build.ts
|
|
122
366
|
function registerBuild(program) {
|
|
123
|
-
const cmd = program.command("build").description("build / update the code map (incremental, deterministic)").argument("[paths...]", "folders or files to map (default: current folder)").option("--only <langs>", "restrict to languages, e.g. ts,py,go").option("--exclude <glob>", "extra ignore glob (repeatable)", collect, []).option("--no-html", "do not write graph.html").option("--no-report", "do not write GRAPH_REPORT.md").option("--no-ground", "do not attach grounding (Phase 2)").option("--jobs <n>", "worker count (1 = single-threaded)").option("--scip <file>", "ingest a SCIP index for precise resolution (default: auto-detect index.scip)").option("--no-scip", "ignore any SCIP index").option("--no-tsc", "skip the in-process TypeScript resolver (heuristic floor only)").option("--no-warm", "do not warm the semantic index in the background after building").option("--grammars <dir>", "directory of grammar .wasm files (offline / air-gapped)").option("-o, --export <file>", "also write the map to a file (format inferred)").action(async function(paths, opts) {
|
|
367
|
+
const cmd = program.command("build").description("build / update the code map (incremental, deterministic)").argument("[paths...]", "folders or files to map (default: current folder)").option("--only <langs>", "restrict to languages, e.g. ts,py,go").option("--exclude <glob>", "extra ignore glob (repeatable)", collect, []).option("--no-html", "do not write graph.html").option("--no-report", "do not write GRAPH_REPORT.md").option("--no-ground", "do not attach grounding (Phase 2)").option("--jobs <n>", "worker count (1 = single-threaded)").option("--scip <file>", "ingest a SCIP index for precise resolution (default: auto-detect index.scip)").option("--no-scip", "ignore any SCIP index").option("--no-tsc", "skip the in-process TypeScript resolver (heuristic floor only)").option("--no-warm", "do not warm the semantic index in the background after building").option("--grammars <dir>", "directory of grammar .wasm files (offline / air-gapped)").option("-o, --export <file>", "also write the map to a file (format inferred)").option("--attest", "sign the built graph \u2192 .vibgrate/attestation.intoto.jsonl").option("--verify", "verify a committed attestation against the graph (no rebuild)").option("--attest-key <path>", "signing key PEM (else $VG_ATTEST_KEY, else .vibgrate/attest-key.pem)").option("--attestation <file>", "attestation path (out for --attest, in for --verify)").option("--pub <path>", "public key PEM to pin the signer (with --verify)").action(async function(paths, opts) {
|
|
124
368
|
await runBuild(paths, opts, readGlobal(this));
|
|
125
369
|
});
|
|
126
370
|
applyGlobalOptions(cmd);
|
|
127
371
|
}
|
|
128
372
|
async function runBuild(paths, opts, global) {
|
|
129
|
-
const root =
|
|
373
|
+
const root = path.resolve(global.cwd ?? ".");
|
|
374
|
+
if (opts.verify) {
|
|
375
|
+
await verifyGraph(root, opts, global);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
130
378
|
const interactive = !global.json && !global.quiet;
|
|
131
|
-
if (interactive) printLogo(
|
|
379
|
+
if (interactive) printLogo(path.basename(root) || root);
|
|
132
380
|
const bar = interactive ? new ProgressBar(c.dim("parsing")) : void 0;
|
|
133
381
|
const jobs = opts.jobs ? Number(opts.jobs) : void 0;
|
|
134
382
|
if (jobs !== void 0 && (!Number.isInteger(jobs) || jobs < 1)) {
|
|
@@ -180,6 +428,16 @@ async function runBuild(paths, opts, global) {
|
|
|
180
428
|
});
|
|
181
429
|
}
|
|
182
430
|
if (opts.export) writeExport(result.graph, opts.export);
|
|
431
|
+
let attestation;
|
|
432
|
+
const attestNotices = [];
|
|
433
|
+
if (opts.attest) {
|
|
434
|
+
const signed = await signGraphAttestation(root, result.graph, {
|
|
435
|
+
key: opts.attestKey,
|
|
436
|
+
attestation: opts.attestation
|
|
437
|
+
});
|
|
438
|
+
attestation = signed.summary;
|
|
439
|
+
attestNotices.push(...signed.notices);
|
|
440
|
+
}
|
|
183
441
|
if (global.json) {
|
|
184
442
|
json({
|
|
185
443
|
ok: true,
|
|
@@ -191,8 +449,11 @@ async function runBuild(paths, opts, global) {
|
|
|
191
449
|
resolve: result.resolveStats,
|
|
192
450
|
tsc: result.tsc,
|
|
193
451
|
scip: result.scip,
|
|
452
|
+
epistemic: epistemicBreakdown(result.graph.edges),
|
|
194
453
|
artifacts: written,
|
|
195
454
|
corpusHash: result.graph.provenance.corpusHash,
|
|
455
|
+
toolchain: result.graph.provenance.toolchain,
|
|
456
|
+
attestation,
|
|
196
457
|
timingMs: result.timing.totalMs,
|
|
197
458
|
warnings: result.warnings
|
|
198
459
|
});
|
|
@@ -201,7 +462,7 @@ async function runBuild(paths, opts, global) {
|
|
|
201
462
|
const { counts } = result.graph.meta;
|
|
202
463
|
const incremental = result.reused > 0 ? `incremental: ${result.reparsed} of ${result.totalFiles} files re-parsed` : `${result.totalFiles} files parsed`;
|
|
203
464
|
const seconds = (result.timing.totalMs / 1e3).toFixed(2);
|
|
204
|
-
info(`${c.cyan("vg")} \xB7 mapped ${
|
|
465
|
+
info(`${c.cyan("vg")} \xB7 mapped ${rel2(root)} in ${seconds}s (${incremental})`);
|
|
205
466
|
info(
|
|
206
467
|
` nodes ${c.bold(String(counts.nodes))} edges ${c.bold(String(counts.edges))} areas ${counts.areas} langs ${result.graph.meta.languages.join(",") || "\u2014"}`
|
|
207
468
|
);
|
|
@@ -218,13 +479,67 @@ async function runBuild(paths, opts, global) {
|
|
|
218
479
|
if (result.scip) {
|
|
219
480
|
info(c.dim(` scip: ${result.scip.resolved} precise edges from ${result.scip.tool ?? "index"} (${result.scip.documents} docs)`));
|
|
220
481
|
}
|
|
221
|
-
const
|
|
482
|
+
const ep = epistemicBreakdown(result.graph.edges);
|
|
483
|
+
info(
|
|
484
|
+
c.dim(
|
|
485
|
+
` edges by evidence \xB7 observed ${ep.observed} \xB7 name-matched ${ep["name-matched"]} \xB7 declared ${ep.declared}`
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
const artifactList = [written.graphPath, written.htmlPath, written.reportPath].filter(Boolean).map((p) => path.relative(root, p)).join(" ");
|
|
222
489
|
info(` \u2192 ${artifactList}`);
|
|
223
490
|
if (result.warnings.length) {
|
|
224
491
|
info(c.yellow(` ${result.warnings.length} parse warning(s) \u2014 run with --json for detail`));
|
|
225
492
|
}
|
|
493
|
+
if (attestation) {
|
|
494
|
+
for (const n of attestNotices) info(c.yellow(` ${n}`));
|
|
495
|
+
info(
|
|
496
|
+
c.dim(
|
|
497
|
+
` attested \xB7 keyid ${attestation.keyid} \xB7 digest ${attestation.graphDigest.slice(0, 16)}\u2026 \u2192 ${attestation.out}`
|
|
498
|
+
)
|
|
499
|
+
);
|
|
500
|
+
}
|
|
226
501
|
maybeWarmEmbeddings(root, result.graph, global, opts.warm !== false);
|
|
227
502
|
}
|
|
503
|
+
async function verifyGraph(root, opts, global) {
|
|
504
|
+
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
505
|
+
const jobs = opts.jobs ? Number(opts.jobs) : void 0;
|
|
506
|
+
const det = await verifyDeterminism({ root, only, exclude: opts.exclude, jobs });
|
|
507
|
+
const attest = verifyGraphAttestation(root, { attestation: opts.attestation, pub: opts.pub });
|
|
508
|
+
const attestFailed = !attest.missing && attest.result?.status === "failed";
|
|
509
|
+
if (global.json) {
|
|
510
|
+
json({
|
|
511
|
+
ok: det.ok && !attestFailed,
|
|
512
|
+
determinism: { ok: det.ok, checks: det.checks, digest: det.digest },
|
|
513
|
+
attestation: attest.missing ? null : {
|
|
514
|
+
status: attest.result?.status,
|
|
515
|
+
signatureValid: attest.result?.signatureValid,
|
|
516
|
+
signerPinned: attest.result?.signerPinned,
|
|
517
|
+
digestMatches: attest.result?.digestMatches,
|
|
518
|
+
dirty: attest.result?.dirty,
|
|
519
|
+
keyid: attest.result?.keyid,
|
|
520
|
+
reason: attest.result?.reason
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
} else {
|
|
524
|
+
info(`${c.cyan("vg build --verify")} \xB7 ${path.relative(process.cwd(), root) || "."}`);
|
|
525
|
+
for (const check of det.checks) {
|
|
526
|
+
const mark = check.ok ? c.green("\u2714") : c.red("\u2718");
|
|
527
|
+
const detail = check.detail ? c.dim(` (${check.detail})`) : "";
|
|
528
|
+
info(` ${mark} ${check.name}${detail}`);
|
|
529
|
+
}
|
|
530
|
+
info(det.ok ? c.green(` deterministic \xB7 digest ${det.digest.slice(0, 16)}\u2026`) : c.red(" NON-DETERMINISTIC"));
|
|
531
|
+
if (attest.missing) {
|
|
532
|
+
info(c.dim(` attestation: none (sign one with \`vg build --attest\`)`));
|
|
533
|
+
} else {
|
|
534
|
+
const r = attest.result;
|
|
535
|
+
const badge = r?.status === "verified" ? c.green("\u2714 attestation verified") : r?.status === "signature-valid" ? c.yellow("~ attestation signature valid") : c.red("\u2718 attestation failed");
|
|
536
|
+
info(` ${badge}${r?.keyid ? c.dim(` \xB7 keyid ${r.keyid}`) : ""}`);
|
|
537
|
+
info(c.dim(` ${r?.reason ?? ""}`));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!det.ok) throw new CliError("determinism self-check failed", ExitCode.NON_DETERMINISTIC);
|
|
541
|
+
if (attestFailed) throw new CliError("attestation verification failed", ExitCode.GATE_FAILED);
|
|
542
|
+
}
|
|
228
543
|
function maybeWarmEmbeddings(root, graph, global, warm) {
|
|
229
544
|
if (!warm || global.json || global.quiet || global.local) return;
|
|
230
545
|
if (!process.stdout.isTTY && !process.stderr.isTTY) return;
|
|
@@ -250,7 +565,7 @@ function writeExport(graph, target) {
|
|
|
250
565
|
out(serializeGraph(graph).trimEnd());
|
|
251
566
|
return;
|
|
252
567
|
}
|
|
253
|
-
const ext =
|
|
568
|
+
const ext = path.extname(target).toLowerCase();
|
|
254
569
|
let content;
|
|
255
570
|
switch (ext) {
|
|
256
571
|
case ".json":
|
|
@@ -268,14 +583,14 @@ function writeExport(graph, target) {
|
|
|
268
583
|
ExitCode.USAGE_ERROR
|
|
269
584
|
);
|
|
270
585
|
}
|
|
271
|
-
|
|
272
|
-
|
|
586
|
+
fs5.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
|
|
587
|
+
fs5.writeFileSync(target, content);
|
|
273
588
|
}
|
|
274
589
|
function collect(value, prev) {
|
|
275
590
|
return [...prev, value];
|
|
276
591
|
}
|
|
277
|
-
function
|
|
278
|
-
const r =
|
|
592
|
+
function rel2(root) {
|
|
593
|
+
const r = path.relative(process.cwd(), root);
|
|
279
594
|
return r === "" ? "." : r;
|
|
280
595
|
}
|
|
281
596
|
function registerStatus(program) {
|
|
@@ -285,10 +600,10 @@ function registerStatus(program) {
|
|
|
285
600
|
applyGlobalOptions(cmd);
|
|
286
601
|
}
|
|
287
602
|
async function runStatus(global) {
|
|
288
|
-
const root =
|
|
603
|
+
const root = path.resolve(global.cwd ?? ".");
|
|
289
604
|
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
290
605
|
const graph = loadGraph(root, graphPath);
|
|
291
|
-
const hasCache =
|
|
606
|
+
const hasCache = fs5.existsSync(path.join(cacheDir(root), "parse-cache.json"));
|
|
292
607
|
let stale = null;
|
|
293
608
|
let staleExact = false;
|
|
294
609
|
if (graph) {
|
|
@@ -308,8 +623,8 @@ async function runStatus(global) {
|
|
|
308
623
|
}
|
|
309
624
|
if (global.json) {
|
|
310
625
|
json({
|
|
311
|
-
root:
|
|
312
|
-
graphPath:
|
|
626
|
+
root: path.relative(process.cwd(), root) || ".",
|
|
627
|
+
graphPath: path.relative(root, graphPath),
|
|
313
628
|
built: graph !== null,
|
|
314
629
|
generatedAt: graph?.generatedAt ?? null,
|
|
315
630
|
counts: graph?.meta.counts ?? null,
|
|
@@ -324,11 +639,11 @@ async function runStatus(global) {
|
|
|
324
639
|
return;
|
|
325
640
|
}
|
|
326
641
|
if (!graph) {
|
|
327
|
-
info(`${c.cyan("vg")} \xB7 no map yet at ${c.dim(
|
|
642
|
+
info(`${c.cyan("vg")} \xB7 no map yet at ${c.dim(path.relative(root, graphPath))}`);
|
|
328
643
|
info(` run ${c.bold("vg")} to build one`);
|
|
329
644
|
return;
|
|
330
645
|
}
|
|
331
|
-
info(`${c.cyan("vg")} \xB7 ${
|
|
646
|
+
info(`${c.cyan("vg")} \xB7 ${path.relative(root, graphPath)}`);
|
|
332
647
|
info(` generated ${graph.generatedAt}`);
|
|
333
648
|
info(
|
|
334
649
|
` nodes ${c.bold(String(graph.meta.counts.nodes))} edges ${c.bold(
|
|
@@ -344,34 +659,8 @@ async function runStatus(global) {
|
|
|
344
659
|
}
|
|
345
660
|
if (vibgrateDir(root)) ;
|
|
346
661
|
}
|
|
347
|
-
function registerVerify(program) {
|
|
348
|
-
const cmd = program.command("verify").description("determinism self-check (exit 4 on mismatch)").option("--only <langs>", "restrict to languages, e.g. ts,py").option("--jobs <n>", "worker count").action(async function(opts) {
|
|
349
|
-
const global = readGlobal(this);
|
|
350
|
-
const root = path9.resolve(global.cwd ?? ".");
|
|
351
|
-
const result = await verifyDeterminism({
|
|
352
|
-
root,
|
|
353
|
-
only: opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
354
|
-
jobs: opts.jobs ? Number(opts.jobs) : void 0
|
|
355
|
-
});
|
|
356
|
-
if (global.json) {
|
|
357
|
-
json({ ok: result.ok, checks: result.checks, digest: result.digest });
|
|
358
|
-
} else {
|
|
359
|
-
info(`${c.cyan("vg verify")} \xB7 ${path9.relative(process.cwd(), root) || "."}`);
|
|
360
|
-
for (const check of result.checks) {
|
|
361
|
-
const mark = check.ok ? c.green("\u2714") : c.red("\u2718");
|
|
362
|
-
const detail = check.detail ? c.dim(` (${check.detail})`) : "";
|
|
363
|
-
info(` ${mark} ${check.name}${detail}`);
|
|
364
|
-
}
|
|
365
|
-
info(result.ok ? c.green(` deterministic \xB7 digest ${result.digest.slice(0, 16)}\u2026`) : c.red(" NON-DETERMINISTIC"));
|
|
366
|
-
}
|
|
367
|
-
if (!result.ok) {
|
|
368
|
-
throw new CliError("determinism self-check failed", ExitCode.NON_DETERMINISTIC);
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
applyGlobalOptions(cmd);
|
|
372
|
-
}
|
|
373
662
|
function rootOf(global) {
|
|
374
|
-
return
|
|
663
|
+
return path.resolve(global.cwd ?? ".");
|
|
375
664
|
}
|
|
376
665
|
function requireGraph(global) {
|
|
377
666
|
const root = rootOf(global);
|
|
@@ -655,10 +944,10 @@ function registerTree(program) {
|
|
|
655
944
|
applyGlobalOptions(cmd);
|
|
656
945
|
}
|
|
657
946
|
function next(index, node, direction) {
|
|
658
|
-
const
|
|
947
|
+
const rel3 = direction === "callees" ? index.callees(node.id) : index.callers(node.id);
|
|
659
948
|
const seen = /* @__PURE__ */ new Set();
|
|
660
949
|
const out2 = [];
|
|
661
|
-
for (const { node: n } of
|
|
950
|
+
for (const { node: n } of rel3) {
|
|
662
951
|
if (seen.has(n.id)) continue;
|
|
663
952
|
seen.add(n.id);
|
|
664
953
|
out2.push(n);
|
|
@@ -834,6 +1123,113 @@ function registerImpact(program) {
|
|
|
834
1123
|
});
|
|
835
1124
|
applyGlobalOptions(cmd);
|
|
836
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
// src/commands/unknowns.ts
|
|
1128
|
+
var IMPACT_CANDIDATE_CAP = 200;
|
|
1129
|
+
function registerUnknowns(program) {
|
|
1130
|
+
const cmd = program.command("unknowns").description("what the graph cannot resolve, ranked by blast radius").option("-n, --limit <n>", "how many sites to show", "20").action(function(opts) {
|
|
1131
|
+
const global = readGlobal(this);
|
|
1132
|
+
const { graph } = requireGraph(global);
|
|
1133
|
+
const limit = Number(opts.limit) || 20;
|
|
1134
|
+
const ranked = rankUnknowns(graph, limit);
|
|
1135
|
+
const { sites, totalSites, capped } = ranked;
|
|
1136
|
+
const totalRefs = (graph.unknowns ?? []).reduce((n, u) => n + u.count, 0);
|
|
1137
|
+
if (global.json) {
|
|
1138
|
+
json({
|
|
1139
|
+
unresolvedReferences: totalRefs,
|
|
1140
|
+
sites: totalSites,
|
|
1141
|
+
shown: sites.length,
|
|
1142
|
+
blastRankedCandidateCap: capped ? IMPACT_CANDIDATE_CAP : null,
|
|
1143
|
+
top: sites.map((s) => ({
|
|
1144
|
+
id: s.node.id,
|
|
1145
|
+
name: s.node.qualifiedName,
|
|
1146
|
+
kind: s.node.kind,
|
|
1147
|
+
file: s.node.file,
|
|
1148
|
+
line: s.node.span.start,
|
|
1149
|
+
importance: s.node.importance,
|
|
1150
|
+
blastRadius: s.blastRadius,
|
|
1151
|
+
direct: s.direct,
|
|
1152
|
+
transitive: s.transitive,
|
|
1153
|
+
unresolved: s.refs.map((r) => ({ name: r.name, kind: r.kind, count: r.count }))
|
|
1154
|
+
}))
|
|
1155
|
+
});
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (totalRefs === 0) {
|
|
1159
|
+
info(
|
|
1160
|
+
`${c.cyan("vg unknowns")} \xB7 none \u2014 every heuristic reference resolved (or was covered by a precise rung)`
|
|
1161
|
+
);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
info(
|
|
1165
|
+
`${c.cyan("vg unknowns")} \xB7 ${c.bold(String(totalRefs))} unresolved reference(s) at ${totalSites} site(s) \xB7 top ${sites.length} by blast radius`
|
|
1166
|
+
);
|
|
1167
|
+
for (const s of sites) {
|
|
1168
|
+
info(
|
|
1169
|
+
` ${c.yellow(pad2(String(s.blastRadius), 5))} ${c.bold(s.node.qualifiedName)} ` + c.dim(`${s.node.file}:${s.node.span.start} \xB7 importance ${s.node.importance.toFixed(3)}`)
|
|
1170
|
+
);
|
|
1171
|
+
info(` ${c.dim("cannot resolve:")} ${s.refs.map(fmtRef).join(", ")}`);
|
|
1172
|
+
}
|
|
1173
|
+
info(
|
|
1174
|
+
c.dim(
|
|
1175
|
+
` blast radius = transitive dependents; these are the code paths the graph is blind about`
|
|
1176
|
+
)
|
|
1177
|
+
);
|
|
1178
|
+
if (capped) {
|
|
1179
|
+
info(
|
|
1180
|
+
c.dim(
|
|
1181
|
+
` note: ${totalSites} sites have unknowns; only the top ${IMPACT_CANDIDATE_CAP} by centrality were blast-radius scored (the tail is ordered by centrality alone)`
|
|
1182
|
+
)
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
applyGlobalOptions(cmd);
|
|
1187
|
+
}
|
|
1188
|
+
function rankUnknowns(graph, limit) {
|
|
1189
|
+
const unknowns = graph.unknowns ?? [];
|
|
1190
|
+
if (unknowns.length === 0) return { sites: [], totalSites: 0, capped: false };
|
|
1191
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1192
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1193
|
+
for (const u of unknowns) {
|
|
1194
|
+
const list = grouped.get(u.from);
|
|
1195
|
+
if (list) list.push(u);
|
|
1196
|
+
else grouped.set(u.from, [u]);
|
|
1197
|
+
}
|
|
1198
|
+
const candidates = [];
|
|
1199
|
+
for (const [from, refs] of grouped) {
|
|
1200
|
+
const node = byId.get(from);
|
|
1201
|
+
if (!node) continue;
|
|
1202
|
+
candidates.push({ node, refs, total: refs.reduce((n, r) => n + r.count, 0) });
|
|
1203
|
+
}
|
|
1204
|
+
candidates.sort(
|
|
1205
|
+
(a, b) => b.node.importance - a.node.importance || a.node.id.localeCompare(b.node.id)
|
|
1206
|
+
);
|
|
1207
|
+
const scored = candidates.slice(0, IMPACT_CANDIDATE_CAP).map((cand) => {
|
|
1208
|
+
const impact = impactOf(graph, cand.node.id);
|
|
1209
|
+
return {
|
|
1210
|
+
...cand,
|
|
1211
|
+
blastRadius: impact.affected.length,
|
|
1212
|
+
direct: impact.direct,
|
|
1213
|
+
transitive: impact.transitive
|
|
1214
|
+
};
|
|
1215
|
+
});
|
|
1216
|
+
scored.sort(
|
|
1217
|
+
(a, b) => b.blastRadius - a.blastRadius || b.node.importance - a.node.importance || b.total - a.total || a.node.id.localeCompare(b.node.id)
|
|
1218
|
+
);
|
|
1219
|
+
return {
|
|
1220
|
+
sites: scored.slice(0, limit),
|
|
1221
|
+
totalSites: candidates.length,
|
|
1222
|
+
capped: candidates.length > IMPACT_CANDIDATE_CAP
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function fmtRef(r) {
|
|
1226
|
+
const times = r.count > 1 ? ` \xD7${r.count}` : "";
|
|
1227
|
+
const kind = r.kind === "call" ? "" : ` (${r.kind})`;
|
|
1228
|
+
return `${r.name}${kind}${times}`;
|
|
1229
|
+
}
|
|
1230
|
+
function pad2(s, n) {
|
|
1231
|
+
return s.padStart(n, " ");
|
|
1232
|
+
}
|
|
837
1233
|
function registerServe(program) {
|
|
838
1234
|
const cmd = program.command("serve").description("start Vibgrate AI Context \u2014 local-first MCP serving your code map, drift & version-correct docs to your AI").option("--http", "serve over streamable HTTP instead of stdio").option("--port <n>", "port for --http", "7437").option("--host <h>", "host for --http", "127.0.0.1").option("--savings", "record local, counts-only usage savings (opt-in; off by default)").option("--dedup", "collapse a node's heavy relation lists on repeat reads within a session (opt-in; saves tokens)").option("--no-refresh", "serve the map as built \u2014 skip the auto-rebuild when files change").action(async function(opts) {
|
|
839
1235
|
const global = readGlobal(this);
|
|
@@ -846,7 +1242,7 @@ function registerServe(program) {
|
|
|
846
1242
|
dedup: opts.dedup === true,
|
|
847
1243
|
refresh
|
|
848
1244
|
};
|
|
849
|
-
if (!
|
|
1245
|
+
if (!fs5.existsSync(graphPath)) {
|
|
850
1246
|
throw new CliError(
|
|
851
1247
|
`no map found at ${graphPath} \u2014 run \`vg\` to build one first`,
|
|
852
1248
|
ExitCode.NOT_FOUND
|
|
@@ -885,7 +1281,7 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
|
|
|
885
1281
|
info(c.red(`vg serve: request error: ${err.message}`));
|
|
886
1282
|
}
|
|
887
1283
|
});
|
|
888
|
-
await new Promise((
|
|
1284
|
+
await new Promise((resolve16) => httpServer.listen(port, host, resolve16));
|
|
889
1285
|
info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
|
|
890
1286
|
await new Promise(() => {
|
|
891
1287
|
});
|
|
@@ -902,9 +1298,9 @@ function registerInstall(program) {
|
|
|
902
1298
|
json(ASSISTANTS.map((a) => ({ id: a.id, label: a.label, mcp: !!a.mcp, skill: !!a.skill, nudge: !!a.nudge })));
|
|
903
1299
|
} else {
|
|
904
1300
|
info(`${c.cyan("vg install")} \xB7 supported assistants`);
|
|
905
|
-
const
|
|
1301
|
+
const pad4 = Math.max(...ASSISTANTS.map((a) => a.id.length)) + 2;
|
|
906
1302
|
for (const a of ASSISTANTS) {
|
|
907
|
-
info(` ${c.bold(a.id.padEnd(
|
|
1303
|
+
info(` ${c.bold(a.id.padEnd(pad4))} ${a.label} ${c.dim(`mcp:${a.mcp ? "\u2713" : "\u2014"} skill:${a.skill ? "\u2713" : "\u2014"} nudge:${a.nudge ? "\u2713" : "\u2014"}`)}`);
|
|
908
1304
|
}
|
|
909
1305
|
}
|
|
910
1306
|
return;
|
|
@@ -964,8 +1360,8 @@ function registerShare(program) {
|
|
|
964
1360
|
const cmd = program.command("share").description("make the map committable + auto-updating for your team (git)").option("--undo", "reverse what `vg share` installed").option("--reports", "also commit graph.html / GRAPH_REPORT.md (default: gitignored)").action(function(opts) {
|
|
965
1361
|
const global = readGlobal(this);
|
|
966
1362
|
const root = rootOf(global);
|
|
967
|
-
const gitDir =
|
|
968
|
-
if (!
|
|
1363
|
+
const gitDir = path.join(root, ".git");
|
|
1364
|
+
if (!fs5.existsSync(gitDir)) {
|
|
969
1365
|
throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
|
|
970
1366
|
}
|
|
971
1367
|
if (opts.undo) {
|
|
@@ -986,23 +1382,23 @@ function registerShare(program) {
|
|
|
986
1382
|
applyGlobalOptions(cmd);
|
|
987
1383
|
}
|
|
988
1384
|
function writeVibgrateGitignore(root, reports) {
|
|
989
|
-
const dir =
|
|
990
|
-
|
|
1385
|
+
const dir = path.join(root, ".vibgrate");
|
|
1386
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
991
1387
|
const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
|
|
992
1388
|
if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
|
|
993
|
-
|
|
1389
|
+
fs5.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
|
|
994
1390
|
`);
|
|
995
1391
|
}
|
|
996
1392
|
function writeGitAttributes(root) {
|
|
997
|
-
const file =
|
|
1393
|
+
const file = path.join(root, ".gitattributes");
|
|
998
1394
|
const line = ".vibgrate/graph.json merge=vg";
|
|
999
|
-
let content =
|
|
1395
|
+
let content = fs5.existsSync(file) ? fs5.readFileSync(file, "utf8") : "";
|
|
1000
1396
|
if (!content.split("\n").some((l) => l.trim() === line)) {
|
|
1001
1397
|
content = content.length ? `${content.replace(/\s*$/, "")}
|
|
1002
1398
|
${line}
|
|
1003
1399
|
` : `${line}
|
|
1004
1400
|
`;
|
|
1005
|
-
|
|
1401
|
+
fs5.writeFileSync(file, content);
|
|
1006
1402
|
}
|
|
1007
1403
|
}
|
|
1008
1404
|
function configureMergeDriver(root) {
|
|
@@ -1011,8 +1407,8 @@ function configureMergeDriver(root) {
|
|
|
1011
1407
|
}
|
|
1012
1408
|
function installPreCommitHook(root) {
|
|
1013
1409
|
const hooksDir = hookDir(root);
|
|
1014
|
-
|
|
1015
|
-
const file =
|
|
1410
|
+
fs5.mkdirSync(hooksDir, { recursive: true });
|
|
1411
|
+
const file = path.join(hooksDir, "pre-commit");
|
|
1016
1412
|
const block = [
|
|
1017
1413
|
HOOK_BEGIN,
|
|
1018
1414
|
"if command -v vg >/dev/null 2>&1; then",
|
|
@@ -1021,7 +1417,7 @@ function installPreCommitHook(root) {
|
|
|
1021
1417
|
"fi",
|
|
1022
1418
|
HOOK_END
|
|
1023
1419
|
].join("\n");
|
|
1024
|
-
let content =
|
|
1420
|
+
let content = fs5.existsSync(file) ? fs5.readFileSync(file, "utf8") : "#!/bin/sh\n";
|
|
1025
1421
|
const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
|
|
1026
1422
|
if (re.test(content)) content = content.replace(re, block);
|
|
1027
1423
|
else content = `${content.replace(/\s*$/, "")}
|
|
@@ -1030,21 +1426,21 @@ ${block}
|
|
|
1030
1426
|
`;
|
|
1031
1427
|
if (!content.startsWith("#!")) content = `#!/bin/sh
|
|
1032
1428
|
${content}`;
|
|
1033
|
-
|
|
1034
|
-
|
|
1429
|
+
fs5.writeFileSync(file, content);
|
|
1430
|
+
fs5.chmodSync(file, 493);
|
|
1035
1431
|
}
|
|
1036
1432
|
function undo(root) {
|
|
1037
|
-
const hookFile =
|
|
1038
|
-
if (
|
|
1433
|
+
const hookFile = path.join(hookDir(root), "pre-commit");
|
|
1434
|
+
if (fs5.existsSync(hookFile)) {
|
|
1039
1435
|
const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
|
|
1040
|
-
const next2 =
|
|
1041
|
-
if (next2.trim() === "#!/bin/sh")
|
|
1042
|
-
else
|
|
1436
|
+
const next2 = fs5.readFileSync(hookFile, "utf8").replace(re, "\n");
|
|
1437
|
+
if (next2.trim() === "#!/bin/sh") fs5.rmSync(hookFile);
|
|
1438
|
+
else fs5.writeFileSync(hookFile, next2);
|
|
1043
1439
|
}
|
|
1044
|
-
const attr =
|
|
1045
|
-
if (
|
|
1046
|
-
const next2 =
|
|
1047
|
-
|
|
1440
|
+
const attr = path.join(root, ".gitattributes");
|
|
1441
|
+
if (fs5.existsSync(attr)) {
|
|
1442
|
+
const next2 = fs5.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
|
|
1443
|
+
fs5.writeFileSync(attr, next2);
|
|
1048
1444
|
}
|
|
1049
1445
|
try {
|
|
1050
1446
|
git(root, ["config", "--remove-section", "merge.vg"]);
|
|
@@ -1054,10 +1450,10 @@ function undo(root) {
|
|
|
1054
1450
|
function hookDir(root) {
|
|
1055
1451
|
try {
|
|
1056
1452
|
const configured = git(root, ["config", "--get", "core.hooksPath"]).trim();
|
|
1057
|
-
if (configured) return
|
|
1453
|
+
if (configured) return path.resolve(root, configured);
|
|
1058
1454
|
} catch {
|
|
1059
1455
|
}
|
|
1060
|
-
return
|
|
1456
|
+
return path.join(root, ".git", "hooks");
|
|
1061
1457
|
}
|
|
1062
1458
|
function git(root, args) {
|
|
1063
1459
|
return execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
@@ -1088,7 +1484,7 @@ function registerBenchmark(program) {
|
|
|
1088
1484
|
if (tokenReduction.questions.length) {
|
|
1089
1485
|
info(` token reduction vs grep/read baseline (${c.dim("estimates")}):`);
|
|
1090
1486
|
for (const q of tokenReduction.questions) {
|
|
1091
|
-
info(` ${
|
|
1487
|
+
info(` ${pad3(`${q.ratio.toFixed(1)}\xD7`, 6)} ${c.dim(`vg ${q.vgTokens} vs ~${q.baselineTokens}`)} "${q.question}"`);
|
|
1092
1488
|
}
|
|
1093
1489
|
info(` ${c.bold(`aggregate \u2248 ${tokenReduction.aggregateRatio.toFixed(1)}\xD7 fewer tokens`)} ${c.dim("(honest estimate, scales with repo size)")}`);
|
|
1094
1490
|
}
|
|
@@ -1177,7 +1573,7 @@ function grepBaselineTokens(term, files) {
|
|
|
1177
1573
|
if (f.size > 512 * 1024) continue;
|
|
1178
1574
|
let content = "";
|
|
1179
1575
|
try {
|
|
1180
|
-
content =
|
|
1576
|
+
content = fs5.readFileSync(f.abs, "utf8");
|
|
1181
1577
|
} catch {
|
|
1182
1578
|
continue;
|
|
1183
1579
|
}
|
|
@@ -1187,7 +1583,7 @@ function grepBaselineTokens(term, files) {
|
|
|
1187
1583
|
}
|
|
1188
1584
|
function safeSize(abs) {
|
|
1189
1585
|
try {
|
|
1190
|
-
return
|
|
1586
|
+
return fs5.statSync(abs).size;
|
|
1191
1587
|
} catch {
|
|
1192
1588
|
return 0;
|
|
1193
1589
|
}
|
|
@@ -1204,7 +1600,7 @@ function round(x) {
|
|
|
1204
1600
|
function round6(x) {
|
|
1205
1601
|
return Math.round(x * 1e6) / 1e6;
|
|
1206
1602
|
}
|
|
1207
|
-
function
|
|
1603
|
+
function pad3(s, n) {
|
|
1208
1604
|
return s.padStart(n, " ");
|
|
1209
1605
|
}
|
|
1210
1606
|
function registerTests(program) {
|
|
@@ -1517,11 +1913,11 @@ function normalizeStandards(raw) {
|
|
|
1517
1913
|
return { banned };
|
|
1518
1914
|
}
|
|
1519
1915
|
function loadStandards(root) {
|
|
1520
|
-
for (const
|
|
1521
|
-
const p =
|
|
1916
|
+
for (const rel3 of STANDARDS_FILES) {
|
|
1917
|
+
const p = path__default.join(root, rel3);
|
|
1522
1918
|
let text;
|
|
1523
1919
|
try {
|
|
1524
|
-
text =
|
|
1920
|
+
text = fs5__default.readFileSync(p, "utf8");
|
|
1525
1921
|
} catch {
|
|
1526
1922
|
continue;
|
|
1527
1923
|
}
|
|
@@ -1716,7 +2112,7 @@ async function addCmd(root, rest, opts, asJson) {
|
|
|
1716
2112
|
}
|
|
1717
2113
|
function readPackageVersion(root) {
|
|
1718
2114
|
try {
|
|
1719
|
-
const pkg = JSON.parse(
|
|
2115
|
+
const pkg = JSON.parse(fs5__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
|
|
1720
2116
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
1721
2117
|
} catch {
|
|
1722
2118
|
return void 0;
|
|
@@ -1724,7 +2120,7 @@ function readPackageVersion(root) {
|
|
|
1724
2120
|
}
|
|
1725
2121
|
function readOptionalFile(p) {
|
|
1726
2122
|
try {
|
|
1727
|
-
const t =
|
|
2123
|
+
const t = fs5__default.readFileSync(p, "utf8");
|
|
1728
2124
|
return t.trim() ? t : void 0;
|
|
1729
2125
|
} catch {
|
|
1730
2126
|
return void 0;
|
|
@@ -1739,8 +2135,8 @@ async function publishCmd(root, rest, opts, global, asJson) {
|
|
|
1739
2135
|
if (!parsed) throw new CliError("publishing a private library requires a DSN \u2014 run `vibgrate login` or set VIBGRATE_DSN", ExitCode.USAGE_ERROR);
|
|
1740
2136
|
const version = (opts.version || readPackageVersion(root) || "").trim();
|
|
1741
2137
|
if (!version) throw usageError("a --version is required (or run from a package whose package.json has a version)");
|
|
1742
|
-
const readme = readOptionalFile(opts.readme ?
|
|
1743
|
-
const dts = opts.dts ? readOptionalFile(
|
|
2138
|
+
const readme = readOptionalFile(opts.readme ? path__default.resolve(opts.readme) : path__default.join(root, "README.md"));
|
|
2139
|
+
const dts = opts.dts ? readOptionalFile(path__default.resolve(opts.dts)) : void 0;
|
|
1744
2140
|
if (!readme && !dts) throw usageError("nothing to publish \u2014 no README.md found and no --dts given");
|
|
1745
2141
|
const result = await publishPrivateLibrary(
|
|
1746
2142
|
{ name, version, readme, dts, language: opts.language },
|
|
@@ -1852,7 +2248,7 @@ async function showCmd(root, name, budget, asJson, opts = {}) {
|
|
|
1852
2248
|
out(doc);
|
|
1853
2249
|
}
|
|
1854
2250
|
function registerExport(program) {
|
|
1855
|
-
const cmd = program.command("export").description("export the map (format inferred: json|ndjson|graphml|dot|cypher|md|html|cdx.json|spdx.json)").argument("[file]", "target file (or - for stdout)", "map.json").action(function(file) {
|
|
2251
|
+
const cmd = program.command("export").description("export the map (format inferred: json|ndjson|graphml|dot|cypher|sql|md|html|cdx.json|spdx.json)").argument("[file]", "target file (or - for stdout)", "map.json").action(function(file) {
|
|
1856
2252
|
const global = readGlobal(this);
|
|
1857
2253
|
const { graph } = requireGraph(global);
|
|
1858
2254
|
const root = rootOf(global);
|
|
@@ -1869,8 +2265,8 @@ function registerExport(program) {
|
|
|
1869
2265
|
out(content.trimEnd());
|
|
1870
2266
|
return;
|
|
1871
2267
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
2268
|
+
fs5.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
2269
|
+
fs5.writeFileSync(file, content);
|
|
1874
2270
|
info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
|
|
1875
2271
|
});
|
|
1876
2272
|
applyGlobalOptions(cmd);
|
|
@@ -1880,37 +2276,37 @@ function detectFormat(file) {
|
|
|
1880
2276
|
if (lower.endsWith(".cdx.json")) return "cyclonedx";
|
|
1881
2277
|
if (lower.endsWith(".spdx.json")) return "spdx";
|
|
1882
2278
|
if (file === "-") return "json";
|
|
1883
|
-
return formatForExt(
|
|
2279
|
+
return formatForExt(path.extname(file));
|
|
1884
2280
|
}
|
|
1885
2281
|
function registerBundle(program) {
|
|
1886
2282
|
const cmd = program.command("bundle").description("build an air-gapped bundle (grammars + graph + library catalog)").option("--offline", "offline bundle (default; present for clarity)").option("-o, --out <dir>", "output directory", "vg-bundle").action(function(opts) {
|
|
1887
2283
|
const global = readGlobal(this);
|
|
1888
2284
|
const root = rootOf(global);
|
|
1889
|
-
const outDir =
|
|
2285
|
+
const outDir = path.resolve(root, opts.out ?? "vg-bundle");
|
|
1890
2286
|
let grammarFiles;
|
|
1891
2287
|
try {
|
|
1892
2288
|
grammarFiles = resolvedGrammarFiles();
|
|
1893
2289
|
} catch {
|
|
1894
2290
|
throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
|
|
1895
2291
|
}
|
|
1896
|
-
|
|
2292
|
+
fs5.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
|
|
1897
2293
|
let grammarCount = 0;
|
|
1898
2294
|
for (const { fileName, absPath } of grammarFiles) {
|
|
1899
|
-
|
|
2295
|
+
fs5.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
|
|
1900
2296
|
grammarCount++;
|
|
1901
2297
|
}
|
|
1902
2298
|
const included = [`grammars/ (${grammarCount} .wasm)`];
|
|
1903
2299
|
const graphSrc = global.graph ?? defaultGraphPath(root);
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
2300
|
+
if (fs5.existsSync(graphSrc)) {
|
|
2301
|
+
fs5.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
|
|
1906
2302
|
included.push("graph.json");
|
|
1907
2303
|
}
|
|
1908
2304
|
const cat = catalogPath(root);
|
|
1909
|
-
if (
|
|
1910
|
-
|
|
2305
|
+
if (fs5.existsSync(cat)) {
|
|
2306
|
+
fs5.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
|
|
1911
2307
|
included.push("vibgrate.lib.json");
|
|
1912
|
-
if (
|
|
1913
|
-
|
|
2308
|
+
if (fs5.existsSync(libDir(root))) {
|
|
2309
|
+
fs5.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
|
|
1914
2310
|
included.push("lib/");
|
|
1915
2311
|
}
|
|
1916
2312
|
}
|
|
@@ -1922,38 +2318,38 @@ function registerBundle(program) {
|
|
|
1922
2318
|
included,
|
|
1923
2319
|
usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
|
|
1924
2320
|
};
|
|
1925
|
-
|
|
2321
|
+
fs5.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
|
|
1926
2322
|
`);
|
|
1927
2323
|
if (global.json) {
|
|
1928
|
-
json({ out:
|
|
2324
|
+
json({ out: path.relative(root, outDir), ...manifest });
|
|
1929
2325
|
return;
|
|
1930
2326
|
}
|
|
1931
|
-
info(`${c.cyan("vg bundle")} \xB7 wrote ${
|
|
2327
|
+
info(`${c.cyan("vg bundle")} \xB7 wrote ${path.relative(root, outDir)}/`);
|
|
1932
2328
|
for (const i of included) info(` ${c.green("\u2714")} ${i}`);
|
|
1933
|
-
info(c.dim(` offline: vg build --grammars "${
|
|
2329
|
+
info(c.dim(` offline: vg build --grammars "${path.relative(root, outDir)}/grammars"`));
|
|
1934
2330
|
});
|
|
1935
2331
|
applyGlobalOptions(cmd);
|
|
1936
2332
|
}
|
|
1937
2333
|
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) => {
|
|
1938
|
-
const rootDir =
|
|
1939
|
-
const vibgrateDir2 =
|
|
2334
|
+
const rootDir = path.resolve(targetPath);
|
|
2335
|
+
const vibgrateDir2 = path.join(rootDir, ".vibgrate");
|
|
1940
2336
|
await ensureDir(vibgrateDir2);
|
|
1941
|
-
console.log(
|
|
1942
|
-
const configPath =
|
|
2337
|
+
console.log(chalk7.green("\u2714") + ` Created ${chalk7.bold(".vibgrate/")} directory`);
|
|
2338
|
+
const configPath = path.join(rootDir, "vibgrate.config.ts");
|
|
1943
2339
|
if (await pathExists(configPath)) {
|
|
1944
|
-
console.log(
|
|
2340
|
+
console.log(chalk7.dim(" vibgrate.config.ts already exists, skipping"));
|
|
1945
2341
|
} else {
|
|
1946
2342
|
await writeDefaultConfig(rootDir);
|
|
1947
|
-
console.log(
|
|
2343
|
+
console.log(chalk7.green("\u2714") + ` Created ${chalk7.bold("vibgrate.config.ts")}`);
|
|
1948
2344
|
}
|
|
1949
2345
|
if (opts.baseline) {
|
|
1950
|
-
const { runBaseline } = await import('./baseline-
|
|
2346
|
+
const { runBaseline } = await import('./baseline-RRO4NNNF.js');
|
|
1951
2347
|
await runBaseline(rootDir);
|
|
1952
2348
|
}
|
|
1953
2349
|
console.log("");
|
|
1954
|
-
console.log(
|
|
1955
|
-
console.log(` ${
|
|
1956
|
-
console.log(` ${
|
|
2350
|
+
console.log(chalk7.bold("Next steps:"));
|
|
2351
|
+
console.log(` ${chalk7.cyan("vibgrate scan")} Scan for upgrade drift`);
|
|
2352
|
+
console.log(` ${chalk7.cyan("vibgrate baseline")} Create a drift baseline`);
|
|
1957
2353
|
console.log("");
|
|
1958
2354
|
});
|
|
1959
2355
|
|
|
@@ -1980,7 +2376,7 @@ var AI_ASSISTANT_DETECTORS = [
|
|
|
1980
2376
|
async function detectAiAssistant(rootDir) {
|
|
1981
2377
|
for (const detector of AI_ASSISTANT_DETECTORS) {
|
|
1982
2378
|
for (const file of detector.files) {
|
|
1983
|
-
if (await pathExists$1(
|
|
2379
|
+
if (await pathExists$1(path.join(rootDir, file))) {
|
|
1984
2380
|
return detector.name;
|
|
1985
2381
|
}
|
|
1986
2382
|
}
|
|
@@ -1988,22 +2384,22 @@ async function detectAiAssistant(rootDir) {
|
|
|
1988
2384
|
return null;
|
|
1989
2385
|
}
|
|
1990
2386
|
function printAiContextPrompt(detectedAssistant) {
|
|
1991
|
-
const teal2 =
|
|
1992
|
-
const mint2 =
|
|
2387
|
+
const teal2 = chalk7.hex("#3FB0A4");
|
|
2388
|
+
const mint2 = chalk7.hex("#4FE3C1");
|
|
1993
2389
|
const cli = resolveCliInvocation();
|
|
1994
2390
|
const installArgs = detectedAssistant ? `install ${detectedAssistant}` : "install --all";
|
|
1995
2391
|
const installCmd = `${cli} ${installArgs}`;
|
|
1996
|
-
const detectedNote = detectedAssistant ?
|
|
2392
|
+
const detectedNote = detectedAssistant ? chalk7.dim(` (${detectedAssistant} config detected)`) : "";
|
|
1997
2393
|
console.log("");
|
|
1998
2394
|
console.log(teal2(" \u256D\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\u256E"));
|
|
1999
|
-
console.log(teal2(" \u2502") + " " + mint2("\u25C6") + " " +
|
|
2395
|
+
console.log(teal2(" \u2502") + " " + mint2("\u25C6") + " " + chalk7.bold.white("Get AI-aware answers in your editor") + " " + teal2("\u2502"));
|
|
2000
2396
|
console.log(teal2(" \u2570\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\u256F"));
|
|
2001
2397
|
console.log("");
|
|
2002
|
-
console.log(" " +
|
|
2398
|
+
console.log(" " + chalk7.bold.white(installCmd) + detectedNote);
|
|
2003
2399
|
console.log("");
|
|
2004
|
-
console.log(" " + teal2("\xB7") + " " +
|
|
2005
|
-
console.log(" " + teal2("\xB7") + " " +
|
|
2006
|
-
console.log(" " + teal2("\xB7") + " " +
|
|
2400
|
+
console.log(" " + teal2("\xB7") + " " + chalk7.white("Code map in your assistant") + chalk7.dim(" \u2014 call trees, impact surfaces, import paths"));
|
|
2401
|
+
console.log(" " + teal2("\xB7") + " " + chalk7.white("Offline drift") + chalk7.dim(" \u2014 DriftScore and upgrade priorities, right inside your editor"));
|
|
2402
|
+
console.log(" " + teal2("\xB7") + " " + chalk7.white("Version-correct library docs") + chalk7.dim(" \u2014 pinned to your lockfile, no hallucinated APIs"));
|
|
2007
2403
|
console.log("");
|
|
2008
2404
|
}
|
|
2009
2405
|
|
|
@@ -2014,14 +2410,14 @@ function shouldBuildCodeMap(opts) {
|
|
|
2014
2410
|
async function autoPush(artifact, rootDir, opts) {
|
|
2015
2411
|
const dsn = resolveDsn(opts.dsn);
|
|
2016
2412
|
if (!dsn) {
|
|
2017
|
-
console.error(
|
|
2018
|
-
console.error(
|
|
2413
|
+
console.error(chalk7.red("No DSN provided for push."));
|
|
2414
|
+
console.error(chalk7.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
|
|
2019
2415
|
if (opts.strict) process.exit(1);
|
|
2020
2416
|
return;
|
|
2021
2417
|
}
|
|
2022
2418
|
const parsed = parseDsn(dsn);
|
|
2023
2419
|
if (!parsed) {
|
|
2024
|
-
console.error(
|
|
2420
|
+
console.error(chalk7.red("Invalid DSN format."));
|
|
2025
2421
|
if (opts.strict) process.exit(1);
|
|
2026
2422
|
return;
|
|
2027
2423
|
}
|
|
@@ -2032,7 +2428,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2032
2428
|
try {
|
|
2033
2429
|
host = resolveIngestHost(opts.region);
|
|
2034
2430
|
} catch (e) {
|
|
2035
|
-
console.error(
|
|
2431
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
2036
2432
|
if (opts.strict) process.exit(1);
|
|
2037
2433
|
return;
|
|
2038
2434
|
}
|
|
@@ -2040,7 +2436,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2040
2436
|
const originalSize = JSON.stringify(artifact).length;
|
|
2041
2437
|
const compressedSize = body.length;
|
|
2042
2438
|
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
2043
|
-
console.log(
|
|
2439
|
+
console.log(chalk7.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
2044
2440
|
try {
|
|
2045
2441
|
const { response, host: uploadedHost } = await uploadScanArtifact({
|
|
2046
2442
|
scheme: parsed.scheme,
|
|
@@ -2060,36 +2456,36 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2060
2456
|
const result = await response.json();
|
|
2061
2457
|
if (result.unchanged) {
|
|
2062
2458
|
console.log(
|
|
2063
|
-
|
|
2459
|
+
chalk7.green("\u2714") + ` Repository unchanged since ${result.lastScannedAt ?? "last scan"} \u2014 skipped upload (no credit used).`
|
|
2064
2460
|
);
|
|
2065
2461
|
if (result.previousIngestId) {
|
|
2066
2462
|
emitIngestIdLine(result.previousIngestId, { unchanged: true });
|
|
2067
2463
|
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.previousIngestId}`;
|
|
2068
|
-
console.log(
|
|
2464
|
+
console.log(chalk7.dim(` Previous report: ${dashUrl}`));
|
|
2069
2465
|
} else if (opts.strict) {
|
|
2070
|
-
console.error(
|
|
2466
|
+
console.error(chalk7.red("Repository unchanged but no previous ingest id returned."));
|
|
2071
2467
|
process.exit(1);
|
|
2072
2468
|
}
|
|
2073
2469
|
return;
|
|
2074
2470
|
}
|
|
2075
|
-
console.log(
|
|
2471
|
+
console.log(chalk7.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
2076
2472
|
if (result.ingestId) {
|
|
2077
2473
|
emitIngestIdLine(result.ingestId);
|
|
2078
2474
|
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
2079
2475
|
const CLEAR_LINE = process.platform === "win32" ? "\x1B[0G\x1B[2K" : "";
|
|
2080
2476
|
console.log("");
|
|
2081
|
-
console.log(CLEAR_LINE +
|
|
2477
|
+
console.log(CLEAR_LINE + chalk7.dim(" Processing continues in the background. Results available shortly."));
|
|
2082
2478
|
console.log("");
|
|
2083
|
-
console.log(CLEAR_LINE +
|
|
2084
|
-
console.log(CLEAR_LINE +
|
|
2085
|
-
console.log(CLEAR_LINE + " " +
|
|
2086
|
-
console.log(CLEAR_LINE +
|
|
2479
|
+
console.log(CLEAR_LINE + chalk7.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"));
|
|
2480
|
+
console.log(CLEAR_LINE + chalk7.bold(" \u{1F4CA} View Scan Report"));
|
|
2481
|
+
console.log(CLEAR_LINE + " " + chalk7.underline.cyan(dashUrl));
|
|
2482
|
+
console.log(CLEAR_LINE + chalk7.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"));
|
|
2087
2483
|
console.log("");
|
|
2088
2484
|
console.log("");
|
|
2089
2485
|
}
|
|
2090
2486
|
} catch (e) {
|
|
2091
2487
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2092
|
-
console.error(
|
|
2488
|
+
console.error(chalk7.red(`Upload failed: ${msg}`));
|
|
2093
2489
|
if (opts.strict) process.exit(1);
|
|
2094
2490
|
}
|
|
2095
2491
|
}
|
|
@@ -2101,15 +2497,15 @@ function reportStandards(rootDir) {
|
|
|
2101
2497
|
if (!loaded.policy) return;
|
|
2102
2498
|
const violations = checkStandards(loaded.policy, inventory(rootDir).records);
|
|
2103
2499
|
if (violations.length === 0) {
|
|
2104
|
-
console.log(
|
|
2500
|
+
console.log(chalk7.green("\u2714") + " Standards: no banned dependencies in use");
|
|
2105
2501
|
return;
|
|
2106
2502
|
}
|
|
2107
|
-
console.log(
|
|
2503
|
+
console.log(chalk7.red(`
|
|
2108
2504
|
\u26A0 Standards: ${violations.length} banned dependency(ies) in use`));
|
|
2109
2505
|
for (const v of violations) {
|
|
2110
|
-
const fix = v.use ?
|
|
2111
|
-
const why = v.reason ?
|
|
2112
|
-
console.log(` ${
|
|
2506
|
+
const fix = v.use ? chalk7.dim(` \u2192 use ${v.use}`) : "";
|
|
2507
|
+
const why = v.reason ? chalk7.dim(` (${v.reason})`) : "";
|
|
2508
|
+
console.log(` ${chalk7.red("banned")} ${v.ecosystem}:${v.name}${v.installed ? chalk7.dim(` ${v.installed}`) : ""}${fix}${why}`);
|
|
2113
2509
|
}
|
|
2114
2510
|
}
|
|
2115
2511
|
function parseNonNegativeNumber(value, label) {
|
|
@@ -2126,16 +2522,18 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2126
2522
|
collectExcludes,
|
|
2127
2523
|
[]
|
|
2128
2524
|
).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("--full", "Comprehensive scan: turns on known-vulnerability detection (= --vulns) and, when a standards policy exists, a banned-dependency report \u2014 on top of drift scoring and the code map").option("--vulns", "Also scan installed dependencies for known vulnerabilities (OSV online, or advisories from --package-manifest when offline)").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").option("--repository-name <name>", "Override the repository name recorded for this scan (defaults to the directory or package.json name)").option("--force", "Always create a fresh scan ingest, even if the repository is unchanged since the last scan (skips the unchanged/reuse optimization). Used by scheduled and dashboard-triggered scans.").option("--no-graph", "Skip building the local code map (the AI/docs index) that scan produces after scoring drift").option("--quiet", "Suppress promotional output (the free-plan tracking panel and the AI Context install prompt); scan results are unaffected").action(async (targetPath, opts) => {
|
|
2129
|
-
const rootDir =
|
|
2525
|
+
const rootDir = path.resolve(targetPath);
|
|
2130
2526
|
if (!await pathExists$1(rootDir)) {
|
|
2131
|
-
console.error(
|
|
2527
|
+
console.error(chalk7.red(`Path does not exist: ${rootDir}`));
|
|
2132
2528
|
process.exit(1);
|
|
2133
2529
|
}
|
|
2134
2530
|
const hasDsn = !!resolveDsn(opts.dsn);
|
|
2135
2531
|
const willPush = !opts.offline && (opts.push || hasDsn);
|
|
2136
2532
|
const noLocalArtifacts = opts.localArtifacts === false;
|
|
2137
|
-
const isFirstRun = !await pathExists$1(
|
|
2533
|
+
const isFirstRun = !await pathExists$1(path.join(rootDir, ".vibgrate", "scan_result.json"));
|
|
2138
2534
|
let pinnedRegion;
|
|
2535
|
+
let planTier;
|
|
2536
|
+
let upgradeUrl;
|
|
2139
2537
|
if (willPush && hasDsn) {
|
|
2140
2538
|
const dsn = resolveDsn(opts.dsn);
|
|
2141
2539
|
const parsed = parseDsn(dsn);
|
|
@@ -2150,10 +2548,12 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2150
2548
|
vcsSha: fingerprint.vcsSha
|
|
2151
2549
|
});
|
|
2152
2550
|
pinnedRegion = preflight.region;
|
|
2551
|
+
planTier = preflight.plan?.tier;
|
|
2552
|
+
upgradeUrl = preflight.upgradeUrl ?? `https://${dashHostForIngestHost(preflight.ingestHost ?? ingestHost)}/${parsed.workspaceId}`;
|
|
2153
2553
|
if (preflight.vm && !preflight.vm.allowed) {
|
|
2154
|
-
console.error(
|
|
2554
|
+
console.error(chalk7.red(preflight.error ?? "VM meter usage exhausted"));
|
|
2155
2555
|
console.error(
|
|
2156
|
-
|
|
2556
|
+
chalk7.dim(
|
|
2157
2557
|
`VM minutes: ${preflight.vm.used}/${preflight.vm.limit} (${preflight.plan.label} plan) \u2014 enable overages or upgrade your plan.`
|
|
2158
2558
|
)
|
|
2159
2559
|
);
|
|
@@ -2161,25 +2561,25 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2161
2561
|
}
|
|
2162
2562
|
if (preflight.repositories && !preflight.repositories.allowed) {
|
|
2163
2563
|
console.error(
|
|
2164
|
-
|
|
2564
|
+
chalk7.red(
|
|
2165
2565
|
preflight.error ?? `Repository limit reached for the ${preflight.plan.label} plan \u2014 cannot scan a new repository.`
|
|
2166
2566
|
)
|
|
2167
2567
|
);
|
|
2168
2568
|
const max = preflight.repositories.max < 0 ? "unlimited" : String(preflight.repositories.max);
|
|
2169
2569
|
console.error(
|
|
2170
|
-
|
|
2570
|
+
chalk7.dim(
|
|
2171
2571
|
`Repositories: ${preflight.repositories.total}/${max} (${preflight.plan.label} plan) \u2014 archive a repository or upgrade your plan.`
|
|
2172
2572
|
)
|
|
2173
2573
|
);
|
|
2174
2574
|
if (preflight.upgradeUrl) {
|
|
2175
|
-
console.error(
|
|
2575
|
+
console.error(chalk7.dim(` Upgrade: ${preflight.upgradeUrl}`));
|
|
2176
2576
|
}
|
|
2177
2577
|
process.exit(1);
|
|
2178
2578
|
}
|
|
2179
2579
|
if (preflight.status === "error" || !preflight.scans.allowed) {
|
|
2180
|
-
console.error(
|
|
2580
|
+
console.error(chalk7.red(preflight.error ?? "Scan ingestion not allowed for this workspace."));
|
|
2181
2581
|
console.error(
|
|
2182
|
-
|
|
2582
|
+
chalk7.dim(
|
|
2183
2583
|
`Credits: ${preflight.scans.used}/${preflight.scans.limit} (${preflight.plan.label} plan)`
|
|
2184
2584
|
)
|
|
2185
2585
|
);
|
|
@@ -2187,27 +2587,27 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2187
2587
|
process.exit(1);
|
|
2188
2588
|
}
|
|
2189
2589
|
console.log(
|
|
2190
|
-
|
|
2590
|
+
chalk7.dim(
|
|
2191
2591
|
`Plan: ${preflight.plan.label} \u2014 scan credits ${preflight.scans.used}/${preflight.scans.limit} this month`
|
|
2192
2592
|
)
|
|
2193
2593
|
);
|
|
2194
2594
|
if (preflight.repository?.unchanged && !opts.force) {
|
|
2195
2595
|
console.log(
|
|
2196
|
-
|
|
2596
|
+
chalk7.green("\u2714") + ` Repository unchanged at ${preflight.repository.lastVcsSha?.slice(0, 7) ?? "same revision"} \u2014 skipping scan.`
|
|
2197
2597
|
);
|
|
2198
2598
|
if (preflight.repository.lastIngestId) {
|
|
2199
2599
|
emitIngestIdLine(preflight.repository.lastIngestId, { unchanged: true });
|
|
2200
2600
|
const dashUrl = `https://${dashHostForIngestHost(ingestHost)}/${parsed.workspaceId}/scan/${preflight.repository.lastIngestId}`;
|
|
2201
|
-
console.log(
|
|
2601
|
+
console.log(chalk7.dim(` Latest report: ${dashUrl}`));
|
|
2202
2602
|
} else if (opts.strict) {
|
|
2203
|
-
console.error(
|
|
2603
|
+
console.error(chalk7.red("Repository unchanged but no previous ingest id available."));
|
|
2204
2604
|
process.exit(1);
|
|
2205
2605
|
}
|
|
2206
2606
|
return;
|
|
2207
2607
|
}
|
|
2208
2608
|
} catch (e) {
|
|
2209
2609
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2210
|
-
console.error(
|
|
2610
|
+
console.error(chalk7.yellow(`Preflight check failed: ${msg}`));
|
|
2211
2611
|
if (opts.strict) process.exit(1);
|
|
2212
2612
|
}
|
|
2213
2613
|
}
|
|
@@ -2240,6 +2640,17 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2240
2640
|
repositoryName: opts.repositoryName?.trim() || void 0,
|
|
2241
2641
|
force: opts.force,
|
|
2242
2642
|
quiet: opts.quiet,
|
|
2643
|
+
// Auth + plan signals for the free-plan upsell panel. `hasDsn` resolves the
|
|
2644
|
+
// full credential precedence — `--dsn`, `VIBGRATE_DSN`, and the stored login
|
|
2645
|
+
// credential (`~/.vibgrate/credentials.json`) — which the scanner's own
|
|
2646
|
+
// dsn/env-only check cannot see. `planTier`/`upgradeUrl` come from preflight.
|
|
2647
|
+
// Together they pick the panel audience: signed out → login CTA; signed in
|
|
2648
|
+
// on free → upgrade CTA; signed in on a paid plan → no panel. Without the
|
|
2649
|
+
// auth signal, a logged-in user running a bare scan was mislabelled
|
|
2650
|
+
// "Vibgrate Free" and shown the login panel even as the run pushed.
|
|
2651
|
+
authenticated: hasDsn,
|
|
2652
|
+
planTier,
|
|
2653
|
+
upgradeUrl,
|
|
2243
2654
|
// Prefix for the upsell panel's `login → push` hint — `vg` when installed,
|
|
2244
2655
|
// `npx @vibgrate/cli` when the user ran via npx (where bare `vg` fails).
|
|
2245
2656
|
invocation: resolveCliInvocation()
|
|
@@ -2270,24 +2681,24 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2270
2681
|
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
2271
2682
|
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
2272
2683
|
if (opts.failOn === "error" && hasErrors) {
|
|
2273
|
-
console.error(
|
|
2684
|
+
console.error(chalk7.red(`
|
|
2274
2685
|
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
2275
2686
|
process.exit(2);
|
|
2276
2687
|
}
|
|
2277
2688
|
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
2278
|
-
console.error(
|
|
2689
|
+
console.error(chalk7.red(`
|
|
2279
2690
|
Failing: findings detected at warn level or above.`));
|
|
2280
2691
|
process.exit(2);
|
|
2281
2692
|
}
|
|
2282
2693
|
}
|
|
2283
2694
|
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
2284
|
-
console.error(
|
|
2695
|
+
console.error(chalk7.red(`
|
|
2285
2696
|
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
2286
2697
|
process.exit(2);
|
|
2287
2698
|
}
|
|
2288
2699
|
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
2289
2700
|
if (artifact.delta === void 0) {
|
|
2290
|
-
console.error(
|
|
2701
|
+
console.error(chalk7.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
2291
2702
|
process.exit(2);
|
|
2292
2703
|
}
|
|
2293
2704
|
if (artifact.delta > 0) {
|
|
@@ -2295,7 +2706,7 @@ Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget
|
|
|
2295
2706
|
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
2296
2707
|
const worseningPercent = artifact.delta / denominator * 100;
|
|
2297
2708
|
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
2298
|
-
console.error(
|
|
2709
|
+
console.error(chalk7.red(`
|
|
2299
2710
|
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
2300
2711
|
process.exit(2);
|
|
2301
2712
|
}
|
|
@@ -2310,22 +2721,853 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
|
|
|
2310
2721
|
printAiContextPrompt(detectedAssistant);
|
|
2311
2722
|
}
|
|
2312
2723
|
});
|
|
2724
|
+
var WATCHED_FILES = /* @__PURE__ */ new Set([
|
|
2725
|
+
// npm
|
|
2726
|
+
"package.json",
|
|
2727
|
+
"package-lock.json",
|
|
2728
|
+
"npm-shrinkwrap.json",
|
|
2729
|
+
"pnpm-lock.yaml",
|
|
2730
|
+
"yarn.lock",
|
|
2731
|
+
"bun.lockb",
|
|
2732
|
+
// pypi
|
|
2733
|
+
"requirements.txt",
|
|
2734
|
+
"pyproject.toml",
|
|
2735
|
+
"poetry.lock",
|
|
2736
|
+
"Pipfile",
|
|
2737
|
+
"Pipfile.lock",
|
|
2738
|
+
"uv.lock",
|
|
2739
|
+
// go
|
|
2740
|
+
"go.mod",
|
|
2741
|
+
"go.sum",
|
|
2742
|
+
// rust
|
|
2743
|
+
"Cargo.toml",
|
|
2744
|
+
"Cargo.lock",
|
|
2745
|
+
// ruby
|
|
2746
|
+
"Gemfile",
|
|
2747
|
+
"Gemfile.lock",
|
|
2748
|
+
// php
|
|
2749
|
+
"composer.json",
|
|
2750
|
+
"composer.lock",
|
|
2751
|
+
// swift
|
|
2752
|
+
"Package.swift",
|
|
2753
|
+
"Package.resolved",
|
|
2754
|
+
// dart
|
|
2755
|
+
"pubspec.yaml",
|
|
2756
|
+
"pubspec.lock",
|
|
2757
|
+
// java
|
|
2758
|
+
"pom.xml",
|
|
2759
|
+
"build.gradle",
|
|
2760
|
+
"build.gradle.kts",
|
|
2761
|
+
"gradle.lockfile",
|
|
2762
|
+
// dotnet
|
|
2763
|
+
"packages.lock.json"
|
|
2764
|
+
]);
|
|
2765
|
+
var WATCHED_EXT = /\.(cs|fs)proj$/i;
|
|
2766
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2767
|
+
"node_modules",
|
|
2768
|
+
".git",
|
|
2769
|
+
"dist",
|
|
2770
|
+
"build",
|
|
2771
|
+
"out",
|
|
2772
|
+
".vibgrate",
|
|
2773
|
+
"vendor",
|
|
2774
|
+
".venv",
|
|
2775
|
+
"venv",
|
|
2776
|
+
"env",
|
|
2777
|
+
"__pycache__",
|
|
2778
|
+
"target",
|
|
2779
|
+
".next",
|
|
2780
|
+
".nuxt",
|
|
2781
|
+
"coverage",
|
|
2782
|
+
".cache"
|
|
2783
|
+
]);
|
|
2784
|
+
function scanStaleness(rootDir, artifactPath) {
|
|
2785
|
+
let ref;
|
|
2786
|
+
try {
|
|
2787
|
+
ref = fs5.statSync(artifactPath).mtimeMs;
|
|
2788
|
+
} catch {
|
|
2789
|
+
return { stale: false };
|
|
2790
|
+
}
|
|
2791
|
+
let newest = -Infinity;
|
|
2792
|
+
let newestChanged;
|
|
2793
|
+
const MAX_ENTRIES = 2e4;
|
|
2794
|
+
let scanned = 0;
|
|
2795
|
+
const walk2 = (dir, depth) => {
|
|
2796
|
+
if (depth > 8 || scanned > MAX_ENTRIES) return;
|
|
2797
|
+
let entries;
|
|
2798
|
+
try {
|
|
2799
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2800
|
+
} catch {
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
for (const e of entries) {
|
|
2804
|
+
scanned++;
|
|
2805
|
+
if (scanned > MAX_ENTRIES) break;
|
|
2806
|
+
if (e.isDirectory()) {
|
|
2807
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk2(path.join(dir, e.name), depth + 1);
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
if (!WATCHED_FILES.has(e.name) && !WATCHED_EXT.test(e.name)) continue;
|
|
2811
|
+
const abs = path.join(dir, e.name);
|
|
2812
|
+
let mtimeMs;
|
|
2813
|
+
try {
|
|
2814
|
+
mtimeMs = fs5.statSync(abs).mtimeMs;
|
|
2815
|
+
} catch {
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
if (mtimeMs > newest) {
|
|
2819
|
+
newest = mtimeMs;
|
|
2820
|
+
newestChanged = path.relative(rootDir, abs) || e.name;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
walk2(rootDir, 0);
|
|
2825
|
+
return newest > ref ? { stale: true, newestChanged } : { stale: false };
|
|
2826
|
+
}
|
|
2827
|
+
var MAX_CONTRACTS = 40;
|
|
2828
|
+
function escapeRegExp(s) {
|
|
2829
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2830
|
+
}
|
|
2831
|
+
function parseJsClause(clause, out2) {
|
|
2832
|
+
const trimmed = clause.trim();
|
|
2833
|
+
if (!trimmed) return;
|
|
2834
|
+
if (/^\*\s+as\s+\w+/.test(trimmed)) {
|
|
2835
|
+
out2.add("* (namespace)");
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
const braceMatch = /\{([^}]*)\}/.exec(trimmed);
|
|
2839
|
+
const beforeBrace = braceMatch ? trimmed.slice(0, braceMatch.index) : trimmed;
|
|
2840
|
+
const defaultId = beforeBrace.replace(/,/g, "").trim();
|
|
2841
|
+
if (defaultId && /^\w+$/.test(defaultId)) out2.add("default");
|
|
2842
|
+
if (braceMatch) {
|
|
2843
|
+
for (const raw of braceMatch[1].split(",")) {
|
|
2844
|
+
const name = raw.trim().split(/\s+as\s+/)[0]?.trim();
|
|
2845
|
+
if (name && /^[\w$]+$/.test(name)) out2.add(name);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function extractContracts(content, pkg, ecosystem) {
|
|
2850
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
2851
|
+
const e = escapeRegExp(pkg);
|
|
2852
|
+
if (ecosystem === "npm") {
|
|
2853
|
+
const importRe = new RegExp(`import\\s+([^;'"\\n]*?)\\s+from\\s+['"]${e}(?:/[^'"]*)?['"]`, "g");
|
|
2854
|
+
let m;
|
|
2855
|
+
while (m = importRe.exec(content)) parseJsClause(m[1], out2);
|
|
2856
|
+
const requireDestructureRe = new RegExp(`\\{([^}]*)\\}\\s*=\\s*require\\(\\s*['"]${e}(?:/[^'"]*)?['"]\\s*\\)`, "g");
|
|
2857
|
+
while (m = requireDestructureRe.exec(content)) {
|
|
2858
|
+
for (const raw of m[1].split(",")) {
|
|
2859
|
+
const name = raw.trim().split(":")[0]?.trim();
|
|
2860
|
+
if (name && /^[\w$]+$/.test(name)) out2.add(name);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
} else if (ecosystem === "pypi") {
|
|
2864
|
+
const fromRe = new RegExp(`^\\s*from\\s+${e}(?:\\.[\\w.]+)?\\s+import\\s+(.+)$`, "gm");
|
|
2865
|
+
let m;
|
|
2866
|
+
while (m = fromRe.exec(content)) {
|
|
2867
|
+
const names = m[1].replace(/[()]/g, "").split(",");
|
|
2868
|
+
for (const raw of names) {
|
|
2869
|
+
const name = raw.trim().split(/\s+as\s+/)[0]?.trim();
|
|
2870
|
+
if (name === "*") out2.add("* (module)");
|
|
2871
|
+
else if (name && /^[\w.]+$/.test(name)) out2.add(name);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
const importRe = new RegExp(`^\\s*import\\s+${e}\\b`, "gm");
|
|
2875
|
+
if (importRe.test(content)) out2.add(`${pkg} (module)`);
|
|
2876
|
+
}
|
|
2877
|
+
return [...out2].sort();
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// src/reporting/planning/usage.ts
|
|
2881
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
2882
|
+
"node_modules",
|
|
2883
|
+
".git",
|
|
2884
|
+
"dist",
|
|
2885
|
+
"build",
|
|
2886
|
+
"out",
|
|
2887
|
+
".vibgrate",
|
|
2888
|
+
"vendor",
|
|
2889
|
+
".venv",
|
|
2890
|
+
"venv",
|
|
2891
|
+
"env",
|
|
2892
|
+
"__pycache__",
|
|
2893
|
+
"target",
|
|
2894
|
+
".next",
|
|
2895
|
+
".nuxt",
|
|
2896
|
+
"coverage",
|
|
2897
|
+
".cache"
|
|
2898
|
+
]);
|
|
2899
|
+
var MAX_FILES = 8e3;
|
|
2900
|
+
var MAX_DEPTH = 12;
|
|
2901
|
+
var MAX_FILE_BYTES = 1048576;
|
|
2902
|
+
var EXT_ECOSYSTEM = {
|
|
2903
|
+
".ts": "npm",
|
|
2904
|
+
".tsx": "npm",
|
|
2905
|
+
".js": "npm",
|
|
2906
|
+
".jsx": "npm",
|
|
2907
|
+
".mjs": "npm",
|
|
2908
|
+
".cjs": "npm",
|
|
2909
|
+
".mts": "npm",
|
|
2910
|
+
".cts": "npm",
|
|
2911
|
+
".vue": "npm",
|
|
2912
|
+
".svelte": "npm",
|
|
2913
|
+
".py": "pypi"
|
|
2914
|
+
};
|
|
2915
|
+
function escapeRegExp2(s) {
|
|
2916
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2917
|
+
}
|
|
2918
|
+
function importPattern(ecosystem, pkg) {
|
|
2919
|
+
const e = escapeRegExp2(pkg);
|
|
2920
|
+
if (ecosystem === "npm") {
|
|
2921
|
+
return new RegExp(`(?:from\\s+|require\\(\\s*|import\\(\\s*)['"]${e}(?:/[^'"]*)?['"]`, "g");
|
|
2922
|
+
}
|
|
2923
|
+
return new RegExp(`^\\s*(?:import\\s+${e}(?:[.\\s]|$)|from\\s+${e}(?:[.\\s]))`, "gm");
|
|
2924
|
+
}
|
|
2925
|
+
function analyzeTree(root, packages) {
|
|
2926
|
+
const result = /* @__PURE__ */ new Map();
|
|
2927
|
+
const tracked = [];
|
|
2928
|
+
for (const p of packages) {
|
|
2929
|
+
if (p.ecosystem === "npm" || p.ecosystem === "pypi") {
|
|
2930
|
+
tracked.push({
|
|
2931
|
+
name: p.name,
|
|
2932
|
+
ecosystem: p.ecosystem,
|
|
2933
|
+
pattern: importPattern(p.ecosystem, p.name),
|
|
2934
|
+
importSites: 0,
|
|
2935
|
+
filesTouched: 0,
|
|
2936
|
+
contracts: /* @__PURE__ */ new Set()
|
|
2937
|
+
});
|
|
2938
|
+
} else {
|
|
2939
|
+
result.set(p.name, { importSites: 0, filesTouched: 0, contracts: [] });
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (tracked.length === 0) return result;
|
|
2943
|
+
let filesSeen = 0;
|
|
2944
|
+
const walk2 = (dir, depth) => {
|
|
2945
|
+
if (depth > MAX_DEPTH || filesSeen >= MAX_FILES) return;
|
|
2946
|
+
let entries;
|
|
2947
|
+
try {
|
|
2948
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2949
|
+
} catch {
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2953
|
+
for (const entry of entries) {
|
|
2954
|
+
if (filesSeen >= MAX_FILES) return;
|
|
2955
|
+
if (entry.isDirectory()) {
|
|
2956
|
+
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
2957
|
+
walk2(path.join(dir, entry.name), depth + 1);
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
const fileEco = EXT_ECOSYSTEM[path.extname(entry.name)];
|
|
2961
|
+
if (!fileEco || fileEco === "unknown") continue;
|
|
2962
|
+
filesSeen++;
|
|
2963
|
+
const full = path.join(dir, entry.name);
|
|
2964
|
+
let content;
|
|
2965
|
+
try {
|
|
2966
|
+
if (fs5.statSync(full).size > MAX_FILE_BYTES) continue;
|
|
2967
|
+
content = fs5.readFileSync(full, "utf8");
|
|
2968
|
+
} catch {
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
for (const t of tracked) {
|
|
2972
|
+
if (t.ecosystem !== fileEco) continue;
|
|
2973
|
+
t.pattern.lastIndex = 0;
|
|
2974
|
+
const matches = content.match(t.pattern);
|
|
2975
|
+
if (matches && matches.length) {
|
|
2976
|
+
t.importSites += matches.length;
|
|
2977
|
+
t.filesTouched++;
|
|
2978
|
+
if (t.contracts.size < MAX_CONTRACTS) {
|
|
2979
|
+
for (const sym of extractContracts(content, t.name, t.ecosystem)) t.contracts.add(sym);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
};
|
|
2985
|
+
walk2(root, 0);
|
|
2986
|
+
for (const t of tracked) {
|
|
2987
|
+
result.set(t.name, {
|
|
2988
|
+
importSites: t.importSites,
|
|
2989
|
+
filesTouched: t.filesTouched,
|
|
2990
|
+
contracts: [...t.contracts].sort().slice(0, MAX_CONTRACTS)
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
return result;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
// src/reporting/utils/fix-plan.ts
|
|
2997
|
+
function postOnce(input, host) {
|
|
2998
|
+
return fetch(`${input.scheme}://${host}/v1/fix/plan`, {
|
|
2999
|
+
method: "POST",
|
|
3000
|
+
headers: {
|
|
3001
|
+
"Content-Type": "application/json",
|
|
3002
|
+
"X-Vibgrate-Timestamp": input.timestamp,
|
|
3003
|
+
Authorization: `VibgrateDSN ${input.keyId}:${input.secret}`,
|
|
3004
|
+
Connection: "close"
|
|
3005
|
+
},
|
|
3006
|
+
body: JSON.stringify(input.request)
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
async function regionRedirectHost(response) {
|
|
3010
|
+
try {
|
|
3011
|
+
const payload = await response.clone().json();
|
|
3012
|
+
if (payload?.code !== "REGION_MISMATCH" || !payload.region) return void 0;
|
|
3013
|
+
return ingestHostForRegionId(payload.region);
|
|
3014
|
+
} catch {
|
|
3015
|
+
return void 0;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
async function requestFixPlan(input) {
|
|
3019
|
+
let host = input.host;
|
|
3020
|
+
let response = await postOnce(input, host);
|
|
3021
|
+
if (response.status === 409) {
|
|
3022
|
+
const target = await regionRedirectHost(response);
|
|
3023
|
+
if (target && target !== host) {
|
|
3024
|
+
host = target;
|
|
3025
|
+
response = await postOnce(input, host);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
return { response, host };
|
|
3029
|
+
}
|
|
3030
|
+
async function parseFixPlanResponse(response) {
|
|
3031
|
+
return await response.json();
|
|
3032
|
+
}
|
|
3033
|
+
var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "unknown"];
|
|
3034
|
+
function deltaSummary(delta) {
|
|
3035
|
+
if (delta.total === 0) return "none";
|
|
3036
|
+
const parts = SEVERITY_ORDER.filter((s) => delta.bySeverity[s]).map((s) => `${delta.bySeverity[s]} ${s}`);
|
|
3037
|
+
return parts.length ? parts.join(", ") : `${delta.total}`;
|
|
3038
|
+
}
|
|
3039
|
+
function riskLabel(score) {
|
|
3040
|
+
if (score <= 15) return chalk7.green(`${score}/100 low`);
|
|
3041
|
+
if (score <= 40) return chalk7.yellow(`${score}/100 moderate`);
|
|
3042
|
+
return chalk7.red(`${score}/100 high`);
|
|
3043
|
+
}
|
|
3044
|
+
function planBlock(plan, recommended) {
|
|
3045
|
+
const lines = [];
|
|
3046
|
+
const marker = recommended ? chalk7.green("\u25CF recommended") : chalk7.dim("\u25CB");
|
|
3047
|
+
lines.push(`${marker} ${chalk7.bold(plan.label)} \u2014 ${chalk7.dim(plan.description)}`);
|
|
3048
|
+
const drift = typeof plan.expectedDriftScore === "number" && typeof plan.driftDelta === "number" ? ` \xB7 DriftScore \u2192${plan.expectedDriftScore} (${plan.driftDelta <= 0 ? "" : "+"}${plan.driftDelta})` : "";
|
|
3049
|
+
lines.push(
|
|
3050
|
+
` risk ${riskLabel(plan.riskScore)} \xB7 ${plan.upgrades.length} upgrade(s) \xB7 fixes ${deltaSummary(plan.fixes)}${drift}`
|
|
3051
|
+
);
|
|
3052
|
+
if (plan.introduces.total) {
|
|
3053
|
+
lines.push(chalk7.red(` \u26A0 introduces ${deltaSummary(plan.introduces)} advisory(ies) in target versions`));
|
|
3054
|
+
}
|
|
3055
|
+
const shown = plan.upgrades.slice(0, 12);
|
|
3056
|
+
for (const u of shown) {
|
|
3057
|
+
const codemod = u.playbook?.codemod ? chalk7.magenta(` [codemod: ${u.playbook.codemod}]`) : "";
|
|
3058
|
+
lines.push(` ${chalk7.cyan(u.package)} ${chalk7.dim(`${u.from ?? "?"} \u2192 ${u.to ?? "?"}`)} ${chalk7.dim(u.reason)}${codemod}`);
|
|
3059
|
+
}
|
|
3060
|
+
if (plan.upgrades.length > shown.length) {
|
|
3061
|
+
lines.push(chalk7.dim(` \u2026 and ${plan.upgrades.length - shown.length} more`));
|
|
3062
|
+
}
|
|
3063
|
+
if (plan.upgrades.length === 0) {
|
|
3064
|
+
lines.push(chalk7.dim(" (no upgrades in this plan)"));
|
|
3065
|
+
}
|
|
3066
|
+
return lines.join("\n");
|
|
3067
|
+
}
|
|
3068
|
+
function dataNote(report) {
|
|
3069
|
+
if (report.vulnerabilityData === "unavailable") {
|
|
3070
|
+
return chalk7.dim("Advisory data was unavailable \u2014 vulnerability impact is not shown.");
|
|
3071
|
+
}
|
|
3072
|
+
if (report.vulnerabilityData === "partial") {
|
|
3073
|
+
return chalk7.dim("Advisory data was partial \u2014 some ecosystems were not checked.");
|
|
3074
|
+
}
|
|
3075
|
+
return null;
|
|
3076
|
+
}
|
|
3077
|
+
function renderText(report) {
|
|
3078
|
+
const out2 = [];
|
|
3079
|
+
out2.push(chalk7.bold("Vibgrate fix \u2014 upgrade plan"));
|
|
3080
|
+
out2.push(
|
|
3081
|
+
chalk7.dim(
|
|
3082
|
+
`${report.totalCandidates} drifted dependency(ies) analysed \xB7 ${report.deepAnalysis ? "deep (major) analysis on" : "preflight only"}`
|
|
3083
|
+
)
|
|
3084
|
+
);
|
|
3085
|
+
const note = dataNote(report);
|
|
3086
|
+
if (note) out2.push(note);
|
|
3087
|
+
if (report.exploitability && report.exploitability.kevPackages > 0) {
|
|
3088
|
+
const epss = report.exploitability.maxEpss != null ? `, peak EPSS ${(report.exploitability.maxEpss * 100).toFixed(0)}%` : "";
|
|
3089
|
+
out2.push(chalk7.red(`\u26A0 ${report.exploitability.kevPackages} package(s) carry a KNOWN-EXPLOITED (KEV) advisory${epss} \u2014 prioritise these.`));
|
|
3090
|
+
}
|
|
3091
|
+
out2.push("");
|
|
3092
|
+
for (const plan of report.plans) {
|
|
3093
|
+
out2.push(planBlock(plan, plan.tier === report.recommended));
|
|
3094
|
+
out2.push("");
|
|
3095
|
+
}
|
|
3096
|
+
out2.push(chalk7.bold("Recommendation"));
|
|
3097
|
+
out2.push(` ${chalk7.green(report.plans.find((p) => p.tier === report.recommended)?.label ?? report.recommended)} \u2014 ${report.rationale}`);
|
|
3098
|
+
if (report.unresolved.total) {
|
|
3099
|
+
out2.push("");
|
|
3100
|
+
out2.push(chalk7.red(`\u26A0 ${deltaSummary(report.unresolved)} advisory(ies) have no upgrade path in any plan.`));
|
|
3101
|
+
}
|
|
3102
|
+
return out2.join("\n");
|
|
3103
|
+
}
|
|
3104
|
+
function renderMarkdown(report) {
|
|
3105
|
+
const out2 = [];
|
|
3106
|
+
out2.push("# Vibgrate fix \u2014 upgrade plan");
|
|
3107
|
+
out2.push("");
|
|
3108
|
+
out2.push(
|
|
3109
|
+
`${report.totalCandidates} drifted dependency(ies) analysed \xB7 ${report.deepAnalysis ? "deep (major) analysis on" : "preflight only"}.`
|
|
3110
|
+
);
|
|
3111
|
+
const note = dataNote(report);
|
|
3112
|
+
if (note) {
|
|
3113
|
+
out2.push("");
|
|
3114
|
+
out2.push(`_${report.vulnerabilityData === "unavailable" ? "Advisory data was unavailable \u2014 vulnerability impact is not shown." : "Advisory data was partial \u2014 some ecosystems were not checked."}_`);
|
|
3115
|
+
}
|
|
3116
|
+
out2.push("");
|
|
3117
|
+
for (const plan of report.plans) {
|
|
3118
|
+
const rec = plan.tier === report.recommended ? " \u2705 **recommended**" : "";
|
|
3119
|
+
out2.push(`## ${plan.label}${rec}`);
|
|
3120
|
+
out2.push("");
|
|
3121
|
+
out2.push(`_${plan.description}_`);
|
|
3122
|
+
out2.push("");
|
|
3123
|
+
out2.push(`- Risk: **${plan.riskScore}/100** (${plan.confidence} confidence)`);
|
|
3124
|
+
out2.push(`- Upgrades: **${plan.upgrades.length}**`);
|
|
3125
|
+
out2.push(`- Fixes advisories: ${deltaSummary(plan.fixes)}`);
|
|
3126
|
+
if (plan.introduces.total) {
|
|
3127
|
+
out2.push(`- \u26A0 Introduces advisories in target versions: ${deltaSummary(plan.introduces)}`);
|
|
3128
|
+
}
|
|
3129
|
+
out2.push("");
|
|
3130
|
+
if (plan.upgrades.length) {
|
|
3131
|
+
out2.push("| Package | From | To | Kind | Reason |");
|
|
3132
|
+
out2.push("|---|---|---|---|---|");
|
|
3133
|
+
for (const u of plan.upgrades) {
|
|
3134
|
+
out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
|
|
3135
|
+
}
|
|
3136
|
+
out2.push("");
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
out2.push("## Recommendation");
|
|
3140
|
+
out2.push("");
|
|
3141
|
+
const recLabel = report.plans.find((p) => p.tier === report.recommended)?.label ?? report.recommended;
|
|
3142
|
+
out2.push(`**${recLabel}** \u2014 ${report.rationale}`);
|
|
3143
|
+
if (report.unresolved.total) {
|
|
3144
|
+
out2.push("");
|
|
3145
|
+
out2.push(`> \u26A0 ${deltaSummary(report.unresolved)} advisory(ies) have no upgrade path in any plan.`);
|
|
3146
|
+
}
|
|
3147
|
+
return out2.join("\n");
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// src/reporting/planning/expected-drift.ts
|
|
3151
|
+
function estimateDriftScore(artifact, upgraded) {
|
|
3152
|
+
const projects = JSON.parse(JSON.stringify(artifact.projects ?? []));
|
|
3153
|
+
for (const p of projects) {
|
|
3154
|
+
const buckets = p.dependencyAgeBuckets;
|
|
3155
|
+
for (const dep of p.dependencies ?? []) {
|
|
3156
|
+
if (!upgraded.has(dep.package)) continue;
|
|
3157
|
+
const mb3 = dep.majorsBehind ?? 0;
|
|
3158
|
+
if (mb3 >= 2 && buckets.twoPlusBehind > 0) {
|
|
3159
|
+
buckets.twoPlusBehind--;
|
|
3160
|
+
buckets.current++;
|
|
3161
|
+
} else if (mb3 === 1 && buckets.oneBehind > 0) {
|
|
3162
|
+
buckets.oneBehind--;
|
|
3163
|
+
buckets.current++;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
for (const fw of p.frameworks ?? []) {
|
|
3167
|
+
if (upgraded.has(fw.name)) fw.majorsBehind = 0;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
return computeDriftScore(projects).score;
|
|
3171
|
+
}
|
|
3172
|
+
function pmCommandFor(ecosystem, pkg, version, pm = "npm") {
|
|
3173
|
+
switch (ecosystem) {
|
|
3174
|
+
case "npm":
|
|
3175
|
+
if (pm === "pnpm") return { cmd: "pnpm", args: ["add", `${pkg}@${version}`] };
|
|
3176
|
+
if (pm === "yarn") return { cmd: "yarn", args: ["add", `${pkg}@${version}`] };
|
|
3177
|
+
if (pm === "bun") return { cmd: "bun", args: ["add", `${pkg}@${version}`] };
|
|
3178
|
+
return { cmd: "npm", args: ["install", `${pkg}@${version}`] };
|
|
3179
|
+
case "pypi":
|
|
3180
|
+
return { cmd: "pip", args: ["install", `${pkg}==${version}`] };
|
|
3181
|
+
case "cargo":
|
|
3182
|
+
return { cmd: "cargo", args: ["add", `${pkg}@${version}`] };
|
|
3183
|
+
case "go":
|
|
3184
|
+
return { cmd: "go", args: ["get", `${pkg}@${version.startsWith("v") ? version : `v${version}`}`] };
|
|
3185
|
+
case "composer":
|
|
3186
|
+
return { cmd: "composer", args: ["require", `${pkg}:${version}`] };
|
|
3187
|
+
case "nuget":
|
|
3188
|
+
return { cmd: "dotnet", args: ["add", "package", pkg, "--version", version] };
|
|
3189
|
+
case "dotnet":
|
|
3190
|
+
return { cmd: "dotnet", args: ["add", "package", pkg, "--version", version] };
|
|
3191
|
+
case "dart":
|
|
3192
|
+
case "pub":
|
|
3193
|
+
return { cmd: "dart", args: ["pub", "add", `${pkg}:${version}`] };
|
|
3194
|
+
case "rubygems":
|
|
3195
|
+
case "ruby":
|
|
3196
|
+
return { cmd: "bundle", args: ["update", pkg] };
|
|
3197
|
+
default:
|
|
3198
|
+
return null;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
function defaultRun(cmd, cwd) {
|
|
3202
|
+
const res = spawnSync(cmd.cmd, cmd.args, { cwd, stdio: "inherit", shell: false });
|
|
3203
|
+
if (res.error) return { ok: false, detail: res.error.message };
|
|
3204
|
+
if (res.status !== 0) return { ok: false, detail: `exit ${res.status ?? "signal"}` };
|
|
3205
|
+
return { ok: true };
|
|
3206
|
+
}
|
|
3207
|
+
function applyPlan(rootDir, upgrades, opts = {}) {
|
|
3208
|
+
const run = opts.run ?? defaultRun;
|
|
3209
|
+
const results = [];
|
|
3210
|
+
for (const u of upgrades) {
|
|
3211
|
+
if (!u.to) {
|
|
3212
|
+
results.push({ package: u.package, to: u.to, status: "skipped", detail: "no target version" });
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
const command = pmCommandFor(u.ecosystem, u.package, u.to, opts.packageManager);
|
|
3216
|
+
if (!command) {
|
|
3217
|
+
results.push({ package: u.package, to: u.to, status: "manual", detail: `${u.ecosystem}: update the manifest to ${u.to} manually` });
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
if (opts.dryRun) {
|
|
3221
|
+
results.push({ package: u.package, to: u.to, status: "skipped", detail: `would run: ${command.cmd} ${command.args.join(" ")}` });
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
const outcome = run(command, rootDir);
|
|
3225
|
+
results.push({
|
|
3226
|
+
package: u.package,
|
|
3227
|
+
to: u.to,
|
|
3228
|
+
status: outcome.ok ? "applied" : "failed",
|
|
3229
|
+
detail: outcome.detail
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
return results;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
// src/reporting/commands/fix.ts
|
|
3236
|
+
var SEVERITY_RANK = { unknown: 0, low: 1, moderate: 2, high: 3, critical: 4 };
|
|
3237
|
+
function ecosystemId(type) {
|
|
3238
|
+
return projectTypeToVulnEcosystem(type) ?? type;
|
|
3239
|
+
}
|
|
3240
|
+
function sourceEcosystem(ecosystem) {
|
|
3241
|
+
if (ecosystem === "npm") return "npm";
|
|
3242
|
+
if (ecosystem === "pypi") return "pypi";
|
|
3243
|
+
return "unknown";
|
|
3244
|
+
}
|
|
3245
|
+
function collectCandidates(artifact) {
|
|
3246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3247
|
+
const out2 = [];
|
|
3248
|
+
for (const project of artifact.projects ?? []) {
|
|
3249
|
+
const ecosystem = ecosystemId(project.type);
|
|
3250
|
+
for (const dep of project.dependencies ?? []) {
|
|
3251
|
+
const key = `${ecosystem}\0${dep.package}`;
|
|
3252
|
+
if (seen.has(key)) continue;
|
|
3253
|
+
if (!dep.latestStable || dep.latestStable === dep.resolvedVersion) continue;
|
|
3254
|
+
if (dep.drift === "current" || dep.drift === "unknown") continue;
|
|
3255
|
+
seen.add(key);
|
|
3256
|
+
out2.push({
|
|
3257
|
+
package: dep.package,
|
|
3258
|
+
ecosystem,
|
|
3259
|
+
source: sourceEcosystem(ecosystem),
|
|
3260
|
+
from: dep.resolvedVersion,
|
|
3261
|
+
to: dep.latestStable,
|
|
3262
|
+
majorsBehind: dep.majorsBehind,
|
|
3263
|
+
section: dep.section
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
out2.sort((a, b) => a.ecosystem.localeCompare(b.ecosystem) || a.package.localeCompare(b.package));
|
|
3268
|
+
return out2;
|
|
3269
|
+
}
|
|
3270
|
+
var defaultFixScanner = async (rootDir) => {
|
|
3271
|
+
const scratch = path.join(os.tmpdir(), `vg-fix-scan-${process.pid}.json`);
|
|
3272
|
+
const scanOpts = { vibgrateVersion: VERSION, format: "json", out: scratch, quiet: true, concurrency: 8 };
|
|
3273
|
+
const advanced = await loadAdvancedScanHook();
|
|
3274
|
+
return runCoreScan(rootDir, scanOpts, advanced);
|
|
3275
|
+
};
|
|
3276
|
+
async function loadArtifact(rootDir, inFile, scan = defaultFixScanner) {
|
|
3277
|
+
const artifactPath = path.isAbsolute(inFile) ? inFile : path.join(rootDir, inFile);
|
|
3278
|
+
if (await pathExists(artifactPath)) {
|
|
3279
|
+
const staleness = scanStaleness(rootDir, artifactPath);
|
|
3280
|
+
if (!staleness.stale) {
|
|
3281
|
+
return readJsonFile(artifactPath);
|
|
3282
|
+
}
|
|
3283
|
+
console.error(
|
|
3284
|
+
chalk7.dim(
|
|
3285
|
+
`Scan is out of date (${staleness.newestChanged ?? "a dependency manifest"} changed since the last scan) \u2014 re-running a drift scan first (code map skipped)\u2026`
|
|
3286
|
+
)
|
|
3287
|
+
);
|
|
3288
|
+
} else {
|
|
3289
|
+
console.error(chalk7.dim("No scan found \u2014 running a drift scan first (code map skipped)\u2026"));
|
|
3290
|
+
}
|
|
3291
|
+
return scan(rootDir);
|
|
3292
|
+
}
|
|
3293
|
+
async function repositoryIdentity(rootDir, override) {
|
|
3294
|
+
try {
|
|
3295
|
+
const vcs = await detectVcs(rootDir);
|
|
3296
|
+
const fingerprint = await computeRepoFingerprint(rootDir, vcs);
|
|
3297
|
+
const name = override?.trim() || await resolveRepositoryName(rootDir);
|
|
3298
|
+
return { name, vcsSha: fingerprint.vcsSha };
|
|
3299
|
+
} catch {
|
|
3300
|
+
return { name: override?.trim() || void 0 };
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
var fixCommand = new Command("fix").description("Analyse drift and get ranked, risk-tiered upgrade plans from the hosted planner (requires login/DSN; read-only \u2014 never edits your project)").argument("[path]", "Path to analyse", ".").option("--in <file>", "Scan artifact to read", ".vibgrate/scan_result.json").option("--format <format>", "Output format (text|json|md)", "text").option("--dsn <dsn>", 'DSN token (or use VIBGRATE_DSN env / "vg login")').option("--region <region>", "Override data residency region (us, eu)").option("--repository-name <name>", "Override the repository name recorded for this plan").option("--plan <tier>", "Apply a specific plan non-interactively (safe|balanced|aggressive)").option("--yes", "Apply the recommended plan without prompting").option("--dry-run", "Show what would change without applying").option("--no-apply", "Only print the plans; never modify the project").option("--fail-on-vulns <severity>", "Exit non-zero if the recommended plan leaves an advisory at/above this severity unresolved (low|moderate|high|critical)").action(async (targetPath, opts) => {
|
|
3304
|
+
const rootDir = path.resolve(targetPath);
|
|
3305
|
+
if (!await pathExists(rootDir)) {
|
|
3306
|
+
console.error(chalk7.red(`Path does not exist: ${rootDir}`));
|
|
3307
|
+
process.exit(1);
|
|
3308
|
+
}
|
|
3309
|
+
const failOn = opts.failOnVulns;
|
|
3310
|
+
if (failOn && !(failOn in SEVERITY_RANK)) {
|
|
3311
|
+
console.error(chalk7.red(`Invalid --fail-on-vulns value '${opts.failOnVulns}'. Use one of: low, moderate, high, critical.`));
|
|
3312
|
+
process.exit(1);
|
|
3313
|
+
}
|
|
3314
|
+
const dsn = resolveDsn(opts.dsn);
|
|
3315
|
+
if (!dsn) {
|
|
3316
|
+
const cli = resolveCliInvocation();
|
|
3317
|
+
console.error(chalk7.red("vg fix needs a Vibgrate login."));
|
|
3318
|
+
console.error(
|
|
3319
|
+
chalk7.dim(
|
|
3320
|
+
`Run "${cli} login" (or set VIBGRATE_DSN / pass --dsn) to analyse upgrades with the hosted planner. See https://vibgrate.com/cli for details.`
|
|
3321
|
+
)
|
|
3322
|
+
);
|
|
3323
|
+
process.exit(1);
|
|
3324
|
+
}
|
|
3325
|
+
const parsed = parseDsn(dsn);
|
|
3326
|
+
if (!parsed) {
|
|
3327
|
+
console.error(chalk7.red('Invalid DSN format. Re-run "vg login" or check VIBGRATE_DSN.'));
|
|
3328
|
+
process.exit(1);
|
|
3329
|
+
}
|
|
3330
|
+
let host;
|
|
3331
|
+
try {
|
|
3332
|
+
host = opts.region ? resolveIngestHost(opts.region) : parsed.host;
|
|
3333
|
+
} catch (e) {
|
|
3334
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
3335
|
+
process.exit(1);
|
|
3336
|
+
}
|
|
3337
|
+
let artifact;
|
|
3338
|
+
try {
|
|
3339
|
+
artifact = await loadArtifact(rootDir, opts.in);
|
|
3340
|
+
} catch (e) {
|
|
3341
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
3342
|
+
process.exit(1);
|
|
3343
|
+
}
|
|
3344
|
+
const raw = collectCandidates(artifact);
|
|
3345
|
+
const usage = analyzeTree(
|
|
3346
|
+
rootDir,
|
|
3347
|
+
raw.filter((c2) => c2.source !== "unknown").map((c2) => ({ name: c2.package, ecosystem: c2.source }))
|
|
3348
|
+
);
|
|
3349
|
+
const candidates = raw.map((c2) => {
|
|
3350
|
+
const u = usage.get(c2.package);
|
|
3351
|
+
return {
|
|
3352
|
+
package: c2.package,
|
|
3353
|
+
ecosystem: c2.ecosystem,
|
|
3354
|
+
currentVersion: c2.from,
|
|
3355
|
+
latestVersion: c2.to,
|
|
3356
|
+
majorsBehind: c2.majorsBehind,
|
|
3357
|
+
section: c2.section,
|
|
3358
|
+
...u && (u.importSites > 0 || u.filesTouched > 0) ? { usage: { importSites: u.importSites, filesTouched: u.filesTouched } } : {},
|
|
3359
|
+
...u && u.contracts.length ? { contracts: u.contracts } : {}
|
|
3360
|
+
};
|
|
3361
|
+
});
|
|
3362
|
+
const request = {
|
|
3363
|
+
cliVersion: VERSION,
|
|
3364
|
+
repository: await repositoryIdentity(rootDir, opts.repositoryName),
|
|
3365
|
+
candidates
|
|
3366
|
+
};
|
|
3367
|
+
let response;
|
|
3368
|
+
try {
|
|
3369
|
+
const { response: httpResponse } = await requestFixPlan({
|
|
3370
|
+
scheme: parsed.scheme,
|
|
3371
|
+
host,
|
|
3372
|
+
keyId: parsed.keyId,
|
|
3373
|
+
secret: parsed.secret,
|
|
3374
|
+
request,
|
|
3375
|
+
timestamp: String(Date.now())
|
|
3376
|
+
});
|
|
3377
|
+
if (!httpResponse.ok) {
|
|
3378
|
+
handleHttpError(httpResponse, parsed.workspaceId, host);
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
response = await parseFixPlanResponse(httpResponse);
|
|
3382
|
+
} catch (e) {
|
|
3383
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3384
|
+
console.error(chalk7.red(`Could not reach the upgrade planner: ${msg}`));
|
|
3385
|
+
console.error(chalk7.dim("Check your connection and try again, or see https://vibgrate.com/help."));
|
|
3386
|
+
process.exit(1);
|
|
3387
|
+
}
|
|
3388
|
+
if (response.status === "error") {
|
|
3389
|
+
console.error(chalk7.red(response.error ?? "The planner returned an error."));
|
|
3390
|
+
if (response.requestId) console.error(chalk7.dim(` (ref ${response.requestId})`));
|
|
3391
|
+
process.exit(1);
|
|
3392
|
+
}
|
|
3393
|
+
const currentDrift = artifact.drift?.score;
|
|
3394
|
+
if (typeof currentDrift === "number") {
|
|
3395
|
+
response.currentDriftScore = currentDrift;
|
|
3396
|
+
for (const plan of response.plans) {
|
|
3397
|
+
const upgraded = new Set(plan.upgrades.map((u) => u.package));
|
|
3398
|
+
const expected = estimateDriftScore(artifact, upgraded);
|
|
3399
|
+
plan.expectedDriftScore = expected;
|
|
3400
|
+
plan.driftDelta = expected - currentDrift;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
emit(response, opts.format);
|
|
3404
|
+
if (opts.format === "text" && opts.apply !== false) {
|
|
3405
|
+
await runApplyFlow(rootDir, artifact, response, opts);
|
|
3406
|
+
}
|
|
3407
|
+
if (failOn) {
|
|
3408
|
+
const recommended = response.plans.find((p) => p.tier === response.recommended);
|
|
3409
|
+
const threshold = SEVERITY_RANK[failOn];
|
|
3410
|
+
let stillOpen = 0;
|
|
3411
|
+
for (const sev of Object.keys(SEVERITY_RANK)) {
|
|
3412
|
+
if (SEVERITY_RANK[sev] < threshold) continue;
|
|
3413
|
+
const open = response.unresolved.bySeverity[sev] ?? 0;
|
|
3414
|
+
const notFixedByRecommended = (response.plans.find((p) => p.tier === "aggressive")?.fixes.bySeverity[sev] ?? 0) - (recommended?.fixes.bySeverity[sev] ?? 0);
|
|
3415
|
+
stillOpen += open + Math.max(0, notFixedByRecommended);
|
|
3416
|
+
}
|
|
3417
|
+
if (stillOpen > 0) {
|
|
3418
|
+
console.error(
|
|
3419
|
+
chalk7.red(`
|
|
3420
|
+
Failing: the recommended plan leaves ${stillOpen} advisory(ies) at/above ${failOn} unresolved.`)
|
|
3421
|
+
);
|
|
3422
|
+
process.exit(2);
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
function handleHttpError(response, workspaceId, host) {
|
|
3427
|
+
const upgradeUrl = `https://${dashHostForIngestHost(host)}/${workspaceId}`;
|
|
3428
|
+
if (response.status === 401 || response.status === 403) {
|
|
3429
|
+
console.error(chalk7.red("Not authorised. Your DSN may be invalid or lack access to this workspace."));
|
|
3430
|
+
console.error(chalk7.dim('Re-run "vg login" or check VIBGRATE_DSN.'));
|
|
3431
|
+
process.exit(1);
|
|
3432
|
+
}
|
|
3433
|
+
if (response.status === 402) {
|
|
3434
|
+
console.error(chalk7.red("vg fix is a paid capability that is not enabled on your current plan."));
|
|
3435
|
+
console.error(chalk7.dim(`Upgrade to enable the hosted upgrade planner: ${upgradeUrl}`));
|
|
3436
|
+
process.exit(1);
|
|
3437
|
+
}
|
|
3438
|
+
if (response.status === 429) {
|
|
3439
|
+
console.error(chalk7.red("Rate limited by the planner. Wait a moment and try again."));
|
|
3440
|
+
process.exit(1);
|
|
3441
|
+
}
|
|
3442
|
+
console.error(chalk7.red(`The planner returned HTTP ${response.status}.`));
|
|
3443
|
+
console.error(chalk7.dim("Try again shortly, or see https://vibgrate.com/help."));
|
|
3444
|
+
process.exit(1);
|
|
3445
|
+
}
|
|
3446
|
+
function emit(report, format) {
|
|
3447
|
+
switch (format) {
|
|
3448
|
+
case "json":
|
|
3449
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3450
|
+
break;
|
|
3451
|
+
case "md":
|
|
3452
|
+
console.log(renderMarkdown(report));
|
|
3453
|
+
break;
|
|
3454
|
+
case "text":
|
|
3455
|
+
default:
|
|
3456
|
+
console.log(renderText(report));
|
|
3457
|
+
break;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function npmPackageManager(artifact) {
|
|
3461
|
+
for (const p of artifact.projects ?? []) {
|
|
3462
|
+
if ((p.type === "node" || p.type === "typescript") && p.packageManager) {
|
|
3463
|
+
const pm = p.packageManager;
|
|
3464
|
+
if (pm === "pnpm" || pm === "yarn" || pm === "bun" || pm === "npm") return pm;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
return "npm";
|
|
3468
|
+
}
|
|
3469
|
+
function promptPlanSelection(response) {
|
|
3470
|
+
const plans = response.plans.filter((p) => p.upgrades.length > 0);
|
|
3471
|
+
console.log(chalk7.bold("\nSelect a plan to apply:"));
|
|
3472
|
+
plans.forEach((p, i) => {
|
|
3473
|
+
const rec = p.tier === response.recommended ? chalk7.green(" (recommended)") : "";
|
|
3474
|
+
const drift = typeof p.expectedDriftScore === "number" && typeof response.currentDriftScore === "number" ? ` \xB7 DriftScore ${response.currentDriftScore}\u2192${p.expectedDriftScore}` : "";
|
|
3475
|
+
console.log(` ${i + 1}) ${chalk7.bold(p.label)}${rec} risk ${p.riskScore}/100 \xB7 ${p.upgrades.length} upgrade(s)${drift}`);
|
|
3476
|
+
});
|
|
3477
|
+
const defaultIdx = plans.findIndex((p) => p.tier === response.recommended);
|
|
3478
|
+
const def = defaultIdx >= 0 ? defaultIdx + 1 : 1;
|
|
3479
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3480
|
+
return new Promise((resolve16) => {
|
|
3481
|
+
rl.question(`Plan [1-${plans.length}, default ${def}, q to cancel]: `, (ans) => {
|
|
3482
|
+
rl.close();
|
|
3483
|
+
const t = ans.trim().toLowerCase();
|
|
3484
|
+
if (t === "q" || t === "quit") return resolve16(null);
|
|
3485
|
+
const n = t === "" ? def : Number.parseInt(t, 10);
|
|
3486
|
+
if (!Number.isInteger(n) || n < 1 || n > plans.length) return resolve16(null);
|
|
3487
|
+
resolve16(plans[n - 1].tier);
|
|
3488
|
+
});
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
async function runApplyFlow(rootDir, artifact, response, opts) {
|
|
3492
|
+
const nonEmpty = response.plans.filter((p) => p.upgrades.length > 0);
|
|
3493
|
+
if (nonEmpty.length === 0) {
|
|
3494
|
+
console.log(chalk7.green("\n\u2714 Nothing to upgrade \u2014 every tracked dependency is current."));
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
let chosen;
|
|
3498
|
+
if (opts.plan) {
|
|
3499
|
+
chosen = response.plans.find((p) => p.tier === opts.plan);
|
|
3500
|
+
if (!chosen) {
|
|
3501
|
+
console.error(chalk7.red(`Unknown plan '${opts.plan}'. Use safe, balanced, or aggressive.`));
|
|
3502
|
+
process.exit(1);
|
|
3503
|
+
}
|
|
3504
|
+
} else if (nonEmpty.length === 1) {
|
|
3505
|
+
chosen = nonEmpty[0];
|
|
3506
|
+
} else if (opts.yes) {
|
|
3507
|
+
chosen = response.plans.find((p) => p.tier === response.recommended) ?? nonEmpty[0];
|
|
3508
|
+
} else if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3509
|
+
const tier = await promptPlanSelection(response);
|
|
3510
|
+
if (!tier) {
|
|
3511
|
+
console.log(chalk7.dim("No plan applied."));
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
chosen = response.plans.find((p) => p.tier === tier);
|
|
3515
|
+
} else {
|
|
3516
|
+
console.log(chalk7.dim("\nMultiple plans available \u2014 re-run with --plan <tier> or --yes to apply, or --dry-run to preview."));
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
if (!chosen || chosen.upgrades.length === 0) {
|
|
3520
|
+
console.log(chalk7.dim("Selected plan has no upgrades."));
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
const pm = npmPackageManager(artifact);
|
|
3524
|
+
const results = applyPlan(rootDir, chosen.upgrades, { dryRun: opts.dryRun, packageManager: pm });
|
|
3525
|
+
if (opts.dryRun) {
|
|
3526
|
+
console.log(chalk7.bold(`
|
|
3527
|
+
Dry run \u2014 ${chosen.label} plan (${chosen.upgrades.length} upgrade(s)):`));
|
|
3528
|
+
for (const r of results) {
|
|
3529
|
+
if (r.status === "manual") console.log(chalk7.yellow(` \u26A0 ${r.package}: ${r.detail}`));
|
|
3530
|
+
else console.log(chalk7.dim(` ${r.package}: ${r.detail}`));
|
|
3531
|
+
}
|
|
3532
|
+
if (typeof chosen.expectedDriftScore === "number") {
|
|
3533
|
+
console.log(chalk7.dim(`Expected DriftScore after apply: ~${chosen.expectedDriftScore} (was ${response.currentDriftScore ?? "?"}).`));
|
|
3534
|
+
}
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
const applied = results.filter((r) => r.status === "applied");
|
|
3538
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
3539
|
+
const manual = results.filter((r) => r.status === "manual");
|
|
3540
|
+
console.log(chalk7.bold(`
|
|
3541
|
+
Applied the ${chosen.label} plan:`));
|
|
3542
|
+
console.log(
|
|
3543
|
+
chalk7.green(` \u2714 ${applied.length} upgraded`) + (failed.length ? chalk7.red(` \u2716 ${failed.length} failed`) : "") + (manual.length ? chalk7.yellow(` \u26A0 ${manual.length} need manual work`) : "")
|
|
3544
|
+
);
|
|
3545
|
+
for (const r of failed) console.log(chalk7.red(` \u2716 ${r.package} \u2192 ${r.to}: ${r.detail ?? "failed"}`));
|
|
3546
|
+
for (const r of manual) console.log(chalk7.yellow(` \u26A0 ${r.package} \u2192 ${r.to}: ${r.detail}`));
|
|
3547
|
+
if (typeof chosen.expectedDriftScore === "number" && typeof response.currentDriftScore === "number") {
|
|
3548
|
+
const delta = chosen.expectedDriftScore - response.currentDriftScore;
|
|
3549
|
+
console.log(
|
|
3550
|
+
` Expected DriftScore: ${response.currentDriftScore} \u2192 ~${chosen.expectedDriftScore} (${delta <= 0 ? "" : "+"}${delta}). Re-run \`vg\` to confirm the actual score.`
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
if (failed.length) process.exitCode = 2;
|
|
3554
|
+
}
|
|
2313
3555
|
function formatText(artifact) {
|
|
2314
3556
|
const lines = [];
|
|
2315
|
-
const teal2 =
|
|
2316
|
-
const mint2 =
|
|
3557
|
+
const teal2 = chalk7.hex("#3FB0A4");
|
|
3558
|
+
const mint2 = chalk7.hex("#4FE3C1");
|
|
2317
3559
|
lines.push("");
|
|
2318
3560
|
lines.push(" " + teal2("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u256E") + mint2("\u279C"));
|
|
2319
|
-
lines.push(" " +
|
|
2320
|
-
lines.push(" " +
|
|
3561
|
+
lines.push(" " + chalk7.dim("\u2524") + teal2("\u2502") + " " + mint2("\u25FC") + " " + mint2("\u25FC") + " " + teal2("\u2502") + chalk7.dim("\u251C") + " " + chalk7.bold.white("vibgrate"));
|
|
3562
|
+
lines.push(" " + chalk7.dim("\u2524") + teal2("\u2502") + " " + chalk7.dim("\u2581\u2581") + " " + teal2("\u2502") + chalk7.dim("\u251C") + " " + chalk7.dim(`Drift Intelligence Engine v${VERSION}`));
|
|
2321
3563
|
lines.push(" " + teal2("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
2322
3564
|
lines.push("");
|
|
2323
3565
|
lines.push(...titleBox("Vibgrate Drift Report", teal2));
|
|
2324
3566
|
lines.push("");
|
|
2325
3567
|
for (const project of artifact.projects) {
|
|
2326
|
-
lines.push(
|
|
3568
|
+
lines.push(chalk7.bold(` \u2500\u2500 ${project.name} `) + chalk7.dim(`(${project.type}) ${project.path}`));
|
|
2327
3569
|
if (project.runtime) {
|
|
2328
|
-
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ?
|
|
3570
|
+
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? chalk7.yellow(` (${project.runtimeMajorsBehind} major${project.runtimeMajorsBehind > 1 ? "s" : ""} behind)`) : chalk7.green(" (current)");
|
|
2329
3571
|
lines.push(` Runtime: ${project.runtime}${behindStr}`);
|
|
2330
3572
|
}
|
|
2331
3573
|
if (project.targetFramework) {
|
|
@@ -2334,7 +3576,7 @@ function formatText(artifact) {
|
|
|
2334
3576
|
if (project.frameworks.length > 0) {
|
|
2335
3577
|
lines.push(" Frameworks:");
|
|
2336
3578
|
for (const fw of project.frameworks) {
|
|
2337
|
-
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ?
|
|
3579
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? chalk7.green("current") : chalk7.yellow(`${fw.majorsBehind} behind`) : chalk7.dim("unknown");
|
|
2338
3580
|
lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
2339
3581
|
}
|
|
2340
3582
|
}
|
|
@@ -2342,13 +3584,13 @@ function formatText(artifact) {
|
|
|
2342
3584
|
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
2343
3585
|
if (total > 0) {
|
|
2344
3586
|
lines.push(" Dependencies:");
|
|
2345
|
-
lines.push(` ${
|
|
3587
|
+
lines.push(` ${chalk7.green(`${b.current} current`)} ${chalk7.yellow(`${b.oneBehind} 1-behind`)} ${chalk7.red(`${b.twoPlusBehind} 2+ behind`)} ${chalk7.dim(`${b.unknown} unknown`)}`);
|
|
2346
3588
|
}
|
|
2347
3589
|
lines.push("");
|
|
2348
3590
|
}
|
|
2349
3591
|
if (artifact.delta !== void 0) {
|
|
2350
|
-
const deltaStr = artifact.delta > 0 ?
|
|
2351
|
-
lines.push(
|
|
3592
|
+
const deltaStr = artifact.delta > 0 ? chalk7.red(`+${artifact.delta}`) : artifact.delta < 0 ? chalk7.green(`${artifact.delta}`) : chalk7.dim("0");
|
|
3593
|
+
lines.push(chalk7.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
|
|
2352
3594
|
lines.push("");
|
|
2353
3595
|
}
|
|
2354
3596
|
if (artifact.extended) {
|
|
@@ -2359,15 +3601,15 @@ function formatText(artifact) {
|
|
|
2359
3601
|
const warnings = artifact.findings.filter((f) => f.level === "warning");
|
|
2360
3602
|
const notes = artifact.findings.filter((f) => f.level === "note");
|
|
2361
3603
|
const summary = [
|
|
2362
|
-
errors.length > 0 ?
|
|
2363
|
-
warnings.length > 0 ?
|
|
2364
|
-
notes.length > 0 ?
|
|
2365
|
-
].filter(Boolean).join(
|
|
2366
|
-
lines.push(
|
|
3604
|
+
errors.length > 0 ? chalk7.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
|
|
3605
|
+
warnings.length > 0 ? chalk7.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
|
|
3606
|
+
notes.length > 0 ? chalk7.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
|
|
3607
|
+
].filter(Boolean).join(chalk7.dim(", "));
|
|
3608
|
+
lines.push(chalk7.bold.underline(` Findings`) + chalk7.dim(` (${summary})`));
|
|
2367
3609
|
for (const f of artifact.findings) {
|
|
2368
|
-
const icon = f.level === "error" ?
|
|
3610
|
+
const icon = f.level === "error" ? chalk7.red("\u2716") : f.level === "warning" ? chalk7.yellow("\u26A0") : chalk7.blue("\u2139");
|
|
2369
3611
|
lines.push(` ${icon} ${f.message}`);
|
|
2370
|
-
lines.push(
|
|
3612
|
+
lines.push(chalk7.dim(` ${f.ruleId} in ${f.location}`));
|
|
2371
3613
|
}
|
|
2372
3614
|
lines.push("");
|
|
2373
3615
|
}
|
|
@@ -2377,10 +3619,10 @@ function formatText(artifact) {
|
|
|
2377
3619
|
lines.push("");
|
|
2378
3620
|
for (let i = 0; i < actions.length; i++) {
|
|
2379
3621
|
const a = actions[i];
|
|
2380
|
-
const num =
|
|
2381
|
-
lines.push(`${num} ${
|
|
2382
|
-
lines.push(
|
|
2383
|
-
if (a.impact) lines.push(` Impact: ${
|
|
3622
|
+
const num = chalk7.bold.cyan(` ${i + 1}.`);
|
|
3623
|
+
lines.push(`${num} ${chalk7.bold(a.title)}`);
|
|
3624
|
+
lines.push(chalk7.dim(` ${a.explanation}`));
|
|
3625
|
+
if (a.impact) lines.push(` Impact: ${chalk7.green(a.impact)}`);
|
|
2384
3626
|
lines.push("");
|
|
2385
3627
|
}
|
|
2386
3628
|
}
|
|
@@ -2392,30 +3634,30 @@ function formatText(artifact) {
|
|
|
2392
3634
|
lines.push("");
|
|
2393
3635
|
for (const solution of artifact.solutions) {
|
|
2394
3636
|
const solScore = solution.drift?.score;
|
|
2395
|
-
const color = typeof solScore === "number" ? solScore <= 30 ?
|
|
2396
|
-
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) :
|
|
3637
|
+
const color = typeof solScore === "number" ? solScore <= 30 ? chalk7.green : solScore <= 60 ? chalk7.yellow : chalk7.red : chalk7.dim;
|
|
3638
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk7.dim("n/a")}`);
|
|
2397
3639
|
}
|
|
2398
3640
|
lines.push("");
|
|
2399
3641
|
}
|
|
2400
|
-
const scoreColor = artifact.drift.score <= 30 ?
|
|
2401
|
-
lines.push(...titleBox("
|
|
3642
|
+
const scoreColor = artifact.drift.score <= 30 ? chalk7.green : artifact.drift.score <= 60 ? chalk7.yellow : chalk7.red;
|
|
3643
|
+
lines.push(...titleBox("DriftScore Summary"));
|
|
2402
3644
|
lines.push("");
|
|
2403
|
-
lines.push(
|
|
2404
|
-
lines.push(
|
|
2405
|
-
lines.push(
|
|
3645
|
+
lines.push(chalk7.bold(" DriftScore: ") + scoreColor.bold(`${artifact.drift.score}/100`));
|
|
3646
|
+
lines.push(chalk7.bold(" Risk Level: ") + riskBadge(artifact.drift.riskLevel));
|
|
3647
|
+
lines.push(chalk7.bold(" Projects: ") + `${artifact.projects.length}`);
|
|
2406
3648
|
if (artifact.vcs) {
|
|
2407
3649
|
const vcsParts = [artifact.vcs.type];
|
|
2408
3650
|
if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
|
|
2409
|
-
if (artifact.vcs.shortSha) vcsParts.push(
|
|
2410
|
-
lines.push(
|
|
3651
|
+
if (artifact.vcs.shortSha) vcsParts.push(chalk7.dim(artifact.vcs.shortSha));
|
|
3652
|
+
lines.push(chalk7.bold(" VCS: ") + vcsParts.join(" "));
|
|
2411
3653
|
}
|
|
2412
3654
|
lines.push("");
|
|
2413
3655
|
const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
|
|
2414
|
-
lines.push(" " +
|
|
2415
|
-
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) :
|
|
2416
|
-
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) :
|
|
2417
|
-
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) :
|
|
2418
|
-
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) :
|
|
3656
|
+
lines.push(" " + chalk7.bold.underline("Score Breakdown"));
|
|
3657
|
+
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk7.dim("n/a")}`);
|
|
3658
|
+
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk7.dim("n/a")}`);
|
|
3659
|
+
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk7.dim("n/a")}`);
|
|
3660
|
+
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk7.dim("n/a")}`);
|
|
2419
3661
|
lines.push("");
|
|
2420
3662
|
const scannedParts = [`Scanned at ${artifact.timestamp}`];
|
|
2421
3663
|
if (artifact.durationMs !== void 0) {
|
|
@@ -2429,18 +3671,18 @@ function formatText(artifact) {
|
|
|
2429
3671
|
scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
|
|
2430
3672
|
scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
2431
3673
|
}
|
|
2432
|
-
lines.push(
|
|
3674
|
+
lines.push(chalk7.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
2433
3675
|
lines.push("");
|
|
2434
3676
|
return lines.join("\n");
|
|
2435
3677
|
}
|
|
2436
3678
|
function riskBadge(level) {
|
|
2437
3679
|
switch (level) {
|
|
2438
3680
|
case "low":
|
|
2439
|
-
return
|
|
3681
|
+
return chalk7.bgGreen.black(" LOW ");
|
|
2440
3682
|
case "moderate":
|
|
2441
|
-
return
|
|
3683
|
+
return chalk7.bgYellow.black(" MODERATE ");
|
|
2442
3684
|
case "high":
|
|
2443
|
-
return
|
|
3685
|
+
return chalk7.bgRed.white(" HIGH ");
|
|
2444
3686
|
default:
|
|
2445
3687
|
return level;
|
|
2446
3688
|
}
|
|
@@ -2475,11 +3717,11 @@ function formatExtended(ext) {
|
|
|
2475
3717
|
const inv = ext.toolingInventory;
|
|
2476
3718
|
const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
|
|
2477
3719
|
if (categories.length > 0) {
|
|
2478
|
-
lines.push(
|
|
3720
|
+
lines.push(chalk7.bold.underline(" Tech Stack"));
|
|
2479
3721
|
for (const [cat, items] of categories) {
|
|
2480
3722
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2481
|
-
const names = items.map((i) =>
|
|
2482
|
-
lines.push(` ${
|
|
3723
|
+
const names = items.map((i) => chalk7.white(i.name)).join(chalk7.dim(", "));
|
|
3724
|
+
lines.push(` ${chalk7.cyan(label)}: ${names}`);
|
|
2483
3725
|
}
|
|
2484
3726
|
lines.push("");
|
|
2485
3727
|
}
|
|
@@ -2488,14 +3730,14 @@ function formatExtended(ext) {
|
|
|
2488
3730
|
const svc = ext.serviceDependencies;
|
|
2489
3731
|
const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
|
|
2490
3732
|
if (categories.length > 0) {
|
|
2491
|
-
lines.push(
|
|
3733
|
+
lines.push(chalk7.bold.underline(" Services & Integrations"));
|
|
2492
3734
|
for (const [cat, items] of categories) {
|
|
2493
3735
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2494
3736
|
const names = items.map((i) => {
|
|
2495
|
-
const ver = i.version ?
|
|
2496
|
-
return
|
|
2497
|
-
}).join(
|
|
2498
|
-
lines.push(` ${
|
|
3737
|
+
const ver = i.version ? chalk7.dim(` ${i.version}`) : "";
|
|
3738
|
+
return chalk7.white(i.name) + ver;
|
|
3739
|
+
}).join(chalk7.dim(", "));
|
|
3740
|
+
lines.push(` ${chalk7.cyan(label)}: ${names}`);
|
|
2499
3741
|
}
|
|
2500
3742
|
lines.push("");
|
|
2501
3743
|
}
|
|
@@ -2503,19 +3745,19 @@ function formatExtended(ext) {
|
|
|
2503
3745
|
if (ext.breakingChangeExposure) {
|
|
2504
3746
|
const bc = ext.breakingChangeExposure;
|
|
2505
3747
|
if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
|
|
2506
|
-
lines.push(
|
|
2507
|
-
const exposureColor = bc.exposureScore >= 40 ?
|
|
3748
|
+
lines.push(chalk7.bold.underline(" Breaking Change Exposure"));
|
|
3749
|
+
const exposureColor = bc.exposureScore >= 40 ? chalk7.red : bc.exposureScore >= 20 ? chalk7.yellow : chalk7.green;
|
|
2508
3750
|
lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
|
|
2509
3751
|
if (bc.deprecatedPackages.length > 0) {
|
|
2510
|
-
lines.push(` ${
|
|
3752
|
+
lines.push(` ${chalk7.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk7.dim(p)).join(", ")}`);
|
|
2511
3753
|
}
|
|
2512
3754
|
if (bc.legacyPolyfills.length > 0) {
|
|
2513
|
-
lines.push(` ${
|
|
3755
|
+
lines.push(` ${chalk7.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk7.dim(p)).join(", ")}`);
|
|
2514
3756
|
}
|
|
2515
3757
|
if (bc.peerConflictsDetected) {
|
|
2516
|
-
lines.push(` ${
|
|
3758
|
+
lines.push(` ${chalk7.red("\u26A0")} Peer dependency conflicts detected`);
|
|
2517
3759
|
}
|
|
2518
|
-
lines.push(` Recommendation: ${
|
|
3760
|
+
lines.push(` Recommendation: ${chalk7.bold(bc.overallRecommendation)}`);
|
|
2519
3761
|
const projectsWithPlans = bc.projectIntelligence.filter((p) => p.packages.length > 0).slice(0, 3);
|
|
2520
3762
|
if (projectsWithPlans.length > 0) {
|
|
2521
3763
|
lines.push(" Major Upgrade Intelligence:");
|
|
@@ -2531,21 +3773,21 @@ function formatExtended(ext) {
|
|
|
2531
3773
|
}
|
|
2532
3774
|
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
2533
3775
|
const ts = ext.tsModernity;
|
|
2534
|
-
lines.push(
|
|
3776
|
+
lines.push(chalk7.bold.underline(" TypeScript"));
|
|
2535
3777
|
const parts = [];
|
|
2536
3778
|
parts.push(`v${ts.typescriptVersion}`);
|
|
2537
|
-
if (ts.strict === true) parts.push(
|
|
2538
|
-
else if (ts.strict === false) parts.push(
|
|
3779
|
+
if (ts.strict === true) parts.push(chalk7.green("strict \u2714"));
|
|
3780
|
+
else if (ts.strict === false) parts.push(chalk7.yellow("strict \u2716"));
|
|
2539
3781
|
if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
|
|
2540
3782
|
if (ts.target) parts.push(`target: ${ts.target}`);
|
|
2541
|
-
lines.push(` ${parts.join(
|
|
3783
|
+
lines.push(` ${parts.join(chalk7.dim(" \xB7 "))}`);
|
|
2542
3784
|
lines.push("");
|
|
2543
3785
|
}
|
|
2544
3786
|
if (ext.buildDeploy) {
|
|
2545
3787
|
const bd = ext.buildDeploy;
|
|
2546
3788
|
const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
|
|
2547
3789
|
if (hasSomething) {
|
|
2548
|
-
lines.push(
|
|
3790
|
+
lines.push(chalk7.bold.underline(" Build & Deploy"));
|
|
2549
3791
|
if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
|
|
2550
3792
|
if (bd.docker.dockerfileCount > 0) {
|
|
2551
3793
|
lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
|
|
@@ -2558,42 +3800,42 @@ function formatExtended(ext) {
|
|
|
2558
3800
|
}
|
|
2559
3801
|
if (ext.uiPurpose) {
|
|
2560
3802
|
const up = ext.uiPurpose;
|
|
2561
|
-
lines.push(
|
|
2562
|
-
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") :
|
|
2563
|
-
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ?
|
|
3803
|
+
lines.push(chalk7.bold.underline(" Product Purpose Signals"));
|
|
3804
|
+
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk7.dim("unknown")}`);
|
|
3805
|
+
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk7.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
|
|
2564
3806
|
const top = up.topEvidence.slice(0, 8);
|
|
2565
3807
|
if (top.length > 0) {
|
|
2566
3808
|
lines.push(" Top Signals:");
|
|
2567
3809
|
for (const item of top) {
|
|
2568
|
-
lines.push(` - [${item.kind}] ${item.value} ${
|
|
3810
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk7.dim(`(${item.file})`)}`);
|
|
2569
3811
|
}
|
|
2570
3812
|
}
|
|
2571
3813
|
if (up.unknownSignals.length > 0) {
|
|
2572
3814
|
lines.push(" Unknowns:");
|
|
2573
3815
|
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
2574
|
-
lines.push(` - ${
|
|
3816
|
+
lines.push(` - ${chalk7.yellow(u)}`);
|
|
2575
3817
|
}
|
|
2576
3818
|
}
|
|
2577
3819
|
lines.push("");
|
|
2578
3820
|
}
|
|
2579
3821
|
if (ext.securityPosture) {
|
|
2580
3822
|
const sec = ext.securityPosture;
|
|
2581
|
-
lines.push(
|
|
3823
|
+
lines.push(chalk7.bold.underline(" Security Posture"));
|
|
2582
3824
|
const checks = [];
|
|
2583
|
-
checks.push(sec.lockfilePresent ?
|
|
2584
|
-
checks.push(sec.gitignoreCoversEnv ?
|
|
2585
|
-
checks.push(sec.gitignoreCoversNodeModules ?
|
|
2586
|
-
if (sec.multipleLockfileTypes) checks.push(
|
|
2587
|
-
if (sec.envFilesTracked) checks.push(
|
|
2588
|
-
lines.push(` ${checks.join(
|
|
3825
|
+
checks.push(sec.lockfilePresent ? chalk7.green("Lockfile \u2714") : chalk7.red("Lockfile \u2716"));
|
|
3826
|
+
checks.push(sec.gitignoreCoversEnv ? chalk7.green(".env \u2714") : chalk7.red(".env \u2716"));
|
|
3827
|
+
checks.push(sec.gitignoreCoversNodeModules ? chalk7.green("node_modules \u2714") : chalk7.yellow("node_modules \u2716"));
|
|
3828
|
+
if (sec.multipleLockfileTypes) checks.push(chalk7.yellow("Multiple lockfiles \u26A0"));
|
|
3829
|
+
if (sec.envFilesTracked) checks.push(chalk7.red("Env files tracked \u2716"));
|
|
3830
|
+
lines.push(` ${checks.join(chalk7.dim(" \xB7 "))}`);
|
|
2589
3831
|
lines.push("");
|
|
2590
3832
|
}
|
|
2591
3833
|
if (ext.platformMatrix) {
|
|
2592
3834
|
const pm = ext.platformMatrix;
|
|
2593
3835
|
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
2594
|
-
lines.push(
|
|
3836
|
+
lines.push(chalk7.bold.underline(" Platform"));
|
|
2595
3837
|
if (pm.nativeModules.length > 0) {
|
|
2596
|
-
lines.push(` Native modules: ${pm.nativeModules.map((m) =>
|
|
3838
|
+
lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk7.dim(m)).join(", ")}`);
|
|
2597
3839
|
}
|
|
2598
3840
|
if (pm.osAssumptions.length > 0) {
|
|
2599
3841
|
lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
|
|
@@ -2603,25 +3845,25 @@ function formatExtended(ext) {
|
|
|
2603
3845
|
}
|
|
2604
3846
|
if (ext.codeQuality) {
|
|
2605
3847
|
const cq = ext.codeQuality;
|
|
2606
|
-
lines.push(
|
|
2607
|
-
lines.push(` Files: ${
|
|
3848
|
+
lines.push(chalk7.bold.underline(" Code Quality"));
|
|
3849
|
+
lines.push(` Files: ${chalk7.white(`${cq.filesAnalyzed}`)} \xB7 Functions: ${chalk7.white(`${cq.functionsAnalyzed}`)} \xB7 Avg complexity: ${chalk7.white(`${cq.avgCyclomaticComplexity}`)} \xB7 Avg length: ${chalk7.white(`${cq.avgFunctionLength}`)} lines`);
|
|
2608
3850
|
lines.push(` Max nesting: ${cq.maxNestingDepth} \xB7 Circular deps: ${cq.circularDependencies} \xB7 Dead code: ${cq.deadCodePercent}%`);
|
|
2609
3851
|
if (cq.godFiles.length > 0) {
|
|
2610
3852
|
const preview = cq.godFiles.slice(0, 3).map((f) => `${f.path} (${f.lines} lines)`).join(", ");
|
|
2611
|
-
lines.push(` ${
|
|
3853
|
+
lines.push(` ${chalk7.yellow("God files")}: ${preview}`);
|
|
2612
3854
|
}
|
|
2613
3855
|
lines.push("");
|
|
2614
3856
|
}
|
|
2615
3857
|
if (ext.dependencyGraph) {
|
|
2616
3858
|
const dg = ext.dependencyGraph;
|
|
2617
3859
|
if (dg.lockfileType) {
|
|
2618
|
-
lines.push(
|
|
2619
|
-
lines.push(` ${dg.lockfileType}: ${
|
|
3860
|
+
lines.push(chalk7.bold.underline(" Dependency Graph"));
|
|
3861
|
+
lines.push(` ${dg.lockfileType}: ${chalk7.white(`${dg.totalUnique}`)} unique, ${chalk7.white(`${dg.totalInstalled}`)} installed`);
|
|
2620
3862
|
if (dg.duplicatedPackages.length > 0) {
|
|
2621
|
-
lines.push(` ${
|
|
3863
|
+
lines.push(` ${chalk7.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
|
|
2622
3864
|
}
|
|
2623
3865
|
if (dg.phantomDependencies.length > 0) {
|
|
2624
|
-
lines.push(` ${
|
|
3866
|
+
lines.push(` ${chalk7.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
|
|
2625
3867
|
}
|
|
2626
3868
|
lines.push("");
|
|
2627
3869
|
}
|
|
@@ -2632,13 +3874,13 @@ function formatArchitectureDiagram(arch) {
|
|
|
2632
3874
|
const lines = [];
|
|
2633
3875
|
lines.push(...titleBox("Architecture Layers"));
|
|
2634
3876
|
lines.push("");
|
|
2635
|
-
lines.push(
|
|
2636
|
-
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ?
|
|
3877
|
+
lines.push(chalk7.bold(" Archetype: ") + `${arch.archetype}` + chalk7.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
|
|
3878
|
+
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk7.dim(` (${arch.unclassified} unclassified)`) : ""));
|
|
2637
3879
|
lines.push("");
|
|
2638
3880
|
if (arch.layers.length > 0) {
|
|
2639
3881
|
for (const layer of arch.layers) {
|
|
2640
|
-
const risk = layer.riskLevel === "none" ?
|
|
2641
|
-
lines.push(` ${
|
|
3882
|
+
const risk = layer.riskLevel === "none" ? chalk7.dim("none") : layer.riskLevel === "low" ? chalk7.green("low") : layer.riskLevel === "moderate" ? chalk7.yellow("moderate") : chalk7.red("high");
|
|
3883
|
+
lines.push(` ${chalk7.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
|
|
2642
3884
|
}
|
|
2643
3885
|
lines.push("");
|
|
2644
3886
|
}
|
|
@@ -2900,7 +4142,7 @@ function formatMarkdown(artifact) {
|
|
|
2900
4142
|
lines.push("");
|
|
2901
4143
|
lines.push(`| Metric | Value |`);
|
|
2902
4144
|
lines.push(`|--------|-------|`);
|
|
2903
|
-
lines.push(`| **
|
|
4145
|
+
lines.push(`| **DriftScore** | ${artifact.drift.score}/100 _(lower is better; 0 = no drift)_ |`);
|
|
2904
4146
|
lines.push(`| **Risk Level** | ${artifact.drift.riskLevel.toUpperCase()} |`);
|
|
2905
4147
|
lines.push(`| **Projects** | ${artifact.projects.length} |`);
|
|
2906
4148
|
const scannedMeta = [artifact.timestamp];
|
|
@@ -3007,10 +4249,10 @@ function formatMarkdown(artifact) {
|
|
|
3007
4249
|
|
|
3008
4250
|
// src/reporting/commands/report.ts
|
|
3009
4251
|
var reportCommand = new Command("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) => {
|
|
3010
|
-
const artifactPath =
|
|
4252
|
+
const artifactPath = path.resolve(opts.in);
|
|
3011
4253
|
if (!await pathExists(artifactPath)) {
|
|
3012
|
-
console.error(
|
|
3013
|
-
console.error(
|
|
4254
|
+
console.error(chalk7.red(`Artifact not found: ${artifactPath}`));
|
|
4255
|
+
console.error(chalk7.dim('Run "vibgrate scan" first to generate a scan artifact.'));
|
|
3014
4256
|
process.exit(1);
|
|
3015
4257
|
}
|
|
3016
4258
|
const artifact = await readJsonFile(artifactPath);
|
|
@@ -3053,10 +4295,10 @@ function openUrl(url) {
|
|
|
3053
4295
|
}
|
|
3054
4296
|
|
|
3055
4297
|
// src/reporting/commands/login.ts
|
|
3056
|
-
var delay = (ms) => new Promise((
|
|
4298
|
+
var delay = (ms) => new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
3057
4299
|
function isInside(dir, file) {
|
|
3058
|
-
const
|
|
3059
|
-
return !!
|
|
4300
|
+
const rel3 = path.relative(dir, file);
|
|
4301
|
+
return !!rel3 && !rel3.startsWith("..") && !path.isAbsolute(rel3);
|
|
3060
4302
|
}
|
|
3061
4303
|
var loginCommand = new Command("login").description("Authenticate the CLI with your Vibgrate workspace via the browser").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", `Data residency region (${availableRegionIds().join(", ")})`, "us").option("--no-browser", "Do not attempt to open a browser automatically").option(
|
|
3062
4304
|
"--local",
|
|
@@ -3066,7 +4308,7 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3066
4308
|
try {
|
|
3067
4309
|
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
3068
4310
|
} catch (e) {
|
|
3069
|
-
console.error(
|
|
4311
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
3070
4312
|
process.exit(1);
|
|
3071
4313
|
}
|
|
3072
4314
|
const base = `https://${ingestHost}/v1/auth/device`;
|
|
@@ -3078,28 +4320,28 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3078
4320
|
body: "{}"
|
|
3079
4321
|
});
|
|
3080
4322
|
if (!res.ok) {
|
|
3081
|
-
console.error(
|
|
4323
|
+
console.error(chalk7.red(`Failed to start login (HTTP ${res.status}).`));
|
|
3082
4324
|
process.exit(1);
|
|
3083
4325
|
}
|
|
3084
4326
|
start = await res.json();
|
|
3085
4327
|
} catch (e) {
|
|
3086
|
-
console.error(
|
|
4328
|
+
console.error(chalk7.red(`Could not reach ${ingestHost}: ${e instanceof Error ? e.message : String(e)}`));
|
|
3087
4329
|
process.exit(1);
|
|
3088
4330
|
}
|
|
3089
4331
|
console.log("");
|
|
3090
4332
|
console.log("To finish signing in, open this URL and approve the request:");
|
|
3091
4333
|
console.log("");
|
|
3092
|
-
console.log(" " +
|
|
4334
|
+
console.log(" " + chalk7.cyan(start.verificationUri));
|
|
3093
4335
|
console.log("");
|
|
3094
|
-
console.log(" Your code: " +
|
|
4336
|
+
console.log(" Your code: " + chalk7.bold(start.userCode));
|
|
3095
4337
|
console.log("");
|
|
3096
4338
|
if (opts.browser) {
|
|
3097
4339
|
const opened = openUrl(start.verificationUriComplete);
|
|
3098
4340
|
if (opened) {
|
|
3099
|
-
console.log(
|
|
4341
|
+
console.log(chalk7.dim("Opening your browser\u2026 (if it does not open, use the URL above)"));
|
|
3100
4342
|
}
|
|
3101
4343
|
}
|
|
3102
|
-
console.log(
|
|
4344
|
+
console.log(chalk7.dim("Waiting for approval\u2026"));
|
|
3103
4345
|
const intervalMs = Math.max(2, start.interval || 5) * 1e3;
|
|
3104
4346
|
const deadline = Date.now() + (start.expiresIn || 900) * 1e3;
|
|
3105
4347
|
while (Date.now() < deadline) {
|
|
@@ -3127,7 +4369,7 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3127
4369
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3128
4370
|
};
|
|
3129
4371
|
} else {
|
|
3130
|
-
console.log(
|
|
4372
|
+
console.log(chalk7.dim("Setting up your workspace\u2026"));
|
|
3131
4373
|
try {
|
|
3132
4374
|
const provisioned = await createWorkspaceDsn({
|
|
3133
4375
|
region: opts.region,
|
|
@@ -3142,10 +4384,10 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3142
4384
|
};
|
|
3143
4385
|
} catch (e) {
|
|
3144
4386
|
console.error(
|
|
3145
|
-
|
|
4387
|
+
chalk7.red("\u2716 Signed in, but workspace setup failed: ") + (e instanceof Error ? e.message : String(e))
|
|
3146
4388
|
);
|
|
3147
4389
|
console.error(
|
|
3148
|
-
|
|
4390
|
+
chalk7.dim(' Finish setup with "vibgrate dsn create --workspace new".')
|
|
3149
4391
|
);
|
|
3150
4392
|
process.exit(1);
|
|
3151
4393
|
}
|
|
@@ -3154,40 +4396,40 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3154
4396
|
writeStoredCredentials(creds, storeOpts);
|
|
3155
4397
|
const credsFile = credentialsPath(storeOpts);
|
|
3156
4398
|
console.log("");
|
|
3157
|
-
console.log(
|
|
4399
|
+
console.log(chalk7.green("\u2714") + " Logged in.");
|
|
3158
4400
|
if (creds.workspaceId) {
|
|
3159
|
-
console.log(" Workspace: " +
|
|
4401
|
+
console.log(" Workspace: " + chalk7.bold(creds.workspaceId));
|
|
3160
4402
|
}
|
|
3161
|
-
console.log(
|
|
4403
|
+
console.log(chalk7.dim(` Credentials saved to ${credsFile}`));
|
|
3162
4404
|
try {
|
|
3163
4405
|
const root = findGitRoot();
|
|
3164
4406
|
if (root && isInside(root, credsFile)) {
|
|
3165
4407
|
const res = ensureGitignored(gitignoreEntryForCredentials(root, credsFile), root);
|
|
3166
4408
|
if (res.status === "created") {
|
|
3167
|
-
console.log(
|
|
4409
|
+
console.log(chalk7.dim(` Created .gitignore and ignored ${res.entry}`));
|
|
3168
4410
|
} else if (res.status === "added") {
|
|
3169
|
-
console.log(
|
|
4411
|
+
console.log(chalk7.dim(` Added ${res.entry} to .gitignore`));
|
|
3170
4412
|
}
|
|
3171
4413
|
}
|
|
3172
4414
|
} catch {
|
|
3173
4415
|
}
|
|
3174
|
-
console.log(
|
|
4416
|
+
console.log(chalk7.dim(` You can now run "${resolveCliInvocation()} scan --push".`));
|
|
3175
4417
|
return;
|
|
3176
4418
|
}
|
|
3177
4419
|
if (token.status === "access_denied") {
|
|
3178
|
-
console.error(
|
|
4420
|
+
console.error(chalk7.red("\u2716 Login was denied in the browser."));
|
|
3179
4421
|
process.exit(1);
|
|
3180
4422
|
}
|
|
3181
4423
|
if (token.status === "expired" || token.status === "invalid") {
|
|
3182
|
-
console.error(
|
|
4424
|
+
console.error(chalk7.red('\u2716 Login request expired. Run "vibgrate login" again.'));
|
|
3183
4425
|
process.exit(1);
|
|
3184
4426
|
}
|
|
3185
4427
|
if (token.status === "error") {
|
|
3186
|
-
console.error(
|
|
4428
|
+
console.error(chalk7.red(`\u2716 ${token.error ?? "Login failed."}`));
|
|
3187
4429
|
process.exit(1);
|
|
3188
4430
|
}
|
|
3189
4431
|
}
|
|
3190
|
-
console.error(
|
|
4432
|
+
console.error(chalk7.red('\u2716 Timed out waiting for approval. Run "vibgrate login" again.'));
|
|
3191
4433
|
process.exit(1);
|
|
3192
4434
|
});
|
|
3193
4435
|
|
|
@@ -3208,23 +4450,23 @@ function unsetEnvCommand(varName, env = {}) {
|
|
|
3208
4450
|
var logoutCommand = new Command("logout").description("Clear stored Vibgrate login credentials").action(() => {
|
|
3209
4451
|
const cleared = clearStoredCredentials();
|
|
3210
4452
|
if (cleared) {
|
|
3211
|
-
console.log(
|
|
4453
|
+
console.log(chalk7.green("\u2714") + " Logged out. Stored credentials removed.");
|
|
3212
4454
|
} else {
|
|
3213
|
-
console.log(
|
|
4455
|
+
console.log(chalk7.dim(`No stored credentials found at ${credentialsPath()}.`));
|
|
3214
4456
|
}
|
|
3215
4457
|
if (process.env.VIBGRATE_DSN) {
|
|
3216
4458
|
console.log("");
|
|
3217
4459
|
console.log(
|
|
3218
|
-
|
|
4460
|
+
chalk7.yellow("\u26A0") + " VIBGRATE_DSN is still set in your environment, so commands will keep using it."
|
|
3219
4461
|
);
|
|
3220
4462
|
console.log(
|
|
3221
|
-
|
|
4463
|
+
chalk7.dim(` Unset it to fully sign out: ${unsetEnvCommand("VIBGRATE_DSN")}`)
|
|
3222
4464
|
);
|
|
3223
4465
|
}
|
|
3224
4466
|
});
|
|
3225
4467
|
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
3226
|
-
var CACHE_DIR =
|
|
3227
|
-
var CACHE_FILE =
|
|
4468
|
+
var CACHE_DIR = path.join(os.homedir(), ".vibgrate");
|
|
4469
|
+
var CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
|
|
3228
4470
|
async function fetchLatestVersion() {
|
|
3229
4471
|
try {
|
|
3230
4472
|
const controller = new AbortController();
|
|
@@ -3251,8 +4493,8 @@ async function fetchLatestVersion() {
|
|
|
3251
4493
|
}
|
|
3252
4494
|
async function writeCache(data) {
|
|
3253
4495
|
try {
|
|
3254
|
-
await
|
|
3255
|
-
await
|
|
4496
|
+
await fs14.mkdir(CACHE_DIR, { recursive: true });
|
|
4497
|
+
await fs14.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
3256
4498
|
} catch {
|
|
3257
4499
|
}
|
|
3258
4500
|
}
|
|
@@ -3285,9 +4527,9 @@ function getGlobalUpdateCommand(pm, pkg, version) {
|
|
|
3285
4527
|
}
|
|
3286
4528
|
}
|
|
3287
4529
|
async function detectPackageManager(cwd) {
|
|
3288
|
-
if (await pathExists(
|
|
3289
|
-
if (await pathExists(
|
|
3290
|
-
if (await pathExists(
|
|
4530
|
+
if (await pathExists(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4531
|
+
if (await pathExists(path.join(cwd, "bun.lockb"))) return "bun";
|
|
4532
|
+
if (await pathExists(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
3291
4533
|
return "npm";
|
|
3292
4534
|
}
|
|
3293
4535
|
function getInstallCommand(pm, pkg, version, isDev) {
|
|
@@ -3306,7 +4548,7 @@ function getInstallCommand(pm, pkg, version, isDev) {
|
|
|
3306
4548
|
}
|
|
3307
4549
|
async function isDevDependency(cwd) {
|
|
3308
4550
|
try {
|
|
3309
|
-
const pkgPath =
|
|
4551
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
3310
4552
|
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
3311
4553
|
const pkg = JSON.parse(raw);
|
|
3312
4554
|
return Boolean(pkg.devDependencies?.["@vibgrate/cli"]);
|
|
@@ -3315,21 +4557,21 @@ async function isDevDependency(cwd) {
|
|
|
3315
4557
|
}
|
|
3316
4558
|
}
|
|
3317
4559
|
var updateCommand = new Command("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) => {
|
|
3318
|
-
console.log(
|
|
3319
|
-
console.log(
|
|
4560
|
+
console.log(chalk7.dim(`Current version: ${VERSION}`));
|
|
4561
|
+
console.log(chalk7.dim("Checking npm registry..."));
|
|
3320
4562
|
const latest = await fetchLatestVersion();
|
|
3321
4563
|
if (!latest) {
|
|
3322
|
-
console.error(
|
|
4564
|
+
console.error(chalk7.red("Could not reach the npm registry. Check your network connection."));
|
|
3323
4565
|
process.exit(1);
|
|
3324
4566
|
}
|
|
3325
4567
|
const semver2 = await import('semver');
|
|
3326
4568
|
if (!semver2.gt(latest, VERSION)) {
|
|
3327
|
-
console.log(
|
|
4569
|
+
console.log(chalk7.green("\u2714") + ` You are on the latest version (${VERSION}).`);
|
|
3328
4570
|
return;
|
|
3329
4571
|
}
|
|
3330
|
-
console.log(
|
|
4572
|
+
console.log(chalk7.yellow(`Update available: ${VERSION} \u2192 ${latest}`));
|
|
3331
4573
|
if (opts.check) {
|
|
3332
|
-
console.log(
|
|
4574
|
+
console.log(chalk7.dim('Run "vibgrate update" to install.'));
|
|
3333
4575
|
return;
|
|
3334
4576
|
}
|
|
3335
4577
|
const cwd = process.cwd();
|
|
@@ -3339,17 +4581,17 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
3339
4581
|
let cmd;
|
|
3340
4582
|
if (isGlobal) {
|
|
3341
4583
|
cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
|
|
3342
|
-
console.log(
|
|
4584
|
+
console.log(chalk7.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
3343
4585
|
} else {
|
|
3344
4586
|
const isDev = await isDevDependency(cwd);
|
|
3345
4587
|
cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev);
|
|
3346
|
-
console.log(
|
|
4588
|
+
console.log(chalk7.dim(`Using ${pm}: ${cmd}`));
|
|
3347
4589
|
}
|
|
3348
4590
|
try {
|
|
3349
4591
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
3350
|
-
console.log(
|
|
4592
|
+
console.log(chalk7.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
|
|
3351
4593
|
} catch {
|
|
3352
|
-
console.error(
|
|
4594
|
+
console.error(chalk7.red(`Update failed. Run manually: ${cmd}`));
|
|
3353
4595
|
process.exit(1);
|
|
3354
4596
|
}
|
|
3355
4597
|
});
|
|
@@ -3442,9 +4684,9 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3442
4684
|
try {
|
|
3443
4685
|
const statements = [];
|
|
3444
4686
|
if (opts.from) {
|
|
3445
|
-
const fromPath =
|
|
4687
|
+
const fromPath = path.resolve(opts.from);
|
|
3446
4688
|
if (!await pathExists(fromPath)) {
|
|
3447
|
-
process.stderr.write(
|
|
4689
|
+
process.stderr.write(chalk7.red(`Statements file not found: ${fromPath}
|
|
3448
4690
|
`));
|
|
3449
4691
|
process.exit(1);
|
|
3450
4692
|
}
|
|
@@ -3460,9 +4702,9 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3460
4702
|
});
|
|
3461
4703
|
const json2 = JSON.stringify(doc, null, 2);
|
|
3462
4704
|
if (opts.out) {
|
|
3463
|
-
await writeTextFile(
|
|
4705
|
+
await writeTextFile(path.resolve(opts.out), json2 + "\n");
|
|
3464
4706
|
process.stderr.write(
|
|
3465
|
-
|
|
4707
|
+
chalk7.green("\u2714") + ` Wrote OpenVEX document (${doc.statements.length} statement(s)) to ${path.resolve(opts.out)}
|
|
3466
4708
|
`
|
|
3467
4709
|
);
|
|
3468
4710
|
} else {
|
|
@@ -3470,7 +4712,7 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3470
4712
|
}
|
|
3471
4713
|
} catch (err) {
|
|
3472
4714
|
if (err instanceof VexValidationError) {
|
|
3473
|
-
process.stderr.write(
|
|
4715
|
+
process.stderr.write(chalk7.red(`VEX validation error: ${err.message}
|
|
3474
4716
|
`));
|
|
3475
4717
|
process.exit(1);
|
|
3476
4718
|
}
|
|
@@ -3645,9 +4887,9 @@ function formatDeltaText(base, current) {
|
|
|
3645
4887
|
return lines.join("\n");
|
|
3646
4888
|
}
|
|
3647
4889
|
async function readArtifactOrExit(filePath) {
|
|
3648
|
-
const absolutePath =
|
|
4890
|
+
const absolutePath = path.resolve(filePath);
|
|
3649
4891
|
if (!await pathExists(absolutePath)) {
|
|
3650
|
-
console.error(
|
|
4892
|
+
console.error(chalk7.red(`Artifact not found: ${absolutePath}`));
|
|
3651
4893
|
process.exit(1);
|
|
3652
4894
|
}
|
|
3653
4895
|
return readJsonFile(absolutePath);
|
|
@@ -3656,14 +4898,14 @@ var exportCommand = new Command("export").description("Export scan artifact as S
|
|
|
3656
4898
|
const artifact = await readArtifactOrExit(opts.in);
|
|
3657
4899
|
const format = opts.format.toLowerCase();
|
|
3658
4900
|
if (format !== "cyclonedx" && format !== "spdx") {
|
|
3659
|
-
console.error(
|
|
4901
|
+
console.error(chalk7.red("Invalid SBOM format. Use cyclonedx or spdx."));
|
|
3660
4902
|
process.exit(1);
|
|
3661
4903
|
}
|
|
3662
4904
|
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
3663
4905
|
const body = JSON.stringify(sbom, null, 2);
|
|
3664
4906
|
if (opts.out) {
|
|
3665
|
-
await writeTextFile(
|
|
3666
|
-
console.log(
|
|
4907
|
+
await writeTextFile(path.resolve(opts.out), body);
|
|
4908
|
+
console.log(chalk7.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
3667
4909
|
} else {
|
|
3668
4910
|
console.log(body);
|
|
3669
4911
|
}
|
|
@@ -3673,8 +4915,8 @@ var deltaCommand = new Command("delta").description("Show SBOM delta between two
|
|
|
3673
4915
|
const current = await readArtifactOrExit(opts.to);
|
|
3674
4916
|
const report = formatDeltaText(base, current);
|
|
3675
4917
|
if (opts.out) {
|
|
3676
|
-
await writeTextFile(
|
|
3677
|
-
console.log(
|
|
4918
|
+
await writeTextFile(path.resolve(opts.out), report);
|
|
4919
|
+
console.log(chalk7.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
3678
4920
|
} else {
|
|
3679
4921
|
console.log(report);
|
|
3680
4922
|
}
|
|
@@ -3685,13 +4927,13 @@ var sbomCommand = new Command("sbom").description("Supply-chain evidence: SBOM e
|
|
|
3685
4927
|
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
3686
4928
|
"build",
|
|
3687
4929
|
"status",
|
|
3688
|
-
"verify",
|
|
3689
4930
|
"ask",
|
|
3690
4931
|
"embed",
|
|
3691
4932
|
"show",
|
|
3692
4933
|
"path",
|
|
3693
4934
|
"tree",
|
|
3694
4935
|
"impact",
|
|
4936
|
+
"unknowns",
|
|
3695
4937
|
"tests",
|
|
3696
4938
|
"facts",
|
|
3697
4939
|
"guide",
|
|
@@ -3717,6 +4959,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
3717
4959
|
// Drift-reporting verbs (merged from the Vibgrate CLI).
|
|
3718
4960
|
"init",
|
|
3719
4961
|
"scan",
|
|
4962
|
+
"fix",
|
|
3720
4963
|
"baseline",
|
|
3721
4964
|
"report",
|
|
3722
4965
|
"login",
|
|
@@ -3739,7 +4982,6 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3739
4982
|
});
|
|
3740
4983
|
registerBuild(program);
|
|
3741
4984
|
registerStatus(program);
|
|
3742
|
-
registerVerify(program);
|
|
3743
4985
|
registerAsk(program);
|
|
3744
4986
|
registerEmbed(program);
|
|
3745
4987
|
registerShow(program);
|
|
@@ -3747,6 +4989,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3747
4989
|
registerTree(program);
|
|
3748
4990
|
registerInsights(program);
|
|
3749
4991
|
registerImpact(program);
|
|
4992
|
+
registerUnknowns(program);
|
|
3750
4993
|
registerServe(program);
|
|
3751
4994
|
registerInstall(program);
|
|
3752
4995
|
registerShare(program);
|
|
@@ -3764,6 +5007,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3764
5007
|
registerBundle(program);
|
|
3765
5008
|
program.addCommand(initCommand);
|
|
3766
5009
|
program.addCommand(scanCommand);
|
|
5010
|
+
program.addCommand(fixCommand);
|
|
3767
5011
|
program.addCommand(baselineCommand);
|
|
3768
5012
|
program.addCommand(reportCommand);
|
|
3769
5013
|
program.addCommand(loginCommand);
|
|
@@ -3828,11 +5072,19 @@ function dispatch(argv, cwd) {
|
|
|
3828
5072
|
}
|
|
3829
5073
|
const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
|
|
3830
5074
|
if (looksLikeQuestion) return ["ask", ...args];
|
|
3831
|
-
if (
|
|
5075
|
+
if (fs5.existsSync(path.resolve(cwd, first))) {
|
|
3832
5076
|
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
3833
5077
|
}
|
|
5078
|
+
const moved = MOVED_COMMANDS[first];
|
|
5079
|
+
if (moved) {
|
|
5080
|
+
throw new CliError(`\`vg ${first}\` has moved to \`${moved}\``, ExitCode.USAGE_ERROR);
|
|
5081
|
+
}
|
|
3834
5082
|
return ["ask", ...args];
|
|
3835
5083
|
}
|
|
5084
|
+
var MOVED_COMMANDS = {
|
|
5085
|
+
verify: "vg build --verify",
|
|
5086
|
+
attest: "vg build --attest"
|
|
5087
|
+
};
|
|
3836
5088
|
function findFirstPositional(args) {
|
|
3837
5089
|
for (let i = 0; i < args.length; i++) {
|
|
3838
5090
|
const a = args[i];
|
|
@@ -3848,7 +5100,6 @@ async function main(argv = process.argv) {
|
|
|
3848
5100
|
const raw = argv.slice(2);
|
|
3849
5101
|
if (process.env.NO_COLOR || raw.includes("--no-color")) disableColor();
|
|
3850
5102
|
const cwd = readCwd(raw);
|
|
3851
|
-
const dispatched = dispatch(raw, cwd);
|
|
3852
5103
|
const program = buildProgram();
|
|
3853
5104
|
program.exitOverride((err) => {
|
|
3854
5105
|
throw err;
|
|
@@ -3858,6 +5109,7 @@ async function main(argv = process.argv) {
|
|
|
3858
5109
|
writeOut: (str) => process.stdout.write(str)
|
|
3859
5110
|
});
|
|
3860
5111
|
try {
|
|
5112
|
+
const dispatched = dispatch(raw, cwd);
|
|
3861
5113
|
await program.parseAsync(dispatched, { from: "user" });
|
|
3862
5114
|
} catch (err) {
|
|
3863
5115
|
handleError(err);
|
|
@@ -3865,9 +5117,9 @@ async function main(argv = process.argv) {
|
|
|
3865
5117
|
}
|
|
3866
5118
|
function readCwd(raw) {
|
|
3867
5119
|
const i = raw.findIndex((a) => a === "-C" || a === "--cwd");
|
|
3868
|
-
if (i >= 0 && raw[i + 1]) return
|
|
5120
|
+
if (i >= 0 && raw[i + 1]) return path.resolve(raw[i + 1]);
|
|
3869
5121
|
const eq = raw.find((a) => a.startsWith("--cwd="));
|
|
3870
|
-
if (eq) return
|
|
5122
|
+
if (eq) return path.resolve(eq.slice("--cwd=".length));
|
|
3871
5123
|
return process.cwd();
|
|
3872
5124
|
}
|
|
3873
5125
|
function handleError(err) {
|