@kaynooo/guard-tools 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +89 -45
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -298,6 +298,26 @@ function upTo(text, separator) {
298
298
  }
299
299
  var ESCAPE = String.fromCharCode(27);
300
300
 
301
+ // ../kit/src/text.ts
302
+ var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
303
+ function distance(a, b) {
304
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
305
+ for (const [i, left] of [...a].entries()) {
306
+ const current = [i + 1];
307
+ for (const [j, right] of [...b].entries())
308
+ current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
309
+ previous = current;
310
+ }
311
+ return previous[b.length] ?? 0;
312
+ }
313
+ function closest(name, names) {
314
+ const started = names.find((known) => known.startsWith(name));
315
+ if (started !== undefined)
316
+ return started;
317
+ const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
318
+ return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
319
+ }
320
+
301
321
  // ../kit/src/args.ts
302
322
  function jsonOption(holds) {
303
323
  return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
@@ -317,8 +337,8 @@ function fail(message, code = 2, hint) {
317
337
  }
318
338
 
319
339
  class UsageError extends Exit {
320
- constructor(message) {
321
- super(message, 2);
340
+ constructor(message, hint) {
341
+ super(message, 2, hint);
322
342
  }
323
343
  }
324
344
  function badUsage(message) {
@@ -341,6 +361,7 @@ var completion = { active: false };
341
361
  var OPTIONAL = /\[[^\]]+\]/g;
342
362
  var USAGE_PREFIX = /^usage:\s*/i;
343
363
  var FULL_STOP = /\.$/;
364
+ var DASHES = /^-+/;
344
365
  function usageLine(usage, colors = paint(process3.stdout)) {
345
366
  return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
346
367
  }
@@ -377,10 +398,10 @@ function optionRows(options, colors = paint(process3.stdout)) {
377
398
  const rows = Object.entries(options).map(([name, option]) => {
378
399
  const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
379
400
  const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
380
- return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description };
401
+ return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
381
402
  });
382
403
  const width = Math.max(0, ...rows.map((row) => row.width));
383
- return rows.map((row) => ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
404
+ return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
384
405
  }
385
406
  var OUTPUT_OPTIONS = {
386
407
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
@@ -412,7 +433,8 @@ function parse(args, usage, options, settings = {}) {
412
433
  try {
413
434
  parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
414
435
  } catch (error) {
415
- badUsage(parseError(error));
436
+ const message = parseError(error);
437
+ throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
416
438
  }
417
439
  const values = parsed.values;
418
440
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
@@ -455,6 +477,22 @@ function parseError(error) {
455
477
  const message = upTo(error instanceof Error ? error.message : String(error), ". ");
456
478
  return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
457
479
  }
480
+ var UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
481
+ function nearestOption(message, options, allowNegative) {
482
+ const typed = UNKNOWN_OPTION.exec(message)?.[1];
483
+ if (typed === undefined)
484
+ return;
485
+ const word = typed.replace(DASHES, "");
486
+ if (word.length < 2 && !typed.startsWith("--"))
487
+ return;
488
+ const known = Object.entries(options).flatMap(([name, option]) => [
489
+ ...option.short === undefined ? [] : [`-${option.short}`],
490
+ `--${name}`,
491
+ ...allowNegative && option.type === "boolean" ? [`--no-${name}`] : []
492
+ ]);
493
+ const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
494
+ return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
495
+ }
458
496
  async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
459
497
  if (path === "-") {
460
498
  if (process3.stdin.isTTY)
@@ -468,33 +506,19 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
468
506
  }
469
507
  // ../kit/src/cli.ts
470
508
  import process4 from "process";
471
-
472
- // ../kit/src/text.ts
473
- var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
474
- function distance(a, b) {
475
- let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
476
- for (const [i, left] of [...a].entries()) {
477
- const current = [i + 1];
478
- for (const [j, right] of [...b].entries())
479
- current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
480
- previous = current;
481
- }
482
- return previous[b.length] ?? 0;
483
- }
484
-
485
- // ../kit/src/cli.ts
486
509
  var LEVEL_OPTIONS = {
487
510
  ...OUTPUT_OPTIONS,
488
511
  help: { type: "boolean", short: "h", description: "show this help" }
489
512
  };
490
513
  var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
491
- function commandRows(commands, colors) {
514
+ async function commandRows(commands, colors) {
492
515
  const width = Math.max(...Object.keys(commands).map((name) => name.length));
493
516
  const groups = new Map([["Commands:", []]]);
494
- for (const [name, { summary, group }] of Object.entries(commands)) {
517
+ const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
518
+ for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
495
519
  const heading = group === undefined ? "Commands:" : `${group}:`;
496
520
  const rows = groups.get(heading) ?? [];
497
- rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summary)}`);
521
+ rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
498
522
  groups.set(heading, rows);
499
523
  }
500
524
  return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
@@ -514,28 +538,38 @@ function takeOutputFlags(argv) {
514
538
  }
515
539
  return argv.slice(index);
516
540
  }
517
- function closest(name, names) {
518
- const started = names.find((known) => known.startsWith(name));
519
- if (started !== undefined)
520
- return started;
521
- const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
522
- return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
541
+ async function resolveSummary(summary) {
542
+ return typeof summary === "string" ? summary : summary();
543
+ }
544
+ function scopeOf(commands, extra, aliases) {
545
+ return { commands: typeof commands === "function" ? {} : commands, lazy: [...typeof commands === "function" ? [commands] : [], ...extra === undefined ? [] : [extra]], aliases };
523
546
  }
524
- async function help({ path, header, summary, commands, defaultCommand, examples, replaces }, stream) {
547
+ async function expand(scope, words) {
548
+ if (scope.lazy.length === 0)
549
+ return scope;
550
+ const commands = { ...scope.commands };
551
+ for (const added of await Promise.all(scope.lazy.map(async (lazy) => lazy(words)))) {
552
+ for (const [name, command] of Object.entries(added))
553
+ commands[name] ??= command;
554
+ }
555
+ return { ...scope, commands, lazy: [] };
556
+ }
557
+ async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
525
558
  const colors = paint(stream);
526
559
  const prefix = path.join(" ");
560
+ const summary = await resolveSummary(lazySummary);
527
561
  if (wantsHelpJson())
528
562
  return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
529
563
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
530
564
  const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
531
565
  const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
532
566
  return [
533
- header(colors),
567
+ header(colors, summary),
534
568
  "",
535
569
  usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
536
570
  ...replacesRows(replaces, colors),
537
571
  "",
538
- ...commandRows(commands, colors),
572
+ ...await commandRows(commands, colors),
539
573
  ...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
540
574
  "",
541
575
  colors.bold("Options:"),
@@ -581,11 +615,11 @@ async function dispatch(level, typed, { version, redact }) {
581
615
  }
582
616
  const [name, ...args] = argv;
583
617
  if (name === undefined && level.path.length > 1) {
584
- console.error(await help(level, process4.stderr));
618
+ console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stderr));
585
619
  return 2;
586
620
  }
587
621
  if (name === undefined || name === "help" || name === "--help" || name === "-h") {
588
- console.log(await help(level, process4.stdout));
622
+ console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stdout));
589
623
  return 0;
590
624
  }
591
625
  if (level.path.length === 1 && (name === "--version" || name === "-v")) {
@@ -594,6 +628,10 @@ async function dispatch(level, typed, { version, redact }) {
594
628
  }
595
629
  let command = level.commands[level.aliases?.[name] ?? name];
596
630
  let rest = args;
631
+ if (command === undefined && level.lazy.length > 0) {
632
+ level = await expand(level, [...level.path.slice(1), ...argv]);
633
+ command = level.commands[level.aliases?.[name] ?? name];
634
+ }
597
635
  if (command === undefined && level.defaultCommand !== undefined) {
598
636
  command = level.commands[level.defaultCommand];
599
637
  rest = argv;
@@ -605,7 +643,7 @@ async function dispatch(level, typed, { version, redact }) {
605
643
  }
606
644
  if ("commands" in command) {
607
645
  const { summary } = command;
608
- return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${summary}`, summary, commands: command.commands, aliases: command.aliases, defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
646
+ 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 });
609
647
  }
610
648
  try {
611
649
  return await command.run(rest);
@@ -630,18 +668,24 @@ async function optionsOf(run) {
630
668
  async function leafCandidates(run, words) {
631
669
  if (!(words.at(-1) ?? "").startsWith("-"))
632
670
  return [FILES];
633
- return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
671
+ return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
634
672
  }
635
- async function candidates(commands, aliases, defaultCommand, words) {
673
+ async function candidates(scope, defaultCommand, words, before = []) {
636
674
  const [word = "", ...rest] = words;
637
- const command = commands[aliases?.[word] ?? word];
638
- if (rest.length === 0 && !word.startsWith("-"))
639
- return Object.entries(commands).map(([name, { summary }]) => `${name} ${summary}`);
675
+ const listing = rest.length === 0 && !word.startsWith("-");
676
+ let { commands, aliases } = scope;
677
+ let command = commands[aliases?.[word] ?? word];
678
+ if (listing || command === undefined) {
679
+ ({ commands, aliases } = await expand(scope, [...before, ...words]));
680
+ command = commands[aliases?.[word] ?? word];
681
+ }
682
+ if (listing)
683
+ return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
640
684
  if (command === undefined) {
641
685
  const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
642
686
  return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
643
687
  }
644
- return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
688
+ return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
645
689
  }
646
690
  async function printCandidates(found) {
647
691
  for (const line of await found)
@@ -650,8 +694,8 @@ async function printCandidates(found) {
650
694
  }
651
695
  async function runCli(cli, argv = process4.argv.slice(2)) {
652
696
  if (argv[0] === COMPLETE)
653
- return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
654
- return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${cli.about}`, summary: cli.about, commands: cli.commands, aliases: cli.aliases, defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
697
+ return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
698
+ 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 });
655
699
  }
656
700
  // ../kit/src/process.ts
657
701
  import { constants } from "os";
@@ -675,7 +719,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
675
719
  return { code: exitCodeOf(proc.exitCode, proc.signalCode), chunks, output, text: output.toString() };
676
720
  }
677
721
  // package.json
678
- var version = "0.4.0";
722
+ var version = "0.4.1";
679
723
 
680
724
  // ../kit/src/shell.ts
681
725
  var SEPARATORS = new Set([`
@@ -1171,7 +1215,7 @@ var EXEC_VALUE_OPTIONS = new Set(["-e", "--env", "--env-file", "-u", "--user", "
1171
1215
  var CONFIG_LISTS = new Set(["--services", "--volumes", "--profiles", "--images", "--networks", "--hash", "-q", "--quiet"]);
1172
1216
  var FORMAT_OPTIONS = new Set(["-f", "--format"]);
1173
1217
  var WHOLE_FORMAT = /Env|\{\{\s*json\s+\.\s*\}\}|^json$/;
1174
- var USE_EV = '.env files hold secrets that must never reach the transcript, so they are not read, edited or printed directly. Use `ev` instead: `ev list`, `ev get KEY` (masked), `ev grep <regex>` for the keys matching it, `ev show <rev>:<file>` for one at a git revision, `printf %s "$value" | ev set KEY [file]` (`--empty` for an empty value), `ev unset KEY`, `ev mv KEY --from .env --to .env.local`, `ev diff a b`, `ev missing`; and pipe output that may carry a secret through `redact`, as `docker compose exec <svc> printenv | redact` or `docker compose config | redact`. If `ev` is missing: `bun install -g @kaynooo/env-tools`.';
1218
+ var USE_EV = '.env files hold secrets that must never reach the transcript, so they are not read, edited or printed directly. Use `ev` instead: `ev list`, `ev get KEY` (masked), `ev grep <regex>` for the keys matching it, `ev show <rev>:<file>` for one at a git revision, `printf %s "$value" | ev set KEY [file]` (`--empty` for an empty value), `ev unset KEY`, `ev cp KEY --from .env --to .env.local`, `ev mv KEY --from .env --to .env.local`, `ev diff a b`, `ev missing`; and pipe output that may carry a secret through `redact`, as `docker compose exec <svc> printenv | redact` or `docker compose config | redact`. If `ev` is missing: `bun install -g @kaynooo/env-tools`.';
1175
1219
  function isEnvFile(path) {
1176
1220
  const name = path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf(":")) + 1);
1177
1221
  return (ENV_FILE.test(name) || name === ".env*") && !TEMPLATE.test(name);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaynooo/guard-tools",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "description": "guard: the safety rules a Claude Code Bash call is held to (git, remote machines, SQL writes, env files, package managers, commit messages), each refusal naming the route.",
6
6
  "author": "Kaynooo <contact@kaynooo.fr>",
7
7
  "license": "MIT",