返回 CodeWhale
registry.mjs
根目录 / crates / tui / plugins / computer-use / src / registry.mjs
1 // Computer registry: named computers (local, ssh, hdc) with a sticky active
2 // computer. Every codewhale-cu tool defaults to the active computer; passing
3 // `computer` on any tool switches to it first (switch-by-use).
4 import fs from "node:fs";
5 import path from "node:path";
6 import os from "node:os";
7 import { validateSshTarget } from "./ssh-args.mjs";
8
9 export class RegistryError extends Error {
10 constructor(code, message) { super(message); this.name = "RegistryError"; this.code = code; }
11 }
12
13 const ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
14
15 export function stateDir() {
16 return process.env.CODEWHALE_CU_STATE_DIR || path.join(os.homedir(), ".codewhale-cu");
17 }
18
19 function registryPath() { return path.join(stateDir(), "computers.json"); }
20
21 const PLATFORM = process.platform; // darwin | linux | win32
22
23 function localEntry() {
24 return {
25 id: "local",
26 transport: "local",
27 platform: PLATFORM,
28 label: `This ${PLATFORM === "darwin" ? "Mac" : PLATFORM === "win32" ? "Windows PC" : "Linux machine"}`,
29 registeredAt: new Date().toISOString(),
30 };
31 }
32
33 /** An entry read from computers.json that could not have been registered. */
34 function unusableEntry(entry) {
35 if (!entry || typeof entry !== "object") return true;
36 if (entry.transport !== "ssh") return false;
37 try { validateSshTarget(entry); return false; } catch { return true; }
38 }
39
40 export function load() {
41 try {
42 const raw = JSON.parse(fs.readFileSync(registryPath(), "utf8"));
43 if (!raw || typeof raw !== "object" || !raw.computers) throw new Error("bad shape");
44 // The file is state, not authority: an entry that registration would
45 // refuse (an ssh host or user that reads as an option) is dropped.
46 for (const [id, entry] of Object.entries(raw.computers)) {
47 if (unusableEntry(entry)) delete raw.computers[id];
48 }
49 return raw;
50 } catch {
51 const fresh = { version: 1, active: "local", computers: { local: localEntry() } };
52 return fresh;
53 }
54 }
55
56 export function save(reg) {
57 fs.mkdirSync(stateDir(), { recursive: true });
58 const tmp = registryPath() + ".tmp";
59 fs.writeFileSync(tmp, JSON.stringify(reg, null, 2) + "\n");
60 fs.renameSync(tmp, registryPath());
61 return reg;
62 }
63
64 export function list() {
65 const reg = load();
66 // Keep the local entry truthful even if the state file is stale.
67 reg.computers.local = { ...(reg.computers.local ?? {}), ...localEntry() };
68 if (!reg.computers[reg.active]) reg.active = "local";
69 return reg;
70 }
71
72 export function get(id) {
73 const reg = list();
74 const c = reg.computers[id];
75 if (!c) throw new RegistryError("unknown_computer", `no computer registered with id "${id}". Use computer_list.`);
76 return c;
77 }
78
79 export function active() { return get(list().active); }
80
81 /** Switch the active computer. Returns the computer entry. */
82 export function switchTo(id) {
83 const c = get(id); // throws unknown_computer
84 const reg = load();
85 reg.active = c.id;
86 save(reg);
87 return c;
88 }
89
90 /** Register or update a computer. Returns the entry. */
91 export function register({ id, transport, label, ...rest }) {
92 if (!id || !ID_RE.test(id)) throw new RegistryError("invalid_id", "computer id must match " + ID_RE);
93 if (!["local", "ssh", "hdc", "docker"].includes(transport)) {
94 throw new RegistryError("invalid_transport", "transport must be one of: local, ssh, hdc, docker");
95 }
96 if (id === "local" && transport !== "local") {
97 throw new RegistryError("reserved_id", '"local" is reserved for this machine');
98 }
99 if (transport === "ssh") {
100 try {
101 validateSshTarget(rest);
102 } catch (e) {
103 throw new RegistryError(e.code ?? "invalid_host", e.message);
104 }
105 }
106 if (transport === "hdc") {
107 if (rest.target != null && !/^[A-Za-z0-9._-]*$/.test(rest.target)) {
108 throw new RegistryError("invalid_target", "hdc target key contains invalid characters");
109 }
110 rest.platform = "harmonyos";
111 }
112 if (transport === "docker") {
113 if (!rest.container || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(rest.container)) {
114 throw new RegistryError("invalid_container", "docker computers need a valid container name");
115 }
116 // Spawned containers always run the Linux desktop image.
117 rest.platform = rest.platform ?? "linux";
118 }
119 const reg = load();
120 const prev = reg.computers[id];
121 reg.computers[id] = {
122 ...prev,
123 id,
124 transport,
125 label: label ?? prev?.label ?? id,
126 registeredAt: prev?.registeredAt ?? new Date().toISOString(),
127 ...rest,
128 };
129 save(reg);
130 return reg.computers[id];
131 }
132
133 export function remove(id) {
134 if (id === "local") throw new RegistryError("reserved_id", '"local" cannot be removed');
135 const reg = load();
136 if (!reg.computers[id]) throw new RegistryError("unknown_computer", `no computer registered with id "${id}"`);
137 delete reg.computers[id];
138 if (reg.active === id) reg.active = "local";
139 save(reg);
140 return { removed: id, active: reg.active };
141 }
142
142 lines Plain Text