@kaynooo/cli-doctor 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,6 +17,14 @@ doctor --json # [{check, ok, detail, fix?}]
17
17
 
18
18
  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
19
 
20
+ ## cli stats
21
+
22
+ ```sh
23
+ cli stats [--since 7d] [--json] # runs per bin, failures, and the options usage errors did not know
24
+ ```
25
+
26
+ Every bin records each run in `~/.local/state/kaynooo-cli/usage.jsonl`: the bin, its command, the flag names it saw (never values or arguments), the exit code and, on a usage error, the unknown option. `KAYNOOO_CLI_NO_USAGE=1` turns it off; `guard` never records.
27
+
20
28
  ## cli list
21
29
 
22
30
  ```sh
package/dist/cli-bin.js CHANGED
@@ -2,12 +2,15 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli-bin.ts
5
- import process8 from "process";
5
+ import process11 from "process";
6
6
 
7
7
  // ../kit/src/args.ts
8
- import process2 from "process";
8
+ import process4 from "process";
9
9
  import { parseArgs } from "util";
10
10
 
11
+ // ../kit/src/format.ts
12
+ import process2 from "process";
13
+
11
14
  // ../kit/src/style.ts
12
15
  import process from "process";
13
16
 
@@ -293,11 +296,102 @@ function upTo(text, separator) {
293
296
  const end = text.indexOf(separator);
294
297
  return end === -1 ? text : text.slice(0, end);
295
298
  }
299
+ function truncate(text, width) {
300
+ return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
301
+ }
302
+ var LINE_BREAKS = /\s*\n\s*/g;
296
303
  var ESCAPE = String.fromCharCode(27);
304
+ function textWidth(text) {
305
+ return Bun.stringWidth(text);
306
+ }
307
+ function table(rows, { cellWidth, header = false, align = [], fit } = {}) {
308
+ const cells = cellWidth === undefined ? rows : rows.map((row) => row.map((cell) => truncate(cell.replaceAll(LINE_BREAKS, " "), cellWidth)));
309
+ const widths = [];
310
+ for (const row of cells) {
311
+ for (const [index, cell] of row.entries())
312
+ widths[index] = Math.max(widths[index] ?? 0, textWidth(cell));
313
+ }
314
+ const shown = fit === undefined ? cells : narrow(cells, widths, fit);
315
+ const colors = paint(process2.stdout);
316
+ const lines = shown.map((row) => row.map((cell, index) => {
317
+ const width = widths[index] ?? 0;
318
+ const gap = " ".repeat(Math.max(0, width - textWidth(cell)));
319
+ if (align[index] === "right")
320
+ return `${gap}${cell}`;
321
+ return index === row.length - 1 ? cell : `${cell}${gap}`;
322
+ }).join(" ").trimEnd());
323
+ return lines.map((line, index) => header && index === 0 ? colors.bold(line) : line).join(`
324
+ `);
325
+ }
326
+ function narrow(rows, widths, limit) {
327
+ const used = () => widths.reduce((sum, width) => sum + width, 0) + 2 * Math.max(0, widths.length - 1);
328
+ const cuttable = (index) => rows.every((row) => !(row[index] ?? "").includes(ESCAPE));
329
+ const floor = 8;
330
+ while (used() > limit) {
331
+ let widest = -1;
332
+ for (const [index, width] of widths.entries()) {
333
+ if (cuttable(index) && width > floor && width > (widths[widest] ?? 0))
334
+ widest = index;
335
+ }
336
+ if (widest === -1)
337
+ break;
338
+ widths[widest] = Math.max(floor, (widths[widest] ?? 0) - (used() - limit));
339
+ }
340
+ return rows.map((row) => row.map((cell, index) => textWidth(cell) > (widths[index] ?? 0) ? truncate(cell, widths[index] ?? 0) : cell));
341
+ }
297
342
  function printJson(value) {
298
343
  console.log(JSON.stringify(value, undefined, 2));
299
344
  }
300
345
 
346
+ // ../kit/src/help.ts
347
+ import process3 from "process";
348
+ var OPTIONAL = /\[[^\]]+\]/g;
349
+ var USAGE_PREFIX = /^usage:\s*/i;
350
+ var FULL_STOP = /\.$/;
351
+ function usageLine(usage, colors = paint(process3.stdout)) {
352
+ return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
353
+ }
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(`
356
+ `);
357
+ }
358
+ function replacesRows(replaces, colors = paint(process3.stdout)) {
359
+ return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
360
+ }
361
+ function wantsHelpJson(env = process3.env) {
362
+ return env.KIT_HELP === "json";
363
+ }
364
+ function description(usage, { examples = [], replaces } = {}) {
365
+ const lines = usage.replace(USAGE_PREFIX, "").split(`
366
+ `);
367
+ const first = lines[0];
368
+ const colon = first.indexOf(": ");
369
+ const prose = lines[lines.indexOf("") + 1] ?? "";
370
+ const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
371
+ return {
372
+ synopsis: colon === -1 ? first : first.slice(0, colon),
373
+ summary: colon === -1 ? fallback : first.slice(colon + 2),
374
+ ...replaces === undefined ? {} : { replaces },
375
+ examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
376
+ };
377
+ }
378
+ function exampleRows(examples, colors = paint(process3.stdout)) {
379
+ if (examples.length === 0)
380
+ return [];
381
+ return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
382
+ }
383
+ function optionRows(options, colors = paint(process3.stdout)) {
384
+ const rows = Object.entries(options).map(([name, option]) => {
385
+ const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
386
+ const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
387
+ const placeholder = option.optional === undefined ? named : `[${named}]`;
388
+ const value = option.type === "string" ? ` ${colors.dim(placeholder)}` : "";
389
+ return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? placeholder.length + 1 : 0), description: option.description ?? "" };
390
+ });
391
+ const width = Math.max(0, ...rows.map((row) => row.width));
392
+ return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
393
+ }
394
+
301
395
  // ../kit/src/text.ts
302
396
  var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
303
397
  function distance(a, b) {
@@ -317,8 +411,21 @@ function closest(name, names) {
317
411
  const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
318
412
  return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
319
413
  }
414
+ // ../kit/src/input.ts
415
+ var DURATION = /^(\d+(?:\.\d+)?)(ms|[smhd])?$/;
416
+ var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
417
+ function duration(value, name, fallback, unit = "s") {
418
+ if (value === undefined)
419
+ return fallback;
420
+ const match = DURATION.exec(value.trim());
421
+ const ms = match === null ? 0 : Number(match[1]) * UNIT_MS[match[2] ?? unit];
422
+ if (ms < 1)
423
+ badUsage(`--${name} takes a duration such as 30s, 5m, 1h or 7d, not ${value}`);
424
+ return ms;
425
+ }
320
426
 
321
427
  // ../kit/src/args.ts
428
+ var DASHES = /^-+/;
322
429
  function jsonOption(holds) {
323
430
  return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
324
431
  }
@@ -358,58 +465,11 @@ class Completing {
358
465
  }
359
466
  }
360
467
  var completion = { active: false };
361
- var OPTIONAL = /\[[^\]]+\]/g;
362
- var USAGE_PREFIX = /^usage:\s*/i;
363
- var FULL_STOP = /\.$/;
364
- var DASHES = /^-+/;
365
- function usageLine(usage, colors = paint(process2.stdout)) {
366
- return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
367
- }
368
- function helpText(usage, options, colors = paint(process2.stdout), { examples = [], replaces } = {}) {
369
- return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
370
- `);
371
- }
372
- function replacesRows(replaces, colors = paint(process2.stdout)) {
373
- return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
374
- }
375
- function wantsHelpJson(env = process2.env) {
376
- return env.KIT_HELP === "json";
377
- }
378
- function description(usage, { examples = [], replaces } = {}) {
379
- const lines = usage.replace(USAGE_PREFIX, "").split(`
380
- `);
381
- const first = lines[0];
382
- const colon = first.indexOf(": ");
383
- const prose = lines[lines.indexOf("") + 1] ?? "";
384
- const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
385
- return {
386
- synopsis: colon === -1 ? first : first.slice(0, colon),
387
- summary: colon === -1 ? fallback : first.slice(colon + 2),
388
- ...replaces === undefined ? {} : { replaces },
389
- examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
390
- };
391
- }
392
- function exampleRows(examples, colors = paint(process2.stdout)) {
393
- if (examples.length === 0)
394
- return [];
395
- return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
396
- }
397
- function optionRows(options, colors = paint(process2.stdout)) {
398
- const rows = Object.entries(options).map(([name, option]) => {
399
- const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
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 ?? "" };
404
- });
405
- const width = Math.max(0, ...rows.map((row) => row.width));
406
- return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
407
- }
408
468
  var OUTPUT_OPTIONS = {
409
469
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
410
470
  plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
411
471
  };
