@kaynooo/forgejo-tools 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +881 -502
- 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,35 +621,363 @@ 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/
|
|
624
|
+
// ../kit/src/progress.ts
|
|
625
|
+
import process6 from "process";
|
|
626
|
+
|
|
627
|
+
// ../kit/src/process.ts
|
|
628
|
+
import { constants } from "os";
|
|
586
629
|
import process5 from "process";
|
|
630
|
+
var KILL_GRACE_MS = 2000;
|
|
631
|
+
var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
632
|
+
function exitCodeOf(code, signal) {
|
|
633
|
+
if (code !== null)
|
|
634
|
+
return code;
|
|
635
|
+
if (typeof signal === "number")
|
|
636
|
+
return 128 + signal;
|
|
637
|
+
return signal === null ? 129 : 128 + constants.signals[signal];
|
|
638
|
+
}
|
|
639
|
+
function holdSignals(cleanup) {
|
|
640
|
+
let received;
|
|
641
|
+
let child;
|
|
642
|
+
const held = {
|
|
643
|
+
get signal() {
|
|
644
|
+
return received;
|
|
645
|
+
},
|
|
646
|
+
forward: (to) => {
|
|
647
|
+
child = to;
|
|
648
|
+
},
|
|
649
|
+
release: () => {
|
|
650
|
+
for (const signal of SIGNALS)
|
|
651
|
+
process5.off(signal, onSignal);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
function onSignal(signal) {
|
|
655
|
+
received = signal;
|
|
656
|
+
if (child !== undefined) {
|
|
657
|
+
child.kill(signal);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (cleanup === undefined)
|
|
661
|
+
return;
|
|
662
|
+
held.release();
|
|
663
|
+
cleanup(signal);
|
|
664
|
+
process5.kill(process5.pid, signal);
|
|
665
|
+
}
|
|
666
|
+
for (const signal of SIGNALS)
|
|
667
|
+
process5.on(signal, onSignal);
|
|
668
|
+
return held;
|
|
669
|
+
}
|
|
670
|
+
async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal, maxBytes = Infinity, onChunk, onStart } = {}) {
|
|
671
|
+
const chunks = [];
|
|
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
|
+
};
|
|
747
|
+
}
|
|
587
748
|
|
|
588
|
-
// ../kit/src/
|
|
589
|
-
var
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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;
|
|
597
865
|
}
|
|
598
|
-
return previous[b.length] ?? 0;
|
|
599
866
|
}
|
|
600
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
|
+
}
|
|
601
966
|
// ../kit/src/cli.ts
|
|
967
|
+
import process8 from "process";
|
|
602
968
|
var LEVEL_OPTIONS = {
|
|
603
969
|
...OUTPUT_OPTIONS,
|
|
604
970
|
help: { type: "boolean", short: "h", description: "show this help" }
|
|
605
971
|
};
|
|
606
972
|
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
607
|
-
function commandRows(commands, colors) {
|
|
973
|
+
async function commandRows(commands, colors) {
|
|
608
974
|
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
609
975
|
const groups = new Map([["Commands:", []]]);
|
|
610
|
-
|
|
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()) {
|
|
611
978
|
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
612
979
|
const rows = groups.get(heading) ?? [];
|
|
613
|
-
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(
|
|
980
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
|
|
614
981
|
groups.set(heading, rows);
|
|
615
982
|
}
|
|
616
983
|
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
@@ -630,28 +997,38 @@ function takeOutputFlags(argv) {
|
|
|
630
997
|
}
|
|
631
998
|
return argv.slice(index);
|
|
632
999
|
}
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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: [] };
|
|
639
1015
|
}
|
|
640
|
-
async function help({ path, header, summary, commands, defaultCommand, examples, replaces }, stream) {
|
|
1016
|
+
async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
|
|
641
1017
|
const colors = paint(stream);
|
|
642
1018
|
const prefix = path.join(" ");
|
|
1019
|
+
const summary = await resolveSummary(lazySummary);
|
|
643
1020
|
if (wantsHelpJson())
|
|
644
1021
|
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
645
1022
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
646
1023
|
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
647
1024
|
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
648
1025
|
return [
|
|
649
|
-
header(colors),
|
|
1026
|
+
header(colors, summary),
|
|
650
1027
|
"",
|
|
651
1028
|
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
652
1029
|
...replacesRows(replaces, colors),
|
|
653
1030
|
"",
|
|
654
|
-
...commandRows(commands, colors),
|
|
1031
|
+
...await commandRows(commands, colors),
|
|
655
1032
|
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
656
1033
|
"",
|
|
657
1034
|
colors.bold("Options:"),
|
|
@@ -663,8 +1040,8 @@ async function help({ path, header, summary, commands, defaultCommand, examples,
|
|
|
663
1040
|
`);
|
|
664
1041
|
}
|
|
665
1042
|
function printError(prefix, message, hint, cause) {
|
|
666
|
-
const colors = paint(
|
|
667
|
-
const styled = takesControlCodes(
|
|
1043
|
+
const colors = paint(process8.stderr);
|
|
1044
|
+
const styled = takesControlCodes(process8.stderr);
|
|
668
1045
|
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
669
1046
|
const under = (label, text, tint) => text === undefined ? "" : `
|
|
670
1047
|
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
@@ -674,8 +1051,8 @@ function runHint(command, effect) {
|
|
|
674
1051
|
return `run \`${command}\`${effect === undefined ? "" : ` to ${effect}`}`;
|
|
675
1052
|
}
|
|
676
1053
|
function printHint(hint) {
|
|
677
|
-
const colors = paint(
|
|
678
|
-
console.error(` ${takesControlCodes(
|
|
1054
|
+
const colors = paint(process8.stderr);
|
|
1055
|
+
console.error(` ${takesControlCodes(process8.stderr) ? colors.yellow("hint") : "hint:"} ${hint}`);
|
|
679
1056
|
}
|
|
680
1057
|
function printJsonError(message, hint, cause) {
|
|
681
1058
|
console.error(JSON.stringify({ error: message, ...hint === undefined ? {} : { hint }, ...cause === undefined ? {} : { cause } }));
|
|
@@ -704,11 +1081,11 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
704
1081
|
}
|
|
705
1082
|
const [name, ...args] = argv;
|
|
706
1083
|
if (name === undefined && level.path.length > 1) {
|
|
707
|
-
console.error(await help(level,
|
|
1084
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process8.stderr));
|
|
708
1085
|
return 2;
|
|
709
1086
|
}
|
|
710
1087
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
711
|
-
console.log(await help(level,
|
|
1088
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process8.stdout));
|
|
712
1089
|
return 0;
|
|
713
1090
|
}
|
|
714
1091
|
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
@@ -717,6 +1094,10 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
717
1094
|
}
|
|
718
1095
|
let command = level.commands[level.aliases?.[name] ?? name];
|
|
719
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
|
+
}
|
|
720
1101
|
if (command === undefined && level.defaultCommand !== undefined) {
|
|
721
1102
|
command = level.commands[level.defaultCommand];
|
|
722
1103
|
rest = argv;
|
|
@@ -728,7 +1109,7 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
728
1109
|
}
|
|
729
1110
|
if ("commands" in command) {
|
|
730
1111
|
const { summary } = command;
|
|
731
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${
|
|
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 });
|
|
732
1113
|
}
|
|
733
1114
|
try {
|
|
734
1115
|
return await command.run(rest);
|
|
@@ -753,28 +1134,68 @@ async function optionsOf(run) {
|
|
|
753
1134
|
async function leafCandidates(run, words) {
|
|
754
1135
|
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
755
1136
|
return [FILES];
|
|
756
|
-
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
1137
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
757
1138
|
}
|
|
758
|
-
async function candidates(
|
|
1139
|
+
async function candidates(scope, defaultCommand, words, before = []) {
|
|
759
1140
|
const [word = "", ...rest] = words;
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
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)}`));
|
|
763
1150
|
if (command === undefined) {
|
|
764
1151
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
765
1152
|
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
766
1153
|
}
|
|
767
|
-
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
1154
|
+
return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
|
|
768
1155
|
}
|
|
769
1156
|
async function printCandidates(found) {
|
|
770
1157
|
for (const line of await found)
|
|
771
1158
|
console.log(line);
|
|
772
1159
|
return 0;
|
|
773
1160
|
}
|
|
774
|
-
async function runCli(cli, argv =
|
|
1161
|
+
async function runCli(cli, argv = process8.argv.slice(2)) {
|
|
775
1162
|
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)} ${
|
|
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));
|
|
778
1199
|
}
|
|
779
1200
|
// ../kit/src/limit.ts
|
|
780
1201
|
function limitOptions(fallback) {
|
|
@@ -797,62 +1218,12 @@ function capped(rows, limit) {
|
|
|
797
1218
|
const more = rows.length - limit;
|
|
798
1219
|
return more > 0 ? { shown: rows.slice(0, limit), footer: moreFooter(more) } : { shown: [...rows], footer: undefined };
|
|
799
1220
|
}
|
|
800
|
-
// ../kit/src/
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
var
|
|
804
|
-
async function collect(stream, name, chunks) {
|
|
805
|
-
for await (const data of stream)
|
|
806
|
-
chunks.push({ stream: name, data });
|
|
807
|
-
}
|
|
808
|
-
function exitCodeOf(code, signal) {
|
|
809
|
-
if (code !== null)
|
|
810
|
-
return code;
|
|
811
|
-
if (typeof signal === "number")
|
|
812
|
-
return 128 + signal;
|
|
813
|
-
return signal === null ? 129 : 128 + constants.signals[signal];
|
|
814
|
-
}
|
|
815
|
-
function holdSignals(cleanup) {
|
|
816
|
-
let received;
|
|
817
|
-
let child;
|
|
818
|
-
const held = {
|
|
819
|
-
get signal() {
|
|
820
|
-
return received;
|
|
821
|
-
},
|
|
822
|
-
forward: (to) => {
|
|
823
|
-
child = to;
|
|
824
|
-
},
|
|
825
|
-
release: () => {
|
|
826
|
-
for (const signal of SIGNALS)
|
|
827
|
-
process6.off(signal, onSignal);
|
|
828
|
-
}
|
|
829
|
-
};
|
|
830
|
-
function onSignal(signal) {
|
|
831
|
-
received = signal;
|
|
832
|
-
if (child !== undefined) {
|
|
833
|
-
child.kill(signal);
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
if (cleanup === undefined)
|
|
837
|
-
return;
|
|
838
|
-
held.release();
|
|
839
|
-
cleanup(signal);
|
|
840
|
-
process6.kill(process6.pid, signal);
|
|
841
|
-
}
|
|
842
|
-
for (const signal of SIGNALS)
|
|
843
|
-
process6.on(signal, onSignal);
|
|
844
|
-
return held;
|
|
845
|
-
}
|
|
846
|
-
async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
|
|
847
|
-
const chunks = [];
|
|
848
|
-
const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe" });
|
|
849
|
-
await Promise.all([collect(proc.stdout, "stdout", chunks), collect(proc.stderr, "stderr", chunks)]);
|
|
850
|
-
await proc.exited;
|
|
851
|
-
const output = Buffer.concat(chunks.map(({ data }) => data));
|
|
852
|
-
return { code: exitCodeOf(proc.exitCode, proc.signalCode), chunks, output, text: output.toString() };
|
|
853
|
-
}
|
|
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"]);
|
|
854
1225
|
// package.json
|
|
855
|
-
var version = "0.
|
|
1226
|
+
var version = "0.7.0";
|
|
856
1227
|
|
|
857
1228
|
// src/args.ts
|
|
858
1229
|
var repoOption = {
|
|
@@ -864,27 +1235,41 @@ function issueNumber(value, usage) {
|
|
|
864
1235
|
fail(`an issue number is needed
|
|
865
1236
|
|
|
866
1237
|
${usage}`);
|
|
867
|
-
return wholeNumber(value.replace(ISSUE_HASH, ""), "issue", 0);
|
|
1238
|
+
return wholeNumber(value.replace(ISSUE_HASH, ""), "<issue>", 0);
|
|
868
1239
|
}
|
|
869
1240
|
function issueNumbers(values, usage) {
|
|
870
1241
|
return values.length === 0 ? [issueNumber(undefined, usage)] : values.map((value) => issueNumber(value, usage));
|
|
871
1242
|
}
|
|
872
1243
|
|
|
873
1244
|
// src/checklist.ts
|
|
874
|
-
var BOX = /^(\s*[-*+] \[)([ x])(\]\s)(
|
|
875
|
-
var FENCE = /^\s*(
|
|
1245
|
+
var BOX = /^(\s*[-*+] \[)([ x])(\]\s)(.*?)\r?$/is;
|
|
1246
|
+
var FENCE = /^\s*(`{3,}|~{3,})/;
|
|
1247
|
+
var LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s/;
|
|
1248
|
+
var INDENT_CODE = /^(?: {4}|\t)/;
|
|
876
1249
|
function boxes(body) {
|
|
877
1250
|
const found = [];
|
|
878
|
-
let
|
|
1251
|
+
let fence;
|
|
1252
|
+
let inList = false;
|
|
879
1253
|
for (const [line, text] of body.split(`
|
|
880
1254
|
`).entries()) {
|
|
881
|
-
|
|
882
|
-
|
|
1255
|
+
const marker = FENCE.exec(text)?.[1];
|
|
1256
|
+
if (fence !== undefined) {
|
|
1257
|
+
if (marker?.startsWith(fence[0] ?? "") === true && marker.length >= fence.length && text.trim() === marker)
|
|
1258
|
+
fence = undefined;
|
|
883
1259
|
continue;
|
|
884
1260
|
}
|
|
885
|
-
|
|
1261
|
+
if (marker !== undefined) {
|
|
1262
|
+
fence = marker;
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const blank = text.trim() === "";
|
|
1266
|
+
const match = INDENT_CODE.test(text) && !inList ? null : BOX.exec(text);
|
|
886
1267
|
if (match !== null)
|
|
887
1268
|
found.push({ number: found.length + 1, line, done: match[2] !== " ", text: match[4] ?? "" });
|
|
1269
|
+
if (LIST_ITEM.test(text))
|
|
1270
|
+
inList = true;
|
|
1271
|
+
else if (!blank && !text.startsWith(" ") && !text.startsWith("\t"))
|
|
1272
|
+
inList = false;
|
|
888
1273
|
}
|
|
889
1274
|
return found;
|
|
890
1275
|
}
|
|
@@ -917,13 +1302,17 @@ function numberedBody(body) {
|
|
|
917
1302
|
return lines.join(`
|
|
918
1303
|
`).trim();
|
|
919
1304
|
}
|
|
1305
|
+
var NUMBERED_PICK = /^(\d+):(.+)$/s;
|
|
1306
|
+
var WHOLE_PICK = /^\d+$/;
|
|
920
1307
|
function setBoxes(body, picks, done) {
|
|
921
1308
|
const all = boxes(body);
|
|
922
1309
|
const chosen = new Map;
|
|
923
1310
|
for (const pick of picks) {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const
|
|
1311
|
+
if (pick.trim() === "")
|
|
1312
|
+
throw new Error("an empty box pick matches every box");
|
|
1313
|
+
const guarded = NUMBERED_PICK.exec(pick);
|
|
1314
|
+
const needle = (guarded?.[2] ?? pick).trim().toLowerCase();
|
|
1315
|
+
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
1316
|
if (matches.length === 0)
|
|
928
1317
|
throw new Error(`no box matches ${pick}`);
|
|
929
1318
|
if (matches.length > 1)
|
|
@@ -961,8 +1350,8 @@ function tickReferences(body, wanted) {
|
|
|
961
1350
|
}
|
|
962
1351
|
|
|
963
1352
|
// src/claims.ts
|
|
964
|
-
var TAKEN = /^\s*taken by session ([\w.@:-]+?)\.?\s*$/
|
|
965
|
-
var RELEASED = /^\s*released by session ([\w.@:-]+?)\.?\s*$/
|
|
1353
|
+
var TAKEN = /^\s*taken by session ([\w.@:-]+?)\.?\s*$/i;
|
|
1354
|
+
var RELEASED = /^\s*released by session ([\w.@:-]+?)\.?\s*$/i;
|
|
966
1355
|
var LANE = /^lane-([^-]+)-/;
|
|
967
1356
|
function takenComment(name) {
|
|
968
1357
|
return `Taken by session ${name}.`;
|
|
@@ -970,9 +1359,12 @@ function takenComment(name) {
|
|
|
970
1359
|
function releasedComment(name) {
|
|
971
1360
|
return `Released by session ${name}.`;
|
|
972
1361
|
}
|
|
973
|
-
function
|
|
1362
|
+
function isClaimant(comment, author) {
|
|
1363
|
+
return author === undefined || comment.user === undefined || comment.user.login === author;
|
|
1364
|
+
}
|
|
1365
|
+
function latestClaim(comments, author) {
|
|
974
1366
|
let claim;
|
|
975
|
-
for (const comment of
|
|
1367
|
+
for (const comment of comments.filter((each) => isClaimant(each, author)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))) {
|
|
976
1368
|
const taken = TAKEN.exec(comment.body)?.[1];
|
|
977
1369
|
if (taken !== undefined) {
|
|
978
1370
|
claim = { holder: taken, at: comment.created_at };
|
|
@@ -992,87 +1384,22 @@ function latestNote(comments) {
|
|
|
992
1384
|
}
|
|
993
1385
|
function holderSession(holder, sessions) {
|
|
994
1386
|
const own = sessions.find((session) => session.name === holder);
|
|
995
|
-
if (own !== undefined)
|
|
996
|
-
return own;
|
|
997
|
-
const lane = LANE.exec(holder)?.[1];
|
|
998
|
-
return lane === undefined ? undefined : sessions.find((session) => session.name.endsWith(`-${lane}`));
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// src/tea.ts
|
|
1002
|
-
var {$: $2 } = globalThis.Bun;
|
|
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));
|
|
1387
|
+
if (own !== undefined)
|
|
1388
|
+
return own;
|
|
1389
|
+
const lane = LANE.exec(holder)?.[1];
|
|
1390
|
+
return lane === undefined ? undefined : sessions.find((session) => session.name.endsWith(`-${lane}`));
|
|
1060
1391
|
}
|
|
1061
1392
|
|
|
1062
1393
|
// src/tea.ts
|
|
1394
|
+
var TEA_MS = 60000;
|
|
1063
1395
|
function repoFlag(repo) {
|
|
1064
1396
|
return repo === undefined ? [] : ["--repo", repo];
|
|
1065
1397
|
}
|
|
1066
1398
|
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();
|
|
1399
|
+
const result = await runTool(`tea api ${path}`, ["tea", "api", ...repoFlag(repo), path], { timeoutMs: TEA_MS });
|
|
1400
|
+
if (result.code !== 0)
|
|
1401
|
+
throw new Error(`tea api ${path}: ${result.stderr.trim() || `exit ${result.code}`}`);
|
|
1402
|
+
return result.stdout;
|
|
1076
1403
|
}
|
|
1077
1404
|
function parseJson(path, text) {
|
|
1078
1405
|
let value;
|
|
@@ -1082,10 +1409,16 @@ function parseJson(path, text) {
|
|
|
1082
1409
|
throw new Error(`tea api ${path}: ${upTo(text.trim(), `
|
|
1083
1410
|
`) || "empty answer"}`);
|
|
1084
1411
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1412
|
+
const failure = apiError(value);
|
|
1413
|
+
if (failure !== undefined)
|
|
1414
|
+
throw new Error(`tea api ${path}: ${failure}`);
|
|
1087
1415
|
return value;
|
|
1088
1416
|
}
|
|
1417
|
+
function apiError(value) {
|
|
1418
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && "message" in value && typeof value.message === "string" && "url" in value)
|
|
1419
|
+
return value.message;
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1089
1422
|
async function api(path, repo) {
|
|
1090
1423
|
return parseJson(path, await request(path, repo));
|
|
1091
1424
|
}
|
|
@@ -1095,29 +1428,30 @@ async function repoApi(path, repo) {
|
|
|
1095
1428
|
async function repoWrite(method, path, fields, repo) {
|
|
1096
1429
|
const full = `repos/{owner}/{repo}/${path}`;
|
|
1097
1430
|
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());
|
|
1431
|
+
const result = await runTool(`tea api ${full}`, ["tea", "api", ...repoFlag(repo), "-X", method, ...flags, full], { timeoutMs: TEA_MS });
|
|
1432
|
+
if (result.code !== 0)
|
|
1433
|
+
throw new Error(`tea api ${full}: ${result.stderr.trim() || `exit ${result.code}`}`);
|
|
1434
|
+
return parseJson(full, result.stdout);
|
|
1107
1435
|
}
|
|
1108
1436
|
async function repoApiText(path, repo) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
let result;
|
|
1437
|
+
const full = `repos/{owner}/{repo}/${path}`;
|
|
1438
|
+
const text = await request(full, repo);
|
|
1439
|
+
let value;
|
|
1113
1440
|
try {
|
|
1114
|
-
|
|
1115
|
-
} catch
|
|
1116
|
-
|
|
1441
|
+
value = JSON.parse(text);
|
|
1442
|
+
} catch {
|
|
1443
|
+
return text;
|
|
1117
1444
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1445
|
+
const failure = apiError(value);
|
|
1446
|
+
if (failure !== undefined)
|
|
1447
|
+
throw new Error(`tea api ${full}: ${failure}`);
|
|
1448
|
+
return text;
|
|
1449
|
+
}
|
|
1450
|
+
async function tea(command, args, repo) {
|
|
1451
|
+
const result = await runTool(`tea ${command.join(" ")}`, ["tea", ...command, ...repoFlag(repo), ...args], { timeoutMs: TEA_MS });
|
|
1452
|
+
if (result.code !== 0)
|
|
1453
|
+
throw new Error(`tea ${command.join(" ")}: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
1454
|
+
return result.stdout;
|
|
1121
1455
|
}
|
|
1122
1456
|
|
|
1123
1457
|
// src/issues.ts
|
|
@@ -1155,46 +1489,75 @@ async function commentsOfAll(issues, repo) {
|
|
|
1155
1489
|
return result;
|
|
1156
1490
|
}
|
|
1157
1491
|
async function editIssue(number, change, repo) {
|
|
1158
|
-
const args = [
|
|
1492
|
+
const args = [String(number)];
|
|
1159
1493
|
if (change.title !== undefined)
|
|
1160
1494
|
args.push("-t", change.title);
|
|
1161
1495
|
if (change.body !== undefined)
|
|
1162
1496
|
args.push("-d", change.body);
|
|
1163
1497
|
if (change.milestone !== undefined)
|
|
1164
1498
|
args.push("-m", change.milestone);
|
|
1165
|
-
await tea(args, repo);
|
|
1499
|
+
await tea(["issues", "edit"], args, repo);
|
|
1166
1500
|
}
|
|
1167
1501
|
async function setBody(number, body, repo) {
|
|
1168
1502
|
await editIssue(number, { body }, repo);
|
|
1169
1503
|
}
|
|
1170
1504
|
async function createIssue(title, body, labels, milestone, repo) {
|
|
1171
|
-
const args = ["
|
|
1505
|
+
const args = ["-t", title, "-d", body];
|
|
1506
|
+
const same = (found) => found.title === title && sameText(found.body, body);
|
|
1507
|
+
const known = new Set((await openIssues(undefined, repo)).filter(same).map((found) => found.number));
|
|
1172
1508
|
if (labels !== undefined)
|
|
1173
1509
|
args.push("-L", labels);
|
|
1174
1510
|
if (milestone !== undefined)
|
|
1175
1511
|
args.push("-m", milestone);
|
|
1176
|
-
await tea(args, repo);
|
|
1177
|
-
const created = (await openIssues(undefined, repo)).filter((found) => found
|
|
1178
|
-
if (created
|
|
1512
|
+
await tea(["issues", "create"], args, repo);
|
|
1513
|
+
const created = (await openIssues(undefined, repo)).filter((found) => same(found) && !known.has(found.number));
|
|
1514
|
+
if (created.length > 1)
|
|
1515
|
+
throw new Error(`created "${title}" but ${created.map((found) => `#${found.number}`).join(" and ")} both match: another session created the same issue at once`);
|
|
1516
|
+
if (created[0] === undefined)
|
|
1179
1517
|
throw new Error(`created "${title}" but cannot find it among the open issues`);
|
|
1180
|
-
return created;
|
|
1518
|
+
return created[0];
|
|
1519
|
+
}
|
|
1520
|
+
function sameText(a, b) {
|
|
1521
|
+
return a.replaceAll(`\r
|
|
1522
|
+
`, `
|
|
1523
|
+
`).trimEnd() === b.replaceAll(`\r
|
|
1524
|
+
`, `
|
|
1525
|
+
`).trimEnd();
|
|
1181
1526
|
}
|
|
1527
|
+
var EDIT_TRIES = 3;
|
|
1182
1528
|
async function editBody(number, change, repo) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1529
|
+
let wrote = false;
|
|
1530
|
+
for (let attempt = 0;attempt < EDIT_TRIES; attempt++) {
|
|
1531
|
+
const { body } = await getIssue(number, repo);
|
|
1532
|
+
const next = change(body);
|
|
1533
|
+
if (next === body)
|
|
1534
|
+
return wrote;
|
|
1535
|
+
await setBody(number, next, repo);
|
|
1536
|
+
wrote = true;
|
|
1537
|
+
if (sameText((await getIssue(number, repo)).body, next))
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
throw new Error(`#${number}: its description keeps changing under the edit, so it was not written reliably`);
|
|
1541
|
+
}
|
|
1542
|
+
var login;
|
|
1543
|
+
async function sessionAuthor() {
|
|
1544
|
+
if (login !== undefined)
|
|
1545
|
+
return login;
|
|
1546
|
+
try {
|
|
1547
|
+
login = (await api("user")).login;
|
|
1548
|
+
} catch {
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
return login;
|
|
1189
1552
|
}
|
|
1190
1553
|
async function comment(number, body, repo) {
|
|
1191
|
-
await
|
|
1554
|
+
await repoWrite("POST", `issues/${number}/comments`, { body }, repo);
|
|
1192
1555
|
}
|
|
1193
1556
|
async function close(number, repo) {
|
|
1194
|
-
await tea(["issues", "close", String(number)], repo);
|
|
1557
|
+
await tea(["issues", "close"], [String(number)], repo);
|
|
1195
1558
|
}
|
|
1196
1559
|
async function reopen(number, repo) {
|
|
1197
|
-
await tea(["issues", "reopen", String(number)], repo);
|
|
1560
|
+
await tea(["issues", "reopen"], [String(number)], repo);
|
|
1198
1561
|
}
|
|
1199
1562
|
async function repoLabels(repo) {
|
|
1200
1563
|
const names = [];
|
|
@@ -1206,12 +1569,12 @@ async function repoLabels(repo) {
|
|
|
1206
1569
|
}
|
|
1207
1570
|
}
|
|
1208
1571
|
async function editLabels(number, add, remove, repo) {
|
|
1209
|
-
const args = [
|
|
1572
|
+
const args = [String(number)];
|
|
1210
1573
|
if (add.length > 0)
|
|
1211
1574
|
args.push("--add-labels", add.join(","));
|
|
1212
1575
|
if (remove.length > 0)
|
|
1213
1576
|
args.push("--remove-labels", remove.join(","));
|
|
1214
|
-
await tea(args, repo);
|
|
1577
|
+
await tea(["issues", "edit"], args, repo);
|
|
1215
1578
|
}
|
|
1216
1579
|
|
|
1217
1580
|
// src/milestones.ts
|
|
@@ -1265,15 +1628,15 @@ async function closeMilestone(id, repo) {
|
|
|
1265
1628
|
// src/sessions.ts
|
|
1266
1629
|
import { homedir } from "os";
|
|
1267
1630
|
import { join } from "path";
|
|
1268
|
-
import
|
|
1631
|
+
import process11 from "process";
|
|
1269
1632
|
var {Glob } = globalThis.Bun;
|
|
1270
1633
|
var SESSION_FILES = new Glob("*.json");
|
|
1271
1634
|
var ACCOUNT_DIRS = new Glob("*/sessions");
|
|
1272
1635
|
async function sessionDirs() {
|
|
1273
1636
|
const home = homedir();
|
|
1274
1637
|
const dirs = new Set([join(home, ".claude", "sessions")]);
|
|
1275
|
-
if (
|
|
1276
|
-
dirs.add(join(
|
|
1638
|
+
if (process11.env.CLAUDE_CONFIG_DIR !== undefined)
|
|
1639
|
+
dirs.add(join(process11.env.CLAUDE_CONFIG_DIR, "sessions"));
|
|
1277
1640
|
try {
|
|
1278
1641
|
for await (const dir of ACCOUNT_DIRS.scan({ cwd: join(home, ".claude-accounts"), onlyFiles: false, absolute: true }))
|
|
1279
1642
|
dirs.add(dir);
|
|
@@ -1319,7 +1682,7 @@ async function liveSessions() {
|
|
|
1319
1682
|
}
|
|
1320
1683
|
async function currentSession(sessions) {
|
|
1321
1684
|
const byPid = new Map(sessions.map((session) => [session.pid, session]));
|
|
1322
|
-
let pid =
|
|
1685
|
+
let pid = process11.ppid;
|
|
1323
1686
|
for (let depth = 0;depth < 32 && pid > 1; depth++) {
|
|
1324
1687
|
const session = byPid.get(pid);
|
|
1325
1688
|
if (session !== undefined)
|
|
@@ -1381,9 +1744,10 @@ ${lines.join(`
|
|
|
1381
1744
|
}
|
|
1382
1745
|
async function boardRows(issues, sessions, repo) {
|
|
1383
1746
|
const comments = await commentsOfAll(issues, repo);
|
|
1747
|
+
const author = await sessionAuthor();
|
|
1384
1748
|
return issues.map((issue) => {
|
|
1385
1749
|
const own = comments.get(issue.number) ?? [];
|
|
1386
|
-
return { issue, standing: standing(latestClaim(own), sessions), comments: own };
|
|
1750
|
+
return { issue, standing: standing(latestClaim(own, author), sessions), comments: own };
|
|
1387
1751
|
});
|
|
1388
1752
|
}
|
|
1389
1753
|
function boardJson(rows) {
|
|
@@ -1499,12 +1863,13 @@ async function take(args) {
|
|
|
1499
1863
|
const numbers = issueNumbers(positionals, takeUsage);
|
|
1500
1864
|
const sessions = await liveSessions();
|
|
1501
1865
|
const nameFor = await claimant(values, sessions);
|
|
1866
|
+
const author = await sessionAuthor();
|
|
1502
1867
|
return eachIssue("take", numbers, async (number) => {
|
|
1503
1868
|
const name = nameFor(number);
|
|
1504
1869
|
const [found, comments] = await Promise.all([getIssue(number, values.repo), commentsOf(number, values.repo)]);
|
|
1505
1870
|
if (found.state === "closed")
|
|
1506
1871
|
fail(`#${number} is closed`, 1, runHint(`ft reopen ${number}`, "reopen it"));
|
|
1507
|
-
const claim = latestClaim(comments);
|
|
1872
|
+
const claim = latestClaim(comments, author);
|
|
1508
1873
|
const current = standing(claim, sessions);
|
|
1509
1874
|
if (claim?.holder === name) {
|
|
1510
1875
|
console.log(`#${number} is already taken by ${name}`);
|
|
@@ -1515,6 +1880,9 @@ async function take(args) {
|
|
|
1515
1880
|
if (current.state === "away" && values["take-stale"] !== true)
|
|
1516
1881
|
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
1882
|
await comment(number, takenComment(name), values.repo);
|
|
1883
|
+
const standingNow = latestClaim(await commentsOf(number, values.repo), author);
|
|
1884
|
+
if (standingNow?.holder !== name)
|
|
1885
|
+
fail(`#${number} was taken by ${standingNow?.holder ?? "another session"} at the same moment`, 1);
|
|
1518
1886
|
console.log(`took #${number} as ${name}: ${found.title}`);
|
|
1519
1887
|
});
|
|
1520
1888
|
}
|
|
@@ -1533,9 +1901,10 @@ async function drop(args) {
|
|
|
1533
1901
|
const { values, positionals } = parsed;
|
|
1534
1902
|
const numbers = issueNumbers(positionals, dropUsage);
|
|
1535
1903
|
const nameFor = await claimant(values, await liveSessions());
|
|
1904
|
+
const author = await sessionAuthor();
|
|
1536
1905
|
return eachIssue("drop", numbers, async (number) => {
|
|
1537
1906
|
const name = nameFor(number);
|
|
1538
|
-
const claim = latestClaim(await commentsOf(number, values.repo));
|
|
1907
|
+
const claim = latestClaim(await commentsOf(number, values.repo), author);
|
|
1539
1908
|
if (claim?.holder !== name)
|
|
1540
1909
|
fail(`#${number} is ${claim === undefined ? "not taken" : `taken by ${claim.holder}, not ${name}`}`, 1);
|
|
1541
1910
|
await comment(number, releasedComment(name), values.repo);
|
|
@@ -1544,114 +1913,20 @@ async function drop(args) {
|
|
|
1544
1913
|
}
|
|
1545
1914
|
|
|
1546
1915
|
// src/commands/ci.ts
|
|
1547
|
-
import
|
|
1916
|
+
import process12 from "process";
|
|
1548
1917
|
|
|
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 : ""}`;
|
|
1918
|
+
// src/git.ts
|
|
1919
|
+
async function tagsAt(commit) {
|
|
1920
|
+
return (await git(["tag", "--points-at", commit])).split(`
|
|
1921
|
+
`).filter(Boolean);
|
|
1637
1922
|
}
|
|
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 };
|
|
1923
|
+
async function commitsNaming(issue, ref) {
|
|
1924
|
+
const out = await git(["log", ref, "--reverse", "--extended-regexp", `--grep=#${issue}([^0-9]|$)`, "--format=%H %s"]);
|
|
1925
|
+
return out.split(`
|
|
1926
|
+
`).filter(Boolean).map((line) => {
|
|
1927
|
+
const space = line.indexOf(" ");
|
|
1928
|
+
return { sha: line.slice(0, space), subject: line.slice(space + 1) };
|
|
1648
1929
|
});
|
|
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
1930
|
}
|
|
1656
1931
|
|
|
1657
1932
|
// src/log.ts
|
|
@@ -1728,7 +2003,13 @@ async function recentRuns(limit, repo, workflow) {
|
|
|
1728
2003
|
return (await runsListing(limit, repo, workflow)).runs;
|
|
1729
2004
|
}
|
|
1730
2005
|
async function runsOn(sha, repo) {
|
|
1731
|
-
|
|
2006
|
+
const runs = [];
|
|
2007
|
+
for (let page = 1;; page++) {
|
|
2008
|
+
const list = (await repoApi(`actions/runs?head_sha=${sha}&page=${page}&limit=${RUNS_PAGE}`, repo)).workflow_runs ?? [];
|
|
2009
|
+
runs.push(...list);
|
|
2010
|
+
if (list.length < RUNS_PAGE)
|
|
2011
|
+
return runs;
|
|
2012
|
+
}
|
|
1732
2013
|
}
|
|
1733
2014
|
async function runById(id, repo) {
|
|
1734
2015
|
return repoApi(`actions/runs/${id}`, repo);
|
|
@@ -1814,9 +2095,9 @@ var options = {
|
|
|
1814
2095
|
job: { type: "string", short: "j", description: "a job whose skipping means the run left its work to a newer push" },
|
|
1815
2096
|
timeout: { type: "string", short: "t", description: "how long before giving up (30m), a bare number in minutes" }
|
|
1816
2097
|
};
|
|
1817
|
-
var POLL = 15000;
|
|
1818
2098
|
var NO_RUN = 3 * 60000;
|
|
1819
2099
|
var STALLED = 3 * 60000;
|
|
2100
|
+
var MAX_LOOKUPS = 20;
|
|
1820
2101
|
function judge(runs, jobSkipped, tagged) {
|
|
1821
2102
|
if (runs.length === 0)
|
|
1822
2103
|
return { kind: "none" };
|
|
@@ -1828,7 +2109,13 @@ function judge(runs, jobSkipped, tagged) {
|
|
|
1828
2109
|
return { kind: "runs", runs };
|
|
1829
2110
|
return tagged ? { kind: "runs", runs: runs.map((run) => ({ ...run, status: "skipped" })) } : { kind: "passed-over" };
|
|
1830
2111
|
}
|
|
1831
|
-
|
|
2112
|
+
function commitsToAsk(commits, passedOver, max = MAX_LOOKUPS) {
|
|
2113
|
+
return commits.filter((commit) => !passedOver.has(commit)).slice(0, max);
|
|
2114
|
+
}
|
|
2115
|
+
function settled(latest) {
|
|
2116
|
+
return latest !== undefined && latest.every((run) => FINISHED.has(run.status)) && judge(latest, false, false).kind === "runs" ? latest : undefined;
|
|
2117
|
+
}
|
|
2118
|
+
async function ci2(args) {
|
|
1832
2119
|
const parsed = parse(args, usage, options);
|
|
1833
2120
|
const { values, positionals } = parsed;
|
|
1834
2121
|
const branch = values.branch ?? await defaultBranch();
|
|
@@ -1852,71 +2139,44 @@ async function ci(args) {
|
|
|
1852
2139
|
return coverage;
|
|
1853
2140
|
}
|
|
1854
2141
|
async function covering() {
|
|
1855
|
-
await
|
|
2142
|
+
await fetchBranch(branch);
|
|
1856
2143
|
if (!await gitOk(["merge-base", "--is-ancestor", sha, `origin/${branch}`]))
|
|
1857
2144
|
fail(`${short} is not on origin/${branch}`, 2, runHint(`git push origin ${branch}`));
|
|
1858
2145
|
const later = (await git(["rev-list", "--reverse", "--first-parent", `${sha}..origin/${branch}`])).split(`
|
|
1859
2146
|
`).filter(Boolean);
|
|
1860
|
-
for (const commit of [sha, ...later]) {
|
|
2147
|
+
for (const commit of commitsToAsk([sha, ...later], passedOver)) {
|
|
1861
2148
|
const coverage = await coverageOf(commit);
|
|
1862
2149
|
if (coverage.kind === "runs")
|
|
1863
2150
|
return coverage.runs;
|
|
1864
2151
|
}
|
|
1865
2152
|
return;
|
|
1866
2153
|
}
|
|
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
|
-
}
|
|
2154
|
+
const { dim } = paint(process12.stderr);
|
|
2155
|
+
const runs = await waitForCi({
|
|
2156
|
+
tool: "ft ci",
|
|
2157
|
+
short,
|
|
2158
|
+
giveUpMs: giveUp,
|
|
2159
|
+
noneMs: NO_RUN,
|
|
2160
|
+
stalledMs: STALLED,
|
|
2161
|
+
noneMessage: `no run covers ${short} yet: its own was cancelled by a later push or never started, so the next push's will`,
|
|
2162
|
+
covering,
|
|
2163
|
+
finished: (found) => found.every((run) => FINISHED.has(run.status)),
|
|
2164
|
+
key: (found) => found.map((run) => run.id).join(","),
|
|
2165
|
+
announce: (found) => {
|
|
2166
|
+
for (const run of found)
|
|
2167
|
+
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}`);
|
|
2168
|
+
},
|
|
2169
|
+
links: (found) => found.map((run) => run.html_url).join(" "),
|
|
2170
|
+
settle: async (found) => settled(await refreshed(found)),
|
|
2171
|
+
stall: async (found) => {
|
|
2172
|
+
const stall = await stallReason(found);
|
|
2173
|
+
return stall === undefined ? undefined : { message: `runs queued on ${short} are not moving: ${stall.reason}`, certain: stall.certain };
|
|
2174
|
+
},
|
|
2175
|
+
lines: (spinner, found, started) => [
|
|
2176
|
+
`${spinner} CI on ${short} ${dim(formatDuration(Date.now() - started))}`,
|
|
2177
|
+
...(found ?? []).map((run) => ` ${badge(run.status, process12.stderr)} ${run.workflow_id} ${dim(`run ${run.id}`)}`)
|
|
2178
|
+
]
|
|
2179
|
+
});
|
|
1920
2180
|
const status = overall(runs);
|
|
1921
2181
|
if (status !== "failure") {
|
|
1922
2182
|
console.log(`CI ${status === "success" ? "passed" : "skipped its tests, a release tag's workflow testing them"} on ${short}`);
|
|
@@ -1933,7 +2193,7 @@ run ${run.id} failed${run.commit_sha === sha ? "" : `, testing every commit from
|
|
|
1933
2193
|
}
|
|
1934
2194
|
|
|
1935
2195
|
// src/commands/digest.ts
|
|
1936
|
-
import
|
|
2196
|
+
import process13 from "process";
|
|
1937
2197
|
var RUNS_SCANNED = 50;
|
|
1938
2198
|
var TITLE_WIDTH2 = 70;
|
|
1939
2199
|
var SHOWN = 8;
|
|
@@ -1983,7 +2243,7 @@ async function quietly(read) {
|
|
|
1983
2243
|
}
|
|
1984
2244
|
}
|
|
1985
2245
|
async function staleTodos() {
|
|
1986
|
-
const bin = Bun.which("todo-scan", { PATH:
|
|
2246
|
+
const bin = Bun.which("todo-scan", { PATH: process13.env.PATH ?? "" });
|
|
1987
2247
|
if (bin === null)
|
|
1988
2248
|
return [];
|
|
1989
2249
|
let found;
|
|
@@ -2044,7 +2304,8 @@ async function digest(args) {
|
|
|
2044
2304
|
const [issues, sessions, branch, runs] = await Promise.all([openIssues(undefined, values.repo), liveSessions(), defaultBranch(), recentRuns(RUNS_SCANNED, values.repo)]);
|
|
2045
2305
|
const comments = await commentsOfAll(issues, values.repo);
|
|
2046
2306
|
const me = await currentSession(sessions);
|
|
2047
|
-
const
|
|
2307
|
+
const author = await sessionAuthor();
|
|
2308
|
+
const rows = issues.map((issue) => ({ issue, claim: latestClaim(comments.get(issue.number) ?? [], author) }));
|
|
2048
2309
|
const [unpushed, dirty, stale] = await Promise.all([quietly(unpushedCommits), quietly(dirtyTree), staleTodos()]);
|
|
2049
2310
|
const summary = { ...claims(rows, sessions, me), ci: unsettled(runs, branch), unpushed, dirty, stale };
|
|
2050
2311
|
if (values.json === true)
|
|
@@ -2161,7 +2422,7 @@ var issueOptions = {
|
|
|
2161
2422
|
...repoOption
|
|
2162
2423
|
};
|
|
2163
2424
|
function renderIssue(found, comments, view, now = Date.now()) {
|
|
2164
|
-
const claim = latestClaim(comments);
|
|
2425
|
+
const claim = latestClaim(comments, view.author);
|
|
2165
2426
|
const list = boxes(found.body);
|
|
2166
2427
|
const left = list.filter((box) => !box.done);
|
|
2167
2428
|
const facts = [found.state];
|
|
@@ -2189,14 +2450,14 @@ ${each.body.replaceAll(`\r
|
|
|
2189
2450
|
|
|
2190
2451
|
`);
|
|
2191
2452
|
}
|
|
2192
|
-
function issueJson(found, comments) {
|
|
2453
|
+
function issueJson(found, comments, author) {
|
|
2193
2454
|
return {
|
|
2194
2455
|
number: found.number,
|
|
2195
2456
|
title: found.title,
|
|
2196
2457
|
state: found.state,
|
|
2197
2458
|
url: found.html_url,
|
|
2198
2459
|
labels: found.labels.map((label) => label.name),
|
|
2199
|
-
claim: latestClaim(comments) ?? null,
|
|
2460
|
+
claim: latestClaim(comments, author) ?? null,
|
|
2200
2461
|
boxes: boxes(found.body).map(({ number, done, text }) => ({ number, done, text })),
|
|
2201
2462
|
body: found.body,
|
|
2202
2463
|
comments: comments.map((each) => ({ author: each.user.login, at: each.created_at, body: each.body }))
|
|
@@ -2216,12 +2477,13 @@ async function issue(args) {
|
|
|
2216
2477
|
return 0;
|
|
2217
2478
|
}
|
|
2218
2479
|
const read = await Promise.all(numbers.map(async (number) => Promise.all([getIssue(number, values.repo), commentsOf(number, values.repo)])));
|
|
2480
|
+
const author = await sessionAuthor();
|
|
2219
2481
|
if (values.json === true) {
|
|
2220
|
-
const json = read.map(([found, comments]) => issueJson(found, comments));
|
|
2482
|
+
const json = read.map(([found, comments]) => issueJson(found, comments, author));
|
|
2221
2483
|
printJson(json.length === 1 ? json[0] : json);
|
|
2222
2484
|
return 0;
|
|
2223
2485
|
}
|
|
2224
|
-
const view = { todo: values.todo === true, comments: values.comments === true };
|
|
2486
|
+
const view = { todo: values.todo === true, comments: values.comments === true, author };
|
|
2225
2487
|
console.log(read.map(([found, comments]) => renderIssue(found, comments, view)).join(view.todo ? `
|
|
2226
2488
|
|
|
2227
2489
|
` : `
|
|
@@ -2240,11 +2502,14 @@ async function noteText(values) {
|
|
|
2240
2502
|
const text = values.file === undefined ? values.message : await readInput(values.file);
|
|
2241
2503
|
return text?.trim() === "" ? undefined : text?.trim();
|
|
2242
2504
|
}
|
|
2505
|
+
var NOT_FOUND = /not exist|not found|couldn't be found/i;
|
|
2243
2506
|
async function isClosed(number, repo) {
|
|
2244
2507
|
try {
|
|
2245
2508
|
return (await getIssue(number, repo)).state === "closed";
|
|
2246
|
-
} catch {
|
|
2247
|
-
|
|
2509
|
+
} catch (error) {
|
|
2510
|
+
if (error instanceof Error && NOT_FOUND.test(error.message))
|
|
2511
|
+
return false;
|
|
2512
|
+
throw error;
|
|
2248
2513
|
}
|
|
2249
2514
|
}
|
|
2250
2515
|
async function closedReferences(body, repo) {
|
|
@@ -2317,17 +2582,21 @@ ${editUsage}`);
|
|
|
2317
2582
|
const milestone = await milestoneTitle(values.milestone, values.repo);
|
|
2318
2583
|
if (milestone !== undefined)
|
|
2319
2584
|
change.milestone = milestone;
|
|
2320
|
-
|
|
2585
|
+
const grows = text !== undefined && values.append === true || added.length > 0;
|
|
2586
|
+
if (text !== undefined && !grows)
|
|
2321
2587
|
change.body = text;
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
if (
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2588
|
+
if (Object.keys(change).length > 0)
|
|
2589
|
+
await editIssue(number, change, values.repo);
|
|
2590
|
+
if (grows) {
|
|
2591
|
+
await editBody(number, (body) => {
|
|
2592
|
+
const appended = text !== undefined && values.append === true && !body.trimEnd().endsWith(text) ? `${body.trimEnd()}
|
|
2593
|
+
|
|
2594
|
+
${text}` : body;
|
|
2595
|
+
const present = new Set(boxes(appended).map((box) => box.text));
|
|
2596
|
+
const missing = added.filter((box) => !present.has(box));
|
|
2597
|
+
return missing.length > 0 ? appendBoxes(appended, missing) : appended;
|
|
2598
|
+
}, values.repo);
|
|
2599
|
+
}
|
|
2331
2600
|
console.log(`edited #${number}`);
|
|
2332
2601
|
return 0;
|
|
2333
2602
|
}
|
|
@@ -2343,6 +2612,13 @@ var tickOptions = {
|
|
|
2343
2612
|
...noteOptions,
|
|
2344
2613
|
...repoOption
|
|
2345
2614
|
};
|
|
2615
|
+
function tickedOrFail(number, run) {
|
|
2616
|
+
try {
|
|
2617
|
+
return run();
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
fail(`#${number}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2346
2622
|
async function tick(args) {
|
|
2347
2623
|
const parsed = parse(args, tickUsage, tickOptions);
|
|
2348
2624
|
const { values, positionals } = parsed;
|
|
@@ -2360,17 +2636,13 @@ ${tickUsage}`);
|
|
|
2360
2636
|
const found = await getIssue(number, values.repo);
|
|
2361
2637
|
const done = values.undo !== true;
|
|
2362
2638
|
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
|
-
}
|
|
2639
|
+
const apply = (body) => closed === undefined ? setBoxes(body, picks, done) : tickReferences(body, (issue) => closed.has(issue));
|
|
2640
|
+
let result = tickedOrFail(number, () => apply(found.body));
|
|
2369
2641
|
if (result.changed.length > 0) {
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2642
|
+
await editBody(number, (body) => {
|
|
2643
|
+
result = tickedOrFail(number, () => apply(body));
|
|
2644
|
+
return result.body;
|
|
2645
|
+
}, values.repo);
|
|
2374
2646
|
}
|
|
2375
2647
|
for (const box of result.changed)
|
|
2376
2648
|
console.log(`${done ? "ticked" : "unticked"} ${box.number}: ${box.text}`);
|
|
@@ -2461,7 +2733,7 @@ async function fixed(args) {
|
|
|
2461
2733
|
fail(`#${number} is already closed`, 1, runHint(`ft reopen ${number}`, "reopen it"));
|
|
2462
2734
|
const left = boxes(found.body).filter((box) => !box.done);
|
|
2463
2735
|
if (left.length > 0 && values.tick === true) {
|
|
2464
|
-
await
|
|
2736
|
+
await editBody(number, (body) => setBoxes(body, boxes(body).filter((box) => !box.done).map((box) => String(box.number)), true).body, values.repo);
|
|
2465
2737
|
console.log(`ticked ${left.length} box${left.length === 1 ? "" : "es"} of #${number}`);
|
|
2466
2738
|
} else if (left.length > 0 && values.force !== true) {
|
|
2467
2739
|
fail(`#${number} has ${left.length} boxes left:
|
|
@@ -2469,7 +2741,9 @@ ${left.map((box) => `${String(box.number).padStart(3)} [ ] ${box.text}`).join(`
|
|
|
2469
2741
|
`)}`, 1, `${runHint(`ft fixed ${number} --tick`, "tick them and close")}, or ${runHint(`ft fixed ${number} --force`, "close with them left")}`);
|
|
2470
2742
|
}
|
|
2471
2743
|
const shas = joinShas(commits.map((each) => each.sha.slice(0, 7)));
|
|
2472
|
-
|
|
2744
|
+
const line = `Fixed in ${shas}: ${values.message ?? commits[0]?.subject ?? ""}`;
|
|
2745
|
+
if ((await commentsOf(number, values.repo)).at(-1)?.body.trim() !== line.trim())
|
|
2746
|
+
await comment(number, line, values.repo);
|
|
2473
2747
|
await close(number, values.repo);
|
|
2474
2748
|
console.log(`closed #${number}: fixed in ${shas}`);
|
|
2475
2749
|
await tickListings(number, values.repo);
|
|
@@ -2484,22 +2758,22 @@ With no failed job it prints every job's last lines; --full prints whole logs.
|
|
|
2484
2758
|
|
|
2485
2759
|
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
2760
|
var options3 = {
|
|
2487
|
-
job: { type: "string", short: "j", description: "only this job, by name" },
|
|
2761
|
+
job: { type: "string", short: "j", description: "only this job, by name or id" },
|
|
2488
2762
|
full: { type: "boolean", short: "f", description: "the whole log, timestamps stripped" },
|
|
2489
2763
|
...repoOption
|
|
2490
2764
|
};
|
|
2491
|
-
function pickJobs(jobs,
|
|
2492
|
-
if (
|
|
2493
|
-
return jobs.filter((job) => job.name ===
|
|
2765
|
+
function pickJobs(jobs, wanted) {
|
|
2766
|
+
if (wanted !== undefined)
|
|
2767
|
+
return jobs.filter((job) => job.name === wanted || String(job.id) === wanted);
|
|
2494
2768
|
const failed = jobs.filter((job) => job.status === "failure");
|
|
2495
2769
|
return failed.length > 0 ? failed : jobs;
|
|
2496
2770
|
}
|
|
2497
|
-
async function
|
|
2771
|
+
async function logs2(args) {
|
|
2498
2772
|
const parsed = parse(args, usage3, options3);
|
|
2499
2773
|
const { values, positionals } = parsed;
|
|
2500
2774
|
if (positionals[0] === undefined)
|
|
2501
2775
|
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);
|
|
2776
|
+
const run = await runById(wholeNumber(positionals[0], "<run id>", 0), values.repo);
|
|
2503
2777
|
const jobs = await jobsOf(run.id, values.repo);
|
|
2504
2778
|
const picked = pickJobs(jobs, values.job);
|
|
2505
2779
|
if (picked.length === 0)
|
|
@@ -2776,8 +3050,8 @@ function report(plan, added, skipped) {
|
|
|
2776
3050
|
|
|
2777
3051
|
// src/commands/release.ts
|
|
2778
3052
|
import { basename as basename2, join as join2, relative, resolve } from "path";
|
|
2779
|
-
import
|
|
2780
|
-
var {
|
|
3053
|
+
import process14 from "process";
|
|
3054
|
+
var {$ } = globalThis.Bun;
|
|
2781
3055
|
|
|
2782
3056
|
// src/commands/watch.ts
|
|
2783
3057
|
var usage4 = `ft watch [--after <id>]
|
|
@@ -2795,18 +3069,15 @@ var STALLED2 = 3 * 60000;
|
|
|
2795
3069
|
async function watchRuns(pick, timeout, repo, idle) {
|
|
2796
3070
|
const started = Date.now();
|
|
2797
3071
|
const reported = new Map;
|
|
3072
|
+
const followed = new Set;
|
|
3073
|
+
const failed = new Set;
|
|
2798
3074
|
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
|
-
}
|
|
3075
|
+
async function poll() {
|
|
3076
|
+
const newest = await recentRuns(LIMIT, repo);
|
|
3077
|
+
const gone = await Promise.all([...followed].filter((id) => !newest.some((run) => run.id === id)).map(async (id) => runById(id, repo)));
|
|
3078
|
+
const runs = [...pick(newest), ...gone];
|
|
3079
|
+
for (const run of runs)
|
|
3080
|
+
followed.add(run.id);
|
|
2810
3081
|
for (const run of runs) {
|
|
2811
3082
|
if (reported.get(run.id) !== run.status)
|
|
2812
3083
|
console.log(`${run.workflow_id} run ${run.id} on ${run.prettyref}: ${badge(run.status, process.stdout)}`);
|
|
@@ -2817,26 +3088,44 @@ async function watchRuns(pick, timeout, repo, idle) {
|
|
|
2817
3088
|
console.log("no run started, so nothing was waited for");
|
|
2818
3089
|
return 0;
|
|
2819
3090
|
}
|
|
2820
|
-
|
|
3091
|
+
return;
|
|
2821
3092
|
}
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
if (run.status === "failure")
|
|
2826
|
-
console.error(`
|
|
3093
|
+
for (const run of runs) {
|
|
3094
|
+
if (run.status === "failure" && !failed.has(run.id)) {
|
|
3095
|
+
console.error(`
|
|
2827
3096
|
${await failureReport(run, repo)}
|
|
2828
3097
|
|
|
2829
3098
|
${run.html_url}`);
|
|
3099
|
+
failed.add(run.id);
|
|
2830
3100
|
}
|
|
2831
|
-
return 1;
|
|
2832
3101
|
}
|
|
2833
|
-
if (runs.every((run) => FINISHED.has(run.status)))
|
|
2834
|
-
|
|
3102
|
+
if (runs.every((run) => FINISHED.has(run.status))) {
|
|
3103
|
+
const status = overall(runs);
|
|
3104
|
+
return status === "failure" || status === "cancelled" ? 1 : 0;
|
|
3105
|
+
}
|
|
2835
3106
|
const stall = await stallReason(runs, repo);
|
|
2836
3107
|
if (stall === undefined)
|
|
2837
3108
|
moving = Date.now();
|
|
2838
3109
|
else if (stall.certain || Date.now() - moving > STALLED2)
|
|
2839
3110
|
fail(`queued runs are not moving: ${stall.reason}`);
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
for (;; ) {
|
|
3114
|
+
await Bun.sleep(timing.poll);
|
|
3115
|
+
if (Date.now() - started > timeout)
|
|
3116
|
+
fail(`the runs did not finish in ${formatDuration(timeout)}`);
|
|
3117
|
+
try {
|
|
3118
|
+
const code = await poll();
|
|
3119
|
+
if (code !== undefined)
|
|
3120
|
+
return code;
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
if (error instanceof Exit)
|
|
3123
|
+
throw error;
|
|
3124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3125
|
+
if (isPermanentFailure(error))
|
|
3126
|
+
fail(message);
|
|
3127
|
+
console.warn(`ft watch: ${message}, polling again`);
|
|
3128
|
+
}
|
|
2840
3129
|
}
|
|
2841
3130
|
}
|
|
2842
3131
|
async function watch(args) {
|
|
@@ -2862,7 +3151,7 @@ ft release --tagged
|
|
|
2862
3151
|
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
3152
|
--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
3153
|
--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.
|
|
3154
|
+
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
3155
|
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
3156
|
--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
3157
|
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 +3168,8 @@ var PARTS = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/;
|
|
|
2879
3168
|
var LAST_NUMBER = /\d+$/;
|
|
2880
3169
|
var REGISTRY_TRIES = 12;
|
|
2881
3170
|
var REGISTRY_POLL = 5000;
|
|
3171
|
+
var limits = { pushMs: 5 * 60000 };
|
|
3172
|
+
var REGISTRY_INFO_MS = 30000;
|
|
2882
3173
|
var NO_RUN2 = 3 * 60000;
|
|
2883
3174
|
function taggedVersion(tag, prefix) {
|
|
2884
3175
|
const version = tag.slice(prefix.length);
|
|
@@ -2921,7 +3212,7 @@ async function commitPackageBump(dir, prefix) {
|
|
|
2921
3212
|
const { version } = await Bun.file(join2(dir, "package.json")).json();
|
|
2922
3213
|
const root = await git(["rev-parse", "--show-toplevel"]);
|
|
2923
3214
|
const tag = `${prefix}${version}`;
|
|
2924
|
-
const lock = await
|
|
3215
|
+
const lock = await $`bun install --lockfile-only`.cwd(root).quiet().nothrow();
|
|
2925
3216
|
if (lock.exitCode !== 0)
|
|
2926
3217
|
fail(`bun install --lockfile-only after the bump: ${lock.stderr.toString().trim()}`);
|
|
2927
3218
|
await git(["add", join2(dir, "package.json"), join2(root, "bun.lock")]);
|
|
@@ -2930,8 +3221,14 @@ async function commitPackageBump(dir, prefix) {
|
|
|
2930
3221
|
}
|
|
2931
3222
|
async function waitForRegistry(name, version) {
|
|
2932
3223
|
for (let attempt = 0;attempt < REGISTRY_TRIES; attempt++) {
|
|
2933
|
-
|
|
3224
|
+
let served;
|
|
3225
|
+
try {
|
|
3226
|
+
served = (await runTool("bun info", ["bun", "info", name, "version", "--no-cache"], { timeoutMs: REGISTRY_INFO_MS })).stdout.trim().split(`
|
|
2934
3227
|
`).at(-1);
|
|
3228
|
+
} catch (error) {
|
|
3229
|
+
if (isPermanentFailure(error))
|
|
3230
|
+
throw error;
|
|
3231
|
+
}
|
|
2935
3232
|
if (served === version)
|
|
2936
3233
|
return true;
|
|
2937
3234
|
await Bun.sleep(REGISTRY_POLL);
|
|
@@ -2940,10 +3237,18 @@ async function waitForRegistry(name, version) {
|
|
|
2940
3237
|
}
|
|
2941
3238
|
async function testsPass(sha, branch, timeout) {
|
|
2942
3239
|
if (!await gitOk(["merge-base", "--is-ancestor", sha, `origin/${branch}`])) {
|
|
2943
|
-
const
|
|
2944
|
-
|
|
3240
|
+
const held = holdSignals();
|
|
3241
|
+
let push;
|
|
3242
|
+
try {
|
|
3243
|
+
push = await runTool("git push", ["git", "push", "origin", `${sha}:refs/heads/${branch}`], { timeoutMs: limits.pushMs, onStart: (child) => held.forward(child) });
|
|
3244
|
+
} finally {
|
|
3245
|
+
held.release();
|
|
3246
|
+
}
|
|
3247
|
+
if (held.signal !== undefined)
|
|
3248
|
+
fail(`interrupted by ${held.signal} while pushing ${sha.slice(0, 7)}, so nothing was bumped`, exitCodeOf(null, held.signal));
|
|
3249
|
+
if (push.code !== 0)
|
|
2945
3250
|
fail(`origin refused the push of ${sha.slice(0, 7)}, so nothing was bumped:
|
|
2946
|
-
${push.stderr.
|
|
3251
|
+
${push.stderr.trim()}`, 1);
|
|
2947
3252
|
console.log(`pushed ${branch} at ${sha.slice(0, 7)}, waiting for its runs before the bump`);
|
|
2948
3253
|
}
|
|
2949
3254
|
return await watchRuns((runs) => newestPerWorkflow(runs.filter((run) => run.commit_sha === sha)), timeout, undefined, NO_RUN2) === 0;
|
|
@@ -2951,8 +3256,8 @@ ${push.stderr.toString().trim()}`, 1);
|
|
|
2951
3256
|
async function printPlan({ level, dir, prefix, branch, base, timeout, registry }) {
|
|
2952
3257
|
const file = Bun.file(join2(dir ?? ".", "package.json"));
|
|
2953
3258
|
const pkg = await file.exists() ? await file.json() : undefined;
|
|
2954
|
-
const name = pkg?.name ?? basename2(dir ??
|
|
2955
|
-
const where = dir === undefined ? "" : ` in ${relative(
|
|
3259
|
+
const name = pkg?.name ?? basename2(dir ?? process14.cwd());
|
|
3260
|
+
const where = dir === undefined ? "" : ` in ${relative(process14.cwd(), dir) || "."}`;
|
|
2956
3261
|
const sha = base.slice(0, 7);
|
|
2957
3262
|
let version;
|
|
2958
3263
|
let bump;
|
|
@@ -2983,6 +3288,17 @@ async function printPlan({ level, dir, prefix, branch, base, timeout, registry }
|
|
|
2983
3288
|
console.log("dry run: nothing was pushed, committed or tagged");
|
|
2984
3289
|
return 0;
|
|
2985
3290
|
}
|
|
3291
|
+
async function rollBack(preBump, tagsBefore) {
|
|
3292
|
+
try {
|
|
3293
|
+
for (const name of await tagsAt("HEAD")) {
|
|
3294
|
+
if (!tagsBefore.has(name))
|
|
3295
|
+
await git(["tag", "-d", name]);
|
|
3296
|
+
}
|
|
3297
|
+
await git(["reset", "--keep", preBump]);
|
|
3298
|
+
} catch (error) {
|
|
3299
|
+
warn(`could not undo the bump, back to ${preBump.slice(0, 7)} by hand`, error instanceof Error ? error.message : String(error));
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
2986
3302
|
async function release(args) {
|
|
2987
3303
|
const parsed = parse(args, usage5, options6);
|
|
2988
3304
|
const { values, positionals } = parsed;
|
|
@@ -3003,34 +3319,61 @@ ${usage5}`);
|
|
|
3003
3319
|
fail(`a release is cut from ${branch}, not ${current || "a detached HEAD"}`, 2, runHint(`git switch ${branch}`));
|
|
3004
3320
|
if (await git(["status", "--porcelain"]) !== "")
|
|
3005
3321
|
fail("the tree has uncommitted changes", 2, runHint("gst", "see them, then commit them with gc"));
|
|
3006
|
-
await
|
|
3007
|
-
if ((await
|
|
3322
|
+
await fetchBranch(branch);
|
|
3323
|
+
if ((await $`git merge-base --is-ancestor ${`origin/${branch}`} HEAD`.quiet().nothrow()).exitCode !== 0)
|
|
3008
3324
|
fail(`origin/${branch} has commits HEAD lacks`, 2, runHint(`git pull --rebase origin ${branch}`));
|
|
3009
3325
|
const base = await git(["rev-parse", tagged ? "HEAD~1" : "HEAD"]);
|
|
3010
3326
|
if (values["dry-run"] === true)
|
|
3011
3327
|
return printPlan({ level, dir, prefix, branch, base, timeout, registry: values["no-registry"] !== true });
|
|
3012
3328
|
if (!await testsPass(base, branch, timeout))
|
|
3013
3329
|
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`);
|
|
3330
|
+
const preBump = await git(["rev-parse", "HEAD"]);
|
|
3331
|
+
const tagsBefore = new Set(await tagsAt("HEAD"));
|
|
3024
3332
|
const before = (await recentRuns(1))[0]?.id ?? 0;
|
|
3025
|
-
const
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3333
|
+
const held = holdSignals();
|
|
3334
|
+
const signalOf = () => held.signal;
|
|
3335
|
+
let tag = "";
|
|
3336
|
+
let head = "";
|
|
3337
|
+
let pushed = false;
|
|
3338
|
+
let isPushUnknown = false;
|
|
3339
|
+
try {
|
|
3340
|
+
if (level !== undefined) {
|
|
3341
|
+
const bump = dir === undefined ? await $`bun pm version ${level}`.quiet().nothrow() : await $`bun pm version ${level} --no-git-tag-version`.cwd(dir).quiet().nothrow();
|
|
3342
|
+
if (bump.exitCode !== 0)
|
|
3343
|
+
fail(`bun pm version ${level}: ${bump.stderr.toString().trim() || bump.stdout.toString().trim()}`);
|
|
3344
|
+
if (dir !== undefined)
|
|
3345
|
+
await commitPackageBump(dir, prefix);
|
|
3346
|
+
}
|
|
3347
|
+
const found = (await tagsAt("HEAD")).find((name) => taggedVersion(name, prefix) !== undefined);
|
|
3348
|
+
if (found === undefined)
|
|
3349
|
+
fail(tagged ? `HEAD carries no ${prefix}* tag` : `the bump made no ${prefix}* tag`);
|
|
3350
|
+
tag = found;
|
|
3351
|
+
head = await git(["rev-parse", "HEAD"]);
|
|
3352
|
+
if (held.signal !== undefined)
|
|
3353
|
+
fail(`interrupted by ${held.signal} before the push`, exitCodeOf(null, held.signal));
|
|
3354
|
+
let push;
|
|
3355
|
+
try {
|
|
3356
|
+
push = await runTool("git push", ["git", "push", "--atomic", "origin", `HEAD:refs/heads/${branch}`, tag], { timeoutMs: limits.pushMs, onStart: (child) => held.forward(child) });
|
|
3357
|
+
} catch (error) {
|
|
3358
|
+
isPushUnknown = true;
|
|
3359
|
+
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`);
|
|
3360
|
+
}
|
|
3361
|
+
if (push.code !== 0) {
|
|
3362
|
+
const signal = signalOf();
|
|
3363
|
+
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`}:
|
|
3364
|
+
${push.stderr.trim()}`, signal === undefined ? 1 : exitCodeOf(null, signal));
|
|
3365
|
+
}
|
|
3366
|
+
pushed = true;
|
|
3367
|
+
} finally {
|
|
3368
|
+
held.release();
|
|
3369
|
+
if (!pushed && !isPushUnknown && level !== undefined)
|
|
3370
|
+
await rollBack(preBump, tagsBefore);
|
|
3031
3371
|
}
|
|
3372
|
+
const late = signalOf();
|
|
3373
|
+
if (late !== undefined)
|
|
3374
|
+
fail(`interrupted by ${late} after ${tag} was pushed: its runs are not waited for`, exitCodeOf(null, late));
|
|
3032
3375
|
console.log(`pushed ${branch} and ${tag}, waiting for their runs`);
|
|
3033
|
-
const code = await watchRuns((runs) => runs.filter((run) => run.id > before), timeout);
|
|
3376
|
+
const code = await watchRuns((runs) => runs.filter((run) => run.id > before && run.commit_sha === head), timeout, undefined, NO_RUN2);
|
|
3034
3377
|
if (code !== 0)
|
|
3035
3378
|
return code;
|
|
3036
3379
|
const file = Bun.file(join2(dir ?? ".", "package.json"));
|
|
@@ -3069,7 +3412,7 @@ ${done.map((issue) => ` #${issue.number} ${issue.title}`).join(`
|
|
|
3069
3412
|
// src/commands/remote.ts
|
|
3070
3413
|
import { lookup } from "dns/promises";
|
|
3071
3414
|
import { homedir as homedir3 } from "os";
|
|
3072
|
-
var {$: $
|
|
3415
|
+
var {$: $2 } = globalThis.Bun;
|
|
3073
3416
|
|
|
3074
3417
|
// src/config.ts
|
|
3075
3418
|
import { homedir as homedir2 } from "os";
|
|
@@ -3078,30 +3421,53 @@ function settingsPath(home = homedir2()) {
|
|
|
3078
3421
|
return join3(home, ".config", "ft", "config.json");
|
|
3079
3422
|
}
|
|
3080
3423
|
async function loadSettings(home = homedir2()) {
|
|
3081
|
-
const
|
|
3424
|
+
const path = settingsPath(home);
|
|
3425
|
+
const file = Bun.file(path);
|
|
3082
3426
|
if (!await file.exists())
|
|
3083
3427
|
return {};
|
|
3084
3428
|
let raw;
|
|
3085
3429
|
try {
|
|
3086
3430
|
raw = await file.json();
|
|
3087
|
-
} catch {
|
|
3431
|
+
} catch (error) {
|
|
3432
|
+
warn(`${path} is not valid JSON, so its settings are ignored`, error instanceof Error ? error.message : String(error));
|
|
3433
|
+
return {};
|
|
3434
|
+
}
|
|
3435
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
3436
|
+
warn(`${path} is not a JSON object, so its settings are ignored`);
|
|
3088
3437
|
return {};
|
|
3089
3438
|
}
|
|
3090
|
-
const host =
|
|
3439
|
+
const host = "remoteHost" in raw ? raw.remoteHost : undefined;
|
|
3440
|
+
if (host !== undefined && (typeof host !== "string" || host === ""))
|
|
3441
|
+
warn(`remoteHost in ${path} must be a non-empty string, so it is ignored`);
|
|
3091
3442
|
return typeof host === "string" && host !== "" ? { remoteHost: host } : {};
|
|
3092
3443
|
}
|
|
3093
3444
|
|
|
3094
3445
|
// src/remote.ts
|
|
3095
3446
|
var SCP_LIKE = /^(?:([^@/]+)@)?([^:/]+):(?!\/)(.+?)(?:\.git)?\/?$/;
|
|
3096
|
-
var SSH_URL = /^ssh:\/\/(?:([^@/]+)@)?([^:/]+)(
|
|
3447
|
+
var SSH_URL = /^ssh:\/\/(?:([^@/]+)@)?([^:/]+)(?::(\d+))?\/(.+?)(?:\.git)?\/?$/;
|
|
3448
|
+
var USERINFO = /^([a-z][a-z\d+.-]*:\/\/)[^@/]*@/i;
|
|
3449
|
+
var DEFAULT_PORT = "22";
|
|
3097
3450
|
function parseSshRemote(url) {
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3451
|
+
const ssh = SSH_URL.exec(url);
|
|
3452
|
+
const scp = ssh === null && !url.includes("://") ? SCP_LIKE.exec(url) : null;
|
|
3453
|
+
const match = ssh ?? scp;
|
|
3454
|
+
if (match === null || match[2]?.startsWith("-") === true)
|
|
3455
|
+
return;
|
|
3456
|
+
const login = match[1] ?? "git";
|
|
3457
|
+
const user = login.includes(":") ? login.slice(0, login.indexOf(":")) : login;
|
|
3458
|
+
if (ssh === null)
|
|
3459
|
+
return { user, host: match[2] ?? "", path: match[3] ?? "" };
|
|
3460
|
+
return { user, host: match[2] ?? "", path: match[4] ?? "", ...ssh[3] === undefined ? {} : { port: ssh[3] } };
|
|
3461
|
+
}
|
|
3462
|
+
function redactUrl(url) {
|
|
3463
|
+
return url.replace(USERINFO, "$1");
|
|
3100
3464
|
}
|
|
3101
3465
|
function canonicalRemote(url, host, reaches) {
|
|
3102
3466
|
const remote = parseSshRemote(url);
|
|
3103
3467
|
if (remote === undefined || remote.host === host || !reaches(remote.host))
|
|
3104
3468
|
return;
|
|
3469
|
+
if (remote.port !== undefined && remote.port !== DEFAULT_PORT)
|
|
3470
|
+
return `ssh://${remote.user}@${host}:${remote.port}/${remote.path}.git`;
|
|
3105
3471
|
return `${remote.user}@${host}:${remote.path}.git`;
|
|
3106
3472
|
}
|
|
3107
3473
|
async function reachesHost(hostname, host, hostAddresses, addressesOf) {
|
|
@@ -3124,7 +3490,7 @@ var options7 = {
|
|
|
3124
3490
|
};
|
|
3125
3491
|
var HOSTNAME = /^hostname (.+)$/m;
|
|
3126
3492
|
async function sshHostname(alias) {
|
|
3127
|
-
return HOSTNAME.exec(await $
|
|
3493
|
+
return HOSTNAME.exec(await $2`ssh -G -- ${alias}`.quiet().nothrow().text())?.[1];
|
|
3128
3494
|
}
|
|
3129
3495
|
async function addresses(name) {
|
|
3130
3496
|
try {
|
|
@@ -3133,6 +3499,14 @@ async function addresses(name) {
|
|
|
3133
3499
|
return [];
|
|
3134
3500
|
}
|
|
3135
3501
|
}
|
|
3502
|
+
async function restoreOrigin(fetchUrl, pushUrls, changes) {
|
|
3503
|
+
if (changes.some(([from]) => from === fetchUrl))
|
|
3504
|
+
await git(["remote", "set-url", "origin", fetchUrl]);
|
|
3505
|
+
for (const [from, to] of changes) {
|
|
3506
|
+
if (pushUrls.includes(from))
|
|
3507
|
+
await git(["remote", "set-url", "--push", "origin", from, to]);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3136
3510
|
function originHost(url) {
|
|
3137
3511
|
return parseSshRemote(url)?.host ?? (URL.canParse(url) ? new URL(url).hostname : undefined);
|
|
3138
3512
|
}
|
|
@@ -3144,7 +3518,7 @@ async function remote(args, home = homedir3()) {
|
|
|
3144
3518
|
return 0;
|
|
3145
3519
|
}
|
|
3146
3520
|
const fetchUrl = await git(["remote", "get-url", "origin"]);
|
|
3147
|
-
const pushUrls = (await $
|
|
3521
|
+
const pushUrls = (await $2`git config --get-all remote.origin.pushurl`.quiet().nothrow().text()).split(`
|
|
3148
3522
|
`).filter(Boolean);
|
|
3149
3523
|
const hostAddresses = new Set(await addresses(host));
|
|
3150
3524
|
const hostnames = new Map;
|
|
@@ -3162,15 +3536,15 @@ async function remote(args, home = homedir3()) {
|
|
|
3162
3536
|
if (changes.length === 0) {
|
|
3163
3537
|
const named = originHost(fetchUrl);
|
|
3164
3538
|
if (named === host) {
|
|
3165
|
-
console.log(`origin already names ${host}: ${fetchUrl}`);
|
|
3539
|
+
console.log(`origin already names ${host}: ${redactUrl(fetchUrl)}`);
|
|
3166
3540
|
return 0;
|
|
3167
3541
|
}
|
|
3168
3542
|
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`);
|
|
3543
|
+
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
3544
|
return 1;
|
|
3171
3545
|
}
|
|
3172
3546
|
for (const [from, to] of changes)
|
|
3173
|
-
console.log(`${from} -> ${to}`);
|
|
3547
|
+
console.log(`${redactUrl(from)} -> ${redactUrl(to)}`);
|
|
3174
3548
|
if (values.fix !== true) {
|
|
3175
3549
|
console.log("run `ft remote --fix` to switch them");
|
|
3176
3550
|
return 1;
|
|
@@ -3182,7 +3556,12 @@ async function remote(args, home = homedir3()) {
|
|
|
3182
3556
|
if (pushUrls.includes(from))
|
|
3183
3557
|
await git(["remote", "set-url", "--push", "origin", to, from]);
|
|
3184
3558
|
}
|
|
3185
|
-
|
|
3559
|
+
try {
|
|
3560
|
+
await git(["ls-remote", "origin", "HEAD"]);
|
|
3561
|
+
} catch (error) {
|
|
3562
|
+
await restoreOrigin(fetchUrl, pushUrls, changes);
|
|
3563
|
+
return fail(`the switched origin does not answer, so the old URLs were put back: ${redactUrl(error instanceof Error ? error.message : String(error))}`, 1);
|
|
3564
|
+
}
|
|
3186
3565
|
console.log("origin switched, and git ls-remote reaches it");
|
|
3187
3566
|
return 0;
|
|
3188
3567
|
}
|
|
@@ -3234,7 +3613,7 @@ async function runs(args) {
|
|
|
3234
3613
|
}
|
|
3235
3614
|
|
|
3236
3615
|
// src/cli.ts
|
|
3237
|
-
|
|
3616
|
+
process15.exitCode = await runCli({
|
|
3238
3617
|
name: "ft",
|
|
3239
3618
|
version,
|
|
3240
3619
|
about: "Forgejo tools over tea, run from a repository whose origin is on Forgejo",
|
|
@@ -3242,9 +3621,9 @@ process14.exitCode = await runCli({
|
|
|
3242
3621
|
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
3622
|
commands: {
|
|
3244
3623
|
runs: { run: runs, summary: "recent runs with workflow, ref, duration and age" },
|
|
3245
|
-
ci: { run:
|
|
3624
|
+
ci: { run: ci2, summary: "wait for the runs covering a commit, printing what failed" },
|
|
3246
3625
|
watch: { run: watch, summary: "wait for runs newer than an id, or those in progress" },
|
|
3247
|
-
logs: { run:
|
|
3626
|
+
logs: { run: logs2, summary: "what the failed jobs of a run say went wrong" },
|
|
3248
3627
|
release: { run: release, summary: "bump, push branch and tag atomically, wait for runs and registry" },
|
|
3249
3628
|
board: { run: board, summary: "open issues: available, taken by a live session, or claimed by one gone" },
|
|
3250
3629
|
digest: { run: digest, summary: "repo state for a cold start: held and abandoned claims, red CI, unpushed commits, dirty files, stale TODOs" },
|