@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.
- package/README.md +10 -4
- package/dist/cli.js +375 -172
- 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,18 +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
|
|
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
|
|
11
|
+
import process3 from "process";
|
|
14
12
|
|
|
15
13
|
// ../kit/src/style.ts
|
|
16
|
-
import
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
727
|
+
process6.kill(process6.pid, signal);
|
|
684
728
|
}
|
|
685
729
|
for (const signal of SIGNALS)
|
|
686
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
782
|
-
const styled = takesControlCodes(
|
|
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
|
-
|
|
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
|
-
|
|
1029
|
+
code = await run(argv);
|
|
871
1030
|
} catch (error) {
|
|
872
|
-
|
|
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
|
-
|
|
913
|
-
|
|
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
|
|
1226
|
+
import process13 from "process";
|
|
1115
1227
|
|
|
1116
1228
|
// src/logs.ts
|
|
1117
|
-
import { join as
|
|
1118
|
-
import
|
|
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 =
|
|
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
|
|
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 {
|
|
1384
|
-
import
|
|
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 =
|
|
1395
|
-
return
|
|
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
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|