@kaynooo/cli-doctor 0.2.0 → 0.3.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/dist/cli-bin.js +268 -58
  2. package/dist/cli.js +229 -43
  3. package/package.json +2 -2
package/dist/cli-bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli-bin.ts
5
- import process7 from "process";
5
+ import process8 from "process";
6
6
 
7
7
  // ../kit/src/args.ts
8
8
  import process2 from "process";
@@ -11,7 +11,7 @@ import { parseArgs } from "util";
11
11
  // ../kit/src/style.ts
12
12
  import process from "process";
13
13
 
14
- // ../../node_modules/.bun/@kaynooo+utils@2.31.0+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
14
+ // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
15
15
  function x(s) {
16
16
  let r = s.startsWith("#") ? s.slice(1).toLowerCase() : s.toLowerCase();
17
17
  if (!/^[a-f0-9]+$/.test(r))
@@ -231,7 +231,7 @@ class e {
231
231
  }
232
232
  }
233
233
 
234
- // ../../node_modules/.bun/@kaynooo+utils@2.31.0+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
234
+ // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
235
235
  var r = 39;
236
236
  var o = 49;
237
237
  var f2 = "\x1B[";
@@ -349,8 +349,12 @@ class HelpShown extends Exit {
349
349
 
350
350
  class Completing {
351
351
  options;
352
- constructor(options) {
352
+ positionals;
353
+ candidates;
354
+ constructor(options, positionals = [], candidates = {}) {
353
355
  this.options = options;
356
+ this.positionals = positionals;
357
+ this.candidates = candidates;
354
358
  }
355
359
  }
356
360
  var completion = { active: false };
@@ -393,8 +397,10 @@ function exampleRows(examples, colors = paint(process2.stdout)) {
393
397
  function optionRows(options, colors = paint(process2.stdout)) {
394
398
  const rows = Object.entries(options).map(([name, option]) => {
395
399
  const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
396
- const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
397
- return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
400
+ const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
401
+ const placeholder = option.optional === undefined ? named : `[${named}]`;
402
+ const value = option.type === "string" ? ` ${colors.dim(placeholder)}` : "";
403
+ return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? placeholder.length + 1 : 0), description: option.description ?? "" };
398
404
  });
399
405
  const width = Math.max(0, ...rows.map((row) => row.width));
400
406
  return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
@@ -420,14 +426,13 @@ function applyOutput(color, plain, env = process2.env) {
420
426
  function parse(args, usage, options, settings = {}) {
421
427
  const output = Object.fromEntries(Object.entries(OUTPUT_OPTIONS).filter(([name]) => !(name in options)));
422
428
  const all = { ...options, ...output, help: { type: "boolean", short: "h", description: "show this help" } };
429
+ const listing = listed(all, settings.allowNegative === true);
423
430
  if (completion.active)
424
- throw new Completing(all);
425
- const separator = settings.rest === true ? args.indexOf("--") : -1;
426
- const own = separator === -1 ? args : args.slice(0, separator);
427
- const rest = separator === -1 ? undefined : args.slice(separator + 1);
431
+ throw new Completing(listing, settings.positionals, settings.candidates);
432
+ const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
428
433
  let parsed;
429
434
  try {
430
- parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
435
+ parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
431
436
  } catch (error) {
432
437
  const message = parseError(error);
433
438
  throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
@@ -435,12 +440,84 @@ function parse(args, usage, options, settings = {}) {
435
440
  const values = parsed.values;
436
441
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
437
442
  if (values.help === true) {
438
- console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, all, undefined, settings));
443
+ console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
439
444
  throw new HelpShown;
440
445
  }
446
+ checkChoices(parsed.values, all);
447
+ checkConflicts(parsed.values, all);
441
448
  if (settings.positionals !== undefined)
442
449
  checkPositionals(parsed.positionals, settings.positionals);
443
- return { ...parsed, positionals: parsed.positionals, rest };
450
+ return { ...parsed, values: parsed.values, positionals: parsed.positionals, rest };
451
+ }
452
+ function splitRest(args, passes) {
453
+ const separator = passes ? args.indexOf("--") : -1;
454
+ return { own: separator === -1 ? args : args.slice(0, separator), rest: separator === -1 ? undefined : args.slice(separator + 1) };
455
+ }
456
+ function splitTrailing(args, options, count) {
457
+ const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
458
+ let seen = 0;
459
+ if (count === 0)
460
+ return { own: args, rest: undefined };
461
+ for (let at = 0;at < args.length; at++) {
462
+ const word = args[at];
463
+ if (word === "--")
464
+ break;
465
+ if (word.startsWith("-")) {
466
+ if (takesValue(word))
467
+ at++;
468
+ continue;
469
+ }
470
+ seen++;
471
+ if (seen === count)
472
+ return { own: args.slice(0, at + 1), rest: args.slice(at + 1) };
473
+ }
474
+ return { own: args, rest: undefined };
475
+ }
476
+ function listed(options, allowNegative) {
477
+ if (!allowNegative)
478
+ return options;
479
+ return Object.fromEntries(Object.entries(options).flatMap(([name, option]) => {
480
+ if (option.type !== "boolean" || option.negative === undefined)
481
+ return [[name, option]];
482
+ return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
483
+ }));
484
+ }
485
+ function withBare(args, options) {
486
+ const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
487
+ if (bare.size === 0)
488
+ return args;
489
+ const separator = args.indexOf("--");
490
+ const own = separator === -1 ? args : args.slice(0, separator);
491
+ return [...own.flatMap((arg, index) => {
492
+ const optional = bare.get(arg);
493
+ const next = own[index + 1];
494
+ const value = next !== undefined && (optional?.accepts === undefined ? !next.startsWith("-") : optional.accepts.test(next));
495
+ return optional === undefined || value ? [arg] : [arg, optional.bare];
496
+ }), ...separator === -1 ? [] : args.slice(separator)];
497
+ }
498
+ function checkConflicts(values, options) {
499
+ const given = (name) => values[name] !== undefined && values[name] !== false;
500
+ for (const [name, { conflicts = [] }] of Object.entries(options)) {
501
+ const other = given(name) ? conflicts.find(given) : undefined;
502
+ if (other !== undefined)
503
+ badUsage(`--${name} cannot be used with --${other}`);
504
+ }
505
+ }
506
+ function checkChoices(values, options) {
507
+ for (const [name, option] of Object.entries(options)) {
508
+ if (option.choices === undefined)
509
+ continue;
510
+ const given = values[name];
511
+ for (const value of Array.isArray(given) ? given : [given]) {
512
+ if (typeof value === "string")
513
+ oneOf(value, name, option.choices);
514
+ }
515
+ }
516
+ }
517
+ function oneOf(value, name, choices) {
518
+ if (!choices.includes(value))
519
+ badUsage(`${name.startsWith("<") ? name : `--${name}`} takes one of ${choices.join(", ")}, not ${value}`);
520
+ return value;
444
521
  }
445
522
  function checkPositionals(given, names) {
446
523
  const variadic = names.at(-1)?.replace("]", "").endsWith("...") === true;
@@ -591,8 +668,9 @@ var LEVEL_OPTIONS = {
591
668
  help: { type: "boolean", short: "h", description: "show this help" }
592
669
  };
593
670
  var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
594
- async function commandRows(commands, colors) {
595
- const width = Math.max(...Object.keys(commands).map((name) => name.length));
671
+ async function commandRows(all, colors) {
672
+ const commands = shown(all);
673
+ const width = Math.max(0, ...Object.keys(commands).map((name) => name.length));
596
674
  const groups = new Map([["Commands:", []]]);
597
675
  const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
598
676
  for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
@@ -603,6 +681,9 @@ async function commandRows(commands, colors) {
603
681
  }
604
682
  return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
605
683
  }
684
+ function shown(commands) {
685
+ return Object.fromEntries(Object.entries(commands).filter(([, { hidden }]) => hidden !== true));
686
+ }
606
687
  function takeOutputFlags(argv) {
607
688
  let index = 0;
608
689
  for (;index < argv.length; index++) {
@@ -694,6 +775,14 @@ async function dispatch(level, typed, { version, redact }) {
694
775
  return reportThrown(prefix, error);
695
776
  }
696
777
  const [name, ...args] = argv;
778
+ const fallback = level.defaultCommand === undefined ? undefined : level.commands[level.defaultCommand];
779
+ if (name === undefined && level.runBare && fallback !== undefined && "run" in fallback) {
780
+ try {
781
+ return await fallback.run([]);
782
+ } catch (error) {
783
+ return reportThrown(`${prefix} ${level.defaultCommand}`, error, redact, false);
784
+ }
785
+ }
697
786
  if (name === undefined && level.path.length > 1) {
698
787
  console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stderr));
699
788
  return 2;
@@ -708,6 +797,7 @@ async function dispatch(level, typed, { version, redact }) {
708
797
  }
709
798
  let command = level.commands[level.aliases?.[name] ?? name];
710
799
  let rest = args;
800
+ let ran = name;
711
801
  if (command === undefined && level.lazy.length > 0) {
712
802
  level = await expand(level, [...level.path.slice(1), ...argv]);
713
803
  command = level.commands[level.aliases?.[name] ?? name];
@@ -715,40 +805,77 @@ async function dispatch(level, typed, { version, redact }) {
715
805
  if (command === undefined && level.defaultCommand !== undefined) {
716
806
  command = level.commands[level.defaultCommand];
717
807
  rest = argv;
808
+ ran = level.defaultCommand;
718
809
  }
719
810
  if (command === undefined) {
720
- const meant = closest(name, Object.keys(level.commands));
811
+ const meant = closest(name, Object.keys(shown(level.commands)));
721
812
  printError(prefix, `no command ${name}`, meant === undefined ? `run ${prefix} --help for its commands` : `did you mean ${meant}?`);
722
813
  return 2;
723
814
  }
724
815
  if ("commands" in command) {
725
816
  const { summary } = command;
726
- 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 });
817
+ return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, runBare: false, examples: command.examples, replaces: undefined }, rest, { version, redact });
727
818
  }
728
819
  try {
729
820
  return await command.run(rest);
730
821
  } catch (error) {
731
- return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
822
+ return reportThrown(`${prefix} ${ran}`, error, redact, wantsJson(rest));
732
823
  }
733
824
  }
734
825
  var COMPLETE = "__complete";
735
826
  var FILES = "__files__";
736
- async function optionsOf(run) {
827
+ async function completingOf(run) {
737
828
  completion.active = true;
738
829
  try {
739
830
  await run([]);
740
831
  } catch (error) {
741
832
  if (error instanceof Completing)
742
- return error.options;
833
+ return error;
743
834
  } finally {
744
835
  completion.active = false;
745
836
  }
746
- return {};
837
+ return new Completing({});
838
+ }
839
+ async function optionsOf(run) {
840
+ return (await completingOf(run)).options;
841
+ }
842
+ function candidateLine(candidate) {
843
+ return typeof candidate === "string" ? candidate : candidate.join("\t");
844
+ }
845
+ function positionalBeingTyped(before, { options, positionals }) {
846
+ const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
847
+ let index = 0;
848
+ let at = 0;
849
+ while (at < before.length) {
850
+ const word = before[at];
851
+ if (word === "--") {
852
+ index += before.length - at - 1;
853
+ break;
854
+ }
855
+ if (!word.startsWith("-"))
856
+ index++;
857
+ else if (takesValue(word))
858
+ at++;
859
+ at++;
860
+ }
861
+ const last = positionals.at(-1);
862
+ if (index < positionals.length)
863
+ return positionals[index];
864
+ return last?.replace("]", "").endsWith("...") === true ? last : undefined;
747
865
  }
748
866
  async function leafCandidates(run, words) {
749
- if (!(words.at(-1) ?? "").startsWith("-"))
750
- return [FILES];
751
- return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
867
+ const completing = await completingOf(run);
868
+ if ((words.at(-1) ?? "").startsWith("-"))
869
+ return Object.entries(completing.options).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
870
+ const flag = words.at(-2);
871
+ const option = flag?.startsWith("-") === true ? Object.entries(completing.options).find(([name, { short }]) => flag === `--${name}` || flag === `-${short}`)?.[1] : undefined;
872
+ if (option?.type === "string") {
873
+ const values = option.choices === undefined ? await option.candidates?.() : option.choices;
874
+ return values === undefined ? [FILES] : values.map(candidateLine);
875
+ }
876
+ const name = positionalBeingTyped(words.slice(0, -1), completing);
877
+ const values = name === undefined ? undefined : await completing.candidates[name]?.();
878
+ return values === undefined ? [FILES] : values.map(candidateLine);
752
879
  }
753
880
  async function candidates(scope, defaultCommand, words, before = []) {
754
881
  const [word = "", ...rest] = words;
@@ -759,8 +886,13 @@ async function candidates(scope, defaultCommand, words, before = []) {
759
886
  ({ commands, aliases } = await expand(scope, [...before, ...words]));
760
887
  command = commands[aliases?.[word] ?? word];
761
888
  }
762
- if (listing)
763
- return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
889
+ if (listing) {
890
+ const own = await Promise.all(Object.entries(shown(commands)).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
891
+ const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
892
+ const values = fallback !== undefined && "run" in fallback ? await leafCandidates(fallback.run, [word]) : [];
893
+ const valueOf = (line) => line.slice(0, line.includes("\t") ? line.indexOf("\t") : undefined);
894
+ return [...own, ...values.filter((line) => line !== FILES && !(valueOf(line) in commands))];
895
+ }
764
896
  if (command === undefined) {
765
897
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
766
898
  return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
@@ -775,7 +907,7 @@ async function printCandidates(found) {
775
907
  async function runCli(cli, argv = process4.argv.slice(2)) {
776
908
  if (argv[0] === COMPLETE)
777
909
  return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
778
- 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 });
910
+ 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, runBare: cli.runBare === true, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
779
911
  }
780
912
  async function runMain({ name, version, run, redact }, argv = process4.argv.slice(2)) {
781
913
  if (argv[0] === COMPLETE)
@@ -799,33 +931,16 @@ var {Glob } = globalThis.Bun;
799
931
  var SESSION_FILES = new Glob("*.json");
800
932
  var ACCOUNT_DIRS = new Glob("*/sessions");
801
933
  // package.json
802
- var version = "0.2.0";
934
+ var version = "0.3.0";
803
935
 
804
- // src/host.ts
805
- import { homedir } from "os";
806
- import { join } from "path";
936
+ // src/completions.ts
937
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, rmSync } from "fs";
938
+ import { join as join2 } from "path";
807
939
  import process6 from "process";
808
- var RUN_TIMEOUT_MS = 20000;
809
- function hostHere() {
810
- const home = homedir();
811
- return {
812
- home,
813
- globalModules: join(process6.env.BUN_INSTALL ?? join(home, ".bun"), "install", "global", "node_modules"),
814
- bunVersion: Bun.version,
815
- which: (bin) => Bun.which(bin),
816
- run: async (argv, env) => {
817
- const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process6.env, ...env } } });
818
- if (ran.timedOut)
819
- return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
820
- const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
821
- return { code: ran.code, stdout: of("stdout"), stderr: of("stderr") };
822
- }
823
- };
824
- }
825
940
 
826
941
  // src/checks.ts
827
942
  import { existsSync, readFileSync } from "fs";
828
- import { basename, join as join2 } from "path";
943
+ import { basename, join } from "path";
829
944
  var TOOLS = {
830
945
  "@kaynooo/cli-base": ["base", "proof"],
831
946
  "@kaynooo/cli-changelog": ["changelog"],
@@ -842,6 +957,7 @@ var TOOLS = {
842
957
  "@kaynooo/cli-out": ["out"],
843
958
  "@kaynooo/cli-pkg": ["pkg"],
844
959
  "@kaynooo/cli-stack": ["stack"],
960
+ "@kaynooo/cli-view": ["view"],
845
961
  "@kaynooo/cli-wait": ["waitfor", "slot"]
846
962
  };
847
963
  var FORGE = "git.kaynooo.fr";
@@ -868,12 +984,12 @@ function firstLine(text) {
868
984
  return end === -1 ? trimmed : trimmed.slice(0, end);
869
985
  }
870
986
  function installed(host, name) {
871
- return readJson(join2(host.globalModules, name, "package.json"));
987
+ return readJson(join(host.globalModules, name, "package.json"));
872
988
  }
873
989
  function ownBin(host, name, bin) {
874
990
  const declared = installed(host, name)?.bin;
875
991
  const file = typeof declared === "string" ? declared : declared?.[bin];
876
- return file === undefined ? undefined : join2(host.globalModules, name, file);
992
+ return file === undefined ? undefined : join(host.globalModules, name, file);
877
993
  }
878
994
  async function checkTool(host, name, bins) {
879
995
  const check = name.slice(name.indexOf("/") + 1);
@@ -882,7 +998,7 @@ async function checkTool(host, name, bins) {
882
998
  return { check, ok: false, detail: "not installed", fix: `bun install -g ${name}` };
883
999
  const off = bins.filter((bin) => host.which(bin) === null);
884
1000
  if (off.length > 0)
885
- return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join2(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1001
+ return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
886
1002
  const info = await host.run(["bun", "info", name, "version", "--no-cache"]);
887
1003
  const answered = info.code === 0 ? firstLine(info.stdout.trim().split(`
888
1004
  `).at(-1) ?? "") : "";
@@ -909,9 +1025,9 @@ async function checkTea(host) {
909
1025
  return { check: "tea", ok: true, detail: `logged in to ${forge} as ${login.name}, Forgejo ${served}` };
910
1026
  }
911
1027
  function checkHooks(host) {
912
- const settings = readJson(join2(host.home, ".claude", "settings.json")) ?? {};
1028
+ const settings = readJson(join(host.home, ".claude", "settings.json")) ?? {};
913
1029
  const plugin = settings.enabledPlugins?.[PLUGIN] === true;
914
- const mod = (settings.env?.CLAUDE_CODE_PLUGIN_DIRS ?? "").split(":").map((dir) => dir.replace(LEADING_TILDE, host.home)).find((dir) => basename(dir) === "cli" && existsSync(join2(dir, "hooks", "hooks.json")));
1030
+ const mod = (settings.env?.CLAUDE_CODE_PLUGIN_DIRS ?? "").split(":").map((dir) => dir.replace(LEADING_TILDE, host.home)).find((dir) => basename(dir) === "cli" && existsSync(join(dir, "hooks", "hooks.json")));
915
1031
  if (plugin && mod !== undefined)
916
1032
  return { check: "hooks", ok: false, detail: `both the ${PLUGIN} plugin and the mod in ${mod} run guard, so every call is checked twice`, fix: `claude plugin disable ${PLUGIN}` };
917
1033
  if (plugin)
@@ -927,12 +1043,82 @@ function checkBun(host) {
927
1043
  return { check: "bun", ok: false, detail: `${host.bunVersion}, the tools ask for ${[...new Set(unmet)].join(" and ")}`, fix: "bun upgrade" };
928
1044
  return { check: "bun", ok: true, detail: floors.length === 0 ? host.bunVersion : `${host.bunVersion}, the tools ask for ${[...new Set(floors)].join(" and ")}` };
929
1045
  }
1046
+ function checkCompletions(host) {
1047
+ if (host.which("fish") === null)
1048
+ return { check: "completions", ok: true, detail: "fish is not installed" };
1049
+ const dir = completionDir(host.home);
1050
+ const missing = missingCompletions(host, dir);
1051
+ if (missing.length > 0)
1052
+ return { check: "completions", ok: false, detail: `no fish completion in ${dir} for ${missing.join(", ")}`, fix: "cli completions fish --install" };
1053
+ return { check: "completions", ok: true, detail: dir };
1054
+ }
930
1055
  async function runChecks(host) {
931
1056
  const entries = Object.entries(TOOLS);
932
1057
  const tools = [];
933
1058
  for (let at = 0;at < entries.length; at += INFO_AT_ONCE)
934
1059
  tools.push(...await Promise.all(entries.slice(at, at + INFO_AT_ONCE).map(async ([name, bins]) => checkTool(host, name, bins))));
935
- return [...tools, await checkTea(host), checkHooks(host), checkBun(host)];
1060
+ return [...tools, await checkTea(host), checkHooks(host), checkBun(host), checkCompletions(host)];
1061
+ }
1062
+
1063
+ // src/completions.ts
1064
+ var MARKER = "# Generated by `cli completions fish`";
1065
+ var FISH_FILE = /\.fish$/;
1066
+ var FUNCTION = `functions -q __kaynooo_cli_complete; or function __kaynooo_cli_complete --argument-names bin --description 'what a @kaynooo/cli-* bin offers for the word being typed'
1067
+ set -l tokens (commandline -xpc 2>/dev/null; or commandline -opc)
1068
+ set -l current (commandline -ct)
1069
+ for line in ($bin __complete $tokens[2..-1] $current 2>/dev/null)
1070
+ if test "$line" = __files__
1071
+ __fish_complete_path $current
1072
+ else
1073
+ echo $line
1074
+ end
1075
+ end
1076
+ end`;
1077
+ function completionDir(home, env = process6.env) {
1078
+ return join2(env.XDG_CACHE_HOME ?? join2(home, ".cache"), "fish", "kaynooo-cli");
1079
+ }
1080
+ function installedBins(host) {
1081
+ return Object.values(TOOLS).flat().filter((bin) => host.which(bin) !== null);
1082
+ }
1083
+ function completionFile(bin) {
1084
+ return `${MARKER}
1085
+ ${FUNCTION}
1086
+ complete -c ${bin} -f -a '(__kaynooo_cli_complete ${bin})'
1087
+ `;
1088
+ }
1089
+ async function installCompletions(host, dir) {
1090
+ const bins = installedBins(host);
1091
+ mkdirSync(dir, { recursive: true });
1092
+ const removed = readdirSync(dir).filter((name) => FISH_FILE.test(name) && !bins.includes(name.replace(FISH_FILE, "")) && readFileSync2(join2(dir, name), "utf8").startsWith(MARKER));
1093
+ for (const name of removed)
1094
+ rmSync(join2(dir, name));
1095
+ await Promise.all(bins.map(async (bin) => Bun.write(join2(dir, `${bin}.fish`), completionFile(bin))));
1096
+ return { written: bins, removed: removed.map((name) => name.replace(FISH_FILE, "")) };
1097
+ }
1098
+ function missingCompletions(host, dir) {
1099
+ return installedBins(host).filter((bin) => !existsSync2(join2(dir, `${bin}.fish`)));
1100
+ }
1101
+
1102
+ // src/host.ts
1103
+ import { homedir } from "os";
1104
+ import { join as join3 } from "path";
1105
+ import process7 from "process";
1106
+ var RUN_TIMEOUT_MS = 20000;
1107
+ function hostHere() {
1108
+ const home = homedir();
1109
+ return {
1110
+ home,
1111
+ globalModules: join3(process7.env.BUN_INSTALL ?? join3(home, ".bun"), "install", "global", "node_modules"),
1112
+ bunVersion: Bun.version,
1113
+ which: (bin) => Bun.which(bin),
1114
+ run: async (argv, env) => {
1115
+ const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process7.env, ...env } } });
1116
+ if (ran.timedOut)
1117
+ return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
1118
+ const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
1119
+ return { code: ran.code, stdout: of("stdout"), stderr: of("stderr") };
1120
+ }
1121
+ };
936
1122
  }
937
1123
 
938
1124
  // src/list.ts
@@ -960,7 +1146,7 @@ var LIST_OPTIONS = {
960
1146
  json: jsonOption("[{bin, package, installed, synopsis?, summary?, replaces?, examples?}]")
961
1147
  };
962
1148
  function print(listed) {
963
- const { cyan, dim } = paint(process7.stdout);
1149
+ const { cyan, dim } = paint(process8.stdout);
964
1150
  const width = Math.max(...listed.map((each) => each.bin.length));
965
1151
  for (const each of listed) {
966
1152
  const what = !each.installed ? dim(`not installed: bun install -g ${each.package}`) : `${each.summary ?? ""}${each.replaces === undefined ? "" : dim(`; instead of ${each.replaces}`)}`;
@@ -976,18 +1162,42 @@ async function list(args, host = hostHere()) {
976
1162
  print(listed);
977
1163
  return 0;
978
1164
  }
1165
+ var COMPLETIONS_USAGE = `cli completions fish [--install] [--dir <folder>]: fish completions for every installed bin of the @kaynooo/cli-* packages
1166
+
1167
+ Each bin completes itself through \`<bin> __complete\`; this writes the one line fish needs per bin. Without --install the files print on stdout, with it they go to ~/.cache/fish/kaynooo-cli (or $XDG_CACHE_HOME), one <bin>.fish each, the stale ones this command wrote taken out. That folder has to be in fish_complete_path, first so a completion fish ships for the same name does not win: \`set -p fish_complete_path ~/.cache/fish/kaynooo-cli\`.
1168
+ Exits 0 when it printed or wrote them, 2 on a usage error.`;
1169
+ var COMPLETIONS_OPTIONS = {
1170
+ install: { type: "boolean", description: "write the files to the folder instead of printing them" },
1171
+ dir: { type: "string", short: "d", description: "the folder to write to, instead of ~/.cache/fish/kaynooo-cli" }
1172
+ };
1173
+ async function completions(args, host = hostHere()) {
1174
+ const { values, positionals } = parse(args, COMPLETIONS_USAGE, COMPLETIONS_OPTIONS, { positionals: ["<shell>"], candidates: { "<shell>": () => ["fish"] }, examples: ["cli completions fish", ["cli completions fish --install", "write one file per bin"]] });
1175
+ if (positionals[0] !== "fish")
1176
+ badUsage("only fish is supported: cli completions fish");
1177
+ if (values.install !== true) {
1178
+ console.log(installedBins(host).map(completionFile).join(`
1179
+ `));
1180
+ return 0;
1181
+ }
1182
+ const dir = values.dir ?? completionDir(host.home);
1183
+ const { written, removed } = await installCompletions(host, dir);
1184
+ console.log(`wrote ${written.length} completions to ${dir}${removed.length === 0 ? "" : `, took out ${removed.join(", ")}`}`);
1185
+ return 0;
1186
+ }
979
1187
  if (import.meta.main) {
980
- process7.exitCode = await runCli({
1188
+ process8.exitCode = await runCli({
981
1189
  name: "cli",
982
1190
  version,
983
1191
  about: "the bins of the @kaynooo/cli-* packages, described by their own help",
984
1192
  replaces: "running each bin's --help to find the one that fits",
985
1193
  examples: ["cli list", ["cli list --json", "with each bin's synopsis and examples"]],
986
1194
  commands: {
987
- list: { summary: "every bin in one line: what it does and what it is reached for instead of", run: async (args) => list(args) }
1195
+ list: { summary: "every bin in one line: what it does and what it is reached for instead of", run: async (args) => list(args) },
1196
+ completions: { summary: "fish completions for every installed bin, printed or written to the folder fish reads", run: async (args) => completions(args) }
988
1197
  }
989
1198
  });
990
1199
  }
991
1200
  export {
1201
+ completions,
992
1202
  list
993
1203
  };
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli.ts
5
- import process7 from "process";
5
+ import process8 from "process";
6
6
 
7
7
  // ../kit/src/args.ts
8
8
  import process2 from "process";
@@ -11,7 +11,7 @@ import { parseArgs } from "util";
11
11
  // ../kit/src/style.ts
12
12
  import process from "process";
13
13
 
14
- // ../../node_modules/.bun/@kaynooo+utils@2.31.0+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
14
+ // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
15
15
  function x(s) {
16
16
  let r = s.startsWith("#") ? s.slice(1).toLowerCase() : s.toLowerCase();
17
17
  if (!/^[a-f0-9]+$/.test(r))
@@ -231,7 +231,7 @@ class e {
231
231
  }
232
232
  }
233
233
 
234
- // ../../node_modules/.bun/@kaynooo+utils@2.31.0+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
234
+ // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
235
235
  var r = 39;
236
236
  var o = 49;
237
237
  var f2 = "\x1B[";
@@ -349,8 +349,12 @@ class HelpShown extends Exit {
349
349
 
350
350
  class Completing {
351
351
  options;
352
- constructor(options) {
352
+ positionals;
353
+ candidates;
354
+ constructor(options, positionals = [], candidates = {}) {
353
355
  this.options = options;
356
+ this.positionals = positionals;
357
+ this.candidates = candidates;
354
358
  }
355
359
  }
356
360
  var completion = { active: false };
@@ -393,8 +397,10 @@ function exampleRows(examples, colors = paint(process2.stdout)) {
393
397
  function optionRows(options, colors = paint(process2.stdout)) {
394
398
  const rows = Object.entries(options).map(([name, option]) => {
395
399
  const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
396
- const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
397
- return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
400
+ const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
401
+ const placeholder = option.optional === undefined ? named : `[${named}]`;
402
+ const value = option.type === "string" ? ` ${colors.dim(placeholder)}` : "";
403
+ return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? placeholder.length + 1 : 0), description: option.description ?? "" };
398
404
  });
399
405
  const width = Math.max(0, ...rows.map((row) => row.width));
400
406
  return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
@@ -420,14 +426,13 @@ function applyOutput(color, plain, env = process2.env) {
420
426
  function parse(args, usage, options, settings = {}) {
421
427
  const output = Object.fromEntries(Object.entries(OUTPUT_OPTIONS).filter(([name]) => !(name in options)));
422
428
  const all = { ...options, ...output, help: { type: "boolean", short: "h", description: "show this help" } };
429
+ const listing = listed(all, settings.allowNegative === true);
423
430
  if (completion.active)
424
- throw new Completing(all);
425
- const separator = settings.rest === true ? args.indexOf("--") : -1;
426
- const own = separator === -1 ? args : args.slice(0, separator);
427
- const rest = separator === -1 ? undefined : args.slice(separator + 1);
431
+ throw new Completing(listing, settings.positionals, settings.candidates);
432
+ const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
428
433
  let parsed;
429
434
  try {
430
- parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
435
+ parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
431
436
  } catch (error) {
432
437
  const message = parseError(error);
433
438
  throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
@@ -435,12 +440,84 @@ function parse(args, usage, options, settings = {}) {
435
440
  const values = parsed.values;
436
441
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
437
442
  if (values.help === true) {
438
- console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, all, undefined, settings));
443
+ console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
439
444
  throw new HelpShown;
440
445
  }
446
+ checkChoices(parsed.values, all);
447
+ checkConflicts(parsed.values, all);
441
448
  if (settings.positionals !== undefined)
442
449
  checkPositionals(parsed.positionals, settings.positionals);
443
- return { ...parsed, positionals: parsed.positionals, rest };
450
+ return { ...parsed, values: parsed.values, positionals: parsed.positionals, rest };
451
+ }
452
+ function splitRest(args, passes) {
453
+ const separator = passes ? args.indexOf("--") : -1;
454
+ return { own: separator === -1 ? args : args.slice(0, separator), rest: separator === -1 ? undefined : args.slice(separator + 1) };
455
+ }
456
+ function splitTrailing(args, options, count) {
457
+ const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
458
+ let seen = 0;
459
+ if (count === 0)
460
+ return { own: args, rest: undefined };
461
+ for (let at = 0;at < args.length; at++) {
462
+ const word = args[at];
463
+ if (word === "--")
464
+ break;
465
+ if (word.startsWith("-")) {
466
+ if (takesValue(word))
467
+ at++;
468
+ continue;
469
+ }
470
+ seen++;
471
+ if (seen === count)
472
+ return { own: args.slice(0, at + 1), rest: args.slice(at + 1) };
473
+ }
474
+ return { own: args, rest: undefined };
475
+ }
476
+ function listed(options, allowNegative) {
477
+ if (!allowNegative)
478
+ return options;
479
+ return Object.fromEntries(Object.entries(options).flatMap(([name, option]) => {
480
+ if (option.type !== "boolean" || option.negative === undefined)
481
+ return [[name, option]];
482
+ return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
483
+ }));
484
+ }
485
+ function withBare(args, options) {
486
+ const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
487
+ if (bare.size === 0)
488
+ return args;
489
+ const separator = args.indexOf("--");
490
+ const own = separator === -1 ? args : args.slice(0, separator);
491
+ return [...own.flatMap((arg, index) => {
492
+ const optional = bare.get(arg);
493
+ const next = own[index + 1];
494
+ const value = next !== undefined && (optional?.accepts === undefined ? !next.startsWith("-") : optional.accepts.test(next));
495
+ return optional === undefined || value ? [arg] : [arg, optional.bare];
496
+ }), ...separator === -1 ? [] : args.slice(separator)];
497
+ }
498
+ function checkConflicts(values, options) {
499
+ const given = (name) => values[name] !== undefined && values[name] !== false;
500
+ for (const [name, { conflicts = [] }] of Object.entries(options)) {
501
+ const other = given(name) ? conflicts.find(given) : undefined;
502
+ if (other !== undefined)
503
+ badUsage(`--${name} cannot be used with --${other}`);
504
+ }
505
+ }
506
+ function checkChoices(values, options) {
507
+ for (const [name, option] of Object.entries(options)) {
508
+ if (option.choices === undefined)
509
+ continue;
510
+ const given = values[name];
511
+ for (const value of Array.isArray(given) ? given : [given]) {
512
+ if (typeof value === "string")
513
+ oneOf(value, name, option.choices);
514
+ }
515
+ }
516
+ }
517
+ function oneOf(value, name, choices) {
518
+ if (!choices.includes(value))
519
+ badUsage(`${name.startsWith("<") ? name : `--${name}`} takes one of ${choices.join(", ")}, not ${value}`);
520
+ return value;
444
521
  }
445
522
  function checkPositionals(given, names) {
446
523
  const variadic = names.at(-1)?.replace("]", "").endsWith("...") === true;
@@ -591,8 +668,9 @@ var LEVEL_OPTIONS = {
591
668
  help: { type: "boolean", short: "h", description: "show this help" }
592
669
  };
593
670
  var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
594
- async function commandRows(commands, colors) {
595
- const width = Math.max(...Object.keys(commands).map((name) => name.length));
671
+ async function commandRows(all, colors) {
672
+ const commands = shown(all);
673
+ const width = Math.max(0, ...Object.keys(commands).map((name) => name.length));
596
674
  const groups = new Map([["Commands:", []]]);
597
675
  const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
598
676
  for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
@@ -603,6 +681,9 @@ async function commandRows(commands, colors) {
603
681
  }
604
682
  return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
605
683
  }
684
+ function shown(commands) {
685
+ return Object.fromEntries(Object.entries(commands).filter(([, { hidden }]) => hidden !== true));
686
+ }
606
687
  function takeOutputFlags(argv) {
607
688
  let index = 0;
608
689
  for (;index < argv.length; index++) {
@@ -694,6 +775,14 @@ async function dispatch(level, typed, { version, redact }) {
694
775
  return reportThrown(prefix, error);
695
776
  }
696
777
  const [name, ...args] = argv;
778
+ const fallback = level.defaultCommand === undefined ? undefined : level.commands[level.defaultCommand];
779
+ if (name === undefined && level.runBare && fallback !== undefined && "run" in fallback) {
780
+ try {
781
+ return await fallback.run([]);
782
+ } catch (error) {
783
+ return reportThrown(`${prefix} ${level.defaultCommand}`, error, redact, false);
784
+ }
785
+ }
697
786
  if (name === undefined && level.path.length > 1) {
698
787
  console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stderr));
699
788
  return 2;
@@ -708,6 +797,7 @@ async function dispatch(level, typed, { version, redact }) {
708
797
  }
709
798
  let command = level.commands[level.aliases?.[name] ?? name];
710
799
  let rest = args;
800
+ let ran = name;
711
801
  if (command === undefined && level.lazy.length > 0) {
712
802
  level = await expand(level, [...level.path.slice(1), ...argv]);
713
803
  command = level.commands[level.aliases?.[name] ?? name];
@@ -715,40 +805,77 @@ async function dispatch(level, typed, { version, redact }) {
715
805
  if (command === undefined && level.defaultCommand !== undefined) {
716
806
  command = level.commands[level.defaultCommand];
717
807
  rest = argv;
808
+ ran = level.defaultCommand;
718
809
  }
719
810
  if (command === undefined) {
720
- const meant = closest(name, Object.keys(level.commands));
811
+ const meant = closest(name, Object.keys(shown(level.commands)));
721
812
  printError(prefix, `no command ${name}`, meant === undefined ? `run ${prefix} --help for its commands` : `did you mean ${meant}?`);
722
813
  return 2;
723
814
  }
724
815
  if ("commands" in command) {
725
816
  const { summary } = command;
726
- 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 });
817
+ return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, runBare: false, examples: command.examples, replaces: undefined }, rest, { version, redact });
727
818
  }
728
819
  try {
729
820
  return await command.run(rest);
730
821
  } catch (error) {
731
- return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
822
+ return reportThrown(`${prefix} ${ran}`, error, redact, wantsJson(rest));
732
823
  }
733
824
  }
734
825
  var COMPLETE = "__complete";
735
826
  var FILES = "__files__";
736
- async function optionsOf(run) {
827
+ async function completingOf(run) {
737
828
  completion.active = true;
738
829
  try {
739
830
  await run([]);
740
831
  } catch (error) {
741
832
  if (error instanceof Completing)
742
- return error.options;
833
+ return error;
743
834
  } finally {
744
835
  completion.active = false;
745
836
  }
746
- return {};
837
+ return new Completing({});
838
+ }
839
+ async function optionsOf(run) {
840
+ return (await completingOf(run)).options;
841
+ }
842
+ function candidateLine(candidate) {
843
+ return typeof candidate === "string" ? candidate : candidate.join("\t");
844
+ }
845
+ function positionalBeingTyped(before, { options, positionals }) {
846
+ const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
847
+ let index = 0;
848
+ let at = 0;
849
+ while (at < before.length) {
850
+ const word = before[at];
851
+ if (word === "--") {
852
+ index += before.length - at - 1;
853
+ break;
854
+ }
855
+ if (!word.startsWith("-"))
856
+ index++;
857
+ else if (takesValue(word))
858
+ at++;
859
+ at++;
860
+ }
861
+ const last = positionals.at(-1);
862
+ if (index < positionals.length)
863
+ return positionals[index];
864
+ return last?.replace("]", "").endsWith("...") === true ? last : undefined;
747
865
  }
748
866
  async function leafCandidates(run, words) {
749
- if (!(words.at(-1) ?? "").startsWith("-"))
750
- return [FILES];
751
- return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
867
+ const completing = await completingOf(run);
868
+ if ((words.at(-1) ?? "").startsWith("-"))
869
+ return Object.entries(completing.options).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
870
+ const flag = words.at(-2);
871
+ const option = flag?.startsWith("-") === true ? Object.entries(completing.options).find(([name, { short }]) => flag === `--${name}` || flag === `-${short}`)?.[1] : undefined;
872
+ if (option?.type === "string") {
873
+ const values = option.choices === undefined ? await option.candidates?.() : option.choices;
874
+ return values === undefined ? [FILES] : values.map(candidateLine);
875
+ }
876
+ const name = positionalBeingTyped(words.slice(0, -1), completing);
877
+ const values = name === undefined ? undefined : await completing.candidates[name]?.();
878
+ return values === undefined ? [FILES] : values.map(candidateLine);
752
879
  }
753
880
  async function candidates(scope, defaultCommand, words, before = []) {
754
881
  const [word = "", ...rest] = words;
@@ -759,8 +886,13 @@ async function candidates(scope, defaultCommand, words, before = []) {
759
886
  ({ commands, aliases } = await expand(scope, [...before, ...words]));
760
887
  command = commands[aliases?.[word] ?? word];
761
888
  }
762
- if (listing)
763
- return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
889
+ if (listing) {
890
+ const own = await Promise.all(Object.entries(shown(commands)).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
891
+ const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
892
+ const values = fallback !== undefined && "run" in fallback ? await leafCandidates(fallback.run, [word]) : [];
893
+ const valueOf = (line) => line.slice(0, line.includes("\t") ? line.indexOf("\t") : undefined);
894
+ return [...own, ...values.filter((line) => line !== FILES && !(valueOf(line) in commands))];
895
+ }
764
896
  if (command === undefined) {
765
897
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
766
898
  return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
@@ -775,7 +907,7 @@ async function printCandidates(found) {
775
907
  async function runCli(cli, argv = process4.argv.slice(2)) {
776
908
  if (argv[0] === COMPLETE)
777
909
  return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
778
- 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 });
910
+ 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, runBare: cli.runBare === true, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
779
911
  }
780
912
  async function runMain({ name, version, run, redact }, argv = process4.argv.slice(2)) {
781
913
  if (argv[0] === COMPLETE)
@@ -799,11 +931,55 @@ var {Glob } = globalThis.Bun;
799
931
  var SESSION_FILES = new Glob("*.json");
800
932
  var ACCOUNT_DIRS = new Glob("*/sessions");
801
933
  // package.json
802
- var version = "0.2.0";
934
+ var version = "0.3.0";
935
+
936
+ // src/checks.ts
937
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
938
+ import { basename, join as join2 } from "path";
939
+
940
+ // src/completions.ts
941
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "fs";
942
+ import { join } from "path";
943
+ import process6 from "process";
944
+ var MARKER = "# Generated by `cli completions fish`";
945
+ var FISH_FILE = /\.fish$/;
946
+ var FUNCTION = `functions -q __kaynooo_cli_complete; or function __kaynooo_cli_complete --argument-names bin --description 'what a @kaynooo/cli-* bin offers for the word being typed'
947
+ set -l tokens (commandline -xpc 2>/dev/null; or commandline -opc)
948
+ set -l current (commandline -ct)
949
+ for line in ($bin __complete $tokens[2..-1] $current 2>/dev/null)
950
+ if test "$line" = __files__
951
+ __fish_complete_path $current
952
+ else
953
+ echo $line
954
+ end
955
+ end
956
+ end`;
957
+ function completionDir(home, env = process6.env) {
958
+ return join(env.XDG_CACHE_HOME ?? join(home, ".cache"), "fish", "kaynooo-cli");
959
+ }
960
+ function installedBins(host) {
961
+ return Object.values(TOOLS).flat().filter((bin) => host.which(bin) !== null);
962
+ }
963
+ function completionFile(bin) {
964
+ return `${MARKER}
965
+ ${FUNCTION}
966
+ complete -c ${bin} -f -a '(__kaynooo_cli_complete ${bin})'
967
+ `;
968
+ }
969
+ async function installCompletions(host, dir) {
970
+ const bins = installedBins(host);
971
+ mkdirSync(dir, { recursive: true });
972
+ const removed = readdirSync(dir).filter((name) => FISH_FILE.test(name) && !bins.includes(name.replace(FISH_FILE, "")) && readFileSync(join(dir, name), "utf8").startsWith(MARKER));
973
+ for (const name of removed)
974
+ rmSync(join(dir, name));
975
+ await Promise.all(bins.map(async (bin) => Bun.write(join(dir, `${bin}.fish`), completionFile(bin))));
976
+ return { written: bins, removed: removed.map((name) => name.replace(FISH_FILE, "")) };
977
+ }
978
+ function missingCompletions(host, dir) {
979
+ return installedBins(host).filter((bin) => !existsSync(join(dir, `${bin}.fish`)));
980
+ }
803
981
 
804
982
  // src/checks.ts
805
- import { existsSync, readFileSync } from "fs";
806
- import { basename, join } from "path";
807
983
  var TOOLS = {
808
984
  "@kaynooo/cli-base": ["base", "proof"],
809
985
  "@kaynooo/cli-changelog": ["changelog"],
@@ -820,6 +996,7 @@ var TOOLS = {
820
996
  "@kaynooo/cli-out": ["out"],
821
997
  "@kaynooo/cli-pkg": ["pkg"],
822
998
  "@kaynooo/cli-stack": ["stack"],
999
+ "@kaynooo/cli-view": ["view"],
823
1000
  "@kaynooo/cli-wait": ["waitfor", "slot"]
824
1001
  };
825
1002
  var FORGE = "git.kaynooo.fr";
@@ -837,7 +1014,7 @@ function readJsonText(text) {
837
1014
  }
838
1015
  }
839
1016
  function readJson(path) {
840
- return existsSync(path) ? readJsonText(readFileSync(path, "utf8")) : undefined;
1017
+ return existsSync2(path) ? readJsonText(readFileSync2(path, "utf8")) : undefined;
841
1018
  }
842
1019
  function firstLine(text) {
843
1020
  const trimmed = text.trim();
@@ -846,12 +1023,12 @@ function firstLine(text) {
846
1023
  return end === -1 ? trimmed : trimmed.slice(0, end);
847
1024
  }
848
1025
  function installed(host, name) {
849
- return readJson(join(host.globalModules, name, "package.json"));
1026
+ return readJson(join2(host.globalModules, name, "package.json"));
850
1027
  }
851
1028
  function ownBin(host, name, bin) {
852
1029
  const declared = installed(host, name)?.bin;
853
1030
  const file = typeof declared === "string" ? declared : declared?.[bin];
854
- return file === undefined ? undefined : join(host.globalModules, name, file);
1031
+ return file === undefined ? undefined : join2(host.globalModules, name, file);
855
1032
  }
856
1033
  async function checkTool(host, name, bins) {
857
1034
  const check = name.slice(name.indexOf("/") + 1);
@@ -860,7 +1037,7 @@ async function checkTool(host, name, bins) {
860
1037
  return { check, ok: false, detail: "not installed", fix: `bun install -g ${name}` };
861
1038
  const off = bins.filter((bin) => host.which(bin) === null);
862
1039
  if (off.length > 0)
863
- return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1040
+ return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join2(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
864
1041
  const info = await host.run(["bun", "info", name, "version", "--no-cache"]);
865
1042
  const answered = info.code === 0 ? firstLine(info.stdout.trim().split(`
866
1043
  `).at(-1) ?? "") : "";
@@ -887,9 +1064,9 @@ async function checkTea(host) {
887
1064
  return { check: "tea", ok: true, detail: `logged in to ${forge} as ${login.name}, Forgejo ${served}` };
888
1065
  }
889
1066
  function checkHooks(host) {
890
- const settings = readJson(join(host.home, ".claude", "settings.json")) ?? {};
1067
+ const settings = readJson(join2(host.home, ".claude", "settings.json")) ?? {};
891
1068
  const plugin = settings.enabledPlugins?.[PLUGIN] === true;
892
- const mod = (settings.env?.CLAUDE_CODE_PLUGIN_DIRS ?? "").split(":").map((dir) => dir.replace(LEADING_TILDE, host.home)).find((dir) => basename(dir) === "cli" && existsSync(join(dir, "hooks", "hooks.json")));
1069
+ const mod = (settings.env?.CLAUDE_CODE_PLUGIN_DIRS ?? "").split(":").map((dir) => dir.replace(LEADING_TILDE, host.home)).find((dir) => basename(dir) === "cli" && existsSync2(join2(dir, "hooks", "hooks.json")));
893
1070
  if (plugin && mod !== undefined)
894
1071
  return { check: "hooks", ok: false, detail: `both the ${PLUGIN} plugin and the mod in ${mod} run guard, so every call is checked twice`, fix: `claude plugin disable ${PLUGIN}` };
895
1072
  if (plugin)
@@ -905,28 +1082,37 @@ function checkBun(host) {
905
1082
  return { check: "bun", ok: false, detail: `${host.bunVersion}, the tools ask for ${[...new Set(unmet)].join(" and ")}`, fix: "bun upgrade" };
906
1083
  return { check: "bun", ok: true, detail: floors.length === 0 ? host.bunVersion : `${host.bunVersion}, the tools ask for ${[...new Set(floors)].join(" and ")}` };
907
1084
  }
1085
+ function checkCompletions(host) {
1086
+ if (host.which("fish") === null)
1087
+ return { check: "completions", ok: true, detail: "fish is not installed" };
1088
+ const dir = completionDir(host.home);
1089
+ const missing = missingCompletions(host, dir);
1090
+ if (missing.length > 0)
1091
+ return { check: "completions", ok: false, detail: `no fish completion in ${dir} for ${missing.join(", ")}`, fix: "cli completions fish --install" };
1092
+ return { check: "completions", ok: true, detail: dir };
1093
+ }
908
1094
  async function runChecks(host) {
909
1095
  const entries = Object.entries(TOOLS);
910
1096
  const tools = [];
911
1097
  for (let at = 0;at < entries.length; at += INFO_AT_ONCE)
912
1098
  tools.push(...await Promise.all(entries.slice(at, at + INFO_AT_ONCE).map(async ([name, bins]) => checkTool(host, name, bins))));
913
- return [...tools, await checkTea(host), checkHooks(host), checkBun(host)];
1099
+ return [...tools, await checkTea(host), checkHooks(host), checkBun(host), checkCompletions(host)];
914
1100
  }
915
1101
 
916
1102
  // src/host.ts
917
1103
  import { homedir } from "os";
918
- import { join as join2 } from "path";
919
- import process6 from "process";
1104
+ import { join as join3 } from "path";
1105
+ import process7 from "process";
920
1106
  var RUN_TIMEOUT_MS = 20000;
921
1107
  function hostHere() {
922
1108
  const home = homedir();
923
1109
  return {
924
1110
  home,
925
- globalModules: join2(process6.env.BUN_INSTALL ?? join2(home, ".bun"), "install", "global", "node_modules"),
1111
+ globalModules: join3(process7.env.BUN_INSTALL ?? join3(home, ".bun"), "install", "global", "node_modules"),
926
1112
  bunVersion: Bun.version,
927
1113
  which: (bin) => Bun.which(bin),
928
1114
  run: async (argv, env) => {
929
- const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process6.env, ...env } } });
1115
+ const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process7.env, ...env } } });
930
1116
  if (ran.timedOut)
931
1117
  return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
932
1118
  const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
@@ -945,7 +1131,7 @@ var OPTIONS = {
945
1131
  json: jsonOption("[{check, ok, detail, fix?}]")
946
1132
  };
947
1133
  function print(results) {
948
- const { green, red, dim } = paint(process7.stdout);
1134
+ const { green, red, dim } = paint(process8.stdout);
949
1135
  const width = Math.max(...results.map((result) => result.check.length));
950
1136
  for (const { check, ok, detail, fix } of results)
951
1137
  console.log(`${ok ? green("ok ") : red("FAIL")} ${check.padEnd(width)} ${detail}${fix === undefined ? "" : ` ${dim("fix:")} ${fix}`}`);
@@ -964,7 +1150,7 @@ async function main(args, host = hostHere()) {
964
1150
  return results.every((result) => result.ok) ? 0 : 1;
965
1151
  }
966
1152
  if (import.meta.main)
967
- process7.exitCode = await runMain({ name: "doctor", version, run: async (args) => main(args) });
1153
+ process8.exitCode = await runMain({ name: "doctor", version, run: async (args) => main(args) });
968
1154
  export {
969
1155
  main
970
1156
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaynooo/cli-doctor",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "doctor: checks the @kaynooo/cli-* install of a host: each bin's version against the registry, tea's login to Forgejo, the Claude Code hooks and the Bun floor; cli list: every bin in one line, read from its own help.",
6
6
  "author": "Kaynooo <contact@kaynooo.fr>",
7
7
  "license": "MIT",
@@ -36,6 +36,6 @@
36
36
  "doctor": "$npm_execpath src/cli.ts"
37
37
  },
38
38
  "devDependencies": {
39
- "@kaynooo/cli-kit": "0.15.0"
39
+ "@kaynooo/cli-kit": "0.19.0"
40
40
  }
41
41
  }