@kaynooo/guard-tools 0.3.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.
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/cli.js +167 -57
- package/package.json +6 -6
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Kaynooo <kaynooo
|
|
3
|
+
Copyright (c) 2026 Kaynooo <contact@kaynooo.fr>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ guard check --json --cwd "$PWD" - < command.txt
|
|
|
13
13
|
| Rule | Refuses | Lives in |
|
|
14
14
|
| --- | --- | --- |
|
|
15
15
|
| `remote` | `ssh`, `scp`, `sftp`, `mosh`, `rsync` to a host, `DOCKER_HOST=ssh://`, `docker -H ssh://` | `src/remote.ts` |
|
|
16
|
-
| `git` | `git stash` (pointing at `base`), `git add -A`/`.`/`-u`, `git commit -a` in any flag cluster, `git push` the user did not ask for, `git push --tags` without `--atomic` | `src/git.ts` |
|
|
16
|
+
| `git` | `git stash` (pointing at `proof`, `base`, a commit first or `git worktree add`), `git add -A`/`.`/`-u`, `git commit -a` in any flag cluster, `git push` the user did not ask for, `git push --tags` without `--atomic` | `src/git.ts` |
|
|
17
17
|
| `commit` | a commit message breaking the format: no Conventional Commit title, a trailer, a body over two lines, a bullet list, a hard wrap; `-m`, `-F <file>` and `-F -` with a heredoc are read; the refusal spells the rule out and points at `gc` | `src/commit.ts`, the validator in `@kaynooo/git-tools` |
|
|
18
18
|
| `sql` | a SQL client, console or dump tool fed a write (INSERT, UPDATE, DELETE, DDL, migrations, fixtures) outside a test database | `@kaynooo/db-tools` (`src/clients.ts`, `src/guard.ts`) |
|
|
19
19
|
| `env` | a command reading, writing or printing an env file or the whole environment; `guard path` refuses a file tool opening one | `@kaynooo/env-tools` (`src/guard.ts`) |
|
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) {
|
|
@@ -340,13 +360,35 @@ class Completing {
|
|
|
340
360
|
var completion = { active: false };
|
|
341
361
|
var OPTIONAL = /\[[^\]]+\]/g;
|
|
342
362
|
var USAGE_PREFIX = /^usage:\s*/i;
|
|
363
|
+
var FULL_STOP = /\.$/;
|
|
364
|
+
var DASHES = /^-+/;
|
|
343
365
|
function usageLine(usage, colors = paint(process3.stdout)) {
|
|
344
366
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
345
367
|
}
|
|
346
|
-
function helpText(usage, options, colors = paint(process3.stdout), examples = []) {
|
|
347
|
-
return [usageLine(usage, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
368
|
+
function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
|
|
369
|
+
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
348
370
|
`);
|
|
349
371
|
}
|
|
372
|
+
function replacesRows(replaces, colors = paint(process3.stdout)) {
|
|
373
|
+
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
374
|
+
}
|
|
375
|
+
function wantsHelpJson(env = process3.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
|
+
}
|
|
350
392
|
function exampleRows(examples, colors = paint(process3.stdout)) {
|
|
351
393
|
if (examples.length === 0)
|
|
352
394
|
return [];
|
|
@@ -356,10 +398,10 @@ function optionRows(options, colors = paint(process3.stdout)) {
|
|
|
356
398
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
357
399
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
358
400
|
const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
|
|
359
|
-
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 ?? "" };
|
|
360
402
|
});
|
|
361
403
|
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
362
|
-
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)}`);
|
|
363
405
|
}
|
|
364
406
|
var OUTPUT_OPTIONS = {
|
|
365
407
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
@@ -389,14 +431,15 @@ function parse(args, usage, options, settings = {}) {
|
|
|
389
431
|
const rest = separator === -1 ? undefined : args.slice(separator + 1);
|
|
390
432
|
let parsed;
|
|
391
433
|
try {
|
|
392
|
-
parsed = parseArgs({ args: own, options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
434
|
+
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
393
435
|
} catch (error) {
|
|
394
|
-
|
|
436
|
+
const message = parseError(error);
|
|
437
|
+
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
395
438
|
}
|
|
396
439
|
const values = parsed.values;
|
|
397
440
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
398
441
|
if (values.help === true) {
|
|
399
|
-
console.log(helpText(usage, all, undefined, settings
|
|
442
|
+
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, all, undefined, settings));
|
|
400
443
|
throw new HelpShown;
|
|
401
444
|
}
|
|
402
445
|
if (settings.positionals !== undefined)
|
|
@@ -411,10 +454,45 @@ function checkPositionals(given, names) {
|
|
|
411
454
|
if (!variadic && given.length > names.length)
|
|
412
455
|
badUsage(`unexpected argument ${given[names.length]}`);
|
|
413
456
|
}
|
|
457
|
+
function joinDashValues(args, options) {
|
|
458
|
+
const longOf = new Map(Object.entries(options).flatMap(([name, option]) => option.short === undefined ? [[`--${name}`, name]] : [[`--${name}`, name], [`-${option.short}`, name]]));
|
|
459
|
+
const isOption = (arg) => arg === "--" || longOf.has(arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg);
|
|
460
|
+
const joined = [];
|
|
461
|
+
for (let i = 0;i < args.length; i++) {
|
|
462
|
+
const arg = args[i] ?? "";
|
|
463
|
+
if (arg === "--")
|
|
464
|
+
return [...joined, ...args.slice(i)];
|
|
465
|
+
const name = longOf.get(arg);
|
|
466
|
+
const next = args[i + 1];
|
|
467
|
+
if (name !== undefined && options[name]?.type === "string" && next !== undefined && next.startsWith("-") && next !== "-" && !isOption(next)) {
|
|
468
|
+
joined.push(`--${name}=${next}`);
|
|
469
|
+
i++;
|
|
470
|
+
} else {
|
|
471
|
+
joined.push(arg);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return joined;
|
|
475
|
+
}
|
|
414
476
|
function parseError(error) {
|
|
415
477
|
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
416
478
|
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
417
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
|
+
}
|
|
418
496
|
async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
419
497
|
if (path === "-") {
|
|
420
498
|
if (process3.stdin.isTTY)
|
|
@@ -428,33 +506,19 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
|
428
506
|
}
|
|
429
507
|
// ../kit/src/cli.ts
|
|
430
508
|
import process4 from "process";
|
|
431
|
-
|
|
432
|
-
// ../kit/src/text.ts
|
|
433
|
-
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
434
|
-
function distance(a, b) {
|
|
435
|
-
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
436
|
-
for (const [i, left] of [...a].entries()) {
|
|
437
|
-
const current = [i + 1];
|
|
438
|
-
for (const [j, right] of [...b].entries())
|
|
439
|
-
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
440
|
-
previous = current;
|
|
441
|
-
}
|
|
442
|
-
return previous[b.length] ?? 0;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// ../kit/src/cli.ts
|
|
446
509
|
var LEVEL_OPTIONS = {
|
|
447
510
|
...OUTPUT_OPTIONS,
|
|
448
511
|
help: { type: "boolean", short: "h", description: "show this help" }
|
|
449
512
|
};
|
|
450
513
|
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
451
|
-
function commandRows(commands, colors) {
|
|
514
|
+
async function commandRows(commands, colors) {
|
|
452
515
|
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
453
516
|
const groups = new Map([["Commands:", []]]);
|
|
454
|
-
|
|
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()) {
|
|
455
519
|
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
456
520
|
const rows = groups.get(heading) ?? [];
|
|
457
|
-
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(
|
|
521
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
|
|
458
522
|
groups.set(heading, rows);
|
|
459
523
|
}
|
|
460
524
|
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
@@ -474,25 +538,38 @@ function takeOutputFlags(argv) {
|
|
|
474
538
|
}
|
|
475
539
|
return argv.slice(index);
|
|
476
540
|
}
|
|
477
|
-
function
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
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 };
|
|
483
546
|
}
|
|
484
|
-
async function
|
|
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) {
|
|
485
558
|
const colors = paint(stream);
|
|
486
559
|
const prefix = path.join(" ");
|
|
560
|
+
const summary = await resolveSummary(lazySummary);
|
|
561
|
+
if (wantsHelpJson())
|
|
562
|
+
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
487
563
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
488
564
|
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
489
565
|
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
490
566
|
return [
|
|
491
|
-
header(colors),
|
|
567
|
+
header(colors, summary),
|
|
492
568
|
"",
|
|
493
569
|
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
570
|
+
...replacesRows(replaces, colors),
|
|
494
571
|
"",
|
|
495
|
-
...commandRows(commands, colors),
|
|
572
|
+
...await commandRows(commands, colors),
|
|
496
573
|
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
497
574
|
"",
|
|
498
575
|
colors.bold("Options:"),
|
|
@@ -511,13 +588,21 @@ function printError(prefix, message, hint, cause) {
|
|
|
511
588
|
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
512
589
|
console.error(`${head} ${message}${under("cause", cause, colors.dim)}${under("hint", hint, colors.yellow)}`);
|
|
513
590
|
}
|
|
514
|
-
function
|
|
591
|
+
function printJsonError(message, hint, cause) {
|
|
592
|
+
console.error(JSON.stringify({ error: message, ...hint === undefined ? {} : { hint }, ...cause === undefined ? {} : { cause } }));
|
|
593
|
+
}
|
|
594
|
+
function wantsJson(argv) {
|
|
595
|
+
const end = argv.indexOf("--");
|
|
596
|
+
return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
|
|
597
|
+
}
|
|
598
|
+
function reportThrown(prefix, error, redact = (text) => text, json = false) {
|
|
515
599
|
if (error instanceof HelpShown)
|
|
516
600
|
return 0;
|
|
517
601
|
const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
|
|
518
602
|
const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
|
|
519
603
|
const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
|
|
520
|
-
|
|
604
|
+
const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
|
|
605
|
+
report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
|
|
521
606
|
return error instanceof Exit ? error.code : 1;
|
|
522
607
|
}
|
|
523
608
|
async function dispatch(level, typed, { version, redact }) {
|
|
@@ -530,11 +615,11 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
530
615
|
}
|
|
531
616
|
const [name, ...args] = argv;
|
|
532
617
|
if (name === undefined && level.path.length > 1) {
|
|
533
|
-
console.error(await help(level, process4.stderr));
|
|
618
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stderr));
|
|
534
619
|
return 2;
|
|
535
620
|
}
|
|
536
621
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
537
|
-
console.log(await help(level, process4.stdout));
|
|
622
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process4.stdout));
|
|
538
623
|
return 0;
|
|
539
624
|
}
|
|
540
625
|
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
@@ -543,6 +628,10 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
543
628
|
}
|
|
544
629
|
let command = level.commands[level.aliases?.[name] ?? name];
|
|
545
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
|
+
}
|
|
546
635
|
if (command === undefined && level.defaultCommand !== undefined) {
|
|
547
636
|
command = level.commands[level.defaultCommand];
|
|
548
637
|
rest = argv;
|
|
@@ -554,12 +643,12 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
554
643
|
}
|
|
555
644
|
if ("commands" in command) {
|
|
556
645
|
const { summary } = command;
|
|
557
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${
|
|
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 });
|
|
558
647
|
}
|
|
559
648
|
try {
|
|
560
649
|
return await command.run(rest);
|
|
561
650
|
} catch (error) {
|
|
562
|
-
return reportThrown(`${prefix} ${name}`, error, redact);
|
|
651
|
+
return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
|
|
563
652
|
}
|
|
564
653
|
}
|
|
565
654
|
var COMPLETE = "__complete";
|
|
@@ -579,18 +668,24 @@ async function optionsOf(run) {
|
|
|
579
668
|
async function leafCandidates(run, words) {
|
|
580
669
|
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
581
670
|
return [FILES];
|
|
582
|
-
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}`);
|
|
583
672
|
}
|
|
584
|
-
async function candidates(
|
|
673
|
+
async function candidates(scope, defaultCommand, words, before = []) {
|
|
585
674
|
const [word = "", ...rest] = words;
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
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)}`));
|
|
589
684
|
if (command === undefined) {
|
|
590
685
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
591
686
|
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
592
687
|
}
|
|
593
|
-
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);
|
|
594
689
|
}
|
|
595
690
|
async function printCandidates(found) {
|
|
596
691
|
for (const line of await found)
|
|
@@ -599,8 +694,8 @@ async function printCandidates(found) {
|
|
|
599
694
|
}
|
|
600
695
|
async function runCli(cli, argv = process4.argv.slice(2)) {
|
|
601
696
|
if (argv[0] === COMPLETE)
|
|
602
|
-
return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
|
|
603
|
-
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${
|
|
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 });
|
|
604
699
|
}
|
|
605
700
|
// ../kit/src/process.ts
|
|
606
701
|
import { constants } from "os";
|
|
@@ -624,7 +719,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
|
|
|
624
719
|
return { code: exitCodeOf(proc.exitCode, proc.signalCode), chunks, output, text: output.toString() };
|
|
625
720
|
}
|
|
626
721
|
// package.json
|
|
627
|
-
var version = "0.
|
|
722
|
+
var version = "0.4.1";
|
|
628
723
|
|
|
629
724
|
// ../kit/src/shell.ts
|
|
630
725
|
var SEPARATORS = new Set([`
|
|
@@ -1120,7 +1215,7 @@ var EXEC_VALUE_OPTIONS = new Set(["-e", "--env", "--env-file", "-u", "--user", "
|
|
|
1120
1215
|
var CONFIG_LISTS = new Set(["--services", "--volumes", "--profiles", "--images", "--networks", "--hash", "-q", "--quiet"]);
|
|
1121
1216
|
var FORMAT_OPTIONS = new Set(["-f", "--format"]);
|
|
1122
1217
|
var WHOLE_FORMAT = /Env|\{\{\s*json\s+\.\s*\}\}|^json$/;
|
|
1123
|
-
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`.';
|
|
1124
1219
|
function isEnvFile(path) {
|
|
1125
1220
|
const name = path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf(":")) + 1);
|
|
1126
1221
|
return (ENV_FILE.test(name) || name === ".env*") && !TEMPLATE.test(name);
|
|
@@ -1523,7 +1618,7 @@ var SHORT_CLUSTER = /^-[A-Z]+$/i;
|
|
|
1523
1618
|
var STAGES_ALL = /[Au]/;
|
|
1524
1619
|
var ALL_TAGS = new Set(["--tags", "--follow-tags"]);
|
|
1525
1620
|
var STAGE_BY_PATH = `Stage by path: the tree carries unrelated work and peer sessions edit it live. ${USE_GC}`;
|
|
1526
|
-
var STASH = "Never `git stash`: a pop with nothing saved applies someone else's old stash. To
|
|
1621
|
+
var STASH = "Never `git stash`: a pop with nothing saved applies someone else's old stash. To prove a test fails without your change, `proof --keep <test> -- <cmd>` runs it at HEAD in a temporary worktree and again here; `base -- <cmd>` runs any command at HEAD (`--ref <rev>` another commit), the working tree untouched. To set edits aside before a cherry-pick, a rebase or a checkout, commit them with `gc` first, or do the other work in `git worktree add`.";
|
|
1527
1622
|
var ADD_ALL = `\`git add\` over the whole tree is refused. ${STAGE_BY_PATH}`;
|
|
1528
1623
|
var COMMIT_ALL = `\`git commit -a\` stages the whole tree and is refused. ${STAGE_BY_PATH}`;
|
|
1529
1624
|
var PUSH = "`git push` runs only when the user asked for a push in their latest prompt: say the commit is ready and let them ask.";
|
|
@@ -2092,18 +2187,25 @@ function checkPath(path) {
|
|
|
2092
2187
|
var BINS = {
|
|
2093
2188
|
check: "`check`: runs the repo's check, typecheck, lint and test steps and prints only what failed, each full log saved to a file; validate a change with it rather than chaining the scripts (`--only lint,test`, `--list`).",
|
|
2094
2189
|
fix: "`fix`: runs the repo's fixers and formatters (its fix, lint:fix and format scripts, just fmt, go fmt, cargo fmt, ruff) and lists the files they changed.",
|
|
2190
|
+
errs: "`errs [log]`, `errs -- <cmd>`: turns tsc, tsgo, eslint, bun check and bun test output into one `file:line:col: message (rule)` line per error, counting the lines it did not recognise (`--json`); use it instead of reading raw type, lint or test logs.",
|
|
2095
2191
|
db: '`db "<sql>"`: one read-only statement against the Postgres in DATABASE_URL (`-e KEY` for another) or, without one, the compose project\'s running Postgres service (`-s <service>`, `-d <database>`), credentials never shown, a write printed for the user to run; `db tables`, `db describe <table>`, `db dbs`.',
|
|
2096
2192
|
ev: "`ev`: .env files read and edited with values masked (`ev list`, `ev get KEY`, `ev grep <regex>`, `ev show <rev>:<file>`, `ev set`, `ev diff`, `ev missing`); `ev audit [dir]` finds env files others can read or git tracks, and compose files writing secrets out.",
|
|
2097
2193
|
redact: "`redact`: masks secrets in piped output, `<cmd> | redact`.",
|
|
2098
2194
|
pkg: "`pkg <name>[@version]`: checks an npm package before it is added: whether the name exists or is a near miss of a popular one, the newest version at least two weeks old, licence, install scripts; `bun add`, `npm install`, `pnpm add` and `yarn add` run it first and are refused on what it flags. `pkg bump <@scope/lib> [version]` waits until the registry serves a release, then `bun add --exact`s it in every consumer under ~/Workspace (`--dry-run`), `pkg consumers <name>` lists them; use it instead of a `bun info` and sleep loop and a `bun add` per repo.",
|
|
2099
2195
|
stack: "`stack port <name>`: the port a service name always hashes to, for every host port a compose file publishes; `stack audit [dir]` cross-checks release.config.ts against its compose files.",
|
|
2100
|
-
ft: "`ft`: Forgejo CI runs, logs and waits, releases, issue checklists and claims, in a repo whose origin is on Forgejo.",
|
|
2196
|
+
ft: "`ft`: Forgejo CI runs, logs and waits, releases, issue checklists and claims, milestones (`ft milestones`, `ft milestone <m>`, `ft new -m`), in a repo whose origin is on Forgejo.",
|
|
2101
2197
|
sub: "`sub --from <text> --to <text> <file>...`: replaces text, writing nothing unless each match count is as expected (1, `-n N`, `--all`), `--regex`, `--pairs <json|->` for several in order, `--dry-run --diff`; use it instead of sed -i, a bun -e or python editor.",
|
|
2102
2198
|
base: "`base [--ref <rev>] [--keep <path>...] -- <cmd>`: runs a command at HEAD (or a ref) in a temporary worktree, removed afterwards, ignored env files and node_modules/vendor linked; use it instead of git stash to see whether a test fails without your change (`--keep` the new test).",
|
|
2199
|
+
proof: "`proof [--ref <rev>] [--keep <path>...] -- <cmd>`: runs the command at HEAD (or a ref) in a temporary worktree, then in the working tree, and exits 0 only when it fails before and passes now, printing the failing side's output; use it to prove a new test fails without your change (`--keep` the test).",
|
|
2103
2200
|
gst: "`gst [--log N]`: branch, upstream, changed counts and HEAD in one line, then the changed paths by staged, unstaged and untracked; use it instead of `git status --short` and `git log --oneline`.",
|
|
2104
2201
|
gc: "`gc <title> [-m <line>]... [--ticket <line>]... -- <path>...`: commits exactly those paths, refusing with nothing staged when the message breaks a commit rule, a path has no change or other files are already staged; use it instead of chaining git add, status and commit.",
|
|
2105
2202
|
waitfor: "`waitfor --file <log> --until <regex> [--fail <regex>]`, `--port <n>`, `--url <url>`, `--pid <n>` (`-t 5m`): waits until the line shows, the port listens, the URL answers 2xx or the process exits, exits 1 on a --fail line with the lines around it, 2 on a timeout with the last lines; `waitfor task <id>` waits for a background Bash task and prints its exit code, last lines and failure lines; use it instead of sleep and tail, `until ! pgrep -f` or polling loops.",
|
|
2106
2203
|
gl: "`gl ci [ref]` waits for the GitLab pipeline covering a commit and prints each failed job's tests (exit 0/1/2), `gl runs`, `gl logs <pipeline> [-j job] [--full]`; use it instead of `glab ci` or `glab api .../pipelines/<id>/jobs` polled in a sleep loop.",
|
|
2204
|
+
peek: "`peek <file:from-to>...` prints several spans with line numbers in one call, `peek --def <symbol>` a whole declaration, `--refs <symbol>` its usages by file, `--outline <file>` the declarations and members, `--around <regex> -C 3` the lines around matches; use it instead of `sed -n N,Mp`, `grep -n` chains and whole-file reads.",
|
|
2205
|
+
"todo-scan": "`todo-scan [path...]` lists the TODO, FIXME and owner/repo#N comments as `<file>:<line>: <comment>`, `--stale` only those whose issue is closed (exit 1), through ft; use it to find the workarounds a shipped fix lets you drop.",
|
|
2206
|
+
doctor: "`doctor`: checks this host's agent-tools install, one line per check with its fix: each bin's version against the registry, tea's login, the Claude Code hooks, the Bun floor (`--json`); run it when a bin lacks a command or a flag its docs name.",
|
|
2207
|
+
"agent-tools": "`agent-tools list`: every agent-tools bin in one line, what it does and what it is reached for instead of, read from each bin's own --help (`--json` adds the synopsis and examples); run it to find the bin that fits a task.",
|
|
2208
|
+
hotspots: "`hotspots [dir] [--since <date>] [-n 20]`: ranks the source files by complexity times git churn (commits touching each in the window, 6 months by default), tests and generated files left out, `--json`; use it to pick which complex files are worth splitting first.",
|
|
2107
2209
|
out: "`out -- <cmd>`: folds long output, keeping the errors and saving the full log; run noisy builds, installs, tests and linters through it."
|
|
2108
2210
|
};
|
|
2109
2211
|
function binsSection(installed) {
|
|
@@ -2180,7 +2282,9 @@ var checkUsage = `guard check [options] [<command> | -]
|
|
|
2180
2282
|
|
|
2181
2283
|
Checks a shell command against the rules: remote machines, git (stash, whole-tree add and commit, push, tags), commit messages, SQL writes, env files, package managers and in-place edits (sed -i and replace scripts, pointed at sub).
|
|
2182
2284
|
Prints each broken rule with the route to take instead and exits 1, prints nothing and exits 0 when the command is clean.
|
|
2183
|
-
The command is read from stdin with - or when none is given; a rule may rewrite it instead (the pinned package manager, a noisy run through out), printed as \`runs as: <command
|
|
2285
|
+
The command is read from stdin with - or when none is given; a rule may rewrite it instead (the pinned package manager, a noisy run through out), printed as \`runs as: <command>\`.
|
|
2286
|
+
|
|
2287
|
+
Exits 0 when the command is clean, 1 when a rule is broken, 2 on a usage error.`;
|
|
2184
2288
|
var checkOptions = {
|
|
2185
2289
|
json: jsonOption("{violations, command, notes, packages} for a hook"),
|
|
2186
2290
|
cwd: { type: "string", description: "the directory the command runs in (default: here)" },
|
|
@@ -2189,7 +2293,9 @@ var checkOptions = {
|
|
|
2189
2293
|
};
|
|
2190
2294
|
var pathUsage = `guard path [--json] <path>...
|
|
2191
2295
|
|
|
2192
|
-
Checks the paths a file tool would read, edit or write: env files are refused
|
|
2296
|
+
Checks the paths a file tool would read, edit or write: env files are refused.
|
|
2297
|
+
|
|
2298
|
+
Exits 0 when every path is clean, 1 when one is refused, 2 on a usage error.`;
|
|
2193
2299
|
var pathOptions = {
|
|
2194
2300
|
json: jsonOption("{violations} for a hook")
|
|
2195
2301
|
};
|
|
@@ -2197,7 +2303,9 @@ var hookUsage = `guard hook <${EVENTS.join("|")}>
|
|
|
2197
2303
|
|
|
2198
2304
|
Answers a Claude Code hook: reads the event JSON on stdin and prints the reply JSON, nothing when the call goes ahead untouched.
|
|
2199
2305
|
pre-tool-use refuses a Bash call breaking a rule, an install pkg refuses, and a Read, Edit or Write of an env file; user-prompt-submit remembers whether the prompt asked for a push; session-start lists the installed agent-tools bins.
|
|
2200
|
-
A failure exits 1, which Claude Code reports without blocking the call
|
|
2306
|
+
A failure exits 1, which Claude Code reports without blocking the call.
|
|
2307
|
+
|
|
2308
|
+
Exits 0 when it answered, 1 on a failure or an unknown event (never 2, which would block the call).`;
|
|
2201
2309
|
function readText(path) {
|
|
2202
2310
|
try {
|
|
2203
2311
|
return readFileSync(path, "utf8");
|
|
@@ -2283,6 +2391,8 @@ process6.exitCode = await runCli({
|
|
|
2283
2391
|
name: "guard",
|
|
2284
2392
|
version,
|
|
2285
2393
|
about: "the safety rules a Claude Code Bash call is held to, each refusal naming the route",
|
|
2394
|
+
replaces: "a hook script per rule, each wording its own refusal",
|
|
2395
|
+
examples: [['guard check "git push --force"', "the rules a command breaks, with their routes"], ["guard path .env", "whether a file tool may open a path"]],
|
|
2286
2396
|
commands: {
|
|
2287
2397
|
check: { run: check, summary: "check a shell command, printing each broken rule with its route" },
|
|
2288
2398
|
path: { run: async (args) => Promise.resolve(path(args)), summary: "check a path a file tool would open" },
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/guard-tools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "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
|
-
"author": "Kaynooo <kaynooo
|
|
6
|
+
"author": "Kaynooo <contact@kaynooo.fr>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://git.
|
|
8
|
+
"homepage": "https://git.kaynooo.fr/kaynooo/agent-tools/src/branch/main/packages/guard#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://git.
|
|
11
|
+
"url": "git+https://git.kaynooo.fr/kaynooo/agent-tools.git",
|
|
12
12
|
"directory": "packages/guard"
|
|
13
13
|
},
|
|
14
|
-
"bugs": "https://git.
|
|
14
|
+
"bugs": "https://git.kaynooo.fr/kaynooo/agent-tools/issues",
|
|
15
15
|
"keywords": [
|
|
16
16
|
"claude-code",
|
|
17
17
|
"cli",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"bun": ">=1.4.3"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
|
-
"registry": "https://git.
|
|
32
|
+
"registry": "https://git.kaynooo.fr/api/packages/kaynooo/npm/"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "$npm_execpath build src/cli.ts --target bun --outdir dist",
|