412
- function applyOutput(color, plain, env = process2.env) {
472
+ function applyOutput(color, plain, env = process4.env) {
413
473
  const when = plain === true ? "never" : color ?? "auto";
414
474
  if (when === "auto")
415
475
  return;
@@ -568,7 +628,7 @@ function nearestOption(message, options, allowNegative) {
568
628
  }
569
629
  // ../kit/src/process.ts
570
630
  import { constants } from "os";
571
- import process3 from "process";
631
+ import process5 from "process";
572
632
  var KILL_GRACE_MS = 2000;
573
633
  function exitCodeOf(code, signal) {
574
634
  if (code !== null)
@@ -588,7 +648,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal,
588
648
  return;
589
649
  try {
590
650
  if (grouped)
591
- process3.kill(-proc.pid, killing);
651
+ process5.kill(-proc.pid, killing);
592
652
  else
593
653
  proc.kill(killing);
594
654
  } catch {}
@@ -662,7 +722,95 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
662
722
  var RESET_STYLE = `${ESC}[0m`;
663
723
  var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
664
724
  // ../kit/src/cli.ts
665
- import process4 from "process";
725
+ import process7 from "process";
726
+
727
+ // ../kit/src/usage.ts
728
+ import { appendFileSync, mkdirSync } from "fs";
729
+ import { homedir } from "os";
730
+ import { dirname, isAbsolute, join } from "path";
731
+ import process6 from "process";
732
+
733
+ // ../kit/src/logs.ts
734
+ var DAY_MS = 24 * 60 * 60 * 1000;
735
+ var CLI_FOLDER = "kaynooo-cli";
736
+
737
+ // ../kit/src/usage.ts
738
+ var UNKNOWN_OPTION2 = /unknown option '(--[a-z][a-z0-9-]*)[=']/;
739
+ var FLAG_NAME = /^(?:--[a-z][a-z0-9-]*|-[a-zA-Z])$/;
740
+ var LONGEST_NAME = 40;
741
+ var usageError;
742
+ function noteUsageError(message) {
743
+ usageError = message;
744
+ }
745
+ function stateHome(env = process6.env) {
746
+ const dir = env.XDG_STATE_HOME;
747
+ return dir !== undefined && isAbsolute(dir) ? dir : join(homedir(), ".local", "state");
748
+ }
749
+ function usagePath(env = process6.env) {
750
+ return join(stateHome(env), CLI_FOLDER, "usage.jsonl");
751
+ }
752
+ function usageRecorded(env = process6.env) {
753
+ const off = env.KAYNOOO_CLI_NO_USAGE;
754
+ return env.NODE_ENV !== "test" && (off === undefined || off === "" || off === "0");
755
+ }
756
+ function flagNames(argv) {
757
+ const end = argv.indexOf("--");
758
+ const names = (end === -1 ? argv : argv.slice(0, end)).filter((word) => word.startsWith("-") && word !== "-").map((word) => word.includes("=") ? word.slice(0, word.indexOf("=")) : word);
759
+ return [...new Set(names)].filter((name) => name.length <= LONGEST_NAME && FLAG_NAME.test(name));
760
+ }
761
+ function usageEntry(bin, command, argv, code, now = new Date) {
762
+ const unknown = code === 2 && usageError !== undefined ? UNKNOWN_OPTION2.exec(usageError)?.[1] : undefined;
763
+ usageError = undefined;
764
+ return { at: now.toISOString(), bin, ...command === undefined ? {} : { command }, flags: flagNames(argv), code, ...unknown === undefined ? {} : { unknown } };
765
+ }
766
+ function recordUsage(entry, env = process6.env) {
767
+ if (!usageRecorded(env))
768
+ return;
769
+ try {
770
+ const path = usagePath(env);
771
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
772
+ appendFileSync(path, `${JSON.stringify(entry)}
773
+ `, { mode: 384 });
774
+ } catch {}
775
+ }
776
+ async function readUsage(path = usagePath(), since = 0) {
777
+ const file = Bun.file(path);
778
+ if (!await file.exists())
779
+ return [];
780
+ return (await file.text()).split(`
781
+ `).flatMap((line) => {
782
+ try {
783
+ const entry = JSON.parse(line);
784
+ return Date.parse(entry.at) >= since ? [entry] : [];
785
+ } catch {
786
+ return [];
787
+ }
788
+ });
789
+ }
790
+ function summarizeUsage(entries) {
791
+ const bins = new Map;
792
+ const errors = new Map;
793
+ for (const entry of entries) {
794
+ const bin = bins.get(entry.bin) ?? { runs: 0, failed: 0, usageErrors: 0 };
795
+ bin.runs++;
796
+ bin.failed += entry.code === 0 ? 0 : 1;
797
+ bin.usageErrors += entry.code === 2 ? 1 : 0;
798
+ bins.set(entry.bin, bin);
799
+ if (entry.code === 2 && entry.unknown !== undefined) {
800
+ const key = `${entry.bin} ${entry.command ?? ""} ${entry.unknown}`;
801
+ const error = errors.get(key) ?? { bin: entry.bin, ...entry.command === undefined ? {} : { command: entry.command }, unknown: entry.unknown, count: 0 };
802
+ error.count++;
803
+ errors.set(key, error);
804
+ }
805
+ }
806
+ return {
807
+ runs: entries.length,
808
+ bins: [...bins].map(([bin, counts]) => ({ bin, ...counts })).toSorted((a, b) => b.runs - a.runs),
809
+ usageErrors: [...errors.values()].toSorted((a, b) => b.count - a.count)
810
+ };
811
+ }
812
+
813
+ // ../kit/src/cli.ts
666
814
  var LEVEL_OPTIONS = {
667
815
  ...OUTPUT_OPTIONS,
668
816
  help: { type: "boolean", short: "h", description: "show this help" }
@@ -715,7 +863,7 @@ async function expand(scope, words) {
715
863
  }
716
864
  return { ...scope, commands, lazy: [] };
717
865
  }
718
- async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
866
+ async function help2({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
719
867
  const colors = paint(stream);
720
868
  const prefix = path.join(" ");
721
869
  const summary = await resolveSummary(lazySummary);
@@ -742,8 +890,8 @@ async function help({ path, header, summary: lazySummary, commands, defaultComma
742
890
  `);
743
891
  }
744
892
  function printError(prefix, message, hint, cause) {
745
- const colors = paint(process4.stderr);
746
- const styled = takesControlCodes(process4.stderr);
893
+ const colors = paint(process7.stderr);
894
+ const styled = takesControlCodes(process7.stderr);
747
895
  const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
748
896
  const under = (label, text, tint) => text === undefined ? "" : `
749
897
  ${styled ? tint(label) : `${label}:`} ${text}`;
@@ -762,6 +910,8 @@ function reportThrown(prefix, error, redact = (text) => text, json = false) {
762
910
  const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
763
911
  const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
764
912
  const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
913
+ if (error instanceof UsageError)
914
+ noteUsageError(error.message);
765
915
  const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
766
916
  report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
767
917
  return error instanceof Exit ? error.code : 1;
@@ -784,11 +934,11 @@ async function dispatch(level, typed, { version, redact }) {
784
934
  }
785
935
  }
786
936
  if (name === undefined && level.path.length > 1) {
787
- console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stderr));
937
+ console.error(await help2(await expand(level, [...level.path.slice(1), ...argv]), process7.stderr));
788
938
  return 2;
789
939
  }
790
940
  if (name === undefined || name === "help" || name === "--help" || name === "-h") {
791
- console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stdout));
941
+ console.log(await help2(await expand(level, [...level.path.slice(1), ...argv]), process7.stdout));
792
942
  return 0;
793
943
  }
794
944
  if (level.path.length === 1 && (name === "--version" || name === "-v")) {
@@ -904,26 +1054,49 @@ async function printCandidates(found) {
904
1054
  console.log(line);
905
1055
  return 0;
906
1056
  }
907
- async function runCli(cli, argv = process4.argv.slice(2)) {
1057
+ async function runCli(cli, argv = process7.argv.slice(2)) {
908
1058
  if (argv[0] === COMPLETE)
909
1059
  return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
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 });
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 });
1061
+ if (cli.usage !== false) {
1062
+ try {
1063
+ recordUsage(usageEntry(cli.name, commandPath(scopeOf(cli.commands, cli.extraCommands, cli.aliases), argv), argv, code));
1064
+ } catch {}
1065
+ }
1066
+ return code;
1067
+ }
1068
+ function commandPath(scope, argv) {
1069
+ const path = [];
1070
+ let level = scope;
1071
+ for (const word of argv.filter((each) => !each.startsWith("-"))) {
1072
+ const name = level.aliases !== undefined && Object.hasOwn(level.aliases, word) ? level.aliases[word] ?? word : word;
1073
+ const command = Object.hasOwn(level.commands, name) ? level.commands[name] : undefined;
1074
+ if (command === undefined)
1075
+ break;
1076
+ path.push(word);
1077
+ if (!("commands" in command))
1078
+ break;
1079
+ level = scopeOf(command.commands, command.extraCommands, command.aliases);
1080
+ }
1081
+ return path.length === 0 ? undefined : path.join(" ");
911
1082
  }
912
- async function runMain({ name, version, run, redact }, argv = process4.argv.slice(2)) {
1083
+ async function runMain({ name, version, run, redact, usage }, argv = process7.argv.slice(2)) {
913
1084
  if (argv[0] === COMPLETE)
914
1085
  return printCandidates(leafCandidates(run, argv.slice(1)));
915
1086
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
916
1087
  console.log(version);
917
1088
  return 0;
918
1089
  }
1090
+ let code;
919
1091
  try {
920
- return await run(argv);
1092
+ code = await run(argv);
921
1093
  } catch (error) {
922
- return reportThrown(name, error, redact, wantsJson(argv));
1094
+ code = reportThrown(name, error, redact, wantsJson(argv));
923
1095
  }
1096
+ if (usage !== false)
1097
+ recordUsage(usageEntry(name, undefined, argv, code));
1098
+ return code;
924
1099
  }
925
- // ../kit/src/logs.ts
926
- var DAY_MS = 24 * 60 * 60 * 1000;
927
1100
  // ../kit/src/secrets.ts
928
1101
  var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
929
1102
  // ../kit/src/sessions.ts
@@ -931,16 +1104,16 @@ var {Glob } = globalThis.Bun;
931
1104
  var SESSION_FILES = new Glob("*.json");
932
1105
  var ACCOUNT_DIRS = new Glob("*/sessions");
933
1106
  // package.json
934
- var version = "0.3.0";
1107
+ var version = "0.4.0";
935
1108
 
936
1109
  // src/completions.ts
937
- import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, rmSync } from "fs";
938
- import { join as join2 } from "path";
939
- import process6 from "process";
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";
940
1113
 
941
1114
  // src/checks.ts
942
1115
  import { existsSync, readFileSync } from "fs";
943
- import { basename, join } from "path";
1116
+ import { basename, join as join2 } from "path";
944
1117
  var TOOLS = {
945
1118
  "@kaynooo/cli-base": ["base", "proof"],
946
1119
  "@kaynooo/cli-changelog": ["changelog"],
@@ -955,8 +1128,13 @@ var TOOLS = {
955
1128
  "@kaynooo/cli-gitlab": ["gl"],
956
1129
  "@kaynooo/cli-guard": ["guard"],
957
1130
  "@kaynooo/cli-out": ["out"],
1131
+ "@kaynooo/cli-dev": ["dev"],
1132
+ "@kaynooo/cli-pick": ["pick"],
958
1133
  "@kaynooo/cli-pkg": ["pkg"],
1134
+ "@kaynooo/cli-probe": ["probe"],
1135
+ "@kaynooo/cli-repos": ["repos"],
959
1136
  "@kaynooo/cli-stack": ["stack"],
1137
+ "@kaynooo/cli-sweep": ["sweep"],
960
1138
  "@kaynooo/cli-view": ["view"],
961
1139
  "@kaynooo/cli-wait": ["waitfor", "slot"]
962
1140
  };
@@ -984,12 +1162,12 @@ function firstLine(text) {
984
1162
  return end === -1 ? trimmed : trimmed.slice(0, end);
985
1163
  }
986
1164
  function installed(host, name) {
987
- return readJson(join(host.globalModules, name, "package.json"));
1165
+ return readJson(join2(host.globalModules, name, "package.json"));
988
1166
  }
989
1167
  function ownBin(host, name, bin) {
990
1168
  const declared = installed(host, name)?.bin;
991
1169
  const file = typeof declared === "string" ? declared : declared?.[bin];
992
- return file === undefined ? undefined : join(host.globalModules, name, file);
1170
+ return file === undefined ? undefined : join2(host.globalModules, name, file);
993
1171
  }
994
1172
  async function checkTool(host, name, bins) {
995
1173
  const check = name.slice(name.indexOf("/") + 1);
@@ -998,7 +1176,7 @@ async function checkTool(host, name, bins) {
998
1176
  return { check, ok: false, detail: "not installed", fix: `bun install -g ${name}` };
999
1177
  const off = bins.filter((bin) => host.which(bin) === null);
1000
1178
  if (off.length > 0)
1001
- return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1179
+ return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join2(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1002
1180
  const info = await host.run(["bun", "info", name, "version", "--no-cache"]);
1003
1181
  const answered = info.code === 0 ? firstLine(info.stdout.trim().split(`
1004
1182
  `).at(-1) ?? "") : "";
@@ -1025,9 +1203,9 @@ async function checkTea(host) {
1025
1203
  return { check: "tea", ok: true, detail: `logged in to ${forge} as ${login.name}, Forgejo ${served}` };
1026
1204
  }
1027
1205
  function checkHooks(host) {
1028
- const settings = readJson(join(host.home, ".claude", "settings.json")) ?? {};
1206
+ const settings = readJson(join2(host.home, ".claude", "settings.json")) ?? {};
1029
1207
  const plugin = settings.enabledPlugins?.[PLUGIN] === true;
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")));
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")));
1031
1209
  if (plugin && mod !== undefined)
1032
1210
  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}` };
1033
1211
  if (plugin)
@@ -1074,8 +1252,8 @@ var FUNCTION = `functions -q __kaynooo_cli_complete; or function __kaynooo_cli_c
1074
1252
  end
1075
1253
  end
1076
1254
  end`;
1077
- function completionDir(home, env = process6.env) {
1078
- return join2(env.XDG_CACHE_HOME ?? join2(home, ".cache"), "fish", "kaynooo-cli");
1255
+ function completionDir(home, env = process9.env) {
1256
+ return join3(env.XDG_CACHE_HOME ?? join3(home, ".cache"), "fish", "kaynooo-cli");
1079
1257
  }
1080
1258
  function installedBins(host) {
1081
1259
  return Object.values(TOOLS).flat().filter((bin) => host.which(bin) !== null);
@@ -1088,31 +1266,31 @@ complete -c ${bin} -f -a '(__kaynooo_cli_complete ${bin})'
1088
1266
  }
1089
1267
  async function installCompletions(host, dir) {
1090
1268
  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));
1269
+ 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));
1093
1271
  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))));
