@roamcode.ai/server 1.0.0 → 1.0.2

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.
Files changed (99) hide show
  1. package/dist/index.d.ts +2406 -82
  2. package/dist/index.js +8324 -44
  3. package/dist/managed-update-helper.js +631 -26
  4. package/dist/mcp-send.js +76 -90
  5. package/dist/start.d.ts +591 -9
  6. package/dist/start.js +7629 -353
  7. package/package.json +3 -3
  8. package/dist/auth.d.ts +0 -63
  9. package/dist/auth.js +0 -133
  10. package/dist/claude-auth-service.d.ts +0 -76
  11. package/dist/claude-auth-service.js +0 -217
  12. package/dist/claude-latest-service.d.ts +0 -34
  13. package/dist/claude-latest-service.js +0 -61
  14. package/dist/config.d.ts +0 -78
  15. package/dist/config.js +0 -59
  16. package/dist/data-dir.d.ts +0 -42
  17. package/dist/data-dir.js +0 -70
  18. package/dist/diag.d.ts +0 -43
  19. package/dist/diag.js +0 -83
  20. package/dist/fs-service.d.ts +0 -90
  21. package/dist/fs-service.js +0 -290
  22. package/dist/managed-runtime.d.ts +0 -51
  23. package/dist/managed-runtime.js +0 -411
  24. package/dist/managed-update-helper.d.ts +0 -2
  25. package/dist/mcp-send.d.ts +0 -33
  26. package/dist/origin-check.d.ts +0 -37
  27. package/dist/origin-check.js +0 -101
  28. package/dist/pane-status.d.ts +0 -61
  29. package/dist/pane-status.js +0 -145
  30. package/dist/providers/claude-metadata-service.d.ts +0 -58
  31. package/dist/providers/claude-metadata-service.js +0 -352
  32. package/dist/providers/claude-provider.d.ts +0 -11
  33. package/dist/providers/claude-provider.js +0 -166
  34. package/dist/providers/codex-activity.d.ts +0 -21
  35. package/dist/providers/codex-activity.js +0 -122
  36. package/dist/providers/codex-app-server-client.d.ts +0 -90
  37. package/dist/providers/codex-app-server-client.js +0 -485
  38. package/dist/providers/codex-latest-service.d.ts +0 -50
  39. package/dist/providers/codex-latest-service.js +0 -174
  40. package/dist/providers/codex-metadata-service.d.ts +0 -161
  41. package/dist/providers/codex-metadata-service.js +0 -686
  42. package/dist/providers/codex-profile-client.d.ts +0 -16
  43. package/dist/providers/codex-profile-client.js +0 -52
  44. package/dist/providers/codex-profile-security.d.ts +0 -23
  45. package/dist/providers/codex-profile-security.js +0 -161
  46. package/dist/providers/codex-provider.d.ts +0 -15
  47. package/dist/providers/codex-provider.js +0 -174
  48. package/dist/providers/codex-thread-coordinator.d.ts +0 -18
  49. package/dist/providers/codex-thread-coordinator.js +0 -93
  50. package/dist/providers/codex-thread-persistence.d.ts +0 -9
  51. package/dist/providers/codex-thread-persistence.js +0 -45
  52. package/dist/providers/codex-thread-resolver.d.ts +0 -59
  53. package/dist/providers/codex-thread-resolver.js +0 -322
  54. package/dist/providers/options.d.ts +0 -7
  55. package/dist/providers/options.js +0 -155
  56. package/dist/providers/provider-artifacts.d.ts +0 -3
  57. package/dist/providers/provider-artifacts.js +0 -30
  58. package/dist/providers/registry.d.ts +0 -7
  59. package/dist/providers/registry.js +0 -23
  60. package/dist/providers/types.d.ts +0 -95
  61. package/dist/providers/types.js +0 -8
  62. package/dist/push-dispatch.d.ts +0 -81
  63. package/dist/push-dispatch.js +0 -100
  64. package/dist/push-store.d.ts +0 -25
  65. package/dist/push-store.js +0 -79
  66. package/dist/rate-limit.d.ts +0 -52
  67. package/dist/rate-limit.js +0 -72
  68. package/dist/server-config.d.ts +0 -60
  69. package/dist/server-config.js +0 -77
  70. package/dist/service-install.d.ts +0 -60
  71. package/dist/service-install.js +0 -221
  72. package/dist/session-defaults.d.ts +0 -26
  73. package/dist/session-defaults.js +0 -60
  74. package/dist/session-store.d.ts +0 -81
  75. package/dist/session-store.js +0 -654
  76. package/dist/static-routes.d.ts +0 -101
  77. package/dist/static-routes.js +0 -188
  78. package/dist/terminal-capability.d.ts +0 -5
  79. package/dist/terminal-capability.js +0 -27
  80. package/dist/terminal-manager.d.ts +0 -224
  81. package/dist/terminal-manager.js +0 -917
  82. package/dist/terminal-process.d.ts +0 -85
  83. package/dist/terminal-process.js +0 -238
  84. package/dist/terminal-shared.d.ts +0 -36
  85. package/dist/terminal-shared.js +0 -43
  86. package/dist/tmux-list.d.ts +0 -11
  87. package/dist/tmux-list.js +0 -39
  88. package/dist/transport.d.ts +0 -123
  89. package/dist/transport.js +0 -1559
  90. package/dist/updater.d.ts +0 -161
  91. package/dist/updater.js +0 -451
  92. package/dist/usage-service.d.ts +0 -118
  93. package/dist/usage-service.js +0 -173
  94. package/dist/vapid.d.ts +0 -17
  95. package/dist/vapid.js +0 -31
  96. package/dist/web-push-send.d.ts +0 -20
  97. package/dist/web-push-send.js +0 -21
  98. package/dist/ws-ticket.d.ts +0 -47
  99. package/dist/ws-ticket.js +0 -62
