@kaynooo/cli-doctor 0.2.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/dist/cli-bin.js CHANGED
@@ -2,16 +2,19 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli-bin.ts
5
- import process7 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
 
14
- // ../../node_modules/.bun/@kaynooo+utils@2.31.0+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
17
+ // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
15
18
  function x(s) {
16
19
  let r = s.startsWith("#") ? s.slice(1).toLowerCase() : s.toLowerCase();
17
20
  if (!/^[a-f0-9]+$/.test(r))
@@ -231,7 +234,7 @@ class e {
231
234
  }
232
235
  }
233
236
 
234
- // ../../node_modules/.bun/@kaynooo+utils@2.31.0+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
237
+ // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
235
238
  var r = 39;
236
239
  var o = 49;
237
240
  var f2 = "\x1B[";
@@ -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
  }
@@ -349,61 +456,20 @@ class HelpShown extends Exit {
349
456
 
350
457
  class Completing {
351
458
  options;
352
- constructor(options) {
459
+ positionals;
460
+ candidates;
461
+ constructor(options, positionals = [], candidates = {}) {
353
462
  this.options = options;
463
+ this.positionals = positionals;
464
+ this.candidates = candidates;
354
465
  }
355
466
  }
356
467
  var completion = { active: false };
357
- var OPTIONAL = /\[[^\]]+\]/g;
358
- var USAGE_PREFIX = /^usage:\s*/i;
359
- var FULL_STOP = /\.$/;
360
- var DASHES = /^-+/;
361
- function usageLine(usage, colors = paint(process2.stdout)) {
362
- return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
363
- }
364
- function helpText(usage, options, colors = paint(process2.stdout), { examples = [], replaces } = {}) {
365
- return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
366
- `);
367
- }
368
- function replacesRows(replaces, colors = paint(process2.stdout)) {
369
- return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
370
- }
371
- function wantsHelpJson(env = process2.env) {
372
- return env.KIT_HELP === "json";
373
- }
374
- function description(usage, { examples = [], replaces } = {}) {
375
- const lines = usage.replace(USAGE_PREFIX, "").split(`
376
- `);
377
- const first = lines[0];
378
- const colon = first.indexOf(": ");
379
- const prose = lines[lines.indexOf("") + 1] ?? "";
380
- const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
381
- return {
382
- synopsis: colon === -1 ? first : first.slice(0, colon),
383
- summary: colon === -1 ? fallback : first.slice(colon + 2),
384
- ...replaces === undefined ? {} : { replaces },
385
- examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
386
- };
387
- }
388
- function exampleRows(examples, colors = paint(process2.stdout)) {
389
- if (examples.length === 0)
390
- return [];
391
- return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
392
- }
393
- function optionRows(options, colors = paint(process2.stdout)) {
394
- const rows = Object.entries(options).map(([name, option]) => {
395
- const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
396
- const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
397
- return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
398
- });
399
- const width = Math.max(0, ...rows.map((row) => row.width));
400
- return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
401
- }
402
468
  var OUTPUT_OPTIONS = {
403
469
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
404
470
  plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
405
471
  };
406
- function applyOutput(color, plain, env = process2.env) {
472
+ function applyOutput(color, plain, env = process4.env) {
407
473
  const when = plain === true ? "never" : color ?? "auto";
408
474
  if (when === "auto")
409
475
  return;
@@ -420,14 +486,13 @@ function applyOutput(color, plain, env = process2.env) {
420
486
  function parse(args, usage, options, settings = {}) {
421
487
  const output = Object.fromEntries(Object.entries(OUTPUT_OPTIONS).filter(([name]) => !(name in options)));
422
488
  const all = { ...options, ...output, help: { type: "boolean", short: "h", description: "show this help" } };
489
+ const listing = listed(all, settings.allowNegative === true);
423
490
  if (completion.active)
424
- throw new Completing(all);
425
- const separator = settings.rest === true ? args.indexOf("--") : -1;
426
- const own = separator === -1 ? args : args.slice(0, separator);
427
- const rest = separator === -1 ? undefined : args.slice(separator + 1);
491
+ throw new Completing(listing, settings.positionals, settings.candidates);
492
+ const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
428
493
  let parsed;
429
494
  try {
430
- parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
495
+ parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
431
496
  } catch (error) {
432
497
  const message = parseError(error);
433
498
  throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
@@ -435,12 +500,84 @@ function parse(args, usage, options, settings = {}) {
435
500
  const values = parsed.values;
436
501
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
437
502
  if (values.help === true) {
438
- console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, all, undefined, settings));
503
+ console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
439
504
  throw new HelpShown;
440
505
  }
506
+ checkChoices(parsed.values, all);
507
+ checkConflicts(parsed.values, all);
441
508
  if (settings.positionals !== undefined)
442
509
  checkPositionals(parsed.positionals, settings.positionals);
443
- return { ...parsed, positionals: parsed.positionals, rest };
510
+ return { ...parsed, values: parsed.values, positionals: parsed.positionals, rest };
511
+ }
512
+ function splitRest(args, passes) {
513
+ const separator = passes ? args.indexOf("--") : -1;
514
+ return { own: separator === -1 ? args : args.slice(0, separator), rest: separator === -1 ? undefined : args.slice(separator + 1) };
515
+ }
516
+ function splitTrailing(args, options, count) {
517
+ const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
518
+ let seen = 0;
519
+ if (count === 0)
520
+ return { own: args, rest: undefined };
521
+ for (let at = 0;at < args.length; at++) {
522
+ const word = args[at];
523
+ if (word === "--")
524
+ break;
525
+ if (word.startsWith("-")) {
526
+ if (takesValue(word))
527
+ at++;
528
+ continue;
529
+ }
530
+ seen++;
531
+ if (seen === count)
532
+ return { own: args.slice(0, at + 1), rest: args.slice(at + 1) };
533
+ }
534
+ return { own: args, rest: undefined };
535
+ }
536
+ function listed(options, allowNegative) {
537
+ if (!allowNegative)
538
+ return options;
539
+ return Object.fromEntries(Object.entries(options).flatMap(([name, option]) => {
540
+ if (option.type !== "boolean" || option.negative === undefined)
541
+ return [[name, option]];
542
+ return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
543
+ }));
544
+ }
545
+ function withBare(args, options) {
546
+ const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
547
+ if (bare.size === 0)
548
+ return args;
549
+ const separator = args.indexOf("--");
550
+ const own = separator === -1 ? args : args.slice(0, separator);
551
+ return [...own.flatMap((arg, index) => {
552
+ const optional = bare.get(arg);
553
+ const next = own[index + 1];
554
+ const value = next !== undefined && (optional?.accepts === undefined ? !next.startsWith("-") : optional.accepts.test(next));
555
+ return optional === undefined || value ? [arg] : [arg, optional.bare];
556
+ }), ...separator === -1 ? [] : args.slice(separator)];
557
+ }
558
+ function checkConflicts(values, options) {
559
+ const given = (name) => values[name] !== undefined && values[name] !== false;
560
+ for (const [name, { conflicts = [] }] of Object.entries(options)) {
561
+ const other = given(name) ? conflicts.find(given) : undefined;
562
+ if (other !== undefined)
563
+ badUsage(`--${name} cannot be used with --${other}`);
564
+ }
565
+ }
566
+ function checkChoices(values, options) {
567
+ for (const [name, option] of Object.entries(options)) {
568
+ if (option.choices === undefined)
569
+ continue;
570
+ const given = values[name];
571
+ for (const value of Array.isArray(given) ? given : [given]) {
572
+ if (typeof value === "string")
573
+ oneOf(value, name, option.choices);
574
+ }
575
+ }
576
+ }
577
+ function oneOf(value, name, choices) {
578
+ if (!choices.includes(value))
579
+ badUsage(`${name.startsWith("<") ? name : `--${name}`} takes one of ${choices.join(", ")}, not ${value}`);
580
+ return value;
444
581
  }
445
582
  function checkPositionals(given, names) {
446
583
  const variadic = names.at(-1)?.replace("]", "").endsWith("...") === true;
@@ -491,7 +628,7 @@ function nearestOption(message, options, allowNegative) {
491
628
  }
492
629
  // ../kit/src/process.ts
493
630
  import { constants } from "os";
494
- import process3 from "process";
631
+ import process5 from "process";
495
632
  var KILL_GRACE_MS = 2000;
496
633
  function exitCodeOf(code, signal) {
497
634
  if (code !== null)
@@ -511,7 +648,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal,
511
648
  return;
512
649
  try {
513
650
  if (grouped)
514
- process3.kill(-proc.pid, killing);
651
+ process5.kill(-proc.pid, killing);
515
652
  else
516
653
  proc.kill(killing);
517
654
  } catch {}
@@ -585,14 +722,103 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
585
722
  var RESET_STYLE = `${ESC}[0m`;
586
723
  var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
587
724
  // ../kit/src/cli.ts
588
- 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
589
814
  var LEVEL_OPTIONS = {
590
815
  ...OUTPUT_OPTIONS,
591
816
  help: { type: "boolean", short: "h", description: "show this help" }
592
817
  };
593
818
  var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
594
- async function commandRows(commands, colors) {
595
- const width = Math.max(...Object.keys(commands).map((name) => name.length));
819
+ async function commandRows(all, colors) {
820
+ const commands = shown(all);
821
+ const width = Math.max(0, ...Object.keys(commands).map((name) => name.length));
596
822
  const groups = new Map([["Commands:", []]]);
597
823
  const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
598
824
  for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
@@ -603,6 +829,9 @@ async function commandRows(commands, colors) {
603
829
  }
604
830
  return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
605
831
  }
832
+ function shown(commands) {
833
+ return Object.fromEntries(Object.entries(commands).filter(([, { hidden }]) => hidden !== true));
834
+ }
606
835
  function takeOutputFlags(argv) {
607
836
  let index = 0;
608
837
  for (;index < argv.length; index++) {
@@ -634,7 +863,7 @@ async function expand(scope, words) {
634
863
  }
635
864
  return { ...scope, commands, lazy: [] };
636
865
  }
637
- 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) {
638
867
  const colors = paint(stream);
639
868
  const prefix = path.join(" ");
640
869
  const summary = await resolveSummary(lazySummary);
@@ -661,8 +890,8 @@ async function help({ path, header, summary: lazySummary, commands, defaultComma
661
890
  `);
662
891
  }
663
892
  function printError(prefix, message, hint, cause) {
664
- const colors = paint(process4.stderr);
665
- const styled = takesControlCodes(process4.stderr);
893
+ const colors = paint(process7.stderr);
894
+ const styled = takesControlCodes(process7.stderr);
666
895
  const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
667
896
  const under = (label, text, tint) => text === undefined ? "" : `
668
897
  ${styled ? tint(label) : `${label}:`} ${text}`;
@@ -681,6 +910,8 @@ function reportThrown(prefix, error, redact = (text) => text, json = false) {
681
910
  const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
682
911
  const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
683
912
  const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
913
+ if (error instanceof UsageError)
914
+ noteUsageError(error.message);
684
915
  const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
685
916
  report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
686
917
  return error instanceof Exit ? error.code : 1;
@@ -694,12 +925,20 @@ async function dispatch(level, typed, { version, redact }) {
694
925
  return reportThrown(prefix, error);
695
926
  }
696
927
  const [name, ...args] = argv;
928
+ const fallback = level.defaultCommand === undefined ? undefined : level.commands[level.defaultCommand];
929
+ if (name === undefined && level.runBare && fallback !== undefined && "run" in fallback) {
930
+ try {
931
+ return await fallback.run([]);
932
+ } catch (error) {
933
+ return reportThrown(`${prefix} ${level.defaultCommand}`, error, redact, false);
934
+ }
935
+ }
697
936
  if (name === undefined && level.path.length > 1) {
698
- 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));
699
938
  return 2;
700
939
  }
701
940
  if (name === undefined || name === "help" || name === "--help" || name === "-h") {
702
- 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));
703
942
  return 0;