1272
+ rmSync(join3(dir, name));
1273
+ await Promise.all(bins.map(async (bin) => Bun.write(join3(dir, `${bin}.fish`), completionFile(bin))));
1096
1274
  return { written: bins, removed: removed.map((name) => name.replace(FISH_FILE, "")) };
1097
1275
  }
1098
1276
  function missingCompletions(host, dir) {
1099
- return installedBins(host).filter((bin) => !existsSync2(join2(dir, `${bin}.fish`)));
1277
+ return installedBins(host).filter((bin) => !existsSync2(join3(dir, `${bin}.fish`)));
1100
1278
  }
1101
1279
 
1102
1280
  // src/host.ts
1103
- import { homedir } from "os";
1104
- import { join as join3 } from "path";
1105
- import process7 from "process";
1281
+ import { homedir as homedir2 } from "os";
1282
+ import { join as join4 } from "path";
1283
+ import process10 from "process";
1106
1284
  var RUN_TIMEOUT_MS = 20000;
1107
1285
  function hostHere() {
1108
- const home = homedir();
1286
+ const home = homedir2();
1109
1287
  return {
1110
1288
  home,
1111
- globalModules: join3(process7.env.BUN_INSTALL ?? join3(home, ".bun"), "install", "global", "node_modules"),
1289
+ globalModules: join4(process10.env.BUN_INSTALL ?? join4(home, ".bun"), "install", "global", "node_modules"),
1112
1290
  bunVersion: Bun.version,
1113
1291
  which: (bin) => Bun.which(bin),
1114
1292
  run: async (argv, env) => {
1115
- const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process7.env, ...env } } });
1293
+ const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process10.env, ...env } } });
1116
1294
  if (ran.timedOut)
