@kaynooo/cli-doctor 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # doctor
2
2
 
3
- `doctor` checks a host's @kaynooo/cli-* install in one call: one line per check, the command that fixes a failing one beside it.
3
+ `doctor` checks a host's @kaynooo/cli-* install in one call: one line per check, the command that fixes a failing one beside it. The package also ships a second bin, `cli` (`cli stats`, `cli list`, below), which is not `doctor`.
4
4
 
5
5
  ```sh
6
6
  bun install -g @kaynooo/cli-doctor
7
7
  doctor # one line per check
8
8
  doctor --json # [{check, ok, detail, fix?}]
9
+ doctor -q # only the failing checks, nothing when all pass
10
+ doctor --only tea --only bun # just these checks (a package such as cli-forgejo, tea, hooks, bun, completions)
11
+ doctor --fix --dry-run # what --fix would run
12
+ doctor --fix # run the fixes, then check again
9
13
  ```
10
14
 
11
15
  | Check | Passes when | Fix |
@@ -17,6 +21,20 @@ doctor --json # [{check, ok, detail, fix?}]
17
21
 
18
22
  A registry that does not answer leaves a package passing, with a note. Exit codes: 0 every check passed, 1 one failed, 2 a usage error.
19
23
 
24
+ ## --fix
25
+
26
+ `--fix` runs the `fix:` lines it may: the `bun install -g` ones (joined into one call), `cli completions fish --install`, and `claude plugin install` or `disable`. It then checks again, prints the checks as they stand and exits on those. Nothing that installs a system package is run, and a line that only describes a step (`tea` from its releases page, adding a folder to the PATH, `bun upgrade`, which replaces Bun itself) is printed as `not run`. `--dry-run` prints each line as `would` and runs nothing (alone it implies `--fix`).
27
+ `--json` keeps its shape and prints the checks after the fixes, the fixes going to stderr; `--quiet` filters it to the failing ones.
28
+
29
+ ## cli completions
30
+
31
+ ```sh
32
+ cli completions fish --install # one file per installed bin in ~/.cache/fish/kaynooo-cli
33
+ source <(cli completions bash) # in ~/.bashrc
34
+ ```
35
+
36
+ Each bin completes itself through `<bin> __complete`. fish gets a file per bin (put the folder first in `fish_complete_path`), bash one script sourced from `~/.bashrc`. zsh has no script of its own: load `compinit` and `bashcompinit` in `~/.zshrc`, then the bash line above (not tested, zsh is not installed on the machine this was written on). The fish files are checked by `doctor`.
37
+
20
38
  ## cli stats
21
39
 
22
40
  ```sh
package/dist/cli-bin.js CHANGED
@@ -2,17 +2,16 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli-bin.ts
5
- import process11 from "process";
6
-
5
+ import process12 from "process";
7
6
  // ../kit/src/args.ts
8
- import process4 from "process";
7
+ import process5 from "process";
9
8
  import { parseArgs } from "util";
10
9
 
11
10
  // ../kit/src/format.ts
12
- import process2 from "process";
11
+ import process3 from "process";
13
12
 
14
13
  // ../kit/src/style.ts
15
- import process from "process";
14
+ import process2 from "process";
16
15
 
17
16
  // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
18
17
  function x(s) {
@@ -276,10 +275,10 @@ function le(i = {}) {
276
275
  var t = le();
277
276
 
278
277
  // ../kit/src/style.ts
279
- function takesControlCodes(stream, env = process.env) {
278
+ function takesControlCodes(stream, env = process2.env) {
280
279
  return stream.isTTY === true && env.NO_COLOR === undefined && env.NODE_DISABLE_COLORS === undefined && env.TERM !== "dumb";
281
280
  }
282
- function paint(stream, env = process.env) {
281
+ function paint(stream, env = process2.env) {
283
282
  return le({ stream: { isTTY: takesControlCodes(stream, env) }, env });
284
283
  }
285
284
  var STATUSES = {
@@ -312,7 +311,7 @@ function table(rows, { cellWidth, header = false, align = [], fit } = {}) {
312
311
  widths[index] = Math.max(widths[index] ?? 0, textWidth(cell));
313
312
  }
314
313
  const shown = fit === undefined ? cells : narrow(cells, widths, fit);
315
- const colors = paint(process2.stdout);
314
+ const colors = paint(process3.stdout);
316
315
  const lines = shown.map((row) => row.map((cell, index) => {
317
316
  const width = widths[index] ?? 0;
318
317
  const gap = " ".repeat(Math.max(0, width - textWidth(cell)));
@@ -344,24 +343,30 @@ function printJson(value) {
344
343
  }
345
344
 
346
345
  // ../kit/src/help.ts
347
- import process3 from "process";
346
+ import process4 from "process";
348
347
  var OPTIONAL = /\[[^\]]+\]/g;
349
348
  var USAGE_PREFIX = /^usage:\s*/i;
350
349
  var FULL_STOP = /\.$/;
351
- function usageLine(usage, colors = paint(process3.stdout)) {
350
+ function usageLine(usage, colors = paint(process4.stdout)) {
352
351
  return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
353
352
  }
354
- function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
355
- return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
353
+ function helpText(usage, options, colors = paint(process4.stdout), { examples = [], replaces, exits = [] } = {}) {
354
+ return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exitRows(exits, colors), ...exampleRows(examples, colors)].join(`
356
355
  `);
357
356
  }
358
- function replacesRows(replaces, colors = paint(process3.stdout)) {
357
+ function exitRows(exits, colors = paint(process4.stdout)) {
358
+ if (exits.length === 0)
359
+ return [];
360
+ const width = Math.max(...exits.map(([code]) => String(code).length));
361
+ return ["", colors.bold("Exits:"), ...exits.map(([code, meaning]) => ` ${colors.cyan(String(code).padEnd(width))} ${meaning}`)];
362
+ }
363
+ function replacesRows(replaces, colors = paint(process4.stdout)) {
359
364
  return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
360
365
  }
361
- function wantsHelpJson(env = process3.env) {
366
+ function wantsHelpJson(env = process4.env) {
362
367
  return env.KIT_HELP === "json";
363
368
  }
364
- function description(usage, { examples = [], replaces } = {}) {
369
+ function description(usage, { examples = [], replaces, exits } = {}, options) {
365
370
  const lines = usage.replace(USAGE_PREFIX, "").split(`
