@kaynooo/cli-out 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.js +368 -176
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ out --stats --since 7d
|
|
|
11
11
|
|
|
12
12
|
## Behaviour
|
|
13
13
|
|
|
14
|
-
- Output of at most 150 lines (`-n <lines>` changes it) passes through byte for byte, stdout and stderr to their own streams.
|
|
14
|
+
- Output of at most 150 lines (`-n <lines>` changes it) and about 8 KB (55 bytes per allowed line) passes through byte for byte, stdout and stderr to their own streams.
|
|
15
15
|
- Longer output is captured (both streams, in arrival order) and printed folded, after a header with the line count, how many lines were left out and the path of the full log.
|
|
16
16
|
- Lines alike in shape (ANSI codes stripped; numbers, hashes, durations, percentages, paths and file names normalized) fold into one line with a count and the last line: `[1/300] compiling a.ts (x300, last: [300/300] compiling z.ts)`.
|
|
17
17
|
- A block of 2 to 20 lines repeated back to back is shown once, followed by `(block repeated N times)`.
|
|
18
18
|
- A line or block seen earlier is replaced by `(same as line N)`, N being a line of the full log, and the header lists the most repeated messages with their counts.
|
|
19
19
|
- Carriage-return progress (spinners, bars) keeps only the final state of each line.
|
|
20
|
-
- 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.
|
|
20
|
+
- 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
21
|
- 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
22
|
- 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
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.
|
|
@@ -26,7 +26,7 @@ out --stats --since 7d
|
|
|
26
26
|
|
|
27
27
|
- 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
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.
|
|
29
|
-
- `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.
|
|
29
|
+
- `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
30
|
|
|
31
31
|
## Wrapping from a hook
|
|
32
32
|
|
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import
|
|
5
|
+
import process13 from "process";
|
|
6
6
|
|
|
7
7
|
// ../kit/src/args.ts
|
|
8
|
-
import
|
|
9
|
-
import process3 from "process";
|
|
8
|
+
import process4 from "process";
|
|
10
9
|
import { parseArgs } from "util";
|
|
11
10
|
|
|
12
11
|
// ../kit/src/format.ts
|
|
@@ -15,7 +14,7 @@ import process2 from "process";
|
|
|
15
14
|
// ../kit/src/style.ts
|
|
16
15
|
import process from "process";
|
|
17
16
|
|
|
18
|
-
// ../../node_modules/.bun/@kaynooo+utils@2.31.
|
|
17
|
+
// ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
|
|
19
18
|
function x(s) {
|
|
20
19
|
let r = s.startsWith("#") ? s.slice(1).toLowerCase() : s.toLowerCase();
|
|
21
20
|
if (!/^[a-f0-9]+$/.test(r))
|
|
@@ -235,7 +234,7 @@ class e {
|
|
|
235
234
|
}
|
|
236
235
|
}
|
|
237
236
|
|
|
238
|
-
// ../../node_modules/.bun/@kaynooo+utils@2.31.
|
|
237
|
+
// ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
|
|
239
238
|
var r = 39;
|
|
240
239
|
var o = 49;
|
|
241
240
|
var f2 = "\x1B[";
|
|
@@ -344,6 +343,55 @@ function printJson(value) {
|
|
|
344
343
|
console.log(JSON.stringify(value, undefined, 2));
|
|
345
344
|
}
|
|
346
345
|
|
|
346
|
+
// ../kit/src/help.ts
|
|
347
|
+
import process3 from "process";
|
|
348
|
+
var OPTIONAL = /\[[^\]]+\]/g;
|
|
349
|
+
var USAGE_PREFIX = /^usage:\s*/i;
|
|
350
|
+
var FULL_STOP = /\.$/;
|
|
351
|
+
function usageLine(usage, colors = paint(process3.stdout)) {
|
|
352
|
+
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
353
|
+
}
|
|
354
|
+
function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
|
|
355
|
+
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
356
|
+
`);
|
|
357
|
+
}
|
|
358
|
+
function replacesRows(replaces, colors = paint(process3.stdout)) {
|
|
359
|
+
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
360
|
+
}
|
|
361
|
+
function wantsHelpJson(env = process3.env) {
|
|
362
|
+
return env.KIT_HELP === "json";
|
|
363
|
+
}
|
|
364
|
+
function description(usage, { examples = [], replaces } = {}) {
|
|
365
|
+
const lines = usage.replace(USAGE_PREFIX, "").split(`
|
|
366
|
+
`);
|
|
367
|
+
const first = lines[0];
|
|
368
|
+
const colon = first.indexOf(": ");
|
|
369
|
+
const prose = lines[lines.indexOf("") + 1] ?? "";
|
|
370
|
+
const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
|
|
371
|
+
return {
|
|
372
|
+
synopsis: colon === -1 ? first : first.slice(0, colon),
|
|
373
|
+
summary: colon === -1 ? fallback : first.slice(colon + 2),
|
|
374
|
+
...replaces === undefined ? {} : { replaces },
|
|
375
|
+
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function exampleRows(examples, colors = paint(process3.stdout)) {
|
|
379
|
+
if (examples.length === 0)
|
|
380
|
+
return [];
|
|
381
|
+
return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
|
|
382
|
+
}
|
|
383
|
+
function optionRows(options, colors = paint(process3.stdout)) {
|
|
384
|
+
const rows = Object.entries(options).map(([name, option]) => {
|
|
385
|
+
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
386
|
+
const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
|
|
387
|
+
const placeholder = option.optional === undefined ? named : `[${named}]`;
|
|
388
|
+
const value = option.type === "string" ? ` ${colors.dim(placeholder)}` : "";
|
|
389
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? placeholder.length + 1 : 0), description: option.description ?? "" };
|
|
390
|
+
});
|
|
391
|
+
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
392
|
+
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
347
395
|
// ../kit/src/text.ts
|
|
348
396
|
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
349
397
|
function stripAnsi(text) {
|
|
@@ -366,8 +414,35 @@ function closest(name, names) {
|
|
|
366
414
|
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
367
415
|
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
368
416
|
}
|
|
417
|
+
// ../kit/src/input.ts
|
|
418
|
+
import { fstatSync } from "fs";
|
|
419
|
+
var WHOLE = /^\d+$/;
|
|
420
|
+
function wholeNumber(value, name, fallback, min = 1) {
|
|
421
|
+
if (value === undefined)
|
|
422
|
+
return fallback;
|
|
423
|
+
const number = Number(value);
|
|
424
|
+
if (!WHOLE.test(value) || !Number.isSafeInteger(number) || number < min)
|
|
425
|
+
badUsage(`${name.startsWith("<") ? name : `--${name}`} takes a whole number from ${min}, not ${value}`);
|
|
426
|
+
return number;
|
|
427
|
+
}
|
|
428
|
+
var DURATION = /^(\d+(?:\.\d+)?)(ms|[smhd])?$/;
|
|
429
|
+
var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
430
|
+
function duration(value, name, fallback, unit = "s") {
|
|
431
|
+
if (value === undefined)
|
|
432
|
+
return fallback;
|
|
433
|
+
const match = DURATION.exec(value.trim());
|
|
434
|
+
const ms = match === null ? 0 : Number(match[1]) * UNIT_MS[match[2] ?? unit];
|
|
435
|
+
if (ms < 1)
|
|
436
|
+
badUsage(`--${name} takes a duration such as 30s, 5m, 1h or 7d, not ${value}`);
|
|
437
|
+
return ms;
|
|
438
|
+
}
|
|
439
|
+
function isStdinPiped() {
|
|
440
|
+
const stat = fstatSync(0);
|
|
441
|
+
return stat.isFIFO() || stat.isFile();
|
|
442
|
+
}
|
|
369
443
|
|
|
370
444
|
// ../kit/src/args.ts
|
|
445
|
+
var DASHES = /^-+/;
|
|
371
446
|
function jsonOption(holds) {
|
|
372
447
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
373
448
|
}
|
|
@@ -402,61 +477,20 @@ class HelpShown extends Exit {
|
|
|
402
477
|
|
|
403
478
|
class Completing {
|
|
404
479
|
options;
|
|
405
|
-
|
|
480
|
+
positionals;
|
|
481
|
+
candidates;
|
|
482
|
+
constructor(options, positionals = [], candidates = {}) {
|
|
406
483
|
this.options = options;
|
|
484
|
+
this.positionals = positionals;
|
|
485
|
+
this.candidates = candidates;
|
|
407
486
|
}
|
|
408
487
|
}
|
|
409
488
|
var completion = { active: false };
|
|
410
|
-
var OPTIONAL = /\[[^\]]+\]/g;
|
|
411
|
-
var USAGE_PREFIX = /^usage:\s*/i;
|
|
412
|
-
var FULL_STOP = /\.$/;
|
|
413
|
-
var DASHES = /^-+/;
|
|
414
|
-
function usageLine(usage, colors = paint(process3.stdout)) {
|
|
415
|
-
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
416
|
-
}
|
|
417
|
-
function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
|
|
418
|
-
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
419
|
-
`);
|
|
420
|
-
}
|
|
421
|
-
function replacesRows(replaces, colors = paint(process3.stdout)) {
|
|
422
|
-
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
423
|
-
}
|
|
424
|
-
function wantsHelpJson(env = process3.env) {
|
|
425
|
-
return env.KIT_HELP === "json";
|
|
426
|
-
}
|
|
427
|
-
function description(usage, { examples = [], replaces } = {}) {
|
|
428
|
-
const lines = usage.replace(USAGE_PREFIX, "").split(`
|
|
429
|
-
`);
|
|
430
|
-
const first = lines[0];
|
|
431
|
-
const colon = first.indexOf(": ");
|
|
432
|
-
const prose = lines[lines.indexOf("") + 1] ?? "";
|
|
433
|
-
const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
|
|
434
|
-
return {
|
|
435
|
-
synopsis: colon === -1 ? first : first.slice(0, colon),
|
|
436
|
-
summary: colon === -1 ? fallback : first.slice(colon + 2),
|
|
437
|
-
...replaces === undefined ? {} : { replaces },
|
|
438
|
-
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
function exampleRows(examples, colors = paint(process3.stdout)) {
|
|
442
|
-
if (examples.length === 0)
|
|
443
|
-
return [];
|
|
444
|
-
return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
|
|
445
|
-
}
|
|
446
|
-
function optionRows(options, colors = paint(process3.stdout)) {
|
|
447
|
-
const rows = Object.entries(options).map(([name, option]) => {
|
|
448
|
-
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
449
|
-
const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
|
|
450
|
-
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
|
|
451
|
-
});
|
|
452
|
-
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
453
|
-
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
454
|
-
}
|
|
455
489
|
var OUTPUT_OPTIONS = {
|
|
456
490
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
457
491
|
plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
|
|
458
492
|
};
|
|
459
|
-
function applyOutput(color, plain, env =
|
|
493
|
+
function applyOutput(color, plain, env = process4.env) {
|
|
460
494
|
const when = plain === true ? "never" : color ?? "auto";
|
|
461
495
|
if (when === "auto")
|
|
462
496
|
return;
|
|
@@ -473,14 +507,13 @@ function applyOutput(color, plain, env = process3.env) {
|
|
|
473
507
|
function parse(args, usage, options, settings = {}) {
|
|
474
508
|
const output = Object.fromEntries(Object.entries(OUTPUT_OPTIONS).filter(([name]) => !(name in options)));
|
|
475
509
|
const all = { ...options, ...output, help: { type: "boolean", short: "h", description: "show this help" } };
|
|
510
|
+
const listing = listed(all, settings.allowNegative === true);
|
|
476
511
|
if (completion.active)
|
|
477
|
-
throw new Completing(
|
|
478
|
-
const
|
|
479
|
-
const own = separator === -1 ? args : args.slice(0, separator);
|
|
480
|
-
const rest = separator === -1 ? undefined : args.slice(separator + 1);
|
|
512
|
+
throw new Completing(listing, settings.positionals, settings.candidates);
|
|
513
|
+
const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
|
|
481
514
|
let parsed;
|
|
482
515
|
try {
|
|
483
|
-
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
516
|
+
parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
484
517
|
} catch (error) {
|
|
485
518
|
const message = parseError(error);
|
|
486
519
|
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
@@ -488,12 +521,84 @@ function parse(args, usage, options, settings = {}) {
|
|
|
488
521
|
const values = parsed.values;
|
|
489
522
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
490
523
|
if (values.help === true) {
|
|
491
|
-
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage,
|
|
524
|
+
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
|
|
492
525
|
throw new HelpShown;
|
|
493
526
|
}
|
|
527
|
+
checkChoices(parsed.values, all);
|
|
528
|
+
checkConflicts(parsed.values, all);
|
|
494
529
|
if (settings.positionals !== undefined)
|
|
495
530
|
checkPositionals(parsed.positionals, settings.positionals);
|
|
496
|
-
return { ...parsed, positionals: parsed.positionals, rest };
|
|
531
|
+
return { ...parsed, values: parsed.values, positionals: parsed.positionals, rest };
|
|
532
|
+
}
|
|
533
|
+
function splitRest(args, passes) {
|
|
534
|
+
const separator = passes ? args.indexOf("--") : -1;
|
|
535
|
+
return { own: separator === -1 ? args : args.slice(0, separator), rest: separator === -1 ? undefined : args.slice(separator + 1) };
|
|
536
|
+
}
|
|
537
|
+
function splitTrailing(args, options, count) {
|
|
538
|
+
const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
|
|
539
|
+
let seen = 0;
|
|
540
|
+
if (count === 0)
|
|
541
|
+
return { own: args, rest: undefined };
|
|
542
|
+
for (let at = 0;at < args.length; at++) {
|
|
543
|
+
const word = args[at];
|
|
544
|
+
if (word === "--")
|
|
545
|
+
break;
|
|
546
|
+
if (word.startsWith("-")) {
|
|
547
|
+
if (takesValue(word))
|
|
548
|
+
at++;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
seen++;
|
|
552
|
+
if (seen === count)
|
|
553
|
+
return { own: args.slice(0, at + 1), rest: args.slice(at + 1) };
|
|
554
|
+
}
|
|
555
|
+
return { own: args, rest: undefined };
|
|
556
|
+
}
|
|
557
|
+
function listed(options, allowNegative) {
|
|
558
|
+
if (!allowNegative)
|
|
559
|
+
return options;
|
|
560
|
+
return Object.fromEntries(Object.entries(options).flatMap(([name, option]) => {
|
|
561
|
+
if (option.type !== "boolean" || option.negative === undefined)
|
|
562
|
+
return [[name, option]];
|
|
563
|
+
return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
function withBare(args, options) {
|
|
567
|
+
const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
|
|
568
|
+
if (bare.size === 0)
|
|
569
|
+
return args;
|
|
570
|
+
const separator = args.indexOf("--");
|
|
571
|
+
const own = separator === -1 ? args : args.slice(0, separator);
|
|
572
|
+
return [...own.flatMap((arg, index) => {
|
|
573
|
+
const optional = bare.get(arg);
|
|
574
|
+
const next = own[index + 1];
|
|
575
|
+
const value = next !== undefined && (optional?.accepts === undefined ? !next.startsWith("-") : optional.accepts.test(next));
|
|
576
|
+
return optional === undefined || value ? [arg] : [arg, optional.bare];
|
|
577
|
+
}), ...separator === -1 ? [] : args.slice(separator)];
|
|
578
|
+
}
|
|
579
|
+
function checkConflicts(values, options) {
|
|
580
|
+
const given = (name) => values[name] !== undefined && values[name] !== false;
|
|
581
|
+
for (const [name, { conflicts = [] }] of Object.entries(options)) {
|
|
582
|
+
const other = given(name) ? conflicts.find(given) : undefined;
|
|
583
|
+
if (other !== undefined)
|
|
584
|
+
badUsage(`--${name} cannot be used with --${other}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function checkChoices(values, options) {
|
|
588
|
+
for (const [name, option] of Object.entries(options)) {
|
|
589
|
+
if (option.choices === undefined)
|
|
590
|
+
continue;
|
|
591
|
+
const given = values[name];
|
|
592
|
+
for (const value of Array.isArray(given) ? given : [given]) {
|
|
593
|
+
if (typeof value === "string")
|
|
594
|
+
oneOf(value, name, option.choices);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function oneOf(value, name, choices) {
|
|
599
|
+
if (!choices.includes(value))
|
|
600
|
+
badUsage(`${name.startsWith("<") ? name : `--${name}`} takes one of ${choices.join(", ")}, not ${value}`);
|
|
601
|
+
return value;
|
|
497
602
|
}
|
|
498
603
|
function checkPositionals(given, names) {
|
|
499
604
|
const variadic = names.at(-1)?.replace("]", "").endsWith("...") === true;
|
|
@@ -542,33 +647,9 @@ function nearestOption(message, options, allowNegative) {
|
|
|
542
647
|
const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
|
|
543
648
|
return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
|
|
544
649
|
}
|
|
545
|
-
var WHOLE = /^\d+$/;
|
|
546
|
-
function wholeNumber(value, name, fallback, min = 1) {
|
|
547
|
-
if (value === undefined)
|
|
548
|
-
return fallback;
|
|
549
|
-
const number = Number(value);
|
|
550
|
-
if (!WHOLE.test(value) || !Number.isSafeInteger(number) || number < min)
|
|
551
|
-
badUsage(`${name.startsWith("<") ? name : `--${name}`} takes a whole number from ${min}, not ${value}`);
|
|
552
|
-
return number;
|
|
553
|
-
}
|
|
554
|
-
var DURATION = /^(\d+(?:\.\d+)?)(ms|[smhd])?$/;
|
|
555
|
-
var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
556
|
-
function duration(value, name, fallback, unit = "s") {
|
|
557
|
-
if (value === undefined)
|
|
558
|
-
return fallback;
|
|
559
|
-
const match = DURATION.exec(value.trim());
|
|
560
|
-
const ms = match === null ? 0 : Number(match[1]) * UNIT_MS[match[2] ?? unit];
|
|
561
|
-
if (ms < 1)
|
|
562
|
-
badUsage(`--${name} takes a duration such as 30s, 5m, 1h or 7d, not ${value}`);
|
|
563
|
-
return ms;
|
|
564
|
-
}
|
|
565
|
-
function isStdinPiped() {
|
|
566
|
-
const stat = fstatSync(0);
|
|
567
|
-
return stat.isFIFO() || stat.isFile();
|
|
568
|
-
}
|
|
569
650
|
// ../kit/src/process.ts
|
|
570
651
|
import { constants } from "os";
|
|
571
|
-
import
|
|
652
|
+
import process5 from "process";
|
|
572
653
|
var KILL_GRACE_MS = 2000;
|
|
573
654
|
var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
574
655
|
function exitCodeOf(code, signal) {
|
|
@@ -590,7 +671,7 @@ function holdSignals(cleanup) {
|
|
|
590
671
|
},
|
|
591
672
|
release: () => {
|
|
592
673
|
for (const signal of SIGNALS)
|
|
593
|
-
|
|
674
|
+
process5.off(signal, onSignal);
|
|
594
675
|
}
|
|
595
676
|
};
|
|
596
677
|
function onSignal(signal) {
|
|
@@ -603,10 +684,10 @@ function holdSignals(cleanup) {
|
|
|
603
684
|
return;
|
|
604
685
|
held.release();
|
|
605
686
|
cleanup(signal);
|
|
606
|
-
|
|
687
|
+
process5.kill(process5.pid, signal);
|
|
607
688
|
}
|
|
608
689
|
for (const signal of SIGNALS)
|
|
609
|
-
|
|
690
|
+
process5.on(signal, onSignal);
|
|
610
691
|
return held;
|
|
611
692
|
}
|
|
612
693
|
async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal, maxBytes = Infinity, onChunk, onStart } = {}) {
|
|
@@ -620,7 +701,7 @@ async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal,
|
|
|
620
701
|
return;
|
|
621
702
|
try {
|
|
622
703
|
if (grouped)
|
|
623
|
-
|
|
704
|
+
process5.kill(-proc.pid, killing);
|
|
624
705
|
else
|
|
625
706
|
proc.kill(killing);
|
|
626
707
|
} catch {}
|
|
@@ -694,15 +775,114 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
|
|
|
694
775
|
var RESET_STYLE = `${ESC}[0m`;
|
|
695
776
|
var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
|
|
696
777
|
// ../kit/src/cli.ts
|
|
697
|
-
import
|
|
778
|
+
import process8 from "process";
|
|
779
|
+
|
|
780
|
+
// ../kit/src/usage.ts
|
|
781
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
782
|
+
import { homedir as homedir2 } from "os";
|
|
783
|
+
import { dirname, isAbsolute, join as join2 } from "path";
|
|
784
|
+
import process7 from "process";
|
|
785
|
+
|
|
786
|
+
// ../kit/src/logs.ts
|
|
787
|
+
import { lstatSync, openSync } from "fs";
|
|
788
|
+
import { mkdir, mkdtemp, readdir, rm, stat } from "fs/promises";
|
|
789
|
+
import { homedir, tmpdir } from "os";
|
|
790
|
+
import { join } from "path";
|
|
791
|
+
import process6 from "process";
|
|
792
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
793
|
+
var CLI_FOLDER = "kaynooo-cli";
|
|
794
|
+
function isPrivateDirectory(path) {
|
|
795
|
+
try {
|
|
796
|
+
const info = lstatSync(path, { throwIfNoEntry: false });
|
|
797
|
+
return info !== undefined && info.isDirectory() && info.uid === (process6.getuid?.() ?? info.uid) && (info.mode & 63) === 0;
|
|
798
|
+
} catch {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function privateLogDirectory(name, env = process6.env) {
|
|
803
|
+
const runtime = env.XDG_RUNTIME_DIR;
|
|
804
|
+
const base = runtime === undefined || runtime === "" ? join(homedir(), ".cache", CLI_FOLDER) : join(runtime, CLI_FOLDER);
|
|
805
|
+
const path = join(base, name);
|
|
806
|
+
try {
|
|
807
|
+
await mkdir(path, { recursive: true, mode: 448 });
|
|
808
|
+
if (isPrivateDirectory(path))
|
|
809
|
+
return path;
|
|
810
|
+
} catch {}
|
|
811
|
+
return mkdtemp(join(tmpdir(), `${name.replaceAll("/", "-")}-`));
|
|
812
|
+
}
|
|
813
|
+
async function sweepLogs(dir, maxAgeMs = DAY_MS, now = Date.now()) {
|
|
814
|
+
let names;
|
|
815
|
+
try {
|
|
816
|
+
names = await readdir(dir);
|
|
817
|
+
} catch {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
for (const name of names) {
|
|
821
|
+
const path = join(dir, name);
|
|
822
|
+
try {
|
|
823
|
+
const info = await stat(path);
|
|
824
|
+
if (info.isDirectory())
|
|
825
|
+
await sweepLogs(path, maxAgeMs, now);
|
|
826
|
+
if (now - info.mtimeMs > maxAgeMs)
|
|
827
|
+
await rm(path, { force: true, recursive: info.isDirectory() });
|
|
828
|
+
} catch {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function createLog(path) {
|
|
834
|
+
return openSync(path, "wx", 384);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// ../kit/src/usage.ts
|
|
838
|
+
var UNKNOWN_OPTION2 = /unknown option '(--[a-z][a-z0-9-]*)[=']/;
|
|
839
|
+
var FLAG_NAME = /^(?:--[a-z][a-z0-9-]*|-[a-zA-Z])$/;
|
|
840
|
+
var LONGEST_NAME = 40;
|
|
841
|
+
var usageError;
|
|
842
|
+
function noteUsageError(message) {
|
|
843
|
+
usageError = message;
|
|
844
|
+
}
|
|
845
|
+
function stateHome(env = process7.env) {
|
|
846
|
+
const dir = env.XDG_STATE_HOME;
|
|
847
|
+
return dir !== undefined && isAbsolute(dir) ? dir : join2(homedir2(), ".local", "state");
|
|
848
|
+
}
|
|
849
|
+
function usagePath(env = process7.env) {
|
|
850
|
+
return join2(stateHome(env), CLI_FOLDER, "usage.jsonl");
|
|
851
|
+
}
|
|
852
|
+
function usageRecorded(env = process7.env) {
|
|
853
|
+
const off = env.KAYNOOO_CLI_NO_USAGE;
|
|
854
|
+
return env.NODE_ENV !== "test" && (off === undefined || off === "" || off === "0");
|
|
855
|
+
}
|
|
856
|
+
function flagNames(argv) {
|
|
857
|
+
const end = argv.indexOf("--");
|
|
858
|
+
const names = (end === -1 ? argv : argv.slice(0, end)).filter((word) => word.startsWith("-") && word !== "-").map((word) => word.includes("=") ? word.slice(0, word.indexOf("=")) : word);
|
|
859
|
+
return [...new Set(names)].filter((name) => name.length <= LONGEST_NAME && FLAG_NAME.test(name));
|
|
860
|
+
}
|
|
861
|
+
function usageEntry(bin, command, argv, code, now = new Date) {
|
|
862
|
+
const unknown = code === 2 && usageError !== undefined ? UNKNOWN_OPTION2.exec(usageError)?.[1] : undefined;
|
|
863
|
+
usageError = undefined;
|
|
864
|
+
return { at: now.toISOString(), bin, ...command === undefined ? {} : { command }, flags: flagNames(argv), code, ...unknown === undefined ? {} : { unknown } };
|
|
865
|
+
}
|
|
866
|
+
function recordUsage(entry, env = process7.env) {
|
|
867
|
+
if (!usageRecorded(env))
|
|
868
|
+
return;
|
|
869
|
+
try {
|
|
870
|
+
const path = usagePath(env);
|
|
871
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
872
|
+
appendFileSync(path, `${JSON.stringify(entry)}
|
|
873
|
+
`, { mode: 384 });
|
|
874
|
+
} catch {}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ../kit/src/cli.ts
|
|
698
878
|
var LEVEL_OPTIONS = {
|
|
699
879
|
...OUTPUT_OPTIONS,
|
|
700
880
|
help: { type: "boolean", short: "h", description: "show this help" }
|
|
701
881
|
};
|
|
702
882
|
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
703
883
|
function printError(prefix, message, hint, cause) {
|
|
704
|
-
const colors = paint(
|
|
705
|
-
const styled = takesControlCodes(
|
|
884
|
+
const colors = paint(process8.stderr);
|
|
885
|
+
const styled = takesControlCodes(process8.stderr);
|
|
706
886
|
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
707
887
|
const under = (label, text, tint) => text === undefined ? "" : `
|
|
708
888
|
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
@@ -721,96 +901,85 @@ function reportThrown(prefix, error, redact = (text) => text, json = false) {
|
|
|
721
901
|
const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
|
|
722
902
|
const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
|
|
723
903
|
const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
|
|
904
|
+
if (error instanceof UsageError)
|
|
905
|
+
noteUsageError(error.message);
|
|
724
906
|
const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
|
|
725
907
|
report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
|
|
726
908
|
return error instanceof Exit ? error.code : 1;
|
|
727
909
|
}
|
|
728
910
|
var COMPLETE = "__complete";
|
|
729
911
|
var FILES = "__files__";
|
|
730
|
-
async function
|
|
912
|
+
async function completingOf(run) {
|
|
731
913
|
completion.active = true;
|
|
732
914
|
try {
|
|
733
915
|
await run([]);
|
|
734
916
|
} catch (error) {
|
|
735
917
|
if (error instanceof Completing)
|
|
736
|
-
return error
|
|
918
|
+
return error;
|
|
737
919
|
} finally {
|
|
738
920
|
completion.active = false;
|
|
739
921
|
}
|
|
740
|
-
return {};
|
|
922
|
+
return new Completing({});
|
|
923
|
+
}
|
|
924
|
+
function candidateLine(candidate) {
|
|
925
|
+
return typeof candidate === "string" ? candidate : candidate.join("\t");
|
|
926
|
+
}
|
|
927
|
+
function positionalBeingTyped(before, { options, positionals }) {
|
|
928
|
+
const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
|
|
929
|
+
let index = 0;
|
|
930
|
+
let at = 0;
|
|
931
|
+
while (at < before.length) {
|
|
932
|
+
const word = before[at];
|
|
933
|
+
if (word === "--") {
|
|
934
|
+
index += before.length - at - 1;
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
if (!word.startsWith("-"))
|
|
938
|
+
index++;
|
|
939
|
+
else if (takesValue(word))
|
|
940
|
+
at++;
|
|
941
|
+
at++;
|
|
942
|
+
}
|
|
943
|
+
const last = positionals.at(-1);
|
|
944
|
+
if (index < positionals.length)
|
|
945
|
+
return positionals[index];
|
|
946
|
+
return last?.replace("]", "").endsWith("...") === true ? last : undefined;
|
|
741
947
|
}
|
|
742
948
|
async function leafCandidates(run, words) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
949
|
+
const completing = await completingOf(run);
|
|
950
|
+
if ((words.at(-1) ?? "").startsWith("-"))
|
|
951
|
+
return Object.entries(completing.options).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
952
|
+
const flag = words.at(-2);
|
|
953
|
+
const option = flag?.startsWith("-") === true ? Object.entries(completing.options).find(([name, { short }]) => flag === `--${name}` || flag === `-${short}`)?.[1] : undefined;
|
|
954
|
+
if (option?.type === "string") {
|
|
955
|
+
const values = option.choices === undefined ? await option.candidates?.() : option.choices;
|
|
956
|
+
return values === undefined ? [FILES] : values.map(candidateLine);
|
|
957
|
+
}
|
|
958
|
+
const name = positionalBeingTyped(words.slice(0, -1), completing);
|
|
959
|
+
const values = name === undefined ? undefined : await completing.candidates[name]?.();
|
|
960
|
+
return values === undefined ? [FILES] : values.map(candidateLine);
|
|
746
961
|
}
|
|
747
962
|
async function printCandidates(found) {
|
|
748
963
|
for (const line of await found)
|
|
749
964
|
console.log(line);
|
|
750
965
|
return 0;
|
|
751
966
|
}
|
|
752
|
-
async function runMain({ name, version, run, redact }, argv =
|
|
967
|
+
async function runMain({ name, version, run, redact, usage }, argv = process8.argv.slice(2)) {
|
|
753
968
|
if (argv[0] === COMPLETE)
|
|
754
969
|
return printCandidates(leafCandidates(run, argv.slice(1)));
|
|
755
970
|
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
|
|
756
971
|
console.log(version);
|
|
757
972
|
return 0;
|
|
758
973
|
}
|
|
974
|
+
let code;
|
|
759
975
|
try {
|
|
760
|
-
|
|
976
|
+
code = await run(argv);
|
|
761
977
|
} catch (error) {
|
|
762
|
-
|
|
978
|
+
code = reportThrown(name, error, redact, wantsJson(argv));
|
|
763
979
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
import { mkdir, mkdtemp, readdir, rm, stat } from "fs/promises";
|
|
768
|
-
import { homedir, tmpdir } from "os";
|
|
769
|
-
import { join } from "path";
|
|
770
|
-
import process6 from "process";
|
|
771
|
-
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
772
|
-
var CLI_FOLDER = "kaynooo-cli";
|
|
773
|
-
function isPrivateDirectory(path) {
|
|
774
|
-
try {
|
|
775
|
-
const info = lstatSync(path, { throwIfNoEntry: false });
|
|
776
|
-
return info !== undefined && info.isDirectory() && info.uid === (process6.getuid?.() ?? info.uid) && (info.mode & 63) === 0;
|
|
777
|
-
} catch {
|
|
778
|
-
return false;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
async function privateLogDirectory(name, env = process6.env) {
|
|
782
|
-
const runtime = env.XDG_RUNTIME_DIR;
|
|
783
|
-
const base = runtime === undefined || runtime === "" ? join(homedir(), ".cache", CLI_FOLDER) : join(runtime, CLI_FOLDER);
|
|
784
|
-
const path = join(base, name);
|
|
785
|
-
try {
|
|
786
|
-
await mkdir(path, { recursive: true, mode: 448 });
|
|
787
|
-
if (isPrivateDirectory(path))
|
|
788
|
-
return path;
|
|
789
|
-
} catch {}
|
|
790
|
-
return mkdtemp(join(tmpdir(), `${name.replaceAll("/", "-")}-`));
|
|
791
|
-
}
|
|
792
|
-
async function sweepLogs(dir, maxAgeMs = DAY_MS, now = Date.now()) {
|
|
793
|
-
let names;
|
|
794
|
-
try {
|
|
795
|
-
names = await readdir(dir);
|
|
796
|
-
} catch {
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
for (const name of names) {
|
|
800
|
-
const path = join(dir, name);
|
|
801
|
-
try {
|
|
802
|
-
const info = await stat(path);
|
|
803
|
-
if (info.isDirectory())
|
|
804
|
-
await sweepLogs(path, maxAgeMs, now);
|
|
805
|
-
if (now - info.mtimeMs > maxAgeMs)
|
|
806
|
-
await rm(path, { force: true, recursive: info.isDirectory() });
|
|
807
|
-
} catch {
|
|
808
|
-
continue;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
function createLog(path) {
|
|
813
|
-
return openSync(path, "wx", 384);
|
|
980
|
+
if (usage !== false)
|
|
981
|
+
recordUsage(usageEntry(name, undefined, argv, code));
|
|
982
|
+
return code;
|
|
814
983
|
}
|
|
815
984
|
// ../kit/src/secrets.ts
|
|
816
985
|
var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
|
|
@@ -819,7 +988,7 @@ var {Glob } = globalThis.Bun;
|
|
|
819
988
|
var SESSION_FILES = new Glob("*.json");
|
|
820
989
|
var ACCOUNT_DIRS = new Glob("*/sessions");
|
|
821
990
|
// package.json
|
|
822
|
-
var version = "0.
|
|
991
|
+
var version = "0.7.1";
|
|
823
992
|
|
|
824
993
|
// src/shape.ts
|
|
825
994
|
var HEX_LITERAL = /\b0x[0-9a-f]+\b/gi;
|
|
@@ -844,6 +1013,9 @@ var HEAD = 40;
|
|
|
844
1013
|
var TAIL = 60;
|
|
845
1014
|
var MAX_BLOCK = 20;
|
|
846
1015
|
var MIN_REFERENCED = 20;
|
|
1016
|
+
var BYTES_PER_LINE = 55;
|
|
1017
|
+
var MAX_WIDTH = 300;
|
|
1018
|
+
var MAX_KEPT_IMPORTANT = 40;
|
|
847
1019
|
var TOP_REPEATED = 3;
|
|
848
1020
|
var LINE_BREAK = /\r\n|\r|\n/;
|
|
849
1021
|
var LINE_END = /(?:\r\n|\r|\n)$/;
|
|
@@ -854,7 +1026,10 @@ function lineCount(text) {
|
|
|
854
1026
|
return LINE_END.test(text) ? count - 1 : count;
|
|
855
1027
|
}
|
|
856
1028
|
function shouldFold(text, threshold) {
|
|
857
|
-
return lineCount(text) > threshold;
|
|
1029
|
+
return lineCount(text) > threshold || Buffer.byteLength(text) > threshold * BYTES_PER_LINE;
|
|
1030
|
+
}
|
|
1031
|
+
function cutWide(line) {
|
|
1032
|
+
return line.length > MAX_WIDTH ? `${line.slice(0, MAX_WIDTH)}... [+${line.length - MAX_WIDTH} chars]` : line;
|
|
858
1033
|
}
|
|
859
1034
|
function finalState(line) {
|
|
860
1035
|
const segments = line.split("\r").filter((segment) => segment !== "");
|
|
@@ -948,11 +1123,16 @@ function toEntries(units) {
|
|
|
948
1123
|
function fold(text, { threshold, logPath }) {
|
|
949
1124
|
const lines = splitLines(text);
|
|
950
1125
|
const { entries, references, repeated } = toEntries(toUnits(lines, lines.map((line) => shape(line))));
|
|
951
|
-
|
|
952
|
-
|
|
1126
|
+
for (const entry of entries)
|
|
1127
|
+
entry.text = cutWide(entry.text);
|
|
1128
|
+
const heavy = entries.reduce((sum, { text }) => sum + text.length + 1, 0) > threshold * BYTES_PER_LINE;
|
|
1129
|
+
const shrink = heavy && entries.length <= threshold ? 2 : 1;
|
|
1130
|
+
const head = Math.floor(Math.min(HEAD, Math.floor(threshold / 3)) / shrink);
|
|
1131
|
+
const tail = Math.floor(Math.min(TAIL, Math.floor(threshold / 2)) / shrink);
|
|
953
1132
|
const out = [];
|
|
954
1133
|
let dropped = 0;
|
|
955
|
-
|
|
1134
|
+
let kept = 0;
|
|
1135
|
+
if (entries.length <= threshold && !heavy) {
|
|
956
1136
|
out.push(...entries.map(({ text }) => text));
|
|
957
1137
|
} else {
|
|
958
1138
|
const middleEnd = entries.length - tail;
|
|
@@ -969,7 +1149,8 @@ function fold(text, { threshold, logPath }) {
|
|
|
969
1149
|
out.push(entry.text);
|
|
970
1150
|
continue;
|
|
971
1151
|
}
|
|
972
|
-
if (entry.important) {
|
|
1152
|
+
if (entry.important && kept < MAX_KEPT_IMPORTANT) {
|
|
1153
|
+
kept++;
|
|
973
1154
|
closeGap();
|
|
974
1155
|
out.push(entry.text);
|
|
975
1156
|
continue;
|
|
@@ -989,11 +1170,11 @@ function fold(text, { threshold, logPath }) {
|
|
|
989
1170
|
|
|
990
1171
|
// src/run.ts
|
|
991
1172
|
import { closeSync, rmSync, writeSync } from "fs";
|
|
992
|
-
import
|
|
1173
|
+
import process12 from "process";
|
|
993
1174
|
|
|
994
1175
|
// src/logs.ts
|
|
995
|
-
import { join as
|
|
996
|
-
import
|
|
1176
|
+
import { join as join3 } from "path";
|
|
1177
|
+
import process10 from "process";
|
|
997
1178
|
var MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
998
1179
|
async function logDir() {
|
|
999
1180
|
return privateLogDirectory("out");
|
|
@@ -1002,7 +1183,7 @@ async function cleanLogs(dir, now = Date.now()) {
|
|
|
1002
1183
|
await sweepLogs(dir, MAX_AGE_MS, now);
|
|
1003
1184
|
}
|
|
1004
1185
|
function newLog(dir) {
|
|
1005
|
-
const path =
|
|
1186
|
+
const path = join3(dir, `${new Date().toISOString().replaceAll(/[:.]/g, "-")}-${process10.pid}.log`);
|
|
1006
1187
|
return { path, fd: createLog(path) };
|
|
1007
1188
|
}
|
|
1008
1189
|
|
|
@@ -1257,10 +1438,9 @@ function filtered(text, stages) {
|
|
|
1257
1438
|
}
|
|
1258
1439
|
|
|
1259
1440
|
// src/stats.ts
|
|
1260
|
-
import { appendFileSync, mkdirSync } from "fs";
|
|
1261
|
-
import {
|
|
1262
|
-
import
|
|
1263
|
-
import process9 from "process";
|
|
1441
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1442
|
+
import { basename, dirname as dirname2, join as join4 } from "path";
|
|
1443
|
+
import process11 from "process";
|
|
1264
1444
|
var BYTES_PER_TOKEN = 4;
|
|
1265
1445
|
var KEY_WORD = /^[a-z][a-z:_-]*$/;
|
|
1266
1446
|
var KEY_WORDS = 3;
|
|
@@ -1269,8 +1449,8 @@ var ASSIGNMENT = /^\w+=/;
|
|
|
1269
1449
|
var CHAIN = /&&|\|\||;/;
|
|
1270
1450
|
var SHELLS2 = new Set(["sh", "bash", "zsh"]);
|
|
1271
1451
|
var TOP = 10;
|
|
1272
|
-
function statsPath(env =
|
|
1273
|
-
return
|
|
1452
|
+
function statsPath(env = process11.env) {
|
|
1453
|
+
return join4(stateHome(env), CLI_FOLDER, "out", "stats.jsonl");
|
|
1274
1454
|
}
|
|
1275
1455
|
function commandKey(command) {
|
|
1276
1456
|
const [program = "", ...rest] = command.slice(Math.max(0, command.findIndex((word) => !ASSIGNMENT.test(word))));
|
|
@@ -1288,8 +1468,8 @@ function commandKey(command) {
|
|
|
1288
1468
|
}
|
|
1289
1469
|
function record(entry, path = statsPath()) {
|
|
1290
1470
|
try {
|
|
1291
|
-
|
|
1292
|
-
|
|
1471
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
1472
|
+
appendFileSync2(path, `${JSON.stringify(entry)}
|
|
1293
1473
|
`, { mode: 384 });
|
|
1294
1474
|
} catch {}
|
|
1295
1475
|
}
|
|
@@ -1307,29 +1487,41 @@ async function readEntries(path = statsPath(), since = 0) {
|
|
|
1307
1487
|
}
|
|
1308
1488
|
});
|
|
1309
1489
|
}
|
|
1490
|
+
function median(values) {
|
|
1491
|
+
const sorted = values.toSorted((a, b) => a - b);
|
|
1492
|
+
const middle = Math.floor(sorted.length / 2);
|
|
1493
|
+
if (sorted.length === 0)
|
|
1494
|
+
return;
|
|
1495
|
+
return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
1496
|
+
}
|
|
1310
1497
|
function tokens(bytes) {
|
|
1311
1498
|
return Math.round(bytes / BYTES_PER_TOKEN);
|
|
1312
1499
|
}
|
|
1313
1500
|
function formatNumber(value) {
|
|
1314
1501
|
return value.toLocaleString("en-US");
|
|
1315
1502
|
}
|
|
1503
|
+
function cut2(runs) {
|
|
1504
|
+
const value = median(runs.filter((run) => run.folded && run.baseline > 0).map((run) => (1 - run.shown / run.baseline) * 100));
|
|
1505
|
+
return value === undefined ? undefined : Math.round(value);
|
|
1506
|
+
}
|
|
1316
1507
|
function summarize(entries) {
|
|
1317
1508
|
const commands = Map.groupBy(entries, (entry) => entry.command).entries().map(([command, runs]) => ({
|
|
1318
1509
|
command,
|
|
1319
1510
|
runs: runs.length,
|
|
1320
1511
|
folded: runs.filter((run) => run.folded).length,
|
|
1321
|
-
tokensSaved: tokens(runs.reduce((sum, run) => sum + run.baseline - run.shown, 0))
|
|
1512
|
+
tokensSaved: tokens(runs.reduce((sum, run) => sum + run.baseline - run.shown, 0)),
|
|
1513
|
+
medianCut: cut2(runs)
|
|
1322
1514
|
}));
|
|
1323
1515
|
const sum = (field) => entries.reduce((total, entry) => total + entry[field], 0);
|
|
1324
|
-
return { runs: entries.length, folded: entries.filter((entry) => entry.folded).length, bytes: sum("bytes"), shown: sum("shown"), tokensSaved: tokens(sum("baseline") - sum("shown")), commands: [...commands].sort((a, b) => b.tokensSaved - a.tokensSaved) };
|
|
1516
|
+
return { runs: entries.length, folded: entries.filter((entry) => entry.folded).length, bytes: sum("bytes"), shown: sum("shown"), tokensSaved: tokens(sum("baseline") - sum("shown")), foldedShare: entries.length === 0 ? 0 : Math.round(entries.filter((entry) => entry.folded).length / entries.length * 100), commands: [...commands].sort((a, b) => b.tokensSaved - a.tokensSaved) };
|
|
1325
1517
|
}
|
|
1326
1518
|
function renderSummary(summary, period) {
|
|
1327
1519
|
if (summary.runs === 0)
|
|
1328
1520
|
return `no run of out recorded ${period}`;
|
|
1329
1521
|
const top = summary.commands.slice(0, TOP).filter((each) => each.tokensSaved !== 0);
|
|
1330
1522
|
return [
|
|
1331
|
-
`${formatNumber(summary.runs)} runs ${period}, ${formatNumber(summary.folded)} folded: ${formatNumber(summary.bytes)} bytes printed, ${formatNumber(summary.shown)} shown, about ${formatNumber(summary.tokensSaved)} tokens saved (bytes / ${BYTES_PER_TOKEN})`,
|
|
1332
|
-
...top.length === 0 ? [] : ["", table([["command", "runs", "folded", "tokens saved"], ...top.map((each) => [each.command, formatNumber(each.runs), formatNumber(each.folded), formatNumber(each.tokensSaved)])])]
|
|
1523
|
+
`${formatNumber(summary.runs)} runs ${period}, ${formatNumber(summary.folded)} folded (${summary.foldedShare}%): ${formatNumber(summary.bytes)} bytes printed, ${formatNumber(summary.shown)} shown, about ${formatNumber(summary.tokensSaved)} tokens saved (bytes / ${BYTES_PER_TOKEN})`,
|
|
1524
|
+
...top.length === 0 ? [] : ["", table([["command", "runs", "folded", "median cut", "tokens saved"], ...top.map((each) => [each.command, formatNumber(each.runs), formatNumber(each.folded), each.medianCut === undefined ? "-" : `${each.medianCut}%`, formatNumber(each.tokensSaved)])])]
|
|
1333
1525
|
].join(`
|
|
1334
1526
|
`);
|
|
1335
1527
|
}
|
|
@@ -1403,11 +1595,11 @@ ${decode(this.tail)}`;
|
|
|
1403
1595
|
}
|
|
1404
1596
|
function replay(chunks) {
|
|
1405
1597
|
for (const { stream, data } of chunks)
|
|
1406
|
-
|
|
1598
|
+
process12[stream].write(data);
|
|
1407
1599
|
}
|
|
1408
1600
|
function count(recorder, shown, recorded) {
|
|
1409
1601
|
const baseline = recorded.was.length === 0 || recorder.overflowing ? undefined : filtered(recorder.text, recorded.was);
|
|
1410
|
-
record({ at: new Date().toISOString(), dir:
|
|
1602
|
+
record({ at: new Date().toISOString(), dir: process12.cwd(), command: recorded.command, bytes: recorder.bytes, baseline: baseline === undefined ? recorder.bytes : Buffer.byteLength(baseline), shown, folded: shown !== recorder.bytes });
|
|
1411
1603
|
}
|
|
1412
1604
|
async function run(command, threshold, was = []) {
|
|
1413
1605
|
const dir = await logDir();
|
|
@@ -1471,7 +1663,7 @@ function present(recorder, threshold) {
|
|
|
1471
1663
|
replay(recorder.chunks);
|
|
1472
1664
|
return recorder.bytes;
|
|
1473
1665
|
}
|
|
1474
|
-
|
|
1666
|
+
process12.stdout.write(printed);
|
|
1475
1667
|
return Buffer.byteLength(printed);
|
|
1476
1668
|
}
|
|
1477
1669
|
|
|
@@ -1520,4 +1712,4 @@ async function main(argv) {
|
|
|
1520
1712
|
${USAGE}`);
|
|
1521
1713
|
return run(command, lines, was);
|
|
1522
1714
|
}
|
|
1523
|
-
|
|
1715
|
+
process13.exitCode = await runMain({ name: "out", version, 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.7.1",
|
|
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.20.0"
|
|
32
32
|
}
|
|
33
33
|
}
|