@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.
Files changed (4) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +27 -2
  3. package/dist/cli.js +553 -189
  4. package/package.json +6 -6
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Kaynooo <kaynooo@kevyn.fr>
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.examples));
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 reportThrown(prefix, error, redact = (text) => text) {
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
- printError(prefix, redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
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
- return analyze({ spec, packument, downloads, registry: new URL(registry.url).host, now: context.now, minAgeDays: context.minAgeDays });
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
- Exits 1 when a package is missing or the version asked for is under the minimum age.`, options;
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
- // ../kit/src/progress.ts
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 liveRegion(render, stream = process8.stderr, env = process8.env) {
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
- process8.off("exit", onExit);
2015
+ process9.off("exit", onExit);
1701
2016
  signals.release();
1702
2017
  }
1703
2018
  timer.unref();
1704
- process8.on("exit", onExit);
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]), [process8.stdout, "write"], [process8.stderr, "write"]];
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 join2, relative } from "path";
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(join2(dir, entry.name));
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(join2(dir, entry.name), depth + 1, found);
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(join2(at, file)));
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 join3 } from "path";
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)}`, 1);
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 = join3(settings.root, consumer.dir);
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 === 0) {
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, usage2, options2, { positionals: ["<name>", "[version]"] });
2297
+ const { values, positionals: [name, asked] } = parse(args, usage3, options3, { positionals: ["<name>", "[version]"] });
1945
2298
  const settings = {
1946
- root: values.root ?? join3(homedir2(), "Workspace"),
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 usage2 = `pkg bump <name> [version]
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
- Exits 1 when an install fails or the version never shows, 2 on a usage error.`, ROOT_OPTION, options2, POLL = 5000;
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
- options2 = {
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 join4 } from "path";
2336
+ import { join as join5 } from "path";
1980
2337
  function consumers(args) {
1981
- const { values, positionals: [name] } = parse(args, usage3, options3, { positionals: ["<name>"] });
1982
- const root = values.root ?? join4(homedir3(), "Workspace");
1983
- const found = findConsumers(name, root);
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(found);
1986
- else if (found.length === 0)
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"], ...found.map((each) => [each.dir, each.kind, each.spec, each.manager])], { header: true }));
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 usage3 = `pkg consumers <name>
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
- 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.`, options3;
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
- options3 = {
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 process9 from "process";
2368
+ import process10 from "process";
2008
2369
  // package.json
2009
- var version = "0.3.0";
2370
+ var version = "0.4.0";
2010
2371
 
2011
2372
  // src/cli.ts
2012
- process9.exitCode = await runCli({
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.3.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@kevyn.fr>",
6
+ "author": "Kaynooo <contact@kaynooo.fr>",
7
7
  "license": "MIT",
8
- "homepage": "https://git.kevyn.fr/kaynooo/agent-tools/src/branch/main/packages/pkg#readme",
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.kevyn.fr/kaynooo/agent-tools.git",
11
+ "url": "git+https://git.kaynooo.fr/kaynooo/agent-tools.git",
12
12
  "directory": "packages/pkg"
13
13
  },
14
- "bugs": "https://git.kevyn.fr/kaynooo/agent-tools/issues",
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.kevyn.fr/api/packages/kaynooo/npm/"
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",