trawly 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +155 -0
- package/SECURITY.md +18 -0
- package/dist/cli.js +1316 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
5
|
+
import kleur4 from "kleur";
|
|
6
|
+
|
|
7
|
+
// src/commands/add.ts
|
|
8
|
+
import kleur from "kleur";
|
|
9
|
+
|
|
10
|
+
// src/sources/osv.ts
|
|
11
|
+
var OSV_QUERYBATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
12
|
+
var OSV_VULN_URL = "https://api.osv.dev/v1/vulns";
|
|
13
|
+
var QUERY_CHUNK_SIZE = 500;
|
|
14
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
15
|
+
var MAX_RETRIES = 2;
|
|
16
|
+
function dedupeForQuery(packages) {
|
|
17
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const pkg of packages) {
|
|
20
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
21
|
+
if (seen.has(key)) continue;
|
|
22
|
+
seen.add(key);
|
|
23
|
+
out.push({ name: pkg.name, version: pkg.version });
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
async function queryOsv(packages, deps = {}) {
|
|
28
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
29
|
+
const unique = dedupeForQuery(packages);
|
|
30
|
+
if (unique.length === 0) return [];
|
|
31
|
+
const idsByPackage = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const chunk of chunked(unique, QUERY_CHUNK_SIZE)) {
|
|
33
|
+
const body = {
|
|
34
|
+
queries: chunk.map((q) => ({
|
|
35
|
+
package: { ecosystem: "npm", name: q.name },
|
|
36
|
+
version: q.version
|
|
37
|
+
}))
|
|
38
|
+
};
|
|
39
|
+
const res = await postJson(
|
|
40
|
+
fetchImpl,
|
|
41
|
+
OSV_QUERYBATCH_URL,
|
|
42
|
+
body
|
|
43
|
+
);
|
|
44
|
+
res.results.forEach((result, i) => {
|
|
45
|
+
const q = chunk[i];
|
|
46
|
+
if (!q) return;
|
|
47
|
+
const key = `${q.name}@${q.version}`;
|
|
48
|
+
if (!result.vulns || result.vulns.length === 0) return;
|
|
49
|
+
const ids = idsByPackage.get(key) ?? /* @__PURE__ */ new Set();
|
|
50
|
+
for (const v of result.vulns) ids.add(v.id);
|
|
51
|
+
idsByPackage.set(key, ids);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
55
|
+
for (const ids of idsByPackage.values()) {
|
|
56
|
+
for (const id of ids) allIds.add(id);
|
|
57
|
+
}
|
|
58
|
+
const detailsById = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const id of allIds) {
|
|
60
|
+
try {
|
|
61
|
+
const detail = await getJson(
|
|
62
|
+
fetchImpl,
|
|
63
|
+
`${OSV_VULN_URL}/${encodeURIComponent(id)}`
|
|
64
|
+
);
|
|
65
|
+
detailsById.set(id, detail);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const findings = [];
|
|
70
|
+
for (const pkg of packages) {
|
|
71
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
72
|
+
const ids = idsByPackage.get(key);
|
|
73
|
+
if (!ids) continue;
|
|
74
|
+
for (const id of ids) {
|
|
75
|
+
const detail = detailsById.get(id);
|
|
76
|
+
findings.push(buildFinding(pkg, id, detail));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
function buildFinding(pkg, id, detail) {
|
|
82
|
+
const severity = detail ? parseSeverity(detail) : "unknown";
|
|
83
|
+
const summary = detail?.summary ?? detail?.details ?? id;
|
|
84
|
+
return {
|
|
85
|
+
id,
|
|
86
|
+
source: "osv",
|
|
87
|
+
type: "vulnerability",
|
|
88
|
+
severity,
|
|
89
|
+
packageName: pkg.name,
|
|
90
|
+
installedVersion: pkg.version,
|
|
91
|
+
summary: truncate(summary, 240),
|
|
92
|
+
url: pickAdvisoryUrl(detail) ?? `https://osv.dev/vulnerability/${id}`,
|
|
93
|
+
fixedVersions: detail ? collectFixedVersions(detail, pkg.name) : [],
|
|
94
|
+
affectedPaths: [pkg.path]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function parseSeverity(detail) {
|
|
98
|
+
const dbSpecific = detail.database_specific?.severity?.toLowerCase();
|
|
99
|
+
if (dbSpecific === "critical" || dbSpecific === "high" || dbSpecific === "moderate" || dbSpecific === "low") {
|
|
100
|
+
return dbSpecific;
|
|
101
|
+
}
|
|
102
|
+
if (dbSpecific === "medium") return "moderate";
|
|
103
|
+
const cvss = detail.severity?.find((s) => s.type?.startsWith("CVSS_"));
|
|
104
|
+
if (cvss) {
|
|
105
|
+
const score = parseCvssScore(cvss.score);
|
|
106
|
+
if (score === void 0) return "unknown";
|
|
107
|
+
if (score >= 9) return "critical";
|
|
108
|
+
if (score >= 7) return "high";
|
|
109
|
+
if (score >= 4) return "moderate";
|
|
110
|
+
if (score > 0) return "low";
|
|
111
|
+
}
|
|
112
|
+
return "unknown";
|
|
113
|
+
}
|
|
114
|
+
function parseCvssScore(vector) {
|
|
115
|
+
const direct = Number.parseFloat(vector);
|
|
116
|
+
if (!Number.isNaN(direct) && vector.trim() !== "") return direct;
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
function pickAdvisoryUrl(detail) {
|
|
120
|
+
if (!detail?.references) return void 0;
|
|
121
|
+
const advisory = detail.references.find((r) => r.type === "ADVISORY");
|
|
122
|
+
return advisory?.url ?? detail.references[0]?.url;
|
|
123
|
+
}
|
|
124
|
+
function collectFixedVersions(detail, packageName) {
|
|
125
|
+
const out = /* @__PURE__ */ new Set();
|
|
126
|
+
for (const aff of detail.affected ?? []) {
|
|
127
|
+
if (aff.package?.name && aff.package.name !== packageName) continue;
|
|
128
|
+
for (const range of aff.ranges ?? []) {
|
|
129
|
+
for (const event of range.events ?? []) {
|
|
130
|
+
if (event.fixed) out.add(event.fixed);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [...out];
|
|
135
|
+
}
|
|
136
|
+
function* chunked(items, size) {
|
|
137
|
+
for (let i = 0; i < items.length; i += size) {
|
|
138
|
+
yield items.slice(i, i + size);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function postJson(fetchImpl, url, body) {
|
|
142
|
+
return withRetry(async () => {
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetchImpl(url, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "content-type": "application/json" },
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
signal: controller.signal
|
|
151
|
+
});
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
throw new HttpError(`OSV ${res.status}: ${res.statusText}`, res.status);
|
|
154
|
+
}
|
|
155
|
+
return await res.json();
|
|
156
|
+
} finally {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async function getJson(fetchImpl, url) {
|
|
162
|
+
return withRetry(async () => {
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetchImpl(url, { signal: controller.signal });
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw new HttpError(`OSV ${res.status}: ${res.statusText}`, res.status);
|
|
169
|
+
}
|
|
170
|
+
return await res.json();
|
|
171
|
+
} finally {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
var HttpError = class extends Error {
|
|
177
|
+
constructor(message, status) {
|
|
178
|
+
super(message);
|
|
179
|
+
this.status = status;
|
|
180
|
+
}
|
|
181
|
+
status;
|
|
182
|
+
};
|
|
183
|
+
async function withRetry(fn) {
|
|
184
|
+
let lastErr;
|
|
185
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
186
|
+
try {
|
|
187
|
+
return await fn();
|
|
188
|
+
} catch (err) {
|
|
189
|
+
lastErr = err;
|
|
190
|
+
if (!isRetryable(err) || attempt === MAX_RETRIES) break;
|
|
191
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
throw lastErr;
|
|
195
|
+
}
|
|
196
|
+
function isRetryable(err) {
|
|
197
|
+
if (err instanceof HttpError) return err.status >= 500;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
function truncate(s, max) {
|
|
201
|
+
if (s.length <= max) return s;
|
|
202
|
+
return `${s.slice(0, max - 1)}\u2026`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/scanner.ts
|
|
206
|
+
import { existsSync, statSync } from "fs";
|
|
207
|
+
import { resolve as resolve2, join } from "path";
|
|
208
|
+
|
|
209
|
+
// src/extractors/npm-package-lock.ts
|
|
210
|
+
import { readFileSync } from "fs";
|
|
211
|
+
import { resolve } from "path";
|
|
212
|
+
function parseNpmPackageLock(filePath) {
|
|
213
|
+
const absolute = resolve(filePath);
|
|
214
|
+
const raw = readFileSync(absolute, "utf8");
|
|
215
|
+
let parsed;
|
|
216
|
+
try {
|
|
217
|
+
parsed = JSON.parse(raw);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Failed to parse ${absolute}: ${err.message}`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (parsed.lockfileVersion !== 2 && parsed.lockfileVersion !== 3) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Unsupported npm lockfileVersion ${String(
|
|
226
|
+
parsed.lockfileVersion
|
|
227
|
+
)} in ${absolute}. Only v2 and v3 are supported.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const packages = parsed.packages;
|
|
231
|
+
if (!packages || typeof packages !== "object") {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Lockfile ${absolute} has no "packages" map; cannot extract installed versions.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const directDeps = collectDirectDependencyNames(packages[""] ?? {});
|
|
237
|
+
const instances = [];
|
|
238
|
+
for (const [path, entry] of Object.entries(packages)) {
|
|
239
|
+
if (path === "") continue;
|
|
240
|
+
if (entry.link) continue;
|
|
241
|
+
const name = packagePathToName(path);
|
|
242
|
+
if (!name) continue;
|
|
243
|
+
if (!entry.version) continue;
|
|
244
|
+
instances.push({
|
|
245
|
+
name,
|
|
246
|
+
version: entry.version,
|
|
247
|
+
ecosystem: "npm",
|
|
248
|
+
path,
|
|
249
|
+
direct: directDeps.has(name) && isTopLevelInstance(path),
|
|
250
|
+
dev: Boolean(entry.dev || entry.devOptional),
|
|
251
|
+
optional: Boolean(entry.optional || entry.devOptional),
|
|
252
|
+
resolved: entry.resolved,
|
|
253
|
+
integrity: entry.integrity
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return instances;
|
|
257
|
+
}
|
|
258
|
+
function collectDirectDependencyNames(rootEntry) {
|
|
259
|
+
const names = /* @__PURE__ */ new Set();
|
|
260
|
+
for (const key of [
|
|
261
|
+
"dependencies",
|
|
262
|
+
"devDependencies",
|
|
263
|
+
"optionalDependencies",
|
|
264
|
+
"peerDependencies"
|
|
265
|
+
]) {
|
|
266
|
+
const block = rootEntry[key];
|
|
267
|
+
if (!block) continue;
|
|
268
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
269
|
+
}
|
|
270
|
+
return names;
|
|
271
|
+
}
|
|
272
|
+
function packagePathToName(path) {
|
|
273
|
+
const marker = "node_modules/";
|
|
274
|
+
const idx = path.lastIndexOf(marker);
|
|
275
|
+
if (idx === -1) return null;
|
|
276
|
+
const tail = path.slice(idx + marker.length);
|
|
277
|
+
if (!tail) return null;
|
|
278
|
+
if (tail.startsWith("@")) {
|
|
279
|
+
const firstSlash = tail.indexOf("/");
|
|
280
|
+
if (firstSlash === -1) return null;
|
|
281
|
+
const secondSlash = tail.indexOf("/", firstSlash + 1);
|
|
282
|
+
return secondSlash === -1 ? tail : tail.slice(0, secondSlash);
|
|
283
|
+
}
|
|
284
|
+
const next = tail.indexOf("/");
|
|
285
|
+
return next === -1 ? tail : tail.slice(0, next);
|
|
286
|
+
}
|
|
287
|
+
function isTopLevelInstance(path) {
|
|
288
|
+
const first = path.indexOf("node_modules/");
|
|
289
|
+
if (first === -1) return false;
|
|
290
|
+
return path.indexOf("node_modules/", first + 1) === -1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/types.ts
|
|
294
|
+
var SEVERITY_RANK = {
|
|
295
|
+
critical: 4,
|
|
296
|
+
high: 3,
|
|
297
|
+
moderate: 2,
|
|
298
|
+
low: 1,
|
|
299
|
+
unknown: 0
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/scanner.ts
|
|
303
|
+
async function scanProject(options = {}) {
|
|
304
|
+
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
305
|
+
const lockfilePath = options.lockfile ? resolve2(cwd, options.lockfile) : detectLockfile(cwd);
|
|
306
|
+
if (!lockfilePath) {
|
|
307
|
+
throw new ScanInputError(
|
|
308
|
+
`No npm lockfile found in ${cwd}. Pass --lockfile or run in a directory with package-lock.json.`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return scanLockfile({
|
|
312
|
+
lockfilePath,
|
|
313
|
+
includeDev: options.includeDev,
|
|
314
|
+
prodOnly: options.prodOnly,
|
|
315
|
+
fetchImpl: options.fetchImpl
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async function scanLockfile(options) {
|
|
319
|
+
const { lockfilePath } = options;
|
|
320
|
+
if (!existsSync(lockfilePath)) {
|
|
321
|
+
throw new ScanInputError(`Lockfile does not exist: ${lockfilePath}`);
|
|
322
|
+
}
|
|
323
|
+
const stat = statSync(lockfilePath);
|
|
324
|
+
if (!stat.isFile()) {
|
|
325
|
+
throw new ScanInputError(`Lockfile path is not a file: ${lockfilePath}`);
|
|
326
|
+
}
|
|
327
|
+
const allInstances = parseNpmPackageLock(lockfilePath);
|
|
328
|
+
const instances = filterInstances(allInstances, options);
|
|
329
|
+
const errors = [];
|
|
330
|
+
let findings = [];
|
|
331
|
+
try {
|
|
332
|
+
findings = await queryOsv(instances, { fetchImpl: options.fetchImpl });
|
|
333
|
+
} catch (err) {
|
|
334
|
+
errors.push({
|
|
335
|
+
message: "Failed to query OSV advisory database",
|
|
336
|
+
cause: err.message
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
findings.sort(compareFindings);
|
|
340
|
+
return {
|
|
341
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
342
|
+
packagesScanned: instances.length,
|
|
343
|
+
findings,
|
|
344
|
+
summary: summarize(findings),
|
|
345
|
+
errors
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
var ScanInputError = class extends Error {
|
|
349
|
+
constructor(message) {
|
|
350
|
+
super(message);
|
|
351
|
+
this.name = "ScanInputError";
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
function detectLockfile(cwd) {
|
|
355
|
+
const candidate = join(cwd, "package-lock.json");
|
|
356
|
+
return existsSync(candidate) ? candidate : void 0;
|
|
357
|
+
}
|
|
358
|
+
function filterInstances(instances, options) {
|
|
359
|
+
const includeDev = options.prodOnly ? false : options.includeDev !== false;
|
|
360
|
+
if (includeDev) return instances;
|
|
361
|
+
return instances.filter((p) => !p.dev);
|
|
362
|
+
}
|
|
363
|
+
function compareFindings(a, b) {
|
|
364
|
+
const sev = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
|
|
365
|
+
if (sev !== 0) return sev;
|
|
366
|
+
if (a.packageName !== b.packageName) {
|
|
367
|
+
return a.packageName.localeCompare(b.packageName);
|
|
368
|
+
}
|
|
369
|
+
if (a.installedVersion !== b.installedVersion) {
|
|
370
|
+
return a.installedVersion.localeCompare(b.installedVersion);
|
|
371
|
+
}
|
|
372
|
+
return a.id.localeCompare(b.id);
|
|
373
|
+
}
|
|
374
|
+
function summarize(findings) {
|
|
375
|
+
const summary = {
|
|
376
|
+
critical: 0,
|
|
377
|
+
high: 0,
|
|
378
|
+
moderate: 0,
|
|
379
|
+
low: 0,
|
|
380
|
+
unknown: 0
|
|
381
|
+
};
|
|
382
|
+
for (const f of findings) summary[f.severity]++;
|
|
383
|
+
return summary;
|
|
384
|
+
}
|
|
385
|
+
function meetsThreshold(findings, threshold) {
|
|
386
|
+
if (threshold === "none") return false;
|
|
387
|
+
const min = SEVERITY_RANK[threshold];
|
|
388
|
+
return findings.some((f) => SEVERITY_RANK[f.severity] >= min);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/installer/pm-detect.ts
|
|
392
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
393
|
+
import { join as join2 } from "path";
|
|
394
|
+
var LOCKFILES = [
|
|
395
|
+
{ file: "pnpm-lock.yaml", pm: "pnpm" },
|
|
396
|
+
{ file: "yarn.lock", pm: "yarn" },
|
|
397
|
+
{ file: "bun.lockb", pm: "bun" },
|
|
398
|
+
{ file: "bun.lock", pm: "bun" },
|
|
399
|
+
{ file: "package-lock.json", pm: "npm" },
|
|
400
|
+
{ file: "npm-shrinkwrap.json", pm: "npm" }
|
|
401
|
+
];
|
|
402
|
+
function detectPackageManager(opts = {}) {
|
|
403
|
+
if (opts.override) return opts.override;
|
|
404
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
405
|
+
const fromField = readPackageManagerField(cwd);
|
|
406
|
+
if (fromField) return fromField;
|
|
407
|
+
for (const { file, pm } of LOCKFILES) {
|
|
408
|
+
if (existsSync2(join2(cwd, file))) return pm;
|
|
409
|
+
}
|
|
410
|
+
return "npm";
|
|
411
|
+
}
|
|
412
|
+
function readPackageManagerField(cwd) {
|
|
413
|
+
const pkgPath = join2(cwd, "package.json");
|
|
414
|
+
if (!existsSync2(pkgPath)) return void 0;
|
|
415
|
+
try {
|
|
416
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
417
|
+
if (typeof pkg.packageManager !== "string") return void 0;
|
|
418
|
+
const name = pkg.packageManager.split("@")[0];
|
|
419
|
+
if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") {
|
|
420
|
+
return name;
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
return void 0;
|
|
425
|
+
}
|
|
426
|
+
function buildAddCommand(pm, packages, flags) {
|
|
427
|
+
switch (pm) {
|
|
428
|
+
case "npm":
|
|
429
|
+
return { bin: "npm", args: ["install", ...flags, ...packages] };
|
|
430
|
+
case "pnpm":
|
|
431
|
+
return { bin: "pnpm", args: ["add", ...flags, ...packages] };
|
|
432
|
+
case "yarn":
|
|
433
|
+
return { bin: "yarn", args: ["add", ...flags, ...packages] };
|
|
434
|
+
case "bun":
|
|
435
|
+
return { bin: "bun", args: ["add", ...flags, ...packages] };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function buildInstallCommand(pm, flags) {
|
|
439
|
+
return { bin: pm, args: ["install", ...flags] };
|
|
440
|
+
}
|
|
441
|
+
function buildRemoveCommand(pm, packages, flags) {
|
|
442
|
+
switch (pm) {
|
|
443
|
+
case "npm":
|
|
444
|
+
return { bin: "npm", args: ["uninstall", ...flags, ...packages] };
|
|
445
|
+
case "pnpm":
|
|
446
|
+
return { bin: "pnpm", args: ["remove", ...flags, ...packages] };
|
|
447
|
+
case "yarn":
|
|
448
|
+
return { bin: "yarn", args: ["remove", ...flags, ...packages] };
|
|
449
|
+
case "bun":
|
|
450
|
+
return { bin: "bun", args: ["remove", ...flags, ...packages] };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/installer/runner.ts
|
|
455
|
+
import { spawn } from "child_process";
|
|
456
|
+
function runPackageManager(cmd, opts = {}) {
|
|
457
|
+
return new Promise((resolve3, reject) => {
|
|
458
|
+
const child = spawn(cmd.bin, cmd.args, {
|
|
459
|
+
cwd: opts.cwd,
|
|
460
|
+
stdio: "inherit",
|
|
461
|
+
shell: process.platform === "win32"
|
|
462
|
+
});
|
|
463
|
+
child.on("error", reject);
|
|
464
|
+
child.on("close", (code) => resolve3(code ?? 0));
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/installer/spec-parser.ts
|
|
469
|
+
var URL_PROTOCOLS = ["http:", "https:", "git:", "git+ssh:", "git+https:", "git+http:"];
|
|
470
|
+
function parseSpec(raw) {
|
|
471
|
+
const trimmed = raw.trim();
|
|
472
|
+
if (trimmed === "") {
|
|
473
|
+
return { raw, name: "", unsupported: "invalid" };
|
|
474
|
+
}
|
|
475
|
+
if (trimmed.startsWith("file:")) return { raw, name: trimmed, unsupported: "file" };
|
|
476
|
+
if (trimmed.startsWith("workspace:")) {
|
|
477
|
+
return { raw, name: trimmed, unsupported: "workspace" };
|
|
478
|
+
}
|
|
479
|
+
if (URL_PROTOCOLS.some((p) => trimmed.startsWith(p))) {
|
|
480
|
+
const reason = trimmed.includes("git") ? "git" : "url";
|
|
481
|
+
return { raw, name: trimmed, unsupported: reason };
|
|
482
|
+
}
|
|
483
|
+
if (/^[^@/].*@npm:/.test(trimmed) || /^@[^/]+\/[^@]+@npm:/.test(trimmed)) {
|
|
484
|
+
return { raw, name: trimmed, unsupported: "alias" };
|
|
485
|
+
}
|
|
486
|
+
if (trimmed.startsWith("@")) {
|
|
487
|
+
const slash = trimmed.indexOf("/");
|
|
488
|
+
if (slash === -1) return { raw, name: trimmed, unsupported: "invalid" };
|
|
489
|
+
const rest = trimmed.slice(slash + 1);
|
|
490
|
+
const at2 = rest.indexOf("@");
|
|
491
|
+
if (at2 === -1) {
|
|
492
|
+
return { raw, name: trimmed };
|
|
493
|
+
}
|
|
494
|
+
const subname = rest.slice(0, at2);
|
|
495
|
+
const requested2 = rest.slice(at2 + 1);
|
|
496
|
+
if (subname === "" || requested2 === "") {
|
|
497
|
+
return { raw, name: trimmed, unsupported: "invalid" };
|
|
498
|
+
}
|
|
499
|
+
return { raw, name: `${trimmed.slice(0, slash)}/${subname}`, requested: requested2 };
|
|
500
|
+
}
|
|
501
|
+
const at = trimmed.indexOf("@");
|
|
502
|
+
if (at === -1) return { raw, name: trimmed };
|
|
503
|
+
const name = trimmed.slice(0, at);
|
|
504
|
+
const requested = trimmed.slice(at + 1);
|
|
505
|
+
if (name === "" || requested === "") {
|
|
506
|
+
return { raw, name: trimmed, unsupported: "invalid" };
|
|
507
|
+
}
|
|
508
|
+
return { raw, name, requested };
|
|
509
|
+
}
|
|
510
|
+
function partitionArgs(args) {
|
|
511
|
+
const specs = [];
|
|
512
|
+
const flags = [];
|
|
513
|
+
for (const arg of args) {
|
|
514
|
+
if (arg.startsWith("-")) {
|
|
515
|
+
flags.push(arg);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
specs.push(parseSpec(arg));
|
|
519
|
+
}
|
|
520
|
+
return { specs, flags };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/installer/version-resolver.ts
|
|
524
|
+
var REGISTRY_URL = "https://registry.npmjs.org";
|
|
525
|
+
var REQUEST_TIMEOUT_MS2 = 15e3;
|
|
526
|
+
var EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][\w.+-]+)?$/;
|
|
527
|
+
var RANGE_CHARS_RE = /[\^~><=|\s*x]/i;
|
|
528
|
+
async function resolveVersion(name, requested, deps = {}) {
|
|
529
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
530
|
+
const registry = deps.registryUrl ?? REGISTRY_URL;
|
|
531
|
+
const packument = await fetchPackument(fetchImpl, registry, name);
|
|
532
|
+
const distTags = packument["dist-tags"] ?? {};
|
|
533
|
+
const latest = distTags.latest;
|
|
534
|
+
if (!requested) {
|
|
535
|
+
if (!latest) {
|
|
536
|
+
throw new VersionResolveError(
|
|
537
|
+
`Package ${name} has no "latest" dist-tag in the registry.`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return { version: latest, source: "dist-tag", requested: "latest" };
|
|
541
|
+
}
|
|
542
|
+
if (EXACT_VERSION_RE.test(requested)) {
|
|
543
|
+
if (packument.versions && !packument.versions[requested]) {
|
|
544
|
+
throw new VersionResolveError(
|
|
545
|
+
`Version ${requested} of ${name} is not published.`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return { version: requested, source: "exact" };
|
|
549
|
+
}
|
|
550
|
+
if (!RANGE_CHARS_RE.test(requested) && distTags[requested]) {
|
|
551
|
+
return {
|
|
552
|
+
version: distTags[requested],
|
|
553
|
+
source: "dist-tag",
|
|
554
|
+
requested
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (!latest) {
|
|
558
|
+
throw new VersionResolveError(
|
|
559
|
+
`Cannot resolve ${name}@${requested}: no "latest" dist-tag available to fall back on.`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return { version: latest, source: "fallback-latest", requested };
|
|
563
|
+
}
|
|
564
|
+
var VersionResolveError = class extends Error {
|
|
565
|
+
constructor(message) {
|
|
566
|
+
super(message);
|
|
567
|
+
this.name = "VersionResolveError";
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
async function fetchPackument(fetchImpl, registry, name) {
|
|
571
|
+
const url = `${registry.replace(/\/$/, "")}/${encodePackageName(name)}`;
|
|
572
|
+
const controller = new AbortController();
|
|
573
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
574
|
+
try {
|
|
575
|
+
const res = await fetchImpl(url, {
|
|
576
|
+
signal: controller.signal,
|
|
577
|
+
headers: { accept: "application/json" }
|
|
578
|
+
});
|
|
579
|
+
if (res.status === 404) {
|
|
580
|
+
throw new VersionResolveError(`Package ${name} not found in registry.`);
|
|
581
|
+
}
|
|
582
|
+
if (!res.ok) {
|
|
583
|
+
throw new VersionResolveError(
|
|
584
|
+
`Registry ${res.status} for ${name}: ${res.statusText}`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return await res.json();
|
|
588
|
+
} finally {
|
|
589
|
+
clearTimeout(timer);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function encodePackageName(name) {
|
|
593
|
+
if (name.startsWith("@")) {
|
|
594
|
+
const slash = name.indexOf("/");
|
|
595
|
+
if (slash !== -1) {
|
|
596
|
+
return `${encodeURIComponent(name.slice(0, slash))}%2F${encodeURIComponent(name.slice(slash + 1))}`;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return encodeURIComponent(name);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/commands/add.ts
|
|
603
|
+
async function runAdd(args, options) {
|
|
604
|
+
const { specs, flags } = partitionArgs(args);
|
|
605
|
+
const skipped = [];
|
|
606
|
+
const resolvable = [];
|
|
607
|
+
for (const spec of specs) {
|
|
608
|
+
if (spec.unsupported) {
|
|
609
|
+
skipped.push({ spec, reason: spec.unsupported });
|
|
610
|
+
} else {
|
|
611
|
+
resolvable.push(spec);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const resolved = [];
|
|
615
|
+
const errored = [];
|
|
616
|
+
await Promise.all(
|
|
617
|
+
resolvable.map(async (spec) => {
|
|
618
|
+
try {
|
|
619
|
+
const r = await resolveVersion(spec.name, spec.requested, {
|
|
620
|
+
fetchImpl: options.fetchImpl
|
|
621
|
+
});
|
|
622
|
+
resolved.push({ spec, resolved: r, findings: [] });
|
|
623
|
+
} catch (err) {
|
|
624
|
+
const message = err instanceof VersionResolveError ? err.message : `Registry error: ${err.message}`;
|
|
625
|
+
errored.push({ spec, message });
|
|
626
|
+
}
|
|
627
|
+
})
|
|
628
|
+
);
|
|
629
|
+
let findings = [];
|
|
630
|
+
if (resolved.length > 0) {
|
|
631
|
+
const instances = resolved.map((r) => ({
|
|
632
|
+
name: r.spec.name,
|
|
633
|
+
version: r.resolved.version,
|
|
634
|
+
ecosystem: "npm",
|
|
635
|
+
path: r.spec.raw,
|
|
636
|
+
direct: true,
|
|
637
|
+
dev: false,
|
|
638
|
+
optional: false
|
|
639
|
+
}));
|
|
640
|
+
try {
|
|
641
|
+
findings = await queryOsv(instances, { fetchImpl: options.fetchImpl });
|
|
642
|
+
} catch (err) {
|
|
643
|
+
const message = `OSV query failed: ${err.message}`;
|
|
644
|
+
for (const r of resolved) errored.push({ spec: r.spec, message });
|
|
645
|
+
resolved.length = 0;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
for (const f of findings) {
|
|
649
|
+
const owner = resolved.find(
|
|
650
|
+
(r) => r.spec.name === f.packageName && r.resolved.version === f.installedVersion
|
|
651
|
+
);
|
|
652
|
+
if (owner) owner.findings.push(f);
|
|
653
|
+
}
|
|
654
|
+
const blocked = [];
|
|
655
|
+
const installed = [];
|
|
656
|
+
for (const r of resolved) {
|
|
657
|
+
if (shouldBlock(r.findings, options)) {
|
|
658
|
+
blocked.push({ ...r, reason: "vulnerable" });
|
|
659
|
+
} else {
|
|
660
|
+
installed.push(r);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
findings.sort(compareFindings);
|
|
664
|
+
let pmExitCode;
|
|
665
|
+
if (installed.length > 0) {
|
|
666
|
+
const pm = detectPackageManager({ override: options.pm, cwd: options.cwd });
|
|
667
|
+
const cmd = buildAddCommand(
|
|
668
|
+
pm,
|
|
669
|
+
installed.map((r) => r.spec.raw),
|
|
670
|
+
flags
|
|
671
|
+
);
|
|
672
|
+
process.stdout.write(
|
|
673
|
+
kleur.gray(`> ${cmd.bin} ${cmd.args.join(" ")}
|
|
674
|
+
`)
|
|
675
|
+
);
|
|
676
|
+
const runner = options.runner ?? runPackageManager;
|
|
677
|
+
pmExitCode = await runner(cmd, { cwd: options.cwd });
|
|
678
|
+
}
|
|
679
|
+
return { installed, blocked, skipped, errored, findings, pmExitCode };
|
|
680
|
+
}
|
|
681
|
+
function reportAdd(result) {
|
|
682
|
+
const lines = [];
|
|
683
|
+
if (result.skipped.length > 0) {
|
|
684
|
+
for (const s of result.skipped) {
|
|
685
|
+
lines.push(
|
|
686
|
+
kleur.yellow(
|
|
687
|
+
`~ Skipped ${s.spec.raw}: ${describeUnsupported(s.reason)} (cannot scan; not forwarded to install)`
|
|
688
|
+
)
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (result.errored.length > 0) {
|
|
693
|
+
for (const e of result.errored) {
|
|
694
|
+
lines.push(kleur.red(`! ${e.spec.raw}: ${e.message}`));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (result.blocked.length > 0) {
|
|
698
|
+
for (const b of result.blocked) {
|
|
699
|
+
const sev = summarize(b.findings);
|
|
700
|
+
const counts = describeSeverityCounts(sev);
|
|
701
|
+
lines.push(
|
|
702
|
+
kleur.red(
|
|
703
|
+
`\u2717 Blocked ${b.spec.name}@${b.resolved.version}: ${counts}`
|
|
704
|
+
)
|
|
705
|
+
);
|
|
706
|
+
if (b.resolved.source === "fallback-latest") {
|
|
707
|
+
lines.push(
|
|
708
|
+
kleur.gray(
|
|
709
|
+
` (scanned latest because we don't resolve semver ranges yet; you asked for "${b.resolved.requested}")`
|
|
710
|
+
)
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
for (const f of b.findings) {
|
|
714
|
+
lines.push(
|
|
715
|
+
` - [${colorSeverity(f.severity)}] ${f.id} : ${f.summary}`
|
|
716
|
+
);
|
|
717
|
+
if (f.fixedVersions.length > 0) {
|
|
718
|
+
lines.push(kleur.gray(` fixed in: ${f.fixedVersions.join(", ")}`));
|
|
719
|
+
}
|
|
720
|
+
if (f.url) lines.push(kleur.gray(` ${f.url}`));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (result.installed.length > 0) {
|
|
725
|
+
const names = result.installed.map((r) => `${r.spec.name}@${r.resolved.version}`).join(", ");
|
|
726
|
+
lines.push(kleur.green(`\u2713 Installing: ${names}`));
|
|
727
|
+
} else if (result.blocked.length > 0) {
|
|
728
|
+
lines.push(
|
|
729
|
+
kleur.red("Nothing installed : all requested packages were blocked.")
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
return `${lines.join("\n")}
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
function shouldBlock(findings, options) {
|
|
736
|
+
if (options.allowVulnerable) return false;
|
|
737
|
+
if (options.failOn === "none") return false;
|
|
738
|
+
const threshold = SEVERITY_RANK[options.failOn];
|
|
739
|
+
return findings.some((f) => SEVERITY_RANK[f.severity] >= threshold);
|
|
740
|
+
}
|
|
741
|
+
function describeUnsupported(reason) {
|
|
742
|
+
switch (reason) {
|
|
743
|
+
case "git":
|
|
744
|
+
return "git specs cannot be scanned against OSV";
|
|
745
|
+
case "url":
|
|
746
|
+
return "URL specs cannot be scanned against OSV";
|
|
747
|
+
case "file":
|
|
748
|
+
return "local file specs cannot be scanned against OSV";
|
|
749
|
+
case "alias":
|
|
750
|
+
return "npm aliases are not supported in v1";
|
|
751
|
+
case "workspace":
|
|
752
|
+
return "workspace protocol specs are not scanned";
|
|
753
|
+
case "invalid":
|
|
754
|
+
return "could not parse spec";
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function describeSeverityCounts(summary) {
|
|
758
|
+
const parts = [];
|
|
759
|
+
for (const sev of ["critical", "high", "moderate", "low", "unknown"]) {
|
|
760
|
+
if (summary[sev] > 0) parts.push(`${summary[sev]} ${sev}`);
|
|
761
|
+
}
|
|
762
|
+
return parts.length === 0 ? "no advisories" : `${parts.join(", ")} advisor${total(summary) === 1 ? "y" : "ies"}`;
|
|
763
|
+
}
|
|
764
|
+
function total(summary) {
|
|
765
|
+
return Object.values(summary).reduce((a, b) => a + b, 0);
|
|
766
|
+
}
|
|
767
|
+
function colorSeverity(sev) {
|
|
768
|
+
switch (sev) {
|
|
769
|
+
case "critical":
|
|
770
|
+
return kleur.bold().red(sev);
|
|
771
|
+
case "high":
|
|
772
|
+
return kleur.red(sev);
|
|
773
|
+
case "moderate":
|
|
774
|
+
return kleur.yellow(sev);
|
|
775
|
+
case "low":
|
|
776
|
+
return kleur.cyan(sev);
|
|
777
|
+
case "unknown":
|
|
778
|
+
return kleur.gray(sev);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/reporters/json.ts
|
|
783
|
+
function reportJson(result) {
|
|
784
|
+
return JSON.stringify(result, null, 2);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/reporters/table.ts
|
|
788
|
+
import kleur3 from "kleur";
|
|
789
|
+
|
|
790
|
+
// src/reporters/banner.ts
|
|
791
|
+
import kleur2 from "kleur";
|
|
792
|
+
var BORDER = {
|
|
793
|
+
tl: "\u250C",
|
|
794
|
+
tr: "\u2510",
|
|
795
|
+
bl: "\u2514",
|
|
796
|
+
br: "\u2518",
|
|
797
|
+
h: "\u2500",
|
|
798
|
+
v: "\u2502"
|
|
799
|
+
};
|
|
800
|
+
var TITLE = "trawly";
|
|
801
|
+
var SIDE_PAD = 1;
|
|
802
|
+
var MIN_TITLE_FILLER = 4;
|
|
803
|
+
function renderBanner(props) {
|
|
804
|
+
const contentRows = [props.metrics, props.timestamp];
|
|
805
|
+
const titleSegment = ` ${TITLE} `;
|
|
806
|
+
const minTitleWidth = 1 + titleSegment.length + MIN_TITLE_FILLER;
|
|
807
|
+
const innerWidth = Math.max(
|
|
808
|
+
...contentRows.map((r) => r.length + SIDE_PAD * 2),
|
|
809
|
+
minTitleWidth
|
|
810
|
+
);
|
|
811
|
+
const top = renderTop(innerWidth);
|
|
812
|
+
const bottom = renderBottom(innerWidth);
|
|
813
|
+
const metricsLine = renderRow(innerWidth, props.metrics, kleur2.bold);
|
|
814
|
+
const timestampLine = renderRow(innerWidth, props.timestamp, kleur2.gray);
|
|
815
|
+
return [top, metricsLine, timestampLine, bottom].join("\n");
|
|
816
|
+
}
|
|
817
|
+
function renderTop(innerWidth) {
|
|
818
|
+
const titleSegment = ` ${kleur2.bold().cyan(TITLE)} `;
|
|
819
|
+
const titleVisibleLen = TITLE.length + 2;
|
|
820
|
+
const fillerCount = innerWidth - 1 - titleVisibleLen;
|
|
821
|
+
return kleur2.gray(BORDER.tl) + kleur2.gray(BORDER.h) + titleSegment + kleur2.gray(BORDER.h.repeat(Math.max(MIN_TITLE_FILLER, fillerCount))) + kleur2.gray(BORDER.tr);
|
|
822
|
+
}
|
|
823
|
+
function renderBottom(innerWidth) {
|
|
824
|
+
return kleur2.gray(`${BORDER.bl}${BORDER.h.repeat(innerWidth)}${BORDER.br}`);
|
|
825
|
+
}
|
|
826
|
+
function renderRow(innerWidth, content, colorize) {
|
|
827
|
+
const fill = Math.max(0, innerWidth - SIDE_PAD * 2 - content.length);
|
|
828
|
+
return kleur2.gray(BORDER.v) + " ".repeat(SIDE_PAD) + colorize(content) + " ".repeat(fill + SIDE_PAD) + kleur2.gray(BORDER.v);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/reporters/table.ts
|
|
832
|
+
var SEVERITY_COLOR = {
|
|
833
|
+
critical: (s) => kleur3.bold().red(s),
|
|
834
|
+
high: (s) => kleur3.red(s),
|
|
835
|
+
moderate: (s) => kleur3.yellow(s),
|
|
836
|
+
low: (s) => kleur3.cyan(s),
|
|
837
|
+
unknown: (s) => kleur3.gray(s)
|
|
838
|
+
};
|
|
839
|
+
var SEVERITY_ORDER = [
|
|
840
|
+
"critical",
|
|
841
|
+
"high",
|
|
842
|
+
"moderate",
|
|
843
|
+
"low",
|
|
844
|
+
"unknown"
|
|
845
|
+
];
|
|
846
|
+
function reportTable(result, options = {}) {
|
|
847
|
+
const view = options.view ?? (options.details ? "details" : "grouped");
|
|
848
|
+
const lines = [];
|
|
849
|
+
if (options.brand) {
|
|
850
|
+
const { metricsLine, timestamp } = headerParts(result);
|
|
851
|
+
lines.push(renderBanner({ metrics: metricsLine, timestamp }));
|
|
852
|
+
} else {
|
|
853
|
+
lines.push(kleur3.bold(formatHeader(result)));
|
|
854
|
+
}
|
|
855
|
+
if (result.errors.length > 0) {
|
|
856
|
+
for (const err of result.errors) {
|
|
857
|
+
lines.push(
|
|
858
|
+
kleur3.red(`! ${err.message}${err.cause ? ` (${err.cause})` : ""}`)
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (result.findings.length === 0) {
|
|
863
|
+
lines.push(kleur3.green("\u2713 No known advisories found."));
|
|
864
|
+
lines.push(
|
|
865
|
+
kleur3.gray(
|
|
866
|
+
" Note: this only checks known advisories. It cannot prove a package is safe."
|
|
867
|
+
)
|
|
868
|
+
);
|
|
869
|
+
return lines.join("\n");
|
|
870
|
+
}
|
|
871
|
+
if (view === "summary") {
|
|
872
|
+
lines.push(formatSummary(result.summary));
|
|
873
|
+
lines.push(reminder());
|
|
874
|
+
return lines.join("\n");
|
|
875
|
+
}
|
|
876
|
+
lines.push("");
|
|
877
|
+
lines.push(formatSummary(result.summary));
|
|
878
|
+
lines.push("");
|
|
879
|
+
if (view === "details") {
|
|
880
|
+
lines.push(formatDetailRows(sortFindings(result.findings)));
|
|
881
|
+
} else {
|
|
882
|
+
const groups = groupByPackage(result.findings);
|
|
883
|
+
lines.push(formatGroupedRows(groups));
|
|
884
|
+
lines.push("");
|
|
885
|
+
lines.push(
|
|
886
|
+
kleur3.gray("Run `trawly scan --details` to see individual advisories.")
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
lines.push("");
|
|
890
|
+
lines.push(reminder());
|
|
891
|
+
return lines.join("\n");
|
|
892
|
+
}
|
|
893
|
+
function formatHeader(result) {
|
|
894
|
+
const { metricsLine, timestamp } = headerParts(result);
|
|
895
|
+
return `trawly: ${metricsLine} (${timestamp})`;
|
|
896
|
+
}
|
|
897
|
+
function headerParts(result) {
|
|
898
|
+
const vulnerable = new Set(
|
|
899
|
+
result.findings.map((f) => `${f.packageName}@${f.installedVersion}`)
|
|
900
|
+
).size;
|
|
901
|
+
const advisories = result.findings.length;
|
|
902
|
+
const metricsLine = [
|
|
903
|
+
`${result.packagesScanned} packages`,
|
|
904
|
+
`${vulnerable} vulnerable`,
|
|
905
|
+
`${advisories} ${advisories === 1 ? "advisory" : "advisories"}`
|
|
906
|
+
].join(" \xB7 ");
|
|
907
|
+
return { metricsLine, timestamp: result.scannedAt };
|
|
908
|
+
}
|
|
909
|
+
function formatSummary(summary) {
|
|
910
|
+
const parts = [];
|
|
911
|
+
for (const sev of SEVERITY_ORDER) {
|
|
912
|
+
const count = summary[sev];
|
|
913
|
+
if (count === 0) continue;
|
|
914
|
+
parts.push(SEVERITY_COLOR[sev](`${sev}: ${count}`));
|
|
915
|
+
}
|
|
916
|
+
if (parts.length === 0) return kleur3.green("No findings.");
|
|
917
|
+
return `Findings : ${parts.join(" ")}`;
|
|
918
|
+
}
|
|
919
|
+
function reminder() {
|
|
920
|
+
return kleur3.gray(
|
|
921
|
+
"Reminder: trawly reports known advisories only. Absence of findings is not proof of safety."
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
function sortFindings(findings) {
|
|
925
|
+
return [...findings].sort((a, b) => {
|
|
926
|
+
const sev = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
|
|
927
|
+
if (sev !== 0) return sev;
|
|
928
|
+
const name = a.packageName.localeCompare(b.packageName);
|
|
929
|
+
if (name !== 0) return name;
|
|
930
|
+
return a.id.localeCompare(b.id);
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
function groupByPackage(findings) {
|
|
934
|
+
const map = /* @__PURE__ */ new Map();
|
|
935
|
+
for (const f of findings) {
|
|
936
|
+
const key = `${f.packageName}@${f.installedVersion}`;
|
|
937
|
+
let group = map.get(key);
|
|
938
|
+
if (!group) {
|
|
939
|
+
group = {
|
|
940
|
+
packageName: f.packageName,
|
|
941
|
+
installedVersion: f.installedVersion,
|
|
942
|
+
topSeverity: f.severity,
|
|
943
|
+
counts: { critical: 0, high: 0, moderate: 0, low: 0, unknown: 0 },
|
|
944
|
+
findings: [],
|
|
945
|
+
recommendedFix: null
|
|
946
|
+
};
|
|
947
|
+
map.set(key, group);
|
|
948
|
+
}
|
|
949
|
+
group.findings.push(f);
|
|
950
|
+
group.counts[f.severity] += 1;
|
|
951
|
+
if (SEVERITY_RANK[f.severity] > SEVERITY_RANK[group.topSeverity]) {
|
|
952
|
+
group.topSeverity = f.severity;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
for (const group of map.values()) {
|
|
956
|
+
group.recommendedFix = pickRecommendedFix(group.findings);
|
|
957
|
+
}
|
|
958
|
+
return [...map.values()].sort((a, b) => {
|
|
959
|
+
const sev = SEVERITY_RANK[b.topSeverity] - SEVERITY_RANK[a.topSeverity];
|
|
960
|
+
if (sev !== 0) return sev;
|
|
961
|
+
const totalA = a.findings.length;
|
|
962
|
+
const totalB = b.findings.length;
|
|
963
|
+
if (totalA !== totalB) return totalB - totalA;
|
|
964
|
+
return a.packageName.localeCompare(b.packageName);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
function formatGroupedRows(groups) {
|
|
968
|
+
const rows = [
|
|
969
|
+
["PACKAGE", "VERSION", "SEVERITY", "FIX"]
|
|
970
|
+
];
|
|
971
|
+
for (const g of groups) {
|
|
972
|
+
rows.push([
|
|
973
|
+
g.packageName,
|
|
974
|
+
g.installedVersion,
|
|
975
|
+
formatSeverityCounts(g.counts),
|
|
976
|
+
g.recommendedFix ? `>=${g.recommendedFix}` : ":"
|
|
977
|
+
]);
|
|
978
|
+
}
|
|
979
|
+
return renderTable(rows, (rowIdx, _row, cells) => {
|
|
980
|
+
if (rowIdx === 0) return kleur3.bold().underline(cells.join(" "));
|
|
981
|
+
return cells.join(" ");
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function formatDetailRows(findings) {
|
|
985
|
+
const rows = [
|
|
986
|
+
["SEV", "PACKAGE", "VERSION", "ID", "FIXED IN", "SUMMARY"]
|
|
987
|
+
];
|
|
988
|
+
for (const f of findings) {
|
|
989
|
+
rows.push([
|
|
990
|
+
f.severity,
|
|
991
|
+
f.packageName,
|
|
992
|
+
f.installedVersion,
|
|
993
|
+
f.id,
|
|
994
|
+
f.fixedVersions.length ? f.fixedVersions.join(", ") : ":",
|
|
995
|
+
truncate2(f.summary, 70)
|
|
996
|
+
]);
|
|
997
|
+
}
|
|
998
|
+
return renderTable(rows, (rowIdx, row, cells) => {
|
|
999
|
+
if (rowIdx === 0) return kleur3.bold().underline(cells.join(" "));
|
|
1000
|
+
const sev = row[0];
|
|
1001
|
+
const colorize = SEVERITY_COLOR[sev] ?? ((s) => s);
|
|
1002
|
+
cells[0] = colorize(cells[0]);
|
|
1003
|
+
return cells.join(" ");
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function formatSeverityCounts(counts) {
|
|
1007
|
+
const parts = [];
|
|
1008
|
+
for (const sev of SEVERITY_ORDER) {
|
|
1009
|
+
const n = counts[sev];
|
|
1010
|
+
if (n === 0) continue;
|
|
1011
|
+
parts.push(SEVERITY_COLOR[sev](`${n} ${sev}`));
|
|
1012
|
+
}
|
|
1013
|
+
return parts.join(", ");
|
|
1014
|
+
}
|
|
1015
|
+
function renderTable(rows, format) {
|
|
1016
|
+
const widths = rows[0].map(
|
|
1017
|
+
(_, col) => Math.max(...rows.map((r) => visibleLength(r[col])))
|
|
1018
|
+
);
|
|
1019
|
+
return rows.map((row, i) => {
|
|
1020
|
+
const cells = row.map((cell, col) => padEndVisible(cell, widths[col]));
|
|
1021
|
+
return format(i, row, cells);
|
|
1022
|
+
}).join("\n");
|
|
1023
|
+
}
|
|
1024
|
+
function pickRecommendedFix(findings) {
|
|
1025
|
+
const candidates = [];
|
|
1026
|
+
for (const f of findings) {
|
|
1027
|
+
for (const v of f.fixedVersions) candidates.push(v);
|
|
1028
|
+
}
|
|
1029
|
+
if (candidates.length === 0) return null;
|
|
1030
|
+
const unique = [...new Set(candidates)];
|
|
1031
|
+
unique.sort(compareSemver);
|
|
1032
|
+
return unique[unique.length - 1] ?? null;
|
|
1033
|
+
}
|
|
1034
|
+
function compareSemver(a, b) {
|
|
1035
|
+
const pa = parseSemverParts(a);
|
|
1036
|
+
const pb = parseSemverParts(b);
|
|
1037
|
+
for (let i = 0; i < 3; i++) {
|
|
1038
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1039
|
+
if (diff !== 0) return diff;
|
|
1040
|
+
}
|
|
1041
|
+
return a.localeCompare(b);
|
|
1042
|
+
}
|
|
1043
|
+
function parseSemverParts(v) {
|
|
1044
|
+
const m = v.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
1045
|
+
if (!m) return [0, 0, 0];
|
|
1046
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
1047
|
+
}
|
|
1048
|
+
function truncate2(s, max) {
|
|
1049
|
+
if (s.length <= max) return s;
|
|
1050
|
+
return `${s.slice(0, max - 1)}\u2026`;
|
|
1051
|
+
}
|
|
1052
|
+
var ANSI_RE = /\u001B\[[0-9;]*m/g;
|
|
1053
|
+
function visibleLength(s) {
|
|
1054
|
+
return s.replace(ANSI_RE, "").length;
|
|
1055
|
+
}
|
|
1056
|
+
function padEndVisible(s, width) {
|
|
1057
|
+
const pad = Math.max(0, width - visibleLength(s));
|
|
1058
|
+
return s + " ".repeat(pad);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/cli.ts
|
|
1062
|
+
var FAIL_ON_VALUES = [
|
|
1063
|
+
"critical",
|
|
1064
|
+
"high",
|
|
1065
|
+
"moderate",
|
|
1066
|
+
"low",
|
|
1067
|
+
"none"
|
|
1068
|
+
];
|
|
1069
|
+
var FORMAT_VALUES = ["table", "json"];
|
|
1070
|
+
var PM_VALUES = ["npm", "pnpm", "yarn", "bun"];
|
|
1071
|
+
var EXIT = {
|
|
1072
|
+
ok: 0,
|
|
1073
|
+
findings: 1,
|
|
1074
|
+
operational: 2,
|
|
1075
|
+
invalidInput: 3
|
|
1076
|
+
};
|
|
1077
|
+
function parseFailOn(value) {
|
|
1078
|
+
if (!FAIL_ON_VALUES.includes(value)) {
|
|
1079
|
+
throw new InvalidArgumentError(
|
|
1080
|
+
`must be one of: ${FAIL_ON_VALUES.join(", ")}`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
return value;
|
|
1084
|
+
}
|
|
1085
|
+
function parseFormat(value) {
|
|
1086
|
+
if (!FORMAT_VALUES.includes(value)) {
|
|
1087
|
+
throw new InvalidArgumentError(
|
|
1088
|
+
`must be one of: ${FORMAT_VALUES.join(", ")}`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
return value;
|
|
1092
|
+
}
|
|
1093
|
+
function parsePm(value) {
|
|
1094
|
+
if (!PM_VALUES.includes(value)) {
|
|
1095
|
+
throw new InvalidArgumentError(`must be one of: ${PM_VALUES.join(", ")}`);
|
|
1096
|
+
}
|
|
1097
|
+
return value;
|
|
1098
|
+
}
|
|
1099
|
+
var TRAWLY_VERSION = "0.1.0";
|
|
1100
|
+
var program = new Command();
|
|
1101
|
+
program.name("trawly").description(
|
|
1102
|
+
"Dependency sanity scanner. Checks installed npm packages against the OSV advisory database."
|
|
1103
|
+
).version(TRAWLY_VERSION).enablePositionalOptions().exitOverride((err) => {
|
|
1104
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.help") {
|
|
1105
|
+
process.exit(EXIT.ok);
|
|
1106
|
+
}
|
|
1107
|
+
if (err.code === "commander.version") process.exit(EXIT.ok);
|
|
1108
|
+
process.exit(EXIT.invalidInput);
|
|
1109
|
+
});
|
|
1110
|
+
program.command("scan", { isDefault: true }).description(
|
|
1111
|
+
"Scan a project and gate on findings. Exits non-zero when --fail-on is met. Use `inspect` for a log-only run."
|
|
1112
|
+
).argument("[path]", "Project directory to scan", ".").option("--lockfile <path>", "Explicit path to package-lock.json").option(
|
|
1113
|
+
"--format <format>",
|
|
1114
|
+
"Output format: table | json",
|
|
1115
|
+
parseFormat,
|
|
1116
|
+
"table"
|
|
1117
|
+
).option(
|
|
1118
|
+
"--fail-on <level>",
|
|
1119
|
+
`Exit non-zero when a finding meets this severity (${FAIL_ON_VALUES.join("|")})`,
|
|
1120
|
+
parseFailOn,
|
|
1121
|
+
"high"
|
|
1122
|
+
).option("--prod", "Only scan production dependencies (excludes dev)").option("--include-dev", "Include dev dependencies (default)").option("--no-cache", "Bypass any local cache").option(
|
|
1123
|
+
"-v, --details",
|
|
1124
|
+
"Show one row per advisory (full table). Default groups by package."
|
|
1125
|
+
).option(
|
|
1126
|
+
"-q, --summary",
|
|
1127
|
+
"Show only the one-line severity summary. Mutually exclusive with --details."
|
|
1128
|
+
).action(async (path, opts) => {
|
|
1129
|
+
await runScanCommand(path, opts, { gate: true });
|
|
1130
|
+
});
|
|
1131
|
+
program.command("inspect").description(
|
|
1132
|
+
"Scan a project and print findings without gating. Always exits 0 unless an operational error occurs. Use `scan` for CI gating."
|
|
1133
|
+
).argument("[path]", "Project directory to scan", ".").option("--lockfile <path>", "Explicit path to package-lock.json").option(
|
|
1134
|
+
"--format <format>",
|
|
1135
|
+
"Output format: table | json",
|
|
1136
|
+
parseFormat,
|
|
1137
|
+
"table"
|
|
1138
|
+
).option("--prod", "Only scan production dependencies (excludes dev)").option("--include-dev", "Include dev dependencies (default)").option("--no-cache", "Bypass any local cache").option(
|
|
1139
|
+
"-v, --details",
|
|
1140
|
+
"Show one row per advisory (full table). Default groups by package."
|
|
1141
|
+
).option(
|
|
1142
|
+
"-q, --summary",
|
|
1143
|
+
"Show only the one-line severity summary. Mutually exclusive with --details."
|
|
1144
|
+
).action(async (path, opts) => {
|
|
1145
|
+
await runScanCommand(
|
|
1146
|
+
path,
|
|
1147
|
+
{ ...opts, failOn: "none" },
|
|
1148
|
+
{ gate: false }
|
|
1149
|
+
);
|
|
1150
|
+
});
|
|
1151
|
+
program.command("add").description(
|
|
1152
|
+
"Resolve, scan, and install packages. Vulnerable packages are blocked; clean ones are forwarded to your package manager."
|
|
1153
|
+
).argument("<args...>", "Packages to add (e.g. next vitest@1) : PM flags after the first package are passed through").option(
|
|
1154
|
+
"--fail-on <level>",
|
|
1155
|
+
`Block install when a finding meets this severity (${FAIL_ON_VALUES.join("|")})`,
|
|
1156
|
+
parseFailOn,
|
|
1157
|
+
"high"
|
|
1158
|
+
).option(
|
|
1159
|
+
"--pm <name>",
|
|
1160
|
+
`Force a package manager (${PM_VALUES.join("|")}). Auto-detected by default.`,
|
|
1161
|
+
parsePm
|
|
1162
|
+
).option(
|
|
1163
|
+
"--allow-vulnerable",
|
|
1164
|
+
"Install even if vulnerabilities are found (still prints findings)."
|
|
1165
|
+
).passThroughOptions().action(async (args, opts) => {
|
|
1166
|
+
await executeAdd(args, opts);
|
|
1167
|
+
});
|
|
1168
|
+
program.command("install").alias("i").description(
|
|
1169
|
+
"Run the project's package manager install. With package args, behaves like `add` (gates on vulnerabilities). With none, forwards directly."
|
|
1170
|
+
).argument("[args...]", "Optional packages to add").option(
|
|
1171
|
+
"--fail-on <level>",
|
|
1172
|
+
`Block install when a finding meets this severity (${FAIL_ON_VALUES.join("|")})`,
|
|
1173
|
+
parseFailOn,
|
|
1174
|
+
"high"
|
|
1175
|
+
).option(
|
|
1176
|
+
"--pm <name>",
|
|
1177
|
+
`Force a package manager (${PM_VALUES.join("|")})`,
|
|
1178
|
+
parsePm
|
|
1179
|
+
).option("--allow-vulnerable", "Install even if vulnerabilities are found.").passThroughOptions().action(async (args, opts) => {
|
|
1180
|
+
if (args.length === 0) {
|
|
1181
|
+
const pm = detectPackageManager({ override: opts.pm });
|
|
1182
|
+
const command = buildInstallCommand(pm, []);
|
|
1183
|
+
process.stdout.write(
|
|
1184
|
+
kleur4.gray(`> ${command.bin} ${command.args.join(" ")}
|
|
1185
|
+
`)
|
|
1186
|
+
);
|
|
1187
|
+
try {
|
|
1188
|
+
const code = await runPackageManager(command);
|
|
1189
|
+
process.exit(code);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
printErr(`trawly: ${err.message}`);
|
|
1192
|
+
process.exit(EXIT.operational);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
await executeAdd(args, opts);
|
|
1196
|
+
});
|
|
1197
|
+
program.command("remove").alias("uninstall").description(
|
|
1198
|
+
"Remove packages by delegating to the project's package manager (no scan)."
|
|
1199
|
+
).argument("<args...>", "Packages to remove").option(
|
|
1200
|
+
"--pm <name>",
|
|
1201
|
+
`Force a package manager (${PM_VALUES.join("|")})`,
|
|
1202
|
+
parsePm
|
|
1203
|
+
).passThroughOptions().action(async (args, opts) => {
|
|
1204
|
+
const pm = detectPackageManager({ override: opts.pm });
|
|
1205
|
+
const { specs, flags } = splitArgs(args);
|
|
1206
|
+
const command = buildRemoveCommand(pm, specs, flags);
|
|
1207
|
+
process.stdout.write(
|
|
1208
|
+
kleur4.gray(`> ${command.bin} ${command.args.join(" ")}
|
|
1209
|
+
`)
|
|
1210
|
+
);
|
|
1211
|
+
try {
|
|
1212
|
+
const code = await runPackageManager(command);
|
|
1213
|
+
process.exit(code);
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
printErr(`trawly: ${err.message}`);
|
|
1216
|
+
process.exit(EXIT.operational);
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
async function runScanCommand(path, opts, { gate }) {
|
|
1220
|
+
if (opts.prod && opts.includeDev) {
|
|
1221
|
+
printErr("Cannot combine --prod and --include-dev. Choose one.");
|
|
1222
|
+
process.exit(EXIT.invalidInput);
|
|
1223
|
+
}
|
|
1224
|
+
if (opts.details && opts.summary) {
|
|
1225
|
+
printErr("Cannot combine --details and --summary. Choose one.");
|
|
1226
|
+
process.exit(EXIT.invalidInput);
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
const result = await scanProject({
|
|
1230
|
+
cwd: path,
|
|
1231
|
+
lockfile: opts.lockfile,
|
|
1232
|
+
includeDev: opts.includeDev,
|
|
1233
|
+
prodOnly: opts.prod,
|
|
1234
|
+
cache: opts.cache
|
|
1235
|
+
});
|
|
1236
|
+
if (opts.format === "json") {
|
|
1237
|
+
process.stdout.write(`${reportJson(result)}
|
|
1238
|
+
`);
|
|
1239
|
+
} else {
|
|
1240
|
+
const view = opts.summary ? "summary" : opts.details ? "details" : "grouped";
|
|
1241
|
+
const brand = process.stdout.isTTY === true;
|
|
1242
|
+
process.stdout.write(`${reportTable(result, { view, brand })}
|
|
1243
|
+
`);
|
|
1244
|
+
}
|
|
1245
|
+
if (result.errors.length > 0) {
|
|
1246
|
+
process.exit(EXIT.operational);
|
|
1247
|
+
}
|
|
1248
|
+
if (!gate) {
|
|
1249
|
+
if (opts.format !== "json" && result.findings.length > 0) {
|
|
1250
|
+
process.stdout.write(
|
|
1251
|
+
`${kleur4.gray(
|
|
1252
|
+
"\u2139 inspect mode: exiting 0 regardless of findings. Run `trawly scan` to gate CI."
|
|
1253
|
+
)}
|
|
1254
|
+
`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
process.exit(EXIT.ok);
|
|
1258
|
+
}
|
|
1259
|
+
if (meetsThreshold(result.findings, opts.failOn)) {
|
|
1260
|
+
if (opts.format !== "json") {
|
|
1261
|
+
process.stderr.write(
|
|
1262
|
+
`${kleur4.red(
|
|
1263
|
+
`\xD7 Failing because at least one finding meets --fail-on=${opts.failOn}.`
|
|
1264
|
+
)}
|
|
1265
|
+
${kleur4.gray(
|
|
1266
|
+
" Run `trawly inspect` to log without exiting non-zero, or `trawly scan --fail-on=none` to disable the gate."
|
|
1267
|
+
)}
|
|
1268
|
+
`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
process.exit(EXIT.findings);
|
|
1272
|
+
}
|
|
1273
|
+
process.exit(EXIT.ok);
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
if (err instanceof ScanInputError) {
|
|
1276
|
+
printErr(err.message);
|
|
1277
|
+
process.exit(EXIT.invalidInput);
|
|
1278
|
+
}
|
|
1279
|
+
printErr(`trawly: ${err.message}`);
|
|
1280
|
+
process.exit(EXIT.operational);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function executeAdd(args, opts) {
|
|
1284
|
+
try {
|
|
1285
|
+
const result = await runAdd(args, {
|
|
1286
|
+
failOn: opts.failOn,
|
|
1287
|
+
pm: opts.pm,
|
|
1288
|
+
allowVulnerable: opts.allowVulnerable
|
|
1289
|
+
});
|
|
1290
|
+
process.stdout.write(reportAdd(result));
|
|
1291
|
+
if (result.errored.length > 0) process.exit(EXIT.operational);
|
|
1292
|
+
if (result.pmExitCode !== void 0 && result.pmExitCode !== 0) {
|
|
1293
|
+
process.exit(result.pmExitCode);
|
|
1294
|
+
}
|
|
1295
|
+
if (result.blocked.length > 0) process.exit(EXIT.findings);
|
|
1296
|
+
process.exit(EXIT.ok);
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
printErr(`trawly: ${err.message}`);
|
|
1299
|
+
process.exit(EXIT.operational);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function splitArgs(args) {
|
|
1303
|
+
const specs = [];
|
|
1304
|
+
const flags = [];
|
|
1305
|
+
for (const a of args) {
|
|
1306
|
+
if (a.startsWith("-")) flags.push(a);
|
|
1307
|
+
else specs.push(a);
|
|
1308
|
+
}
|
|
1309
|
+
return { specs, flags };
|
|
1310
|
+
}
|
|
1311
|
+
function printErr(msg) {
|
|
1312
|
+
process.stderr.write(`${kleur4.red(msg)}
|
|
1313
|
+
`);
|
|
1314
|
+
}
|
|
1315
|
+
await program.parseAsync(process.argv);
|
|
1316
|
+
//# sourceMappingURL=cli.js.map
|