@vibgrate/cli 2026.704.1 → 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-FESLZK5O.js.map → baseline-RRO4NNNF.js.map} +1 -1
- package/dist/{chunk-YODVLV37.js → chunk-2J7DPIAL.js} +748 -371
- package/dist/chunk-2J7DPIAL.js.map +1 -0
- package/dist/{chunk-W4ANUENJ.js → chunk-FMCI3MXH.js} +3 -3
- package/dist/{chunk-W4ANUENJ.js.map → chunk-FMCI3MXH.js.map} +1 -1
- package/dist/{chunk-RJHYTD62.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 +1594 -336
- 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-FESLZK5O.js +0 -6
- package/dist/chunk-RJHYTD62.js.map +0 -1
- package/dist/chunk-YODVLV37.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,28 +479,84 @@ 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;
|
|
231
|
-
const modelId = resolveEmbedModel();
|
|
232
|
-
if (!isModelReady(modelId)) return;
|
|
233
|
-
if (countPending(graph, root, modelId) === 0) return;
|
|
234
546
|
const cli = process.argv[1];
|
|
235
547
|
if (!cli) return;
|
|
548
|
+
const modelId = resolveEmbedModel();
|
|
549
|
+
const ready = isModelReady(modelId);
|
|
550
|
+
if (ready && countPending(graph, root, modelId) === 0) return;
|
|
551
|
+
const args = ready ? [cli, "embed", "-C", root, "--bg"] : [cli, "embed", "-C", root, "--bg", "--download"];
|
|
236
552
|
try {
|
|
237
|
-
const child = spawn(process.execPath,
|
|
238
|
-
detached: true,
|
|
239
|
-
stdio: "ignore"
|
|
240
|
-
});
|
|
553
|
+
const child = spawn(process.execPath, args, { detached: true, stdio: "ignore" });
|
|
241
554
|
child.unref();
|
|
242
|
-
info(
|
|
555
|
+
info(
|
|
556
|
+
c.dim(
|
|
557
|
+
ready ? " warming the semantic index in the background \u2014 `vg ask` will be instant" : " downloading the semantic model in the background (once) \u2014 `vg ask`/`vg serve` will be instant; disable with --no-warm"
|
|
558
|
+
)
|
|
559
|
+
);
|
|
243
560
|
} catch {
|
|
244
561
|
}
|
|
245
562
|
}
|
|
@@ -248,7 +565,7 @@ function writeExport(graph, target) {
|
|
|
248
565
|
out(serializeGraph(graph).trimEnd());
|
|
249
566
|
return;
|
|
250
567
|
}
|
|
251
|
-
const ext =
|
|
568
|
+
const ext = path.extname(target).toLowerCase();
|
|
252
569
|
let content;
|
|
253
570
|
switch (ext) {
|
|
254
571
|
case ".json":
|
|
@@ -266,14 +583,14 @@ function writeExport(graph, target) {
|
|
|
266
583
|
ExitCode.USAGE_ERROR
|
|
267
584
|
);
|
|
268
585
|
}
|
|
269
|
-
|
|
270
|
-
|
|
586
|
+
fs5.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
|
|
587
|
+
fs5.writeFileSync(target, content);
|
|
271
588
|
}
|
|
272
589
|
function collect(value, prev) {
|
|
273
590
|
return [...prev, value];
|
|
274
591
|
}
|
|
275
|
-
function
|
|
276
|
-
const r =
|
|
592
|
+
function rel2(root) {
|
|
593
|
+
const r = path.relative(process.cwd(), root);
|
|
277
594
|
return r === "" ? "." : r;
|
|
278
595
|
}
|
|
279
596
|
function registerStatus(program) {
|
|
@@ -283,10 +600,10 @@ function registerStatus(program) {
|
|
|
283
600
|
applyGlobalOptions(cmd);
|
|
284
601
|
}
|
|
285
602
|
async function runStatus(global) {
|
|
286
|
-
const root =
|
|
603
|
+
const root = path.resolve(global.cwd ?? ".");
|
|
287
604
|
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
288
605
|
const graph = loadGraph(root, graphPath);
|
|
289
|
-
const hasCache =
|
|
606
|
+
const hasCache = fs5.existsSync(path.join(cacheDir(root), "parse-cache.json"));
|
|
290
607
|
let stale = null;
|
|
291
608
|
let staleExact = false;
|
|
292
609
|
if (graph) {
|
|
@@ -306,8 +623,8 @@ async function runStatus(global) {
|
|
|
306
623
|
}
|
|
307
624
|
if (global.json) {
|
|
308
625
|
json({
|
|
309
|
-
root:
|
|
310
|
-
graphPath:
|
|
626
|
+
root: path.relative(process.cwd(), root) || ".",
|
|
627
|
+
graphPath: path.relative(root, graphPath),
|
|
311
628
|
built: graph !== null,
|
|
312
629
|
generatedAt: graph?.generatedAt ?? null,
|
|
313
630
|
counts: graph?.meta.counts ?? null,
|
|
@@ -322,11 +639,11 @@ async function runStatus(global) {
|
|
|
322
639
|
return;
|
|
323
640
|
}
|
|
324
641
|
if (!graph) {
|
|
325
|
-
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))}`);
|
|
326
643
|
info(` run ${c.bold("vg")} to build one`);
|
|
327
644
|
return;
|
|
328
645
|
}
|
|
329
|
-
info(`${c.cyan("vg")} \xB7 ${
|
|
646
|
+
info(`${c.cyan("vg")} \xB7 ${path.relative(root, graphPath)}`);
|
|
330
647
|
info(` generated ${graph.generatedAt}`);
|
|
331
648
|
info(
|
|
332
649
|
` nodes ${c.bold(String(graph.meta.counts.nodes))} edges ${c.bold(
|
|
@@ -342,34 +659,8 @@ async function runStatus(global) {
|
|
|
342
659
|
}
|
|
343
660
|
if (vibgrateDir(root)) ;
|
|
344
661
|
}
|
|
345
|
-
function registerVerify(program) {
|
|
346
|
-
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) {
|
|
347
|
-
const global = readGlobal(this);
|
|
348
|
-
const root = path9.resolve(global.cwd ?? ".");
|
|
349
|
-
const result = await verifyDeterminism({
|
|
350
|
-
root,
|
|
351
|
-
only: opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
352
|
-
jobs: opts.jobs ? Number(opts.jobs) : void 0
|
|
353
|
-
});
|
|
354
|
-
if (global.json) {
|
|
355
|
-
json({ ok: result.ok, checks: result.checks, digest: result.digest });
|
|
356
|
-
} else {
|
|
357
|
-
info(`${c.cyan("vg verify")} \xB7 ${path9.relative(process.cwd(), root) || "."}`);
|
|
358
|
-
for (const check of result.checks) {
|
|
359
|
-
const mark = check.ok ? c.green("\u2714") : c.red("\u2718");
|
|
360
|
-
const detail = check.detail ? c.dim(` (${check.detail})`) : "";
|
|
361
|
-
info(` ${mark} ${check.name}${detail}`);
|
|
362
|
-
}
|
|
363
|
-
info(result.ok ? c.green(` deterministic \xB7 digest ${result.digest.slice(0, 16)}\u2026`) : c.red(" NON-DETERMINISTIC"));
|
|
364
|
-
}
|
|
365
|
-
if (!result.ok) {
|
|
366
|
-
throw new CliError("determinism self-check failed", ExitCode.NON_DETERMINISTIC);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
applyGlobalOptions(cmd);
|
|
370
|
-
}
|
|
371
662
|
function rootOf(global) {
|
|
372
|
-
return
|
|
663
|
+
return path.resolve(global.cwd ?? ".");
|
|
373
664
|
}
|
|
374
665
|
function requireGraph(global) {
|
|
375
666
|
const root = rootOf(global);
|
|
@@ -456,7 +747,7 @@ function mb(bytes) {
|
|
|
456
747
|
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
457
748
|
}
|
|
458
749
|
function registerEmbed(program) {
|
|
459
|
-
const cmd = program.command("embed").description("precompute the semantic index for instant `vg ask` (or --where / --clear)").option("--where", "show where the model is cached, and its size").option("--clear", "remove the downloaded model from the shared cache").addOption(new Option("--bg").hideHelp()).action(async function(opts) {
|
|
750
|
+
const cmd = program.command("embed").description("precompute the semantic index for instant `vg ask` (or --where / --clear)").option("--where", "show where the model is cached, and its size").option("--clear", "remove the downloaded model from the shared cache").addOption(new Option("--bg").hideHelp()).addOption(new Option("--download").hideHelp()).action(async function(opts) {
|
|
460
751
|
const global = readGlobal(this);
|
|
461
752
|
const modelId = resolveEmbedModel();
|
|
462
753
|
if (opts.where) return showWhere(rootOf(global), modelId, global.json);
|
|
@@ -477,8 +768,10 @@ function registerEmbed(program) {
|
|
|
477
768
|
let reason;
|
|
478
769
|
const embedder = await loadEmbedder({
|
|
479
770
|
local: global.local,
|
|
480
|
-
|
|
481
|
-
//
|
|
771
|
+
// Background warm-ups skip the download unless explicitly asked to fetch
|
|
772
|
+
// (`--bg --download`, spawned by `vg build` so the model is ready by the
|
|
773
|
+
// time `vg serve`/`vg ask` needs it).
|
|
774
|
+
noDownload: bg && opts.download !== true,
|
|
482
775
|
showDownloadProgress: !bg,
|
|
483
776
|
onUnavailable: (r) => reason = r
|
|
484
777
|
});
|
|
@@ -651,10 +944,10 @@ function registerTree(program) {
|
|
|
651
944
|
applyGlobalOptions(cmd);
|
|
652
945
|
}
|
|
653
946
|
function next(index, node, direction) {
|
|
654
|
-
const
|
|
947
|
+
const rel3 = direction === "callees" ? index.callees(node.id) : index.callers(node.id);
|
|
655
948
|
const seen = /* @__PURE__ */ new Set();
|
|
656
949
|
const out2 = [];
|
|
657
|
-
for (const { node: n } of
|
|
950
|
+
for (const { node: n } of rel3) {
|
|
658
951
|
if (seen.has(n.id)) continue;
|
|
659
952
|
seen.add(n.id);
|
|
660
953
|
out2.push(n);
|
|
@@ -830,6 +1123,113 @@ function registerImpact(program) {
|
|
|
830
1123
|
});
|
|
831
1124
|
applyGlobalOptions(cmd);
|
|
832
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
|
+
}
|
|
833
1233
|
function registerServe(program) {
|
|
834
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) {
|
|
835
1235
|
const global = readGlobal(this);
|
|
@@ -842,7 +1242,7 @@ function registerServe(program) {
|
|
|
842
1242
|
dedup: opts.dedup === true,
|
|
843
1243
|
refresh
|
|
844
1244
|
};
|
|
845
|
-
if (!
|
|
1245
|
+
if (!fs5.existsSync(graphPath)) {
|
|
846
1246
|
throw new CliError(
|
|
847
1247
|
`no map found at ${graphPath} \u2014 run \`vg\` to build one first`,
|
|
848
1248
|
ExitCode.NOT_FOUND
|
|
@@ -881,7 +1281,7 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
|
|
|
881
1281
|
info(c.red(`vg serve: request error: ${err.message}`));
|
|
882
1282
|
}
|
|
883
1283
|
});
|
|
884
|
-
await new Promise((
|
|
1284
|
+
await new Promise((resolve16) => httpServer.listen(port, host, resolve16));
|
|
885
1285
|
info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
|
|
886
1286
|
await new Promise(() => {
|
|
887
1287
|
});
|
|
@@ -898,9 +1298,9 @@ function registerInstall(program) {
|
|
|
898
1298
|
json(ASSISTANTS.map((a) => ({ id: a.id, label: a.label, mcp: !!a.mcp, skill: !!a.skill, nudge: !!a.nudge })));
|
|
899
1299
|
} else {
|
|
900
1300
|
info(`${c.cyan("vg install")} \xB7 supported assistants`);
|
|
901
|
-
const
|
|
1301
|
+
const pad4 = Math.max(...ASSISTANTS.map((a) => a.id.length)) + 2;
|
|
902
1302
|
for (const a of ASSISTANTS) {
|
|
903
|
-
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"}`)}`);
|
|
904
1304
|
}
|
|
905
1305
|
}
|
|
906
1306
|
return;
|
|
@@ -912,8 +1312,9 @@ function registerInstall(program) {
|
|
|
912
1312
|
const smallRepo = graph !== null && fileCount > 0 && fileCount < SMALL_REPO_FILES;
|
|
913
1313
|
const launch = detectServeLaunch();
|
|
914
1314
|
const results = targets.map((a) => ({ id: a.id, ...installAssistant(a, { root, hook: opts.hook, smallRepo, launch }) }));
|
|
1315
|
+
const navConfig = writeNavigationConfig(root);
|
|
915
1316
|
if (global.json) {
|
|
916
|
-
json({ root, smallRepo, launch: { command: launch.command, args: launch.args, note: launch.note ?? null }, results });
|
|
1317
|
+
json({ root, smallRepo, navConfig, launch: { command: launch.command, args: launch.args, note: launch.note ?? null }, results });
|
|
917
1318
|
return;
|
|
918
1319
|
}
|
|
919
1320
|
for (const r of results) {
|
|
@@ -921,6 +1322,7 @@ function registerInstall(program) {
|
|
|
921
1322
|
}
|
|
922
1323
|
if (launch.note && results.some((r) => r.note)) info(`${c.yellow("!")} ${launch.note}`);
|
|
923
1324
|
if (smallRepo) info(c.dim(` note: small repo (${fileCount} files) \u2014 nudge says searching is fine; vg is still used for impact/tests`));
|
|
1325
|
+
info(c.dim(` wrote ${navConfig} \u2014 deferred-loading config for Claude-API agents (lower per-step token cost)`));
|
|
924
1326
|
info(c.dim(" run `vg serve` is wired via MCP; build the map with `vg` if you have not yet"));
|
|
925
1327
|
});
|
|
926
1328
|
applyGlobalOptions(install);
|
|
@@ -958,8 +1360,8 @@ function registerShare(program) {
|
|
|
958
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) {
|
|
959
1361
|
const global = readGlobal(this);
|
|
960
1362
|
const root = rootOf(global);
|
|
961
|
-
const gitDir =
|
|
962
|
-
if (!
|
|
1363
|
+
const gitDir = path.join(root, ".git");
|
|
1364
|
+
if (!fs5.existsSync(gitDir)) {
|
|
963
1365
|
throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
|
|
964
1366
|
}
|
|
965
1367
|
if (opts.undo) {
|
|
@@ -980,23 +1382,23 @@ function registerShare(program) {
|
|
|
980
1382
|
applyGlobalOptions(cmd);
|
|
981
1383
|
}
|
|
982
1384
|
function writeVibgrateGitignore(root, reports) {
|
|
983
|
-
const dir =
|
|
984
|
-
|
|
1385
|
+
const dir = path.join(root, ".vibgrate");
|
|
1386
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
985
1387
|
const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
|
|
986
1388
|
if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
|
|
987
|
-
|
|
1389
|
+
fs5.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
|
|
988
1390
|
`);
|
|
989
1391
|
}
|
|
990
1392
|
function writeGitAttributes(root) {
|
|
991
|
-
const file =
|
|
1393
|
+
const file = path.join(root, ".gitattributes");
|
|
992
1394
|
const line = ".vibgrate/graph.json merge=vg";
|
|
993
|
-
let content =
|
|
1395
|
+
let content = fs5.existsSync(file) ? fs5.readFileSync(file, "utf8") : "";
|
|
994
1396
|
if (!content.split("\n").some((l) => l.trim() === line)) {
|
|
995
1397
|
content = content.length ? `${content.replace(/\s*$/, "")}
|
|
996
1398
|
${line}
|
|
997
1399
|
` : `${line}
|
|
998
1400
|
`;
|
|
999
|
-
|
|
1401
|
+
fs5.writeFileSync(file, content);
|
|
1000
1402
|
}
|
|
1001
1403
|
}
|
|
1002
1404
|
function configureMergeDriver(root) {
|
|
@@ -1005,8 +1407,8 @@ function configureMergeDriver(root) {
|
|
|
1005
1407
|
}
|
|
1006
1408
|
function installPreCommitHook(root) {
|
|
1007
1409
|
const hooksDir = hookDir(root);
|
|
1008
|
-
|
|
1009
|
-
const file =
|
|
1410
|
+
fs5.mkdirSync(hooksDir, { recursive: true });
|
|
1411
|
+
const file = path.join(hooksDir, "pre-commit");
|
|
1010
1412
|
const block = [
|
|
1011
1413
|
HOOK_BEGIN,
|
|
1012
1414
|
"if command -v vg >/dev/null 2>&1; then",
|
|
@@ -1015,7 +1417,7 @@ function installPreCommitHook(root) {
|
|
|
1015
1417
|
"fi",
|
|
1016
1418
|
HOOK_END
|
|
1017
1419
|
].join("\n");
|
|
1018
|
-
let content =
|
|
1420
|
+
let content = fs5.existsSync(file) ? fs5.readFileSync(file, "utf8") : "#!/bin/sh\n";
|
|
1019
1421
|
const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
|
|
1020
1422
|
if (re.test(content)) content = content.replace(re, block);
|
|
1021
1423
|
else content = `${content.replace(/\s*$/, "")}
|
|
@@ -1024,21 +1426,21 @@ ${block}
|
|
|
1024
1426
|
`;
|
|
1025
1427
|
if (!content.startsWith("#!")) content = `#!/bin/sh
|
|
1026
1428
|
${content}`;
|
|
1027
|
-
|
|
1028
|
-
|
|
1429
|
+
fs5.writeFileSync(file, content);
|
|
1430
|
+
fs5.chmodSync(file, 493);
|
|
1029
1431
|
}
|
|
1030
1432
|
function undo(root) {
|
|
1031
|
-
const hookFile =
|
|
1032
|
-
if (
|
|
1433
|
+
const hookFile = path.join(hookDir(root), "pre-commit");
|
|
1434
|
+
if (fs5.existsSync(hookFile)) {
|
|
1033
1435
|
const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
|
|
1034
|
-
const next2 =
|
|
1035
|
-
if (next2.trim() === "#!/bin/sh")
|
|
1036
|
-
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);
|
|
1037
1439
|
}
|
|
1038
|
-
const attr =
|
|
1039
|
-
if (
|
|
1040
|
-
const next2 =
|
|
1041
|
-
|
|
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);
|
|
1042
1444
|
}
|
|
1043
1445
|
try {
|
|
1044
1446
|
git(root, ["config", "--remove-section", "merge.vg"]);
|
|
@@ -1048,10 +1450,10 @@ function undo(root) {
|
|
|
1048
1450
|
function hookDir(root) {
|
|
1049
1451
|
try {
|
|
1050
1452
|
const configured = git(root, ["config", "--get", "core.hooksPath"]).trim();
|
|
1051
|
-
if (configured) return
|
|
1453
|
+
if (configured) return path.resolve(root, configured);
|
|
1052
1454
|
} catch {
|
|
1053
1455
|
}
|
|
1054
|
-
return
|
|
1456
|
+
return path.join(root, ".git", "hooks");
|
|
1055
1457
|
}
|
|
1056
1458
|
function git(root, args) {
|
|
1057
1459
|
return execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
@@ -1082,7 +1484,7 @@ function registerBenchmark(program) {
|
|
|
1082
1484
|
if (tokenReduction.questions.length) {
|
|
1083
1485
|
info(` token reduction vs grep/read baseline (${c.dim("estimates")}):`);
|
|
1084
1486
|
for (const q of tokenReduction.questions) {
|
|
1085
|
-
info(` ${
|
|
1487
|
+
info(` ${pad3(`${q.ratio.toFixed(1)}\xD7`, 6)} ${c.dim(`vg ${q.vgTokens} vs ~${q.baselineTokens}`)} "${q.question}"`);
|
|
1086
1488
|
}
|
|
1087
1489
|
info(` ${c.bold(`aggregate \u2248 ${tokenReduction.aggregateRatio.toFixed(1)}\xD7 fewer tokens`)} ${c.dim("(honest estimate, scales with repo size)")}`);
|
|
1088
1490
|
}
|
|
@@ -1171,7 +1573,7 @@ function grepBaselineTokens(term, files) {
|
|
|
1171
1573
|
if (f.size > 512 * 1024) continue;
|
|
1172
1574
|
let content = "";
|
|
1173
1575
|
try {
|
|
1174
|
-
content =
|
|
1576
|
+
content = fs5.readFileSync(f.abs, "utf8");
|
|
1175
1577
|
} catch {
|
|
1176
1578
|
continue;
|
|
1177
1579
|
}
|
|
@@ -1181,7 +1583,7 @@ function grepBaselineTokens(term, files) {
|
|
|
1181
1583
|
}
|
|
1182
1584
|
function safeSize(abs) {
|
|
1183
1585
|
try {
|
|
1184
|
-
return
|
|
1586
|
+
return fs5.statSync(abs).size;
|
|
1185
1587
|
} catch {
|
|
1186
1588
|
return 0;
|
|
1187
1589
|
}
|
|
@@ -1198,7 +1600,7 @@ function round(x) {
|
|
|
1198
1600
|
function round6(x) {
|
|
1199
1601
|
return Math.round(x * 1e6) / 1e6;
|
|
1200
1602
|
}
|
|
1201
|
-
function
|
|
1603
|
+
function pad3(s, n) {
|
|
1202
1604
|
return s.padStart(n, " ");
|
|
1203
1605
|
}
|
|
1204
1606
|
function registerTests(program) {
|
|
@@ -1511,11 +1913,11 @@ function normalizeStandards(raw) {
|
|
|
1511
1913
|
return { banned };
|
|
1512
1914
|
}
|
|
1513
1915
|
function loadStandards(root) {
|
|
1514
|
-
for (const
|
|
1515
|
-
const p =
|
|
1916
|
+
for (const rel3 of STANDARDS_FILES) {
|
|
1917
|
+
const p = path__default.join(root, rel3);
|
|
1516
1918
|
let text;
|
|
1517
1919
|
try {
|
|
1518
|
-
text =
|
|
1920
|
+
text = fs5__default.readFileSync(p, "utf8");
|
|
1519
1921
|
} catch {
|
|
1520
1922
|
continue;
|
|
1521
1923
|
}
|
|
@@ -1710,7 +2112,7 @@ async function addCmd(root, rest, opts, asJson) {
|
|
|
1710
2112
|
}
|
|
1711
2113
|
function readPackageVersion(root) {
|
|
1712
2114
|
try {
|
|
1713
|
-
const pkg = JSON.parse(
|
|
2115
|
+
const pkg = JSON.parse(fs5__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
|
|
1714
2116
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
1715
2117
|
} catch {
|
|
1716
2118
|
return void 0;
|
|
@@ -1718,7 +2120,7 @@ function readPackageVersion(root) {
|
|
|
1718
2120
|
}
|
|
1719
2121
|
function readOptionalFile(p) {
|
|
1720
2122
|
try {
|
|
1721
|
-
const t =
|
|
2123
|
+
const t = fs5__default.readFileSync(p, "utf8");
|
|
1722
2124
|
return t.trim() ? t : void 0;
|
|
1723
2125
|
} catch {
|
|
1724
2126
|
return void 0;
|
|
@@ -1733,8 +2135,8 @@ async function publishCmd(root, rest, opts, global, asJson) {
|
|
|
1733
2135
|
if (!parsed) throw new CliError("publishing a private library requires a DSN \u2014 run `vibgrate login` or set VIBGRATE_DSN", ExitCode.USAGE_ERROR);
|
|
1734
2136
|
const version = (opts.version || readPackageVersion(root) || "").trim();
|
|
1735
2137
|
if (!version) throw usageError("a --version is required (or run from a package whose package.json has a version)");
|
|
1736
|
-
const readme = readOptionalFile(opts.readme ?
|
|
1737
|
-
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;
|
|
1738
2140
|
if (!readme && !dts) throw usageError("nothing to publish \u2014 no README.md found and no --dts given");
|
|
1739
2141
|
const result = await publishPrivateLibrary(
|
|
1740
2142
|
{ name, version, readme, dts, language: opts.language },
|
|
@@ -1846,7 +2248,7 @@ async function showCmd(root, name, budget, asJson, opts = {}) {
|
|
|
1846
2248
|
out(doc);
|
|
1847
2249
|
}
|
|
1848
2250
|
function registerExport(program) {
|
|
1849
|
-
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) {
|
|
1850
2252
|
const global = readGlobal(this);
|
|
1851
2253
|
const { graph } = requireGraph(global);
|
|
1852
2254
|
const root = rootOf(global);
|
|
@@ -1863,8 +2265,8 @@ function registerExport(program) {
|
|
|
1863
2265
|
out(content.trimEnd());
|
|
1864
2266
|
return;
|
|
1865
2267
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
2268
|
+
fs5.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
2269
|
+
fs5.writeFileSync(file, content);
|
|
1868
2270
|
info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
|
|
1869
2271
|
});
|
|
1870
2272
|
applyGlobalOptions(cmd);
|
|
@@ -1874,37 +2276,37 @@ function detectFormat(file) {
|
|
|
1874
2276
|
if (lower.endsWith(".cdx.json")) return "cyclonedx";
|
|
1875
2277
|
if (lower.endsWith(".spdx.json")) return "spdx";
|
|
1876
2278
|
if (file === "-") return "json";
|
|
1877
|
-
return formatForExt(
|
|
2279
|
+
return formatForExt(path.extname(file));
|
|
1878
2280
|
}
|
|
1879
2281
|
function registerBundle(program) {
|
|
1880
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) {
|
|
1881
2283
|
const global = readGlobal(this);
|
|
1882
2284
|
const root = rootOf(global);
|
|
1883
|
-
const outDir =
|
|
2285
|
+
const outDir = path.resolve(root, opts.out ?? "vg-bundle");
|
|
1884
2286
|
let grammarFiles;
|
|
1885
2287
|
try {
|
|
1886
2288
|
grammarFiles = resolvedGrammarFiles();
|
|
1887
2289
|
} catch {
|
|
1888
2290
|
throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
|
|
1889
2291
|
}
|
|
1890
|
-
|
|
2292
|
+
fs5.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
|
|
1891
2293
|
let grammarCount = 0;
|
|
1892
2294
|
for (const { fileName, absPath } of grammarFiles) {
|
|
1893
|
-
|
|
2295
|
+
fs5.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
|
|
1894
2296
|
grammarCount++;
|
|
1895
2297
|
}
|
|
1896
2298
|
const included = [`grammars/ (${grammarCount} .wasm)`];
|
|
1897
2299
|
const graphSrc = global.graph ?? defaultGraphPath(root);
|
|
1898
|
-
if (
|
|
1899
|
-
|
|
2300
|
+
if (fs5.existsSync(graphSrc)) {
|
|
2301
|
+
fs5.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
|
|
1900
2302
|
included.push("graph.json");
|
|
1901
2303
|
}
|
|
1902
2304
|
const cat = catalogPath(root);
|
|
1903
|
-
if (
|
|
1904
|
-
|
|
2305
|
+
if (fs5.existsSync(cat)) {
|
|
2306
|
+
fs5.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
|
|
1905
2307
|
included.push("vibgrate.lib.json");
|
|
1906
|
-
if (
|
|
1907
|
-
|
|
2308
|
+
if (fs5.existsSync(libDir(root))) {
|
|
2309
|
+
fs5.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
|
|
1908
2310
|
included.push("lib/");
|
|
1909
2311
|
}
|
|
1910
2312
|
}
|
|
@@ -1916,38 +2318,38 @@ function registerBundle(program) {
|
|
|
1916
2318
|
included,
|
|
1917
2319
|
usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
|
|
1918
2320
|
};
|
|
1919
|
-
|
|
2321
|
+
fs5.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
|
|
1920
2322
|
`);
|
|
1921
2323
|
if (global.json) {
|
|
1922
|
-
json({ out:
|
|
2324
|
+
json({ out: path.relative(root, outDir), ...manifest });
|
|
1923
2325
|
return;
|
|
1924
2326
|
}
|
|
1925
|
-
info(`${c.cyan("vg bundle")} \xB7 wrote ${
|
|
2327
|
+
info(`${c.cyan("vg bundle")} \xB7 wrote ${path.relative(root, outDir)}/`);
|
|
1926
2328
|
for (const i of included) info(` ${c.green("\u2714")} ${i}`);
|
|
1927
|
-
info(c.dim(` offline: vg build --grammars "${
|
|
2329
|
+
info(c.dim(` offline: vg build --grammars "${path.relative(root, outDir)}/grammars"`));
|
|
1928
2330
|
});
|
|
1929
2331
|
applyGlobalOptions(cmd);
|
|
1930
2332
|
}
|
|
1931
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) => {
|
|
1932
|
-
const rootDir =
|
|
1933
|
-
const vibgrateDir2 =
|
|
2334
|
+
const rootDir = path.resolve(targetPath);
|
|
2335
|
+
const vibgrateDir2 = path.join(rootDir, ".vibgrate");
|
|
1934
2336
|
await ensureDir(vibgrateDir2);
|
|
1935
|
-
console.log(
|
|
1936
|
-
const configPath =
|
|
2337
|
+
console.log(chalk7.green("\u2714") + ` Created ${chalk7.bold(".vibgrate/")} directory`);
|
|
2338
|
+
const configPath = path.join(rootDir, "vibgrate.config.ts");
|
|
1937
2339
|
if (await pathExists(configPath)) {
|
|
1938
|
-
console.log(
|
|
2340
|
+
console.log(chalk7.dim(" vibgrate.config.ts already exists, skipping"));
|
|
1939
2341
|
} else {
|
|
1940
2342
|
await writeDefaultConfig(rootDir);
|
|
1941
|
-
console.log(
|
|
2343
|
+
console.log(chalk7.green("\u2714") + ` Created ${chalk7.bold("vibgrate.config.ts")}`);
|
|
1942
2344
|
}
|
|
1943
2345
|
if (opts.baseline) {
|
|
1944
|
-
const { runBaseline } = await import('./baseline-
|
|
2346
|
+
const { runBaseline } = await import('./baseline-RRO4NNNF.js');
|
|
1945
2347
|
await runBaseline(rootDir);
|
|
1946
2348
|
}
|
|
1947
2349
|
console.log("");
|
|
1948
|
-
console.log(
|
|
1949
|
-
console.log(` ${
|
|
1950
|
-
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`);
|
|
1951
2353
|
console.log("");
|
|
1952
2354
|
});
|
|
1953
2355
|
|
|
@@ -1974,7 +2376,7 @@ var AI_ASSISTANT_DETECTORS = [
|
|
|
1974
2376
|
async function detectAiAssistant(rootDir) {
|
|
1975
2377
|
for (const detector of AI_ASSISTANT_DETECTORS) {
|
|
1976
2378
|
for (const file of detector.files) {
|
|
1977
|
-
if (await pathExists$1(
|
|
2379
|
+
if (await pathExists$1(path.join(rootDir, file))) {
|
|
1978
2380
|
return detector.name;
|
|
1979
2381
|
}
|
|
1980
2382
|
}
|
|
@@ -1982,22 +2384,22 @@ async function detectAiAssistant(rootDir) {
|
|
|
1982
2384
|
return null;
|
|
1983
2385
|
}
|
|
1984
2386
|
function printAiContextPrompt(detectedAssistant) {
|
|
1985
|
-
const teal2 =
|
|
1986
|
-
const mint2 =
|
|
2387
|
+
const teal2 = chalk7.hex("#3FB0A4");
|
|
2388
|
+
const mint2 = chalk7.hex("#4FE3C1");
|
|
1987
2389
|
const cli = resolveCliInvocation();
|
|
1988
2390
|
const installArgs = detectedAssistant ? `install ${detectedAssistant}` : "install --all";
|
|
1989
2391
|
const installCmd = `${cli} ${installArgs}`;
|
|
1990
|
-
const detectedNote = detectedAssistant ?
|
|
2392
|
+
const detectedNote = detectedAssistant ? chalk7.dim(` (${detectedAssistant} config detected)`) : "";
|
|
1991
2393
|
console.log("");
|
|
1992
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"));
|
|
1993
|
-
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"));
|
|
1994
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"));
|
|
1995
2397
|
console.log("");
|
|
1996
|
-
console.log(" " +
|
|
2398
|
+
console.log(" " + chalk7.bold.white(installCmd) + detectedNote);
|
|
1997
2399
|
console.log("");
|
|
1998
|
-
console.log(" " + teal2("\xB7") + " " +
|
|
1999
|
-
console.log(" " + teal2("\xB7") + " " +
|
|
2000
|
-
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"));
|
|
2001
2403
|
console.log("");
|
|
2002
2404
|
}
|
|
2003
2405
|
|
|
@@ -2008,14 +2410,14 @@ function shouldBuildCodeMap(opts) {
|
|
|
2008
2410
|
async function autoPush(artifact, rootDir, opts) {
|
|
2009
2411
|
const dsn = resolveDsn(opts.dsn);
|
|
2010
2412
|
if (!dsn) {
|
|
2011
|
-
console.error(
|
|
2012
|
-
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.'));
|
|
2013
2415
|
if (opts.strict) process.exit(1);
|
|
2014
2416
|
return;
|
|
2015
2417
|
}
|
|
2016
2418
|
const parsed = parseDsn(dsn);
|
|
2017
2419
|
if (!parsed) {
|
|
2018
|
-
console.error(
|
|
2420
|
+
console.error(chalk7.red("Invalid DSN format."));
|
|
2019
2421
|
if (opts.strict) process.exit(1);
|
|
2020
2422
|
return;
|
|
2021
2423
|
}
|
|
@@ -2026,7 +2428,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2026
2428
|
try {
|
|
2027
2429
|
host = resolveIngestHost(opts.region);
|
|
2028
2430
|
} catch (e) {
|
|
2029
|
-
console.error(
|
|
2431
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
2030
2432
|
if (opts.strict) process.exit(1);
|
|
2031
2433
|
return;
|
|
2032
2434
|
}
|
|
@@ -2034,7 +2436,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2034
2436
|
const originalSize = JSON.stringify(artifact).length;
|
|
2035
2437
|
const compressedSize = body.length;
|
|
2036
2438
|
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
2037
|
-
console.log(
|
|
2439
|
+
console.log(chalk7.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
2038
2440
|
try {
|
|
2039
2441
|
const { response, host: uploadedHost } = await uploadScanArtifact({
|
|
2040
2442
|
scheme: parsed.scheme,
|
|
@@ -2054,36 +2456,36 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2054
2456
|
const result = await response.json();
|
|
2055
2457
|
if (result.unchanged) {
|
|
2056
2458
|
console.log(
|
|
2057
|
-
|
|
2459
|
+
chalk7.green("\u2714") + ` Repository unchanged since ${result.lastScannedAt ?? "last scan"} \u2014 skipped upload (no credit used).`
|
|
2058
2460
|
);
|
|
2059
2461
|
if (result.previousIngestId) {
|
|
2060
2462
|
emitIngestIdLine(result.previousIngestId, { unchanged: true });
|
|
2061
2463
|
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.previousIngestId}`;
|
|
2062
|
-
console.log(
|
|
2464
|
+
console.log(chalk7.dim(` Previous report: ${dashUrl}`));
|
|
2063
2465
|
} else if (opts.strict) {
|
|
2064
|
-
console.error(
|
|
2466
|
+
console.error(chalk7.red("Repository unchanged but no previous ingest id returned."));
|
|
2065
2467
|
process.exit(1);
|
|
2066
2468
|
}
|
|
2067
2469
|
return;
|
|
2068
2470
|
}
|
|
2069
|
-
console.log(
|
|
2471
|
+
console.log(chalk7.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
2070
2472
|
if (result.ingestId) {
|
|
2071
2473
|
emitIngestIdLine(result.ingestId);
|
|
2072
2474
|
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
2073
2475
|
const CLEAR_LINE = process.platform === "win32" ? "\x1B[0G\x1B[2K" : "";
|
|
2074
2476
|
console.log("");
|
|
2075
|
-
console.log(CLEAR_LINE +
|
|
2477
|
+
console.log(CLEAR_LINE + chalk7.dim(" Processing continues in the background. Results available shortly."));
|
|
2076
2478
|
console.log("");
|
|
2077
|
-
console.log(CLEAR_LINE +
|
|
2078
|
-
console.log(CLEAR_LINE +
|
|
2079
|
-
console.log(CLEAR_LINE + " " +
|
|
2080
|
-
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"));
|
|
2081
2483
|
console.log("");
|
|
2082
2484
|
console.log("");
|
|
2083
2485
|
}
|
|
2084
2486
|
} catch (e) {
|
|
2085
2487
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2086
|
-
console.error(
|
|
2488
|
+
console.error(chalk7.red(`Upload failed: ${msg}`));
|
|
2087
2489
|
if (opts.strict) process.exit(1);
|
|
2088
2490
|
}
|
|
2089
2491
|
}
|
|
@@ -2095,15 +2497,15 @@ function reportStandards(rootDir) {
|
|
|
2095
2497
|
if (!loaded.policy) return;
|
|
2096
2498
|
const violations = checkStandards(loaded.policy, inventory(rootDir).records);
|
|
2097
2499
|
if (violations.length === 0) {
|
|
2098
|
-
console.log(
|
|
2500
|
+
console.log(chalk7.green("\u2714") + " Standards: no banned dependencies in use");
|
|
2099
2501
|
return;
|
|
2100
2502
|
}
|
|
2101
|
-
console.log(
|
|
2503
|
+
console.log(chalk7.red(`
|
|
2102
2504
|
\u26A0 Standards: ${violations.length} banned dependency(ies) in use`));
|
|
2103
2505
|
for (const v of violations) {
|
|
2104
|
-
const fix = v.use ?
|
|
2105
|
-
const why = v.reason ?
|
|
2106
|
-
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}`);
|
|
2107
2509
|
}
|
|
2108
2510
|
}
|
|
2109
2511
|
function parseNonNegativeNumber(value, label) {
|
|
@@ -2120,16 +2522,18 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2120
2522
|
collectExcludes,
|
|
2121
2523
|
[]
|
|
2122
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) => {
|
|
2123
|
-
const rootDir =
|
|
2525
|
+
const rootDir = path.resolve(targetPath);
|
|
2124
2526
|
if (!await pathExists$1(rootDir)) {
|
|
2125
|
-
console.error(
|
|
2527
|
+
console.error(chalk7.red(`Path does not exist: ${rootDir}`));
|
|
2126
2528
|
process.exit(1);
|
|
2127
2529
|
}
|
|
2128
2530
|
const hasDsn = !!resolveDsn(opts.dsn);
|
|
2129
2531
|
const willPush = !opts.offline && (opts.push || hasDsn);
|
|
2130
2532
|
const noLocalArtifacts = opts.localArtifacts === false;
|
|
2131
|
-
const isFirstRun = !await pathExists$1(
|
|
2533
|
+
const isFirstRun = !await pathExists$1(path.join(rootDir, ".vibgrate", "scan_result.json"));
|
|
2132
2534
|
let pinnedRegion;
|
|
2535
|
+
let planTier;
|
|
2536
|
+
let upgradeUrl;
|
|
2133
2537
|
if (willPush && hasDsn) {
|
|
2134
2538
|
const dsn = resolveDsn(opts.dsn);
|
|
2135
2539
|
const parsed = parseDsn(dsn);
|
|
@@ -2144,10 +2548,12 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2144
2548
|
vcsSha: fingerprint.vcsSha
|
|
2145
2549
|
});
|
|
2146
2550
|
pinnedRegion = preflight.region;
|
|
2551
|
+
planTier = preflight.plan?.tier;
|
|
2552
|
+
upgradeUrl = preflight.upgradeUrl ?? `https://${dashHostForIngestHost(preflight.ingestHost ?? ingestHost)}/${parsed.workspaceId}`;
|
|
2147
2553
|
if (preflight.vm && !preflight.vm.allowed) {
|
|
2148
|
-
console.error(
|
|
2554
|
+
console.error(chalk7.red(preflight.error ?? "VM meter usage exhausted"));
|
|
2149
2555
|
console.error(
|
|
2150
|
-
|
|
2556
|
+
chalk7.dim(
|
|
2151
2557
|
`VM minutes: ${preflight.vm.used}/${preflight.vm.limit} (${preflight.plan.label} plan) \u2014 enable overages or upgrade your plan.`
|
|
2152
2558
|
)
|
|
2153
2559
|
);
|
|
@@ -2155,25 +2561,25 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2155
2561
|
}
|
|
2156
2562
|
if (preflight.repositories && !preflight.repositories.allowed) {
|
|
2157
2563
|
console.error(
|
|
2158
|
-
|
|
2564
|
+
chalk7.red(
|
|
2159
2565
|
preflight.error ?? `Repository limit reached for the ${preflight.plan.label} plan \u2014 cannot scan a new repository.`
|
|
2160
2566
|
)
|
|
2161
2567
|
);
|
|
2162
2568
|
const max = preflight.repositories.max < 0 ? "unlimited" : String(preflight.repositories.max);
|
|
2163
2569
|
console.error(
|
|
2164
|
-
|
|
2570
|
+
chalk7.dim(
|
|
2165
2571
|
`Repositories: ${preflight.repositories.total}/${max} (${preflight.plan.label} plan) \u2014 archive a repository or upgrade your plan.`
|
|
2166
2572
|
)
|
|
2167
2573
|
);
|
|
2168
2574
|
if (preflight.upgradeUrl) {
|
|
2169
|
-
console.error(
|
|
2575
|
+
console.error(chalk7.dim(` Upgrade: ${preflight.upgradeUrl}`));
|
|
2170
2576
|
}
|
|
2171
2577
|
process.exit(1);
|
|
2172
2578
|
}
|
|
2173
2579
|
if (preflight.status === "error" || !preflight.scans.allowed) {
|
|
2174
|
-
console.error(
|
|
2580
|
+
console.error(chalk7.red(preflight.error ?? "Scan ingestion not allowed for this workspace."));
|
|
2175
2581
|
console.error(
|
|
2176
|
-
|
|
2582
|
+
chalk7.dim(
|
|
2177
2583
|
`Credits: ${preflight.scans.used}/${preflight.scans.limit} (${preflight.plan.label} plan)`
|
|
2178
2584
|
)
|
|
2179
2585
|
);
|
|
@@ -2181,27 +2587,27 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2181
2587
|
process.exit(1);
|
|
2182
2588
|
}
|
|
2183
2589
|
console.log(
|
|
2184
|
-
|
|
2590
|
+
chalk7.dim(
|
|
2185
2591
|
`Plan: ${preflight.plan.label} \u2014 scan credits ${preflight.scans.used}/${preflight.scans.limit} this month`
|
|
2186
2592
|
)
|
|
2187
2593
|
);
|
|
2188
2594
|
if (preflight.repository?.unchanged && !opts.force) {
|
|
2189
2595
|
console.log(
|
|
2190
|
-
|
|
2596
|
+
chalk7.green("\u2714") + ` Repository unchanged at ${preflight.repository.lastVcsSha?.slice(0, 7) ?? "same revision"} \u2014 skipping scan.`
|
|
2191
2597
|
);
|
|
2192
2598
|
if (preflight.repository.lastIngestId) {
|
|
2193
2599
|
emitIngestIdLine(preflight.repository.lastIngestId, { unchanged: true });
|
|
2194
2600
|
const dashUrl = `https://${dashHostForIngestHost(ingestHost)}/${parsed.workspaceId}/scan/${preflight.repository.lastIngestId}`;
|
|
2195
|
-
console.log(
|
|
2601
|
+
console.log(chalk7.dim(` Latest report: ${dashUrl}`));
|
|
2196
2602
|
} else if (opts.strict) {
|
|
2197
|
-
console.error(
|
|
2603
|
+
console.error(chalk7.red("Repository unchanged but no previous ingest id available."));
|
|
2198
2604
|
process.exit(1);
|
|
2199
2605
|
}
|
|
2200
2606
|
return;
|
|
2201
2607
|
}
|
|
2202
2608
|
} catch (e) {
|
|
2203
2609
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2204
|
-
console.error(
|
|
2610
|
+
console.error(chalk7.yellow(`Preflight check failed: ${msg}`));
|
|
2205
2611
|
if (opts.strict) process.exit(1);
|
|
2206
2612
|
}
|
|
2207
2613
|
}
|
|
@@ -2234,6 +2640,17 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2234
2640
|
repositoryName: opts.repositoryName?.trim() || void 0,
|
|
2235
2641
|
force: opts.force,
|
|
2236
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,
|
|
2237
2654
|
// Prefix for the upsell panel's `login → push` hint — `vg` when installed,
|
|
2238
2655
|
// `npx @vibgrate/cli` when the user ran via npx (where bare `vg` fails).
|
|
2239
2656
|
invocation: resolveCliInvocation()
|
|
@@ -2264,24 +2681,24 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2264
2681
|
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
2265
2682
|
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
2266
2683
|
if (opts.failOn === "error" && hasErrors) {
|
|
2267
|
-
console.error(
|
|
2684
|
+
console.error(chalk7.red(`
|
|
2268
2685
|
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
2269
2686
|
process.exit(2);
|
|
2270
2687
|
}
|
|
2271
2688
|
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
2272
|
-
console.error(
|
|
2689
|
+
console.error(chalk7.red(`
|
|
2273
2690
|
Failing: findings detected at warn level or above.`));
|
|
2274
2691
|
process.exit(2);
|
|
2275
2692
|
}
|
|
2276
2693
|
}
|
|
2277
2694
|
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
2278
|
-
console.error(
|
|
2695
|
+
console.error(chalk7.red(`
|
|
2279
2696
|
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
2280
2697
|
process.exit(2);
|
|
2281
2698
|
}
|
|
2282
2699
|
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
2283
2700
|
if (artifact.delta === void 0) {
|
|
2284
|
-
console.error(
|
|
2701
|
+
console.error(chalk7.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
2285
2702
|
process.exit(2);
|
|
2286
2703
|
}
|
|
2287
2704
|
if (artifact.delta > 0) {
|
|
@@ -2289,7 +2706,7 @@ Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget
|
|
|
2289
2706
|
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
2290
2707
|
const worseningPercent = artifact.delta / denominator * 100;
|
|
2291
2708
|
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
2292
|
-
console.error(
|
|
2709
|
+
console.error(chalk7.red(`
|
|
2293
2710
|
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
2294
2711
|
process.exit(2);
|
|
2295
2712
|
}
|
|
@@ -2304,22 +2721,853 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
|
|
|
2304
2721
|
printAiContextPrompt(detectedAssistant);
|
|
2305
2722
|
}
|
|
2306
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
|
+
}
|
|
2307
3555
|
function formatText(artifact) {
|
|
2308
3556
|
const lines = [];
|
|
2309
|
-
const teal2 =
|
|
2310
|
-
const mint2 =
|
|
3557
|
+
const teal2 = chalk7.hex("#3FB0A4");
|
|
3558
|
+
const mint2 = chalk7.hex("#4FE3C1");
|
|
2311
3559
|
lines.push("");
|
|
2312
3560
|
lines.push(" " + teal2("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u256E") + mint2("\u279C"));
|
|
2313
|
-
lines.push(" " +
|
|
2314
|
-
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}`));
|
|
2315
3563
|
lines.push(" " + teal2("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
2316
3564
|
lines.push("");
|
|
2317
3565
|
lines.push(...titleBox("Vibgrate Drift Report", teal2));
|
|
2318
3566
|
lines.push("");
|
|
2319
3567
|
for (const project of artifact.projects) {
|
|
2320
|
-
lines.push(
|
|
3568
|
+
lines.push(chalk7.bold(` \u2500\u2500 ${project.name} `) + chalk7.dim(`(${project.type}) ${project.path}`));
|
|
2321
3569
|
if (project.runtime) {
|
|
2322
|
-
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)");
|
|
2323
3571
|
lines.push(` Runtime: ${project.runtime}${behindStr}`);
|
|
2324
3572
|
}
|
|
2325
3573
|
if (project.targetFramework) {
|
|
@@ -2328,7 +3576,7 @@ function formatText(artifact) {
|
|
|
2328
3576
|
if (project.frameworks.length > 0) {
|
|
2329
3577
|
lines.push(" Frameworks:");
|
|
2330
3578
|
for (const fw of project.frameworks) {
|
|
2331
|
-
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");
|
|
2332
3580
|
lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
2333
3581
|
}
|
|
2334
3582
|
}
|
|
@@ -2336,13 +3584,13 @@ function formatText(artifact) {
|
|
|
2336
3584
|
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
2337
3585
|
if (total > 0) {
|
|
2338
3586
|
lines.push(" Dependencies:");
|
|
2339
|
-
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`)}`);
|
|
2340
3588
|
}
|
|
2341
3589
|
lines.push("");
|
|
2342
3590
|
}
|
|
2343
3591
|
if (artifact.delta !== void 0) {
|
|
2344
|
-
const deltaStr = artifact.delta > 0 ?
|
|
2345
|
-
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)");
|
|
2346
3594
|
lines.push("");
|
|
2347
3595
|
}
|
|
2348
3596
|
if (artifact.extended) {
|
|
@@ -2353,15 +3601,15 @@ function formatText(artifact) {
|
|
|
2353
3601
|
const warnings = artifact.findings.filter((f) => f.level === "warning");
|
|
2354
3602
|
const notes = artifact.findings.filter((f) => f.level === "note");
|
|
2355
3603
|
const summary = [
|
|
2356
|
-
errors.length > 0 ?
|
|
2357
|
-
warnings.length > 0 ?
|
|
2358
|
-
notes.length > 0 ?
|
|
2359
|
-
].filter(Boolean).join(
|
|
2360
|
-
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})`));
|
|
2361
3609
|
for (const f of artifact.findings) {
|
|
2362
|
-
const icon = f.level === "error" ?
|
|
3610
|
+
const icon = f.level === "error" ? chalk7.red("\u2716") : f.level === "warning" ? chalk7.yellow("\u26A0") : chalk7.blue("\u2139");
|
|
2363
3611
|
lines.push(` ${icon} ${f.message}`);
|
|
2364
|
-
lines.push(
|
|
3612
|
+
lines.push(chalk7.dim(` ${f.ruleId} in ${f.location}`));
|
|
2365
3613
|
}
|
|
2366
3614
|
lines.push("");
|
|
2367
3615
|
}
|
|
@@ -2371,10 +3619,10 @@ function formatText(artifact) {
|
|
|
2371
3619
|
lines.push("");
|
|
2372
3620
|
for (let i = 0; i < actions.length; i++) {
|
|
2373
3621
|
const a = actions[i];
|
|
2374
|
-
const num =
|
|
2375
|
-
lines.push(`${num} ${
|
|
2376
|
-
lines.push(
|
|
2377
|
-
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)}`);
|
|
2378
3626
|
lines.push("");
|
|
2379
3627
|
}
|
|
2380
3628
|
}
|
|
@@ -2386,30 +3634,30 @@ function formatText(artifact) {
|
|
|
2386
3634
|
lines.push("");
|
|
2387
3635
|
for (const solution of artifact.solutions) {
|
|
2388
3636
|
const solScore = solution.drift?.score;
|
|
2389
|
-
const color = typeof solScore === "number" ? solScore <= 30 ?
|
|
2390
|
-
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")}`);
|
|
2391
3639
|
}
|
|
2392
3640
|
lines.push("");
|
|
2393
3641
|
}
|
|
2394
|
-
const scoreColor = artifact.drift.score <= 30 ?
|
|
2395
|
-
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"));
|
|
2396
3644
|
lines.push("");
|
|
2397
|
-
lines.push(
|
|
2398
|
-
lines.push(
|
|
2399
|
-
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}`);
|
|
2400
3648
|
if (artifact.vcs) {
|
|
2401
3649
|
const vcsParts = [artifact.vcs.type];
|
|
2402
3650
|
if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
|
|
2403
|
-
if (artifact.vcs.shortSha) vcsParts.push(
|
|
2404
|
-
lines.push(
|
|
3651
|
+
if (artifact.vcs.shortSha) vcsParts.push(chalk7.dim(artifact.vcs.shortSha));
|
|
3652
|
+
lines.push(chalk7.bold(" VCS: ") + vcsParts.join(" "));
|
|
2405
3653
|
}
|
|
2406
3654
|
lines.push("");
|
|
2407
3655
|
const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
|
|
2408
|
-
lines.push(" " +
|
|
2409
|
-
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) :
|
|
2410
|
-
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) :
|
|
2411
|
-
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) :
|
|
2412
|
-
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")}`);
|
|
2413
3661
|
lines.push("");
|
|
2414
3662
|
const scannedParts = [`Scanned at ${artifact.timestamp}`];
|
|
2415
3663
|
if (artifact.durationMs !== void 0) {
|
|
@@ -2423,18 +3671,18 @@ function formatText(artifact) {
|
|
|
2423
3671
|
scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
|
|
2424
3672
|
scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
2425
3673
|
}
|
|
2426
|
-
lines.push(
|
|
3674
|
+
lines.push(chalk7.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
2427
3675
|
lines.push("");
|
|
2428
3676
|
return lines.join("\n");
|
|
2429
3677
|
}
|
|
2430
3678
|
function riskBadge(level) {
|
|
2431
3679
|
switch (level) {
|
|
2432
3680
|
case "low":
|
|
2433
|
-
return
|
|
3681
|
+
return chalk7.bgGreen.black(" LOW ");
|
|
2434
3682
|
case "moderate":
|
|
2435
|
-
return
|
|
3683
|
+
return chalk7.bgYellow.black(" MODERATE ");
|
|
2436
3684
|
case "high":
|
|
2437
|
-
return
|
|
3685
|
+
return chalk7.bgRed.white(" HIGH ");
|
|
2438
3686
|
default:
|
|
2439
3687
|
return level;
|
|
2440
3688
|
}
|
|
@@ -2469,11 +3717,11 @@ function formatExtended(ext) {
|
|
|
2469
3717
|
const inv = ext.toolingInventory;
|
|
2470
3718
|
const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
|
|
2471
3719
|
if (categories.length > 0) {
|
|
2472
|
-
lines.push(
|
|
3720
|
+
lines.push(chalk7.bold.underline(" Tech Stack"));
|
|
2473
3721
|
for (const [cat, items] of categories) {
|
|
2474
3722
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2475
|
-
const names = items.map((i) =>
|
|
2476
|
-
lines.push(` ${
|
|
3723
|
+
const names = items.map((i) => chalk7.white(i.name)).join(chalk7.dim(", "));
|
|
3724
|
+
lines.push(` ${chalk7.cyan(label)}: ${names}`);
|
|
2477
3725
|
}
|
|
2478
3726
|
lines.push("");
|
|
2479
3727
|
}
|
|
@@ -2482,14 +3730,14 @@ function formatExtended(ext) {
|
|
|
2482
3730
|
const svc = ext.serviceDependencies;
|
|
2483
3731
|
const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
|
|
2484
3732
|
if (categories.length > 0) {
|
|
2485
|
-
lines.push(
|
|
3733
|
+
lines.push(chalk7.bold.underline(" Services & Integrations"));
|
|
2486
3734
|
for (const [cat, items] of categories) {
|
|
2487
3735
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2488
3736
|
const names = items.map((i) => {
|
|
2489
|
-
const ver = i.version ?
|
|
2490
|
-
return
|
|
2491
|
-
}).join(
|
|
2492
|
-
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}`);
|
|
2493
3741
|
}
|
|
2494
3742
|
lines.push("");
|
|
2495
3743
|
}
|
|
@@ -2497,19 +3745,19 @@ function formatExtended(ext) {
|
|
|
2497
3745
|
if (ext.breakingChangeExposure) {
|
|
2498
3746
|
const bc = ext.breakingChangeExposure;
|
|
2499
3747
|
if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
|
|
2500
|
-
lines.push(
|
|
2501
|
-
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;
|
|
2502
3750
|
lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
|
|
2503
3751
|
if (bc.deprecatedPackages.length > 0) {
|
|
2504
|
-
lines.push(` ${
|
|
3752
|
+
lines.push(` ${chalk7.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk7.dim(p)).join(", ")}`);
|
|
2505
3753
|
}
|
|
2506
3754
|
if (bc.legacyPolyfills.length > 0) {
|
|
2507
|
-
lines.push(` ${
|
|
3755
|
+
lines.push(` ${chalk7.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk7.dim(p)).join(", ")}`);
|
|
2508
3756
|
}
|
|
2509
3757
|
if (bc.peerConflictsDetected) {
|
|
2510
|
-
lines.push(` ${
|
|
3758
|
+
lines.push(` ${chalk7.red("\u26A0")} Peer dependency conflicts detected`);
|
|
2511
3759
|
}
|
|
2512
|
-
lines.push(` Recommendation: ${
|
|
3760
|
+
lines.push(` Recommendation: ${chalk7.bold(bc.overallRecommendation)}`);
|
|
2513
3761
|
const projectsWithPlans = bc.projectIntelligence.filter((p) => p.packages.length > 0).slice(0, 3);
|
|
2514
3762
|
if (projectsWithPlans.length > 0) {
|
|
2515
3763
|
lines.push(" Major Upgrade Intelligence:");
|
|
@@ -2525,21 +3773,21 @@ function formatExtended(ext) {
|
|
|
2525
3773
|
}
|
|
2526
3774
|
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
2527
3775
|
const ts = ext.tsModernity;
|
|
2528
|
-
lines.push(
|
|
3776
|
+
lines.push(chalk7.bold.underline(" TypeScript"));
|
|
2529
3777
|
const parts = [];
|
|
2530
3778
|
parts.push(`v${ts.typescriptVersion}`);
|
|
2531
|
-
if (ts.strict === true) parts.push(
|
|
2532
|
-
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"));
|
|
2533
3781
|
if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
|
|
2534
3782
|
if (ts.target) parts.push(`target: ${ts.target}`);
|
|
2535
|
-
lines.push(` ${parts.join(
|
|
3783
|
+
lines.push(` ${parts.join(chalk7.dim(" \xB7 "))}`);
|
|
2536
3784
|
lines.push("");
|
|
2537
3785
|
}
|
|
2538
3786
|
if (ext.buildDeploy) {
|
|
2539
3787
|
const bd = ext.buildDeploy;
|
|
2540
3788
|
const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
|
|
2541
3789
|
if (hasSomething) {
|
|
2542
|
-
lines.push(
|
|
3790
|
+
lines.push(chalk7.bold.underline(" Build & Deploy"));
|
|
2543
3791
|
if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
|
|
2544
3792
|
if (bd.docker.dockerfileCount > 0) {
|
|
2545
3793
|
lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
|
|
@@ -2552,42 +3800,42 @@ function formatExtended(ext) {
|
|
|
2552
3800
|
}
|
|
2553
3801
|
if (ext.uiPurpose) {
|
|
2554
3802
|
const up = ext.uiPurpose;
|
|
2555
|
-
lines.push(
|
|
2556
|
-
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") :
|
|
2557
|
-
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)`) : ""}`);
|
|
2558
3806
|
const top = up.topEvidence.slice(0, 8);
|
|
2559
3807
|
if (top.length > 0) {
|
|
2560
3808
|
lines.push(" Top Signals:");
|
|
2561
3809
|
for (const item of top) {
|
|
2562
|
-
lines.push(` - [${item.kind}] ${item.value} ${
|
|
3810
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk7.dim(`(${item.file})`)}`);
|
|
2563
3811
|
}
|
|
2564
3812
|
}
|
|
2565
3813
|
if (up.unknownSignals.length > 0) {
|
|
2566
3814
|
lines.push(" Unknowns:");
|
|
2567
3815
|
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
2568
|
-
lines.push(` - ${
|
|
3816
|
+
lines.push(` - ${chalk7.yellow(u)}`);
|
|
2569
3817
|
}
|
|
2570
3818
|
}
|
|
2571
3819
|
lines.push("");
|
|
2572
3820
|
}
|
|
2573
3821
|
if (ext.securityPosture) {
|
|
2574
3822
|
const sec = ext.securityPosture;
|
|
2575
|
-
lines.push(
|
|
3823
|
+
lines.push(chalk7.bold.underline(" Security Posture"));
|
|
2576
3824
|
const checks = [];
|
|
2577
|
-
checks.push(sec.lockfilePresent ?
|
|
2578
|
-
checks.push(sec.gitignoreCoversEnv ?
|
|
2579
|
-
checks.push(sec.gitignoreCoversNodeModules ?
|
|
2580
|
-
if (sec.multipleLockfileTypes) checks.push(
|
|
2581
|
-
if (sec.envFilesTracked) checks.push(
|
|
2582
|
-
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 "))}`);
|
|
2583
3831
|
lines.push("");
|
|
2584
3832
|
}
|
|
2585
3833
|
if (ext.platformMatrix) {
|
|
2586
3834
|
const pm = ext.platformMatrix;
|
|
2587
3835
|
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
2588
|
-
lines.push(
|
|
3836
|
+
lines.push(chalk7.bold.underline(" Platform"));
|
|
2589
3837
|
if (pm.nativeModules.length > 0) {
|
|
2590
|
-
lines.push(` Native modules: ${pm.nativeModules.map((m) =>
|
|
3838
|
+
lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk7.dim(m)).join(", ")}`);
|
|
2591
3839
|
}
|
|
2592
3840
|
if (pm.osAssumptions.length > 0) {
|
|
2593
3841
|
lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
|
|
@@ -2597,25 +3845,25 @@ function formatExtended(ext) {
|
|
|
2597
3845
|
}
|
|
2598
3846
|
if (ext.codeQuality) {
|
|
2599
3847
|
const cq = ext.codeQuality;
|
|
2600
|
-
lines.push(
|
|
2601
|
-
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`);
|
|
2602
3850
|
lines.push(` Max nesting: ${cq.maxNestingDepth} \xB7 Circular deps: ${cq.circularDependencies} \xB7 Dead code: ${cq.deadCodePercent}%`);
|
|
2603
3851
|
if (cq.godFiles.length > 0) {
|
|
2604
3852
|
const preview = cq.godFiles.slice(0, 3).map((f) => `${f.path} (${f.lines} lines)`).join(", ");
|
|
2605
|
-
lines.push(` ${
|
|
3853
|
+
lines.push(` ${chalk7.yellow("God files")}: ${preview}`);
|
|
2606
3854
|
}
|
|
2607
3855
|
lines.push("");
|
|
2608
3856
|
}
|
|
2609
3857
|
if (ext.dependencyGraph) {
|
|
2610
3858
|
const dg = ext.dependencyGraph;
|
|
2611
3859
|
if (dg.lockfileType) {
|
|
2612
|
-
lines.push(
|
|
2613
|
-
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`);
|
|
2614
3862
|
if (dg.duplicatedPackages.length > 0) {
|
|
2615
|
-
lines.push(` ${
|
|
3863
|
+
lines.push(` ${chalk7.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
|
|
2616
3864
|
}
|
|
2617
3865
|
if (dg.phantomDependencies.length > 0) {
|
|
2618
|
-
lines.push(` ${
|
|
3866
|
+
lines.push(` ${chalk7.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
|
|
2619
3867
|
}
|
|
2620
3868
|
lines.push("");
|
|
2621
3869
|
}
|
|
@@ -2626,13 +3874,13 @@ function formatArchitectureDiagram(arch) {
|
|
|
2626
3874
|
const lines = [];
|
|
2627
3875
|
lines.push(...titleBox("Architecture Layers"));
|
|
2628
3876
|
lines.push("");
|
|
2629
|
-
lines.push(
|
|
2630
|
-
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)`) : ""));
|
|
2631
3879
|
lines.push("");
|
|
2632
3880
|
if (arch.layers.length > 0) {
|
|
2633
3881
|
for (const layer of arch.layers) {
|
|
2634
|
-
const risk = layer.riskLevel === "none" ?
|
|
2635
|
-
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}`);
|
|
2636
3884
|
}
|
|
2637
3885
|
lines.push("");
|
|
2638
3886
|
}
|
|
@@ -2894,7 +4142,7 @@ function formatMarkdown(artifact) {
|
|
|
2894
4142
|
lines.push("");
|
|
2895
4143
|
lines.push(`| Metric | Value |`);
|
|
2896
4144
|
lines.push(`|--------|-------|`);
|
|
2897
|
-
lines.push(`| **
|
|
4145
|
+
lines.push(`| **DriftScore** | ${artifact.drift.score}/100 _(lower is better; 0 = no drift)_ |`);
|
|
2898
4146
|
lines.push(`| **Risk Level** | ${artifact.drift.riskLevel.toUpperCase()} |`);
|
|
2899
4147
|
lines.push(`| **Projects** | ${artifact.projects.length} |`);
|
|
2900
4148
|
const scannedMeta = [artifact.timestamp];
|
|
@@ -3001,10 +4249,10 @@ function formatMarkdown(artifact) {
|
|
|
3001
4249
|
|
|
3002
4250
|
// src/reporting/commands/report.ts
|
|
3003
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) => {
|
|
3004
|
-
const artifactPath =
|
|
4252
|
+
const artifactPath = path.resolve(opts.in);
|
|
3005
4253
|
if (!await pathExists(artifactPath)) {
|
|
3006
|
-
console.error(
|
|
3007
|
-
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.'));
|
|
3008
4256
|
process.exit(1);
|
|
3009
4257
|
}
|
|
3010
4258
|
const artifact = await readJsonFile(artifactPath);
|
|
@@ -3047,10 +4295,10 @@ function openUrl(url) {
|
|
|
3047
4295
|
}
|
|
3048
4296
|
|
|
3049
4297
|
// src/reporting/commands/login.ts
|
|
3050
|
-
var delay = (ms) => new Promise((
|
|
4298
|
+
var delay = (ms) => new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
3051
4299
|
function isInside(dir, file) {
|
|
3052
|
-
const
|
|
3053
|
-
return !!
|
|
4300
|
+
const rel3 = path.relative(dir, file);
|
|
4301
|
+
return !!rel3 && !rel3.startsWith("..") && !path.isAbsolute(rel3);
|
|
3054
4302
|
}
|
|
3055
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(
|
|
3056
4304
|
"--local",
|
|
@@ -3060,7 +4308,7 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3060
4308
|
try {
|
|
3061
4309
|
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
3062
4310
|
} catch (e) {
|
|
3063
|
-
console.error(
|
|
4311
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
3064
4312
|
process.exit(1);
|
|
3065
4313
|
}
|
|
3066
4314
|
const base = `https://${ingestHost}/v1/auth/device`;
|
|
@@ -3072,28 +4320,28 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3072
4320
|
body: "{}"
|
|
3073
4321
|
});
|
|
3074
4322
|
if (!res.ok) {
|
|
3075
|
-
console.error(
|
|
4323
|
+
console.error(chalk7.red(`Failed to start login (HTTP ${res.status}).`));
|
|
3076
4324
|
process.exit(1);
|
|
3077
4325
|
}
|
|
3078
4326
|
start = await res.json();
|
|
3079
4327
|
} catch (e) {
|
|
3080
|
-
console.error(
|
|
4328
|
+
console.error(chalk7.red(`Could not reach ${ingestHost}: ${e instanceof Error ? e.message : String(e)}`));
|
|
3081
4329
|
process.exit(1);
|
|
3082
4330
|
}
|
|
3083
4331
|
console.log("");
|
|
3084
4332
|
console.log("To finish signing in, open this URL and approve the request:");
|
|
3085
4333
|
console.log("");
|
|
3086
|
-
console.log(" " +
|
|
4334
|
+
console.log(" " + chalk7.cyan(start.verificationUri));
|
|
3087
4335
|
console.log("");
|
|
3088
|
-
console.log(" Your code: " +
|
|
4336
|
+
console.log(" Your code: " + chalk7.bold(start.userCode));
|
|
3089
4337
|
console.log("");
|
|
3090
4338
|
if (opts.browser) {
|
|
3091
4339
|
const opened = openUrl(start.verificationUriComplete);
|
|
3092
4340
|
if (opened) {
|
|
3093
|
-
console.log(
|
|
4341
|
+
console.log(chalk7.dim("Opening your browser\u2026 (if it does not open, use the URL above)"));
|
|
3094
4342
|
}
|
|
3095
4343
|
}
|
|
3096
|
-
console.log(
|
|
4344
|
+
console.log(chalk7.dim("Waiting for approval\u2026"));
|
|
3097
4345
|
const intervalMs = Math.max(2, start.interval || 5) * 1e3;
|
|
3098
4346
|
const deadline = Date.now() + (start.expiresIn || 900) * 1e3;
|
|
3099
4347
|
while (Date.now() < deadline) {
|
|
@@ -3121,7 +4369,7 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3121
4369
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3122
4370
|
};
|
|
3123
4371
|
} else {
|
|
3124
|
-
console.log(
|
|
4372
|
+
console.log(chalk7.dim("Setting up your workspace\u2026"));
|
|
3125
4373
|
try {
|
|
3126
4374
|
const provisioned = await createWorkspaceDsn({
|
|
3127
4375
|
region: opts.region,
|
|
@@ -3136,10 +4384,10 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3136
4384
|
};
|
|
3137
4385
|
} catch (e) {
|
|
3138
4386
|
console.error(
|
|
3139
|
-
|
|
4387
|
+
chalk7.red("\u2716 Signed in, but workspace setup failed: ") + (e instanceof Error ? e.message : String(e))
|
|
3140
4388
|
);
|
|
3141
4389
|
console.error(
|
|
3142
|
-
|
|
4390
|
+
chalk7.dim(' Finish setup with "vibgrate dsn create --workspace new".')
|
|
3143
4391
|
);
|
|
3144
4392
|
process.exit(1);
|
|
3145
4393
|
}
|
|
@@ -3148,40 +4396,40 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3148
4396
|
writeStoredCredentials(creds, storeOpts);
|
|
3149
4397
|
const credsFile = credentialsPath(storeOpts);
|
|
3150
4398
|
console.log("");
|
|
3151
|
-
console.log(
|
|
4399
|
+
console.log(chalk7.green("\u2714") + " Logged in.");
|
|
3152
4400
|
if (creds.workspaceId) {
|
|
3153
|
-
console.log(" Workspace: " +
|
|
4401
|
+
console.log(" Workspace: " + chalk7.bold(creds.workspaceId));
|
|
3154
4402
|
}
|
|
3155
|
-
console.log(
|
|
4403
|
+
console.log(chalk7.dim(` Credentials saved to ${credsFile}`));
|
|
3156
4404
|
try {
|
|
3157
4405
|
const root = findGitRoot();
|
|
3158
4406
|
if (root && isInside(root, credsFile)) {
|
|
3159
4407
|
const res = ensureGitignored(gitignoreEntryForCredentials(root, credsFile), root);
|
|
3160
4408
|
if (res.status === "created") {
|
|
3161
|
-
console.log(
|
|
4409
|
+
console.log(chalk7.dim(` Created .gitignore and ignored ${res.entry}`));
|
|
3162
4410
|
} else if (res.status === "added") {
|
|
3163
|
-
console.log(
|
|
4411
|
+
console.log(chalk7.dim(` Added ${res.entry} to .gitignore`));
|
|
3164
4412
|
}
|
|
3165
4413
|
}
|
|
3166
4414
|
} catch {
|
|
3167
4415
|
}
|
|
3168
|
-
console.log(
|
|
4416
|
+
console.log(chalk7.dim(` You can now run "${resolveCliInvocation()} scan --push".`));
|
|
3169
4417
|
return;
|
|
3170
4418
|
}
|
|
3171
4419
|
if (token.status === "access_denied") {
|
|
3172
|
-
console.error(
|
|
4420
|
+
console.error(chalk7.red("\u2716 Login was denied in the browser."));
|
|
3173
4421
|
process.exit(1);
|
|
3174
4422
|
}
|
|
3175
4423
|
if (token.status === "expired" || token.status === "invalid") {
|
|
3176
|
-
console.error(
|
|
4424
|
+
console.error(chalk7.red('\u2716 Login request expired. Run "vibgrate login" again.'));
|
|
3177
4425
|
process.exit(1);
|
|
3178
4426
|
}
|
|
3179
4427
|
if (token.status === "error") {
|
|
3180
|
-
console.error(
|
|
4428
|
+
console.error(chalk7.red(`\u2716 ${token.error ?? "Login failed."}`));
|
|
3181
4429
|
process.exit(1);
|
|
3182
4430
|
}
|
|
3183
4431
|
}
|
|
3184
|
-
console.error(
|
|
4432
|
+
console.error(chalk7.red('\u2716 Timed out waiting for approval. Run "vibgrate login" again.'));
|
|
3185
4433
|
process.exit(1);
|
|
3186
4434
|
});
|
|
3187
4435
|
|
|
@@ -3202,23 +4450,23 @@ function unsetEnvCommand(varName, env = {}) {
|
|
|
3202
4450
|
var logoutCommand = new Command("logout").description("Clear stored Vibgrate login credentials").action(() => {
|
|
3203
4451
|
const cleared = clearStoredCredentials();
|
|
3204
4452
|
if (cleared) {
|
|
3205
|
-
console.log(
|
|
4453
|
+
console.log(chalk7.green("\u2714") + " Logged out. Stored credentials removed.");
|
|
3206
4454
|
} else {
|
|
3207
|
-
console.log(
|
|
4455
|
+
console.log(chalk7.dim(`No stored credentials found at ${credentialsPath()}.`));
|
|
3208
4456
|
}
|
|
3209
4457
|
if (process.env.VIBGRATE_DSN) {
|
|
3210
4458
|
console.log("");
|
|
3211
4459
|
console.log(
|
|
3212
|
-
|
|
4460
|
+
chalk7.yellow("\u26A0") + " VIBGRATE_DSN is still set in your environment, so commands will keep using it."
|
|
3213
4461
|
);
|
|
3214
4462
|
console.log(
|
|
3215
|
-
|
|
4463
|
+
chalk7.dim(` Unset it to fully sign out: ${unsetEnvCommand("VIBGRATE_DSN")}`)
|
|
3216
4464
|
);
|
|
3217
4465
|
}
|
|
3218
4466
|
});
|
|
3219
4467
|
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
3220
|
-
var CACHE_DIR =
|
|
3221
|
-
var CACHE_FILE =
|
|
4468
|
+
var CACHE_DIR = path.join(os.homedir(), ".vibgrate");
|
|
4469
|
+
var CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
|
|
3222
4470
|
async function fetchLatestVersion() {
|
|
3223
4471
|
try {
|
|
3224
4472
|
const controller = new AbortController();
|
|
@@ -3245,8 +4493,8 @@ async function fetchLatestVersion() {
|
|
|
3245
4493
|
}
|
|
3246
4494
|
async function writeCache(data) {
|
|
3247
4495
|
try {
|
|
3248
|
-
await
|
|
3249
|
-
await
|
|
4496
|
+
await fs14.mkdir(CACHE_DIR, { recursive: true });
|
|
4497
|
+
await fs14.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
3250
4498
|
} catch {
|
|
3251
4499
|
}
|
|
3252
4500
|
}
|
|
@@ -3279,9 +4527,9 @@ function getGlobalUpdateCommand(pm, pkg, version) {
|
|
|
3279
4527
|
}
|
|
3280
4528
|
}
|
|
3281
4529
|
async function detectPackageManager(cwd) {
|
|
3282
|
-
if (await pathExists(
|
|
3283
|
-
if (await pathExists(
|
|
3284
|
-
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";
|
|
3285
4533
|
return "npm";
|
|
3286
4534
|
}
|
|
3287
4535
|
function getInstallCommand(pm, pkg, version, isDev) {
|
|
@@ -3300,7 +4548,7 @@ function getInstallCommand(pm, pkg, version, isDev) {
|
|
|
3300
4548
|
}
|
|
3301
4549
|
async function isDevDependency(cwd) {
|
|
3302
4550
|
try {
|
|
3303
|
-
const pkgPath =
|
|
4551
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
3304
4552
|
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
3305
4553
|
const pkg = JSON.parse(raw);
|
|
3306
4554
|
return Boolean(pkg.devDependencies?.["@vibgrate/cli"]);
|
|
@@ -3309,21 +4557,21 @@ async function isDevDependency(cwd) {
|
|
|
3309
4557
|
}
|
|
3310
4558
|
}
|
|
3311
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) => {
|
|
3312
|
-
console.log(
|
|
3313
|
-
console.log(
|
|
4560
|
+
console.log(chalk7.dim(`Current version: ${VERSION}`));
|
|
4561
|
+
console.log(chalk7.dim("Checking npm registry..."));
|
|
3314
4562
|
const latest = await fetchLatestVersion();
|
|
3315
4563
|
if (!latest) {
|
|
3316
|
-
console.error(
|
|
4564
|
+
console.error(chalk7.red("Could not reach the npm registry. Check your network connection."));
|
|
3317
4565
|
process.exit(1);
|
|
3318
4566
|
}
|
|
3319
4567
|
const semver2 = await import('semver');
|
|
3320
4568
|
if (!semver2.gt(latest, VERSION)) {
|
|
3321
|
-
console.log(
|
|
4569
|
+
console.log(chalk7.green("\u2714") + ` You are on the latest version (${VERSION}).`);
|
|
3322
4570
|
return;
|
|
3323
4571
|
}
|
|
3324
|
-
console.log(
|
|
4572
|
+
console.log(chalk7.yellow(`Update available: ${VERSION} \u2192 ${latest}`));
|
|
3325
4573
|
if (opts.check) {
|
|
3326
|
-
console.log(
|
|
4574
|
+
console.log(chalk7.dim('Run "vibgrate update" to install.'));
|
|
3327
4575
|
return;
|
|
3328
4576
|
}
|
|
3329
4577
|
const cwd = process.cwd();
|
|
@@ -3333,17 +4581,17 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
3333
4581
|
let cmd;
|
|
3334
4582
|
if (isGlobal) {
|
|
3335
4583
|
cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
|
|
3336
|
-
console.log(
|
|
4584
|
+
console.log(chalk7.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
3337
4585
|
} else {
|
|
3338
4586
|
const isDev = await isDevDependency(cwd);
|
|
3339
4587
|
cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev);
|
|
3340
|
-
console.log(
|
|
4588
|
+
console.log(chalk7.dim(`Using ${pm}: ${cmd}`));
|
|
3341
4589
|
}
|
|
3342
4590
|
try {
|
|
3343
4591
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
3344
|
-
console.log(
|
|
4592
|
+
console.log(chalk7.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
|
|
3345
4593
|
} catch {
|
|
3346
|
-
console.error(
|
|
4594
|
+
console.error(chalk7.red(`Update failed. Run manually: ${cmd}`));
|
|
3347
4595
|
process.exit(1);
|
|
3348
4596
|
}
|
|
3349
4597
|
});
|
|
@@ -3436,9 +4684,9 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3436
4684
|
try {
|
|
3437
4685
|
const statements = [];
|
|
3438
4686
|
if (opts.from) {
|
|
3439
|
-
const fromPath =
|
|
4687
|
+
const fromPath = path.resolve(opts.from);
|
|
3440
4688
|
if (!await pathExists(fromPath)) {
|
|
3441
|
-
process.stderr.write(
|
|
4689
|
+
process.stderr.write(chalk7.red(`Statements file not found: ${fromPath}
|
|
3442
4690
|
`));
|
|
3443
4691
|
process.exit(1);
|
|
3444
4692
|
}
|
|
@@ -3454,9 +4702,9 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3454
4702
|
});
|
|
3455
4703
|
const json2 = JSON.stringify(doc, null, 2);
|
|
3456
4704
|
if (opts.out) {
|
|
3457
|
-
await writeTextFile(
|
|
4705
|
+
await writeTextFile(path.resolve(opts.out), json2 + "\n");
|
|
3458
4706
|
process.stderr.write(
|
|
3459
|
-
|
|
4707
|
+
chalk7.green("\u2714") + ` Wrote OpenVEX document (${doc.statements.length} statement(s)) to ${path.resolve(opts.out)}
|
|
3460
4708
|
`
|
|
3461
4709
|
);
|
|
3462
4710
|
} else {
|
|
@@ -3464,7 +4712,7 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3464
4712
|
}
|
|
3465
4713
|
} catch (err) {
|
|
3466
4714
|
if (err instanceof VexValidationError) {
|
|
3467
|
-
process.stderr.write(
|
|
4715
|
+
process.stderr.write(chalk7.red(`VEX validation error: ${err.message}
|
|
3468
4716
|
`));
|
|
3469
4717
|
process.exit(1);
|
|
3470
4718
|
}
|
|
@@ -3639,9 +4887,9 @@ function formatDeltaText(base, current) {
|
|
|
3639
4887
|
return lines.join("\n");
|
|
3640
4888
|
}
|
|
3641
4889
|
async function readArtifactOrExit(filePath) {
|
|
3642
|
-
const absolutePath =
|
|
4890
|
+
const absolutePath = path.resolve(filePath);
|
|
3643
4891
|
if (!await pathExists(absolutePath)) {
|
|
3644
|
-
console.error(
|
|
4892
|
+
console.error(chalk7.red(`Artifact not found: ${absolutePath}`));
|
|
3645
4893
|
process.exit(1);
|
|
3646
4894
|
}
|
|
3647
4895
|
return readJsonFile(absolutePath);
|
|
@@ -3650,14 +4898,14 @@ var exportCommand = new Command("export").description("Export scan artifact as S
|
|
|
3650
4898
|
const artifact = await readArtifactOrExit(opts.in);
|
|
3651
4899
|
const format = opts.format.toLowerCase();
|
|
3652
4900
|
if (format !== "cyclonedx" && format !== "spdx") {
|
|
3653
|
-
console.error(
|
|
4901
|
+
console.error(chalk7.red("Invalid SBOM format. Use cyclonedx or spdx."));
|
|
3654
4902
|
process.exit(1);
|
|
3655
4903
|
}
|
|
3656
4904
|
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
3657
4905
|
const body = JSON.stringify(sbom, null, 2);
|
|
3658
4906
|
if (opts.out) {
|
|
3659
|
-
await writeTextFile(
|
|
3660
|
-
console.log(
|
|
4907
|
+
await writeTextFile(path.resolve(opts.out), body);
|
|
4908
|
+
console.log(chalk7.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
3661
4909
|
} else {
|
|
3662
4910
|
console.log(body);
|
|
3663
4911
|
}
|
|
@@ -3667,8 +4915,8 @@ var deltaCommand = new Command("delta").description("Show SBOM delta between two
|
|
|
3667
4915
|
const current = await readArtifactOrExit(opts.to);
|
|
3668
4916
|
const report = formatDeltaText(base, current);
|
|
3669
4917
|
if (opts.out) {
|
|
3670
|
-
await writeTextFile(
|
|
3671
|
-
console.log(
|
|
4918
|
+
await writeTextFile(path.resolve(opts.out), report);
|
|
4919
|
+
console.log(chalk7.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
3672
4920
|
} else {
|
|
3673
4921
|
console.log(report);
|
|
3674
4922
|
}
|
|
@@ -3679,13 +4927,13 @@ var sbomCommand = new Command("sbom").description("Supply-chain evidence: SBOM e
|
|
|
3679
4927
|
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
3680
4928
|
"build",
|
|
3681
4929
|
"status",
|
|
3682
|
-
"verify",
|
|
3683
4930
|
"ask",
|
|
3684
4931
|
"embed",
|
|
3685
4932
|
"show",
|
|
3686
4933
|
"path",
|
|
3687
4934
|
"tree",
|
|
3688
4935
|
"impact",
|
|
4936
|
+
"unknowns",
|
|
3689
4937
|
"tests",
|
|
3690
4938
|
"facts",
|
|
3691
4939
|
"guide",
|
|
@@ -3711,6 +4959,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
3711
4959
|
// Drift-reporting verbs (merged from the Vibgrate CLI).
|
|
3712
4960
|
"init",
|
|
3713
4961
|
"scan",
|
|
4962
|
+
"fix",
|
|
3714
4963
|
"baseline",
|
|
3715
4964
|
"report",
|
|
3716
4965
|
"login",
|
|
@@ -3733,7 +4982,6 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3733
4982
|
});
|
|
3734
4983
|
registerBuild(program);
|
|
3735
4984
|
registerStatus(program);
|
|
3736
|
-
registerVerify(program);
|
|
3737
4985
|
registerAsk(program);
|
|
3738
4986
|
registerEmbed(program);
|
|
3739
4987
|
registerShow(program);
|
|
@@ -3741,6 +4989,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3741
4989
|
registerTree(program);
|
|
3742
4990
|
registerInsights(program);
|
|
3743
4991
|
registerImpact(program);
|
|
4992
|
+
registerUnknowns(program);
|
|
3744
4993
|
registerServe(program);
|
|
3745
4994
|
registerInstall(program);
|
|
3746
4995
|
registerShare(program);
|
|
@@ -3758,6 +5007,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3758
5007
|
registerBundle(program);
|
|
3759
5008
|
program.addCommand(initCommand);
|
|
3760
5009
|
program.addCommand(scanCommand);
|
|
5010
|
+
program.addCommand(fixCommand);
|
|
3761
5011
|
program.addCommand(baselineCommand);
|
|
3762
5012
|
program.addCommand(reportCommand);
|
|
3763
5013
|
program.addCommand(loginCommand);
|
|
@@ -3822,11 +5072,19 @@ function dispatch(argv, cwd) {
|
|
|
3822
5072
|
}
|
|
3823
5073
|
const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
|
|
3824
5074
|
if (looksLikeQuestion) return ["ask", ...args];
|
|
3825
|
-
if (
|
|
5075
|
+
if (fs5.existsSync(path.resolve(cwd, first))) {
|
|
3826
5076
|
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
3827
5077
|
}
|
|
5078
|
+
const moved = MOVED_COMMANDS[first];
|
|
5079
|
+
if (moved) {
|
|
5080
|
+
throw new CliError(`\`vg ${first}\` has moved to \`${moved}\``, ExitCode.USAGE_ERROR);
|
|
5081
|
+
}
|
|
3828
5082
|
return ["ask", ...args];
|
|
3829
5083
|
}
|
|
5084
|
+
var MOVED_COMMANDS = {
|
|
5085
|
+
verify: "vg build --verify",
|
|
5086
|
+
attest: "vg build --attest"
|
|
5087
|
+
};
|
|
3830
5088
|
function findFirstPositional(args) {
|
|
3831
5089
|
for (let i = 0; i < args.length; i++) {
|
|
3832
5090
|
const a = args[i];
|
|
@@ -3842,7 +5100,6 @@ async function main(argv = process.argv) {
|
|
|
3842
5100
|
const raw = argv.slice(2);
|
|
3843
5101
|
if (process.env.NO_COLOR || raw.includes("--no-color")) disableColor();
|
|
3844
5102
|
const cwd = readCwd(raw);
|
|
3845
|
-
const dispatched = dispatch(raw, cwd);
|
|
3846
5103
|
const program = buildProgram();
|
|
3847
5104
|
program.exitOverride((err) => {
|
|
3848
5105
|
throw err;
|
|
@@ -3852,6 +5109,7 @@ async function main(argv = process.argv) {
|
|
|
3852
5109
|
writeOut: (str) => process.stdout.write(str)
|
|
3853
5110
|
});
|
|
3854
5111
|
try {
|
|
5112
|
+
const dispatched = dispatch(raw, cwd);
|
|
3855
5113
|
await program.parseAsync(dispatched, { from: "user" });
|
|
3856
5114
|
} catch (err) {
|
|
3857
5115
|
handleError(err);
|
|
@@ -3859,9 +5117,9 @@ async function main(argv = process.argv) {
|
|
|
3859
5117
|
}
|
|
3860
5118
|
function readCwd(raw) {
|
|
3861
5119
|
const i = raw.findIndex((a) => a === "-C" || a === "--cwd");
|
|
3862
|
-
if (i >= 0 && raw[i + 1]) return
|
|
5120
|
+
if (i >= 0 && raw[i + 1]) return path.resolve(raw[i + 1]);
|
|
3863
5121
|
const eq = raw.find((a) => a.startsWith("--cwd="));
|
|
3864
|
-
if (eq) return
|
|
5122
|
+
if (eq) return path.resolve(eq.slice("--cwd=".length));
|
|
3865
5123
|
return process.cwd();
|
|
3866
5124
|
}
|
|
3867
5125
|
function handleError(err) {
|