366
371
  `);
367
372
  const first = lines[0];
@@ -372,15 +377,17 @@ function description(usage, { examples = [], replaces } = {}) {
372
377
  synopsis: colon === -1 ? first : first.slice(0, colon),
373
378
  summary: colon === -1 ? fallback : first.slice(colon + 2),
374
379
  ...replaces === undefined ? {} : { replaces },
375
- examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
380
+ examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] }),
381
+ ...exits === undefined ? {} : { exits: exits.map(([code, meaning]) => ({ code, meaning })) },
382
+ ...options === undefined ? {} : { options: Object.entries(options).map(([name, { short, description }]) => ({ name, ...short === undefined ? {} : { short }, ...description === undefined ? {} : { description } })) }
376
383
  };
377
384
  }
378
- function exampleRows(examples, colors = paint(process3.stdout)) {
385
+ function exampleRows(examples, colors = paint(process4.stdout)) {
379
386
  if (examples.length === 0)
380
387
  return [];
381
388
  return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
382
389
  }
383
- function optionRows(options, colors = paint(process3.stdout)) {
390
+ function optionRows(options, colors = paint(process4.stdout)) {
384
391
  const rows = Object.entries(options).map(([name, option]) => {
385
392
  const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
386
393
  const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
@@ -469,7 +476,7 @@ var OUTPUT_OPTIONS = {
469
476
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
470
477
  plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
471
478
  };
472
- function applyOutput(color, plain, env = process4.env) {
479
+ function applyOutput(color, plain, env = process5.env) {
473
480
  const when = plain === true ? "never" : color ?? "auto";
474
481
  if (when === "auto")
475
482
  return;
@@ -492,7 +499,7 @@ function parse(args, usage, options, settings = {}) {
492
499
  const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
493
500
  let parsed;
494
501
  try {
495
- parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
502
+ parsed = parseArgs({ args: joinDashValues(withBare(withAliases(own, all), all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
496
503
  } catch (error) {
497
504
  const message = parseError(error);
498
505
  throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
@@ -500,7 +507,7 @@ function parse(args, usage, options, settings = {}) {
500
507
  const values = parsed.values;
501
508
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
502
509
  if (values.help === true) {
503
- console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
510
+ console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings, listing)) : helpText(usage, listing, undefined, settings));
504
511
  throw new HelpShown;
505
512
  }
506
513
  checkChoices(parsed.values, all);
@@ -542,6 +549,18 @@ function listed(options, allowNegative) {
542
549
  return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
543
550
  }));
544
551
  }
552
+ function withAliases(args, options) {
553
+ const named = new Map(Object.entries(options).flatMap(([name, { aliases = [] }]) => aliases.map((alias) => [`--${alias}`, `--${name}`])));
554
+ if (named.size === 0)
555
+ return args;
556
+ const separator = args.indexOf("--");
557
+ const own = separator === -1 ? args : args.slice(0, separator);
558
+ return [...own.map((arg) => {
559
+ const at = arg.indexOf("=");
560
+ const current = named.get(at === -1 ? arg : arg.slice(0, at));
561
+ return current === undefined ? arg : `${current}${at === -1 ? "" : arg.slice(at)}`;
562
+ }), ...separator === -1 ? [] : args.slice(separator)];
563
+ }
545
564
  function withBare(args, options) {
546
565
  const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
547
566
  if (bare.size === 0)
@@ -628,7 +647,7 @@ function nearestOption(message, options, allowNegative) {
628
647
  }
629
648
  // ../kit/src/process.ts
630
649
  import { constants } from "os";
631
- import process5 from "process";
650
+ import process6 from "process";
632
651
  var KILL_GRACE_MS = 2000;
633
652
  function exitCodeOf(code, signal) {
634
653
  if (code !== null)
@@ -648,7 +667,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal,
648
667
  return;
649
668
  try {
650
669
  if (grouped)
651
- process5.kill(-proc.pid, killing);
670
+ process6.kill(-proc.pid, killing);
652
671
  else
653
672
  proc.kill(killing);
654
673
  } catch {}
@@ -722,13 +741,15 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
722
741
  var RESET_STYLE = `${ESC}[0m`;
723
742
  var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
724
743
  // ../kit/src/cli.ts
725
- import process7 from "process";
744
+ import { readFileSync, realpathSync } from "fs";
745
+ import { dirname as dirname2, join as join2 } from "path";
746
+ import process8 from "process";
726
747
 
727
748
  // ../kit/src/usage.ts
728
749
  import { appendFileSync, mkdirSync } from "fs";
729
750
  import { homedir } from "os";
730
751
  import { dirname, isAbsolute, join } from "path";
731
- import process6 from "process";
752
+ import process7 from "process";
732
753
 
733
754
  // ../kit/src/logs.ts
734
755
  var DAY_MS = 24 * 60 * 60 * 1000;
@@ -742,14 +763,14 @@ var usageError;
742
763
  function noteUsageError(message) {
743
764
  usageError = message;
744
765
  }
745
- function stateHome(env = process6.env) {
766
+ function stateHome(env = process7.env) {
746
767
  const dir = env.XDG_STATE_HOME;
747
768
  return dir !== undefined && isAbsolute(dir) ? dir : join(homedir(), ".local", "state");
748
769
  }
749
- function usagePath(env = process6.env) {
770
+ function usagePath(env = process7.env) {
750
771
  return join(stateHome(env), CLI_FOLDER, "usage.jsonl");
751
772
  }
752
- function usageRecorded(env = process6.env) {
773
+ function usageRecorded(env = process7.env) {
753
774
  const off = env.KAYNOOO_CLI_NO_USAGE;
754
775
  return env.NODE_ENV !== "test" && (off === undefined || off === "" || off === "0");
755
776
  }
@@ -763,7 +784,7 @@ function usageEntry(bin, command, argv, code, now = new Date) {
763
784
  usageError = undefined;
764
785
  return { at: now.toISOString(), bin, ...command === undefined ? {} : { command }, flags: flagNames(argv), code, ...unknown === undefined ? {} : { unknown } };
765
786
  }
766
- function recordUsage(entry, env = process6.env) {
787
+ function recordUsage(entry, env = process7.env) {
767
788
  if (!usageRecorded(env))
768
789
  return;
769
790
  try {
@@ -890,8 +911,8 @@ async function help2({ path, header, summary: lazySummary, commands, defaultComm
890
911
  `);
891
912
  }
892
913
  function printError(prefix, message, hint, cause) {
893
- const colors = paint(process7.stderr);
894
- const styled = takesControlCodes(process7.stderr);
914
+ const colors = paint(process8.stderr);
915
+ const styled = takesControlCodes(process8.stderr);
895
916
  const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
896
917
  const under = (label, text, tint) => text === undefined ? "" : `
897
918
  ${styled ? tint(label) : `${label}:`} ${text}`;
@@ -934,11 +955,11 @@ async function dispatch(level, typed, { version, redact }) {
934
955
  }
935
956
  }
936
957
  if (name === undefined && level.path.length > 1) {
937
- console.error(await help2(await expand(level, [...level.path.slice(1), ...argv]), process7.stderr));
958
+ console.error(await help2(await expand(level, [...level.path.slice(1), ...argv]), process8.stderr));
938
959
  return 2;
939
960
  }
940
961
  if (name === undefined || name === "help" || name === "--help" || name === "-h") {
941
- console.log(await help2(await expand(level, [...level.path.slice(1), ...argv]), process7.stdout));
962
+ console.log(await help2(await expand(level, [...level.path.slice(1), ...argv]), process8.stdout));
942
963
  return 0;
943
964
  }