1117
1295
  return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
1118
1296
  const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
@@ -1146,7 +1324,7 @@ var LIST_OPTIONS = {
1146
1324
  json: jsonOption("[{bin, package, installed, synopsis?, summary?, replaces?, examples?}]")
1147
1325
  };
1148
1326
  function print(listed) {
1149
- const { cyan, dim } = paint(process8.stdout);
1327
+ const { cyan, dim } = paint(process11.stdout);
1150
1328
  const width = Math.max(...listed.map((each) => each.bin.length));
1151
1329
  for (const each of listed) {
1152
1330
  const what = !each.installed ? dim(`not installed: bun install -g ${each.package}`) : `${each.summary ?? ""}${each.replaces === undefined ? "" : dim(`; instead of ${each.replaces}`)}`;
@@ -1184,8 +1362,37 @@ async function completions(args, host = hostHere()) {
1184
1362
  console.log(`wrote ${written.length} completions to ${dir}${removed.length === 0 ? "" : `, took out ${removed.join(", ")}`}`);
1185
1363
  return 0;
1186
1364
  }
1365
+ var STATS_USAGE = `cli stats [--since 7d] [--json]: how often each bin ran, and the usage errors by the option it did not know
1366
+
1367
+ Every bin records each run in ~/.local/state/kaynooo-cli/usage.jsonl (or $XDG_STATE_HOME): the bin, its command, the names of the flags it saw (never a value or an argument), the exit code and, on a usage error, the unknown option. Set KAYNOOO_CLI_NO_USAGE=1 to stop recording; \`guard\` never records.
1368
+ Exits 0 when it printed the summary, 2 on a usage error.`;
1369
+ var STATS_OPTIONS = {
1370
+ since: { type: "string", description: "only the runs this recent (7d, 12h)" },
1371
+ json: jsonOption("{runs, bins: [{bin, runs, failed, usageErrors}], usageErrors: [{bin, command?, unknown, count}]}")
1372
+ };
1373
+ async function stats(args, path = usagePath()) {
1374
+ const { values } = parse(args, STATS_USAGE, STATS_OPTIONS, { positionals: [], examples: ["cli stats", ["cli stats --since 7d", "the last week"]] });
1375
+ const window = values.since === undefined ? undefined : duration(values.since, "since", 0, "h");
1376
+ const summary = summarizeUsage(await readUsage(path, window === undefined ? 0 : Date.now() - window));
1377
+ if (values.json === true) {
1378
+ printJson(summary);
1379
+ return 0;
1380
+ }
1381
+ if (summary.runs === 0) {
1382
+ console.log("no run recorded");
1383
+ return 0;
1384
+ }
1385
+ console.log(`${summary.runs} runs
1386
+
1387
+ ${table(summary.bins.map((each) => [each.bin, String(each.runs), `${each.failed} failed`, `${each.usageErrors} usage errors`]))}`);
1388
+ if (summary.usageErrors.length > 0)
1389
+ console.log(`
1390
+ unknown options
1391
+ ${table(summary.usageErrors.map((each) => [`${each.bin}${each.command === undefined ? "" : ` ${each.command}`}`, each.unknown, `${each.count}x`]))}`);
1392
+ return 0;
1393
+ }
1187
1394
  if (import.meta.main) {
1188
- process8.exitCode = await runCli({
1395
+ process11.exitCode = await runCli({
1189
1396
  name: "cli",
1190
1397
  version,
1191
1398
  about: "the bins of the @kaynooo/cli-* packages, described by their own help",
@@ -1193,11 +1400,13 @@ if (import.meta.main) {
1193
1400
  examples: ["cli list", ["cli list --json", "with each bin's synopsis and examples"]],
1194
1401
  commands: {
1195
1402
  list: { summary: "every bin in one line: what it does and what it is reached for instead of", run: async (args) => list(args) },
1403
+ stats: { summary: "how often each bin ran and the options usage errors did not know", run: async (args) => stats(args) },
1196
1404
  completions: { summary: "fish completions for every installed bin, printed or written to the folder fish reads", run: async (args) => completions(args) }
1197
1405
  }
1198
1406
  });
1199
1407
  }
1200
1408
  export {
1201
1409
  completions,
1202
- list
1410
+ list,
1411
+ stats
1203
1412
  };
package/dist/cli.js CHANGED
@@ -2,12 +2,15 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli.ts
5
- import process8 from "process";
5
+ import process11 from "process";
6
6
 
7
7
  // ../kit/src/args.ts
8
- import process2 from "process";
8
+ import process4 from "process";
9
9
  import { parseArgs } from "util";
10
10
 
11
+ // ../kit/src/format.ts
12
+ import process2 from "process";
13
+
11
14
  // ../kit/src/style.ts
12
15
  import process from "process";
13
16
 
@@ -293,11 +296,102 @@ function upTo(text, separator) {
293
296
  const end = text.indexOf(separator);
294
297
  return end === -1 ? text : text.slice(0, end);
295
298
  }
299
+ function truncate(text, width) {
300
+ return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
301
+ }
302
+ var LINE_BREAKS = /\s*\n\s*/g;
296
303
  var ESCAPE = String.fromCharCode(27);
304
+ function textWidth(text) {
305
+ return Bun.stringWidth(text);
306
+ }
307
+ function table(rows, { cellWidth, header = false, align = [], fit } = {}) {
308
+ const cells = cellWidth === undefined ? rows : rows.map((row) => row.map((cell) => truncate(cell.replaceAll(LINE_BREAKS, " "), cellWidth)));
309
+ const widths = [];
310
+ for (const row of cells) {
311
+ for (const [index, cell] of row.entries())
312
+ widths[index] = Math.max(widths[index] ?? 0, textWidth(cell));
313
+ }
314
+ const shown = fit === undefined ? cells : narrow(cells, widths, fit);
315
+ const colors = paint(process2.stdout);
316
+ const lines = shown.map((row) => row.map((cell, index) => {
317
+ const width = widths[index] ?? 0;
318
+ const gap = " ".repeat(Math.max(0, width - textWidth(cell)));
319
+ if (align[index] === "right")
320
+ return `${gap}${cell}`;
321
+ return index === row.length - 1 ? cell : `${cell}${gap}`;
322
+ }).join(" ").trimEnd());
323
+ return lines.map((line, index) => header && index === 0 ? colors.bold(line) : line).join(`
324
+ `);
325
+ }
326
+ function narrow(rows, widths, limit) {
327
+ const used = () => widths.reduce((sum, width) => sum + width, 0) + 2 * Math.max(0, widths.length - 1);
328
+ const cuttable = (index) => rows.every((row) => !(row[index] ?? "").includes(ESCAPE));
329
+ const floor = 8;
330
+ while (used() > limit) {
331
+ let widest = -1;
332
+ for (const [index, width] of widths.entries()) {
333
+ if (cuttable(index) && width > floor && width > (widths[widest] ?? 0))
334
+ widest = index;
335
+ }
336
+ if (widest === -1)
337
+ break;
338
+ widths[widest] = Math.max(floor, (widths[widest] ?? 0) - (used() - limit));
339
+ }
340
+ return rows.map((row) => row.map((cell, index) => textWidth(cell) > (widths[index] ?? 0) ? truncate(cell, widths[index] ?? 0) : cell));
341
+ }
297
342
  function printJson(value) {
298
343
  console.log(JSON.stringify(value, undefined, 2));
299
344
  }
300
345
 
346
+ // ../kit/src/help.ts
347
+ import process3 from "process";
348
+ var OPTIONAL = /\[[^\]]+\]/g;
349
+ var USAGE_PREFIX = /^usage:\s*/i;
350
+ var FULL_STOP = /\.$/;
351
+ function usageLine(usage, colors = paint(process3.stdout)) {
352
+ return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
353
+ }
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(`
356
+ `);
357
+ }
358
+ function replacesRows(replaces, colors = paint(process3.stdout)) {
359
+ return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
360
+ }
361
+ function wantsHelpJson(env = process3.env) {
362
+ return env.KIT_HELP === "json";
363
+ }
364
+ function description(usage, { examples = [], replaces } = {}) {
365
+ const lines = usage.replace(USAGE_PREFIX, "").split(`
366
+ `);
367
+ const first = lines[0];
368
+ const colon = first.indexOf(": ");
369
+ const prose = lines[lines.indexOf("") + 1] ?? "";
370
+ const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
371
+ return {
372
+ synopsis: colon === -1 ? first : first.slice(0, colon),
373
+ summary: colon === -1 ? fallback : first.slice(colon + 2),
374
+ ...replaces === undefined ? {} : { replaces },
375
+ examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
376
+ };
377
+ }
378
+ function exampleRows(examples, colors = paint(process3.stdout)) {
379
+ if (examples.length === 0)
380
+ return [];
381
+ return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
382
+ }
383
+ function optionRows(options, colors = paint(process3.stdout)) {
384
+ const rows = Object.entries(options).map(([name, option]) => {
385
+ const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
386
+ const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
387
+ const placeholder = option.optional === undefined ? named : `[${named}]`;
388
+ const value = option.type === "string" ? ` ${colors.dim(placeholder)}` : "";
389
+ return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? placeholder.length + 1 : 0), description: option.description ?? "" };
390
+ });
391
+ const width = Math.max(0, ...rows.map((row) => row.width));
392
+ return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
393
+ }
394
+
301
395
  // ../kit/src/text.ts
302
396
  var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
303
397
  function distance(a, b) {
@@ -317,8 +411,21 @@ function closest(name, names) {
317
411
  const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
318
412
  return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
319
413
  }
414
+ // ../kit/src/input.ts
415
+ var DURATION = /^(\d+(?:\.\d+)?)(ms|[smhd])?$/;
416
+ var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
417
+ function duration(value, name, fallback, unit = "s") {
418
+ if (value === undefined)
419
+ return fallback;
420
+ const match = DURATION.exec(value.trim());
421
+ const ms = match === null ? 0 : Number(match[1]) * UNIT_MS[match[2] ?? unit];
422
+ if (ms < 1)
423
+ badUsage(`--${name} takes a duration such as 30s, 5m, 1h or 7d, not ${value}`);
424
+ return ms;
425
+ }
320
426
 
321
427
  // ../kit/src/args.ts
428
+ var DASHES = /^-+/;
322
429
  function jsonOption(holds) {
323
430
  return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
324
431
  }
@@ -358,58 +465,11 @@ class Completing {
358
465
  }
359
466
  }
360
467
  var completion = { active: false };
361
- var OPTIONAL = /\[[^\]]+\]/g;
362
- var USAGE_PREFIX = /^usage:\s*/i;
363
- var FULL_STOP = /\.$/;
364
- var DASHES = /^-+/;
365
- function usageLine(usage, colors = paint(process2.stdout)) {
366
- return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
367
- }
368
- function helpText(usage, options, colors = paint(process2.stdout), { examples = [], replaces } = {}) {
369
- return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
370
- `);
371
- }
372
- function replacesRows(replaces, colors = paint(process2.stdout)) {
373
- return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
374
- }
375
- function wantsHelpJson(env = process2.env) {
376
- return env.KIT_HELP === "json";
377
- }
378
- function description(usage, { examples = [], replaces } = {}) {
379
- const lines = usage.replace(USAGE_PREFIX, "").split(`
380
- `);
381
- const first = lines[0];
382
- const colon = first.indexOf(": ");
383
- const prose = lines[lines.indexOf("") + 1] ?? "";
384
- const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
385
- return {
386
- synopsis: colon === -1 ? first : first.slice(0, colon),
387
- summary: colon === -1 ? fallback : first.slice(colon + 2),
388
- ...replaces === undefined ? {} : { replaces },
389
- examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
390
- };
391
- }
392
- function exampleRows(examples, colors = paint(process2.stdout)) {
393
- if (examples.length === 0)
394
- return [];
395
- return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
396
- }
397
- function optionRows(options, colors = paint(process2.stdout)) {
398
- const rows = Object.entries(options).map(([name, option]) => {
399
- const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
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 ?? "" };
404
- });
405
- const width = Math.max(0, ...rows.map((row) => row.width));
406
- return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
407
- }
408
468
  var OUTPUT_OPTIONS = {
409
469
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
410
470
  plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
411
471
  };
412
- function applyOutput(color, plain, env = process2.env) {
472
+ function applyOutput(color, plain, env = process4.env) {
413
473
  const when = plain === true ? "never" : color ?? "auto";
414
474
  if (when === "auto")
415
475
  return;
@@ -568,7 +628,7 @@ function nearestOption(message, options, allowNegative) {
568
628
  }
569
629
  // ../kit/src/process.ts
570
630
  import { constants } from "os";
571
- import process3 from "process";
631
+ import process5 from "process";
572
632
  var KILL_GRACE_MS = 2000;
573
633
  function exitCodeOf(code, signal) {
574
634
  if (code !== null)
@@ -588,7 +648,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal,
588
648
  return;
589
649
  try {
590
650
  if (grouped)
591
- process3.kill(-proc.pid, killing);
651
+ process5.kill(-proc.pid, killing);
592
652
  else
593
653
  proc.kill(killing);
594
654
  } catch {}
@@ -662,7 +722,95 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
662
722
  var RESET_STYLE = `${ESC}[0m`;
663
723
  var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
664
724
  // ../kit/src/cli.ts
665
- import process4 from "process";
725
+ import process7 from "process";
726
+
727
+ // ../kit/src/usage.ts
728
+ import { appendFileSync, mkdirSync } from "fs";
729
+ import { homedir } from "os";
730
+ import { dirname, isAbsolute, join } from "path";
731
+ import process6 from "process";
732
+
733
+ // ../kit/src/logs.ts
734
+ var DAY_MS = 24 * 60 * 60 * 1000;
735
+ var CLI_FOLDER = "kaynooo-cli";
736
+
737
+ // ../kit/src/usage.ts
738
+ var UNKNOWN_OPTION2 = /unknown option '(--[a-z][a-z0-9-]*)[=']/;
739
+ var FLAG_NAME = /^(?:--[a-z][a-z0-9-]*|-[a-zA-Z])$/;
740
+ var LONGEST_NAME = 40;
741
+ var usageError;
742
+ function noteUsageError(message) {
743
+ usageError = message;
744
+ }
745
+ function stateHome(env = process6.env) {
746
+ const dir = env.XDG_STATE_HOME;
747
+ return dir !== undefined && isAbsolute(dir) ? dir : join(homedir(), ".local", "state");
748
+ }
749
+ function usagePath(env = process6.env) {
750
+ return join(stateHome(env), CLI_FOLDER, "usage.jsonl");
751
+ }
752
+ function usageRecorded(env = process6.env) {
753
+ const off = env.KAYNOOO_CLI_NO_USAGE;
754
+ return env.NODE_ENV !== "test" && (off === undefined || off === "" || off === "0");
755
+ }
756
+ function flagNames(argv) {
757
+ const end = argv.indexOf("--");
758
+ const names = (end === -1 ? argv : argv.slice(0, end)).filter((word) => word.startsWith("-") && word !== "-").map((word) => word.includes("=") ? word.slice(0, word.indexOf("=")) : word);
759
+ return [...new Set(names)].filter((name) => name.length <= LONGEST_NAME && FLAG_NAME.test(name));
760
+ }
761
+ function usageEntry(bin, command, argv, code, now = new Date) {
762
+ const unknown = code === 2 && usageError !== undefined ? UNKNOWN_OPTION2.exec(usageError)?.[1] : undefined;
763
+ usageError = undefined;
764
+ return { at: now.toISOString(), bin, ...command === undefined ? {} : { command }, flags: flagNames(argv), code, ...unknown === undefined ? {} : { unknown } };
765
+ }
766
+ function recordUsage(entry, env = process6.env) {
767
+ if (!usageRecorded(env))
768
+ return;
769
+ try {
770
+ const path = usagePath(env);
771
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
772
+ appendFileSync(path, `${JSON.stringify(entry)}
773
+ `, { mode: 384 });
774
+ } catch {}
775
+ }
776
+ async function readUsage(path = usagePath(), since = 0) {
777
+ const file = Bun.file(path);
778
+ if (!await file.exists())
779
+ return [];
780
+ return (await file.text()).split(`
781
+ `).flatMap((line) => {
782
+ try {
783
+ const entry = JSON.parse(line);
784
+ return Date.parse(entry.at) >= since ? [entry] : [];
785
+ } catch {
786
+ return [];
787
+ }
788
+ });
789
+ }
790
+ function summarizeUsage(entries) {
791
+ const bins = new Map;
792
+ const errors = new Map;
793
+ for (const entry of entries) {
794
+ const bin = bins.get(entry.bin) ?? { runs: 0, failed: 0, usageErrors: 0 };
795
+ bin.runs++;
796
+ bin.failed += entry.code === 0 ? 0 : 1;
797
+ bin.usageErrors += entry.code === 2 ? 1 : 0;
798
+ bins.set(entry.bin, bin);
799
+ if (entry.code === 2 && entry.unknown !== undefined) {
800
+ const key = `${entry.bin} ${entry.command ?? ""} ${entry.unknown}`;
801
+ const error = errors.get(key) ?? { bin: entry.bin, ...entry.command === undefined ? {} : { command: entry.command }, unknown: entry.unknown, count: 0 };
802
+ error.count++;
803
+ errors.set(key, error);
804
+ }
805
+ }
806
+ return {
807
+ runs: entries.length,
808
+ bins: [...bins].map(([bin, counts]) => ({ bin, ...counts })).toSorted((a, b) => b.runs - a.runs),
809
+ usageErrors: [...errors.values()].toSorted((a, b) => b.count - a.count)
810
+ };
811
+ }
812
+
813
+ // ../kit/src/cli.ts
666
814
  var LEVEL_OPTIONS = {
667
815
  ...OUTPUT_OPTIONS,
668
816
  help: { type: "boolean", short: "h", description: "show this help" }
@@ -715,7 +863,7 @@ async function expand(scope, words) {
715
863
  }
716
864
  return { ...scope, commands, lazy: [] };
717
865
  }
718
- async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
866
+ async function help2({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
719
867
  const colors = paint(stream);
720
868
  const prefix = path.join(" ");
721
869
  const summary = await resolveSummary(lazySummary);
@@ -742,8 +890,8 @@ async function help({ path, header, summary: lazySummary, commands, defaultComma
742
890
  `);
743
891
  }
