@kaynooo/forgejo-tools 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 +1 -1
- package/dist/cli.js +1112 -734
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import
|
|
5
|
+
import process15 from "process";
|
|
6
6
|
|
|
7
7
|
// ../kit/src/args.ts
|
|
8
8
|
import process4 from "process";
|
|
@@ -395,6 +395,26 @@ function printJson(value) {
|
|
|
395
395
|
console.log(JSON.stringify(value, undefined, 2));
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
// ../kit/src/text.ts
|
|
399
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
400
|
+
function distance(a, b) {
|
|
401
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
402
|
+
for (const [i, left] of [...a].entries()) {
|
|
403
|
+
const current = [i + 1];
|
|
404
|
+
for (const [j, right] of [...b].entries())
|
|
405
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
406
|
+
previous = current;
|
|
407
|
+
}
|
|
408
|
+
return previous[b.length] ?? 0;
|
|
409
|
+
}
|
|
410
|
+
function closest(name, names) {
|
|
411
|
+
const started = names.find((known) => known.startsWith(name));
|
|
412
|
+
if (started !== undefined)
|
|
413
|
+
return started;
|
|
414
|
+
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
415
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
398
418
|
// ../kit/src/args.ts
|
|
399
419
|
function jsonOption(holds) {
|
|
400
420
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
@@ -414,8 +434,8 @@ function fail(message, code = 2, hint) {
|
|
|
414
434
|
}
|
|
415
435
|
|
|
416
436
|
class UsageError extends Exit {
|
|
417
|
-
constructor(message) {
|
|
418
|
-
super(message, 2);
|
|
437
|
+
constructor(message, hint) {
|
|
438
|
+
super(message, 2, hint);
|
|
419
439
|
}
|
|
420
440
|
}
|
|
421
441
|
function badUsage(message) {
|
|
@@ -438,6 +458,7 @@ var completion = { active: false };
|
|
|
438
458
|
var OPTIONAL = /\[[^\]]+\]/g;
|
|
439
459
|
var USAGE_PREFIX = /^usage:\s*/i;
|
|
440
460
|
var FULL_STOP = /\.$/;
|
|
461
|
+
var DASHES = /^-+/;
|
|
441
462
|
function usageLine(usage, colors = paint(process4.stdout)) {
|
|
442
463
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
443
464
|
}
|
|
@@ -474,10 +495,10 @@ function optionRows(options, colors = paint(process4.stdout)) {
|
|
|
474
495
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
475
496
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
476
497
|
const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
|
|
477
|
-
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description };
|
|
498
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
|
|
478
499
|
});
|
|
479
500
|
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
480
|
-
return rows.map((row) => ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
501
|
+
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
481
502
|
}
|
|
482
503
|
var OUTPUT_OPTIONS = {
|
|
483
504
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
@@ -509,7 +530,8 @@ function parse(args, usage, options, settings = {}) {
|
|
|
509
530
|
try {
|
|
510
531
|
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
511
532
|
} catch (error) {
|
|
512
|
-
|
|
533
|
+
const message = parseError(error);
|
|
534
|
+
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
513
535
|
}
|
|
514
536
|
const values = parsed.values;
|
|
515
537
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
@@ -552,12 +574,29 @@ function parseError(error) {
|
|
|
552
574
|
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
553
575
|
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
554
576
|
}
|
|
577
|
+
var UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
|
|
578
|
+
function nearestOption(message, options, allowNegative) {
|
|
579
|
+
const typed = UNKNOWN_OPTION.exec(message)?.[1];
|
|
580
|
+
if (typed === undefined)
|
|
581
|
+
return;
|
|
582
|
+
const word = typed.replace(DASHES, "");
|
|
583
|
+
if (word.length < 2 && !typed.startsWith("--"))
|
|
584
|
+
return;
|
|
585
|
+
const known = Object.entries(options).flatMap(([name, option]) => [
|
|
586
|
+
...option.short === undefined ? [] : [`-${option.short}`],
|
|
587
|
+
`--${name}`,
|
|
588
|
+
...allowNegative && option.type === "boolean" ? [`--no-${name}`] : []
|
|
589
|
+
]);
|
|
590
|
+
const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
|
|
591
|
+
return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
|
|
592
|
+
}
|
|
593
|
+
var WHOLE = /^\d+$/;
|
|
555
594
|
function wholeNumber(value, name, fallback, min = 1) {
|
|
556
595
|
if (value === undefined)
|
|
557
596
|
return fallback;
|
|
558
597
|
const number = Number(value);
|
|
559
|
-
if (!Number.
|
|
560
|
-
badUsage(`--${name} takes a whole number from ${min}, not ${value}`);
|
|
598
|
+
if (!WHOLE.test(value) || !Number.isSafeInteger(number) || number < min)
|
|
599
|
+
badUsage(`${name.startsWith("<") ? name : `--${name}`} takes a whole number from ${min}, not ${value}`);
|
|
561
600
|
return number;
|
|
562
601
|
}
|
|
563
602
|
var DURATION = /^(\d+(?:\.\d+)?)(ms|[smh])?$/;
|
|
@@ -582,229 +621,14 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
|
582
621
|
fail(`no file ${path}`);
|
|
583
622
|
return file.text();
|
|
584
623
|
}
|
|
585
|
-
// ../kit/src/
|
|
586
|
-
import
|
|
587
|
-
|
|
588
|
-
// ../kit/src/text.ts
|
|
589
|
-
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
590
|
-
function distance(a, b) {
|
|
591
|
-
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
592
|
-
for (const [i, left] of [...a].entries()) {
|
|
593
|
-
const current = [i + 1];
|
|
594
|
-
for (const [j, right] of [...b].entries())
|
|
595
|
-
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
596
|
-
previous = current;
|
|
597
|
-
}
|
|
598
|
-
return previous[b.length] ?? 0;
|
|
599
|
-
}
|
|
624
|
+
// ../kit/src/progress.ts
|
|
625
|
+
import process6 from "process";
|
|
600
626
|
|
|
601
|
-
// ../kit/src/cli.ts
|
|
602
|
-
var LEVEL_OPTIONS = {
|
|
603
|
-
...OUTPUT_OPTIONS,
|
|
604
|
-
help: { type: "boolean", short: "h", description: "show this help" }
|
|
605
|
-
};
|
|
606
|
-
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
607
|
-
function commandRows(commands, colors) {
|
|
608
|
-
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
609
|
-
const groups = new Map([["Commands:", []]]);
|
|
610
|
-
for (const [name, { summary, group }] of Object.entries(commands)) {
|
|
611
|
-
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
612
|
-
const rows = groups.get(heading) ?? [];
|
|
613
|
-
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summary)}`);
|
|
614
|
-
groups.set(heading, rows);
|
|
615
|
-
}
|
|
616
|
-
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
617
|
-
}
|
|
618
|
-
function takeOutputFlags(argv) {
|
|
619
|
-
let index = 0;
|
|
620
|
-
for (;index < argv.length; index++) {
|
|
621
|
-
const word = argv[index];
|
|
622
|
-
if (word === "--plain")
|
|
623
|
-
applyOutput(undefined, true);
|
|
624
|
-
else if (word.startsWith("--color="))
|
|
625
|
-
applyOutput(word.slice("--color=".length), undefined);
|
|
626
|
-
else if (word === "--color" && argv[index + 1] !== undefined)
|
|
627
|
-
applyOutput(argv[++index], undefined);
|
|
628
|
-
else
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
631
|
-
return argv.slice(index);
|
|
632
|
-
}
|
|
633
|
-
function closest(name, names) {
|
|
634
|
-
const started = names.find((known) => known.startsWith(name));
|
|
635
|
-
if (started !== undefined)
|
|
636
|
-
return started;
|
|
637
|
-
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
638
|
-
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
639
|
-
}
|
|
640
|
-
async function help({ path, header, summary, commands, defaultCommand, examples, replaces }, stream) {
|
|
641
|
-
const colors = paint(stream);
|
|
642
|
-
const prefix = path.join(" ");
|
|
643
|
-
if (wantsHelpJson())
|
|
644
|
-
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
645
|
-
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
646
|
-
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
647
|
-
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
648
|
-
return [
|
|
649
|
-
header(colors),
|
|
650
|
-
"",
|
|
651
|
-
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
652
|
-
...replacesRows(replaces, colors),
|
|
653
|
-
"",
|
|
654
|
-
...commandRows(commands, colors),
|
|
655
|
-
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
656
|
-
"",
|
|
657
|
-
colors.bold("Options:"),
|
|
658
|
-
...optionRows(path.length === 1 ? TOP_OPTIONS : LEVEL_OPTIONS, colors),
|
|
659
|
-
...exampleRows(examples ?? [], colors),
|
|
660
|
-
"",
|
|
661
|
-
`${colors.dim("Run")} ${prefix} ${colors.cyan("<command>")} --help ${colors.dim("for its options.")}`
|
|
662
|
-
].join(`
|
|
663
|
-
`);
|
|
664
|
-
}
|
|
665
|
-
function printError(prefix, message, hint, cause) {
|
|
666
|
-
const colors = paint(process5.stderr);
|
|
667
|
-
const styled = takesControlCodes(process5.stderr);
|
|
668
|
-
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
669
|
-
const under = (label, text, tint) => text === undefined ? "" : `
|
|
670
|
-
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
671
|
-
console.error(`${head} ${message}${under("cause", cause, colors.dim)}${under("hint", hint, colors.yellow)}`);
|
|
672
|
-
}
|
|
673
|
-
function runHint(command, effect) {
|
|
674
|
-
return `run \`${command}\`${effect === undefined ? "" : ` to ${effect}`}`;
|
|
675
|
-
}
|
|
676
|
-
function printHint(hint) {
|
|
677
|
-
const colors = paint(process5.stderr);
|
|
678
|
-
console.error(` ${takesControlCodes(process5.stderr) ? colors.yellow("hint") : "hint:"} ${hint}`);
|
|
679
|
-
}
|
|
680
|
-
function printJsonError(message, hint, cause) {
|
|
681
|
-
console.error(JSON.stringify({ error: message, ...hint === undefined ? {} : { hint }, ...cause === undefined ? {} : { cause } }));
|
|
682
|
-
}
|
|
683
|
-
function wantsJson(argv) {
|
|
684
|
-
const end = argv.indexOf("--");
|
|
685
|
-
return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
|
|
686
|
-
}
|
|
687
|
-
function reportThrown(prefix, error, redact = (text) => text, json = false) {
|
|
688
|
-
if (error instanceof HelpShown)
|
|
689
|
-
return 0;
|
|
690
|
-
const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
|
|
691
|
-
const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
|
|
692
|
-
const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
|
|
693
|
-
const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
|
|
694
|
-
report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
|
|
695
|
-
return error instanceof Exit ? error.code : 1;
|
|
696
|
-
}
|
|
697
|
-
async function dispatch(level, typed, { version, redact }) {
|
|
698
|
-
const prefix = level.path.join(" ");
|
|
699
|
-
let argv;
|
|
700
|
-
try {
|
|
701
|
-
argv = takeOutputFlags(typed);
|
|
702
|
-
} catch (error) {
|
|
703
|
-
return reportThrown(prefix, error);
|
|
704
|
-
}
|
|
705
|
-
const [name, ...args] = argv;
|
|
706
|
-
if (name === undefined && level.path.length > 1) {
|
|
707
|
-
console.error(await help(level, process5.stderr));
|
|
708
|
-
return 2;
|
|
709
|
-
}
|
|
710
|
-
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
711
|
-
console.log(await help(level, process5.stdout));
|
|
712
|
-
return 0;
|
|
713
|
-
}
|
|
714
|
-
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
715
|
-
console.log(version);
|
|
716
|
-
return 0;
|
|
717
|
-
}
|
|
718
|
-
let command = level.commands[level.aliases?.[name] ?? name];
|
|
719
|
-
let rest = args;
|
|
720
|
-
if (command === undefined && level.defaultCommand !== undefined) {
|
|
721
|
-
command = level.commands[level.defaultCommand];
|
|
722
|
-
rest = argv;
|
|
723
|
-
}
|
|
724
|
-
if (command === undefined) {
|
|
725
|
-
const meant = closest(name, Object.keys(level.commands));
|
|
726
|
-
printError(prefix, `no command ${name}`, meant === undefined ? `run ${prefix} --help for its commands` : `did you mean ${meant}?`);
|
|
727
|
-
return 2;
|
|
728
|
-
}
|
|
729
|
-
if ("commands" in command) {
|
|
730
|
-
const { summary } = command;
|
|
731
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${summary}`, summary, commands: command.commands, aliases: command.aliases, defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
|
|
732
|
-
}
|
|
733
|
-
try {
|
|
734
|
-
return await command.run(rest);
|
|
735
|
-
} catch (error) {
|
|
736
|
-
return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
var COMPLETE = "__complete";
|
|
740
|
-
var FILES = "__files__";
|
|
741
|
-
async function optionsOf(run) {
|
|
742
|
-
completion.active = true;
|
|
743
|
-
try {
|
|
744
|
-
await run([]);
|
|
745
|
-
} catch (error) {
|
|
746
|
-
if (error instanceof Completing)
|
|
747
|
-
return error.options;
|
|
748
|
-
} finally {
|
|
749
|
-
completion.active = false;
|
|
750
|
-
}
|
|
751
|
-
return {};
|
|
752
|
-
}
|
|
753
|
-
async function leafCandidates(run, words) {
|
|
754
|
-
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
755
|
-
return [FILES];
|
|
756
|
-
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
757
|
-
}
|
|
758
|
-
async function candidates(commands, aliases, defaultCommand, words) {
|
|
759
|
-
const [word = "", ...rest] = words;
|
|
760
|
-
const command = commands[aliases?.[word] ?? word];
|
|
761
|
-
if (rest.length === 0 && !word.startsWith("-"))
|
|
762
|
-
return Object.entries(commands).map(([name, { summary }]) => `${name} ${summary}`);
|
|
763
|
-
if (command === undefined) {
|
|
764
|
-
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
765
|
-
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
766
|
-
}
|
|
767
|
-
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
768
|
-
}
|
|
769
|
-
async function printCandidates(found) {
|
|
770
|
-
for (const line of await found)
|
|
771
|
-
console.log(line);
|
|
772
|
-
return 0;
|
|
773
|
-
}
|
|
774
|
-
async function runCli(cli, argv = process5.argv.slice(2)) {
|
|
775
|
-
if (argv[0] === COMPLETE)
|
|
776
|
-
return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
|
|
777
|
-
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${cli.about}`, summary: cli.about, commands: cli.commands, aliases: cli.aliases, defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
|
|
778
|
-
}
|
|
779
|
-
// ../kit/src/limit.ts
|
|
780
|
-
function limitOptions(fallback) {
|
|
781
|
-
return {
|
|
782
|
-
limit: { type: "string", description: `show at most this many rows, the rest counted (${fallback})` },
|
|
783
|
-
full: { type: "boolean", description: "show every row" }
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
function limitOf(values, fallback) {
|
|
787
|
-
if (values.full === true && values.limit !== undefined)
|
|
788
|
-
badUsage("--limit and --full exclude each other");
|
|
789
|
-
if (values.full === true || values.json === true && values.limit === undefined)
|
|
790
|
-
return Number.POSITIVE_INFINITY;
|
|
791
|
-
return wholeNumber(values.limit, "limit", fallback);
|
|
792
|
-
}
|
|
793
|
-
function moreFooter(more) {
|
|
794
|
-
return `${more} more, rerun with --full`;
|
|
795
|
-
}
|
|
796
|
-
function capped(rows, limit) {
|
|
797
|
-
const more = rows.length - limit;
|
|
798
|
-
return more > 0 ? { shown: rows.slice(0, limit), footer: moreFooter(more) } : { shown: [...rows], footer: undefined };
|
|
799
|
-
}
|
|
800
627
|
// ../kit/src/process.ts
|
|
801
628
|
import { constants } from "os";
|
|
802
|
-
import
|
|
629
|
+
import process5 from "process";
|
|
630
|
+
var KILL_GRACE_MS = 2000;
|
|
803
631
|
var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
804
|
-
async function collect(stream, name, chunks) {
|
|
805
|
-
for await (const data of stream)
|
|
806
|
-
chunks.push({ stream: name, data });
|
|
807
|
-
}
|
|
808
632
|
function exitCodeOf(code, signal) {
|
|
809
633
|
if (code !== null)
|
|
810
634
|
return code;
|
|
@@ -824,7 +648,7 @@ function holdSignals(cleanup) {
|
|
|
824
648
|
},
|
|
825
649
|
release: () => {
|
|
826
650
|
for (const signal of SIGNALS)
|
|
827
|
-
|
|
651
|
+
process5.off(signal, onSignal);
|
|
828
652
|
}
|
|
829
653
|
};
|
|
830
654
|
function onSignal(signal) {
|
|
@@ -837,54 +661,684 @@ function holdSignals(cleanup) {
|
|
|
837
661
|
return;
|
|
838
662
|
held.release();
|
|
839
663
|
cleanup(signal);
|
|
840
|
-
|
|
664
|
+
process5.kill(process5.pid, signal);
|
|
841
665
|
}
|
|
842
666
|
for (const signal of SIGNALS)
|
|
843
|
-
|
|
667
|
+
process5.on(signal, onSignal);
|
|
844
668
|
return held;
|
|
845
669
|
}
|
|
846
|
-
async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
|
|
670
|
+
async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal, maxBytes = Infinity, onChunk, onStart } = {}) {
|
|
847
671
|
const chunks = [];
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
672
|
+
const grouped = timeoutMs !== undefined || signal !== undefined || onStart !== undefined || onChunk !== undefined;
|
|
673
|
+
const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe", ...grouped ? { detached: true } : {} });
|
|
674
|
+
const state = { kept: 0, truncated: false, timedOut: false, failure: undefined, done: false };
|
|
675
|
+
const timers = [];
|
|
676
|
+
const killGroup = (killing) => {
|
|
677
|
+
if (state.done)
|
|
678
|
+
return;
|
|
679
|
+
try {
|
|
680
|
+
if (grouped)
|
|
681
|
+
process5.kill(-proc.pid, killing);
|
|
682
|
+
else
|
|
683
|
+
proc.kill(killing);
|
|
684
|
+
} catch {}
|
|
685
|
+
};
|
|
686
|
+
const stop = () => {
|
|
687
|
+
killGroup("SIGTERM");
|
|
688
|
+
timers.push(setTimeout(killGroup, KILL_GRACE_MS, "SIGKILL"));
|
|
689
|
+
};
|
|
690
|
+
const onAbort = () => stop();
|
|
691
|
+
if (timeoutMs !== undefined) {
|
|
692
|
+
timers.push(setTimeout(() => {
|
|
693
|
+
state.timedOut = true;
|
|
694
|
+
stop();
|
|
695
|
+
}, timeoutMs));
|
|
696
|
+
}
|
|
697
|
+
if (signal?.aborted === true)
|
|
698
|
+
onAbort();
|
|
699
|
+
else
|
|
700
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
701
|
+
onStart?.({ kill: (killing) => killGroup(killing) });
|
|
702
|
+
const drain = async (stream, name) => {
|
|
703
|
+
for await (const data of stream) {
|
|
704
|
+
if (state.failure !== undefined)
|
|
705
|
+
continue;
|
|
706
|
+
try {
|
|
707
|
+
onChunk?.({ stream: name, data });
|
|
708
|
+
} catch (error) {
|
|
709
|
+
state.failure = error;
|
|
710
|
+
stop();
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (state.kept + data.length > maxBytes) {
|
|
714
|
+
state.truncated = true;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
state.kept += data.length;
|
|
718
|
+
chunks.push({ stream: name, data });
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
try {
|
|
722
|
+
await Promise.all([drain(proc.stdout, "stdout"), drain(proc.stderr, "stderr")]);
|
|
723
|
+
await proc.exited;
|
|
724
|
+
} finally {
|
|
725
|
+
state.done = true;
|
|
726
|
+
for (const timer of timers)
|
|
727
|
+
clearTimeout(timer);
|
|
728
|
+
signal?.removeEventListener("abort", onAbort);
|
|
729
|
+
}
|
|
730
|
+
if (state.failure !== undefined)
|
|
731
|
+
throw state.failure;
|
|
732
|
+
const joined = () => Buffer.concat(chunks.map(({ data }) => data));
|
|
733
|
+
let output;
|
|
734
|
+
let text;
|
|
735
|
+
return {
|
|
736
|
+
code: exitCodeOf(proc.exitCode, proc.signalCode),
|
|
737
|
+
chunks,
|
|
738
|
+
timedOut: state.timedOut,
|
|
739
|
+
truncated: state.truncated,
|
|
740
|
+
get output() {
|
|
741
|
+
return output ??= joined();
|
|
742
|
+
},
|
|
743
|
+
get text() {
|
|
744
|
+
return text ??= (output ??= joined()).toString();
|
|
745
|
+
}
|
|
746
|
+
};
|
|
853
747
|
}
|
|
854
|
-
// package.json
|
|
855
|
-
var version = "0.6.0";
|
|
856
|
-
|
|
857
|
-
// src/args.ts
|
|
858
|
-
var repoOption = {
|
|
859
|
-
repo: { type: "string", short: "r", description: "owner/name of the repository, instead of the one origin points at" }
|
|
860
|
-
};
|
|
861
|
-
var ISSUE_HASH = /^#/;
|
|
862
|
-
function issueNumber(value, usage) {
|
|
863
|
-
if (value === undefined)
|
|
864
|
-
fail(`an issue number is needed
|
|
865
748
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
749
|
+
// ../kit/src/progress.ts
|
|
750
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
751
|
+
var FRAME_MS = 80;
|
|
752
|
+
var CLEAR_LINE = "\r\x1B[2K";
|
|
753
|
+
var LINE_UP = "\x1B[1A\x1B[2K";
|
|
754
|
+
var ESC = String.fromCharCode(27);
|
|
755
|
+
var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, "y");
|
|
756
|
+
var RESET_STYLE = `${ESC}[0m`;
|
|
757
|
+
var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
|
|
758
|
+
var CONSOLE_METHODS = ["log", "info", "warn", "error", "debug", "dir", "dirxml", "table", "trace", "group", "groupCollapsed", "count", "timeEnd", "timeLog", "assert"];
|
|
759
|
+
function liveRegion(render, stream = process6.stderr, env = process6.env) {
|
|
760
|
+
if (!takesControlCodes(stream, env))
|
|
761
|
+
return { redraw: () => {
|
|
762
|
+
return;
|
|
763
|
+
}, print: (text) => void stream.write(`${text}
|
|
764
|
+
`), stop: () => {
|
|
765
|
+
return;
|
|
766
|
+
} };
|
|
767
|
+
const write = stream.write.bind(stream);
|
|
768
|
+
let frame = 0;
|
|
769
|
+
let drawn = 0;
|
|
770
|
+
const clear = () => {
|
|
771
|
+
if (drawn > 0)
|
|
772
|
+
write(`${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}`);
|
|
773
|
+
drawn = 0;
|
|
774
|
+
};
|
|
775
|
+
const draw = () => {
|
|
776
|
+
const width = (stream.columns === undefined || stream.columns === 0 ? 80 : stream.columns) - 1;
|
|
777
|
+
const height = stream.rows === undefined || stream.rows === 0 ? 24 : stream.rows;
|
|
778
|
+
const lines = render(FRAMES[frame++ % FRAMES.length]).slice(0, Math.max(1, height - 1)).map((line) => cutToWidth(line, width));
|
|
779
|
+
if (lines.length === 0) {
|
|
780
|
+
clear();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
write(`${drawn > 0 ? `${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}` : CLEAR_LINE}${lines.join(`
|
|
784
|
+
`)}`);
|
|
785
|
+
drawn = lines.length;
|
|
786
|
+
};
|
|
787
|
+
const timer = setInterval(draw, FRAME_MS);
|
|
788
|
+
const restore = interceptOutput(clear);
|
|
789
|
+
const onExit = () => clear();
|
|
790
|
+
const signals = holdSignals(stop);
|
|
791
|
+
function stop() {
|
|
792
|
+
clearInterval(timer);
|
|
793
|
+
clear();
|
|
794
|
+
restore();
|
|
795
|
+
process6.off("exit", onExit);
|
|
796
|
+
signals.release();
|
|
797
|
+
}
|
|
798
|
+
timer.unref();
|
|
799
|
+
process6.on("exit", onExit);
|
|
800
|
+
return {
|
|
801
|
+
redraw: draw,
|
|
802
|
+
print: (text) => {
|
|
803
|
+
clear();
|
|
804
|
+
write(`${text}
|
|
805
|
+
`);
|
|
806
|
+
draw();
|
|
807
|
+
},
|
|
808
|
+
stop
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function cutToWidth(line, width) {
|
|
812
|
+
if (Bun.stringWidth(line) <= width)
|
|
813
|
+
return line;
|
|
814
|
+
let room = width;
|
|
815
|
+
let kept = "";
|
|
816
|
+
let index = 0;
|
|
817
|
+
let linked = false;
|
|
818
|
+
while (index < line.length) {
|
|
819
|
+
ESCAPE_CODE.lastIndex = index;
|
|
820
|
+
const escape = ESCAPE_CODE.exec(line);
|
|
821
|
+
if (escape !== null) {
|
|
822
|
+
kept += escape[0];
|
|
823
|
+
linked ||= escape[0].startsWith(`${ESC}]`);
|
|
824
|
+
index = ESCAPE_CODE.lastIndex;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const char = String.fromCodePoint(line.codePointAt(index));
|
|
828
|
+
const columns = Bun.stringWidth(char);
|
|
829
|
+
if (columns > room)
|
|
830
|
+
break;
|
|
831
|
+
kept += char;
|
|
832
|
+
room -= columns;
|
|
833
|
+
index += char.length;
|
|
834
|
+
}
|
|
835
|
+
return `${kept}${RESET_STYLE}${linked ? CLOSE_LINK : ""}`;
|
|
836
|
+
}
|
|
837
|
+
function interceptOutput(clear) {
|
|
838
|
+
const targets = [...CONSOLE_METHODS.map((name) => [console, name]), [process6.stdout, "write"], [process6.stderr, "write"]];
|
|
839
|
+
const wrapped = targets.map(([target, name]) => {
|
|
840
|
+
const original = Reflect.get(target, name);
|
|
841
|
+
const wrapper = (...args) => {
|
|
842
|
+
clear();
|
|
843
|
+
return original.apply(target, args);
|
|
844
|
+
};
|
|
845
|
+
Reflect.set(target, name, wrapper);
|
|
846
|
+
return { target, name, original, wrapper };
|
|
847
|
+
});
|
|
848
|
+
return () => {
|
|
849
|
+
for (const { target, name, original, wrapper } of wrapped) {
|
|
850
|
+
if (Reflect.get(target, name) === wrapper)
|
|
851
|
+
Reflect.set(target, name, original);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ../kit/src/tool.ts
|
|
857
|
+
import process7 from "process";
|
|
858
|
+
var PERMANENT = /\bHTTP (?:401|403|404)\b|unauthorized|forbidden|not found|couldn't be found|token is required/i;
|
|
859
|
+
function cwdGone() {
|
|
860
|
+
try {
|
|
861
|
+
process7.cwd();
|
|
862
|
+
return false;
|
|
863
|
+
} catch {
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
class SpawnError extends Error {
|
|
869
|
+
missing;
|
|
870
|
+
constructor(message, missing) {
|
|
871
|
+
super(message);
|
|
872
|
+
this.missing = missing;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function spawnFailure(command, error) {
|
|
876
|
+
const gone = cwdGone();
|
|
877
|
+
const missing = !gone && error?.code === "ENOENT";
|
|
878
|
+
return new SpawnError(`${command}: ${error instanceof Error ? error.message : String(error)}${gone ? " (the working directory was removed)" : ""}`, missing);
|
|
879
|
+
}
|
|
880
|
+
function isPermanentFailure(error) {
|
|
881
|
+
if (!(error instanceof Error))
|
|
882
|
+
return false;
|
|
883
|
+
return error instanceof SpawnError && error.missing || PERMANENT.test(error.message);
|
|
884
|
+
}
|
|
885
|
+
async function runTool(label, argv, { timeoutMs, cwd, onStart } = {}) {
|
|
886
|
+
let ran;
|
|
887
|
+
try {
|
|
888
|
+
ran = await runCommand(argv, { env: process7.env, ...timeoutMs === undefined ? {} : { timeoutMs }, ...cwd === undefined ? {} : { cwd }, ...onStart === undefined ? {} : { onStart } });
|
|
889
|
+
} catch (error) {
|
|
890
|
+
throw spawnFailure(label, error);
|
|
891
|
+
}
|
|
892
|
+
if (ran.timedOut)
|
|
893
|
+
throw new Error(`${label}: no answer in ${formatDuration(timeoutMs ?? 0)}`);
|
|
894
|
+
const decoder = new TextDecoder;
|
|
895
|
+
const streams = { stdout: "", stderr: "" };
|
|
896
|
+
for (const { stream, data } of ran.chunks)
|
|
897
|
+
streams[stream] += decoder.decode(data, { stream: true });
|
|
898
|
+
return { code: ran.code, ...streams };
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ../kit/src/ci.ts
|
|
902
|
+
var POLL = 15000;
|
|
903
|
+
async function waitForCi(wait) {
|
|
904
|
+
const { tool, short, giveUpMs, noneMs, stalledMs, pollMs = POLL } = wait;
|
|
905
|
+
const started = Date.now();
|
|
906
|
+
let seen = started;
|
|
907
|
+
let moving = started;
|
|
908
|
+
let followed = "";
|
|
909
|
+
let found;
|
|
910
|
+
const region = wait.lines === undefined ? undefined : liveRegion((spinner) => wait.lines?.(spinner, found, started) ?? []);
|
|
911
|
+
const suffix = () => found === undefined ? "" : `: ${wait.links(found)}`;
|
|
912
|
+
async function stallOf(current) {
|
|
913
|
+
try {
|
|
914
|
+
return await wait.stall?.(current);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
if (error instanceof Exit || isPermanentFailure(error))
|
|
917
|
+
throw error;
|
|
918
|
+
console.warn(`${tool}: ${error instanceof Error ? error.message : String(error)}, polling again`);
|
|
919
|
+
return "unknown";
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
for (;; ) {
|
|
924
|
+
if (Date.now() - started > giveUpMs)
|
|
925
|
+
fail(`CI has not finished on ${short} in ${formatDuration(giveUpMs)}${suffix()}`);
|
|
926
|
+
try {
|
|
927
|
+
found = await wait.covering();
|
|
928
|
+
} catch (error) {
|
|
929
|
+
if (error instanceof Exit)
|
|
930
|
+
throw error;
|
|
931
|
+
if (cwdGone())
|
|
932
|
+
fail(`the working directory was removed while waiting for CI on ${short}${suffix()}`);
|
|
933
|
+
if (isPermanentFailure(error))
|
|
934
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
935
|
+
console.warn(`${tool}: ${error instanceof Error ? error.message : String(error)}, polling again`);
|
|
936
|
+
const latest = found === undefined ? undefined : await wait.settle(found);
|
|
937
|
+
if (latest !== undefined)
|
|
938
|
+
return latest;
|
|
939
|
+
await Bun.sleep(pollMs);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (found !== undefined) {
|
|
943
|
+
seen = Date.now();
|
|
944
|
+
const key = wait.key(found);
|
|
945
|
+
if (key !== followed) {
|
|
946
|
+
followed = key;
|
|
947
|
+
moving = Date.now();
|
|
948
|
+
wait.announce(found);
|
|
949
|
+
}
|
|
950
|
+
if (wait.finished(found))
|
|
951
|
+
return found;
|
|
952
|
+
const stall = await stallOf(found);
|
|
953
|
+
if (stall === undefined)
|
|
954
|
+
moving = Date.now();
|
|
955
|
+
else if (stall !== "unknown" && (stall.certain === true || Date.now() - moving > stalledMs))
|
|
956
|
+
fail(stall.message);
|
|
957
|
+
} else if (Date.now() - seen > noneMs) {
|
|
958
|
+
fail(wait.noneMessage);
|
|
959
|
+
}
|
|
960
|
+
await Bun.sleep(pollMs);
|
|
961
|
+
}
|
|
962
|
+
} finally {
|
|
963
|
+
region?.stop();
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// ../kit/src/cli.ts
|
|
967
|
+
import process8 from "process";
|
|
968
|
+
var LEVEL_OPTIONS = {
|
|
969
|
+
...OUTPUT_OPTIONS,
|
|
970
|
+
help: { type: "boolean", short: "h", description: "show this help" }
|
|
971
|
+
};
|
|
972
|
+
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
973
|
+
async function commandRows(commands, colors) {
|
|
974
|
+
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
975
|
+
const groups = new Map([["Commands:", []]]);
|
|
976
|
+
const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
|
|
977
|
+
for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
|
|
978
|
+
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
979
|
+
const rows = groups.get(heading) ?? [];
|
|
980
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
|
|
981
|
+
groups.set(heading, rows);
|
|
982
|
+
}
|
|
983
|
+
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
984
|
+
}
|
|
985
|
+
function takeOutputFlags(argv) {
|
|
986
|
+
let index = 0;
|
|
987
|
+
for (;index < argv.length; index++) {
|
|
988
|
+
const word = argv[index];
|
|
989
|
+
if (word === "--plain")
|
|
990
|
+
applyOutput(undefined, true);
|
|
991
|
+
else if (word.startsWith("--color="))
|
|
992
|
+
applyOutput(word.slice("--color=".length), undefined);
|
|
993
|
+
else if (word === "--color" && argv[index + 1] !== undefined)
|
|
994
|
+
applyOutput(argv[++index], undefined);
|
|
995
|
+
else
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
return argv.slice(index);
|
|
999
|
+
}
|
|
1000
|
+
async function resolveSummary(summary) {
|
|
1001
|
+
return typeof summary === "string" ? summary : summary();
|
|
1002
|
+
}
|
|
1003
|
+
function scopeOf(commands, extra, aliases) {
|
|
1004
|
+
return { commands: typeof commands === "function" ? {} : commands, lazy: [...typeof commands === "function" ? [commands] : [], ...extra === undefined ? [] : [extra]], aliases };
|
|
1005
|
+
}
|
|
1006
|
+
async function expand(scope, words) {
|
|
1007
|
+
if (scope.lazy.length === 0)
|
|
1008
|
+
return scope;
|
|
1009
|
+
const commands = { ...scope.commands };
|
|
1010
|
+
for (const added of await Promise.all(scope.lazy.map(async (lazy) => lazy(words)))) {
|
|
1011
|
+
for (const [name, command] of Object.entries(added))
|
|
1012
|
+
commands[name] ??= command;
|
|
1013
|
+
}
|
|
1014
|
+
return { ...scope, commands, lazy: [] };
|
|
1015
|
+
}
|
|
1016
|
+
async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
|
|
1017
|
+
const colors = paint(stream);
|
|
1018
|
+
const prefix = path.join(" ");
|
|
1019
|
+
const summary = await resolveSummary(lazySummary);
|
|
1020
|
+
if (wantsHelpJson())
|
|
1021
|
+
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
1022
|
+
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
1023
|
+
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
1024
|
+
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
1025
|
+
return [
|
|
1026
|
+
header(colors, summary),
|
|
1027
|
+
"",
|
|
1028
|
+
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
1029
|
+
...replacesRows(replaces, colors),
|
|
1030
|
+
"",
|
|
1031
|
+
...await commandRows(commands, colors),
|
|
1032
|
+
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
1033
|
+
"",
|
|
1034
|
+
colors.bold("Options:"),
|
|
1035
|
+
...optionRows(path.length === 1 ? TOP_OPTIONS : LEVEL_OPTIONS, colors),
|
|
1036
|
+
...exampleRows(examples ?? [], colors),
|
|
1037
|
+
"",
|
|
1038
|
+
`${colors.dim("Run")} ${prefix} ${colors.cyan("<command>")} --help ${colors.dim("for its options.")}`
|
|
1039
|
+
].join(`
|
|
1040
|
+
`);
|
|
1041
|
+
}
|
|
1042
|
+
function printError(prefix, message, hint, cause) {
|
|
1043
|
+
const colors = paint(process8.stderr);
|
|
1044
|
+
const styled = takesControlCodes(process8.stderr);
|
|
1045
|
+
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
1046
|
+
const under = (label, text, tint) => text === undefined ? "" : `
|
|
1047
|
+
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
1048
|
+
console.error(`${head} ${message}${under("cause", cause, colors.dim)}${under("hint", hint, colors.yellow)}`);
|
|
1049
|
+
}
|
|
1050
|
+
function runHint(command, effect) {
|
|
1051
|
+
return `run \`${command}\`${effect === undefined ? "" : ` to ${effect}`}`;
|
|
1052
|
+
}
|
|
1053
|
+
function printHint(hint) {
|
|
1054
|
+
const colors = paint(process8.stderr);
|
|
1055
|
+
console.error(` ${takesControlCodes(process8.stderr) ? colors.yellow("hint") : "hint:"} ${hint}`);
|
|
1056
|
+
}
|
|
1057
|
+
function printJsonError(message, hint, cause) {
|
|
1058
|
+
console.error(JSON.stringify({ error: message, ...hint === undefined ? {} : { hint }, ...cause === undefined ? {} : { cause } }));
|
|
1059
|
+
}
|
|
1060
|
+
function wantsJson(argv) {
|
|
1061
|
+
const end = argv.indexOf("--");
|
|
1062
|
+
return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
|
|
1063
|
+
}
|
|
1064
|
+
function reportThrown(prefix, error, redact = (text) => text, json = false) {
|
|
1065
|
+
if (error instanceof HelpShown)
|
|
1066
|
+
return 0;
|
|
1067
|
+
const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
|
|
1068
|
+
const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
|
|
1069
|
+
const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
|
|
1070
|
+
const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
|
|
1071
|
+
report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
|
|
1072
|
+
return error instanceof Exit ? error.code : 1;
|
|
1073
|
+
}
|
|
1074
|
+
async function dispatch(level, typed, { version, redact }) {
|
|
1075
|
+
const prefix = level.path.join(" ");
|
|
1076
|
+
let argv;
|
|
1077
|
+
try {
|
|
1078
|
+
argv = takeOutputFlags(typed);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
return reportThrown(prefix, error);
|
|
1081
|
+
}
|
|
1082
|
+
const [name, ...args] = argv;
|
|
1083
|
+
if (name === undefined && level.path.length > 1) {
|
|
1084
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process8.stderr));
|
|
1085
|
+
return 2;
|
|
1086
|
+
}
|
|
1087
|
+
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
1088
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process8.stdout));
|
|
1089
|
+
return 0;
|
|
1090
|
+
}
|
|
1091
|
+
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
1092
|
+
console.log(version);
|
|
1093
|
+
return 0;
|
|
1094
|
+
}
|
|
1095
|
+
let command = level.commands[level.aliases?.[name] ?? name];
|
|
1096
|
+
let rest = args;
|
|
1097
|
+
if (command === undefined && level.lazy.length > 0) {
|
|
1098
|
+
level = await expand(level, [...level.path.slice(1), ...argv]);
|
|
1099
|
+
command = level.commands[level.aliases?.[name] ?? name];
|
|
1100
|
+
}
|
|
1101
|
+
if (command === undefined && level.defaultCommand !== undefined) {
|
|
1102
|
+
command = level.commands[level.defaultCommand];
|
|
1103
|
+
rest = argv;
|
|
1104
|
+
}
|
|
1105
|
+
if (command === undefined) {
|
|
1106
|
+
const meant = closest(name, Object.keys(level.commands));
|
|
1107
|
+
printError(prefix, `no command ${name}`, meant === undefined ? `run ${prefix} --help for its commands` : `did you mean ${meant}?`);
|
|
1108
|
+
return 2;
|
|
1109
|
+
}
|
|
1110
|
+
if ("commands" in command) {
|
|
1111
|
+
const { summary } = command;
|
|
1112
|
+
return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
return await command.run(rest);
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
var COMPLETE = "__complete";
|
|
1121
|
+
var FILES = "__files__";
|
|
1122
|
+
async function optionsOf(run) {
|
|
1123
|
+
completion.active = true;
|
|
1124
|
+
try {
|
|
1125
|
+
await run([]);
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
if (error instanceof Completing)
|
|
1128
|
+
return error.options;
|
|
1129
|
+
} finally {
|
|
1130
|
+
completion.active = false;
|
|
1131
|
+
}
|
|
1132
|
+
return {};
|
|
1133
|
+
}
|
|
1134
|
+
async function leafCandidates(run, words) {
|
|
1135
|
+
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
1136
|
+
return [FILES];
|
|
1137
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
1138
|
+
}
|
|
1139
|
+
async function candidates(scope, defaultCommand, words, before = []) {
|
|
1140
|
+
const [word = "", ...rest] = words;
|
|
1141
|
+
const listing = rest.length === 0 && !word.startsWith("-");
|
|
1142
|
+
let { commands, aliases } = scope;
|
|
1143
|
+
let command = commands[aliases?.[word] ?? word];
|
|
1144
|
+
if (listing || command === undefined) {
|
|
1145
|
+
({ commands, aliases } = await expand(scope, [...before, ...words]));
|
|
1146
|
+
command = commands[aliases?.[word] ?? word];
|
|
1147
|
+
}
|
|
1148
|
+
if (listing)
|
|
1149
|
+
return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
|
|
1150
|
+
if (command === undefined) {
|
|
1151
|
+
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
1152
|
+
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
1153
|
+
}
|
|
1154
|
+
return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
|
|
1155
|
+
}
|
|
1156
|
+
async function printCandidates(found) {
|
|
1157
|
+
for (const line of await found)
|
|
1158
|
+
console.log(line);
|
|
1159
|
+
return 0;
|
|
1160
|
+
}
|
|
1161
|
+
async function runCli(cli, argv = process8.argv.slice(2)) {
|
|
1162
|
+
if (argv[0] === COMPLETE)
|
|
1163
|
+
return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
|
|
1164
|
+
return dispatch({ path: [cli.name], header: (colors, about) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${about}`, summary: cli.about, ...scopeOf(cli.commands, cli.extraCommands, cli.aliases), defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
|
|
1165
|
+
}
|
|
1166
|
+
// ../kit/src/git.ts
|
|
1167
|
+
import { basename, dirname } from "path";
|
|
1168
|
+
import process9 from "process";
|
|
1169
|
+
var ORIGIN_PREFIX = /^origin\//;
|
|
1170
|
+
var GIT_NETWORK_MS = 60000;
|
|
1171
|
+
async function git(args, timeoutMs) {
|
|
1172
|
+
const result = await runTool(`git ${args.join(" ")}`, ["git", ...args], { timeoutMs });
|
|
1173
|
+
if (result.code !== 0)
|
|
1174
|
+
throw new Error(`git ${args.join(" ")}: ${result.stderr.trim() || `exit ${result.code}`}${cwdGone() ? " (the working directory was removed)" : ""}`);
|
|
1175
|
+
return result.stdout.trim();
|
|
1176
|
+
}
|
|
1177
|
+
async function gitOk(args) {
|
|
1178
|
+
return (await runTool(`git ${args.join(" ")}`, ["git", ...args])).code === 0;
|
|
1179
|
+
}
|
|
1180
|
+
async function resolveCommit(ref) {
|
|
1181
|
+
const sha = (await runTool("git rev-parse", ["git", "rev-parse", "--verify", "-q", "--end-of-options", `${ref}^{commit}`])).stdout.trim();
|
|
1182
|
+
if (sha === "")
|
|
1183
|
+
fail(`no commit ${ref}`);
|
|
1184
|
+
return sha;
|
|
1185
|
+
}
|
|
1186
|
+
async function fetchBranch(branch) {
|
|
1187
|
+
if (branch.startsWith("-") || !await gitOk(["check-ref-format", "--branch", branch]))
|
|
1188
|
+
fail(`${branch} is not a branch name`);
|
|
1189
|
+
await git(["fetch", "-q", "origin", `refs/heads/${branch}`], GIT_NETWORK_MS);
|
|
1190
|
+
}
|
|
1191
|
+
async function defaultBranch() {
|
|
1192
|
+
const head = (await runTool("git symbolic-ref", ["git", "symbolic-ref", "--short", "-q", "refs/remotes/origin/HEAD"])).stdout.trim();
|
|
1193
|
+
return head === "" ? "main" : head.replace(ORIGIN_PREFIX, "");
|
|
1194
|
+
}
|
|
1195
|
+
async function leaveWorktree() {
|
|
1196
|
+
const common = await git(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
1197
|
+
if (basename(common) === ".git")
|
|
1198
|
+
process9.chdir(dirname(common));
|
|
1199
|
+
}
|
|
1200
|
+
// ../kit/src/limit.ts
|
|
1201
|
+
function limitOptions(fallback) {
|
|
1202
|
+
return {
|
|
1203
|
+
limit: { type: "string", description: `show at most this many rows, the rest counted (${fallback})` },
|
|
1204
|
+
full: { type: "boolean", description: "show every row" }
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
function limitOf(values, fallback) {
|
|
1208
|
+
if (values.full === true && values.limit !== undefined)
|
|
1209
|
+
badUsage("--limit and --full exclude each other");
|
|
1210
|
+
if (values.full === true || values.json === true && values.limit === undefined)
|
|
1211
|
+
return Number.POSITIVE_INFINITY;
|
|
1212
|
+
return wholeNumber(values.limit, "limit", fallback);
|
|
1213
|
+
}
|
|
1214
|
+
function moreFooter(more) {
|
|
1215
|
+
return `${more} more, rerun with --full`;
|
|
1216
|
+
}
|
|
1217
|
+
function capped(rows, limit) {
|
|
1218
|
+
const more = rows.length - limit;
|
|
1219
|
+
return more > 0 ? { shown: rows.slice(0, limit), footer: moreFooter(more) } : { shown: [...rows], footer: undefined };
|
|
1220
|
+
}
|
|
1221
|
+
// ../kit/src/logs.ts
|
|
1222
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
1223
|
+
// ../kit/src/secrets.ts
|
|
1224
|
+
var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
|
|
1225
|
+
// ../kit/src/sessions.ts
|
|
1226
|
+
import { homedir } from "os";
|
|
1227
|
+
import { join } from "path";
|
|
1228
|
+
import process10 from "process";
|
|
1229
|
+
var {Glob } = globalThis.Bun;
|
|
1230
|
+
var SESSION_FILES = new Glob("*.json");
|
|
1231
|
+
var ACCOUNT_DIRS = new Glob("*/sessions");
|
|
1232
|
+
async function sessionDirs() {
|
|
1233
|
+
const home = homedir();
|
|
1234
|
+
const dirs = new Set([join(home, ".claude", "sessions")]);
|
|
1235
|
+
if (process10.env.CLAUDE_CONFIG_DIR !== undefined)
|
|
1236
|
+
dirs.add(join(process10.env.CLAUDE_CONFIG_DIR, "sessions"));
|
|
1237
|
+
try {
|
|
1238
|
+
for await (const dir of ACCOUNT_DIRS.scan({ cwd: join(home, ".claude-accounts"), onlyFiles: false, absolute: true }))
|
|
1239
|
+
dirs.add(dir);
|
|
1240
|
+
} catch {}
|
|
1241
|
+
return [...dirs];
|
|
1242
|
+
}
|
|
1243
|
+
async function procStat(pid) {
|
|
1244
|
+
try {
|
|
1245
|
+
const stat = await Bun.file(`/proc/${pid}/stat`).text();
|
|
1246
|
+
return stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
1247
|
+
} catch {
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function isAlive(pid, procStart) {
|
|
1252
|
+
const stat = await procStat(pid);
|
|
1253
|
+
if (stat === undefined)
|
|
1254
|
+
return false;
|
|
1255
|
+
return procStart === undefined || stat[19] === procStart;
|
|
1256
|
+
}
|
|
1257
|
+
async function liveSessions() {
|
|
1258
|
+
const sessions = new Map;
|
|
1259
|
+
for (const dir of await sessionDirs()) {
|
|
1260
|
+
let files;
|
|
1261
|
+
try {
|
|
1262
|
+
files = await Array.fromAsync(SESSION_FILES.scan({ cwd: dir, absolute: true }));
|
|
1263
|
+
} catch {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
for (const file of files) {
|
|
1267
|
+
let entry;
|
|
1268
|
+
try {
|
|
1269
|
+
entry = await Bun.file(file).json();
|
|
1270
|
+
} catch {
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
if (entry.name === undefined || entry.pid === undefined || sessions.has(entry.pid) || !await isAlive(entry.pid, entry.procStart))
|
|
1274
|
+
continue;
|
|
1275
|
+
sessions.set(entry.pid, { name: entry.name, pid: entry.pid, status: entry.status ?? "unknown", cwd: entry.cwd ?? "" });
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return [...sessions.values()];
|
|
1279
|
+
}
|
|
1280
|
+
async function currentSession(sessions) {
|
|
1281
|
+
const byPid = new Map(sessions.map((session) => [session.pid, session]));
|
|
1282
|
+
let pid = process10.ppid;
|
|
1283
|
+
for (let depth = 0;depth < 32 && pid > 1; depth++) {
|
|
1284
|
+
const session = byPid.get(pid);
|
|
1285
|
+
if (session !== undefined)
|
|
1286
|
+
return session;
|
|
1287
|
+
const parent = Number((await procStat(pid))?.[1]);
|
|
1288
|
+
if (!Number.isInteger(parent))
|
|
1289
|
+
return;
|
|
1290
|
+
pid = parent;
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
// package.json
|
|
1295
|
+
var version = "0.7.1";
|
|
1296
|
+
|
|
1297
|
+
// src/args.ts
|
|
1298
|
+
var repoOption = {
|
|
1299
|
+
repo: { type: "string", short: "r", description: "owner/name of the repository, instead of the one origin points at" }
|
|
1300
|
+
};
|
|
1301
|
+
var ISSUE_HASH = /^#/;
|
|
1302
|
+
function issueNumber(value, usage) {
|
|
1303
|
+
if (value === undefined)
|
|
1304
|
+
fail(`an issue number is needed
|
|
1305
|
+
|
|
1306
|
+
${usage}`);
|
|
1307
|
+
return wholeNumber(value.replace(ISSUE_HASH, ""), "<issue>", 0);
|
|
1308
|
+
}
|
|
1309
|
+
function issueNumbers(values, usage) {
|
|
870
1310
|
return values.length === 0 ? [issueNumber(undefined, usage)] : values.map((value) => issueNumber(value, usage));
|
|
871
1311
|
}
|
|
872
1312
|
|
|
873
1313
|
// src/checklist.ts
|
|
874
|
-
var BOX = /^(\s*[-*+] \[)([ x])(\]\s)(
|
|
875
|
-
var FENCE = /^\s*(
|
|
1314
|
+
var BOX = /^(\s*[-*+] \[)([ x])(\]\s)(.*?)\r?$/is;
|
|
1315
|
+
var FENCE = /^\s*(`{3,}|~{3,})/;
|
|
1316
|
+
var LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s/;
|
|
1317
|
+
var INDENT_CODE = /^(?: {4}|\t)/;
|
|
876
1318
|
function boxes(body) {
|
|
877
1319
|
const found = [];
|
|
878
|
-
let
|
|
1320
|
+
let fence;
|
|
1321
|
+
let inList = false;
|
|
879
1322
|
for (const [line, text] of body.split(`
|
|
880
1323
|
`).entries()) {
|
|
881
|
-
|
|
882
|
-
|
|
1324
|
+
const marker = FENCE.exec(text)?.[1];
|
|
1325
|
+
if (fence !== undefined) {
|
|
1326
|
+
if (marker?.startsWith(fence[0] ?? "") === true && marker.length >= fence.length && text.trim() === marker)
|
|
1327
|
+
fence = undefined;
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (marker !== undefined) {
|
|
1331
|
+
fence = marker;
|
|
883
1332
|
continue;
|
|
884
1333
|
}
|
|
885
|
-
const
|
|
1334
|
+
const blank = text.trim() === "";
|
|
1335
|
+
const match = INDENT_CODE.test(text) && !inList ? null : BOX.exec(text);
|
|
886
1336
|
if (match !== null)
|
|
887
1337
|
found.push({ number: found.length + 1, line, done: match[2] !== " ", text: match[4] ?? "" });
|
|
1338
|
+
if (LIST_ITEM.test(text))
|
|
1339
|
+
inList = true;
|
|
1340
|
+
else if (!blank && !text.startsWith(" ") && !text.startsWith("\t"))
|
|
1341
|
+
inList = false;
|
|
888
1342
|
}
|
|
889
1343
|
return found;
|
|
890
1344
|
}
|
|
@@ -917,13 +1371,17 @@ function numberedBody(body) {
|
|
|
917
1371
|
return lines.join(`
|
|
918
1372
|
`).trim();
|
|
919
1373
|
}
|
|
1374
|
+
var NUMBERED_PICK = /^(\d+):(.+)$/s;
|
|
1375
|
+
var WHOLE_PICK = /^\d+$/;
|
|
920
1376
|
function setBoxes(body, picks, done) {
|
|
921
1377
|
const all = boxes(body);
|
|
922
1378
|
const chosen = new Map;
|
|
923
1379
|
for (const pick of picks) {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const
|
|
1380
|
+
if (pick.trim() === "")
|
|
1381
|
+
throw new Error("an empty box pick matches every box");
|
|
1382
|
+
const guarded = NUMBERED_PICK.exec(pick);
|
|
1383
|
+
const needle = (guarded?.[2] ?? pick).trim().toLowerCase();
|
|
1384
|
+
const matches = guarded === null && !WHOLE_PICK.test(pick) ? all.filter((box) => box.text.toLowerCase().includes(needle)) : all.filter((box) => box.number === Number(guarded?.[1] ?? pick) && (guarded === null || box.text.toLowerCase().startsWith(needle)));
|
|
927
1385
|
if (matches.length === 0)
|
|
928
1386
|
throw new Error(`no box matches ${pick}`);
|
|
929
1387
|
if (matches.length > 1)
|
|
@@ -961,8 +1419,8 @@ function tickReferences(body, wanted) {
|
|
|
961
1419
|
}
|
|
962
1420
|
|
|
963
1421
|
// src/claims.ts
|
|
964
|
-
var TAKEN = /^\s*taken by session ([\w.@:-]+?)\.?\s*$/
|
|
965
|
-
var RELEASED = /^\s*released by session ([\w.@:-]+?)\.?\s*$/
|
|
1422
|
+
var TAKEN = /^\s*taken by session ([\w.@:-]+?)\.?\s*$/i;
|
|
1423
|
+
var RELEASED = /^\s*released by session ([\w.@:-]+?)\.?\s*$/i;
|
|
966
1424
|
var LANE = /^lane-([^-]+)-/;
|
|
967
1425
|
function takenComment(name) {
|
|
968
1426
|
return `Taken by session ${name}.`;
|
|
@@ -970,9 +1428,12 @@ function takenComment(name) {
|
|
|
970
1428
|
function releasedComment(name) {
|
|
971
1429
|
return `Released by session ${name}.`;
|
|
972
1430
|
}
|
|
973
|
-
function
|
|
1431
|
+
function isClaimant(comment, author) {
|
|
1432
|
+
return author === undefined || comment.user === undefined || comment.user.login === author;
|
|
1433
|
+
}
|
|
1434
|
+
function latestClaim(comments, author) {
|
|
974
1435
|
let claim;
|
|
975
|
-
for (const comment of
|
|
1436
|
+
for (const comment of comments.filter((each) => isClaimant(each, author)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))) {
|
|
976
1437
|
const taken = TAKEN.exec(comment.body)?.[1];
|
|
977
1438
|
if (taken !== undefined) {
|
|
978
1439
|
claim = { holder: taken, at: comment.created_at };
|
|
@@ -999,80 +1460,15 @@ function holderSession(holder, sessions) {
|
|
|
999
1460
|
}
|
|
1000
1461
|
|
|
1001
1462
|
// src/tea.ts
|
|
1002
|
-
var
|
|
1003
|
-
|
|
1004
|
-
// src/git.ts
|
|
1005
|
-
import { basename, dirname } from "path";
|
|
1006
|
-
import process8 from "process";
|
|
1007
|
-
var {$ } = globalThis.Bun;
|
|
1008
|
-
var ORIGIN_PREFIX = /^origin\//;
|
|
1009
|
-
function spawnFailure(command, error) {
|
|
1010
|
-
return new Error(`${command}: ${error instanceof Error ? error.message : String(error)}${cwdGone() ? " (the working directory was removed)" : ""}`);
|
|
1011
|
-
}
|
|
1012
|
-
async function git(args) {
|
|
1013
|
-
let result;
|
|
1014
|
-
try {
|
|
1015
|
-
result = await $`git ${args}`.quiet().nothrow();
|
|
1016
|
-
} catch (error) {
|
|
1017
|
-
throw spawnFailure(`git ${args.join(" ")}`, error);
|
|
1018
|
-
}
|
|
1019
|
-
if (result.exitCode !== 0)
|
|
1020
|
-
throw new Error(`git ${args.join(" ")}: ${result.stderr.toString().trim() || `exit ${result.exitCode}`}`);
|
|
1021
|
-
return result.stdout.toString().trim();
|
|
1022
|
-
}
|
|
1023
|
-
async function gitOk(args) {
|
|
1024
|
-
return (await $`git ${args}`.quiet().nothrow()).exitCode === 0;
|
|
1025
|
-
}
|
|
1026
|
-
async function resolveCommit(ref) {
|
|
1027
|
-
const sha = (await $`git rev-parse --verify -q ${`${ref}^{commit}`}`.quiet().nothrow().text()).trim();
|
|
1028
|
-
if (sha === "")
|
|
1029
|
-
fail(`no commit ${ref}`);
|
|
1030
|
-
return sha;
|
|
1031
|
-
}
|
|
1032
|
-
async function defaultBranch() {
|
|
1033
|
-
const head = (await $`git symbolic-ref --short -q refs/remotes/origin/HEAD`.quiet().nothrow().text()).trim();
|
|
1034
|
-
return head === "" ? "main" : head.replace(ORIGIN_PREFIX, "");
|
|
1035
|
-
}
|
|
1036
|
-
async function tagsAt(commit) {
|
|
1037
|
-
return (await git(["tag", "--points-at", commit])).split(`
|
|
1038
|
-
`).filter(Boolean);
|
|
1039
|
-
}
|
|
1040
|
-
async function commitsNaming(issue, ref) {
|
|
1041
|
-
const out = await git(["log", ref, "--reverse", "--extended-regexp", `--grep=#${issue}([^0-9]|$)`, "--format=%H %s"]);
|
|
1042
|
-
return out.split(`
|
|
1043
|
-
`).filter(Boolean).map((line) => {
|
|
1044
|
-
const space = line.indexOf(" ");
|
|
1045
|
-
return { sha: line.slice(0, space), subject: line.slice(space + 1) };
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
1048
|
-
function cwdGone() {
|
|
1049
|
-
try {
|
|
1050
|
-
process8.cwd();
|
|
1051
|
-
return false;
|
|
1052
|
-
} catch {
|
|
1053
|
-
return true;
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
async function leaveWorktree() {
|
|
1057
|
-
const common = await git(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
1058
|
-
if (basename(common) === ".git")
|
|
1059
|
-
process8.chdir(dirname(common));
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// src/tea.ts
|
|
1463
|
+
var TEA_MS = 60000;
|
|
1063
1464
|
function repoFlag(repo) {
|
|
1064
1465
|
return repo === undefined ? [] : ["--repo", repo];
|
|
1065
1466
|
}
|
|
1066
1467
|
async function request(path, repo) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
throw spawnFailure(`tea api ${path}`, error);
|
|
1072
|
-
}
|
|
1073
|
-
if (result.exitCode !== 0)
|
|
1074
|
-
throw new Error(`tea api ${path}: ${result.stderr.toString().trim() || `exit ${result.exitCode}`}`);
|
|
1075
|
-
return result.stdout.toString();
|
|
1468
|
+
const result = await runTool(`tea api ${path}`, ["tea", "api", ...repoFlag(repo), path], { timeoutMs: TEA_MS });
|
|
1469
|
+
if (result.code !== 0)
|
|
1470
|
+
throw new Error(`tea api ${path}: ${result.stderr.trim() || `exit ${result.code}`}`);
|
|
1471
|
+
return result.stdout;
|
|
1076
1472
|
}
|
|
1077
1473
|
function parseJson(path, text) {
|
|
1078
1474
|
let value;
|
|
@@ -1082,10 +1478,16 @@ function parseJson(path, text) {
|
|
|
1082
1478
|
throw new Error(`tea api ${path}: ${upTo(text.trim(), `
|
|
1083
1479
|
`) || "empty answer"}`);
|
|
1084
1480
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1481
|
+
const failure = apiError(value);
|
|
1482
|
+
if (failure !== undefined)
|
|
1483
|
+
throw new Error(`tea api ${path}: ${failure}`);
|
|
1087
1484
|
return value;
|
|
1088
1485
|
}
|
|
1486
|
+
function apiError(value) {
|
|
1487
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && "message" in value && typeof value.message === "string" && "url" in value)
|
|
1488
|
+
return value.message;
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1089
1491
|
async function api(path, repo) {
|
|
1090
1492
|
return parseJson(path, await request(path, repo));
|
|
1091
1493
|
}
|
|
@@ -1095,29 +1497,30 @@ async function repoApi(path, repo) {
|
|
|
1095
1497
|
async function repoWrite(method, path, fields, repo) {
|
|
1096
1498
|
const full = `repos/{owner}/{repo}/${path}`;
|
|
1097
1499
|
const flags = Object.entries(fields).flatMap(([name, value]) => ["-f", `${name}=${value}`]);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
throw spawnFailure(`tea api ${full}`, error);
|
|
1103
|
-
}
|
|
1104
|
-
if (result.exitCode !== 0)
|
|
1105
|
-
throw new Error(`tea api ${full}: ${result.stderr.toString().trim() || `exit ${result.exitCode}`}`);
|
|
1106
|
-
return parseJson(full, result.stdout.toString());
|
|
1500
|
+
const result = await runTool(`tea api ${full}`, ["tea", "api", ...repoFlag(repo), "-X", method, ...flags, full], { timeoutMs: TEA_MS });
|
|
1501
|
+
if (result.code !== 0)
|
|
1502
|
+
throw new Error(`tea api ${full}: ${result.stderr.trim() || `exit ${result.code}`}`);
|
|
1503
|
+
return parseJson(full, result.stdout);
|
|
1107
1504
|
}
|
|
1108
1505
|
async function repoApiText(path, repo) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
let result;
|
|
1506
|
+
const full = `repos/{owner}/{repo}/${path}`;
|
|
1507
|
+
const text = await request(full, repo);
|
|
1508
|
+
let value;
|
|
1113
1509
|
try {
|
|
1114
|
-
|
|
1115
|
-
} catch
|
|
1116
|
-
|
|
1510
|
+
value = JSON.parse(text);
|
|
1511
|
+
} catch {
|
|
1512
|
+
return text;
|
|
1117
1513
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1514
|
+
const failure = apiError(value);
|
|
1515
|
+
if (failure !== undefined)
|
|
1516
|
+
throw new Error(`tea api ${full}: ${failure}`);
|
|
1517
|
+
return text;
|
|
1518
|
+
}
|
|
1519
|
+
async function tea(command, args, repo) {
|
|
1520
|
+
const result = await runTool(`tea ${command.join(" ")}`, ["tea", ...command, ...repoFlag(repo), ...args], { timeoutMs: TEA_MS });
|
|
1521
|
+
if (result.code !== 0)
|
|
1522
|
+
throw new Error(`tea ${command.join(" ")}: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
1523
|
+
return result.stdout;
|
|
1121
1524
|
}
|
|
1122
1525
|
|
|
1123
1526
|
// src/issues.ts
|
|
@@ -1155,46 +1558,75 @@ async function commentsOfAll(issues, repo) {
|
|
|
1155
1558
|
return result;
|
|
1156
1559
|
}
|
|
1157
1560
|
async function editIssue(number, change, repo) {
|
|
1158
|
-
const args = [
|
|
1561
|
+
const args = [String(number)];
|
|
1159
1562
|
if (change.title !== undefined)
|
|
1160
1563
|
args.push("-t", change.title);
|
|
1161
1564
|
if (change.body !== undefined)
|
|
1162
1565
|
args.push("-d", change.body);
|
|
1163
1566
|
if (change.milestone !== undefined)
|
|
1164
1567
|
args.push("-m", change.milestone);
|
|
1165
|
-
await tea(args, repo);
|
|
1568
|
+
await tea(["issues", "edit"], args, repo);
|
|
1166
1569
|
}
|
|
1167
1570
|
async function setBody(number, body, repo) {
|
|
1168
1571
|
await editIssue(number, { body }, repo);
|
|
1169
1572
|
}
|
|
1170
1573
|
async function createIssue(title, body, labels, milestone, repo) {
|
|
1171
|
-
const args = ["
|
|
1574
|
+
const args = ["-t", title, "-d", body];
|
|
1575
|
+
const same = (found) => found.title === title && sameText(found.body, body);
|
|
1576
|
+
const known = new Set((await openIssues(undefined, repo)).filter(same).map((found) => found.number));
|
|
1172
1577
|
if (labels !== undefined)
|
|
1173
1578
|
args.push("-L", labels);
|
|
1174
1579
|
if (milestone !== undefined)
|
|
1175
1580
|
args.push("-m", milestone);
|
|
1176
|
-
await tea(args, repo);
|
|
1177
|
-
const created = (await openIssues(undefined, repo)).filter((found) => found
|
|
1178
|
-
if (created
|
|
1581
|
+
await tea(["issues", "create"], args, repo);
|
|
1582
|
+
const created = (await openIssues(undefined, repo)).filter((found) => same(found) && !known.has(found.number));
|
|
1583
|
+
if (created.length > 1)
|
|
1584
|
+
throw new Error(`created "${title}" but ${created.map((found) => `#${found.number}`).join(" and ")} both match: another session created the same issue at once`);
|
|
1585
|
+
if (created[0] === undefined)
|
|
1179
1586
|
throw new Error(`created "${title}" but cannot find it among the open issues`);
|
|
1180
|
-
return created;
|
|
1587
|
+
return created[0];
|
|
1588
|
+
}
|
|
1589
|
+
function sameText(a, b) {
|
|
1590
|
+
return a.replaceAll(`\r
|
|
1591
|
+
`, `
|
|
1592
|
+
`).trimEnd() === b.replaceAll(`\r
|
|
1593
|
+
`, `
|
|
1594
|
+
`).trimEnd();
|
|
1181
1595
|
}
|
|
1596
|
+
var EDIT_TRIES = 3;
|
|
1182
1597
|
async function editBody(number, change, repo) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1598
|
+
let wrote = false;
|
|
1599
|
+
for (let attempt = 0;attempt < EDIT_TRIES; attempt++) {
|
|
1600
|
+
const { body } = await getIssue(number, repo);
|
|
1601
|
+
const next = change(body);
|
|
1602
|
+
if (next === body)
|
|
1603
|
+
return wrote;
|
|
1604
|
+
await setBody(number, next, repo);
|
|
1605
|
+
wrote = true;
|
|
1606
|
+
if (sameText((await getIssue(number, repo)).body, next))
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
throw new Error(`#${number}: its description keeps changing under the edit, so it was not written reliably`);
|
|
1610
|
+
}
|
|
1611
|
+
var login;
|
|
1612
|
+
async function sessionAuthor() {
|
|
1613
|
+
if (login !== undefined)
|
|
1614
|
+
return login;
|
|
1615
|
+
try {
|
|
1616
|
+
login = (await api("user")).login;
|
|
1617
|
+
} catch {
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
return login;
|
|
1189
1621
|
}
|
|
1190
1622
|
async function comment(number, body, repo) {
|
|
1191
|
-
await
|
|
1623
|
+
await repoWrite("POST", `issues/${number}/comments`, { body }, repo);
|
|
1192
1624
|
}
|
|
1193
1625
|
async function close(number, repo) {
|
|
1194
|
-
await tea(["issues", "close", String(number)], repo);
|
|
1626
|
+
await tea(["issues", "close"], [String(number)], repo);
|
|
1195
1627
|
}
|
|
1196
1628
|
async function reopen(number, repo) {
|
|
1197
|
-
await tea(["issues", "reopen", String(number)], repo);
|
|
1629
|
+
await tea(["issues", "reopen"], [String(number)], repo);
|
|
1198
1630
|
}
|
|
1199
1631
|
async function repoLabels(repo) {
|
|
1200
1632
|
const names = [];
|
|
@@ -1206,12 +1638,12 @@ async function repoLabels(repo) {
|
|
|
1206
1638
|
}
|
|
1207
1639
|
}
|
|
1208
1640
|
async function editLabels(number, add, remove, repo) {
|
|
1209
|
-
const args = [
|
|
1641
|
+
const args = [String(number)];
|
|
1210
1642
|
if (add.length > 0)
|
|
1211
1643
|
args.push("--add-labels", add.join(","));
|
|
1212
1644
|
if (remove.length > 0)
|
|
1213
1645
|
args.push("--remove-labels", remove.join(","));
|
|
1214
|
-
await tea(args, repo);
|
|
1646
|
+
await tea(["issues", "edit"], args, repo);
|
|
1215
1647
|
}
|
|
1216
1648
|
|
|
1217
1649
|
// src/milestones.ts
|
|
@@ -1237,99 +1669,29 @@ async function findMilestone(reference, repo) {
|
|
|
1237
1669
|
function dueOn(value) {
|
|
1238
1670
|
const match = DATE.exec(value);
|
|
1239
1671
|
const date = new Date(`${value}T00:00:00Z`);
|
|
1240
|
-
if (match === null || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value || date.getUTCFullYear() < 2000)
|
|
1241
|
-
fail(`--due takes a date as YYYY-MM-DD, not ${value}`);
|
|
1242
|
-
return date.toISOString();
|
|
1243
|
-
}
|
|
1244
|
-
function dueDate(milestone) {
|
|
1245
|
-
if (milestone.due_on === null || new Date(milestone.due_on).getUTCFullYear() >= NO_DUE_YEAR)
|
|
1246
|
-
return "";
|
|
1247
|
-
return milestone.due_on.slice(0, 10);
|
|
1248
|
-
}
|
|
1249
|
-
function progress(milestone) {
|
|
1250
|
-
const total = milestone.open_issues + milestone.closed_issues;
|
|
1251
|
-
return total === 0 ? "" : `${Math.round(milestone.closed_issues / total * 100)}%`;
|
|
1252
|
-
}
|
|
1253
|
-
async function createMilestone(title, description, due, repo) {
|
|
1254
|
-
const fields = { title };
|
|
1255
|
-
if (description !== undefined)
|
|
1256
|
-
fields.description = description;
|
|
1257
|
-
if (due !== undefined)
|
|
1258
|
-
fields.due_on = due;
|
|
1259
|
-
return repoWrite("POST", "milestones", fields, repo);
|
|
1260
|
-
}
|
|
1261
|
-
async function closeMilestone(id, repo) {
|
|
1262
|
-
await repoWrite("PATCH", `milestones/${id}`, { state: "closed" }, repo);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// src/sessions.ts
|
|
1266
|
-
import { homedir } from "os";
|
|
1267
|
-
import { join } from "path";
|
|
1268
|
-
import process9 from "process";
|
|
1269
|
-
var {Glob } = globalThis.Bun;
|
|
1270
|
-
var SESSION_FILES = new Glob("*.json");
|
|
1271
|
-
var ACCOUNT_DIRS = new Glob("*/sessions");
|
|
1272
|
-
async function sessionDirs() {
|
|
1273
|
-
const home = homedir();
|
|
1274
|
-
const dirs = new Set([join(home, ".claude", "sessions")]);
|
|
1275
|
-
if (process9.env.CLAUDE_CONFIG_DIR !== undefined)
|
|
1276
|
-
dirs.add(join(process9.env.CLAUDE_CONFIG_DIR, "sessions"));
|
|
1277
|
-
try {
|
|
1278
|
-
for await (const dir of ACCOUNT_DIRS.scan({ cwd: join(home, ".claude-accounts"), onlyFiles: false, absolute: true }))
|
|
1279
|
-
dirs.add(dir);
|
|
1280
|
-
} catch {}
|
|
1281
|
-
return [...dirs];
|
|
1282
|
-
}
|
|
1283
|
-
async function procStat(pid) {
|
|
1284
|
-
try {
|
|
1285
|
-
const stat = await Bun.file(`/proc/${pid}/stat`).text();
|
|
1286
|
-
return stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
1287
|
-
} catch {
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
async function isAlive(pid, procStart) {
|
|
1292
|
-
const stat = await procStat(pid);
|
|
1293
|
-
if (stat === undefined)
|
|
1294
|
-
return false;
|
|
1295
|
-
return procStart === undefined || stat[19] === procStart;
|
|
1296
|
-
}
|
|
1297
|
-
async function liveSessions() {
|
|
1298
|
-
const sessions = new Map;
|
|
1299
|
-
for (const dir of await sessionDirs()) {
|
|
1300
|
-
let files;
|
|
1301
|
-
try {
|
|
1302
|
-
files = await Array.fromAsync(SESSION_FILES.scan({ cwd: dir, absolute: true }));
|
|
1303
|
-
} catch {
|
|
1304
|
-
continue;
|
|
1305
|
-
}
|
|
1306
|
-
for (const file of files) {
|
|
1307
|
-
let entry;
|
|
1308
|
-
try {
|
|
1309
|
-
entry = await Bun.file(file).json();
|
|
1310
|
-
} catch {
|
|
1311
|
-
continue;
|
|
1312
|
-
}
|
|
1313
|
-
if (entry.name === undefined || entry.pid === undefined || sessions.has(entry.pid) || !await isAlive(entry.pid, entry.procStart))
|
|
1314
|
-
continue;
|
|
1315
|
-
sessions.set(entry.pid, { name: entry.name, pid: entry.pid, status: entry.status ?? "unknown", cwd: entry.cwd ?? "" });
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
return [...sessions.values()];
|
|
1672
|
+
if (match === null || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value || date.getUTCFullYear() < 2000)
|
|
1673
|
+
fail(`--due takes a date as YYYY-MM-DD, not ${value}`);
|
|
1674
|
+
return date.toISOString();
|
|
1319
1675
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1676
|
+
function dueDate(milestone) {
|
|
1677
|
+
if (milestone.due_on === null || new Date(milestone.due_on).getUTCFullYear() >= NO_DUE_YEAR)
|
|
1678
|
+
return "";
|
|
1679
|
+
return milestone.due_on.slice(0, 10);
|
|
1680
|
+
}
|
|
1681
|
+
function progress(milestone) {
|
|
1682
|
+
const total = milestone.open_issues + milestone.closed_issues;
|
|
1683
|
+
return total === 0 ? "" : `${Math.round(milestone.closed_issues / total * 100)}%`;
|
|
1684
|
+
}
|
|
1685
|
+
async function createMilestone(title, description, due, repo) {
|
|
1686
|
+
const fields = { title };
|
|
1687
|
+
if (description !== undefined)
|
|
1688
|
+
fields.description = description;
|
|
1689
|
+
if (due !== undefined)
|
|
1690
|
+
fields.due_on = due;
|
|
1691
|
+
return repoWrite("POST", "milestones", fields, repo);
|
|
1692
|
+
}
|
|
1693
|
+
async function closeMilestone(id, repo) {
|
|
1694
|
+
await repoWrite("PATCH", `milestones/${id}`, { state: "closed" }, repo);
|
|
1333
1695
|
}
|
|
1334
1696
|
|
|
1335
1697
|
// src/commands/board.ts
|
|
@@ -1381,9 +1743,10 @@ ${lines.join(`
|
|
|
1381
1743
|
}
|
|
1382
1744
|
async function boardRows(issues, sessions, repo) {
|
|
1383
1745
|
const comments = await commentsOfAll(issues, repo);
|
|
1746
|
+
const author = await sessionAuthor();
|
|
1384
1747
|
return issues.map((issue) => {
|
|
1385
1748
|
const own = comments.get(issue.number) ?? [];
|
|
1386
|
-
return { issue, standing: standing(latestClaim(own), sessions), comments: own };
|
|
1749
|
+
return { issue, standing: standing(latestClaim(own, author), sessions), comments: own };
|
|
1387
1750
|
});
|
|
1388
1751
|
}
|
|
1389
1752
|
function boardJson(rows) {
|
|
@@ -1499,12 +1862,13 @@ async function take(args) {
|
|
|
1499
1862
|
const numbers = issueNumbers(positionals, takeUsage);
|
|
1500
1863
|
const sessions = await liveSessions();
|
|
1501
1864
|
const nameFor = await claimant(values, sessions);
|
|
1865
|
+
const author = await sessionAuthor();
|
|
1502
1866
|
return eachIssue("take", numbers, async (number) => {
|
|
1503
1867
|
const name = nameFor(number);
|
|
1504
1868
|
const [found, comments] = await Promise.all([getIssue(number, values.repo), commentsOf(number, values.repo)]);
|
|
1505
1869
|
if (found.state === "closed")
|
|
1506
1870
|
fail(`#${number} is closed`, 1, runHint(`ft reopen ${number}`, "reopen it"));
|
|
1507
|
-
const claim = latestClaim(comments);
|
|
1871
|
+
const claim = latestClaim(comments, author);
|
|
1508
1872
|
const current = standing(claim, sessions);
|
|
1509
1873
|
if (claim?.holder === name) {
|
|
1510
1874
|
console.log(`#${number} is already taken by ${name}`);
|
|
@@ -1515,6 +1879,9 @@ async function take(args) {
|
|
|
1515
1879
|
if (current.state === "away" && values["take-stale"] !== true)
|
|
1516
1880
|
fail(`#${number} was taken by ${current.claim.holder} ${formatAge(current.claim.at)}, which no session here answers to: it may run on another machine`, 1, runHint(`ft take ${number} --take-stale`, "take it over"));
|
|
1517
1881
|
await comment(number, takenComment(name), values.repo);
|
|
1882
|
+
const standingNow = latestClaim(await commentsOf(number, values.repo), author);
|
|
1883
|
+
if (standingNow?.holder !== name)
|
|
1884
|
+
fail(`#${number} was taken by ${standingNow?.holder ?? "another session"} at the same moment`, 1);
|
|
1518
1885
|
console.log(`took #${number} as ${name}: ${found.title}`);
|
|
1519
1886
|
});
|
|
1520
1887
|
}
|
|
@@ -1533,9 +1900,10 @@ async function drop(args) {
|
|
|
1533
1900
|
const { values, positionals } = parsed;
|
|
1534
1901
|
const numbers = issueNumbers(positionals, dropUsage);
|
|
1535
1902
|
const nameFor = await claimant(values, await liveSessions());
|
|
1903
|
+
const author = await sessionAuthor();
|
|
1536
1904
|
return eachIssue("drop", numbers, async (number) => {
|
|
1537
1905
|
const name = nameFor(number);
|
|
1538
|
-
const claim = latestClaim(await commentsOf(number, values.repo));
|
|
1906
|
+
const claim = latestClaim(await commentsOf(number, values.repo), author);
|
|
1539
1907
|
if (claim?.holder !== name)
|
|
1540
1908
|
fail(`#${number} is ${claim === undefined ? "not taken" : `taken by ${claim.holder}, not ${name}`}`, 1);
|
|
1541
1909
|
await comment(number, releasedComment(name), values.repo);
|
|
@@ -1544,114 +1912,20 @@ async function drop(args) {
|
|
|
1544
1912
|
}
|
|
1545
1913
|
|
|
1546
1914
|
// src/commands/ci.ts
|
|
1547
|
-
import
|
|
1915
|
+
import process12 from "process";
|
|
1548
1916
|
|
|
1549
|
-
//
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
var CLEAR_LINE = "\r\x1B[2K";
|
|
1554
|
-
var LINE_UP = "\x1B[1A\x1B[2K";
|
|
1555
|
-
var ESC = String.fromCharCode(27);
|
|
1556
|
-
var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, "y");
|
|
1557
|
-
var RESET_STYLE = `${ESC}[0m`;
|
|
1558
|
-
var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
|
|
1559
|
-
var CONSOLE_METHODS = ["log", "info", "warn", "error", "debug", "dir", "dirxml", "table", "trace", "group", "groupCollapsed", "count", "timeEnd", "timeLog", "assert"];
|
|
1560
|
-
function liveRegion(render, stream = process10.stderr, env = process10.env) {
|
|
1561
|
-
if (!takesControlCodes(stream, env))
|
|
1562
|
-
return { redraw: () => {
|
|
1563
|
-
return;
|
|
1564
|
-
}, print: (text) => void stream.write(`${text}
|
|
1565
|
-
`), stop: () => {
|
|
1566
|
-
return;
|
|
1567
|
-
} };
|
|
1568
|
-
const write = stream.write.bind(stream);
|
|
1569
|
-
let frame = 0;
|
|
1570
|
-
let drawn = 0;
|
|
1571
|
-
const clear = () => {
|
|
1572
|
-
if (drawn > 0)
|
|
1573
|
-
write(`${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}`);
|
|
1574
|
-
drawn = 0;
|
|
1575
|
-
};
|
|
1576
|
-
const draw = () => {
|
|
1577
|
-
const width = (stream.columns === undefined || stream.columns === 0 ? 80 : stream.columns) - 1;
|
|
1578
|
-
const height = stream.rows === undefined || stream.rows === 0 ? 24 : stream.rows;
|
|
1579
|
-
const lines = render(FRAMES[frame++ % FRAMES.length]).slice(0, Math.max(1, height - 1)).map((line) => cutToWidth(line, width));
|
|
1580
|
-
if (lines.length === 0) {
|
|
1581
|
-
clear();
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
write(`${drawn > 0 ? `${CLEAR_LINE}${LINE_UP.repeat(drawn - 1)}` : CLEAR_LINE}${lines.join(`
|
|
1585
|
-
`)}`);
|
|
1586
|
-
drawn = lines.length;
|
|
1587
|
-
};
|
|
1588
|
-
const timer = setInterval(draw, FRAME_MS);
|
|
1589
|
-
const restore = interceptOutput(clear);
|
|
1590
|
-
const onExit = () => clear();
|
|
1591
|
-
const signals = holdSignals(stop);
|
|
1592
|
-
function stop() {
|
|
1593
|
-
clearInterval(timer);
|
|
1594
|
-
clear();
|
|
1595
|
-
restore();
|
|
1596
|
-
process10.off("exit", onExit);
|
|
1597
|
-
signals.release();
|
|
1598
|
-
}
|
|
1599
|
-
timer.unref();
|
|
1600
|
-
process10.on("exit", onExit);
|
|
1601
|
-
return {
|
|
1602
|
-
redraw: draw,
|
|
1603
|
-
print: (text) => {
|
|
1604
|
-
clear();
|
|
1605
|
-
write(`${text}
|
|
1606
|
-
`);
|
|
1607
|
-
draw();
|
|
1608
|
-
},
|
|
1609
|
-
stop
|
|
1610
|
-
};
|
|
1611
|
-
}
|
|
1612
|
-
function cutToWidth(line, width) {
|
|
1613
|
-
if (Bun.stringWidth(line) <= width)
|
|
1614
|
-
return line;
|
|
1615
|
-
let room = width;
|
|
1616
|
-
let kept = "";
|
|
1617
|
-
let index = 0;
|
|
1618
|
-
let linked = false;
|
|
1619
|
-
while (index < line.length) {
|
|
1620
|
-
ESCAPE_CODE.lastIndex = index;
|
|
1621
|
-
const escape = ESCAPE_CODE.exec(line);
|
|
1622
|
-
if (escape !== null) {
|
|
1623
|
-
kept += escape[0];
|
|
1624
|
-
linked ||= escape[0].startsWith(`${ESC}]`);
|
|
1625
|
-
index = ESCAPE_CODE.lastIndex;
|
|
1626
|
-
continue;
|
|
1627
|
-
}
|
|
1628
|
-
const char = String.fromCodePoint(line.codePointAt(index));
|
|
1629
|
-
const columns = Bun.stringWidth(char);
|
|
1630
|
-
if (columns > room)
|
|
1631
|
-
break;
|
|
1632
|
-
kept += char;
|
|
1633
|
-
room -= columns;
|
|
1634
|
-
index += char.length;
|
|
1635
|
-
}
|
|
1636
|
-
return `${kept}${RESET_STYLE}${linked ? CLOSE_LINK : ""}`;
|
|
1917
|
+
// src/git.ts
|
|
1918
|
+
async function tagsAt(commit) {
|
|
1919
|
+
return (await git(["tag", "--points-at", commit])).split(`
|
|
1920
|
+
`).filter(Boolean);
|
|
1637
1921
|
}
|
|
1638
|
-
function
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
return original.apply(target, args);
|
|
1645
|
-
};
|
|
1646
|
-
Reflect.set(target, name, wrapper);
|
|
1647
|
-
return { target, name, original, wrapper };
|
|
1922
|
+
async function commitsNaming(issue, ref) {
|
|
1923
|
+
const out = await git(["log", ref, "--reverse", "--extended-regexp", `--grep=#${issue}([^0-9]|$)`, "--format=%H %s"]);
|
|
1924
|
+
return out.split(`
|
|
1925
|
+
`).filter(Boolean).map((line) => {
|
|
1926
|
+
const space = line.indexOf(" ");
|
|
1927
|
+
return { sha: line.slice(0, space), subject: line.slice(space + 1) };
|
|
1648
1928
|
});
|
|
1649
|
-
return () => {
|
|
1650
|
-
for (const { target, name, original, wrapper } of wrapped) {
|
|
1651
|
-
if (Reflect.get(target, name) === wrapper)
|
|
1652
|
-
Reflect.set(target, name, original);
|
|
1653
|
-
}
|
|
1654
|
-
};
|
|
1655
1929
|
}
|
|
1656
1930
|
|
|
1657
1931
|
// src/log.ts
|
|
@@ -1728,7 +2002,13 @@ async function recentRuns(limit, repo, workflow) {
|
|
|
1728
2002
|
return (await runsListing(limit, repo, workflow)).runs;
|
|
1729
2003
|
}
|
|
1730
2004
|
async function runsOn(sha, repo) {
|
|
1731
|
-
|
|
2005
|
+
const runs = [];
|
|
2006
|
+
for (let page = 1;; page++) {
|
|
2007
|
+
const list = (await repoApi(`actions/runs?head_sha=${sha}&page=${page}&limit=${RUNS_PAGE}`, repo)).workflow_runs ?? [];
|
|
2008
|
+
runs.push(...list);
|
|
2009
|
+
if (list.length < RUNS_PAGE)
|
|
2010
|
+
return runs;
|
|
2011
|
+
}
|
|
1732
2012
|
}
|
|
1733
2013
|
async function runById(id, repo) {
|
|
1734
2014
|
return repoApi(`actions/runs/${id}`, repo);
|
|
@@ -1814,9 +2094,9 @@ var options = {
|
|
|
1814
2094
|
job: { type: "string", short: "j", description: "a job whose skipping means the run left its work to a newer push" },
|
|
1815
2095
|
timeout: { type: "string", short: "t", description: "how long before giving up (30m), a bare number in minutes" }
|
|
1816
2096
|
};
|
|
1817
|
-
var POLL = 15000;
|
|
1818
2097
|
var NO_RUN = 3 * 60000;
|
|
1819
2098
|
var STALLED = 3 * 60000;
|
|
2099
|
+
var MAX_LOOKUPS = 20;
|
|
1820
2100
|
function judge(runs, jobSkipped, tagged) {
|
|
1821
2101
|
if (runs.length === 0)
|
|
1822
2102
|
return { kind: "none" };
|
|
@@ -1828,7 +2108,13 @@ function judge(runs, jobSkipped, tagged) {
|
|
|
1828
2108
|
return { kind: "runs", runs };
|
|
1829
2109
|
return tagged ? { kind: "runs", runs: runs.map((run) => ({ ...run, status: "skipped" })) } : { kind: "passed-over" };
|
|
1830
2110
|
}
|
|
1831
|
-
|
|
2111
|
+
function commitsToAsk(commits, passedOver, max = MAX_LOOKUPS) {
|
|
2112
|
+
return commits.filter((commit) => !passedOver.has(commit)).slice(0, max);
|
|
2113
|
+
}
|
|
2114
|
+
function settled(latest) {
|
|
2115
|
+
return latest !== undefined && latest.every((run) => FINISHED.has(run.status)) && judge(latest, false, false).kind === "runs" ? latest : undefined;
|
|
2116
|
+
}
|
|
2117
|
+
async function ci2(args) {
|
|
1832
2118
|
const parsed = parse(args, usage, options);
|
|
1833
2119
|
const { values, positionals } = parsed;
|
|
1834
2120
|
const branch = values.branch ?? await defaultBranch();
|
|
@@ -1852,71 +2138,44 @@ async function ci(args) {
|
|
|
1852
2138
|
return coverage;
|
|
1853
2139
|
}
|
|
1854
2140
|
async function covering() {
|
|
1855
|
-
await
|
|
2141
|
+
await fetchBranch(branch);
|
|
1856
2142
|
if (!await gitOk(["merge-base", "--is-ancestor", sha, `origin/${branch}`]))
|
|
1857
2143
|
fail(`${short} is not on origin/${branch}`, 2, runHint(`git push origin ${branch}`));
|
|
1858
2144
|
const later = (await git(["rev-list", "--reverse", "--first-parent", `${sha}..origin/${branch}`])).split(`
|
|
1859
2145
|
`).filter(Boolean);
|
|
1860
|
-
for (const commit of [sha, ...later]) {
|
|
2146
|
+
for (const commit of commitsToAsk([sha, ...later], passedOver)) {
|
|
1861
2147
|
const coverage = await coverageOf(commit);
|
|
1862
2148
|
if (coverage.kind === "runs")
|
|
1863
2149
|
return coverage.runs;
|
|
1864
2150
|
}
|
|
1865
2151
|
return;
|
|
1866
2152
|
}
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
}
|
|
1894
|
-
await Bun.sleep(POLL);
|
|
1895
|
-
continue;
|
|
1896
|
-
}
|
|
1897
|
-
if (runs !== undefined) {
|
|
1898
|
-
seen = Date.now();
|
|
1899
|
-
const ids = runs.map((run) => run.id).join(",");
|
|
1900
|
-
if (ids !== followed) {
|
|
1901
|
-
followed = ids;
|
|
1902
|
-
for (const run of runs)
|
|
1903
|
-
console.log(`following ${run.workflow_id} run ${run.id} on ${run.commit_sha.slice(0, 7)}${run.commit_sha === sha ? "" : `, which covers ${short}`}: ${run.html_url}`);
|
|
1904
|
-
}
|
|
1905
|
-
if (runs.every((run) => FINISHED.has(run.status)))
|
|
1906
|
-
break;
|
|
1907
|
-
const stall = await stallReason(runs);
|
|
1908
|
-
if (stall === undefined)
|
|
1909
|
-
moving = Date.now();
|
|
1910
|
-
else if (stall.certain || Date.now() - moving > STALLED)
|
|
1911
|
-
fail(`runs queued on ${short} are not moving: ${stall.reason}`);
|
|
1912
|
-
} else if (Date.now() - seen > NO_RUN) {
|
|
1913
|
-
fail(`no run covers ${short} yet: its own was cancelled by a later push or never started, so the next push's will`);
|
|
1914
|
-
}
|
|
1915
|
-
await Bun.sleep(POLL);
|
|
1916
|
-
}
|
|
1917
|
-
} finally {
|
|
1918
|
-
region.stop();
|
|
1919
|
-
}
|
|
2153
|
+
const { dim } = paint(process12.stderr);
|
|
2154
|
+
const runs = await waitForCi({
|
|
2155
|
+
tool: "ft ci",
|
|
2156
|
+
short,
|
|
2157
|
+
giveUpMs: giveUp,
|
|
2158
|
+
noneMs: NO_RUN,
|
|
2159
|
+
stalledMs: STALLED,
|
|
2160
|
+
noneMessage: `no run covers ${short} yet: its own was cancelled by a later push or never started, so the next push's will`,
|
|
2161
|
+
covering,
|
|
2162
|
+
finished: (found) => found.every((run) => FINISHED.has(run.status)),
|
|
2163
|
+
key: (found) => found.map((run) => run.id).join(","),
|
|
2164
|
+
announce: (found) => {
|
|
2165
|
+
for (const run of found)
|
|
2166
|
+
console.log(`following ${run.workflow_id} run ${run.id} on ${run.commit_sha.slice(0, 7)}${run.commit_sha === sha ? "" : `, which covers ${short}`}: ${run.html_url}`);
|
|
2167
|
+
},
|
|
2168
|
+
links: (found) => found.map((run) => run.html_url).join(" "),
|
|
2169
|
+
settle: async (found) => settled(await refreshed(found)),
|
|
2170
|
+
stall: async (found) => {
|
|
2171
|
+
const stall = await stallReason(found);
|
|
2172
|
+
return stall === undefined ? undefined : { message: `runs queued on ${short} are not moving: ${stall.reason}`, certain: stall.certain };
|
|
2173
|
+
},
|
|
2174
|
+
lines: (spinner, found, started) => [
|
|
2175
|
+
`${spinner} CI on ${short} ${dim(formatDuration(Date.now() - started))}`,
|
|
2176
|
+
...(found ?? []).map((run) => ` ${badge(run.status, process12.stderr)} ${run.workflow_id} ${dim(`run ${run.id}`)}`)
|
|
2177
|
+
]
|
|
2178
|
+
});
|
|
1920
2179
|
const status = overall(runs);
|
|
1921
2180
|
if (status !== "failure") {
|
|
1922
2181
|
console.log(`CI ${status === "success" ? "passed" : "skipped its tests, a release tag's workflow testing them"} on ${short}`);
|
|
@@ -1933,7 +2192,7 @@ run ${run.id} failed${run.commit_sha === sha ? "" : `, testing every commit from
|
|
|
1933
2192
|
}
|
|
1934
2193
|
|
|
1935
2194
|
// src/commands/digest.ts
|
|
1936
|
-
import
|
|
2195
|
+
import process13 from "process";
|
|
1937
2196
|
var RUNS_SCANNED = 50;
|
|
1938
2197
|
var TITLE_WIDTH2 = 70;
|
|
1939
2198
|
var SHOWN = 8;
|
|
@@ -1983,7 +2242,7 @@ async function quietly(read) {
|
|
|
1983
2242
|
}
|
|
1984
2243
|
}
|
|
1985
2244
|
async function staleTodos() {
|
|
1986
|
-
const bin = Bun.which("todo-scan", { PATH:
|
|
2245
|
+
const bin = Bun.which("todo-scan", { PATH: process13.env.PATH ?? "" });
|
|
1987
2246
|
if (bin === null)
|
|
1988
2247
|
return [];
|
|
1989
2248
|
let found;
|
|
@@ -2044,7 +2303,8 @@ async function digest(args) {
|
|
|
2044
2303
|
const [issues, sessions, branch, runs] = await Promise.all([openIssues(undefined, values.repo), liveSessions(), defaultBranch(), recentRuns(RUNS_SCANNED, values.repo)]);
|
|
2045
2304
|
const comments = await commentsOfAll(issues, values.repo);
|
|
2046
2305
|
const me = await currentSession(sessions);
|
|
2047
|
-
const
|
|
2306
|
+
const author = await sessionAuthor();
|
|
2307
|
+
const rows = issues.map((issue) => ({ issue, claim: latestClaim(comments.get(issue.number) ?? [], author) }));
|
|
2048
2308
|
const [unpushed, dirty, stale] = await Promise.all([quietly(unpushedCommits), quietly(dirtyTree), staleTodos()]);
|
|
2049
2309
|
const summary = { ...claims(rows, sessions, me), ci: unsettled(runs, branch), unpushed, dirty, stale };
|
|
2050
2310
|
if (values.json === true)
|
|
@@ -2161,7 +2421,7 @@ var issueOptions = {
|
|
|
2161
2421
|
...repoOption
|
|
2162
2422
|
};
|
|
2163
2423
|
function renderIssue(found, comments, view, now = Date.now()) {
|
|
2164
|
-
const claim = latestClaim(comments);
|
|
2424
|
+
const claim = latestClaim(comments, view.author);
|
|
2165
2425
|
const list = boxes(found.body);
|
|
2166
2426
|
const left = list.filter((box) => !box.done);
|
|
2167
2427
|
const facts = [found.state];
|
|
@@ -2189,14 +2449,14 @@ ${each.body.replaceAll(`\r
|
|
|
2189
2449
|
|
|
2190
2450
|
`);
|
|
2191
2451
|
}
|
|
2192
|
-
function issueJson(found, comments) {
|
|
2452
|
+
function issueJson(found, comments, author) {
|
|
2193
2453
|
return {
|
|
2194
2454
|
number: found.number,
|
|
2195
2455
|
title: found.title,
|
|
2196
2456
|
state: found.state,
|
|
2197
2457
|
url: found.html_url,
|
|
2198
2458
|
labels: found.labels.map((label) => label.name),
|
|
2199
|
-
claim: latestClaim(comments) ?? null,
|
|
2459
|
+
claim: latestClaim(comments, author) ?? null,
|
|
2200
2460
|
boxes: boxes(found.body).map(({ number, done, text }) => ({ number, done, text })),
|
|
2201
2461
|
body: found.body,
|
|
2202
2462
|
comments: comments.map((each) => ({ author: each.user.login, at: each.created_at, body: each.body }))
|
|
@@ -2216,12 +2476,13 @@ async function issue(args) {
|
|
|
2216
2476
|
return 0;
|
|
2217
2477
|
}
|
|
2218
2478
|
const read = await Promise.all(numbers.map(async (number) => Promise.all([getIssue(number, values.repo), commentsOf(number, values.repo)])));
|
|
2479
|
+
const author = await sessionAuthor();
|
|
2219
2480
|
if (values.json === true) {
|
|
2220
|
-
const json = read.map(([found, comments]) => issueJson(found, comments));
|
|
2481
|
+
const json = read.map(([found, comments]) => issueJson(found, comments, author));
|
|
2221
2482
|
printJson(json.length === 1 ? json[0] : json);
|
|
2222
2483
|
return 0;
|
|
2223
2484
|
}
|
|
2224
|
-
const view = { todo: values.todo === true, comments: values.comments === true };
|
|
2485
|
+
const view = { todo: values.todo === true, comments: values.comments === true, author };
|
|
2225
2486
|
console.log(read.map(([found, comments]) => renderIssue(found, comments, view)).join(view.todo ? `
|
|
2226
2487
|
|
|
2227
2488
|
` : `
|
|
@@ -2240,11 +2501,14 @@ async function noteText(values) {
|
|
|
2240
2501
|
const text = values.file === undefined ? values.message : await readInput(values.file);
|
|
2241
2502
|
return text?.trim() === "" ? undefined : text?.trim();
|
|
2242
2503
|
}
|
|
2504
|
+
var NOT_FOUND = /not exist|not found|couldn't be found/i;
|
|
2243
2505
|
async function isClosed(number, repo) {
|
|
2244
2506
|
try {
|
|
2245
2507
|
return (await getIssue(number, repo)).state === "closed";
|
|
2246
|
-
} catch {
|
|
2247
|
-
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
if (error instanceof Error && NOT_FOUND.test(error.message))
|
|
2510
|
+
return false;
|
|
2511
|
+
throw error;
|
|
2248
2512
|
}
|
|
2249
2513
|
}
|
|
2250
2514
|
async function closedReferences(body, repo) {
|
|
@@ -2317,17 +2581,21 @@ ${editUsage}`);
|
|
|
2317
2581
|
const milestone = await milestoneTitle(values.milestone, values.repo);
|
|
2318
2582
|
if (milestone !== undefined)
|
|
2319
2583
|
change.milestone = milestone;
|
|
2320
|
-
|
|
2584
|
+
const grows = text !== undefined && values.append === true || added.length > 0;
|
|
2585
|
+
if (text !== undefined && !grows)
|
|
2321
2586
|
change.body = text;
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
if (
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2587
|
+
if (Object.keys(change).length > 0)
|
|
2588
|
+
await editIssue(number, change, values.repo);
|
|
2589
|
+
if (grows) {
|
|
2590
|
+
await editBody(number, (body) => {
|
|
2591
|
+
const appended = text !== undefined && values.append === true && !body.trimEnd().endsWith(text) ? `${body.trimEnd()}
|
|
2592
|
+
|
|
2593
|
+
${text}` : body;
|
|
2594
|
+
const present = new Set(boxes(appended).map((box) => box.text));
|
|
2595
|
+
const missing = added.filter((box) => !present.has(box));
|
|
2596
|
+
return missing.length > 0 ? appendBoxes(appended, missing) : appended;
|
|
2597
|
+
}, values.repo);
|
|
2598
|
+
}
|
|
2331
2599
|
console.log(`edited #${number}`);
|
|
2332
2600
|
return 0;
|
|
2333
2601
|
}
|
|
@@ -2343,6 +2611,13 @@ var tickOptions = {
|
|
|
2343
2611
|
...noteOptions,
|
|
2344
2612
|
...repoOption
|
|
2345
2613
|
};
|
|
2614
|
+
function tickedOrFail(number, run) {
|
|
2615
|
+
try {
|
|
2616
|
+
return run();
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
fail(`#${number}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2346
2621
|
async function tick(args) {
|
|
2347
2622
|
const parsed = parse(args, tickUsage, tickOptions);
|
|
2348
2623
|
const { values, positionals } = parsed;
|
|
@@ -2360,17 +2635,13 @@ ${tickUsage}`);
|
|
|
2360
2635
|
const found = await getIssue(number, values.repo);
|
|
2361
2636
|
const done = values.undo !== true;
|
|
2362
2637
|
const closed = values.closed === true ? await closedReferences(found.body, values.repo) : undefined;
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
result = closed === undefined ? setBoxes(found.body, picks, done) : tickReferences(found.body, (issue) => closed.has(issue));
|
|
2366
|
-
} catch (error) {
|
|
2367
|
-
fail(`#${number}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2368
|
-
}
|
|
2638
|
+
const apply = (body) => closed === undefined ? setBoxes(body, picks, done) : tickReferences(body, (issue) => closed.has(issue));
|
|
2639
|
+
let result = tickedOrFail(number, () => apply(found.body));
|
|
2369
2640
|
if (result.changed.length > 0) {
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2641
|
+
await editBody(number, (body) => {
|
|
2642
|
+
result = tickedOrFail(number, () => apply(body));
|
|
2643
|
+
return result.body;
|
|
2644
|
+
}, values.repo);
|
|
2374
2645
|
}
|
|
2375
2646
|
for (const box of result.changed)
|
|
2376
2647
|
console.log(`${done ? "ticked" : "unticked"} ${box.number}: ${box.text}`);
|
|
@@ -2461,7 +2732,7 @@ async function fixed(args) {
|
|
|
2461
2732
|
fail(`#${number} is already closed`, 1, runHint(`ft reopen ${number}`, "reopen it"));
|
|
2462
2733
|
const left = boxes(found.body).filter((box) => !box.done);
|
|
2463
2734
|
if (left.length > 0 && values.tick === true) {
|
|
2464
|
-
await
|
|
2735
|
+
await editBody(number, (body) => setBoxes(body, boxes(body).filter((box) => !box.done).map((box) => String(box.number)), true).body, values.repo);
|
|
2465
2736
|
console.log(`ticked ${left.length} box${left.length === 1 ? "" : "es"} of #${number}`);
|
|
2466
2737
|
} else if (left.length > 0 && values.force !== true) {
|
|
2467
2738
|
fail(`#${number} has ${left.length} boxes left:
|
|
@@ -2469,7 +2740,9 @@ ${left.map((box) => `${String(box.number).padStart(3)} [ ] ${box.text}`).join(`
|
|
|
2469
2740
|
`)}`, 1, `${runHint(`ft fixed ${number} --tick`, "tick them and close")}, or ${runHint(`ft fixed ${number} --force`, "close with them left")}`);
|
|
2470
2741
|
}
|
|
2471
2742
|
const shas = joinShas(commits.map((each) => each.sha.slice(0, 7)));
|
|
2472
|
-
|
|
2743
|
+
const line = `Fixed in ${shas}: ${values.message ?? commits[0]?.subject ?? ""}`;
|
|
2744
|
+
if ((await commentsOf(number, values.repo)).at(-1)?.body.trim() !== line.trim())
|
|
2745
|
+
await comment(number, line, values.repo);
|
|
2473
2746
|
await close(number, values.repo);
|
|
2474
2747
|
console.log(`closed #${number}: fixed in ${shas}`);
|
|
2475
2748
|
await tickListings(number, values.repo);
|
|
@@ -2484,22 +2757,22 @@ With no failed job it prints every job's last lines; --full prints whole logs.
|
|
|
2484
2757
|
|
|
2485
2758
|
Exits 0 when it printed the logs, 1 when Forgejo cannot be read, 2 on a usage error or a job the run lacks.`;
|
|
2486
2759
|
var options3 = {
|
|
2487
|
-
job: { type: "string", short: "j", description: "only this job, by name" },
|
|
2760
|
+
job: { type: "string", short: "j", description: "only this job, by name or id" },
|
|
2488
2761
|
full: { type: "boolean", short: "f", description: "the whole log, timestamps stripped" },
|
|
2489
2762
|
...repoOption
|
|
2490
2763
|
};
|
|
2491
|
-
function pickJobs(jobs,
|
|
2492
|
-
if (
|
|
2493
|
-
return jobs.filter((job) => job.name ===
|
|
2764
|
+
function pickJobs(jobs, wanted) {
|
|
2765
|
+
if (wanted !== undefined)
|
|
2766
|
+
return jobs.filter((job) => job.name === wanted || String(job.id) === wanted);
|
|
2494
2767
|
const failed = jobs.filter((job) => job.status === "failure");
|
|
2495
2768
|
return failed.length > 0 ? failed : jobs;
|
|
2496
2769
|
}
|
|
2497
|
-
async function
|
|
2770
|
+
async function logs2(args) {
|
|
2498
2771
|
const parsed = parse(args, usage3, options3);
|
|
2499
2772
|
const { values, positionals } = parsed;
|
|
2500
2773
|
if (positionals[0] === undefined)
|
|
2501
2774
|
fail("a run id is needed", 2, runHint("ft runs", "list them"));
|
|
2502
|
-
const run = await runById(wholeNumber(positionals[0], "run id", 0), values.repo);
|
|
2775
|
+
const run = await runById(wholeNumber(positionals[0], "<run id>", 0), values.repo);
|
|
2503
2776
|
const jobs = await jobsOf(run.id, values.repo);
|
|
2504
2777
|
const picked = pickJobs(jobs, values.job);
|
|
2505
2778
|
if (picked.length === 0)
|
|
@@ -2776,8 +3049,8 @@ function report(plan, added, skipped) {
|
|
|
2776
3049
|
|
|
2777
3050
|
// src/commands/release.ts
|
|
2778
3051
|
import { basename as basename2, join as join2, relative, resolve } from "path";
|
|
2779
|
-
import
|
|
2780
|
-
var {
|
|
3052
|
+
import process14 from "process";
|
|
3053
|
+
var {$ } = globalThis.Bun;
|
|
2781
3054
|
|
|
2782
3055
|
// src/commands/watch.ts
|
|
2783
3056
|
var usage4 = `ft watch [--after <id>]
|
|
@@ -2795,18 +3068,15 @@ var STALLED2 = 3 * 60000;
|
|
|
2795
3068
|
async function watchRuns(pick, timeout, repo, idle) {
|
|
2796
3069
|
const started = Date.now();
|
|
2797
3070
|
const reported = new Map;
|
|
3071
|
+
const followed = new Set;
|
|
3072
|
+
const failed = new Set;
|
|
2798
3073
|
let moving = started;
|
|
2799
|
-
|
|
2800
|
-
await
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
runs = pick(await recentRuns(LIMIT, repo));
|
|
2806
|
-
} catch (error) {
|
|
2807
|
-
console.warn(`ft watch: ${error instanceof Error ? error.message : String(error)}, polling again`);
|
|
2808
|
-
continue;
|
|
2809
|
-
}
|
|
3074
|
+
async function poll() {
|
|
3075
|
+
const newest = await recentRuns(LIMIT, repo);
|
|
3076
|
+
const gone = await Promise.all([...followed].filter((id) => !newest.some((run) => run.id === id)).map(async (id) => runById(id, repo)));
|
|
3077
|
+
const runs = [...pick(newest), ...gone];
|
|
3078
|
+
for (const run of runs)
|
|
3079
|
+
followed.add(run.id);
|
|
2810
3080
|
for (const run of runs) {
|
|
2811
3081
|
if (reported.get(run.id) !== run.status)
|
|
2812
3082
|
console.log(`${run.workflow_id} run ${run.id} on ${run.prettyref}: ${badge(run.status, process.stdout)}`);
|
|
@@ -2817,26 +3087,44 @@ async function watchRuns(pick, timeout, repo, idle) {
|
|
|
2817
3087
|
console.log("no run started, so nothing was waited for");
|
|
2818
3088
|
return 0;
|
|
2819
3089
|
}
|
|
2820
|
-
|
|
3090
|
+
return;
|
|
2821
3091
|
}
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
if (run.status === "failure")
|
|
2826
|
-
console.error(`
|
|
3092
|
+
for (const run of runs) {
|
|
3093
|
+
if (run.status === "failure" && !failed.has(run.id)) {
|
|
3094
|
+
console.error(`
|
|
2827
3095
|
${await failureReport(run, repo)}
|
|
2828
3096
|
|
|
2829
3097
|
${run.html_url}`);
|
|
3098
|
+
failed.add(run.id);
|
|
2830
3099
|
}
|
|
2831
|
-
return 1;
|
|
2832
3100
|
}
|
|
2833
|
-
if (runs.every((run) => FINISHED.has(run.status)))
|
|
2834
|
-
|
|
3101
|
+
if (runs.every((run) => FINISHED.has(run.status))) {
|
|
3102
|
+
const status = overall(runs);
|
|
3103
|
+
return status === "failure" || status === "cancelled" ? 1 : 0;
|
|
3104
|
+
}
|
|
2835
3105
|
const stall = await stallReason(runs, repo);
|
|
2836
3106
|
if (stall === undefined)
|
|
2837
3107
|
moving = Date.now();
|
|
2838
3108
|
else if (stall.certain || Date.now() - moving > STALLED2)
|
|
2839
3109
|
fail(`queued runs are not moving: ${stall.reason}`);
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
for (;; ) {
|
|
3113
|
+
await Bun.sleep(timing.poll);
|
|
3114
|
+
if (Date.now() - started > timeout)
|
|
3115
|
+
fail(`the runs did not finish in ${formatDuration(timeout)}`);
|
|
3116
|
+
try {
|
|
3117
|
+
const code = await poll();
|
|
3118
|
+
if (code !== undefined)
|
|
3119
|
+
return code;
|
|
3120
|
+
} catch (error) {
|
|
3121
|
+
if (error instanceof Exit)
|
|
3122
|
+
throw error;
|
|
3123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3124
|
+
if (isPermanentFailure(error))
|
|
3125
|
+
fail(message);
|
|
3126
|
+
console.warn(`ft watch: ${message}, polling again`);
|
|
3127
|
+
}
|
|
2840
3128
|
}
|
|
2841
3129
|
}
|
|
2842
3130
|
async function watch(args) {
|
|
@@ -2862,7 +3150,7 @@ ft release --tagged
|
|
|
2862
3150
|
Cuts a release from the default branch: first the commit it sits on is pushed and its runs waited for, so failing tests stop it before anything is bumped or tagged. Then \`bun pm version\` commits and tags the bump, one atomic push sends the branch and its tag, and it waits for the runs the push started and for the registry to serve the version.
|
|
2863
3151
|
--tagged releases a version commit already tagged at HEAD instead, for repos bumping with their own script; the runs waited for first are then those of the commit below it.
|
|
2864
3152
|
--package releases one workspace package of a monorepo on its own, tagged \`<folder>-v<version>\` (\`ft release patch -p ft\` tags packages/ft as ft-v0.3.0).
|
|
2865
|
-
A refused push drops the bump commit and its tag, so a retry starts clean.
|
|
3153
|
+
A refused or interrupted push drops the bump commit and its tag, so a retry starts clean; with --tagged the commit and tag are yours and stay.
|
|
2866
3154
|
Once released, an open milestone titled like the version (\`1.2.0\`, \`v1.2.0\` or the tag) has its closed issues listed and is closed; a failure there only warns.
|
|
2867
3155
|
--dry-run checks the same and prints the plan: the version bump, the tag, the pushes and the runs and registry it would wait for, changing nothing.
|
|
2868
3156
|
Exits 0 once released or planned, 1 when a run fails, origin refuses a push or the registry never serves the version, 2 on a timeout, a stalled queue, a refusal before anything ran (another branch, uncommitted changes, origin ahead) or a usage error.`;
|
|
@@ -2879,6 +3167,8 @@ var PARTS = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/;
|
|
|
2879
3167
|
var LAST_NUMBER = /\d+$/;
|
|
2880
3168
|
var REGISTRY_TRIES = 12;
|
|
2881
3169
|
var REGISTRY_POLL = 5000;
|
|
3170
|
+
var limits = { pushMs: 5 * 60000 };
|
|
3171
|
+
var REGISTRY_INFO_MS = 30000;
|
|
2882
3172
|
var NO_RUN2 = 3 * 60000;
|
|
2883
3173
|
function taggedVersion(tag, prefix) {
|
|
2884
3174
|
const version = tag.slice(prefix.length);
|
|
@@ -2921,7 +3211,7 @@ async function commitPackageBump(dir, prefix) {
|
|
|
2921
3211
|
const { version } = await Bun.file(join2(dir, "package.json")).json();
|
|
2922
3212
|
const root = await git(["rev-parse", "--show-toplevel"]);
|
|
2923
3213
|
const tag = `${prefix}${version}`;
|
|
2924
|
-
const lock = await
|
|
3214
|
+
const lock = await $`bun install --lockfile-only`.cwd(root).quiet().nothrow();
|
|
2925
3215
|
if (lock.exitCode !== 0)
|
|
2926
3216
|
fail(`bun install --lockfile-only after the bump: ${lock.stderr.toString().trim()}`);
|
|
2927
3217
|
await git(["add", join2(dir, "package.json"), join2(root, "bun.lock")]);
|
|
@@ -2930,8 +3220,14 @@ async function commitPackageBump(dir, prefix) {
|
|
|
2930
3220
|
}
|
|
2931
3221
|
async function waitForRegistry(name, version) {
|
|
2932
3222
|
for (let attempt = 0;attempt < REGISTRY_TRIES; attempt++) {
|
|
2933
|
-
|
|
3223
|
+
let served;
|
|
3224
|
+
try {
|
|
3225
|
+
served = (await runTool("bun info", ["bun", "info", name, "version", "--no-cache"], { timeoutMs: REGISTRY_INFO_MS })).stdout.trim().split(`
|
|
2934
3226
|
`).at(-1);
|
|
3227
|
+
} catch (error) {
|
|
3228
|
+
if (isPermanentFailure(error))
|
|
3229
|
+
throw error;
|
|
3230
|
+
}
|
|
2935
3231
|
if (served === version)
|
|
2936
3232
|
return true;
|
|
2937
3233
|
await Bun.sleep(REGISTRY_POLL);
|
|
@@ -2940,10 +3236,18 @@ async function waitForRegistry(name, version) {
|
|
|
2940
3236
|
}
|
|
2941
3237
|
async function testsPass(sha, branch, timeout) {
|
|
2942
3238
|
if (!await gitOk(["merge-base", "--is-ancestor", sha, `origin/${branch}`])) {
|
|
2943
|
-
const
|
|
2944
|
-
|
|
3239
|
+
const held = holdSignals();
|
|
3240
|
+
let push;
|
|
3241
|
+
try {
|
|
3242
|
+
push = await runTool("git push", ["git", "push", "origin", `${sha}:refs/heads/${branch}`], { timeoutMs: limits.pushMs, onStart: (child) => held.forward(child) });
|
|
3243
|
+
} finally {
|
|
3244
|
+
held.release();
|
|
3245
|
+
}
|
|
3246
|
+
if (held.signal !== undefined)
|
|
3247
|
+
fail(`interrupted by ${held.signal} while pushing ${sha.slice(0, 7)}, so nothing was bumped`, exitCodeOf(null, held.signal));
|
|
3248
|
+
if (push.code !== 0)
|
|
2945
3249
|
fail(`origin refused the push of ${sha.slice(0, 7)}, so nothing was bumped:
|
|
2946
|
-
${push.stderr.
|
|
3250
|
+
${push.stderr.trim()}`, 1);
|
|
2947
3251
|
console.log(`pushed ${branch} at ${sha.slice(0, 7)}, waiting for its runs before the bump`);
|
|
2948
3252
|
}
|
|
2949
3253
|
return await watchRuns((runs) => newestPerWorkflow(runs.filter((run) => run.commit_sha === sha)), timeout, undefined, NO_RUN2) === 0;
|
|
@@ -2951,8 +3255,8 @@ ${push.stderr.toString().trim()}`, 1);
|
|
|
2951
3255
|
async function printPlan({ level, dir, prefix, branch, base, timeout, registry }) {
|
|
2952
3256
|
const file = Bun.file(join2(dir ?? ".", "package.json"));
|
|
2953
3257
|
const pkg = await file.exists() ? await file.json() : undefined;
|
|
2954
|
-
const name = pkg?.name ?? basename2(dir ??
|
|
2955
|
-
const where = dir === undefined ? "" : ` in ${relative(
|
|
3258
|
+
const name = pkg?.name ?? basename2(dir ?? process14.cwd());
|
|
3259
|
+
const where = dir === undefined ? "" : ` in ${relative(process14.cwd(), dir) || "."}`;
|
|
2956
3260
|
const sha = base.slice(0, 7);
|
|
2957
3261
|
let version;
|
|
2958
3262
|
let bump;
|
|
@@ -2983,6 +3287,17 @@ async function printPlan({ level, dir, prefix, branch, base, timeout, registry }
|
|
|
2983
3287
|
console.log("dry run: nothing was pushed, committed or tagged");
|
|
2984
3288
|
return 0;
|
|
2985
3289
|
}
|
|
3290
|
+
async function rollBack(preBump, tagsBefore) {
|
|
3291
|
+
try {
|
|
3292
|
+
for (const name of await tagsAt("HEAD")) {
|
|
3293
|
+
if (!tagsBefore.has(name))
|
|
3294
|
+
await git(["tag", "-d", name]);
|
|
3295
|
+
}
|
|
3296
|
+
await git(["reset", "--keep", preBump]);
|
|
3297
|
+
} catch (error) {
|
|
3298
|
+
warn(`could not undo the bump, back to ${preBump.slice(0, 7)} by hand`, error instanceof Error ? error.message : String(error));
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
2986
3301
|
async function release(args) {
|
|
2987
3302
|
const parsed = parse(args, usage5, options6);
|
|
2988
3303
|
const { values, positionals } = parsed;
|
|
@@ -3003,34 +3318,61 @@ ${usage5}`);
|
|
|
3003
3318
|
fail(`a release is cut from ${branch}, not ${current || "a detached HEAD"}`, 2, runHint(`git switch ${branch}`));
|
|
3004
3319
|
if (await git(["status", "--porcelain"]) !== "")
|
|
3005
3320
|
fail("the tree has uncommitted changes", 2, runHint("gst", "see them, then commit them with gc"));
|
|
3006
|
-
await
|
|
3007
|
-
if ((await
|
|
3321
|
+
await fetchBranch(branch);
|
|
3322
|
+
if ((await $`git merge-base --is-ancestor ${`origin/${branch}`} HEAD`.quiet().nothrow()).exitCode !== 0)
|
|
3008
3323
|
fail(`origin/${branch} has commits HEAD lacks`, 2, runHint(`git pull --rebase origin ${branch}`));
|
|
3009
3324
|
const base = await git(["rev-parse", tagged ? "HEAD~1" : "HEAD"]);
|
|
3010
3325
|
if (values["dry-run"] === true)
|
|
3011
3326
|
return printPlan({ level, dir, prefix, branch, base, timeout, registry: values["no-registry"] !== true });
|
|
3012
3327
|
if (!await testsPass(base, branch, timeout))
|
|
3013
3328
|
fail(`the runs on ${base.slice(0, 7)} failed, so nothing was bumped or tagged`, 1, runHint(`ft runs --sha ${base.slice(0, 7)}`, "list the runs, then ft logs <id> reads the failure"));
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
if (bump.exitCode !== 0)
|
|
3017
|
-
fail(`bun pm version ${level}: ${bump.stderr.toString().trim() || bump.stdout.toString().trim()}`);
|
|
3018
|
-
if (dir !== undefined)
|
|
3019
|
-
await commitPackageBump(dir, prefix);
|
|
3020
|
-
}
|
|
3021
|
-
const tag = (await tagsAt("HEAD")).find((name) => taggedVersion(name, prefix) !== undefined);
|
|
3022
|
-
if (tag === undefined)
|
|
3023
|
-
fail(tagged ? `HEAD carries no ${prefix}* tag` : `the bump made no ${prefix}* tag`);
|
|
3329
|
+
const preBump = await git(["rev-parse", "HEAD"]);
|
|
3330
|
+
const tagsBefore = new Set(await tagsAt("HEAD"));
|
|
3024
3331
|
const before = (await recentRuns(1))[0]?.id ?? 0;
|
|
3025
|
-
const
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3332
|
+
const held = holdSignals();
|
|
3333
|
+
const signalOf = () => held.signal;
|
|
3334
|
+
let tag = "";
|
|
3335
|
+
let head = "";
|
|
3336
|
+
let pushed = false;
|
|
3337
|
+
let isPushUnknown = false;
|
|
3338
|
+
try {
|
|
3339
|
+
if (level !== undefined) {
|
|
3340
|
+
const bump = dir === undefined ? await $`bun pm version ${level}`.quiet().nothrow() : await $`bun pm version ${level} --no-git-tag-version`.cwd(dir).quiet().nothrow();
|
|
3341
|
+
if (bump.exitCode !== 0)
|
|
3342
|
+
fail(`bun pm version ${level}: ${bump.stderr.toString().trim() || bump.stdout.toString().trim()}`);
|
|
3343
|
+
if (dir !== undefined)
|
|
3344
|
+
await commitPackageBump(dir, prefix);
|
|
3345
|
+
}
|
|
3346
|
+
const found = (await tagsAt("HEAD")).find((name) => taggedVersion(name, prefix) !== undefined);
|
|
3347
|
+
if (found === undefined)
|
|
3348
|
+
fail(tagged ? `HEAD carries no ${prefix}* tag` : `the bump made no ${prefix}* tag`);
|
|
3349
|
+
tag = found;
|
|
3350
|
+
head = await git(["rev-parse", "HEAD"]);
|
|
3351
|
+
if (held.signal !== undefined)
|
|
3352
|
+
fail(`interrupted by ${held.signal} before the push`, exitCodeOf(null, held.signal));
|
|
3353
|
+
let push;
|
|
3354
|
+
try {
|
|
3355
|
+
push = await runTool("git push", ["git", "push", "--atomic", "origin", `HEAD:refs/heads/${branch}`, tag], { timeoutMs: limits.pushMs, onStart: (child) => held.forward(child) });
|
|
3356
|
+
} catch (error) {
|
|
3357
|
+
isPushUnknown = true;
|
|
3358
|
+
fail(`${error instanceof Error ? error.message : String(error)}, so ${tag} and its commit are kept: the push may have landed`, 2, `git ls-remote origin refs/tags/${tag} tells; if it is there, ft release --tagged waits for its runs`);
|
|
3359
|
+
}
|
|
3360
|
+
if (push.code !== 0) {
|
|
3361
|
+
const signal = signalOf();
|
|
3362
|
+
fail(`origin refused the push${signal === undefined ? "" : `, or ${signal} interrupted it`}, so ${level === undefined ? `${tag} and its commit are kept for a retry` : `${tag} and its commit were dropped for a clean retry`}:
|
|
3363
|
+
${push.stderr.trim()}`, signal === undefined ? 1 : exitCodeOf(null, signal));
|
|
3364
|
+
}
|
|
3365
|
+
pushed = true;
|
|
3366
|
+
} finally {
|
|
3367
|
+
held.release();
|
|
3368
|
+
if (!pushed && !isPushUnknown && level !== undefined)
|
|
3369
|
+
await rollBack(preBump, tagsBefore);
|
|
3031
3370
|
}
|
|
3371
|
+
const late = signalOf();
|
|
3372
|
+
if (late !== undefined)
|
|
3373
|
+
fail(`interrupted by ${late} after ${tag} was pushed: its runs are not waited for`, exitCodeOf(null, late));
|
|
3032
3374
|
console.log(`pushed ${branch} and ${tag}, waiting for their runs`);
|
|
3033
|
-
const code = await watchRuns((runs) => runs.filter((run) => run.id > before), timeout);
|
|
3375
|
+
const code = await watchRuns((runs) => runs.filter((run) => run.id > before && run.commit_sha === head), timeout, undefined, NO_RUN2);
|
|
3034
3376
|
if (code !== 0)
|
|
3035
3377
|
return code;
|
|
3036
3378
|
const file = Bun.file(join2(dir ?? ".", "package.json"));
|
|
@@ -3069,7 +3411,7 @@ ${done.map((issue) => ` #${issue.number} ${issue.title}`).join(`
|
|
|
3069
3411
|
// src/commands/remote.ts
|
|
3070
3412
|
import { lookup } from "dns/promises";
|
|
3071
3413
|
import { homedir as homedir3 } from "os";
|
|
3072
|
-
var {$: $
|
|
3414
|
+
var {$: $2 } = globalThis.Bun;
|
|
3073
3415
|
|
|
3074
3416
|
// src/config.ts
|
|
3075
3417
|
import { homedir as homedir2 } from "os";
|
|
@@ -3078,30 +3420,53 @@ function settingsPath(home = homedir2()) {
|
|
|
3078
3420
|
return join3(home, ".config", "ft", "config.json");
|
|
3079
3421
|
}
|
|
3080
3422
|
async function loadSettings(home = homedir2()) {
|
|
3081
|
-
const
|
|
3423
|
+
const path = settingsPath(home);
|
|
3424
|
+
const file = Bun.file(path);
|
|
3082
3425
|
if (!await file.exists())
|
|
3083
3426
|
return {};
|
|
3084
3427
|
let raw;
|
|
3085
3428
|
try {
|
|
3086
3429
|
raw = await file.json();
|
|
3087
|
-
} catch {
|
|
3430
|
+
} catch (error) {
|
|
3431
|
+
warn(`${path} is not valid JSON, so its settings are ignored`, error instanceof Error ? error.message : String(error));
|
|
3432
|
+
return {};
|
|
3433
|
+
}
|
|
3434
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
3435
|
+
warn(`${path} is not a JSON object, so its settings are ignored`);
|
|
3088
3436
|
return {};
|
|
3089
3437
|
}
|
|
3090
|
-
const host =
|
|
3438
|
+
const host = "remoteHost" in raw ? raw.remoteHost : undefined;
|
|
3439
|
+
if (host !== undefined && (typeof host !== "string" || host === ""))
|
|
3440
|
+
warn(`remoteHost in ${path} must be a non-empty string, so it is ignored`);
|
|
3091
3441
|
return typeof host === "string" && host !== "" ? { remoteHost: host } : {};
|
|
3092
3442
|
}
|
|
3093
3443
|
|
|
3094
3444
|
// src/remote.ts
|
|
3095
3445
|
var SCP_LIKE = /^(?:([^@/]+)@)?([^:/]+):(?!\/)(.+?)(?:\.git)?\/?$/;
|
|
3096
|
-
var SSH_URL = /^ssh:\/\/(?:([^@/]+)@)?([^:/]+)(
|
|
3446
|
+
var SSH_URL = /^ssh:\/\/(?:([^@/]+)@)?([^:/]+)(?::(\d+))?\/(.+?)(?:\.git)?\/?$/;
|
|
3447
|
+
var USERINFO = /^([a-z][a-z\d+.-]*:\/\/)[^@/]*@/i;
|
|
3448
|
+
var DEFAULT_PORT = "22";
|
|
3097
3449
|
function parseSshRemote(url) {
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3450
|
+
const ssh = SSH_URL.exec(url);
|
|
3451
|
+
const scp = ssh === null && !url.includes("://") ? SCP_LIKE.exec(url) : null;
|
|
3452
|
+
const match = ssh ?? scp;
|
|
3453
|
+
if (match === null || match[2]?.startsWith("-") === true)
|
|
3454
|
+
return;
|
|
3455
|
+
const login = match[1] ?? "git";
|
|
3456
|
+
const user = login.includes(":") ? login.slice(0, login.indexOf(":")) : login;
|
|
3457
|
+
if (ssh === null)
|
|
3458
|
+
return { user, host: match[2] ?? "", path: match[3] ?? "" };
|
|
3459
|
+
return { user, host: match[2] ?? "", path: match[4] ?? "", ...ssh[3] === undefined ? {} : { port: ssh[3] } };
|
|
3460
|
+
}
|
|
3461
|
+
function redactUrl(url) {
|
|
3462
|
+
return url.replace(USERINFO, "$1");
|
|
3100
3463
|
}
|
|
3101
3464
|
function canonicalRemote(url, host, reaches) {
|
|
3102
3465
|
const remote = parseSshRemote(url);
|
|
3103
3466
|
if (remote === undefined || remote.host === host || !reaches(remote.host))
|
|
3104
3467
|
return;
|
|
3468
|
+
if (remote.port !== undefined && remote.port !== DEFAULT_PORT)
|
|
3469
|
+
return `ssh://${remote.user}@${host}:${remote.port}/${remote.path}.git`;
|
|
3105
3470
|
return `${remote.user}@${host}:${remote.path}.git`;
|
|
3106
3471
|
}
|
|
3107
3472
|
async function reachesHost(hostname, host, hostAddresses, addressesOf) {
|
|
@@ -3124,7 +3489,7 @@ var options7 = {
|
|
|
3124
3489
|
};
|
|
3125
3490
|
var HOSTNAME = /^hostname (.+)$/m;
|
|
3126
3491
|
async function sshHostname(alias) {
|
|
3127
|
-
return HOSTNAME.exec(await $
|
|
3492
|
+
return HOSTNAME.exec(await $2`ssh -G -- ${alias}`.quiet().nothrow().text())?.[1];
|
|
3128
3493
|
}
|
|
3129
3494
|
async function addresses(name) {
|
|
3130
3495
|
try {
|
|
@@ -3133,6 +3498,14 @@ async function addresses(name) {
|
|
|
3133
3498
|
return [];
|
|
3134
3499
|
}
|
|
3135
3500
|
}
|
|
3501
|
+
async function restoreOrigin(fetchUrl, pushUrls, changes) {
|
|
3502
|
+
if (changes.some(([from]) => from === fetchUrl))
|
|
3503
|
+
await git(["remote", "set-url", "origin", fetchUrl]);
|
|
3504
|
+
for (const [from, to] of changes) {
|
|
3505
|
+
if (pushUrls.includes(from))
|
|
3506
|
+
await git(["remote", "set-url", "--push", "origin", from, to]);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3136
3509
|
function originHost(url) {
|
|
3137
3510
|
return parseSshRemote(url)?.host ?? (URL.canParse(url) ? new URL(url).hostname : undefined);
|
|
3138
3511
|
}
|
|
@@ -3144,7 +3517,7 @@ async function remote(args, home = homedir3()) {
|
|
|
3144
3517
|
return 0;
|
|
3145
3518
|
}
|
|
3146
3519
|
const fetchUrl = await git(["remote", "get-url", "origin"]);
|
|
3147
|
-
const pushUrls = (await $
|
|
3520
|
+
const pushUrls = (await $2`git config --get-all remote.origin.pushurl`.quiet().nothrow().text()).split(`
|
|
3148
3521
|
`).filter(Boolean);
|
|
3149
3522
|
const hostAddresses = new Set(await addresses(host));
|
|
3150
3523
|
const hostnames = new Map;
|
|
@@ -3162,15 +3535,15 @@ async function remote(args, home = homedir3()) {
|
|
|
3162
3535
|
if (changes.length === 0) {
|
|
3163
3536
|
const named = originHost(fetchUrl);
|
|
3164
3537
|
if (named === host) {
|
|
3165
|
-
console.log(`origin already names ${host}: ${fetchUrl}`);
|
|
3538
|
+
console.log(`origin already names ${host}: ${redactUrl(fetchUrl)}`);
|
|
3166
3539
|
return 0;
|
|
3167
3540
|
}
|
|
3168
3541
|
const hostname = named === undefined ? undefined : hostnames.get(named);
|
|
3169
|
-
console.log(`origin ${fetchUrl} does not reach ${host}${hostname === undefined || hostname === named ? "" : `: ssh connects ${named} to ${hostname}`}, pass --host if Forgejo lives elsewhere`);
|
|
3542
|
+
console.log(`origin ${redactUrl(fetchUrl)} does not reach ${host}${hostname === undefined || hostname === named ? "" : `: ssh connects ${named} to ${hostname}`}, pass --host if Forgejo lives elsewhere`);
|
|
3170
3543
|
return 1;
|
|
3171
3544
|
}
|
|
3172
3545
|
for (const [from, to] of changes)
|
|
3173
|
-
console.log(`${from} -> ${to}`);
|
|
3546
|
+
console.log(`${redactUrl(from)} -> ${redactUrl(to)}`);
|
|
3174
3547
|
if (values.fix !== true) {
|
|
3175
3548
|
console.log("run `ft remote --fix` to switch them");
|
|
3176
3549
|
return 1;
|
|
@@ -3182,7 +3555,12 @@ async function remote(args, home = homedir3()) {
|
|
|
3182
3555
|
if (pushUrls.includes(from))
|
|
3183
3556
|
await git(["remote", "set-url", "--push", "origin", to, from]);
|
|
3184
3557
|
}
|
|
3185
|
-
|
|
3558
|
+
try {
|
|
3559
|
+
await git(["ls-remote", "origin", "HEAD"]);
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
await restoreOrigin(fetchUrl, pushUrls, changes);
|
|
3562
|
+
return fail(`the switched origin does not answer, so the old URLs were put back: ${redactUrl(error instanceof Error ? error.message : String(error))}`, 1);
|
|
3563
|
+
}
|
|
3186
3564
|
console.log("origin switched, and git ls-remote reaches it");
|
|
3187
3565
|
return 0;
|
|
3188
3566
|
}
|
|
@@ -3234,7 +3612,7 @@ async function runs(args) {
|
|
|
3234
3612
|
}
|
|
3235
3613
|
|
|
3236
3614
|
// src/cli.ts
|
|
3237
|
-
|
|
3615
|
+
process15.exitCode = await runCli({
|
|
3238
3616
|
name: "ft",
|
|
3239
3617
|
version,
|
|
3240
3618
|
about: "Forgejo tools over tea, run from a repository whose origin is on Forgejo",
|
|
@@ -3242,9 +3620,9 @@ process14.exitCode = await runCli({
|
|
|
3242
3620
|
examples: [["ft ci", "wait for the runs of origin's tip, failures printed"], ["ft issue 12 -c", "an issue with its boxes numbered and its comments"]],
|
|
3243
3621
|
commands: {
|
|
3244
3622
|
runs: { run: runs, summary: "recent runs with workflow, ref, duration and age" },
|
|
3245
|
-
ci: { run:
|
|
3623
|
+
ci: { run: ci2, summary: "wait for the runs covering a commit, printing what failed" },
|
|
3246
3624
|
watch: { run: watch, summary: "wait for runs newer than an id, or those in progress" },
|
|
3247
|
-
logs: { run:
|
|
3625
|
+
logs: { run: logs2, summary: "what the failed jobs of a run say went wrong" },
|
|
3248
3626
|
release: { run: release, summary: "bump, push branch and tag atomically, wait for runs and registry" },
|
|
3249
3627
|
board: { run: board, summary: "open issues: available, taken by a live session, or claimed by one gone" },
|
|
3250
3628
|
digest: { run: digest, summary: "repo state for a cold start: held and abandoned claims, red CI, unpushed commits, dirty files, stale TODOs" },
|