704
943
  }
705
944
  if (level.path.length === 1 && (name === "--version" || name === "-v")) {
@@ -708,6 +947,7 @@ async function dispatch(level, typed, { version, redact }) {
708
947
  }
709
948
  let command = level.commands[level.aliases?.[name] ?? name];
710
949
  let rest = args;
950
+ let ran = name;
711
951
  if (command === undefined && level.lazy.length > 0) {
712
952
  level = await expand(level, [...level.path.slice(1), ...argv]);
713
953
  command = level.commands[level.aliases?.[name] ?? name];
@@ -715,40 +955,77 @@ async function dispatch(level, typed, { version, redact }) {
715
955
  if (command === undefined && level.defaultCommand !== undefined) {
716
956
  command = level.commands[level.defaultCommand];
717
957
  rest = argv;
958
+ ran = level.defaultCommand;
718
959
  }
719
960
  if (command === undefined) {
720
- const meant = closest(name, Object.keys(level.commands));
961
+ const meant = closest(name, Object.keys(shown(level.commands)));
721
962
  printError(prefix, `no command ${name}`, meant === undefined ? `run ${prefix} --help for its commands` : `did you mean ${meant}?`);
722
963
  return 2;
723
964
  }
724
965
  if ("commands" in command) {
725
966
  const { summary } = command;
726
- return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
967
+ return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, runBare: false, examples: command.examples, replaces: undefined }, rest, { version, redact });
727
968
  }
728
969
  try {
729
970
  return await command.run(rest);
730
971
  } catch (error) {
731
- return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
972
+ return reportThrown(`${prefix} ${ran}`, error, redact, wantsJson(rest));
732
973
  }
733
974
  }
