@kaynooo/pkg-tools 0.3.0 → 0.4.0
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 +1 -1
- package/README.md +27 -2
- package/dist/cli.js +553 -189
- package/package.json +6 -6
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Kaynooo <kaynooo
|
|
3
|
+
Copyright (c) 2026 Kaynooo <contact@kaynooo.fr>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -14,7 +14,31 @@ pkg left-pad@1.3.0 @types/node --json
|
|
|
14
14
|
- Weekly downloads (public registry only), licence and repository.
|
|
15
15
|
- Warnings for a version under the minimum age (with the `safe` version to use instead), `preinstall`, `install` and `postinstall` scripts, a deprecated version, and a name that is a near miss of a popular package (one or two edits, other separators or case, or the same name under another scope).
|
|
16
16
|
|
|
17
|
-
`pkg` exits 1 when a package is missing, the request matches no version, or the resolved version is too fresh; other warnings exit 0. `--json` prints one report, or an array for several names, `--min-age <days>` changes the 14 days.
|
|
17
|
+
`pkg` exits 1 when a package is missing, the request matches no version, or the resolved version is too fresh, 2 on a usage error; other warnings exit 0. `--json` prints one report, or an array for several names, `--min-age <days>` changes the 14 days.
|
|
18
|
+
|
|
19
|
+
## Config: licence allowlist and advisory threshold
|
|
20
|
+
|
|
21
|
+
`pkg` reads its own settings from a `.pkgrc.json` in the current folder or any folder above it, then from `~/.config/pkg/config.json`; each key comes from the nearest file that sets it, and comments are allowed.
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{ "licenses": ["MIT", "ISC", "Apache-2.0", "BSD-3-Clause"], "blockSeverity": "high" }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `licenses` is the allowlist of SPDX ids, compared without regard to case. An expression is allowed when one side of an `OR` is and every term of an `AND` is; a `WITH` exception is ignored. With an allowlist set, `pkg check` refuses a package whose licence is outside it or missing (a legacy `SEE LICENSE IN` or `UNLICENSED` counts as outside), so the `bun add` the guard checks through `pkg check` is refused with it. Without the key the licence is only printed.
|
|
28
|
+
- `blockSeverity` (`low`, `moderate`, `high` or `critical`) makes `pkg check` ask the registry for the advisories of the exact `name@version` and refuse it when one is at that severity or above. The request is the one `bun audit` makes: a `POST` of `{"name": ["version"]}` to `<registry>/-/npm/v1/security/advisories/bulk`. A registry without that endpoint (a private one, say) answers nothing and the version is not refused for it.
|
|
29
|
+
|
|
30
|
+
## Auditing a lockfile
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pkg audit
|
|
34
|
+
pkg audit ~/Workspace/app --audit-level high --ignore GHSA-238p-pmpm-9mq7 --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`pkg audit [dir]` reads the `bun.lock` of the folder and runs the checks of `pkg check` on every registry package it pins (workspace, git and link sources left out): the age of the version, install scripts, a deprecated version, a near miss of a popular name and the licence allowlist. It also lists the known advisories of every pinned version, each with its GHSA, severity and the version that fixes it when the range ends at one. Findings come grouped per package, each package looked up on the registry once per run, and all advisories fetched in one request per registry.
|
|
38
|
+
|
|
39
|
+
- `--audit-level <low|moderate|high|critical>` (`low`) keeps only the advisories at that severity or above, and `--ignore <GHSA or id>` (repeatable) skips an advisory; `blockSeverity` is for `pkg check` and does not apply here.
|
|
40
|
+
- `--json` prints the packages with a finding as `[{name, version, license, blocked, warnings, advisories}]`; `--min-age <days>` changes the 14 days.
|
|
41
|
+
- It exits 1 when a package is missing, too fresh or outside the allowlist, or an advisory remains after the level and the ignores; install scripts and near misses alone exit 0.
|
|
18
42
|
|
|
19
43
|
## Moving consumers onto a release
|
|
20
44
|
|
|
@@ -24,7 +48,8 @@ pkg bump @kaynooo/cli-kit 0.8.0 --dry-run
|
|
|
24
48
|
```
|
|
25
49
|
|
|
26
50
|
- `pkg consumers <name>` lists every `package.json` under `--root` (`~/Workspace`) depending on the package, `node_modules`, `dist` and dot folders left out: the folder, the kind of dependency, the version it asks for and the manager its nearest lockfile pins; `--json` for the rows.
|
|
27
|
-
- `pkg bump <name> [version]` waits until the package's registry serves the version (every 5 s, `-t 10m`; the latest when none is given), then runs `bun add --exact --no-cache <name>@<version>` in each of those folders, one after another, as the same kind of dependency (`--dev`, `--optional`, `--peer`), a line per folder. A `workspace:`, `link:` or `file:` dependency, a folder already on the version and one whose lockfile pins another manager are left alone and said so; `--dry-run` prints what would run. It exits 1 when an install fails or the version never shows.
|
|
51
|
+
- `pkg bump <name> [version]` waits until the package's registry serves the version (every 5 s, `-t 10m`; the latest when none is given), then runs `bun add --exact --no-cache <name>@<version>` in each of those folders, one after another, as the same kind of dependency (`--dev`, `--optional`, `--peer`), a line per folder. A `workspace:`, `link:` or `file:` dependency, a folder already on the version and one whose lockfile pins another manager are left alone and said so; `--dry-run` prints what would run. It exits 1 when an install fails or the registry has no such package, 2 when the version never shows within the timeout.
|
|
52
|
+
- `pkg bump <name> [version] --all-consumers` goes on in each folder whose install went through: it runs the folder's own `check` and, only when that passes, commits with `gc 'chore(deps): bump <name> to <version>' -- package.json <lockfile>` (the `bun.lock` or `bun.lockb` nearest the folder, a monorepo's at its root). A folder whose check fails stays uncommitted. It ends on a summary line of the folders bumped and committed, bumped and failing, and skipped, exits 1 when an install, a check or a commit failed, and never pushes. With `--dry-run` it lists the `bun add`, `check` and `gc` each folder would run.
|
|
28
53
|
- `pkg <name>` alone still runs `pkg check`.
|
|
29
54
|
|
|
30
55
|
## Registries
|
package/dist/cli.js
CHANGED
|
@@ -440,10 +440,30 @@ class Completing {
|
|
|
440
440
|
function usageLine(usage, colors = paint(process3.stdout)) {
|
|
441
441
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
442
442
|
}
|
|
443
|
-
function helpText(usage, options, colors = paint(process3.stdout), examples = []) {
|
|
444
|
-
return [usageLine(usage, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
443
|
+
function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
|
|
444
|
+
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
445
445
|
`);
|
|
446
446
|
}
|
|
447
|
+
function replacesRows(replaces, colors = paint(process3.stdout)) {
|
|
448
|
+
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
449
|
+
}
|
|
450
|
+
function wantsHelpJson(env = process3.env) {
|
|
451
|
+
return env.KIT_HELP === "json";
|
|
452
|
+
}
|
|
453
|
+
function description(usage, { examples = [], replaces } = {}) {
|
|
454
|
+
const lines = usage.replace(USAGE_PREFIX, "").split(`
|
|
455
|
+
`);
|
|
456
|
+
const first = lines[0];
|
|
457
|
+
const colon = first.indexOf(": ");
|
|
458
|
+
const prose = lines[lines.indexOf("") + 1] ?? "";
|
|
459
|
+
const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
|
|
460
|
+
return {
|
|
461
|
+
synopsis: colon === -1 ? first : first.slice(0, colon),
|
|
462
|
+
summary: colon === -1 ? fallback : first.slice(colon + 2),
|
|
463
|
+
...replaces === undefined ? {} : { replaces },
|
|
464
|
+
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
|
|
465
|
+
};
|
|
466
|
+
}
|
|
447
467
|
function exampleRows(examples, colors = paint(process3.stdout)) {
|
|
448
468
|
if (examples.length === 0)
|
|
449
469
|
return [];
|
|
@@ -482,14 +502,14 @@ function parse(args, usage, options, settings = {}) {
|
|
|
482
502
|
const rest = separator === -1 ? undefined : args.slice(separator + 1);
|
|
483
503
|
let parsed;
|
|
484
504
|
try {
|
|
485
|
-
parsed = parseArgs({ args: own, options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
505
|
+
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
486
506
|
} catch (error) {
|
|
487
507
|
badUsage(parseError(error));
|
|
488
508
|
}
|
|
489
509
|
const values = parsed.values;
|
|
490
510
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
491
511
|
if (values.help === true) {
|
|
492
|
-
console.log(helpText(usage, all, undefined, settings
|
|
512
|
+
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, all, undefined, settings));
|
|
493
513
|
throw new HelpShown;
|
|
494
514
|
}
|
|
495
515
|
if (settings.positionals !== undefined)
|
|
@@ -504,6 +524,25 @@ function checkPositionals(given, names) {
|
|
|
504
524
|
if (!variadic && given.length > names.length)
|
|
505
525
|
badUsage(`unexpected argument ${given[names.length]}`);
|
|
506
526
|
}
|
|
527
|
+
function joinDashValues(args, options) {
|
|
528
|
+
const longOf = new Map(Object.entries(options).flatMap(([name, option]) => option.short === undefined ? [[`--${name}`, name]] : [[`--${name}`, name], [`-${option.short}`, name]]));
|
|
529
|
+
const isOption = (arg) => arg === "--" || longOf.has(arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg);
|
|
530
|
+
const joined = [];
|
|
531
|
+
for (let i = 0;i < args.length; i++) {
|
|
532
|
+
const arg = args[i] ?? "";
|
|
533
|
+
if (arg === "--")
|
|
534
|
+
return [...joined, ...args.slice(i)];
|
|
535
|
+
const name = longOf.get(arg);
|
|
536
|
+
const next = args[i + 1];
|
|
537
|
+
if (name !== undefined && options[name]?.type === "string" && next !== undefined && next.startsWith("-") && next !== "-" && !isOption(next)) {
|
|
538
|
+
joined.push(`--${name}=${next}`);
|
|
539
|
+
i++;
|
|
540
|
+
} else {
|
|
541
|
+
joined.push(arg);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return joined;
|
|
545
|
+
}
|
|
507
546
|
function parseError(error) {
|
|
508
547
|
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
509
548
|
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
@@ -525,7 +564,7 @@ function duration(value, name, fallback, unit = "s") {
|
|
|
525
564
|
badUsage(`--${name} takes a duration such as 30s, 5m or 1h, not ${value}`);
|
|
526
565
|
return ms;
|
|
527
566
|
}
|
|
528
|
-
var Exit, UsageError, HelpShown, completion, OPTIONAL, USAGE_PREFIX, OUTPUT_OPTIONS, DURATION, UNIT_MS;
|
|
567
|
+
var Exit, UsageError, HelpShown, completion, OPTIONAL, USAGE_PREFIX, FULL_STOP, OUTPUT_OPTIONS, DURATION, UNIT_MS;
|
|
529
568
|
var init_args = __esm(() => {
|
|
530
569
|
init_format();
|
|
531
570
|
init_style();
|
|
@@ -551,6 +590,7 @@ var init_args = __esm(() => {
|
|
|
551
590
|
completion = { active: false };
|
|
552
591
|
OPTIONAL = /\[[^\]]+\]/g;
|
|
553
592
|
USAGE_PREFIX = /^usage:\s*/i;
|
|
593
|
+
FULL_STOP = /\.$/;
|
|
554
594
|
OUTPUT_OPTIONS = {
|
|
555
595
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
556
596
|
plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
|
|
@@ -616,9 +656,11 @@ function closest(name, names) {
|
|
|
616
656
|
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
617
657
|
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
618
658
|
}
|
|
619
|
-
async function help({ path, header, commands, defaultCommand, examples }, stream) {
|
|
659
|
+
async function help({ path, header, summary, commands, defaultCommand, examples, replaces }, stream) {
|
|
620
660
|
const colors = paint(stream);
|
|
621
661
|
const prefix = path.join(" ");
|
|
662
|
+
if (wantsHelpJson())
|
|
663
|
+
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
622
664
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
623
665
|
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
624
666
|
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
@@ -626,6 +668,7 @@ async function help({ path, header, commands, defaultCommand, examples }, stream
|
|
|
626
668
|
header(colors),
|
|
627
669
|
"",
|
|
628
670
|
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
671
|
+
...replacesRows(replaces, colors),
|
|
629
672
|
"",
|
|
630
673
|
...commandRows(commands, colors),
|
|
631
674
|
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
@@ -646,13 +689,21 @@ function printError(prefix, message, hint, cause) {
|
|
|
646
689
|
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
647
690
|
console.error(`${head} ${message}${under("cause", cause, colors.dim)}${under("hint", hint, colors.yellow)}`);
|
|
648
691
|
}
|
|
649
|
-
function
|
|
692
|
+
function printJsonError(message, hint, cause) {
|
|
693
|
+
console.error(JSON.stringify({ error: message, ...hint === undefined ? {} : { hint }, ...cause === undefined ? {} : { cause } }));
|
|
694
|
+
}
|
|
695
|
+
function wantsJson(argv) {
|
|
696
|
+
const end = argv.indexOf("--");
|
|
697
|
+
return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
|
|
698
|
+
}
|
|
699
|
+
function reportThrown(prefix, error, redact = (text) => text, json = false) {
|
|
650
700
|
if (error instanceof HelpShown)
|
|
651
701
|
return 0;
|
|
652
702
|
const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
|
|
653
703
|
const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
|
|
654
704
|
const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
|
|
655
|
-
|
|
705
|
+
const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
|
|
706
|
+
report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
|
|
656
707
|
return error instanceof Exit ? error.code : 1;
|
|
657
708
|
}
|
|
658
709
|
async function dispatch(level, typed, { version, redact }) {
|
|
@@ -689,12 +740,12 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
689
740
|
}
|
|
690
741
|
if ("commands" in command) {
|
|
691
742
|
const { summary } = command;
|
|
692
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${summary}`, commands: command.commands, aliases: command.aliases, defaultCommand: undefined, examples: command.examples }, rest, { version, redact });
|
|
743
|
+
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${summary}`, summary, commands: command.commands, aliases: command.aliases, defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
|
|
693
744
|
}
|
|
694
745
|
try {
|
|
695
746
|
return await command.run(rest);
|
|
696
747
|
} catch (error) {
|
|
697
|
-
return reportThrown(`${prefix} ${name}`, error, redact);
|
|
748
|
+
return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
|
|
698
749
|
}
|
|
699
750
|
}
|
|
700
751
|
async function optionsOf(run) {
|
|
@@ -733,7 +784,7 @@ async function printCandidates(found) {
|
|
|
733
784
|
async function runCli(cli, argv = process4.argv.slice(2)) {
|
|
734
785
|
if (argv[0] === COMPLETE)
|
|
735
786
|
return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
|
|
736
|
-
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${cli.about}`, commands: cli.commands, aliases: cli.aliases, defaultCommand: cli.defaultCommand, examples: cli.examples }, argv, { version: cli.version, redact: cli.redact });
|
|
787
|
+
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${cli.about}`, summary: cli.about, commands: cli.commands, aliases: cli.aliases, defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
|
|
737
788
|
}
|
|
738
789
|
var LEVEL_OPTIONS, TOP_OPTIONS, COMPLETE = "__complete", FILES = "__files__";
|
|
739
790
|
var init_cli = __esm(() => {
|
|
@@ -752,6 +803,31 @@ var init_config = __esm(() => {
|
|
|
752
803
|
init_args();
|
|
753
804
|
});
|
|
754
805
|
|
|
806
|
+
// ../kit/src/limit.ts
|
|
807
|
+
function limitOptions(fallback) {
|
|
808
|
+
return {
|
|
809
|
+
limit: { type: "string", description: `show at most this many rows, the rest counted (${fallback})` },
|
|
810
|
+
full: { type: "boolean", description: "show every row" }
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function limitOf(values, fallback) {
|
|
814
|
+
if (values.full === true && values.limit !== undefined)
|
|
815
|
+
badUsage("--limit and --full exclude each other");
|
|
816
|
+
if (values.full === true || values.json === true && values.limit === undefined)
|
|
817
|
+
return Number.POSITIVE_INFINITY;
|
|
818
|
+
return wholeNumber(values.limit, "limit", fallback);
|
|
819
|
+
}
|
|
820
|
+
function moreFooter(more) {
|
|
821
|
+
return `${more} more, rerun with --full`;
|
|
822
|
+
}
|
|
823
|
+
function capped(rows, limit) {
|
|
824
|
+
const more = rows.length - limit;
|
|
825
|
+
return more > 0 ? { shown: rows.slice(0, limit), footer: moreFooter(more) } : { shown: [...rows], footer: undefined };
|
|
826
|
+
}
|
|
827
|
+
var init_limit = __esm(() => {
|
|
828
|
+
init_args();
|
|
829
|
+
});
|
|
830
|
+
|
|
755
831
|
// ../kit/src/process.ts
|
|
756
832
|
import { constants } from "os";
|
|
757
833
|
import process5 from "process";
|
|
@@ -817,11 +893,243 @@ var init_src = __esm(() => {
|
|
|
817
893
|
init_cli();
|
|
818
894
|
init_config();
|
|
819
895
|
init_format();
|
|
896
|
+
init_limit();
|
|
820
897
|
init_process();
|
|
821
898
|
init_style();
|
|
822
899
|
init_text();
|
|
823
900
|
});
|
|
824
901
|
|
|
902
|
+
// src/registry.ts
|
|
903
|
+
function isPublic(registry) {
|
|
904
|
+
return registry.url.replace(TRAILING_SLASHES, "") === PUBLIC_REGISTRY;
|
|
905
|
+
}
|
|
906
|
+
async function get(fetcher, url, token) {
|
|
907
|
+
try {
|
|
908
|
+
return await fetcher(url, { headers: { accept: "application/json", ...token === undefined ? {} : { authorization: `Bearer ${token}` } } });
|
|
909
|
+
} catch (error) {
|
|
910
|
+
return fail(`cannot reach ${new URL(url).host}: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
async function fetchPackument(name, registry, fetcher = fetch) {
|
|
914
|
+
const response = await get(fetcher, `${registry.url.replace(TRAILING_SLASHES, "")}/${name.replace("/", "%2f")}`, registry.token);
|
|
915
|
+
if (response.status === 404)
|
|
916
|
+
return;
|
|
917
|
+
if (!response.ok)
|
|
918
|
+
fail(`${new URL(registry.url).host} answered ${response.status} for ${name}`, 1);
|
|
919
|
+
return await response.json();
|
|
920
|
+
}
|
|
921
|
+
async function fetchDownloads(name, fetcher = fetch) {
|
|
922
|
+
const response = await get(fetcher, `${DOWNLOADS_API}/${name}`);
|
|
923
|
+
if (!response.ok)
|
|
924
|
+
return;
|
|
925
|
+
const body = await response.json();
|
|
926
|
+
return body.downloads;
|
|
927
|
+
}
|
|
928
|
+
var PUBLIC_REGISTRY = "https://registry.npmjs.org", DOWNLOADS_API = "https://api.npmjs.org/downloads/point/last-week", TRAILING_SLASHES;
|
|
929
|
+
var init_registry = __esm(() => {
|
|
930
|
+
init_src();
|
|
931
|
+
TRAILING_SLASHES = /\/+$/;
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// src/config.ts
|
|
935
|
+
import { homedir } from "os";
|
|
936
|
+
import { dirname, join } from "path";
|
|
937
|
+
import process7 from "process";
|
|
938
|
+
function expand(value, env) {
|
|
939
|
+
let missing = false;
|
|
940
|
+
const expanded = value.replace(ENV_REFERENCE, (_, braced, bare) => {
|
|
941
|
+
const found = env[braced ?? bare ?? ""];
|
|
942
|
+
missing ||= found === undefined;
|
|
943
|
+
return found ?? "";
|
|
944
|
+
});
|
|
945
|
+
return missing ? undefined : expanded;
|
|
946
|
+
}
|
|
947
|
+
function parseNpmrc(text, env) {
|
|
948
|
+
const config = { scopes: {}, tokens: {}, scopeTokens: {} };
|
|
949
|
+
for (const line of text.split(`
|
|
950
|
+
`)) {
|
|
951
|
+
const at = line.indexOf("=");
|
|
952
|
+
if (at === -1 || COMMENT.test(line))
|
|
953
|
+
continue;
|
|
954
|
+
const key = line.slice(0, at).trim();
|
|
955
|
+
const value = expand(line.slice(at + 1).trim().replace(QUOTES, "$2"), env);
|
|
956
|
+
if (value === undefined || value === "")
|
|
957
|
+
continue;
|
|
958
|
+
const scope = SCOPE_REGISTRY.exec(key)?.[1];
|
|
959
|
+
const tokenFor = AUTH_TOKEN.exec(key)?.[1];
|
|
960
|
+
if (scope !== undefined)
|
|
961
|
+
config.scopes[scope] = value;
|
|
962
|
+
else if (tokenFor !== undefined)
|
|
963
|
+
config.tokens[tokenFor] = value;
|
|
964
|
+
else if (key === "registry")
|
|
965
|
+
config.registry = value;
|
|
966
|
+
}
|
|
967
|
+
return config;
|
|
968
|
+
}
|
|
969
|
+
function bunEntry(entry, env) {
|
|
970
|
+
const url = typeof entry === "string" ? entry : entry.url;
|
|
971
|
+
const token = typeof entry === "string" || entry.token === undefined ? undefined : expand(entry.token, env);
|
|
972
|
+
return { ...url === undefined ? {} : { url: expand(url, env) ?? url }, ...token === undefined ? {} : { token } };
|
|
973
|
+
}
|
|
974
|
+
function parseBunfig(text, env) {
|
|
975
|
+
const config = { scopes: {}, tokens: {}, scopeTokens: {} };
|
|
976
|
+
const install = Bun.TOML.parse(text).install;
|
|
977
|
+
if (install?.registry !== undefined) {
|
|
978
|
+
const { url, token } = bunEntry(install.registry, env);
|
|
979
|
+
if (url !== undefined)
|
|
980
|
+
config.registry = url;
|
|
981
|
+
if (url !== undefined && token !== undefined)
|
|
982
|
+
config.tokens[url.replace(PROTOCOL, "")] = token;
|
|
983
|
+
}
|
|
984
|
+
for (const [scope, entry] of Object.entries(install?.scopes ?? {})) {
|
|
985
|
+
const { url, token } = bunEntry(entry, env);
|
|
986
|
+
if (url !== undefined)
|
|
987
|
+
config.scopes[scope.startsWith("@") ? scope : `@${scope}`] = url;
|
|
988
|
+
if (token !== undefined)
|
|
989
|
+
config.scopeTokens[scope.startsWith("@") ? scope : `@${scope}`] = token;
|
|
990
|
+
}
|
|
991
|
+
return config;
|
|
992
|
+
}
|
|
993
|
+
function tokenFor(config, url) {
|
|
994
|
+
const address = url.replace(PROTOCOL, "");
|
|
995
|
+
const best = Object.keys(config.tokens).filter((key) => address.startsWith(key)).sort((a, b) => b.length - a.length)[0];
|
|
996
|
+
return best === undefined ? undefined : config.tokens[best];
|
|
997
|
+
}
|
|
998
|
+
function registryFor(name, configs) {
|
|
999
|
+
const scope = name.startsWith("@") ? name.slice(0, name.indexOf("/")) : undefined;
|
|
1000
|
+
for (const config of configs) {
|
|
1001
|
+
const url = scope === undefined ? config.registry : config.scopes[scope] ?? config.registry;
|
|
1002
|
+
if (url === undefined)
|
|
1003
|
+
continue;
|
|
1004
|
+
const token = (scope === undefined ? undefined : config.scopeTokens[scope]) ?? tokenFor(config, url) ?? configs.map((other) => tokenFor(other, url)).find((found) => found !== undefined);
|
|
1005
|
+
return { url, token };
|
|
1006
|
+
}
|
|
1007
|
+
return { url: PUBLIC_REGISTRY };
|
|
1008
|
+
}
|
|
1009
|
+
async function readIfThere(path) {
|
|
1010
|
+
const file = Bun.file(path);
|
|
1011
|
+
return await file.exists() ? file.text() : undefined;
|
|
1012
|
+
}
|
|
1013
|
+
async function loadConfigs(cwd = process7.cwd(), env = process7.env, home = homedir()) {
|
|
1014
|
+
const directories = [];
|
|
1015
|
+
for (let dir = cwd;!directories.includes(dir); dir = dirname(dir))
|
|
1016
|
+
directories.push(dir);
|
|
1017
|
+
if (!directories.includes(home))
|
|
1018
|
+
directories.push(home);
|
|
1019
|
+
const configs = [];
|
|
1020
|
+
for (const dir of directories) {
|
|
1021
|
+
const bunfig = await readIfThere(join(dir, "bunfig.toml"));
|
|
1022
|
+
const npmrc = await readIfThere(join(dir, ".npmrc"));
|
|
1023
|
+
if (bunfig !== undefined)
|
|
1024
|
+
configs.push(parseBunfig(bunfig, env));
|
|
1025
|
+
if (npmrc !== undefined)
|
|
1026
|
+
configs.push(parseNpmrc(npmrc, env));
|
|
1027
|
+
}
|
|
1028
|
+
return configs;
|
|
1029
|
+
}
|
|
1030
|
+
function isSeverity(value) {
|
|
1031
|
+
return SEVERITIES.includes(value);
|
|
1032
|
+
}
|
|
1033
|
+
function parseSettings(text) {
|
|
1034
|
+
const raw = Bun.JSONC.parse(text);
|
|
1035
|
+
const settings = {};
|
|
1036
|
+
if (Array.isArray(raw?.licenses) && raw.licenses.every((each) => typeof each === "string"))
|
|
1037
|
+
settings.licenses = raw.licenses;
|
|
1038
|
+
if (isSeverity(raw?.blockSeverity))
|
|
1039
|
+
settings.blockSeverity = raw.blockSeverity;
|
|
1040
|
+
return settings;
|
|
1041
|
+
}
|
|
1042
|
+
async function loadSettings(cwd = process7.cwd(), home = homedir()) {
|
|
1043
|
+
const files = [];
|
|
1044
|
+
for (let dir = cwd;!files.includes(join(dir, SETTINGS_FILE)); dir = dirname(dir))
|
|
1045
|
+
files.push(join(dir, SETTINGS_FILE));
|
|
1046
|
+
files.push(join(home, ".config", "pkg", "config.json"));
|
|
1047
|
+
const settings = {};
|
|
1048
|
+
for (const file of files) {
|
|
1049
|
+
const text = await readIfThere(file);
|
|
1050
|
+
if (text === undefined)
|
|
1051
|
+
continue;
|
|
1052
|
+
const found = parseSettings(text);
|
|
1053
|
+
settings.licenses ??= found.licenses;
|
|
1054
|
+
settings.blockSeverity ??= found.blockSeverity;
|
|
1055
|
+
}
|
|
1056
|
+
return settings;
|
|
1057
|
+
}
|
|
1058
|
+
var ENV_REFERENCE, COMMENT, SCOPE_REGISTRY, AUTH_TOKEN, PROTOCOL, QUOTES, SEVERITIES, SETTINGS_FILE = ".pkgrc.json";
|
|
1059
|
+
var init_config2 = __esm(() => {
|
|
1060
|
+
init_registry();
|
|
1061
|
+
ENV_REFERENCE = /\$\{(\w+)\}|\$(\w+)/g;
|
|
1062
|
+
COMMENT = /^\s*[#;]/;
|
|
1063
|
+
SCOPE_REGISTRY = /^(@[^:]+):registry$/;
|
|
1064
|
+
AUTH_TOKEN = /^(\/\/.+?):_authToken$/;
|
|
1065
|
+
PROTOCOL = /^\w+:/;
|
|
1066
|
+
QUOTES = /^(["'])(.*)\1$/;
|
|
1067
|
+
SEVERITIES = ["low", "moderate", "high", "critical"];
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// src/advisories.ts
|
|
1071
|
+
function severityRank(severity) {
|
|
1072
|
+
return SEVERITIES.indexOf(severity);
|
|
1073
|
+
}
|
|
1074
|
+
function fixedVersion(vulnerable, version) {
|
|
1075
|
+
const range = vulnerable.split("||").map((each) => each.trim()).find((each) => Bun.semver.satisfies(version, each));
|
|
1076
|
+
return range === undefined ? undefined : UPPER_BOUND.exec(range)?.[1];
|
|
1077
|
+
}
|
|
1078
|
+
async function fetchAdvisories(versions, registry, fetcher = fetch) {
|
|
1079
|
+
const url = `${registry.url.replace(TRAILING_SLASHES2, "")}${BULK_PATH}`;
|
|
1080
|
+
let response;
|
|
1081
|
+
try {
|
|
1082
|
+
response = await fetcher(url, {
|
|
1083
|
+
method: "POST",
|
|
1084
|
+
headers: { accept: "application/json", "content-type": "application/json", ...registry.token === undefined ? {} : { authorization: `Bearer ${registry.token}` } },
|
|
1085
|
+
body: JSON.stringify(versions)
|
|
1086
|
+
});
|
|
1087
|
+
} catch {
|
|
1088
|
+
return {};
|
|
1089
|
+
}
|
|
1090
|
+
if (!response.ok)
|
|
1091
|
+
return {};
|
|
1092
|
+
const body = await response.json();
|
|
1093
|
+
const found = {};
|
|
1094
|
+
for (const [name, raws] of Object.entries(body)) {
|
|
1095
|
+
found[name] = raws.filter((raw) => SEVERITIES.includes(raw.severity)).map((raw) => ({
|
|
1096
|
+
id: raw.id,
|
|
1097
|
+
ghsa: GHSA.exec(raw.url)?.[0],
|
|
1098
|
+
url: raw.url,
|
|
1099
|
+
title: raw.title,
|
|
1100
|
+
severity: raw.severity,
|
|
1101
|
+
vulnerable: raw.vulnerable_versions
|
|
1102
|
+
}));
|
|
1103
|
+
}
|
|
1104
|
+
return found;
|
|
1105
|
+
}
|
|
1106
|
+
function advisoriesFor(advisories, version) {
|
|
1107
|
+
return (advisories ?? []).filter((each) => Bun.semver.satisfies(version, each.vulnerable)).map((each) => ({ ...each, fixed: fixedVersion(each.vulnerable, version) }));
|
|
1108
|
+
}
|
|
1109
|
+
function filterAdvisories(advisories, level, ignore = []) {
|
|
1110
|
+
return advisories.filter((each) => severityRank(each.severity) >= severityRank(level) && !ignore.includes(each.ghsa ?? "") && !ignore.includes(String(each.id)));
|
|
1111
|
+
}
|
|
1112
|
+
var BULK_PATH = "/-/npm/v1/security/advisories/bulk", TRAILING_SLASHES2, GHSA, UPPER_BOUND;
|
|
1113
|
+
var init_advisories = __esm(() => {
|
|
1114
|
+
init_config2();
|
|
1115
|
+
TRAILING_SLASHES2 = /\/+$/;
|
|
1116
|
+
GHSA = /GHSA-[\w-]+$/;
|
|
1117
|
+
UPPER_BOUND = /(?:^|\s)<([\w.+-]+)/;
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// src/licenses.ts
|
|
1121
|
+
function isLicenseAllowed(expression, allowed) {
|
|
1122
|
+
const known = new Set(allowed.map((each) => each.toLowerCase()));
|
|
1123
|
+
return expression.replace(PARENS, " ").trim().split(OR_SPLIT).some((alternative) => alternative.split(AND_SPLIT).every((term) => known.has(term.replace(WITH_EXCEPTION, "").trim().toLowerCase())));
|
|
1124
|
+
}
|
|
1125
|
+
var OR_SPLIT, AND_SPLIT, WITH_EXCEPTION, PARENS;
|
|
1126
|
+
var init_licenses = __esm(() => {
|
|
1127
|
+
OR_SPLIT = /\s+OR\s+/i;
|
|
1128
|
+
AND_SPLIT = /\s+AND\s+/i;
|
|
1129
|
+
WITH_EXCEPTION = /\s+WITH\s+\S+$/i;
|
|
1130
|
+
PARENS = /[()]/g;
|
|
1131
|
+
});
|
|
1132
|
+
|
|
825
1133
|
// src/popular.json
|
|
826
1134
|
var popular_default;
|
|
827
1135
|
var init_popular = __esm(() => {
|
|
@@ -1405,7 +1713,7 @@ function safeRelease(packument, now, minAgeDays) {
|
|
|
1405
1713
|
}
|
|
1406
1714
|
function analyze(input) {
|
|
1407
1715
|
const { spec, packument, registry, now = Date.now(), minAgeDays = DEFAULT_MIN_AGE_DAYS } = input;
|
|
1408
|
-
const report = { name: spec.name, registry, exists: packument !== undefined, requested: spec.request, installScripts: {}, nearMisses: nearMisses(spec.name), warnings: [], blocked: false, downloads: input.downloads };
|
|
1716
|
+
const report = { name: spec.name, registry, exists: packument !== undefined, requested: spec.request, installScripts: {}, nearMisses: nearMisses(spec.name), advisories: input.advisories ?? [], warnings: [], blocked: false, downloads: input.downloads };
|
|
1409
1717
|
if (report.nearMisses.length > 0)
|
|
1410
1718
|
report.warnings.push(`name looks like ${report.nearMisses.join("; ")}, check it is the package meant`);
|
|
1411
1719
|
if (packument === undefined) {
|
|
@@ -1442,155 +1750,35 @@ function analyze(input) {
|
|
|
1442
1750
|
report.warnings.push(`${version} runs ${scripts.join(", ")} at install`);
|
|
1443
1751
|
if (report.deprecated !== undefined)
|
|
1444
1752
|
report.warnings.push(`${version} is deprecated: ${report.deprecated}`);
|
|
1753
|
+
if (input.licenses !== undefined) {
|
|
1754
|
+
const isMissing = report.license === undefined || report.license === "";
|
|
1755
|
+
if (isMissing) {
|
|
1756
|
+
report.blocked = true;
|
|
1757
|
+
report.warnings.push(`${version} declares no licence, and the allowlist needs one`);
|
|
1758
|
+
} else if (!isLicenseAllowed(report.license ?? "", input.licenses)) {
|
|
1759
|
+
report.blocked = true;
|
|
1760
|
+
report.warnings.push(`licence ${report.license} is not in the allowlist (${input.licenses.join(", ")})`);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (input.blockSeverity !== undefined) {
|
|
1764
|
+
const blocking = filterAdvisories(report.advisories, input.blockSeverity);
|
|
1765
|
+
if (blocking.length > 0) {
|
|
1766
|
+
report.blocked = true;
|
|
1767
|
+
report.warnings.push(`${version} has ${blocking.length} advisor${blocking.length === 1 ? "y" : "ies"} at ${input.blockSeverity} or above: ${blocking.map((each) => each.ghsa ?? each.id).join(", ")}`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1445
1770
|
return report;
|
|
1446
1771
|
}
|
|
1447
1772
|
var DEFAULT_MIN_AGE_DAYS = 14, DAY = 86400000, INSTALL_SCRIPTS, GIT_PREFIX, VERSION_SEPARATOR;
|
|
1448
1773
|
var init_analyze = __esm(() => {
|
|
1774
|
+
init_advisories();
|
|
1775
|
+
init_licenses();
|
|
1449
1776
|
init_nearmiss();
|
|
1450
1777
|
INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
|
|
1451
1778
|
GIT_PREFIX = /^git\+/;
|
|
1452
1779
|
VERSION_SEPARATOR = /(?<=.)@/;
|
|
1453
1780
|
});
|
|
1454
1781
|
|
|
1455
|
-
// src/registry.ts
|
|
1456
|
-
function isPublic(registry) {
|
|
1457
|
-
return registry.url.replace(TRAILING_SLASHES, "") === PUBLIC_REGISTRY;
|
|
1458
|
-
}
|
|
1459
|
-
async function get(fetcher, url, token) {
|
|
1460
|
-
try {
|
|
1461
|
-
return await fetcher(url, { headers: { accept: "application/json", ...token === undefined ? {} : { authorization: `Bearer ${token}` } } });
|
|
1462
|
-
} catch (error) {
|
|
1463
|
-
return fail(`cannot reach ${new URL(url).host}: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
async function fetchPackument(name, registry, fetcher = fetch) {
|
|
1467
|
-
const response = await get(fetcher, `${registry.url.replace(TRAILING_SLASHES, "")}/${name.replace("/", "%2f")}`, registry.token);
|
|
1468
|
-
if (response.status === 404)
|
|
1469
|
-
return;
|
|
1470
|
-
if (!response.ok)
|
|
1471
|
-
fail(`${new URL(registry.url).host} answered ${response.status} for ${name}`, 1);
|
|
1472
|
-
return await response.json();
|
|
1473
|
-
}
|
|
1474
|
-
async function fetchDownloads(name, fetcher = fetch) {
|
|
1475
|
-
const response = await get(fetcher, `${DOWNLOADS_API}/${name}`);
|
|
1476
|
-
if (!response.ok)
|
|
1477
|
-
return;
|
|
1478
|
-
const body = await response.json();
|
|
1479
|
-
return body.downloads;
|
|
1480
|
-
}
|
|
1481
|
-
var PUBLIC_REGISTRY = "https://registry.npmjs.org", DOWNLOADS_API = "https://api.npmjs.org/downloads/point/last-week", TRAILING_SLASHES;
|
|
1482
|
-
var init_registry = __esm(() => {
|
|
1483
|
-
init_src();
|
|
1484
|
-
TRAILING_SLASHES = /\/+$/;
|
|
1485
|
-
});
|
|
1486
|
-
|
|
1487
|
-
// src/config.ts
|
|
1488
|
-
import { homedir } from "os";
|
|
1489
|
-
import { dirname, join } from "path";
|
|
1490
|
-
import process7 from "process";
|
|
1491
|
-
function expand(value, env) {
|
|
1492
|
-
let missing = false;
|
|
1493
|
-
const expanded = value.replace(ENV_REFERENCE, (_, braced, bare) => {
|
|
1494
|
-
const found = env[braced ?? bare ?? ""];
|
|
1495
|
-
missing ||= found === undefined;
|
|
1496
|
-
return found ?? "";
|
|
1497
|
-
});
|
|
1498
|
-
return missing ? undefined : expanded;
|
|
1499
|
-
}
|
|
1500
|
-
function parseNpmrc(text, env) {
|
|
1501
|
-
const config = { scopes: {}, tokens: {}, scopeTokens: {} };
|
|
1502
|
-
for (const line of text.split(`
|
|
1503
|
-
`)) {
|
|
1504
|
-
const at = line.indexOf("=");
|
|
1505
|
-
if (at === -1 || COMMENT.test(line))
|
|
1506
|
-
continue;
|
|
1507
|
-
const key = line.slice(0, at).trim();
|
|
1508
|
-
const value = expand(line.slice(at + 1).trim().replace(QUOTES, "$2"), env);
|
|
1509
|
-
if (value === undefined || value === "")
|
|
1510
|
-
continue;
|
|
1511
|
-
const scope = SCOPE_REGISTRY.exec(key)?.[1];
|
|
1512
|
-
const tokenFor = AUTH_TOKEN.exec(key)?.[1];
|
|
1513
|
-
if (scope !== undefined)
|
|
1514
|
-
config.scopes[scope] = value;
|
|
1515
|
-
else if (tokenFor !== undefined)
|
|
1516
|
-
config.tokens[tokenFor] = value;
|
|
1517
|
-
else if (key === "registry")
|
|
1518
|
-
config.registry = value;
|
|
1519
|
-
}
|
|
1520
|
-
return config;
|
|
1521
|
-
}
|
|
1522
|
-
function bunEntry(entry, env) {
|
|
1523
|
-
const url = typeof entry === "string" ? entry : entry.url;
|
|
1524
|
-
const token = typeof entry === "string" || entry.token === undefined ? undefined : expand(entry.token, env);
|
|
1525
|
-
return { ...url === undefined ? {} : { url: expand(url, env) ?? url }, ...token === undefined ? {} : { token } };
|
|
1526
|
-
}
|
|
1527
|
-
function parseBunfig(text, env) {
|
|
1528
|
-
const config = { scopes: {}, tokens: {}, scopeTokens: {} };
|
|
1529
|
-
const install = Bun.TOML.parse(text).install;
|
|
1530
|
-
if (install?.registry !== undefined) {
|
|
1531
|
-
const { url, token } = bunEntry(install.registry, env);
|
|
1532
|
-
if (url !== undefined)
|
|
1533
|
-
config.registry = url;
|
|
1534
|
-
if (url !== undefined && token !== undefined)
|
|
1535
|
-
config.tokens[url.replace(PROTOCOL, "")] = token;
|
|
1536
|
-
}
|
|
1537
|
-
for (const [scope, entry] of Object.entries(install?.scopes ?? {})) {
|
|
1538
|
-
const { url, token } = bunEntry(entry, env);
|
|
1539
|
-
if (url !== undefined)
|
|
1540
|
-
config.scopes[scope.startsWith("@") ? scope : `@${scope}`] = url;
|
|
1541
|
-
if (token !== undefined)
|
|
1542
|
-
config.scopeTokens[scope.startsWith("@") ? scope : `@${scope}`] = token;
|
|
1543
|
-
}
|
|
1544
|
-
return config;
|
|
1545
|
-
}
|
|
1546
|
-
function tokenFor(config, url) {
|
|
1547
|
-
const address = url.replace(PROTOCOL, "");
|
|
1548
|
-
const best = Object.keys(config.tokens).filter((key) => address.startsWith(key)).sort((a, b) => b.length - a.length)[0];
|
|
1549
|
-
return best === undefined ? undefined : config.tokens[best];
|
|
1550
|
-
}
|
|
1551
|
-
function registryFor(name, configs) {
|
|
1552
|
-
const scope = name.startsWith("@") ? name.slice(0, name.indexOf("/")) : undefined;
|
|
1553
|
-
for (const config of configs) {
|
|
1554
|
-
const url = scope === undefined ? config.registry : config.scopes[scope] ?? config.registry;
|
|
1555
|
-
if (url === undefined)
|
|
1556
|
-
continue;
|
|
1557
|
-
const token = (scope === undefined ? undefined : config.scopeTokens[scope]) ?? tokenFor(config, url) ?? configs.map((other) => tokenFor(other, url)).find((found) => found !== undefined);
|
|
1558
|
-
return { url, token };
|
|
1559
|
-
}
|
|
1560
|
-
return { url: PUBLIC_REGISTRY };
|
|
1561
|
-
}
|
|
1562
|
-
async function readIfThere(path) {
|
|
1563
|
-
const file = Bun.file(path);
|
|
1564
|
-
return await file.exists() ? file.text() : undefined;
|
|
1565
|
-
}
|
|
1566
|
-
async function loadConfigs(cwd = process7.cwd(), env = process7.env, home = homedir()) {
|
|
1567
|
-
const directories = [];
|
|
1568
|
-
for (let dir = cwd;!directories.includes(dir); dir = dirname(dir))
|
|
1569
|
-
directories.push(dir);
|
|
1570
|
-
if (!directories.includes(home))
|
|
1571
|
-
directories.push(home);
|
|
1572
|
-
const configs = [];
|
|
1573
|
-
for (const dir of directories) {
|
|
1574
|
-
const bunfig = await readIfThere(join(dir, "bunfig.toml"));
|
|
1575
|
-
const npmrc = await readIfThere(join(dir, ".npmrc"));
|
|
1576
|
-
if (bunfig !== undefined)
|
|
1577
|
-
configs.push(parseBunfig(bunfig, env));
|
|
1578
|
-
if (npmrc !== undefined)
|
|
1579
|
-
configs.push(parseNpmrc(npmrc, env));
|
|
1580
|
-
}
|
|
1581
|
-
return configs;
|
|
1582
|
-
}
|
|
1583
|
-
var ENV_REFERENCE, COMMENT, SCOPE_REGISTRY, AUTH_TOKEN, PROTOCOL, QUOTES;
|
|
1584
|
-
var init_config2 = __esm(() => {
|
|
1585
|
-
init_registry();
|
|
1586
|
-
ENV_REFERENCE = /\$\{(\w+)\}|\$(\w+)/g;
|
|
1587
|
-
COMMENT = /^\s*[#;]/;
|
|
1588
|
-
SCOPE_REGISTRY = /^(@[^:]+):registry$/;
|
|
1589
|
-
AUTH_TOKEN = /^(\/\/.+?):_authToken$/;
|
|
1590
|
-
PROTOCOL = /^\w+:/;
|
|
1591
|
-
QUOTES = /^(["'])(.*)\1$/;
|
|
1592
|
-
});
|
|
1593
|
-
|
|
1594
1782
|
// src/commands/check.ts
|
|
1595
1783
|
var exports_check = {};
|
|
1596
1784
|
__export(exports_check, {
|
|
@@ -1601,7 +1789,13 @@ async function checkPackage(text, context) {
|
|
|
1601
1789
|
const registry = registryFor(spec.name, context.configs);
|
|
1602
1790
|
const packument = await fetchPackument(spec.name, registry, context.fetcher);
|
|
1603
1791
|
const downloads = packument !== undefined && isPublic(registry) ? await fetchDownloads(spec.name, context.fetcher) : undefined;
|
|
1604
|
-
|
|
1792
|
+
const input = { spec, packument, downloads, registry: new URL(registry.url).host, now: context.now, minAgeDays: context.minAgeDays, licenses: context.settings?.licenses };
|
|
1793
|
+
const report = analyze(input);
|
|
1794
|
+
const { blockSeverity } = context.settings ?? {};
|
|
1795
|
+
if (blockSeverity === undefined || report.version === undefined)
|
|
1796
|
+
return report;
|
|
1797
|
+
const found = await fetchAdvisories({ [spec.name]: [report.version] }, registry, context.fetcher);
|
|
1798
|
+
return analyze({ ...input, blockSeverity, advisories: advisoriesFor(found[spec.name], report.version) });
|
|
1605
1799
|
}
|
|
1606
1800
|
function when(release, now) {
|
|
1607
1801
|
return release?.published === undefined ? "" : `, published ${formatAge(release.published, now)} (${release.published.slice(0, 10)})`;
|
|
@@ -1634,7 +1828,7 @@ async function check(args) {
|
|
|
1634
1828
|
fail(`a package name is needed
|
|
1635
1829
|
|
|
1636
1830
|
${usage}`);
|
|
1637
|
-
const context = { configs: await loadConfigs(), minAgeDays: wholeNumber(values["min-age"], "min-age", DEFAULT_MIN_AGE_DAYS, 0) };
|
|
1831
|
+
const context = { configs: await loadConfigs(), settings: await loadSettings(), minAgeDays: wholeNumber(values["min-age"], "min-age", DEFAULT_MIN_AGE_DAYS, 0) };
|
|
1638
1832
|
const reports = await Promise.all(positionals.map(async (text) => checkPackage(text, context)));
|
|
1639
1833
|
if (values.json === true)
|
|
1640
1834
|
printJson(reports.length === 1 ? reports[0] : reports);
|
|
@@ -1647,9 +1841,13 @@ ${usage}`);
|
|
|
1647
1841
|
var usage = `pkg [check] <name>[@version]...
|
|
1648
1842
|
|
|
1649
1843
|
Checks npm packages before they are added: whether the name exists, the latest and the newest version at least two weeks old, weekly downloads, licence, repository, install scripts and a near miss of a popular name.
|
|
1650
|
-
|
|
1844
|
+
|
|
1845
|
+
A \`.pkgrc.json\` (or ~/.config/pkg/config.json) with \`licenses\` (SPDX ids) refuses a licence outside the list or a missing one, and \`blockSeverity\` (low, moderate, high, critical) refuses a version with an advisory at that level or above.
|
|
1846
|
+
|
|
1847
|
+
Exits 0 when every package passes, 1 when one is missing, the version asked for is under the minimum age, its licence is outside the allowlist or an advisory reaches the threshold, 2 on a usage error.`, options;
|
|
1651
1848
|
var init_check = __esm(() => {
|
|
1652
1849
|
init_src();
|
|
1850
|
+
init_advisories();
|
|
1653
1851
|
init_analyze();
|
|
1654
1852
|
init_config2();
|
|
1655
1853
|
init_registry();
|
|
@@ -1659,9 +1857,126 @@ var init_check = __esm(() => {
|
|
|
1659
1857
|
};
|
|
1660
1858
|
});
|
|
1661
1859
|
|
|
1662
|
-
//
|
|
1860
|
+
// src/lockfile.ts
|
|
1861
|
+
function parseBunLock(text) {
|
|
1862
|
+
const lock = Bun.JSONC.parse(text);
|
|
1863
|
+
const seen = new Map;
|
|
1864
|
+
for (const entry of Object.values(lock?.packages ?? {})) {
|
|
1865
|
+
const resolution = entry[0];
|
|
1866
|
+
if (typeof resolution !== "string")
|
|
1867
|
+
continue;
|
|
1868
|
+
const at = resolution.search(VERSION_SEPARATOR2);
|
|
1869
|
+
const name = resolution.slice(0, at);
|
|
1870
|
+
const version = resolution.slice(at + 1);
|
|
1871
|
+
if (at !== -1 && !NOT_REGISTRY.test(version))
|
|
1872
|
+
seen.set(`${name}@${version}`, { name, version });
|
|
1873
|
+
}
|
|
1874
|
+
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name) || Bun.semver.order(a.version, b.version));
|
|
1875
|
+
}
|
|
1876
|
+
var NOT_REGISTRY, VERSION_SEPARATOR2;
|
|
1877
|
+
var init_lockfile = __esm(() => {
|
|
1878
|
+
NOT_REGISTRY = /^(?:workspace|link|file|github|git|http|https|npm):|^[\w.-]+\/[\w.-]+$/;
|
|
1879
|
+
VERSION_SEPARATOR2 = /(?<=.)@/;
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
// src/commands/audit.ts
|
|
1883
|
+
var exports_audit = {};
|
|
1884
|
+
__export(exports_audit, {
|
|
1885
|
+
audit: () => audit
|
|
1886
|
+
});
|
|
1887
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
1663
1888
|
import process8 from "process";
|
|
1664
|
-
function
|
|
1889
|
+
function hasFinding(audited) {
|
|
1890
|
+
return audited.blocked || audited.warnings.length > 0 || audited.advisories.length > 0;
|
|
1891
|
+
}
|
|
1892
|
+
async function auditPackages(packages, context) {
|
|
1893
|
+
const packuments = new Map;
|
|
1894
|
+
const lookup = async (name, registry) => {
|
|
1895
|
+
const key = `${registry.url}|${name}`;
|
|
1896
|
+
if (!packuments.has(key))
|
|
1897
|
+
packuments.set(key, fetchPackument(name, registry, context.fetcher));
|
|
1898
|
+
return packuments.get(key);
|
|
1899
|
+
};
|
|
1900
|
+
const registries = new Map;
|
|
1901
|
+
for (const { name, version } of packages) {
|
|
1902
|
+
const registry = registryFor(name, context.configs);
|
|
1903
|
+
const group = registries.get(registry.url) ?? { registry, versions: {} };
|
|
1904
|
+
group.versions[name] = [...group.versions[name] ?? [], version];
|
|
1905
|
+
registries.set(registry.url, group);
|
|
1906
|
+
}
|
|
1907
|
+
const advisories = {};
|
|
1908
|
+
for (const { registry, versions } of registries.values())
|
|
1909
|
+
Object.assign(advisories, await fetchAdvisories(versions, registry, context.fetcher));
|
|
1910
|
+
const audited = [];
|
|
1911
|
+
for (let start = 0;start < packages.length; start += BATCH) {
|
|
1912
|
+
audited.push(...await Promise.all(packages.slice(start, start + BATCH).map(async ({ name, version }) => {
|
|
1913
|
+
const registry = registryFor(name, context.configs);
|
|
1914
|
+
const found = filterAdvisories(advisoriesFor(advisories[name], version), context.level, context.ignore);
|
|
1915
|
+
const report = analyze({ spec: { name, request: version }, packument: await lookup(name, registry), registry: new URL(registry.url).host, now: context.now, minAgeDays: context.minAgeDays, licenses: context.settings.licenses });
|
|
1916
|
+
return { name, version, license: report.license, blocked: report.blocked || found.length > 0, warnings: report.warnings, advisories: found };
|
|
1917
|
+
})));
|
|
1918
|
+
}
|
|
1919
|
+
return audited;
|
|
1920
|
+
}
|
|
1921
|
+
function renderAudit(findings, total) {
|
|
1922
|
+
const blocks = findings.map(({ name, version, warnings, advisories }) => [
|
|
1923
|
+
`${name}@${version}`,
|
|
1924
|
+
...warnings.map((warning) => ` warning: ${warning}`),
|
|
1925
|
+
...advisories.map((each) => ` ${each.ghsa ?? each.id} ${each.severity}: ${each.title}${each.fixed === undefined ? "" : ` (fixed in ${each.fixed})`}`)
|
|
1926
|
+
].join(`
|
|
1927
|
+
`));
|
|
1928
|
+
return [...blocks, `${findings.length} of ${total} packages with a finding, ${findings.filter((each) => each.blocked).length} blocking`].join(`
|
|
1929
|
+
|
|
1930
|
+
`);
|
|
1931
|
+
}
|
|
1932
|
+
async function audit(args) {
|
|
1933
|
+
const { values, positionals: [asked] } = parse(args, usage2, options2, { positionals: ["[dir]"] });
|
|
1934
|
+
const level = values["audit-level"] ?? "low";
|
|
1935
|
+
if (!isSeverity(level))
|
|
1936
|
+
fail(`--audit-level takes ${SEVERITIES.join(", ")}, not ${level}`);
|
|
1937
|
+
const dir = resolve2(asked ?? process8.cwd());
|
|
1938
|
+
const lock = Bun.file(join2(dir, "bun.lock"));
|
|
1939
|
+
if (!await lock.exists())
|
|
1940
|
+
fail(`no bun.lock in ${dir}`);
|
|
1941
|
+
const packages = parseBunLock(await lock.text());
|
|
1942
|
+
const context = {
|
|
1943
|
+
configs: await loadConfigs(dir),
|
|
1944
|
+
settings: await loadSettings(dir),
|
|
1945
|
+
minAgeDays: wholeNumber(values["min-age"], "min-age", DEFAULT_MIN_AGE_DAYS, 0),
|
|
1946
|
+
level,
|
|
1947
|
+
ignore: values.ignore ?? []
|
|
1948
|
+
};
|
|
1949
|
+
const findings = (await auditPackages(packages, context)).filter(hasFinding);
|
|
1950
|
+
if (values.json === true)
|
|
1951
|
+
printJson(findings);
|
|
1952
|
+
else
|
|
1953
|
+
console.log(renderAudit(findings, packages.length));
|
|
1954
|
+
return findings.some((each) => each.blocked) ? 1 : 0;
|
|
1955
|
+
}
|
|
1956
|
+
var usage2 = `pkg audit [dir]
|
|
1957
|
+
|
|
1958
|
+
Reads the bun.lock in the folder (the current one by default) and runs on every registry package it pins the checks \`pkg check\` runs before an add: age, install scripts, a deprecated version, a near miss of a popular name, and the licence allowlist of the pkg config. It also lists the known advisories of each version, with the GHSA, the severity and the fixed version, from the registry's bulk advisory endpoint, the one \`bun audit\` reads.
|
|
1959
|
+
Findings are grouped per package, each registry package looked up once per run.
|
|
1960
|
+
|
|
1961
|
+
Exits 0 when nothing blocks, 1 when a package is missing, too fresh or outside the licence allowlist, or an advisory reaches --audit-level, 2 on a usage error or without a bun.lock.`, options2, BATCH = 8;
|
|
1962
|
+
var init_audit = __esm(() => {
|
|
1963
|
+
init_src();
|
|
1964
|
+
init_advisories();
|
|
1965
|
+
init_analyze();
|
|
1966
|
+
init_config2();
|
|
1967
|
+
init_lockfile();
|
|
1968
|
+
init_registry();
|
|
1969
|
+
options2 = {
|
|
1970
|
+
json: jsonOption("[{name, version, license, blocked, warnings, advisories}] for the packages with a finding"),
|
|
1971
|
+
"min-age": { type: "string", description: `days a version must be old (${DEFAULT_MIN_AGE_DAYS})` },
|
|
1972
|
+
"audit-level": { type: "string", description: "lowest advisory severity listed and failing: low, moderate, high, critical (low)" },
|
|
1973
|
+
ignore: { type: "string", multiple: true, description: "an advisory skipped by GHSA or numeric id, repeatable" }
|
|
1974
|
+
};
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
// ../kit/src/progress.ts
|
|
1978
|
+
import process9 from "process";
|
|
1979
|
+
function liveRegion(render, stream = process9.stderr, env = process9.env) {
|
|
1665
1980
|
if (!takesControlCodes(stream, env))
|
|
1666
1981
|
return { redraw: () => {
|
|
1667
1982
|
return;
|
|
@@ -1697,11 +2012,11 @@ function liveRegion(render, stream = process8.stderr, env = process8.env) {
|
|
|
1697
2012
|
clearInterval(timer);
|
|
1698
2013
|
clear();
|
|
1699
2014
|
restore();
|
|
1700
|
-
|
|
2015
|
+
process9.off("exit", onExit);
|
|
1701
2016
|
signals.release();
|
|
1702
2017
|
}
|
|
1703
2018
|
timer.unref();
|
|
1704
|
-
|
|
2019
|
+
process9.on("exit", onExit);
|
|
1705
2020
|
return {
|
|
1706
2021
|
redraw: draw,
|
|
1707
2022
|
print: (text) => {
|
|
@@ -1740,7 +2055,7 @@ function cutToWidth(line, width) {
|
|
|
1740
2055
|
return `${kept}${RESET_STYLE}${linked ? CLOSE_LINK : ""}`;
|
|
1741
2056
|
}
|
|
1742
2057
|
function interceptOutput(clear) {
|
|
1743
|
-
const targets = [...CONSOLE_METHODS.map((name) => [console, name]), [
|
|
2058
|
+
const targets = [...CONSOLE_METHODS.map((name) => [console, name]), [process9.stdout, "write"], [process9.stderr, "write"]];
|
|
1744
2059
|
const wrapped = targets.map(([target, name]) => {
|
|
1745
2060
|
const original = Reflect.get(target, name);
|
|
1746
2061
|
const wrapper = (...args) => {
|
|
@@ -1820,7 +2135,7 @@ var init_pin = __esm(() => {
|
|
|
1820
2135
|
|
|
1821
2136
|
// src/consumers.ts
|
|
1822
2137
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
1823
|
-
import { dirname as dirname2, join as
|
|
2138
|
+
import { dirname as dirname2, join as join3, relative } from "path";
|
|
1824
2139
|
function packageFiles(dir, depth, found) {
|
|
1825
2140
|
let entries;
|
|
1826
2141
|
try {
|
|
@@ -1830,15 +2145,15 @@ function packageFiles(dir, depth, found) {
|
|
|
1830
2145
|
}
|
|
1831
2146
|
for (const entry of entries) {
|
|
1832
2147
|
if (entry.isFile() && entry.name === "package.json")
|
|
1833
|
-
found.push(
|
|
2148
|
+
found.push(join3(dir, entry.name));
|
|
1834
2149
|
else if (entry.isDirectory() && depth < MAX_DEPTH && !entry.name.startsWith(".") && !SKIPPED.has(entry.name))
|
|
1835
|
-
packageFiles(
|
|
2150
|
+
packageFiles(join3(dir, entry.name), depth + 1, found);
|
|
1836
2151
|
}
|
|
1837
2152
|
return found;
|
|
1838
2153
|
}
|
|
1839
2154
|
function managerFor(dir, root) {
|
|
1840
2155
|
for (let at = dir;; at = dirname2(at)) {
|
|
1841
|
-
const lockfile = LOCKFILES.find(([file]) => existsSync(
|
|
2156
|
+
const lockfile = LOCKFILES.find(([file]) => existsSync(join3(at, file)));
|
|
1842
2157
|
if (lockfile !== undefined)
|
|
1843
2158
|
return lockfile[1];
|
|
1844
2159
|
if (at === root || at === dirname2(at))
|
|
@@ -1882,8 +2197,9 @@ __export(exports_bump, {
|
|
|
1882
2197
|
ROOT_OPTION: () => ROOT_OPTION,
|
|
1883
2198
|
bump: () => bump
|
|
1884
2199
|
});
|
|
2200
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1885
2201
|
import { homedir as homedir2 } from "os";
|
|
1886
|
-
import { join as
|
|
2202
|
+
import { dirname as dirname3, join as join4, relative as relative2 } from "path";
|
|
1887
2203
|
async function servedVersion(name, version, settings) {
|
|
1888
2204
|
const registry = registryFor(name, settings.configs);
|
|
1889
2205
|
const host = new URL(registry.url).host;
|
|
@@ -1900,7 +2216,7 @@ async function servedVersion(name, version, settings) {
|
|
|
1900
2216
|
if (packument?.versions?.[version] !== undefined)
|
|
1901
2217
|
return version;
|
|
1902
2218
|
if (Date.now() - started >= settings.timeout)
|
|
1903
|
-
fail(`${host} does not serve ${name}@${version} after ${formatDuration(settings.timeout)}
|
|
2219
|
+
fail(`${host} does not serve ${name}@${version} after ${formatDuration(settings.timeout)}`);
|
|
1904
2220
|
region.redraw();
|
|
1905
2221
|
await Bun.sleep(settings.poll ?? POLL);
|
|
1906
2222
|
}
|
|
@@ -1908,11 +2224,33 @@ async function servedVersion(name, version, settings) {
|
|
|
1908
2224
|
region.stop();
|
|
1909
2225
|
}
|
|
1910
2226
|
}
|
|
2227
|
+
function lockfileOf(dir, root) {
|
|
2228
|
+
for (let at = dir;; at = dirname3(at)) {
|
|
2229
|
+
const found = ["bun.lock", "bun.lockb"].find((file) => existsSync2(join4(at, file)));
|
|
2230
|
+
if (found !== undefined)
|
|
2231
|
+
return relative2(dir, join4(at, found));
|
|
2232
|
+
if (at === root || at === dirname3(at))
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
async function checkAndCommit(name, version, where, root, run) {
|
|
2237
|
+
const checked = await run(["check"], where);
|
|
2238
|
+
if (checked.code !== 0)
|
|
2239
|
+
return `check exited ${checked.code}: ${checked.text.trim().split(`
|
|
2240
|
+
`).at(-1) ?? ""}`;
|
|
2241
|
+
const lockfile = lockfileOf(where, root);
|
|
2242
|
+
const committed = await run(["gc", `chore(deps): bump ${name} to ${version}`, "--", "package.json", ...lockfile === undefined ? [] : [lockfile]], where);
|
|
2243
|
+
return committed.code === 0 ? undefined : `gc exited ${committed.code}: ${committed.text.trim().split(`
|
|
2244
|
+
`).at(-1) ?? ""}`;
|
|
2245
|
+
}
|
|
1911
2246
|
async function bumpConsumers(name, version, settings) {
|
|
1912
2247
|
const found = findConsumers(name, settings.root);
|
|
1913
2248
|
const consumers = found.filter((each) => !isLocal(each));
|
|
1914
2249
|
const local = found.length - consumers.length;
|
|
1915
2250
|
const run = settings.run ?? (async (argv, cwd) => runCommand(argv, { cwd }));
|
|
2251
|
+
const committed = [];
|
|
2252
|
+
const failing = [];
|
|
2253
|
+
const skipped = [];
|
|
1916
2254
|
let isClean = true;
|
|
1917
2255
|
if (found.length === 0)
|
|
1918
2256
|
note(`nothing under ${settings.root} depends on ${name}`);
|
|
@@ -1920,42 +2258,60 @@ async function bumpConsumers(name, version, settings) {
|
|
|
1920
2258
|
note(`${local} workspace:, link: or file: dependenc${local === 1 ? "y" : "ies"}`, "left as they are");
|
|
1921
2259
|
for (const consumer of consumers) {
|
|
1922
2260
|
const argv = addCommand(consumer, name, version);
|
|
1923
|
-
const where =
|
|
2261
|
+
const where = join4(settings.root, consumer.dir);
|
|
1924
2262
|
if (consumer.spec === version) {
|
|
2263
|
+
skipped.push(consumer.dir);
|
|
1925
2264
|
note(consumer.dir, `already on ${version}`);
|
|
1926
2265
|
} else if (consumer.manager !== "bun") {
|
|
2266
|
+
skipped.push(consumer.dir);
|
|
1927
2267
|
warn(consumer.dir, `${consumer.spec}, its lockfile pins ${consumer.manager}: put ${version} in by hand`);
|
|
1928
2268
|
} else if (settings.isDryRun === true) {
|
|
1929
|
-
note(consumer.dir, `would run ${argv.join(" ")}`);
|
|
2269
|
+
note(consumer.dir, `would run ${[argv.join(" "), ...settings.isAll === true ? ["check", `gc 'chore(deps): bump ${name} to ${version}'`] : []].join(", then ")}`);
|
|
1930
2270
|
} else {
|
|
1931
2271
|
const ran = await run(argv, where);
|
|
1932
|
-
if (ran.code
|
|
1933
|
-
ok(consumer.dir, `${consumer.spec} -> ${version}`);
|
|
1934
|
-
} else {
|
|
2272
|
+
if (ran.code !== 0) {
|
|
1935
2273
|
isClean = false;
|
|
2274
|
+
failing.push(consumer.dir);
|
|
1936
2275
|
problem(consumer.dir, `${argv.join(" ")} exited ${ran.code}: ${ran.text.trim().split(`
|
|
1937
2276
|
`).at(-1) ?? ""}`);
|
|
2277
|
+
} else if (settings.isAll !== true) {
|
|
2278
|
+
ok(consumer.dir, `${consumer.spec} -> ${version}`);
|
|
2279
|
+
} else {
|
|
2280
|
+
const failure = await checkAndCommit(name, version, where, settings.root, run);
|
|
2281
|
+
if (failure === undefined) {
|
|
2282
|
+
committed.push(consumer.dir);
|
|
2283
|
+
ok(consumer.dir, `${consumer.spec} -> ${version}, checked and committed`);
|
|
2284
|
+
} else {
|
|
2285
|
+
isClean = false;
|
|
2286
|
+
failing.push(consumer.dir);
|
|
2287
|
+
problem(consumer.dir, `${consumer.spec} -> ${version}, left uncommitted: ${failure}`);
|
|
2288
|
+
}
|
|
1938
2289
|
}
|
|
1939
2290
|
}
|
|
1940
2291
|
}
|
|
2292
|
+
if (settings.isAll === true && settings.isDryRun !== true)
|
|
2293
|
+
note("summary", `${committed.length} bumped and committed, ${failing.length} bumped and failing${failing.length > 0 ? ` (${failing.join(", ")})` : ""}, ${skipped.length + local} skipped`);
|
|
1941
2294
|
return isClean;
|
|
1942
2295
|
}
|
|
1943
2296
|
async function bump(args) {
|
|
1944
|
-
const { values, positionals: [name, asked] } = parse(args,
|
|
2297
|
+
const { values, positionals: [name, asked] } = parse(args, usage3, options3, { positionals: ["<name>", "[version]"] });
|
|
1945
2298
|
const settings = {
|
|
1946
|
-
root: values.root ??
|
|
2299
|
+
root: values.root ?? join4(homedir2(), "Workspace"),
|
|
1947
2300
|
configs: await loadConfigs(),
|
|
1948
2301
|
timeout: duration(values.timeout, "timeout", 10 * 60000),
|
|
1949
|
-
isDryRun: values["dry-run"] === true
|
|
2302
|
+
isDryRun: values["dry-run"] === true,
|
|
2303
|
+
isAll: values["all-consumers"] === true
|
|
1950
2304
|
};
|
|
1951
2305
|
const version = await servedVersion(name, asked, settings);
|
|
1952
2306
|
return await bumpConsumers(name, version, settings) ? 0 : 1;
|
|
1953
2307
|
}
|
|
1954
|
-
var
|
|
2308
|
+
var usage3 = `pkg bump <name> [version]
|
|
1955
2309
|
|
|
1956
2310
|
Waits until the registry serves the version (the latest when none is given), then runs \`bun add --exact --no-cache\` in each folder under --root whose package.json depends on the package, the kind of dependency kept, a line per folder.
|
|
1957
2311
|
A workspace:, link: or file: dependency, a folder already on the version and one whose lockfile pins another manager are left as they are, each said so.
|
|
1958
|
-
|
|
2312
|
+
With --all-consumers each folder whose install went through then runs its own \`check\` (the check bin) and is committed with \`gc 'chore(deps): bump <name> to <version>' -- package.json <lockfile>\` only when that passes; a failing one is left uncommitted and listed in the summary with the skipped ones, and nothing is ever pushed.
|
|
2313
|
+
|
|
2314
|
+
Exits 0 when every install ran or was left as it is (with --all-consumers, and every check passed and every commit landed), 1 when an install, a check or a commit fails or the registry has no such package, 2 when the version never shows within the timeout or on a usage error.`, ROOT_OPTION, options3, POLL = 5000;
|
|
1959
2315
|
var init_bump = __esm(() => {
|
|
1960
2316
|
init_src();
|
|
1961
2317
|
init_progress();
|
|
@@ -1963,9 +2319,10 @@ var init_bump = __esm(() => {
|
|
|
1963
2319
|
init_consumers();
|
|
1964
2320
|
init_registry();
|
|
1965
2321
|
ROOT_OPTION = { type: "string", description: "the folder holding the repositories (~/Workspace)" };
|
|
1966
|
-
|
|
2322
|
+
options3 = {
|
|
1967
2323
|
root: ROOT_OPTION,
|
|
1968
2324
|
timeout: { type: "string", short: "t", description: "how long to wait for the registry (10m): 30s, 5m, 1h" },
|
|
2325
|
+
"all-consumers": { type: "boolean", description: "after each install run the folder's check and commit it through gc when it passes" },
|
|
1969
2326
|
"dry-run": { type: "boolean", description: "print what would run, changing nothing" }
|
|
1970
2327
|
};
|
|
1971
2328
|
});
|
|
@@ -1976,46 +2333,53 @@ __export(exports_consumers, {
|
|
|
1976
2333
|
consumers: () => consumers
|
|
1977
2334
|
});
|
|
1978
2335
|
import { homedir as homedir3 } from "os";
|
|
1979
|
-
import { join as
|
|
2336
|
+
import { join as join5 } from "path";
|
|
1980
2337
|
function consumers(args) {
|
|
1981
|
-
const { values, positionals: [name] } = parse(args,
|
|
1982
|
-
const root = values.root ??
|
|
1983
|
-
const
|
|
2338
|
+
const { values, positionals: [name] } = parse(args, usage4, options4, { positionals: ["<name>"] });
|
|
2339
|
+
const root = values.root ?? join5(homedir3(), "Workspace");
|
|
2340
|
+
const { shown, footer } = capped(findConsumers(name, root), limitOf(values, CONSUMERS_LIMIT));
|
|
1984
2341
|
if (values.json === true)
|
|
1985
|
-
printJson(
|
|
1986
|
-
else if (
|
|
2342
|
+
printJson(shown);
|
|
2343
|
+
else if (shown.length === 0)
|
|
1987
2344
|
console.log(`nothing under ${root} depends on ${name}`);
|
|
1988
2345
|
else
|
|
1989
|
-
console.log(table([["FOLDER", "KIND", "VERSION", "MANAGER"], ...
|
|
2346
|
+
console.log([table([["FOLDER", "KIND", "VERSION", "MANAGER"], ...shown.map((each) => [each.dir, each.kind, each.spec, each.manager])], { header: true }), ...footer === undefined ? [] : [footer]].join(`
|
|
2347
|
+
`));
|
|
1990
2348
|
return 0;
|
|
1991
2349
|
}
|
|
1992
|
-
var
|
|
2350
|
+
var usage4 = `pkg consumers <name>
|
|
2351
|
+
|
|
2352
|
+
Lists the folders under --root whose package.json depends on the package: the folder, the kind of dependency, the version it asks for and the manager its lockfile pins, 30 at most with the count of the rest after them (--limit <n>, --full for all; --json is never cut unless --limit is given).
|
|
1993
2353
|
|
|
1994
|
-
|
|
2354
|
+
Exits 0 when it listed them, 2 on a usage error.`, CONSUMERS_LIMIT = 30, options4;
|
|
1995
2355
|
var init_consumers2 = __esm(() => {
|
|
1996
2356
|
init_src();
|
|
1997
2357
|
init_consumers();
|
|
1998
2358
|
init_bump();
|
|
1999
|
-
|
|
2359
|
+
options4 = {
|
|
2000
2360
|
root: ROOT_OPTION,
|
|
2361
|
+
...limitOptions(CONSUMERS_LIMIT),
|
|
2001
2362
|
json: jsonOption("[{dir, path, kind, spec, manager}]")
|
|
2002
2363
|
};
|
|
2003
2364
|
});
|
|
2004
2365
|
|
|
2005
2366
|
// src/cli.ts
|
|
2006
2367
|
init_src();
|
|
2007
|
-
import
|
|
2368
|
+
import process10 from "process";
|
|
2008
2369
|
// package.json
|
|
2009
|
-
var version = "0.
|
|
2370
|
+
var version = "0.4.0";
|
|
2010
2371
|
|
|
2011
2372
|
// src/cli.ts
|
|
2012
|
-
|
|
2373
|
+
process10.exitCode = await runCli({
|
|
2013
2374
|
name: "pkg",
|
|
2014
2375
|
version,
|
|
2015
2376
|
about: "checks npm packages before they are added, and moves a lib's consumers onto a new release",
|
|
2377
|
+
replaces: "bun add on trust, and a bun info and sleep loop then a bun add per consumer",
|
|
2378
|
+
examples: [["pkg zod", "the name, the newest version two weeks old, its licence and scripts"], ["pkg bump @kaynooo/cli-kit --dry-run", "the consumers a release would move"]],
|
|
2016
2379
|
defaultCommand: "check",
|
|
2017
2380
|
commands: {
|
|
2018
2381
|
check: { summary: "check packages before they are added (`pkg <name>` alone runs it)", run: async (args) => (await Promise.resolve().then(() => (init_check(), exports_check))).check(args) },
|
|
2382
|
+
audit: { summary: "check every package a bun.lock pins, with its known advisories", run: async (args) => (await Promise.resolve().then(() => (init_audit(), exports_audit))).audit(args) },
|
|
2019
2383
|
bump: { summary: "wait for a release on the registry, then bun add it in every consumer", run: async (args) => (await Promise.resolve().then(() => (init_bump(), exports_bump))).bump(args) },
|
|
2020
2384
|
consumers: { summary: "list the folders depending on a package, with the version each asks for", run: async (args) => (await Promise.resolve().then(() => (init_consumers2(), exports_consumers))).consumers(args) }
|
|
2021
2385
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/pkg-tools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "pkg: check an npm package before it is added, for a guessed name, a version under two weeks old and install scripts.",
|
|
6
|
-
"author": "Kaynooo <kaynooo
|
|
6
|
+
"author": "Kaynooo <contact@kaynooo.fr>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://git.
|
|
8
|
+
"homepage": "https://git.kaynooo.fr/kaynooo/agent-tools/src/branch/main/packages/pkg#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://git.
|
|
11
|
+
"url": "git+https://git.kaynooo.fr/kaynooo/agent-tools.git",
|
|
12
12
|
"directory": "packages/pkg"
|
|
13
13
|
},
|
|
14
|
-
"bugs": "https://git.
|
|
14
|
+
"bugs": "https://git.kaynooo.fr/kaynooo/agent-tools/issues",
|
|
15
15
|
"keywords": ["npm", "package", "typosquat", "supply-chain", "dependencies"],
|
|
16
16
|
"bin": {
|
|
17
17
|
"pkg": "./dist/cli.js"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"bun": ">=1.4.3"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
|
-
"registry": "https://git.
|
|
24
|
+
"registry": "https://git.kaynooo.fr/api/packages/kaynooo/npm/"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "$npm_execpath build src/cli.ts --target bun --outdir dist",
|