944
965
  if (level.path.length === 1 && (name === "--version" || name === "-v")) {
@@ -1054,10 +1075,22 @@ async function printCandidates(found) {
1054
1075
  console.log(line);
1055
1076
  return 0;
1056
1077
  }
1057
- async function runCli(cli, argv = process7.argv.slice(2)) {
1078
+ function packageVersion(entry = Bun.main) {
1079
+ for (let dir = dirname2(realpathSync(entry));; dir = dirname2(dir)) {
1080
+ try {
1081
+ const { version } = JSON.parse(readFileSync(join2(dir, "package.json"), "utf8"));
1082
+ if (typeof version === "string")
1083
+ return version;
1084
+ } catch {}
1085
+ if (dirname2(dir) === dir)
1086
+ return "0.0.0";
1087
+ }
1088
+ }
1089
+ async function runCli(cli, argv = process8.argv.slice(2)) {
1090
+ const version = cli.version ?? packageVersion();
1058
1091
  if (argv[0] === COMPLETE)
1059
1092
  return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
1060
- const code = await 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 });
1093
+ const code = await dispatch({ path: [cli.name], header: (colors, about) => `${colors.bold(cli.name)} ${colors.dim(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, redact: cli.redact });
1061
1094
  if (cli.usage !== false) {
1062
1095
  try {
1063
1096
  recordUsage(usageEntry(cli.name, commandPath(scopeOf(cli.commands, cli.extraCommands, cli.aliases), argv), argv, code));
@@ -1080,11 +1113,11 @@ function commandPath(scope, argv) {
1080
1113
  }
1081
1114
  return path.length === 0 ? undefined : path.join(" ");
1082
1115
  }
1083
- async function runMain({ name, version, run, redact, usage }, argv = process7.argv.slice(2)) {
1116
+ async function runMain({ name, version, run, redact, usage }, argv = process8.argv.slice(2)) {
1084
1117
  if (argv[0] === COMPLETE)
1085
1118
  return printCandidates(leafCandidates(run, argv.slice(1)));
1086
1119
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
1087
- console.log(version);
1120
+ console.log(version ?? packageVersion());
1088
1121
  return 0;
1089
1122
  }
1090
1123
  let code;
@@ -1093,8 +1126,11 @@ async function runMain({ name, version, run, redact, usage }, argv = process7.ar
1093
1126
  } catch (error) {
1094
1127
  code = reportThrown(name, error, redact, wantsJson(argv));
1095
1128
  }
1096
- if (usage !== false)
1097
- recordUsage(usageEntry(name, undefined, argv, code));
1129
+ if (usage !== false) {
1130
+ try {
1131
+ recordUsage(usageEntry(name, undefined, argv, code));
1132
+ } catch {}
1133
+ }
1098
1134
  return code;
1099
1135
  }
1100
1136
  // ../kit/src/secrets.ts
@@ -1103,28 +1139,27 @@ var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "ses
1103
1139
  var {Glob } = globalThis.Bun;
1104
1140
  var SESSION_FILES = new Glob("*.json");
1105
1141
  var ACCOUNT_DIRS = new Glob("*/sessions");
1106
- // package.json
1107
- var version = "0.4.0";
1108
-
1109
1142
  // src/completions.ts
1110
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, rmSync } from "fs";
1111
- import { join as join3 } from "path";
1112
- import process9 from "process";
1143
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, rmSync } from "fs";
1144
+ import { join as join4 } from "path";
1145
+ import process10 from "process";
1113
1146
 
1114
1147
  // src/checks.ts
1115
- import { existsSync, readFileSync } from "fs";
1116
- import { basename, join as join2 } from "path";
1148
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
1149
+ import { basename, join as join3 } from "path";
1117
1150
  var TOOLS = {
1151
+ "@kaynooo/cli-audit": ["audit"],
1118
1152
  "@kaynooo/cli-base": ["base", "proof"],
1119
1153
  "@kaynooo/cli-changelog": ["changelog"],
1120
- "@kaynooo/cli-check": ["check", "fix", "errs"],
1154
+ "@kaynooo/cli-check": ["check", "fix", "errs", "j"],
1121
1155
  "@kaynooo/cli-code": ["peek", "todo-scan", "hotspots"],
1122
1156
  "@kaynooo/cli-db": ["db"],
1157
+ "@kaynooo/cli-docker": ["dc", "dlog", "dx", "sf"],
1123
1158
  "@kaynooo/cli-doctor": ["doctor", "cli"],
1124
1159
  "@kaynooo/cli-edit": ["sub"],
1125
1160
  "@kaynooo/cli-env": ["ev", "redact"],
1126
1161
  "@kaynooo/cli-forgejo": ["ft"],
1127
- "@kaynooo/cli-git": ["gst", "gc"],
1162
+ "@kaynooo/cli-git": ["gst", "gc", "gb"],
1128
1163
  "@kaynooo/cli-gitlab": ["gl"],
1129
1164
  "@kaynooo/cli-guard": ["guard"],
1130
1165
  "@kaynooo/cli-out": ["out"],
@@ -1132,6 +1167,7 @@ var TOOLS = {
1132
1167
  "@kaynooo/cli-pick": ["pick"],
1133
1168
  "@kaynooo/cli-pkg": ["pkg"],
1134
1169
  "@kaynooo/cli-probe": ["probe"],
1170
+ "@kaynooo/cli-proc": ["pid", "ports", "procs"],
1135
1171
  "@kaynooo/cli-repos": ["repos"],
1136
1172
  "@kaynooo/cli-stack": ["stack"],
1137
1173
  "@kaynooo/cli-sweep": ["sweep"],
@@ -1153,7 +1189,7 @@ function readJsonText(text) {
1153
1189
  }
1154
1190
  }
1155
1191
  function readJson(path) {
1156
- return existsSync(path) ? readJsonText(readFileSync(path, "utf8")) : undefined;
1192
+ return existsSync(path) ? readJsonText(readFileSync2(path, "utf8")) : undefined;
1157
1193
  }
1158
1194
  function firstLine(text) {
1159
1195
  const trimmed = text.trim();
@@ -1162,12 +1198,12 @@ function firstLine(text) {
1162
1198
  return end === -1 ? trimmed : trimmed.slice(0, end);
1163
1199
  }
1164
1200
  function installed(host, name) {
1165
- return readJson(join2(host.globalModules, name, "package.json"));
1201
+ return readJson(join3(host.globalModules, name, "package.json"));
1166
1202
  }
1167
1203
  function ownBin(host, name, bin) {
1168
1204
  const declared = installed(host, name)?.bin;
1169
1205
  const file = typeof declared === "string" ? declared : declared?.[bin];
1170
- return file === undefined ? undefined : join2(host.globalModules, name, file);
1206
+ return file === undefined ? undefined : join3(host.globalModules, name, file);
1171
1207
  }
1172
1208
  async function checkTool(host, name, bins) {
1173
1209
  const check = name.slice(name.indexOf("/") + 1);
@@ -1176,7 +1212,7 @@ async function checkTool(host, name, bins) {
1176
1212
  return { check, ok: false, detail: "not installed", fix: `bun install -g ${name}` };
1177
1213
  const off = bins.filter((bin) => host.which(bin) === null);
1178
1214
  if (off.length > 0)
1179
- return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join2(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1215
+ return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join3(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1180
1216
  const info = await host.run(["bun", "info", name, "version", "--no-cache"]);
1181
1217
  const answered = info.code === 0 ? firstLine(info.stdout.trim().split(`
1182
1218
  `).at(-1) ?? "") : "";
@@ -1203,9 +1239,9 @@ async function checkTea(host) {
1203
1239
  return { check: "tea", ok: true, detail: `logged in to ${forge} as ${login.name}, Forgejo ${served}` };
1204
1240
  }
1205
1241
  function checkHooks(host) {
1206
- const settings = readJson(join2(host.home, ".claude", "settings.json")) ?? {};
1242
+ const settings = readJson(join3(host.home, ".claude", "settings.json")) ?? {};
1207
1243
  const plugin = settings.enabledPlugins?.[PLUGIN] === true;
1208
- 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")));
1244
+ const mod = (settings.env?.CLAUDE_CODE_PLUGIN_DIRS ?? "").split(":").map((dir) => dir.replace(LEADING_TILDE, host.home)).find((dir) => basename(dir) === "cli" && existsSync(join3(dir, "hooks", "hooks.json")));
1209
1245
  if (plugin && mod !== undefined)
1210
1246
  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}` };
1211
1247
  if (plugin)
@@ -1230,12 +1266,22 @@ function checkCompletions(host) {
1230
1266
  return { check: "completions", ok: false, detail: `no fish completion in ${dir} for ${missing.join(", ")}`, fix: "cli completions fish --install" };
1231
1267
  return { check: "completions", ok: true, detail: dir };
1232
1268
  }
1233
- async function runChecks(host) {
1234
- const entries = Object.entries(TOOLS);
1269
+ var toolCheck = (name) => name.slice(name.indexOf("/") + 1);
1270
+ var OTHER_CHECKS = ["tea", "hooks", "bun", "completions"];
1271
+ var CHECK_NAMES = [...Object.keys(TOOLS).map(toolCheck), ...OTHER_CHECKS];
1272
+ async function runChecks(host, only = []) {
1273
+ const wanted = (check) => only.length === 0 || only.includes(check);
1274
+ const entries = Object.entries(TOOLS).filter(([name]) => wanted(toolCheck(name)));
1235
1275
  const tools = [];
1236
1276
  for (let at = 0;at < entries.length; at += INFO_AT_ONCE)
1237
1277
  tools.push(...await Promise.all(entries.slice(at, at + INFO_AT_ONCE).map(async ([name, bins]) => checkTool(host, name, bins))));
1238
- return [...tools, await checkTea(host), checkHooks(host), checkBun(host), checkCompletions(host)];
1278
+ return [
1279
+ ...tools,
1280
+ ...wanted("tea") ? [await checkTea(host)] : [],
1281
+ ...wanted("hooks") ? [checkHooks(host)] : [],
1282
+ ...wanted("bun") ? [checkBun(host)] : [],
1283
+ ...wanted("completions") ? [checkCompletions(host)] : []
1284
+ ];
1239
1285
  }
1240
1286
 
1241
1287
  // src/completions.ts
@@ -1252,8 +1298,28 @@ var FUNCTION = `functions -q __kaynooo_cli_complete; or function __kaynooo_cli_c
1252
1298
  end
1253
1299
  end
1254
1300
  end`;
1255
- function completionDir(home, env = process9.env) {
1256
- return join3(env.XDG_CACHE_HOME ?? join3(home, ".cache"), "fish", "kaynooo-cli");
1301
+ var BASH_FUNCTION = `__kaynooo_cli_complete() {
1302
+ local IFS=$'\\n' cur=\${COMP_WORDS[COMP_CWORD]} line words=
1303
+ local -a lines
1304
+ COMPREPLY=()
1305
+ lines=($("\${COMP_WORDS[0]}" __complete "\${COMP_WORDS[@]:1:COMP_CWORD}" 2>/dev/null))
1306
+ for line in "\${lines[@]}"; do
1307
+ if [[ $line == __files__ ]]; then
1308
+ COMPREPLY+=($(compgen -f -- "$cur"))
1309
+ else
1310
+ words+="\${line%%$'\\t'*}"$'\\n'
1311
+ fi
1312
+ done
1313
+ COMPREPLY+=($(compgen -W "$words" -- "$cur"))
1314
+ }`;
1315
+ function bashScript(bins) {
1316
+ return `# Generated by \`cli completions bash\`
1317
+ ${BASH_FUNCTION}
1318
+ complete -F __kaynooo_cli_complete ${bins.join(" ")}
1319
+ `;
1320
+ }
1321
+ function completionDir(home, env = process10.env) {
1322
+ return join4(env.XDG_CACHE_HOME ?? join4(home, ".cache"), "fish", "kaynooo-cli");
1257
1323
  }
1258
1324
  function installedBins(host) {
1259
1325
  return Object.values(TOOLS).flat().filter((bin) => host.which(bin) !== null);
@@ -1267,30 +1333,30 @@ complete -c ${bin} -f -a '(__kaynooo_cli_complete ${bin})'
1267
1333
  async function installCompletions(host, dir) {
1268
1334
  const bins = installedBins(host);
1269
1335
  mkdirSync2(dir, { recursive: true });
1270
- const removed = readdirSync(dir).filter((name) => FISH_FILE.test(name) && !bins.includes(name.replace(FISH_FILE, "")) && readFileSync2(join3(dir, name), "utf8").startsWith(MARKER));
1336
+ const removed = readdirSync(dir).filter((name) => FISH_FILE.test(name) && !bins.includes(name.replace(FISH_FILE, "")) && readFileSync3(join4(dir, name), "utf8").startsWith(MARKER));
1271
1337
  for (const name of removed)
1272
- rmSync(join3(dir, name));
1273
- await Promise.all(bins.map(async (bin) => Bun.write(join3(dir, `${bin}.fish`), completionFile(bin))));
1338
+ rmSync(join4(dir, name));
1339
+ await Promise.all(bins.map(async (bin) => Bun.write(join4(dir, `${bin}.fish`), completionFile(bin))));
1274
1340
  return { written: bins, removed: removed.map((name) => name.replace(FISH_FILE, "")) };
1275
1341
  }
1276
1342
  function missingCompletions(host, dir) {
1277
- return installedBins(host).filter((bin) => !existsSync2(join3(dir, `${bin}.fish`)));
1343
+ return installedBins(host).filter((bin) => !existsSync2(join4(dir, `${bin}.fish`)));
1278
1344
  }
1279
1345
 
1280
1346
  // src/host.ts
1281
1347
  import { homedir as homedir2 } from "os";
1282
- import { join as join4 } from "path";
1283
- import process10 from "process";
1348
+ import { join as join5 } from "path";
1349
+ import process11 from "process";
1284
1350
  var RUN_TIMEOUT_MS = 20000;
1285
1351
  function hostHere() {
1286
1352
  const home = homedir2();
1287
1353
  return {
1288
1354
  home,
1289
- globalModules: join4(process10.env.BUN_INSTALL ?? join4(home, ".bun"), "install", "global", "node_modules"),
1355
+ globalModules: join5(process11.env.BUN_INSTALL ?? join5(home, ".bun"), "install", "global", "node_modules"),
1290
1356
  bunVersion: Bun.version,
1291
1357
  which: (bin) => Bun.which(bin),
1292
1358
  run: async (argv, env) => {
1293
- const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process10.env, ...env } } });
1359
+ const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process11.env, ...env } } });
1294
1360
  if (ran.timedOut)
