@vantaloom/runtime-linux-x64 0.12.49 → 0.12.51

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 (40) hide show
  1. package/VERSION +1 -1
  2. package/bin/vantaloom-agent +0 -0
  3. package/bin/vantaloom-api +0 -0
  4. package/bin/vantaloom-browser +0 -0
  5. package/bin/vantaloomctl +0 -0
  6. package/cli/package.json +1 -1
  7. package/cli/src/cli.mjs +460 -2100
  8. package/cli/src/lib/auth.mjs +155 -0
  9. package/cli/src/lib/constants.mjs +34 -0
  10. package/cli/src/lib/install.mjs +627 -0
  11. package/cli/src/lib/lifecycle.mjs +187 -0
  12. package/cli/src/lib/mesh.mjs +236 -0
  13. package/cli/src/lib/package.mjs +206 -0
  14. package/cli/src/lib/platform.mjs +165 -0
  15. package/cli/src/lib/registry.mjs +237 -0
  16. package/manifest.json +2 -2
  17. package/package.json +1 -1
  18. package/web/404.html +1 -1
  19. package/web/__next.__PAGE__.txt +2 -2
  20. package/web/__next._full.txt +3 -3
  21. package/web/__next._head.txt +1 -1
  22. package/web/__next._index.txt +2 -2
  23. package/web/__next._tree.txt +2 -2
  24. package/web/_next/static/chunks/5131a5e74e730c2c.css +2 -0
  25. package/web/_next/static/chunks/8ed6033ffe9bc476.js +66 -0
  26. package/web/_not-found/__next._full.txt +2 -2
  27. package/web/_not-found/__next._head.txt +1 -1
  28. package/web/_not-found/__next._index.txt +2 -2
  29. package/web/_not-found/__next._not-found/__PAGE__.txt +1 -1
  30. package/web/_not-found/__next._not-found.txt +1 -1
  31. package/web/_not-found/__next._tree.txt +2 -2
  32. package/web/_not-found.html +1 -1
  33. package/web/_not-found.txt +2 -2
  34. package/web/index.html +1 -1
  35. package/web/index.txt +3 -3
  36. package/web/_next/static/chunks/e6cf1bffaf46e8a8.css +0 -2
  37. package/web/_next/static/chunks/fb49ea3bfe8faeb0.js +0 -66
  38. /package/web/_next/static/{jpoLF2PZ-dWfDd5NxkZG0 → i_RwVLHU7xKejCSV52htQ}/_buildManifest.js +0 -0
  39. /package/web/_next/static/{jpoLF2PZ-dWfDd5NxkZG0 → i_RwVLHU7xKejCSV52htQ}/_clientMiddlewareManifest.json +0 -0
  40. /package/web/_next/static/{jpoLF2PZ-dWfDd5NxkZG0 → i_RwVLHU7xKejCSV52htQ}/_ssgManifest.js +0 -0
