@kaynooo/pkg-tools 0.4.0 → 0.6.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 (3) hide show
  1. package/README.md +10 -8
  2. package/dist/cli.js +699 -317
  3. package/package.json +1 -1
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";
@@ -473,10 +496,10 @@ function optionRows(options, colors = paint(process3.stdout)) {
473
496
  const rows = Object.entries(options).map(([name, option]) => {
474
497
  const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
475
498
  const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
476
- 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 ?? "" };
477
500
  });
478
501
  const width = Math.max(0, ...rows.map((row) => row.width));
479
- 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)}`);
480
503
  }
481
504
  function applyOutput(color, plain, env = process3.env) {
482
505
  const when = plain === true ? "never" : color ?? "auto";
@@ -504,7 +527,8 @@ function parse(args, usage, options, settings = {}) {
504
527
  try {
505
528
  parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
506
529
  } catch (error) {
507
- badUsage(parseError(error));
530
+ const message = parseError(error);
531
+ throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
508
532
  }
509
533
  const values = parsed.values;
510
534
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
@@ -547,12 +571,27 @@ function parseError(error) {
547
571
  const message = upTo(error instanceof Error ? error.message : String(error), ". ");
548
572
  return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
549
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
+ }
550
589
  function wholeNumber(value, name, fallback, min = 1) {
551
590
  if (value === undefined)
552
591
  return fallback;
553
592
  const number = Number(value);
554
- if (!Number.isInteger(number) || number < min)
555
- 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}`);
556
595
  return number;
557
596
  }
558
597
  function duration(value, name, fallback, unit = "s") {
@@ -564,10 +603,11 @@ function duration(value, name, fallback, unit = "s") {
564
603
  badUsage(`--${name} takes a duration such as 30s, 5m or 1h, not ${value}`);
565
604
  return ms;
566
605
  }
567
- var Exit, UsageError, HelpShown, completion, OPTIONAL, USAGE_PREFIX, FULL_STOP, 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;
568
607
  var init_args = __esm(() => {
569
608
  init_format();
570
609
  init_style();
610
+ init_text();
571
611
  Exit = class Exit extends Error {
572
612
  code;
573
613
  hint;
@@ -578,8 +618,8 @@ var init_args = __esm(() => {
578
618
  }
579
619
  };
580
620
  UsageError = class UsageError extends Exit {
581
- constructor(message) {
582
- super(message, 2);
621
+ constructor(message, hint) {
622
+ super(message, 2, hint);
583
623
  }
584
624
  };
585
625
  HelpShown = class HelpShown extends Exit {
@@ -591,10 +631,13 @@ var init_args = __esm(() => {
591
631
  OPTIONAL = /\[[^\]]+\]/g;
592
632
  USAGE_PREFIX = /^usage:\s*/i;
593
633
  FULL_STOP = /\.$/;
634
+ DASHES = /^-+/;
594
635
  OUTPUT_OPTIONS = {
595
636
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
596
637
  plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
597
638
  };
639
+ UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
640
+ WHOLE = /^\d+$/;
598
641
  DURATION = /^(\d+(?:\.\d+)?)(ms|[smh])?$/;
599
642
  UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000 };
600
643
  });
@@ -605,31 +648,265 @@ var init_chart = __esm(() => {
605
648
  init_style();
606
649
  });
607
650
 
608
- // ../kit/src/text.ts
609
- function distance(a, b) {
610
- let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
611
- for (const [i, left] of [...a].entries()) {
612
- const current = [i + 1];
613
- for (const [j, right] of [...b].entries())
614
- current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
615
- 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);
616
687
  }
617
- return previous[b.length] ?? 0;
688
+ for (const signal of SIGNALS)
689
+ process4.on(signal, onSignal);
690
+ return held;
618
691
  }
619
- var ANSI;
620
- var init_text = __esm(() => {
621
- 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();
622
898
  });
623
899
 
624
900
  // ../kit/src/cli.ts
625
- import process4 from "process";
626
- function commandRows(commands, colors) {
901
+ import process6 from "process";
902
+ async function commandRows(commands, colors) {
627
903
  const width = Math.max(...Object.keys(commands).map((name) => name.length));
628
904
  const groups = new Map([["Commands:", []]]);
629
- 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()) {
630
907
  const heading = group === undefined ? "Commands:" : `${group}:`;
631
908
  const rows = groups.get(heading) ?? [];
632
- rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summary)}`);
909
+ rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
633
910
  groups.set(heading, rows);
634
911
  }
635
912
  return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
@@ -649,28 +926,38 @@ function takeOutputFlags(argv) {
649
926
  }
650
927
  return argv.slice(index);
651
928
  }
652
- function closest(name, names) {
653
- const started = names.find((known) => known.startsWith(name));
654
- if (started !== undefined)
655
- return started;
656
- const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
657
- 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 };
934
+ }
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: [] };
658
944
  }
659
- async function help({ path, header, summary, commands, defaultCommand, examples, replaces }, stream) {
945
+ async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
660
946
  const colors = paint(stream);
661
947
  const prefix = path.join(" ");
948
+ const summary = await resolveSummary(lazySummary);
662
949
  if (wantsHelpJson())
663
950
  return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
664
951
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
665
952
  const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
666
953
  const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
667
954
  return [
668
- header(colors),
955
+ header(colors, summary),
669
956
  "",
670
957
  usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
671
958
  ...replacesRows(replaces, colors),
672
959
  "",
673
- ...commandRows(commands, colors),
960
+ ...await commandRows(commands, colors),
674
961
  ...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
675
962
  "",
676
963
  colors.bold("Options:"),
@@ -682,8 +969,8 @@ async function help({ path, header, summary, commands, defaultCommand, examples,
682
969
  `);
683
970
  }
684
971
  function printError(prefix, message, hint, cause) {
685
- const colors = paint(process4.stderr);
686
- const styled = takesControlCodes(process4.stderr);
972
+ const colors = paint(process6.stderr);
973
+ const styled = takesControlCodes(process6.stderr);
687
974
  const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
688
975
  const under = (label, text, tint) => text === undefined ? "" : `
689
976
  ${styled ? tint(label) : `${label}:`} ${text}`;
@@ -716,11 +1003,11 @@ async function dispatch(level, typed, { version, redact }) {
716
1003
  }
717
1004
  const [name, ...args] = argv;
718
1005
  if (name === undefined && level.path.length > 1) {
719
- console.error(await help(level, process4.stderr));
1006
+ console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process6.stderr));
720
1007
  return 2;
721
1008
  }
722
1009
  if (name === undefined || name === "help" || name === "--help" || name === "-h") {
723
- console.log(await help(level, process4.stdout));
1010
+ console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process6.stdout));
724
1011
  return 0;
725
1012
  }
726
1013
  if (level.path.length === 1 && (name === "--version" || name === "-v")) {
@@ -729,6 +1016,10 @@ async function dispatch(level, typed, { version, redact }) {
729
1016
  }
730
1017
  let command = level.commands[level.aliases?.[name] ?? name];
731
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
+ }
732
1023
  if (command === undefined && level.defaultCommand !== undefined) {
733
1024
  command = level.commands[level.defaultCommand];
734
1025
  rest = argv;
@@ -740,7 +1031,7 @@ async function dispatch(level, typed, { version, redact }) {
740
1031
  }
741
1032
  if ("commands" in command) {
742
1033
  const { summary } = command;
743
- return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${summary}`, summary, commands: command.commands, aliases: command.aliases, defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
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 });
744
1035
  }
745
1036
  try {
746
1037
  return await command.run(rest);
@@ -763,28 +1054,34 @@ async function optionsOf(run) {
763
1054
  async function leafCandidates(run, words) {
764
1055
  if (!(words.at(-1) ?? "").startsWith("-"))
765
1056
  return [FILES];
766
- 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}`);
767
1058
  }
768
- async function candidates(commands, aliases, defaultCommand, words) {
1059
+ async function candidates(scope, defaultCommand, words, before = []) {
769
1060
  const [word = "", ...rest] = words;
770
- const command = commands[aliases?.[word] ?? word];
771
- if (rest.length === 0 && !word.startsWith("-"))
772
- 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)}`));
773
1070
  if (command === undefined) {
774
1071
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
775
1072
  return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
776
1073
  }
777
- 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);
778
1075
  }
779
1076
  async function printCandidates(found) {
780
1077
  for (const line of await found)
781
1078
  console.log(line);
782
1079
  return 0;
783
1080
  }
784
- async function runCli(cli, argv = process4.argv.slice(2)) {
1081
+ async function runCli(cli, argv = process6.argv.slice(2)) {
785
1082
  if (argv[0] === COMPLETE)
786
- return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
787
- return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${cli.about}`, summary: cli.about, commands: cli.commands, aliases: cli.aliases, defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
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 });
788
1085
  }
