@kaynooo/pkg-tools 0.3.0 → 0.5.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 +30 -4
  3. package/dist/cli.js +1130 -406
  4. package/package.json +6 -6
package/dist/cli.js CHANGED
@@ -418,6 +418,29 @@ var init_format = __esm(() => {
418
418
  ESCAPE = String.fromCharCode(27);
419
419
  });
420
420
 
421
+ // ../kit/src/text.ts
422
+ function distance(a, b) {
423
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
424
+ for (const [i, left] of [...a].entries()) {
425
+ const current = [i + 1];
426
+ for (const [j, right] of [...b].entries())
427
+ current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
428
+ previous = current;
429
+ }
430
+ return previous[b.length] ?? 0;
431
+ }
432
+ function closest(name, names) {
433
+ const started = names.find((known) => known.startsWith(name));
434
+ if (started !== undefined)
435
+ return started;
436
+ const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
437
+ return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
438
+ }
439
+ var ANSI;
440
+ var init_text = __esm(() => {
441
+ ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
442
+ });
443
+
421
444
  // ../kit/src/args.ts
422
445
  import process3 from "process";
423
446
  import { parseArgs } from "util";
@@ -440,9 +463,29 @@ class Completing {
440
463
  function usageLine(usage, colors = paint(process3.stdout)) {
441
464
  return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
442
465
  }
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(`
466
+ function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
467
+ return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
468
+ `);
469
+ }
470
+ function replacesRows(replaces, colors = paint(process3.stdout)) {
471
+ return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
472
+ }
473
+ function wantsHelpJson(env = process3.env) {
474
+ return env.KIT_HELP === "json";
475
+ }
476
+ function description(usage, { examples = [], replaces } = {}) {
477
+ const lines = usage.replace(USAGE_PREFIX, "").split(`
445
478
  `);
479
+ const first = lines[0];
480
+ const colon = first.indexOf(": ");
481
+ const prose = lines[lines.indexOf("") + 1] ?? "";
482
+ const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
483
+ return {
484
+ synopsis: colon === -1 ? first : first.slice(0, colon),
485
+ summary: colon === -1 ? fallback : first.slice(colon + 2),
486
+ ...replaces === undefined ? {} : { replaces },
487
+ examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
488
+ };
446
489
  }