package/cli/src/cli.mjs CHANGED
@@ -1,2100 +1,460 @@
1
- import { execFileSync, spawnSync } from "node:child_process"
2
- import {
3
- appendFileSync,
4
- chmodSync,
5
- copyFileSync,
6
- cpSync,
7
- existsSync,
8
- mkdirSync,
9
- mkdtempSync,
10
- readdirSync,
11
- readFileSync,
12
- renameSync,
13
- rmSync,
14
- writeFileSync,
15
- } from "node:fs"
16
- import { cp, writeFile } from "node:fs/promises"
17
- import { createHash } from "node:crypto"
18
- import os from "node:os"
19
- import path from "node:path"
20
- import readline from "node:readline"
21
- import { fileURLToPath } from "node:url"
22
-
23
- // The mesh binaries the privileged service holds open while running. On Windows
24
- // a file copy can't overwrite them in place, so the lock-safe `apply` path swaps
25
- // them after stopping the service.
26
- const WINDOWS_MESH_BINARIES = ["easytier-core.exe", "easytier-cli.exe", "wintun.dll", "Packet.dll", "WinDivert64.sys", "vantaloom-mesh.exe"]
27
-
28
- // The privileged-mesh files that must NOT be needlessly overwritten on update:
29
- // the service binary (vantaloom-mesh) and the EasyTier engine (easytier-core,
30
- // the Linux setcap target), plus the Windows support DLLs the running service
31
- // holds open. Leaving an unchanged copy in place keeps the service running and
32
- // preserves its Linux file capability — so the update needs no elevation.
33
- function meshBinarySet(platform) {
34
- return platform === "win32"
35
- ? WINDOWS_MESH_BINARIES
36
- : ["vantaloom-mesh", "easytier-core"]
37
- }
38
-
39
- // sleepSync blocks for ms milliseconds without async (install runs top-to-bottom
40
- // and must not race the file copy against a process that is still releasing its
41
- // handles). Uses Atomics.wait on a throwaway buffer — no busy spin.
42
- function sleepSync(ms) {
43
- try {
44
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms))
45
- } catch {
46
- const end = Date.now() + ms
47
- while (Date.now() < end) {} // fallback busy-wait if SAB is unavailable
48
- }
49
- }
50
-
51
- const LOCK_ERROR_CODES = new Set(["EPERM", "EBUSY", "EACCES", "ETXTBSY"])
52
-
53
- // copyFileResilient overwrites dst with src, surviving the Windows case where dst
54
- // is a running/locked executable. Windows refuses to OVERWRITE or DELETE an in-use
55
- // .exe (EPERM/EBUSY) but DOES allow RENAMING it — the file handle tracks the file
56
- // object, not its path. So on a lock error we move the locked file aside and copy
57
- // the new one into the freed path; the runtime picks up the new binary on its next
58
- // start, and the moved-aside `.old-*` file is reaped on a later install. This is
59
- // the fix for the EPERM that blocked update/restart when a prior process (api,
60
- // agent, or an orphaned easytier/mesh) still held a binary open.
61
- function copyFileResilient(src, dst) {
62
- for (let attempt = 0; ; attempt += 1) {
63
- try {
64
- copyFileSync(src, dst)
65
- return
66
- } catch (err) {
67
- if (!LOCK_ERROR_CODES.has(err.code) || !existsSync(dst)) {
68
- if (attempt >= 4) throw err
69
- sleepSync(250)
70
- continue
71
- }
72
- try {
73
- const aside = `${dst}.old-${process.pid}-${attempt}`
74
- renameSync(dst, aside)
75
- copyFileSync(src, dst)
76
- try { rmSync(aside, { force: true }) } catch {} // best-effort; may still be locked
77
- return
78
- } catch (moveErr) {
79
- if (attempt >= 4) throw moveErr
80
- sleepSync(300)
81
- }
82
- }
83
- }
84
- }
85
-
86
- // copyDirResilient recursively copies src→dst using copyFileResilient for every
87
- // file, so a single locked binary can't abort the whole bin/ refresh. filter(srcPath)
88
- // gates which entries are copied (mirrors fs.cp's filter).
89
- function copyDirResilient(src, dst, filter) {
90
- mkdirSync(dst, { recursive: true })
91
- for (const entry of readdirSync(src, { withFileTypes: true })) {
92
- const s = path.join(src, entry.name)
93
- if (filter && !filter(s)) continue
94
- const d = path.join(dst, entry.name)
95
- if (entry.isDirectory()) {
96
- copyDirResilient(s, d, filter)
97
- } else {
98
- copyFileResilient(s, d)
99
- }
100
- }
101
- }
102
-
103
- // cleanupStaleReplacements deletes the `.old-*` files left behind by a prior
104
- // lock-safe replace once their handles have been released (best-effort).
105
- function cleanupStaleReplacements(dir) {
106
- if (!existsSync(dir)) return
107
- for (const entry of readdirSync(dir)) {
108
- if (/\.old-\d+-\d+$/.test(entry)) {
109
- try { rmSync(path.join(dir, entry), { force: true }) } catch {}
110
- }
111
- }
112
- }
113
-
114
- const cliRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..")
115
- const repoCandidate = path.resolve(cliRoot, "..", "..")
116
- const installedConfigPath = path.join(cliRoot, "config.json")
117
- const defaultReleaseTag = "runtime-latest"
118
- const defaultRepo = "Timefiles404/Vantaloom-next"
119
- const defaultNpmRegistry = "https://registry.npmjs.org"
120
- const fallbackNpmRegistries = [
121
- "https://registry.npmmirror.com",
122
- ]
123
-
124
- // The Claude SDK bridge the Go backend spawns as
125
- // `node <installDir>/adapters/claude-sdk-bridge/index.mjs`. The bridge source ships
126
- // inside this CLI package (packages/cli/adapters/claude-sdk-bridge); bundleAdapter
127
- // installs its sole dependency (@anthropic-ai/claude-agent-sdk, version pinned in the
128
- // bridge's own package.json) into adapters/claude-sdk-bridge/node_modules at
129
- // install-adapter time. There is NO @zed-industries/claude-code-acp anymore — the
130
- // engine speaks the SDK's native stream-json directly.
131
-
132
- // Inherit strict-ssl=false from npm/npx config, or respect NODE_TLS_REJECT_UNAUTHORIZED.
133
- // When npm runs us via npx with strict-ssl disabled, it sets npm_config_strict_ssl="false".
134
- // Also check user .npmrc for strict-ssl=false (common in China behind proxies).
135
- function shouldDisableTLS() {
136
- if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") return true
137
- if (process.env.npm_config_strict_ssl === "false") return true
138
- if (process.env.npm_config_strict_ssl === "") return true
139
- try {
140
- const npmrcPath = path.join(os.homedir(), ".npmrc")
141
- if (existsSync(npmrcPath)) {
142
- const content = readFileSync(npmrcPath, "utf8")
143
- if (/^\s*strict-ssl\s*=\s*false/m.test(content)) return true
144
- }
145
- } catch {}
146
- return false
147
- }
148
- if (shouldDisableTLS()) {
149
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
150
- }
151
-
152
- const cliVersion = readJSONIfExists(path.join(cliRoot, "package.json")).version ?? "unknown"
153
-
154
- export async function main(argv) {
155
- const command = argv[0] ?? "help"
156
- const options = parseOptions(argv.slice(1))
157
-
158
- // --no-strict-ssl flag disables TLS certificate verification for fetch calls
159
- if (options.noStrictSsl) {
160
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
161
- }
162
-
163
- if (command === "install" || command === "update") {
164
- console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
165
- }
166
-
167
- switch (command) {
168
- case "install":
169
- if (options.package) {
170
- await installFromPackage({ ...options, update: false })
171
- } else if (shouldUseSourceInstall(options)) {
172
- await installFromSource(options)
173
- } else if (shouldUseReleaseSync(options)) {
174
- await syncFromRelease(options, "install")
175
- } else {
176
- await syncFromNpmRegistry(options, "install")
177
- }
178
- return
179
- case "update":
180
- if (options.package) {
181
- await installFromPackage({ ...options, update: true })
182
- } else if (shouldUseSourceInstall(options)) {
183
- await installFromSource({ ...options, update: true })
184
- } else if (shouldUseReleaseSync(options)) {
185
- await syncFromRelease(options, "update")
186
- } else {
187
- await syncFromNpmRegistry(options, "update")
188
- }
189
- return
190
- case "package":
191
- await packageRuntime(options)
192
- return
193
- case "install-adapter":
194
- await installAdapter(options)
195
- return
196
- case "platform":
197
- console.log(platformId())
198
- return
199
- case "start":
200
- case "stop":
201
- case "restart":
202
- case "status":
203
- case "ports":
204
- runCtl(command, options)
205
- return
206
- case "path":
207
- printPaths(options)
208
- return
209
- case "uninstall":
210
- await uninstallRuntime(options)
211
- return
212
- case "login":
213
- await runLogin(options)
214
- return
215
- case "logout":
216
- await runLogout(options)
217
- return
218
- case "help":
219
- case "-h":
220
- case "--help":
221
- printHelp()
222
- return
223
- default:
224
- throw new Error(`unknown command "${command}"`)
225
- }
226
- }
227
-
228
- async function installFromSource(options) {
229
- const sourceRoot = findSourceRoot(options.source)
230
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
231
- const buildRoot = path.join(sourceRoot, "artifacts", "local-install", platformId())
232
-
233
- const { version } = await buildRuntimePackage(sourceRoot, buildRoot, {
234
- buildWeb: options.buildWeb,
235
- skipAdapter: options.skipAdapter,
236
- })
237
-
238
- await applyPackage(buildRoot, prefix, {
239
- noStart: options.noStart,
240
- sourceRoot,
241
- update: options.update,
242
- withMesh: options.withMesh,
243
- skipMesh: options.skipMesh,
244
- })
245
-
246
- console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
247
- console.log(`version: ${version}`)
248
- ensureInPath(prefix)
249
- console.log(`run: ${displayCommand(prefix)} status`)
250
- }
251
-
252
- async function installFromPackage(options) {
253
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
254
- const packageRoot = safeDirectory(options.package)
255
- const version = await applyPackage(packageRoot, prefix, {
256
- noStart: options.noStart,
257
- update: options.update,
258
- withMesh: options.withMesh,
259
- skipMesh: options.skipMesh,
260
- })
261
-
262
- console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
263
- console.log(`version: ${version}`)
264
- ensureInPath(prefix)
265
- console.log(`run: ${displayCommand(prefix)} status`)
266
- }
267
-
268
- async function packageRuntime(options) {
269
- const sourceRoot = findSourceRoot(options.source)
270
- const targetPlatform = options.target ?? platformId()
271
- const packageRoot = safeDirectory(
272
- options.output ?? path.join(sourceRoot, "artifacts", "packages", `vantaloom-${targetPlatform}`)
273
- )
274
- const { platform: builtPlatform, version } = await buildRuntimePackage(sourceRoot, packageRoot, {
275
- buildWeb: options.buildWeb,
276
- target: targetPlatform,
277
- skipAdapter: options.skipAdapter,
278
- })
279
-
280
- if (options.npmPackage) {
281
- writeRuntimePackageMetadata(packageRoot, sourceRoot, builtPlatform)
282
- }
283
-
284
- if (options.archive) {
285
- const archivePath = `${packageRoot}.tar.gz`
286
- removeKnownPath(archivePath, path.dirname(archivePath))
287
- run("tar", [
288
- "-czf",
289
- archivePath,
290
- "-C",
291
- path.dirname(packageRoot),
292
- path.basename(packageRoot),
293
- ])
294
- console.log(`archive: ${archivePath}`)
295
- }
296
-
297
- console.log(`packaged Vantaloom: ${packageRoot}`)
298
- console.log(`platform: ${builtPlatform}`)
299
- console.log(`version: ${version}`)
300
- }
301
-
302
- // installAdapter downloads + bundles the Claude Code ACP adapter into a LIVE
303
- // install's adapters/ dir (node_modules + patched local-command forwarding +
304
- // reclaude shim), so the user can add Claude Code / reclaude support on demand
305
- // instead of shipping ~50-100MB inside every runtime tarball. The Go backend
306
- // invokes this (silent, no console window) from the settings "install adapter"
307
- // button; it can also be run by hand: `vantaloom install-adapter`.
308
- async function installAdapter(options) {
309
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
310
- console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
311
- console.log(`installing Claude Code ACP adapter into ${path.join(prefix, "adapters")} ...`)
312
- await bundleAdapter(prefix, { platform: platformId() })
313
- console.log(`adapter ready`)
314
- }
315
-
316
- async function buildRuntimePackage(sourceRoot, packageRoot, options) {
317
- const version = gitVersion(sourceRoot)
318
- const platform = options.target ?? platformId()
319
- const buildBin = path.join(packageRoot, "bin")
320
- const buildWeb = path.join(packageRoot, "web")
321
-
322
- removeKnownPath(packageRoot, path.dirname(packageRoot))
323
- mkdirSync(buildBin, { recursive: true })
324
-
325
- buildGo(sourceRoot, buildBin, "vantaloom-api", platform)
326
- buildGo(sourceRoot, buildBin, "vantaloom-agent", platform)
327
- buildGo(sourceRoot, buildBin, "vantaloomctl", platform)
328
- // Official "Vantaloom 浏览器" extension sidecar. Pure-Go (chromedp/CDP, no
329
- // CGO), cross-compiles for every target. Ships dormant: it's only started by
330
- // the api when the user installs+enables the browser extension (the heavy
331
- // Chromium binary is downloaded on demand, NOT bundled here).
332
- buildGo(sourceRoot, buildBin, "vantaloom-browser", platform)
333
- // Privileged EasyTier sidecar (runs the mesh node as a service on Windows/macOS).
334
- // Pure-Go (no CGO), so it cross-compiles cleanly for every target.
335
- buildGo(sourceRoot, buildBin, "vantaloom-mesh", platform)
336
- // The Windows system-tray app (vantaloom-tray) was removed — it crash-looped
337
- // on some Windows 11 builds and got respawned until OOM. No longer built.
338
-
339
- // Bundle the EasyTier mesh binaries (P2P virtual network) for the target platform.
340
- await copyEasyTier(sourceRoot, buildBin, platform)
341
-
342
- if (options.buildWeb) {
343
- runPnpm(["--filter", "vantaloom-app", "build"], { cwd: sourceRoot })
344
- }
345
-
346
- await copyStaticWeb(sourceRoot, buildWeb)
347
- await copyCliDirectory(path.join(packageRoot, "cli"), sourceRoot, runtimeConfigFromSource(sourceRoot))
348
- // The Claude Code ACP adapter is NO LONGER bundled into the runtime package
349
- // (it bloated every platform tarball by ~50-100MB). Instead the user installs
350
- // it on demand from 设置 → CC 引擎 → "下载/安装适配器", which runs the
351
- // `install-adapter` CLI command into <installDir>/adapters. Because applyPackage
352
- // leaves a missing adapters/ dir untouched on update, the user-installed adapter
353
- // survives runtime updates.
354
- writeBuildManifest(packageRoot, version, platform)
355
-
356
- return { platform, version }
357
- }
358
-
359
- // bundleAdapter installs the Claude Agent SDK into
360
- // <packageRoot>/adapters/claude-sdk-bridge/node_modules and copies the bridge source
361
- // (the thin node sidecar that runs the SDK's query() and pipes its native stream-json
362
- // to the Go runtime). It also writes the reclaude shim the Go engine points the SDK's
363
- // pathToClaudeCodeExecutable at. The SDK version is pinned in the bridge's own
364
- // package.json (single source of truth); native optional deps (e.g. sharp) are
365
- // selected per TARGET platform.
366
- async function bundleAdapter(packageRoot, options = {}) {
367
- const adaptersRoot = path.join(packageRoot, "adapters")
368
- const bridgeDest = path.join(adaptersRoot, "claude-sdk-bridge")
369
- const bridgeSrc = path.join(cliRoot, "adapters", "claude-sdk-bridge")
370
- const shimPath = path.join(adaptersRoot, "reclaude-shim.mjs")
371
- const shimSource = `// Vantaloom reclaude shim: the Claude Agent SDK runs \`node <this>\` as the claude
372
- // executable, so we re-exec the real reclaude binary (path in VANTALOOM_RECLAUDE_EXE)
373
- // with the same args + inherited stdio. reclaude's banner goes to stderr, keeping the
374
- // SDK's stdout protocol clean.
375
- import { spawn } from "node:child_process";
376
- const exe = process.env.VANTALOOM_RECLAUDE_EXE;
377
- if (!exe) {
378
- process.stderr.write("[reclaude-shim] VANTALOOM_RECLAUDE_EXE not set\\n");
379
- process.exit(127);
380
- }
381
- const child = spawn(exe, process.argv.slice(2), { stdio: "inherit", windowsHide: true });
382
- child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
383
- child.on("error", (e) => {
384
- process.stderr.write(\`[reclaude-shim] failed to launch reclaude: \${e.message}\\n\`);
385
- process.exit(127);
386
- });
387
- `
388
-
389
- if (!existsSync(path.join(bridgeSrc, "index.mjs"))) {
390
- const msg = `claude-sdk-bridge source missing at ${bridgeSrc}`
391
- if (options.skipAdapter) {
392
- console.warn(` warning: skipping SDK bridge bundle (${msg})`)
393
- return
394
- }
395
- throw new Error(msg)
396
- }
397
-
398
- // Resolve the SDK's platform-gated native optional deps (e.g. sharp's
399
- // @img/sharp-<os>-<cpu>) for the TARGET platform, not the build host — otherwise a
400
- // Windows-built bundle would ship the wrong native binary to the mac/linux packages.
401
- const { os: targetOs, cpu: targetCpu } = parsePlatformId(options.platform ?? platformId())
402
- console.log(` bundling Claude Agent SDK bridge (os=${targetOs} cpu=${targetCpu}) ...`)
403
- const tmp = mkdtempSync(path.join(os.tmpdir(), "vantaloom-sdk-bridge-"))
404
- try {
405
- // Install the bridge's OWN dependencies (the SDK, version pinned in its
406
- // package.json) into a temp prefix from the PUBLIC registry explicitly: the
407
- // machine's global .npmrc points at npmmirror, which may not mirror this package.
408
- cpSync(path.join(bridgeSrc, "package.json"), path.join(tmp, "package.json"))
409
- const npmArgs = [
410
- "install",
411
- "--prefix",
412
- tmp,
413
- "--registry",
414
- "https://registry.npmjs.org/",
415
- "--no-audit",
416
- "--no-fund",
417
- "--omit=dev",
418
- "--os",
419
- targetOs,
420
- "--cpu",
421
- targetCpu,
422
- ]
423
- if (targetOs === "linux") {
424
- npmArgs.push("--libc", "glibc")
425
- }
426
- runNpm(npmArgs)
427
-
428
- const tmpModules = path.join(tmp, "node_modules")
429
- if (!existsSync(tmpModules)) {
430
- throw new Error(`SDK install produced no node_modules at ${tmpModules}`)
431
- }
432
-
433
- removeKnownPath(adaptersRoot, path.dirname(adaptersRoot))
434
- mkdirSync(bridgeDest, { recursive: true })
435
- // Bridge source: index.mjs (+ package.json, for reference/version).
436
- cpSync(path.join(bridgeSrc, "index.mjs"), path.join(bridgeDest, "index.mjs"))
437
- cpSync(path.join(bridgeSrc, "package.json"), path.join(bridgeDest, "package.json"))
438
- // The SDK + its dependency tree.
439
- cpSync(tmpModules, path.join(bridgeDest, "node_modules"), { recursive: true })
440
-
441
- writeFileSync(shimPath, shimSource)
442
- console.log(` bundled SDK bridge -> adapters/claude-sdk-bridge + reclaude-shim.mjs`)
443
- } catch (error) {
444
- if (options.skipAdapter) {
445
- console.warn(` warning: skipping SDK bridge bundle (${error.message})`)
446
- return
447
- }
448
- throw new Error(`failed to bundle SDK bridge: ${error.message}`)
449
- } finally {
450
- rmSync(tmp, { recursive: true, force: true })
451
- }
452
- }
453
-
454
- // copyEasyTier bundles the vendored EasyTier binaries (easytier-core/easytier-cli,
455
- // plus wintun.dll on Windows) for the target platform into the package bin/ dir.
456
- async function copyEasyTier(sourceRoot, buildBin, platform) {
457
- const vendorMap = {
458
- "win32-x64": "windows-x86_64",
459
- "darwin-arm64": "macos-aarch64",
460
- "linux-x64": "linux-x86_64",
461
- }
462
- const vendorName = vendorMap[platform]
463
- if (!vendorName) {
464
- console.warn(` warning: no EasyTier mapping for ${platform}; mesh disabled for this platform`)
465
- return
466
- }
467
- const srcDir = path.join(sourceRoot, "vendor", "easytier", vendorName, `easytier-${vendorName}`)
468
- if (!existsSync(srcDir)) {
469
- console.warn(` warning: EasyTier binaries not found at ${srcDir}; run vendor download first`)
470
- return
471
- }
472
- const isWin = platform.startsWith("win32")
473
- // Packet.dll + WinDivert64.sys are REQUIRED: easytier-core.exe statically
474
- // imports Packet.dll, so without it the process dies at launch with
475
- // STATUS_DLL_NOT_FOUND (0xC0000135) before writing any log. wintun.dll is the
476
- // TUN backend. easytier-cli.exe does not need Packet.dll (it still launches).
477
- const files = isWin
478
- ? ["easytier-core.exe", "easytier-cli.exe", "wintun.dll", "Packet.dll", "WinDivert64.sys"]
479
- : ["easytier-core", "easytier-cli"]
480
- let copied = 0
481
- for (const f of files) {
482
- const src = path.join(srcDir, f)
483
- if (!existsSync(src)) {
484
- console.warn(` warning: EasyTier file missing: ${f}`)
485
- continue
486
- }
487
- await cp(src, path.join(buildBin, f), { force: true })
488
- copied++
489
- }
490
- console.log(` bundled EasyTier ${vendorName} (${copied} files)`)
491
- }
492
-
493
- async function applyPackage(packageRoot, prefix, options) {
494
- assertRuntimePackage(packageRoot)
495
-
496
- const existingConfig = readInstalledConfig(prefix)
497
- const packageConfig = readJSONIfExists(path.join(packageRoot, "cli", "config.json"))
498
- const mergedConfig = mergeRuntimeConfig(packageConfig, existingConfig, {
499
- sourceRoot: options.sourceRoot,
500
- })
501
- const version = readVersion(packageRoot)
502
-
503
- mkdirSync(prefix, { recursive: true })
504
- const existingCtl = path.join(prefix, "bin", binaryName("vantaloomctl"))
505
- if (existsSync(existingCtl)) {
506
- spawnSync(existingCtl, ["stop", "--prefix", prefix], { stdio: "inherit", windowsHide: true })
507
- }
508
-
509
- // Kill lingering tray process that may hold locks on bin/ (older versions
510
- // don't write tray.pid, so vantaloomctl stop won't find them).
511
- killTrayProcess(prefix)
512
-
513
- // Windows releases a stopped process's file handles asynchronously; copying
514
- // bin/ the instant after `stop` can still hit the old exe's lock (EPERM). Give
515
- // the OS a moment to close handles, and sweep any `.old-*` files a previous
516
- // lock-safe replace left behind (now that those handles are likely released).
517
- // copyFileResilient below is the real safety net if a handle is still open.
518
- if (existsSync(path.join(prefix, "bin"))) {
519
- sleepSync(600)
520
- cleanupStaleReplacements(path.join(prefix, "bin"))
521
- }
522
-
523
- // On Windows the privileged mesh service holds its binaries open, so we can't
524
- // wipe or overwrite them with a plain copy. Detect that up front: if the
525
- // service is running, skip those files in the bin/ copy (the elevated `apply`
526
- // swaps them after stopping the service).
527
- const meshLocked =
528
- process.platform === "win32" &&
529
- meshServiceRunning(path.join(packageRoot, "bin"), "win32")
530
-
531
- // Copy package contents to install prefix
532
- const meshNames = meshBinarySet(process.platform)
533
- // "adapters" carries the bundled Claude Code ACP adapter + reclaude shim. Older
534
- // packages predate it, so its absence is tolerated (no warning) — bin/web/cli are
535
- // still required.
536
- const optionalDirs = new Set(["adapters"])
537
- for (const name of ["bin", "web", "cli", "adapters"]) {
538
- const src = path.join(packageRoot, name)
539
- const dst = path.join(prefix, name)
540
- if (!existsSync(src)) {
541
- if (!optionalDirs.has(name)) {
542
- console.error(` warning: package missing ${name}/ directory`)
543
- }
544
- continue
545
- }
546
- const copyOpts = { recursive: true, force: true, dereference: false }
547
- if (name === "bin") {
548
- // Never wipe bin/: that would drop the running mesh binaries (and, on Linux,
549
- // clear easytier-core's setcap capability). Overwrite in place, but SKIP a
550
- // mesh binary when the running service holds it open (Windows) OR it is
551
- // byte-identical to what's installed. The upshot: an update where the mesh
552
- // is unchanged leaves the privileged service entirely untouched, so it needs
553
- // no UAC/sudo. Changed mesh binaries and all non-mesh files are copied.
554
- copyOpts.filter = (s) => {
555
- const base = path.basename(s)
556
- if (!meshNames.includes(base)) return true
557
- if (meshLocked) return false
558
- const installed = path.join(dst, base)
559
- return !(existsSync(installed) && fileSha(s) === fileSha(installed))
560
- }
561
- // bin/ holds the long-lived executables (api, agent, ctl, easytier, mesh).
562
- // Copy each file with the lock-safe move-aside replace so a still-locked
563
- // binary from a not-fully-exited prior process never aborts the update with
564
- // EPERM (the bug that wedged update/restart).
565
- copyDirResilient(src, dst, copyOpts.filter)
566
- continue
567
- } else {
568
- removeKnownPath(dst, prefix)
569
- }
570
- await cp(src, dst, copyOpts)
571
- }
572
-
573
- // Ensure binaries are executable on Unix (cross-compiled from Windows they lose +x)
574
- const binDir = path.join(prefix, "bin")
575
- if (process.platform !== "win32" && existsSync(binDir)) {
576
- for (const entry of readdirSync(binDir)) {
577
- const binPath = path.join(binDir, entry)
578
- try { chmodSync(binPath, 0o755) } catch {}
579
- }
580
- }
581
-
582
- // Linux: grant easytier-core the TUN capability so the unprivileged runtime can
583
- // bring up the mesh virtual network. This is the one privileged step of the
584
- // install (a single sudo); the app itself stays unprivileged.
585
- ensureLinuxMeshCapabilities(binDir)
586
-
587
- // Windows/macOS: register (or lock-safely update) the privileged mesh service
588
- // that runs easytier-core. This is the one elevation of the install on those
589
- // platforms. Skipped for --no-start and gated to avoid prompting on every
590
- // source rebuild (see ensureMeshService).
591
- if (!options.noStart) {
592
- ensureMeshService(packageRoot, prefix, {
593
- sourceInstall: Boolean(options.sourceRoot),
594
- withMesh: Boolean(options.withMesh),
595
- skipMesh: Boolean(options.skipMesh),
596
- })
597
- }
598
-
599
- // Make the runtime start at login/boot (per-user, no elevation). Idempotent.
600
- if (!options.noStart && !options.skipAutostart) {
601
- enableRuntimeAutostart(prefix)
602
- }
603
-
604
- // Verify vantaloomctl binary exists before trying to run it
605
- const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
606
- if (!existsSync(ctlBin)) {
607
- const binContents = existsSync(binDir) ? readdirSync(binDir) : []
608
- const srcBinContents = existsSync(path.join(packageRoot, "bin")) ? readdirSync(path.join(packageRoot, "bin")) : []
609
- throw new Error(
610
- `vantaloomctl binary not found at ${ctlBin}\n` +
611
- ` installed bin/: [${binContents.join(", ")}]\n` +
612
- ` package bin/: [${srcBinContents.join(", ")}]\n` +
613
- ` platform: ${platformId()}\n` +
614
- ` This may indicate a corrupt download. Try again or install from source.`
615
- )
616
- }
617
-
618
- await writeLauncher(prefix)
619
- await writeText(path.join(prefix, "cli", "config.json"), `${JSON.stringify(mergedConfig, null, 2)}\n`)
620
- await writeText(path.join(prefix, "VERSION"), `${version}\n`)
621
- await cp(path.join(packageRoot, "manifest.json"), path.join(prefix, "manifest.json"), {
622
- force: true,
623
- })
624
-
625
- run(ctlBin, [
626
- "install",
627
- "--prefix",
628
- prefix,
629
- "--version",
630
- version,
631
- ])
632
-
633
- if (!options.noStart) {
634
- run(ctlBin, [
635
- "start",
636
- "--prefix",
637
- prefix,
638
- ])
639
- }
640
-
641
- return version
642
- }
643
-
644
- // ensureLinuxMeshCapabilities grants easytier-core the network capabilities it
645
- // needs to create a TUN device, so the unprivileged Vantaloom runtime can bring
646
- // up the EasyTier mesh without running as root. Re-run on every update because
647
- // replacing the binary clears file capabilities. Non-fatal: if it can't elevate
648
- // or setcap is missing, mesh just falls back to the Hub relay.
649
- function ensureLinuxMeshCapabilities(binDir) {
650
- if (process.platform !== "linux") {
651
- return
652
- }
653
- const core = path.join(binDir, "easytier-core")
654
- if (!existsSync(core)) {
655
- console.warn(" mesh: easytier-core not bundled; skipping TUN capability setup")
656
- return
657
- }
658
- const caps = "cap_net_admin,cap_net_bind_service+ep"
659
- // If the capability is already present (we kept an unchanged easytier-core in
660
- // place above), there's nothing to do — and crucially, no sudo prompt. setcap
661
- // only runs when the binary was actually replaced or never had the cap.
662
- const have = spawnSync("getcap", [core], { encoding: "utf8", windowsHide: true })
663
- if (
664
- !have.error &&
665
- typeof have.stdout === "string" &&
666
- have.stdout.includes("cap_net_admin") &&
667
- have.stdout.includes("cap_net_bind_service")
668
- ) {
669
- return
670
- }
671
- const isRoot = typeof process.getuid === "function" && process.getuid() === 0
672
- console.log(` mesh: granting TUN capability to easytier-core${isRoot ? "" : " (sudo)"} ...`)
673
- const result = isRoot
674
- ? spawnSync("setcap", [caps, core], { stdio: "inherit", windowsHide: true })
675
- : spawnSync("sudo", ["setcap", caps, core], { stdio: "inherit", windowsHide: true })
676
- if (result.error || result.status !== 0) {
677
- console.warn(
678
- " mesh: could not set capabilities (P2P will fall back to the Hub relay).\n" +
679
- ` enable it manually with: sudo setcap ${caps} ${core}\n` +
680
- " (needs the 'setcap' tool, e.g. apt install libcap2-bin)"
681
- )
682
- }
683
- }
684
-
685
- // ensureMeshService registers (or lock-safely updates) the privileged EasyTier
686
- // sidecar service on Windows/macOS — the one elevation of the install on those
687
- // platforms. Linux uses setcap instead (see ensureLinuxMeshCapabilities).
688
- //
689
- // To keep the source/dev rebuild loop frictionless it only elevates when the
690
- // work is actually needed (service missing, or the mesh binaries changed) and,
691
- // for source installs, defers to a printed one-liner unless --with-mesh is set.
692
- function ensureMeshService(packageRoot, prefix, opts) {
693
- const platform = process.platform
694
- if (platform !== "win32" && platform !== "darwin") return // Linux: setcap path
695
- if (opts.skipMesh) return
696
-
697
- const pkgBin = path.join(packageRoot, "bin")
698
- const meshExe = path.join(pkgBin, binaryName("vantaloom-mesh"))
699
- if (!existsSync(meshExe)) return // sidecar not bundled
700
-
701
- if (!meshServiceNeedsApply(pkgBin, path.join(prefix, "bin"), platform)) {
702
- return // installed binaries current and service already managing them
703
- }
704
-
705
- if (opts.sourceInstall && !opts.withMesh) {
706
- printMeshManualHint(prefix, platform)
707
- return
708
- }
709
-
710
- if (platform === "win32") {
711
- elevateWindowsMeshApply(pkgBin, prefix)
712
- } else {
713
- elevateDarwinMeshInstall(prefix)
714
- }
715
- }
716
-
717
- // meshServiceNeedsApply reports whether the privileged service has to be
718
- // (re)applied: true if the installed mesh binaries are missing/outdated, or if
719
- // the service isn't currently up.
720
- function meshServiceNeedsApply(pkgBin, installBin, platform) {
721
- // On Windows compare every lockable mesh file (incl. Packet.dll/WinDivert/wintun)
722
- // so a missing or changed support DLL also triggers a (re)apply.
723
- const names = platform === "win32"
724
- ? WINDOWS_MESH_BINARIES
725
- : [binaryName("vantaloom-mesh"), binaryName("easytier-core")]
726
- for (const name of names) {
727
- const installed = path.join(installBin, name)
728
- if (!existsSync(installed)) return true
729
- const staged = path.join(pkgBin, name)
730
- if (existsSync(staged) && fileSha(staged) !== fileSha(installed)) return true
731
- }
732
- return !meshServiceRunning(pkgBin, platform)
733
- }
734
-
735
- // meshServiceRunning queries the OS service state via the (unprivileged) mesh
736
- // `status` command. binDir is where to find the vantaloom-mesh binary to run.
737
- function meshServiceRunning(binDir, platform) {
738
- const exe = path.join(binDir, binaryName("vantaloom-mesh"))
739
- if (!existsSync(exe)) return false
740
- const r = spawnSync(exe, ["status"], { encoding: "utf8", windowsHide: true })
741
- if (r.error || typeof r.stdout !== "string") return false
742
- const out = r.stdout.toLowerCase()
743
- if (platform === "win32") return out.includes("running")
744
- return r.status === 0 && !out.includes("not loaded") && !out.includes("not installed")
745
- }
746
-
747
- function fileSha(file) {
748
- return createHash("sha256").update(readFileSync(file)).digest("hex")
749
- }
750
-
751
- // elevateWindowsMeshApply runs the lock-safe `apply` elevated (one UAC prompt).
752
- // It launches from the package copy so it can overwrite the installed binary.
753
- function elevateWindowsMeshApply(pkgBin, prefix) {
754
- const exe = path.join(pkgBin, "vantaloom-mesh.exe")
755
- console.log(" mesh: registering privileged P2P service (UAC prompt) ...")
756
- const argList = ["apply", "--pkg-bin", pkgBin, "--install-dir", prefix].map(psQuote).join(",")
757
- const ps = `$ErrorActionPreference='Stop'; $p = Start-Process -FilePath ${psQuote(exe)} -ArgumentList ${argList} -Verb RunAs -Wait -PassThru; exit $p.ExitCode`
758
- const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "inherit", windowsHide: true })
759
- if (r.error || r.status !== 0) {
760
- const installed = path.join(prefix, "bin", "vantaloom-mesh.exe")
761
- console.warn(" mesh: service registration was cancelled or failed; P2P falls back to the Hub relay.")
762
- console.warn(` enable it later (as Administrator): "${installed}" install --install-dir "${prefix}"`)
763
- } else {
764
- console.log(" mesh: privileged P2P service active")
765
- }
766
- }
767
-
768
- // elevateDarwinMeshInstall registers the LaunchDaemon via sudo (one prompt). The
769
- // runtime already copied the fresh binary into place; install reloads the daemon.
770
- function elevateDarwinMeshInstall(prefix) {
771
- const exe = path.join(prefix, "bin", "vantaloom-mesh")
772
- console.log(" mesh: registering privileged P2P service (sudo) ...")
773
- const r = spawnSync("sudo", [exe, "install", "--install-dir", prefix], { stdio: "inherit", windowsHide: true })
774
- if (r.error || r.status !== 0) {
775
- console.warn(" mesh: LaunchDaemon registration failed; P2P falls back to the Hub relay.")
776
- console.warn(` enable it later: sudo "${exe}" install --install-dir "${prefix}"`)
777
- } else {
778
- console.log(" mesh: privileged P2P service active")
779
- }
780
- }
781
-
782
- function printMeshManualHint(prefix, platform) {
783
- console.log(" mesh: P2P service not auto-registered for source installs.")
784
- if (platform === "win32") {
785
- const exe = path.join(prefix, "bin", "vantaloom-mesh.exe")
786
- console.log(` enable it once (as Administrator): "${exe}" install --install-dir "${prefix}"`)
787
- console.log(" or re-run install/update with --with-mesh")
788
- } else {
789
- const exe = path.join(prefix, "bin", "vantaloom-mesh")
790
- console.log(` enable it once: sudo "${exe}" install --install-dir "${prefix}"`)
791
- console.log(" or re-run install/update with --with-mesh")
792
- }
793
- }
794
-
795
- // psQuote wraps a value as a PowerShell single-quoted string literal.
796
- function psQuote(value) {
797
- return "'" + String(value).replace(/'/g, "''") + "'"
798
- }
799
-
800
- // uninstallRuntime tears down a local install: stop the runtime, remove the
801
- // privileged mesh service (elevated), then delete the install directory.
802
- async function uninstallRuntime(options) {
803
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
804
- if (!existsSync(prefix)) {
805
- console.log(`Vantaloom is not installed at ${prefix}`)
806
- return
807
- }
808
- console.log(`Uninstalling Vantaloom from ${prefix} ...`)
809
-
810
- // 1. Stop the runtime (api/agent/web/tray). Best-effort.
811
- const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
812
- if (existsSync(ctlBin)) {
813
- spawnSync(ctlBin, ["stop", "--prefix", prefix], { stdio: "inherit", windowsHide: true })
814
- }
815
- killTrayProcess(prefix)
816
-
817
- // 2. Remove the privileged mesh service (releases TUN adapter + file locks).
818
- removeMeshService(prefix, options)
819
-
820
- // 2b. Remove the login-autostart entry (lives outside the install dir).
821
- disableRuntimeAutostart()
822
-
823
- // 3. Delete the install directory.
824
- try {
825
- const parent = path.dirname(prefix)
826
- removeKnownPath(prefix, parent)
827
- console.log(`removed ${prefix}`)
828
- } catch (error) {
829
- console.warn(` could not remove ${prefix}: ${error.message}`)
830
- console.warn(" some files may still be locked; re-run after closing Vantaloom processes.")
831
- }
832
-
833
- console.log("Vantaloom uninstalled.")
834
- console.log("note: the install dir was left out of PATH edits; remove the bin/ entry from your shell profile if you added it.")
835
- }
836
-
837
- function removeMeshService(prefix, options) {
838
- const platform = process.platform
839
- if (platform !== "win32" && platform !== "darwin") return // Linux: nothing registered
840
- const exe = path.join(prefix, "bin", binaryName("vantaloom-mesh"))
841
- if (!existsSync(exe)) return
842
- if (options.skipMesh) return
843
-
844
- if (platform === "win32") {
845
- if (!meshServiceRunningOrInstalled(path.join(prefix, "bin"))) return
846
- console.log(" mesh: removing privileged P2P service (UAC prompt) ...")
847
- const ps = `$ErrorActionPreference='Stop'; $p = Start-Process -FilePath ${psQuote(exe)} -ArgumentList 'uninstall' -Verb RunAs -Wait -PassThru; exit $p.ExitCode`
848
- const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "inherit", windowsHide: true })
849
- if (r.error || r.status !== 0) {
850
- console.warn(` mesh: could not remove service; run manually (as Administrator): "${exe}" uninstall`)
851
- }
852
- } else {
853
- console.log(" mesh: removing privileged P2P service (sudo) ...")
854
- const r = spawnSync("sudo", [exe, "uninstall"], { stdio: "inherit", windowsHide: true })
855
- if (r.error || r.status !== 0) {
856
- console.warn(` mesh: could not remove service; run manually: sudo "${exe}" uninstall`)
857
- }
858
- }
859
- }
860
-
861
- // meshServiceRunningOrInstalled reports whether the Windows service exists at
862
- // all (running or stopped), so uninstall only prompts for elevation when there
863
- // is actually something to remove.
864
- function meshServiceRunningOrInstalled(binDir) {
865
- const exe = path.join(binDir, "vantaloom-mesh.exe")
866
- if (!existsSync(exe)) return false
867
- const r = spawnSync(exe, ["status"], { encoding: "utf8", windowsHide: true })
868
- if (r.error || typeof r.stdout !== "string") return false
869
- return !r.stdout.toLowerCase().includes("not installed")
870
- }
871
-
872
- // ── Runtime login-autostart (no elevation) ──
873
- // Start the local runtime (api/agent/web/tray) at login/boot using only
874
- // per-user mechanisms — no UAC/sudo. This is separate from the privileged mesh
875
- // service: the mesh sidecar autostarts via the OS service manager; this brings
876
- // up the unprivileged runtime that joins the mesh and serves the local API.
877
-
878
- const AUTOSTART_LABEL = "online.timefiles.vantaloom.runtime"
879
- // PowerShell HKCU: drive path. Set-ItemProperty handles values with spaces and
880
- // embedded quotes cleanly (reg.exe's /d escaping is brittle), and the Run key is
881
- // not blocked by Controlled Folder Access the way the Startup folder is.
882
- const WINDOWS_RUN_KEY = "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
883
- const WINDOWS_RUN_VALUE = "Vantaloom"
884
-
885
- function enableRuntimeAutostart(prefix) {
886
- try {
887
- if (process.platform === "win32") {
888
- // A .vbs launches the runtime with a hidden window (no console flash); the
889
- // HKCU Run key runs it at login without elevation.
890
- const launcher = path.join(prefix, "vantaloom.cmd")
891
- const vbsPath = path.join(prefix, "autostart.vbs")
892
- const vbs = `' Vantaloom runtime autostart (hidden)\r\nCreateObject("WScript.Shell").Run """${launcher}"" start", 0, False\r\n`
893
- writeFileSync(vbsPath, vbs)
894
- const result = spawnSync(
895
- "powershell",
896
- [
897
- "-NoProfile",
898
- "-NonInteractive",
899
- "-Command",
900
- `Set-ItemProperty -Path '${WINDOWS_RUN_KEY}' -Name '${WINDOWS_RUN_VALUE}' -Value 'wscript.exe "${vbsPath}"'`,
901
- ],
902
- { stdio: "ignore", windowsHide: true }
903
- )
904
- if (result.error || result.status !== 0) {
905
- console.warn(" autostart: could not register login entry (HKCU Run)")
906
- } else {
907
- console.log(" autostart: enabled (login Run key)")
908
- }
909
- return
910
- }
911
- if (process.platform === "darwin") {
912
- const launcher = path.join(prefix, "vantaloom")
913
- const dir = path.join(os.homedir(), "Library", "LaunchAgents")
914
- mkdirSync(dir, { recursive: true })
915
- const plistPath = path.join(dir, `${AUTOSTART_LABEL}.plist`)
916
- // RunAtLoad only (no KeepAlive): `vantaloom start` spawns detached and exits.
917
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
918
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
919
- <plist version="1.0"><dict>
920
- <key>Label</key><string>${AUTOSTART_LABEL}</string>
921
- <key>ProgramArguments</key><array><string>${launcher}</string><string>start</string></array>
922
- <key>RunAtLoad</key><true/>
923
- </dict></plist>
924
- `
925
- writeFileSync(plistPath, plist)
926
- spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore", windowsHide: true })
927
- spawnSync("launchctl", ["load", "-w", plistPath], { stdio: "ignore", windowsHide: true })
928
- console.log(" autostart: enabled (LaunchAgent)")
929
- return
930
- }
931
- // Linux: user systemd unit + linger (no root).
932
- const launcher = path.join(prefix, "vantaloom")
933
- const dir = path.join(os.homedir(), ".config", "systemd", "user")
934
- mkdirSync(dir, { recursive: true })
935
- const unit = `[Unit]
936
- Description=Vantaloom local runtime
937
- After=network-online.target
938
- Wants=network-online.target
939
-
940
- [Service]
941
- Type=oneshot
942
- RemainAfterExit=yes
943
- ExecStart=${launcher} start
944
- ExecStop=${launcher} stop
945
-
946
- [Install]
947
- WantedBy=default.target
948
- `
949
- writeFileSync(path.join(dir, "vantaloom-runtime.service"), unit)
950
- spawnSync("loginctl", ["enable-linger"], { stdio: "ignore", windowsHide: true })
951
- spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore", windowsHide: true })
952
- spawnSync("systemctl", ["--user", "enable", "vantaloom-runtime.service"], { stdio: "ignore", windowsHide: true })
953
- console.log(" autostart: enabled (systemd user unit)")
954
- } catch (error) {
955
- console.warn(` autostart: could not enable (${error.message})`)
956
- }
957
- }
958
-
959
- function disableRuntimeAutostart() {
960
- try {
961
- if (process.platform === "win32") {
962
- // Remove the Run-key entry; the autostart.vbs lives in the install dir and
963
- // is deleted with it.
964
- spawnSync(
965
- "powershell",
966
- ["-NoProfile", "-NonInteractive", "-Command", `Remove-ItemProperty -Path '${WINDOWS_RUN_KEY}' -Name '${WINDOWS_RUN_VALUE}' -ErrorAction SilentlyContinue`],
967
- { stdio: "ignore", windowsHide: true }
968
- )
969
- return
970
- }
971
- if (process.platform === "darwin") {
972
- const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`)
973
- if (existsSync(plistPath)) {
974
- spawnSync("launchctl", ["unload", "-w", plistPath], { stdio: "ignore", windowsHide: true })
975
- rmSync(plistPath, { force: true })
976
- }
977
- return
978
- }
979
- const unit = path.join(os.homedir(), ".config", "systemd", "user", "vantaloom-runtime.service")
980
- if (existsSync(unit)) {
981
- spawnSync("systemctl", ["--user", "disable", "vantaloom-runtime.service"], { stdio: "ignore", windowsHide: true })
982
- rmSync(unit, { force: true })
983
- spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore", windowsHide: true })
984
- }
985
- } catch (error) {
986
- console.warn(` autostart: could not disable (${error.message})`)
987
- }
988
- }
989
-
990
- async function syncFromNpmRegistry(options, action) {
991
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
992
- const installedConfig = readInstalledConfig(prefix)
993
- // Always derive runtimePackage from current platform — never trust stale config
994
- // from a different platform (e.g. win32 config baked into a darwin package).
995
- // Only explicit --runtime-package flag can override.
996
- const runtimePackage = options.runtimePackage || runtimePackageName(platformId())
997
- const runtimeVersion = options.runtimeVersion || installedConfig.runtimeVersion || "latest"
998
- const explicitRegistry = options.npmRegistry || installedConfig.npmRegistry
999
- const registry = normalizeRegistry(explicitRegistry || detectNpmRegistry() || defaultNpmRegistry)
1000
- const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-npm-"))
1001
-
1002
- try {
1003
- const extractRoot = path.join(tempRoot, "extract")
1004
- const archive = path.join(tempRoot, `${packageBasename(runtimePackage)}-${runtimeVersion}.tgz`)
1005
- mkdirSync(extractRoot, { recursive: true })
1006
-
1007
- const resolved = await resolveNpmPackageWithFallback({
1008
- registries: explicitRegistry ? [registry] : [registry, ...fallbackNpmRegistries.map(normalizeRegistry)],
1009
- name: runtimePackage,
1010
- version: runtimeVersion,
1011
- })
1012
- await downloadNpmTarball({
1013
- tarballUrl: resolved.tarball,
1014
- target: archive,
1015
- packageName: runtimePackage,
1016
- version: resolved.version,
1017
- })
1018
- run("tar", ["-xzf", archive, "-C", extractRoot])
1019
-
1020
- const packageRoot = findExtractedNpmPackage(extractRoot)
1021
- const version = await applyPackage(packageRoot, prefix, {
1022
- noStart: options.noStart,
1023
- runtimePackage,
1024
- runtimeVersion: options.runtimeVersion ? resolved.version : "latest",
1025
- npmRegistry: resolved.registry,
1026
- update: action === "update",
1027
- withMesh: options.withMesh,
1028
- skipMesh: options.skipMesh,
1029
- })
1030
-
1031
- console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
1032
- console.log(`version: ${version}`)
1033
- console.log(`source: ${runtimePackage}@${resolved.version}`)
1034
- console.log(`registry: ${resolved.registry}`)
1035
- ensureInPath(prefix)
1036
- console.log(`run: ${displayCommand(prefix)} status`)
1037
- } finally {
1038
- removeKnownPath(tempRoot, os.tmpdir())
1039
- }
1040
- }
1041
-
1042
- async function syncFromRelease(options, action) {
1043
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
1044
- const installedConfig = readInstalledConfig(prefix)
1045
- const sourceRoot = installedConfig.sourceRoot
1046
- ? tryAssertSourceRoot(installedConfig.sourceRoot)
1047
- : tryFindSourceRoot()
1048
- const sourceRemote = sourceRoot ? gitRemoteUrl(sourceRoot) : ""
1049
- const repo = options.repo || installedConfig.repo || inferGitHubRepo(options.remote ?? installedConfig.remote ?? sourceRemote) || defaultRepo
1050
- const releaseTag = options.releaseTag || installedConfig.releaseTag || defaultReleaseTag
1051
- const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-update-"))
1052
-
1053
- try {
1054
- const extractRoot = path.join(tempRoot, "extract")
1055
- const archive = path.join(tempRoot, `vantaloom-${platformId()}.tar.gz`)
1056
- mkdirSync(extractRoot, { recursive: true })
1057
-
1058
- await downloadReleaseAsset({
1059
- repo,
1060
- releaseTag,
1061
- assetName: `vantaloom-${platformId()}.tar.gz`,
1062
- target: archive,
1063
- token: options.githubToken,
1064
- })
1065
- run("tar", ["-xzf", archive, "-C", extractRoot])
1066
-
1067
- const packageRoot = findExtractedPackage(extractRoot, platformId())
1068
- const version = await applyPackage(packageRoot, prefix, {
1069
- noStart: options.noStart,
1070
- sourceRoot: installedConfig.sourceRoot,
1071
- update: action === "update",
1072
- withMesh: options.withMesh,
1073
- skipMesh: options.skipMesh,
1074
- })
1075
-
1076
- console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
1077
- console.log(`version: ${version}`)
1078
- console.log(`source: ${repo}@${releaseTag}`)
1079
- ensureInPath(prefix)
1080
- console.log(`run: ${displayCommand(prefix)} status`)
1081
- } finally {
1082
- removeKnownPath(tempRoot, os.tmpdir())
1083
- }
1084
- }
1085
-
1086
- async function downloadReleaseAsset({ repo, releaseTag, assetName, target, token }) {
1087
- const releaseUrl = `https://api.github.com/repos/${repo}/releases/tags/${releaseTag}`
1088
- const headers = {
1089
- "User-Agent": "vantaloom-cli",
1090
- }
1091
- const githubToken = token || process.env.VANTALOOM_GITHUB_TOKEN || process.env.GITHUB_TOKEN
1092
- if (githubToken) {
1093
- headers.Authorization = `Bearer ${githubToken}`
1094
- }
1095
-
1096
- const releaseResponse = await fetch(releaseUrl, { headers })
1097
- if (!releaseResponse.ok) {
1098
- const detail = await releaseResponse.text().catch(() => "")
1099
- const authHint = releaseResponse.status === 404 || releaseResponse.status === 403
1100
- ? " If the repository is private, set VANTALOOM_GITHUB_TOKEN to a GitHub token that can read releases."
1101
- : ""
1102
- throw new Error(`failed to inspect ${repo}@${releaseTag}: HTTP ${releaseResponse.status}.${authHint}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
1103
- }
1104
-
1105
- const release = await releaseResponse.json()
1106
- const asset = release.assets?.find((asset) => asset.name === assetName)
1107
- if (!asset) {
1108
- const names = release.assets?.map((asset) => asset.name).join(", ") || "none"
1109
- throw new Error(`missing release asset ${assetName} in ${repo}@${releaseTag}; available assets: ${names}`)
1110
- }
1111
-
1112
- const response = await fetch(asset.url, {
1113
- headers: {
1114
- ...headers,
1115
- Accept: "application/octet-stream",
1116
- },
1117
- })
1118
- if (!response.ok) {
1119
- const detail = await response.text().catch(() => "")
1120
- const authHint = response.status === 404 || response.status === 403
1121
- ? " If the repository is private, set VANTALOOM_GITHUB_TOKEN to a GitHub token that can read releases."
1122
- : ""
1123
- throw new Error(`failed to download ${assetName} from ${repo}@${releaseTag}: HTTP ${response.status}.${authHint}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
1124
- }
1125
-
1126
- const buffer = Buffer.from(await response.arrayBuffer())
1127
- await writeFile(target, buffer)
1128
- }
1129
-
1130
- function detectNpmRegistry() {
1131
- // 1. NPM_CONFIG_REGISTRY env var (highest priority, set by npm/npx when running)
1132
- if (process.env.NPM_CONFIG_REGISTRY) {
1133
- return process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY
1134
- }
1135
- // npm also sets the lowercase variant
1136
- if (process.env.npm_config_registry) {
1137
- return process.env.npm_config_registry
1138
- }
1139
-
1140
- // 2. Read user .npmrc
1141
- try {
1142
- const npmrcPaths = [
1143
- path.join(os.homedir(), ".npmrc"),
1144
- ]
1145
- // Also check project-level .npmrc
1146
- const localNpmrc = path.resolve(".npmrc")
1147
- if (localNpmrc !== npmrcPaths[0]) {
1148
- npmrcPaths.unshift(localNpmrc)
1149
- }
1150
- for (const npmrcPath of npmrcPaths) {
1151
- if (existsSync(npmrcPath)) {
1152
- const content = readFileSync(npmrcPath, "utf8")
1153
- const match = content.match(/^\s*registry\s*=\s*(.+)/m)
1154
- if (match) {
1155
- return match[1].trim()
1156
- }
1157
- }
1158
- }
1159
- } catch {
1160
- // Ignore .npmrc read errors
1161
- }
1162
-
1163
- return ""
1164
- }
1165
-
1166
- async function resolveNpmPackageWithFallback({ registries, name, version }) {
1167
- const errors = []
1168
- for (const registry of registries) {
1169
- try {
1170
- const result = await resolveNpmPackage({ registry, name, version })
1171
- return { ...result, registry }
1172
- } catch (error) {
1173
- const causeCode = error?.cause?.code || ""
1174
- const isNetworkError = causeCode === "ECONNREFUSED"
1175
- || causeCode === "ENOTFOUND"
1176
- || causeCode === "ETIMEDOUT"
1177
- || causeCode === "ECONNRESET"
1178
- || causeCode === "UND_ERR_CONNECT_TIMEOUT"
1179
- || (error instanceof TypeError && error.message === "fetch failed")
1180
- const isSslError = causeCode === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
1181
- || causeCode === "CERT_HAS_EXPIRED"
1182
- || causeCode === "DEPTH_ZERO_SELF_SIGNED_CERT"
1183
- || causeCode === "SELF_SIGNED_CERT_IN_CHAIN"
1184
- || causeCode === "ERR_TLS_CERT_ALTNAME_INVALID"
1185
- const isRetryable = isNetworkError || isSslError
1186
- errors.push({ registry, error, isRetryable, isSslError })
1187
-
1188
- if (isRetryable && registries.length > 1) {
1189
- const hint = isSslError ? " (SSL certificate error)" : ""
1190
- console.error(`vantaloom: ${registry} unreachable${hint}, trying next registry...`)
1191
- continue
1192
- }
1193
- if (isSslError) {
1194
- throw new Error(
1195
- `SSL certificate error connecting to ${registry}: ${causeCode}\n` +
1196
- ` Fix: run with --no-strict-ssl, or set npm config: npm config set strict-ssl false`
1197
- )
1198
- }
1199
- // Non-network error (404, parse error, etc.) — don't try fallbacks
1200
- throw error
1201
- }
1202
- }
1203
-
1204
- // All registries failed
1205
- const hasSslError = errors.some((e) => e.isSslError)
1206
- const tried = errors.map((e) => e.registry).join(", ")
1207
- const sslHint = hasSslError
1208
- ? `\n SSL fix: run with --no-strict-ssl, or set npm config: npm config set strict-ssl false`
1209
- : ""
1210
- throw new Error(
1211
- `all registries unreachable (tried: ${tried}). ` +
1212
- `Check your network or specify --npm-registry <url>${sslHint}`
1213
- )
1214
- }
1215
-
1216
- async function resolveNpmPackage({ registry, name, version }) {
1217
- const metadataUrl = `${registry}/${encodeURIComponent(name).replace("%2F", "%2f")}`
1218
- let response
1219
- try {
1220
- response = await fetch(metadataUrl, {
1221
- headers: {
1222
- Accept: "application/vnd.npm.install-v1+json",
1223
- "User-Agent": "vantaloom-cli",
1224
- },
1225
- signal: AbortSignal.timeout(15000),
1226
- })
1227
- } catch (error) {
1228
- if (error instanceof TypeError && error.message === "fetch failed") {
1229
- const causeCode = error.cause?.code || ""
1230
- const causeMsg = causeCode || error.cause?.message || String(error.cause || "")
1231
- throw Object.assign(
1232
- new Error(`cannot reach registry ${registry} (${causeMsg})`),
1233
- { cause: error.cause }
1234
- )
1235
- }
1236
- throw error
1237
- }
1238
- if (!response.ok) {
1239
- const detail = await response.text().catch(() => "")
1240
- throw new Error(`failed to inspect npm package ${name}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
1241
- }
1242
-
1243
- const metadata = await response.json()
1244
- const selectedVersion = metadata["dist-tags"]?.[version] ?? version
1245
- const packageVersion = metadata.versions?.[selectedVersion]
1246
- if (!packageVersion?.dist?.tarball) {
1247
- const available = Object.keys(metadata.versions ?? {}).slice(-8).join(", ") || "none"
1248
- throw new Error(`missing npm package ${name}@${version}; available versions: ${available}`)
1249
- }
1250
- return {
1251
- version: selectedVersion,
1252
- tarball: packageVersion.dist.tarball,
1253
- }
1254
- }
1255
-
1256
- async function downloadNpmTarball({ tarballUrl, target, packageName, version }) {
1257
- console.log(` downloading ${packageName}@${version}...`)
1258
- let response
1259
- try {
1260
- response = await fetch(tarballUrl, {
1261
- headers: {
1262
- "User-Agent": "vantaloom-cli",
1263
- },
1264
- signal: AbortSignal.timeout(120000),
1265
- })
1266
- } catch (error) {
1267
- const causeCode = error?.cause?.code || ""
1268
- const causeMsg = causeCode || error?.cause?.message || error.message || ""
1269
- const isSsl = causeCode.includes("CERT") || causeCode === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
1270
- const hint = isSsl ? `\n Fix: run with --no-strict-ssl` : ""
1271
- throw new Error(`failed to download ${packageName}@${version} from ${tarballUrl}: ${causeMsg}${hint}`)
1272
- }
1273
- if (!response.ok) {
1274
- const detail = await response.text().catch(() => "")
1275
- throw new Error(`failed to download ${packageName}@${version}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
1276
- }
1277
-
1278
- const buffer = Buffer.from(await response.arrayBuffer())
1279
- await writeFile(target, buffer)
1280
- console.log(` downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`)
1281
- }
1282
-
1283
- function shouldUseSourceInstall(options) {
1284
- return Boolean(options.local || options.source || options.buildWeb)
1285
- }
1286
-
1287
- function shouldUseReleaseSync(options) {
1288
- return Boolean(options.repo || options.remote || options.releaseTag || options.githubToken)
1289
- }
1290
-
1291
- function assertRuntimePackage(packageRoot) {
1292
- for (const name of ["bin", "web", "cli", "manifest.json"]) {
1293
- if (!existsSync(path.join(packageRoot, name))) {
1294
- throw new Error(`invalid Vantaloom package, missing ${name}: ${packageRoot}`)
1295
- }
1296
- }
1297
- }
1298
-
1299
- function readVersion(packageRoot) {
1300
- const versionPath = path.join(packageRoot, "VERSION")
1301
- if (existsSync(versionPath)) {
1302
- return readFileSync(versionPath, "utf8").trim() || "dev"
1303
- }
1304
- const manifest = readJSONIfExists(path.join(packageRoot, "manifest.json"))
1305
- return manifest.version ?? "dev"
1306
- }
1307
-
1308
- function readInstalledConfig(prefix) {
1309
- return readJSONIfExists(path.join(prefix, "cli", "config.json"))
1310
- }
1311
-
1312
- function readJSONIfExists(filePath) {
1313
- if (!existsSync(filePath)) {
1314
- return {}
1315
- }
1316
- return JSON.parse(readFileSync(filePath, "utf8"))
1317
- }
1318
-
1319
- function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
1320
- const merged = { ...packageConfig }
1321
- for (const key of ["sourceRoot", "remote", "repo", "releaseTag", "runtimePackage", "runtimeVersion", "npmRegistry"]) {
1322
- if (!merged[key] && existingConfig[key]) {
1323
- merged[key] = existingConfig[key]
1324
- }
1325
- }
1326
- if (overrides.sourceRoot) {
1327
- merged.sourceRoot = overrides.sourceRoot
1328
- }
1329
- if (overrides.runtimePackage) {
1330
- merged.runtimePackage = overrides.runtimePackage
1331
- }
1332
- if (overrides.runtimeVersion) {
1333
- merged.runtimeVersion = overrides.runtimeVersion
1334
- }
1335
- if (overrides.npmRegistry) {
1336
- merged.npmRegistry = overrides.npmRegistry
1337
- }
1338
- if (!merged.repo) {
1339
- merged.repo = defaultRepo
1340
- }
1341
- if (!merged.releaseTag) {
1342
- merged.releaseTag = defaultReleaseTag
1343
- }
1344
- // Always force runtimePackage to match the running platform — a cross-compiled
1345
- // package may carry a config for a different platform (e.g. win32 inside darwin).
1346
- merged.runtimePackage = runtimePackageName(platformId())
1347
- if (!merged.runtimeVersion) {
1348
- merged.runtimeVersion = "latest"
1349
- }
1350
- if (!merged.npmRegistry) {
1351
- merged.npmRegistry = defaultNpmRegistry
1352
- }
1353
- return merged
1354
- }
1355
-
1356
- function runtimeConfigFromSource(sourceRoot) {
1357
- const remote = gitRemoteUrl(sourceRoot)
1358
- const repo = inferGitHubRepo(remote) || defaultRepo
1359
- return {
1360
- ...(process.env.GITHUB_ACTIONS ? {} : { sourceRoot }),
1361
- ...(remote ? { remote } : {}),
1362
- repo,
1363
- releaseTag: defaultReleaseTag,
1364
- runtimePackage: runtimePackageName(platformId()),
1365
- runtimeVersion: "latest",
1366
- npmRegistry: detectNpmRegistry() || defaultNpmRegistry,
1367
- }
1368
- }
1369
-
1370
- function findExtractedPackage(extractRoot, platform) {
1371
- const expected = path.join(extractRoot, `vantaloom-${platform}`)
1372
- if (existsSync(expected)) {
1373
- return expected
1374
- }
1375
-
1376
- const directories = readdirSync(extractRoot, { withFileTypes: true })
1377
- .filter((entry) => entry.isDirectory())
1378
- .map((entry) => path.join(extractRoot, entry.name))
1379
- if (directories.length === 1) {
1380
- return directories[0]
1381
- }
1382
-
1383
- throw new Error(`could not find extracted Vantaloom package in ${extractRoot}`)
1384
- }
1385
-
1386
- function findExtractedNpmPackage(extractRoot) {
1387
- const expected = path.join(extractRoot, "package")
1388
- if (existsSync(expected)) {
1389
- return expected
1390
- }
1391
-
1392
- const directories = readdirSync(extractRoot, { withFileTypes: true })
1393
- .filter((entry) => entry.isDirectory())
1394
- .map((entry) => path.join(extractRoot, entry.name))
1395
- if (directories.length === 1) {
1396
- return directories[0]
1397
- }
1398
-
1399
- throw new Error(`could not find extracted npm package in ${extractRoot}`)
1400
- }
1401
-
1402
- function tryFindSourceRoot() {
1403
- try {
1404
- return findSourceRoot()
1405
- } catch {
1406
- return ""
1407
- }
1408
- }
1409
-
1410
- function tryAssertSourceRoot(sourceRoot) {
1411
- try {
1412
- return assertSourceRoot(sourceRoot)
1413
- } catch {
1414
- return ""
1415
- }
1416
- }
1417
-
1418
- function gitRemoteUrl(sourceRoot) {
1419
- const result = spawnSync("git", ["remote", "get-url", "origin"], {
1420
- cwd: sourceRoot,
1421
- encoding: "utf8",
1422
- windowsHide: true,
1423
- })
1424
- if (result.status === 0) {
1425
- return result.stdout.trim()
1426
- }
1427
- return ""
1428
- }
1429
-
1430
- function inferGitHubRepo(remote) {
1431
- if (!remote) {
1432
- return ""
1433
- }
1434
- const normalized = remote.replace(/\.git$/, "")
1435
- const httpsMatch = normalized.match(/github\.com[:/]([^/]+\/[^/]+)$/)
1436
- if (httpsMatch) {
1437
- return httpsMatch[1]
1438
- }
1439
- const sshMatch = normalized.match(/^[^:]+:([^/]+\/[^/]+)$/)
1440
- return sshMatch?.[1] ?? ""
1441
- }
1442
-
1443
- async function copyStaticWeb(sourceRoot, buildWeb) {
1444
- const exportRoot = path.join(sourceRoot, "apps", "vantaloom", "out")
1445
- if (!existsSync(exportRoot)) {
1446
- throw new Error(
1447
- "missing Next static export output; let GitHub CI run production build, or pass --build-web for a local one-off build"
1448
- )
1449
- }
1450
-
1451
- removeKnownPath(buildWeb, path.dirname(buildWeb))
1452
- await copyDir(exportRoot, buildWeb)
1453
- }
1454
-
1455
- async function copyCliDirectory(target, sourceRoot, config) {
1456
- const sourceCliRoot = path.join(sourceRoot, "packages", "cli")
1457
- if (!existsSync(path.join(sourceCliRoot, "bin", "vantaloom.mjs"))) {
1458
- throw new Error(`missing source CLI package: ${sourceCliRoot}`)
1459
- }
1460
- removeKnownPath(target, path.dirname(target))
1461
- mkdirSync(target, { recursive: true })
1462
- await copyDir(sourceCliRoot, target)
1463
- await writeText(
1464
- path.join(target, "config.json"),
1465
- `${JSON.stringify(config, null, 2)}\n`
1466
- )
1467
- }
1468
-
1469
- async function writeLauncher(prefix) {
1470
- if (process.platform === "win32") {
1471
- await writeText(
1472
- path.join(prefix, "vantaloom.cmd"),
1473
- `@echo off\r\nnode "%~dp0cli\\bin\\vantaloom.mjs" %*\r\n`
1474
- )
1475
- } else {
1476
- const launcher = `#!/usr/bin/env sh\nexec node "$(dirname "$0")/cli/bin/vantaloom.mjs" "$@"\n`
1477
- const launcherPath = path.join(prefix, "vantaloom")
1478
- await writeText(launcherPath, launcher)
1479
- chmodSync(launcherPath, 0o755)
1480
- }
1481
- }
1482
-
1483
- function runCtl(command, options) {
1484
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
1485
- const ctl = path.join(prefix, "bin", binaryName("vantaloomctl"))
1486
- if (!existsSync(ctl)) {
1487
- throw new Error(`missing installed runtime at ${prefix}; run "vantaloom install" first`)
1488
- }
1489
-
1490
- const args = [command, "--prefix", prefix]
1491
- if (options.component) {
1492
- args.push("--component", options.component)
1493
- }
1494
- run(ctl, args)
1495
- }
1496
-
1497
- function printPaths(options) {
1498
- const sourceRoot = findSourceRoot(options.source)
1499
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
1500
- console.log(JSON.stringify({ sourceRoot, prefix }, null, 2))
1501
- }
1502
-
1503
- // ── Hub login (headless / no-web) ──
1504
- // Authenticates to the Hub entirely from the command line, so a machine that
1505
- // never opens the web UI (e.g. a server with no public access) can still join
1506
- // the workgroup. Reuses the exact endpoints the web app uses, then tells the
1507
- // local runtime to connect (which persists the Hub config to local.json).
1508
-
1509
- const DEFAULT_HUB_URL = "http://vanta.timefiles.online"
1510
-
1511
- function localApiBase(prefix) {
1512
- let port
1513
- try {
1514
- port = readFileSync(path.join(prefix, "runtime", "api.port"), "utf8").trim()
1515
- } catch {}
1516
- if (!port) {
1517
- const cfg = readJSONIfExists(path.join(prefix, "config", "local.json"))
1518
- port = cfg.apiPort ? String(cfg.apiPort) : "8780"
1519
- }
1520
- return `http://127.0.0.1:${port}`
1521
- }
1522
-
1523
- function localMachineInfo() {
1524
- const platMap = { win32: "windows", darwin: "darwin", linux: "linux" }
1525
- const archMap = { x64: "amd64", arm64: "arm64" }
1526
- return {
1527
- name: `${platMap[process.platform] ?? process.platform}-${os.hostname() || "unknown"}`,
1528
- platform: platMap[process.platform] ?? process.platform,
1529
- arch: archMap[process.arch] ?? process.arch,
1530
- localIp: "127.0.0.1",
1531
- }
1532
- }
1533
-
1534
- async function hubFetch(url, { method = "GET", body, token } = {}) {
1535
- const headers = { "Content-Type": "application/json" }
1536
- if (token) headers.Authorization = `Bearer ${token}`
1537
- const res = await fetch(url, {
1538
- method,
1539
- headers,
1540
- body: body ? JSON.stringify(body) : undefined,
1541
- })
1542
- const data = await res.json().catch(() => ({}))
1543
- if (!res.ok) {
1544
- throw new Error(data.error || `${url}: HTTP ${res.status}`)
1545
- }
1546
- return data
1547
- }
1548
-
1549
- function promptVisible(query) {
1550
- return new Promise((resolve) => {
1551
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
1552
- rl.question(query, (answer) => {
1553
- rl.close()
1554
- resolve(answer.trim())
1555
- })
1556
- })
1557
- }
1558
-
1559
- function promptHidden(query) {
1560
- return new Promise((resolve) => {
1561
- const rl = readline.createInterface({
1562
- input: process.stdin,
1563
- output: process.stdout,
1564
- terminal: true,
1565
- })
1566
- process.stdout.write(query)
1567
- // Suppress echo so the password isn't shown as it's typed.
1568
- rl._writeToOutput = () => {}
1569
- rl.question("", (answer) => {
1570
- rl.close()
1571
- process.stdout.write("\n")
1572
- resolve(answer)
1573
- })
1574
- })
1575
- }
1576
-
1577
- async function runLogin(options) {
1578
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
1579
- const existing = readJSONIfExists(path.join(prefix, "config", "local.json"))
1580
- const hubUrl = (options.hub ?? existing.hubUrl ?? DEFAULT_HUB_URL).replace(/\/+$/, "")
1581
- const localApi = localApiBase(prefix)
1582
-
1583
- // The local runtime must be running: we need its stable hardware id (so the
1584
- // Hub dedups re-registrations) and it's what actually connects to the Hub.
1585
- let hardwareId = null
1586
- try {
1587
- const res = await fetch(`${localApi}/v1/hub/status`)
1588
- if (res.ok) hardwareId = (await res.json()).hardwareId ?? null
1589
- } catch {}
1590
- if (!hardwareId) {
1591
- throw new Error(
1592
- `local runtime not reachable at ${localApi}. Start it first with "vantaloom start", then retry "vantaloom login".`
1593
- )
1594
- }
1595
-
1596
- const email = options.email ?? (await promptVisible("Hub email: "))
1597
- const password = options.password ?? (await promptHidden("Hub password: "))
1598
- if (!email || !password) {
1599
- throw new Error("email and password are required")
1600
- }
1601
-
1602
- console.log(`logging in to ${hubUrl} ...`)
1603
- const login = await hubFetch(`${hubUrl}/api/auth/login`, {
1604
- method: "POST",
1605
- body: { email, password },
1606
- })
1607
- const userToken = login.token
1608
- if (!userToken) throw new Error("login failed: no token returned")
1609
-
1610
- const { deviceToken } = await hubFetch(`${hubUrl}/api/machines/token`, {
1611
- method: "POST",
1612
- token: userToken,
1613
- })
1614
- const info = localMachineInfo()
1615
- const connect = await hubFetch(`${hubUrl}/api/connect`, {
1616
- method: "POST",
1617
- body: { deviceToken, hardwareId, ...info },
1618
- })
1619
- const machineId = connect.machine?.id
1620
- if (!machineId) throw new Error("machine registration failed: no machine id")
1621
-
1622
- // Hand the credentials to the running runtime; it persists them to local.json
1623
- // and brings up the Hub WebSocket + heartbeat + mesh.
1624
- const res = await fetch(`${localApi}/v1/hub/connect`, {
1625
- method: "POST",
1626
- headers: { "Content-Type": "application/json" },
1627
- body: JSON.stringify({ hubUrl, hubToken: userToken, machineId }),
1628
- })
1629
- if (!res.ok) {
1630
- throw new Error(`runtime failed to connect to Hub: HTTP ${res.status}`)
1631
- }
1632
-
1633
- console.log(`\n✓ logged in as ${login.user?.email ?? email}`)
1634
- console.log(`✓ machine registered: ${info.name} (${machineId})`)
1635
- console.log(`✓ runtime connected to Hub at ${hubUrl}`)
1636
- console.log(`\nThis machine is now in your workgroup. It will reconnect automatically on restart.`)
1637
- }
1638
-
1639
- async function runLogout(options) {
1640
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
1641
- const localApi = localApiBase(prefix)
1642
- try {
1643
- const res = await fetch(`${localApi}/v1/hub/disconnect`, { method: "POST" })
1644
- if (!res.ok) throw new Error(`HTTP ${res.status}`)
1645
- console.log("✓ disconnected from Hub (runtime keeps running locally).")
1646
- } catch (error) {
1647
- throw new Error(
1648
- `could not reach local runtime at ${localApi}: ${error.message}`
1649
- )
1650
- }
1651
- }
1652
-
1653
- function buildGo(sourceRoot, buildBin, name, targetPlatform) {
1654
- const goEnv = targetPlatform ? platformToGoEnv(targetPlatform) : {}
1655
- const isWindowsTarget = targetPlatform
1656
- ? targetPlatform.startsWith("win32")
1657
- : process.platform === "win32"
1658
- const ext = isWindowsTarget ? ".exe" : ""
1659
- let ldflags = "-s -w"
1660
- const args = ["build"]
1661
- // The privileged mesh sidecar is built REPRODUCIBLY: with no embedded git VCS
1662
- // stamp or build id, unchanged mesh source yields byte-identical output across
1663
- // releases. That lets an update where the mesh hasn't changed be detected as a
1664
- // no-op and SKIP the privileged service re-apply — so routine updates need no
1665
- // UAC (Windows) / sudo (Linux/macOS) and can run fully unattended/remotely.
1666
- if (name === "vantaloom-mesh") {
1667
- args.push("-trimpath", "-buildvcs=false")
1668
- ldflags += " -buildid="
1669
- }
1670
- args.push(
1671
- "-ldflags", ldflags,
1672
- "-o", path.join(buildBin, `${name}${ext}`),
1673
- `./apps/api/cmd/${name}`
1674
- )
1675
- run("go", args, { cwd: sourceRoot, env: { ...process.env, ...goEnv } })
1676
- }
1677
-
1678
- async function copyDir(source, destination, options = {}) {
1679
- if (!existsSync(source)) {
1680
- throw new Error(`missing source directory: ${source}`)
1681
- }
1682
- removeKnownPath(destination, path.dirname(destination))
1683
- mkdirSync(destination, { recursive: true })
1684
- await cp(source, destination, {
1685
- recursive: true,
1686
- force: true,
1687
- dereference: options.dereference ?? true,
1688
- })
1689
- }
1690
-
1691
- function writeBuildManifest(buildRoot, version, platform) {
1692
- const components = ["api", "agent", "web", "ctl"]
1693
- writeFileSync(path.join(buildRoot, "VERSION"), `${version}\n`)
1694
- writeFileSync(
1695
- path.join(buildRoot, "manifest.json"),
1696
- `${JSON.stringify(
1697
- {
1698
- name: "Vantaloom Local Runtime",
1699
- version,
1700
- platform,
1701
- updatedAt: new Date().toISOString(),
1702
- components,
1703
- },
1704
- null,
1705
- 2
1706
- )}\n`
1707
- )
1708
- }
1709
-
1710
- function writeRuntimePackageMetadata(packageRoot, sourceRoot, platform) {
1711
- const version = npmPackageVersion(sourceRoot)
1712
- const { os: runtimeOS, cpu } = parsePlatformId(platform)
1713
- const name = runtimePackageName(platform)
1714
- writeFileSync(
1715
- path.join(packageRoot, "package.json"),
1716
- `${JSON.stringify(
1717
- {
1718
- name,
1719
- version,
1720
- private: false,
1721
- description: `Vantaloom local runtime for ${platform}.`,
1722
- type: "module",
1723
- os: [runtimeOS],
1724
- cpu: [cpu],
1725
- files: ["bin", "web", "cli", "manifest.json", "VERSION", "README.md"],
1726
- publishConfig: {
1727
- access: "public",
1728
- },
1729
- engines: {
1730
- node: ">=20",
1731
- },
1732
- },
1733
- null,
1734
- 2
1735
- )}\n`
1736
- )
1737
- writeFileSync(
1738
- path.join(packageRoot, "README.md"),
1739
- `# ${name}\n\nPlatform runtime package for Vantaloom ${platform}. Install @vantaloom/cli instead of this package directly.\n`
1740
- )
1741
- }
1742
-
1743
- function findSourceRoot(sourceOption) {
1744
- if (sourceOption) {
1745
- return assertSourceRoot(path.resolve(sourceOption))
1746
- }
1747
- if (process.env.VANTALOOM_SOURCE) {
1748
- return assertSourceRoot(path.resolve(process.env.VANTALOOM_SOURCE))
1749
- }
1750
- if (existsSync(installedConfigPath)) {
1751
- const config = JSON.parse(readFileSync(installedConfigPath, "utf8"))
1752
- if (config.sourceRoot) {
1753
- return assertSourceRoot(path.resolve(config.sourceRoot))
1754
- }
1755
- }
1756
- return assertSourceRoot(repoCandidate)
1757
- }
1758
-
1759
- function npmPackageVersion(sourceRoot) {
1760
- const packageJSON = readJSONIfExists(path.join(sourceRoot, "packages", "cli", "package.json"))
1761
- return packageJSON.version ?? "0.0.0"
1762
- }
1763
-
1764
- function assertSourceRoot(sourceRoot) {
1765
- if (!existsSync(path.join(sourceRoot, "apps", "api", "go.mod"))) {
1766
- throw new Error(`not a Vantaloom source root: ${sourceRoot}`)
1767
- }
1768
- return sourceRoot
1769
- }
1770
-
1771
- function gitVersion(sourceRoot) {
1772
- const result = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
1773
- cwd: sourceRoot,
1774
- encoding: "utf8",
1775
- windowsHide: true,
1776
- })
1777
- if (result.status === 0) {
1778
- return result.stdout.trim() || "dev"
1779
- }
1780
- return "dev"
1781
- }
1782
-
1783
- function parseOptions(args) {
1784
- const options = {}
1785
- for (let index = 0; index < args.length; index += 1) {
1786
- const arg = args[index]
1787
- if (!arg.startsWith("--")) {
1788
- throw new Error(`unexpected argument "${arg}"`)
1789
- }
1790
- const [key, inlineValue] = arg.slice(2).split("=", 2)
1791
- switch (key) {
1792
- case "prefix":
1793
- case "source":
1794
- case "component":
1795
- case "package":
1796
- case "output":
1797
- case "repo":
1798
- case "remote":
1799
- case "release-tag":
1800
- case "github-token":
1801
- case "runtime-package":
1802
- case "runtime-version":
1803
- case "npm-registry":
1804
- case "hub":
1805
- case "email":
1806
- case "password":
1807
- options[toCamel(key)] = inlineValue ?? args[++index]
1808
- if (!options[toCamel(key)]) {
1809
- throw new Error(`missing value for --${key}`)
1810
- }
1811
- break
1812
- case "build-web":
1813
- options.buildWeb = true
1814
- break
1815
- case "no-start":
1816
- options.noStart = true
1817
- break
1818
- case "archive":
1819
- options.archive = true
1820
- break
1821
- case "npm-package":
1822
- options.npmPackage = true
1823
- break
1824
- case "target":
1825
- options.target = inlineValue ?? args[++index]
1826
- if (!options.target) {
1827
- throw new Error("missing value for --target")
1828
- }
1829
- break
1830
- case "local":
1831
- options.local = true
1832
- break
1833
- case "with-mesh":
1834
- options.withMesh = true
1835
- break
1836
- case "skip-mesh":
1837
- options.skipMesh = true
1838
- break
1839
- case "skip-adapter":
1840
- options.skipAdapter = true
1841
- break
1842
- case "skip-autostart":
1843
- options.skipAutostart = true
1844
- break
1845
- case "no-strict-ssl":
1846
- options.noStrictSsl = true
1847
- break
1848
- default:
1849
- throw new Error(`unknown option --${key}`)
1850
- }
1851
- }
1852
- return options
1853
- }
1854
-
1855
- function safeDirectory(value) {
1856
- const full = path.resolve(value)
1857
- const parsed = path.parse(full)
1858
- if (full === parsed.root) {
1859
- throw new Error(`refusing to operate on unsafe directory: ${value}`)
1860
- }
1861
- return full
1862
- }
1863
-
1864
- function removeKnownPath(target, expectedParent) {
1865
- if (!existsSync(target)) {
1866
- return
1867
- }
1868
- const full = path.resolve(target)
1869
- const parent = path.resolve(expectedParent)
1870
- const prefix = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`
1871
- if (!full.startsWith(prefix)) {
1872
- throw new Error(`refusing to remove path outside expected parent: ${full}`)
1873
- }
1874
- rmSync(full, { recursive: true, force: true })
1875
- }
1876
-
1877
- function run(command, args, options = {}) {
1878
- try {
1879
- execFileSync(command, args, { stdio: "inherit", windowsHide: true, ...options })
1880
- } catch (error) {
1881
- if (error && typeof error.status === "number") {
1882
- throw new Error(`${command} exited with ${error.status}`)
1883
- }
1884
- throw error
1885
- }
1886
- }
1887
-
1888
- function runPnpm(args, options = {}) {
1889
- if (process.platform === "win32") {
1890
- run("cmd.exe", ["/d", "/s", "/c", "pnpm", ...args], options)
1891
- return
1892
- }
1893
- run("pnpm", args, options)
1894
- }
1895
-
1896
- // runNpm invokes npm cross-platform. On Windows npm is `npm.cmd`, which
1897
- // execFileSync (used by run) can't resolve directly (ENOENT) — so wrap via
1898
- // cmd.exe, mirroring runPnpm.
1899
- function runNpm(args, options = {}) {
1900
- if (process.platform === "win32") {
1901
- run("cmd.exe", ["/d", "/s", "/c", "npm", ...args], options)
1902
- return
1903
- }
1904
- run("npm", args, options)
1905
- }
1906
-
1907
- function binaryName(name, targetPlatform) {
1908
- const isWindows = targetPlatform
1909
- ? targetPlatform.startsWith("win32")
1910
- : process.platform === "win32"
1911
- return isWindows ? `${name}.exe` : name
1912
- }
1913
-
1914
- function platformToGoEnv(platform) {
1915
- const { os: runtimeOS, cpu } = parsePlatformId(platform)
1916
- const goosMap = { win32: "windows", linux: "linux", darwin: "darwin" }
1917
- const goarchMap = { x64: "amd64", arm64: "arm64" }
1918
- return {
1919
- GOOS: goosMap[runtimeOS] ?? runtimeOS,
1920
- GOARCH: goarchMap[cpu] ?? cpu,
1921
- CGO_ENABLED: "0",
1922
- }
1923
- }
1924
-
1925
- function platformId() {
1926
- return `${process.platform}-${process.arch}`
1927
- }
1928
-
1929
- function runtimePackageName(platform) {
1930
- return `@vantaloom/runtime-${platform}`
1931
- }
1932
-
1933
- function parsePlatformId(platform) {
1934
- const parts = platform.split("-")
1935
- const cpu = parts.pop()
1936
- const runtimeOS = parts.join("-")
1937
- if (!runtimeOS || !cpu) {
1938
- throw new Error(`invalid platform id: ${platform}`)
1939
- }
1940
- return { os: runtimeOS, cpu }
1941
- }
1942
-
1943
- function packageBasename(name) {
1944
- return name.split("/").pop() ?? name
1945
- }
1946
-
1947
- function normalizeRegistry(value) {
1948
- return value.replace(/\/+$/, "")
1949
- }
1950
-
1951
- function defaultPrefix() {
1952
- if (process.env.VANTALOOM_HOME) {
1953
- return process.env.VANTALOOM_HOME
1954
- }
1955
- if (process.platform === "win32") {
1956
- // Prefer D: (the historical default) but fall back to C: when this machine
1957
- // has no D: drive. Keep in sync with the desktop shell's DefaultPrefix().
1958
- return existsSync("D:\\") ? "D:\\Vantaloom" : "C:\\Vantaloom"
1959
- }
1960
- if (process.platform === "darwin") {
1961
- return path.join(os.homedir(), "Applications", "Vantaloom")
1962
- }
1963
- return path.join(os.homedir(), ".local", "vantaloom")
1964
- }
1965
-
1966
- function displayCommand(prefix) {
1967
- if (process.platform === "win32") {
1968
- return path.join(prefix, "vantaloom.cmd")
1969
- }
1970
- return path.join(prefix, "vantaloom")
1971
- }
1972
-
1973
- function toCamel(value) {
1974
- return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
1975
- }
1976
-
1977
- async function writeText(filePath, content) {
1978
- mkdirSync(path.dirname(filePath), { recursive: true })
1979
- await writeFile(filePath, content)
1980
- }
1981
-
1982
- // ensureInPath adds the Vantaloom install directory to the user's shell PATH
1983
- // on macOS and Linux, so `vantaloom` can be run directly after install.
1984
- // On Windows this is not needed (vantaloom.cmd is run by full path or added via installer).
1985
- function ensureInPath(prefix) {
1986
- if (process.platform === "win32") return
1987
-
1988
- // Check if already in PATH
1989
- const pathDirs = (process.env.PATH || "").split(":")
1990
- if (pathDirs.includes(prefix)) return
1991
-
1992
- // Determine shell profile file
1993
- const home = os.homedir()
1994
- const shell = process.env.SHELL || ""
1995
- let profilePath
1996
- if (shell.endsWith("/zsh") || existsSync(path.join(home, ".zshrc"))) {
1997
- profilePath = path.join(home, ".zshrc")
1998
- } else if (shell.endsWith("/bash")) {
1999
- // On macOS, bash uses .bash_profile; on Linux, .bashrc
2000
- profilePath = process.platform === "darwin"
2001
- ? path.join(home, ".bash_profile")
2002
- : path.join(home, ".bashrc")
2003
- } else if (existsSync(path.join(home, ".profile"))) {
2004
- profilePath = path.join(home, ".profile")
2005
- } else {
2006
- profilePath = path.join(home, ".profile")
2007
- }
2008
-
2009
- // Use $HOME-relative path for portability
2010
- const homeRelative = prefix.startsWith(home)
2011
- ? `$HOME${prefix.slice(home.length)}`
2012
- : prefix
2013
- const exportLine = `export PATH="${homeRelative}:$PATH"`
2014
- const marker = "# vantaloom"
2015
-
2016
- // Check if already added to profile
2017
- try {
2018
- if (existsSync(profilePath)) {
2019
- const content = readFileSync(profilePath, "utf8")
2020
- if (content.includes("vantaloom") && content.includes("PATH")) return
2021
- }
2022
- } catch {}
2023
-
2024
- // Append to profile
2025
- try {
2026
- const entry = `\n${marker}\n${exportLine}\n`
2027
- appendFileSync(profilePath, entry)
2028
- console.log(`PATH: added ${prefix} to ${profilePath}`)
2029
- console.log(` run: source ${profilePath} (or open a new terminal)`)
2030
- } catch (error) {
2031
- console.log(`PATH: could not update ${profilePath}: ${error.message}`)
2032
- console.log(` add manually: ${exportLine}`)
2033
- }
2034
- }
2035
-
2036
- function killTrayProcess(prefix) {
2037
- if (process.platform === "win32") {
2038
- // Try PID file first (new tray versions write runtime/tray.pid).
2039
- const pidFile = path.join(prefix, "runtime", "tray.pid")
2040
- if (existsSync(pidFile)) {
2041
- const pid = readFileSync(pidFile, "utf8").trim()
2042
- if (pid) {
2043
- spawnSync("taskkill", ["/PID", pid, "/F"], { stdio: "ignore", windowsHide: true })
2044
- try { rmSync(pidFile, { force: true }) } catch {}
2045
- }
2046
- }
2047
- // Fallback: kill by image name if the binary is inside our prefix.
2048
- const result = spawnSync("tasklist", ["/FI", "IMAGENAME eq vantaloom-tray.exe", "/FO", "CSV", "/NH"], {
2049
- encoding: "utf8",
2050
- windowsHide: true,
2051
- })
2052
- if (result.stdout) {
2053
- for (const line of result.stdout.split("\n")) {
2054
- const match = line.match(/"vantaloom-tray\.exe","(\d+)"/)
2055
- if (match) {
2056
- spawnSync("taskkill", ["/PID", match[1], "/F"], { stdio: "ignore", windowsHide: true })
2057
- }
2058
- }
2059
- }
2060
- // Brief pause to let file handles release.
2061
- spawnSync("timeout", ["/t", "1", "/nobreak"], { stdio: "ignore", windowsHide: true })
2062
- } else {
2063
- // Unix: pkill by name (best-effort).
2064
- spawnSync("pkill", ["-f", "vantaloom-tray"], { stdio: "ignore", windowsHide: true })
2065
- }
2066
- }
2067
-
2068
- function printHelp() {
2069
- console.log(`Vantaloom CLI
2070
-
2071
- Usage:
2072
- vantaloom install [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start] [--with-mesh|--skip-mesh]
2073
- vantaloom install --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start] [--with-mesh]
2074
- vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start] [--with-mesh|--skip-mesh]
2075
- vantaloom update --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start] [--with-mesh]
2076
- vantaloom uninstall [--prefix <dir>] [--skip-mesh]
2077
- vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package] [--target <platform>]
2078
- vantaloom install-adapter [--prefix <dir>] (download the Claude Code ACP adapter into the install)
2079
- vantaloom start [--prefix <dir>] [--component all|api|agent|web]
2080
- vantaloom stop [--prefix <dir>] [--component all|api|agent|web]
2081
- vantaloom restart [--prefix <dir>] [--component all|api|agent|web]
2082
- vantaloom status [--prefix <dir>]
2083
- vantaloom ports [--prefix <dir>]
2084
- vantaloom path [--prefix <dir>] [--source <repo>]
2085
- vantaloom platform
2086
- vantaloom login [--hub <url>] [--email <email>] [--password <pw>] [--prefix <dir>]
2087
- vantaloom logout [--prefix <dir>]
2088
-
2089
- Hub login (for machines with no web access):
2090
- "vantaloom login" authenticates to the Hub from the terminal and joins this
2091
- machine to your workgroup — no browser needed. Email/password may be passed as
2092
- flags (for scripts) or entered interactively. The local runtime must be running
2093
- (run "vantaloom start" first).
2094
-
2095
- Mesh (P2P) service:
2096
- Windows/macOS register a privileged EasyTier service at install (one UAC/sudo prompt).
2097
- Linux grants TUN capability via setcap instead. Use --skip-mesh to opt out,
2098
- or --with-mesh to force registration during a --local source install.
2099
- `)
2100
- }
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ } from "node:fs"
7
+ import os from "node:os"
8
+ import path from "node:path"
9
+
10
+ import {
11
+ cliRoot,
12
+ defaultNpmRegistry,
13
+ defaultRepo,
14
+ defaultReleaseTag,
15
+ fallbackNpmRegistries,
16
+ } from "./lib/constants.mjs"
17
+ import {
18
+ platformId,
19
+ runtimePackageName,
20
+ safeDirectory,
21
+ defaultPrefix,
22
+ displayCommand,
23
+ readJSONIfExists,
24
+ normalizeRegistry,
25
+ packageBasename,
26
+ toCamel,
27
+ removeKnownPath,
28
+ run,
29
+ } from "./lib/platform.mjs"
30
+ import {
31
+ applyPackage,
32
+ findSourceRoot,
33
+ tryFindSourceRoot,
34
+ tryAssertSourceRoot,
35
+ gitRemoteUrl,
36
+ inferGitHubRepo,
37
+ readInstalledConfig,
38
+ writeRuntimePackageMetadata,
39
+ ensureInPath,
40
+ } from "./lib/install.mjs"
41
+ import {
42
+ buildRuntimePackage,
43
+ bundleAdapter,
44
+ } from "./lib/package.mjs"
45
+ import {
46
+ detectNpmRegistry,
47
+ resolveNpmPackageWithFallback,
48
+ downloadNpmTarball,
49
+ downloadReleaseAsset,
50
+ findExtractedNpmPackage,
51
+ findExtractedPackage,
52
+ } from "./lib/registry.mjs"
53
+ import {
54
+ runCtl,
55
+ uninstallRuntime,
56
+ } from "./lib/lifecycle.mjs"
57
+ import {
58
+ runLogin,
59
+ runLogout,
60
+ } from "./lib/auth.mjs"
61
+
62
+ // Inherit strict-ssl=false from npm/npx config, or respect NODE_TLS_REJECT_UNAUTHORIZED.
63
+ // When npm runs us via npx with strict-ssl disabled, it sets npm_config_strict_ssl="false".
64
+ // Also check user .npmrc for strict-ssl=false (common in China behind proxies).
65
+ function shouldDisableTLS() {
66
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") return true
67
+ if (process.env.npm_config_strict_ssl === "false") return true
68
+ if (process.env.npm_config_strict_ssl === "") return true
69
+ try {
70
+ const npmrcPath = path.join(os.homedir(), ".npmrc")
71
+ if (existsSync(npmrcPath)) {
72
+ const content = readFileSync(npmrcPath, "utf8")
73
+ if (/^\s*strict-ssl\s*=\s*false/m.test(content)) return true
74
+ }
75
+ } catch {}
76
+ return false
77
+ }
78
+ if (shouldDisableTLS()) {
79
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
80
+ }
81
+
82
+ const cliVersion = readJSONIfExists(path.join(cliRoot, "package.json")).version ?? "unknown"
83
+
84
+ export async function main(argv) {
85
+ const command = argv[0] ?? "help"
86
+ const options = parseOptions(argv.slice(1))
87
+
88
+ // --no-strict-ssl flag disables TLS certificate verification for fetch calls
89
+ if (options.noStrictSsl) {
90
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
91
+ }
92
+
93
+ if (command === "install" || command === "update") {
94
+ console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
95
+ }
96
+
97
+ switch (command) {
98
+ case "install":
99
+ if (options.package) {
100
+ await installFromPackage({ ...options, update: false })
101
+ } else if (shouldUseSourceInstall(options)) {
102
+ await installFromSource(options)
103
+ } else if (shouldUseReleaseSync(options)) {
104
+ await syncFromRelease(options, "install")
105
+ } else {
106
+ await syncFromNpmRegistry(options, "install")
107
+ }
108
+ return
109
+ case "update":
110
+ if (options.package) {
111
+ await installFromPackage({ ...options, update: true })
112
+ } else if (shouldUseSourceInstall(options)) {
113
+ await installFromSource({ ...options, update: true })
114
+ } else if (shouldUseReleaseSync(options)) {
115
+ await syncFromRelease(options, "update")
116
+ } else {
117
+ await syncFromNpmRegistry(options, "update")
118
+ }
119
+ return
120
+ case "package":
121
+ await packageRuntime(options)
122
+ return
123
+ case "install-adapter":
124
+ await installAdapter(options)
125
+ return
126
+ case "platform":
127
+ console.log(platformId())
128
+ return
129
+ case "start":
130
+ case "stop":
131
+ case "restart":
132
+ case "status":
133
+ case "ports":
134
+ runCtl(command, options)
135
+ return
136
+ case "path":
137
+ printPaths(options)
138
+ return
139
+ case "uninstall":
140
+ await uninstallRuntime(options)
141
+ return
142
+ case "login":
143
+ await runLogin(options)
144
+ return
145
+ case "logout":
146
+ await runLogout(options)
147
+ return
148
+ case "help":
149
+ case "-h":
150
+ case "--help":
151
+ printHelp()
152
+ return
153
+ default:
154
+ throw new Error(`unknown command "${command}"`)
155
+ }
156
+ }
157
+
158
+ async function installFromSource(options) {
159
+ const sourceRoot = findSourceRoot(options.source)
160
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
161
+ const buildRoot = path.join(sourceRoot, "artifacts", "local-install", platformId())
162
+
163
+ const { version } = await buildRuntimePackage(sourceRoot, buildRoot, {
164
+ buildWeb: options.buildWeb,
165
+ skipAdapter: options.skipAdapter,
166
+ })
167
+
168
+ await applyPackage(buildRoot, prefix, {
169
+ noStart: options.noStart,
170
+ sourceRoot,
171
+ update: options.update,
172
+ withMesh: options.withMesh,
173
+ skipMesh: options.skipMesh,
174
+ })
175
+
176
+ console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
177
+ console.log(`version: ${version}`)
178
+ ensureInPath(prefix)
179
+ console.log(`run: ${displayCommand(prefix)} status`)
180
+ }
181
+
182
+ async function installFromPackage(options) {
183
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
184
+ const packageRoot = safeDirectory(options.package)
185
+ const version = await applyPackage(packageRoot, prefix, {
186
+ noStart: options.noStart,
187
+ update: options.update,
188
+ withMesh: options.withMesh,
189
+ skipMesh: options.skipMesh,
190
+ })
191
+
192
+ console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
193
+ console.log(`version: ${version}`)
194
+ ensureInPath(prefix)
195
+ console.log(`run: ${displayCommand(prefix)} status`)
196
+ }
197
+
198
+ async function packageRuntime(options) {
199
+ const sourceRoot = findSourceRoot(options.source)
200
+ const targetPlatform = options.target ?? platformId()
201
+ const packageRoot = safeDirectory(
202
+ options.output ?? path.join(sourceRoot, "artifacts", "packages", `vantaloom-${targetPlatform}`)
203
+ )
204
+ const { platform: builtPlatform, version } = await buildRuntimePackage(sourceRoot, packageRoot, {
205
+ buildWeb: options.buildWeb,
206
+ target: targetPlatform,
207
+ skipAdapter: options.skipAdapter,
208
+ })
209
+
210
+ if (options.npmPackage) {
211
+ writeRuntimePackageMetadata(packageRoot, sourceRoot, builtPlatform)
212
+ }
213
+
214
+ if (options.archive) {
215
+ const archivePath = `${packageRoot}.tar.gz`
216
+ removeKnownPath(archivePath, path.dirname(archivePath))
217
+ run("tar", [
218
+ "-czf",
219
+ archivePath,
220
+ "-C",
221
+ path.dirname(packageRoot),
222
+ path.basename(packageRoot),
223
+ ])
224
+ console.log(`archive: ${archivePath}`)
225
+ }
226
+
227
+ console.log(`packaged Vantaloom: ${packageRoot}`)
228
+ console.log(`platform: ${builtPlatform}`)
229
+ console.log(`version: ${version}`)
230
+ }
231
+
232
+ // installAdapter downloads + bundles the Claude Code ACP adapter into a LIVE
233
+ // install's adapters/ dir (node_modules + patched local-command forwarding +
234
+ // reclaude shim), so the user can add Claude Code / reclaude support on demand
235
+ // instead of shipping ~50-100MB inside every runtime tarball. The Go backend
236
+ // invokes this (silent, no console window) from the settings "install adapter"
237
+ // button; it can also be run by hand: `vantaloom install-adapter`.
238
+ async function installAdapter(options) {
239
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
240
+ console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
241
+ console.log(`installing Claude Code ACP adapter into ${path.join(prefix, "adapters")} ...`)
242
+ await bundleAdapter(prefix, { platform: platformId() })
243
+ console.log(`adapter ready`)
244
+ }
245
+
246
+ async function syncFromNpmRegistry(options, action) {
247
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
248
+ const installedConfig = readInstalledConfig(prefix)
249
+ // Always derive runtimePackage from current platform — never trust stale config
250
+ // from a different platform (e.g. win32 config baked into a darwin package).
251
+ // Only explicit --runtime-package flag can override.
252
+ const runtimePackage = options.runtimePackage || runtimePackageName(platformId())
253
+ const runtimeVersion = options.runtimeVersion || installedConfig.runtimeVersion || "latest"
254
+ const explicitRegistry = options.npmRegistry || installedConfig.npmRegistry
255
+ const registry = normalizeRegistry(explicitRegistry || detectNpmRegistry() || defaultNpmRegistry)
256
+ const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-npm-"))
257
+
258
+ try {
259
+ const extractRoot = path.join(tempRoot, "extract")
260
+ const archive = path.join(tempRoot, `${packageBasename(runtimePackage)}-${runtimeVersion}.tgz`)
261
+ mkdirSync(extractRoot, { recursive: true })
262
+
263
+ const resolved = await resolveNpmPackageWithFallback({
264
+ registries: explicitRegistry ? [registry] : [registry, ...fallbackNpmRegistries.map(normalizeRegistry)],
265
+ name: runtimePackage,
266
+ version: runtimeVersion,
267
+ })
268
+ await downloadNpmTarball({
269
+ tarballUrl: resolved.tarball,
270
+ target: archive,
271
+ packageName: runtimePackage,
272
+ version: resolved.version,
273
+ })
274
+ run("tar", ["-xzf", archive, "-C", extractRoot])
275
+
276
+ const packageRoot = findExtractedNpmPackage(extractRoot)
277
+ const version = await applyPackage(packageRoot, prefix, {
278
+ noStart: options.noStart,
279
+ runtimePackage,
280
+ runtimeVersion: options.runtimeVersion ? resolved.version : "latest",
281
+ npmRegistry: resolved.registry,
282
+ update: action === "update",
283
+ withMesh: options.withMesh,
284
+ skipMesh: options.skipMesh,
285
+ })
286
+
287
+ console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
288
+ console.log(`version: ${version}`)
289
+ console.log(`source: ${runtimePackage}@${resolved.version}`)
290
+ console.log(`registry: ${resolved.registry}`)
291
+ ensureInPath(prefix)
292
+ console.log(`run: ${displayCommand(prefix)} status`)
293
+ } finally {
294
+ removeKnownPath(tempRoot, os.tmpdir())
295
+ }
296
+ }
297
+
298
+ async function syncFromRelease(options, action) {
299
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
300
+ const installedConfig = readInstalledConfig(prefix)
301
+ const sourceRoot = installedConfig.sourceRoot
302
+ ? tryAssertSourceRoot(installedConfig.sourceRoot)
303
+ : tryFindSourceRoot()
304
+ const sourceRemote = sourceRoot ? gitRemoteUrl(sourceRoot) : ""
305
+ const repo = options.repo || installedConfig.repo || inferGitHubRepo(options.remote ?? installedConfig.remote ?? sourceRemote) || defaultRepo
306
+ const releaseTag = options.releaseTag || installedConfig.releaseTag || defaultReleaseTag
307
+ const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-update-"))
308
+
309
+ try {
310
+ const extractRoot = path.join(tempRoot, "extract")
311
+ const archive = path.join(tempRoot, `vantaloom-${platformId()}.tar.gz`)
312
+ mkdirSync(extractRoot, { recursive: true })
313
+
314
+ await downloadReleaseAsset({
315
+ repo,
316
+ releaseTag,
317
+ assetName: `vantaloom-${platformId()}.tar.gz`,
318
+ target: archive,
319
+ token: options.githubToken,
320
+ })
321
+ run("tar", ["-xzf", archive, "-C", extractRoot])
322
+
323
+ const packageRoot = findExtractedPackage(extractRoot, platformId())
324
+ const version = await applyPackage(packageRoot, prefix, {
325
+ noStart: options.noStart,
326
+ sourceRoot: installedConfig.sourceRoot,
327
+ update: action === "update",
328
+ withMesh: options.withMesh,
329
+ skipMesh: options.skipMesh,
330
+ })
331
+
332
+ console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
333
+ console.log(`version: ${version}`)
334
+ console.log(`source: ${repo}@${releaseTag}`)
335
+ ensureInPath(prefix)
336
+ console.log(`run: ${displayCommand(prefix)} status`)
337
+ } finally {
338
+ removeKnownPath(tempRoot, os.tmpdir())
339
+ }
340
+ }
341
+
342
+ function shouldUseSourceInstall(options) {
343
+ return Boolean(options.local || options.source || options.buildWeb)
344
+ }
345
+
346
+ function shouldUseReleaseSync(options) {
347
+ return Boolean(options.repo || options.remote || options.releaseTag || options.githubToken)
348
+ }
349
+
350
+ function printPaths(options) {
351
+ const sourceRoot = findSourceRoot(options.source)
352
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
353
+ console.log(JSON.stringify({ sourceRoot, prefix }, null, 2))
354
+ }
355
+
356
+ function parseOptions(args) {
357
+ const options = {}
358
+ for (let index = 0; index < args.length; index += 1) {
359
+ const arg = args[index]
360
+ if (!arg.startsWith("--")) {
361
+ throw new Error(`unexpected argument "${arg}"`)
362
+ }
363
+ const [key, inlineValue] = arg.slice(2).split("=", 2)
364
+ switch (key) {
365
+ case "prefix":
366
+ case "source":
367
+ case "component":
368
+ case "package":
369
+ case "output":
370
+ case "repo":
371
+ case "remote":
372
+ case "release-tag":
373
+ case "github-token":
374
+ case "runtime-package":
375
+ case "runtime-version":
376
+ case "npm-registry":
377
+ case "hub":
378
+ case "email":
379
+ case "password":
380
+ options[toCamel(key)] = inlineValue ?? args[++index]
381
+ if (!options[toCamel(key)]) {
382
+ throw new Error(`missing value for --${key}`)
383
+ }
384
+ break
385
+ case "build-web":
386
+ options.buildWeb = true
387
+ break
388
+ case "no-start":
389
+ options.noStart = true
390
+ break
391
+ case "archive":
392
+ options.archive = true
393
+ break
394
+ case "npm-package":
395
+ options.npmPackage = true
396
+ break
397
+ case "target":
398
+ options.target = inlineValue ?? args[++index]
399
+ if (!options.target) {
400
+ throw new Error("missing value for --target")
401
+ }
402
+ break
403
+ case "local":
404
+ options.local = true
405
+ break
406
+ case "with-mesh":
407
+ options.withMesh = true
408
+ break
409
+ case "skip-mesh":
410
+ options.skipMesh = true
411
+ break
412
+ case "skip-adapter":
413
+ options.skipAdapter = true
414
+ break
415
+ case "skip-autostart":
416
+ options.skipAutostart = true
417
+ break
418
+ case "no-strict-ssl":
419
+ options.noStrictSsl = true
420
+ break
421
+ default:
422
+ throw new Error(`unknown option --${key}`)
423
+ }
424
+ }
425
+ return options
426
+ }
427
+
428
+ function printHelp() {
429
+ console.log(`Vantaloom CLI
430
+
431
+ Usage:
432
+ vantaloom install [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start] [--with-mesh|--skip-mesh]
433
+ vantaloom install --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start] [--with-mesh]
434
+ vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start] [--with-mesh|--skip-mesh]
435
+ vantaloom update --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start] [--with-mesh]
436
+ vantaloom uninstall [--prefix <dir>] [--skip-mesh]
437
+ vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package] [--target <platform>]
438
+ vantaloom install-adapter [--prefix <dir>] (download the Claude Code ACP adapter into the install)
439
+ vantaloom start [--prefix <dir>] [--component all|api|agent|web]
440
+ vantaloom stop [--prefix <dir>] [--component all|api|agent|web]
441
+ vantaloom restart [--prefix <dir>] [--component all|api|agent|web]
442
+ vantaloom status [--prefix <dir>]
443
+ vantaloom ports [--prefix <dir>]
444
+ vantaloom path [--prefix <dir>] [--source <repo>]
445
+ vantaloom platform
446
+ vantaloom login [--hub <url>] [--email <email>] [--password <pw>] [--prefix <dir>]
447
+ vantaloom logout [--prefix <dir>]
448
+
449
+ Hub login (for machines with no web access):
450
+ "vantaloom login" authenticates to the Hub from the terminal and joins this
451
+ machine to your workgroup — no browser needed. Email/password may be passed as
452
+ flags (for scripts) or entered interactively. The local runtime must be running
453
+ (run "vantaloom start" first).
454
+
455
+ Mesh (P2P) service:
456
+ Windows/macOS register a privileged EasyTier service at install (one UAC/sudo prompt).
457
+ Linux grants TUN capability via setcap instead. Use --skip-mesh to opt out,
458
+ or --with-mesh to force registration during a --local source install.
459
+ `)
460
+ }