@vibgrate/cli 2026.704.3 → 2026.708.2
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-AMQ7VEB5.js +6 -0
- package/dist/{baseline-64EUPWUH.js.map → baseline-AMQ7VEB5.js.map} +1 -1
- package/dist/{chunk-JL7FIC6W.js → chunk-AWEAREOE.js} +562 -258
- package/dist/chunk-AWEAREOE.js.map +1 -0
- package/dist/{chunk-WHQRV66O.js → chunk-GHLYQZWT.js} +3 -3
- package/dist/{chunk-WHQRV66O.js.map → chunk-GHLYQZWT.js.map} +1 -1
- package/dist/{chunk-YN5OGRWU.js → chunk-ICBBG6U3.js} +55 -22
- package/dist/chunk-ICBBG6U3.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +1680 -365
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +28 -4
- package/dist/index.js +2 -2
- package/dist/parse-worker.d.ts +1 -1
- package/dist/{types-C6ZyQpa9.d.ts → types-CXnp1uGj.d.ts} +33 -1
- package/package.json +3 -3
- package/dist/baseline-64EUPWUH.js +0 -6
- package/dist/chunk-JL7FIC6W.js.map +0 -1
- package/dist/chunk-YN5OGRWU.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,267 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, dsnCommand, pushCommand,
|
|
3
|
-
import { resolvedGrammarFiles, grammarSetVersion } from './chunk-X5YT263H.js';
|
|
4
|
-
import { loadAdvancedScanHook, baselineCommand } from './chunk-
|
|
5
|
-
import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, prepareCompressedUpload, titleBox, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, brandProgressBar } from './chunk-
|
|
2
|
+
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, countPending, resolveOne, GraphIndex, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, 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-AWEAREOE.js';
|
|
3
|
+
import { resolvedGrammarFiles, grammarSetVersion, canonicalize } from './chunk-X5YT263H.js';
|
|
4
|
+
import { loadAdvancedScanHook, baselineCommand } from './chunk-GHLYQZWT.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-ICBBG6U3.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
|
|
@@ -51,7 +295,7 @@ function logoLines(root) {
|
|
|
51
295
|
` ${ROBOT[1]}`,
|
|
52
296
|
` ${ROBOT[2]}`,
|
|
53
297
|
` ${ROBOT[3]}`,
|
|
54
|
-
` ${c.bold.white("
|
|
298
|
+
` ${c.bold.white("Vibgrate")} ${teal("Graph")} ${c.dim(`\xB7 Code Intelligence Engine \xB7 v${VERSION}`)}`,
|
|
55
299
|
...root ? [` ${c.dim(root)}`] : [],
|
|
56
300
|
""
|
|
57
301
|
];
|
|
@@ -120,15 +364,19 @@ function readGlobal(cmd) {
|
|
|
120
364
|
|
|
121
365
|
// src/commands/build.ts
|
|
122
366
|
function registerBuild(program) {
|
|
123
|
-
const cmd = program.command("build").description("build / update the code map (incremental, deterministic)").argument("[paths...]", "folders or files to map (default: current folder)").option("--only <langs>", "restrict to languages, e.g. ts,py,go").option("--exclude <glob>", "extra ignore glob (repeatable)", collect, []).option("--no-html", "do not write graph.html").option("--no-report", "do not write GRAPH_REPORT.md").option("--no-ground", "do not attach grounding (Phase 2)").option("--jobs <n>", "worker count (1 = single-threaded)").option("--scip <file>", "ingest a SCIP index for precise resolution (default: auto-detect index.scip)").option("--no-scip", "ignore any SCIP index").option("--no-tsc", "skip the in-process TypeScript resolver (heuristic floor only)").option("--no-warm", "do not warm the semantic index in the background after building").option("--grammars <dir>", "directory of grammar .wasm files (offline / air-gapped)").option("-o, --export <file>", "also write the map to a file (format inferred)").action(async function(paths, opts) {
|
|
367
|
+
const cmd = program.command("build").description("build / update the code map (incremental, deterministic)").argument("[paths...]", "folders or files to map (default: current folder)").option("--only <langs>", "restrict to languages, e.g. ts,py,go").option("--exclude <glob>", "extra ignore glob (repeatable)", collect, []).option("--no-html", "do not write graph.html").option("--no-report", "do not write GRAPH_REPORT.md").option("--no-ground", "do not attach grounding (Phase 2)").option("--jobs <n>", "worker count (1 = single-threaded)").option("--scip <file>", "ingest a SCIP index for precise resolution (default: auto-detect index.scip)").option("--no-scip", "ignore any SCIP index").option("--no-tsc", "skip the in-process TypeScript resolver (heuristic floor only)").option("--no-warm", "do not warm the semantic index in the background after building").option("--grammars <dir>", "directory of grammar .wasm files (offline / air-gapped)").option("-o, --export <file>", "also write the map to a file (format inferred)").option("--attest", "sign the built graph \u2192 .vibgrate/attestation.intoto.jsonl").option("--verify", "verify a committed attestation against the graph (no rebuild)").option("--attest-key <path>", "signing key PEM (else $VG_ATTEST_KEY, else .vibgrate/attest-key.pem)").option("--attestation <file>", "attestation path (out for --attest, in for --verify)").option("--pub <path>", "public key PEM to pin the signer (with --verify)").action(async function(paths, opts) {
|
|
124
368
|
await runBuild(paths, opts, readGlobal(this));
|
|
125
369
|
});
|
|
126
370
|
applyGlobalOptions(cmd);
|
|
127
371
|
}
|
|
128
372
|
async function runBuild(paths, opts, global) {
|
|
129
|
-
const root =
|
|
373
|
+
const root = path.resolve(global.cwd ?? ".");
|
|
374
|
+
if (opts.verify) {
|
|
375
|
+
await verifyGraph(root, opts, global);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
130
378
|
const interactive = !global.json && !global.quiet;
|
|
131
|
-
if (interactive) printLogo(
|
|
379
|
+
if (interactive) printLogo(path.basename(root) || root);
|
|
132
380
|
const bar = interactive ? new ProgressBar(c.dim("parsing")) : void 0;
|
|
133
381
|
const jobs = opts.jobs ? Number(opts.jobs) : void 0;
|
|
134
382
|
if (jobs !== void 0 && (!Number.isInteger(jobs) || jobs < 1)) {
|
|
@@ -180,6 +428,16 @@ async function runBuild(paths, opts, global) {
|
|
|
180
428
|
});
|
|
181
429
|
}
|
|
182
430
|
if (opts.export) writeExport(result.graph, opts.export);
|
|
431
|
+
let attestation;
|
|
432
|
+
const attestNotices = [];
|
|
433
|
+
if (opts.attest) {
|
|
434
|
+
const signed = await signGraphAttestation(root, result.graph, {
|
|
435
|
+
key: opts.attestKey,
|
|
436
|
+
attestation: opts.attestation
|
|
437
|
+
});
|
|
438
|
+
attestation = signed.summary;
|
|
439
|
+
attestNotices.push(...signed.notices);
|
|
440
|
+
}
|
|
183
441
|
if (global.json) {
|
|
184
442
|
json({
|
|
185
443
|
ok: true,
|
|
@@ -191,8 +449,11 @@ async function runBuild(paths, opts, global) {
|
|
|
191
449
|
resolve: result.resolveStats,
|
|
192
450
|
tsc: result.tsc,
|
|
193
451
|
scip: result.scip,
|
|
452
|
+
epistemic: epistemicBreakdown(result.graph.edges),
|
|
194
453
|
artifacts: written,
|
|
195
454
|
corpusHash: result.graph.provenance.corpusHash,
|
|
455
|
+
toolchain: result.graph.provenance.toolchain,
|
|
456
|
+
attestation,
|
|
196
457
|
timingMs: result.timing.totalMs,
|
|
197
458
|
warnings: result.warnings
|
|
198
459
|
});
|
|
@@ -201,7 +462,7 @@ async function runBuild(paths, opts, global) {
|
|
|
201
462
|
const { counts } = result.graph.meta;
|
|
202
463
|
const incremental = result.reused > 0 ? `incremental: ${result.reparsed} of ${result.totalFiles} files re-parsed` : `${result.totalFiles} files parsed`;
|
|
203
464
|
const seconds = (result.timing.totalMs / 1e3).toFixed(2);
|
|
204
|
-
info(`${c.cyan("vg")} \xB7 mapped ${
|
|
465
|
+
info(`${c.cyan("vg")} \xB7 mapped ${rel2(root)} in ${seconds}s (${incremental})`);
|
|
205
466
|
info(
|
|
206
467
|
` nodes ${c.bold(String(counts.nodes))} edges ${c.bold(String(counts.edges))} areas ${counts.areas} langs ${result.graph.meta.languages.join(",") || "\u2014"}`
|
|
207
468
|
);
|
|
@@ -218,13 +479,67 @@ async function runBuild(paths, opts, global) {
|
|
|
218
479
|
if (result.scip) {
|
|
219
480
|
info(c.dim(` scip: ${result.scip.resolved} precise edges from ${result.scip.tool ?? "index"} (${result.scip.documents} docs)`));
|
|
220
481
|
}
|
|
221
|
-
const
|
|
482
|
+
const ep = epistemicBreakdown(result.graph.edges);
|
|
483
|
+
info(
|
|
484
|
+
c.dim(
|
|
485
|
+
` edges by evidence \xB7 observed ${ep.observed} \xB7 name-matched ${ep["name-matched"]} \xB7 declared ${ep.declared}`
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
const artifactList = [written.graphPath, written.htmlPath, written.reportPath].filter(Boolean).map((p) => path.relative(root, p)).join(" ");
|
|
222
489
|
info(` \u2192 ${artifactList}`);
|
|
223
490
|
if (result.warnings.length) {
|
|
224
491
|
info(c.yellow(` ${result.warnings.length} parse warning(s) \u2014 run with --json for detail`));
|
|
225
492
|
}
|
|
493
|
+
if (attestation) {
|
|
494
|
+
for (const n of attestNotices) info(c.yellow(` ${n}`));
|
|
495
|
+
info(
|
|
496
|
+
c.dim(
|
|
497
|
+
` attested \xB7 keyid ${attestation.keyid} \xB7 digest ${attestation.graphDigest.slice(0, 16)}\u2026 \u2192 ${attestation.out}`
|
|
498
|
+
)
|
|
499
|
+
);
|
|
500
|
+
}
|
|
226
501
|
maybeWarmEmbeddings(root, result.graph, global, opts.warm !== false);
|
|
227
502
|
}
|
|
503
|
+
async function verifyGraph(root, opts, global) {
|
|
504
|
+
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
505
|
+
const jobs = opts.jobs ? Number(opts.jobs) : void 0;
|
|
506
|
+
const det = await verifyDeterminism({ root, only, exclude: opts.exclude, jobs });
|
|
507
|
+
const attest = verifyGraphAttestation(root, { attestation: opts.attestation, pub: opts.pub });
|
|
508
|
+
const attestFailed = !attest.missing && attest.result?.status === "failed";
|
|
509
|
+
if (global.json) {
|
|
510
|
+
json({
|
|
511
|
+
ok: det.ok && !attestFailed,
|
|
512
|
+
determinism: { ok: det.ok, checks: det.checks, digest: det.digest },
|
|
513
|
+
attestation: attest.missing ? null : {
|
|
514
|
+
status: attest.result?.status,
|
|
515
|
+
signatureValid: attest.result?.signatureValid,
|
|
516
|
+
signerPinned: attest.result?.signerPinned,
|
|
517
|
+
digestMatches: attest.result?.digestMatches,
|
|
518
|
+
dirty: attest.result?.dirty,
|
|
519
|
+
keyid: attest.result?.keyid,
|
|
520
|
+
reason: attest.result?.reason
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
} else {
|
|
524
|
+
info(`${c.cyan("vg build --verify")} \xB7 ${path.relative(process.cwd(), root) || "."}`);
|
|
525
|
+
for (const check of det.checks) {
|
|
526
|
+
const mark = check.ok ? c.green("\u2714") : c.red("\u2718");
|
|
527
|
+
const detail = check.detail ? c.dim(` (${check.detail})`) : "";
|
|
528
|
+
info(` ${mark} ${check.name}${detail}`);
|
|
529
|
+
}
|
|
530
|
+
info(det.ok ? c.green(` deterministic \xB7 digest ${det.digest.slice(0, 16)}\u2026`) : c.red(" NON-DETERMINISTIC"));
|
|
531
|
+
if (attest.missing) {
|
|
532
|
+
info(c.dim(` attestation: none (sign one with \`vg build --attest\`)`));
|
|
533
|
+
} else {
|
|
534
|
+
const r = attest.result;
|
|
535
|
+
const badge = r?.status === "verified" ? c.green("\u2714 attestation verified") : r?.status === "signature-valid" ? c.yellow("~ attestation signature valid") : c.red("\u2718 attestation failed");
|
|
536
|
+
info(` ${badge}${r?.keyid ? c.dim(` \xB7 keyid ${r.keyid}`) : ""}`);
|
|
537
|
+
info(c.dim(` ${r?.reason ?? ""}`));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!det.ok) throw new CliError("determinism self-check failed", ExitCode.NON_DETERMINISTIC);
|
|
541
|
+
if (attestFailed) throw new CliError("attestation verification failed", ExitCode.GATE_FAILED);
|
|
542
|
+
}
|
|
228
543
|
function maybeWarmEmbeddings(root, graph, global, warm) {
|
|
229
544
|
if (!warm || global.json || global.quiet || global.local) return;
|
|
230
545
|
if (!process.stdout.isTTY && !process.stderr.isTTY) return;
|
|
@@ -250,7 +565,7 @@ function writeExport(graph, target) {
|
|
|
250
565
|
out(serializeGraph(graph).trimEnd());
|
|
251
566
|
return;
|
|
252
567
|
}
|
|
253
|
-
const ext =
|
|
568
|
+
const ext = path.extname(target).toLowerCase();
|
|
254
569
|
let content;
|
|
255
570
|
switch (ext) {
|
|
256
571
|
case ".json":
|
|
@@ -268,14 +583,14 @@ function writeExport(graph, target) {
|
|
|
268
583
|
ExitCode.USAGE_ERROR
|
|
269
584
|
);
|
|
270
585
|
}
|
|
271
|
-
|
|
272
|
-
|
|
586
|
+
fs5.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
|
|
587
|
+
fs5.writeFileSync(target, content);
|
|
273
588
|
}
|
|
274
589
|
function collect(value, prev) {
|
|
275
590
|
return [...prev, value];
|
|
276
591
|
}
|
|
277
|
-
function
|
|
278
|
-
const r =
|
|
592
|
+
function rel2(root) {
|
|
593
|
+
const r = path.relative(process.cwd(), root);
|
|
279
594
|
return r === "" ? "." : r;
|
|
280
595
|
}
|
|
281
596
|
function registerStatus(program) {
|
|
@@ -285,10 +600,10 @@ function registerStatus(program) {
|
|
|
285
600
|
applyGlobalOptions(cmd);
|
|
286
601
|
}
|
|
287
602
|
async function runStatus(global) {
|
|
288
|
-
const root =
|
|
603
|
+
const root = path.resolve(global.cwd ?? ".");
|
|
289
604
|
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
290
605
|
const graph = loadGraph(root, graphPath);
|
|
291
|
-
const hasCache =
|
|
606
|
+
const hasCache = fs5.existsSync(path.join(cacheDir(root), "parse-cache.json"));
|
|
292
607
|
let stale = null;
|
|
293
608
|
let staleExact = false;
|
|
294
609
|
if (graph) {
|
|
@@ -308,8 +623,8 @@ async function runStatus(global) {
|
|
|
308
623
|
}
|
|
309
624
|
if (global.json) {
|
|
310
625
|
json({
|
|
311
|
-
root:
|
|
312
|
-
graphPath:
|
|
626
|
+
root: path.relative(process.cwd(), root) || ".",
|
|
627
|
+
graphPath: path.relative(root, graphPath),
|
|
313
628
|
built: graph !== null,
|
|
314
629
|
generatedAt: graph?.generatedAt ?? null,
|
|
315
630
|
counts: graph?.meta.counts ?? null,
|
|
@@ -324,11 +639,11 @@ async function runStatus(global) {
|
|
|
324
639
|
return;
|
|
325
640
|
}
|
|
326
641
|
if (!graph) {
|
|
327
|
-
info(`${c.cyan("vg")} \xB7 no map yet at ${c.dim(
|
|
642
|
+
info(`${c.cyan("vg")} \xB7 no map yet at ${c.dim(path.relative(root, graphPath))}`);
|
|
328
643
|
info(` run ${c.bold("vg")} to build one`);
|
|
329
644
|
return;
|
|
330
645
|
}
|
|
331
|
-
info(`${c.cyan("vg")} \xB7 ${
|
|
646
|
+
info(`${c.cyan("vg")} \xB7 ${path.relative(root, graphPath)}`);
|
|
332
647
|
info(` generated ${graph.generatedAt}`);
|
|
333
648
|
info(
|
|
334
649
|
` nodes ${c.bold(String(graph.meta.counts.nodes))} edges ${c.bold(
|
|
@@ -344,34 +659,8 @@ async function runStatus(global) {
|
|
|
344
659
|
}
|
|
345
660
|
if (vibgrateDir(root)) ;
|
|
346
661
|
}
|
|
347
|
-
function registerVerify(program) {
|
|
348
|
-
const cmd = program.command("verify").description("determinism self-check (exit 4 on mismatch)").option("--only <langs>", "restrict to languages, e.g. ts,py").option("--jobs <n>", "worker count").action(async function(opts) {
|
|
349
|
-
const global = readGlobal(this);
|
|
350
|
-
const root = path9.resolve(global.cwd ?? ".");
|
|
351
|
-
const result = await verifyDeterminism({
|
|
352
|
-
root,
|
|
353
|
-
only: opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
354
|
-
jobs: opts.jobs ? Number(opts.jobs) : void 0
|
|
355
|
-
});
|
|
356
|
-
if (global.json) {
|
|
357
|
-
json({ ok: result.ok, checks: result.checks, digest: result.digest });
|
|
358
|
-
} else {
|
|
359
|
-
info(`${c.cyan("vg verify")} \xB7 ${path9.relative(process.cwd(), root) || "."}`);
|
|
360
|
-
for (const check of result.checks) {
|
|
361
|
-
const mark = check.ok ? c.green("\u2714") : c.red("\u2718");
|
|
362
|
-
const detail = check.detail ? c.dim(` (${check.detail})`) : "";
|
|
363
|
-
info(` ${mark} ${check.name}${detail}`);
|
|
364
|
-
}
|
|
365
|
-
info(result.ok ? c.green(` deterministic \xB7 digest ${result.digest.slice(0, 16)}\u2026`) : c.red(" NON-DETERMINISTIC"));
|
|
366
|
-
}
|
|
367
|
-
if (!result.ok) {
|
|
368
|
-
throw new CliError("determinism self-check failed", ExitCode.NON_DETERMINISTIC);
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
applyGlobalOptions(cmd);
|
|
372
|
-
}
|
|
373
662
|
function rootOf(global) {
|
|
374
|
-
return
|
|
663
|
+
return path.resolve(global.cwd ?? ".");
|
|
375
664
|
}
|
|
376
665
|
function requireGraph(global) {
|
|
377
666
|
const root = rootOf(global);
|
|
@@ -655,10 +944,10 @@ function registerTree(program) {
|
|
|
655
944
|
applyGlobalOptions(cmd);
|
|
656
945
|
}
|
|
657
946
|
function next(index, node, direction) {
|
|
658
|
-
const
|
|
947
|
+
const rel3 = direction === "callees" ? index.callees(node.id) : index.callers(node.id);
|
|
659
948
|
const seen = /* @__PURE__ */ new Set();
|
|
660
949
|
const out2 = [];
|
|
661
|
-
for (const { node: n } of
|
|
950
|
+
for (const { node: n } of rel3) {
|
|
662
951
|
if (seen.has(n.id)) continue;
|
|
663
952
|
seen.add(n.id);
|
|
664
953
|
out2.push(n);
|
|
@@ -834,6 +1123,113 @@ function registerImpact(program) {
|
|
|
834
1123
|
});
|
|
835
1124
|
applyGlobalOptions(cmd);
|
|
836
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
// src/commands/unknowns.ts
|
|
1128
|
+
var IMPACT_CANDIDATE_CAP = 200;
|
|
1129
|
+
function registerUnknowns(program) {
|
|
1130
|
+
const cmd = program.command("unknowns").description("what the graph cannot resolve, ranked by blast radius").option("-n, --limit <n>", "how many sites to show", "20").action(function(opts) {
|
|
1131
|
+
const global = readGlobal(this);
|
|
1132
|
+
const { graph } = requireGraph(global);
|
|
1133
|
+
const limit = Number(opts.limit) || 20;
|
|
1134
|
+
const ranked = rankUnknowns(graph, limit);
|
|
1135
|
+
const { sites, totalSites, capped } = ranked;
|
|
1136
|
+
const totalRefs = (graph.unknowns ?? []).reduce((n, u) => n + u.count, 0);
|
|
1137
|
+
if (global.json) {
|
|
1138
|
+
json({
|
|
1139
|
+
unresolvedReferences: totalRefs,
|
|
1140
|
+
sites: totalSites,
|
|
1141
|
+
shown: sites.length,
|
|
1142
|
+
blastRankedCandidateCap: capped ? IMPACT_CANDIDATE_CAP : null,
|
|
1143
|
+
top: sites.map((s) => ({
|
|
1144
|
+
id: s.node.id,
|
|
1145
|
+
name: s.node.qualifiedName,
|
|
1146
|
+
kind: s.node.kind,
|
|
1147
|
+
file: s.node.file,
|
|
1148
|
+
line: s.node.span.start,
|
|
1149
|
+
importance: s.node.importance,
|
|
1150
|
+
blastRadius: s.blastRadius,
|
|
1151
|
+
direct: s.direct,
|
|
1152
|
+
transitive: s.transitive,
|
|
1153
|
+
unresolved: s.refs.map((r) => ({ name: r.name, kind: r.kind, count: r.count }))
|
|
1154
|
+
}))
|
|
1155
|
+
});
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (totalRefs === 0) {
|
|
1159
|
+
info(
|
|
1160
|
+
`${c.cyan("vg unknowns")} \xB7 none \u2014 every heuristic reference resolved (or was covered by a precise rung)`
|
|
1161
|
+
);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
info(
|
|
1165
|
+
`${c.cyan("vg unknowns")} \xB7 ${c.bold(String(totalRefs))} unresolved reference(s) at ${totalSites} site(s) \xB7 top ${sites.length} by blast radius`
|
|
1166
|
+
);
|
|
1167
|
+
for (const s of sites) {
|
|
1168
|
+
info(
|
|
1169
|
+
` ${c.yellow(pad2(String(s.blastRadius), 5))} ${c.bold(s.node.qualifiedName)} ` + c.dim(`${s.node.file}:${s.node.span.start} \xB7 importance ${s.node.importance.toFixed(3)}`)
|
|
1170
|
+
);
|
|
1171
|
+
info(` ${c.dim("cannot resolve:")} ${s.refs.map(fmtRef).join(", ")}`);
|
|
1172
|
+
}
|
|
1173
|
+
info(
|
|
1174
|
+
c.dim(
|
|
1175
|
+
` blast radius = transitive dependents; these are the code paths the graph is blind about`
|
|
1176
|
+
)
|
|
1177
|
+
);
|
|
1178
|
+
if (capped) {
|
|
1179
|
+
info(
|
|
1180
|
+
c.dim(
|
|
1181
|
+
` note: ${totalSites} sites have unknowns; only the top ${IMPACT_CANDIDATE_CAP} by centrality were blast-radius scored (the tail is ordered by centrality alone)`
|
|
1182
|
+
)
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
applyGlobalOptions(cmd);
|
|
1187
|
+
}
|
|
1188
|
+
function rankUnknowns(graph, limit) {
|
|
1189
|
+
const unknowns = graph.unknowns ?? [];
|
|
1190
|
+
if (unknowns.length === 0) return { sites: [], totalSites: 0, capped: false };
|
|
1191
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1192
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1193
|
+
for (const u of unknowns) {
|
|
1194
|
+
const list = grouped.get(u.from);
|
|
1195
|
+
if (list) list.push(u);
|
|
1196
|
+
else grouped.set(u.from, [u]);
|
|
1197
|
+
}
|
|
1198
|
+
const candidates = [];
|
|
1199
|
+
for (const [from, refs] of grouped) {
|
|
1200
|
+
const node = byId.get(from);
|
|
1201
|
+
if (!node) continue;
|
|
1202
|
+
candidates.push({ node, refs, total: refs.reduce((n, r) => n + r.count, 0) });
|
|
1203
|
+
}
|
|
1204
|
+
candidates.sort(
|
|
1205
|
+
(a, b) => b.node.importance - a.node.importance || a.node.id.localeCompare(b.node.id)
|
|
1206
|
+
);
|
|
1207
|
+
const scored = candidates.slice(0, IMPACT_CANDIDATE_CAP).map((cand) => {
|
|
1208
|
+
const impact = impactOf(graph, cand.node.id);
|
|
1209
|
+
return {
|
|
1210
|
+
...cand,
|
|
1211
|
+
blastRadius: impact.affected.length,
|
|
1212
|
+
direct: impact.direct,
|
|
1213
|
+
transitive: impact.transitive
|
|
1214
|
+
};
|
|
1215
|
+
});
|
|
1216
|
+
scored.sort(
|
|
1217
|
+
(a, b) => b.blastRadius - a.blastRadius || b.node.importance - a.node.importance || b.total - a.total || a.node.id.localeCompare(b.node.id)
|
|
1218
|
+
);
|
|
1219
|
+
return {
|
|
1220
|
+
sites: scored.slice(0, limit),
|
|
1221
|
+
totalSites: candidates.length,
|
|
1222
|
+
capped: candidates.length > IMPACT_CANDIDATE_CAP
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function fmtRef(r) {
|
|
1226
|
+
const times = r.count > 1 ? ` \xD7${r.count}` : "";
|
|
1227
|
+
const kind = r.kind === "call" ? "" : ` (${r.kind})`;
|
|
1228
|
+
return `${r.name}${kind}${times}`;
|
|
1229
|
+
}
|
|
1230
|
+
function pad2(s, n) {
|
|
1231
|
+
return s.padStart(n, " ");
|
|
1232
|
+
}
|
|
837
1233
|
function registerServe(program) {
|
|
838
1234
|
const cmd = program.command("serve").description("start Vibgrate AI Context \u2014 local-first MCP serving your code map, drift & version-correct docs to your AI").option("--http", "serve over streamable HTTP instead of stdio").option("--port <n>", "port for --http", "7437").option("--host <h>", "host for --http", "127.0.0.1").option("--savings", "record local, counts-only usage savings (opt-in; off by default)").option("--dedup", "collapse a node's heavy relation lists on repeat reads within a session (opt-in; saves tokens)").option("--no-refresh", "serve the map as built \u2014 skip the auto-rebuild when files change").action(async function(opts) {
|
|
839
1235
|
const global = readGlobal(this);
|
|
@@ -846,7 +1242,7 @@ function registerServe(program) {
|
|
|
846
1242
|
dedup: opts.dedup === true,
|
|
847
1243
|
refresh
|
|
848
1244
|
};
|
|
849
|
-
if (!
|
|
1245
|
+
if (!fs5.existsSync(graphPath)) {
|
|
850
1246
|
throw new CliError(
|
|
851
1247
|
`no map found at ${graphPath} \u2014 run \`vg\` to build one first`,
|
|
852
1248
|
ExitCode.NOT_FOUND
|
|
@@ -885,7 +1281,7 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
|
|
|
885
1281
|
info(c.red(`vg serve: request error: ${err.message}`));
|
|
886
1282
|
}
|
|
887
1283
|
});
|
|
888
|
-
await new Promise((
|
|
1284
|
+
await new Promise((resolve16) => httpServer.listen(port, host, resolve16));
|
|
889
1285
|
info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
|
|
890
1286
|
await new Promise(() => {
|
|
891
1287
|
});
|
|
@@ -902,9 +1298,9 @@ function registerInstall(program) {
|
|
|
902
1298
|
json(ASSISTANTS.map((a) => ({ id: a.id, label: a.label, mcp: !!a.mcp, skill: !!a.skill, nudge: !!a.nudge })));
|
|
903
1299
|
} else {
|
|
904
1300
|
info(`${c.cyan("vg install")} \xB7 supported assistants`);
|
|
905
|
-
const
|
|
1301
|
+
const pad4 = Math.max(...ASSISTANTS.map((a) => a.id.length)) + 2;
|
|
906
1302
|
for (const a of ASSISTANTS) {
|
|
907
|
-
info(` ${c.bold(a.id.padEnd(
|
|
1303
|
+
info(` ${c.bold(a.id.padEnd(pad4))} ${a.label} ${c.dim(`mcp:${a.mcp ? "\u2713" : "\u2014"} skill:${a.skill ? "\u2713" : "\u2014"} nudge:${a.nudge ? "\u2713" : "\u2014"}`)}`);
|
|
908
1304
|
}
|
|
909
1305
|
}
|
|
910
1306
|
return;
|
|
@@ -964,8 +1360,8 @@ function registerShare(program) {
|
|
|
964
1360
|
const cmd = program.command("share").description("make the map committable + auto-updating for your team (git)").option("--undo", "reverse what `vg share` installed").option("--reports", "also commit graph.html / GRAPH_REPORT.md (default: gitignored)").action(function(opts) {
|
|
965
1361
|
const global = readGlobal(this);
|
|
966
1362
|
const root = rootOf(global);
|
|
967
|
-
const gitDir =
|
|
968
|
-
if (!
|
|
1363
|
+
const gitDir = path.join(root, ".git");
|
|
1364
|
+
if (!fs5.existsSync(gitDir)) {
|
|
969
1365
|
throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
|
|
970
1366
|
}
|
|
971
1367
|
if (opts.undo) {
|
|
@@ -986,23 +1382,23 @@ function registerShare(program) {
|
|
|
986
1382
|
applyGlobalOptions(cmd);
|
|
987
1383
|
}
|
|
988
1384
|
function writeVibgrateGitignore(root, reports) {
|
|
989
|
-
const dir =
|
|
990
|
-
|
|
1385
|
+
const dir = path.join(root, ".vibgrate");
|
|
1386
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
991
1387
|
const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
|
|
992
1388
|
if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
|
|
993
|
-
|
|
1389
|
+
fs5.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
|
|
994
1390
|
`);
|
|
995
1391
|
}
|
|
996
1392
|
function writeGitAttributes(root) {
|
|
997
|
-
const file =
|
|
1393
|
+
const file = path.join(root, ".gitattributes");
|
|
998
1394
|
const line = ".vibgrate/graph.json merge=vg";
|
|
999
|
-
let content =
|
|
1395
|
+
let content = fs5.existsSync(file) ? fs5.readFileSync(file, "utf8") : "";
|
|
1000
1396
|
if (!content.split("\n").some((l) => l.trim() === line)) {
|
|
1001
1397
|
content = content.length ? `${content.replace(/\s*$/, "")}
|
|
1002
1398
|
${line}
|
|
1003
1399
|
` : `${line}
|
|
1004
1400
|
`;
|
|
1005
|
-
|
|
1401
|
+
fs5.writeFileSync(file, content);
|
|
1006
1402
|
}
|
|
1007
1403
|
}
|
|
1008
1404
|
function configureMergeDriver(root) {
|
|
@@ -1011,8 +1407,8 @@ function configureMergeDriver(root) {
|
|
|
1011
1407
|
}
|
|
1012
1408
|
function installPreCommitHook(root) {
|
|
1013
1409
|
const hooksDir = hookDir(root);
|
|
1014
|
-
|
|
1015
|
-
const file =
|
|
1410
|
+
fs5.mkdirSync(hooksDir, { recursive: true });
|
|
1411
|
+
const file = path.join(hooksDir, "pre-commit");
|
|
1016
1412
|
const block = [
|
|
1017
1413
|
HOOK_BEGIN,
|
|
1018
1414
|
"if command -v vg >/dev/null 2>&1; then",
|
|
@@ -1021,7 +1417,7 @@ function installPreCommitHook(root) {
|
|
|
1021
1417
|
"fi",
|
|
1022
1418
|
HOOK_END
|
|
1023
1419
|
].join("\n");
|
|
1024
|
-
let content =
|
|
1420
|
+
let content = fs5.existsSync(file) ? fs5.readFileSync(file, "utf8") : "#!/bin/sh\n";
|
|
1025
1421
|
const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
|
|
1026
1422
|
if (re.test(content)) content = content.replace(re, block);
|
|
1027
1423
|
else content = `${content.replace(/\s*$/, "")}
|
|
@@ -1030,21 +1426,21 @@ ${block}
|
|
|
1030
1426
|
`;
|
|
1031
1427
|
if (!content.startsWith("#!")) content = `#!/bin/sh
|
|
1032
1428
|
${content}`;
|
|
1033
|
-
|
|
1034
|
-
|
|
1429
|
+
fs5.writeFileSync(file, content);
|
|
1430
|
+
fs5.chmodSync(file, 493);
|
|
1035
1431
|
}
|
|
1036
1432
|
function undo(root) {
|
|
1037
|
-
const hookFile =
|
|
1038
|
-
if (
|
|
1433
|
+
const hookFile = path.join(hookDir(root), "pre-commit");
|
|
1434
|
+
if (fs5.existsSync(hookFile)) {
|
|
1039
1435
|
const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
|
|
1040
|
-
const next2 =
|
|
1041
|
-
if (next2.trim() === "#!/bin/sh")
|
|
1042
|
-
else
|
|
1436
|
+
const next2 = fs5.readFileSync(hookFile, "utf8").replace(re, "\n");
|
|
1437
|
+
if (next2.trim() === "#!/bin/sh") fs5.rmSync(hookFile);
|
|
1438
|
+
else fs5.writeFileSync(hookFile, next2);
|
|
1043
1439
|
}
|
|
1044
|
-
const attr =
|
|
1045
|
-
if (
|
|
1046
|
-
const next2 =
|
|
1047
|
-
|
|
1440
|
+
const attr = path.join(root, ".gitattributes");
|
|
1441
|
+
if (fs5.existsSync(attr)) {
|
|
1442
|
+
const next2 = fs5.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
|
|
1443
|
+
fs5.writeFileSync(attr, next2);
|
|
1048
1444
|
}
|
|
1049
1445
|
try {
|
|
1050
1446
|
git(root, ["config", "--remove-section", "merge.vg"]);
|
|
@@ -1054,10 +1450,10 @@ function undo(root) {
|
|
|
1054
1450
|
function hookDir(root) {
|
|
1055
1451
|
try {
|
|
1056
1452
|
const configured = git(root, ["config", "--get", "core.hooksPath"]).trim();
|
|
1057
|
-
if (configured) return
|
|
1453
|
+
if (configured) return path.resolve(root, configured);
|
|
1058
1454
|
} catch {
|
|
1059
1455
|
}
|
|
1060
|
-
return
|
|
1456
|
+
return path.join(root, ".git", "hooks");
|
|
1061
1457
|
}
|
|
1062
1458
|
function git(root, args) {
|
|
1063
1459
|
return execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
@@ -1088,7 +1484,7 @@ function registerBenchmark(program) {
|
|
|
1088
1484
|
if (tokenReduction.questions.length) {
|
|
1089
1485
|
info(` token reduction vs grep/read baseline (${c.dim("estimates")}):`);
|
|
1090
1486
|
for (const q of tokenReduction.questions) {
|
|
1091
|
-
info(` ${
|
|
1487
|
+
info(` ${pad3(`${q.ratio.toFixed(1)}\xD7`, 6)} ${c.dim(`vg ${q.vgTokens} vs ~${q.baselineTokens}`)} "${q.question}"`);
|
|
1092
1488
|
}
|
|
1093
1489
|
info(` ${c.bold(`aggregate \u2248 ${tokenReduction.aggregateRatio.toFixed(1)}\xD7 fewer tokens`)} ${c.dim("(honest estimate, scales with repo size)")}`);
|
|
1094
1490
|
}
|
|
@@ -1177,7 +1573,7 @@ function grepBaselineTokens(term, files) {
|
|
|
1177
1573
|
if (f.size > 512 * 1024) continue;
|
|
1178
1574
|
let content = "";
|
|
1179
1575
|
try {
|
|
1180
|
-
content =
|
|
1576
|
+
content = fs5.readFileSync(f.abs, "utf8");
|
|
1181
1577
|
} catch {
|
|
1182
1578
|
continue;
|
|
1183
1579
|
}
|
|
@@ -1187,7 +1583,7 @@ function grepBaselineTokens(term, files) {
|
|
|
1187
1583
|
}
|
|
1188
1584
|
function safeSize(abs) {
|
|
1189
1585
|
try {
|
|
1190
|
-
return
|
|
1586
|
+
return fs5.statSync(abs).size;
|
|
1191
1587
|
} catch {
|
|
1192
1588
|
return 0;
|
|
1193
1589
|
}
|
|
@@ -1204,7 +1600,7 @@ function round(x) {
|
|
|
1204
1600
|
function round6(x) {
|
|
1205
1601
|
return Math.round(x * 1e6) / 1e6;
|
|
1206
1602
|
}
|
|
1207
|
-
function
|
|
1603
|
+
function pad3(s, n) {
|
|
1208
1604
|
return s.padStart(n, " ");
|
|
1209
1605
|
}
|
|
1210
1606
|
function registerTests(program) {
|
|
@@ -1517,11 +1913,11 @@ function normalizeStandards(raw) {
|
|
|
1517
1913
|
return { banned };
|
|
1518
1914
|
}
|
|
1519
1915
|
function loadStandards(root) {
|
|
1520
|
-
for (const
|
|
1521
|
-
const p =
|
|
1916
|
+
for (const rel3 of STANDARDS_FILES) {
|
|
1917
|
+
const p = path__default.join(root, rel3);
|
|
1522
1918
|
let text;
|
|
1523
1919
|
try {
|
|
1524
|
-
text =
|
|
1920
|
+
text = fs5__default.readFileSync(p, "utf8");
|
|
1525
1921
|
} catch {
|
|
1526
1922
|
continue;
|
|
1527
1923
|
}
|
|
@@ -1638,23 +2034,29 @@ function registerSavings(program) {
|
|
|
1638
2034
|
const cmd = program.command("savings").description("local, privacy-safe report of tokens/$ saved vs a grep baseline (estimates)").option("--days <n>", "window in days", "30").action(function() {
|
|
1639
2035
|
const global = readGlobal(this);
|
|
1640
2036
|
const days = Number(this.opts().days) || 30;
|
|
1641
|
-
const
|
|
2037
|
+
const root = rootOf(global);
|
|
2038
|
+
const now2 = Date.now();
|
|
2039
|
+
const report = readSavings(root, days, now2);
|
|
2040
|
+
const usage = readUsage(root, days, now2);
|
|
1642
2041
|
if (global.json) {
|
|
1643
|
-
json(report);
|
|
2042
|
+
json({ ...report, usage });
|
|
1644
2043
|
return;
|
|
1645
2044
|
}
|
|
1646
2045
|
info(`${c.cyan("vg savings")} \xB7 last ${days} days ${c.dim("(local, nothing left your machine)")}`);
|
|
1647
|
-
if (
|
|
1648
|
-
info(
|
|
2046
|
+
if (usage.totals.calls === 0) {
|
|
2047
|
+
info(
|
|
2048
|
+
c.dim(
|
|
2049
|
+
report.enabled ? " no calls recorded yet in this window" : " recording is off. Enable with `vg serve --savings`, then query via MCP."
|
|
2050
|
+
)
|
|
2051
|
+
);
|
|
1649
2052
|
return;
|
|
1650
2053
|
}
|
|
1651
|
-
if (report.queries
|
|
1652
|
-
info(
|
|
1653
|
-
|
|
2054
|
+
if (report.queries > 0) {
|
|
2055
|
+
info(` queries ${report.queries} \xB7 context tokens ${fmt(report.vgTokens)} (grep/read baseline \u2248 ${fmt(report.baselineTokens)}) \u2192 ${report.ratio}\xD7 fewer`);
|
|
2056
|
+
info(` est. cost (${report.rateLabel}): $${report.estCostVg} vs $${report.estCostBaseline} \u2192 saved \u2248 $${report.saved}`);
|
|
2057
|
+
info(c.dim(" estimates with stated assumptions (~4 chars/token; ~400 tokens/file baseline); scales with repo size."));
|
|
1654
2058
|
}
|
|
1655
|
-
|
|
1656
|
-
info(` est. cost (${report.rateLabel}): $${report.estCostVg} vs $${report.estCostBaseline} \u2192 saved \u2248 $${report.saved}`);
|
|
1657
|
-
info(c.dim(" estimates with stated assumptions (~4 chars/token; ~400 tokens/file baseline); scales with repo size."));
|
|
2059
|
+
printBreakdown(usage);
|
|
1658
2060
|
});
|
|
1659
2061
|
applyGlobalOptions(cmd);
|
|
1660
2062
|
}
|
|
@@ -1663,6 +2065,24 @@ function fmt(n) {
|
|
|
1663
2065
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
1664
2066
|
return String(n);
|
|
1665
2067
|
}
|
|
2068
|
+
function printBreakdown(usage) {
|
|
2069
|
+
info("");
|
|
2070
|
+
info(c.bold(" by command") + c.dim(" (complete = full result \xB7 partial = capped/paginated \xB7 miss = no result)"));
|
|
2071
|
+
const nameW = Math.max(7, ...usage.commands.map((cmd) => cmd.tool.length));
|
|
2072
|
+
const header = " " + "command".padEnd(nameW) + ["calls", "complete", "partial", "miss", "success%"].map((h) => h.padStart(9)).join("");
|
|
2073
|
+
info(c.dim(header));
|
|
2074
|
+
for (const cmd of usage.commands) {
|
|
2075
|
+
info(
|
|
2076
|
+
" " + cmd.tool.padEnd(nameW) + String(cmd.calls).padStart(9) + String(cmd.complete).padStart(9) + String(cmd.partial).padStart(9) + String(cmd.miss).padStart(9) + `${cmd.successPct}%`.padStart(9)
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
const t = usage.totals;
|
|
2080
|
+
info(c.dim(" " + "\u2500".repeat(nameW + 45)));
|
|
2081
|
+
info(
|
|
2082
|
+
" " + c.bold("total".padEnd(nameW)) + String(t.calls).padStart(9) + String(t.complete).padStart(9) + String(t.partial).padStart(9) + String(t.miss).padStart(9) + "".padStart(9)
|
|
2083
|
+
);
|
|
2084
|
+
info(c.dim(` avg success across commands: ${usage.avgSuccessPct}%`));
|
|
2085
|
+
}
|
|
1666
2086
|
function registerLib(program) {
|
|
1667
2087
|
const cmd = program.command("lib").description("library currency: version-correct, drift-annotated usage docs").argument("[args...]", "a library name, or: add <source> | publish <name> | resolve <name> | refresh").option("--name <name>", "library name for `add`").option("--version <v>", "pin the doc version for `add`/`publish`").option("--online", "(deprecated; network is on by default) allow network for add/refresh URL sources").option("-b, --budget <n>", "trim docs to ~N tokens").option("--readme <path>", "README path for `publish` (default ./README.md)").option("--dts <path>", "TypeScript declaration (.d.ts) path for `publish`").option("--language <lang>", "primary language for `publish`").option("--region <region>", "data-residency region for the hosted catalog (same as scans; default us)").option("--ingest <url>", "hosted catalog/ingest URL override (host extracted; wins over --region)").action(async function(args, opts) {
|
|
1668
2088
|
const global = readGlobal(this);
|
|
@@ -1716,7 +2136,7 @@ async function addCmd(root, rest, opts, asJson) {
|
|
|
1716
2136
|
}
|
|
1717
2137
|
function readPackageVersion(root) {
|
|
1718
2138
|
try {
|
|
1719
|
-
const pkg = JSON.parse(
|
|
2139
|
+
const pkg = JSON.parse(fs5__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
|
|
1720
2140
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
1721
2141
|
} catch {
|
|
1722
2142
|
return void 0;
|
|
@@ -1724,7 +2144,7 @@ function readPackageVersion(root) {
|
|
|
1724
2144
|
}
|
|
1725
2145
|
function readOptionalFile(p) {
|
|
1726
2146
|
try {
|
|
1727
|
-
const t =
|
|
2147
|
+
const t = fs5__default.readFileSync(p, "utf8");
|
|
1728
2148
|
return t.trim() ? t : void 0;
|
|
1729
2149
|
} catch {
|
|
1730
2150
|
return void 0;
|
|
@@ -1739,8 +2159,8 @@ async function publishCmd(root, rest, opts, global, asJson) {
|
|
|
1739
2159
|
if (!parsed) throw new CliError("publishing a private library requires a DSN \u2014 run `vibgrate login` or set VIBGRATE_DSN", ExitCode.USAGE_ERROR);
|
|
1740
2160
|
const version = (opts.version || readPackageVersion(root) || "").trim();
|
|
1741
2161
|
if (!version) throw usageError("a --version is required (or run from a package whose package.json has a version)");
|
|
1742
|
-
const readme = readOptionalFile(opts.readme ?
|
|
1743
|
-
const dts = opts.dts ? readOptionalFile(
|
|
2162
|
+
const readme = readOptionalFile(opts.readme ? path__default.resolve(opts.readme) : path__default.join(root, "README.md"));
|
|
2163
|
+
const dts = opts.dts ? readOptionalFile(path__default.resolve(opts.dts)) : void 0;
|
|
1744
2164
|
if (!readme && !dts) throw usageError("nothing to publish \u2014 no README.md found and no --dts given");
|
|
1745
2165
|
const result = await publishPrivateLibrary(
|
|
1746
2166
|
{ name, version, readme, dts, language: opts.language },
|
|
@@ -1852,7 +2272,7 @@ async function showCmd(root, name, budget, asJson, opts = {}) {
|
|
|
1852
2272
|
out(doc);
|
|
1853
2273
|
}
|
|
1854
2274
|
function registerExport(program) {
|
|
1855
|
-
const cmd = program.command("export").description("export the map (format inferred: json|ndjson|graphml|dot|cypher|md|html|cdx.json|spdx.json)").argument("[file]", "target file (or - for stdout)", "map.json").action(function(file) {
|
|
2275
|
+
const cmd = program.command("export").description("export the map (format inferred: json|ndjson|graphml|dot|cypher|sql|md|html|cdx.json|spdx.json)").argument("[file]", "target file (or - for stdout)", "map.json").action(function(file) {
|
|
1856
2276
|
const global = readGlobal(this);
|
|
1857
2277
|
const { graph } = requireGraph(global);
|
|
1858
2278
|
const root = rootOf(global);
|
|
@@ -1869,8 +2289,8 @@ function registerExport(program) {
|
|
|
1869
2289
|
out(content.trimEnd());
|
|
1870
2290
|
return;
|
|
1871
2291
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
2292
|
+
fs5.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
2293
|
+
fs5.writeFileSync(file, content);
|
|
1874
2294
|
info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
|
|
1875
2295
|
});
|
|
1876
2296
|
applyGlobalOptions(cmd);
|
|
@@ -1880,37 +2300,37 @@ function detectFormat(file) {
|
|
|
1880
2300
|
if (lower.endsWith(".cdx.json")) return "cyclonedx";
|
|
1881
2301
|
if (lower.endsWith(".spdx.json")) return "spdx";
|
|
1882
2302
|
if (file === "-") return "json";
|
|
1883
|
-
return formatForExt(
|
|
2303
|
+
return formatForExt(path.extname(file));
|
|
1884
2304
|
}
|
|
1885
2305
|
function registerBundle(program) {
|
|
1886
2306
|
const cmd = program.command("bundle").description("build an air-gapped bundle (grammars + graph + library catalog)").option("--offline", "offline bundle (default; present for clarity)").option("-o, --out <dir>", "output directory", "vg-bundle").action(function(opts) {
|
|
1887
2307
|
const global = readGlobal(this);
|
|
1888
2308
|
const root = rootOf(global);
|
|
1889
|
-
const outDir =
|
|
2309
|
+
const outDir = path.resolve(root, opts.out ?? "vg-bundle");
|
|
1890
2310
|
let grammarFiles;
|
|
1891
2311
|
try {
|
|
1892
2312
|
grammarFiles = resolvedGrammarFiles();
|
|
1893
2313
|
} catch {
|
|
1894
2314
|
throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
|
|
1895
2315
|
}
|
|
1896
|
-
|
|
2316
|
+
fs5.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
|
|
1897
2317
|
let grammarCount = 0;
|
|
1898
2318
|
for (const { fileName, absPath } of grammarFiles) {
|
|
1899
|
-
|
|
2319
|
+
fs5.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
|
|
1900
2320
|
grammarCount++;
|
|
1901
2321
|
}
|
|
1902
2322
|
const included = [`grammars/ (${grammarCount} .wasm)`];
|
|
1903
2323
|
const graphSrc = global.graph ?? defaultGraphPath(root);
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
2324
|
+
if (fs5.existsSync(graphSrc)) {
|
|
2325
|
+
fs5.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
|
|
1906
2326
|
included.push("graph.json");
|
|
1907
2327
|
}
|
|
1908
2328
|
const cat = catalogPath(root);
|
|
1909
|
-
if (
|
|
1910
|
-
|
|
2329
|
+
if (fs5.existsSync(cat)) {
|
|
2330
|
+
fs5.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
|
|
1911
2331
|
included.push("vibgrate.lib.json");
|
|
1912
|
-
if (
|
|
1913
|
-
|
|
2332
|
+
if (fs5.existsSync(libDir(root))) {
|
|
2333
|
+
fs5.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
|
|
1914
2334
|
included.push("lib/");
|
|
1915
2335
|
}
|
|
1916
2336
|
}
|
|
@@ -1922,38 +2342,38 @@ function registerBundle(program) {
|
|
|
1922
2342
|
included,
|
|
1923
2343
|
usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
|
|
1924
2344
|
};
|
|
1925
|
-
|
|
2345
|
+
fs5.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
|
|
1926
2346
|
`);
|
|
1927
2347
|
if (global.json) {
|
|
1928
|
-
json({ out:
|
|
2348
|
+
json({ out: path.relative(root, outDir), ...manifest });
|
|
1929
2349
|
return;
|
|
1930
2350
|
}
|
|
1931
|
-
info(`${c.cyan("vg bundle")} \xB7 wrote ${
|
|
2351
|
+
info(`${c.cyan("vg bundle")} \xB7 wrote ${path.relative(root, outDir)}/`);
|
|
1932
2352
|
for (const i of included) info(` ${c.green("\u2714")} ${i}`);
|
|
1933
|
-
info(c.dim(` offline: vg build --grammars "${
|
|
2353
|
+
info(c.dim(` offline: vg build --grammars "${path.relative(root, outDir)}/grammars"`));
|
|
1934
2354
|
});
|
|
1935
2355
|
applyGlobalOptions(cmd);
|
|
1936
2356
|
}
|
|
1937
2357
|
var initCommand = new Command("init").description("Initialize vibgrate in a project").argument("[path]", "Path to initialize", ".").option("--baseline", "Create initial baseline after init").option("--yes", "Skip confirmation prompts").action(async (targetPath, opts) => {
|
|
1938
|
-
const rootDir =
|
|
1939
|
-
const vibgrateDir2 =
|
|
2358
|
+
const rootDir = path.resolve(targetPath);
|
|
2359
|
+
const vibgrateDir2 = path.join(rootDir, ".vibgrate");
|
|
1940
2360
|
await ensureDir(vibgrateDir2);
|
|
1941
|
-
console.log(
|
|
1942
|
-
const configPath =
|
|
2361
|
+
console.log(chalk7.green("\u2714") + ` Created ${chalk7.bold(".vibgrate/")} directory`);
|
|
2362
|
+
const configPath = path.join(rootDir, "vibgrate.config.ts");
|
|
1943
2363
|
if (await pathExists(configPath)) {
|
|
1944
|
-
console.log(
|
|
2364
|
+
console.log(chalk7.dim(" vibgrate.config.ts already exists, skipping"));
|
|
1945
2365
|
} else {
|
|
1946
2366
|
await writeDefaultConfig(rootDir);
|
|
1947
|
-
console.log(
|
|
2367
|
+
console.log(chalk7.green("\u2714") + ` Created ${chalk7.bold("vibgrate.config.ts")}`);
|
|
1948
2368
|
}
|
|
1949
2369
|
if (opts.baseline) {
|
|
1950
|
-
const { runBaseline } = await import('./baseline-
|
|
2370
|
+
const { runBaseline } = await import('./baseline-AMQ7VEB5.js');
|
|
1951
2371
|
await runBaseline(rootDir);
|
|
1952
2372
|
}
|
|
1953
2373
|
console.log("");
|
|
1954
|
-
console.log(
|
|
1955
|
-
console.log(` ${
|
|
1956
|
-
console.log(` ${
|
|
2374
|
+
console.log(chalk7.bold("Next steps:"));
|
|
2375
|
+
console.log(` ${chalk7.cyan("vibgrate scan")} Scan for upgrade drift`);
|
|
2376
|
+
console.log(` ${chalk7.cyan("vibgrate baseline")} Create a drift baseline`);
|
|
1957
2377
|
console.log("");
|
|
1958
2378
|
});
|
|
1959
2379
|
|
|
@@ -1980,7 +2400,7 @@ var AI_ASSISTANT_DETECTORS = [
|
|
|
1980
2400
|
async function detectAiAssistant(rootDir) {
|
|
1981
2401
|
for (const detector of AI_ASSISTANT_DETECTORS) {
|
|
1982
2402
|
for (const file of detector.files) {
|
|
1983
|
-
if (await pathExists$1(
|
|
2403
|
+
if (await pathExists$1(path.join(rootDir, file))) {
|
|
1984
2404
|
return detector.name;
|
|
1985
2405
|
}
|
|
1986
2406
|
}
|
|
@@ -1988,22 +2408,22 @@ async function detectAiAssistant(rootDir) {
|
|
|
1988
2408
|
return null;
|
|
1989
2409
|
}
|
|
1990
2410
|
function printAiContextPrompt(detectedAssistant) {
|
|
1991
|
-
const teal2 =
|
|
1992
|
-
const mint2 =
|
|
2411
|
+
const teal2 = chalk7.hex("#3FB0A4");
|
|
2412
|
+
const mint2 = chalk7.hex("#4FE3C1");
|
|
1993
2413
|
const cli = resolveCliInvocation();
|
|
1994
2414
|
const installArgs = detectedAssistant ? `install ${detectedAssistant}` : "install --all";
|
|
1995
2415
|
const installCmd = `${cli} ${installArgs}`;
|
|
1996
|
-
const detectedNote = detectedAssistant ?
|
|
2416
|
+
const detectedNote = detectedAssistant ? chalk7.dim(` (${detectedAssistant} config detected)`) : "";
|
|
1997
2417
|
console.log("");
|
|
1998
2418
|
console.log(teal2(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
1999
|
-
console.log(teal2(" \u2502") + " " + mint2("\u25C6") + " " +
|
|
2419
|
+
console.log(teal2(" \u2502") + " " + mint2("\u25C6") + " " + chalk7.bold.white("Get AI-aware answers in your editor") + " " + teal2("\u2502"));
|
|
2000
2420
|
console.log(teal2(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
2001
2421
|
console.log("");
|
|
2002
|
-
console.log(" " +
|
|
2422
|
+
console.log(" " + chalk7.bold.white(installCmd) + detectedNote);
|
|
2003
2423
|
console.log("");
|
|
2004
|
-
console.log(" " + teal2("\xB7") + " " +
|
|
2005
|
-
console.log(" " + teal2("\xB7") + " " +
|
|
2006
|
-
console.log(" " + teal2("\xB7") + " " +
|
|
2424
|
+
console.log(" " + teal2("\xB7") + " " + chalk7.white("Code map in your assistant") + chalk7.dim(" \u2014 call trees, impact surfaces, import paths"));
|
|
2425
|
+
console.log(" " + teal2("\xB7") + " " + chalk7.white("Offline drift") + chalk7.dim(" \u2014 DriftScore and upgrade priorities, right inside your editor"));
|
|
2426
|
+
console.log(" " + teal2("\xB7") + " " + chalk7.white("Version-correct library docs") + chalk7.dim(" \u2014 pinned to your lockfile, no hallucinated APIs"));
|
|
2007
2427
|
console.log("");
|
|
2008
2428
|
}
|
|
2009
2429
|
|
|
@@ -2014,14 +2434,14 @@ function shouldBuildCodeMap(opts) {
|
|
|
2014
2434
|
async function autoPush(artifact, rootDir, opts) {
|
|
2015
2435
|
const dsn = resolveDsn(opts.dsn);
|
|
2016
2436
|
if (!dsn) {
|
|
2017
|
-
console.error(
|
|
2018
|
-
console.error(
|
|
2437
|
+
console.error(chalk7.red("No DSN provided for push."));
|
|
2438
|
+
console.error(chalk7.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
|
|
2019
2439
|
if (opts.strict) process.exit(1);
|
|
2020
2440
|
return;
|
|
2021
2441
|
}
|
|
2022
2442
|
const parsed = parseDsn(dsn);
|
|
2023
2443
|
if (!parsed) {
|
|
2024
|
-
console.error(
|
|
2444
|
+
console.error(chalk7.red("Invalid DSN format."));
|
|
2025
2445
|
if (opts.strict) process.exit(1);
|
|
2026
2446
|
return;
|
|
2027
2447
|
}
|
|
@@ -2032,7 +2452,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2032
2452
|
try {
|
|
2033
2453
|
host = resolveIngestHost(opts.region);
|
|
2034
2454
|
} catch (e) {
|
|
2035
|
-
console.error(
|
|
2455
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
2036
2456
|
if (opts.strict) process.exit(1);
|
|
2037
2457
|
return;
|
|
2038
2458
|
}
|
|
@@ -2040,7 +2460,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2040
2460
|
const originalSize = JSON.stringify(artifact).length;
|
|
2041
2461
|
const compressedSize = body.length;
|
|
2042
2462
|
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
2043
|
-
console.log(
|
|
2463
|
+
console.log(chalk7.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
2044
2464
|
try {
|
|
2045
2465
|
const { response, host: uploadedHost } = await uploadScanArtifact({
|
|
2046
2466
|
scheme: parsed.scheme,
|
|
@@ -2060,36 +2480,36 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
2060
2480
|
const result = await response.json();
|
|
2061
2481
|
if (result.unchanged) {
|
|
2062
2482
|
console.log(
|
|
2063
|
-
|
|
2483
|
+
chalk7.green("\u2714") + ` Repository unchanged since ${result.lastScannedAt ?? "last scan"} \u2014 skipped upload (no credit used).`
|
|
2064
2484
|
);
|
|
2065
2485
|
if (result.previousIngestId) {
|
|
2066
2486
|
emitIngestIdLine(result.previousIngestId, { unchanged: true });
|
|
2067
2487
|
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.previousIngestId}`;
|
|
2068
|
-
console.log(
|
|
2488
|
+
console.log(chalk7.dim(` Previous report: ${dashUrl}`));
|
|
2069
2489
|
} else if (opts.strict) {
|
|
2070
|
-
console.error(
|
|
2490
|
+
console.error(chalk7.red("Repository unchanged but no previous ingest id returned."));
|
|
2071
2491
|
process.exit(1);
|
|
2072
2492
|
}
|
|
2073
2493
|
return;
|
|
2074
2494
|
}
|
|
2075
|
-
console.log(
|
|
2495
|
+
console.log(chalk7.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
2076
2496
|
if (result.ingestId) {
|
|
2077
2497
|
emitIngestIdLine(result.ingestId);
|
|
2078
2498
|
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
2079
2499
|
const CLEAR_LINE = process.platform === "win32" ? "\x1B[0G\x1B[2K" : "";
|
|
2080
2500
|
console.log("");
|
|
2081
|
-
console.log(CLEAR_LINE +
|
|
2501
|
+
console.log(CLEAR_LINE + chalk7.dim(" Processing continues in the background. Results available shortly."));
|
|
2082
2502
|
console.log("");
|
|
2083
|
-
console.log(CLEAR_LINE +
|
|
2084
|
-
console.log(CLEAR_LINE +
|
|
2085
|
-
console.log(CLEAR_LINE + " " +
|
|
2086
|
-
console.log(CLEAR_LINE +
|
|
2503
|
+
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"));
|
|
2504
|
+
console.log(CLEAR_LINE + chalk7.bold(" \u{1F4CA} View Scan Report"));
|
|
2505
|
+
console.log(CLEAR_LINE + " " + chalk7.underline.cyan(dashUrl));
|
|
2506
|
+
console.log(CLEAR_LINE + chalk7.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2087
2507
|
console.log("");
|
|
2088
2508
|
console.log("");
|
|
2089
2509
|
}
|
|
2090
2510
|
} catch (e) {
|
|
2091
2511
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2092
|
-
console.error(
|
|
2512
|
+
console.error(chalk7.red(`Upload failed: ${msg}`));
|
|
2093
2513
|
if (opts.strict) process.exit(1);
|
|
2094
2514
|
}
|
|
2095
2515
|
}
|
|
@@ -2101,15 +2521,15 @@ function reportStandards(rootDir) {
|
|
|
2101
2521
|
if (!loaded.policy) return;
|
|
2102
2522
|
const violations = checkStandards(loaded.policy, inventory(rootDir).records);
|
|
2103
2523
|
if (violations.length === 0) {
|
|
2104
|
-
console.log(
|
|
2524
|
+
console.log(chalk7.green("\u2714") + " Standards: no banned dependencies in use");
|
|
2105
2525
|
return;
|
|
2106
2526
|
}
|
|
2107
|
-
console.log(
|
|
2527
|
+
console.log(chalk7.red(`
|
|
2108
2528
|
\u26A0 Standards: ${violations.length} banned dependency(ies) in use`));
|
|
2109
2529
|
for (const v of violations) {
|
|
2110
|
-
const fix = v.use ?
|
|
2111
|
-
const why = v.reason ?
|
|
2112
|
-
console.log(` ${
|
|
2530
|
+
const fix = v.use ? chalk7.dim(` \u2192 use ${v.use}`) : "";
|
|
2531
|
+
const why = v.reason ? chalk7.dim(` (${v.reason})`) : "";
|
|
2532
|
+
console.log(` ${chalk7.red("banned")} ${v.ecosystem}:${v.name}${v.installed ? chalk7.dim(` ${v.installed}`) : ""}${fix}${why}`);
|
|
2113
2533
|
}
|
|
2114
2534
|
}
|
|
2115
2535
|
function parseNonNegativeNumber(value, label) {
|
|
@@ -2126,16 +2546,18 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2126
2546
|
collectExcludes,
|
|
2127
2547
|
[]
|
|
2128
2548
|
).option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--full", "Comprehensive scan: turns on known-vulnerability detection (= --vulns) and, when a standards policy exists, a banned-dependency report \u2014 on top of drift scoring and the code map").option("--vulns", "Also scan installed dependencies for known vulnerabilities (OSV online, or advisories from --package-manifest when offline)").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").option("--repository-name <name>", "Override the repository name recorded for this scan (defaults to the directory or package.json name)").option("--force", "Always create a fresh scan ingest, even if the repository is unchanged since the last scan (skips the unchanged/reuse optimization). Used by scheduled and dashboard-triggered scans.").option("--no-graph", "Skip building the local code map (the AI/docs index) that scan produces after scoring drift").option("--quiet", "Suppress promotional output (the free-plan tracking panel and the AI Context install prompt); scan results are unaffected").action(async (targetPath, opts) => {
|
|
2129
|
-
const rootDir =
|
|
2549
|
+
const rootDir = path.resolve(targetPath);
|
|
2130
2550
|
if (!await pathExists$1(rootDir)) {
|
|
2131
|
-
console.error(
|
|
2551
|
+
console.error(chalk7.red(`Path does not exist: ${rootDir}`));
|
|
2132
2552
|
process.exit(1);
|
|
2133
2553
|
}
|
|
2134
2554
|
const hasDsn = !!resolveDsn(opts.dsn);
|
|
2135
2555
|
const willPush = !opts.offline && (opts.push || hasDsn);
|
|
2136
2556
|
const noLocalArtifacts = opts.localArtifacts === false;
|
|
2137
|
-
const isFirstRun = !await pathExists$1(
|
|
2557
|
+
const isFirstRun = !await pathExists$1(path.join(rootDir, ".vibgrate", "scan_result.json"));
|
|
2138
2558
|
let pinnedRegion;
|
|
2559
|
+
let planTier;
|
|
2560
|
+
let upgradeUrl;
|
|
2139
2561
|
if (willPush && hasDsn) {
|
|
2140
2562
|
const dsn = resolveDsn(opts.dsn);
|
|
2141
2563
|
const parsed = parseDsn(dsn);
|
|
@@ -2150,10 +2572,12 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2150
2572
|
vcsSha: fingerprint.vcsSha
|
|
2151
2573
|
});
|
|
2152
2574
|
pinnedRegion = preflight.region;
|
|
2575
|
+
planTier = preflight.plan?.tier;
|
|
2576
|
+
upgradeUrl = preflight.upgradeUrl ?? `https://${dashHostForIngestHost(preflight.ingestHost ?? ingestHost)}/${parsed.workspaceId}`;
|
|
2153
2577
|
if (preflight.vm && !preflight.vm.allowed) {
|
|
2154
|
-
console.error(
|
|
2578
|
+
console.error(chalk7.red(preflight.error ?? "VM meter usage exhausted"));
|
|
2155
2579
|
console.error(
|
|
2156
|
-
|
|
2580
|
+
chalk7.dim(
|
|
2157
2581
|
`VM minutes: ${preflight.vm.used}/${preflight.vm.limit} (${preflight.plan.label} plan) \u2014 enable overages or upgrade your plan.`
|
|
2158
2582
|
)
|
|
2159
2583
|
);
|
|
@@ -2161,25 +2585,25 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2161
2585
|
}
|
|
2162
2586
|
if (preflight.repositories && !preflight.repositories.allowed) {
|
|
2163
2587
|
console.error(
|
|
2164
|
-
|
|
2588
|
+
chalk7.red(
|
|
2165
2589
|
preflight.error ?? `Repository limit reached for the ${preflight.plan.label} plan \u2014 cannot scan a new repository.`
|
|
2166
2590
|
)
|
|
2167
2591
|
);
|
|
2168
2592
|
const max = preflight.repositories.max < 0 ? "unlimited" : String(preflight.repositories.max);
|
|
2169
2593
|
console.error(
|
|
2170
|
-
|
|
2594
|
+
chalk7.dim(
|
|
2171
2595
|
`Repositories: ${preflight.repositories.total}/${max} (${preflight.plan.label} plan) \u2014 archive a repository or upgrade your plan.`
|
|
2172
2596
|
)
|
|
2173
2597
|
);
|
|
2174
2598
|
if (preflight.upgradeUrl) {
|
|
2175
|
-
console.error(
|
|
2599
|
+
console.error(chalk7.dim(` Upgrade: ${preflight.upgradeUrl}`));
|
|
2176
2600
|
}
|
|
2177
2601
|
process.exit(1);
|
|
2178
2602
|
}
|
|
2179
2603
|
if (preflight.status === "error" || !preflight.scans.allowed) {
|
|
2180
|
-
console.error(
|
|
2604
|
+
console.error(chalk7.red(preflight.error ?? "Scan ingestion not allowed for this workspace."));
|
|
2181
2605
|
console.error(
|
|
2182
|
-
|
|
2606
|
+
chalk7.dim(
|
|
2183
2607
|
`Credits: ${preflight.scans.used}/${preflight.scans.limit} (${preflight.plan.label} plan)`
|
|
2184
2608
|
)
|
|
2185
2609
|
);
|
|
@@ -2187,27 +2611,27 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2187
2611
|
process.exit(1);
|
|
2188
2612
|
}
|
|
2189
2613
|
console.log(
|
|
2190
|
-
|
|
2614
|
+
chalk7.dim(
|
|
2191
2615
|
`Plan: ${preflight.plan.label} \u2014 scan credits ${preflight.scans.used}/${preflight.scans.limit} this month`
|
|
2192
2616
|
)
|
|
2193
2617
|
);
|
|
2194
2618
|
if (preflight.repository?.unchanged && !opts.force) {
|
|
2195
2619
|
console.log(
|
|
2196
|
-
|
|
2620
|
+
chalk7.green("\u2714") + ` Repository unchanged at ${preflight.repository.lastVcsSha?.slice(0, 7) ?? "same revision"} \u2014 skipping scan.`
|
|
2197
2621
|
);
|
|
2198
2622
|
if (preflight.repository.lastIngestId) {
|
|
2199
2623
|
emitIngestIdLine(preflight.repository.lastIngestId, { unchanged: true });
|
|
2200
2624
|
const dashUrl = `https://${dashHostForIngestHost(ingestHost)}/${parsed.workspaceId}/scan/${preflight.repository.lastIngestId}`;
|
|
2201
|
-
console.log(
|
|
2625
|
+
console.log(chalk7.dim(` Latest report: ${dashUrl}`));
|
|
2202
2626
|
} else if (opts.strict) {
|
|
2203
|
-
console.error(
|
|
2627
|
+
console.error(chalk7.red("Repository unchanged but no previous ingest id available."));
|
|
2204
2628
|
process.exit(1);
|
|
2205
2629
|
}
|
|
2206
2630
|
return;
|
|
2207
2631
|
}
|
|
2208
2632
|
} catch (e) {
|
|
2209
2633
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2210
|
-
console.error(
|
|
2634
|
+
console.error(chalk7.yellow(`Preflight check failed: ${msg}`));
|
|
2211
2635
|
if (opts.strict) process.exit(1);
|
|
2212
2636
|
}
|
|
2213
2637
|
}
|
|
@@ -2240,6 +2664,17 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2240
2664
|
repositoryName: opts.repositoryName?.trim() || void 0,
|
|
2241
2665
|
force: opts.force,
|
|
2242
2666
|
quiet: opts.quiet,
|
|
2667
|
+
// Auth + plan signals for the free-plan upsell panel. `hasDsn` resolves the
|
|
2668
|
+
// full credential precedence — `--dsn`, `VIBGRATE_DSN`, and the stored login
|
|
2669
|
+
// credential (`~/.vibgrate/credentials.json`) — which the scanner's own
|
|
2670
|
+
// dsn/env-only check cannot see. `planTier`/`upgradeUrl` come from preflight.
|
|
2671
|
+
// Together they pick the panel audience: signed out → login CTA; signed in
|
|
2672
|
+
// on free → upgrade CTA; signed in on a paid plan → no panel. Without the
|
|
2673
|
+
// auth signal, a logged-in user running a bare scan was mislabelled
|
|
2674
|
+
// "Vibgrate Free" and shown the login panel even as the run pushed.
|
|
2675
|
+
authenticated: hasDsn,
|
|
2676
|
+
planTier,
|
|
2677
|
+
upgradeUrl,
|
|
2243
2678
|
// Prefix for the upsell panel's `login → push` hint — `vg` when installed,
|
|
2244
2679
|
// `npx @vibgrate/cli` when the user ran via npx (where bare `vg` fails).
|
|
2245
2680
|
invocation: resolveCliInvocation()
|
|
@@ -2270,24 +2705,24 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
2270
2705
|
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
2271
2706
|
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
2272
2707
|
if (opts.failOn === "error" && hasErrors) {
|
|
2273
|
-
console.error(
|
|
2708
|
+
console.error(chalk7.red(`
|
|
2274
2709
|
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
2275
2710
|
process.exit(2);
|
|
2276
2711
|
}
|
|
2277
2712
|
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
2278
|
-
console.error(
|
|
2713
|
+
console.error(chalk7.red(`
|
|
2279
2714
|
Failing: findings detected at warn level or above.`));
|
|
2280
2715
|
process.exit(2);
|
|
2281
2716
|
}
|
|
2282
2717
|
}
|
|
2283
2718
|
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
2284
|
-
console.error(
|
|
2719
|
+
console.error(chalk7.red(`
|
|
2285
2720
|
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
2286
2721
|
process.exit(2);
|
|
2287
2722
|
}
|
|
2288
2723
|
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
2289
2724
|
if (artifact.delta === void 0) {
|
|
2290
|
-
console.error(
|
|
2725
|
+
console.error(chalk7.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
2291
2726
|
process.exit(2);
|
|
2292
2727
|
}
|
|
2293
2728
|
if (artifact.delta > 0) {
|
|
@@ -2295,7 +2730,7 @@ Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget
|
|
|
2295
2730
|
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
2296
2731
|
const worseningPercent = artifact.delta / denominator * 100;
|
|
2297
2732
|
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
2298
|
-
console.error(
|
|
2733
|
+
console.error(chalk7.red(`
|
|
2299
2734
|
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
2300
2735
|
process.exit(2);
|
|
2301
2736
|
}
|
|
@@ -2310,22 +2745,853 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
|
|
|
2310
2745
|
printAiContextPrompt(detectedAssistant);
|
|
2311
2746
|
}
|
|
2312
2747
|
});
|
|
2748
|
+
var WATCHED_FILES = /* @__PURE__ */ new Set([
|
|
2749
|
+
// npm
|
|
2750
|
+
"package.json",
|
|
2751
|
+
"package-lock.json",
|
|
2752
|
+
"npm-shrinkwrap.json",
|
|
2753
|
+
"pnpm-lock.yaml",
|
|
2754
|
+
"yarn.lock",
|
|
2755
|
+
"bun.lockb",
|
|
2756
|
+
// pypi
|
|
2757
|
+
"requirements.txt",
|
|
2758
|
+
"pyproject.toml",
|
|
2759
|
+
"poetry.lock",
|
|
2760
|
+
"Pipfile",
|
|
2761
|
+
"Pipfile.lock",
|
|
2762
|
+
"uv.lock",
|
|
2763
|
+
// go
|
|
2764
|
+
"go.mod",
|
|
2765
|
+
"go.sum",
|
|
2766
|
+
// rust
|
|
2767
|
+
"Cargo.toml",
|
|
2768
|
+
"Cargo.lock",
|
|
2769
|
+
// ruby
|
|
2770
|
+
"Gemfile",
|
|
2771
|
+
"Gemfile.lock",
|
|
2772
|
+
// php
|
|
2773
|
+
"composer.json",
|
|
2774
|
+
"composer.lock",
|
|
2775
|
+
// swift
|
|
2776
|
+
"Package.swift",
|
|
2777
|
+
"Package.resolved",
|
|
2778
|
+
// dart
|
|
2779
|
+
"pubspec.yaml",
|
|
2780
|
+
"pubspec.lock",
|
|
2781
|
+
// java
|
|
2782
|
+
"pom.xml",
|
|
2783
|
+
"build.gradle",
|
|
2784
|
+
"build.gradle.kts",
|
|
2785
|
+
"gradle.lockfile",
|
|
2786
|
+
// dotnet
|
|
2787
|
+
"packages.lock.json"
|
|
2788
|
+
]);
|
|
2789
|
+
var WATCHED_EXT = /\.(cs|fs)proj$/i;
|
|
2790
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2791
|
+
"node_modules",
|
|
2792
|
+
".git",
|
|
2793
|
+
"dist",
|
|
2794
|
+
"build",
|
|
2795
|
+
"out",
|
|
2796
|
+
".vibgrate",
|
|
2797
|
+
"vendor",
|
|
2798
|
+
".venv",
|
|
2799
|
+
"venv",
|
|
2800
|
+
"env",
|
|
2801
|
+
"__pycache__",
|
|
2802
|
+
"target",
|
|
2803
|
+
".next",
|
|
2804
|
+
".nuxt",
|
|
2805
|
+
"coverage",
|
|
2806
|
+
".cache"
|
|
2807
|
+
]);
|
|
2808
|
+
function scanStaleness(rootDir, artifactPath) {
|
|
2809
|
+
let ref;
|
|
2810
|
+
try {
|
|
2811
|
+
ref = fs5.statSync(artifactPath).mtimeMs;
|
|
2812
|
+
} catch {
|
|
2813
|
+
return { stale: false };
|
|
2814
|
+
}
|
|
2815
|
+
let newest = -Infinity;
|
|
2816
|
+
let newestChanged;
|
|
2817
|
+
const MAX_ENTRIES = 2e4;
|
|
2818
|
+
let scanned = 0;
|
|
2819
|
+
const walk2 = (dir, depth) => {
|
|
2820
|
+
if (depth > 8 || scanned > MAX_ENTRIES) return;
|
|
2821
|
+
let entries;
|
|
2822
|
+
try {
|
|
2823
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2824
|
+
} catch {
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
for (const e of entries) {
|
|
2828
|
+
scanned++;
|
|
2829
|
+
if (scanned > MAX_ENTRIES) break;
|
|
2830
|
+
if (e.isDirectory()) {
|
|
2831
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk2(path.join(dir, e.name), depth + 1);
|
|
2832
|
+
continue;
|
|
2833
|
+
}
|
|
2834
|
+
if (!WATCHED_FILES.has(e.name) && !WATCHED_EXT.test(e.name)) continue;
|
|
2835
|
+
const abs = path.join(dir, e.name);
|
|
2836
|
+
let mtimeMs;
|
|
2837
|
+
try {
|
|
2838
|
+
mtimeMs = fs5.statSync(abs).mtimeMs;
|
|
2839
|
+
} catch {
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
if (mtimeMs > newest) {
|
|
2843
|
+
newest = mtimeMs;
|
|
2844
|
+
newestChanged = path.relative(rootDir, abs) || e.name;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
walk2(rootDir, 0);
|
|
2849
|
+
return newest > ref ? { stale: true, newestChanged } : { stale: false };
|
|
2850
|
+
}
|
|
2851
|
+
var MAX_CONTRACTS = 40;
|
|
2852
|
+
function escapeRegExp(s) {
|
|
2853
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2854
|
+
}
|
|
2855
|
+
function parseJsClause(clause, out2) {
|
|
2856
|
+
const trimmed = clause.trim();
|
|
2857
|
+
if (!trimmed) return;
|
|
2858
|
+
if (/^\*\s+as\s+\w+/.test(trimmed)) {
|
|
2859
|
+
out2.add("* (namespace)");
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
const braceMatch = /\{([^}]*)\}/.exec(trimmed);
|
|
2863
|
+
const beforeBrace = braceMatch ? trimmed.slice(0, braceMatch.index) : trimmed;
|
|
2864
|
+
const defaultId = beforeBrace.replace(/,/g, "").trim();
|
|
2865
|
+
if (defaultId && /^\w+$/.test(defaultId)) out2.add("default");
|
|
2866
|
+
if (braceMatch) {
|
|
2867
|
+
for (const raw of braceMatch[1].split(",")) {
|
|
2868
|
+
const name = raw.trim().split(/\s+as\s+/)[0]?.trim();
|
|
2869
|
+
if (name && /^[\w$]+$/.test(name)) out2.add(name);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
function extractContracts(content, pkg, ecosystem) {
|
|
2874
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
2875
|
+
const e = escapeRegExp(pkg);
|
|
2876
|
+
if (ecosystem === "npm") {
|
|
2877
|
+
const importRe = new RegExp(`import\\s+([^;'"\\n]*?)\\s+from\\s+['"]${e}(?:/[^'"]*)?['"]`, "g");
|
|
2878
|
+
let m;
|
|
2879
|
+
while (m = importRe.exec(content)) parseJsClause(m[1], out2);
|
|
2880
|
+
const requireDestructureRe = new RegExp(`\\{([^}]*)\\}\\s*=\\s*require\\(\\s*['"]${e}(?:/[^'"]*)?['"]\\s*\\)`, "g");
|
|
2881
|
+
while (m = requireDestructureRe.exec(content)) {
|
|
2882
|
+
for (const raw of m[1].split(",")) {
|
|
2883
|
+
const name = raw.trim().split(":")[0]?.trim();
|
|
2884
|
+
if (name && /^[\w$]+$/.test(name)) out2.add(name);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
} else if (ecosystem === "pypi") {
|
|
2888
|
+
const fromRe = new RegExp(`^\\s*from\\s+${e}(?:\\.[\\w.]+)?\\s+import\\s+(.+)$`, "gm");
|
|
2889
|
+
let m;
|
|
2890
|
+
while (m = fromRe.exec(content)) {
|
|
2891
|
+
const names = m[1].replace(/[()]/g, "").split(",");
|
|
2892
|
+
for (const raw of names) {
|
|
2893
|
+
const name = raw.trim().split(/\s+as\s+/)[0]?.trim();
|
|
2894
|
+
if (name === "*") out2.add("* (module)");
|
|
2895
|
+
else if (name && /^[\w.]+$/.test(name)) out2.add(name);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
const importRe = new RegExp(`^\\s*import\\s+${e}\\b`, "gm");
|
|
2899
|
+
if (importRe.test(content)) out2.add(`${pkg} (module)`);
|
|
2900
|
+
}
|
|
2901
|
+
return [...out2].sort();
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// src/reporting/planning/usage.ts
|
|
2905
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
2906
|
+
"node_modules",
|
|
2907
|
+
".git",
|
|
2908
|
+
"dist",
|
|
2909
|
+
"build",
|
|
2910
|
+
"out",
|
|
2911
|
+
".vibgrate",
|
|
2912
|
+
"vendor",
|
|
2913
|
+
".venv",
|
|
2914
|
+
"venv",
|
|
2915
|
+
"env",
|
|
2916
|
+
"__pycache__",
|
|
2917
|
+
"target",
|
|
2918
|
+
".next",
|
|
2919
|
+
".nuxt",
|
|
2920
|
+
"coverage",
|
|
2921
|
+
".cache"
|
|
2922
|
+
]);
|
|
2923
|
+
var MAX_FILES = 8e3;
|
|
2924
|
+
var MAX_DEPTH = 12;
|
|
2925
|
+
var MAX_FILE_BYTES = 1048576;
|
|
2926
|
+
var EXT_ECOSYSTEM = {
|
|
2927
|
+
".ts": "npm",
|
|
2928
|
+
".tsx": "npm",
|
|
2929
|
+
".js": "npm",
|
|
2930
|
+
".jsx": "npm",
|
|
2931
|
+
".mjs": "npm",
|
|
2932
|
+
".cjs": "npm",
|
|
2933
|
+
".mts": "npm",
|
|
2934
|
+
".cts": "npm",
|
|
2935
|
+
".vue": "npm",
|
|
2936
|
+
".svelte": "npm",
|
|
2937
|
+
".py": "pypi"
|
|
2938
|
+
};
|
|
2939
|
+
function escapeRegExp2(s) {
|
|
2940
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2941
|
+
}
|
|
2942
|
+
function importPattern(ecosystem, pkg) {
|
|
2943
|
+
const e = escapeRegExp2(pkg);
|
|
2944
|
+
if (ecosystem === "npm") {
|
|
2945
|
+
return new RegExp(`(?:from\\s+|require\\(\\s*|import\\(\\s*)['"]${e}(?:/[^'"]*)?['"]`, "g");
|
|
2946
|
+
}
|
|
2947
|
+
return new RegExp(`^\\s*(?:import\\s+${e}(?:[.\\s]|$)|from\\s+${e}(?:[.\\s]))`, "gm");
|
|
2948
|
+
}
|
|
2949
|
+
function analyzeTree(root, packages) {
|
|
2950
|
+
const result = /* @__PURE__ */ new Map();
|
|
2951
|
+
const tracked = [];
|
|
2952
|
+
for (const p of packages) {
|
|
2953
|
+
if (p.ecosystem === "npm" || p.ecosystem === "pypi") {
|
|
2954
|
+
tracked.push({
|
|
2955
|
+
name: p.name,
|
|
2956
|
+
ecosystem: p.ecosystem,
|
|
2957
|
+
pattern: importPattern(p.ecosystem, p.name),
|
|
2958
|
+
importSites: 0,
|
|
2959
|
+
filesTouched: 0,
|
|
2960
|
+
contracts: /* @__PURE__ */ new Set()
|
|
2961
|
+
});
|
|
2962
|
+
} else {
|
|
2963
|
+
result.set(p.name, { importSites: 0, filesTouched: 0, contracts: [] });
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
if (tracked.length === 0) return result;
|
|
2967
|
+
let filesSeen = 0;
|
|
2968
|
+
const walk2 = (dir, depth) => {
|
|
2969
|
+
if (depth > MAX_DEPTH || filesSeen >= MAX_FILES) return;
|
|
2970
|
+
let entries;
|
|
2971
|
+
try {
|
|
2972
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2973
|
+
} catch {
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2977
|
+
for (const entry of entries) {
|
|
2978
|
+
if (filesSeen >= MAX_FILES) return;
|
|
2979
|
+
if (entry.isDirectory()) {
|
|
2980
|
+
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
2981
|
+
walk2(path.join(dir, entry.name), depth + 1);
|
|
2982
|
+
continue;
|
|
2983
|
+
}
|
|
2984
|
+
const fileEco = EXT_ECOSYSTEM[path.extname(entry.name)];
|
|
2985
|
+
if (!fileEco || fileEco === "unknown") continue;
|
|
2986
|
+
filesSeen++;
|
|
2987
|
+
const full = path.join(dir, entry.name);
|
|
2988
|
+
let content;
|
|
2989
|
+
try {
|
|
2990
|
+
if (fs5.statSync(full).size > MAX_FILE_BYTES) continue;
|
|
2991
|
+
content = fs5.readFileSync(full, "utf8");
|
|
2992
|
+
} catch {
|
|
2993
|
+
continue;
|
|
2994
|
+
}
|
|
2995
|
+
for (const t of tracked) {
|
|
2996
|
+
if (t.ecosystem !== fileEco) continue;
|
|
2997
|
+
t.pattern.lastIndex = 0;
|
|
2998
|
+
const matches = content.match(t.pattern);
|
|
2999
|
+
if (matches && matches.length) {
|
|
3000
|
+
t.importSites += matches.length;
|
|
3001
|
+
t.filesTouched++;
|
|
3002
|
+
if (t.contracts.size < MAX_CONTRACTS) {
|
|
3003
|
+
for (const sym of extractContracts(content, t.name, t.ecosystem)) t.contracts.add(sym);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
3009
|
+
walk2(root, 0);
|
|
3010
|
+
for (const t of tracked) {
|
|
3011
|
+
result.set(t.name, {
|
|
3012
|
+
importSites: t.importSites,
|
|
3013
|
+
filesTouched: t.filesTouched,
|
|
3014
|
+
contracts: [...t.contracts].sort().slice(0, MAX_CONTRACTS)
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
return result;
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// src/reporting/utils/fix-plan.ts
|
|
3021
|
+
function postOnce(input, host) {
|
|
3022
|
+
return fetch(`${input.scheme}://${host}/v1/fix/plan`, {
|
|
3023
|
+
method: "POST",
|
|
3024
|
+
headers: {
|
|
3025
|
+
"Content-Type": "application/json",
|
|
3026
|
+
"X-Vibgrate-Timestamp": input.timestamp,
|
|
3027
|
+
Authorization: `VibgrateDSN ${input.keyId}:${input.secret}`,
|
|
3028
|
+
Connection: "close"
|
|
3029
|
+
},
|
|
3030
|
+
body: JSON.stringify(input.request)
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
async function regionRedirectHost(response) {
|
|
3034
|
+
try {
|
|
3035
|
+
const payload = await response.clone().json();
|
|
3036
|
+
if (payload?.code !== "REGION_MISMATCH" || !payload.region) return void 0;
|
|
3037
|
+
return ingestHostForRegionId(payload.region);
|
|
3038
|
+
} catch {
|
|
3039
|
+
return void 0;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
async function requestFixPlan(input) {
|
|
3043
|
+
let host = input.host;
|
|
3044
|
+
let response = await postOnce(input, host);
|
|
3045
|
+
if (response.status === 409) {
|
|
3046
|
+
const target = await regionRedirectHost(response);
|
|
3047
|
+
if (target && target !== host) {
|
|
3048
|
+
host = target;
|
|
3049
|
+
response = await postOnce(input, host);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
return { response, host };
|
|
3053
|
+
}
|
|
3054
|
+
async function parseFixPlanResponse(response) {
|
|
3055
|
+
return await response.json();
|
|
3056
|
+
}
|
|
3057
|
+
var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "unknown"];
|
|
3058
|
+
function deltaSummary(delta) {
|
|
3059
|
+
if (delta.total === 0) return "none";
|
|
3060
|
+
const parts = SEVERITY_ORDER.filter((s) => delta.bySeverity[s]).map((s) => `${delta.bySeverity[s]} ${s}`);
|
|
3061
|
+
return parts.length ? parts.join(", ") : `${delta.total}`;
|
|
3062
|
+
}
|
|
3063
|
+
function riskLabel(score) {
|
|
3064
|
+
if (score <= 15) return chalk7.green(`${score}/100 low`);
|
|
3065
|
+
if (score <= 40) return chalk7.yellow(`${score}/100 moderate`);
|
|
3066
|
+
return chalk7.red(`${score}/100 high`);
|
|
3067
|
+
}
|
|
3068
|
+
function planBlock(plan, recommended) {
|
|
3069
|
+
const lines = [];
|
|
3070
|
+
const marker = recommended ? chalk7.green("\u25CF recommended") : chalk7.dim("\u25CB");
|
|
3071
|
+
lines.push(`${marker} ${chalk7.bold(plan.label)} \u2014 ${chalk7.dim(plan.description)}`);
|
|
3072
|
+
const drift = typeof plan.expectedDriftScore === "number" && typeof plan.driftDelta === "number" ? ` \xB7 DriftScore \u2192${plan.expectedDriftScore} (${plan.driftDelta <= 0 ? "" : "+"}${plan.driftDelta})` : "";
|
|
3073
|
+
lines.push(
|
|
3074
|
+
` risk ${riskLabel(plan.riskScore)} \xB7 ${plan.upgrades.length} upgrade(s) \xB7 fixes ${deltaSummary(plan.fixes)}${drift}`
|
|
3075
|
+
);
|
|
3076
|
+
if (plan.introduces.total) {
|
|
3077
|
+
lines.push(chalk7.red(` \u26A0 introduces ${deltaSummary(plan.introduces)} advisory(ies) in target versions`));
|
|
3078
|
+
}
|
|
3079
|
+
const shown = plan.upgrades.slice(0, 12);
|
|
3080
|
+
for (const u of shown) {
|
|
3081
|
+
const codemod = u.playbook?.codemod ? chalk7.magenta(` [codemod: ${u.playbook.codemod}]`) : "";
|
|
3082
|
+
lines.push(` ${chalk7.cyan(u.package)} ${chalk7.dim(`${u.from ?? "?"} \u2192 ${u.to ?? "?"}`)} ${chalk7.dim(u.reason)}${codemod}`);
|
|
3083
|
+
}
|
|
3084
|
+
if (plan.upgrades.length > shown.length) {
|
|
3085
|
+
lines.push(chalk7.dim(` \u2026 and ${plan.upgrades.length - shown.length} more`));
|
|
3086
|
+
}
|
|
3087
|
+
if (plan.upgrades.length === 0) {
|
|
3088
|
+
lines.push(chalk7.dim(" (no upgrades in this plan)"));
|
|
3089
|
+
}
|
|
3090
|
+
return lines.join("\n");
|
|
3091
|
+
}
|
|
3092
|
+
function dataNote(report) {
|
|
3093
|
+
if (report.vulnerabilityData === "unavailable") {
|
|
3094
|
+
return chalk7.dim("Advisory data was unavailable \u2014 vulnerability impact is not shown.");
|
|
3095
|
+
}
|
|
3096
|
+
if (report.vulnerabilityData === "partial") {
|
|
3097
|
+
return chalk7.dim("Advisory data was partial \u2014 some ecosystems were not checked.");
|
|
3098
|
+
}
|
|
3099
|
+
return null;
|
|
3100
|
+
}
|
|
3101
|
+
function renderText(report) {
|
|
3102
|
+
const out2 = [];
|
|
3103
|
+
out2.push(chalk7.bold("Vibgrate fix \u2014 upgrade plan"));
|
|
3104
|
+
out2.push(
|
|
3105
|
+
chalk7.dim(
|
|
3106
|
+
`${report.totalCandidates} drifted dependency(ies) analysed \xB7 ${report.deepAnalysis ? "deep (major) analysis on" : "preflight only"}`
|
|
3107
|
+
)
|
|
3108
|
+
);
|
|
3109
|
+
const note = dataNote(report);
|
|
3110
|
+
if (note) out2.push(note);
|
|
3111
|
+
if (report.exploitability && report.exploitability.kevPackages > 0) {
|
|
3112
|
+
const epss = report.exploitability.maxEpss != null ? `, peak EPSS ${(report.exploitability.maxEpss * 100).toFixed(0)}%` : "";
|
|
3113
|
+
out2.push(chalk7.red(`\u26A0 ${report.exploitability.kevPackages} package(s) carry a KNOWN-EXPLOITED (KEV) advisory${epss} \u2014 prioritise these.`));
|
|
3114
|
+
}
|
|
3115
|
+
out2.push("");
|
|
3116
|
+
for (const plan of report.plans) {
|
|
3117
|
+
out2.push(planBlock(plan, plan.tier === report.recommended));
|
|
3118
|
+
out2.push("");
|
|
3119
|
+
}
|
|
3120
|
+
out2.push(chalk7.bold("Recommendation"));
|
|
3121
|
+
out2.push(` ${chalk7.green(report.plans.find((p) => p.tier === report.recommended)?.label ?? report.recommended)} \u2014 ${report.rationale}`);
|
|
3122
|
+
if (report.unresolved.total) {
|
|
3123
|
+
out2.push("");
|
|
3124
|
+
out2.push(chalk7.red(`\u26A0 ${deltaSummary(report.unresolved)} advisory(ies) have no upgrade path in any plan.`));
|
|
3125
|
+
}
|
|
3126
|
+
return out2.join("\n");
|
|
3127
|
+
}
|
|
3128
|
+
function renderMarkdown(report) {
|
|
3129
|
+
const out2 = [];
|
|
3130
|
+
out2.push("# Vibgrate fix \u2014 upgrade plan");
|
|
3131
|
+
out2.push("");
|
|
3132
|
+
out2.push(
|
|
3133
|
+
`${report.totalCandidates} drifted dependency(ies) analysed \xB7 ${report.deepAnalysis ? "deep (major) analysis on" : "preflight only"}.`
|
|
3134
|
+
);
|
|
3135
|
+
const note = dataNote(report);
|
|
3136
|
+
if (note) {
|
|
3137
|
+
out2.push("");
|
|
3138
|
+
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."}_`);
|
|
3139
|
+
}
|
|
3140
|
+
out2.push("");
|
|
3141
|
+
for (const plan of report.plans) {
|
|
3142
|
+
const rec = plan.tier === report.recommended ? " \u2705 **recommended**" : "";
|
|
3143
|
+
out2.push(`## ${plan.label}${rec}`);
|
|
3144
|
+
out2.push("");
|
|
3145
|
+
out2.push(`_${plan.description}_`);
|
|
3146
|
+
out2.push("");
|
|
3147
|
+
out2.push(`- Risk: **${plan.riskScore}/100** (${plan.confidence} confidence)`);
|
|
3148
|
+
out2.push(`- Upgrades: **${plan.upgrades.length}**`);
|
|
3149
|
+
out2.push(`- Fixes advisories: ${deltaSummary(plan.fixes)}`);
|
|
3150
|
+
if (plan.introduces.total) {
|
|
3151
|
+
out2.push(`- \u26A0 Introduces advisories in target versions: ${deltaSummary(plan.introduces)}`);
|
|
3152
|
+
}
|
|
3153
|
+
out2.push("");
|
|
3154
|
+
if (plan.upgrades.length) {
|
|
3155
|
+
out2.push("| Package | From | To | Kind | Reason |");
|
|
3156
|
+
out2.push("|---|---|---|---|---|");
|
|
3157
|
+
for (const u of plan.upgrades) {
|
|
3158
|
+
out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
|
|
3159
|
+
}
|
|
3160
|
+
out2.push("");
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
out2.push("## Recommendation");
|
|
3164
|
+
out2.push("");
|
|
3165
|
+
const recLabel = report.plans.find((p) => p.tier === report.recommended)?.label ?? report.recommended;
|
|
3166
|
+
out2.push(`**${recLabel}** \u2014 ${report.rationale}`);
|
|
3167
|
+
if (report.unresolved.total) {
|
|
3168
|
+
out2.push("");
|
|
3169
|
+
out2.push(`> \u26A0 ${deltaSummary(report.unresolved)} advisory(ies) have no upgrade path in any plan.`);
|
|
3170
|
+
}
|
|
3171
|
+
return out2.join("\n");
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
// src/reporting/planning/expected-drift.ts
|
|
3175
|
+
function estimateDriftScore(artifact, upgraded) {
|
|
3176
|
+
const projects = JSON.parse(JSON.stringify(artifact.projects ?? []));
|
|
3177
|
+
for (const p of projects) {
|
|
3178
|
+
const buckets = p.dependencyAgeBuckets;
|
|
3179
|
+
for (const dep of p.dependencies ?? []) {
|
|
3180
|
+
if (!upgraded.has(dep.package)) continue;
|
|
3181
|
+
const mb3 = dep.majorsBehind ?? 0;
|
|
3182
|
+
if (mb3 >= 2 && buckets.twoPlusBehind > 0) {
|
|
3183
|
+
buckets.twoPlusBehind--;
|
|
3184
|
+
buckets.current++;
|
|
3185
|
+
} else if (mb3 === 1 && buckets.oneBehind > 0) {
|
|
3186
|
+
buckets.oneBehind--;
|
|
3187
|
+
buckets.current++;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
for (const fw of p.frameworks ?? []) {
|
|
3191
|
+
if (upgraded.has(fw.name)) fw.majorsBehind = 0;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
return computeDriftScore(projects).score;
|
|
3195
|
+
}
|
|
3196
|
+
function pmCommandFor(ecosystem, pkg, version, pm = "npm") {
|
|
3197
|
+
switch (ecosystem) {
|
|
3198
|
+
case "npm":
|
|
3199
|
+
if (pm === "pnpm") return { cmd: "pnpm", args: ["add", `${pkg}@${version}`] };
|
|
3200
|
+
if (pm === "yarn") return { cmd: "yarn", args: ["add", `${pkg}@${version}`] };
|
|
3201
|
+
if (pm === "bun") return { cmd: "bun", args: ["add", `${pkg}@${version}`] };
|
|
3202
|
+
return { cmd: "npm", args: ["install", `${pkg}@${version}`] };
|
|
3203
|
+
case "pypi":
|
|
3204
|
+
return { cmd: "pip", args: ["install", `${pkg}==${version}`] };
|
|
3205
|
+
case "cargo":
|
|
3206
|
+
return { cmd: "cargo", args: ["add", `${pkg}@${version}`] };
|
|
3207
|
+
case "go":
|
|
3208
|
+
return { cmd: "go", args: ["get", `${pkg}@${version.startsWith("v") ? version : `v${version}`}`] };
|
|
3209
|
+
case "composer":
|
|
3210
|
+
return { cmd: "composer", args: ["require", `${pkg}:${version}`] };
|
|
3211
|
+
case "nuget":
|
|
3212
|
+
return { cmd: "dotnet", args: ["add", "package", pkg, "--version", version] };
|
|
3213
|
+
case "dotnet":
|
|
3214
|
+
return { cmd: "dotnet", args: ["add", "package", pkg, "--version", version] };
|
|
3215
|
+
case "dart":
|
|
3216
|
+
case "pub":
|
|
3217
|
+
return { cmd: "dart", args: ["pub", "add", `${pkg}:${version}`] };
|
|
3218
|
+
case "rubygems":
|
|
3219
|
+
case "ruby":
|
|
3220
|
+
return { cmd: "bundle", args: ["update", pkg] };
|
|
3221
|
+
default:
|
|
3222
|
+
return null;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
function defaultRun(cmd, cwd) {
|
|
3226
|
+
const res = spawnSync(cmd.cmd, cmd.args, { cwd, stdio: "inherit", shell: false });
|
|
3227
|
+
if (res.error) return { ok: false, detail: res.error.message };
|
|
3228
|
+
if (res.status !== 0) return { ok: false, detail: `exit ${res.status ?? "signal"}` };
|
|
3229
|
+
return { ok: true };
|
|
3230
|
+
}
|
|
3231
|
+
function applyPlan(rootDir, upgrades, opts = {}) {
|
|
3232
|
+
const run = opts.run ?? defaultRun;
|
|
3233
|
+
const results = [];
|
|
3234
|
+
for (const u of upgrades) {
|
|
3235
|
+
if (!u.to) {
|
|
3236
|
+
results.push({ package: u.package, to: u.to, status: "skipped", detail: "no target version" });
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
const command = pmCommandFor(u.ecosystem, u.package, u.to, opts.packageManager);
|
|
3240
|
+
if (!command) {
|
|
3241
|
+
results.push({ package: u.package, to: u.to, status: "manual", detail: `${u.ecosystem}: update the manifest to ${u.to} manually` });
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
if (opts.dryRun) {
|
|
3245
|
+
results.push({ package: u.package, to: u.to, status: "skipped", detail: `would run: ${command.cmd} ${command.args.join(" ")}` });
|
|
3246
|
+
continue;
|
|
3247
|
+
}
|
|
3248
|
+
const outcome = run(command, rootDir);
|
|
3249
|
+
results.push({
|
|
3250
|
+
package: u.package,
|
|
3251
|
+
to: u.to,
|
|
3252
|
+
status: outcome.ok ? "applied" : "failed",
|
|
3253
|
+
detail: outcome.detail
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
return results;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
// src/reporting/commands/fix.ts
|
|
3260
|
+
var SEVERITY_RANK = { unknown: 0, low: 1, moderate: 2, high: 3, critical: 4 };
|
|
3261
|
+
function ecosystemId(type) {
|
|
3262
|
+
return projectTypeToVulnEcosystem(type) ?? type;
|
|
3263
|
+
}
|
|
3264
|
+
function sourceEcosystem(ecosystem) {
|
|
3265
|
+
if (ecosystem === "npm") return "npm";
|
|
3266
|
+
if (ecosystem === "pypi") return "pypi";
|
|
3267
|
+
return "unknown";
|
|
3268
|
+
}
|
|
3269
|
+
function collectCandidates(artifact) {
|
|
3270
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3271
|
+
const out2 = [];
|
|
3272
|
+
for (const project of artifact.projects ?? []) {
|
|
3273
|
+
const ecosystem = ecosystemId(project.type);
|
|
3274
|
+
for (const dep of project.dependencies ?? []) {
|
|
3275
|
+
const key = `${ecosystem}\0${dep.package}`;
|
|
3276
|
+
if (seen.has(key)) continue;
|
|
3277
|
+
if (!dep.latestStable || dep.latestStable === dep.resolvedVersion) continue;
|
|
3278
|
+
if (dep.drift === "current" || dep.drift === "unknown") continue;
|
|
3279
|
+
seen.add(key);
|
|
3280
|
+
out2.push({
|
|
3281
|
+
package: dep.package,
|
|
3282
|
+
ecosystem,
|
|
3283
|
+
source: sourceEcosystem(ecosystem),
|
|
3284
|
+
from: dep.resolvedVersion,
|
|
3285
|
+
to: dep.latestStable,
|
|
3286
|
+
majorsBehind: dep.majorsBehind,
|
|
3287
|
+
section: dep.section
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
out2.sort((a, b) => a.ecosystem.localeCompare(b.ecosystem) || a.package.localeCompare(b.package));
|
|
3292
|
+
return out2;
|
|
3293
|
+
}
|
|
3294
|
+
var defaultFixScanner = async (rootDir) => {
|
|
3295
|
+
const scratch = path.join(os.tmpdir(), `vg-fix-scan-${process.pid}.json`);
|
|
3296
|
+
const scanOpts = { vibgrateVersion: VERSION, format: "json", out: scratch, quiet: true, concurrency: 8 };
|
|
3297
|
+
const advanced = await loadAdvancedScanHook();
|
|
3298
|
+
return runCoreScan(rootDir, scanOpts, advanced);
|
|
3299
|
+
};
|
|
3300
|
+
async function loadArtifact(rootDir, inFile, scan = defaultFixScanner) {
|
|
3301
|
+
const artifactPath = path.isAbsolute(inFile) ? inFile : path.join(rootDir, inFile);
|
|
3302
|
+
if (await pathExists(artifactPath)) {
|
|
3303
|
+
const staleness = scanStaleness(rootDir, artifactPath);
|
|
3304
|
+
if (!staleness.stale) {
|
|
3305
|
+
return readJsonFile(artifactPath);
|
|
3306
|
+
}
|
|
3307
|
+
console.error(
|
|
3308
|
+
chalk7.dim(
|
|
3309
|
+
`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`
|
|
3310
|
+
)
|
|
3311
|
+
);
|
|
3312
|
+
} else {
|
|
3313
|
+
console.error(chalk7.dim("No scan found \u2014 running a drift scan first (code map skipped)\u2026"));
|
|
3314
|
+
}
|
|
3315
|
+
return scan(rootDir);
|
|
3316
|
+
}
|
|
3317
|
+
async function repositoryIdentity(rootDir, override) {
|
|
3318
|
+
try {
|
|
3319
|
+
const vcs = await detectVcs(rootDir);
|
|
3320
|
+
const fingerprint = await computeRepoFingerprint(rootDir, vcs);
|
|
3321
|
+
const name = override?.trim() || await resolveRepositoryName(rootDir);
|
|
3322
|
+
return { name, vcsSha: fingerprint.vcsSha };
|
|
3323
|
+
} catch {
|
|
3324
|
+
return { name: override?.trim() || void 0 };
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
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) => {
|
|
3328
|
+
const rootDir = path.resolve(targetPath);
|
|
3329
|
+
if (!await pathExists(rootDir)) {
|
|
3330
|
+
console.error(chalk7.red(`Path does not exist: ${rootDir}`));
|
|
3331
|
+
process.exit(1);
|
|
3332
|
+
}
|
|
3333
|
+
const failOn = opts.failOnVulns;
|
|
3334
|
+
if (failOn && !(failOn in SEVERITY_RANK)) {
|
|
3335
|
+
console.error(chalk7.red(`Invalid --fail-on-vulns value '${opts.failOnVulns}'. Use one of: low, moderate, high, critical.`));
|
|
3336
|
+
process.exit(1);
|
|
3337
|
+
}
|
|
3338
|
+
const dsn = resolveDsn(opts.dsn);
|
|
3339
|
+
if (!dsn) {
|
|
3340
|
+
const cli = resolveCliInvocation();
|
|
3341
|
+
console.error(chalk7.red("vg fix needs a Vibgrate login."));
|
|
3342
|
+
console.error(
|
|
3343
|
+
chalk7.dim(
|
|
3344
|
+
`Run "${cli} login" (or set VIBGRATE_DSN / pass --dsn) to analyse upgrades with the hosted planner. See https://vibgrate.com/cli for details.`
|
|
3345
|
+
)
|
|
3346
|
+
);
|
|
3347
|
+
process.exit(1);
|
|
3348
|
+
}
|
|
3349
|
+
const parsed = parseDsn(dsn);
|
|
3350
|
+
if (!parsed) {
|
|
3351
|
+
console.error(chalk7.red('Invalid DSN format. Re-run "vg login" or check VIBGRATE_DSN.'));
|
|
3352
|
+
process.exit(1);
|
|
3353
|
+
}
|
|
3354
|
+
let host;
|
|
3355
|
+
try {
|
|
3356
|
+
host = opts.region ? resolveIngestHost(opts.region) : parsed.host;
|
|
3357
|
+
} catch (e) {
|
|
3358
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
3359
|
+
process.exit(1);
|
|
3360
|
+
}
|
|
3361
|
+
let artifact;
|
|
3362
|
+
try {
|
|
3363
|
+
artifact = await loadArtifact(rootDir, opts.in);
|
|
3364
|
+
} catch (e) {
|
|
3365
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
3366
|
+
process.exit(1);
|
|
3367
|
+
}
|
|
3368
|
+
const raw = collectCandidates(artifact);
|
|
3369
|
+
const usage = analyzeTree(
|
|
3370
|
+
rootDir,
|
|
3371
|
+
raw.filter((c2) => c2.source !== "unknown").map((c2) => ({ name: c2.package, ecosystem: c2.source }))
|
|
3372
|
+
);
|
|
3373
|
+
const candidates = raw.map((c2) => {
|
|
3374
|
+
const u = usage.get(c2.package);
|
|
3375
|
+
return {
|
|
3376
|
+
package: c2.package,
|
|
3377
|
+
ecosystem: c2.ecosystem,
|
|
3378
|
+
currentVersion: c2.from,
|
|
3379
|
+
latestVersion: c2.to,
|
|
3380
|
+
majorsBehind: c2.majorsBehind,
|
|
3381
|
+
section: c2.section,
|
|
3382
|
+
...u && (u.importSites > 0 || u.filesTouched > 0) ? { usage: { importSites: u.importSites, filesTouched: u.filesTouched } } : {},
|
|
3383
|
+
...u && u.contracts.length ? { contracts: u.contracts } : {}
|
|
3384
|
+
};
|
|
3385
|
+
});
|
|
3386
|
+
const request = {
|
|
3387
|
+
cliVersion: VERSION,
|
|
3388
|
+
repository: await repositoryIdentity(rootDir, opts.repositoryName),
|
|
3389
|
+
candidates
|
|
3390
|
+
};
|
|
3391
|
+
let response;
|
|
3392
|
+
try {
|
|
3393
|
+
const { response: httpResponse } = await requestFixPlan({
|
|
3394
|
+
scheme: parsed.scheme,
|
|
3395
|
+
host,
|
|
3396
|
+
keyId: parsed.keyId,
|
|
3397
|
+
secret: parsed.secret,
|
|
3398
|
+
request,
|
|
3399
|
+
timestamp: String(Date.now())
|
|
3400
|
+
});
|
|
3401
|
+
if (!httpResponse.ok) {
|
|
3402
|
+
handleHttpError(httpResponse, parsed.workspaceId, host);
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
response = await parseFixPlanResponse(httpResponse);
|
|
3406
|
+
} catch (e) {
|
|
3407
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3408
|
+
console.error(chalk7.red(`Could not reach the upgrade planner: ${msg}`));
|
|
3409
|
+
console.error(chalk7.dim("Check your connection and try again, or see https://vibgrate.com/help."));
|
|
3410
|
+
process.exit(1);
|
|
3411
|
+
}
|
|
3412
|
+
if (response.status === "error") {
|
|
3413
|
+
console.error(chalk7.red(response.error ?? "The planner returned an error."));
|
|
3414
|
+
if (response.requestId) console.error(chalk7.dim(` (ref ${response.requestId})`));
|
|
3415
|
+
process.exit(1);
|
|
3416
|
+
}
|
|
3417
|
+
const currentDrift = artifact.drift?.score;
|
|
3418
|
+
if (typeof currentDrift === "number") {
|
|
3419
|
+
response.currentDriftScore = currentDrift;
|
|
3420
|
+
for (const plan of response.plans) {
|
|
3421
|
+
const upgraded = new Set(plan.upgrades.map((u) => u.package));
|
|
3422
|
+
const expected = estimateDriftScore(artifact, upgraded);
|
|
3423
|
+
plan.expectedDriftScore = expected;
|
|
3424
|
+
plan.driftDelta = expected - currentDrift;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
emit(response, opts.format);
|
|
3428
|
+
if (opts.format === "text" && opts.apply !== false) {
|
|
3429
|
+
await runApplyFlow(rootDir, artifact, response, opts);
|
|
3430
|
+
}
|
|
3431
|
+
if (failOn) {
|
|
3432
|
+
const recommended = response.plans.find((p) => p.tier === response.recommended);
|
|
3433
|
+
const threshold = SEVERITY_RANK[failOn];
|
|
3434
|
+
let stillOpen = 0;
|
|
3435
|
+
for (const sev of Object.keys(SEVERITY_RANK)) {
|
|
3436
|
+
if (SEVERITY_RANK[sev] < threshold) continue;
|
|
3437
|
+
const open = response.unresolved.bySeverity[sev] ?? 0;
|
|
3438
|
+
const notFixedByRecommended = (response.plans.find((p) => p.tier === "aggressive")?.fixes.bySeverity[sev] ?? 0) - (recommended?.fixes.bySeverity[sev] ?? 0);
|
|
3439
|
+
stillOpen += open + Math.max(0, notFixedByRecommended);
|
|
3440
|
+
}
|
|
3441
|
+
if (stillOpen > 0) {
|
|
3442
|
+
console.error(
|
|
3443
|
+
chalk7.red(`
|
|
3444
|
+
Failing: the recommended plan leaves ${stillOpen} advisory(ies) at/above ${failOn} unresolved.`)
|
|
3445
|
+
);
|
|
3446
|
+
process.exit(2);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
});
|
|
3450
|
+
function handleHttpError(response, workspaceId, host) {
|
|
3451
|
+
const upgradeUrl = `https://${dashHostForIngestHost(host)}/${workspaceId}`;
|
|
3452
|
+
if (response.status === 401 || response.status === 403) {
|
|
3453
|
+
console.error(chalk7.red("Not authorised. Your DSN may be invalid or lack access to this workspace."));
|
|
3454
|
+
console.error(chalk7.dim('Re-run "vg login" or check VIBGRATE_DSN.'));
|
|
3455
|
+
process.exit(1);
|
|
3456
|
+
}
|
|
3457
|
+
if (response.status === 402) {
|
|
3458
|
+
console.error(chalk7.red("vg fix is a paid capability that is not enabled on your current plan."));
|
|
3459
|
+
console.error(chalk7.dim(`Upgrade to enable the hosted upgrade planner: ${upgradeUrl}`));
|
|
3460
|
+
process.exit(1);
|
|
3461
|
+
}
|
|
3462
|
+
if (response.status === 429) {
|
|
3463
|
+
console.error(chalk7.red("Rate limited by the planner. Wait a moment and try again."));
|
|
3464
|
+
process.exit(1);
|
|
3465
|
+
}
|
|
3466
|
+
console.error(chalk7.red(`The planner returned HTTP ${response.status}.`));
|
|
3467
|
+
console.error(chalk7.dim("Try again shortly, or see https://vibgrate.com/help."));
|
|
3468
|
+
process.exit(1);
|
|
3469
|
+
}
|
|
3470
|
+
function emit(report, format) {
|
|
3471
|
+
switch (format) {
|
|
3472
|
+
case "json":
|
|
3473
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3474
|
+
break;
|
|
3475
|
+
case "md":
|
|
3476
|
+
console.log(renderMarkdown(report));
|
|
3477
|
+
break;
|
|
3478
|
+
case "text":
|
|
3479
|
+
default:
|
|
3480
|
+
console.log(renderText(report));
|
|
3481
|
+
break;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
function npmPackageManager(artifact) {
|
|
3485
|
+
for (const p of artifact.projects ?? []) {
|
|
3486
|
+
if ((p.type === "node" || p.type === "typescript") && p.packageManager) {
|
|
3487
|
+
const pm = p.packageManager;
|
|
3488
|
+
if (pm === "pnpm" || pm === "yarn" || pm === "bun" || pm === "npm") return pm;
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
return "npm";
|
|
3492
|
+
}
|
|
3493
|
+
function promptPlanSelection(response) {
|
|
3494
|
+
const plans = response.plans.filter((p) => p.upgrades.length > 0);
|
|
3495
|
+
console.log(chalk7.bold("\nSelect a plan to apply:"));
|
|
3496
|
+
plans.forEach((p, i) => {
|
|
3497
|
+
const rec = p.tier === response.recommended ? chalk7.green(" (recommended)") : "";
|
|
3498
|
+
const drift = typeof p.expectedDriftScore === "number" && typeof response.currentDriftScore === "number" ? ` \xB7 DriftScore ${response.currentDriftScore}\u2192${p.expectedDriftScore}` : "";
|
|
3499
|
+
console.log(` ${i + 1}) ${chalk7.bold(p.label)}${rec} risk ${p.riskScore}/100 \xB7 ${p.upgrades.length} upgrade(s)${drift}`);
|
|
3500
|
+
});
|
|
3501
|
+
const defaultIdx = plans.findIndex((p) => p.tier === response.recommended);
|
|
3502
|
+
const def = defaultIdx >= 0 ? defaultIdx + 1 : 1;
|
|
3503
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3504
|
+
return new Promise((resolve16) => {
|
|
3505
|
+
rl.question(`Plan [1-${plans.length}, default ${def}, q to cancel]: `, (ans) => {
|
|
3506
|
+
rl.close();
|
|
3507
|
+
const t = ans.trim().toLowerCase();
|
|
3508
|
+
if (t === "q" || t === "quit") return resolve16(null);
|
|
3509
|
+
const n = t === "" ? def : Number.parseInt(t, 10);
|
|
3510
|
+
if (!Number.isInteger(n) || n < 1 || n > plans.length) return resolve16(null);
|
|
3511
|
+
resolve16(plans[n - 1].tier);
|
|
3512
|
+
});
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
async function runApplyFlow(rootDir, artifact, response, opts) {
|
|
3516
|
+
const nonEmpty = response.plans.filter((p) => p.upgrades.length > 0);
|
|
3517
|
+
if (nonEmpty.length === 0) {
|
|
3518
|
+
console.log(chalk7.green("\n\u2714 Nothing to upgrade \u2014 every tracked dependency is current."));
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
let chosen;
|
|
3522
|
+
if (opts.plan) {
|
|
3523
|
+
chosen = response.plans.find((p) => p.tier === opts.plan);
|
|
3524
|
+
if (!chosen) {
|
|
3525
|
+
console.error(chalk7.red(`Unknown plan '${opts.plan}'. Use safe, balanced, or aggressive.`));
|
|
3526
|
+
process.exit(1);
|
|
3527
|
+
}
|
|
3528
|
+
} else if (nonEmpty.length === 1) {
|
|
3529
|
+
chosen = nonEmpty[0];
|
|
3530
|
+
} else if (opts.yes) {
|
|
3531
|
+
chosen = response.plans.find((p) => p.tier === response.recommended) ?? nonEmpty[0];
|
|
3532
|
+
} else if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3533
|
+
const tier = await promptPlanSelection(response);
|
|
3534
|
+
if (!tier) {
|
|
3535
|
+
console.log(chalk7.dim("No plan applied."));
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
chosen = response.plans.find((p) => p.tier === tier);
|
|
3539
|
+
} else {
|
|
3540
|
+
console.log(chalk7.dim("\nMultiple plans available \u2014 re-run with --plan <tier> or --yes to apply, or --dry-run to preview."));
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
if (!chosen || chosen.upgrades.length === 0) {
|
|
3544
|
+
console.log(chalk7.dim("Selected plan has no upgrades."));
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
const pm = npmPackageManager(artifact);
|
|
3548
|
+
const results = applyPlan(rootDir, chosen.upgrades, { dryRun: opts.dryRun, packageManager: pm });
|
|
3549
|
+
if (opts.dryRun) {
|
|
3550
|
+
console.log(chalk7.bold(`
|
|
3551
|
+
Dry run \u2014 ${chosen.label} plan (${chosen.upgrades.length} upgrade(s)):`));
|
|
3552
|
+
for (const r of results) {
|
|
3553
|
+
if (r.status === "manual") console.log(chalk7.yellow(` \u26A0 ${r.package}: ${r.detail}`));
|
|
3554
|
+
else console.log(chalk7.dim(` ${r.package}: ${r.detail}`));
|
|
3555
|
+
}
|
|
3556
|
+
if (typeof chosen.expectedDriftScore === "number") {
|
|
3557
|
+
console.log(chalk7.dim(`Expected DriftScore after apply: ~${chosen.expectedDriftScore} (was ${response.currentDriftScore ?? "?"}).`));
|
|
3558
|
+
}
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
const applied = results.filter((r) => r.status === "applied");
|
|
3562
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
3563
|
+
const manual = results.filter((r) => r.status === "manual");
|
|
3564
|
+
console.log(chalk7.bold(`
|
|
3565
|
+
Applied the ${chosen.label} plan:`));
|
|
3566
|
+
console.log(
|
|
3567
|
+
chalk7.green(` \u2714 ${applied.length} upgraded`) + (failed.length ? chalk7.red(` \u2716 ${failed.length} failed`) : "") + (manual.length ? chalk7.yellow(` \u26A0 ${manual.length} need manual work`) : "")
|
|
3568
|
+
);
|
|
3569
|
+
for (const r of failed) console.log(chalk7.red(` \u2716 ${r.package} \u2192 ${r.to}: ${r.detail ?? "failed"}`));
|
|
3570
|
+
for (const r of manual) console.log(chalk7.yellow(` \u26A0 ${r.package} \u2192 ${r.to}: ${r.detail}`));
|
|
3571
|
+
if (typeof chosen.expectedDriftScore === "number" && typeof response.currentDriftScore === "number") {
|
|
3572
|
+
const delta = chosen.expectedDriftScore - response.currentDriftScore;
|
|
3573
|
+
console.log(
|
|
3574
|
+
` Expected DriftScore: ${response.currentDriftScore} \u2192 ~${chosen.expectedDriftScore} (${delta <= 0 ? "" : "+"}${delta}). Re-run \`vg\` to confirm the actual score.`
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
if (failed.length) process.exitCode = 2;
|
|
3578
|
+
}
|
|
2313
3579
|
function formatText(artifact) {
|
|
2314
3580
|
const lines = [];
|
|
2315
|
-
const teal2 =
|
|
2316
|
-
const mint2 =
|
|
3581
|
+
const teal2 = chalk7.hex("#3FB0A4");
|
|
3582
|
+
const mint2 = chalk7.hex("#4FE3C1");
|
|
2317
3583
|
lines.push("");
|
|
2318
3584
|
lines.push(" " + teal2("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u256E") + mint2("\u279C"));
|
|
2319
|
-
lines.push(" " +
|
|
2320
|
-
lines.push(" " +
|
|
3585
|
+
lines.push(" " + chalk7.dim("\u2524") + teal2("\u2502") + " " + mint2("\u25FC") + " " + mint2("\u25FC") + " " + teal2("\u2502") + chalk7.dim("\u251C") + " " + chalk7.bold.white("vibgrate"));
|
|
3586
|
+
lines.push(" " + chalk7.dim("\u2524") + teal2("\u2502") + " " + chalk7.dim("\u2581\u2581") + " " + teal2("\u2502") + chalk7.dim("\u251C") + " " + chalk7.dim(`Code Intelligence Engine v${VERSION}`));
|
|
2321
3587
|
lines.push(" " + teal2("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
2322
3588
|
lines.push("");
|
|
2323
3589
|
lines.push(...titleBox("Vibgrate Drift Report", teal2));
|
|
2324
3590
|
lines.push("");
|
|
2325
3591
|
for (const project of artifact.projects) {
|
|
2326
|
-
lines.push(
|
|
3592
|
+
lines.push(chalk7.bold(` \u2500\u2500 ${project.name} `) + chalk7.dim(`(${project.type}) ${project.path}`));
|
|
2327
3593
|
if (project.runtime) {
|
|
2328
|
-
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ?
|
|
3594
|
+
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? chalk7.yellow(` (${project.runtimeMajorsBehind} major${project.runtimeMajorsBehind > 1 ? "s" : ""} behind)`) : chalk7.green(" (current)");
|
|
2329
3595
|
lines.push(` Runtime: ${project.runtime}${behindStr}`);
|
|
2330
3596
|
}
|
|
2331
3597
|
if (project.targetFramework) {
|
|
@@ -2334,7 +3600,7 @@ function formatText(artifact) {
|
|
|
2334
3600
|
if (project.frameworks.length > 0) {
|
|
2335
3601
|
lines.push(" Frameworks:");
|
|
2336
3602
|
for (const fw of project.frameworks) {
|
|
2337
|
-
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ?
|
|
3603
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? chalk7.green("current") : chalk7.yellow(`${fw.majorsBehind} behind`) : chalk7.dim("unknown");
|
|
2338
3604
|
lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
2339
3605
|
}
|
|
2340
3606
|
}
|
|
@@ -2342,13 +3608,13 @@ function formatText(artifact) {
|
|
|
2342
3608
|
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
2343
3609
|
if (total > 0) {
|
|
2344
3610
|
lines.push(" Dependencies:");
|
|
2345
|
-
lines.push(` ${
|
|
3611
|
+
lines.push(` ${chalk7.green(`${b.current} current`)} ${chalk7.yellow(`${b.oneBehind} 1-behind`)} ${chalk7.red(`${b.twoPlusBehind} 2+ behind`)} ${chalk7.dim(`${b.unknown} unknown`)}`);
|
|
2346
3612
|
}
|
|
2347
3613
|
lines.push("");
|
|
2348
3614
|
}
|
|
2349
3615
|
if (artifact.delta !== void 0) {
|
|
2350
|
-
const deltaStr = artifact.delta > 0 ?
|
|
2351
|
-
lines.push(
|
|
3616
|
+
const deltaStr = artifact.delta > 0 ? chalk7.red(`+${artifact.delta}`) : artifact.delta < 0 ? chalk7.green(`${artifact.delta}`) : chalk7.dim("0");
|
|
3617
|
+
lines.push(chalk7.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
|
|
2352
3618
|
lines.push("");
|
|
2353
3619
|
}
|
|
2354
3620
|
if (artifact.extended) {
|
|
@@ -2359,15 +3625,15 @@ function formatText(artifact) {
|
|
|
2359
3625
|
const warnings = artifact.findings.filter((f) => f.level === "warning");
|
|
2360
3626
|
const notes = artifact.findings.filter((f) => f.level === "note");
|
|
2361
3627
|
const summary = [
|
|
2362
|
-
errors.length > 0 ?
|
|
2363
|
-
warnings.length > 0 ?
|
|
2364
|
-
notes.length > 0 ?
|
|
2365
|
-
].filter(Boolean).join(
|
|
2366
|
-
lines.push(
|
|
3628
|
+
errors.length > 0 ? chalk7.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
|
|
3629
|
+
warnings.length > 0 ? chalk7.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
|
|
3630
|
+
notes.length > 0 ? chalk7.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
|
|
3631
|
+
].filter(Boolean).join(chalk7.dim(", "));
|
|
3632
|
+
lines.push(chalk7.bold.underline(` Findings`) + chalk7.dim(` (${summary})`));
|
|
2367
3633
|
for (const f of artifact.findings) {
|
|
2368
|
-
const icon = f.level === "error" ?
|
|
3634
|
+
const icon = f.level === "error" ? chalk7.red("\u2716") : f.level === "warning" ? chalk7.yellow("\u26A0") : chalk7.blue("\u2139");
|
|
2369
3635
|
lines.push(` ${icon} ${f.message}`);
|
|
2370
|
-
lines.push(
|
|
3636
|
+
lines.push(chalk7.dim(` ${f.ruleId} in ${f.location}`));
|
|
2371
3637
|
}
|
|
2372
3638
|
lines.push("");
|
|
2373
3639
|
}
|
|
@@ -2377,10 +3643,10 @@ function formatText(artifact) {
|
|
|
2377
3643
|
lines.push("");
|
|
2378
3644
|
for (let i = 0; i < actions.length; i++) {
|
|
2379
3645
|
const a = actions[i];
|
|
2380
|
-
const num =
|
|
2381
|
-
lines.push(`${num} ${
|
|
2382
|
-
lines.push(
|
|
2383
|
-
if (a.impact) lines.push(` Impact: ${
|
|
3646
|
+
const num = chalk7.bold.cyan(` ${i + 1}.`);
|
|
3647
|
+
lines.push(`${num} ${chalk7.bold(a.title)}`);
|
|
3648
|
+
lines.push(chalk7.dim(` ${a.explanation}`));
|
|
3649
|
+
if (a.impact) lines.push(` Impact: ${chalk7.green(a.impact)}`);
|
|
2384
3650
|
lines.push("");
|
|
2385
3651
|
}
|
|
2386
3652
|
}
|
|
@@ -2392,30 +3658,30 @@ function formatText(artifact) {
|
|
|
2392
3658
|
lines.push("");
|
|
2393
3659
|
for (const solution of artifact.solutions) {
|
|
2394
3660
|
const solScore = solution.drift?.score;
|
|
2395
|
-
const color = typeof solScore === "number" ? solScore <= 30 ?
|
|
2396
|
-
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) :
|
|
3661
|
+
const color = typeof solScore === "number" ? solScore <= 30 ? chalk7.green : solScore <= 60 ? chalk7.yellow : chalk7.red : chalk7.dim;
|
|
3662
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk7.dim("n/a")}`);
|
|
2397
3663
|
}
|
|
2398
3664
|
lines.push("");
|
|
2399
3665
|
}
|
|
2400
|
-
const scoreColor = artifact.drift.score <= 30 ?
|
|
2401
|
-
lines.push(...titleBox("
|
|
3666
|
+
const scoreColor = artifact.drift.score <= 30 ? chalk7.green : artifact.drift.score <= 60 ? chalk7.yellow : chalk7.red;
|
|
3667
|
+
lines.push(...titleBox("DriftScore Summary"));
|
|
2402
3668
|
lines.push("");
|
|
2403
|
-
lines.push(
|
|
2404
|
-
lines.push(
|
|
2405
|
-
lines.push(
|
|
3669
|
+
lines.push(chalk7.bold(" DriftScore: ") + scoreColor.bold(`${artifact.drift.score}/100`));
|
|
3670
|
+
lines.push(chalk7.bold(" Risk Level: ") + riskBadge(artifact.drift.riskLevel));
|
|
3671
|
+
lines.push(chalk7.bold(" Projects: ") + `${artifact.projects.length}`);
|
|
2406
3672
|
if (artifact.vcs) {
|
|
2407
3673
|
const vcsParts = [artifact.vcs.type];
|
|
2408
3674
|
if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
|
|
2409
|
-
if (artifact.vcs.shortSha) vcsParts.push(
|
|
2410
|
-
lines.push(
|
|
3675
|
+
if (artifact.vcs.shortSha) vcsParts.push(chalk7.dim(artifact.vcs.shortSha));
|
|
3676
|
+
lines.push(chalk7.bold(" VCS: ") + vcsParts.join(" "));
|
|
2411
3677
|
}
|
|
2412
3678
|
lines.push("");
|
|
2413
3679
|
const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
|
|
2414
|
-
lines.push(" " +
|
|
2415
|
-
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) :
|
|
2416
|
-
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) :
|
|
2417
|
-
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) :
|
|
2418
|
-
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) :
|
|
3680
|
+
lines.push(" " + chalk7.bold.underline("Score Breakdown"));
|
|
3681
|
+
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk7.dim("n/a")}`);
|
|
3682
|
+
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk7.dim("n/a")}`);
|
|
3683
|
+
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk7.dim("n/a")}`);
|
|
3684
|
+
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk7.dim("n/a")}`);
|
|
2419
3685
|
lines.push("");
|
|
2420
3686
|
const scannedParts = [`Scanned at ${artifact.timestamp}`];
|
|
2421
3687
|
if (artifact.durationMs !== void 0) {
|
|
@@ -2429,18 +3695,18 @@ function formatText(artifact) {
|
|
|
2429
3695
|
scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
|
|
2430
3696
|
scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
2431
3697
|
}
|
|
2432
|
-
lines.push(
|
|
3698
|
+
lines.push(chalk7.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
2433
3699
|
lines.push("");
|
|
2434
3700
|
return lines.join("\n");
|
|
2435
3701
|
}
|
|
2436
3702
|
function riskBadge(level) {
|
|
2437
3703
|
switch (level) {
|
|
2438
3704
|
case "low":
|
|
2439
|
-
return
|
|
3705
|
+
return chalk7.bgGreen.black(" LOW ");
|
|
2440
3706
|
case "moderate":
|
|
2441
|
-
return
|
|
3707
|
+
return chalk7.bgYellow.black(" MODERATE ");
|
|
2442
3708
|
case "high":
|
|
2443
|
-
return
|
|
3709
|
+
return chalk7.bgRed.white(" HIGH ");
|
|
2444
3710
|
default:
|
|
2445
3711
|
return level;
|
|
2446
3712
|
}
|
|
@@ -2475,11 +3741,11 @@ function formatExtended(ext) {
|
|
|
2475
3741
|
const inv = ext.toolingInventory;
|
|
2476
3742
|
const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
|
|
2477
3743
|
if (categories.length > 0) {
|
|
2478
|
-
lines.push(
|
|
3744
|
+
lines.push(chalk7.bold.underline(" Tech Stack"));
|
|
2479
3745
|
for (const [cat, items] of categories) {
|
|
2480
3746
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2481
|
-
const names = items.map((i) =>
|
|
2482
|
-
lines.push(` ${
|
|
3747
|
+
const names = items.map((i) => chalk7.white(i.name)).join(chalk7.dim(", "));
|
|
3748
|
+
lines.push(` ${chalk7.cyan(label)}: ${names}`);
|
|
2483
3749
|
}
|
|
2484
3750
|
lines.push("");
|
|
2485
3751
|
}
|
|
@@ -2488,14 +3754,14 @@ function formatExtended(ext) {
|
|
|
2488
3754
|
const svc = ext.serviceDependencies;
|
|
2489
3755
|
const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
|
|
2490
3756
|
if (categories.length > 0) {
|
|
2491
|
-
lines.push(
|
|
3757
|
+
lines.push(chalk7.bold.underline(" Services & Integrations"));
|
|
2492
3758
|
for (const [cat, items] of categories) {
|
|
2493
3759
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2494
3760
|
const names = items.map((i) => {
|
|
2495
|
-
const ver = i.version ?
|
|
2496
|
-
return
|
|
2497
|
-
}).join(
|
|
2498
|
-
lines.push(` ${
|
|
3761
|
+
const ver = i.version ? chalk7.dim(` ${i.version}`) : "";
|
|
3762
|
+
return chalk7.white(i.name) + ver;
|
|
3763
|
+
}).join(chalk7.dim(", "));
|
|
3764
|
+
lines.push(` ${chalk7.cyan(label)}: ${names}`);
|
|
2499
3765
|
}
|
|
2500
3766
|
lines.push("");
|
|
2501
3767
|
}
|
|
@@ -2503,19 +3769,19 @@ function formatExtended(ext) {
|
|
|
2503
3769
|
if (ext.breakingChangeExposure) {
|
|
2504
3770
|
const bc = ext.breakingChangeExposure;
|
|
2505
3771
|
if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
|
|
2506
|
-
lines.push(
|
|
2507
|
-
const exposureColor = bc.exposureScore >= 40 ?
|
|
3772
|
+
lines.push(chalk7.bold.underline(" Breaking Change Exposure"));
|
|
3773
|
+
const exposureColor = bc.exposureScore >= 40 ? chalk7.red : bc.exposureScore >= 20 ? chalk7.yellow : chalk7.green;
|
|
2508
3774
|
lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
|
|
2509
3775
|
if (bc.deprecatedPackages.length > 0) {
|
|
2510
|
-
lines.push(` ${
|
|
3776
|
+
lines.push(` ${chalk7.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk7.dim(p)).join(", ")}`);
|
|
2511
3777
|
}
|
|
2512
3778
|
if (bc.legacyPolyfills.length > 0) {
|
|
2513
|
-
lines.push(` ${
|
|
3779
|
+
lines.push(` ${chalk7.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk7.dim(p)).join(", ")}`);
|
|
2514
3780
|
}
|
|
2515
3781
|
if (bc.peerConflictsDetected) {
|
|
2516
|
-
lines.push(` ${
|
|
3782
|
+
lines.push(` ${chalk7.red("\u26A0")} Peer dependency conflicts detected`);
|
|
2517
3783
|
}
|
|
2518
|
-
lines.push(` Recommendation: ${
|
|
3784
|
+
lines.push(` Recommendation: ${chalk7.bold(bc.overallRecommendation)}`);
|
|
2519
3785
|
const projectsWithPlans = bc.projectIntelligence.filter((p) => p.packages.length > 0).slice(0, 3);
|
|
2520
3786
|
if (projectsWithPlans.length > 0) {
|
|
2521
3787
|
lines.push(" Major Upgrade Intelligence:");
|
|
@@ -2531,21 +3797,21 @@ function formatExtended(ext) {
|
|
|
2531
3797
|
}
|
|
2532
3798
|
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
2533
3799
|
const ts = ext.tsModernity;
|
|
2534
|
-
lines.push(
|
|
3800
|
+
lines.push(chalk7.bold.underline(" TypeScript"));
|
|
2535
3801
|
const parts = [];
|
|
2536
3802
|
parts.push(`v${ts.typescriptVersion}`);
|
|
2537
|
-
if (ts.strict === true) parts.push(
|
|
2538
|
-
else if (ts.strict === false) parts.push(
|
|
3803
|
+
if (ts.strict === true) parts.push(chalk7.green("strict \u2714"));
|
|
3804
|
+
else if (ts.strict === false) parts.push(chalk7.yellow("strict \u2716"));
|
|
2539
3805
|
if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
|
|
2540
3806
|
if (ts.target) parts.push(`target: ${ts.target}`);
|
|
2541
|
-
lines.push(` ${parts.join(
|
|
3807
|
+
lines.push(` ${parts.join(chalk7.dim(" \xB7 "))}`);
|
|
2542
3808
|
lines.push("");
|
|
2543
3809
|
}
|
|
2544
3810
|
if (ext.buildDeploy) {
|
|
2545
3811
|
const bd = ext.buildDeploy;
|
|
2546
3812
|
const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
|
|
2547
3813
|
if (hasSomething) {
|
|
2548
|
-
lines.push(
|
|
3814
|
+
lines.push(chalk7.bold.underline(" Build & Deploy"));
|
|
2549
3815
|
if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
|
|
2550
3816
|
if (bd.docker.dockerfileCount > 0) {
|
|
2551
3817
|
lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
|
|
@@ -2558,42 +3824,42 @@ function formatExtended(ext) {
|
|
|
2558
3824
|
}
|
|
2559
3825
|
if (ext.uiPurpose) {
|
|
2560
3826
|
const up = ext.uiPurpose;
|
|
2561
|
-
lines.push(
|
|
2562
|
-
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") :
|
|
2563
|
-
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ?
|
|
3827
|
+
lines.push(chalk7.bold.underline(" Product Purpose Signals"));
|
|
3828
|
+
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk7.dim("unknown")}`);
|
|
3829
|
+
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk7.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
|
|
2564
3830
|
const top = up.topEvidence.slice(0, 8);
|
|
2565
3831
|
if (top.length > 0) {
|
|
2566
3832
|
lines.push(" Top Signals:");
|
|
2567
3833
|
for (const item of top) {
|
|
2568
|
-
lines.push(` - [${item.kind}] ${item.value} ${
|
|
3834
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk7.dim(`(${item.file})`)}`);
|
|
2569
3835
|
}
|
|
2570
3836
|
}
|
|
2571
3837
|
if (up.unknownSignals.length > 0) {
|
|
2572
3838
|
lines.push(" Unknowns:");
|
|
2573
3839
|
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
2574
|
-
lines.push(` - ${
|
|
3840
|
+
lines.push(` - ${chalk7.yellow(u)}`);
|
|
2575
3841
|
}
|
|
2576
3842
|
}
|
|
2577
3843
|
lines.push("");
|
|
2578
3844
|
}
|
|
2579
3845
|
if (ext.securityPosture) {
|
|
2580
3846
|
const sec = ext.securityPosture;
|
|
2581
|
-
lines.push(
|
|
3847
|
+
lines.push(chalk7.bold.underline(" Security Posture"));
|
|
2582
3848
|
const checks = [];
|
|
2583
|
-
checks.push(sec.lockfilePresent ?
|
|
2584
|
-
checks.push(sec.gitignoreCoversEnv ?
|
|
2585
|
-
checks.push(sec.gitignoreCoversNodeModules ?
|
|
2586
|
-
if (sec.multipleLockfileTypes) checks.push(
|
|
2587
|
-
if (sec.envFilesTracked) checks.push(
|
|
2588
|
-
lines.push(` ${checks.join(
|
|
3849
|
+
checks.push(sec.lockfilePresent ? chalk7.green("Lockfile \u2714") : chalk7.red("Lockfile \u2716"));
|
|
3850
|
+
checks.push(sec.gitignoreCoversEnv ? chalk7.green(".env \u2714") : chalk7.red(".env \u2716"));
|
|
3851
|
+
checks.push(sec.gitignoreCoversNodeModules ? chalk7.green("node_modules \u2714") : chalk7.yellow("node_modules \u2716"));
|
|
3852
|
+
if (sec.multipleLockfileTypes) checks.push(chalk7.yellow("Multiple lockfiles \u26A0"));
|
|
3853
|
+
if (sec.envFilesTracked) checks.push(chalk7.red("Env files tracked \u2716"));
|
|
3854
|
+
lines.push(` ${checks.join(chalk7.dim(" \xB7 "))}`);
|
|
2589
3855
|
lines.push("");
|
|
2590
3856
|
}
|
|
2591
3857
|
if (ext.platformMatrix) {
|
|
2592
3858
|
const pm = ext.platformMatrix;
|
|
2593
3859
|
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
2594
|
-
lines.push(
|
|
3860
|
+
lines.push(chalk7.bold.underline(" Platform"));
|
|
2595
3861
|
if (pm.nativeModules.length > 0) {
|
|
2596
|
-
lines.push(` Native modules: ${pm.nativeModules.map((m) =>
|
|
3862
|
+
lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk7.dim(m)).join(", ")}`);
|
|
2597
3863
|
}
|
|
2598
3864
|
if (pm.osAssumptions.length > 0) {
|
|
2599
3865
|
lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
|
|
@@ -2603,25 +3869,25 @@ function formatExtended(ext) {
|
|
|
2603
3869
|
}
|
|
2604
3870
|
if (ext.codeQuality) {
|
|
2605
3871
|
const cq = ext.codeQuality;
|
|
2606
|
-
lines.push(
|
|
2607
|
-
lines.push(` Files: ${
|
|
3872
|
+
lines.push(chalk7.bold.underline(" Code Quality"));
|
|
3873
|
+
lines.push(` Files: ${chalk7.white(`${cq.filesAnalyzed}`)} \xB7 Functions: ${chalk7.white(`${cq.functionsAnalyzed}`)} \xB7 Avg complexity: ${chalk7.white(`${cq.avgCyclomaticComplexity}`)} \xB7 Avg length: ${chalk7.white(`${cq.avgFunctionLength}`)} lines`);
|
|
2608
3874
|
lines.push(` Max nesting: ${cq.maxNestingDepth} \xB7 Circular deps: ${cq.circularDependencies} \xB7 Dead code: ${cq.deadCodePercent}%`);
|
|
2609
3875
|
if (cq.godFiles.length > 0) {
|
|
2610
3876
|
const preview = cq.godFiles.slice(0, 3).map((f) => `${f.path} (${f.lines} lines)`).join(", ");
|
|
2611
|
-
lines.push(` ${
|
|
3877
|
+
lines.push(` ${chalk7.yellow("God files")}: ${preview}`);
|
|
2612
3878
|
}
|
|
2613
3879
|
lines.push("");
|
|
2614
3880
|
}
|
|
2615
3881
|
if (ext.dependencyGraph) {
|
|
2616
3882
|
const dg = ext.dependencyGraph;
|
|
2617
3883
|
if (dg.lockfileType) {
|
|
2618
|
-
lines.push(
|
|
2619
|
-
lines.push(` ${dg.lockfileType}: ${
|
|
3884
|
+
lines.push(chalk7.bold.underline(" Dependency Graph"));
|
|
3885
|
+
lines.push(` ${dg.lockfileType}: ${chalk7.white(`${dg.totalUnique}`)} unique, ${chalk7.white(`${dg.totalInstalled}`)} installed`);
|
|
2620
3886
|
if (dg.duplicatedPackages.length > 0) {
|
|
2621
|
-
lines.push(` ${
|
|
3887
|
+
lines.push(` ${chalk7.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
|
|
2622
3888
|
}
|
|
2623
3889
|
if (dg.phantomDependencies.length > 0) {
|
|
2624
|
-
lines.push(` ${
|
|
3890
|
+
lines.push(` ${chalk7.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
|
|
2625
3891
|
}
|
|
2626
3892
|
lines.push("");
|
|
2627
3893
|
}
|
|
@@ -2632,13 +3898,13 @@ function formatArchitectureDiagram(arch) {
|
|
|
2632
3898
|
const lines = [];
|
|
2633
3899
|
lines.push(...titleBox("Architecture Layers"));
|
|
2634
3900
|
lines.push("");
|
|
2635
|
-
lines.push(
|
|
2636
|
-
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ?
|
|
3901
|
+
lines.push(chalk7.bold(" Archetype: ") + `${arch.archetype}` + chalk7.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
|
|
3902
|
+
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk7.dim(` (${arch.unclassified} unclassified)`) : ""));
|
|
2637
3903
|
lines.push("");
|
|
2638
3904
|
if (arch.layers.length > 0) {
|
|
2639
3905
|
for (const layer of arch.layers) {
|
|
2640
|
-
const risk = layer.riskLevel === "none" ?
|
|
2641
|
-
lines.push(` ${
|
|
3906
|
+
const risk = layer.riskLevel === "none" ? chalk7.dim("none") : layer.riskLevel === "low" ? chalk7.green("low") : layer.riskLevel === "moderate" ? chalk7.yellow("moderate") : chalk7.red("high");
|
|
3907
|
+
lines.push(` ${chalk7.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
|
|
2642
3908
|
}
|
|
2643
3909
|
lines.push("");
|
|
2644
3910
|
}
|
|
@@ -2900,7 +4166,7 @@ function formatMarkdown(artifact) {
|
|
|
2900
4166
|
lines.push("");
|
|
2901
4167
|
lines.push(`| Metric | Value |`);
|
|
2902
4168
|
lines.push(`|--------|-------|`);
|
|
2903
|
-
lines.push(`| **
|
|
4169
|
+
lines.push(`| **DriftScore** | ${artifact.drift.score}/100 _(lower is better; 0 = no drift)_ |`);
|
|
2904
4170
|
lines.push(`| **Risk Level** | ${artifact.drift.riskLevel.toUpperCase()} |`);
|
|
2905
4171
|
lines.push(`| **Projects** | ${artifact.projects.length} |`);
|
|
2906
4172
|
const scannedMeta = [artifact.timestamp];
|
|
@@ -3007,10 +4273,10 @@ function formatMarkdown(artifact) {
|
|
|
3007
4273
|
|
|
3008
4274
|
// src/reporting/commands/report.ts
|
|
3009
4275
|
var reportCommand = new Command("report").description("Generate a drift report from a scan artifact").option("--in <file>", "Input artifact file", ".vibgrate/scan_result.json").option("--format <format>", "Output format (md|text|json)", "text").action(async (opts) => {
|
|
3010
|
-
const artifactPath =
|
|
4276
|
+
const artifactPath = path.resolve(opts.in);
|
|
3011
4277
|
if (!await pathExists(artifactPath)) {
|
|
3012
|
-
console.error(
|
|
3013
|
-
console.error(
|
|
4278
|
+
console.error(chalk7.red(`Artifact not found: ${artifactPath}`));
|
|
4279
|
+
console.error(chalk7.dim('Run "vibgrate scan" first to generate a scan artifact.'));
|
|
3014
4280
|
process.exit(1);
|
|
3015
4281
|
}
|
|
3016
4282
|
const artifact = await readJsonFile(artifactPath);
|
|
@@ -3053,10 +4319,10 @@ function openUrl(url) {
|
|
|
3053
4319
|
}
|
|
3054
4320
|
|
|
3055
4321
|
// src/reporting/commands/login.ts
|
|
3056
|
-
var delay = (ms) => new Promise((
|
|
4322
|
+
var delay = (ms) => new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
3057
4323
|
function isInside(dir, file) {
|
|
3058
|
-
const
|
|
3059
|
-
return !!
|
|
4324
|
+
const rel3 = path.relative(dir, file);
|
|
4325
|
+
return !!rel3 && !rel3.startsWith("..") && !path.isAbsolute(rel3);
|
|
3060
4326
|
}
|
|
3061
4327
|
var loginCommand = new Command("login").description("Authenticate the CLI with your Vibgrate workspace via the browser").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", `Data residency region (${availableRegionIds().join(", ")})`, "us").option("--no-browser", "Do not attempt to open a browser automatically").option(
|
|
3062
4328
|
"--local",
|
|
@@ -3066,7 +4332,7 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3066
4332
|
try {
|
|
3067
4333
|
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
3068
4334
|
} catch (e) {
|
|
3069
|
-
console.error(
|
|
4335
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
3070
4336
|
process.exit(1);
|
|
3071
4337
|
}
|
|
3072
4338
|
const base = `https://${ingestHost}/v1/auth/device`;
|
|
@@ -3078,28 +4344,28 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3078
4344
|
body: "{}"
|
|
3079
4345
|
});
|
|
3080
4346
|
if (!res.ok) {
|
|
3081
|
-
console.error(
|
|
4347
|
+
console.error(chalk7.red(`Failed to start login (HTTP ${res.status}).`));
|
|
3082
4348
|
process.exit(1);
|
|
3083
4349
|
}
|
|
3084
4350
|
start = await res.json();
|
|
3085
4351
|
} catch (e) {
|
|
3086
|
-
console.error(
|
|
4352
|
+
console.error(chalk7.red(`Could not reach ${ingestHost}: ${e instanceof Error ? e.message : String(e)}`));
|
|
3087
4353
|
process.exit(1);
|
|
3088
4354
|
}
|
|
3089
4355
|
console.log("");
|
|
3090
4356
|
console.log("To finish signing in, open this URL and approve the request:");
|
|
3091
4357
|
console.log("");
|
|
3092
|
-
console.log(" " +
|
|
4358
|
+
console.log(" " + chalk7.cyan(start.verificationUri));
|
|
3093
4359
|
console.log("");
|
|
3094
|
-
console.log(" Your code: " +
|
|
4360
|
+
console.log(" Your code: " + chalk7.bold(start.userCode));
|
|
3095
4361
|
console.log("");
|
|
3096
4362
|
if (opts.browser) {
|
|
3097
4363
|
const opened = openUrl(start.verificationUriComplete);
|
|
3098
4364
|
if (opened) {
|
|
3099
|
-
console.log(
|
|
4365
|
+
console.log(chalk7.dim("Opening your browser\u2026 (if it does not open, use the URL above)"));
|
|
3100
4366
|
}
|
|
3101
4367
|
}
|
|
3102
|
-
console.log(
|
|
4368
|
+
console.log(chalk7.dim("Waiting for approval\u2026"));
|
|
3103
4369
|
const intervalMs = Math.max(2, start.interval || 5) * 1e3;
|
|
3104
4370
|
const deadline = Date.now() + (start.expiresIn || 900) * 1e3;
|
|
3105
4371
|
while (Date.now() < deadline) {
|
|
@@ -3127,7 +4393,7 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3127
4393
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3128
4394
|
};
|
|
3129
4395
|
} else {
|
|
3130
|
-
console.log(
|
|
4396
|
+
console.log(chalk7.dim("Setting up your workspace\u2026"));
|
|
3131
4397
|
try {
|
|
3132
4398
|
const provisioned = await createWorkspaceDsn({
|
|
3133
4399
|
region: opts.region,
|
|
@@ -3142,10 +4408,10 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3142
4408
|
};
|
|
3143
4409
|
} catch (e) {
|
|
3144
4410
|
console.error(
|
|
3145
|
-
|
|
4411
|
+
chalk7.red("\u2716 Signed in, but workspace setup failed: ") + (e instanceof Error ? e.message : String(e))
|
|
3146
4412
|
);
|
|
3147
4413
|
console.error(
|
|
3148
|
-
|
|
4414
|
+
chalk7.dim(' Finish setup with "vibgrate dsn create --workspace new".')
|
|
3149
4415
|
);
|
|
3150
4416
|
process.exit(1);
|
|
3151
4417
|
}
|
|
@@ -3154,40 +4420,40 @@ var loginCommand = new Command("login").description("Authenticate the CLI with y
|
|
|
3154
4420
|
writeStoredCredentials(creds, storeOpts);
|
|
3155
4421
|
const credsFile = credentialsPath(storeOpts);
|
|
3156
4422
|
console.log("");
|
|
3157
|
-
console.log(
|
|
4423
|
+
console.log(chalk7.green("\u2714") + " Logged in.");
|
|
3158
4424
|
if (creds.workspaceId) {
|
|
3159
|
-
console.log(" Workspace: " +
|
|
4425
|
+
console.log(" Workspace: " + chalk7.bold(creds.workspaceId));
|
|
3160
4426
|
}
|
|
3161
|
-
console.log(
|
|
4427
|
+
console.log(chalk7.dim(` Credentials saved to ${credsFile}`));
|
|
3162
4428
|
try {
|
|
3163
4429
|
const root = findGitRoot();
|
|
3164
4430
|
if (root && isInside(root, credsFile)) {
|
|
3165
4431
|
const res = ensureGitignored(gitignoreEntryForCredentials(root, credsFile), root);
|
|
3166
4432
|
if (res.status === "created") {
|
|
3167
|
-
console.log(
|
|
4433
|
+
console.log(chalk7.dim(` Created .gitignore and ignored ${res.entry}`));
|
|
3168
4434
|
} else if (res.status === "added") {
|
|
3169
|
-
console.log(
|
|
4435
|
+
console.log(chalk7.dim(` Added ${res.entry} to .gitignore`));
|
|
3170
4436
|
}
|
|
3171
4437
|
}
|
|
3172
4438
|
} catch {
|
|
3173
4439
|
}
|
|
3174
|
-
console.log(
|
|
4440
|
+
console.log(chalk7.dim(` You can now run "${resolveCliInvocation()} scan --push".`));
|
|
3175
4441
|
return;
|
|
3176
4442
|
}
|
|
3177
4443
|
if (token.status === "access_denied") {
|
|
3178
|
-
console.error(
|
|
4444
|
+
console.error(chalk7.red("\u2716 Login was denied in the browser."));
|
|
3179
4445
|
process.exit(1);
|
|
3180
4446
|
}
|
|
3181
4447
|
if (token.status === "expired" || token.status === "invalid") {
|
|
3182
|
-
console.error(
|
|
4448
|
+
console.error(chalk7.red('\u2716 Login request expired. Run "vibgrate login" again.'));
|
|
3183
4449
|
process.exit(1);
|
|
3184
4450
|
}
|
|
3185
4451
|
if (token.status === "error") {
|
|
3186
|
-
console.error(
|
|
4452
|
+
console.error(chalk7.red(`\u2716 ${token.error ?? "Login failed."}`));
|
|
3187
4453
|
process.exit(1);
|
|
3188
4454
|
}
|
|
3189
4455
|
}
|
|
3190
|
-
console.error(
|
|
4456
|
+
console.error(chalk7.red('\u2716 Timed out waiting for approval. Run "vibgrate login" again.'));
|
|
3191
4457
|
process.exit(1);
|
|
3192
4458
|
});
|
|
3193
4459
|
|
|
@@ -3208,23 +4474,23 @@ function unsetEnvCommand(varName, env = {}) {
|
|
|
3208
4474
|
var logoutCommand = new Command("logout").description("Clear stored Vibgrate login credentials").action(() => {
|
|
3209
4475
|
const cleared = clearStoredCredentials();
|
|
3210
4476
|
if (cleared) {
|
|
3211
|
-
console.log(
|
|
4477
|
+
console.log(chalk7.green("\u2714") + " Logged out. Stored credentials removed.");
|
|
3212
4478
|
} else {
|
|
3213
|
-
console.log(
|
|
4479
|
+
console.log(chalk7.dim(`No stored credentials found at ${credentialsPath()}.`));
|
|
3214
4480
|
}
|
|
3215
4481
|
if (process.env.VIBGRATE_DSN) {
|
|
3216
4482
|
console.log("");
|
|
3217
4483
|
console.log(
|
|
3218
|
-
|
|
4484
|
+
chalk7.yellow("\u26A0") + " VIBGRATE_DSN is still set in your environment, so commands will keep using it."
|
|
3219
4485
|
);
|
|
3220
4486
|
console.log(
|
|
3221
|
-
|
|
4487
|
+
chalk7.dim(` Unset it to fully sign out: ${unsetEnvCommand("VIBGRATE_DSN")}`)
|
|
3222
4488
|
);
|
|
3223
4489
|
}
|
|
3224
4490
|
});
|
|
3225
4491
|
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
3226
|
-
var CACHE_DIR =
|
|
3227
|
-
var CACHE_FILE =
|
|
4492
|
+
var CACHE_DIR = path.join(os.homedir(), ".vibgrate");
|
|
4493
|
+
var CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
|
|
3228
4494
|
async function fetchLatestVersion() {
|
|
3229
4495
|
try {
|
|
3230
4496
|
const controller = new AbortController();
|
|
@@ -3251,8 +4517,8 @@ async function fetchLatestVersion() {
|
|
|
3251
4517
|
}
|
|
3252
4518
|
async function writeCache(data) {
|
|
3253
4519
|
try {
|
|
3254
|
-
await
|
|
3255
|
-
await
|
|
4520
|
+
await fs14.mkdir(CACHE_DIR, { recursive: true });
|
|
4521
|
+
await fs14.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
3256
4522
|
} catch {
|
|
3257
4523
|
}
|
|
3258
4524
|
}
|
|
@@ -3285,16 +4551,28 @@ function getGlobalUpdateCommand(pm, pkg, version) {
|
|
|
3285
4551
|
}
|
|
3286
4552
|
}
|
|
3287
4553
|
async function detectPackageManager(cwd) {
|
|
3288
|
-
if (await pathExists(
|
|
3289
|
-
if (await pathExists(
|
|
3290
|
-
if (await pathExists(
|
|
4554
|
+
if (await pathExists(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4555
|
+
if (await pathExists(path.join(cwd, "bun.lockb"))) return "bun";
|
|
4556
|
+
if (await pathExists(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
3291
4557
|
return "npm";
|
|
3292
4558
|
}
|
|
3293
|
-
function
|
|
4559
|
+
async function detectWorkspaceRoot(cwd) {
|
|
4560
|
+
if (await pathExists(path.join(cwd, "pnpm-workspace.yaml"))) return true;
|
|
4561
|
+
try {
|
|
4562
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
4563
|
+
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
4564
|
+
const pkg = JSON.parse(raw);
|
|
4565
|
+
return pkg.workspaces != null;
|
|
4566
|
+
} catch {
|
|
4567
|
+
return false;
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
function getInstallCommand(pm, pkg, version, isDev, opts = {}) {
|
|
3294
4571
|
const spec = `${pkg}@${version}`;
|
|
4572
|
+
const ws = opts.workspaceRoot && pm === "pnpm" ? " -w" : "";
|
|
3295
4573
|
switch (pm) {
|
|
3296
4574
|
case "pnpm":
|
|
3297
|
-
return isDev ? `pnpm add -D ${spec}` : `pnpm add ${spec}`;
|
|
4575
|
+
return isDev ? `pnpm add${ws} -D ${spec}` : `pnpm add${ws} ${spec}`;
|
|
3298
4576
|
case "yarn":
|
|
3299
4577
|
return isDev ? `yarn add --dev ${spec}` : `yarn add ${spec}`;
|
|
3300
4578
|
case "bun":
|
|
@@ -3306,7 +4584,7 @@ function getInstallCommand(pm, pkg, version, isDev) {
|
|
|
3306
4584
|
}
|
|
3307
4585
|
async function isDevDependency(cwd) {
|
|
3308
4586
|
try {
|
|
3309
|
-
const pkgPath =
|
|
4587
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
3310
4588
|
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
3311
4589
|
const pkg = JSON.parse(raw);
|
|
3312
4590
|
return Boolean(pkg.devDependencies?.["@vibgrate/cli"]);
|
|
@@ -3314,45 +4592,72 @@ async function isDevDependency(cwd) {
|
|
|
3314
4592
|
return true;
|
|
3315
4593
|
}
|
|
3316
4594
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
console.log(
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
4595
|
+
async function confirmWorkspaceRoot(pm) {
|
|
4596
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) return false;
|
|
4597
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4598
|
+
return new Promise((resolve16) => {
|
|
4599
|
+
rl.question(chalk7.yellow(`Update the ${pm} workspace root with -w? [y/N]: `), (ans) => {
|
|
4600
|
+
rl.close();
|
|
4601
|
+
const t = ans.trim().toLowerCase();
|
|
4602
|
+
resolve16(t === "y" || t === "yes");
|
|
4603
|
+
});
|
|
4604
|
+
});
|
|
4605
|
+
}
|
|
4606
|
+
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").option("-y, --yes", "Skip confirmation prompts (e.g. installing at a workspace root)").option("-w, --workspace-root", "Allow updating the pnpm workspace root (implies --yes for that prompt)").action(
|
|
4607
|
+
async (opts) => {
|
|
4608
|
+
console.log(chalk7.dim(`Current version: ${VERSION}`));
|
|
4609
|
+
console.log(chalk7.dim("Checking npm registry..."));
|
|
4610
|
+
const latest = await fetchLatestVersion();
|
|
4611
|
+
if (!latest) {
|
|
4612
|
+
console.error(chalk7.red("Could not reach the npm registry. Check your network connection."));
|
|
4613
|
+
process.exit(1);
|
|
4614
|
+
}
|
|
4615
|
+
const semver2 = await import('semver');
|
|
4616
|
+
if (!semver2.gt(latest, VERSION)) {
|
|
4617
|
+
console.log(chalk7.green("\u2714") + ` You are on the latest version (${VERSION}).`);
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
console.log(chalk7.yellow(`Update available: ${VERSION} \u2192 ${latest}`));
|
|
4621
|
+
if (opts.check) {
|
|
4622
|
+
console.log(chalk7.dim('Run "vg update" to install.'));
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
const cwd = process.cwd();
|
|
4626
|
+
const globalPm = detectGlobalInstall();
|
|
4627
|
+
const isGlobal = opts.global || globalPm !== null;
|
|
4628
|
+
const pm = opts.pm || (globalPm ?? await detectPackageManager(cwd));
|
|
4629
|
+
let cmd;
|
|
4630
|
+
if (isGlobal) {
|
|
4631
|
+
cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
|
|
4632
|
+
console.log(chalk7.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
4633
|
+
} else {
|
|
4634
|
+
const isDev = await isDevDependency(cwd);
|
|
4635
|
+
let workspaceRoot = false;
|
|
4636
|
+
if (pm === "pnpm" && await detectWorkspaceRoot(cwd)) {
|
|
4637
|
+
console.log(
|
|
4638
|
+
chalk7.yellow("Detected a pnpm workspace root.") + chalk7.dim(" Installing here needs the -w flag and updates the root package.json.")
|
|
4639
|
+
);
|
|
4640
|
+
const proceed = opts.yes || opts.workspaceRoot || await confirmWorkspaceRoot(pm);
|
|
4641
|
+
if (!proceed) {
|
|
4642
|
+
const rootCmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev, { workspaceRoot: true });
|
|
4643
|
+
console.log(chalk7.dim(`Skipped. To update the workspace root, run: ${rootCmd}`));
|
|
4644
|
+
console.log(chalk7.dim('or re-run "vg update --yes" (or --workspace-root) to let vg do it.'));
|
|
4645
|
+
return;
|
|
4646
|
+
}
|
|
4647
|
+
workspaceRoot = true;
|
|
4648
|
+
}
|
|
4649
|
+
cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev, { workspaceRoot });
|
|
4650
|
+
console.log(chalk7.dim(`Using ${pm}: ${cmd}`));
|
|
4651
|
+
}
|
|
4652
|
+
try {
|
|
4653
|
+
execSync(cmd, { cwd, stdio: "inherit" });
|
|
4654
|
+
console.log(chalk7.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
|
|
4655
|
+
} catch {
|
|
4656
|
+
console.error(chalk7.red(`Update failed. Run manually: ${cmd}`));
|
|
4657
|
+
process.exit(1);
|
|
4658
|
+
}
|
|
3354
4659
|
}
|
|
3355
|
-
|
|
4660
|
+
);
|
|
3356
4661
|
var OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
3357
4662
|
var VEX_STATUSES = ["not_affected", "affected", "fixed", "under_investigation"];
|
|
3358
4663
|
var VEX_JUSTIFICATIONS = [
|
|
@@ -3442,9 +4747,9 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3442
4747
|
try {
|
|
3443
4748
|
const statements = [];
|
|
3444
4749
|
if (opts.from) {
|
|
3445
|
-
const fromPath =
|
|
4750
|
+
const fromPath = path.resolve(opts.from);
|
|
3446
4751
|
if (!await pathExists(fromPath)) {
|
|
3447
|
-
process.stderr.write(
|
|
4752
|
+
process.stderr.write(chalk7.red(`Statements file not found: ${fromPath}
|
|
3448
4753
|
`));
|
|
3449
4754
|
process.exit(1);
|
|
3450
4755
|
}
|
|
@@ -3460,9 +4765,9 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3460
4765
|
});
|
|
3461
4766
|
const json2 = JSON.stringify(doc, null, 2);
|
|
3462
4767
|
if (opts.out) {
|
|
3463
|
-
await writeTextFile(
|
|
4768
|
+
await writeTextFile(path.resolve(opts.out), json2 + "\n");
|
|
3464
4769
|
process.stderr.write(
|
|
3465
|
-
|
|
4770
|
+
chalk7.green("\u2714") + ` Wrote OpenVEX document (${doc.statements.length} statement(s)) to ${path.resolve(opts.out)}
|
|
3466
4771
|
`
|
|
3467
4772
|
);
|
|
3468
4773
|
} else {
|
|
@@ -3470,7 +4775,7 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
3470
4775
|
}
|
|
3471
4776
|
} catch (err) {
|
|
3472
4777
|
if (err instanceof VexValidationError) {
|
|
3473
|
-
process.stderr.write(
|
|
4778
|
+
process.stderr.write(chalk7.red(`VEX validation error: ${err.message}
|
|
3474
4779
|
`));
|
|
3475
4780
|
process.exit(1);
|
|
3476
4781
|
}
|
|
@@ -3645,9 +4950,9 @@ function formatDeltaText(base, current) {
|
|
|
3645
4950
|
return lines.join("\n");
|
|
3646
4951
|
}
|
|
3647
4952
|
async function readArtifactOrExit(filePath) {
|
|
3648
|
-
const absolutePath =
|
|
4953
|
+
const absolutePath = path.resolve(filePath);
|
|
3649
4954
|
if (!await pathExists(absolutePath)) {
|
|
3650
|
-
console.error(
|
|
4955
|
+
console.error(chalk7.red(`Artifact not found: ${absolutePath}`));
|
|
3651
4956
|
process.exit(1);
|
|
3652
4957
|
}
|
|
3653
4958
|
return readJsonFile(absolutePath);
|
|
@@ -3656,14 +4961,14 @@ var exportCommand = new Command("export").description("Export scan artifact as S
|
|
|
3656
4961
|
const artifact = await readArtifactOrExit(opts.in);
|
|
3657
4962
|
const format = opts.format.toLowerCase();
|
|
3658
4963
|
if (format !== "cyclonedx" && format !== "spdx") {
|
|
3659
|
-
console.error(
|
|
4964
|
+
console.error(chalk7.red("Invalid SBOM format. Use cyclonedx or spdx."));
|
|
3660
4965
|
process.exit(1);
|
|
3661
4966
|
}
|
|
3662
4967
|
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
3663
4968
|
const body = JSON.stringify(sbom, null, 2);
|
|
3664
4969
|
if (opts.out) {
|
|
3665
|
-
await writeTextFile(
|
|
3666
|
-
console.log(
|
|
4970
|
+
await writeTextFile(path.resolve(opts.out), body);
|
|
4971
|
+
console.log(chalk7.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
3667
4972
|
} else {
|
|
3668
4973
|
console.log(body);
|
|
3669
4974
|
}
|
|
@@ -3673,8 +4978,8 @@ var deltaCommand = new Command("delta").description("Show SBOM delta between two
|
|
|
3673
4978
|
const current = await readArtifactOrExit(opts.to);
|
|
3674
4979
|
const report = formatDeltaText(base, current);
|
|
3675
4980
|
if (opts.out) {
|
|
3676
|
-
await writeTextFile(
|
|
3677
|
-
console.log(
|
|
4981
|
+
await writeTextFile(path.resolve(opts.out), report);
|
|
4982
|
+
console.log(chalk7.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
3678
4983
|
} else {
|
|
3679
4984
|
console.log(report);
|
|
3680
4985
|
}
|
|
@@ -3685,13 +4990,13 @@ var sbomCommand = new Command("sbom").description("Supply-chain evidence: SBOM e
|
|
|
3685
4990
|
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
3686
4991
|
"build",
|
|
3687
4992
|
"status",
|
|
3688
|
-
"verify",
|
|
3689
4993
|
"ask",
|
|
3690
4994
|
"embed",
|
|
3691
4995
|
"show",
|
|
3692
4996
|
"path",
|
|
3693
4997
|
"tree",
|
|
3694
4998
|
"impact",
|
|
4999
|
+
"unknowns",
|
|
3695
5000
|
"tests",
|
|
3696
5001
|
"facts",
|
|
3697
5002
|
"guide",
|
|
@@ -3717,6 +5022,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
3717
5022
|
// Drift-reporting verbs (merged from the Vibgrate CLI).
|
|
3718
5023
|
"init",
|
|
3719
5024
|
"scan",
|
|
5025
|
+
"fix",
|
|
3720
5026
|
"baseline",
|
|
3721
5027
|
"report",
|
|
3722
5028
|
"login",
|
|
@@ -3739,7 +5045,6 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3739
5045
|
});
|
|
3740
5046
|
registerBuild(program);
|
|
3741
5047
|
registerStatus(program);
|
|
3742
|
-
registerVerify(program);
|
|
3743
5048
|
registerAsk(program);
|
|
3744
5049
|
registerEmbed(program);
|
|
3745
5050
|
registerShow(program);
|
|
@@ -3747,6 +5052,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3747
5052
|
registerTree(program);
|
|
3748
5053
|
registerInsights(program);
|
|
3749
5054
|
registerImpact(program);
|
|
5055
|
+
registerUnknowns(program);
|
|
3750
5056
|
registerServe(program);
|
|
3751
5057
|
registerInstall(program);
|
|
3752
5058
|
registerShare(program);
|
|
@@ -3764,6 +5070,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
3764
5070
|
registerBundle(program);
|
|
3765
5071
|
program.addCommand(initCommand);
|
|
3766
5072
|
program.addCommand(scanCommand);
|
|
5073
|
+
program.addCommand(fixCommand);
|
|
3767
5074
|
program.addCommand(baselineCommand);
|
|
3768
5075
|
program.addCommand(reportCommand);
|
|
3769
5076
|
program.addCommand(loginCommand);
|
|
@@ -3828,11 +5135,19 @@ function dispatch(argv, cwd) {
|
|
|
3828
5135
|
}
|
|
3829
5136
|
const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
|
|
3830
5137
|
if (looksLikeQuestion) return ["ask", ...args];
|
|
3831
|
-
if (
|
|
5138
|
+
if (fs5.existsSync(path.resolve(cwd, first))) {
|
|
3832
5139
|
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
3833
5140
|
}
|
|
5141
|
+
const moved = MOVED_COMMANDS[first];
|
|
5142
|
+
if (moved) {
|
|
5143
|
+
throw new CliError(`\`vg ${first}\` has moved to \`${moved}\``, ExitCode.USAGE_ERROR);
|
|
5144
|
+
}
|
|
3834
5145
|
return ["ask", ...args];
|
|
3835
5146
|
}
|
|
5147
|
+
var MOVED_COMMANDS = {
|
|
5148
|
+
verify: "vg build --verify",
|
|
5149
|
+
attest: "vg build --attest"
|
|
5150
|
+
};
|
|
3836
5151
|
function findFirstPositional(args) {
|
|
3837
5152
|
for (let i = 0; i < args.length; i++) {
|
|
3838
5153
|
const a = args[i];
|
|
@@ -3848,7 +5163,6 @@ async function main(argv = process.argv) {
|
|
|
3848
5163
|
const raw = argv.slice(2);
|
|
3849
5164
|
if (process.env.NO_COLOR || raw.includes("--no-color")) disableColor();
|
|
3850
5165
|
const cwd = readCwd(raw);
|
|
3851
|
-
const dispatched = dispatch(raw, cwd);
|
|
3852
5166
|
const program = buildProgram();
|
|
3853
5167
|
program.exitOverride((err) => {
|
|
3854
5168
|
throw err;
|
|
@@ -3858,6 +5172,7 @@ async function main(argv = process.argv) {
|
|
|
3858
5172
|
writeOut: (str) => process.stdout.write(str)
|
|
3859
5173
|
});
|
|
3860
5174
|
try {
|
|
5175
|
+
const dispatched = dispatch(raw, cwd);
|
|
3861
5176
|
await program.parseAsync(dispatched, { from: "user" });
|
|
3862
5177
|
} catch (err) {
|
|
3863
5178
|
handleError(err);
|
|
@@ -3865,9 +5180,9 @@ async function main(argv = process.argv) {
|
|
|
3865
5180
|
}
|
|
3866
5181
|
function readCwd(raw) {
|
|
3867
5182
|
const i = raw.findIndex((a) => a === "-C" || a === "--cwd");
|
|
3868
|
-
if (i >= 0 && raw[i + 1]) return
|
|
5183
|
+
if (i >= 0 && raw[i + 1]) return path.resolve(raw[i + 1]);
|
|
3869
5184
|
const eq = raw.find((a) => a.startsWith("--cwd="));
|
|
3870
|
-
if (eq) return
|
|
5185
|
+
if (eq) return path.resolve(eq.slice("--cwd=".length));
|
|
3871
5186
|
return process.cwd();
|
|
3872
5187
|
}
|
|
3873
5188
|
function handleError(err) {
|