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 import_node_child_process3 = require("node:child_process");
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/start.ts
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 import_node_crypto2 = __toESM(require("node:crypto"), 1);
522
- var import_node_fs8 = __toESM(require("node:fs"), 1);
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/artifact-address.ts
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 = import_node_path7.default.join(dir, ".artifact-id");
995
+ const file = import_node_path9.default.join(dir, ".artifact-id");
546
996
  let id;
547
997
  try {
548
- id = import_node_fs7.default.readFileSync(file, "utf8").trim();
998
+ id = import_node_fs10.default.readFileSync(file, "utf8").trim();
549
999
  } catch (err) {
550
1000
  if (err.code !== "ENOENT") throw err;
551
- id = import_node_crypto.default.randomBytes(16).toString("hex");
1001
+ id = import_node_crypto2.default.randomBytes(16).toString("hex");
552
1002
  try {
553
- import_node_fs7.default.writeFileSync(file, `${id}
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 = import_node_path7.default.join(root, type, slug);
566
- import_node_fs7.default.mkdirSync(dir, { recursive: true });
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
- import_node_fs8.default.mkdirSync(lock);
1054
+ import_node_fs11.default.mkdirSync(lock);
605
1055
  return true;
606
1056
  } catch {
607
1057
  }
608
1058
  try {
609
- if (Date.now() - import_node_fs8.default.statSync(lock).mtimeMs > 6e4) {
610
- import_node_fs8.default.rmdirSync(lock);
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
- import_node_fs8.default.writeFileSync(tmp, [...owners].sort((a, b) => a - b).map((pid) => `${pid}
1090
+ import_node_fs11.default.writeFileSync(tmp, [...owners].sort((a, b) => a - b).map((pid) => `${pid}
641
1091
  `).join(""));
642
- import_node_fs8.default.renameSync(tmp, paths.owners);
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) import_node_fs8.default.writeFileSync(paths.agent, `${JSON.stringify(markers)}
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
- import_node_fs8.default.mkdirSync(paths.state, { recursive: true });
679
- if (!import_node_fs8.default.existsSync(import_node_path8.default.join(paths.state, ".gitignore"))) import_node_fs8.default.writeFileSync(import_node_path8.default.join(paths.state, ".gitignore"), "*\n");
680
- const server = import_node_path8.default.join(__dirname, "server.cjs");
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
- import_node_fs8.default.rmdirSync(paths.lock);
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
- import_node_fs8.default.rmSync(paths.info, { force: true });
707
- const id = import_node_crypto2.default.randomBytes(24).toString("hex");
708
- import_node_fs8.default.writeFileSync(paths.serverId, `${id}
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
- import_node_fs8.default.writeFileSync(paths.version, `${version}
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, import_node_child_process2.spawn)(process.execPath, args, { env, stdio: ["ignore", "pipe", "inherit"] });
716
- import_node_fs8.default.writeFileSync(paths.pid, `${child2.pid}
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
- import_node_readline.default.createInterface({ input: child2.stdout }).on("line", (line) => {
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 && !import_node_fs8.default.existsSync(paths.info); waited += 100) await sleep(100);
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 = import_node_fs8.default.openSync(paths.log, "w");
737
- const child = (0, import_node_child_process2.spawn)(process.execPath, args, { env, detached: true, stdio: ["ignore", log, log] });
738
- import_node_fs8.default.closeSync(log);
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
- import_node_fs8.default.writeFileSync(paths.pid, `${child.pid}
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 HELP = `Usage: ryugi-artifacts [project-directory] [--no-open]
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, import_node_child_process3.execFile)(command, args, () => {
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.32.0");
1463
+ console.log("1.33.0");
1050
1464
  return 0;
1051
1465
  }
1052
1466
  if (command === "--help" || command === "-h") {
1053
- console.log(HELP);
1467
+ console.log(HELP2);
1054
1468
  return 0;
1055
1469
  }
1056
1470
  return await browse(argv);