@kaynooo/cli-out 0.7.0 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +10 -4
  2. package/dist/cli.js +375 -172
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -7,6 +7,7 @@ bun install -g @kaynooo/cli-out
7
7
  out -- bun install
8
8
  bun run build 2>&1 | out
9
9
  out --stats --since 7d
10
+ out --grep 'error TS'
10
11
  ```
11
12
 
12
13
  ## Behaviour
@@ -20,14 +21,19 @@ out --stats --since 7d
20
21
  - The head and tail are kept, and lines with `error`, `fail`, `warn`, `panic`, `exception`, `fatal` or a stack frame stay even in the folded middle, the first 40 of them. Lines wider than 300 characters are cut in the folded output (the full log keeps them), and a few-lines output that is still heavy keeps a shorter head and tail.
21
22
  - The exit code is always the wrapped command's. Piped into with no command, `out` folds what it reads and exits 0, the writer's code left to `set -o pipefail`; only stdout is piped, so `2>&1` brings stderr along. A terminal or `/dev/null` on stdin with no command is a usage error.
22
23
  - The output is streamed to a full log as it arrives once it outgrows the 4 MiB held in memory (the first and last 2 MiB), otherwise the log is written when folding. Logs go to `$XDG_RUNTIME_DIR/kaynooo-cli/out` (`~/.cache/kaynooo-cli/out` without it), a folder only the user can enter, files at mode 0600, and logs older than a day are removed on the next run.
23
- - SIGINT, SIGTERM and SIGHUP go to the command's whole process group, and `out` exits with its code. A watch command (`--watch`, `tsc -w`, `just dev`) is never wrapped by the hook, as its output would sit buffered.
24
+ - SIGINT, SIGTERM and SIGHUP go to the command's whole process group, and `out` exits with its code. A watch command (`--watch`, `tsc -w`, `just dev`) is never wrapped by `guard`, as its output would sit buffered.
25
+
26
+ ## Saved logs
27
+
28
+ - `out --last` prints the path of the most recent full log, `out --list` lists them (path, size, age, newest first) and `out --grep <regex>` prints the matching lines of the last log, or of the one `--log <name|path>` names, each as `N: line` with N the line number the folded output's `(same as line N)` refers to.
29
+ - Exit 0 when found, 1 when `--grep` matches nothing or `--list` finds no log, 2 when there is no saved log to read or the regex is invalid. A run saves a log only when its output was folded, and logs older than a day are removed.
24
30
 
25
31
  ## Stats
26
32
 
27
33
  - Each run appends a line to `$XDG_STATE_HOME/kaynooo-cli/out/stats.jsonl` (`~/.local/state/...` without it, folder 0700, file 0600): the time, the folder, the words naming the command, the bytes it printed, the bytes the session would have read without `out` and the bytes `out` showed. Only lowercase words without digits after the program are kept (`bun run build`, `docker compose exec`), so no argument, assignment or token reaches the file; a `sh -c` chain is named by its last command that is not a `cd`.
28
- - `--was '<stage>'` (repeatable, in pipeline order) names a `tail`, `head` or `grep` stage `out` stands in for, as the hook passes them when it drops one: the session would have read what that stage printed, so the saving counts against it. Output that folding would not make shorter is printed untouched, so `out` never shows more than the command printed. A stage it cannot read (`tail -f`, a long grep option it does not know) counts the whole output.
34
+ - `--was '<stage>'` (repeatable, in pipeline order) names a `tail`, `head` or `grep` stage `out` stands in for, as `guard` passes them when it drops one: the session would have read what that stage printed, so the saving counts against it. Output that folding would not make shorter is printed untouched, so `out` never shows more than the command printed. A stage it cannot read (`tail -f`, a long grep option it does not know) counts the whole output.
29
35
  - `out --stats [--since 7d] [--json]` prints the runs, how many were folded, the bytes printed and shown, and about how many tokens were saved (bytes / 4, an estimate), then the ten commands that saved the most, with the share of runs folded and, per command, the median share of the output a folded run cut.
30
36
 
31
- ## Wrapping from a hook
37
+ ## Wrapping from guard
32
38
 
33
- The pure functions a Claude Code PreToolUse hook needs live in `src/wrap.ts`: `shouldWrap(command)` is true when every part of a command line is a listed noisy command (installs, builds, test runners, linters, `docker build`/`docker compose build|pull`, `just <recipe>`), with no pipe, redirect, background, substitution, `--json` or `--help`. A runner is known by its last path segment (`vendor/bin/phpunit`), through `php` (`php bin/phpunit`) and inside `docker compose exec|run` or `docker exec` (`docker compose exec -T php vendor/bin/paratest`). A line ending in pipes into `tail` or `head` keeping at least 150 lines (`-n 200`, `-200`, `--lines=200`), out's fold size, is wrapped with those stages and the `2>&1` feeding them dropped, since the folded output is no longer and keeps the failures they would have picked out; a `grep` or a shorter cut shows less than `out` would, so that line is left as it is. `wrap(command)` returns `out -- <command>`, through `sh -c '...'` for `&&` chains and leading `VAR=value` assignments, each dropped stage passed ahead as `--was '<stage>'`. The guard hook imports them from this package.
39
+ The pure functions `guard` needs to wrap a command live in `src/wrap.ts`: `shouldWrap(command)` is true when every part of a command line is a listed noisy command (installs, builds, test runners, linters, `docker build`/`docker compose build|pull`, `just <recipe>`), with no pipe, redirect, background, substitution, `--json` or `--help`. A runner is known by its last path segment (`vendor/bin/phpunit`), through `php` (`php bin/phpunit`) and inside `docker compose exec|run` or `docker exec` (`docker compose exec -T php vendor/bin/paratest`). A line ending in pipes into `tail` or `head` keeping at least 150 lines (`-n 200`, `-200`, `--lines=200`), out's fold size, is wrapped with those stages and the `2>&1` feeding them dropped, since the folded output is no longer and keeps the failures they would have picked out; a `grep` or a shorter cut shows less than `out` would, so that line is left as it is. `wrap(command)` returns `out -- <command>`, through `sh -c '...'` for `&&` chains and leading `VAR=value` assignments, each dropped stage passed ahead as `--was '<stage>'`. The `guard` command imports them to rewrite a command into `out -- ...`; its Claude Code hook never wraps, since a hook cannot swap a command without approving it, and refuses with the command to run instead.
package/dist/cli.js CHANGED
@@ -2,18 +2,16 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli.ts
5
- import process11 from "process";
6
-
5
+ import process14 from "process";
7
6
  // ../kit/src/args.ts
8
- import { fstatSync } from "fs";
9
- import process3 from "process";
7
+ import process5 from "process";
10
8
  import { parseArgs } from "util";
11
9
 
12
10
  // ../kit/src/format.ts
13
- import process2 from "process";
11
+ import process3 from "process";
14
12
 
15
13
  // ../kit/src/style.ts
16
- import process from "process";
14
+ import process2 from "process";
17
15
 
18
16
  // ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
19
17
  function x(s) {
@@ -277,10 +275,10 @@ function le(i = {}) {
277
275
  var t = le();
278
276
 
279
277
  // ../kit/src/style.ts
280
- function takesControlCodes(stream, env = process.env) {
278
+ function takesControlCodes(stream, env = process2.env) {
281
279
  return stream.isTTY === true && env.NO_COLOR === undefined && env.NODE_DISABLE_COLORS === undefined && env.TERM !== "dumb";
282
280
  }
283
- function paint(stream, env = process.env) {
281
+ function paint(stream, env = process2.env) {
284
282
  return le({ stream: { isTTY: takesControlCodes(stream, env) }, env });
285
283
  }
286
284
  var STATUSES = {
@@ -293,10 +291,31 @@ var STATUSES = {
293
291
  var STATUS_OF = new Map(Object.entries(STATUSES).flatMap(([status, { words }]) => words.map((word) => [word, status])));
294
292
 
295
293
  // ../kit/src/format.ts
294
+ var UNITS = [["d", 86400], ["h", 3600], ["m", 60], ["s", 1]];
295
+ function formatSeconds(total) {
296
+ let rest = Math.max(0, Math.round(total));
297
+ const amounts = UNITS.map(([unit, size]) => {
298
+ const amount = Math.floor(rest / size);
299
+ rest -= amount * size;
300
+ return `${amount}${unit}`;
301
+ });
302
+ const first = amounts.findIndex((amount) => !amount.startsWith("0"));
303
+ if (first === -1)
304
+ return "0s";
305
+ const next = amounts[first + 1];
306
+ return next === undefined || next.startsWith("0") ? amounts[first] ?? "0s" : `${amounts[first]} ${next}`;
307
+ }
296
308
  function upTo(text, separator) {
297
309
  const end = text.indexOf(separator);
298
310
  return end === -1 ? text : text.slice(0, end);
299
311
  }
312
+ function formatAge(date, now = Date.now()) {
313
+ const at = typeof date === "number" ? date : Date.parse(date);
314
+ if (Number.isNaN(at))
315
+ return "-";
316
+ const seconds = (now - at) / 1000;
317
+ return seconds < 60 ? "just now" : `${upTo(formatSeconds(seconds), " ")} ago`;
318
+ }
300
319
  function truncate(text, width) {
301
320
  return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
302
321
  }
@@ -313,7 +332,7 @@ function table(rows, { cellWidth, header = false, align = [], fit } = {}) {
313
332
  widths[index] = Math.max(widths[index] ?? 0, textWidth(cell));
314
333
  }
315
334
  const shown = fit === undefined ? cells : narrow(cells, widths, fit);
316
- const colors = paint(process2.stdout);
335
+ const colors = paint(process3.stdout);
317
336
  const lines = shown.map((row) => row.map((cell, index) => {
318
337
  const width = widths[index] ?? 0;
319
338
  const gap = " ".repeat(Math.max(0, width - textWidth(cell)));
@@ -344,6 +363,63 @@ function printJson(value) {
344
363
  console.log(JSON.stringify(value, undefined, 2));
345
364
  }
346
365
 
366
+ // ../kit/src/help.ts
367
+ import process4 from "process";
368
+ var OPTIONAL = /\[[^\]]+\]/g;
369
+ var USAGE_PREFIX = /^usage:\s*/i;
370
+ var FULL_STOP = /\.$/;
371
+ function usageLine(usage, colors = paint(process4.stdout)) {
372
+ return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
373
+ }
374
+ function helpText(usage, options, colors = paint(process4.stdout), { examples = [], replaces, exits = [] } = {}) {
375
+ return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exitRows(exits, colors), ...exampleRows(examples, colors)].join(`
376
+ `);
377
+ }
378
+ function exitRows(exits, colors = paint(process4.stdout)) {
379
+ if (exits.length === 0)
380
+ return [];
381
+ const width = Math.max(...exits.map(([code]) => String(code).length));
382
+ return ["", colors.bold("Exits:"), ...exits.map(([code, meaning]) => ` ${colors.cyan(String(code).padEnd(width))} ${meaning}`)];
383
+ }
384
+ function replacesRows(replaces, colors = paint(process4.stdout)) {
385
+ return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
386
+ }
387
+ function wantsHelpJson(env = process4.env) {
388
+ return env.KIT_HELP === "json";
389
+ }
390
+ function description(usage, { examples = [], replaces, exits } = {}, options) {
391
+ const lines = usage.replace(USAGE_PREFIX, "").split(`
392
+ `);
393
+ const first = lines[0];
394
+ const colon = first.indexOf(": ");
395
+ const prose = lines[lines.indexOf("") + 1] ?? "";
396
+ const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
397
+ return {
398
+ synopsis: colon === -1 ? first : first.slice(0, colon),
399
+ summary: colon === -1 ? fallback : first.slice(colon + 2),
400
+ ...replaces === undefined ? {} : { replaces },
401
+ examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] }),
402
+ ...exits === undefined ? {} : { exits: exits.map(([code, meaning]) => ({ code, meaning })) },
403
+ ...options === undefined ? {} : { options: Object.entries(options).map(([name, { short, description }]) => ({ name, ...short === undefined ? {} : { short }, ...description === undefined ? {} : { description } })) }
404
+ };
405
+ }
406
+ function exampleRows(examples, colors = paint(process4.stdout)) {
407
+ if (examples.length === 0)
408
+ return [];
409
+ return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
410
+ }
411
+ function optionRows(options, colors = paint(process4.stdout)) {
412
+ const rows = Object.entries(options).map(([name, option]) => {
413
+ const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
414
+ const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
415
+ const placeholder = option.optional === undefined ? named : `[${named}]`;
416
+ const value = option.type === "string" ? ` ${colors.dim(placeholder)}` : "";
417
+ return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? placeholder.length + 1 : 0), description: option.description ?? "" };
418
+ });
419
+ const width = Math.max(0, ...rows.map((row) => row.width));
420
+ return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
421
+ }
422
+
347
423
  // ../kit/src/text.ts
348
424
  var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
349
425
  function stripAnsi(text) {
@@ -366,8 +442,35 @@ function closest(name, names) {
366
442
  const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
367
443
  return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
368
444
  }
445
+ // ../kit/src/input.ts
446
+ import { fstatSync } from "fs";
447
+ var WHOLE = /^\d+$/;
448
+ function wholeNumber(value, name, fallback, min = 1) {
449
+ if (value === undefined)
450
+ return fallback;
451
+ const number = Number(value);
452
+ if (!WHOLE.test(value) || !Number.isSafeInteger(number) || number < min)
453
+ badUsage(`${name.startsWith("<") ? name : `--${name}`} takes a whole number from ${min}, not ${value}`);
454
+ return number;
455
+ }
456
+ var DURATION = /^(\d+(?:\.\d+)?)(ms|[smhd])?$/;
457
+ var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
458
+ function duration(value, name, fallback, unit = "s") {
459
+ if (value === undefined)
460
+ return fallback;
461
+ const match = DURATION.exec(value.trim());
462
+ const ms = match === null ? 0 : Number(match[1]) * UNIT_MS[match[2] ?? unit];
463
+ if (ms < 1)
464
+ badUsage(`--${name} takes a duration such as 30s, 5m, 1h or 7d, not ${value}`);
465
+ return ms;
466
+ }
467
+ function isStdinPiped() {
468
+ const stat = fstatSync(0);
469
+ return stat.isFIFO() || stat.isFile();
470
+ }
369
471
 
370
472
  // ../kit/src/args.ts
473
+ var DASHES = /^-+/;
371
474
  function jsonOption(holds) {
372
475
  return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
373
476
  }
@@ -411,58 +514,11 @@ class Completing {
411
514
  }
412
515
  }
413
516
  var completion = { active: false };
414
- var OPTIONAL = /\[[^\]]+\]/g;
415
- var USAGE_PREFIX = /^usage:\s*/i;
416
- var FULL_STOP = /\.$/;
417
- var DASHES = /^-+/;
418
- function usageLine(usage, colors = paint(process3.stdout)) {
419
- return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
420
- }
421
- function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
422
- return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
423
- `);
424
- }
425
- function replacesRows(replaces, colors = paint(process3.stdout)) {
426
- return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
427
- }
428
- function wantsHelpJson(env = process3.env) {
429
- return env.KIT_HELP === "json";
430
- }
431
- function description(usage, { examples = [], replaces } = {}) {
432
- const lines = usage.replace(USAGE_PREFIX, "").split(`
433
- `);
434
- const first = lines[0];
435
- const colon = first.indexOf(": ");
436
- const prose = lines[lines.indexOf("") + 1] ?? "";
437
- const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
438
- return {
439
- synopsis: colon === -1 ? first : first.slice(0, colon),
440
- summary: colon === -1 ? fallback : first.slice(colon + 2),
441
- ...replaces === undefined ? {} : { replaces },
442
- examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
443
- };
444
- }
445
- function exampleRows(examples, colors = paint(process3.stdout)) {
446
- if (examples.length === 0)
447
- return [];
448
- return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
449
- }
450
- function optionRows(options, colors = paint(process3.stdout)) {
451
- const rows = Object.entries(options).map(([name, option]) => {
452
- const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
453
- const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
454
- const placeholder = option.optional === undefined ? named : `[${named}]`;
455
- const value = option.type === "string" ? ` ${colors.dim(placeholder)}` : "";
456
- return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? placeholder.length + 1 : 0), description: option.description ?? "" };
457
- });
458
- const width = Math.max(0, ...rows.map((row) => row.width));
459
- return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
460
- }
461
517
  var OUTPUT_OPTIONS = {
462
518
  color: { type: "string", description: "auto (on a terminal alone), always or never" },
463
519
  plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
464
520
  };