744
892
  function printError(prefix, message, hint, cause) {
745
- const colors = paint(process4.stderr);
746
- const styled = takesControlCodes(process4.stderr);
893
+ const colors = paint(process7.stderr);
894
+ const styled = takesControlCodes(process7.stderr);
747
895
  const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
748
896
  const under = (label, text, tint) => text === undefined ? "" : `
749
897
  ${styled ? tint(label) : `${label}:`} ${text}`;
@@ -762,6 +910,8 @@ function reportThrown(prefix, error, redact = (text) => text, json = false) {
762
910
  const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
763
911
  const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
764
912
  const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
913
+ if (error instanceof UsageError)
914
+ noteUsageError(error.message);
765
915
  const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
766
916
  report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
767
917
  return error instanceof Exit ? error.code : 1;
@@ -784,11 +934,11 @@ async function dispatch(level, typed, { version, redact }) {
784
934
  }
785
935
  }
786
936
  if (name === undefined && level.path.length > 1) {
787
- console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stderr));
937
+ console.error(await help2(await expand(level, [...level.path.slice(1), ...argv]), process7.stderr));
788
938
  return 2;
789
939
  }
790
940
  if (name === undefined || name === "help" || name === "--help" || name === "-h") {
791
- console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stdout));
941
+ console.log(await help2(await expand(level, [...level.path.slice(1), ...argv]), process7.stdout));
792
942
  return 0;