447
490
  function exampleRows(examples, colors = paint(process3.stdout)) {
448
491
  if (examples.length === 0)
@@ -453,10 +496,10 @@ function optionRows(options, colors = paint(process3.stdout)) {
453
496
  const rows = Object.entries(options).map(([name, option]) => {
454
497
  const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
455
498
  const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
456
- return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description };
499
+ return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
457
500
  });
458
501
  const width = Math.max(0, ...rows.map((row) => row.width));
459
- return rows.map((row) => ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
502
+ return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
460
503
  }
461
504
  function applyOutput(color, plain, env = process3.env) {
462
505
  const when = plain === true ? "never" : color ?? "auto";
@@ -482,14 +525,15 @@ function parse(args, usage, options, settings = {}) {
482
525
  const rest = separator === -1 ? undefined : args.slice(separator + 1);
483
526
  let parsed;
484
527
  try {
485
- parsed = parseArgs({ args: own, options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
528
+ parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
486
529
  } catch (error) {
487
- badUsage(parseError(error));
530
+ const message = parseError(error);
531
+ throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
488
532
  }
489
533
  const values = parsed.values;
490
534
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
491
535
  if (values.help === true) {
492
- console.log(helpText(usage, all, undefined, settings.examples));
536
+ console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, all, undefined, settings));
493
537
  throw new HelpShown;
494
538
  }
495
539
  if (settings.positionals !== undefined)
@@ -504,16 +548,50 @@ function checkPositionals(given, names) {
504
548
  if (!variadic && given.length > names.length)
505
549
  badUsage(`unexpected argument ${given[names.length]}`);
506
550
  }
551
+ function joinDashValues(args, options) {
552
+ const longOf = new Map(Object.entries(options).flatMap(([name, option]) => option.short === undefined ? [[`--${name}`, name]] : [[`--${name}`, name], [`-${option.short}`, name]]));
553
+ const isOption = (arg) => arg === "--" || longOf.has(arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg);
554
+ const joined = [];
555
+ for (let i = 0;i < args.length; i++) {
556
+ const arg = args[i] ?? "";
557
+ if (arg === "--")
558
+ return [...joined, ...args.slice(i)];
559
+ const name = longOf.get(arg);
560
+ const next = args[i + 1];
561
+ if (name !== undefined && options[name]?.type === "string" && next !== undefined && next.startsWith("-") && next !== "-" && !isOption(next)) {
562
+ joined.push(`--${name}=${next}`);
563
+ i++;
564
+ } else {
565
+ joined.push(arg);
566
+ }
567
+ }
568
+ return joined;
569
+ }
507
570
  function parseError(error) {
508
571
  const message = upTo(error instanceof Error ? error.message : String(error), ". ");
509
572
  return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
510
573
  }
574
+ function nearestOption(message, options, allowNegative) {
575
+ const typed = UNKNOWN_OPTION.exec(message)?.[1];
576
+ if (typed === undefined)
577
+ return;
578
+ const word = typed.replace(DASHES, "");
579
+ if (word.length < 2 && !typed.startsWith("--"))
580
+ return;
581
+ const known = Object.entries(options).flatMap(([name, option]) => [
582
+ ...option.short === undefined ? [] : [`-${option.short}`],
583
+ `--${name}`,
584
+ ...allowNegative && option.type === "boolean" ? [`--no-${name}`] : []
585
+ ]);
586
+ const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
587
+ return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
588
+ }
511
589
  function wholeNumber(value, name, fallback, min = 1) {
512
590
  if (value === undefined)
513
591
  return fallback;
514
592
  const number = Number(value);
515
- if (!Number.isInteger(number) || number < min)
516
- badUsage(`--${name} takes a whole number from ${min}, not ${value}`);
593
+ if (!WHOLE.test(value) || !Number.isSafeInteger(number) || number < min)
594
+ badUsage(`${name.startsWith("<") ? name : `--${name}`} takes a whole number from ${min}, not ${value}`);
517
595
  return number;
518
596
  }
519
597
  function duration(value, name, fallback, unit = "s") {
@@ -525,10 +603,11 @@ function duration(value, name, fallback, unit = "s") {
525
603
  badUsage(`--${name} takes a duration such as 30s, 5m or 1h, not ${value}`);
526
604
  return ms;
527
605
  }
528
- var Exit, UsageError, HelpShown, completion, OPTIONAL, USAGE_PREFIX, OUTPUT_OPTIONS, DURATION, UNIT_MS;
606
+ var Exit, UsageError, HelpShown, completion, OPTIONAL, USAGE_PREFIX, FULL_STOP, DASHES, OUTPUT_OPTIONS, UNKNOWN_OPTION, WHOLE, DURATION, UNIT_MS;
529
607
  var init_args = __esm(() => {
530
608
  init_format();
531
609
  init_style();
610
+ init_text();
532
611
  Exit = class Exit extends Error {
533
612
  code;
534
613
  hint;
@@ -539,8 +618,8 @@ var init_args = __esm(() => {
539
618
  }
540
619
  };
541
620
  UsageError = class UsageError extends Exit {
542
- constructor(message) {
543
- super(message, 2);
621
+ constructor(message, hint) {
622
+ super(message, 2, hint);
544
623
  }
545
624
  };
546
625
  HelpShown = class HelpShown extends Exit {
@@ -551,10 +630,14 @@ var init_args = __esm(() => {
551
630
  completion = { active: false };
552
631
  OPTIONAL = /\[[^\]]+\]/g;
553
632
  USAGE_PREFIX = /^usage:\s*/i;
633
+ FULL_STOP = /\.$/;
634
+ DASHES = /^-+/;
554
635
  OUTPUT_OPTIONS = {
555
636
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
556
637
  plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
557
638
  };
639
+ UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
640
+ WHOLE = /^\d+$/;
558
641
  DURATION = /^(\d+(?:\.\d+)?)(ms|[smh])?$/;
559
642
  UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000 };
560
643
  });
@@ -565,31 +648,265 @@ var init_chart = __esm(() => {
565
648
  init_style();
566
649
  });
567
650
 
568
- // ../kit/src/text.ts
569
- function distance(a, b) {
570
- let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
571
- for (const [i, left] of [...a].entries()) {
572
- const current = [i + 1];
573
- for (const [j, right] of [...b].entries())
574
- current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
575
- previous = current;
651
+ // ../kit/src/process.ts
652
+ import { constants } from "os";
653
+ import process4 from "process";
654
+ function exitCodeOf(code, signal) {
655
+ if (code !== null)
656
+ return code;
657
+ if (typeof signal === "number")
658
+ return 128 + signal;
659
+ return signal === null ? 129 : 128 + constants.signals[signal];
660
+ }
661
+ function holdSignals(cleanup) {
662
+ let received;
663
+ let child;
664
+ const held = {
665
+ get signal() {
666
+ return received;
667
+ },
668
+ forward: (to) => {
669
+ child = to;
670
+ },
671
+ release: () => {
672
+ for (const signal of SIGNALS)
673
+ process4.off(signal, onSignal);
674
+ }
675
+ };
676
+ function onSignal(signal) {
677
+ received = signal;
678
+ if (child !== undefined) {
679
+ child.kill(signal);
680
+ return;
681
+ }
682
+ if (cleanup === undefined)
683
+ return;
684
+ held.release();
685
+ cleanup(signal);
686
+ process4.kill(process4.pid, signal);
576
687
  }
577
- return previous[b.length] ?? 0;
688
+ for (const signal of SIGNALS)
689
+ process4.on(signal, onSignal);
690
+ return held;
578
691
  }
579
- var ANSI;
580
- var init_text = __esm(() => {
581
- ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
692
+ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal, maxBytes = Infinity, onChunk, onStart } = {}) {
693
+ const chunks = [];
694
+ const grouped = timeoutMs !== undefined || signal !== undefined || onStart !== undefined || onChunk !== undefined;
695
+ const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe", ...grouped ? { detached: true } : {} });
696
+ const state = { kept: 0, truncated: false, timedOut: false, failure: undefined, done: false };
697
+ const timers = [];
698
+ const killGroup = (killing) => {
699
+ if (state.done)
700
+ return;
701
+ try {
702
+ if (grouped)
703
+ process4.kill(-proc.pid, killing);
704
+ else
705
+ proc.kill(killing);
706
+ } catch {}
707
+ };
708
+ const stop = () => {
709
+ killGroup("SIGTERM");
710
+ timers.push(setTimeout(killGroup, KILL_GRACE_MS, "SIGKILL"));
711
+ };
712
+ const onAbort = () => stop();
713
+ if (timeoutMs !== undefined) {
714
+ timers.push(setTimeout(() => {
715
+ state.timedOut = true;
716
+ stop();
717
+ }, timeoutMs));
718
+ }
719
+ if (signal?.aborted === true)
720
+ onAbort();
721
+ else
722
+ signal?.addEventListener("abort", onAbort, { once: true });
723
+ onStart?.({ kill: (killing) => killGroup(killing) });
724
+ const drain = async (stream, name) => {
725
+ for await (const data of stream) {
726
+ if (state.failure !== undefined)
727
+ continue;
728
+ try {
729
+ onChunk?.({ stream: name, data });
730
+ } catch (error) {
731
+ state.failure = error;
732
+ stop();
733
+ continue;
734
+ }
735
+ if (state.kept + data.length > maxBytes) {
736
+ state.truncated = true;
737
+ continue;
738
+ }
739
+ state.kept += data.length;
740
+ chunks.push({ stream: name, data });
741
+ }
742
+ };
743
+ try {
744
+ await Promise.all([drain(proc.stdout, "stdout"), drain(proc.stderr, "stderr")]);
745
+ await proc.exited;
746
+ } finally {
747
+ state.done = true;
748
+ for (const timer of timers)
749
+ clearTimeout(timer);
750
+ signal?.removeEventListener("abort", onAbort);
751
+ }
752
+ if (state.failure !== undefined)
753
+ throw state.failure;
754
+ const joined = () => Buffer.concat(chunks.map(({ data }) => data));
755
+ let output;
756
+ let text;
757
+ return {
758
+ code: exitCodeOf(proc.exitCode, proc.signalCode),
759
+ chunks,
760
+ timedOut: state.timedOut,
761
+ truncated: state.truncated,
762
+ get output() {
763
+ return output ??= joined();
764
+ },
765
+ get text() {
766
+ return text ??= (output ??= joined()).toString();
767
+ }
768
+ };
769
+ }
770
+ var KILL_GRACE_MS = 2000, SIGNALS;
771
+ var init_process = __esm(() => {
772
+ SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
773
+ });
774
+
775
+ // ../kit/src/progress.ts
776
+ import process5 from "process";
777
+ function liveRegion(render, stream = process5.stderr, env = process5.env) {
778
+ if (!takesControlCodes(stream, env))
779
+ return { redraw: () => {
780
+ return;
781
+ }, print: (text) => void stream.write(`${text}
782
+ `), stop: () => {
783
+ return;
784
+ } };
785
+ const write = stream.write.bind(stream);
786
+ let frame = 0;
787
+ let drawn = 0;
788
+ const clear = () => {
789
+ if (drawn > 0)
790
+ write(`${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}`);
791
+ drawn = 0;
792
+ };
793
+ const draw = () => {
794
+ const width = (stream.columns === undefined || stream.columns === 0 ? 80 : stream.columns) - 1;
795
+ const height = stream.rows === undefined || stream.rows === 0 ? 24 : stream.rows;
796
+ const lines = render(FRAMES[frame++ % FRAMES.length]).slice(0, Math.max(1, height - 1)).map((line) => cutToWidth(line, width));
797
+ if (lines.length === 0) {
798
+ clear();
799
+ return;
800
+ }
801
+ write(`${drawn > 0 ? `${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}` : CLEAR_LINE}${lines.join(`
802
+ `)}`);
803
+ drawn = lines.length;
804
+ };
805
+ const timer = setInterval(draw, FRAME_MS);
806
+ const restore = interceptOutput(clear);
807
+ const onExit = () => clear();
808
+ const signals = holdSignals(stop);
809
+ function stop() {
810
+ clearInterval(timer);
811
+ clear();
812
+ restore();
813
+ process5.off("exit", onExit);
814
+ signals.release();
815
+ }
816
+ timer.unref();
817
+ process5.on("exit", onExit);
818
+ return {
819
+ redraw: draw,
820
+ print: (text) => {
821
+ clear();
822
+ write(`${text}
823
+ `);
824
+ draw();
825
+ },
826
+ stop
827
+ };
828
+ }
829
+ function cutToWidth(line, width) {
830
+ if (Bun.stringWidth(line) <= width)
831
+ return line;
832
+ let room = width;
833
+ let kept = "";
834
+ let index = 0;
835
+ let linked = false;
836
+ while (index < line.length) {
837
+ ESCAPE_CODE.lastIndex = index;
838
+ const escape = ESCAPE_CODE.exec(line);
839
+ if (escape !== null) {
840
+ kept += escape[0];
841
+ linked ||= escape[0].startsWith(`${ESC}]`);
842
+ index = ESCAPE_CODE.lastIndex;
843
+ continue;
844
+ }
845
+ const char = String.fromCodePoint(line.codePointAt(index));
846
+ const columns = Bun.stringWidth(char);
847
+ if (columns > room)
848
+ break;
849
+ kept += char;
850
+ room -= columns;
851
+ index += char.length;
852
+ }
853
+ return `${kept}${RESET_STYLE}${linked ? CLOSE_LINK : ""}`;
854
+ }
855
+ function interceptOutput(clear) {
856
+ const targets = [...CONSOLE_METHODS.map((name) => [console, name]), [process5.stdout, "write"], [process5.stderr, "write"]];
857
+ const wrapped = targets.map(([target, name]) => {
858
+ const original = Reflect.get(target, name);
859
+ const wrapper = (...args) => {
860
+ clear();
861
+ return original.apply(target, args);
862
+ };
863
+ Reflect.set(target, name, wrapper);
864
+ return { target, name, original, wrapper };
865
+ });
866
+ return () => {
867
+ for (const { target, name, original, wrapper } of wrapped) {
868
+ if (Reflect.get(target, name) === wrapper)
869
+ Reflect.set(target, name, original);
870
+ }
871
+ };
872
+ }
873
+ var FRAMES, FRAME_MS = 80, CLEAR_LINE = "\r\x1B[2K", LINE_UP = "\x1B[1A\x1B[2K", ESC, ESCAPE_CODE, RESET_STYLE, CLOSE_LINK, CONSOLE_METHODS;
874
+ var init_progress = __esm(() => {
875
+ init_format();
876
+ init_process();
877
+ init_style();
878
+ FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
879
+ ESC = String.fromCharCode(27);
880
+ ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, "y");
881
+ RESET_STYLE = `${ESC}[0m`;
882
+ CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
883
+ CONSOLE_METHODS = ["log", "info", "warn", "error", "debug", "dir", "dirxml", "table", "trace", "group", "groupCollapsed", "count", "timeEnd", "timeLog", "assert"];
884
+ });
885
+
886
+ // ../kit/src/tool.ts
887
+ var init_tool = __esm(() => {
888
+ init_format();
889
+ init_process();
890
+ });
891
+
892
+ // ../kit/src/ci.ts
893
+ var init_ci = __esm(() => {
894
+ init_args();
895
+ init_format();
896
+ init_progress();
897
+ init_tool();
582
898
  });
583
899
 
584
900
  // ../kit/src/cli.ts
585
- import process4 from "process";
586
- function commandRows(commands, colors) {
901
+ import process6 from "process";
902
+ async function commandRows(commands, colors) {
587
903
  const width = Math.max(...Object.keys(commands).map((name) => name.length));
588
904
  const groups = new Map([["Commands:", []]]);
589
- for (const [name, { summary, group }] of Object.entries(commands)) {
905
+ const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
906
+ for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
590
907
  const heading = group === undefined ? "Commands:" : `${group}:`;
591
908
  const rows = groups.get(heading) ?? [];
592
- rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summary)}`);
909
+ rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
593
910
  groups.set(heading, rows);
594
911
  }
595
912
  return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
@@ -609,25 +926,38 @@ function takeOutputFlags(argv) {
609
926
  }
610
927
  return argv.slice(index);
611
928
  }
612
- function closest(name, names) {
613
- const started = names.find((known) => known.startsWith(name));
614
- if (started !== undefined)
615
- return started;
616
- const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
617
- return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
929
+ async function resolveSummary(summary) {
930
+ return typeof summary === "string" ? summary : summary();
931
+ }
932
+ function scopeOf(commands, extra, aliases) {
933
+ return { commands: typeof commands === "function" ? {} : commands, lazy: [...typeof commands === "function" ? [commands] : [], ...extra === undefined ? [] : [extra]], aliases };
618
934
  }
619
- async function help({ path, header, commands, defaultCommand, examples }, stream) {
935
+ async function expand(scope, words) {
936
+ if (scope.lazy.length === 0)
937
+ return scope;
938
+ const commands = { ...scope.commands };
939
+ for (const added of await Promise.all(scope.lazy.map(async (lazy) => lazy(words)))) {
940
+ for (const [name, command] of Object.entries(added))
941
+ commands[name] ??= command;
942
+ }
943
+ return { ...scope, commands, lazy: [] };
944
+ }
945
+ async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
620
946
  const colors = paint(stream);
621
947
  const prefix = path.join(" ");
948
+ const summary = await resolveSummary(lazySummary);
949
+ if (wantsHelpJson())
950
+ return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
622
951
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
623
952
  const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
624
953
  const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
625
954
  return [
626
- header(colors),
955
+ header(colors, summary),
627
956
  "",
628
957
  usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
958
+ ...replacesRows(replaces, colors),
629
959
  "",
630
- ...commandRows(commands, colors),
960
+ ...await commandRows(commands, colors),
631
961
  ...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
632
962
  "",
633
963
  colors.bold("Options:"),
@@ -639,20 +969,28 @@ async function help({ path, header, commands, defaultCommand, examples }, stream
639
969
  `);
640
970
  }
641
971
  function printError(prefix, message, hint, cause) {
642
- const colors = paint(process4.stderr);
643
- const styled = takesControlCodes(process4.stderr);
972
+ const colors = paint(process6.stderr);
973
+ const styled = takesControlCodes(process6.stderr);
644
974
  const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
645
975
  const under = (label, text, tint) => text === undefined ? "" : `
646
976
  ${styled ? tint(label) : `${label}:`} ${text}`;
647
977
  console.error(`${head} ${message}${under("cause", cause, colors.dim)}${under("hint", hint, colors.yellow)}`);
648
978
  }
649
- function reportThrown(prefix, error, redact = (text) => text) {
979
+ function printJsonError(message, hint, cause) {
980
+ console.error(JSON.stringify({ error: message, ...hint === undefined ? {} : { hint }, ...cause === undefined ? {} : { cause } }));
981
+ }
982
+ function wantsJson(argv) {
983
+ const end = argv.indexOf("--");
984
+ return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
985
+ }
986
+ function reportThrown(prefix, error, redact = (text) => text, json = false) {
650
987
  if (error instanceof HelpShown)
651
988
  return 0;
652
989
  const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
653
990
  const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
654
991
  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);
992
+ const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
993
+ report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
656
994
  return error instanceof Exit ? error.code : 1;
657
995
  }
658
996
  async function dispatch(level, typed, { version, redact }) {
@@ -665,11 +1003,11 @@ async function dispatch(level, typed, { version, redact }) {
665
1003
  }
666
1004
  const [name, ...args] = argv;
667
1005
  if (name === undefined && level.path.length > 1) {
668
- console.error(await help(level, process4.stderr));
1006
+ console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process6.stderr));
669
1007
  return 2;
670
1008
  }
671
1009
  if (name === undefined || name === "help" || name === "--help" || name === "-h") {
672
- console.log(await help(level, process4.stdout));
1010
+ console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process6.stdout));
673
1011
  return 0;
674
1012
  }
675
1013
  if (level.path.length === 1 && (name === "--version" || name === "-v")) {
@@ -678,6 +1016,10 @@ async function dispatch(level, typed, { version, redact }) {
678
1016
  }
679
1017
  let command = level.commands[level.aliases?.[name] ?? name];
680
1018
  let rest = args;
1019
+ if (command === undefined && level.lazy.length > 0) {
1020
+ level = await expand(level, [...level.path.slice(1), ...argv]);
1021
+ command = level.commands[level.aliases?.[name] ?? name];
1022
+ }
681
1023
  if (command === undefined && level.defaultCommand !== undefined) {
682
1024
  command = level.commands[level.defaultCommand];
683
1025
  rest = argv;
@@ -689,12 +1031,12 @@ async function dispatch(level, typed, { version, redact }) {
689
1031
  }
690
1032
  if ("commands" in command) {
691
1033
  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 });
1034
+ return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
693
1035
  }
694
1036
  try {
695
1037
  return await command.run(rest);
696
1038
  } catch (error) {
697
- return reportThrown(`${prefix} ${name}`, error, redact);
1039
+ return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
698
1040
  }
699
1041
  }
700
1042
  async function optionsOf(run) {
@@ -712,28 +1054,34 @@ async function optionsOf(run) {
712
1054
  async function leafCandidates(run, words) {
713
1055
  if (!(words.at(-1) ?? "").startsWith("-"))
714
1056
  return [FILES];
715
- return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
1057
+ return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
716
1058
  }
717
- async function candidates(commands, aliases, defaultCommand, words) {
1059
+ async function candidates(scope, defaultCommand, words, before = []) {
718
1060
  const [word = "", ...rest] = words;
719
- const command = commands[aliases?.[word] ?? word];
720
- if (rest.length === 0 && !word.startsWith("-"))
721
- return Object.entries(commands).map(([name, { summary }]) => `${name} ${summary}`);
1061
+ const listing = rest.length === 0 && !word.startsWith("-");
1062
+ let { commands, aliases } = scope;
1063
+ let command = commands[aliases?.[word] ?? word];
1064
+ if (listing || command === undefined) {
1065
+ ({ commands, aliases } = await expand(scope, [...before, ...words]));
1066
+ command = commands[aliases?.[word] ?? word];
1067
+ }
1068
+ if (listing)
1069
+ return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
722
1070
  if (command === undefined) {
723
1071
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
724
1072
  return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
725
1073
  }
726
- return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
1074
+ return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
727
1075
  }
728
1076
  async function printCandidates(found) {
729
1077
  for (const line of await found)
730
1078
  console.log(line);
731
1079
  return 0;
732
1080
  }
733
- async function runCli(cli, argv = process4.argv.slice(2)) {
1081
+ async function runCli(cli, argv = process6.argv.slice(2)) {
734
1082
  if (argv[0] === COMPLETE)
735
- 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 });
1083
+ return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
1084
+ return dispatch({ path: [cli.name], header: (colors, about) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${about}`, summary: cli.about, ...scopeOf(cli.commands, cli.extraCommands, cli.aliases), defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
737
1085
  }
738
1086
  var LEVEL_OPTIONS, TOP_OPTIONS, COMPLETE = "__complete", FILES = "__files__";
739
1087
  var init_cli = __esm(() => {
@@ -752,74 +1100,388 @@ var init_config = __esm(() => {
752
1100
  init_args();
753
1101
  });
754
1102
 
755
- // ../kit/src/process.ts
756
- import { constants } from "os";
757
- import process5 from "process";
758
- async function collect(stream, name, chunks) {
759
- for await (const data of stream)
760
- chunks.push({ stream: name, data });
1103
+ // ../kit/src/git.ts
1104
+ var init_git = __esm(() => {
1105
+ init_args();
1106
+ init_tool();
1107
+ });
1108
+
1109
+ // ../kit/src/limit.ts
1110
+ function limitOptions(fallback) {
1111
+ return {
1112
+ limit: { type: "string", description: `show at most this many rows, the rest counted (${fallback})` },
1113
+ full: { type: "boolean", description: "show every row" }
1114
+ };
761
1115
  }
762
- function exitCodeOf(code, signal) {
763
- if (code !== null)
764
- return code;
765
- if (typeof signal === "number")
766
- return 128 + signal;
767
- return signal === null ? 129 : 128 + constants.signals[signal];
1116
+ function limitOf(values, fallback) {
1117
+ if (values.full === true && values.limit !== undefined)
1118
+ badUsage("--limit and --full exclude each other");
1119
+ if (values.full === true || values.json === true && values.limit === undefined)
1120
+ return Number.POSITIVE_INFINITY;
1121
+ return wholeNumber(values.limit, "limit", fallback);
768
1122
  }
769
- function holdSignals(cleanup) {
770
- let received;
771
- let child;
772
- const held = {
773
- get signal() {
774
- return received;
775
- },
776
- forward: (to) => {
777
- child = to;
778
- },
779
- release: () => {
780
- for (const signal of SIGNALS)
781
- process5.off(signal, onSignal);
782
- }
783
- };
784
- function onSignal(signal) {
785
- received = signal;
786
- if (child !== undefined) {
787
- child.kill(signal);
788
- return;
789
- }
790
- if (cleanup === undefined)
791
- return;
792
- held.release();
793
- cleanup(signal);
794
- process5.kill(process5.pid, signal);
795
- }
796
- for (const signal of SIGNALS)
797
- process5.on(signal, onSignal);
798
- return held;
1123
+ function moreFooter(more) {
1124
+ return `${more} more, rerun with --full`;
799
1125
  }
800
- async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
801
- const chunks = [];
802
- const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe" });
803
- await Promise.all([collect(proc.stdout, "stdout", chunks), collect(proc.stderr, "stderr", chunks)]);
804
- await proc.exited;
805
- const output = Buffer.concat(chunks.map(({ data }) => data));
806
- return { code: exitCodeOf(proc.exitCode, proc.signalCode), chunks, output, text: output.toString() };
1126
+ function capped(rows, limit) {
1127
+ const more = rows.length - limit;
1128
+ return more > 0 ? { shown: rows.slice(0, limit), footer: moreFooter(more) } : { shown: [...rows], footer: undefined };
807
1129
  }
808
- var SIGNALS;
809
- var init_process = __esm(() => {
810
- SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
1130
+ var init_limit = __esm(() => {
1131
+ init_args();
1132
+ });
1133
+
1134
+ // ../kit/src/logs.ts
1135
+ var DAY_MS;
1136
+ var init_logs = __esm(() => {
1137
+ DAY_MS = 24 * 60 * 60 * 1000;
1138
+ });
1139
+
1140
+ // ../kit/src/secrets.ts
1141
+ var SECRET_WORDS;
1142
+ var init_secrets = __esm(() => {
1143
+ SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
811
1144
  });
812
1145
 
813
1146
  // ../kit/src/index.ts
814
1147
  var init_src = __esm(() => {
815
1148
  init_args();
816
1149
  init_chart();
1150
+ init_ci();
817
1151
  init_cli();
818
1152
  init_config();
819
1153
  init_format();
1154
+ init_git();
1155
+ init_limit();
1156
+ init_logs();
820
1157
  init_process();
1158
+ init_secrets();
821
1159
  init_style();
822
1160
  init_text();
1161
+ init_tool();
1162
+ });
1163
+
1164
+ // src/registry.ts
1165
+ function isPublic(registry) {
1166
+ return registry.url.replace(TRAILING_SLASHES, "") === PUBLIC_REGISTRY;
1167
+ }
1168
+ function unreachable(message) {
1169
+ return fail(`could not check: ${message}`, 2);
1170
+ }
1171
+ function requestInit(token) {
1172
+ return { headers: { accept: "application/json", ...token === undefined ? {} : { authorization: `Bearer ${token}` } }, signal: AbortSignal.timeout(REQUEST_TIMEOUT) };
1173
+ }
1174
+ async function get(fetcher, url, token) {
1175
+ try {
1176
+ return await fetcher(url, requestInit(token));
1177
+ } catch (error) {
1178
+ return unreachable(`cannot reach ${new URL(url).host}: ${error instanceof Error ? error.message : String(error)}`);
1179
+ }
1180
+ }
1181
+ function isValidName(name) {
1182
+ return PACKAGE_NAME.test(name) && name.length <= 214;
1183
+ }
1184
+ async function fetchPackument(name, registry, fetcher = fetch) {
1185
+ if (!isValidName(name))
1186
+ fail(`${name} is not a valid npm package name`);
1187
+ const response = await get(fetcher, `${registry.url.replace(TRAILING_SLASHES, "")}/${encodeURIComponent(name).replace("%40", "@").replace("%2F", "%2f")}`, registry.token);
1188
+ if (response.status === 404)
1189
+ return;
1190
+ if (!response.ok)
1191
+ unreachable(`${new URL(registry.url).host} answered ${response.status} for ${name}`);
1192
+ let packument;
1193
+ try {
1194
+ packument = await response.json();
1195
+ } catch {
1196
+ return unreachable(`${new URL(registry.url).host} answered ${name} with something that is not JSON`);
1197
+ }
1198
+ if (packument?.name !== name)
1199
+ unreachable(`${new URL(registry.url).host} answered ${name} with the metadata of ${String(packument?.name)}`);
1200
+ return packument;
1201
+ }
1202
+ async function fetchDownloads(name, fetcher = fetch) {
1203
+ const response = await get(fetcher, `${DOWNLOADS_API}/${encodeURIComponent(name)}`);
1204
+ if (!response.ok)
1205
+ return;
1206
+ try {
1207
+ return (await response.json()).downloads;
1208
+ } catch {
1209
+ return;
1210
+ }
1211
+ }
1212
+ var PUBLIC_REGISTRY = "https://registry.npmjs.org", DOWNLOADS_API = "https://api.npmjs.org/downloads/point/last-week", TRAILING_SLASHES, REQUEST_TIMEOUT = 1e4, PACKAGE_NAME;
1213
+ var init_registry = __esm(() => {
1214
+ init_src();
1215
+ TRAILING_SLASHES = /\/+$/;
1216
+ PACKAGE_NAME = /^(?:@[\w~-][\w.~-]*\/)?[\w~-][\w.~-]*$/;
1217
+ });
1218
+
1219
+ // src/config.ts
1220
+ import { homedir } from "os";
1221
+ import { dirname, join } from "path";
1222
+ import process8 from "process";
1223
+ function expand2(value, env) {
1224
+ let missing = false;
1225
+ const expanded = value.replace(ENV_REFERENCE, (_, braced, bare) => {
1226
+ const found = env[braced ?? bare ?? ""];
1227
+ missing ||= found === undefined;
1228
+ return found ?? "";
1229
+ });
1230
+ return missing ? undefined : expanded;
1231
+ }
1232
+ function literal(value) {
1233
+ return expand2(value, {}) === value ? value : undefined;
1234
+ }
1235
+ function parseNpmrc(text, env) {
1236
+ const config = { scopes: {}, tokens: {}, scopeTokens: {} };
1237
+ for (const line of text.split(`
1238
+ `)) {
1239
+ const at = line.indexOf("=");
1240
+ if (at === -1 || COMMENT.test(line))
1241
+ continue;
1242
+ const key = line.slice(0, at).trim();
1243
+ const scope = SCOPE_REGISTRY.exec(key)?.[1];
1244
+ const tokenFor = AUTH_TOKEN.exec(key)?.[1];
1245
+ const raw = line.slice(at + 1).trim().replace(QUOTES, "$2");
1246
+ const value = tokenFor === undefined ? literal(raw) : expand2(raw, env);
1247
+ if (value === undefined || value === "")
1248
+ continue;
1249
+ if (scope !== undefined)
1250
+ config.scopes[scope] = value;
1251
+ else if (tokenFor !== undefined)
1252
+ config.tokens[tokenFor] = value;
1253
+ else if (key === "registry")
1254
+ config.registry = value;
1255
+ }
1256
+ return config;
1257
+ }
1258
+ function bunEntry(entry, env) {
1259
+ const url = typeof entry === "string" ? entry : entry.url;
1260
+ const token = typeof entry === "string" || entry.token === undefined ? undefined : expand2(entry.token, env);
1261
+ return { ...url === undefined || literal(url) === undefined ? {} : { url }, ...token === undefined ? {} : { token } };
1262
+ }
1263
+ function parseBunfig(text, env) {
1264
+ const config = { scopes: {}, tokens: {}, scopeTokens: {} };
1265
+ const install = Bun.TOML.parse(text).install;
1266
+ if (install?.registry !== undefined) {
1267
+ const { url, token } = bunEntry(install.registry, env);
1268
+ if (url !== undefined)
1269
+ config.registry = url;
1270
+ if (url !== undefined && token !== undefined)
1271
+ config.tokens[url.replace(PROTOCOL, "")] = token;
1272
+ }
1273
+ for (const [scope, entry] of Object.entries(install?.scopes ?? {})) {
1274
+ const { url, token } = bunEntry(entry, env);
1275
+ if (url !== undefined)
1276
+ config.scopes[scope.startsWith("@") ? scope : `@${scope}`] = url;
1277
+ if (token !== undefined)
1278
+ config.scopeTokens[scope.startsWith("@") ? scope : `@${scope}`] = token;
1279
+ }
1280
+ return config;
1281
+ }
1282
+ function tokenFor(config, url) {
1283
+ const address = `${url.replace(PROTOCOL, "").replace(TRAILING_SLASHES2, "")}/`;
1284
+ const best = Object.keys(config.tokens).filter((key) => address.startsWith(`${key.replace(TRAILING_SLASHES2, "")}/`)).sort((a, b) => b.length - a.length)[0];
1285
+ return best === undefined ? undefined : config.tokens[best];
1286
+ }
1287
+ function mayCarryToken(url) {
1288
+ const { protocol, hostname } = new URL(url);
1289
+ return protocol === "https:" || LOCAL_HOSTS.has(hostname);
1290
+ }
1291
+ function registryFor(name, configs) {
1292
+ const scope = name.startsWith("@") ? name.slice(0, name.indexOf("/")) : undefined;
1293
+ const scoped = scope === undefined ? undefined : configs.find((config) => config.scopes[scope] !== undefined);
1294
+ const url = (scope === undefined ? undefined : scoped?.scopes[scope]) ?? configs.find((config) => config.registry !== undefined)?.registry;
1295
+ if (url === undefined)
1296
+ return { url: PUBLIC_REGISTRY };
1297
+ const token = (scope === undefined ? undefined : scoped?.scopeTokens[scope]) ?? configs.map((other) => tokenFor(other, url)).find((found) => found !== undefined);
1298
+ return { url, token: mayCarryToken(url) ? token : undefined };
1299
+ }
1300
+ function readable(path, parse) {
1301
+ try {
1302
+ return parse();
1303
+ } catch (error) {
1304
+ return unreachable(`${path} cannot be read: ${error instanceof Error ? error.message : String(error)}`);
1305
+ }
1306
+ }
1307
+ async function readIfThere(path) {
1308
+ const file = Bun.file(path);
1309
+ return await file.exists() ? file.text() : undefined;
1310
+ }
1311
+ async function loadConfigs(cwd = process8.cwd(), env = process8.env, home = homedir()) {
1312
+ const directories = [];
1313
+ for (let dir = cwd;!directories.includes(dir); dir = dirname(dir))
1314
+ directories.push(dir);
1315
+ if (!directories.includes(home))
1316
+ directories.push(home);
1317
+ const configs = [];
1318
+ for (const dir of directories) {
1319
+ const bunfig = await readIfThere(join(dir, "bunfig.toml"));
1320
+ const npmrc = await readIfThere(join(dir, ".npmrc"));
1321
+ if (bunfig !== undefined)
1322
+ configs.push(readable(join(dir, "bunfig.toml"), () => parseBunfig(bunfig, env)));
1323
+ if (npmrc !== undefined)
1324
+ configs.push(parseNpmrc(npmrc, env));
1325
+ }
1326
+ return configs;
1327
+ }
1328
+ function isSeverity(value) {
1329
+ return SEVERITIES.includes(value);
1330
+ }
1331
+ function parseSettings(text) {
1332
+ const raw = Bun.JSONC.parse(text);
1333
+ const settings = {};
1334
+ if (Array.isArray(raw?.licenses) && raw.licenses.every((each) => typeof each === "string"))
1335
+ settings.licenses = raw.licenses;
1336
+ if (isSeverity(raw?.blockSeverity))
1337
+ settings.blockSeverity = raw.blockSeverity;
1338
+ return settings;
1339
+ }
1340
+ async function loadSettings(cwd = process8.cwd(), home = homedir()) {
1341
+ const files = [];
1342
+ for (let dir = cwd;!files.includes(join(dir, SETTINGS_FILE)); dir = dirname(dir))
1343
+ files.push(join(dir, SETTINGS_FILE));
1344
+ files.push(join(home, ".config", "pkg", "config.json"));
1345
+ const settings = {};
1346
+ for (const file of files) {
1347
+ const text = await readIfThere(file);
1348
+ if (text === undefined)
1349
+ continue;
1350
+ const found = readable(file, () => parseSettings(text));
1351
+ settings.licenses ??= found.licenses;
1352
+ settings.blockSeverity ??= found.blockSeverity;
1353
+ }
1354
+ return settings;
1355
+ }
1356
+ var ENV_REFERENCE, COMMENT, SCOPE_REGISTRY, AUTH_TOKEN, PROTOCOL, TRAILING_SLASHES2, LOCAL_HOSTS, QUOTES, SEVERITIES, DEFAULT_BLOCK_SEVERITY = "high", SETTINGS_FILE = ".pkgrc.json";
1357
+ var init_config2 = __esm(() => {
1358
+ init_registry();
1359
+ ENV_REFERENCE = /\$\{(\w+)\}|\$(\w+)/g;
1360
+ COMMENT = /^\s*[#;]/;
1361
+ SCOPE_REGISTRY = /^(@[^:]+):registry$/;
1362
+ AUTH_TOKEN = /^(\/\/.+?):_authToken$/;
1363
+ PROTOCOL = /^\w+:/;
1364
+ TRAILING_SLASHES2 = /\/+$/;
1365
+ LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
1366
+ QUOTES = /^(["'])(.*)\1$/;
1367
+ SEVERITIES = ["low", "moderate", "high", "critical"];
1368
+ });
1369
+
1370
+ // src/semver.ts
1371
+ function isRange(text) {
1372
+ if (text.trim() === "")
1373
+ return true;
1374
+ return text.split(OR).every((alternative) => {
1375
+ const words = alternative.trim().split(WHITESPACE);
1376
+ return words.every((word, index) => word === "-" ? index > 0 && index < words.length - 1 : COMPARATOR.test(word));
1377
+ });
1378
+ }
1379
+ function compare(version, word) {
1380
+ const bound = BOUND.exec(word);
1381
+ if (bound === null)
1382
+ return;
1383
+ const order = Bun.semver.order(version, bound[2] ?? "");
1384
+ const operator = bound[1] ?? "=";
1385
+ if (operator === "<")
1386
+ return order < 0;
1387
+ if (operator === "<=")
1388
+ return order <= 0;
1389
+ if (operator === ">")
1390
+ return order > 0;
1391
+ return operator === ">=" ? order >= 0 : order === 0;
1392
+ }
1393
+ function inRange(version, range) {
1394
+ if (Bun.semver.satisfies(version, range))
1395
+ return true;
1396
+ if (!PRERELEASE.test(version))
1397
+ return false;
1398
+ return range.split(OR).some((alternative) => alternative.trim().split(WHITESPACE).every((word) => compare(version, word) ?? Bun.semver.satisfies(version, word)));
1399
+ }
1400
+ var OR, WHITESPACE, PRERELEASE, PART, COMPARATOR, BOUND, EXACT_VERSION;
1401
+ var init_semver = __esm(() => {
1402
+ OR = /\s*\|\|\s*/;
1403
+ WHITESPACE = /\s+/;
1404
+ PRERELEASE = /-/;
1405
+ PART = String.raw`(?:\d+|[xX*])`;
1406
+ COMPARATOR = new RegExp(String.raw`^(?:[<>]=?|=|\^|~>?)?v?${PART}(?:\.${PART}){0,2}(?:-[\w.-]+)?(?:\+[\w.-]+)?$`);
1407
+ BOUND = /^(<=|>=|[<>=])?v?(\d+\.\d+\.\d+(?:-[\w.-]+)?)$/;
1408
+ EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
1409
+ });
1410
+
1411
+ // src/advisories.ts
1412
+ function severityRank(severity) {
1413
+ return SEVERITIES.indexOf(severity);
1414
+ }
1415
+ function fixedVersion(vulnerable, version) {
1416
+ const range = vulnerable.split("||").map((each) => each.trim()).find((each) => inRange(version, each));
1417
+ return range === undefined ? undefined : UPPER_BOUND.exec(range)?.[1];
1418
+ }
1419
+ async function fetchAdvisories(versions, registry, fetcher = fetch) {
1420
+ const url = `${registry.url.replace(TRAILING_SLASHES3, "")}${BULK_PATH}`;
1421
+ const host = new URL(registry.url).host;
1422
+ let response;
1423
+ try {
1424
+ response = await fetcher(url, {
1425
+ method: "POST",
1426
+ headers: { accept: "application/json", "content-type": "application/json", ...registry.token === undefined ? {} : { authorization: `Bearer ${registry.token}` } },
1427
+ body: JSON.stringify(versions),
1428
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT2)
1429
+ });
1430
+ } catch (error) {
1431
+ return { found: {}, unavailable: `cannot reach ${host} for advisories: ${error instanceof Error ? error.message : String(error)}` };
1432
+ }
1433
+ if (NO_ENDPOINT.has(response.status))
1434
+ return { found: {}, isUnsupported: true, unavailable: `${host} has no advisory endpoint` };
1435
+ if (!response.ok)
1436
+ return { found: {}, unavailable: `${host} answered ${response.status} for advisories` };
1437
+ let body;
1438
+ try {
1439
+ body = await response.json();
1440
+ } catch {
1441
+ return { found: {}, unavailable: `${host} answered advisories with something that is not JSON` };
1442
+ }
1443
+ const found = {};
1444
+ for (const [name, raws] of Object.entries(body)) {
1445
+ if (!Array.isArray(raws))
1446
+ continue;
1447
+ found[name] = raws.filter((raw) => SEVERITIES.includes(raw.severity)).map((raw) => ({
1448
+ id: raw.id,
1449
+ ghsa: GHSA.exec(raw.url)?.[0],
1450
+ url: raw.url,
1451
+ title: raw.title,
1452
+ severity: raw.severity,
1453
+ vulnerable: raw.vulnerable_versions
1454
+ }));
1455
+ }
1456
+ return { found };
1457
+ }
1458
+ function advisoriesFor(advisories, version) {
1459
+ return (advisories ?? []).filter((each) => inRange(version, each.vulnerable)).map((each) => ({ ...each, fixed: fixedVersion(each.vulnerable, version) }));
1460
+ }
1461
+ function filterAdvisories(advisories, level, ignore = []) {
1462
+ return advisories.filter((each) => severityRank(each.severity) >= severityRank(level) && !ignore.includes(each.ghsa ?? "") && !ignore.includes(String(each.id)));
1463
+ }
1464
+ var BULK_PATH = "/-/npm/v1/security/advisories/bulk", TRAILING_SLASHES3, REQUEST_TIMEOUT2 = 1e4, GHSA, UPPER_BOUND, NO_ENDPOINT;
1465
+ var init_advisories = __esm(() => {
1466
+ init_config2();
1467
+ init_semver();
1468
+ TRAILING_SLASHES3 = /\/+$/;
1469
+ GHSA = /GHSA-[\w-]+$/;
1470
+ UPPER_BOUND = /(?:^|\s)<([\w.+-]+)/;
1471
+ NO_ENDPOINT = new Set([404, 405, 501]);
1472
+ });
1473
+
1474
+ // src/licenses.ts
1475
+ function isLicenseAllowed(expression, allowed) {
1476
+ const known = new Set(allowed.map((each) => each.toLowerCase()));
1477
+ return expression.replace(PARENS, " ").trim().split(OR_SPLIT).some((alternative) => alternative.split(AND_SPLIT).every((term) => known.has(term.replace(WITH_EXCEPTION, "").trim().toLowerCase())));
1478
+ }
1479
+ var OR_SPLIT, AND_SPLIT, WITH_EXCEPTION, PARENS;
1480
+ var init_licenses = __esm(() => {
1481
+ OR_SPLIT = /\s+OR\s+/i;
1482
+ AND_SPLIT = /\s+AND\s+/i;
1483
+ WITH_EXCEPTION = /\s+WITH\s+\S+$/i;
1484
+ PARENS = /[()]/g;
823
1485
  });
824
1486
 
825
1487
  // src/popular.json
@@ -1333,20 +1995,23 @@ var init_popular = __esm(() => {
1333
1995
  function baseName(name) {
1334
1996
  return name.slice(name.indexOf("/") + 1);
1335
1997
  }
1336
- function scopeOf(name) {
1998
+ function scopeOf2(name) {
1337
1999
  return name.startsWith("@") ? name.slice(0, name.indexOf("/")) : "";
1338
2000
  }
1339
2001
  function squash(name) {
1340
2002
  return name.toLowerCase().replace(SEPARATORS, "");
1341
2003
  }
1342
2004
  function resemblance(name, popular) {
1343
- if (scopeOf(name) !== scopeOf(popular) && baseName(name) === baseName(popular) && baseName(name).length >= MIN_LENGTH)
2005
+ if (scopeOf2(name) !== scopeOf2(popular) && baseName(name) === baseName(popular) && baseName(name).length >= MIN_LENGTH)
1344
2006
  return `${popular} under another scope`;
1345
2007
  if (squash(name) === squash(popular))
1346
2008
  return `${popular} spelled with other separators or case`;
2009
+ const shortest = Math.min(name.length, popular.length);
2010
+ const allowed = shortest >= MIN_LENGTH * 2 ? 2 : 1;
2011
+ if (shortest < MIN_LENGTH || Math.abs(name.length - popular.length) > allowed)
2012
+ return;
1347
2013
  const edits = distance(name, popular);
1348
- const allowed = Math.min(name.length, popular.length) >= MIN_LENGTH * 2 ? 2 : 1;
1349
- return Math.min(name.length, popular.length) >= MIN_LENGTH && edits <= allowed ? `${popular}, ${edits} ${edits === 1 ? "edit" : "edits"} away` : undefined;
2014
+ return edits <= allowed ? `${popular}, ${edits} ${edits === 1 ? "edit" : "edits"} away` : undefined;
1350
2015
  }
1351
2016
  function nearMisses(name, popular = popular_default) {
1352
2017
  if (popular.includes(name))
@@ -1382,6 +2047,8 @@ function resolve(packument, request) {
1382
2047
  return tags[wanted];
1383
2048
  if (versions.includes(wanted))
1384
2049
  return wanted;
2050
+ if (!isRange(wanted))
2051
+ return;
1385
2052
  return versions.filter((version) => Bun.semver.satisfies(version, wanted)).sort(Bun.semver.order).at(-1);
1386
2053
  }
1387
2054
  function licenseOf(license) {
@@ -1405,7 +2072,7 @@ function safeRelease(packument, now, minAgeDays) {
1405
2072
  }
1406
2073
  function analyze(input) {
1407
2074
  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 };
2075
+ 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
2076
  if (report.nearMisses.length > 0)
1410
2077
  report.warnings.push(`name looks like ${report.nearMisses.join("; ")}, check it is the package meant`);
1411
2078
  if (packument === undefined) {
@@ -1432,7 +2099,9 @@ function analyze(input) {
1432
2099
  }
1433
2100
  const age = ageDays(report.published, now);
1434
2101
  if (age === undefined) {
1435
- report.warnings.push(`${registry} gives no publish date for ${version}, its age cannot be checked`);
2102
+ const isBlocking = minAgeDays > 0 && registry === PUBLIC_HOST;
2103
+ report.blocked ||= isBlocking;
2104
+ report.warnings.push(`${registry} gives no publish date for ${version}, its age cannot be checked${isBlocking ? " and it is refused" : ""}`);
1436
2105
  } else if (age < minAgeDays) {
1437
2106
  report.blocked = true;
1438
2107
  report.warnings.push(`${version} was published ${age < 1 ? "less than a day" : `${Math.floor(age)} day${Math.floor(age) === 1 ? "" : "s"}`} ago, under ${minAgeDays} days${report.safe === undefined ? ", and no older stable version exists" : `, use ${spec.name}@${report.safe.version}`}`);
@@ -1442,166 +2111,61 @@ function analyze(input) {
1442
2111
  report.warnings.push(`${version} runs ${scripts.join(", ")} at install`);
1443
2112
  if (report.deprecated !== undefined)
1444
2113
  report.warnings.push(`${version} is deprecated: ${report.deprecated}`);
2114
+ if (input.licenses !== undefined) {
2115
+ const isMissing = report.license === undefined || report.license === "";
2116
+ if (isMissing) {
2117
+ report.blocked = true;
2118
+ report.warnings.push(`${version} declares no licence, and the allowlist needs one`);
2119
+ } else if (!isLicenseAllowed(report.license ?? "", input.licenses)) {
2120
+ report.blocked = true;
2121
+ report.warnings.push(`licence ${report.license} is not in the allowlist (${input.licenses.join(", ")})`);
2122
+ }
2123
+ }
2124
+ if (input.blockSeverity !== undefined) {
2125
+ const blocking = filterAdvisories(report.advisories, input.blockSeverity);
2126
+ if (blocking.length > 0) {
2127
+ report.blocked = true;
2128
+ 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(", ")}`);
2129
+ }
2130
+ }
1445
2131
  return report;
1446
2132
  }
1447
- var DEFAULT_MIN_AGE_DAYS = 14, DAY = 86400000, INSTALL_SCRIPTS, GIT_PREFIX, VERSION_SEPARATOR;
2133
+ var DEFAULT_MIN_AGE_DAYS = 14, DAY = 86400000, PUBLIC_HOST, INSTALL_SCRIPTS, GIT_PREFIX, VERSION_SEPARATOR;
1448
2134
  var init_analyze = __esm(() => {
2135
+ init_advisories();
2136
+ init_licenses();
1449
2137
  init_nearmiss();
2138
+ init_registry();
2139
+ init_semver();
2140
+ PUBLIC_HOST = new URL(PUBLIC_REGISTRY).host;
1450
2141
  INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
1451
2142
  GIT_PREFIX = /^git\+/;
1452
2143
  VERSION_SEPARATOR = /(?<=.)@/;
1453
2144
  });
1454
2145
 
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
2146
  // src/commands/check.ts
1595
2147
  var exports_check = {};
1596
2148
  __export(exports_check, {
1597
- check: () => check
2149
+ check: () => check,
2150
+ checkPackage: () => checkPackage
1598
2151
  });
1599
2152
  async function checkPackage(text, context) {
1600
2153
  const spec = parseSpec(text);
1601
2154
  const registry = registryFor(spec.name, context.configs);
1602
2155
  const packument = await fetchPackument(spec.name, registry, context.fetcher);
1603
2156
  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 });
2157
+ const input = { spec, packument, downloads, registry: new URL(registry.url).host, now: context.now, minAgeDays: context.minAgeDays, licenses: context.settings?.licenses };
2158
+ const report = analyze(input);
2159
+ const blockSeverity = context.settings?.blockSeverity ?? DEFAULT_BLOCK_SEVERITY;
2160
+ if (report.version === undefined)
2161
+ return report;
2162
+ const { found, unavailable, isUnsupported } = await fetchAdvisories({ [spec.name]: [report.version] }, registry, context.fetcher);
2163
+ if (unavailable !== undefined && isUnsupported !== true)
2164
+ return unreachable(unavailable);
2165
+ const checked = analyze({ ...input, blockSeverity, advisories: advisoriesFor(found[spec.name], report.version) });
2166
+ if (unavailable !== undefined)
2167
+ checked.warnings.push(`${unavailable}, so its known advisories are not checked`);
2168
+ return checked;
1605
2169
  }
1606
2170
  function when(release, now) {
1607
2171
  return release?.published === undefined ? "" : `, published ${formatAge(release.published, now)} (${release.published.slice(0, 10)})`;
@@ -1634,7 +2198,7 @@ async function check(args) {
1634
2198
  fail(`a package name is needed
1635
2199
 
1636
2200
  ${usage}`);
1637
- const context = { configs: await loadConfigs(), minAgeDays: wholeNumber(values["min-age"], "min-age", DEFAULT_MIN_AGE_DAYS, 0) };
2201
+ const context = { configs: await loadConfigs(), settings: await loadSettings(), minAgeDays: wholeNumber(values["min-age"], "min-age", DEFAULT_MIN_AGE_DAYS, 0) };
1638
2202
  const reports = await Promise.all(positionals.map(async (text) => checkPackage(text, context)));
1639
2203
  if (values.json === true)
1640
2204
  printJson(reports.length === 1 ? reports[0] : reports);
@@ -1647,9 +2211,13 @@ ${usage}`);
1647
2211
  var usage = `pkg [check] <name>[@version]...
1648
2212
 
1649
2213
  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;
2214
+
2215
+ 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; high when unset) refuses a version with an advisory at that level or above.
2216
+
2217
+ Exits 0 when every package passes, 1 when one is blocked (missing, the version asked for is under the minimum age or has no date, its licence is outside the allowlist or an advisory reaches the threshold), 2 when it could not check (the registry unreachable, slow or answering badly, a broken .pkgrc.json, bunfig.toml or .npmrc) or on a usage error.`, options;
1651
2218
  var init_check = __esm(() => {
1652
2219
  init_src();
2220
+ init_advisories();
1653
2221
  init_analyze();
1654
2222
  init_config2();
1655
2223
  init_registry();
@@ -1659,125 +2227,176 @@ var init_check = __esm(() => {
1659
2227
  };
1660
2228
  });
1661
2229
 
1662
- // ../kit/src/progress.ts
1663
- import process8 from "process";
1664
- function liveRegion(render, stream = process8.stderr, env = process8.env) {
1665
- if (!takesControlCodes(stream, env))
1666
- return { redraw: () => {
1667
- return;
1668
- }, print: (text) => void stream.write(`${text}
1669
- `), stop: () => {
1670
- return;
1671
- } };
1672
- const write = stream.write.bind(stream);
1673
- let frame = 0;
1674
- let drawn = 0;
1675
- const clear = () => {
1676
- if (drawn > 0)
1677
- write(`${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}`);
1678
- drawn = 0;
1679
- };
1680
- const draw = () => {
1681
- const width = (stream.columns === undefined || stream.columns === 0 ? 80 : stream.columns) - 1;
1682
- const height = stream.rows === undefined || stream.rows === 0 ? 24 : stream.rows;
1683
- const lines = render(FRAMES[frame++ % FRAMES.length]).slice(0, Math.max(1, height - 1)).map((line) => cutToWidth(line, width));
1684
- if (lines.length === 0) {
1685
- clear();
1686
- return;
1687
- }
1688
- write(`${drawn > 0 ? `${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}` : CLEAR_LINE}${lines.join(`
1689
- `)}`);
1690
- drawn = lines.length;
1691
- };
1692
- const timer = setInterval(draw, FRAME_MS);
1693
- const restore = interceptOutput(clear);
1694
- const onExit = () => clear();
1695
- const signals = holdSignals(stop);
1696
- function stop() {
1697
- clearInterval(timer);
1698
- clear();
1699
- restore();
1700
- process8.off("exit", onExit);
1701
- signals.release();
1702
- }
1703
- timer.unref();
1704
- process8.on("exit", onExit);
1705
- return {
1706
- redraw: draw,
1707
- print: (text) => {
1708
- clear();
1709
- write(`${text}
1710
- `);
1711
- draw();
1712
- },
1713
- stop
1714
- };
1715
- }
1716
- function cutToWidth(line, width) {
1717
- if (Bun.stringWidth(line) <= width)
1718
- return line;
1719
- let room = width;
1720
- let kept = "";
1721
- let index = 0;
1722
- let linked = false;
1723
- while (index < line.length) {
1724
- ESCAPE_CODE.lastIndex = index;
1725
- const escape = ESCAPE_CODE.exec(line);
1726
- if (escape !== null) {
1727
- kept += escape[0];
1728
- linked ||= escape[0].startsWith(`${ESC}]`);
1729
- index = ESCAPE_CODE.lastIndex;
2230
+ // src/lockfile.ts
2231
+ function split(resolution) {
2232
+ const at = resolution.search(VERSION_SEPARATOR2);
2233
+ if (at === -1)
2234
+ return;
2235
+ const target = resolution.slice(at + 1);
2236
+ if (target.startsWith(NPM_ALIAS))
2237
+ return split(target.slice(NPM_ALIAS.length));
2238
+ return NOT_REGISTRY.test(target) ? undefined : { name: resolution.slice(0, at), version: target };
2239
+ }
2240
+ function parseBunLock(text) {
2241
+ const lock = Bun.JSONC.parse(text);
2242
+ const seen = new Map;
2243
+ for (const entry of Object.values(lock?.packages ?? {})) {
2244
+ const resolution = entry[0];
2245
+ if (typeof resolution !== "string")
1730
2246
  continue;
1731
- }
1732
- const char = String.fromCodePoint(line.codePointAt(index));
1733
- const columns = Bun.stringWidth(char);
1734
- if (columns > room)
1735
- break;
1736
- kept += char;
1737
- room -= columns;
1738
- index += char.length;
2247
+ const locked = split(resolution);
2248
+ if (locked !== undefined)
2249
+ seen.set(`${locked.name}@${locked.version}`, locked);
1739
2250
  }
1740
- return `${kept}${RESET_STYLE}${linked ? CLOSE_LINK : ""}`;
2251
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name) || Bun.semver.order(a.version, b.version));
1741
2252
  }
1742
- function interceptOutput(clear) {
1743
- const targets = [...CONSOLE_METHODS.map((name) => [console, name]), [process8.stdout, "write"], [process8.stderr, "write"]];
1744
- const wrapped = targets.map(([target, name]) => {
1745
- const original = Reflect.get(target, name);
1746
- const wrapper = (...args) => {
1747
- clear();
1748
- return original.apply(target, args);
1749
- };
1750
- Reflect.set(target, name, wrapper);
1751
- return { target, name, original, wrapper };
1752
- });
1753
- return () => {
1754
- for (const { target, name, original, wrapper } of wrapped) {
1755
- if (Reflect.get(target, name) === wrapper)
1756
- Reflect.set(target, name, original);
1757
- }
2253
+ var NOT_REGISTRY, VERSION_SEPARATOR2, NPM_ALIAS = "npm:";
2254
+ var init_lockfile = __esm(() => {
2255
+ NOT_REGISTRY = /^(?:workspace|link|file|github|git|http|https):|^[\w.-]+\/[\w.-]+$/;
2256
+ VERSION_SEPARATOR2 = /(?<=.)@/;
2257
+ });
2258
+
2259
+ // src/commands/audit.ts
2260
+ var exports_audit = {};
2261
+ __export(exports_audit, {
2262
+ audit: () => audit
2263
+ });
2264
+ import { join as join2, resolve as resolve2 } from "path";
2265
+ import process9 from "process";
2266
+ function hasFinding(audited) {
2267
+ return audited.blocked || audited.warnings.length > 0 || audited.advisories.length > 0;
2268
+ }
2269
+ async function auditPackages(packages, context) {
2270
+ const packuments = new Map;
2271
+ const lookup = async (name, registry) => {
2272
+ const key = `${registry.url}|${name}`;
2273
+ if (!packuments.has(key))
2274
+ packuments.set(key, fetchPackument(name, registry, context.fetcher));
2275
+ return packuments.get(key);
1758
2276
  };
2277
+ const registries = new Map;
2278
+ for (const { name, version } of packages) {
2279
+ const registry = registryFor(name, context.configs);
2280
+ const group = registries.get(registry.url) ?? { registry, versions: {} };
2281
+ group.versions[name] = [...group.versions[name] ?? [], version];
2282
+ registries.set(registry.url, group);
2283
+ }
2284
+ const advisories = {};
2285
+ const unavailable = [];
2286
+ for (const { registry, versions } of registries.values()) {
2287
+ const fetched = await fetchAdvisories(versions, registry, context.fetcher);
2288
+ Object.assign(advisories, fetched.found);
2289
+ if (fetched.unavailable !== undefined)
2290
+ unavailable.push(fetched.unavailable);
2291
+ }
2292
+ const audited = [];
2293
+ for (let start = 0;start < packages.length; start += BATCH) {
2294
+ audited.push(...await Promise.all(packages.slice(start, start + BATCH).map(async ({ name, version }) => {
2295
+ const registry = registryFor(name, context.configs);
2296
+ const found = filterAdvisories(advisoriesFor(advisories[name], version), context.level, context.ignore);
2297
+ 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 });
2298
+ return { name, version, license: report.license, blocked: report.blocked || found.length > 0, warnings: report.warnings, advisories: found };
2299
+ })));
2300
+ }
2301
+ return { audited, unavailable };
1759
2302
  }
1760
- var FRAMES, FRAME_MS = 80, CLEAR_LINE = "\r\x1B[2K", LINE_UP = "\x1B[1A\x1B[2K", ESC, ESCAPE_CODE, RESET_STYLE, CLOSE_LINK, CONSOLE_METHODS;
1761
- var init_progress = __esm(() => {
1762
- init_format();
1763
- init_process();
1764
- init_style();
1765
- FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1766
- ESC = String.fromCharCode(27);
1767
- ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, "y");
1768
- RESET_STYLE = `${ESC}[0m`;
1769
- CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
1770
- CONSOLE_METHODS = ["log", "info", "warn", "error", "debug", "dir", "dirxml", "table", "trace", "group", "groupCollapsed", "count", "timeEnd", "timeLog", "assert"];
2303
+ function renderAudit(findings, total) {
2304
+ const blocks = findings.map(({ name, version, warnings, advisories }) => [
2305
+ `${name}@${version}`,
2306
+ ...warnings.map((warning) => ` warning: ${warning}`),
2307
+ ...advisories.map((each) => ` ${each.ghsa ?? each.id} ${each.severity}: ${each.title}${each.fixed === undefined ? "" : ` (fixed in ${each.fixed})`}`)
2308
+ ].join(`
2309
+ `));
2310
+ return [...blocks, `${findings.length} of ${total} packages with a finding, ${findings.filter((each) => each.blocked).length} blocking`].join(`
2311
+
2312
+ `);
2313
+ }
2314
+ async function audit(args) {
2315
+ const { values, positionals: [asked] } = parse(args, usage2, options2, { positionals: ["[dir]"] });
2316
+ const level = values["audit-level"] ?? "low";
2317
+ if (!isSeverity(level))
2318
+ fail(`--audit-level takes ${SEVERITIES.join(", ")}, not ${level}`);
2319
+ const dir = resolve2(asked ?? process9.cwd());
2320
+ const lock = Bun.file(join2(dir, "bun.lock"));
2321
+ if (!await lock.exists())
2322
+ fail(`no bun.lock in ${dir}`);
2323
+ const packages = parseBunLock(await lock.text());
2324
+ const context = {
2325
+ configs: await loadConfigs(dir),
2326
+ settings: await loadSettings(dir),
2327
+ minAgeDays: wholeNumber(values["min-age"], "min-age", DEFAULT_MIN_AGE_DAYS, 0),
2328
+ level,
2329
+ ignore: values.ignore ?? []
2330
+ };
2331
+ const { audited, unavailable } = await auditPackages(packages, context);
2332
+ const findings = audited.filter(hasFinding);
2333
+ if (values.json === true)
2334
+ printJson(findings);
2335
+ else
2336
+ console.log(renderAudit(findings, packages.length));
2337
+ for (const reason of unavailable)
2338
+ warn(`${reason}, so its advisories are not checked`);
2339
+ if (findings.some((each) => each.blocked))
2340
+ return 1;
2341
+ return unavailable.length > 0 ? 2 : 0;
2342
+ }
2343
+ var usage2 = `pkg audit [dir]
2344
+
2345
+ 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.
2346
+ Findings are grouped per package, each registry package looked up once per run.
2347
+
2348
+ 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 when the registry could not be reached or answered badly (a warning says which, and nothing is reported as clean), on a usage error or without a bun.lock.`, options2, BATCH = 8;
2349
+ var init_audit = __esm(() => {
2350
+ init_src();
2351
+ init_advisories();
2352
+ init_analyze();
2353
+ init_config2();
2354
+ init_lockfile();
2355
+ init_registry();
2356
+ options2 = {
2357
+ json: jsonOption("[{name, version, license, blocked, warnings, advisories}] for the packages with a finding"),
2358
+ "min-age": { type: "string", description: `days a version must be old (${DEFAULT_MIN_AGE_DAYS})` },
2359
+ "audit-level": { type: "string", description: "lowest advisory severity listed and failing: low, moderate, high, critical (low)" },
2360
+ ignore: { type: "string", multiple: true, description: "an advisory skipped by GHSA or numeric id, repeatable" }
2361
+ };
1771
2362
  });
1772
2363
 
1773
2364
  // ../kit/src/shell.ts
1774
- var SEPARATORS2, PLAIN_BREAKERS, DOUBLE_QUOTED_ESCAPES, WRAPPERS;
2365
+ function wrapper(short = "", long = [], operands = 0) {
2366
+ return { short, long, operands };
2367
+ }
2368
+ var SEPARATORS2, PLAIN_BREAKERS, GLOB_CHARS, COMMENT_PREFIXES, DOUBLE_QUOTED_ESCAPES, KEYWORDS, SHELLS, COMMAND_STRING_PROGRAMS, SHELL_VALUE_OPTIONS, PRINTERS, FIND_EXEC, WRAPPERS;
1775
2369
  var init_shell = __esm(() => {
1776
2370
  SEPARATORS2 = new Set([`
1777
2371
  `, ";", "|"]);
1778
2372
  PLAIN_BREAKERS = new Set(["<", ">", "(", ")", "&", "|"]);
2373
+ GLOB_CHARS = new Set(["*", "?", "["]);
2374
+ COMMENT_PREFIXES = new Set([";", "&", "|", "(", ")"]);
1779
2375
  DOUBLE_QUOTED_ESCAPES = new Set(["$", "`", '"', "\\"]);
1780
- WRAPPERS = new Set(["command", "env", "sudo", "exec", "nohup", "time"]);
2376
+ KEYWORDS = new Set(["then", "do", "else", "if", "elif", "while", "until", "!", "{", "}"]);
2377
+ SHELLS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh"]);
2378
+ COMMAND_STRING_PROGRAMS = new Set([...SHELLS, "su", "runuser"]);
2379
+ SHELL_VALUE_OPTIONS = new Set(["-o", "-O", "--rcfile", "--init-file"]);
2380
+ PRINTERS = new Set(["echo", "printf"]);
2381
+ FIND_EXEC = new Set(["-exec", "-execdir", "-ok", "-okdir"]);
2382
+ WRAPPERS = new Map([
2383
+ ["command", wrapper()],
2384
+ ["builtin", wrapper()],
2385
+ ["nohup", wrapper()],
2386
+ ["setsid", wrapper()],
2387
+ ["busybox", wrapper()],
2388
+ ["exec", wrapper("a")],
2389
+ ["time", wrapper("fo", ["--format", "--output"])],
2390
+ ["env", wrapper("uCS", ["--unset", "--chdir", "--split-string", "--argv0"])],
2391
+ ["sudo", wrapper("ughpCTrtUDR", ["--user", "--group", "--host", "--prompt", "--close-from", "--command-timeout", "--role", "--type", "--other-user", "--chdir", "--chroot"])],
2392
+ ["doas", wrapper("uC")],
2393
+ ["timeout", wrapper("sk", ["--signal", "--kill-after"], 1)],
2394
+ ["nice", wrapper("n", ["--adjustment"])],
2395
+ ["ionice", wrapper("cnpPu", ["--class", "--classdata", "--pid", "--pgid", "--uid"])],
2396
+ ["stdbuf", wrapper("ioe", ["--input", "--output", "--error"])],
2397
+ ["chroot", wrapper("", ["--userspec", "--groups"], 1)],
2398
+ ["xargs", wrapper("IndsEaPL", ["--max-args", "--max-procs", "--max-chars", "--delimiter", "--arg-file", "--eof", "--max-lines", "--process-slot-var"])]
2399
+ ]);
1781
2400
  });
1782
2401
 
1783
2402
  // src/pin.ts
@@ -1820,7 +2439,7 @@ var init_pin = __esm(() => {
1820
2439
 
1821
2440
  // src/consumers.ts
1822
2441
  import { existsSync, readdirSync, readFileSync } from "fs";
1823
- import { dirname as dirname2, join as join2, relative } from "path";
2442
+ import { dirname as dirname2, join as join3, relative } from "path";
1824
2443
  function packageFiles(dir, depth, found) {
1825
2444
  let entries;
1826
2445
  try {
@@ -1830,15 +2449,15 @@ function packageFiles(dir, depth, found) {
1830
2449
  }
1831
2450
  for (const entry of entries) {
1832
2451
  if (entry.isFile() && entry.name === "package.json")
1833
- found.push(join2(dir, entry.name));
2452
+ found.push(join3(dir, entry.name));
1834
2453
  else if (entry.isDirectory() && depth < MAX_DEPTH && !entry.name.startsWith(".") && !SKIPPED.has(entry.name))
1835
- packageFiles(join2(dir, entry.name), depth + 1, found);
2454
+ packageFiles(join3(dir, entry.name), depth + 1, found);
1836
2455
  }
1837
2456
  return found;
1838
2457
  }
1839
2458
  function managerFor(dir, root) {
1840
2459
  for (let at = dir;; at = dirname2(at)) {
1841
- const lockfile = LOCKFILES.find(([file]) => existsSync(join2(at, file)));
2460
+ const lockfile = LOCKFILES.find(([file]) => existsSync(join3(at, file)));
1842
2461
  if (lockfile !== undefined)
1843
2462
  return lockfile[1];
1844
2463
  if (at === root || at === dirname2(at))
@@ -1872,7 +2491,7 @@ var init_consumers = __esm(() => {
1872
2491
  init_pin();
1873
2492
  KINDS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
1874
2493
  SKIPPED = new Set(["node_modules", "dist", "build", "vendor", "target", "coverage"]);
1875
- LOCAL_SPEC = /^(?:workspace|link|file|portal):/;
2494
+ LOCAL_SPEC = /[:/]/;
1876
2495
  KIND_FLAGS = { dependencies: [], devDependencies: ["--dev"], optionalDependencies: ["--optional"], peerDependencies: ["--peer"] };
1877
2496
  });
1878
2497
 
@@ -1882,25 +2501,46 @@ __export(exports_bump, {
1882
2501
  ROOT_OPTION: () => ROOT_OPTION,
1883
2502
  bump: () => bump
1884
2503
  });
2504
+ import { existsSync as existsSync2 } from "fs";
1885
2505
  import { homedir as homedir2 } from "os";
1886
- import { join as join3 } from "path";
2506
+ import { dirname as dirname3, join as join4, relative as relative2 } from "path";
2507
+ async function lookup(name, registry, fetcher) {
2508
+ try {
2509
+ return { packument: await fetchPackument(name, registry, fetcher) };
2510
+ } catch (error) {
2511
+ if (error instanceof Exit && error.code === 2)
2512
+ return { error: error.message };
2513
+ throw error;
2514
+ }
2515
+ }
1887
2516
  async function servedVersion(name, version, settings) {
2517
+ if (!isValidName(name))
2518
+ fail(`${name} is not a valid npm package name`);
2519
+ if (version !== undefined && !EXACT_VERSION.test(version))
2520
+ fail(`bump takes an exact version, not ${version}: leave it out for the latest`);
1888
2521
  const registry = registryFor(name, settings.configs);
1889
2522
  const host = new URL(registry.url).host;
1890
2523
  const started = Date.now();
1891
2524
  const region = liveRegion((spinner) => [`${spinner} waiting for ${host} to serve ${name}@${version} ${formatDuration(Date.now() - started)}`]);
2525
+ let lastError = "";
1892
2526
  try {
1893
2527
  for (;; ) {
1894
- const packument = await fetchPackument(name, registry, settings.fetcher);
1895
- const latest = packument?.["dist-tags"]?.latest;
1896
- if (version === undefined && latest !== undefined)
1897
- return latest;
1898
- if (version === undefined)
1899
- fail(`${host} has no ${name}`, 1);
1900
- if (packument?.versions?.[version] !== undefined)
1901
- return version;
2528
+ const looked = await lookup(name, registry, settings.fetcher);
2529
+ if ("error" in looked) {
2530
+ lastError = looked.error;
2531
+ } else {
2532
+ const latest = looked.packument?.["dist-tags"]?.latest;
2533
+ if (looked.packument === undefined)
2534
+ fail(`${host} has no ${name}`, 1);
2535
+ if (version === undefined && latest === undefined)
2536
+ fail(`${host} has no ${name}`, 1);
2537
+ if (version === undefined && latest !== undefined)
2538
+ return latest;
2539
+ if (version !== undefined && looked.packument.versions?.[version] !== undefined)
2540
+ return version;
2541
+ }
1902
2542
  if (Date.now() - started >= settings.timeout)
1903
- fail(`${host} does not serve ${name}@${version} after ${formatDuration(settings.timeout)}`, 1);
2543
+ fail(`${host} does not serve ${name}@${version} after ${formatDuration(settings.timeout)}${lastError === "" ? "" : `, last error: ${lastError}`}`);
1904
2544
  region.redraw();
1905
2545
  await Bun.sleep(settings.poll ?? POLL);
1906
2546
  }
@@ -1908,65 +2548,142 @@ async function servedVersion(name, version, settings) {
1908
2548
  region.stop();
1909
2549
  }
1910
2550
  }
2551
+ async function vet(name, version, settings) {
2552
+ if (settings.isForced === true)
2553
+ return true;
2554
+ const report = await checkPackage(`${name}@${version}`, { configs: settings.configs, fetcher: settings.fetcher, minAgeDays: settings.minAgeDays ?? 0, settings: settings.pkgSettings });
2555
+ if (!report.blocked) {
2556
+ for (const warning of report.warnings)
2557
+ warn(`${name}@${version}`, warning);
2558
+ return true;
2559
+ }
2560
+ for (const warning of report.warnings)
2561
+ problem(`${name}@${version} is refused`, warning);
2562
+ problem(`${name}@${version} is refused`, "no consumer was touched, --force bumps it anyway");
2563
+ return false;
2564
+ }
2565
+ function lockfileOf(dir, root) {
2566
+ for (let at = dir;; at = dirname3(at)) {
2567
+ const found = ["bun.lock", "bun.lockb"].find((file) => existsSync2(join4(at, file)));
2568
+ if (found !== undefined)
2569
+ return relative2(dir, join4(at, found));
2570
+ if (at === root || at === dirname3(at))
2571
+ return;
2572
+ }
2573
+ }
2574
+ async function hasUncommittedChanges(where, root, run) {
2575
+ const lockfile = lockfileOf(where, root);
2576
+ const status = await run(["git", "status", "--porcelain", "--", "package.json", ...lockfile === undefined ? [] : [lockfile]], where);
2577
+ return status.code === 0 && status.text.trim() !== "";
2578
+ }
2579
+ async function checkAndCommit(name, version, where, root, run) {
2580
+ const checked = await run(["check"], where);
2581
+ if (checked.code !== 0)
2582
+ return `check exited ${checked.code}: ${checked.text.trim().split(`
2583
+ `).at(-1) ?? ""}`;
2584
+ const lockfile = lockfileOf(where, root);
2585
+ const committed = await run(["gc", `chore(deps): bump ${name} to ${version}`, "--", "package.json", ...lockfile === undefined ? [] : [lockfile]], where);
2586
+ return committed.code === 0 ? undefined : `gc exited ${committed.code}: ${committed.text.trim().split(`
2587
+ `).at(-1) ?? ""}`;
2588
+ }
1911
2589
  async function bumpConsumers(name, version, settings) {
1912
2590
  const found = findConsumers(name, settings.root);
1913
2591
  const consumers = found.filter((each) => !isLocal(each));
1914
2592
  const local = found.length - consumers.length;
1915
2593
  const run = settings.run ?? (async (argv, cwd) => runCommand(argv, { cwd }));
2594
+ const committed = [];
2595
+ const failing = [];
2596
+ const skipped = [];
1916
2597
  let isClean = true;
1917
2598
  if (found.length === 0)
1918
2599
  note(`nothing under ${settings.root} depends on ${name}`);
1919
2600
  if (local > 0)
1920
- note(`${local} workspace:, link: or file: dependenc${local === 1 ? "y" : "ies"}`, "left as they are");
2601
+ note(`${local} non-registry dependenc${local === 1 ? "y" : "ies"} (workspace:, file:, git, URL, alias, catalog:)`, "left as they are");
1921
2602
  for (const consumer of consumers) {
1922
2603
  const argv = addCommand(consumer, name, version);
1923
- const where = join3(settings.root, consumer.dir);
1924
- if (consumer.spec === version) {
2604
+ const where = join4(settings.root, consumer.dir);
2605
+ if (consumer.kind === "peerDependencies") {
2606
+ skipped.push(consumer.dir);
2607
+ note(consumer.dir, `peer range ${consumer.spec}, left as it is`);
2608
+ } else if (consumer.spec === version) {
2609
+ skipped.push(consumer.dir);
1925
2610
  note(consumer.dir, `already on ${version}`);
1926
2611
  } else if (consumer.manager !== "bun") {
2612
+ skipped.push(consumer.dir);
1927
2613
  warn(consumer.dir, `${consumer.spec}, its lockfile pins ${consumer.manager}: put ${version} in by hand`);
1928
2614
  } else if (settings.isDryRun === true) {
1929
- note(consumer.dir, `would run ${argv.join(" ")}`);
2615
+ note(consumer.dir, `would run ${[argv.join(" "), ...settings.isAll === true ? ["check", `gc 'chore(deps): bump ${name} to ${version}'`] : []].join(", then ")}`);
2616
+ } else if (settings.isAll === true && await hasUncommittedChanges(where, settings.root, run)) {
2617
+ isClean = false;
2618
+ failing.push(consumer.dir);
2619
+ problem(consumer.dir, "package.json or its lockfile has uncommitted changes, left alone so that gc does not commit them");
1930
2620
  } else {
1931
2621
  const ran = await run(argv, where);
1932
- if (ran.code === 0) {
1933
- ok(consumer.dir, `${consumer.spec} -> ${version}`);
1934
- } else {
2622
+ if (ran.code !== 0) {
1935
2623
  isClean = false;
2624
+ failing.push(consumer.dir);
1936
2625
  problem(consumer.dir, `${argv.join(" ")} exited ${ran.code}: ${ran.text.trim().split(`
1937
2626
  `).at(-1) ?? ""}`);
2627
+ } else if (settings.isAll !== true) {
2628
+ ok(consumer.dir, `${consumer.spec} -> ${version}`);
2629
+ } else {
2630
+ const failure = await checkAndCommit(name, version, where, settings.root, run);
2631
+ if (failure === undefined) {
2632
+ committed.push(consumer.dir);
2633
+ ok(consumer.dir, `${consumer.spec} -> ${version}, checked and committed`);
2634
+ } else {
2635
+ isClean = false;
2636
+ failing.push(consumer.dir);
2637
+ problem(consumer.dir, `${consumer.spec} -> ${version}, left uncommitted: ${failure}`);
2638
+ }
1938
2639
  }
1939
2640
  }
1940
2641
  }
2642
+ if (settings.isAll === true && settings.isDryRun !== true)
2643
+ note("summary", `${committed.length} bumped and committed, ${failing.length} bumped and failing${failing.length > 0 ? ` (${failing.join(", ")})` : ""}, ${skipped.length + local} skipped`);
1941
2644
  return isClean;
1942
2645
  }
1943
2646
  async function bump(args) {
1944
- const { values, positionals: [name, asked] } = parse(args, usage2, options2, { positionals: ["<name>", "[version]"] });
2647
+ const { values, positionals: [name, asked] } = parse(args, usage3, options3, { positionals: ["<name>", "[version]"] });
1945
2648
  const settings = {
1946
- root: values.root ?? join3(homedir2(), "Workspace"),
2649
+ root: values.root ?? join4(homedir2(), "Workspace"),
1947
2650
  configs: await loadConfigs(),
1948
2651
  timeout: duration(values.timeout, "timeout", 10 * 60000),
1949
- isDryRun: values["dry-run"] === true
2652
+ isDryRun: values["dry-run"] === true,
2653
+ isAll: values["all-consumers"] === true,
2654
+ isForced: values.force === true,
2655
+ pkgSettings: await loadSettings(),
2656
+ minAgeDays: wholeNumber(values["min-age"], "min-age", 0, 0)
1950
2657
  };
1951
2658
  const version = await servedVersion(name, asked, settings);
2659
+ if (!await vet(name, version, settings))
2660
+ return 1;
1952
2661
  return await bumpConsumers(name, version, settings) ? 0 : 1;
1953
2662
  }
1954
- var usage2 = `pkg bump <name> [version]
2663
+ var usage3 = `pkg bump <name> [version]
1955
2664
 
1956
- 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
- 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;
2665
+ Waits until the registry serves the version (the latest when none is given, an exact version otherwise), vets it as \`pkg check\` does (advisories, licence allowlist, and a minimum age only when --min-age asks for one), 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.
2666
+ A version the vetting blocks is refused before any folder is touched, unless --force is given.
2667
+ A dependency that is not a registry version (workspace:, link:, file:, git, a URL, an npm: alias, catalog:), a peer range, a folder already on the version and one whose lockfile pins another manager are left as they are, each said so.
2668
+ With --all-consumers a folder whose package.json or lockfile already has uncommitted changes is left alone, and 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.
2669
+
2670
+ 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 the vetting refuses the version, an install, a check or a commit fails or the registry has no such package, 2 when the version never shows within the timeout (a registry that cannot be reached is retried until then) or on a usage error.`, ROOT_OPTION, options3, POLL = 5000;
1959
2671
  var init_bump = __esm(() => {
1960
2672
  init_src();
1961
2673
  init_progress();
1962
2674
  init_config2();
1963
2675
  init_consumers();
1964
2676
  init_registry();
2677
+ init_semver();
2678
+ init_check();
1965
2679
  ROOT_OPTION = { type: "string", description: "the folder holding the repositories (~/Workspace)" };
1966
- options2 = {
2680
+ options3 = {
1967
2681
  root: ROOT_OPTION,
1968
2682
  timeout: { type: "string", short: "t", description: "how long to wait for the registry (10m): 30s, 5m, 1h" },
1969
- "dry-run": { type: "boolean", description: "print what would run, changing nothing" }
2683
+ "all-consumers": { type: "boolean", description: "after each install run the folder's check and commit it through gc when it passes" },
2684
+ "dry-run": { type: "boolean", description: "print what would run, changing nothing" },
2685
+ force: { type: "boolean", description: "bump even when the version is blocked by the vetting" },
2686
+ "min-age": { type: "string", description: "days a version must be old (0: a bump adopts a release on purpose, often one just made)" }
1970
2687
  };
1971
2688
  });
1972
2689
 
@@ -1976,46 +2693,53 @@ __export(exports_consumers, {
1976
2693
  consumers: () => consumers
1977
2694
  });
1978
2695
  import { homedir as homedir3 } from "os";
1979
- import { join as join4 } from "path";
2696
+ import { join as join5 } from "path";
1980
2697
  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);
2698
+ const { values, positionals: [name] } = parse(args, usage4, options4, { positionals: ["<name>"] });
2699
+ const root = values.root ?? join5(homedir3(), "Workspace");
2700
+ const { shown, footer } = capped(findConsumers(name, root), limitOf(values, CONSUMERS_LIMIT));
1984
2701
  if (values.json === true)
1985
- printJson(found);
1986
- else if (found.length === 0)
2702
+ printJson(shown);
2703
+ else if (shown.length === 0)
1987
2704
  console.log(`nothing under ${root} depends on ${name}`);
1988
2705
  else
1989
- console.log(table([["FOLDER", "KIND", "VERSION", "MANAGER"], ...found.map((each) => [each.dir, each.kind, each.spec, each.manager])], { header: true }));
2706
+ console.log([table([["FOLDER", "KIND", "VERSION", "MANAGER"], ...shown.map((each) => [each.dir, each.kind, each.spec, each.manager])], { header: true }), ...footer === undefined ? [] : [footer]].join(`
2707
+ `));
1990
2708
  return 0;
1991
2709
  }
1992
- var usage3 = `pkg consumers <name>
2710
+ var usage4 = `pkg consumers <name>
1993
2711
 
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;
2712
+ 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).
2713
+
2714
+ Exits 0 when it listed them, 2 on a usage error.`, CONSUMERS_LIMIT = 30, options4;
1995
2715
  var init_consumers2 = __esm(() => {
1996
2716
  init_src();
1997
2717
  init_consumers();
1998
2718
  init_bump();
1999
- options3 = {
2719
+ options4 = {
2000
2720
  root: ROOT_OPTION,
2721
+ ...limitOptions(CONSUMERS_LIMIT),
2001
2722
  json: jsonOption("[{dir, path, kind, spec, manager}]")
2002
2723
  };
2003
2724
  });
2004
2725
 
2005
2726
  // src/cli.ts
2006
2727
  init_src();
2007
- import process9 from "process";
2728
+ import process10 from "process";
2008
2729
  // package.json
2009
- var version = "0.3.0";
2730
+ var version = "0.5.0";
2010
2731
 
2011
2732
  // src/cli.ts
2012
- process9.exitCode = await runCli({
2733
+ process10.exitCode = await runCli({
2013
2734
  name: "pkg",
2014
2735
  version,
2015
2736
  about: "checks npm packages before they are added, and moves a lib's consumers onto a new release",
2737
+ replaces: "bun add on trust, and a bun info and sleep loop then a bun add per consumer",
2738
+ 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
2739
  defaultCommand: "check",
2017
2740
  commands: {
2018
2741
  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) },
2742
+ 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
2743
  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
2744
  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
2745
  }