@@ -1,34 +1,639 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, unlinkSync } from "node:fs";
3
- import { installManagedRelease, writeManagedStatus } from "./managed-runtime.js";
4
- async function main() {
5
- const configPath = process.argv[2];
6
- if (!configPath)
7
- throw new Error("managed updater config path is required");
8
- const config = JSON.parse(readFileSync(configPath, "utf8"));
2
+ #!/usr/bin/env node
3
+
4
+ // src/managed-update-helper.ts
5
+ import { readFileSync as readFileSync3, unlinkSync as unlinkSync2 } from "fs";
6
+
7
+ // src/managed-runtime.ts
8
+ import { spawn } from "child_process";
9
+ import { randomUUID } from "crypto";
10
+ import {
11
+ chmodSync as chmodSync2,
12
+ existsSync as existsSync2,
13
+ lstatSync,
14
+ mkdirSync as mkdirSync2,
15
+ openSync,
16
+ closeSync,
17
+ readdirSync,
18
+ readFileSync as readFileSync2,
19
+ realpathSync,
20
+ renameSync,
21
+ rmSync,
22
+ symlinkSync,
23
+ unlinkSync,
24
+ writeFileSync as writeFileSync2
25
+ } from "fs";
26
+ import { homedir as homedir2, tmpdir } from "os";
27
+ import { basename, dirname as dirname2, join as join2, relative, resolve } from "path";
28
+
29
+ // src/service-install.ts
30
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
31
+ import { homedir, platform } from "os";
32
+ import { dirname, join } from "path";
33
+ import { spawnSync } from "child_process";
34
+ function escapeXml(value) {
35
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;").replace(/'/g, "&apos;");
36
+ }
37
+ function quoteSystemd(value) {
38
+ return /[\s\"\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/\"/g, '\\"')}"` : value;
39
+ }
40
+ function buildServicePath(nodePath, home) {
41
+ return [
42
+ dirname(nodePath),
43
+ "/opt/homebrew/bin",
44
+ "/usr/local/bin",
45
+ join(home, ".local", "bin"),
46
+ join(home, ".local", "share", "pnpm"),
47
+ join(home, "Library", "pnpm"),
48
+ "/usr/bin",
49
+ "/bin"
50
+ ].join(":");
51
+ }
52
+ function programArguments(opts) {
53
+ if (opts.executablePath) return [opts.executablePath];
54
+ if (opts.nodePath && opts.cliPath) return [opts.nodePath, opts.cliPath];
55
+ throw new Error("service requires executablePath or nodePath + cliPath");
56
+ }
57
+ function renderLaunchdPlist(opts) {
58
+ const args = programArguments(opts).map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
59
+ const pathEntry = opts.servicePath ? `
60
+ <key>PATH</key>
61
+ <string>${escapeXml(opts.servicePath)}</string>` : "";
62
+ const installEntry = opts.installRoot ? `
63
+ <key>ROAMCODE_INSTALL_ROOT</key>
64
+ <string>${escapeXml(opts.installRoot)}</string>` : "";
65
+ return `<?xml version="1.0" encoding="UTF-8"?>
66
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
67
+ <plist version="1.0">
68
+ <dict>
69
+ <key>Label</key>
70
+ <string>${escapeXml(opts.label)}</string>
71
+ <key>ProgramArguments</key>
72
+ <array>
73
+ ${args}
74
+ </array>
75
+ <key>EnvironmentVariables</key>
76
+ <dict>
77
+ <key>ROAMCODE_DATA_DIR</key>
78
+ <string>${escapeXml(opts.dataDir)}</string>${installEntry}${pathEntry}
79
+ </dict>
80
+ <key>RunAtLoad</key>
81
+ <true/>
82
+ <key>KeepAlive</key>
83
+ <true/>
84
+ <key>StandardOutPath</key>
85
+ <string>${escapeXml(join(opts.dataDir, "roamcode.log"))}</string>
86
+ <key>StandardErrorPath</key>
87
+ <string>${escapeXml(join(opts.dataDir, "roamcode.err.log"))}</string>
88
+ </dict>
89
+ </plist>
90
+ `;
91
+ }
92
+ function renderSystemdUnit(opts) {
93
+ const command = programArguments(opts).map(quoteSystemd).join(" ");
94
+ const pathLine = opts.servicePath ? `
95
+ Environment=PATH=${opts.servicePath}` : "";
96
+ const installLine = opts.installRoot ? `
97
+ Environment=ROAMCODE_INSTALL_ROOT=${opts.installRoot}` : "";
98
+ return `[Unit]
99
+ Description=roamcode \u2014 operate Claude Code or Codex sessions remotely
100
+ After=network-online.target
101
+
102
+ [Service]
103
+ ExecStart=${command}
104
+ Environment=ROAMCODE_DATA_DIR=${opts.dataDir}${installLine}${pathLine}
105
+ Restart=always
106
+ RestartSec=3
107
+
108
+ [Install]
109
+ WantedBy=default.target
110
+ `;
111
+ }
112
+ function persistRecord(dataDir, record) {
113
+ mkdirSync(dataDir, { recursive: true, mode: 448 });
114
+ const path = join(dataDir, "service.json");
115
+ writeFileSync(path, JSON.stringify(record, null, 2) + "\n", { mode: 384 });
116
+ chmodSync(path, 384);
117
+ }
118
+ function installService(ctx) {
119
+ const home = ctx.home ?? homedir();
120
+ const os = ctx.os ?? platform();
121
+ const servicePath = buildServicePath(ctx.nodePath, home);
122
+ if (os === "darwin") {
123
+ const label = ctx.label ?? "com.roamcode";
124
+ const dir = join(home, "Library", "LaunchAgents");
125
+ const path = join(dir, `${label}.plist`);
126
+ mkdirSync(dir, { recursive: true });
127
+ writeFileSync(
128
+ path,
129
+ renderLaunchdPlist({
130
+ label,
131
+ nodePath: ctx.nodePath,
132
+ cliPath: ctx.cliPath,
133
+ executablePath: ctx.executablePath,
134
+ dataDir: ctx.dataDir,
135
+ installRoot: ctx.installRoot,
136
+ servicePath
137
+ })
138
+ );
139
+ chmodSync(path, 420);
140
+ const record = {
141
+ manager: "launchd",
142
+ label,
143
+ path,
144
+ ...ctx.executablePath ? { launcherPath: ctx.executablePath } : {},
145
+ ...ctx.installRoot ? { installRoot: ctx.installRoot } : {}
146
+ };
147
+ persistRecord(ctx.dataDir, record);
148
+ return {
149
+ path,
150
+ record,
151
+ instructions: `launchctl load -w "${path}" # start now + at login
152
+ launchctl unload -w "${path}" # stop`
153
+ };
154
+ }
155
+ if (os === "linux") {
156
+ const label = ctx.label ?? "roamcode";
157
+ const dir = join(home, ".config", "systemd", "user");
158
+ const path = join(dir, `${label}.service`);
159
+ mkdirSync(dir, { recursive: true });
160
+ writeFileSync(
161
+ path,
162
+ renderSystemdUnit({
163
+ nodePath: ctx.nodePath,
164
+ cliPath: ctx.cliPath,
165
+ executablePath: ctx.executablePath,
166
+ dataDir: ctx.dataDir,
167
+ installRoot: ctx.installRoot,
168
+ servicePath
169
+ })
170
+ );
171
+ chmodSync(path, 420);
172
+ const record = {
173
+ manager: "systemd",
174
+ label,
175
+ path,
176
+ ...ctx.executablePath ? { launcherPath: ctx.executablePath } : {},
177
+ ...ctx.installRoot ? { installRoot: ctx.installRoot } : {}
178
+ };
179
+ persistRecord(ctx.dataDir, record);
180
+ return {
181
+ path,
182
+ record,
183
+ instructions: [
184
+ "systemctl --user daemon-reload",
185
+ `systemctl --user enable --now ${label} # start now + at login`,
186
+ `systemctl --user disable --now ${label} # stop`,
187
+ "(run 'loginctl enable-linger $USER' so it runs without an active login session)"
188
+ ].join("\n")
189
+ };
190
+ }
191
+ throw new Error(`unsupported platform: ${os} \u2014 run roamcode manually or under a user-level supervisor`);
192
+ }
193
+ function readServiceRecord(dataDir) {
194
+ try {
195
+ const parsed = JSON.parse(readFileSync(join(dataDir, "service.json"), "utf8"));
196
+ if (parsed.manager !== "launchd" && parsed.manager !== "systemd" || typeof parsed.label !== "string") {
197
+ return void 0;
198
+ }
199
+ const fallbackPath = parsed.manager === "launchd" ? join(homedir(), "Library", "LaunchAgents", `${parsed.label}.plist`) : join(homedir(), ".config", "systemd", "user", `${parsed.label}.service`);
200
+ return { ...parsed, path: typeof parsed.path === "string" ? parsed.path : fallbackPath };
201
+ } catch {
202
+ return void 0;
203
+ }
204
+ }
205
+ function migrateServiceToLauncher(opts) {
206
+ const current = readServiceRecord(opts.dataDir);
207
+ if (!current) return void 0;
208
+ return installService({
209
+ nodePath: opts.nodePath,
210
+ executablePath: opts.launcherPath,
211
+ dataDir: opts.dataDir,
212
+ installRoot: opts.installRoot,
213
+ os: current.manager === "launchd" ? "darwin" : "linux",
214
+ label: current.label
215
+ }).record;
216
+ }
217
+ function restartService(record) {
218
+ if (!existsSync(record.path)) return { ok: false, error: `service file not found: ${record.path}` };
219
+ if (record.manager === "launchd") {
220
+ spawnSync("launchctl", ["unload", record.path], { stdio: "ignore" });
221
+ const res = spawnSync("launchctl", ["load", "-w", record.path], { encoding: "utf8" });
222
+ return res.status === 0 ? { ok: true } : { ok: false, error: res.stderr || "launchctl load failed" };
223
+ }
224
+ const reload = spawnSync("systemctl", ["--user", "daemon-reload"], { encoding: "utf8" });
225
+ if (reload.status !== 0) return { ok: false, error: reload.stderr || "systemctl daemon-reload failed" };
226
+ const restart = spawnSync("systemctl", ["--user", "restart", record.label], { encoding: "utf8" });
227
+ return restart.status === 0 ? { ok: true } : { ok: false, error: restart.stderr || "systemctl restart failed" };
228
+ }
229
+
230
+ // src/managed-runtime.ts
231
+ var VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
232
+ var LOCK_STALE_MS = 30 * 6e4;
233
+ var NPM_INSTALL_POLICY = `${JSON.stringify(
234
+ {
235
+ private: true,
236
+ allowScripts: {
237
+ "better-sqlite3@12.11.1": true,
238
+ "node-pty@1.1.0": true
239
+ }
240
+ },
241
+ null,
242
+ 2
243
+ )}
244
+ `;
245
+ function managedPaths(root) {
246
+ return {
247
+ root,
248
+ releases: join2(root, "releases"),
249
+ staging: join2(root, "staging"),
250
+ current: join2(root, "current"),
251
+ previous: join2(root, "previous"),
252
+ bin: join2(root, "bin"),
253
+ launcher: join2(root, "bin", "roamcode"),
254
+ lock: join2(root, "update.lock")
255
+ };
256
+ }
257
+ function isStableVersion(value) {
258
+ return VERSION_RE.test(value);
259
+ }
260
+ function atomicWrite(path, value, mode = 384) {
261
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
262
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
263
+ writeFileSync2(temp, value, { mode });
264
+ chmodSync2(temp, mode);
265
+ renameSync(temp, path);
266
+ }
267
+ function renderManagedLauncher(root, nodePath) {
268
+ const q = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
269
+ return [
270
+ "#!/bin/sh",
271
+ "set -eu",
272
+ `ROOT=${q(root)}`,
273
+ `NODE=${q(nodePath)}`,
274
+ 'if [ ! -x "$NODE" ]; then NODE="$(command -v node || true)"; fi',
275
+ 'if [ -z "$NODE" ]; then echo "Node.js >= 24 is required" >&2; exit 1; fi',
276
+ 'ENTRY="$ROOT/current/node_modules/roamcode/dist/index.js"',
277
+ 'if [ ! -f "$ENTRY" ]; then',
278
+ ' echo "roamcode managed runtime is missing; run: npx --yes --allow-scripts=better-sqlite3,node-pty roamcode@latest install" >&2',
279
+ " exit 1",
280
+ "fi",
281
+ 'export ROAMCODE_INSTALL_ROOT="$ROOT"',
282
+ 'export ROAMCODE_MANAGED_EXEC="1"',
283
+ 'exec "$NODE" "$ENTRY" "$@"',
284
+ ""
285
+ ].join("\n");
286
+ }
287
+ function writeManagedLauncher(root, nodePath = process.execPath) {
288
+ const paths = managedPaths(root);
289
+ mkdirSync2(paths.bin, { recursive: true, mode: 448 });
290
+ atomicWrite(paths.launcher, renderManagedLauncher(root, nodePath), 448);
291
+ return paths.launcher;
292
+ }
293
+ function versionFromReleaseDir(dir) {
294
+ try {
295
+ const manifest = JSON.parse(readFileSync2(join2(dir, "release.json"), "utf8"));
296
+ if (typeof manifest.version === "string" && isStableVersion(manifest.version)) return manifest.version;
297
+ } catch {
298
+ }
299
+ try {
300
+ const pkg = JSON.parse(readFileSync2(join2(dir, "node_modules", "roamcode", "package.json"), "utf8"));
301
+ return typeof pkg.version === "string" && isStableVersion(pkg.version) ? pkg.version : void 0;
302
+ } catch {
303
+ return void 0;
304
+ }
305
+ }
306
+ function integrityFromReleaseDir(dir) {
307
+ try {
308
+ const manifest = JSON.parse(readFileSync2(join2(dir, "release.json"), "utf8"));
309
+ return typeof manifest.integrity === "string" ? manifest.integrity : void 0;
310
+ } catch {
311
+ return void 0;
312
+ }
313
+ }
314
+ function readActiveVersion(root) {
315
+ const current = managedPaths(root).current;
316
+ try {
317
+ return versionFromReleaseDir(realpathSync(current));
318
+ } catch {
319
+ return void 0;
320
+ }
321
+ }
322
+ function acquireLock(path, now) {
323
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
324
+ try {
325
+ const fd = openSync(path, "wx", 384);
326
+ writeFileSync2(fd, JSON.stringify({ pid: process.pid, at: now }) + "\n");
327
+ closeSync(fd);
328
+ } catch {
329
+ try {
330
+ if (now - lstatSync(path).mtimeMs > LOCK_STALE_MS) {
331
+ unlinkSync(path);
332
+ return acquireLock(path, now);
333
+ }
334
+ } catch {
335
+ try {
336
+ return acquireLock(path, now);
337
+ } catch {
338
+ }
339
+ }
340
+ throw new Error("an update is already in progress");
341
+ }
342
+ return () => {
9
343
  try {
10
- await installManagedRelease({
11
- version: config.version,
12
- installRoot: config.installRoot,
13
- dataDir: config.dataDir,
14
- operationId: config.operationId,
15
- expectedIntegrity: config.expectedIntegrity,
16
- expectedIntegrities: config.expectedIntegrities,
17
- nodePath: config.nodePath,
18
- restart: config.restart,
19
- onStatus: (status) => writeManagedStatus(config.dataDir, status),
20
- });
21
- }
22
- finally {
23
- try {
24
- unlinkSync(configPath);
344
+ unlinkSync(path);
345
+ } catch {
346
+ }
347
+ };
348
+ }
349
+ async function runLogged(command, args, opts) {
350
+ await new Promise((resolvePromise, reject) => {
351
+ const child = spawn(command, args, {
352
+ cwd: opts.cwd,
353
+ env: opts.env ?? process.env,
354
+ stdio: ["ignore", "pipe", "pipe"]
355
+ });
356
+ child.stdout?.on("data", (chunk) => opts.log(chunk.toString()));
357
+ child.stderr?.on("data", (chunk) => opts.log(chunk.toString()));
358
+ child.on("error", reject);
359
+ child.on(
360
+ "close",
361
+ (code) => code === 0 ? resolvePromise() : reject(new Error(`${basename(command)} exited ${code}`))
362
+ );
363
+ });
364
+ }
365
+ function npmInvocation(npmCommand, args, nodePath) {
366
+ if (npmCommand.endsWith(".js") || npmCommand.endsWith(".cjs"))
367
+ return { command: nodePath, args: [npmCommand, ...args] };
368
+ return { command: npmCommand, args };
369
+ }
370
+ function npmProjectEnv(env = process.env) {
371
+ const clean = { ...env };
372
+ for (const key of Object.keys(clean)) {
373
+ if (key.toLowerCase() === "npm_config_allow_scripts") delete clean[key];
374
+ }
375
+ return clean;
376
+ }
377
+ async function npmIntegrity(npmCommand, packageName, version, nodePath, log) {
378
+ let output = "";
379
+ const invocation = npmInvocation(
380
+ npmCommand,
381
+ ["view", `${packageName}@${version}`, "dist.integrity", "--json"],
382
+ nodePath
383
+ );
384
+ await new Promise((resolvePromise, reject) => {
385
+ const child = spawn(invocation.command, invocation.args, {
386
+ env: npmProjectEnv(),
387
+ stdio: ["ignore", "pipe", "pipe"]
388
+ });
389
+ child.stdout?.on("data", (chunk) => output += chunk.toString());
390
+ child.stderr?.on("data", (chunk) => log(chunk.toString()));
391
+ child.on("error", reject);
392
+ child.on("close", (code) => code === 0 ? resolvePromise() : reject(new Error(`npm view exited ${code}`)));
393
+ });
394
+ const parsed = JSON.parse(output);
395
+ if (typeof parsed !== "string" || !parsed.startsWith("sha512-")) throw new Error("npm returned no package integrity");
396
+ return parsed;
397
+ }
398
+ async function smokeServer(serverEntry, dataDir, nodePath, log) {
399
+ const smokeDir = join2(tmpdir(), `roamcode-smoke-${process.pid}-${randomUUID()}`);
400
+ mkdirSync2(smokeDir, { recursive: true, mode: 448 });
401
+ let output = "";
402
+ const child = spawn(nodePath, [serverEntry], {
403
+ env: {
404
+ ...process.env,
405
+ PORT: "0",
406
+ BIND_ADDRESS: "127.0.0.1",
407
+ ACCESS_TOKEN: `rc-smoke-${randomUUID()}`,
408
+ ROAMCODE_DATA_DIR: smokeDir,
409
+ RC_TMUX_SOCKET: `rc-smoke-${process.pid}`,
410
+ ROAMCODE_INSTALL_ROOT: ""
411
+ },
412
+ stdio: ["ignore", "pipe", "pipe"]
413
+ });
414
+ const collect = (chunk) => {
415
+ const text = chunk.toString();
416
+ output += text;
417
+ log(text);
418
+ };
419
+ child.stdout?.on("data", collect);
420
+ child.stderr?.on("data", collect);
421
+ try {
422
+ const url = await new Promise((resolveUrl, reject) => {
423
+ const deadline = setTimeout(() => reject(new Error("new release did not start within 25s")), 25e3);
424
+ const poll = setInterval(() => {
425
+ const match = output.match(/listening on (http:\/\/127\.0\.0\.1:\d+)/);
426
+ if (match?.[1]) {
427
+ clearInterval(poll);
428
+ clearTimeout(deadline);
429
+ resolveUrl(match[1]);
25
430
  }
26
- catch {
27
- // best effort; config contains no secret, but should not accumulate
431
+ }, 100);
432
+ child.once("exit", (code) => {
433
+ clearInterval(poll);
434
+ clearTimeout(deadline);
435
+ reject(new Error(`new release exited before health check (${code})`));
436
+ });
437
+ });
438
+ const healthy = await fetch(`${url}/health`, { signal: AbortSignal.timeout(5e3) });
439
+ if (!healthy.ok) throw new Error(`new release health check returned ${healthy.status}`);
440
+ } finally {
441
+ child.kill("SIGTERM");
442
+ rmSync(smokeDir, { recursive: true, force: true });
443
+ void dataDir;
444
+ }
445
+ }
446
+ function replaceSymlink(link, target) {
447
+ const temp = `${link}.${process.pid}.${randomUUID()}.new`;
448
+ try {
449
+ unlinkSync(temp);
450
+ } catch {
451
+ }
452
+ const parent = realpathSync(dirname2(link));
453
+ const canonicalTarget = realpathSync(target);
454
+ symlinkSync(relative(parent, canonicalTarget), temp, "dir");
455
+ renameSync(temp, link);
456
+ }
457
+ function trimLog(log) {
458
+ return log.split("\n").slice(-20).join("\n").slice(-8e3);
459
+ }
460
+ async function installManagedRelease(opts) {
461
+ if (!isStableVersion(opts.version)) throw new Error(`invalid release version: ${opts.version}`);
462
+ const now = opts.now ?? Date.now;
463
+ const operationId = opts.operationId ?? randomUUID();
464
+ const nodePath = opts.nodePath ?? process.execPath;
465
+ const npmCommand = opts.npmCommand ?? process.env.npm_execpath ?? "npm";
466
+ const expectedIntegrities = opts.expectedIntegrities ?? (opts.expectedIntegrity ? { roamcode: opts.expectedIntegrity } : {});
467
+ const rootIntegrity = expectedIntegrities.roamcode;
468
+ const paths = managedPaths(opts.installRoot);
469
+ mkdirSync2(paths.releases, { recursive: true, mode: 448 });
470
+ mkdirSync2(paths.staging, { recursive: true, mode: 448 });
471
+ const release = join2(paths.releases, opts.version);
472
+ const stage = join2(paths.staging, `${opts.version}-${process.pid}-${randomUUID()}`);
473
+ let logText = "";
474
+ const log = (line) => {
475
+ logText += line.endsWith("\n") ? line : `${line}
476
+ `;
477
+ mkdirSync2(opts.dataDir, { recursive: true, mode: 448 });
478
+ writeFileSync2(join2(opts.dataDir, "update.log"), line.endsWith("\n") ? line : `${line}
479
+ `, { flag: "a" });
480
+ };
481
+ const fromVersion = readActiveVersion(opts.installRoot);
482
+ const status = (state, phase, error) => {
483
+ opts.onStatus?.({
484
+ operationId,
485
+ state,
486
+ phase,
487
+ target: opts.version,
488
+ ...fromVersion ? { fromVersion } : {},
489
+ ...error ? { error } : {},
490
+ log: trimLog(logText),
491
+ updatedAt: now()
492
+ });
493
+ };
494
+ const releaseLock = acquireLock(paths.lock, now());
495
+ let activated = false;
496
+ try {
497
+ status("starting", "preparing");
498
+ const reusableRelease = existsSync2(release) && versionFromReleaseDir(release) === opts.version && (!rootIntegrity || integrityFromReleaseDir(release) === rootIntegrity);
499
+ if (!reusableRelease) {
500
+ if (Object.keys(expectedIntegrities).length > 0) {
501
+ status("downloading", "checking package integrity");
502
+ for (const [packageName, expected] of Object.entries(expectedIntegrities)) {
503
+ const actual = await npmIntegrity(npmCommand, packageName, opts.version, nodePath, log);
504
+ if (actual !== expected) throw new Error(`GitHub release and npm integrity do not match for ${packageName}`);
28
505
  }
506
+ }
507
+ status("installing", "installing package");
508
+ mkdirSync2(stage, { recursive: true, mode: 448 });
509
+ atomicWrite(join2(stage, "package.json"), NPM_INSTALL_POLICY);
510
+ const invocation = npmInvocation(
511
+ npmCommand,
512
+ [
513
+ "install",
514
+ "--prefix",
515
+ stage,
516
+ "--omit=dev",
517
+ "--no-audit",
518
+ "--no-fund",
519
+ "--package-lock=false",
520
+ `roamcode@${opts.version}`
521
+ ],
522
+ nodePath
523
+ );
524
+ await runLogged(invocation.command, invocation.args, { env: npmProjectEnv(), log });
525
+ const installed = versionFromReleaseDir(stage);
526
+ if (installed !== opts.version)
527
+ throw new Error(`installed roamcode ${installed ?? "unknown"}, expected ${opts.version}`);
528
+ const serverEntry = join2(stage, "node_modules", "@roamcode.ai", "server", "dist", "start.js");
529
+ if (!existsSync2(serverEntry)) throw new Error("published package is missing @roamcode.ai/server/dist/start.js");
530
+ status("verifying", "boot smoke");
531
+ await smokeServer(serverEntry, opts.dataDir, nodePath, log);
532
+ atomicWrite(
533
+ join2(stage, "release.json"),
534
+ JSON.stringify(
535
+ {
536
+ version: opts.version,
537
+ installedAt: now(),
538
+ package: `roamcode@${opts.version}`,
539
+ ...rootIntegrity ? { integrity: rootIntegrity } : {}
540
+ },
541
+ null,
542
+ 2
543
+ ) + "\n"
544
+ );
545
+ if (existsSync2(release)) rmSync(release, { recursive: true, force: true });
546
+ renameSync(stage, release);
547
+ } else {
548
+ const serverEntry = join2(release, "node_modules", "@roamcode.ai", "server", "dist", "start.js");
549
+ if (!existsSync2(serverEntry)) throw new Error(`managed release ${opts.version} is incomplete`);
550
+ status("verifying", "verifying installed release");
551
+ await smokeServer(serverEntry, opts.dataDir, nodePath, log);
552
+ }
553
+ status("activating", "activating release");
554
+ let oldTarget;
555
+ try {
556
+ oldTarget = realpathSync(paths.current);
557
+ } catch {
558
+ oldTarget = void 0;
559
+ }
560
+ if (oldTarget && resolve(oldTarget) !== resolve(release)) replaceSymlink(paths.previous, oldTarget);
561
+ replaceSymlink(paths.current, release);
562
+ activated = true;
563
+ const launcherPath = writeManagedLauncher(opts.installRoot, nodePath);
564
+ const existingService = readServiceRecord(opts.dataDir);
565
+ const record = existingService ? migrateServiceToLauncher({
566
+ dataDir: opts.dataDir,
567
+ installRoot: opts.installRoot,
568
+ launcherPath,
569
+ nodePath
570
+ }) : void 0;
571
+ if (opts.restart && record) {
572
+ status("restarting", "restarting service");
573
+ const restarted = restartService(record);
574
+ if (!restarted.ok) throw new Error(restarted.error ?? "service restart failed");
575
+ }
576
+ status("done", "done");
577
+ const keep = new Set([opts.version, fromVersion].filter((value) => !!value));
578
+ for (const name of readdirSync(paths.releases)) {
579
+ if (isStableVersion(name) && !keep.has(name))
580
+ rmSync(join2(paths.releases, name), { recursive: true, force: true });
581
+ }
582
+ return {
583
+ version: opts.version,
584
+ ...fromVersion ? { previousVersion: fromVersion } : {},
585
+ releaseDir: release,
586
+ launcherPath
587
+ };
588
+ } catch (error) {
589
+ const message = error instanceof Error ? error.message : String(error);
590
+ log(`FAILED: ${message}`);
591
+ if (activated) {
592
+ try {
593
+ const previous = realpathSync(paths.previous);
594
+ replaceSymlink(paths.current, previous);
595
+ } catch {
596
+ }
597
+ }
598
+ status("failed", "failed", message);
599
+ throw error;
600
+ } finally {
601
+ rmSync(stage, { recursive: true, force: true });
602
+ releaseLock();
603
+ }
604
+ }
605
+ function writeManagedStatus(dataDir, status) {
606
+ atomicWrite(join2(dataDir, "update-status.json"), JSON.stringify(status, null, 2) + "\n", 384);
607
+ }
608
+
609
+ // src/managed-update-helper.ts
610
+ async function main() {
611
+ const configPath = process.argv[2];
612
+ if (!configPath) throw new Error("managed updater config path is required");
613
+ const config = JSON.parse(readFileSync3(configPath, "utf8"));
614
+ try {
615
+ await installManagedRelease({
616
+ version: config.version,
617
+ installRoot: config.installRoot,
618
+ dataDir: config.dataDir,
619
+ operationId: config.operationId,
620
+ expectedIntegrity: config.expectedIntegrity,
621
+ expectedIntegrities: config.expectedIntegrities,
622
+ nodePath: config.nodePath,
623
+ restart: config.restart,
624
+ onStatus: (status) => writeManagedStatus(config.dataDir, status)
625
+ });
626
+ } finally {
627
+ try {
628
+ unlinkSync2(configPath);
629
+ } catch {
29
630
  }
631
+ }
30
632
  }
31
633
  void main().catch((error) => {
32
- process.stderr.write(`roamcode updater failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
33
- process.exitCode = 1;
634
+ process.stderr.write(
635
+ `roamcode updater failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}
636
+ `
637
+ );
638
+ process.exitCode = 1;
34
639
  });