ryugi-artifacts 1.32.0 → 1.33.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/dist/cli.cjs
CHANGED
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli/main.ts
|
|
27
|
-
var
|
|
27
|
+
var import_node_child_process4 = require("node:child_process");
|
|
28
28
|
var import_node_util = require("node:util");
|
|
29
29
|
|
|
30
30
|
// src/cli/paths.ts
|
|
@@ -516,17 +516,467 @@ function config(options, emit) {
|
|
|
516
516
|
return 0;
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
-
// src/cli/
|
|
519
|
+
// src/cli/monitor-view.ts
|
|
520
|
+
var import_node_os2 = __toESM(require("node:os"), 1);
|
|
521
|
+
var REFRESH_SECONDS = 2;
|
|
522
|
+
var BOLD = "\x1B[1m";
|
|
523
|
+
var DIM = "\x1B[2m";
|
|
524
|
+
var INVERSE = "\x1B[7m";
|
|
525
|
+
var RESET = "\x1B[0m";
|
|
526
|
+
var HELP = "\u2191\u2193 select o open c copy URL l logs x stop X stop all safe q quit";
|
|
527
|
+
var COLUMNS = [["PORT", 6], ["PID", 8], ["UP", 7], ["TABS", 5], ["IDLE", 7], ["VERSION", 13]];
|
|
528
|
+
var FIXED = 2 + COLUMNS.reduce((sum, [, width]) => sum + width + 2, 0);
|
|
529
|
+
var TAGS_WIDTH = 26;
|
|
530
|
+
function duration(ms) {
|
|
531
|
+
const minutes = Math.max(0, Math.floor(ms / 6e4));
|
|
532
|
+
if (minutes < 60) return `${minutes}m`;
|
|
533
|
+
const hours = Math.floor(minutes / 60);
|
|
534
|
+
if (hours < 24) return `${hours}h${minutes % 60 ? `${minutes % 60}m` : ""}`;
|
|
535
|
+
return `${Math.floor(hours / 24)}d${hours % 24 ? `${hours % 24}h` : ""}`;
|
|
536
|
+
}
|
|
537
|
+
var shortPath = (project, home = import_node_os2.default.homedir()) => project === home || project.startsWith(`${home}/`) ? `~${project.slice(home.length)}` : project;
|
|
538
|
+
function tags(row) {
|
|
539
|
+
return [row.safeToStop && "safe to stop", row.oldRelease && "old release", row.status === "no-status" && "no status"].filter(Boolean);
|
|
540
|
+
}
|
|
541
|
+
var initialView = () => ({ mode: "list", selected: 0, confirm: null, target: null, message: null });
|
|
542
|
+
var fit = (text2, width) => text2.length > width ? text2.slice(0, width) : text2;
|
|
543
|
+
var pad = (text2, width) => fit(text2, width).padEnd(width);
|
|
544
|
+
var cutLeft = (text2, width) => text2.length > width ? `\u2026${text2.slice(text2.length - width + 1)}` : text2;
|
|
545
|
+
function header(left, right, width) {
|
|
546
|
+
const gap = width - left.length - right.length;
|
|
547
|
+
return `${BOLD}${fit(gap >= 2 ? `${left}${" ".repeat(gap)}${right}` : left, width)}${RESET}`;
|
|
548
|
+
}
|
|
549
|
+
function cells(row, now) {
|
|
550
|
+
return [
|
|
551
|
+
row.port === null ? "-" : String(row.port),
|
|
552
|
+
String(row.pid),
|
|
553
|
+
duration(now - row.startedAt),
|
|
554
|
+
row.clients === null ? "-" : String(row.clients),
|
|
555
|
+
row.lastActivity === null ? "-" : duration(now - row.lastActivity),
|
|
556
|
+
row.version ?? "-"
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
function footer(rows, state, home) {
|
|
560
|
+
if (state.confirm === "stop" && state.target) return `Stop ${shortPath(state.target, home)}? y to confirm`;
|
|
561
|
+
if (state.confirm === "stop-safe") {
|
|
562
|
+
const count = rows.filter((row) => row.safeToStop).length;
|
|
563
|
+
return `Stop ${count} server${count === 1 ? "" : "s"} that ${count === 1 ? "is" : "are"} safe to stop? y to confirm`;
|
|
564
|
+
}
|
|
565
|
+
return state.message ?? HELP;
|
|
566
|
+
}
|
|
567
|
+
function renderList(rows, state, screen) {
|
|
568
|
+
const { width, height, now, home } = screen;
|
|
569
|
+
const count = `${rows.length} server${rows.length === 1 ? "" : "s"} \xB7 refreshes every ${REFRESH_SECONDS}s`;
|
|
570
|
+
const lines = [header("ryugi-artifacts monitor", count, width)];
|
|
571
|
+
if (!rows.length) {
|
|
572
|
+
lines.push("", fit("No artifacts servers are running.", width), "");
|
|
573
|
+
} else {
|
|
574
|
+
const projectWidth = Math.max(12, width - FIXED - TAGS_WIDTH);
|
|
575
|
+
const columns = [" ", pad("PROJECT", projectWidth), ...COLUMNS.map(([name, w]) => ` ${pad(name, w)}`)].join("");
|
|
576
|
+
lines.push(`${DIM}${fit(columns.trimEnd(), width)}${RESET}`);
|
|
577
|
+
const visible = Math.max(1, height - 4);
|
|
578
|
+
const first = Math.min(Math.max(0, state.selected - visible + 1), Math.max(0, rows.length - visible));
|
|
579
|
+
rows.slice(first, first + visible).forEach((row, offset) => {
|
|
580
|
+
const selected = first + offset === state.selected;
|
|
581
|
+
const text2 = [
|
|
582
|
+
selected ? "\u203A " : " ",
|
|
583
|
+
pad(cutLeft(shortPath(row.project, home), projectWidth), projectWidth),
|
|
584
|
+
...cells(row, now).map((cell, i) => ` ${pad(cell, COLUMNS[i][1])}`),
|
|
585
|
+
tags(row).length ? ` ${tags(row).join(", ")}` : ""
|
|
586
|
+
].join("");
|
|
587
|
+
const line = fit(text2.trimEnd(), width);
|
|
588
|
+
lines.push(selected ? `${INVERSE}${line}${RESET}` : line);
|
|
589
|
+
});
|
|
590
|
+
lines.push("");
|
|
591
|
+
}
|
|
592
|
+
lines.push(`${DIM}${fit(footer(rows, state, home), width)}${RESET}`);
|
|
593
|
+
return lines;
|
|
594
|
+
}
|
|
595
|
+
function renderLog(row, text2, screen) {
|
|
596
|
+
const { width, height, home } = screen;
|
|
597
|
+
const lines = [header(`Log \xB7 ${shortPath(row.project, home)}`, row.log, width)];
|
|
598
|
+
const body = text2 === null ? ["No log file: this server runs in the foreground, so its output is in the terminal that started it."] : text2.replace(/\n+$/, "").split("\n").slice(-Math.max(1, height - 2));
|
|
599
|
+
lines.push(...body.map((line) => fit(line, width)));
|
|
600
|
+
lines.push(`${DIM}${fit("Esc back Ctrl+C quit", width)}${RESET}`);
|
|
601
|
+
return lines;
|
|
602
|
+
}
|
|
603
|
+
function handleKey(state, key, rows) {
|
|
604
|
+
if (key.ctrl && key.name === "c") return { state, action: { type: "quit" } };
|
|
605
|
+
const next = { ...state, confirm: null, target: null, message: null };
|
|
606
|
+
if (state.mode === "log") return { state: key.name === "escape" || key.name === "q" ? { ...next, mode: "list" } : state };
|
|
607
|
+
if (state.confirm) {
|
|
608
|
+
if (key.name !== "y") return { state: next };
|
|
609
|
+
const targets = state.confirm === "stop" ? rows.filter((row) => row.project === state.target) : rows.filter((row) => row.safeToStop);
|
|
610
|
+
if (targets.length) return { state: next, action: { type: "stop", rows: targets } };
|
|
611
|
+
return { state: { ...next, message: state.target ? `${shortPath(state.target)} is no longer running.` : "Nothing is safe to stop any more." } };
|
|
612
|
+
}
|
|
613
|
+
const selected = rows[state.selected];
|
|
614
|
+
if (key.sequence === "X" || key.name === "x" && key.shift) {
|
|
615
|
+
if (rows.some((row) => row.safeToStop)) return { state: { ...next, confirm: "stop-safe" } };
|
|
616
|
+
return { state: { ...next, message: "Nothing is safe to stop: every server has an open tab or was used in the last 30 minutes." } };
|
|
617
|
+
}
|
|
618
|
+
switch (key.name) {
|
|
619
|
+
case "q":
|
|
620
|
+
return { state, action: { type: "quit" } };
|
|
621
|
+
case "up":
|
|
622
|
+
case "k":
|
|
623
|
+
return { state: { ...next, selected: Math.max(0, state.selected - 1) } };
|
|
624
|
+
case "down":
|
|
625
|
+
case "j":
|
|
626
|
+
return { state: { ...next, selected: Math.min(Math.max(0, rows.length - 1), state.selected + 1) } };
|
|
627
|
+
case "o":
|
|
628
|
+
case "c":
|
|
629
|
+
if (!selected) return { state: next };
|
|
630
|
+
if (!selected.url) return { state: { ...next, message: "This server has no URL yet." } };
|
|
631
|
+
return { state: next, action: { type: key.name === "o" ? "open" : "copy", row: selected } };
|
|
632
|
+
case "l":
|
|
633
|
+
return { state: selected ? { ...next, mode: "log" } : next };
|
|
634
|
+
case "x":
|
|
635
|
+
return { state: selected ? { ...next, confirm: "stop", target: selected.project } : next };
|
|
636
|
+
default:
|
|
637
|
+
return { state: next };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/cli/monitor-ui.ts
|
|
520
642
|
var import_node_child_process2 = require("node:child_process");
|
|
521
|
-
var
|
|
522
|
-
var
|
|
523
|
-
var import_node_path8 = __toESM(require("node:path"), 1);
|
|
643
|
+
var import_node_fs9 = __toESM(require("node:fs"), 1);
|
|
644
|
+
var import_node_os4 = __toESM(require("node:os"), 1);
|
|
524
645
|
var import_node_readline = __toESM(require("node:readline"), 1);
|
|
525
646
|
|
|
526
|
-
// src/server/
|
|
647
|
+
// src/server/lifecycle.ts
|
|
648
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
|
|
649
|
+
var DEFAULT_LIFECYCLE_CHECK_MS = 60 * 1e3;
|
|
650
|
+
function browserLauncher(platform, url) {
|
|
651
|
+
if (platform === "darwin") return { bin: "open", args: [url] };
|
|
652
|
+
if (platform === "win32") return { bin: "cmd", args: ["/c", "start", '""', url] };
|
|
653
|
+
if (platform === "linux" || platform === "freebsd" || platform === "openbsd") return { bin: "xdg-open", args: [url] };
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/cli/servers.ts
|
|
658
|
+
var import_node_path8 = __toESM(require("node:path"), 1);
|
|
659
|
+
|
|
660
|
+
// src/server/registry.ts
|
|
527
661
|
var import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
528
662
|
var import_node_fs7 = __toESM(require("node:fs"), 1);
|
|
663
|
+
var import_node_os3 = __toESM(require("node:os"), 1);
|
|
529
664
|
var import_node_path7 = __toESM(require("node:path"), 1);
|
|
665
|
+
function registryDir(env = process.env, platform = process.platform, uid = process.getuid?.() ?? -1, tmpdir = import_node_os3.default.tmpdir()) {
|
|
666
|
+
if (env.RYUGI_RUNTIME_DIR) return import_node_path7.default.join(env.RYUGI_RUNTIME_DIR, "servers");
|
|
667
|
+
if (platform === "win32") return import_node_path7.default.join(tmpdir, "ryugi", "servers");
|
|
668
|
+
return `/tmp/ryugi-${uid}/servers`;
|
|
669
|
+
}
|
|
670
|
+
var fileFor = (dir, project) => import_node_path7.default.join(dir, `${import_node_crypto.default.createHash("sha256").update(project).digest("hex").slice(0, 16)}.json`);
|
|
671
|
+
function verifyDir(dir) {
|
|
672
|
+
if (process.platform === "win32") return;
|
|
673
|
+
for (const target of [import_node_path7.default.dirname(dir), dir]) {
|
|
674
|
+
const stat = import_node_fs7.default.lstatSync(target);
|
|
675
|
+
if (!stat.isDirectory() || stat.uid !== process.getuid() || stat.mode & 18) throw new Error(`Unsafe registry directory: ${target}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function readEntry(file) {
|
|
679
|
+
try {
|
|
680
|
+
const value = JSON.parse(import_node_fs7.default.readFileSync(file, "utf8"));
|
|
681
|
+
if (typeof value.project !== "string" || typeof value.root !== "string" || typeof value.serverId !== "string") return null;
|
|
682
|
+
if (!Number.isInteger(value.pid) || !Number.isFinite(value.startedAt)) return null;
|
|
683
|
+
if (value.root !== import_node_path7.default.join(value.project, ".ryugi", "artifacts")) return null;
|
|
684
|
+
return value;
|
|
685
|
+
} catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function readEntries(dir) {
|
|
690
|
+
let names;
|
|
691
|
+
try {
|
|
692
|
+
verifyDir(dir);
|
|
693
|
+
names = import_node_fs7.default.readdirSync(dir);
|
|
694
|
+
} catch {
|
|
695
|
+
return [];
|
|
696
|
+
}
|
|
697
|
+
return names.filter((name) => name.endsWith(".json")).flatMap((name) => readEntry(import_node_path7.default.join(dir, name)) ?? []);
|
|
698
|
+
}
|
|
699
|
+
function removeEntry(dir, entry) {
|
|
700
|
+
const file = fileFor(dir, entry.project);
|
|
701
|
+
if (readEntry(file)?.serverId === entry.serverId) import_node_fs7.default.rmSync(file, { force: true });
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/cli/servers.ts
|
|
705
|
+
var SAFE_IDLE_MS = 30 * 6e4;
|
|
706
|
+
var STATUS_TIMEOUT_MS = 1e3;
|
|
707
|
+
var safeToStop = (status, now) => !!status && status.clients === 0 && now - status.lastActivity >= SAFE_IDLE_MS;
|
|
708
|
+
async function fetchStatus(url) {
|
|
709
|
+
const base = new URL(url);
|
|
710
|
+
if (base.hostname === "localhost") base.hostname = "127.0.0.1";
|
|
711
|
+
try {
|
|
712
|
+
const res = await fetch(`${base.origin}/api/status?key=${encodeURIComponent(base.searchParams.get("key") ?? "")}`, { signal: AbortSignal.timeout(STATUS_TIMEOUT_MS) });
|
|
713
|
+
if (!res.ok) return null;
|
|
714
|
+
const json = await res.json();
|
|
715
|
+
if (typeof json.pid !== "number" || typeof json.clients !== "number" || typeof json.lastActivity !== "number") return null;
|
|
716
|
+
return { pid: json.pid, clients: json.clients, lastActivity: json.lastActivity };
|
|
717
|
+
} catch {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function serverUrl(root) {
|
|
722
|
+
try {
|
|
723
|
+
const { url } = JSON.parse(readText(import_node_path8.default.join(root, ".state", "server-info")) ?? "");
|
|
724
|
+
return typeof url === "string" ? url : null;
|
|
725
|
+
} catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async function listServers(now = Date.now(), platform = process.platform) {
|
|
730
|
+
const dir = registryDir();
|
|
731
|
+
const current = process.env.RYUGI_ARTIFACTS_VERSION;
|
|
732
|
+
const live = readEntries(dir).filter((entry) => {
|
|
733
|
+
if (hasServerId(entry.pid, entry.serverId)) return true;
|
|
734
|
+
try {
|
|
735
|
+
removeEntry(dir, entry);
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
return false;
|
|
739
|
+
});
|
|
740
|
+
const rows = await Promise.all(live.map(async (entry) => {
|
|
741
|
+
const url = serverUrl(entry.root);
|
|
742
|
+
const status = url ? await fetchStatus(url) : null;
|
|
743
|
+
if (status && status.pid !== entry.pid) return null;
|
|
744
|
+
if (!status && platform === "win32") return null;
|
|
745
|
+
const version = readText(import_node_path8.default.join(entry.root, ".state", "server-version")) || null;
|
|
746
|
+
return {
|
|
747
|
+
project: entry.project,
|
|
748
|
+
root: entry.root,
|
|
749
|
+
pid: entry.pid,
|
|
750
|
+
url,
|
|
751
|
+
port: url ? Number(new URL(url).port) : null,
|
|
752
|
+
startedAt: entry.startedAt,
|
|
753
|
+
version,
|
|
754
|
+
oldRelease: !!current && !!version && version !== current,
|
|
755
|
+
clients: status?.clients ?? null,
|
|
756
|
+
lastActivity: status?.lastActivity ?? null,
|
|
757
|
+
safeToStop: safeToStop(status, now),
|
|
758
|
+
status: status ? "ok" : "no-status",
|
|
759
|
+
log: import_node_path8.default.join(entry.root, ".state", "server.log")
|
|
760
|
+
};
|
|
761
|
+
}));
|
|
762
|
+
return rows.filter((row) => row !== null).sort((a, b) => a.project.localeCompare(b.project));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/cli/stop.ts
|
|
766
|
+
var import_node_fs8 = __toESM(require("node:fs"), 1);
|
|
767
|
+
async function stop(projectDir, emit) {
|
|
768
|
+
const paths = projectPaths(projectDir);
|
|
769
|
+
const markStopped = (reason) => {
|
|
770
|
+
import_node_fs8.default.rmSync(paths.info, { force: true });
|
|
771
|
+
import_node_fs8.default.mkdirSync(paths.state, { recursive: true });
|
|
772
|
+
import_node_fs8.default.writeFileSync(paths.stopped, `${JSON.stringify({ reason, timestamp: Math.floor(Date.now() / 1e3) })}
|
|
773
|
+
`);
|
|
774
|
+
};
|
|
775
|
+
const pid = Number(readText(paths.pid));
|
|
776
|
+
if (readText(paths.pid) === null) {
|
|
777
|
+
emit({ status: "not_running" });
|
|
778
|
+
return 0;
|
|
779
|
+
}
|
|
780
|
+
const id = readText(paths.serverId) ?? "";
|
|
781
|
+
if (!Number.isInteger(pid) || pid <= 0 || !/^[A-Za-z0-9_-]{32,64}$/.test(id) || !hasServerId(pid, id)) {
|
|
782
|
+
import_node_fs8.default.rmSync(paths.pid, { force: true });
|
|
783
|
+
import_node_fs8.default.rmSync(paths.serverId, { force: true });
|
|
784
|
+
markStopped("stale_pid");
|
|
785
|
+
emit({ status: "stale_pid" });
|
|
786
|
+
return 0;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
process.kill(pid, "SIGTERM");
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
if (!await waitForExit(pid, 2e3)) {
|
|
793
|
+
try {
|
|
794
|
+
process.kill(pid, "SIGKILL");
|
|
795
|
+
} catch {
|
|
796
|
+
}
|
|
797
|
+
await waitForExit(pid, 1e3);
|
|
798
|
+
}
|
|
799
|
+
if (alive(pid)) {
|
|
800
|
+
emit({ status: "failed", error: "process still running" });
|
|
801
|
+
return 1;
|
|
802
|
+
}
|
|
803
|
+
for (const file of [paths.pid, paths.serverId, paths.log, paths.owners]) import_node_fs8.default.rmSync(file, { force: true });
|
|
804
|
+
markStopped("stop");
|
|
805
|
+
emit({ status: "stopped" });
|
|
806
|
+
return 0;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/cli/monitor-ui.ts
|
|
810
|
+
var LOG_TAIL_BYTES = 64 * 1024;
|
|
811
|
+
var LOG_REFRESH_MS = 500;
|
|
812
|
+
function readTail(file) {
|
|
813
|
+
let fd;
|
|
814
|
+
try {
|
|
815
|
+
fd = import_node_fs9.default.openSync(file, "r");
|
|
816
|
+
} catch {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const size = import_node_fs9.default.fstatSync(fd).size;
|
|
821
|
+
const length = Math.min(size, LOG_TAIL_BYTES);
|
|
822
|
+
const buffer = Buffer.alloc(length);
|
|
823
|
+
import_node_fs9.default.readSync(fd, buffer, 0, length, size - length);
|
|
824
|
+
return buffer.toString("utf8");
|
|
825
|
+
} finally {
|
|
826
|
+
import_node_fs9.default.closeSync(fd);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function clipboardCommands() {
|
|
830
|
+
if (process.platform === "darwin") return [["pbcopy", []]];
|
|
831
|
+
if (process.platform === "win32") return [["clip", []]];
|
|
832
|
+
return [["wl-copy", []], ["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]];
|
|
833
|
+
}
|
|
834
|
+
function copy(text2, commands = clipboardCommands()) {
|
|
835
|
+
const [first, ...rest] = commands;
|
|
836
|
+
if (!first) return Promise.resolve(false);
|
|
837
|
+
return new Promise((resolve) => {
|
|
838
|
+
const child = (0, import_node_child_process2.spawn)(first[0], first[1], { stdio: ["pipe", "ignore", "ignore"] });
|
|
839
|
+
child.on("error", () => resolve(copy(text2, rest)));
|
|
840
|
+
child.on("close", (code) => code === 0 ? resolve(true) : resolve(copy(text2, rest)));
|
|
841
|
+
child.stdin.end(text2);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
async function runMonitorUi() {
|
|
845
|
+
const { stdin, stdout } = process;
|
|
846
|
+
let rows = await listServers();
|
|
847
|
+
let state = initialView();
|
|
848
|
+
let busy = false;
|
|
849
|
+
let closed = false;
|
|
850
|
+
const restore = () => {
|
|
851
|
+
if (closed) return;
|
|
852
|
+
closed = true;
|
|
853
|
+
try {
|
|
854
|
+
stdin.setRawMode(false);
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
stdout.write("\x1B[?25h\x1B[?1049l");
|
|
858
|
+
};
|
|
859
|
+
process.on("exit", restore);
|
|
860
|
+
const draw = () => {
|
|
861
|
+
if (closed) return;
|
|
862
|
+
const screen = { width: stdout.columns || 80, height: stdout.rows || 24, now: Date.now(), home: import_node_os4.default.homedir() };
|
|
863
|
+
const row = rows[state.selected];
|
|
864
|
+
const lines = state.mode === "log" && row ? renderLog(row, readTail(row.log), screen) : renderList(rows, state, screen);
|
|
865
|
+
stdout.write(`\x1B[H${lines.join("\x1B[K\n")}\x1B[K\x1B[J`);
|
|
866
|
+
};
|
|
867
|
+
const refresh = async () => {
|
|
868
|
+
if (busy || closed) return;
|
|
869
|
+
busy = true;
|
|
870
|
+
const selected = rows[state.selected]?.project;
|
|
871
|
+
try {
|
|
872
|
+
rows = await listServers();
|
|
873
|
+
} finally {
|
|
874
|
+
busy = false;
|
|
875
|
+
}
|
|
876
|
+
const index = rows.findIndex((row) => row.project === selected);
|
|
877
|
+
state = { ...state, selected: index >= 0 ? index : Math.min(state.selected, Math.max(0, rows.length - 1)) };
|
|
878
|
+
if (state.mode === "log" && !rows[state.selected]) state = { ...state, mode: "list" };
|
|
879
|
+
draw();
|
|
880
|
+
};
|
|
881
|
+
const say = (message) => {
|
|
882
|
+
state = { ...state, message };
|
|
883
|
+
draw();
|
|
884
|
+
};
|
|
885
|
+
const run = async (action) => {
|
|
886
|
+
if (action.type === "open") {
|
|
887
|
+
const launcher = browserLauncher(process.platform, action.row.url);
|
|
888
|
+
if (launcher) (0, import_node_child_process2.execFile)(launcher.bin, launcher.args, () => {
|
|
889
|
+
});
|
|
890
|
+
say(launcher ? `Opened ${action.row.url}` : `Open ${action.row.url}`);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (action.type === "copy") {
|
|
894
|
+
say(await copy(action.row.url) ? `Copied ${action.row.url}` : `No clipboard tool found; the URL is ${action.row.url}`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (action.type === "stop") {
|
|
898
|
+
say(`Stopping ${action.rows.length === 1 ? shortPath(action.rows[0].project) : `${action.rows.length} servers`}\u2026`);
|
|
899
|
+
const failed = [];
|
|
900
|
+
for (const row of action.rows) {
|
|
901
|
+
let status = "failed";
|
|
902
|
+
try {
|
|
903
|
+
await stop(row.project, (value) => {
|
|
904
|
+
status = String(value.status ?? "failed");
|
|
905
|
+
});
|
|
906
|
+
} catch {
|
|
907
|
+
}
|
|
908
|
+
if (status === "failed") failed.push(shortPath(row.project));
|
|
909
|
+
}
|
|
910
|
+
await refresh();
|
|
911
|
+
say(failed.length ? `Could not stop ${failed.join(", ")}` : `Stopped ${action.rows.length === 1 ? shortPath(action.rows[0].project) : `${action.rows.length} servers`}`);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
return new Promise((resolve) => {
|
|
915
|
+
const timers = [
|
|
916
|
+
setInterval(() => void refresh(), REFRESH_SECONDS * 1e3),
|
|
917
|
+
setInterval(() => {
|
|
918
|
+
if (state.mode === "log") draw();
|
|
919
|
+
}, LOG_REFRESH_MS)
|
|
920
|
+
];
|
|
921
|
+
const quit = (code) => {
|
|
922
|
+
timers.forEach(clearInterval);
|
|
923
|
+
stdin.off("keypress", onKey);
|
|
924
|
+
stdout.off("resize", draw);
|
|
925
|
+
stdin.pause();
|
|
926
|
+
restore();
|
|
927
|
+
resolve(code);
|
|
928
|
+
};
|
|
929
|
+
const onKey = (_, key) => {
|
|
930
|
+
const result = handleKey(state, key ?? {}, rows);
|
|
931
|
+
state = result.state;
|
|
932
|
+
if (result.action?.type === "quit") return quit(0);
|
|
933
|
+
draw();
|
|
934
|
+
if (result.action) void run(result.action);
|
|
935
|
+
};
|
|
936
|
+
for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143], ["SIGHUP", 129]]) process.once(signal, () => quit(code));
|
|
937
|
+
stdout.write("\x1B[?1049h\x1B[?25l");
|
|
938
|
+
import_node_readline.default.emitKeypressEvents(stdin);
|
|
939
|
+
stdin.setRawMode(true);
|
|
940
|
+
stdin.resume();
|
|
941
|
+
stdin.on("keypress", onKey);
|
|
942
|
+
stdout.on("resize", draw);
|
|
943
|
+
draw();
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/cli/monitor.ts
|
|
948
|
+
function monitorText(value, now = Date.now()) {
|
|
949
|
+
if (value.type !== "servers") return null;
|
|
950
|
+
const rows = value.servers;
|
|
951
|
+
if (!rows.length) return "No artifacts servers are running.";
|
|
952
|
+
return rows.map((row) => [
|
|
953
|
+
shortPath(row.project),
|
|
954
|
+
row.url ?? "starting",
|
|
955
|
+
`pid ${row.pid}`,
|
|
956
|
+
row.clients === null ? null : `tabs ${row.clients}`,
|
|
957
|
+
row.lastActivity === null ? null : `idle ${duration(now - row.lastActivity)}`,
|
|
958
|
+
...tags(row).map((tag) => `[${tag}]`)
|
|
959
|
+
].filter(Boolean).join(" ")).join("\n");
|
|
960
|
+
}
|
|
961
|
+
async function monitor(options, emit) {
|
|
962
|
+
if (options.json || !process.stdout.isTTY || !process.stdin.isTTY) {
|
|
963
|
+
emit({ type: "servers", servers: await listServers() });
|
|
964
|
+
return 0;
|
|
965
|
+
}
|
|
966
|
+
return runMonitorUi();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/cli/start.ts
|
|
970
|
+
var import_node_child_process3 = require("node:child_process");
|
|
971
|
+
var import_node_crypto3 = __toESM(require("node:crypto"), 1);
|
|
972
|
+
var import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
973
|
+
var import_node_path10 = __toESM(require("node:path"), 1);
|
|
974
|
+
var import_node_readline2 = __toESM(require("node:readline"), 1);
|
|
975
|
+
|
|
976
|
+
// src/server/artifact-address.ts
|
|
977
|
+
var import_node_crypto2 = __toESM(require("node:crypto"), 1);
|
|
978
|
+
var import_node_fs10 = __toESM(require("node:fs"), 1);
|
|
979
|
+
var import_node_path9 = __toESM(require("node:path"), 1);
|
|
530
980
|
|
|
531
981
|
// src/shared/ids.ts
|
|
532
982
|
var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -542,15 +992,15 @@ function artifactKeyOf(relative) {
|
|
|
542
992
|
|
|
543
993
|
// src/server/artifact-address.ts
|
|
544
994
|
function artifactId(dir) {
|
|
545
|
-
const file =
|
|
995
|
+
const file = import_node_path9.default.join(dir, ".artifact-id");
|
|
546
996
|
let id;
|
|
547
997
|
try {
|
|
548
|
-
id =
|
|
998
|
+
id = import_node_fs10.default.readFileSync(file, "utf8").trim();
|
|
549
999
|
} catch (err) {
|
|
550
1000
|
if (err.code !== "ENOENT") throw err;
|
|
551
|
-
id =
|
|
1001
|
+
id = import_node_crypto2.default.randomBytes(16).toString("hex");
|
|
552
1002
|
try {
|
|
553
|
-
|
|
1003
|
+
import_node_fs10.default.writeFileSync(file, `${id}
|
|
554
1004
|
`, { flag: "wx" });
|
|
555
1005
|
} catch (writeError) {
|
|
556
1006
|
if (writeError.code !== "EEXIST") throw writeError;
|
|
@@ -562,8 +1012,8 @@ function artifactId(dir) {
|
|
|
562
1012
|
}
|
|
563
1013
|
function artifactPath(root, type, slug) {
|
|
564
1014
|
if (!isArtifactType(type) || !isArtifactId(slug)) throw new Error("Invalid artifact locator");
|
|
565
|
-
const dir =
|
|
566
|
-
|
|
1015
|
+
const dir = import_node_path9.default.join(root, type, slug);
|
|
1016
|
+
import_node_fs10.default.mkdirSync(dir, { recursive: true });
|
|
567
1017
|
return `/${type}/${artifactId(dir)}`;
|
|
568
1018
|
}
|
|
569
1019
|
|
|
@@ -601,13 +1051,13 @@ async function withCloudLink(out, artifact) {
|
|
|
601
1051
|
async function acquireLock(lock) {
|
|
602
1052
|
for (let attempt = 0; attempt < 150; attempt++) {
|
|
603
1053
|
try {
|
|
604
|
-
|
|
1054
|
+
import_node_fs11.default.mkdirSync(lock);
|
|
605
1055
|
return true;
|
|
606
1056
|
} catch {
|
|
607
1057
|
}
|
|
608
1058
|
try {
|
|
609
|
-
if (Date.now() -
|
|
610
|
-
|
|
1059
|
+
if (Date.now() - import_node_fs11.default.statSync(lock).mtimeMs > 6e4) {
|
|
1060
|
+
import_node_fs11.default.rmdirSync(lock);
|
|
611
1061
|
continue;
|
|
612
1062
|
}
|
|
613
1063
|
} catch {
|
|
@@ -637,13 +1087,13 @@ function registerOwner(paths) {
|
|
|
637
1087
|
const owner = ownerPid();
|
|
638
1088
|
if (owner) owners.add(owner);
|
|
639
1089
|
const tmp = `${paths.owners}.${process.pid}`;
|
|
640
|
-
|
|
1090
|
+
import_node_fs11.default.writeFileSync(tmp, [...owners].sort((a, b) => a - b).map((pid) => `${pid}
|
|
641
1091
|
`).join(""));
|
|
642
|
-
|
|
1092
|
+
import_node_fs11.default.renameSync(tmp, paths.owners);
|
|
643
1093
|
}
|
|
644
1094
|
function recordAgent(paths) {
|
|
645
1095
|
const markers = Object.fromEntries(AGENT_MARKERS.flatMap((key) => process.env[key] ? [[key, process.env[key]]] : []));
|
|
646
|
-
if (Object.keys(markers).length)
|
|
1096
|
+
if (Object.keys(markers).length) import_node_fs11.default.writeFileSync(paths.agent, `${JSON.stringify(markers)}
|
|
647
1097
|
`);
|
|
648
1098
|
}
|
|
649
1099
|
async function replace(pid, paths) {
|
|
@@ -675,9 +1125,9 @@ async function start(options, emit) {
|
|
|
675
1125
|
const foreground = options.foreground || !options.background && (!!process.env.CODEX_CI || windowsLike());
|
|
676
1126
|
process.umask(63);
|
|
677
1127
|
const paths = projectPaths(options.projectDir);
|
|
678
|
-
|
|
679
|
-
if (!
|
|
680
|
-
const server =
|
|
1128
|
+
import_node_fs11.default.mkdirSync(paths.state, { recursive: true });
|
|
1129
|
+
if (!import_node_fs11.default.existsSync(import_node_path10.default.join(paths.state, ".gitignore"))) import_node_fs11.default.writeFileSync(import_node_path10.default.join(paths.state, ".gitignore"), "*\n");
|
|
1130
|
+
const server = import_node_path10.default.join(__dirname, "server.cjs");
|
|
681
1131
|
const version = process.env.RYUGI_ARTIFACTS_VERSION ?? "";
|
|
682
1132
|
if (!await acquireLock(paths.lock)) throw new CliError("Another start for this project is still starting; retry in a moment");
|
|
683
1133
|
let locked = true;
|
|
@@ -685,7 +1135,7 @@ async function start(options, emit) {
|
|
|
685
1135
|
if (!locked) return;
|
|
686
1136
|
locked = false;
|
|
687
1137
|
try {
|
|
688
|
-
|
|
1138
|
+
import_node_fs11.default.rmdirSync(paths.lock);
|
|
689
1139
|
} catch {
|
|
690
1140
|
}
|
|
691
1141
|
};
|
|
@@ -703,20 +1153,20 @@ async function start(options, emit) {
|
|
|
703
1153
|
}
|
|
704
1154
|
registerOwner(paths);
|
|
705
1155
|
recordAgent(paths);
|
|
706
|
-
|
|
707
|
-
const id =
|
|
708
|
-
|
|
1156
|
+
import_node_fs11.default.rmSync(paths.info, { force: true });
|
|
1157
|
+
const id = import_node_crypto3.default.randomBytes(24).toString("hex");
|
|
1158
|
+
import_node_fs11.default.writeFileSync(paths.serverId, `${id}
|
|
709
1159
|
`);
|
|
710
|
-
|
|
1160
|
+
import_node_fs11.default.writeFileSync(paths.version, `${version}
|
|
711
1161
|
`);
|
|
712
1162
|
const env = serverEnv(paths, options);
|
|
713
1163
|
const args = [server, `--artifacts-server-id=${id}`];
|
|
714
1164
|
if (foreground) {
|
|
715
|
-
const child2 = (0,
|
|
716
|
-
|
|
1165
|
+
const child2 = (0, import_node_child_process3.spawn)(process.execPath, args, { env, stdio: ["ignore", "pipe", "inherit"] });
|
|
1166
|
+
import_node_fs11.default.writeFileSync(paths.pid, `${child2.pid}
|
|
717
1167
|
`);
|
|
718
1168
|
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child2.kill("SIGTERM"));
|
|
719
|
-
|
|
1169
|
+
import_node_readline2.default.createInterface({ input: child2.stdout }).on("line", (line) => {
|
|
720
1170
|
let value;
|
|
721
1171
|
try {
|
|
722
1172
|
value = JSON.parse(line);
|
|
@@ -729,15 +1179,15 @@ async function start(options, emit) {
|
|
|
729
1179
|
else void withCloudLink(withLink(value, options.artifact, false), options.artifact).then(emit);
|
|
730
1180
|
});
|
|
731
1181
|
const exited = new Promise((resolve) => child2.on("close", (code) => resolve(code ?? 1)));
|
|
732
|
-
for (let waited = 0; waited < 5e3 && !
|
|
1182
|
+
for (let waited = 0; waited < 5e3 && !import_node_fs11.default.existsSync(paths.info); waited += 100) await sleep(100);
|
|
733
1183
|
unlock();
|
|
734
1184
|
return await exited;
|
|
735
1185
|
}
|
|
736
|
-
const log =
|
|
737
|
-
const child = (0,
|
|
738
|
-
|
|
1186
|
+
const log = import_node_fs11.default.openSync(paths.log, "w");
|
|
1187
|
+
const child = (0, import_node_child_process3.spawn)(process.execPath, args, { env, detached: true, stdio: ["ignore", log, log] });
|
|
1188
|
+
import_node_fs11.default.closeSync(log);
|
|
739
1189
|
child.unref();
|
|
740
|
-
|
|
1190
|
+
import_node_fs11.default.writeFileSync(paths.pid, `${child.pid}
|
|
741
1191
|
`);
|
|
742
1192
|
for (let waited = 0; waited < 5e3; waited += 100) {
|
|
743
1193
|
const lines = (readText(paths.log) ?? "").split("\n");
|
|
@@ -816,52 +1266,8 @@ async function publish(options, emit) {
|
|
|
816
1266
|
return 0;
|
|
817
1267
|
}
|
|
818
1268
|
|
|
819
|
-
// src/cli/stop.ts
|
|
820
|
-
var import_node_fs9 = __toESM(require("node:fs"), 1);
|
|
821
|
-
async function stop(projectDir, emit) {
|
|
822
|
-
const paths = projectPaths(projectDir);
|
|
823
|
-
const markStopped = (reason) => {
|
|
824
|
-
import_node_fs9.default.rmSync(paths.info, { force: true });
|
|
825
|
-
import_node_fs9.default.mkdirSync(paths.state, { recursive: true });
|
|
826
|
-
import_node_fs9.default.writeFileSync(paths.stopped, `${JSON.stringify({ reason, timestamp: Math.floor(Date.now() / 1e3) })}
|
|
827
|
-
`);
|
|
828
|
-
};
|
|
829
|
-
const pid = Number(readText(paths.pid));
|
|
830
|
-
if (readText(paths.pid) === null) {
|
|
831
|
-
emit({ status: "not_running" });
|
|
832
|
-
return 0;
|
|
833
|
-
}
|
|
834
|
-
const id = readText(paths.serverId) ?? "";
|
|
835
|
-
if (!Number.isInteger(pid) || pid <= 0 || !/^[A-Za-z0-9_-]{32,64}$/.test(id) || !hasServerId(pid, id)) {
|
|
836
|
-
import_node_fs9.default.rmSync(paths.pid, { force: true });
|
|
837
|
-
import_node_fs9.default.rmSync(paths.serverId, { force: true });
|
|
838
|
-
markStopped("stale_pid");
|
|
839
|
-
emit({ status: "stale_pid" });
|
|
840
|
-
return 0;
|
|
841
|
-
}
|
|
842
|
-
try {
|
|
843
|
-
process.kill(pid, "SIGTERM");
|
|
844
|
-
} catch {
|
|
845
|
-
}
|
|
846
|
-
if (!await waitForExit(pid, 2e3)) {
|
|
847
|
-
try {
|
|
848
|
-
process.kill(pid, "SIGKILL");
|
|
849
|
-
} catch {
|
|
850
|
-
}
|
|
851
|
-
await waitForExit(pid, 1e3);
|
|
852
|
-
}
|
|
853
|
-
if (alive(pid)) {
|
|
854
|
-
emit({ status: "failed", error: "process still running" });
|
|
855
|
-
return 1;
|
|
856
|
-
}
|
|
857
|
-
for (const file of [paths.pid, paths.serverId, paths.log, paths.owners]) import_node_fs9.default.rmSync(file, { force: true });
|
|
858
|
-
markStopped("stop");
|
|
859
|
-
emit({ status: "stopped" });
|
|
860
|
-
return 0;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
1269
|
// src/cli/main.ts
|
|
864
|
-
var
|
|
1270
|
+
var HELP2 = `Usage: ryugi-artifacts [project-directory] [--no-open]
|
|
865
1271
|
ryugi-artifacts <command> [options]
|
|
866
1272
|
|
|
867
1273
|
Without a command, opens the project's artifacts in your browser and keeps the server
|
|
@@ -882,6 +1288,8 @@ Commands:
|
|
|
882
1288
|
reply --artifact <document|artboard>/<id> --thread <threadId> [--text <text>]
|
|
883
1289
|
resolve --artifact <document|artboard>/<id> --thread <threadId>
|
|
884
1290
|
Without --text, add and reply read the message from stdin.
|
|
1291
|
+
monitor List every running artifacts server on this machine and manage them. In a terminal it
|
|
1292
|
+
opens a live view (open, copy URL, logs, stop); otherwise, or with --json, prints the list once.
|
|
885
1293
|
config Auto-publish changed artifacts as private cloud copies when the agent's turn ends:
|
|
886
1294
|
--auto-publish on|off for every project
|
|
887
1295
|
--auto-publish on|off|inherit --project override for this project only
|
|
@@ -914,6 +1322,8 @@ ${value.url}`;
|
|
|
914
1322
|
if (value.type === "auto-publish") return `Auto-publish: ${value.global === null ? "not decided" : value.global ? "on" : "off"} for every project${value.project === null ? "" : `, ${value.project ? "on" : "off"} for this project`}.`;
|
|
915
1323
|
const checked = checkText(value);
|
|
916
1324
|
if (checked !== null) return checked;
|
|
1325
|
+
const servers = monitorText(value);
|
|
1326
|
+
if (servers !== null) return servers;
|
|
917
1327
|
const commented = commentText(value);
|
|
918
1328
|
if (commented !== null) return commented;
|
|
919
1329
|
if (typeof value.status === "string") return value.status === "stopped" ? "Stopped." : value.status.replace(/_/g, " ");
|
|
@@ -922,7 +1332,7 @@ ${value.url}`;
|
|
|
922
1332
|
function openBrowser(url) {
|
|
923
1333
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
924
1334
|
const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
|
|
925
|
-
(0,
|
|
1335
|
+
(0, import_node_child_process4.execFile)(command, args, () => {
|
|
926
1336
|
});
|
|
927
1337
|
}
|
|
928
1338
|
async function browse(argv) {
|
|
@@ -1045,12 +1455,16 @@ async function main(argv) {
|
|
|
1045
1455
|
project: !!values.project
|
|
1046
1456
|
}, emit);
|
|
1047
1457
|
}
|
|
1458
|
+
if (command === "monitor") {
|
|
1459
|
+
const { values } = parse(rest, {});
|
|
1460
|
+
return await monitor({ json: !!values.json }, emit);
|
|
1461
|
+
}
|
|
1048
1462
|
if (command === "--version" || command === "-v") {
|
|
1049
|
-
console.log("1.
|
|
1463
|
+
console.log("1.33.0");
|
|
1050
1464
|
return 0;
|
|
1051
1465
|
}
|
|
1052
1466
|
if (command === "--help" || command === "-h") {
|
|
1053
|
-
console.log(
|
|
1467
|
+
console.log(HELP2);
|
|
1054
1468
|
return 0;
|
|
1055
1469
|
}
|
|
1056
1470
|
return await browse(argv);
|