1295
1361
  return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
1296
1362
  const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
@@ -1324,7 +1390,7 @@ var LIST_OPTIONS = {
1324
1390
  json: jsonOption("[{bin, package, installed, synopsis?, summary?, replaces?, examples?}]")
1325
1391
  };
1326
1392
  function print(listed) {
1327
- const { cyan, dim } = paint(process11.stdout);
1393
+ const { cyan, dim } = paint(process12.stdout);
1328
1394
  const width = Math.max(...listed.map((each) => each.bin.length));
1329
1395
  for (const each of listed) {
1330
1396
  const what = !each.installed ? dim(`not installed: bun install -g ${each.package}`) : `${each.summary ?? ""}${each.replaces === undefined ? "" : dim(`; instead of ${each.replaces}`)}`;
@@ -1340,18 +1406,28 @@ async function list(args, host = hostHere()) {
1340
1406
  print(listed);
1341
1407
  return 0;
1342
1408
  }
1343
- var COMPLETIONS_USAGE = `cli completions fish [--install] [--dir <folder>]: fish completions for every installed bin of the @kaynooo/cli-* packages
1409
+ var COMPLETIONS_USAGE = `cli completions <fish|bash> [--install] [--dir <folder>]: shell completions for every installed bin of the @kaynooo/cli-* packages
1344
1410
 
1345
- 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\`.
1346
- Exits 0 when it printed or wrote them, 2 on a usage error.`;
1411
+ Each bin completes itself through \`<bin> __complete\`; this writes the glue a shell needs per bin.
1412
+ fish: 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\`.
1413
+ bash: one script on stdout, sourced from ~/.bashrc with \`source <(cli completions bash)\`; --install and --dir are fish's.
1414
+ zsh: has no script of its own, it reads the bash one through bashcompinit: \`autoload -U +X compinit && compinit\`, \`autoload -U +X bashcompinit && bashcompinit\`, then the bash line above in ~/.zshrc (untested, zsh is not installed on the machine this was written on).`;
1347
1415
  var COMPLETIONS_OPTIONS = {
1348
1416
  install: { type: "boolean", description: "write the files to the folder instead of printing them" },
1349
1417
  dir: { type: "string", short: "d", description: "the folder to write to, instead of ~/.cache/fish/kaynooo-cli" }
1350
1418
  };
1351
1419
  async function completions(args, host = hostHere()) {
1352
- 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"]] });
1353
- if (positionals[0] !== "fish")
1354
- badUsage("only fish is supported: cli completions fish");
1420
+ const { values, positionals } = parse(args, COMPLETIONS_USAGE, COMPLETIONS_OPTIONS, { positionals: ["<shell>"], exits: [[0, "it printed or wrote them"], [2, "a usage error or a shell without a script"]], candidates: { "<shell>": () => ["fish", "bash"] }, examples: ["cli completions fish", ["cli completions fish --install", "write one file per bin"], ["source <(cli completions bash)", "in ~/.bashrc"]] });
1421
+ if (positionals[0] === "zsh")
1422
+ badUsage("zsh has no script of its own: it reads the bash one through bashcompinit, see cli completions --help");
1423
+ if (positionals[0] !== "fish" && positionals[0] !== "bash")
1424
+ badUsage("completions exist for fish and bash: cli completions fish");
1425
+ if (positionals[0] === "bash") {
1426
+ if (values.install === true || values.dir !== undefined)
1427
+ badUsage("--install and --dir are for fish: source the bash script instead");
1428
+ console.log(bashScript(installedBins(host)).trimEnd());
1429
+ return 0;
1430
+ }
1355
1431
  if (values.install !== true) {
1356
1432
  console.log(installedBins(host).map(completionFile).join(`
1357
1433
  `));
@@ -1392,9 +1468,8 @@ ${table(summary.usageErrors.map((each) => [`${each.bin}${each.command === undefi
1392
1468
  return 0;
1393
1469
  }
1394
1470
  if (import.meta.main) {
1395
- process11.exitCode = await runCli({
1471
+ process12.exitCode = await runCli({
1396
1472
  name: "cli",
1397
- version,
1398
1473
  about: "the bins of the @kaynooo/cli-* packages, described by their own help",
1399
1474
  replaces: "running each bin's --help to find the one that fits",
1400
1475
  examples: ["cli list", ["cli list --json", "with each bin's synopsis and examples"]],
package/dist/cli.js CHANGED
@@ -2,17 +2,16 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli.ts
5
- import process11 from "process";
6
-
5
+ import process12 from "process";
7
6
  // ../kit/src/args.ts
8
- import process4 from "process";
7
+ import process5 from "process";
9
8
  import { parseArgs } from "util";
10
9
 
11
10
  // ../kit/src/format.ts
12
- import process2 from "process";
11
+ import process3 from "process";
13
12
 
14
13
  // ../kit/src/style.ts
15
- import process from "process";
14
+ import process2 from "process";
16
15
 
17
16
  // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
18
17
  function x(s) {
@@ -276,10 +275,10 @@ function le(i = {}) {
276
275
  var t = le();
277
276
 
278
277
  // ../kit/src/style.ts
279
- function takesControlCodes(stream, env = process.env) {
278
+ function takesControlCodes(stream, env = process2.env) {
280
279
  return stream.isTTY === true && env.NO_COLOR === undefined && env.NODE_DISABLE_COLORS === undefined && env.TERM !== "dumb";
281
280
  }
282
- function paint(stream, env = process.env) {
281
+ function paint(stream, env = process2.env) {
283
282
  return le({ stream: { isTTY: takesControlCodes(stream, env) }, env });
284
283
  }
285
284
  var STATUSES = {
@@ -312,7 +311,7 @@ function table(rows, { cellWidth, header = false, align = [], fit } = {}) {
312
311
  widths[index] = Math.max(widths[index] ?? 0, textWidth(cell));
313
312
  }
314
313
  const shown = fit === undefined ? cells : narrow(cells, widths, fit);
315
- const colors = paint(process2.stdout);
314
+ const colors = paint(process3.stdout);
316
315
  const lines = shown.map((row) => row.map((cell, index) => {
317
316
  const width = widths[index] ?? 0;
318
317
  const gap = " ".repeat(Math.max(0, width - textWidth(cell)));
@@ -344,24 +343,30 @@ function printJson(value) {
344
343
  }
345
344
 
346
345
  // ../kit/src/help.ts
347
- import process3 from "process";
346
+ import process4 from "process";
348
347
  var OPTIONAL = /\[[^\]]+\]/g;
349
348
  var USAGE_PREFIX = /^usage:\s*/i;
350
349
  var FULL_STOP = /\.$/;
351
- function usageLine(usage, colors = paint(process3.stdout)) {
350
+ function usageLine(usage, colors = paint(process4.stdout)) {
352
351
  return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
353
352
  }
354
- function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
355
- return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
353
+ function helpText(usage, options, colors = paint(process4.stdout), { examples = [], replaces, exits = [] } = {}) {
354
+ return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exitRows(exits, colors), ...exampleRows(examples, colors)].join(`
356
355
  `);
357
356
  }
358
- function replacesRows(replaces, colors = paint(process3.stdout)) {
357
+ function exitRows(exits, colors = paint(process4.stdout)) {
358
+ if (exits.length === 0)
359
+ return [];
360
+ const width = Math.max(...exits.map(([code]) => String(code).length));
361
+ return ["", colors.bold("Exits:"), ...exits.map(([code, meaning]) => ` ${colors.cyan(String(code).padEnd(width))} ${meaning}`)];
362
+ }
363
+ function replacesRows(replaces, colors = paint(process4.stdout)) {
359
364
  return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
360
365
  }
361
- function wantsHelpJson(env = process3.env) {
366
+ function wantsHelpJson(env = process4.env) {
362
367
  return env.KIT_HELP === "json";
363
368
  }
364
- function description(usage, { examples = [], replaces } = {}) {
369
+ function description(usage, { examples = [], replaces, exits } = {}, options) {
365
370
  const lines = usage.replace(USAGE_PREFIX, "").split(`
366
371
  `);
367
372
  const first = lines[0];
@@ -372,15 +377,17 @@ function description(usage, { examples = [], replaces } = {}) {
372
377
  synopsis: colon === -1 ? first : first.slice(0, colon),
373
378
  summary: colon === -1 ? fallback : first.slice(colon + 2),
374
379
  ...replaces === undefined ? {} : { replaces },
375
- examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
380
+ examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] }),
381
+ ...exits === undefined ? {} : { exits: exits.map(([code, meaning]) => ({ code, meaning })) },
382
+ ...options === undefined ? {} : { options: Object.entries(options).map(([name, { short, description }]) => ({ name, ...short === undefined ? {} : { short }, ...description === undefined ? {} : { description } })) }
376
383
  };
377
384
  }
378
- function exampleRows(examples, colors = paint(process3.stdout)) {
385
+ function exampleRows(examples, colors = paint(process4.stdout)) {
379
386
  if (examples.length === 0)
380
387
  return [];
381
388
  return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
382
389
  }
383
- function optionRows(options, colors = paint(process3.stdout)) {
390
+ function optionRows(options, colors = paint(process4.stdout)) {
384
391
  const rows = Object.entries(options).map(([name, option]) => {
385
392
  const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
386
393
  const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
@@ -469,7 +476,7 @@ var OUTPUT_OPTIONS = {
469
476
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
470
477
  plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
471
478
  };
472
- function applyOutput(color, plain, env = process4.env) {
479
+ function applyOutput(color, plain, env = process5.env) {
473
480
  const when = plain === true ? "never" : color ?? "auto";
474
481
  if (when === "auto")
475
482
  return;
@@ -492,7 +499,7 @@ function parse(args, usage, options, settings = {}) {
492
499
  const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
493
500
  let parsed;
494
501
  try {
495
- parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
502
+ parsed = parseArgs({ args: joinDashValues(withBare(withAliases(own, all), all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
496
503
  } catch (error) {
497
504
  const message = parseError(error);
498
505
  throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
@@ -500,7 +507,7 @@ function parse(args, usage, options, settings = {}) {
500
507
  const values = parsed.values;
501
508
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
502
509
  if (values.help === true) {
503
- console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
510
+ console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings, listing)) : helpText(usage, listing, undefined, settings));
504
511
  throw new HelpShown;
505
512
  }
506
513
  checkChoices(parsed.values, all);
@@ -542,6 +549,18 @@ function listed(options, allowNegative) {
542
549
  return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
543
550
  }));
544
551
  }
552
+ function withAliases(args, options) {
553
+ const named = new Map(Object.entries(options).flatMap(([name, { aliases = [] }]) => aliases.map((alias) => [`--${alias}`, `--${name}`])));
554
+ if (named.size === 0)
555
+ return args;
556
+ const separator = args.indexOf("--");
557
+ const own = separator === -1 ? args : args.slice(0, separator);
558
+ return [...own.map((arg) => {
559
+ const at = arg.indexOf("=");
560
+ const current = named.get(at === -1 ? arg : arg.slice(0, at));
561
+ return current === undefined ? arg : `${current}${at === -1 ? "" : arg.slice(at)}`;
562
+ }), ...separator === -1 ? [] : args.slice(separator)];
563
+ }
545
564
  function withBare(args, options) {
546
565
  const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
547
566
  if (bare.size === 0)
@@ -628,7 +647,7 @@ function nearestOption(message, options, allowNegative) {
628
647
  }
629
648
  // ../kit/src/process.ts
630
649
  import { constants } from "os";
631
- import process5 from "process";
650
+ import process6 from "process";
632
651
  var KILL_GRACE_MS = 2000;
633
652
  function exitCodeOf(code, signal) {
634
653
  if (code !== null)
@@ -648,7 +667,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal,
648
667
  return;
649
668
  try {
650
669
  if (grouped)
651
- process5.kill(-proc.pid, killing);
670
+ process6.kill(-proc.pid, killing);
652
671
  else
653
672
  proc.kill(killing);
654
673
  } catch {}
@@ -722,13 +741,15 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
722
741
  var RESET_STYLE = `${ESC}[0m`;
723
742
  var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
724
743
  // ../kit/src/cli.ts
725
- import process7 from "process";
744
+ import { readFileSync, realpathSync } from "fs";
745
+ import { dirname as dirname2, join as join2 } from "path";
746
+ import process8 from "process";
726
747
 
727
748
  // ../kit/src/usage.ts
728
749
  import { appendFileSync, mkdirSync } from "fs";
729
750
  import { homedir } from "os";
730
751
  import { dirname, isAbsolute, join } from "path";
731
- import process6 from "process";
752
+ import process7 from "process";
732
753
 
733
754
  // ../kit/src/logs.ts
734
755
  var DAY_MS = 24 * 60 * 60 * 1000;
@@ -742,14 +763,14 @@ var usageError;
742
763
  function noteUsageError(message) {
743
764
  usageError = message;
744
765
  }
745
- function stateHome(env = process6.env) {
766
+ function stateHome(env = process7.env) {
746
767
  const dir = env.XDG_STATE_HOME;
747
768
  return dir !== undefined && isAbsolute(dir) ? dir : join(homedir(), ".local", "state");
748
769
  }
749
- function usagePath(env = process6.env) {
770
+ function usagePath(env = process7.env) {
750
771
  return join(stateHome(env), CLI_FOLDER, "usage.jsonl");
751
772
  }
752
- function usageRecorded(env = process6.env) {
773
+ function usageRecorded(env = process7.env) {
753
774
  const off = env.KAYNOOO_CLI_NO_USAGE;
754
775
  return env.NODE_ENV !== "test" && (off === undefined || off === "" || off === "0");
755
776
  }
@@ -763,7 +784,7 @@ function usageEntry(bin, command, argv, code, now = new Date) {
763
784
  usageError = undefined;
764
785
  return { at: now.toISOString(), bin, ...command === undefined ? {} : { command }, flags: flagNames(argv), code, ...unknown === undefined ? {} : { unknown } };
765
786
  }
766
- function recordUsage(entry, env = process6.env) {
787
+ function recordUsage(entry, env = process7.env) {
767
788
  if (!usageRecorded(env))
768
789
  return;
769
790
  try {
@@ -890,8 +911,8 @@ async function help2({ path, header, summary: lazySummary, commands, defaultComm
890
911
  `);
891
912
  }
892
913
  function printError(prefix, message, hint, cause) {
893
- const colors = paint(process7.stderr);
894
- const styled = takesControlCodes(process7.stderr);
914
+ const colors = paint(process8.stderr);
915
+ const styled = takesControlCodes(process8.stderr);
895
916
  const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
896
917
  const under = (label, text, tint) => text === undefined ? "" : `
897
918
  ${styled ? tint(label) : `${label}:`} ${text}`;
@@ -934,11 +955,11 @@ async function dispatch(level, typed, { version, redact }) {
934
955
  }
935
956
  }
936
957
  if (name === undefined && level.path.length > 1) {
937
- console.error(await help2(await expand(level, [...level.path.slice(1), ...argv]), process7.stderr));
958
+ console.error(await help2(await expand(level, [...level.path.slice(1), ...argv]), process8.stderr));
938
959
  return 2;
939
960
  }
940
961
  if (name === undefined || name === "help" || name === "--help" || name === "-h") {
941
- console.log(await help2(await expand(level, [...level.path.slice(1), ...argv]), process7.stdout));
962
+ console.log(await help2(await expand(level, [...level.path.slice(1), ...argv]), process8.stdout));
942
963
  return 0;
943
964
  }
944
965
  if (level.path.length === 1 && (name === "--version" || name === "-v")) {
@@ -1054,10 +1075,22 @@ async function printCandidates(found) {
1054
1075
  console.log(line);
1055
1076
  return 0;
1056
1077
  }
1057
- async function runCli(cli, argv = process7.argv.slice(2)) {
1078
+ function packageVersion(entry = Bun.main) {
1079
+ for (let dir = dirname2(realpathSync(entry));; dir = dirname2(dir)) {
1080
+ try {
1081
+ const { version } = JSON.parse(readFileSync(join2(dir, "package.json"), "utf8"));
1082
+ if (typeof version === "string")
1083
+ return version;
1084
+ } catch {}
1085
+ if (dirname2(dir) === dir)
1086
+ return "0.0.0";
1087
+ }
1088
+ }
1089
+ async function runCli(cli, argv = process8.argv.slice(2)) {
1090
+ const version = cli.version ?? packageVersion();
1058
1091
  if (argv[0] === COMPLETE)
1059
1092
  return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
1060
- const code = await 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 });
1093
+ const code = await dispatch({ path: [cli.name], header: (colors, about) => `${colors.bold(cli.name)} ${colors.dim(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, redact: cli.redact });
1061
1094
  if (cli.usage !== false) {
1062
1095
  try {
1063
1096
  recordUsage(usageEntry(cli.name, commandPath(scopeOf(cli.commands, cli.extraCommands, cli.aliases), argv), argv, code));
@@ -1080,11 +1113,11 @@ function commandPath(scope, argv) {
1080
1113
  }
1081
1114
  return path.length === 0 ? undefined : path.join(" ");
1082
1115
  }
1083
- async function runMain({ name, version, run, redact, usage }, argv = process7.argv.slice(2)) {
1116
+ async function runMain({ name, version, run, redact, usage }, argv = process8.argv.slice(2)) {
1084
1117
  if (argv[0] === COMPLETE)
1085
1118
  return printCandidates(leafCandidates(run, argv.slice(1)));
1086
1119
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
1087
- console.log(version);
1120
+ console.log(version ?? packageVersion());
1088
1121
  return 0;
1089
1122
  }
1090
1123
  let code;
@@ -1093,8 +1126,11 @@ async function runMain({ name, version, run, redact, usage }, argv = process7.ar
1093
1126
  } catch (error) {
1094
1127
  code = reportThrown(name, error, redact, wantsJson(argv));
1095
1128
  }
1096
- if (usage !== false)
1097
- recordUsage(usageEntry(name, undefined, argv, code));
1129
+ if (usage !== false) {
1130
+ try {
1131
+ recordUsage(usageEntry(name, undefined, argv, code));
1132
+ } catch {}
1133
+ }
1098
1134
  return code;
1099
1135
  }
1100
1136
  // ../kit/src/secrets.ts
@@ -1103,17 +1139,14 @@ var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "ses
1103
1139
  var {Glob } = globalThis.Bun;
1104
1140
  var SESSION_FILES = new Glob("*.json");
1105
1141
  var ACCOUNT_DIRS = new Glob("*/sessions");
1106
- // package.json
1107
- var version = "0.4.0";
1108
-
1109
1142
  // src/checks.ts
1110
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1111
- import { basename, join as join3 } from "path";
1143
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
1144
+ import { basename, join as join4 } from "path";
1112
1145
 
1113
1146
  // src/completions.ts
1114
- import { existsSync, mkdirSync as mkdirSync2, readdirSync, readFileSync, rmSync } from "fs";
1115
- import { join as join2 } from "path";
1116
- import process9 from "process";
1147
+ import { existsSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, rmSync } from "fs";
1148
+ import { join as join3 } from "path";
1149
+ import process10 from "process";
1117
1150
  var MARKER = "# Generated by `cli completions fish`";
1118
1151
  var FISH_FILE = /\.fish$/;
1119
1152
  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'
@@ -1127,8 +1160,28 @@ var FUNCTION = `functions -q __kaynooo_cli_complete; or function __kaynooo_cli_c
1127
1160
  end
1128
1161
  end
1129
1162
  end`;
1130
- function completionDir(home, env = process9.env) {
1131
- return join2(env.XDG_CACHE_HOME ?? join2(home, ".cache"), "fish", "kaynooo-cli");
1163
+ var BASH_FUNCTION = `__kaynooo_cli_complete() {
1164
+ local IFS=$'\\n' cur=\${COMP_WORDS[COMP_CWORD]} line words=
1165
+ local -a lines
1166
+ COMPREPLY=()
1167
+ lines=($("\${COMP_WORDS[0]}" __complete "\${COMP_WORDS[@]:1:COMP_CWORD}" 2>/dev/null))
1168
+ for line in "\${lines[@]}"; do
1169
+ if [[ $line == __files__ ]]; then
1170
+ COMPREPLY+=($(compgen -f -- "$cur"))
1171
+ else
1172
+ words+="\${line%%$'\\t'*}"$'\\n'
1173
+ fi
1174
+ done
1175
+ COMPREPLY+=($(compgen -W "$words" -- "$cur"))
1176
+ }`;
1177
+ function bashScript(bins) {
1178
+ return `# Generated by \`cli completions bash\`
1179
+ ${BASH_FUNCTION}
1180
+ complete -F __kaynooo_cli_complete ${bins.join(" ")}
1181
+ `;
1182
+ }
1183
+ function completionDir(home, env = process10.env) {
1184
+ return join3(env.XDG_CACHE_HOME ?? join3(home, ".cache"), "fish", "kaynooo-cli");
1132
1185
  }
1133
1186
  function installedBins(host) {
1134
1187
  return Object.values(TOOLS).flat().filter((bin) => host.which(bin) !== null);
@@ -1142,28 +1195,30 @@ complete -c ${bin} -f -a '(__kaynooo_cli_complete ${bin})'
1142
1195
  async function installCompletions(host, dir) {
1143
1196
  const bins = installedBins(host);
1144
1197
  mkdirSync2(dir, { recursive: true });
1145
- const removed = readdirSync(dir).filter((name) => FISH_FILE.test(name) && !bins.includes(name.replace(FISH_FILE, "")) && readFileSync(join2(dir, name), "utf8").startsWith(MARKER));
1198
+ const removed = readdirSync(dir).filter((name) => FISH_FILE.test(name) && !bins.includes(name.replace(FISH_FILE, "")) && readFileSync2(join3(dir, name), "utf8").startsWith(MARKER));
1146
1199
  for (const name of removed)
1147
- rmSync(join2(dir, name));
1148
- await Promise.all(bins.map(async (bin) => Bun.write(join2(dir, `${bin}.fish`), completionFile(bin))));
1200
+ rmSync(join3(dir, name));
1201
+ await Promise.all(bins.map(async (bin) => Bun.write(join3(dir, `${bin}.fish`), completionFile(bin))));
1149
1202
  return { written: bins, removed: removed.map((name) => name.replace(FISH_FILE, "")) };
1150
1203
  }
1151
1204
  function missingCompletions(host, dir) {
1152
- return installedBins(host).filter((bin) => !existsSync(join2(dir, `${bin}.fish`)));
1205
+ return installedBins(host).filter((bin) => !existsSync(join3(dir, `${bin}.fish`)));
1153
1206
  }
1154
1207
 
1155
1208
  // src/checks.ts
1156
1209
  var TOOLS = {
1210
+ "@kaynooo/cli-audit": ["audit"],
1157
1211
  "@kaynooo/cli-base": ["base", "proof"],
1158
1212
  "@kaynooo/cli-changelog": ["changelog"],
1159
- "@kaynooo/cli-check": ["check", "fix", "errs"],
1213
+ "@kaynooo/cli-check": ["check", "fix", "errs", "j"],
1160
1214
  "@kaynooo/cli-code": ["peek", "todo-scan", "hotspots"],
1161
1215
  "@kaynooo/cli-db": ["db"],
1216
+ "@kaynooo/cli-docker": ["dc", "dlog", "dx", "sf"],
1162
1217
  "@kaynooo/cli-doctor": ["doctor", "cli"],
1163
1218
  "@kaynooo/cli-edit": ["sub"],
1164
1219
  "@kaynooo/cli-env": ["ev", "redact"],
1165
1220
  "@kaynooo/cli-forgejo": ["ft"],
1166
- "@kaynooo/cli-git": ["gst", "gc"],
1221
+ "@kaynooo/cli-git": ["gst", "gc", "gb"],
1167
1222
  "@kaynooo/cli-gitlab": ["gl"],
1168
1223
  "@kaynooo/cli-guard": ["guard"],
1169
1224
  "@kaynooo/cli-out": ["out"],
@@ -1171,6 +1226,7 @@ var TOOLS = {
1171
1226
  "@kaynooo/cli-pick": ["pick"],
1172
1227
  "@kaynooo/cli-pkg": ["pkg"],
1173
1228
  "@kaynooo/cli-probe": ["probe"],
1229
+ "@kaynooo/cli-proc": ["pid", "ports", "procs"],
1174
1230
  "@kaynooo/cli-repos": ["repos"],
1175
1231
  "@kaynooo/cli-stack": ["stack"],
1176
1232
  "@kaynooo/cli-sweep": ["sweep"],
@@ -1192,7 +1248,7 @@ function readJsonText(text) {
1192
1248
  }
1193
1249
  }
1194
1250
  function readJson(path) {
1195
- return existsSync2(path) ? readJsonText(readFileSync2(path, "utf8")) : undefined;
1251
+ return existsSync2(path) ? readJsonText(readFileSync3(path, "utf8")) : undefined;
1196
1252
  }
1197
1253
  function firstLine(text) {
1198
1254
  const trimmed = text.trim();
@@ -1201,12 +1257,12 @@ function firstLine(text) {
1201
1257
  return end === -1 ? trimmed : trimmed.slice(0, end);
1202
1258
  }
1203
1259
  function installed(host, name) {
1204
- return readJson(join3(host.globalModules, name, "package.json"));
1260
+ return readJson(join4(host.globalModules, name, "package.json"));
1205
1261
  }
1206
1262
  function ownBin(host, name, bin) {
1207
1263
  const declared = installed(host, name)?.bin;
1208
1264
  const file = typeof declared === "string" ? declared : declared?.[bin];
1209
- return file === undefined ? undefined : join3(host.globalModules, name, file);
1265
+ return file === undefined ? undefined : join4(host.globalModules, name, file);
1210
1266
  }
1211
1267
  async function checkTool(host, name, bins) {
1212
1268
  const check = name.slice(name.indexOf("/") + 1);
@@ -1215,7 +1271,7 @@ async function checkTool(host, name, bins) {
1215
1271
  return { check, ok: false, detail: "not installed", fix: `bun install -g ${name}` };
1216
1272
  const off = bins.filter((bin) => host.which(bin) === null);
1217
1273
  if (off.length > 0)
1218
- return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join3(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1274
+ return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join4(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1219
1275
  const info = await host.run(["bun", "info", name, "version", "--no-cache"]);
1220
1276
  const answered = info.code === 0 ? firstLine(info.stdout.trim().split(`
1221
1277
  `).at(-1) ?? "") : "";
@@ -1242,9 +1298,9 @@ async function checkTea(host) {
1242
1298
  return { check: "tea", ok: true, detail: `logged in to ${forge} as ${login.name}, Forgejo ${served}` };
1243
1299
  }
1244
1300
  function checkHooks(host) {
1245
- const settings = readJson(join3(host.home, ".claude", "settings.json")) ?? {};
1301
+ const settings = readJson(join4(host.home, ".claude", "settings.json")) ?? {};
1246
1302
  const plugin = settings.enabledPlugins?.[PLUGIN] === true;
1247
- const mod = (settings.env?.CLAUDE_CODE_PLUGIN_DIRS ?? "").split(":").map((dir) => dir.replace(LEADING_TILDE, host.home)).find((dir) => basename(dir) === "cli" && existsSync2(join3(dir, "hooks", "hooks.json")));
1303
+ const mod = (settings.env?.CLAUDE_CODE_PLUGIN_DIRS ?? "").split(":").map((dir) => dir.replace(LEADING_TILDE, host.home)).find((dir) => basename(dir) === "cli" && existsSync2(join4(dir, "hooks", "hooks.json")));
1248
1304
  if (plugin && mod !== undefined)
1249
1305
  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}` };
1250
1306
  if (plugin)
@@ -1269,28 +1325,75 @@ function checkCompletions(host) {
1269
1325
  return { check: "completions", ok: false, detail: `no fish completion in ${dir} for ${missing.join(", ")}`, fix: "cli completions fish --install" };
1270
1326
  return { check: "completions", ok: true, detail: dir };
1271
1327
  }
1272
- async function runChecks(host) {
1273
- const entries = Object.entries(TOOLS);
1328
+ var toolCheck = (name) => name.slice(name.indexOf("/") + 1);
1329
+ var OTHER_CHECKS = ["tea", "hooks", "bun", "completions"];
1330
+ var CHECK_NAMES = [...Object.keys(TOOLS).map(toolCheck), ...OTHER_CHECKS];
1331
+ async function runChecks(host, only = []) {
1332
+ const wanted = (check) => only.length === 0 || only.includes(check);
1333
+ const entries = Object.entries(TOOLS).filter(([name]) => wanted(toolCheck(name)));
1274
1334
  const tools = [];
1275
1335
  for (let at = 0;at < entries.length; at += INFO_AT_ONCE)
1276
1336
  tools.push(...await Promise.all(entries.slice(at, at + INFO_AT_ONCE).map(async ([name, bins]) => checkTool(host, name, bins))));
1277
- return [...tools, await checkTea(host), checkHooks(host), checkBun(host), checkCompletions(host)];
1337
+ return [
1338
+ ...tools,
1339
+ ...wanted("tea") ? [await checkTea(host)] : [],
1340
+ ...wanted("hooks") ? [checkHooks(host)] : [],
1341
+ ...wanted("bun") ? [checkBun(host)] : [],
1342
+ ...wanted("completions") ? [checkCompletions(host)] : []
1343
+ ];
1344
+ }
1345
+
1346
+ // src/fix.ts
1347
+ var INSTALL = /^bun install -g (\S+)$/;
1348
+ var RUNNABLE = /^(?:cli completions fish --install|claude plugin (?:install \S+ --marketplace \S+|disable \S+))$/;
1349
+ function planFixes(results) {
1350
+ const steps = [];
1351
+ const installs = [];
1352
+ for (const { ok, fix } of results) {
1353
+ if (ok || fix === undefined)
1354
+ continue;
1355
+ const install = INSTALL.exec(fix);
1356
+ if (install !== null)
1357
+ installs.push(install[1] ?? "");
1358
+ else
1359
+ steps.push({ command: fix, argv: RUNNABLE.test(fix) ? fix.split(" ") : undefined });
1360
+ }
1361
+ if (installs.length === 0)
1362
+ return steps;
1363
+ const command = `bun install -g ${installs.join(" ")}`;
1364
+ return [{ command, argv: command.split(" ") }, ...steps];
1365
+ }
1366
+ async function applyFixes(host, steps, dry) {
1367
+ const outcomes = [];
1368
+ for (const { command, argv } of steps) {
1369
+ if (argv === undefined) {
1370
+ outcomes.push({ command, status: "not run", detail: "not a command doctor runs: do it by hand" });
1371
+ continue;
1372
+ }
1373
+ if (dry) {
1374
+ outcomes.push({ command, status: "dry-run" });
1375
+ continue;
1376
+ }
1377
+ const ran = await host.run(argv);
1378
+ outcomes.push(ran.code === 0 ? { command, status: "ran" } : { command, status: "failed", detail: `exit ${ran.code}: ${firstLine(ran.stderr || ran.stdout)}` });
1379
+ }
1380
+ return outcomes;
1278
1381
  }
1279
1382
 
1280
1383
  // src/host.ts
1281
1384
  import { homedir as homedir2 } from "os";
1282
- import { join as join4 } from "path";
1283
- import process10 from "process";
1385
+ import { join as join5 } from "path";
1386
+ import process11 from "process";
1284
1387
  var RUN_TIMEOUT_MS = 20000;
1285
1388
  function hostHere() {
1286
1389
  const home = homedir2();
1287
1390
  return {
1288
1391
  home,
1289
- globalModules: join4(process10.env.BUN_INSTALL ?? join4(home, ".bun"), "install", "global", "node_modules"),
1392
+ globalModules: join5(process11.env.BUN_INSTALL ?? join5(home, ".bun"), "install", "global", "node_modules"),
1290
1393
  bunVersion: Bun.version,
1291
1394
  which: (bin) => Bun.which(bin),
1292
1395
  run: async (argv, env) => {
1293
- const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process10.env, ...env } } });
1396
+ const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process11.env, ...env } } });
1294
1397
  if (ran.timedOut)
1295
1398
  return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
1296
1399
  const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
@@ -1300,35 +1403,67 @@ function hostHere() {
1300
1403
  }
1301
1404
 
1302
1405
  // src/cli.ts
1303
- var USAGE = `doctor [--json]: checks this host's @kaynooo/cli-* install, one line per check, the command that fixes a failing one beside it
1406
+ var USAGE = `doctor [--only <check>] [--quiet] [--fix [--dry-run]] [--json]: checks this host's @kaynooo/cli-* install, one line per check, the command that fixes a failing one beside it
1304
1407
 
1305
- Each @kaynooo/cli-* package installed globally with its bins on the PATH and no older than the registry's latest; tea logged in to git.kaynooo.fr and reaching it; the Claude Code hooks wired through the cli-hooks plugin or the cli mod, not both; Bun at least the version the tools ask for.
1306
- Exits 0 when every check passes, 1 when one fails, 2 on a usage error.`;
1307
- var INSTALL = "bun install -g ";
1408
+ Each @kaynooo/cli-* package installed globally with its bins on the PATH and no older than the registry's latest; tea logged in to git.kaynooo.fr and reaching it; the Claude Code hooks wired through the cli-hooks plugin or the cli mod, not both; Bun at least the version the tools ask for; fish completions for every bin.
1409
+ --fix runs the printed fix: lines it may (global installs of the packages, the completions, the hooks plugin), the installs in one call, then checks again; a line that installs a system package or only describes a step is printed as not run. --dry-run shows what --fix would run.
1410
+ --quiet prints only the failing checks, nothing when all pass. --only limits the run to the checks named, e.g. --only tea --only bun.`;
1411
+ var EXITS = [[0, "every check passes (after --fix, the ones it fixed too)"], [1, "a check fails"], [2, "a usage error"]];
1412
+ var INSTALL2 = "bun install -g ";
1308
1413
  var OPTIONS = {
1309
- json: jsonOption("[{check, ok, detail, fix?}]")
1414
+ json: jsonOption("[{check, ok, detail, fix?}], as the checks stand after --fix; the fixes it ran go to stderr"),
1415
+ only: { type: "string", multiple: true, choices: CHECK_NAMES, description: "run only this check (a package such as cli-forgejo, tea, hooks, bun or completions), repeatable" },
1416
+ quiet: { type: "boolean", short: "q", description: "print only the failing checks" },
1417
+ fix: { type: "boolean", description: "run the fix: lines doctor may run, then check again" },
1418
+ "dry-run": { type: "boolean", description: "with --fix, show what would run without running it (alone, it implies --fix)" }
1310
1419
  };
1311
1420
  function print(results) {
1312
- const { green, red, dim } = paint(process11.stdout);
1421
+ if (results.length === 0)
1422
+ return;
1423
+ const { green, red, dim } = paint(process12.stdout);
1313
1424
  const width = Math.max(...results.map((result) => result.check.length));
1314
1425
  for (const { check, ok, detail, fix } of results)
1315
1426
  console.log(`${ok ? green("ok ") : red("FAIL")} ${check.padEnd(width)} ${detail}${fix === undefined ? "" : ` ${dim("fix:")} ${fix}`}`);
1316
- const installs = results.flatMap((result) => result.fix?.startsWith(INSTALL) === true ? [result.fix.slice(INSTALL.length)] : []);
1427
+ const installs = results.flatMap((result) => result.fix?.startsWith(INSTALL2) === true ? [result.fix.slice(INSTALL2.length)] : []);
1317
1428
  if (installs.length > 1)
1318
1429
  console.log(`
1319
- ${dim("fix them all:")} ${INSTALL}${installs.join(" ")}`);
1430
+ ${dim("fix them all:")} ${INSTALL2}${installs.join(" ")}`);
1431
+ }
1432
+ function printOutcomes(outcomes, log) {
1433
+ const { green, red, dim } = paint(process12.stdout);
1434
+ const marks = { ran: green("ran "), failed: red("failed "), "dry-run": dim("would "), "not run": dim("not run") };
1435
+ for (const { command, status, detail } of outcomes)
1436
+ log(`${marks[status]} ${command}${detail === undefined ? "" : ` ${dim(detail)}`}`);
1320
1437
  }
1321
1438
  async function main(args, host = hostHere()) {
1322
- const { values } = parse(args, USAGE, OPTIONS, { positionals: [], replaces: "checking each bin's version, tea's login and the hooks by hand", examples: ["doctor", ["doctor --json", "the checks for a script"]] });
1323
- const results = await runChecks(host);
1324
- if (values.json === true)
1325
- printJson(results);
1439
+ const { values } = parse(args, USAGE, OPTIONS, { positionals: [], exits: EXITS, replaces: "checking each bin's version, tea's login and the hooks by hand", examples: ["doctor", ["doctor --json", "the checks for a script"], ["doctor -q", "silent unless something fails"], ["doctor --fix --dry-run", "what --fix would run"]] });
1440
+ const only = values.only ?? [];
1441
+ const dry = values["dry-run"] === true;
1442
+ const json = values.json === true;
1443
+ let results = await runChecks(host, only);
1444
+ let fixed = false;
1445
+ if ((values.fix === true || dry) && results.some((result) => !result.ok)) {
1446
+ if (!json)
1447
+ print(values.quiet === true ? results.filter((result) => !result.ok) : results);
1448
+ const steps = planFixes(results);
1449
+ printOutcomes(await applyFixes(host, steps, dry), json ? console.error : console.log);
1450
+ if (dry)
1451
+ return 1;
1452
+ results = await runChecks(host, only);
1453
+ fixed = true;
1454
+ }
1455
+ const shown = values.quiet === true ? results.filter((result) => !result.ok) : results;
1456
+ if (fixed && !json && shown.length > 0)
1457
+ console.log(`
1458
+ after the fixes:`);
1459
+ if (json)
1460
+ printJson(shown);
1326
1461
  else
1327
- print(results);
1462
+ print(shown);
1328
1463
  return results.every((result) => result.ok) ? 0 : 1;
1329
1464
  }
1330
1465
  if (import.meta.main)
1331
- process11.exitCode = await runMain({ name: "doctor", version, run: async (args) => main(args) });
1466
+ process12.exitCode = await runMain({ name: "doctor", run: async (args) => main(args) });
1332
1467
  export {
1333
1468
  main
1334
1469
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaynooo/cli-doctor",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.5.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",