793
943
  }
794
944
  if (level.path.length === 1 && (name === "--version" || name === "-v")) {
@@ -904,26 +1054,49 @@ async function printCandidates(found) {
904
1054
  console.log(line);
905
1055
  return 0;
906
1056
  }
907
- async function runCli(cli, argv = process4.argv.slice(2)) {
1057
+ async function runCli(cli, argv = process7.argv.slice(2)) {
908
1058
  if (argv[0] === COMPLETE)
909
1059
  return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
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 });
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 });
1061
+ if (cli.usage !== false) {
1062
+ try {
1063
+ recordUsage(usageEntry(cli.name, commandPath(scopeOf(cli.commands, cli.extraCommands, cli.aliases), argv), argv, code));
1064
+ } catch {}
1065
+ }
1066
+ return code;
1067
+ }
1068
+ function commandPath(scope, argv) {
1069
+ const path = [];
1070
+ let level = scope;
1071
+ for (const word of argv.filter((each) => !each.startsWith("-"))) {
1072
+ const name = level.aliases !== undefined && Object.hasOwn(level.aliases, word) ? level.aliases[word] ?? word : word;
1073
+ const command = Object.hasOwn(level.commands, name) ? level.commands[name] : undefined;
1074
+ if (command === undefined)
1075
+ break;
1076
+ path.push(word);
1077
+ if (!("commands" in command))
1078
+ break;
1079
+ level = scopeOf(command.commands, command.extraCommands, command.aliases);
1080
+ }
1081
+ return path.length === 0 ? undefined : path.join(" ");
911
1082
  }