465
- function applyOutput(color, plain, env = process3.env) {
521
+ function applyOutput(color, plain, env = process5.env) {
466
522
  const when = plain === true ? "never" : color ?? "auto";
467
523
  if (when === "auto")
468
524
  return;
@@ -485,7 +541,7 @@ function parse(args, usage, options, settings = {}) {
485
541
  const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
486
542
  let parsed;
487
543
  try {
488
- parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
544
+ parsed = parseArgs({ args: joinDashValues(withBare(withAliases(own, all), all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
489
545
  } catch (error) {
490
546
  const message = parseError(error);
491
547
  throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
@@ -493,7 +549,7 @@ function parse(args, usage, options, settings = {}) {
493
549
  const values = parsed.values;
494
550
  applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
495
551
  if (values.help === true) {
496
- console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
552
+ console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings, listing)) : helpText(usage, listing, undefined, settings));
497
553
  throw new HelpShown;
498
554
  }
499
555
  checkChoices(parsed.values, all);
@@ -535,6 +591,18 @@ function listed(options, allowNegative) {
535
591
  return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
536
592
  }));
537
593
  }
594
+ function withAliases(args, options) {
595
+ const named = new Map(Object.entries(options).flatMap(([name, { aliases = [] }]) => aliases.map((alias) => [`--${alias}`, `--${name}`])));
596
+ if (named.size === 0)
597
+ return args;
598
+ const separator = args.indexOf("--");
599
+ const own = separator === -1 ? args : args.slice(0, separator);
600
+ return [...own.map((arg) => {
601
+ const at = arg.indexOf("=");
602
+ const current = named.get(at === -1 ? arg : arg.slice(0, at));
603
+ return current === undefined ? arg : `${current}${at === -1 ? "" : arg.slice(at)}`;
604
+ }), ...separator === -1 ? [] : args.slice(separator)];
605
+ }
538
606
  function withBare(args, options) {
539
607
  const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
540
608
  if (bare.size === 0)
@@ -619,33 +687,9 @@ function nearestOption(message, options, allowNegative) {
619
687
  const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
620
688
  return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
621
689
  }
622
- var WHOLE = /^\d+$/;
623
- function wholeNumber(value, name, fallback, min = 1) {
624
- if (value === undefined)
625
- return fallback;
626
- const number = Number(value);
627
- if (!WHOLE.test(value) || !Number.isSafeInteger(number) || number < min)
628
- badUsage(`${name.startsWith("<") ? name : `--${name}`} takes a whole number from ${min}, not ${value}`);
629
- return number;
630
- }
631
- var DURATION = /^(\d+(?:\.\d+)?)(ms|[smhd])?$/;
632
- var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
633
- function duration(value, name, fallback, unit = "s") {
634
- if (value === undefined)
635
- return fallback;
636
- const match = DURATION.exec(value.trim());
637
- const ms = match === null ? 0 : Number(match[1]) * UNIT_MS[match[2] ?? unit];
638
- if (ms < 1)
639
- badUsage(`--${name} takes a duration such as 30s, 5m, 1h or 7d, not ${value}`);
640
- return ms;
641
- }
642
- function isStdinPiped() {
643
- const stat = fstatSync(0);
644
- return stat.isFIFO() || stat.isFile();
645
- }
646
690
  // ../kit/src/process.ts
647
691
  import { constants } from "os";
648
- import process4 from "process";
692
+ import process6 from "process";
649
693
  var KILL_GRACE_MS = 2000;
650
694
  var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
651
695
  function exitCodeOf(code, signal) {
@@ -667,7 +711,7 @@ function holdSignals(cleanup) {
667
711
  },
668
712
  release: () => {
669
713
  for (const signal of SIGNALS)
670
- process4.off(signal, onSignal);
714
+ process6.off(signal, onSignal);
671
715
  }
672
716
  };
673
717
  function onSignal(signal) {
@@ -680,10 +724,10 @@ function holdSignals(cleanup) {
680
724
  return;
681
725
  held.release();
682
726
  cleanup(signal);
683
- process4.kill(process4.pid, signal);
727
+ process6.kill(process6.pid, signal);
684
728
  }
685
729
  for (const signal of SIGNALS)
686
- process4.on(signal, onSignal);
730
+ process6.on(signal, onSignal);
687
731
  return held;
688
732
  }
689
733
  async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal, maxBytes = Infinity, onChunk, onStart } = {}) {
@@ -697,7 +741,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal,
697
741
  return;
698
742
  try {
699
743
  if (grouped)
700
- process4.kill(-proc.pid, killing);
744
+ process6.kill(-proc.pid, killing);
701
745
  else
702
746
  proc.kill(killing);
703
747
  } catch {}
@@ -771,15 +815,116 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
771
815
  var RESET_STYLE = `${ESC}[0m`;
772
816
  var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
773
817
  // ../kit/src/cli.ts
774
- import process5 from "process";
818
+ import { readFileSync, realpathSync } from "fs";
819
+ import { dirname as dirname2, join as join3 } from "path";
820
+ import process9 from "process";
821
+
822
+ // ../kit/src/usage.ts
823
+ import { appendFileSync, mkdirSync } from "fs";
824
+ import { homedir as homedir2 } from "os";
825
+ import { dirname, isAbsolute, join as join2 } from "path";
826
+ import process8 from "process";
827
+
828
+ // ../kit/src/logs.ts
829
+ import { lstatSync, openSync } from "fs";
830
+ import { mkdir, mkdtemp, readdir, rm, stat } from "fs/promises";
831
+ import { homedir, tmpdir } from "os";
832
+ import { join } from "path";
833
+ import process7 from "process";
834
+ var DAY_MS = 24 * 60 * 60 * 1000;
835
+ var CLI_FOLDER = "kaynooo-cli";
836
+ function isPrivateDirectory(path) {
837
+ try {
838
+ const info = lstatSync(path, { throwIfNoEntry: false });
839
+ return info !== undefined && info.isDirectory() && info.uid === (process7.getuid?.() ?? info.uid) && (info.mode & 63) === 0;
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
844
+ async function privateLogDirectory(name, env = process7.env) {
845
+ const runtime = env.XDG_RUNTIME_DIR;
846
+ const base = runtime === undefined || runtime === "" ? join(homedir(), ".cache", CLI_FOLDER) : join(runtime, CLI_FOLDER);
847
+ const path = join(base, name);
848
+ try {
849
+ await mkdir(path, { recursive: true, mode: 448 });
850
+ if (isPrivateDirectory(path))
851
+ return path;
852
+ } catch {}
853
+ return mkdtemp(join(tmpdir(), `${name.replaceAll("/", "-")}-`));
854
+ }
855
+ async function sweepLogs(dir, maxAgeMs = DAY_MS, now = Date.now()) {
856
+ let names;
857
+ try {
858
+ names = await readdir(dir);
859
+ } catch {
860
+ return;
861
+ }
862
+ for (const name of names) {
863
+ const path = join(dir, name);
864
+ try {
865
+ const info = await stat(path);
866
+ if (info.isDirectory())
867
+ await sweepLogs(path, maxAgeMs, now);
868
+ if (now - info.mtimeMs > maxAgeMs)
869
+ await rm(path, { force: true, recursive: info.isDirectory() });
870
+ } catch {
871
+ continue;
872
+ }
873
+ }
874
+ }
875
+ function createLog(path) {
876
+ return openSync(path, "wx", 384);
877
+ }
878
+
879
+ // ../kit/src/usage.ts
880
+ var UNKNOWN_OPTION2 = /unknown option '(--[a-z][a-z0-9-]*)[=']/;
881
+ var FLAG_NAME = /^(?:--[a-z][a-z0-9-]*|-[a-zA-Z])$/;
882
+ var LONGEST_NAME = 40;
883
+ var usageError;
884
+ function noteUsageError(message) {
885
+ usageError = message;
886
+ }
887
+ function stateHome(env = process8.env) {
888
+ const dir = env.XDG_STATE_HOME;
889
+ return dir !== undefined && isAbsolute(dir) ? dir : join2(homedir2(), ".local", "state");
890
+ }
891
+ function usagePath(env = process8.env) {
892
+ return join2(stateHome(env), CLI_FOLDER, "usage.jsonl");
893
+ }
894
+ function usageRecorded(env = process8.env) {
895
+ const off = env.KAYNOOO_CLI_NO_USAGE;
896
+ return env.NODE_ENV !== "test" && (off === undefined || off === "" || off === "0");
897
+ }
898
+ function flagNames(argv) {
899
+ const end = argv.indexOf("--");
900
+ const names = (end === -1 ? argv : argv.slice(0, end)).filter((word) => word.startsWith("-") && word !== "-").map((word) => word.includes("=") ? word.slice(0, word.indexOf("=")) : word);
901
+ return [...new Set(names)].filter((name) => name.length <= LONGEST_NAME && FLAG_NAME.test(name));
902
+ }
903
+ function usageEntry(bin, command, argv, code, now = new Date) {
904
+ const unknown = code === 2 && usageError !== undefined ? UNKNOWN_OPTION2.exec(usageError)?.[1] : undefined;
905
+ usageError = undefined;
906
+ return { at: now.toISOString(), bin, ...command === undefined ? {} : { command }, flags: flagNames(argv), code, ...unknown === undefined ? {} : { unknown } };
907
+ }
908
+ function recordUsage(entry, env = process8.env) {
909
+ if (!usageRecorded(env))
910
+ return;
911
+ try {
912
+ const path = usagePath(env);
913
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
914
+ appendFileSync(path, `${JSON.stringify(entry)}
915
+ `, { mode: 384 });
916
+ } catch {}
917
+ }
918
+
919
+ // ../kit/src/cli.ts
775
920
  var LEVEL_OPTIONS = {
776
921
  ...OUTPUT_OPTIONS,
777
922
  help: { type: "boolean", short: "h", description: "show this help" }
778
923
  };
779
924
  var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
780
925
  function printError(prefix, message, hint, cause) {
781
- const colors = paint(process5.stderr);
782
- const styled = takesControlCodes(process5.stderr);
926
+ const colors = paint(process9.stderr);
927
+ const styled = takesControlCodes(process9.stderr);
783
928
  const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
784
929
  const under = (label, text, tint) => text === undefined ? "" : `
785
930
  ${styled ? tint(label) : `${label}:`} ${text}`;
@@ -798,6 +943,8 @@ function reportThrown(prefix, error, redact = (text) => text, json = false) {
798
943
  const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
799
944
  const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
800
945
  const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
946
+ if (error instanceof UsageError)
947
+ noteUsageError(error.message);
801
948
  const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
802
949
  report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
803
950
  return error instanceof Exit ? error.code : 1;
@@ -859,68 +1006,36 @@ async function printCandidates(found) {
859
1006
  console.log(line);
860
1007
  return 0;
861
1008
  }
862
- async function runMain({ name, version, run, redact }, argv = process5.argv.slice(2)) {
1009
+ function packageVersion(entry = Bun.main) {
1010
+ for (let dir = dirname2(realpathSync(entry));; dir = dirname2(dir)) {
1011
+ try {
1012
+ const { version } = JSON.parse(readFileSync(join3(dir, "package.json"), "utf8"));
1013
+ if (typeof version === "string")
1014
+ return version;
1015
+ } catch {}
1016
+ if (dirname2(dir) === dir)
1017
+ return "0.0.0";
1018
+ }
1019
+ }
1020
+ async function runMain({ name, version, run, redact, usage }, argv = process9.argv.slice(2)) {
863
1021
  if (argv[0] === COMPLETE)
864
1022
  return printCandidates(leafCandidates(run, argv.slice(1)));
865
1023
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
866
- console.log(version);
1024
+ console.log(version ?? packageVersion());
867
1025
  return 0;
868
1026
  }
1027
+ let code;
869
1028
  try {
870
- return await run(argv);
1029
+ code = await run(argv);
871
1030
  } catch (error) {
872
- return reportThrown(name, error, redact, wantsJson(argv));
1031
+ code = reportThrown(name, error, redact, wantsJson(argv));
873
1032
  }
874
- }
875
- // ../kit/src/logs.ts
876
- import { lstatSync, openSync } from "fs";
877
- import { mkdir, mkdtemp, readdir, rm, stat } from "fs/promises";
878
- import { homedir, tmpdir } from "os";
879
- import { join } from "path";
880
- import process6 from "process";
881
- var DAY_MS = 24 * 60 * 60 * 1000;
882
- var CLI_FOLDER = "kaynooo-cli";
883
- function isPrivateDirectory(path) {
884
- try {
885
- const info = lstatSync(path, { throwIfNoEntry: false });
886
- return info !== undefined && info.isDirectory() && info.uid === (process6.getuid?.() ?? info.uid) && (info.mode & 63) === 0;
887
- } catch {
888
- return false;
889
- }
890
- }
891
- async function privateLogDirectory(name, env = process6.env) {
892
- const runtime = env.XDG_RUNTIME_DIR;
893
- const base = runtime === undefined || runtime === "" ? join(homedir(), ".cache", CLI_FOLDER) : join(runtime, CLI_FOLDER);
894
- const path = join(base, name);
895
- try {
896
- await mkdir(path, { recursive: true, mode: 448 });
897
- if (isPrivateDirectory(path))
898
- return path;
899
- } catch {}
900
- return mkdtemp(join(tmpdir(), `${name.replaceAll("/", "-")}-`));
901
- }
902
- async function sweepLogs(dir, maxAgeMs = DAY_MS, now = Date.now()) {
903
- let names;
904
- try {
905
- names = await readdir(dir);
906
- } catch {
907
- return;
908
- }
909
- for (const name of names) {
910
- const path = join(dir, name);
1033
+ if (usage !== false) {
911
1034
  try {
912
- const info = await stat(path);
913
- if (info.isDirectory())
914
- await sweepLogs(path, maxAgeMs, now);
915
- if (now - info.mtimeMs > maxAgeMs)
916
- await rm(path, { force: true, recursive: info.isDirectory() });
917
- } catch {
918
- continue;
919
- }
1035
+ recordUsage(usageEntry(name, undefined, argv, code));
1036
+ } catch {}
920
1037
  }
921
- }
922
- function createLog(path) {
923
- return openSync(path, "wx", 384);
1038
+ return code;
924
1039
  }
925
1040
  // ../kit/src/secrets.ts
926
1041
  var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
@@ -928,9 +1043,6 @@ var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "ses
928
1043
  var {Glob } = globalThis.Bun;
929
1044
  var SESSION_FILES = new Glob("*.json");
930
1045
  var ACCOUNT_DIRS = new Glob("*/sessions");
931
- // package.json
932
- var version = "0.7.0";
933
-
934
1046
  // src/shape.ts
935
1047
  var HEX_LITERAL = /\b0x[0-9a-f]+\b/gi;
936
1048
  var PATH = /(?:\.{1,2}\/|\/)?[\w.@~-]+(?:\/[\w.@~-]+)+/g;
@@ -1111,11 +1223,11 @@ function fold(text, { threshold, logPath }) {
1111
1223
 
1112
1224
  // src/run.ts
1113
1225
  import { closeSync, rmSync, writeSync } from "fs";
1114
- import process10 from "process";
1226
+ import process13 from "process";
1115
1227
 
1116
1228
  // src/logs.ts
1117
- import { join as join2 } from "path";
1118
- import process8 from "process";
1229
+ import { join as join4 } from "path";
1230
+ import process11 from "process";
1119
1231
  var MAX_AGE_MS = 24 * 60 * 60 * 1000;
1120
1232
  async function logDir() {
1121
1233
  return privateLogDirectory("out");
@@ -1124,7 +1236,7 @@ async function cleanLogs(dir, now = Date.now()) {
1124
1236
  await sweepLogs(dir, MAX_AGE_MS, now);
1125
1237
  }
1126
1238
  function newLog(dir) {
1127
- const path = join2(dir, `${new Date().toISOString().replaceAll(/[:.]/g, "-")}-${process8.pid}.log`);
1239
+ const path = join4(dir, `${new Date().toISOString().replaceAll(/[:.]/g, "-")}-${process11.pid}.log`);
1128
1240
  return { path, fd: createLog(path) };
1129
1241
  }
1130
1242
 
@@ -1143,13 +1255,16 @@ var ANSI_ESCAPES = { n: `
1143
1255
  var ANSI_DIGITS = { x: 2, u: 4, U: 8 };
1144
1256
  var OCTAL = /^[0-7]{1,3}/;
1145
1257
  var KEYWORDS = new Set(["then", "do", "else", "if", "elif", "while", "until", "!", "{", "}"]);
1146
- var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh"]);
1147
- var COMMAND_STRING_PROGRAMS = new Set([...SHELLS, "su", "runuser"]);
1258
+ var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh", "fish", "csh", "tcsh", "nu", "xonsh", "elvish", "pwsh", "powershell"]);
1259
+ var SCANNED_PROGRAMS = new Set(["su", "runuser", "flock"]);
1260
+ var COMMAND_STRING_PROGRAMS = new Set([...SHELLS, ...SCANNED_PROGRAMS]);
1261
+ var POWERSHELL = new Set(["pwsh", "powershell"]);
1148
1262
  var SHELL_VALUE_OPTIONS = new Set(["-o", "-O", "--rcfile", "--init-file"]);
1263
+ var FISH_VALUE_OPTIONS = new Set(["-C", "--init-command"]);
1149
1264
  var PRINTERS = new Set(["echo", "printf"]);
1150
1265
  var FIND_EXEC = new Set(["-exec", "-execdir", "-ok", "-okdir"]);
1151
- function wrapper(short = "", long = [], operands = 0) {
1152
- return { short, long, operands };
1266
+ function wrapper(short = "", long = [], operands = 0, runsString = false) {
1267
+ return { short, long, operands, runsString };
1153
1268
  }
1154
1269
  var WRAPPERS = new Map([
1155
1270
  ["command", wrapper()],
@@ -1167,6 +1282,11 @@ var WRAPPERS = new Map([
1167
1282
  ["ionice", wrapper("cnpPu", ["--class", "--classdata", "--pid", "--pgid", "--uid"])],
1168
1283
  ["stdbuf", wrapper("ioe", ["--input", "--output", "--error"])],
1169
1284
  ["chroot", wrapper("", ["--userspec", "--groups"], 1)],
1285
+ ["watch", wrapper("n", ["--interval"], 0, true)],
1286
+ ["flock", wrapper("wE", ["--timeout", "--conflict-exit-code"], 1)],
1287
+ ["parallel", wrapper("jnNSaIPlLdTsC", ["--jobs", "--max-args", "--max-replace-args", "--sshlogin", "--arg-file", "--delimiter", "--colsep"], 0, true)],
1288
+ ["unbuffer", wrapper()],
1289
+ ["systemd-run", wrapper("puEMH", ["--property", "--unit", "--description", "--slice", "--setenv", "--machine", "--host", "--uid", "--gid", "--working-directory", "--nice", "--service-type"])],
1170
1290
  ["xargs", wrapper("IndsEaPL", ["--max-args", "--max-procs", "--max-chars", "--delimiter", "--arg-file", "--eof", "--max-lines", "--process-slot-var"])]
1171
1291
  ]);
1172
1292
  function closerOf(quote) {
@@ -1379,10 +1499,9 @@ function filtered(text, stages) {
1379
1499
  }
1380
1500
 
1381
1501
  // src/stats.ts
1382
- import { appendFileSync, mkdirSync } from "fs";
1383
- import { homedir as homedir2 } from "os";
1384
- import { basename, dirname, join as join3 } from "path";
1385
- import process9 from "process";
1502
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "fs";
1503
+ import { basename, dirname as dirname3, join as join5 } from "path";
1504
+ import process12 from "process";
1386
1505
  var BYTES_PER_TOKEN = 4;
1387
1506
  var KEY_WORD = /^[a-z][a-z:_-]*$/;
1388
1507
  var KEY_WORDS = 3;
@@ -1391,8 +1510,8 @@ var ASSIGNMENT = /^\w+=/;
1391
1510
  var CHAIN = /&&|\|\||;/;
1392
1511
  var SHELLS2 = new Set(["sh", "bash", "zsh"]);
1393
1512
  var TOP = 10;
1394
- function statsPath(env = process9.env) {
1395
- return join3(env.XDG_STATE_HOME ?? join3(homedir2(), ".local", "state"), CLI_FOLDER, "out", "stats.jsonl");
1513
+ function statsPath(env = process12.env) {
1514
+ return join5(stateHome(env), CLI_FOLDER, "out", "stats.jsonl");
1396
1515
  }
1397
1516
  function commandKey(command) {
1398
1517
  const [program = "", ...rest] = command.slice(Math.max(0, command.findIndex((word) => !ASSIGNMENT.test(word))));
@@ -1410,8 +1529,8 @@ function commandKey(command) {
1410
1529
  }
1411
1530
  function record(entry, path = statsPath()) {
1412
1531
  try {
1413
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
1414
- appendFileSync(path, `${JSON.stringify(entry)}
1532
+ mkdirSync2(dirname3(path), { recursive: true, mode: 448 });
1533
+ appendFileSync2(path, `${JSON.stringify(entry)}
1415
1534
  `, { mode: 384 });
1416
1535
  } catch {}
1417
1536
  }
@@ -1481,6 +1600,7 @@ class Recorder {
1481
1600
  tailBytes = 0;
1482
1601
  dropped = 0;
1483
1602
  log;
1603
+ logFailed = false;
1484
1604
  constructor(dir) {
1485
1605
  this.dir = dir;
1486
1606
  }
@@ -1490,7 +1610,7 @@ class Recorder {
1490
1610
  this.headBytes += chunk.data.length;
1491
1611
  return;
1492
1612
  }
1493
- writeSync(this.startLog().fd, chunk.data);
1613
+ this.writeToLog(chunk.data);
1494
1614
  this.tail.push(chunk);
1495
1615
  this.tailBytes += chunk.data.length;
1496
1616
  while (this.tail.length > 1 && this.tailBytes - (this.tail[0]?.data.length ?? 0) >= TAIL_BYTES) {
@@ -1499,6 +1619,16 @@ class Recorder {
1499
1619
  this.dropped += gone?.data.length ?? 0;
1500
1620
  }
1501
1621
  }
1622
+ writeToLog(data) {
1623
+ if (this.logFailed)
1624
+ return;
1625
+ try {
1626
+ writeSync(this.startLog().fd, data);
1627
+ } catch (error) {
1628
+ this.logFailed = true;
1629
+ console.error(`out: cannot write the log, the output past ${HEAD_BYTES} bytes is kept in memory only: ${error instanceof Error ? error.message : String(error)}`);
1630
+ }
1631
+ }
1502
1632
  startLog() {
1503
1633
  if (this.log !== undefined)
1504
1634
  return this.log;
@@ -1531,17 +1661,17 @@ class Recorder {
1531
1661
  if (this.dropped === 0)
1532
1662
  return decode(this.chunks);
1533
1663
  return `${decode(this.head)}
1534
- [out: ${this.dropped} bytes left out of memory, all of it is in the log]
1664
+ [out: ${this.dropped} bytes left out of memory${this.logFailed ? "" : ", all of it is in the log"}]
1535
1665
  ${decode(this.tail)}`;
1536
1666
  }
1537
1667
  }
1538
1668
  function replay(chunks) {
1539
1669
  for (const { stream, data } of chunks)
1540
- process10[stream].write(data);
1670
+ process13[stream].write(data);
1541
1671
  }
1542
1672
  function count(recorder, shown, recorded) {
1543
1673
  const baseline = recorded.was.length === 0 || recorder.overflowing ? undefined : filtered(recorder.text, recorded.was);
1544
- record({ at: new Date().toISOString(), dir: process10.cwd(), command: recorded.command, bytes: recorder.bytes, baseline: baseline === undefined ? recorder.bytes : Buffer.byteLength(baseline), shown, folded: shown !== recorder.bytes });
1674
+ record({ at: new Date().toISOString(), dir: process13.cwd(), command: recorded.command, bytes: recorder.bytes, baseline: baseline === undefined ? recorder.bytes : Buffer.byteLength(baseline), shown, folded: shown !== recorder.bytes });
1545
1675
  }
1546
1676
  async function run(command, threshold, was = []) {
1547
1677
  const dir = await logDir();
@@ -1566,7 +1696,7 @@ async function run(command, threshold, was = []) {
1566
1696
  } catch (error) {
1567
1697
  if (!started) {
1568
1698
  console.error(`out: cannot run ${command[0]}: ${error instanceof Error ? error.message : String(error)}`);
1569
- return 127;
1699
+ return error.code === "EACCES" ? 126 : 127;
1570
1700
  }
1571
1701
  console.error(`out: ${error instanceof Error ? error.message : String(error)}`);
1572
1702
  code = 1;
@@ -1605,20 +1735,80 @@ function present(recorder, threshold) {
1605
1735
  replay(recorder.chunks);
1606
1736
  return recorder.bytes;
1607
1737
  }
1608
- process10.stdout.write(printed);
1738
+ process13.stdout.write(printed);
1609
1739
  return Buffer.byteLength(printed);
1610
1740
  }
1611
1741
 
1742
+ // src/saved.ts
1743
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
1744
+ import { basename as basename2, join as join6 } from "path";
1745
+ async function savedLogs() {
1746
+ const dir = await logDir();
1747
+ const logs = [];
1748
+ for (const name of await readdir2(dir)) {
1749
+ const path = join6(dir, name);
1750
+ try {
1751
+ const info = await stat2(path);
1752
+ if (info.isFile() && name.endsWith(".log"))
1753
+ logs.push({ path, size: info.size, mtimeMs: info.mtimeMs });
1754
+ } catch {
1755
+ continue;
1756
+ }
1757
+ }
1758
+ return logs.toSorted((a, b) => b.mtimeMs - a.mtimeMs || b.path.localeCompare(a.path));
1759
+ }
1760
+ async function pickLog(name) {
1761
+ const logs = await savedLogs();
1762
+ const found = name === undefined ? logs[0] : logs.find((log) => log.path === name || basename2(log.path) === name);
1763
+ if (found === undefined)
1764
+ fail(name === undefined ? "no saved log (a run saves one only when its output is folded, and logs older than a day are removed)" : `${name} is not a saved log, \`out --list\` names them`);
1765
+ return found;
1766
+ }
1767
+ async function printLast() {
1768
+ console.log((await pickLog(undefined)).path);
1769
+ return 0;
1770
+ }
1771
+ async function printList() {
1772
+ const logs = await savedLogs();
1773
+ if (logs.length === 0)
1774
+ fail("no saved log", 1);
1775
+ console.log(table(logs.map((log) => [log.path, `${Math.ceil(log.size / 1024)} KiB`, formatAge(log.mtimeMs)])));
1776
+ return 0;
1777
+ }
1778
+ async function grepLog(source, name) {
1779
+ let pattern;
1780
+ try {
1781
+ pattern = new RegExp(source);
1782
+ } catch (error) {
1783
+ return badUsage(`--grep is no valid regular expression: ${error instanceof Error ? error.message : String(error)}`);
1784
+ }
1785
+ const lines = (await Bun.file((await pickLog(name)).path).text()).split(`
1786
+ `);
1787
+ const hits = lines.flatMap((line, i) => pattern.test(line) ? [`${i + 1}: ${line}`] : []);
1788
+ if (hits.length === 0)
1789
+ fail(`no line of the log matches ${source}`, 1);
1790
+ console.log(hits.join(`
1791
+ `));
1792
+ return 0;
1793
+ }
1794
+
1612
1795
  // src/cli.ts
1613
1796
  var USAGE = `out [-n <lines>] -- <command> [args...]: runs the command and shortens its output when it is long, exiting with its code
1614
1797
  <command> | out [-n <lines>]: shortens the output piped in
1615
1798
  out --stats [--since 7d] [--json]: the runs recorded and about how many tokens they saved
1799
+ out --last: the path of the most recent full log
1800
+ out --list: the saved logs, the newest first
1801
+ out --grep <regex> [--log <name>]: the lines of a saved log (the last by default) matching the regex, with their line numbers
1616
1802
 
1617
- Each run is recorded in ~/.local/state/kaynooo-cli/out/stats.jsonl with the words naming the command, never its arguments; tokens are counted as bytes / 4.
1618
- Exits 0 when the command does, with its own code otherwise, 127 when it cannot be started, 2 on a usage error. Piped into, it exits 0.`;
1803
+ Each run is recorded in ~/.local/state/kaynooo-cli/out/stats.jsonl with the words naming the command, never its arguments; tokens are counted as bytes / 4.`;
1804
+ var EXITS = [[0, "the command did, or the output piped in was shortened, or the log was found"], ["its own", "the code of a command that failed"], [1, "--grep matched nothing, or --list found no log"], [126, "the command cannot be run (not executable)"], [127, "the command cannot be started"], [2, "a usage error, or no such saved log"]];
1619
1805
  var OPTIONS = {
1620
1806
  lines: { type: "string", short: "n", description: "longest output left untouched (default 150)" },
1621
1807
  was: { type: "string", multiple: true, description: "a tail, head or grep stage out stands in for, so the stats count against what it would have printed (repeatable, in pipeline order)" },
1808
+ last: { type: "boolean", description: "print the path of the most recent full log" },
1809
+ list: { type: "boolean", description: "list the saved logs, the newest first" },
1810
+ grep: { type: "string", description: "print the lines of a saved log matching this regular expression, with their line numbers" },
1811
+ log: { type: "string", description: "with --grep, the saved log to read (its name or path) instead of the last" },
1622
1812
  stats: { type: "boolean", description: "print the runs recorded and the tokens saved" },
1623
1813
  since: { type: "string", description: "with --stats, only the runs this recent (7d, 12h)" },
1624
1814
  json: jsonOption("with --stats, {runs, folded, bytes, shown, tokensSaved, commands}")
@@ -1634,8 +1824,21 @@ async function stats(since, json) {
1634
1824
  }
1635
1825
  async function main(argv) {
1636
1826
  const bare = argv[0] !== undefined && !argv[0].startsWith("-");
1637
- const parsed = parse(bare ? [] : argv, USAGE, OPTIONS, { rest: true, replaces: "piping a noisy command through tail or head", examples: ["out -- bun install", ["out -n 300 -- bun run build", "up to 300 lines left untouched"], ["bun run build 2>&1 | out", "what is piped in"], ["out --stats --since 7d", "the tokens saved this week"]] });
1827
+ const parsed = parse(bare ? [] : argv, USAGE, OPTIONS, { rest: true, exits: EXITS, replaces: "piping a noisy command through tail or head", examples: ["out -- bun install", ["out -n 300 -- bun run build", "up to 300 lines left untouched"], ["bun run build 2>&1 | out", "what is piped in"], ["out --stats --since 7d", "the tokens saved this week"]] });
1638
1828
  const { values } = parsed;
1829
+ const reads = [values.last === true, values.list === true, values.grep !== undefined, values.stats === true].filter(Boolean).length;
1830
+ if (reads > 1)
1831
+ badUsage("give one of --last, --list, --grep and --stats");
1832
+ if (reads === 1 && ((parsed.rest ?? []).length > 0 || parsed.positionals.length > 0))
1833
+ badUsage("--last, --list, --grep and --stats run no command");
1834
+ if (values.log !== undefined && values.grep === undefined)
1835
+ badUsage("--log goes with --grep");
1836
+ if (values.last === true)
1837
+ return printLast();
1838
+ if (values.list === true)
1839
+ return printList();
1840
+ if (values.grep !== undefined)
1841
+ return grepLog(values.grep, values.log);
1639
1842
  if (values.stats === true) {
1640
1843
  if ((parsed.rest ?? []).length > 0 || parsed.positionals.length > 0)
1641
1844
  badUsage("--stats runs no command");
@@ -1654,4 +1857,4 @@ async function main(argv) {
1654
1857
  ${USAGE}`);
1655
1858
  return run(command, lines, was);
1656
1859
  }
1657
- process11.exitCode = await runMain({ name: "out", version, run: main });
1860
+ process14.exitCode = await runMain({ name: "out", run: main });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaynooo/cli-out",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "description": "out: shortens noisy command output for Claude Code, folding long logs and keeping errors, with the full log saved to a file.",
6
6
  "author": "Kaynooo <contact@kaynooo.fr>",
7
7
  "license": "MIT",
@@ -28,6 +28,6 @@
28
28
  "out": "$npm_execpath src/cli.ts"
29
29
  },
30
30
  "devDependencies": {
31
- "@kaynooo/cli-kit": "0.18.0"
31
+ "@kaynooo/cli-kit": "0.21.0"
32
32
  }
33
33
  }