789
1086
  var LEVEL_OPTIONS, TOP_OPTIONS, COMPLETE = "__complete", FILES = "__files__";
790
1087
  var init_cli = __esm(() => {
@@ -803,6 +1100,12 @@ var init_config = __esm(() => {
803
1100
  init_args();
804
1101
  });
805
1102
 
1103
+ // ../kit/src/git.ts
1104
+ var init_git = __esm(() => {
1105
+ init_args();
1106
+ init_tool();
1107
+ });
1108
+
806
1109
  // ../kit/src/limit.ts
807
1110
  function limitOptions(fallback) {
808
1111
  return {
@@ -828,114 +1131,105 @@ var init_limit = __esm(() => {
828
1131
  init_args();
829
1132
  });
830
1133
 
831
- // ../kit/src/process.ts
832
- import { constants } from "os";
833
- import process5 from "process";
834
- async function collect(stream, name, chunks) {
835
- for await (const data of stream)
836
- chunks.push({ stream: name, data });
837
- }
838
- function exitCodeOf(code, signal) {
839
- if (code !== null)
840
- return code;
841
- if (typeof signal === "number")
842
- return 128 + signal;
843
- return signal === null ? 129 : 128 + constants.signals[signal];
844
- }
845
- function holdSignals(cleanup) {
846
- let received;
847
- let child;
848
- const held = {
849
- get signal() {
850
- return received;
851
- },
852
- forward: (to) => {
853
- child = to;
854
- },
855
- release: () => {
856
- for (const signal of SIGNALS)
857
- process5.off(signal, onSignal);
858
- }
859
- };
860
- function onSignal(signal) {
861
- received = signal;
862
- if (child !== undefined) {
863
- child.kill(signal);
864
- return;
865
- }
866
- if (cleanup === undefined)
867
- return;
868
- held.release();
869
- cleanup(signal);
870
- process5.kill(process5.pid, signal);
871
- }
872
- for (const signal of SIGNALS)
873
- process5.on(signal, onSignal);
874
- return held;
875
- }
876
- async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
877
- const chunks = [];
878
- const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe" });
879
- await Promise.all([collect(proc.stdout, "stdout", chunks), collect(proc.stderr, "stderr", chunks)]);
880
- await proc.exited;
881
- const output = Buffer.concat(chunks.map(({ data }) => data));
882
- return { code: exitCodeOf(proc.exitCode, proc.signalCode), chunks, output, text: output.toString() };
883
- }
884
- var SIGNALS;
885
- var init_process = __esm(() => {
886
- SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
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"]);
1144
+ });
1145
+
1146
+ // ../kit/src/sessions.ts
1147
+ var {Glob } = globalThis.Bun;
1148
+ var SESSION_FILES, ACCOUNT_DIRS;
1149
+ var init_sessions = __esm(() => {
1150
+ SESSION_FILES = new Glob("*.json");
1151
+ ACCOUNT_DIRS = new Glob("*/sessions");
887
1152
  });
888
1153
 
889
1154
  // ../kit/src/index.ts
890
1155
  var init_src = __esm(() => {
891
1156
  init_args();
892
1157
  init_chart();
1158
+ init_ci();
893
1159
  init_cli();
894
1160
  init_config();
895
1161
  init_format();
1162
+ init_git();
896
1163
  init_limit();
1164
+ init_logs();
897
1165
  init_process();
1166
+ init_secrets();
1167
+ init_sessions();
898
1168
  init_style();
899
1169
  init_text();
1170
+ init_tool();
900
1171
  });
901
1172
 
902
1173
  // src/registry.ts
903
1174
  function isPublic(registry) {
904
1175
  return registry.url.replace(TRAILING_SLASHES, "") === PUBLIC_REGISTRY;
905
1176
  }
1177
+ function unreachable(message) {
1178
+ return fail(`could not check: ${message}`, 2);
1179
+ }
1180
+ function requestInit(token) {
1181
+ return { headers: { accept: "application/json", ...token === undefined ? {} : { authorization: `Bearer ${token}` } }, signal: AbortSignal.timeout(REQUEST_TIMEOUT) };
1182
+ }
906
1183
  async function get(fetcher, url, token) {
907
1184
  try {
908
- return await fetcher(url, { headers: { accept: "application/json", ...token === undefined ? {} : { authorization: `Bearer ${token}` } } });
1185
+ return await fetcher(url, requestInit(token));
909
1186
  } catch (error) {
910
- return fail(`cannot reach ${new URL(url).host}: ${error instanceof Error ? error.message : String(error)}`, 1);
1187
+ return unreachable(`cannot reach ${new URL(url).host}: ${error instanceof Error ? error.message : String(error)}`);
911
1188
  }
912
1189
  }
1190
+ function isValidName(name) {
1191
+ return PACKAGE_NAME.test(name) && name.length <= 214;
1192
+ }
913
1193
  async function fetchPackument(name, registry, fetcher = fetch) {
914
- const response = await get(fetcher, `${registry.url.replace(TRAILING_SLASHES, "")}/${name.replace("/", "%2f")}`, registry.token);
1194
+ if (!isValidName(name))
1195
+ fail(`${name} is not a valid npm package name`);
1196
+ const response = await get(fetcher, `${registry.url.replace(TRAILING_SLASHES, "")}/${encodeURIComponent(name).replace("%40", "@").replace("%2F", "%2f")}`, registry.token);
915
1197
  if (response.status === 404)
916
1198
  return;
917
1199
  if (!response.ok)
918
- fail(`${new URL(registry.url).host} answered ${response.status} for ${name}`, 1);
919
- return await response.json();
1200
+ unreachable(`${new URL(registry.url).host} answered ${response.status} for ${name}`);
1201
+ let packument;
1202
+ try {
1203
+ packument = await response.json();
1204
+ } catch {
1205
+ return unreachable(`${new URL(registry.url).host} answered ${name} with something that is not JSON`);
1206
+ }
1207
+ if (packument?.name !== name)
1208
+ unreachable(`${new URL(registry.url).host} answered ${name} with the metadata of ${String(packument?.name)}`);
1209
+ return packument;
920
1210
  }
921
1211
  async function fetchDownloads(name, fetcher = fetch) {
922
- const response = await get(fetcher, `${DOWNLOADS_API}/${name}`);
1212
+ const response = await get(fetcher, `${DOWNLOADS_API}/${encodeURIComponent(name)}`);
923
1213
  if (!response.ok)
924
1214
  return;
925
- const body = await response.json();
926
- return body.downloads;
1215
+ try {
1216
+ return (await response.json()).downloads;
1217
+ } catch {
1218
+ return;
1219
+ }
927
1220
  }
928
- var PUBLIC_REGISTRY = "https://registry.npmjs.org", DOWNLOADS_API = "https://api.npmjs.org/downloads/point/last-week", TRAILING_SLASHES;
1221
+ var PUBLIC_REGISTRY = "https://registry.npmjs.org", DOWNLOADS_API = "https://api.npmjs.org/downloads/point/last-week", TRAILING_SLASHES, REQUEST_TIMEOUT = 1e4, PACKAGE_NAME;
929
1222
  var init_registry = __esm(() => {
930
1223
  init_src();
931
1224
  TRAILING_SLASHES = /\/+$/;
1225
+ PACKAGE_NAME = /^(?:@[\w~-][\w.~-]*\/)?[\w~-][\w.~-]*$/;
932
1226
  });
933
1227
 
934
1228
  // src/config.ts
935
1229
  import { homedir } from "os";
936
1230
  import { dirname, join } from "path";
937
- import process7 from "process";
938
- function expand(value, env) {
1231
+ import process8 from "process";
1232
+ function expand2(value, env) {
939
1233
  let missing = false;
940
1234
  const expanded = value.replace(ENV_REFERENCE, (_, braced, bare) => {
941
1235
  const found = env[braced ?? bare ?? ""];
@@ -944,6 +1238,9 @@ function expand(value, env) {
944
1238
  });
945
1239
  return missing ? undefined : expanded;
946
1240
  }
1241
+ function literal(value) {
1242
+ return expand2(value, {}) === value ? value : undefined;
1243
+ }
947
1244
  function parseNpmrc(text, env) {
948
1245
  const config = { scopes: {}, tokens: {}, scopeTokens: {} };
949
1246
  for (const line of text.split(`
@@ -952,11 +1249,12 @@ function parseNpmrc(text, env) {
952
1249
  if (at === -1 || COMMENT.test(line))
953
1250
  continue;
954
1251
  const key = line.slice(0, at).trim();
955
- const value = expand(line.slice(at + 1).trim().replace(QUOTES, "$2"), env);
956
- if (value === undefined || value === "")
957
- continue;
958
1252
  const scope = SCOPE_REGISTRY.exec(key)?.[1];
959
1253
  const tokenFor = AUTH_TOKEN.exec(key)?.[1];
1254
+ const raw = line.slice(at + 1).trim().replace(QUOTES, "$2");
1255
+ const value = tokenFor === undefined ? literal(raw) : expand2(raw, env);
1256
+ if (value === undefined || value === "")
1257
+ continue;
960
1258
  if (scope !== undefined)
961
1259
  config.scopes[scope] = value;
962
1260
  else if (tokenFor !== undefined)
@@ -968,8 +1266,8 @@ function parseNpmrc(text, env) {
968
1266
  }
969
1267
  function bunEntry(entry, env) {
970
1268
  const url = typeof entry === "string" ? entry : entry.url;
971
- const token = typeof entry === "string" || entry.token === undefined ? undefined : expand(entry.token, env);
972
- return { ...url === undefined ? {} : { url: expand(url, env) ?? url }, ...token === undefined ? {} : { token } };
1269
+ const token = typeof entry === "string" || entry.token === undefined ? undefined : expand2(entry.token, env);
1270
+ return { ...url === undefined || literal(url) === undefined ? {} : { url }, ...token === undefined ? {} : { token } };
973
1271
  }
974
1272
  function parseBunfig(text, env) {
975
1273
  const config = { scopes: {}, tokens: {}, scopeTokens: {} };
@@ -991,26 +1289,35 @@ function parseBunfig(text, env) {
991
1289
  return config;
992
1290
  }
993
1291
  function tokenFor(config, url) {
994
- const address = url.replace(PROTOCOL, "");
995
- const best = Object.keys(config.tokens).filter((key) => address.startsWith(key)).sort((a, b) => b.length - a.length)[0];
1292
+ const address = `${url.replace(PROTOCOL, "").replace(TRAILING_SLASHES2, "")}/`;
1293
+ const best = Object.keys(config.tokens).filter((key) => address.startsWith(`${key.replace(TRAILING_SLASHES2, "")}/`)).sort((a, b) => b.length - a.length)[0];
996
1294
  return best === undefined ? undefined : config.tokens[best];
997
1295
  }
1296
+ function mayCarryToken(url) {
1297
+ const { protocol, hostname } = new URL(url);
1298
+ return protocol === "https:" || LOCAL_HOSTS.has(hostname);
1299
+ }
998
1300
  function registryFor(name, configs) {
999
1301
  const scope = name.startsWith("@") ? name.slice(0, name.indexOf("/")) : undefined;
1000
- for (const config of configs) {
1001
- const url = scope === undefined ? config.registry : config.scopes[scope] ?? config.registry;
1002
- if (url === undefined)
1003
- continue;
1004
- const token = (scope === undefined ? undefined : config.scopeTokens[scope]) ?? tokenFor(config, url) ?? configs.map((other) => tokenFor(other, url)).find((found) => found !== undefined);
1005
- return { url, token };
1302
+ const scoped = scope === undefined ? undefined : configs.find((config) => config.scopes[scope] !== undefined);
1303
+ const url = (scope === undefined ? undefined : scoped?.scopes[scope]) ?? configs.find((config) => config.registry !== undefined)?.registry;
1304
+ if (url === undefined)
1305
+ return { url: PUBLIC_REGISTRY };
1306
+ const token = (scope === undefined ? undefined : scoped?.scopeTokens[scope]) ?? configs.map((other) => tokenFor(other, url)).find((found) => found !== undefined);
1307
+ return { url, token: mayCarryToken(url) ? token : undefined };
1308
+ }
1309
+ function readable(path, parse) {
1310
+ try {
1311
+ return parse();
1312
+ } catch (error) {
1313
+ return unreachable(`${path} cannot be read: ${error instanceof Error ? error.message : String(error)}`);
1006
1314
  }
1007
- return { url: PUBLIC_REGISTRY };
1008
1315
  }
1009
1316
  async function readIfThere(path) {
1010
1317
  const file = Bun.file(path);
1011
1318
  return await file.exists() ? file.text() : undefined;
1012
1319
  }
1013
- async function loadConfigs(cwd = process7.cwd(), env = process7.env, home = homedir()) {
1320
+ async function loadConfigs(cwd = process8.cwd(), env = process8.env, home = homedir()) {
1014
1321
  const directories = [];
1015
1322
  for (let dir = cwd;!directories.includes(dir); dir = dirname(dir))
1016
1323
  directories.push(dir);
@@ -1021,7 +1328,7 @@ async function loadConfigs(cwd = process7.cwd(), env = process7.env, home = home
1021
1328
  const bunfig = await readIfThere(join(dir, "bunfig.toml"));
1022
1329
  const npmrc = await readIfThere(join(dir, ".npmrc"));
1023
1330
  if (bunfig !== undefined)
1024
- configs.push(parseBunfig(bunfig, env));
1331
+ configs.push(readable(join(dir, "bunfig.toml"), () => parseBunfig(bunfig, env)));
1025
1332
  if (npmrc !== undefined)
1026
1333
  configs.push(parseNpmrc(npmrc, env));
1027
1334
  }
@@ -1039,7 +1346,7 @@ function parseSettings(text) {
1039
1346
  settings.blockSeverity = raw.blockSeverity;
1040
1347
  return settings;
1041
1348
  }
1042
- async function loadSettings(cwd = process7.cwd(), home = homedir()) {
1349
+ async function loadSettings(cwd = process8.cwd(), home = homedir()) {
1043
1350
  const files = [];
1044
1351
  for (let dir = cwd;!files.includes(join(dir, SETTINGS_FILE)); dir = dirname(dir))
1045
1352
  files.push(join(dir, SETTINGS_FILE));
@@ -1049,13 +1356,13 @@ async function loadSettings(cwd = process7.cwd(), home = homedir()) {
1049
1356
  const text = await readIfThere(file);
1050
1357
  if (text === undefined)
1051
1358
  continue;
1052
- const found = parseSettings(text);
1359
+ const found = readable(file, () => parseSettings(text));
1053
1360
  settings.licenses ??= found.licenses;
1054
1361
  settings.blockSeverity ??= found.blockSeverity;
1055
1362
  }
1056
1363
  return settings;
1057
1364
  }
1058
- var ENV_REFERENCE, COMMENT, SCOPE_REGISTRY, AUTH_TOKEN, PROTOCOL, QUOTES, SEVERITIES, SETTINGS_FILE = ".pkgrc.json";
1365
+ var ENV_REFERENCE, COMMENT, SCOPE_REGISTRY, AUTH_TOKEN, PROTOCOL, TRAILING_SLASHES2, LOCAL_HOSTS, QUOTES, SEVERITIES, DEFAULT_BLOCK_SEVERITY = "high", SETTINGS_FILE = ".pkgrc.json";
1059
1366
  var init_config2 = __esm(() => {
1060
1367
  init_registry();
1061
1368
  ENV_REFERENCE = /\$\{(\w+)\}|\$(\w+)/g;
@@ -1063,35 +1370,89 @@ var init_config2 = __esm(() => {
1063
1370
  SCOPE_REGISTRY = /^(@[^:]+):registry$/;
1064
1371
  AUTH_TOKEN = /^(\/\/.+?):_authToken$/;
1065
1372
  PROTOCOL = /^\w+:/;
1373
+ TRAILING_SLASHES2 = /\/+$/;
1374
+ LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
1066
1375
  QUOTES = /^(["'])(.*)\1$/;
1067
1376
  SEVERITIES = ["low", "moderate", "high", "critical"];
1068
1377
  });
1069
1378
 
1379
+ // src/semver.ts
1380
+ function isRange(text) {
1381
+ if (text.trim() === "")
1382
+ return true;
1383
+ return text.split(OR).every((alternative) => {
1384
+ const words = alternative.trim().split(WHITESPACE);
1385
+ return words.every((word, index) => word === "-" ? index > 0 && index < words.length - 1 : COMPARATOR.test(word));
1386
+ });
1387
+ }
1388
+ function compare(version, word) {
1389
+ const bound = BOUND.exec(word);
1390
+ if (bound === null)
1391
+ return;
1392
+ const order = Bun.semver.order(version, bound[2] ?? "");
1393
+ const operator = bound[1] ?? "=";
1394
+ if (operator === "<")
1395
+ return order < 0;
1396
+ if (operator === "<=")
1397
+ return order <= 0;
1398
+ if (operator === ">")
1399
+ return order > 0;
1400
+ return operator === ">=" ? order >= 0 : order === 0;
1401
+ }
1402
+ function inRange(version, range) {
1403
+ if (Bun.semver.satisfies(version, range))
1404
+ return true;
1405
+ if (!PRERELEASE.test(version))
1406
+ return false;
1407
+ return range.split(OR).some((alternative) => alternative.trim().split(WHITESPACE).every((word) => compare(version, word) ?? Bun.semver.satisfies(version, word)));
1408
+ }
1409
+ var OR, WHITESPACE, PRERELEASE, PART, COMPARATOR, BOUND, EXACT_VERSION;
1410
+ var init_semver = __esm(() => {
1411
+ OR = /\s*\|\|\s*/;
1412
+ WHITESPACE = /\s+/;
1413
+ PRERELEASE = /-/;
1414
+ PART = String.raw`(?:\d+|[xX*])`;
1415
+ COMPARATOR = new RegExp(String.raw`^(?:[<>]=?|=|\^|~>?)?v?${PART}(?:\.${PART}){0,2}(?:-[\w.-]+)?(?:\+[\w.-]+)?$`);
1416
+ BOUND = /^(<=|>=|[<>=])?v?(\d+\.\d+\.\d+(?:-[\w.-]+)?)$/;
1417
+ EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
1418
+ });
1419
+
1070
1420
  // src/advisories.ts
1071
1421
  function severityRank(severity) {
1072
1422
  return SEVERITIES.indexOf(severity);
1073
1423
  }
1074
1424
  function fixedVersion(vulnerable, version) {
1075
- const range = vulnerable.split("||").map((each) => each.trim()).find((each) => Bun.semver.satisfies(version, each));
1425
+ const range = vulnerable.split("||").map((each) => each.trim()).find((each) => inRange(version, each));
1076
1426
  return range === undefined ? undefined : UPPER_BOUND.exec(range)?.[1];
1077
1427
  }
1078
1428
  async function fetchAdvisories(versions, registry, fetcher = fetch) {
1079
- const url = `${registry.url.replace(TRAILING_SLASHES2, "")}${BULK_PATH}`;
1429
+ const url = `${registry.url.replace(TRAILING_SLASHES3, "")}${BULK_PATH}`;
1430
+ const host = new URL(registry.url).host;
1080
1431
  let response;
1081
1432
  try {
1082
1433
  response = await fetcher(url, {
1083
1434
  method: "POST",
1084
1435
  headers: { accept: "application/json", "content-type": "application/json", ...registry.token === undefined ? {} : { authorization: `Bearer ${registry.token}` } },
1085
- body: JSON.stringify(versions)
1436
+ body: JSON.stringify(versions),
1437
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT2)
1086
1438
  });
1087
- } catch {
1088
- return {};
1439
+ } catch (error) {
1440
+ return { found: {}, unavailable: `cannot reach ${host} for advisories: ${error instanceof Error ? error.message : String(error)}` };
1089
1441
  }
1442
+ if (NO_ENDPOINT.has(response.status))
1443
+ return { found: {}, isUnsupported: true, unavailable: `${host} has no advisory endpoint` };
1090
1444
  if (!response.ok)
1091
- return {};
1092
- const body = await response.json();
1445
+ return { found: {}, unavailable: `${host} answered ${response.status} for advisories` };
1446
+ let body;
1447
+ try {
1448
+ body = await response.json();
1449
+ } catch {
1450
+ return { found: {}, unavailable: `${host} answered advisories with something that is not JSON` };
1451
+ }
1093
1452
  const found = {};
1094
1453
  for (const [name, raws] of Object.entries(body)) {
1454
+ if (!Array.isArray(raws))
1455
+ continue;
1095
1456
  found[name] = raws.filter((raw) => SEVERITIES.includes(raw.severity)).map((raw) => ({
1096
1457
  id: raw.id,
1097
1458
  ghsa: GHSA.exec(raw.url)?.[0],
@@ -1101,20 +1462,22 @@ async function fetchAdvisories(versions, registry, fetcher = fetch) {
1101
1462
  vulnerable: raw.vulnerable_versions
1102
1463
  }));
1103
1464
  }
1104
- return found;
1465
+ return { found };
1105
1466
  }
1106
1467
  function advisoriesFor(advisories, version) {
1107
- return (advisories ?? []).filter((each) => Bun.semver.satisfies(version, each.vulnerable)).map((each) => ({ ...each, fixed: fixedVersion(each.vulnerable, version) }));
1468
+ return (advisories ?? []).filter((each) => inRange(version, each.vulnerable)).map((each) => ({ ...each, fixed: fixedVersion(each.vulnerable, version) }));
1108
1469
  }
1109
1470
  function filterAdvisories(advisories, level, ignore = []) {
1110
1471
  return advisories.filter((each) => severityRank(each.severity) >= severityRank(level) && !ignore.includes(each.ghsa ?? "") && !ignore.includes(String(each.id)));
1111
1472
  }
1112
- var BULK_PATH = "/-/npm/v1/security/advisories/bulk", TRAILING_SLASHES2, GHSA, UPPER_BOUND;
1473
+ var BULK_PATH = "/-/npm/v1/security/advisories/bulk", TRAILING_SLASHES3, REQUEST_TIMEOUT2 = 1e4, GHSA, UPPER_BOUND, NO_ENDPOINT;
1113
1474
  var init_advisories = __esm(() => {
1114
1475
  init_config2();
1115
- TRAILING_SLASHES2 = /\/+$/;
1476
+ init_semver();
1477
+ TRAILING_SLASHES3 = /\/+$/;
1116
1478
  GHSA = /GHSA-[\w-]+$/;
1117
1479
  UPPER_BOUND = /(?:^|\s)<([\w.+-]+)/;
1480
+ NO_ENDPOINT = new Set([404, 405, 501]);
1118
1481
  });
1119
1482
 
1120
1483
  // src/licenses.ts
@@ -1641,20 +2004,23 @@ var init_popular = __esm(() => {
1641
2004
  function baseName(name) {
1642
2005
  return name.slice(name.indexOf("/") + 1);
1643
2006
  }
1644
- function scopeOf(name) {
2007
+ function scopeOf2(name) {
1645
2008
  return name.startsWith("@") ? name.slice(0, name.indexOf("/")) : "";
1646
2009
  }
1647
2010
  function squash(name) {
1648
2011
  return name.toLowerCase().replace(SEPARATORS, "");
1649
2012
  }
1650
2013
  function resemblance(name, popular) {
1651
- if (scopeOf(name) !== scopeOf(popular) && baseName(name) === baseName(popular) && baseName(name).length >= MIN_LENGTH)
2014
+ if (scopeOf2(name) !== scopeOf2(popular) && baseName(name) === baseName(popular) && baseName(name).length >= MIN_LENGTH)
1652
2015
  return `${popular} under another scope`;
1653
2016
  if (squash(name) === squash(popular))
1654
2017
  return `${popular} spelled with other separators or case`;
2018
+ const shortest = Math.min(name.length, popular.length);
2019
+ const allowed = shortest >= MIN_LENGTH * 2 ? 2 : 1;
2020
+ if (shortest < MIN_LENGTH || Math.abs(name.length - popular.length) > allowed)
2021
+ return;
1655
2022
  const edits = distance(name, popular);
1656
- const allowed = Math.min(name.length, popular.length) >= MIN_LENGTH * 2 ? 2 : 1;
1657
- return Math.min(name.length, popular.length) >= MIN_LENGTH && edits <= allowed ? `${popular}, ${edits} ${edits === 1 ? "edit" : "edits"} away` : undefined;
2023
+ return edits <= allowed ? `${popular}, ${edits} ${edits === 1 ? "edit" : "edits"} away` : undefined;
1658
2024
  }
1659
2025
  function nearMisses(name, popular = popular_default) {
1660
2026
  if (popular.includes(name))
@@ -1690,6 +2056,8 @@ function resolve(packument, request) {
1690
2056
  return tags[wanted];
1691
2057
  if (versions.includes(wanted))
1692
2058
  return wanted;
2059
+ if (!isRange(wanted))
2060
+ return;
1693
2061
  return versions.filter((version) => Bun.semver.satisfies(version, wanted)).sort(Bun.semver.order).at(-1);
1694
2062
  }
1695
2063
  function licenseOf(license) {
@@ -1703,6 +2071,9 @@ function ageDays(published, now) {
1703
2071
  const at = Date.parse(published ?? "");
1704
2072
  return Number.isNaN(at) ? undefined : (now - at) / DAY;
1705
2073
  }
2074
+ function daysAgo(age) {
2075
+ return age < 1 ? "less than a day" : `${Math.floor(age)} day${Math.floor(age) === 1 ? "" : "s"}`;
2076
+ }
1706
2077
  function installScriptsOf(version) {
1707
2078
  return Object.fromEntries(INSTALL_SCRIPTS.flatMap((name) => version?.scripts?.[name] === undefined ? [] : [[name, version.scripts[name]]]));
1708
2079
  }
@@ -1713,7 +2084,7 @@ function safeRelease(packument, now, minAgeDays) {
1713
2084
  }
1714
2085
  function analyze(input) {
1715
2086
  const { spec, packument, registry, now = Date.now(), minAgeDays = DEFAULT_MIN_AGE_DAYS } = input;
1716
- const report = { name: spec.name, registry, exists: packument !== undefined, requested: spec.request, installScripts: {}, nearMisses: nearMisses(spec.name), advisories: input.advisories ?? [], warnings: [], blocked: false, downloads: input.downloads };
2087
+ const report = { name: spec.name, registry, exists: packument !== undefined, requested: spec.request, installScripts: {}, nearMisses: nearMisses(spec.name), advisories: input.advisories ?? [], fixes: [], warnings: [], blocked: false, downloads: input.downloads };
1717
2088
  if (report.nearMisses.length > 0)
1718
2089
  report.warnings.push(`name looks like ${report.nearMisses.join("; ")}, check it is the package meant`);
1719
2090
  if (packument === undefined) {
@@ -1739,11 +2110,17 @@ function analyze(input) {
1739
2110
  return report;
1740
2111
  }
1741
2112
  const age = ageDays(report.published, now);
2113
+ const kept = new Set(report.advisories.map((each) => each.id));
2114
+ report.fixes = report.safe?.version === version ? [] : (input.safeAdvisories ?? []).filter((each) => !kept.has(each.id));
1742
2115
  if (age === undefined) {
1743
- report.warnings.push(`${registry} gives no publish date for ${version}, its age cannot be checked`);
2116
+ const isBlocking = minAgeDays > 0 && registry === PUBLIC_HOST;
2117
+ report.blocked ||= isBlocking;
2118
+ report.warnings.push(`${registry} gives no publish date for ${version}, its age cannot be checked${isBlocking ? " and it is refused" : ""}`);
2119
+ } else if (age < minAgeDays && report.fixes.length > 0) {
2120
+ report.warnings.push(`${version} was published ${daysAgo(age)} ago, under ${minAgeDays} days, but it fixes ${report.fixes.map((each) => `${each.ghsa ?? each.id} (${each.severity})`).join(", ")} that ${report.safe?.version} has, so its age does not hold it back`);
1744
2121
  } else if (age < minAgeDays) {
1745
2122
  report.blocked = true;
1746
- 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}`}`);
2123
+ report.warnings.push(`${version} was published ${daysAgo(age)} ago, under ${minAgeDays} days${report.safe === undefined ? ", and no older stable version exists" : `, use ${spec.name}@${report.safe.version}`}`);
1747
2124
  }
1748
2125
  const scripts = Object.keys(report.installScripts);
1749
2126
  if (scripts.length > 0)
@@ -1769,11 +2146,14 @@ function analyze(input) {
1769
2146
  }
1770
2147
  return report;
1771
2148
  }
1772
- var DEFAULT_MIN_AGE_DAYS = 14, DAY = 86400000, INSTALL_SCRIPTS, GIT_PREFIX, VERSION_SEPARATOR;
2149
+ var DEFAULT_MIN_AGE_DAYS = 14, DAY = 86400000, PUBLIC_HOST, INSTALL_SCRIPTS, GIT_PREFIX, VERSION_SEPARATOR;
1773
2150
  var init_analyze = __esm(() => {
1774
2151
  init_advisories();
1775
2152
  init_licenses();
1776
2153
  init_nearmiss();
2154
+ init_registry();
2155
+ init_semver();
2156
+ PUBLIC_HOST = new URL(PUBLIC_REGISTRY).host;
1777
2157
  INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
1778
2158
  GIT_PREFIX = /^git\+/;
1779
2159
  VERSION_SEPARATOR = /(?<=.)@/;
@@ -1782,7 +2162,8 @@ var init_analyze = __esm(() => {
1782
2162
  // src/commands/check.ts
1783
2163
  var exports_check = {};
1784
2164
  __export(exports_check, {
1785
- check: () => check
2165
+ check: () => check,
2166
+ checkPackage: () => checkPackage
1786
2167
  });
1787
2168
  async function checkPackage(text, context) {
1788
2169
  const spec = parseSpec(text);
@@ -1791,11 +2172,17 @@ async function checkPackage(text, context) {
1791
2172
  const downloads = packument !== undefined && isPublic(registry) ? await fetchDownloads(spec.name, context.fetcher) : undefined;
1792
2173
  const input = { spec, packument, downloads, registry: new URL(registry.url).host, now: context.now, minAgeDays: context.minAgeDays, licenses: context.settings?.licenses };
1793
2174
  const report = analyze(input);
1794
- const { blockSeverity } = context.settings ?? {};
1795
- if (blockSeverity === undefined || report.version === undefined)
2175
+ const blockSeverity = context.settings?.blockSeverity ?? DEFAULT_BLOCK_SEVERITY;
2176
+ if (report.version === undefined)
1796
2177
  return report;
1797
- const found = await fetchAdvisories({ [spec.name]: [report.version] }, registry, context.fetcher);
1798
- return analyze({ ...input, blockSeverity, advisories: advisoriesFor(found[spec.name], report.version) });
2178
+ const safe = report.safe?.version;
2179
+ const { found, unavailable, isUnsupported } = await fetchAdvisories({ [spec.name]: safe === undefined || safe === report.version ? [report.version] : [report.version, safe] }, registry, context.fetcher);
2180
+ if (unavailable !== undefined && isUnsupported !== true)
2181
+ return unreachable(unavailable);
2182
+ const checked = analyze({ ...input, blockSeverity, advisories: advisoriesFor(found[spec.name], report.version), safeAdvisories: safe === undefined ? [] : filterAdvisories(advisoriesFor(found[spec.name], safe), blockSeverity) });
2183
+ if (unavailable !== undefined)
2184
+ checked.warnings.push(`${unavailable}, so its known advisories are not checked`);
2185
+ return checked;
1799
2186
  }
1800
2187
  function when(release, now) {
1801
2188
  return release?.published === undefined ? "" : `, published ${formatAge(release.published, now)} (${release.published.slice(0, 10)})`;
@@ -1842,9 +2229,9 @@ var usage = `pkg [check] <name>[@version]...
1842
2229
 
1843
2230
  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.
1844
2231
 
1845
- A \`.pkgrc.json\` (or ~/.config/pkg/config.json) with \`licenses\` (SPDX ids) refuses a licence outside the list or a missing one, and \`blockSeverity\` (low, moderate, high, critical) refuses a version with an advisory at that level or above.
2232
+ 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.
1846
2233
 
1847
- Exits 0 when every package passes, 1 when one is missing, the version asked for is under the minimum age, its licence is outside the allowlist or an advisory reaches the threshold, 2 on a usage error.`, options;
2234
+ 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;
1848
2235
  var init_check = __esm(() => {
1849
2236
  init_src();
1850
2237
  init_advisories();
@@ -1858,6 +2245,15 @@ var init_check = __esm(() => {
1858
2245
  });
1859
2246
 
1860
2247
  // src/lockfile.ts
2248
+ function split(resolution) {
2249
+ const at = resolution.search(VERSION_SEPARATOR2);
2250
+ if (at === -1)
2251
+ return;
2252
+ const target = resolution.slice(at + 1);
2253
+ if (target.startsWith(NPM_ALIAS))
2254
+ return split(target.slice(NPM_ALIAS.length));
2255
+ return NOT_REGISTRY.test(target) ? undefined : { name: resolution.slice(0, at), version: target };
2256
+ }
1861
2257
  function parseBunLock(text) {
1862
2258
  const lock = Bun.JSONC.parse(text);
1863
2259
  const seen = new Map;
@@ -1865,17 +2261,15 @@ function parseBunLock(text) {
1865
2261
  const resolution = entry[0];
1866
2262
  if (typeof resolution !== "string")
1867
2263
  continue;
1868
- const at = resolution.search(VERSION_SEPARATOR2);
1869
- const name = resolution.slice(0, at);
1870
- const version = resolution.slice(at + 1);
1871
- if (at !== -1 && !NOT_REGISTRY.test(version))
1872
- seen.set(`${name}@${version}`, { name, version });
2264
+ const locked = split(resolution);
2265
+ if (locked !== undefined)
2266
+ seen.set(`${locked.name}@${locked.version}`, locked);
1873
2267
  }
1874
2268
  return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name) || Bun.semver.order(a.version, b.version));
1875
2269
  }
1876
- var NOT_REGISTRY, VERSION_SEPARATOR2;
2270
+ var NOT_REGISTRY, VERSION_SEPARATOR2, NPM_ALIAS = "npm:";
1877
2271
  var init_lockfile = __esm(() => {
1878
- NOT_REGISTRY = /^(?:workspace|link|file|github|git|http|https|npm):|^[\w.-]+\/[\w.-]+$/;
2272
+ NOT_REGISTRY = /^(?:workspace|link|file|github|git|http|https):|^[\w.-]+\/[\w.-]+$/;
1879
2273
  VERSION_SEPARATOR2 = /(?<=.)@/;
1880
2274
  });
1881
2275
 
@@ -1885,7 +2279,7 @@ __export(exports_audit, {
1885
2279
  audit: () => audit
1886
2280
  });
1887
2281
  import { join as join2, resolve as resolve2 } from "path";
1888
- import process8 from "process";
2282
+ import process9 from "process";
1889
2283
  function hasFinding(audited) {
1890
2284
  return audited.blocked || audited.warnings.length > 0 || audited.advisories.length > 0;
1891
2285
  }
@@ -1897,26 +2291,36 @@ async function auditPackages(packages, context) {
1897
2291
  packuments.set(key, fetchPackument(name, registry, context.fetcher));
1898
2292
  return packuments.get(key);
1899
2293
  };
2294
+ const looked = [];
2295
+ for (let start = 0;start < packages.length; start += BATCH) {
2296
+ looked.push(...await Promise.all(packages.slice(start, start + BATCH).map(async ({ name, version }) => {
2297
+ const registry = registryFor(name, context.configs);
2298
+ const input = { 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 };
2299
+ return { name, version, registry, input, safe: analyze(input).safe?.version };
2300
+ })));
2301
+ }
1900
2302
  const registries = new Map;
1901
- for (const { name, version } of packages) {
1902
- const registry = registryFor(name, context.configs);
2303
+ for (const { name, version, registry, safe } of looked) {
1903
2304
  const group = registries.get(registry.url) ?? { registry, versions: {} };
1904
- group.versions[name] = [...group.versions[name] ?? [], version];
2305
+ group.versions[name] = [...group.versions[name] ?? [], version, ...safe === undefined || safe === version ? [] : [safe]];
1905
2306
  registries.set(registry.url, group);
1906
2307
  }
1907
2308
  const advisories = {};
1908
- for (const { registry, versions } of registries.values())
1909
- Object.assign(advisories, await fetchAdvisories(versions, registry, context.fetcher));
1910
- const audited = [];
1911
- for (let start = 0;start < packages.length; start += BATCH) {
1912
- audited.push(...await Promise.all(packages.slice(start, start + BATCH).map(async ({ name, version }) => {
1913
- const registry = registryFor(name, context.configs);
1914
- const found = filterAdvisories(advisoriesFor(advisories[name], version), context.level, context.ignore);
1915
- const report = analyze({ spec: { name, request: version }, packument: await lookup(name, registry), registry: new URL(registry.url).host, now: context.now, minAgeDays: context.minAgeDays, licenses: context.settings.licenses });
1916
- return { name, version, license: report.license, blocked: report.blocked || found.length > 0, warnings: report.warnings, advisories: found };
1917
- })));
2309
+ const unavailable = [];
2310
+ for (const { registry, versions } of registries.values()) {
2311
+ const fetched = await fetchAdvisories(versions, registry, context.fetcher);
2312
+ Object.assign(advisories, fetched.found);
2313
+ if (fetched.unavailable !== undefined)
2314
+ unavailable.push(fetched.unavailable);
1918
2315
  }
1919
- return audited;
2316
+ const blockSeverity = context.settings.blockSeverity ?? DEFAULT_BLOCK_SEVERITY;
2317
+ const audited = looked.map(({ name, version, input, safe }) => {
2318
+ const found = filterAdvisories(advisoriesFor(advisories[name], version), context.level, context.ignore);
2319
+ const safeAdvisories = safe === undefined ? [] : filterAdvisories(advisoriesFor(advisories[name], safe), blockSeverity, context.ignore);
2320
+ const report = analyze({ ...input, advisories: advisoriesFor(advisories[name], version), safeAdvisories });
2321
+ return { name, version, license: report.license, blocked: report.blocked || found.length > 0, warnings: report.warnings, advisories: found };
2322
+ });
2323
+ return { audited, unavailable };
1920
2324
  }
1921
2325
  function renderAudit(findings, total) {
1922
2326
  const blocks = findings.map(({ name, version, warnings, advisories }) => [
@@ -1934,7 +2338,7 @@ async function audit(args) {
1934
2338
  const level = values["audit-level"] ?? "low";
1935
2339
  if (!isSeverity(level))
1936
2340
  fail(`--audit-level takes ${SEVERITIES.join(", ")}, not ${level}`);
1937
- const dir = resolve2(asked ?? process8.cwd());
2341
+ const dir = resolve2(asked ?? process9.cwd());
1938
2342
  const lock = Bun.file(join2(dir, "bun.lock"));
1939
2343
  if (!await lock.exists())
1940
2344
  fail(`no bun.lock in ${dir}`);
@@ -1946,19 +2350,24 @@ async function audit(args) {
1946
2350
  level,
1947
2351
  ignore: values.ignore ?? []
1948
2352
  };
1949
- const findings = (await auditPackages(packages, context)).filter(hasFinding);
2353
+ const { audited, unavailable } = await auditPackages(packages, context);
2354
+ const findings = audited.filter(hasFinding);
1950
2355
  if (values.json === true)
1951
2356
  printJson(findings);
1952
2357
  else
1953
2358
  console.log(renderAudit(findings, packages.length));
1954
- return findings.some((each) => each.blocked) ? 1 : 0;
2359
+ for (const reason of unavailable)
2360
+ warn(`${reason}, so its advisories are not checked`);
2361
+ if (findings.some((each) => each.blocked))
2362
+ return 1;
2363
+ return unavailable.length > 0 ? 2 : 0;
1955
2364
  }
1956
2365
  var usage2 = `pkg audit [dir]
1957
2366
 
1958
2367
  Reads the bun.lock in the folder (the current one by default) and runs on every registry package it pins the checks \`pkg check\` runs before an add: age, install scripts, a deprecated version, a near miss of a popular name, and the licence allowlist of the pkg config. It also lists the known advisories of each version, with the GHSA, the severity and the fixed version, from the registry's bulk advisory endpoint, the one \`bun audit\` reads.
1959
2368
  Findings are grouped per package, each registry package looked up once per run.
1960
2369
 
1961
- Exits 0 when nothing blocks, 1 when a package is missing, too fresh or outside the licence allowlist, or an advisory reaches --audit-level, 2 on a usage error or without a bun.lock.`, options2, BATCH = 8;
2370
+ 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;
1962
2371
  var init_audit = __esm(() => {
1963
2372
  init_src();
1964
2373
  init_advisories();
@@ -1974,125 +2383,42 @@ var init_audit = __esm(() => {
1974
2383
  };
1975
2384
  });
1976
2385
 
1977
- // ../kit/src/progress.ts
1978
- import process9 from "process";
1979
- function liveRegion(render, stream = process9.stderr, env = process9.env) {
1980
- if (!takesControlCodes(stream, env))
1981
- return { redraw: () => {
1982
- return;
1983
- }, print: (text) => void stream.write(`${text}
1984
- `), stop: () => {
1985
- return;
1986
- } };
1987
- const write = stream.write.bind(stream);
1988
- let frame = 0;
1989
- let drawn = 0;
1990
- const clear = () => {
1991
- if (drawn > 0)
1992
- write(`${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}`);
1993
- drawn = 0;
1994
- };
1995
- const draw = () => {
1996
- const width = (stream.columns === undefined || stream.columns === 0 ? 80 : stream.columns) - 1;
1997
- const height = stream.rows === undefined || stream.rows === 0 ? 24 : stream.rows;
1998
- const lines = render(FRAMES[frame++ % FRAMES.length]).slice(0, Math.max(1, height - 1)).map((line) => cutToWidth(line, width));
1999
- if (lines.length === 0) {
2000
- clear();
2001
- return;
2002
- }
2003
- write(`${drawn > 0 ? `${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}` : CLEAR_LINE}${lines.join(`
2004
- `)}`);
2005
- drawn = lines.length;
2006
- };
2007
- const timer = setInterval(draw, FRAME_MS);
2008
- const restore = interceptOutput(clear);
2009
- const onExit = () => clear();
2010
- const signals = holdSignals(stop);
2011
- function stop() {
2012
- clearInterval(timer);
2013
- clear();
2014
- restore();
2015
- process9.off("exit", onExit);
2016
- signals.release();
2017
- }
2018
- timer.unref();
2019
- process9.on("exit", onExit);
2020
- return {
2021
- redraw: draw,
2022
- print: (text) => {
2023
- clear();
2024
- write(`${text}
2025
- `);
2026
- draw();
2027
- },
2028
- stop
2029
- };
2030
- }
2031
- function cutToWidth(line, width) {
2032
- if (Bun.stringWidth(line) <= width)
2033
- return line;
2034
- let room = width;
2035
- let kept = "";
2036
- let index = 0;
2037
- let linked = false;
2038
- while (index < line.length) {
2039
- ESCAPE_CODE.lastIndex = index;
2040
- const escape = ESCAPE_CODE.exec(line);
2041
- if (escape !== null) {
2042
- kept += escape[0];
2043
- linked ||= escape[0].startsWith(`${ESC}]`);
2044
- index = ESCAPE_CODE.lastIndex;
2045
- continue;
2046
- }
2047
- const char = String.fromCodePoint(line.codePointAt(index));
2048
- const columns = Bun.stringWidth(char);
2049
- if (columns > room)
2050
- break;
2051
- kept += char;
2052
- room -= columns;
2053
- index += char.length;
2054
- }
2055
- return `${kept}${RESET_STYLE}${linked ? CLOSE_LINK : ""}`;
2056
- }
2057
- function interceptOutput(clear) {
2058
- const targets = [...CONSOLE_METHODS.map((name) => [console, name]), [process9.stdout, "write"], [process9.stderr, "write"]];
2059
- const wrapped = targets.map(([target, name]) => {
2060
- const original = Reflect.get(target, name);
2061
- const wrapper = (...args) => {
2062
- clear();
2063
- return original.apply(target, args);
2064
- };
2065
- Reflect.set(target, name, wrapper);
2066
- return { target, name, original, wrapper };
2067
- });
2068
- return () => {
2069
- for (const { target, name, original, wrapper } of wrapped) {
2070
- if (Reflect.get(target, name) === wrapper)
2071
- Reflect.set(target, name, original);
2072
- }
2073
- };
2074
- }
2075
- 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;
2076
- var init_progress = __esm(() => {
2077
- init_format();
2078
- init_process();
2079
- init_style();
2080
- FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2081
- ESC = String.fromCharCode(27);
2082
- ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, "y");
2083
- RESET_STYLE = `${ESC}[0m`;
2084
- CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
2085
- CONSOLE_METHODS = ["log", "info", "warn", "error", "debug", "dir", "dirxml", "table", "trace", "group", "groupCollapsed", "count", "timeEnd", "timeLog", "assert"];
2086
- });
2087
-
2088
2386
  // ../kit/src/shell.ts
2089
- var SEPARATORS2, PLAIN_BREAKERS, DOUBLE_QUOTED_ESCAPES, WRAPPERS;
2387
+ function wrapper(short = "", long = [], operands = 0) {
2388
+ return { short, long, operands };
2389
+ }
2390
+ var SEPARATORS2, PLAIN_BREAKERS, GLOB_CHARS, COMMENT_PREFIXES, DOUBLE_QUOTED_ESCAPES, KEYWORDS, SHELLS, COMMAND_STRING_PROGRAMS, SHELL_VALUE_OPTIONS, PRINTERS, FIND_EXEC, WRAPPERS;
2090
2391
  var init_shell = __esm(() => {
2091
2392
  SEPARATORS2 = new Set([`
2092
2393
  `, ";", "|"]);
2093
2394
  PLAIN_BREAKERS = new Set(["<", ">", "(", ")", "&", "|"]);
2395
+ GLOB_CHARS = new Set(["*", "?", "["]);
2396
+ COMMENT_PREFIXES = new Set([";", "&", "|", "(", ")"]);
2094
2397
  DOUBLE_QUOTED_ESCAPES = new Set(["$", "`", '"', "\\"]);
2095
- WRAPPERS = new Set(["command", "env", "sudo", "exec", "nohup", "time"]);
2398
+ KEYWORDS = new Set(["then", "do", "else", "if", "elif", "while", "until", "!", "{", "}"]);
2399
+ SHELLS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh"]);
2400
+ COMMAND_STRING_PROGRAMS = new Set([...SHELLS, "su", "runuser"]);
2401
+ SHELL_VALUE_OPTIONS = new Set(["-o", "-O", "--rcfile", "--init-file"]);
2402
+ PRINTERS = new Set(["echo", "printf"]);
2403
+ FIND_EXEC = new Set(["-exec", "-execdir", "-ok", "-okdir"]);
2404
+ WRAPPERS = new Map([
2405
+ ["command", wrapper()],
2406
+ ["builtin", wrapper()],
2407
+ ["nohup", wrapper()],
2408
+ ["setsid", wrapper()],
2409
+ ["busybox", wrapper()],
2410
+ ["exec", wrapper("a")],
2411
+ ["time", wrapper("fo", ["--format", "--output"])],
2412
+ ["env", wrapper("uCS", ["--unset", "--chdir", "--split-string", "--argv0"])],
2413
+ ["sudo", wrapper("ughpCTrtUDR", ["--user", "--group", "--host", "--prompt", "--close-from", "--command-timeout", "--role", "--type", "--other-user", "--chdir", "--chroot"])],
2414
+ ["doas", wrapper("uC")],
2415
+ ["timeout", wrapper("sk", ["--signal", "--kill-after"], 1)],
2416
+ ["nice", wrapper("n", ["--adjustment"])],
2417
+ ["ionice", wrapper("cnpPu", ["--class", "--classdata", "--pid", "--pgid", "--uid"])],
2418
+ ["stdbuf", wrapper("ioe", ["--input", "--output", "--error"])],
2419
+ ["chroot", wrapper("", ["--userspec", "--groups"], 1)],
2420
+ ["xargs", wrapper("IndsEaPL", ["--max-args", "--max-procs", "--max-chars", "--delimiter", "--arg-file", "--eof", "--max-lines", "--process-slot-var"])]
2421
+ ]);
2096
2422
  });
2097
2423
 
2098
2424
  // src/pin.ts
@@ -2187,7 +2513,7 @@ var init_consumers = __esm(() => {
2187
2513
  init_pin();
2188
2514
  KINDS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
2189
2515
  SKIPPED = new Set(["node_modules", "dist", "build", "vendor", "target", "coverage"]);
2190
- LOCAL_SPEC = /^(?:workspace|link|file|portal):/;
2516
+ LOCAL_SPEC = /[:/]/;
2191
2517
  KIND_FLAGS = { dependencies: [], devDependencies: ["--dev"], optionalDependencies: ["--optional"], peerDependencies: ["--peer"] };
2192
2518
  });
2193
2519
 
@@ -2200,23 +2526,43 @@ __export(exports_bump, {
2200
2526
  import { existsSync as existsSync2 } from "fs";
2201
2527
  import { homedir as homedir2 } from "os";
2202
2528
  import { dirname as dirname3, join as join4, relative as relative2 } from "path";
2529
+ async function lookup(name, registry, fetcher) {
2530
+ try {
2531
+ return { packument: await fetchPackument(name, registry, fetcher) };
2532
+ } catch (error) {
2533
+ if (error instanceof Exit && error.code === 2)
2534
+ return { error: error.message };
2535
+ throw error;
2536
+ }
2537
+ }
2203
2538
  async function servedVersion(name, version, settings) {
2539
+ if (!isValidName(name))
2540
+ fail(`${name} is not a valid npm package name`);
2541
+ if (version !== undefined && !EXACT_VERSION.test(version))
2542
+ fail(`bump takes an exact version, not ${version}: leave it out for the latest`);
2204
2543
  const registry = registryFor(name, settings.configs);
2205
2544
  const host = new URL(registry.url).host;
2206
2545
  const started = Date.now();
2207
2546
  const region = liveRegion((spinner) => [`${spinner} waiting for ${host} to serve ${name}@${version} ${formatDuration(Date.now() - started)}`]);
2547
+ let lastError = "";
2208
2548
  try {
2209
2549
  for (;; ) {
2210
- const packument = await fetchPackument(name, registry, settings.fetcher);
2211
- const latest = packument?.["dist-tags"]?.latest;
2212
- if (version === undefined && latest !== undefined)
2213
- return latest;
2214
- if (version === undefined)
2215
- fail(`${host} has no ${name}`, 1);
2216
- if (packument?.versions?.[version] !== undefined)
2217
- return version;
2550
+ const looked = await lookup(name, registry, settings.fetcher);
2551
+ if ("error" in looked) {
2552
+ lastError = looked.error;
2553
+ } else {
2554
+ const latest = looked.packument?.["dist-tags"]?.latest;
2555
+ if (looked.packument === undefined)
2556
+ fail(`${host} has no ${name}`, 1);
2557
+ if (version === undefined && latest === undefined)
2558
+ fail(`${host} has no ${name}`, 1);
2559
+ if (version === undefined && latest !== undefined)
2560
+ return latest;
2561
+ if (version !== undefined && looked.packument.versions?.[version] !== undefined)
2562
+ return version;
2563
+ }
2218
2564
  if (Date.now() - started >= settings.timeout)
2219
- fail(`${host} does not serve ${name}@${version} after ${formatDuration(settings.timeout)}`);
2565
+ fail(`${host} does not serve ${name}@${version} after ${formatDuration(settings.timeout)}${lastError === "" ? "" : `, last error: ${lastError}`}`);
2220
2566
  region.redraw();
2221
2567
  await Bun.sleep(settings.poll ?? POLL);
2222
2568
  }
@@ -2224,6 +2570,20 @@ async function servedVersion(name, version, settings) {
2224
2570
  region.stop();
2225
2571
  }
2226
2572
  }
2573
+ async function vet(name, version, settings) {
2574
+ if (settings.isForced === true)
2575
+ return true;
2576
+ const report = await checkPackage(`${name}@${version}`, { configs: settings.configs, fetcher: settings.fetcher, minAgeDays: settings.minAgeDays ?? 0, settings: settings.pkgSettings });
2577
+ if (!report.blocked) {
2578
+ for (const warning of report.warnings)
2579
+ warn(`${name}@${version}`, warning);
2580
+ return true;
2581
+ }
2582
+ for (const warning of report.warnings)
2583
+ problem(`${name}@${version} is refused`, warning);
2584
+ problem(`${name}@${version} is refused`, "no consumer was touched, --force bumps it anyway");
2585
+ return false;
2586
+ }
2227
2587
  function lockfileOf(dir, root) {
2228
2588
  for (let at = dir;; at = dirname3(at)) {
2229
2589
  const found = ["bun.lock", "bun.lockb"].find((file) => existsSync2(join4(at, file)));
@@ -2233,6 +2593,11 @@ function lockfileOf(dir, root) {
2233
2593
  return;
2234
2594
  }
2235
2595
  }
2596
+ async function hasUncommittedChanges(where, root, run) {
2597
+ const lockfile = lockfileOf(where, root);
2598
+ const status = await run(["git", "status", "--porcelain", "--", "package.json", ...lockfile === undefined ? [] : [lockfile]], where);
2599
+ return status.code === 0 && status.text.trim() !== "";
2600
+ }
2236
2601
  async function checkAndCommit(name, version, where, root, run) {
2237
2602
  const checked = await run(["check"], where);
2238
2603
  if (checked.code !== 0)
@@ -2255,11 +2620,14 @@ async function bumpConsumers(name, version, settings) {
2255
2620
  if (found.length === 0)
2256
2621
  note(`nothing under ${settings.root} depends on ${name}`);
2257
2622
  if (local > 0)
2258
- note(`${local} workspace:, link: or file: dependenc${local === 1 ? "y" : "ies"}`, "left as they are");
2623
+ note(`${local} non-registry dependenc${local === 1 ? "y" : "ies"} (workspace:, file:, git, URL, alias, catalog:)`, "left as they are");
2259
2624
  for (const consumer of consumers) {
2260
2625
  const argv = addCommand(consumer, name, version);
2261
2626
  const where = join4(settings.root, consumer.dir);
2262
- if (consumer.spec === version) {
2627
+ if (consumer.kind === "peerDependencies") {
2628
+ skipped.push(consumer.dir);
2629
+ note(consumer.dir, `peer range ${consumer.spec}, left as it is`);
2630
+ } else if (consumer.spec === version) {
2263
2631
  skipped.push(consumer.dir);
2264
2632
  note(consumer.dir, `already on ${version}`);
2265
2633
  } else if (consumer.manager !== "bun") {
@@ -2267,6 +2635,10 @@ async function bumpConsumers(name, version, settings) {
2267
2635
  warn(consumer.dir, `${consumer.spec}, its lockfile pins ${consumer.manager}: put ${version} in by hand`);
2268
2636
  } else if (settings.isDryRun === true) {
2269
2637
  note(consumer.dir, `would run ${[argv.join(" "), ...settings.isAll === true ? ["check", `gc 'chore(deps): bump ${name} to ${version}'`] : []].join(", then ")}`);
2638
+ } else if (settings.isAll === true && await hasUncommittedChanges(where, settings.root, run)) {
2639
+ isClean = false;
2640
+ failing.push(consumer.dir);
2641
+ problem(consumer.dir, "package.json or its lockfile has uncommitted changes, left alone so that gc does not commit them");
2270
2642
  } else {
2271
2643
  const ran = await run(argv, where);
2272
2644
  if (ran.code !== 0) {
@@ -2300,30 +2672,40 @@ async function bump(args) {
2300
2672
  configs: await loadConfigs(),
2301
2673
  timeout: duration(values.timeout, "timeout", 10 * 60000),
2302
2674
  isDryRun: values["dry-run"] === true,
2303
- isAll: values["all-consumers"] === true
2675
+ isAll: values["all-consumers"] === true,
2676
+ isForced: values.force === true,
2677
+ pkgSettings: await loadSettings(),
2678
+ minAgeDays: wholeNumber(values["min-age"], "min-age", 0, 0)
2304
2679
  };
2305
2680
  const version = await servedVersion(name, asked, settings);
2681
+ if (!await vet(name, version, settings))
2682
+ return 1;
2306
2683
  return await bumpConsumers(name, version, settings) ? 0 : 1;
2307
2684
  }
2308
2685
  var usage3 = `pkg bump <name> [version]
2309
2686
 
2310
- Waits until the registry serves the version (the latest when none is given), then runs \`bun add --exact --no-cache\` in each folder under --root whose package.json depends on the package, the kind of dependency kept, a line per folder.
2311
- A workspace:, link: or file: dependency, a folder already on the version and one whose lockfile pins another manager are left as they are, each said so.
2312
- With --all-consumers each folder whose install went through then runs its own \`check\` (the check bin) and is committed with \`gc 'chore(deps): bump <name> to <version>' -- package.json <lockfile>\` only when that passes; a failing one is left uncommitted and listed in the summary with the skipped ones, and nothing is ever pushed.
2687
+ 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.
2688
+ A version the vetting blocks is refused before any folder is touched, unless --force is given.
2689
+ 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.
2690
+ 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.
2313
2691
 
2314
- Exits 0 when every install ran or was left as it is (with --all-consumers, and every check passed and every commit landed), 1 when an install, a check or a commit fails or the registry has no such package, 2 when the version never shows within the timeout or on a usage error.`, ROOT_OPTION, options3, POLL = 5000;
2692
+ 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;
2315
2693
  var init_bump = __esm(() => {
2316
2694
  init_src();
2317
2695
  init_progress();
2318
2696
  init_config2();
2319
2697
  init_consumers();
2320
2698
  init_registry();
2699
+ init_semver();
2700
+ init_check();
2321
2701
  ROOT_OPTION = { type: "string", description: "the folder holding the repositories (~/Workspace)" };
2322
2702
  options3 = {
2323
2703
  root: ROOT_OPTION,
2324
2704
  timeout: { type: "string", short: "t", description: "how long to wait for the registry (10m): 30s, 5m, 1h" },
2325
2705
  "all-consumers": { type: "boolean", description: "after each install run the folder's check and commit it through gc when it passes" },
2326
- "dry-run": { type: "boolean", description: "print what would run, changing nothing" }
2706
+ "dry-run": { type: "boolean", description: "print what would run, changing nothing" },
2707
+ force: { type: "boolean", description: "bump even when the version is blocked by the vetting" },
2708
+ "min-age": { type: "string", description: "days a version must be old (0: a bump adopts a release on purpose, often one just made)" }
2327
2709
  };
2328
2710
  });
2329
2711
 
@@ -2367,7 +2749,7 @@ var init_consumers2 = __esm(() => {
2367
2749
  init_src();
2368
2750
  import process10 from "process";
2369
2751
  // package.json
2370
- var version = "0.4.0";
2752
+ var version = "0.6.0";
2371
2753
 
2372
2754
  // src/cli.ts
2373
2755
  process10.exitCode = await runCli({