912
- async function runMain({ name, version, run, redact }, argv = process4.argv.slice(2)) {
1083
+ async function runMain({ name, version, run, redact, usage }, argv = process7.argv.slice(2)) {
913
1084
  if (argv[0] === COMPLETE)
914
1085
  return printCandidates(leafCandidates(run, argv.slice(1)));
915
1086
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
916
1087
  console.log(version);
917
1088
  return 0;
918
1089
  }
1090
+ let code;
919
1091
  try {
920
- return await run(argv);
1092
+ code = await run(argv);
921
1093
  } catch (error) {
922
- return reportThrown(name, error, redact, wantsJson(argv));
1094
+ code = reportThrown(name, error, redact, wantsJson(argv));
923
1095
  }
1096
+ if (usage !== false)
1097
+ recordUsage(usageEntry(name, undefined, argv, code));
1098
+ return code;
924
1099
  }
925
- // ../kit/src/logs.ts
926
- var DAY_MS = 24 * 60 * 60 * 1000;
927
1100
  // ../kit/src/secrets.ts
928
1101
  var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
929
1102
  // ../kit/src/sessions.ts
@@ -931,16 +1104,16 @@ var {Glob } = globalThis.Bun;
931
1104
  var SESSION_FILES = new Glob("*.json");
932
1105
  var ACCOUNT_DIRS = new Glob("*/sessions");
933
1106
  // package.json
934
- var version = "0.3.0";
1107
+ var version = "0.4.0";
935
1108
 
936
1109
  // src/checks.ts
937
1110
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
938
- import { basename, join as join2 } from "path";
1111
+ import { basename, join as join3 } from "path";
939
1112
 
940
1113
  // src/completions.ts
941
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "fs";
942
- import { join } from "path";
943
- import process6 from "process";
1114
+ import { existsSync, mkdirSync as mkdirSync2, readdirSync, readFileSync, rmSync } from "fs";
1115
+ import { join as join2 } from "path";
1116
+ import process9 from "process";
944
1117
  var MARKER = "# Generated by `cli completions fish`";
945
1118
  var FISH_FILE = /\.fish$/;
946
1119
  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'
@@ -954,8 +1127,8 @@ var FUNCTION = `functions -q __kaynooo_cli_complete; or function __kaynooo_cli_c
954
1127
  end
955
1128
  end
956
1129
  end`;
