ryugi-artifacts 1.32.0 → 1.34.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,11 +24,12 @@ 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
31
31
  var import_node_fs = __toESM(require("node:fs"), 1);
32
+ var import_node_os = __toESM(require("node:os"), 1);
32
33
  var import_node_path = __toESM(require("node:path"), 1);
33
34
  var CliError = class extends Error {
34
35
  constructor(message, fields = {}, code = 1) {
@@ -39,6 +40,21 @@ var CliError = class extends Error {
39
40
  fields;
40
41
  code;
41
42
  };
43
+ var resolvesRoot = () => !process.env.RYUGI_ARTIFACTS_COMMAND || process.env.RYUGI_ARTIFACTS_RESOLVE_ROOT === "1";
44
+ function projectRoot(dir) {
45
+ let home = "";
46
+ try {
47
+ home = import_node_fs.default.realpathSync(import_node_os.default.homedir());
48
+ } catch {
49
+ }
50
+ const chain = [];
51
+ for (let current = dir; ; current = import_node_path.default.dirname(current)) {
52
+ chain.push(current);
53
+ if (import_node_fs.default.existsSync(import_node_path.default.join(current, ".git")) && (current !== home || dir === home)) break;
54
+ if (import_node_path.default.dirname(current) === current) return dir;
55
+ }
56
+ return chain.find((candidate) => import_node_fs.default.existsSync(import_node_path.default.join(candidate, ".ryugi", "artifacts"))) ?? chain[chain.length - 1];
57
+ }
42
58
  function projectPaths(dir) {
43
59
  let project;
44
60
  try {
@@ -47,6 +63,7 @@ function projectPaths(dir) {
47
63
  } catch {
48
64
  throw new CliError(`Project directory not found: ${dir}`);
49
65
  }
66
+ if (resolvesRoot()) project = projectRoot(project);
50
67
  const root = import_node_path.default.join(project, ".ryugi", "artifacts");
51
68
  const state = import_node_path.default.join(root, ".state");
52
69
  return {
@@ -370,7 +387,7 @@ ${groups.map(groupText).join("\n")}` : head;
370
387
  }
371
388
 
372
389
  // src/cli/config.ts
373
- var import_node_os = __toESM(require("node:os"), 1);
390
+ var import_node_os2 = __toESM(require("node:os"), 1);
374
391
  var import_node_path6 = __toESM(require("node:path"), 1);
375
392
 
376
393
  // src/server/config.ts
@@ -501,7 +518,7 @@ function writeGlobalAutoPublish(file, value) {
501
518
  // src/cli/config.ts
502
519
  var VALUES = { on: true, off: false, inherit: null };
503
520
  function config(options, emit) {
504
- const globalFile = settingsFileFor(credentialsFile(process.env, process.platform, import_node_os.default.homedir()));
521
+ const globalFile = settingsFileFor(credentialsFile(process.env, process.platform, import_node_os2.default.homedir()));
505
522
  const projectFile = import_node_path6.default.join(import_node_path6.default.dirname(projectPaths(options.projectDir).root), "config.json");
506
523
  if (options.autoPublish !== void 0) {
507
524
  const value = VALUES[options.autoPublish];
@@ -516,17 +533,467 @@ function config(options, emit) {
516
533
  return 0;
517
534
  }
518
535
 
519
- // src/cli/start.ts
536
+ // src/cli/monitor-view.ts
537
+ var import_node_os3 = __toESM(require("node:os"), 1);
538
+ var REFRESH_SECONDS = 2;
539
+ var BOLD = "\x1B[1m";
540
+ var DIM = "\x1B[2m";
541
+ var INVERSE = "\x1B[7m";
542
+ var RESET = "\x1B[0m";
543
+ var HELP = "\u2191\u2193 select o open c copy URL l logs x stop X stop all safe q quit";
544
+ var COLUMNS = [["PORT", 6], ["PID", 8], ["UP", 7], ["TABS", 5], ["IDLE", 7], ["VERSION", 13]];
545
+ var FIXED = 2 + COLUMNS.reduce((sum, [, width]) => sum + width + 2, 0);
546
+ var TAGS_WIDTH = 26;
547
+ function duration(ms) {
548
+ const minutes = Math.max(0, Math.floor(ms / 6e4));
549
+ if (minutes < 60) return `${minutes}m`;
550
+ const hours = Math.floor(minutes / 60);
551
+ if (hours < 24) return `${hours}h${minutes % 60 ? `${minutes % 60}m` : ""}`;
552
+ return `${Math.floor(hours / 24)}d${hours % 24 ? `${hours % 24}h` : ""}`;
553
+ }
554
+ var shortPath = (project, home = import_node_os3.default.homedir()) => project === home || project.startsWith(`${home}/`) ? `~${project.slice(home.length)}` : project;
555
+ function tags(row) {
556
+ return [row.safeToStop && "safe to stop", row.oldRelease && "old release", row.status === "no-status" && "no status"].filter(Boolean);
557
+ }
558
+ var initialView = () => ({ mode: "list", selected: 0, confirm: null, target: null, message: null });
559
+ var fit = (text2, width) => text2.length > width ? text2.slice(0, width) : text2;
560
+ var pad = (text2, width) => fit(text2, width).padEnd(width);
561
+ var cutLeft = (text2, width) => text2.length > width ? `\u2026${text2.slice(text2.length - width + 1)}` : text2;
562
+ function header(left, right, width) {
563
+ const gap = width - left.length - right.length;
564
+ return `${BOLD}${fit(gap >= 2 ? `${left}${" ".repeat(gap)}${right}` : left, width)}${RESET}`;
565
+ }
566
+ function cells(row, now) {
567
+ return [
568
+ row.port === null ? "-" : String(row.port),
569
+ String(row.pid),
570
+ duration(now - row.startedAt),
571
+ row.clients === null ? "-" : String(row.clients),
572
+ row.lastActivity === null ? "-" : duration(now - row.lastActivity),
573
+ row.version ?? "-"
574
+ ];
575
+ }
576
+ function footer(rows, state, home) {
577
+ if (state.confirm === "stop" && state.target) return `Stop ${shortPath(state.target, home)}? y to confirm`;
578
+ if (state.confirm === "stop-safe") {
579
+ const count = rows.filter((row) => row.safeToStop).length;
580
+ return `Stop ${count} server${count === 1 ? "" : "s"} that ${count === 1 ? "is" : "are"} safe to stop? y to confirm`;
581
+ }
582
+ return state.message ?? HELP;
583
+ }
584
+ function renderList(rows, state, screen) {
585
+ const { width, height, now, home } = screen;
586
+ const count = `${rows.length} server${rows.length === 1 ? "" : "s"} \xB7 refreshes every ${REFRESH_SECONDS}s`;
587
+ const lines = [header("ryugi-artifacts monitor", count, width)];
588
+ if (!rows.length) {
589
+ lines.push("", fit("No artifacts servers are running.", width), "");
590
+ } else {
591
+ const projectWidth = Math.max(12, width - FIXED - TAGS_WIDTH);
592
+ const columns = [" ", pad("PROJECT", projectWidth), ...COLUMNS.map(([name, w]) => ` ${pad(name, w)}`)].join("");
593
+ lines.push(`${DIM}${fit(columns.trimEnd(), width)}${RESET}`);
594
+ const visible = Math.max(1, height - 4);
595
+ const first = Math.min(Math.max(0, state.selected - visible + 1), Math.max(0, rows.length - visible));
596
+ rows.slice(first, first + visible).forEach((row, offset) => {
597
+ const selected = first + offset === state.selected;
598
+ const text2 = [
599
+ selected ? "\u203A " : " ",
600
+ pad(cutLeft(shortPath(row.project, home), projectWidth), projectWidth),
601
+ ...cells(row, now).map((cell, i) => ` ${pad(cell, COLUMNS[i][1])}`),
602
+ tags(row).length ? ` ${tags(row).join(", ")}` : ""
603
+ ].join("");
604
+ const line = fit(text2.trimEnd(), width);
605
+ lines.push(selected ? `${INVERSE}${line}${RESET}` : line);
606
+ });
607
+ lines.push("");
608
+ }
609
+ lines.push(`${DIM}${fit(footer(rows, state, home), width)}${RESET}`);
610
+ return lines;
611
+ }
612
+ function renderLog(row, text2, screen) {
613
+ const { width, height, home } = screen;
614
+ const lines = [header(`Log \xB7 ${shortPath(row.project, home)}`, row.log, width)];
615
+ 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));
616
+ lines.push(...body.map((line) => fit(line, width)));
617
+ lines.push(`${DIM}${fit("Esc back Ctrl+C quit", width)}${RESET}`);
618
+ return lines;
619
+ }
620
+ function handleKey(state, key, rows) {
621
+ if (key.ctrl && key.name === "c") return { state, action: { type: "quit" } };
622
+ const next = { ...state, confirm: null, target: null, message: null };
623
+ if (state.mode === "log") return { state: key.name === "escape" || key.name === "q" ? { ...next, mode: "list" } : state };
624
+ if (state.confirm) {
625
+ if (key.name !== "y") return { state: next };
626
+ const targets = state.confirm === "stop" ? rows.filter((row) => row.project === state.target) : rows.filter((row) => row.safeToStop);
627
+ if (targets.length) return { state: next, action: { type: "stop", rows: targets } };
628
+ return { state: { ...next, message: state.target ? `${shortPath(state.target)} is no longer running.` : "Nothing is safe to stop any more." } };
629
+ }
630
+ const selected = rows[state.selected];
631
+ if (key.sequence === "X" || key.name === "x" && key.shift) {
632
+ if (rows.some((row) => row.safeToStop)) return { state: { ...next, confirm: "stop-safe" } };
633
+ return { state: { ...next, message: "Nothing is safe to stop: every server has an open tab or was used in the last 30 minutes." } };
634
+ }
635
+ switch (key.name) {
636
+ case "q":
637
+ return { state, action: { type: "quit" } };
638
+ case "up":
639
+ case "k":
640
+ return { state: { ...next, selected: Math.max(0, state.selected - 1) } };
641
+ case "down":
642
+ case "j":
643
+ return { state: { ...next, selected: Math.min(Math.max(0, rows.length - 1), state.selected + 1) } };
644
+ case "o":
645
+ case "c":
646
+ if (!selected) return { state: next };
647
+ if (!selected.url) return { state: { ...next, message: "This server has no URL yet." } };
648
+ return { state: next, action: { type: key.name === "o" ? "open" : "copy", row: selected } };
649
+ case "l":
650
+ return { state: selected ? { ...next, mode: "log" } : next };
651
+ case "x":
652
+ return { state: selected ? { ...next, confirm: "stop", target: selected.project } : next };
653
+ default:
654
+ return { state: next };
655
+ }
656
+ }
657
+
658
+ // src/cli/monitor-ui.ts
520
659
  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);
660
+ var import_node_fs9 = __toESM(require("node:fs"), 1);
661
+ var import_node_os5 = __toESM(require("node:os"), 1);
524
662
  var import_node_readline = __toESM(require("node:readline"), 1);
525
663
 
526
- // src/server/artifact-address.ts
664
+ // src/server/lifecycle.ts
665
+ var DEFAULT_IDLE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
666
+ var DEFAULT_LIFECYCLE_CHECK_MS = 60 * 1e3;
667
+ function browserLauncher(platform, url) {
668
+ if (platform === "darwin") return { bin: "open", args: [url] };
669
+ if (platform === "win32") return { bin: "cmd", args: ["/c", "start", '""', url] };
670
+ if (platform === "linux" || platform === "freebsd" || platform === "openbsd") return { bin: "xdg-open", args: [url] };
671
+ return null;
672
+ }
673
+
674
+ // src/cli/servers.ts
675
+ var import_node_path8 = __toESM(require("node:path"), 1);
676
+
677
+ // src/server/registry.ts
527
678
  var import_node_crypto = __toESM(require("node:crypto"), 1);
528
679
  var import_node_fs7 = __toESM(require("node:fs"), 1);
680
+ var import_node_os4 = __toESM(require("node:os"), 1);
529
681
  var import_node_path7 = __toESM(require("node:path"), 1);
682
+ function registryDir(env = process.env, platform = process.platform, uid = process.getuid?.() ?? -1, tmpdir = import_node_os4.default.tmpdir()) {
683
+ if (env.RYUGI_RUNTIME_DIR) return import_node_path7.default.join(env.RYUGI_RUNTIME_DIR, "servers");
684
+ if (platform === "win32") return import_node_path7.default.join(tmpdir, "ryugi", "servers");
685
+ return `/tmp/ryugi-${uid}/servers`;
686
+ }
687
+ var fileFor = (dir, project) => import_node_path7.default.join(dir, `${import_node_crypto.default.createHash("sha256").update(project).digest("hex").slice(0, 16)}.json`);
688
+ function verifyDir(dir) {
689
+ if (process.platform === "win32") return;
690
+ for (const target of [import_node_path7.default.dirname(dir), dir]) {
691
+ const stat = import_node_fs7.default.lstatSync(target);
692
+ if (!stat.isDirectory() || stat.uid !== process.getuid() || stat.mode & 18) throw new Error(`Unsafe registry directory: ${target}`);
693
+ }
694
+ }
695
+ function readEntry(file) {
696
+ try {
697
+ const value = JSON.parse(import_node_fs7.default.readFileSync(file, "utf8"));
698
+ if (typeof value.project !== "string" || typeof value.root !== "string" || typeof value.serverId !== "string") return null;
699
+ if (!Number.isInteger(value.pid) || !Number.isFinite(value.startedAt)) return null;
700
+ if (value.root !== import_node_path7.default.join(value.project, ".ryugi", "artifacts")) return null;
701
+ return value;
702
+ } catch {
703
+ return null;
704
+ }
705
+ }
706
+ function readEntries(dir) {
707
+ let names;
708
+ try {
709
+ verifyDir(dir);
710
+ names = import_node_fs7.default.readdirSync(dir);
711
+ } catch {
712
+ return [];
713
+ }
714
+ return names.filter((name) => name.endsWith(".json")).flatMap((name) => readEntry(import_node_path7.default.join(dir, name)) ?? []);
715
+ }
716
+ function removeEntry(dir, entry) {
717
+ const file = fileFor(dir, entry.project);
718
+ if (readEntry(file)?.serverId === entry.serverId) import_node_fs7.default.rmSync(file, { force: true });
719
+ }
720
+
721
+ // src/cli/servers.ts
722
+ var SAFE_IDLE_MS = 30 * 6e4;
723
+ var STATUS_TIMEOUT_MS = 1e3;
724
+ var safeToStop = (status, now) => !!status && status.clients === 0 && now - status.lastActivity >= SAFE_IDLE_MS;
725
+ async function fetchStatus(url) {
726
+ const base = new URL(url);
727
+ if (base.hostname === "localhost") base.hostname = "127.0.0.1";
728
+ try {
729
+ const res = await fetch(`${base.origin}/api/status?key=${encodeURIComponent(base.searchParams.get("key") ?? "")}`, { signal: AbortSignal.timeout(STATUS_TIMEOUT_MS) });
730
+ if (!res.ok) return null;
731
+ const json = await res.json();
732
+ if (typeof json.pid !== "number" || typeof json.clients !== "number" || typeof json.lastActivity !== "number") return null;
733
+ return { pid: json.pid, clients: json.clients, lastActivity: json.lastActivity };
734
+ } catch {
735
+ return null;
736
+ }
737
+ }
738
+ function serverUrl(root) {
739
+ try {
740
+ const { url } = JSON.parse(readText(import_node_path8.default.join(root, ".state", "server-info")) ?? "");
741
+ return typeof url === "string" ? url : null;
742
+ } catch {
743
+ return null;
744
+ }
745
+ }
746
+ async function listServers(now = Date.now(), platform = process.platform) {
747
+ const dir = registryDir();
748
+ const current = process.env.RYUGI_ARTIFACTS_VERSION;
749
+ const live = readEntries(dir).filter((entry) => {
750
+ if (hasServerId(entry.pid, entry.serverId)) return true;
751
+ try {
752
+ removeEntry(dir, entry);
753
+ } catch {
754
+ }
755
+ return false;
756
+ });
757
+ const rows = await Promise.all(live.map(async (entry) => {
758
+ const url = serverUrl(entry.root);
759
+ const status = url ? await fetchStatus(url) : null;
760
+ if (status && status.pid !== entry.pid) return null;
761
+ if (!status && platform === "win32") return null;
762
+ const version = readText(import_node_path8.default.join(entry.root, ".state", "server-version")) || null;
763
+ return {
764
+ project: entry.project,
765
+ root: entry.root,
766
+ pid: entry.pid,
767
+ url,
768
+ port: url ? Number(new URL(url).port) : null,
769
+ startedAt: entry.startedAt,
770
+ version,
771
+ oldRelease: !!current && !!version && version !== current,
772
+ clients: status?.clients ?? null,
773
+ lastActivity: status?.lastActivity ?? null,
774
+ safeToStop: safeToStop(status, now),
775
+ status: status ? "ok" : "no-status",
776
+ log: import_node_path8.default.join(entry.root, ".state", "server.log")
777
+ };
778
+ }));
779
+ return rows.filter((row) => row !== null).sort((a, b) => a.project.localeCompare(b.project));
780
+ }
781
+
782
+ // src/cli/stop.ts
783
+ var import_node_fs8 = __toESM(require("node:fs"), 1);
784
+ async function stop(projectDir, emit) {
785
+ const paths = projectPaths(projectDir);
786
+ const markStopped = (reason) => {
787
+ import_node_fs8.default.rmSync(paths.info, { force: true });
788
+ import_node_fs8.default.mkdirSync(paths.state, { recursive: true });
789
+ import_node_fs8.default.writeFileSync(paths.stopped, `${JSON.stringify({ reason, timestamp: Math.floor(Date.now() / 1e3) })}
790
+ `);
791
+ };
792
+ const pid = Number(readText(paths.pid));
793
+ if (readText(paths.pid) === null) {
794
+ emit({ status: "not_running" });
795
+ return 0;
796
+ }
797
+ const id = readText(paths.serverId) ?? "";
798
+ if (!Number.isInteger(pid) || pid <= 0 || !/^[A-Za-z0-9_-]{32,64}$/.test(id) || !hasServerId(pid, id)) {
799
+ import_node_fs8.default.rmSync(paths.pid, { force: true });
800
+ import_node_fs8.default.rmSync(paths.serverId, { force: true });
801
+ markStopped("stale_pid");
802
+ emit({ status: "stale_pid" });
803
+ return 0;
804
+ }
805
+ try {
806
+ process.kill(pid, "SIGTERM");
807
+ } catch {
808
+ }
809
+ if (!await waitForExit(pid, 2e3)) {
810
+ try {
811
+ process.kill(pid, "SIGKILL");
812
+ } catch {
813
+ }
814
+ await waitForExit(pid, 1e3);
815
+ }
816
+ if (alive(pid)) {
817
+ emit({ status: "failed", error: "process still running" });
818
+ return 1;
819
+ }
820
+ for (const file of [paths.pid, paths.serverId, paths.log, paths.owners]) import_node_fs8.default.rmSync(file, { force: true });
821
+ markStopped("stop");
822
+ emit({ status: "stopped" });
823
+ return 0;
824
+ }
825
+
826
+ // src/cli/monitor-ui.ts
827
+ var LOG_TAIL_BYTES = 64 * 1024;
828
+ var LOG_REFRESH_MS = 500;
829
+ function readTail(file) {
830
+ let fd;
831
+ try {
832
+ fd = import_node_fs9.default.openSync(file, "r");
833
+ } catch {
834
+ return null;
835
+ }
836
+ try {
837
+ const size = import_node_fs9.default.fstatSync(fd).size;
838
+ const length = Math.min(size, LOG_TAIL_BYTES);
839
+ const buffer = Buffer.alloc(length);
840
+ import_node_fs9.default.readSync(fd, buffer, 0, length, size - length);
841
+ return buffer.toString("utf8");
842
+ } finally {
843
+ import_node_fs9.default.closeSync(fd);
844
+ }
845
+ }
846
+ function clipboardCommands() {
847
+ if (process.platform === "darwin") return [["pbcopy", []]];
848
+ if (process.platform === "win32") return [["clip", []]];
849
+ return [["wl-copy", []], ["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]];
850
+ }
851
+ function copy(text2, commands = clipboardCommands()) {
852
+ const [first, ...rest] = commands;
853
+ if (!first) return Promise.resolve(false);
854
+ return new Promise((resolve) => {
855
+ const child = (0, import_node_child_process2.spawn)(first[0], first[1], { stdio: ["pipe", "ignore", "ignore"] });
856
+ child.on("error", () => resolve(copy(text2, rest)));
857
+ child.on("close", (code) => code === 0 ? resolve(true) : resolve(copy(text2, rest)));
858
+ child.stdin.end(text2);
859
+ });
860
+ }
861
+ async function runMonitorUi() {
862
+ const { stdin, stdout } = process;
863
+ let rows = await listServers();
864
+ let state = initialView();
865
+ let busy = false;
866
+ let closed = false;
867
+ const restore = () => {
868
+ if (closed) return;
869
+ closed = true;
870
+ try {
871
+ stdin.setRawMode(false);
872
+ } catch {
873
+ }
874
+ stdout.write("\x1B[?25h\x1B[?1049l");
875
+ };
876
+ process.on("exit", restore);
877
+ const draw = () => {
878
+ if (closed) return;
879
+ const screen = { width: stdout.columns || 80, height: stdout.rows || 24, now: Date.now(), home: import_node_os5.default.homedir() };
880
+ const row = rows[state.selected];
881
+ const lines = state.mode === "log" && row ? renderLog(row, readTail(row.log), screen) : renderList(rows, state, screen);
882
+ stdout.write(`\x1B[H${lines.join("\x1B[K\n")}\x1B[K\x1B[J`);
883
+ };
884
+ const refresh = async () => {
885
+ if (busy || closed) return;
886
+ busy = true;
887
+ const selected = rows[state.selected]?.project;
888
+ try {
889
+ rows = await listServers();
890
+ } finally {
891
+ busy = false;
892
+ }
893
+ const index = rows.findIndex((row) => row.project === selected);
894
+ state = { ...state, selected: index >= 0 ? index : Math.min(state.selected, Math.max(0, rows.length - 1)) };
895
+ if (state.mode === "log" && !rows[state.selected]) state = { ...state, mode: "list" };
896
+ draw();
897
+ };
898
+ const say = (message) => {
899
+ state = { ...state, message };
900
+ draw();
901
+ };
902
+ const run = async (action) => {
903
+ if (action.type === "open") {
904
+ const launcher = browserLauncher(process.platform, action.row.url);
905
+ if (launcher) (0, import_node_child_process2.execFile)(launcher.bin, launcher.args, () => {
906
+ });
907
+ say(launcher ? `Opened ${action.row.url}` : `Open ${action.row.url}`);
908
+ return;
909
+ }
910
+ if (action.type === "copy") {
911
+ say(await copy(action.row.url) ? `Copied ${action.row.url}` : `No clipboard tool found; the URL is ${action.row.url}`);
912
+ return;
913
+ }
914
+ if (action.type === "stop") {
915
+ say(`Stopping ${action.rows.length === 1 ? shortPath(action.rows[0].project) : `${action.rows.length} servers`}\u2026`);
916
+ const failed = [];
917
+ for (const row of action.rows) {
918
+ let status = "failed";
919
+ try {
920
+ await stop(row.project, (value) => {
921
+ status = String(value.status ?? "failed");
922
+ });
923
+ } catch {
924
+ }
925
+ if (status === "failed") failed.push(shortPath(row.project));
926
+ }
927
+ await refresh();
928
+ say(failed.length ? `Could not stop ${failed.join(", ")}` : `Stopped ${action.rows.length === 1 ? shortPath(action.rows[0].project) : `${action.rows.length} servers`}`);
929
+ }
930
+ };
931
+ return new Promise((resolve) => {
932
+ const timers = [
933
+ setInterval(() => void refresh(), REFRESH_SECONDS * 1e3),
934
+ setInterval(() => {
935
+ if (state.mode === "log") draw();
936
+ }, LOG_REFRESH_MS)
937
+ ];
938
+ const quit = (code) => {
939
+ timers.forEach(clearInterval);
940
+ stdin.off("keypress", onKey);
941
+ stdout.off("resize", draw);
942
+ stdin.pause();
943
+ restore();
944
+ resolve(code);
945
+ };
946
+ const onKey = (_, key) => {
947
+ const result = handleKey(state, key ?? {}, rows);
948
+ state = result.state;
949
+ if (result.action?.type === "quit") return quit(0);
950
+ draw();
951
+ if (result.action) void run(result.action);
952
+ };
953
+ for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143], ["SIGHUP", 129]]) process.once(signal, () => quit(code));
954
+ stdout.write("\x1B[?1049h\x1B[?25l");
955
+ import_node_readline.default.emitKeypressEvents(stdin);
956
+ stdin.setRawMode(true);
957
+ stdin.resume();
958
+ stdin.on("keypress", onKey);
959
+ stdout.on("resize", draw);
960
+ draw();
961
+ });
962
+ }
963
+
964
+ // src/cli/monitor.ts
965
+ function monitorText(value, now = Date.now()) {
966
+ if (value.type !== "servers") return null;
967
+ const rows = value.servers;
968
+ if (!rows.length) return "No artifacts servers are running.";
969
+ return rows.map((row) => [
970
+ shortPath(row.project),
971
+ row.url ?? "starting",
972
+ `pid ${row.pid}`,
973
+ row.clients === null ? null : `tabs ${row.clients}`,
974
+ row.lastActivity === null ? null : `idle ${duration(now - row.lastActivity)}`,
975
+ ...tags(row).map((tag) => `[${tag}]`)
976
+ ].filter(Boolean).join(" ")).join("\n");
977
+ }
978
+ async function monitor(options, emit) {
979
+ if (options.json || !process.stdout.isTTY || !process.stdin.isTTY) {
980
+ emit({ type: "servers", servers: await listServers() });
981
+ return 0;
982
+ }
983
+ return runMonitorUi();
984
+ }
985
+
986
+ // src/cli/start.ts
987
+ var import_node_child_process3 = require("node:child_process");
988
+ var import_node_crypto3 = __toESM(require("node:crypto"), 1);
989
+ var import_node_fs11 = __toESM(require("node:fs"), 1);
990
+ var import_node_path10 = __toESM(require("node:path"), 1);
991
+ var import_node_readline2 = __toESM(require("node:readline"), 1);
992
+
993
+ // src/server/artifact-address.ts
994
+ var import_node_crypto2 = __toESM(require("node:crypto"), 1);
995
+ var import_node_fs10 = __toESM(require("node:fs"), 1);
996
+ var import_node_path9 = __toESM(require("node:path"), 1);
530
997
 
531
998
  // src/shared/ids.ts
532
999
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
@@ -542,15 +1009,15 @@ function artifactKeyOf(relative) {
542
1009
 
543
1010
  // src/server/artifact-address.ts
544
1011
  function artifactId(dir) {
545
- const file = import_node_path7.default.join(dir, ".artifact-id");
1012
+ const file = import_node_path9.default.join(dir, ".artifact-id");
546
1013
  let id;
547
1014
  try {
548
- id = import_node_fs7.default.readFileSync(file, "utf8").trim();
1015
+ id = import_node_fs10.default.readFileSync(file, "utf8").trim();
549
1016
  } catch (err) {
550
1017
  if (err.code !== "ENOENT") throw err;
551
- id = import_node_crypto.default.randomBytes(16).toString("hex");
1018
+ id = import_node_crypto2.default.randomBytes(16).toString("hex");
552
1019
  try {
553
- import_node_fs7.default.writeFileSync(file, `${id}
1020
+ import_node_fs10.default.writeFileSync(file, `${id}
554
1021
  `, { flag: "wx" });
555
1022
  } catch (writeError) {
556
1023
  if (writeError.code !== "EEXIST") throw writeError;
@@ -562,8 +1029,8 @@ function artifactId(dir) {
562
1029
  }
563
1030
  function artifactPath(root, type, slug) {
564
1031
  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 });
1032
+ const dir = import_node_path9.default.join(root, type, slug);
1033
+ import_node_fs10.default.mkdirSync(dir, { recursive: true });
567
1034
  return `/${type}/${artifactId(dir)}`;
568
1035
  }
569
1036
 
@@ -601,13 +1068,13 @@ async function withCloudLink(out, artifact) {
601
1068
  async function acquireLock(lock) {
602
1069
  for (let attempt = 0; attempt < 150; attempt++) {
603
1070
  try {
604
- import_node_fs8.default.mkdirSync(lock);
1071
+ import_node_fs11.default.mkdirSync(lock);
605
1072
  return true;
606
1073
  } catch {
607
1074
  }
608
1075
  try {
609
- if (Date.now() - import_node_fs8.default.statSync(lock).mtimeMs > 6e4) {
610
- import_node_fs8.default.rmdirSync(lock);
1076
+ if (Date.now() - import_node_fs11.default.statSync(lock).mtimeMs > 6e4) {
1077
+ import_node_fs11.default.rmdirSync(lock);
611
1078
  continue;
612
1079
  }
613
1080
  } catch {
@@ -637,13 +1104,13 @@ function registerOwner(paths) {
637
1104
  const owner = ownerPid();
638
1105
  if (owner) owners.add(owner);
639
1106
  const tmp = `${paths.owners}.${process.pid}`;
640
- import_node_fs8.default.writeFileSync(tmp, [...owners].sort((a, b) => a - b).map((pid) => `${pid}
1107
+ import_node_fs11.default.writeFileSync(tmp, [...owners].sort((a, b) => a - b).map((pid) => `${pid}
641
1108
  `).join(""));
642
- import_node_fs8.default.renameSync(tmp, paths.owners);
1109
+ import_node_fs11.default.renameSync(tmp, paths.owners);
643
1110
  }
644
1111
  function recordAgent(paths) {
645
1112
  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)}
1113
+ if (Object.keys(markers).length) import_node_fs11.default.writeFileSync(paths.agent, `${JSON.stringify(markers)}
647
1114
  `);
648
1115
  }
649
1116
  async function replace(pid, paths) {
@@ -675,9 +1142,9 @@ async function start(options, emit) {
675
1142
  const foreground = options.foreground || !options.background && (!!process.env.CODEX_CI || windowsLike());
676
1143
  process.umask(63);
677
1144
  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");
1145
+ import_node_fs11.default.mkdirSync(paths.state, { recursive: true });
1146
+ 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");
1147
+ const server = import_node_path10.default.join(__dirname, "server.cjs");
681
1148
  const version = process.env.RYUGI_ARTIFACTS_VERSION ?? "";
682
1149
  if (!await acquireLock(paths.lock)) throw new CliError("Another start for this project is still starting; retry in a moment");
683
1150
  let locked = true;
@@ -685,7 +1152,7 @@ async function start(options, emit) {
685
1152
  if (!locked) return;
686
1153
  locked = false;
687
1154
  try {
688
- import_node_fs8.default.rmdirSync(paths.lock);
1155
+ import_node_fs11.default.rmdirSync(paths.lock);
689
1156
  } catch {
690
1157
  }
691
1158
  };
@@ -703,20 +1170,20 @@ async function start(options, emit) {
703
1170
  }
704
1171
  registerOwner(paths);
705
1172
  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}
1173
+ import_node_fs11.default.rmSync(paths.info, { force: true });
1174
+ const id = import_node_crypto3.default.randomBytes(24).toString("hex");
1175
+ import_node_fs11.default.writeFileSync(paths.serverId, `${id}
709
1176
  `);
710
- import_node_fs8.default.writeFileSync(paths.version, `${version}
1177
+ import_node_fs11.default.writeFileSync(paths.version, `${version}
711
1178
  `);
712
1179
  const env = serverEnv(paths, options);
713
1180
  const args = [server, `--artifacts-server-id=${id}`];
714
1181
  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}
1182
+ const child2 = (0, import_node_child_process3.spawn)(process.execPath, args, { env, stdio: ["ignore", "pipe", "inherit"] });
1183
+ import_node_fs11.default.writeFileSync(paths.pid, `${child2.pid}
717
1184
  `);
718
1185
  for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child2.kill("SIGTERM"));
719
- import_node_readline.default.createInterface({ input: child2.stdout }).on("line", (line) => {
1186
+ import_node_readline2.default.createInterface({ input: child2.stdout }).on("line", (line) => {
720
1187
  let value;
721
1188
  try {
722
1189
  value = JSON.parse(line);
@@ -729,15 +1196,15 @@ async function start(options, emit) {
729
1196
  else void withCloudLink(withLink(value, options.artifact, false), options.artifact).then(emit);
730
1197
  });
731
1198
  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);
1199
+ for (let waited = 0; waited < 5e3 && !import_node_fs11.default.existsSync(paths.info); waited += 100) await sleep(100);
733
1200
  unlock();
734
1201
  return await exited;
735
1202
  }
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);
1203
+ const log = import_node_fs11.default.openSync(paths.log, "w");
1204
+ const child = (0, import_node_child_process3.spawn)(process.execPath, args, { env, detached: true, stdio: ["ignore", log, log] });
1205
+ import_node_fs11.default.closeSync(log);
739
1206
  child.unref();
740
- import_node_fs8.default.writeFileSync(paths.pid, `${child.pid}
1207
+ import_node_fs11.default.writeFileSync(paths.pid, `${child.pid}
741
1208
  `);
742
1209
  for (let waited = 0; waited < 5e3; waited += 100) {
743
1210
  const lines = (readText(paths.log) ?? "").split("\n");
@@ -816,52 +1283,8 @@ async function publish(options, emit) {
816
1283
  return 0;
817
1284
  }
818
1285
 
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
1286
  // src/cli/main.ts
864
- var HELP = `Usage: ryugi-artifacts [project-directory] [--no-open]
1287
+ var HELP2 = `Usage: ryugi-artifacts [project-directory] [--no-open]
865
1288
  ryugi-artifacts <command> [options]
866
1289
 
867
1290
  Without a command, opens the project's artifacts in your browser and keeps the server
@@ -882,6 +1305,8 @@ Commands:
882
1305
  reply --artifact <document|artboard>/<id> --thread <threadId> [--text <text>]
883
1306
  resolve --artifact <document|artboard>/<id> --thread <threadId>
884
1307
  Without --text, add and reply read the message from stdin.
1308
+ monitor List every running artifacts server on this machine and manage them. In a terminal it
1309
+ opens a live view (open, copy URL, logs, stop); otherwise, or with --json, prints the list once.
885
1310
  config Auto-publish changed artifacts as private cloud copies when the agent's turn ends:
886
1311
  --auto-publish on|off for every project
887
1312
  --auto-publish on|off|inherit --project override for this project only
@@ -914,6 +1339,8 @@ ${value.url}`;
914
1339
  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
1340
  const checked = checkText(value);
916
1341
  if (checked !== null) return checked;
1342
+ const servers = monitorText(value);
1343
+ if (servers !== null) return servers;
917
1344
  const commented = commentText(value);
918
1345
  if (commented !== null) return commented;
919
1346
  if (typeof value.status === "string") return value.status === "stopped" ? "Stopped." : value.status.replace(/_/g, " ");
@@ -922,7 +1349,7 @@ ${value.url}`;
922
1349
  function openBrowser(url) {
923
1350
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
924
1351
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
925
- (0, import_node_child_process3.execFile)(command, args, () => {
1352
+ (0, import_node_child_process4.execFile)(command, args, () => {
926
1353
  });
927
1354
  }
928
1355
  async function browse(argv) {
@@ -1045,12 +1472,16 @@ async function main(argv) {
1045
1472
  project: !!values.project
1046
1473
  }, emit);
1047
1474
  }
1475
+ if (command === "monitor") {
1476
+ const { values } = parse(rest, {});
1477
+ return await monitor({ json: !!values.json }, emit);
1478
+ }
1048
1479
  if (command === "--version" || command === "-v") {
1049
- console.log("1.32.0");
1480
+ console.log("1.34.0");
1050
1481
  return 0;
1051
1482
  }
1052
1483
  if (command === "--help" || command === "-h") {
1053
- console.log(HELP);
1484
+ console.log(HELP2);
1054
1485
  return 0;
1055
1486
  }
1056
1487
  return await browse(argv);