返回 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
8 export class RegistryError extends Error {
9 constructor(code, message) { super(message); this.name = "RegistryError"; this.code = code; }
10 }
11
12 const ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
13
14 export function stateDir() {
15 return process.env.CODEWHALE_CU_STATE_DIR || path.join(os.homedir(), ".codewhale-cu");
16 }
17
18 function registryPath() { return path.join(stateDir(), "computers.json"); }
19
20 const PLATFORM = process.platform; // darwin | linux | win32
21
22 function localEntry() {
23 return {
24 id: "local",
25 transport: "local",
26 platform: PLATFORM,
27 label: `This ${PLATFORM === "darwin" ? "Mac" : PLATFORM === "win32" ? "Windows PC" : "Linux machine"}`,
28 registeredAt: new Date().toISOString(),
29 };
30 }
31
32 export function load() {
33 try {
34 const raw = JSON.parse(fs.readFileSync(registryPath(), "utf8"));
35 if (!raw || typeof raw !== "object" || !raw.computers) throw new Error("bad shape");
36 return raw;
37 } catch {
38 const fresh = { version: 1, active: "local", computers: { local: localEntry() } };
39 return fresh;
40 }
41 }
42
43 export function save(reg) {
44 fs.mkdirSync(stateDir(), { recursive: true });
45 const tmp = registryPath() + ".tmp";
46 fs.writeFileSync(tmp, JSON.stringify(reg, null, 2) + "\n");
47 fs.renameSync(tmp, registryPath());
48 return reg;
49 }
50
51 export function list() {
52 const reg = load();
53 // Keep the local entry truthful even if the state file is stale.
54 reg.computers.local = { ...(reg.computers.local ?? {}), ...localEntry() };
55 if (!reg.computers[reg.active]) reg.active = "local";
56 return reg;
57 }
58
59 export function get(id) {
60 const reg = list();
61 const c = reg.computers[id];
62 if (!c) throw new RegistryError("unknown_computer", `no computer registered with id "${id}". Use computer_list.`);
63 return c;
64 }
65
66 export function active() { return get(list().active); }
67
68 /** Switch the active computer. Returns the computer entry. */
69 export function switchTo(id) {
70 const c = get(id); // throws unknown_computer
71 const reg = load();
72 reg.active = c.id;
73 save(reg);
74 return c;
75 }
76
77 /** Register or update a computer. Returns the entry. */
78 export function register({ id, transport, label, ...rest }) {
79 if (!id || !ID_RE.test(id)) throw new RegistryError("invalid_id", "computer id must match " + ID_RE);
80 if (!["local", "ssh", "hdc", "docker"].includes(transport)) {
81 throw new RegistryError("invalid_transport", "transport must be one of: local, ssh, hdc, docker");
82 }
83 if (id === "local" && transport !== "local") {
84 throw new RegistryError("reserved_id", '"local" is reserved for this machine');
85 }
86 if (transport === "ssh") {
87 if (!rest.host || !/^[A-Za-z0-9._-]+$/.test(rest.host)) {
88 throw new RegistryError("invalid_host", "ssh computers need a valid host (letters, digits, dot, dash, underscore)");
89 }
90 if (rest.port != null && (!Number.isInteger(rest.port) || rest.port < 1 || rest.port > 65535)) {
91 throw new RegistryError("invalid_port", "port must be an integer in 1..65535");
92 }
93 if (rest.user != null && !/^[a-zA-Z0-9._-]+$/.test(rest.user)) {
94 throw new RegistryError("invalid_user", "user must be a plain name");
95 }
96 }
97 if (transport === "hdc") {
98 if (rest.target != null && !/^[A-Za-z0-9._-]*$/.test(rest.target)) {
99 throw new RegistryError("invalid_target", "hdc target key contains invalid characters");
100 }
101 rest.platform = "harmonyos";
102 }
103 if (transport === "docker") {
104 if (!rest.container || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(rest.container)) {
105 throw new RegistryError("invalid_container", "docker computers need a valid container name");
106 }
107 // Spawned containers always run the Linux desktop image.
108 rest.platform = rest.platform ?? "linux";
109 }
110 const reg = load();
111 const prev = reg.computers[id];
112 reg.computers[id] = {
113 ...prev,
114 id,
115 transport,
116 label: label ?? prev?.label ?? id,
117 registeredAt: prev?.registeredAt ?? new Date().toISOString(),
118 ...rest,
119 };
120 save(reg);
121 return reg.computers[id];
122 }
123
124 export function remove(id) {
125 if (id === "local") throw new RegistryError("reserved_id", '"local" cannot be removed');
126 const reg = load();
127 if (!reg.computers[id]) throw new RegistryError("unknown_computer", `no computer registered with id "${id}"`);
128 delete reg.computers[id];
129 if (reg.active === id) reg.active = "local";
130 save(reg);
131 return { removed: id, active: reg.active };
132 }
133
133 lines Plain Text