@kaynooo/cli-out 0.7.1 → 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.
- package/README.md +10 -4
- package/dist/cli.js +212 -67
- 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
|
|
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
|
|
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
|
|
37
|
+
## Wrapping from guard
|
|
32
38
|
|
|
33
|
-
The pure functions
|
|
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,17 +2,16 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
import process14 from "process";
|
|
7
6
|
// ../kit/src/args.ts
|
|
8
|
-
import
|
|
7
|
+
import process5 from "process";
|
|
9
8
|
import { parseArgs } from "util";
|
|
10
9
|
|
|
11
10
|
// ../kit/src/format.ts
|
|
12
|
-
import
|
|
11
|
+
import process3 from "process";
|
|
13
12
|
|
|
14
13
|
// ../kit/src/style.ts
|
|
15
|
-
import
|
|
14
|
+
import process2 from "process";
|
|
16
15
|
|
|
17
16
|
// ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
|
|
18
17
|
function x(s) {
|
|
@@ -276,10 +275,10 @@ function le(i = {}) {
|
|
|
276
275
|
var t = le();
|
|
277
276
|
|
|
278
277
|
// ../kit/src/style.ts
|
|
279
|
-
function takesControlCodes(stream, env =
|
|
278
|
+
function takesControlCodes(stream, env = process2.env) {
|
|
280
279
|
return stream.isTTY === true && env.NO_COLOR === undefined && env.NODE_DISABLE_COLORS === undefined && env.TERM !== "dumb";
|
|
281
280
|
}
|
|
282
|
-
function paint(stream, env =
|
|
281
|
+
function paint(stream, env = process2.env) {
|
|
283
282
|
return le({ stream: { isTTY: takesControlCodes(stream, env) }, env });
|
|
284
283
|
}
|
|
285
284
|
var STATUSES = {
|
|
@@ -292,10 +291,31 @@ var STATUSES = {
|
|
|
292
291
|
var STATUS_OF = new Map(Object.entries(STATUSES).flatMap(([status, { words }]) => words.map((word) => [word, status])));
|
|
293
292
|
|
|
294
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
|
+
}
|
|
295
308
|
function upTo(text, separator) {
|
|
296
309
|
const end = text.indexOf(separator);
|
|
297
310
|
return end === -1 ? text : text.slice(0, end);
|
|
298
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
|
+
}
|
|
299
319
|
function truncate(text, width) {
|
|
300
320
|
return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
301
321
|
}
|
|
@@ -312,7 +332,7 @@ function table(rows, { cellWidth, header = false, align = [], fit } = {}) {
|
|
|
312
332
|
widths[index] = Math.max(widths[index] ?? 0, textWidth(cell));
|
|
313
333
|
}
|
|
314
334
|
const shown = fit === undefined ? cells : narrow(cells, widths, fit);
|
|
315
|
-
const colors = paint(
|
|
335
|
+
const colors = paint(process3.stdout);
|
|
316
336
|
const lines = shown.map((row) => row.map((cell, index) => {
|
|
317
337
|
const width = widths[index] ?? 0;
|
|
318
338
|
const gap = " ".repeat(Math.max(0, width - textWidth(cell)));
|
|
@@ -344,24 +364,30 @@ function printJson(value) {
|
|
|
344
364
|
}
|
|
345
365
|
|
|
346
366
|
// ../kit/src/help.ts
|
|
347
|
-
import
|
|
367
|
+
import process4 from "process";
|
|
348
368
|
var OPTIONAL = /\[[^\]]+\]/g;
|
|
349
369
|
var USAGE_PREFIX = /^usage:\s*/i;
|
|
350
370
|
var FULL_STOP = /\.$/;
|
|
351
|
-
function usageLine(usage, colors = paint(
|
|
371
|
+
function usageLine(usage, colors = paint(process4.stdout)) {
|
|
352
372
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
353
373
|
}
|
|
354
|
-
function helpText(usage, options, colors = paint(
|
|
355
|
-
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
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(`
|
|
356
376
|
`);
|
|
357
377
|
}
|
|
358
|
-
function
|
|
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)) {
|
|
359
385
|
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
360
386
|
}
|
|
361
|
-
function wantsHelpJson(env =
|
|
387
|
+
function wantsHelpJson(env = process4.env) {
|
|
362
388
|
return env.KIT_HELP === "json";
|
|
363
389
|
}
|
|
364
|
-
function description(usage, { examples = [], replaces } = {}) {
|
|
390
|
+
function description(usage, { examples = [], replaces, exits } = {}, options) {
|
|
365
391
|
const lines = usage.replace(USAGE_PREFIX, "").split(`
|
|
366
392
|
`);
|
|
367
393
|
const first = lines[0];
|
|
@@ -372,15 +398,17 @@ function description(usage, { examples = [], replaces } = {}) {
|
|
|
372
398
|
synopsis: colon === -1 ? first : first.slice(0, colon),
|
|
373
399
|
summary: colon === -1 ? fallback : first.slice(colon + 2),
|
|
374
400
|
...replaces === undefined ? {} : { replaces },
|
|
375
|
-
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
|
|
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 } })) }
|
|
376
404
|
};
|
|
377
405
|
}
|
|
378
|
-
function exampleRows(examples, colors = paint(
|
|
406
|
+
function exampleRows(examples, colors = paint(process4.stdout)) {
|
|
379
407
|
if (examples.length === 0)
|
|
380
408
|
return [];
|
|
381
409
|
return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
|
|
382
410
|
}
|
|
383
|
-
function optionRows(options, colors = paint(
|
|
411
|
+
function optionRows(options, colors = paint(process4.stdout)) {
|
|
384
412
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
385
413
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
386
414
|
const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
|
|
@@ -490,7 +518,7 @@ var OUTPUT_OPTIONS = {
|
|
|
490
518
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
491
519
|
plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
|
|
492
520
|
};
|
|
493
|
-
function applyOutput(color, plain, env =
|
|
521
|
+
function applyOutput(color, plain, env = process5.env) {
|
|
494
522
|
const when = plain === true ? "never" : color ?? "auto";
|
|
495
523
|
if (when === "auto")
|
|
496
524
|
return;
|
|
@@ -513,7 +541,7 @@ function parse(args, usage, options, settings = {}) {
|
|
|
513
541
|
const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
|
|
514
542
|
let parsed;
|
|
515
543
|
try {
|
|
516
|
-
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 });
|
|
517
545
|
} catch (error) {
|
|
518
546
|
const message = parseError(error);
|
|
519
547
|
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
@@ -521,7 +549,7 @@ function parse(args, usage, options, settings = {}) {
|
|
|
521
549
|
const values = parsed.values;
|
|
522
550
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
523
551
|
if (values.help === true) {
|
|
524
|
-
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));
|
|
525
553
|
throw new HelpShown;
|
|
526
554
|
}
|
|
527
555
|
checkChoices(parsed.values, all);
|
|
@@ -563,6 +591,18 @@ function listed(options, allowNegative) {
|
|
|
563
591
|
return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
|
|
564
592
|
}));
|
|
565
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
|
+
}
|
|
566
606
|
function withBare(args, options) {
|
|
567
607
|
const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
|
|
568
608
|
if (bare.size === 0)
|
|
@@ -649,7 +689,7 @@ function nearestOption(message, options, allowNegative) {
|
|
|
649
689
|
}
|
|
650
690
|
// ../kit/src/process.ts
|
|
651
691
|
import { constants } from "os";
|
|
652
|
-
import
|
|
692
|
+
import process6 from "process";
|
|
653
693
|
var KILL_GRACE_MS = 2000;
|
|
654
694
|
var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
655
695
|
function exitCodeOf(code, signal) {
|
|
@@ -671,7 +711,7 @@ function holdSignals(cleanup) {
|
|
|
671
711
|
},
|
|
672
712
|
release: () => {
|
|
673
713
|
for (const signal of SIGNALS)
|
|
674
|
-
|
|
714
|
+
process6.off(signal, onSignal);
|
|
675
715
|
}
|
|
676
716
|
};
|
|
677
717
|
function onSignal(signal) {
|
|
@@ -684,10 +724,10 @@ function holdSignals(cleanup) {
|
|
|
684
724
|
return;
|
|
685
725
|
held.release();
|
|
686
726
|
cleanup(signal);
|
|
687
|
-
|
|
727
|
+
process6.kill(process6.pid, signal);
|
|
688
728
|
}
|
|
689
729
|
for (const signal of SIGNALS)
|
|
690
|
-
|
|
730
|
+
process6.on(signal, onSignal);
|
|
691
731
|
return held;
|
|
692
732
|
}
|
|
693
733
|
async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal, maxBytes = Infinity, onChunk, onStart } = {}) {
|
|
@@ -701,7 +741,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal,
|
|
|
701
741
|
return;
|
|
702
742
|
try {
|
|
703
743
|
if (grouped)
|
|
704
|
-
|
|
744
|
+
process6.kill(-proc.pid, killing);
|
|
705
745
|
else
|
|
706
746
|
proc.kill(killing);
|
|
707
747
|
} catch {}
|
|
@@ -775,31 +815,33 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
|
|
|
775
815
|
var RESET_STYLE = `${ESC}[0m`;
|
|
776
816
|
var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
|
|
777
817
|
// ../kit/src/cli.ts
|
|
778
|
-
import
|
|
818
|
+
import { readFileSync, realpathSync } from "fs";
|
|
819
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
820
|
+
import process9 from "process";
|
|
779
821
|
|
|
780
822
|
// ../kit/src/usage.ts
|
|
781
823
|
import { appendFileSync, mkdirSync } from "fs";
|
|
782
824
|
import { homedir as homedir2 } from "os";
|
|
783
825
|
import { dirname, isAbsolute, join as join2 } from "path";
|
|
784
|
-
import
|
|
826
|
+
import process8 from "process";
|
|
785
827
|
|
|
786
828
|
// ../kit/src/logs.ts
|
|
787
829
|
import { lstatSync, openSync } from "fs";
|
|
788
830
|
import { mkdir, mkdtemp, readdir, rm, stat } from "fs/promises";
|
|
789
831
|
import { homedir, tmpdir } from "os";
|
|
790
832
|
import { join } from "path";
|
|
791
|
-
import
|
|
833
|
+
import process7 from "process";
|
|
792
834
|
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
793
835
|
var CLI_FOLDER = "kaynooo-cli";
|
|
794
836
|
function isPrivateDirectory(path) {
|
|
795
837
|
try {
|
|
796
838
|
const info = lstatSync(path, { throwIfNoEntry: false });
|
|
797
|
-
return info !== undefined && info.isDirectory() && info.uid === (
|
|
839
|
+
return info !== undefined && info.isDirectory() && info.uid === (process7.getuid?.() ?? info.uid) && (info.mode & 63) === 0;
|
|
798
840
|
} catch {
|
|
799
841
|
return false;
|
|
800
842
|
}
|
|
801
843
|
}
|
|
802
|
-
async function privateLogDirectory(name, env =
|
|
844
|
+
async function privateLogDirectory(name, env = process7.env) {
|
|
803
845
|
const runtime = env.XDG_RUNTIME_DIR;
|
|
804
846
|
const base = runtime === undefined || runtime === "" ? join(homedir(), ".cache", CLI_FOLDER) : join(runtime, CLI_FOLDER);
|
|
805
847
|
const path = join(base, name);
|
|
@@ -842,14 +884,14 @@ var usageError;
|
|
|
842
884
|
function noteUsageError(message) {
|
|
843
885
|
usageError = message;
|
|
844
886
|
}
|
|
845
|
-
function stateHome(env =
|
|
887
|
+
function stateHome(env = process8.env) {
|
|
846
888
|
const dir = env.XDG_STATE_HOME;
|
|
847
889
|
return dir !== undefined && isAbsolute(dir) ? dir : join2(homedir2(), ".local", "state");
|
|
848
890
|
}
|
|
849
|
-
function usagePath(env =
|
|
891
|
+
function usagePath(env = process8.env) {
|
|
850
892
|
return join2(stateHome(env), CLI_FOLDER, "usage.jsonl");
|
|
851
893
|
}
|
|
852
|
-
function usageRecorded(env =
|
|
894
|
+
function usageRecorded(env = process8.env) {
|
|
853
895
|
const off = env.KAYNOOO_CLI_NO_USAGE;
|
|
854
896
|
return env.NODE_ENV !== "test" && (off === undefined || off === "" || off === "0");
|
|
855
897
|
}
|
|
@@ -863,7 +905,7 @@ function usageEntry(bin, command, argv, code, now = new Date) {
|
|
|
863
905
|
usageError = undefined;
|
|
864
906
|
return { at: now.toISOString(), bin, ...command === undefined ? {} : { command }, flags: flagNames(argv), code, ...unknown === undefined ? {} : { unknown } };
|
|
865
907
|
}
|
|
866
|
-
function recordUsage(entry, env =
|
|
908
|
+
function recordUsage(entry, env = process8.env) {
|
|
867
909
|
if (!usageRecorded(env))
|
|
868
910
|
return;
|
|
869
911
|
try {
|
|
@@ -881,8 +923,8 @@ var LEVEL_OPTIONS = {
|
|
|
881
923
|
};
|
|
882
924
|
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
883
925
|
function printError(prefix, message, hint, cause) {
|
|
884
|
-
const colors = paint(
|
|
885
|
-
const styled = takesControlCodes(
|
|
926
|
+
const colors = paint(process9.stderr);
|
|
927
|
+
const styled = takesControlCodes(process9.stderr);
|
|
886
928
|
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
887
929
|
const under = (label, text, tint) => text === undefined ? "" : `
|
|
888
930
|
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
@@ -964,11 +1006,22 @@ async function printCandidates(found) {
|
|
|
964
1006
|
console.log(line);
|
|
965
1007
|
return 0;
|
|
966
1008
|
}
|
|
967
|
-
|
|
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)) {
|
|
968
1021
|
if (argv[0] === COMPLETE)
|
|
969
1022
|
return printCandidates(leafCandidates(run, argv.slice(1)));
|
|
970
1023
|
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
|
|
971
|
-
console.log(version);
|
|
1024
|
+
console.log(version ?? packageVersion());
|
|
972
1025
|
return 0;
|
|
973
1026
|
}
|
|
974
1027
|
let code;
|
|
@@ -977,8 +1030,11 @@ async function runMain({ name, version, run, redact, usage }, argv = process8.ar
|
|
|
977
1030
|
} catch (error) {
|
|
978
1031
|
code = reportThrown(name, error, redact, wantsJson(argv));
|
|
979
1032
|
}
|
|
980
|
-
if (usage !== false)
|
|
981
|
-
|
|
1033
|
+
if (usage !== false) {
|
|
1034
|
+
try {
|
|
1035
|
+
recordUsage(usageEntry(name, undefined, argv, code));
|
|
1036
|
+
} catch {}
|
|
1037
|
+
}
|
|
982
1038
|
return code;
|
|
983
1039
|
}
|
|
984
1040
|
// ../kit/src/secrets.ts
|
|
@@ -987,9 +1043,6 @@ var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "ses
|
|
|
987
1043
|
var {Glob } = globalThis.Bun;
|
|
988
1044
|
var SESSION_FILES = new Glob("*.json");
|
|
989
1045
|
var ACCOUNT_DIRS = new Glob("*/sessions");
|
|
990
|
-
// package.json
|
|
991
|
-
var version = "0.7.1";
|
|
992
|
-
|
|
993
1046
|
// src/shape.ts
|
|
994
1047
|
var HEX_LITERAL = /\b0x[0-9a-f]+\b/gi;
|
|
995
1048
|
var PATH = /(?:\.{1,2}\/|\/)?[\w.@~-]+(?:\/[\w.@~-]+)+/g;
|
|
@@ -1170,11 +1223,11 @@ function fold(text, { threshold, logPath }) {
|
|
|
1170
1223
|
|
|
1171
1224
|
// src/run.ts
|
|
1172
1225
|
import { closeSync, rmSync, writeSync } from "fs";
|
|
1173
|
-
import
|
|
1226
|
+
import process13 from "process";
|
|
1174
1227
|
|
|
1175
1228
|
// src/logs.ts
|
|
1176
|
-
import { join as
|
|
1177
|
-
import
|
|
1229
|
+
import { join as join4 } from "path";
|
|
1230
|
+
import process11 from "process";
|
|
1178
1231
|
var MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
1179
1232
|
async function logDir() {
|
|
1180
1233
|
return privateLogDirectory("out");
|
|
@@ -1183,7 +1236,7 @@ async function cleanLogs(dir, now = Date.now()) {
|
|
|
1183
1236
|
await sweepLogs(dir, MAX_AGE_MS, now);
|
|
1184
1237
|
}
|
|
1185
1238
|
function newLog(dir) {
|
|
1186
|
-
const path =
|
|
1239
|
+
const path = join4(dir, `${new Date().toISOString().replaceAll(/[:.]/g, "-")}-${process11.pid}.log`);
|
|
1187
1240
|
return { path, fd: createLog(path) };
|
|
1188
1241
|
}
|
|
1189
1242
|
|
|
@@ -1202,13 +1255,16 @@ var ANSI_ESCAPES = { n: `
|
|
|
1202
1255
|
var ANSI_DIGITS = { x: 2, u: 4, U: 8 };
|
|
1203
1256
|
var OCTAL = /^[0-7]{1,3}/;
|
|
1204
1257
|
var KEYWORDS = new Set(["then", "do", "else", "if", "elif", "while", "until", "!", "{", "}"]);
|
|
1205
|
-
var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh"]);
|
|
1206
|
-
var
|
|
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"]);
|
|
1207
1262
|
var SHELL_VALUE_OPTIONS = new Set(["-o", "-O", "--rcfile", "--init-file"]);
|
|
1263
|
+
var FISH_VALUE_OPTIONS = new Set(["-C", "--init-command"]);
|
|
1208
1264
|
var PRINTERS = new Set(["echo", "printf"]);
|
|
1209
1265
|
var FIND_EXEC = new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
1210
|
-
function wrapper(short = "", long = [], operands = 0) {
|
|
1211
|
-
return { short, long, operands };
|
|
1266
|
+
function wrapper(short = "", long = [], operands = 0, runsString = false) {
|
|
1267
|
+
return { short, long, operands, runsString };
|
|
1212
1268
|
}
|
|
1213
1269
|
var WRAPPERS = new Map([
|
|
1214
1270
|
["command", wrapper()],
|
|
@@ -1226,6 +1282,11 @@ var WRAPPERS = new Map([
|
|
|
1226
1282
|
["ionice", wrapper("cnpPu", ["--class", "--classdata", "--pid", "--pgid", "--uid"])],
|
|
1227
1283
|
["stdbuf", wrapper("ioe", ["--input", "--output", "--error"])],
|
|
1228
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"])],
|
|
1229
1290
|
["xargs", wrapper("IndsEaPL", ["--max-args", "--max-procs", "--max-chars", "--delimiter", "--arg-file", "--eof", "--max-lines", "--process-slot-var"])]
|
|
1230
1291
|
]);
|
|
1231
1292
|
function closerOf(quote) {
|
|
@@ -1439,8 +1500,8 @@ function filtered(text, stages) {
|
|
|
1439
1500
|
|
|
1440
1501
|
// src/stats.ts
|
|
1441
1502
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1442
|
-
import { basename, dirname as
|
|
1443
|
-
import
|
|
1503
|
+
import { basename, dirname as dirname3, join as join5 } from "path";
|
|
1504
|
+
import process12 from "process";
|
|
1444
1505
|
var BYTES_PER_TOKEN = 4;
|
|
1445
1506
|
var KEY_WORD = /^[a-z][a-z:_-]*$/;
|
|
1446
1507
|
var KEY_WORDS = 3;
|
|
@@ -1449,8 +1510,8 @@ var ASSIGNMENT = /^\w+=/;
|
|
|
1449
1510
|
var CHAIN = /&&|\|\||;/;
|
|
1450
1511
|
var SHELLS2 = new Set(["sh", "bash", "zsh"]);
|
|
1451
1512
|
var TOP = 10;
|
|
1452
|
-
function statsPath(env =
|
|
1453
|
-
return
|
|
1513
|
+
function statsPath(env = process12.env) {
|
|
1514
|
+
return join5(stateHome(env), CLI_FOLDER, "out", "stats.jsonl");
|
|
1454
1515
|
}
|
|
1455
1516
|
function commandKey(command) {
|
|
1456
1517
|
const [program = "", ...rest] = command.slice(Math.max(0, command.findIndex((word) => !ASSIGNMENT.test(word))));
|
|
@@ -1468,7 +1529,7 @@ function commandKey(command) {
|
|
|
1468
1529
|
}
|
|
1469
1530
|
function record(entry, path = statsPath()) {
|
|
1470
1531
|
try {
|
|
1471
|
-
mkdirSync2(
|
|
1532
|
+
mkdirSync2(dirname3(path), { recursive: true, mode: 448 });
|
|
1472
1533
|
appendFileSync2(path, `${JSON.stringify(entry)}
|
|
1473
1534
|
`, { mode: 384 });
|
|
1474
1535
|
} catch {}
|
|
@@ -1539,6 +1600,7 @@ class Recorder {
|
|
|
1539
1600
|
tailBytes = 0;
|
|
1540
1601
|
dropped = 0;
|
|
1541
1602
|
log;
|
|
1603
|
+
logFailed = false;
|
|
1542
1604
|
constructor(dir) {
|
|
1543
1605
|
this.dir = dir;
|
|
1544
1606
|
}
|
|
@@ -1548,7 +1610,7 @@ class Recorder {
|
|
|
1548
1610
|
this.headBytes += chunk.data.length;
|
|
1549
1611
|
return;
|
|
1550
1612
|
}
|
|
1551
|
-
|
|
1613
|
+
this.writeToLog(chunk.data);
|
|
1552
1614
|
this.tail.push(chunk);
|
|
1553
1615
|
this.tailBytes += chunk.data.length;
|
|
1554
1616
|
while (this.tail.length > 1 && this.tailBytes - (this.tail[0]?.data.length ?? 0) >= TAIL_BYTES) {
|
|
@@ -1557,6 +1619,16 @@ class Recorder {
|
|
|
1557
1619
|
this.dropped += gone?.data.length ?? 0;
|
|
1558
1620
|
}
|
|
1559
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
|
+
}
|
|
1560
1632
|
startLog() {
|
|
1561
1633
|
if (this.log !== undefined)
|
|
1562
1634
|
return this.log;
|
|
@@ -1589,17 +1661,17 @@ class Recorder {
|
|
|
1589
1661
|
if (this.dropped === 0)
|
|
1590
1662
|
return decode(this.chunks);
|
|
1591
1663
|
return `${decode(this.head)}
|
|
1592
|
-
[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"}]
|
|
1593
1665
|
${decode(this.tail)}`;
|
|
1594
1666
|
}
|
|
1595
1667
|
}
|
|
1596
1668
|
function replay(chunks) {
|
|
1597
1669
|
for (const { stream, data } of chunks)
|
|
1598
|
-
|
|
1670
|
+
process13[stream].write(data);
|
|
1599
1671
|
}
|
|
1600
1672
|
function count(recorder, shown, recorded) {
|
|
1601
1673
|
const baseline = recorded.was.length === 0 || recorder.overflowing ? undefined : filtered(recorder.text, recorded.was);
|
|
1602
|
-
record({ at: new Date().toISOString(), dir:
|
|
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 });
|
|
1603
1675
|
}
|
|
1604
1676
|
async function run(command, threshold, was = []) {
|
|
1605
1677
|
const dir = await logDir();
|
|
@@ -1624,7 +1696,7 @@ async function run(command, threshold, was = []) {
|
|
|
1624
1696
|
} catch (error) {
|
|
1625
1697
|
if (!started) {
|
|
1626
1698
|
console.error(`out: cannot run ${command[0]}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1627
|
-
return 127;
|
|
1699
|
+
return error.code === "EACCES" ? 126 : 127;
|
|
1628
1700
|
}
|
|
1629
1701
|
console.error(`out: ${error instanceof Error ? error.message : String(error)}`);
|
|
1630
1702
|
code = 1;
|
|
@@ -1663,20 +1735,80 @@ function present(recorder, threshold) {
|
|
|
1663
1735
|
replay(recorder.chunks);
|
|
1664
1736
|
return recorder.bytes;
|
|
1665
1737
|
}
|
|
1666
|
-
|
|
1738
|
+
process13.stdout.write(printed);
|
|
1667
1739
|
return Buffer.byteLength(printed);
|
|
1668
1740
|
}
|
|
1669
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
|
+
|
|
1670
1795
|
// src/cli.ts
|
|
1671
1796
|
var USAGE = `out [-n <lines>] -- <command> [args...]: runs the command and shortens its output when it is long, exiting with its code
|
|
1672
1797
|
<command> | out [-n <lines>]: shortens the output piped in
|
|
1673
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
|
|
1674
1802
|
|
|
1675
|
-
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
|
|
1676
|
-
|
|
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"]];
|
|
1677
1805
|
var OPTIONS = {
|
|
1678
1806
|
lines: { type: "string", short: "n", description: "longest output left untouched (default 150)" },
|
|
1679
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" },
|
|
1680
1812
|
stats: { type: "boolean", description: "print the runs recorded and the tokens saved" },
|
|
1681
1813
|
since: { type: "string", description: "with --stats, only the runs this recent (7d, 12h)" },
|
|
1682
1814
|
json: jsonOption("with --stats, {runs, folded, bytes, shown, tokensSaved, commands}")
|
|
@@ -1692,8 +1824,21 @@ async function stats(since, json) {
|
|
|
1692
1824
|
}
|
|
1693
1825
|
async function main(argv) {
|
|
1694
1826
|
const bare = argv[0] !== undefined && !argv[0].startsWith("-");
|
|
1695
|
-
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"]] });
|
|
1696
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);
|
|
1697
1842
|
if (values.stats === true) {
|
|
1698
1843
|
if ((parsed.rest ?? []).length > 0 || parsed.positionals.length > 0)
|
|
1699
1844
|
badUsage("--stats runs no command");
|
|
@@ -1712,4 +1857,4 @@ async function main(argv) {
|
|
|
1712
1857
|
${USAGE}`);
|
|
1713
1858
|
return run(command, lines, was);
|
|
1714
1859
|
}
|
|
1715
|
-
|
|
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.
|
|
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.
|
|
31
|
+
"@kaynooo/cli-kit": "0.21.0"
|
|
32
32
|
}
|
|
33
33
|
}
|