957
- function completionDir(home, env = process6.env) {
958
- return join(env.XDG_CACHE_HOME ?? join(home, ".cache"), "fish", "kaynooo-cli");
1130
+ function completionDir(home, env = process9.env) {
1131
+ return join2(env.XDG_CACHE_HOME ?? join2(home, ".cache"), "fish", "kaynooo-cli");
959
1132
  }
960
1133
  function installedBins(host) {
961
1134
  return Object.values(TOOLS).flat().filter((bin) => host.which(bin) !== null);
@@ -968,15 +1141,15 @@ complete -c ${bin} -f -a '(__kaynooo_cli_complete ${bin})'
968
1141
  }
969
1142
  async function installCompletions(host, dir) {
970
1143
  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));
1144
+ 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));
973
1146
  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))));
1147
+ rmSync(join2(dir, name));
1148
+ await Promise.all(bins.map(async (bin) => Bun.write(join2(dir, `${bin}.fish`), completionFile(bin))));
976
1149
  return { written: bins, removed: removed.map((name) => name.replace(FISH_FILE, "")) };
977
1150
  }
978
1151
  function missingCompletions(host, dir) {
979
- return installedBins(host).filter((bin) => !existsSync(join(dir, `${bin}.fish`)));
1152
+ return installedBins(host).filter((bin) => !existsSync(join2(dir, `${bin}.fish`)));
980
1153
  }
981
1154
 
982
1155
  // src/checks.ts
@@ -994,8 +1167,13 @@ var TOOLS = {
994
1167
  "@kaynooo/cli-gitlab": ["gl"],
995
1168
  "@kaynooo/cli-guard": ["guard"],
996
1169
  "@kaynooo/cli-out": ["out"],
1170
+ "@kaynooo/cli-dev": ["dev"],
1171
+ "@kaynooo/cli-pick": ["pick"],
997
1172
  "@kaynooo/cli-pkg": ["pkg"],
1173
+ "@kaynooo/cli-probe": ["probe"],
1174
+ "@kaynooo/cli-repos": ["repos"],
998
1175
  "@kaynooo/cli-stack": ["stack"],
1176
+ "@kaynooo/cli-sweep": ["sweep"],
999
1177
  "@kaynooo/cli-view": ["view"],
1000
1178
  "@kaynooo/cli-wait": ["waitfor", "slot"]
1001
1179
  };
@@ -1023,12 +1201,12 @@ function firstLine(text) {
1023
1201
  return end === -1 ? trimmed : trimmed.slice(0, end);
1024
1202
  }
1025
1203
  function installed(host, name) {
1026
- return readJson(join2(host.globalModules, name, "package.json"));
1204
+ return readJson(join3(host.globalModules, name, "package.json"));
1027
1205
  }
1028
1206
  function ownBin(host, name, bin) {
1029
1207
  const declared = installed(host, name)?.bin;
1030
1208
  const file = typeof declared === "string" ? declared : declared?.[bin];
1031
- return file === undefined ? undefined : join2(host.globalModules, name, file);
1209
+ return file === undefined ? undefined : join3(host.globalModules, name, file);
1032
1210
  }
1033
1211
  async function checkTool(host, name, bins) {
1034
1212
  const check = name.slice(name.indexOf("/") + 1);
@@ -1037,7 +1215,7 @@ async function checkTool(host, name, bins) {
1037
1215
  return { check, ok: false, detail: "not installed", fix: `bun install -g ${name}` };
1038
1216
  const off = bins.filter((bin) => host.which(bin) === null);
1039
1217
  if (off.length > 0)
1040
- return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join2(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1218
+ return { check, ok: false, detail: `${version}, ${off.join(", ")} not on the PATH`, fix: `add ${join3(host.globalModules, "..", "..", "..", "bin")} to the PATH` };
1041
1219
  const info = await host.run(["bun", "info", name, "version", "--no-cache"]);
1042
1220
  const answered = info.code === 0 ? firstLine(info.stdout.trim().split(`
1043
1221
  `).at(-1) ?? "") : "";
@@ -1064,9 +1242,9 @@ async function checkTea(host) {
1064
1242
  return { check: "tea", ok: true, detail: `logged in to ${forge} as ${login.name}, Forgejo ${served}` };
1065
1243
  }
1066
1244
  function checkHooks(host) {
1067
- const settings = readJson(join2(host.home, ".claude", "settings.json")) ?? {};
1245
+ const settings = readJson(join3(host.home, ".claude", "settings.json")) ?? {};
1068
1246
  const plugin = settings.enabledPlugins?.[PLUGIN] === true;
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")));
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")));
1070
1248
  if (plugin && mod !== undefined)
1071
1249
  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}` };
1072
1250
  if (plugin)
@@ -1100,19 +1278,19 @@ async function runChecks(host) {
1100
1278
  }
1101
1279
 
1102
1280
  // src/host.ts
1103
- import { homedir } from "os";
1104
- import { join as join3 } from "path";
1105
- import process7 from "process";
1281
+ import { homedir as homedir2 } from "os";
1282
+ import { join as join4 } from "path";
1283
+ import process10 from "process";
1106
1284
  var RUN_TIMEOUT_MS = 20000;
1107
1285
  function hostHere() {
1108
- const home = homedir();
1286
+ const home = homedir2();
1109
1287
  return {
1110
1288
  home,
1111
- globalModules: join3(process7.env.BUN_INSTALL ?? join3(home, ".bun"), "install", "global", "node_modules"),
1289
+ globalModules: join4(process10.env.BUN_INSTALL ?? join4(home, ".bun"), "install", "global", "node_modules"),
1112
1290
  bunVersion: Bun.version,
1113
1291
  which: (bin) => Bun.which(bin),
1114
1292
  run: async (argv, env) => {
1115
- const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process7.env, ...env } } });
1293
+ const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process10.env, ...env } } });
1116
1294
  if (ran.timedOut)
1117
1295
  return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
1118
1296
  const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
@@ -1131,7 +1309,7 @@ var OPTIONS = {
1131
1309
  json: jsonOption("[{check, ok, detail, fix?}]")
1132
1310
  };
1133
1311
  function print(results) {
1134
- const { green, red, dim } = paint(process8.stdout);
1312
+ const { green, red, dim } = paint(process11.stdout);
1135
1313
  const width = Math.max(...results.map((result) => result.check.length));
1136
1314
  for (const { check, ok, detail, fix } of results)
1137
1315
  console.log(`${ok ? green("ok ") : red("FAIL")} ${check.padEnd(width)} ${detail}${fix === undefined ? "" : ` ${dim("fix:")} ${fix}`}`);
@@ -1150,7 +1328,7 @@ async function main(args, host = hostHere()) {
1150
1328
  return results.every((result) => result.ok) ? 0 : 1;
1151
1329
  }
1152
1330
  if (import.meta.main)
1153
- process8.exitCode = await runMain({ name: "doctor", version, run: async (args) => main(args) });
1331
+ process11.exitCode = await runMain({ name: "doctor", version, run: async (args) => main(args) });
1154
1332
  export {
1155
1333
  main
1156
1334
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaynooo/cli-doctor",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.4.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.19.0"
39
+ "@kaynooo/cli-kit": "0.20.0"
40
40
  }
41
41
  }