734
975
  var COMPLETE = "__complete";
735
976
  var FILES = "__files__";
736
- async function optionsOf(run) {
977
+ async function completingOf(run) {
737
978
  completion.active = true;
738
979
  try {
739
980
  await run([]);
740
981
  } catch (error) {
741
982
  if (error instanceof Completing)
742
- return error.options;
983
+ return error;
743
984
  } finally {
744
985
  completion.active = false;
745
986
  }
746
- return {};
987
+ return new Completing({});
988
+ }
989
+ async function optionsOf(run) {
990
+ return (await completingOf(run)).options;
991
+ }
992
+ function candidateLine(candidate) {
993
+ return typeof candidate === "string" ? candidate : candidate.join("\t");
994
+ }
995
+ function positionalBeingTyped(before, { options, positionals }) {
996
+ const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
997
+ let index = 0;
998
+ let at = 0;
999
+ while (at < before.length) {
1000
+ const word = before[at];
1001
+ if (word === "--") {
1002
+ index += before.length - at - 1;
1003
+ break;
1004
+ }
1005
+ if (!word.startsWith("-"))
1006
+ index++;
1007
+ else if (takesValue(word))
1008
+ at++;
1009
+ at++;
1010
+ }
1011
+ const last = positionals.at(-1);
1012
+ if (index < positionals.length)
1013
+ return positionals[index];
1014
+ return last?.replace("]", "").endsWith("...") === true ? last : undefined;
747
1015
  }
748
1016
  async function leafCandidates(run, words) {
749
- if (!(words.at(-1) ?? "").startsWith("-"))
750
- return [FILES];
751
- return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
1017
+ const completing = await completingOf(run);
1018
+ if ((words.at(-1) ?? "").startsWith("-"))
1019
+ return Object.entries(completing.options).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
1020
+ const flag = words.at(-2);
1021
+ const option = flag?.startsWith("-") === true ? Object.entries(completing.options).find(([name, { short }]) => flag === `--${name}` || flag === `-${short}`)?.[1] : undefined;
1022
+ if (option?.type === "string") {
1023
+ const values = option.choices === undefined ? await option.candidates?.() : option.choices;
1024
+ return values === undefined ? [FILES] : values.map(candidateLine);
1025
+ }
1026
+ const name = positionalBeingTyped(words.slice(0, -1), completing);
1027
+ const values = name === undefined ? undefined : await completing.candidates[name]?.();
1028
+ return values === undefined ? [FILES] : values.map(candidateLine);
752
1029
  }
753
1030
  async function candidates(scope, defaultCommand, words, before = []) {
754
1031
  const [word = "", ...rest] = words;
@@ -759,8 +1036,13 @@ async function candidates(scope, defaultCommand, words, before = []) {
759
1036
  ({ commands, aliases } = await expand(scope, [...before, ...words]));
760
1037
  command = commands[aliases?.[word] ?? word];
761
1038
  }
762
- if (listing)
763
- return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
1039
+ if (listing) {
1040
+ const own = await Promise.all(Object.entries(shown(commands)).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
1041
+ const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
1042
+ const values = fallback !== undefined && "run" in fallback ? await leafCandidates(fallback.run, [word]) : [];
1043
+ const valueOf = (line) => line.slice(0, line.includes("\t") ? line.indexOf("\t") : undefined);
1044
+ return [...own, ...values.filter((line) => line !== FILES && !(valueOf(line) in commands))];
1045
+ }
764
1046
  if (command === undefined) {
765
1047
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
766
1048
  return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
@@ -772,26 +1054,49 @@ async function printCandidates(found) {
772
1054
  console.log(line);
773
1055
  return 0;
774
1056
  }
775
- async function runCli(cli, argv = process4.argv.slice(2)) {
1057
+ async function runCli(cli, argv = process7.argv.slice(2)) {
776
1058
  if (argv[0] === COMPLETE)
777
1059
  return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
778
- return dispatch({ path: [cli.name], header: (colors, about) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${about}`, summary: cli.about, ...scopeOf(cli.commands, cli.extraCommands, cli.aliases), defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
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(" ");
779
1082
  }
780
- 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)) {
781
1084
  if (argv[0] === COMPLETE)
782
1085
  return printCandidates(leafCandidates(run, argv.slice(1)));
783
1086
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
784
1087
  console.log(version);
785
1088
  return 0;
786
1089
  }
1090
+ let code;
787
1091
  try {
788
- return await run(argv);
1092
+ code = await run(argv);
789
1093
  } catch (error) {
790
- return reportThrown(name, error, redact, wantsJson(argv));
1094
+ code = reportThrown(name, error, redact, wantsJson(argv));
791
1095
  }
1096
+ if (usage !== false)
1097
+ recordUsage(usageEntry(name, undefined, argv, code));
1098
+ return code;
792
1099
  }
793
- // ../kit/src/logs.ts
794
- var DAY_MS = 24 * 60 * 60 * 1000;
795
1100
  // ../kit/src/secrets.ts
796
1101
  var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
797
1102
  // ../kit/src/sessions.ts
@@ -799,29 +1104,12 @@ var {Glob } = globalThis.Bun;
799
1104
  var SESSION_FILES = new Glob("*.json");
800
1105
  var ACCOUNT_DIRS = new Glob("*/sessions");
801
1106
  // package.json
802
- var version = "0.2.0";
1107
+ var version = "0.4.0";
803
1108
 
804
- // src/host.ts
805
- import { homedir } from "os";
806
- import { join } from "path";
807
- import process6 from "process";
808
- var RUN_TIMEOUT_MS = 20000;
809
- function hostHere() {
810
- const home = homedir();
811
- return {
812
- home,
813
- globalModules: join(process6.env.BUN_INSTALL ?? join(home, ".bun"), "install", "global", "node_modules"),
814
- bunVersion: Bun.version,
815
- which: (bin) => Bun.which(bin),
816
- run: async (argv, env) => {
817
- const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process6.env, ...env } } });
818
- if (ran.timedOut)
819
- return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
820
- const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
821
- return { code: ran.code, stdout: of("stdout"), stderr: of("stderr") };
822
- }
823
- };
824
- }
1109
+ // 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";
825
1113
 
826
1114
  // src/checks.ts
827
1115
  import { existsSync, readFileSync } from "fs";
@@ -840,8 +1128,14 @@ var TOOLS = {
840
1128
  "@kaynooo/cli-gitlab": ["gl"],
841
1129
  "@kaynooo/cli-guard": ["guard"],
842
1130
  "@kaynooo/cli-out": ["out"],
1131
+ "@kaynooo/cli-dev": ["dev"],
1132
+ "@kaynooo/cli-pick": ["pick"],
843
1133
  "@kaynooo/cli-pkg": ["pkg"],
1134
+ "@kaynooo/cli-probe": ["probe"],
1135
+ "@kaynooo/cli-repos": ["repos"],
844
1136
  "@kaynooo/cli-stack": ["stack"],
1137
+ "@kaynooo/cli-sweep": ["sweep"],
1138
+ "@kaynooo/cli-view": ["view"],
845
1139
  "@kaynooo/cli-wait": ["waitfor", "slot"]
846
1140
  };
847
1141
  var FORGE = "git.kaynooo.fr";
@@ -927,12 +1221,82 @@ function checkBun(host) {
927
1221
  return { check: "bun", ok: false, detail: `${host.bunVersion}, the tools ask for ${[...new Set(unmet)].join(" and ")}`, fix: "bun upgrade" };
928
1222
  return { check: "bun", ok: true, detail: floors.length === 0 ? host.bunVersion : `${host.bunVersion}, the tools ask for ${[...new Set(floors)].join(" and ")}` };
929
1223
  }
1224
+ function checkCompletions(host) {
1225
+ if (host.which("fish") === null)
1226
+ return { check: "completions", ok: true, detail: "fish is not installed" };
1227
+ const dir = completionDir(host.home);
1228
+ const missing = missingCompletions(host, dir);
1229
+ if (missing.length > 0)
1230
+ return { check: "completions", ok: false, detail: `no fish completion in ${dir} for ${missing.join(", ")}`, fix: "cli completions fish --install" };
1231
+ return { check: "completions", ok: true, detail: dir };
1232
+ }
930
1233
  async function runChecks(host) {
931
1234
  const entries = Object.entries(TOOLS);
932
1235
  const tools = [];
933
1236
  for (let at = 0;at < entries.length; at += INFO_AT_ONCE)
934
1237
  tools.push(...await Promise.all(entries.slice(at, at + INFO_AT_ONCE).map(async ([name, bins]) => checkTool(host, name, bins))));
935
- return [...tools, await checkTea(host), checkHooks(host), checkBun(host)];
1238
+ return [...tools, await checkTea(host), checkHooks(host), checkBun(host), checkCompletions(host)];
1239
+ }
1240
+
1241
+ // src/completions.ts
1242
+ var MARKER = "# Generated by `cli completions fish`";
1243
+ var FISH_FILE = /\.fish$/;
1244
+ 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'
1245
+ set -l tokens (commandline -xpc 2>/dev/null; or commandline -opc)
1246
+ set -l current (commandline -ct)
1247
+ for line in ($bin __complete $tokens[2..-1] $current 2>/dev/null)
1248
+ if test "$line" = __files__
1249
+ __fish_complete_path $current
1250
+ else
1251
+ echo $line
1252
+ end
1253
+ end
1254
+ end`;
1255
+ function completionDir(home, env = process9.env) {
1256
+ return join3(env.XDG_CACHE_HOME ?? join3(home, ".cache"), "fish", "kaynooo-cli");
1257
+ }
1258
+ function installedBins(host) {
1259
+ return Object.values(TOOLS).flat().filter((bin) => host.which(bin) !== null);
1260
+ }
1261
+ function completionFile(bin) {
1262
+ return `${MARKER}
1263
+ ${FUNCTION}
1264
+ complete -c ${bin} -f -a '(__kaynooo_cli_complete ${bin})'
1265
+ `;
1266
+ }
1267
+ async function installCompletions(host, dir) {
1268
+ const bins = installedBins(host);
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));
1271
+ 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))));
1274
+ return { written: bins, removed: removed.map((name) => name.replace(FISH_FILE, "")) };
1275
+ }
1276
+ function missingCompletions(host, dir) {
1277
+ return installedBins(host).filter((bin) => !existsSync2(join3(dir, `${bin}.fish`)));
1278
+ }
1279
+
1280
+ // src/host.ts
1281
+ import { homedir as homedir2 } from "os";
1282
+ import { join as join4 } from "path";
1283
+ import process10 from "process";
1284
+ var RUN_TIMEOUT_MS = 20000;
1285
+ function hostHere() {
1286
+ const home = homedir2();
1287
+ return {
1288
+ home,
1289
+ globalModules: join4(process10.env.BUN_INSTALL ?? join4(home, ".bun"), "install", "global", "node_modules"),
1290
+ bunVersion: Bun.version,
1291
+ which: (bin) => Bun.which(bin),
1292
+ run: async (argv, env) => {
1293
+ const ran = await runCommand(argv, { timeoutMs: RUN_TIMEOUT_MS, ...env === undefined ? {} : { env: { ...process10.env, ...env } } });
1294
+ if (ran.timedOut)
1295
+ return { code: 1, stdout: "", stderr: `timed out after ${RUN_TIMEOUT_MS / 1000}s` };
1296
+ const of = (stream) => Buffer.concat(ran.chunks.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data)).toString();
1297
+ return { code: ran.code, stdout: of("stdout"), stderr: of("stderr") };
1298
+ }
1299
+ };
936
1300
  }
937
1301
 
938
1302
  // src/list.ts
@@ -960,7 +1324,7 @@ var LIST_OPTIONS = {
960
1324
  json: jsonOption("[{bin, package, installed, synopsis?, summary?, replaces?, examples?}]")
961
1325
  };
962
1326
  function print(listed) {
963
- const { cyan, dim } = paint(process7.stdout);
1327
+ const { cyan, dim } = paint(process11.stdout);
964
1328
  const width = Math.max(...listed.map((each) => each.bin.length));
965
1329
  for (const each of listed) {
966
1330
  const what = !each.installed ? dim(`not installed: bun install -g ${each.package}`) : `${each.summary ?? ""}${each.replaces === undefined ? "" : dim(`; instead of ${each.replaces}`)}`;
@@ -976,18 +1340,73 @@ async function list(args, host = hostHere()) {
976
1340
  print(listed);
977
1341
  return 0;
978
1342
  }
1343
+ var COMPLETIONS_USAGE = `cli completions fish [--install] [--dir <folder>]: fish completions for every installed bin of the @kaynooo/cli-* packages
1344
+
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.`;
1347
+ var COMPLETIONS_OPTIONS = {
1348
+ install: { type: "boolean", description: "write the files to the folder instead of printing them" },
1349
+ dir: { type: "string", short: "d", description: "the folder to write to, instead of ~/.cache/fish/kaynooo-cli" }
1350
+ };
1351
+ 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");
1355
+ if (values.install !== true) {
1356
+ console.log(installedBins(host).map(completionFile).join(`
1357
+ `));
1358
+ return 0;
1359
+ }
1360
+ const dir = values.dir ?? completionDir(host.home);
1361
+ const { written, removed } = await installCompletions(host, dir);
1362
+ console.log(`wrote ${written.length} completions to ${dir}${removed.length === 0 ? "" : `, took out ${removed.join(", ")}`}`);
1363
+ return 0;
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
+ }
979
1394
  if (import.meta.main) {
980
- process7.exitCode = await runCli({
1395
+ process11.exitCode = await runCli({
981
1396
  name: "cli",
982
1397
  version,
983
1398
  about: "the bins of the @kaynooo/cli-* packages, described by their own help",
984
1399
  replaces: "running each bin's --help to find the one that fits",
985
1400
  examples: ["cli list", ["cli list --json", "with each bin's synopsis and examples"]],
986
1401
  commands: {
987
- list: { summary: "every bin in one line: what it does and what it is reached for instead of", run: async (args) => list(args) }
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) },
1404
+ completions: { summary: "fish completions for every installed bin, printed or written to the folder fish reads", run: async (args) => completions(args) }
988
1405
  }
989
1406
  });
990
1407
  }
991
1408
  export {
992
- list
1409
+ completions,
1410
+ list,
1411
+ stats
993
1412
  };