@vantaloom/cli 0.6.0 → 0.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.mjs CHANGED
@@ -1,35 +1,60 @@
1
- import { execFileSync, spawnSync } from "node:child_process"
2
1
  import {
3
- appendFileSync,
4
- chmodSync,
5
2
  existsSync,
6
3
  mkdirSync,
7
4
  mkdtempSync,
8
- readdirSync,
9
5
  readFileSync,
10
- rmSync,
11
- writeFileSync,
12
6
  } from "node:fs"
13
- import { cp, writeFile } from "node:fs/promises"
14
- import { createHash } from "node:crypto"
15
7
  import os from "node:os"
16
8
  import path from "node:path"
17
- import { fileURLToPath } from "node:url"
18
9
 
19
- // The mesh binaries the privileged service holds open while running. On Windows
20
- // a file copy can't overwrite them in place, so the lock-safe `apply` path swaps
21
- // them after stopping the service.
22
- const WINDOWS_MESH_BINARIES = ["easytier-core.exe", "easytier-cli.exe", "wintun.dll", "Packet.dll", "WinDivert64.sys", "vantaloom-mesh.exe"]
23
-
24
- const cliRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..")
25
- const repoCandidate = path.resolve(cliRoot, "..", "..")
26
- const installedConfigPath = path.join(cliRoot, "config.json")
27
- const defaultReleaseTag = "runtime-latest"
28
- const defaultRepo = "Timefiles404/Vantaloom-next"
29
- const defaultNpmRegistry = "https://registry.npmjs.org"
30
- const fallbackNpmRegistries = [
31
- "https://registry.npmmirror.com",
32
- ]
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 { buildRuntimePackage } from "./lib/package.mjs"
42
+ import {
43
+ detectNpmRegistry,
44
+ resolveNpmPackageWithFallback,
45
+ downloadNpmTarball,
46
+ downloadReleaseAsset,
47
+ findExtractedNpmPackage,
48
+ findExtractedPackage,
49
+ } from "./lib/registry.mjs"
50
+ import {
51
+ runCtl,
52
+ uninstallRuntime,
53
+ } from "./lib/lifecycle.mjs"
54
+ import {
55
+ runLogin,
56
+ runLogout,
57
+ } from "./lib/auth.mjs"
33
58
 
34
59
  // Inherit strict-ssl=false from npm/npx config, or respect NODE_TLS_REJECT_UNAUTHORIZED.
35
60
  // When npm runs us via npx with strict-ssl disabled, it sets npm_config_strict_ssl="false".
@@ -108,6 +133,12 @@ export async function main(argv) {
108
133
  case "uninstall":
109
134
  await uninstallRuntime(options)
110
135
  return
136
+ case "login":
137
+ await runLogin(options)
138
+ return
139
+ case "logout":
140
+ await runLogout(options)
141
+ return
111
142
  case "help":
112
143
  case "-h":
113
144
  case "--help":
@@ -131,8 +162,6 @@ async function installFromSource(options) {
131
162
  noStart: options.noStart,
132
163
  sourceRoot,
133
164
  update: options.update,
134
- withMesh: options.withMesh,
135
- skipMesh: options.skipMesh,
136
165
  })
137
166
 
138
167
  console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
@@ -147,8 +176,6 @@ async function installFromPackage(options) {
147
176
  const version = await applyPackage(packageRoot, prefix, {
148
177
  noStart: options.noStart,
149
178
  update: options.update,
150
- withMesh: options.withMesh,
151
- skipMesh: options.skipMesh,
152
179
  })
153
180
 
154
181
  console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
@@ -166,6 +193,7 @@ async function packageRuntime(options) {
166
193
  const { platform: builtPlatform, version } = await buildRuntimePackage(sourceRoot, packageRoot, {
167
194
  buildWeb: options.buildWeb,
168
195
  target: targetPlatform,
196
+ npmPackage: options.npmPackage,
169
197
  })
170
198
 
171
199
  if (options.npmPackage) {
@@ -190,533 +218,6 @@ async function packageRuntime(options) {
190
218
  console.log(`version: ${version}`)
191
219
  }
192
220
 
193
- async function buildRuntimePackage(sourceRoot, packageRoot, options) {
194
- const version = gitVersion(sourceRoot)
195
- const platform = options.target ?? platformId()
196
- const buildBin = path.join(packageRoot, "bin")
197
- const buildWeb = path.join(packageRoot, "web")
198
-
199
- removeKnownPath(packageRoot, path.dirname(packageRoot))
200
- mkdirSync(buildBin, { recursive: true })
201
-
202
- buildGo(sourceRoot, buildBin, "vantaloom-api", platform)
203
- buildGo(sourceRoot, buildBin, "vantaloom-agent", platform)
204
- buildGo(sourceRoot, buildBin, "vantaloomctl", platform)
205
- // Privileged EasyTier sidecar (runs the mesh node as a service on Windows/macOS).
206
- // Pure-Go (no CGO), so it cross-compiles cleanly for every target.
207
- buildGo(sourceRoot, buildBin, "vantaloom-mesh", platform)
208
- // Tray app requires CGO (systray), only buildable natively on Windows.
209
- if (platform.startsWith("win32") && process.platform === "win32") {
210
- try {
211
- buildGo(sourceRoot, buildBin, "vantaloom-tray", platform)
212
- } catch { /* optional: skip if systray build fails */ }
213
- }
214
-
215
- // Bundle the EasyTier mesh binaries (P2P virtual network) for the target platform.
216
- await copyEasyTier(sourceRoot, buildBin, platform)
217
-
218
- if (options.buildWeb) {
219
- runPnpm(["--filter", "vantaloom-app", "build"], { cwd: sourceRoot })
220
- }
221
-
222
- await copyStaticWeb(sourceRoot, buildWeb)
223
- await copyCliDirectory(path.join(packageRoot, "cli"), sourceRoot, runtimeConfigFromSource(sourceRoot))
224
- writeBuildManifest(packageRoot, version, platform)
225
-
226
- return { platform, version }
227
- }
228
-
229
- // copyEasyTier bundles the vendored EasyTier binaries (easytier-core/easytier-cli,
230
- // plus wintun.dll on Windows) for the target platform into the package bin/ dir.
231
- async function copyEasyTier(sourceRoot, buildBin, platform) {
232
- const vendorMap = {
233
- "win32-x64": "windows-x86_64",
234
- "darwin-arm64": "macos-aarch64",
235
- "linux-x64": "linux-x86_64",
236
- }
237
- const vendorName = vendorMap[platform]
238
- if (!vendorName) {
239
- console.warn(` warning: no EasyTier mapping for ${platform}; mesh disabled for this platform`)
240
- return
241
- }
242
- const srcDir = path.join(sourceRoot, "vendor", "easytier", vendorName, `easytier-${vendorName}`)
243
- if (!existsSync(srcDir)) {
244
- console.warn(` warning: EasyTier binaries not found at ${srcDir}; run vendor download first`)
245
- return
246
- }
247
- const isWin = platform.startsWith("win32")
248
- // Packet.dll + WinDivert64.sys are REQUIRED: easytier-core.exe statically
249
- // imports Packet.dll, so without it the process dies at launch with
250
- // STATUS_DLL_NOT_FOUND (0xC0000135) before writing any log. wintun.dll is the
251
- // TUN backend. easytier-cli.exe does not need Packet.dll (it still launches).
252
- const files = isWin
253
- ? ["easytier-core.exe", "easytier-cli.exe", "wintun.dll", "Packet.dll", "WinDivert64.sys"]
254
- : ["easytier-core", "easytier-cli"]
255
- let copied = 0
256
- for (const f of files) {
257
- const src = path.join(srcDir, f)
258
- if (!existsSync(src)) {
259
- console.warn(` warning: EasyTier file missing: ${f}`)
260
- continue
261
- }
262
- await cp(src, path.join(buildBin, f), { force: true })
263
- copied++
264
- }
265
- console.log(` bundled EasyTier ${vendorName} (${copied} files)`)
266
- }
267
-
268
- async function applyPackage(packageRoot, prefix, options) {
269
- assertRuntimePackage(packageRoot)
270
-
271
- const existingConfig = readInstalledConfig(prefix)
272
- const packageConfig = readJSONIfExists(path.join(packageRoot, "cli", "config.json"))
273
- const mergedConfig = mergeRuntimeConfig(packageConfig, existingConfig, {
274
- sourceRoot: options.sourceRoot,
275
- })
276
- const version = readVersion(packageRoot)
277
-
278
- mkdirSync(prefix, { recursive: true })
279
- const existingCtl = path.join(prefix, "bin", binaryName("vantaloomctl"))
280
- if (existsSync(existingCtl)) {
281
- spawnSync(existingCtl, ["stop", "--prefix", prefix], { stdio: "inherit" })
282
- }
283
-
284
- // Kill lingering tray process that may hold locks on bin/ (older versions
285
- // don't write tray.pid, so vantaloomctl stop won't find them).
286
- killTrayProcess(prefix)
287
-
288
- // On Windows the privileged mesh service holds its binaries open, so we can't
289
- // wipe or overwrite them with a plain copy. Detect that up front: if the
290
- // service is running, skip those files in the bin/ copy (the elevated `apply`
291
- // swaps them after stopping the service).
292
- const meshLocked =
293
- process.platform === "win32" &&
294
- meshServiceRunning(path.join(packageRoot, "bin"), "win32")
295
-
296
- // Copy package contents to install prefix
297
- for (const name of ["bin", "web", "cli"]) {
298
- const src = path.join(packageRoot, name)
299
- const dst = path.join(prefix, name)
300
- if (!existsSync(src)) {
301
- console.error(` warning: package missing ${name}/ directory`)
302
- continue
303
- }
304
- const copyOpts = { recursive: true, force: true, dereference: false }
305
- if (name === "bin" && meshLocked) {
306
- // Overwrite in place (don't wipe — that would fail on the locked files) and
307
- // skip the mesh binaries the running service holds open.
308
- copyOpts.filter = (s) => !WINDOWS_MESH_BINARIES.includes(path.basename(s))
309
- } else {
310
- removeKnownPath(dst, prefix)
311
- }
312
- await cp(src, dst, copyOpts)
313
- }
314
-
315
- // Ensure binaries are executable on Unix (cross-compiled from Windows they lose +x)
316
- const binDir = path.join(prefix, "bin")
317
- if (process.platform !== "win32" && existsSync(binDir)) {
318
- for (const entry of readdirSync(binDir)) {
319
- const binPath = path.join(binDir, entry)
320
- try { chmodSync(binPath, 0o755) } catch {}
321
- }
322
- }
323
-
324
- // Linux: grant easytier-core the TUN capability so the unprivileged runtime can
325
- // bring up the mesh virtual network. This is the one privileged step of the
326
- // install (a single sudo); the app itself stays unprivileged.
327
- ensureLinuxMeshCapabilities(binDir)
328
-
329
- // Windows/macOS: register (or lock-safely update) the privileged mesh service
330
- // that runs easytier-core. This is the one elevation of the install on those
331
- // platforms. Skipped for --no-start and gated to avoid prompting on every
332
- // source rebuild (see ensureMeshService).
333
- if (!options.noStart) {
334
- ensureMeshService(packageRoot, prefix, {
335
- sourceInstall: Boolean(options.sourceRoot),
336
- withMesh: Boolean(options.withMesh),
337
- skipMesh: Boolean(options.skipMesh),
338
- })
339
- }
340
-
341
- // Make the runtime start at login/boot (per-user, no elevation). Idempotent.
342
- if (!options.noStart && !options.skipAutostart) {
343
- enableRuntimeAutostart(prefix)
344
- }
345
-
346
- // Verify vantaloomctl binary exists before trying to run it
347
- const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
348
- if (!existsSync(ctlBin)) {
349
- const binContents = existsSync(binDir) ? readdirSync(binDir) : []
350
- const srcBinContents = existsSync(path.join(packageRoot, "bin")) ? readdirSync(path.join(packageRoot, "bin")) : []
351
- throw new Error(
352
- `vantaloomctl binary not found at ${ctlBin}\n` +
353
- ` installed bin/: [${binContents.join(", ")}]\n` +
354
- ` package bin/: [${srcBinContents.join(", ")}]\n` +
355
- ` platform: ${platformId()}\n` +
356
- ` This may indicate a corrupt download. Try again or install from source.`
357
- )
358
- }
359
-
360
- await writeLauncher(prefix)
361
- await writeText(path.join(prefix, "cli", "config.json"), `${JSON.stringify(mergedConfig, null, 2)}\n`)
362
- await writeText(path.join(prefix, "VERSION"), `${version}\n`)
363
- await cp(path.join(packageRoot, "manifest.json"), path.join(prefix, "manifest.json"), {
364
- force: true,
365
- })
366
-
367
- run(ctlBin, [
368
- "install",
369
- "--prefix",
370
- prefix,
371
- "--version",
372
- version,
373
- ])
374
-
375
- if (!options.noStart) {
376
- run(ctlBin, [
377
- "start",
378
- "--prefix",
379
- prefix,
380
- ])
381
- }
382
-
383
- return version
384
- }
385
-
386
- // ensureLinuxMeshCapabilities grants easytier-core the network capabilities it
387
- // needs to create a TUN device, so the unprivileged Vantaloom runtime can bring
388
- // up the EasyTier mesh without running as root. Re-run on every update because
389
- // replacing the binary clears file capabilities. Non-fatal: if it can't elevate
390
- // or setcap is missing, mesh just falls back to the Hub relay.
391
- function ensureLinuxMeshCapabilities(binDir) {
392
- if (process.platform !== "linux") {
393
- return
394
- }
395
- const core = path.join(binDir, "easytier-core")
396
- if (!existsSync(core)) {
397
- console.warn(" mesh: easytier-core not bundled; skipping TUN capability setup")
398
- return
399
- }
400
- const caps = "cap_net_admin,cap_net_bind_service+ep"
401
- const isRoot = typeof process.getuid === "function" && process.getuid() === 0
402
- console.log(` mesh: granting TUN capability to easytier-core${isRoot ? "" : " (sudo)"} ...`)
403
- const result = isRoot
404
- ? spawnSync("setcap", [caps, core], { stdio: "inherit" })
405
- : spawnSync("sudo", ["setcap", caps, core], { stdio: "inherit" })
406
- if (result.error || result.status !== 0) {
407
- console.warn(
408
- " mesh: could not set capabilities (P2P will fall back to the Hub relay).\n" +
409
- ` enable it manually with: sudo setcap ${caps} ${core}\n` +
410
- " (needs the 'setcap' tool, e.g. apt install libcap2-bin)"
411
- )
412
- }
413
- }
414
-
415
- // ensureMeshService registers (or lock-safely updates) the privileged EasyTier
416
- // sidecar service on Windows/macOS — the one elevation of the install on those
417
- // platforms. Linux uses setcap instead (see ensureLinuxMeshCapabilities).
418
- //
419
- // To keep the source/dev rebuild loop frictionless it only elevates when the
420
- // work is actually needed (service missing, or the mesh binaries changed) and,
421
- // for source installs, defers to a printed one-liner unless --with-mesh is set.
422
- function ensureMeshService(packageRoot, prefix, opts) {
423
- const platform = process.platform
424
- if (platform !== "win32" && platform !== "darwin") return // Linux: setcap path
425
- if (opts.skipMesh) return
426
-
427
- const pkgBin = path.join(packageRoot, "bin")
428
- const meshExe = path.join(pkgBin, binaryName("vantaloom-mesh"))
429
- if (!existsSync(meshExe)) return // sidecar not bundled
430
-
431
- if (!meshServiceNeedsApply(pkgBin, path.join(prefix, "bin"), platform)) {
432
- return // installed binaries current and service already managing them
433
- }
434
-
435
- if (opts.sourceInstall && !opts.withMesh) {
436
- printMeshManualHint(prefix, platform)
437
- return
438
- }
439
-
440
- if (platform === "win32") {
441
- elevateWindowsMeshApply(pkgBin, prefix)
442
- } else {
443
- elevateDarwinMeshInstall(prefix)
444
- }
445
- }
446
-
447
- // meshServiceNeedsApply reports whether the privileged service has to be
448
- // (re)applied: true if the installed mesh binaries are missing/outdated, or if
449
- // the service isn't currently up.
450
- function meshServiceNeedsApply(pkgBin, installBin, platform) {
451
- // On Windows compare every lockable mesh file (incl. Packet.dll/WinDivert/wintun)
452
- // so a missing or changed support DLL also triggers a (re)apply.
453
- const names = platform === "win32"
454
- ? WINDOWS_MESH_BINARIES
455
- : [binaryName("vantaloom-mesh"), binaryName("easytier-core")]
456
- for (const name of names) {
457
- const installed = path.join(installBin, name)
458
- if (!existsSync(installed)) return true
459
- const staged = path.join(pkgBin, name)
460
- if (existsSync(staged) && fileSha(staged) !== fileSha(installed)) return true
461
- }
462
- return !meshServiceRunning(pkgBin, platform)
463
- }
464
-
465
- // meshServiceRunning queries the OS service state via the (unprivileged) mesh
466
- // `status` command. binDir is where to find the vantaloom-mesh binary to run.
467
- function meshServiceRunning(binDir, platform) {
468
- const exe = path.join(binDir, binaryName("vantaloom-mesh"))
469
- if (!existsSync(exe)) return false
470
- const r = spawnSync(exe, ["status"], { encoding: "utf8" })
471
- if (r.error || typeof r.stdout !== "string") return false
472
- const out = r.stdout.toLowerCase()
473
- if (platform === "win32") return out.includes("running")
474
- return r.status === 0 && !out.includes("not loaded") && !out.includes("not installed")
475
- }
476
-
477
- function fileSha(file) {
478
- return createHash("sha256").update(readFileSync(file)).digest("hex")
479
- }
480
-
481
- // elevateWindowsMeshApply runs the lock-safe `apply` elevated (one UAC prompt).
482
- // It launches from the package copy so it can overwrite the installed binary.
483
- function elevateWindowsMeshApply(pkgBin, prefix) {
484
- const exe = path.join(pkgBin, "vantaloom-mesh.exe")
485
- console.log(" mesh: registering privileged P2P service (UAC prompt) ...")
486
- const argList = ["apply", "--pkg-bin", pkgBin, "--install-dir", prefix].map(psQuote).join(",")
487
- const ps = `$ErrorActionPreference='Stop'; $p = Start-Process -FilePath ${psQuote(exe)} -ArgumentList ${argList} -Verb RunAs -Wait -PassThru; exit $p.ExitCode`
488
- const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "inherit" })
489
- if (r.error || r.status !== 0) {
490
- const installed = path.join(prefix, "bin", "vantaloom-mesh.exe")
491
- console.warn(" mesh: service registration was cancelled or failed; P2P falls back to the Hub relay.")
492
- console.warn(` enable it later (as Administrator): "${installed}" install --install-dir "${prefix}"`)
493
- } else {
494
- console.log(" mesh: privileged P2P service active")
495
- }
496
- }
497
-
498
- // elevateDarwinMeshInstall registers the LaunchDaemon via sudo (one prompt). The
499
- // runtime already copied the fresh binary into place; install reloads the daemon.
500
- function elevateDarwinMeshInstall(prefix) {
501
- const exe = path.join(prefix, "bin", "vantaloom-mesh")
502
- console.log(" mesh: registering privileged P2P service (sudo) ...")
503
- const r = spawnSync("sudo", [exe, "install", "--install-dir", prefix], { stdio: "inherit" })
504
- if (r.error || r.status !== 0) {
505
- console.warn(" mesh: LaunchDaemon registration failed; P2P falls back to the Hub relay.")
506
- console.warn(` enable it later: sudo "${exe}" install --install-dir "${prefix}"`)
507
- } else {
508
- console.log(" mesh: privileged P2P service active")
509
- }
510
- }
511
-
512
- function printMeshManualHint(prefix, platform) {
513
- console.log(" mesh: P2P service not auto-registered for source installs.")
514
- if (platform === "win32") {
515
- const exe = path.join(prefix, "bin", "vantaloom-mesh.exe")
516
- console.log(` enable it once (as Administrator): "${exe}" install --install-dir "${prefix}"`)
517
- console.log(" or re-run install/update with --with-mesh")
518
- } else {
519
- const exe = path.join(prefix, "bin", "vantaloom-mesh")
520
- console.log(` enable it once: sudo "${exe}" install --install-dir "${prefix}"`)
521
- console.log(" or re-run install/update with --with-mesh")
522
- }
523
- }
524
-
525
- // psQuote wraps a value as a PowerShell single-quoted string literal.
526
- function psQuote(value) {
527
- return "'" + String(value).replace(/'/g, "''") + "'"
528
- }
529
-
530
- // uninstallRuntime tears down a local install: stop the runtime, remove the
531
- // privileged mesh service (elevated), then delete the install directory.
532
- async function uninstallRuntime(options) {
533
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
534
- if (!existsSync(prefix)) {
535
- console.log(`Vantaloom is not installed at ${prefix}`)
536
- return
537
- }
538
- console.log(`Uninstalling Vantaloom from ${prefix} ...`)
539
-
540
- // 1. Stop the runtime (api/agent/web/tray). Best-effort.
541
- const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
542
- if (existsSync(ctlBin)) {
543
- spawnSync(ctlBin, ["stop", "--prefix", prefix], { stdio: "inherit" })
544
- }
545
- killTrayProcess(prefix)
546
-
547
- // 2. Remove the privileged mesh service (releases TUN adapter + file locks).
548
- removeMeshService(prefix, options)
549
-
550
- // 2b. Remove the login-autostart entry (lives outside the install dir).
551
- disableRuntimeAutostart()
552
-
553
- // 3. Delete the install directory.
554
- try {
555
- const parent = path.dirname(prefix)
556
- removeKnownPath(prefix, parent)
557
- console.log(`removed ${prefix}`)
558
- } catch (error) {
559
- console.warn(` could not remove ${prefix}: ${error.message}`)
560
- console.warn(" some files may still be locked; re-run after closing Vantaloom processes.")
561
- }
562
-
563
- console.log("Vantaloom uninstalled.")
564
- console.log("note: the install dir was left out of PATH edits; remove the bin/ entry from your shell profile if you added it.")
565
- }
566
-
567
- function removeMeshService(prefix, options) {
568
- const platform = process.platform
569
- if (platform !== "win32" && platform !== "darwin") return // Linux: nothing registered
570
- const exe = path.join(prefix, "bin", binaryName("vantaloom-mesh"))
571
- if (!existsSync(exe)) return
572
- if (options.skipMesh) return
573
-
574
- if (platform === "win32") {
575
- if (!meshServiceRunningOrInstalled(path.join(prefix, "bin"))) return
576
- console.log(" mesh: removing privileged P2P service (UAC prompt) ...")
577
- const ps = `$ErrorActionPreference='Stop'; $p = Start-Process -FilePath ${psQuote(exe)} -ArgumentList 'uninstall' -Verb RunAs -Wait -PassThru; exit $p.ExitCode`
578
- const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "inherit" })
579
- if (r.error || r.status !== 0) {
580
- console.warn(` mesh: could not remove service; run manually (as Administrator): "${exe}" uninstall`)
581
- }
582
- } else {
583
- console.log(" mesh: removing privileged P2P service (sudo) ...")
584
- const r = spawnSync("sudo", [exe, "uninstall"], { stdio: "inherit" })
585
- if (r.error || r.status !== 0) {
586
- console.warn(` mesh: could not remove service; run manually: sudo "${exe}" uninstall`)
587
- }
588
- }
589
- }
590
-
591
- // meshServiceRunningOrInstalled reports whether the Windows service exists at
592
- // all (running or stopped), so uninstall only prompts for elevation when there
593
- // is actually something to remove.
594
- function meshServiceRunningOrInstalled(binDir) {
595
- const exe = path.join(binDir, "vantaloom-mesh.exe")
596
- if (!existsSync(exe)) return false
597
- const r = spawnSync(exe, ["status"], { encoding: "utf8" })
598
- if (r.error || typeof r.stdout !== "string") return false
599
- return !r.stdout.toLowerCase().includes("not installed")
600
- }
601
-
602
- // ── Runtime login-autostart (no elevation) ──
603
- // Start the local runtime (api/agent/web/tray) at login/boot using only
604
- // per-user mechanisms — no UAC/sudo. This is separate from the privileged mesh
605
- // service: the mesh sidecar autostarts via the OS service manager; this brings
606
- // up the unprivileged runtime that joins the mesh and serves the local API.
607
-
608
- const AUTOSTART_LABEL = "online.timefiles.vantaloom.runtime"
609
- // PowerShell HKCU: drive path. Set-ItemProperty handles values with spaces and
610
- // embedded quotes cleanly (reg.exe's /d escaping is brittle), and the Run key is
611
- // not blocked by Controlled Folder Access the way the Startup folder is.
612
- const WINDOWS_RUN_KEY = "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
613
- const WINDOWS_RUN_VALUE = "Vantaloom"
614
-
615
- function enableRuntimeAutostart(prefix) {
616
- try {
617
- if (process.platform === "win32") {
618
- // A .vbs launches the runtime with a hidden window (no console flash); the
619
- // HKCU Run key runs it at login without elevation.
620
- const launcher = path.join(prefix, "vantaloom.cmd")
621
- const vbsPath = path.join(prefix, "autostart.vbs")
622
- const vbs = `' Vantaloom runtime autostart (hidden)\r\nCreateObject("WScript.Shell").Run """${launcher}"" start", 0, False\r\n`
623
- writeFileSync(vbsPath, vbs)
624
- const result = spawnSync(
625
- "powershell",
626
- [
627
- "-NoProfile",
628
- "-NonInteractive",
629
- "-Command",
630
- `Set-ItemProperty -Path '${WINDOWS_RUN_KEY}' -Name '${WINDOWS_RUN_VALUE}' -Value 'wscript.exe "${vbsPath}"'`,
631
- ],
632
- { stdio: "ignore" }
633
- )
634
- if (result.error || result.status !== 0) {
635
- console.warn(" autostart: could not register login entry (HKCU Run)")
636
- } else {
637
- console.log(" autostart: enabled (login Run key)")
638
- }
639
- return
640
- }
641
- if (process.platform === "darwin") {
642
- const launcher = path.join(prefix, "vantaloom")
643
- const dir = path.join(os.homedir(), "Library", "LaunchAgents")
644
- mkdirSync(dir, { recursive: true })
645
- const plistPath = path.join(dir, `${AUTOSTART_LABEL}.plist`)
646
- // RunAtLoad only (no KeepAlive): `vantaloom start` spawns detached and exits.
647
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
648
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
649
- <plist version="1.0"><dict>
650
- <key>Label</key><string>${AUTOSTART_LABEL}</string>
651
- <key>ProgramArguments</key><array><string>${launcher}</string><string>start</string></array>
652
- <key>RunAtLoad</key><true/>
653
- </dict></plist>
654
- `
655
- writeFileSync(plistPath, plist)
656
- spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" })
657
- spawnSync("launchctl", ["load", "-w", plistPath], { stdio: "ignore" })
658
- console.log(" autostart: enabled (LaunchAgent)")
659
- return
660
- }
661
- // Linux: user systemd unit + linger (no root).
662
- const launcher = path.join(prefix, "vantaloom")
663
- const dir = path.join(os.homedir(), ".config", "systemd", "user")
664
- mkdirSync(dir, { recursive: true })
665
- const unit = `[Unit]
666
- Description=Vantaloom local runtime
667
- After=network-online.target
668
- Wants=network-online.target
669
-
670
- [Service]
671
- Type=oneshot
672
- RemainAfterExit=yes
673
- ExecStart=${launcher} start
674
- ExecStop=${launcher} stop
675
-
676
- [Install]
677
- WantedBy=default.target
678
- `
679
- writeFileSync(path.join(dir, "vantaloom-runtime.service"), unit)
680
- spawnSync("loginctl", ["enable-linger"], { stdio: "ignore" })
681
- spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" })
682
- spawnSync("systemctl", ["--user", "enable", "vantaloom-runtime.service"], { stdio: "ignore" })
683
- console.log(" autostart: enabled (systemd user unit)")
684
- } catch (error) {
685
- console.warn(` autostart: could not enable (${error.message})`)
686
- }
687
- }
688
-
689
- function disableRuntimeAutostart() {
690
- try {
691
- if (process.platform === "win32") {
692
- // Remove the Run-key entry; the autostart.vbs lives in the install dir and
693
- // is deleted with it.
694
- spawnSync(
695
- "powershell",
696
- ["-NoProfile", "-NonInteractive", "-Command", `Remove-ItemProperty -Path '${WINDOWS_RUN_KEY}' -Name '${WINDOWS_RUN_VALUE}' -ErrorAction SilentlyContinue`],
697
- { stdio: "ignore" }
698
- )
699
- return
700
- }
701
- if (process.platform === "darwin") {
702
- const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`)
703
- if (existsSync(plistPath)) {
704
- spawnSync("launchctl", ["unload", "-w", plistPath], { stdio: "ignore" })
705
- rmSync(plistPath, { force: true })
706
- }
707
- return
708
- }
709
- const unit = path.join(os.homedir(), ".config", "systemd", "user", "vantaloom-runtime.service")
710
- if (existsSync(unit)) {
711
- spawnSync("systemctl", ["--user", "disable", "vantaloom-runtime.service"], { stdio: "ignore" })
712
- rmSync(unit, { force: true })
713
- spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" })
714
- }
715
- } catch (error) {
716
- console.warn(` autostart: could not disable (${error.message})`)
717
- }
718
- }
719
-
720
221
  async function syncFromNpmRegistry(options, action) {
721
222
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
722
223
  const installedConfig = readInstalledConfig(prefix)
@@ -754,8 +255,6 @@ async function syncFromNpmRegistry(options, action) {
754
255
  runtimeVersion: options.runtimeVersion ? resolved.version : "latest",
755
256
  npmRegistry: resolved.registry,
756
257
  update: action === "update",
757
- withMesh: options.withMesh,
758
- skipMesh: options.skipMesh,
759
258
  })
760
259
 
761
260
  console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
@@ -799,8 +298,6 @@ async function syncFromRelease(options, action) {
799
298
  noStart: options.noStart,
800
299
  sourceRoot: installedConfig.sourceRoot,
801
300
  update: action === "update",
802
- withMesh: options.withMesh,
803
- skipMesh: options.skipMesh,
804
301
  })
805
302
 
806
303
  console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
@@ -813,203 +310,6 @@ async function syncFromRelease(options, action) {
813
310
  }
814
311
  }
815
312
 
816
- async function downloadReleaseAsset({ repo, releaseTag, assetName, target, token }) {
817
- const releaseUrl = `https://api.github.com/repos/${repo}/releases/tags/${releaseTag}`
818
- const headers = {
819
- "User-Agent": "vantaloom-cli",
820
- }
821
- const githubToken = token || process.env.VANTALOOM_GITHUB_TOKEN || process.env.GITHUB_TOKEN
822
- if (githubToken) {
823
- headers.Authorization = `Bearer ${githubToken}`
824
- }
825
-
826
- const releaseResponse = await fetch(releaseUrl, { headers })
827
- if (!releaseResponse.ok) {
828
- const detail = await releaseResponse.text().catch(() => "")
829
- const authHint = releaseResponse.status === 404 || releaseResponse.status === 403
830
- ? " If the repository is private, set VANTALOOM_GITHUB_TOKEN to a GitHub token that can read releases."
831
- : ""
832
- throw new Error(`failed to inspect ${repo}@${releaseTag}: HTTP ${releaseResponse.status}.${authHint}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
833
- }
834
-
835
- const release = await releaseResponse.json()
836
- const asset = release.assets?.find((asset) => asset.name === assetName)
837
- if (!asset) {
838
- const names = release.assets?.map((asset) => asset.name).join(", ") || "none"
839
- throw new Error(`missing release asset ${assetName} in ${repo}@${releaseTag}; available assets: ${names}`)
840
- }
841
-
842
- const response = await fetch(asset.url, {
843
- headers: {
844
- ...headers,
845
- Accept: "application/octet-stream",
846
- },
847
- })
848
- if (!response.ok) {
849
- const detail = await response.text().catch(() => "")
850
- const authHint = response.status === 404 || response.status === 403
851
- ? " If the repository is private, set VANTALOOM_GITHUB_TOKEN to a GitHub token that can read releases."
852
- : ""
853
- throw new Error(`failed to download ${assetName} from ${repo}@${releaseTag}: HTTP ${response.status}.${authHint}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
854
- }
855
-
856
- const buffer = Buffer.from(await response.arrayBuffer())
857
- await writeFile(target, buffer)
858
- }
859
-
860
- function detectNpmRegistry() {
861
- // 1. NPM_CONFIG_REGISTRY env var (highest priority, set by npm/npx when running)
862
- if (process.env.NPM_CONFIG_REGISTRY) {
863
- return process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY
864
- }
865
- // npm also sets the lowercase variant
866
- if (process.env.npm_config_registry) {
867
- return process.env.npm_config_registry
868
- }
869
-
870
- // 2. Read user .npmrc
871
- try {
872
- const npmrcPaths = [
873
- path.join(os.homedir(), ".npmrc"),
874
- ]
875
- // Also check project-level .npmrc
876
- const localNpmrc = path.resolve(".npmrc")
877
- if (localNpmrc !== npmrcPaths[0]) {
878
- npmrcPaths.unshift(localNpmrc)
879
- }
880
- for (const npmrcPath of npmrcPaths) {
881
- if (existsSync(npmrcPath)) {
882
- const content = readFileSync(npmrcPath, "utf8")
883
- const match = content.match(/^\s*registry\s*=\s*(.+)/m)
884
- if (match) {
885
- return match[1].trim()
886
- }
887
- }
888
- }
889
- } catch {
890
- // Ignore .npmrc read errors
891
- }
892
-
893
- return ""
894
- }
895
-
896
- async function resolveNpmPackageWithFallback({ registries, name, version }) {
897
- const errors = []
898
- for (const registry of registries) {
899
- try {
900
- const result = await resolveNpmPackage({ registry, name, version })
901
- return { ...result, registry }
902
- } catch (error) {
903
- const causeCode = error?.cause?.code || ""
904
- const isNetworkError = causeCode === "ECONNREFUSED"
905
- || causeCode === "ENOTFOUND"
906
- || causeCode === "ETIMEDOUT"
907
- || causeCode === "ECONNRESET"
908
- || causeCode === "UND_ERR_CONNECT_TIMEOUT"
909
- || (error instanceof TypeError && error.message === "fetch failed")
910
- const isSslError = causeCode === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
911
- || causeCode === "CERT_HAS_EXPIRED"
912
- || causeCode === "DEPTH_ZERO_SELF_SIGNED_CERT"
913
- || causeCode === "SELF_SIGNED_CERT_IN_CHAIN"
914
- || causeCode === "ERR_TLS_CERT_ALTNAME_INVALID"
915
- const isRetryable = isNetworkError || isSslError
916
- errors.push({ registry, error, isRetryable, isSslError })
917
-
918
- if (isRetryable && registries.length > 1) {
919
- const hint = isSslError ? " (SSL certificate error)" : ""
920
- console.error(`vantaloom: ${registry} unreachable${hint}, trying next registry...`)
921
- continue
922
- }
923
- if (isSslError) {
924
- throw new Error(
925
- `SSL certificate error connecting to ${registry}: ${causeCode}\n` +
926
- ` Fix: run with --no-strict-ssl, or set npm config: npm config set strict-ssl false`
927
- )
928
- }
929
- // Non-network error (404, parse error, etc.) — don't try fallbacks
930
- throw error
931
- }
932
- }
933
-
934
- // All registries failed
935
- const hasSslError = errors.some((e) => e.isSslError)
936
- const tried = errors.map((e) => e.registry).join(", ")
937
- const sslHint = hasSslError
938
- ? `\n SSL fix: run with --no-strict-ssl, or set npm config: npm config set strict-ssl false`
939
- : ""
940
- throw new Error(
941
- `all registries unreachable (tried: ${tried}). ` +
942
- `Check your network or specify --npm-registry <url>${sslHint}`
943
- )
944
- }
945
-
946
- async function resolveNpmPackage({ registry, name, version }) {
947
- const metadataUrl = `${registry}/${encodeURIComponent(name).replace("%2F", "%2f")}`
948
- let response
949
- try {
950
- response = await fetch(metadataUrl, {
951
- headers: {
952
- Accept: "application/vnd.npm.install-v1+json",
953
- "User-Agent": "vantaloom-cli",
954
- },
955
- signal: AbortSignal.timeout(15000),
956
- })
957
- } catch (error) {
958
- if (error instanceof TypeError && error.message === "fetch failed") {
959
- const causeCode = error.cause?.code || ""
960
- const causeMsg = causeCode || error.cause?.message || String(error.cause || "")
961
- throw Object.assign(
962
- new Error(`cannot reach registry ${registry} (${causeMsg})`),
963
- { cause: error.cause }
964
- )
965
- }
966
- throw error
967
- }
968
- if (!response.ok) {
969
- const detail = await response.text().catch(() => "")
970
- throw new Error(`failed to inspect npm package ${name}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
971
- }
972
-
973
- const metadata = await response.json()
974
- const selectedVersion = metadata["dist-tags"]?.[version] ?? version
975
- const packageVersion = metadata.versions?.[selectedVersion]
976
- if (!packageVersion?.dist?.tarball) {
977
- const available = Object.keys(metadata.versions ?? {}).slice(-8).join(", ") || "none"
978
- throw new Error(`missing npm package ${name}@${version}; available versions: ${available}`)
979
- }
980
- return {
981
- version: selectedVersion,
982
- tarball: packageVersion.dist.tarball,
983
- }
984
- }
985
-
986
- async function downloadNpmTarball({ tarballUrl, target, packageName, version }) {
987
- console.log(` downloading ${packageName}@${version}...`)
988
- let response
989
- try {
990
- response = await fetch(tarballUrl, {
991
- headers: {
992
- "User-Agent": "vantaloom-cli",
993
- },
994
- signal: AbortSignal.timeout(120000),
995
- })
996
- } catch (error) {
997
- const causeCode = error?.cause?.code || ""
998
- const causeMsg = causeCode || error?.cause?.message || error.message || ""
999
- const isSsl = causeCode.includes("CERT") || causeCode === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
1000
- const hint = isSsl ? `\n Fix: run with --no-strict-ssl` : ""
1001
- throw new Error(`failed to download ${packageName}@${version} from ${tarballUrl}: ${causeMsg}${hint}`)
1002
- }
1003
- if (!response.ok) {
1004
- const detail = await response.text().catch(() => "")
1005
- throw new Error(`failed to download ${packageName}@${version}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
1006
- }
1007
-
1008
- const buffer = Buffer.from(await response.arrayBuffer())
1009
- await writeFile(target, buffer)
1010
- console.log(` downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`)
1011
- }
1012
-
1013
313
  function shouldUseSourceInstall(options) {
1014
314
  return Boolean(options.local || options.source || options.buildWeb)
1015
315
  }
@@ -1018,340 +318,12 @@ function shouldUseReleaseSync(options) {
1018
318
  return Boolean(options.repo || options.remote || options.releaseTag || options.githubToken)
1019
319
  }
1020
320
 
1021
- function assertRuntimePackage(packageRoot) {
1022
- for (const name of ["bin", "web", "cli", "manifest.json"]) {
1023
- if (!existsSync(path.join(packageRoot, name))) {
1024
- throw new Error(`invalid Vantaloom package, missing ${name}: ${packageRoot}`)
1025
- }
1026
- }
1027
- }
1028
-
1029
- function readVersion(packageRoot) {
1030
- const versionPath = path.join(packageRoot, "VERSION")
1031
- if (existsSync(versionPath)) {
1032
- return readFileSync(versionPath, "utf8").trim() || "dev"
1033
- }
1034
- const manifest = readJSONIfExists(path.join(packageRoot, "manifest.json"))
1035
- return manifest.version ?? "dev"
1036
- }
1037
-
1038
- function readInstalledConfig(prefix) {
1039
- return readJSONIfExists(path.join(prefix, "cli", "config.json"))
1040
- }
1041
-
1042
- function readJSONIfExists(filePath) {
1043
- if (!existsSync(filePath)) {
1044
- return {}
1045
- }
1046
- return JSON.parse(readFileSync(filePath, "utf8"))
1047
- }
1048
-
1049
- function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
1050
- const merged = { ...packageConfig }
1051
- for (const key of ["sourceRoot", "remote", "repo", "releaseTag", "runtimePackage", "runtimeVersion", "npmRegistry"]) {
1052
- if (!merged[key] && existingConfig[key]) {
1053
- merged[key] = existingConfig[key]
1054
- }
1055
- }
1056
- if (overrides.sourceRoot) {
1057
- merged.sourceRoot = overrides.sourceRoot
1058
- }
1059
- if (overrides.runtimePackage) {
1060
- merged.runtimePackage = overrides.runtimePackage
1061
- }
1062
- if (overrides.runtimeVersion) {
1063
- merged.runtimeVersion = overrides.runtimeVersion
1064
- }
1065
- if (overrides.npmRegistry) {
1066
- merged.npmRegistry = overrides.npmRegistry
1067
- }
1068
- if (!merged.repo) {
1069
- merged.repo = defaultRepo
1070
- }
1071
- if (!merged.releaseTag) {
1072
- merged.releaseTag = defaultReleaseTag
1073
- }
1074
- // Always force runtimePackage to match the running platform — a cross-compiled
1075
- // package may carry a config for a different platform (e.g. win32 inside darwin).
1076
- merged.runtimePackage = runtimePackageName(platformId())
1077
- if (!merged.runtimeVersion) {
1078
- merged.runtimeVersion = "latest"
1079
- }
1080
- if (!merged.npmRegistry) {
1081
- merged.npmRegistry = defaultNpmRegistry
1082
- }
1083
- return merged
1084
- }
1085
-
1086
- function runtimeConfigFromSource(sourceRoot) {
1087
- const remote = gitRemoteUrl(sourceRoot)
1088
- const repo = inferGitHubRepo(remote) || defaultRepo
1089
- return {
1090
- ...(process.env.GITHUB_ACTIONS ? {} : { sourceRoot }),
1091
- ...(remote ? { remote } : {}),
1092
- repo,
1093
- releaseTag: defaultReleaseTag,
1094
- runtimePackage: runtimePackageName(platformId()),
1095
- runtimeVersion: "latest",
1096
- npmRegistry: detectNpmRegistry() || defaultNpmRegistry,
1097
- }
1098
- }
1099
-
1100
- function findExtractedPackage(extractRoot, platform) {
1101
- const expected = path.join(extractRoot, `vantaloom-${platform}`)
1102
- if (existsSync(expected)) {
1103
- return expected
1104
- }
1105
-
1106
- const directories = readdirSync(extractRoot, { withFileTypes: true })
1107
- .filter((entry) => entry.isDirectory())
1108
- .map((entry) => path.join(extractRoot, entry.name))
1109
- if (directories.length === 1) {
1110
- return directories[0]
1111
- }
1112
-
1113
- throw new Error(`could not find extracted Vantaloom package in ${extractRoot}`)
1114
- }
1115
-
1116
- function findExtractedNpmPackage(extractRoot) {
1117
- const expected = path.join(extractRoot, "package")
1118
- if (existsSync(expected)) {
1119
- return expected
1120
- }
1121
-
1122
- const directories = readdirSync(extractRoot, { withFileTypes: true })
1123
- .filter((entry) => entry.isDirectory())
1124
- .map((entry) => path.join(extractRoot, entry.name))
1125
- if (directories.length === 1) {
1126
- return directories[0]
1127
- }
1128
-
1129
- throw new Error(`could not find extracted npm package in ${extractRoot}`)
1130
- }
1131
-
1132
- function tryFindSourceRoot() {
1133
- try {
1134
- return findSourceRoot()
1135
- } catch {
1136
- return ""
1137
- }
1138
- }
1139
-
1140
- function tryAssertSourceRoot(sourceRoot) {
1141
- try {
1142
- return assertSourceRoot(sourceRoot)
1143
- } catch {
1144
- return ""
1145
- }
1146
- }
1147
-
1148
- function gitRemoteUrl(sourceRoot) {
1149
- const result = spawnSync("git", ["remote", "get-url", "origin"], {
1150
- cwd: sourceRoot,
1151
- encoding: "utf8",
1152
- })
1153
- if (result.status === 0) {
1154
- return result.stdout.trim()
1155
- }
1156
- return ""
1157
- }
1158
-
1159
- function inferGitHubRepo(remote) {
1160
- if (!remote) {
1161
- return ""
1162
- }
1163
- const normalized = remote.replace(/\.git$/, "")
1164
- const httpsMatch = normalized.match(/github\.com[:/]([^/]+\/[^/]+)$/)
1165
- if (httpsMatch) {
1166
- return httpsMatch[1]
1167
- }
1168
- const sshMatch = normalized.match(/^[^:]+:([^/]+\/[^/]+)$/)
1169
- return sshMatch?.[1] ?? ""
1170
- }
1171
-
1172
- async function copyStaticWeb(sourceRoot, buildWeb) {
1173
- const exportRoot = path.join(sourceRoot, "apps", "vantaloom", "out")
1174
- if (!existsSync(exportRoot)) {
1175
- throw new Error(
1176
- "missing Next static export output; let GitHub CI run production build, or pass --build-web for a local one-off build"
1177
- )
1178
- }
1179
-
1180
- removeKnownPath(buildWeb, path.dirname(buildWeb))
1181
- await copyDir(exportRoot, buildWeb)
1182
- }
1183
-
1184
- async function copyCliDirectory(target, sourceRoot, config) {
1185
- const sourceCliRoot = path.join(sourceRoot, "packages", "cli")
1186
- if (!existsSync(path.join(sourceCliRoot, "bin", "vantaloom.mjs"))) {
1187
- throw new Error(`missing source CLI package: ${sourceCliRoot}`)
1188
- }
1189
- removeKnownPath(target, path.dirname(target))
1190
- mkdirSync(target, { recursive: true })
1191
- await copyDir(sourceCliRoot, target)
1192
- await writeText(
1193
- path.join(target, "config.json"),
1194
- `${JSON.stringify(config, null, 2)}\n`
1195
- )
1196
- }
1197
-
1198
- async function writeLauncher(prefix) {
1199
- if (process.platform === "win32") {
1200
- await writeText(
1201
- path.join(prefix, "vantaloom.cmd"),
1202
- `@echo off\r\nnode "%~dp0cli\\bin\\vantaloom.mjs" %*\r\n`
1203
- )
1204
- } else {
1205
- const launcher = `#!/usr/bin/env sh\nexec node "$(dirname "$0")/cli/bin/vantaloom.mjs" "$@"\n`
1206
- const launcherPath = path.join(prefix, "vantaloom")
1207
- await writeText(launcherPath, launcher)
1208
- chmodSync(launcherPath, 0o755)
1209
- }
1210
- }
1211
-
1212
- function runCtl(command, options) {
1213
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
1214
- const ctl = path.join(prefix, "bin", binaryName("vantaloomctl"))
1215
- if (!existsSync(ctl)) {
1216
- throw new Error(`missing installed runtime at ${prefix}; run "vantaloom install" first`)
1217
- }
1218
-
1219
- const args = [command, "--prefix", prefix]
1220
- if (options.component) {
1221
- args.push("--component", options.component)
1222
- }
1223
- run(ctl, args)
1224
- }
1225
-
1226
321
  function printPaths(options) {
1227
322
  const sourceRoot = findSourceRoot(options.source)
1228
323
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
1229
324
  console.log(JSON.stringify({ sourceRoot, prefix }, null, 2))
1230
325
  }
1231
326
 
1232
- function buildGo(sourceRoot, buildBin, name, targetPlatform) {
1233
- const goEnv = targetPlatform ? platformToGoEnv(targetPlatform) : {}
1234
- const isWindowsTarget = targetPlatform
1235
- ? targetPlatform.startsWith("win32")
1236
- : process.platform === "win32"
1237
- const ext = isWindowsTarget ? ".exe" : ""
1238
- const ldflags = name === "vantaloom-tray" ? "-s -w -H windowsgui" : "-s -w"
1239
- run("go", [
1240
- "build",
1241
- "-ldflags", ldflags,
1242
- "-o",
1243
- path.join(buildBin, `${name}${ext}`),
1244
- `./apps/api/cmd/${name}`,
1245
- ], { cwd: sourceRoot, env: { ...process.env, ...goEnv } })
1246
- }
1247
-
1248
- async function copyDir(source, destination, options = {}) {
1249
- if (!existsSync(source)) {
1250
- throw new Error(`missing source directory: ${source}`)
1251
- }
1252
- removeKnownPath(destination, path.dirname(destination))
1253
- mkdirSync(destination, { recursive: true })
1254
- await cp(source, destination, {
1255
- recursive: true,
1256
- force: true,
1257
- dereference: options.dereference ?? true,
1258
- })
1259
- }
1260
-
1261
- function writeBuildManifest(buildRoot, version, platform) {
1262
- const components = ["api", "agent", "web", "ctl"]
1263
- if (platform.startsWith("win32")) {
1264
- components.push("tray")
1265
- }
1266
- writeFileSync(path.join(buildRoot, "VERSION"), `${version}\n`)
1267
- writeFileSync(
1268
- path.join(buildRoot, "manifest.json"),
1269
- `${JSON.stringify(
1270
- {
1271
- name: "Vantaloom Local Runtime",
1272
- version,
1273
- platform,
1274
- updatedAt: new Date().toISOString(),
1275
- components,
1276
- },
1277
- null,
1278
- 2
1279
- )}\n`
1280
- )
1281
- }
1282
-
1283
- function writeRuntimePackageMetadata(packageRoot, sourceRoot, platform) {
1284
- const version = npmPackageVersion(sourceRoot)
1285
- const { os: runtimeOS, cpu } = parsePlatformId(platform)
1286
- const name = runtimePackageName(platform)
1287
- writeFileSync(
1288
- path.join(packageRoot, "package.json"),
1289
- `${JSON.stringify(
1290
- {
1291
- name,
1292
- version,
1293
- private: false,
1294
- description: `Vantaloom local runtime for ${platform}.`,
1295
- type: "module",
1296
- os: [runtimeOS],
1297
- cpu: [cpu],
1298
- files: ["bin", "web", "cli", "manifest.json", "VERSION", "README.md"],
1299
- publishConfig: {
1300
- access: "public",
1301
- },
1302
- engines: {
1303
- node: ">=20",
1304
- },
1305
- },
1306
- null,
1307
- 2
1308
- )}\n`
1309
- )
1310
- writeFileSync(
1311
- path.join(packageRoot, "README.md"),
1312
- `# ${name}\n\nPlatform runtime package for Vantaloom ${platform}. Install @vantaloom/cli instead of this package directly.\n`
1313
- )
1314
- }
1315
-
1316
- function findSourceRoot(sourceOption) {
1317
- if (sourceOption) {
1318
- return assertSourceRoot(path.resolve(sourceOption))
1319
- }
1320
- if (process.env.VANTALOOM_SOURCE) {
1321
- return assertSourceRoot(path.resolve(process.env.VANTALOOM_SOURCE))
1322
- }
1323
- if (existsSync(installedConfigPath)) {
1324
- const config = JSON.parse(readFileSync(installedConfigPath, "utf8"))
1325
- if (config.sourceRoot) {
1326
- return assertSourceRoot(path.resolve(config.sourceRoot))
1327
- }
1328
- }
1329
- return assertSourceRoot(repoCandidate)
1330
- }
1331
-
1332
- function npmPackageVersion(sourceRoot) {
1333
- const packageJSON = readJSONIfExists(path.join(sourceRoot, "packages", "cli", "package.json"))
1334
- return packageJSON.version ?? "0.0.0"
1335
- }
1336
-
1337
- function assertSourceRoot(sourceRoot) {
1338
- if (!existsSync(path.join(sourceRoot, "apps", "api", "go.mod"))) {
1339
- throw new Error(`not a Vantaloom source root: ${sourceRoot}`)
1340
- }
1341
- return sourceRoot
1342
- }
1343
-
1344
- function gitVersion(sourceRoot) {
1345
- const result = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
1346
- cwd: sourceRoot,
1347
- encoding: "utf8",
1348
- })
1349
- if (result.status === 0) {
1350
- return result.stdout.trim() || "dev"
1351
- }
1352
- return "dev"
1353
- }
1354
-
1355
327
  function parseOptions(args) {
1356
328
  const options = {}
1357
329
  for (let index = 0; index < args.length; index += 1) {
@@ -1373,6 +345,9 @@ function parseOptions(args) {
1373
345
  case "runtime-package":
1374
346
  case "runtime-version":
1375
347
  case "npm-registry":
348
+ case "hub":
349
+ case "email":
350
+ case "password":
1376
351
  options[toCamel(key)] = inlineValue ?? args[++index]
1377
352
  if (!options[toCamel(key)]) {
1378
353
  throw new Error(`missing value for --${key}`)
@@ -1399,12 +374,6 @@ function parseOptions(args) {
1399
374
  case "local":
1400
375
  options.local = true
1401
376
  break
1402
- case "with-mesh":
1403
- options.withMesh = true
1404
- break
1405
- case "skip-mesh":
1406
- options.skipMesh = true
1407
- break
1408
377
  case "skip-autostart":
1409
378
  options.skipAutostart = true
1410
379
  break
@@ -1418,215 +387,15 @@ function parseOptions(args) {
1418
387
  return options
1419
388
  }
1420
389
 
1421
- function safeDirectory(value) {
1422
- const full = path.resolve(value)
1423
- const parsed = path.parse(full)
1424
- if (full === parsed.root) {
1425
- throw new Error(`refusing to operate on unsafe directory: ${value}`)
1426
- }
1427
- return full
1428
- }
1429
-
1430
- function removeKnownPath(target, expectedParent) {
1431
- if (!existsSync(target)) {
1432
- return
1433
- }
1434
- const full = path.resolve(target)
1435
- const parent = path.resolve(expectedParent)
1436
- const prefix = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`
1437
- if (!full.startsWith(prefix)) {
1438
- throw new Error(`refusing to remove path outside expected parent: ${full}`)
1439
- }
1440
- rmSync(full, { recursive: true, force: true })
1441
- }
1442
-
1443
- function run(command, args, options = {}) {
1444
- try {
1445
- execFileSync(command, args, { stdio: "inherit", ...options })
1446
- } catch (error) {
1447
- if (error && typeof error.status === "number") {
1448
- throw new Error(`${command} exited with ${error.status}`)
1449
- }
1450
- throw error
1451
- }
1452
- }
1453
-
1454
- function runPnpm(args, options = {}) {
1455
- if (process.platform === "win32") {
1456
- run("cmd.exe", ["/d", "/s", "/c", "pnpm", ...args], options)
1457
- return
1458
- }
1459
- run("pnpm", args, options)
1460
- }
1461
-
1462
- function binaryName(name, targetPlatform) {
1463
- const isWindows = targetPlatform
1464
- ? targetPlatform.startsWith("win32")
1465
- : process.platform === "win32"
1466
- return isWindows ? `${name}.exe` : name
1467
- }
1468
-
1469
- function platformToGoEnv(platform) {
1470
- const { os: runtimeOS, cpu } = parsePlatformId(platform)
1471
- const goosMap = { win32: "windows", linux: "linux", darwin: "darwin" }
1472
- const goarchMap = { x64: "amd64", arm64: "arm64" }
1473
- return {
1474
- GOOS: goosMap[runtimeOS] ?? runtimeOS,
1475
- GOARCH: goarchMap[cpu] ?? cpu,
1476
- CGO_ENABLED: "0",
1477
- }
1478
- }
1479
-
1480
- function platformId() {
1481
- return `${process.platform}-${process.arch}`
1482
- }
1483
-
1484
- function runtimePackageName(platform) {
1485
- return `@vantaloom/runtime-${platform}`
1486
- }
1487
-
1488
- function parsePlatformId(platform) {
1489
- const parts = platform.split("-")
1490
- const cpu = parts.pop()
1491
- const runtimeOS = parts.join("-")
1492
- if (!runtimeOS || !cpu) {
1493
- throw new Error(`invalid platform id: ${platform}`)
1494
- }
1495
- return { os: runtimeOS, cpu }
1496
- }
1497
-
1498
- function packageBasename(name) {
1499
- return name.split("/").pop() ?? name
1500
- }
1501
-
1502
- function normalizeRegistry(value) {
1503
- return value.replace(/\/+$/, "")
1504
- }
1505
-
1506
- function defaultPrefix() {
1507
- if (process.env.VANTALOOM_HOME) {
1508
- return process.env.VANTALOOM_HOME
1509
- }
1510
- if (process.platform === "win32") {
1511
- return "D:\\Vantaloom"
1512
- }
1513
- if (process.platform === "darwin") {
1514
- return path.join(os.homedir(), "Applications", "Vantaloom")
1515
- }
1516
- return path.join(os.homedir(), ".local", "vantaloom")
1517
- }
1518
-
1519
- function displayCommand(prefix) {
1520
- if (process.platform === "win32") {
1521
- return path.join(prefix, "vantaloom.cmd")
1522
- }
1523
- return path.join(prefix, "vantaloom")
1524
- }
1525
-
1526
- function toCamel(value) {
1527
- return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
1528
- }
1529
-
1530
- async function writeText(filePath, content) {
1531
- mkdirSync(path.dirname(filePath), { recursive: true })
1532
- await writeFile(filePath, content)
1533
- }
1534
-
1535
- // ensureInPath adds the Vantaloom install directory to the user's shell PATH
1536
- // on macOS and Linux, so `vantaloom` can be run directly after install.
1537
- // On Windows this is not needed (vantaloom.cmd is run by full path or added via installer).
1538
- function ensureInPath(prefix) {
1539
- if (process.platform === "win32") return
1540
-
1541
- // Check if already in PATH
1542
- const pathDirs = (process.env.PATH || "").split(":")
1543
- if (pathDirs.includes(prefix)) return
1544
-
1545
- // Determine shell profile file
1546
- const home = os.homedir()
1547
- const shell = process.env.SHELL || ""
1548
- let profilePath
1549
- if (shell.endsWith("/zsh") || existsSync(path.join(home, ".zshrc"))) {
1550
- profilePath = path.join(home, ".zshrc")
1551
- } else if (shell.endsWith("/bash")) {
1552
- // On macOS, bash uses .bash_profile; on Linux, .bashrc
1553
- profilePath = process.platform === "darwin"
1554
- ? path.join(home, ".bash_profile")
1555
- : path.join(home, ".bashrc")
1556
- } else if (existsSync(path.join(home, ".profile"))) {
1557
- profilePath = path.join(home, ".profile")
1558
- } else {
1559
- profilePath = path.join(home, ".profile")
1560
- }
1561
-
1562
- // Use $HOME-relative path for portability
1563
- const homeRelative = prefix.startsWith(home)
1564
- ? `$HOME${prefix.slice(home.length)}`
1565
- : prefix
1566
- const exportLine = `export PATH="${homeRelative}:$PATH"`
1567
- const marker = "# vantaloom"
1568
-
1569
- // Check if already added to profile
1570
- try {
1571
- if (existsSync(profilePath)) {
1572
- const content = readFileSync(profilePath, "utf8")
1573
- if (content.includes("vantaloom") && content.includes("PATH")) return
1574
- }
1575
- } catch {}
1576
-
1577
- // Append to profile
1578
- try {
1579
- const entry = `\n${marker}\n${exportLine}\n`
1580
- appendFileSync(profilePath, entry)
1581
- console.log(`PATH: added ${prefix} to ${profilePath}`)
1582
- console.log(` run: source ${profilePath} (or open a new terminal)`)
1583
- } catch (error) {
1584
- console.log(`PATH: could not update ${profilePath}: ${error.message}`)
1585
- console.log(` add manually: ${exportLine}`)
1586
- }
1587
- }
1588
-
1589
- function killTrayProcess(prefix) {
1590
- if (process.platform === "win32") {
1591
- // Try PID file first (new tray versions write runtime/tray.pid).
1592
- const pidFile = path.join(prefix, "runtime", "tray.pid")
1593
- if (existsSync(pidFile)) {
1594
- const pid = readFileSync(pidFile, "utf8").trim()
1595
- if (pid) {
1596
- spawnSync("taskkill", ["/PID", pid, "/F"], { stdio: "ignore" })
1597
- try { rmSync(pidFile, { force: true }) } catch {}
1598
- }
1599
- }
1600
- // Fallback: kill by image name if the binary is inside our prefix.
1601
- const result = spawnSync("tasklist", ["/FI", "IMAGENAME eq vantaloom-tray.exe", "/FO", "CSV", "/NH"], {
1602
- encoding: "utf8",
1603
- windowsHide: true,
1604
- })
1605
- if (result.stdout) {
1606
- for (const line of result.stdout.split("\n")) {
1607
- const match = line.match(/"vantaloom-tray\.exe","(\d+)"/)
1608
- if (match) {
1609
- spawnSync("taskkill", ["/PID", match[1], "/F"], { stdio: "ignore" })
1610
- }
1611
- }
1612
- }
1613
- // Brief pause to let file handles release.
1614
- spawnSync("timeout", ["/t", "1", "/nobreak"], { stdio: "ignore", windowsHide: true })
1615
- } else {
1616
- // Unix: pkill by name (best-effort).
1617
- spawnSync("pkill", ["-f", "vantaloom-tray"], { stdio: "ignore" })
1618
- }
1619
- }
1620
-
1621
390
  function printHelp() {
1622
391
  console.log(`Vantaloom CLI
1623
392
 
1624
393
  Usage:
1625
- vantaloom install [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start] [--with-mesh|--skip-mesh]
1626
- vantaloom install --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start] [--with-mesh]
1627
- vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start] [--with-mesh|--skip-mesh]
1628
- vantaloom update --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start] [--with-mesh]
1629
- vantaloom uninstall [--prefix <dir>] [--skip-mesh]
394
+ vantaloom install [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start]
395
+ vantaloom install --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
396
+ vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start]
397
+ vantaloom update --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
398
+ vantaloom uninstall [--prefix <dir>]
1630
399
  vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package] [--target <platform>]
1631
400
  vantaloom start [--prefix <dir>] [--component all|api|agent|web]
1632
401
  vantaloom stop [--prefix <dir>] [--component all|api|agent|web]
@@ -1635,10 +404,13 @@ Usage:
1635
404
  vantaloom ports [--prefix <dir>]
1636
405
  vantaloom path [--prefix <dir>] [--source <repo>]
1637
406
  vantaloom platform
1638
-
1639
- Mesh (P2P) service:
1640
- Windows/macOS register a privileged EasyTier service at install (one UAC/sudo prompt).
1641
- Linux grants TUN capability via setcap instead. Use --skip-mesh to opt out,
1642
- or --with-mesh to force registration during a --local source install.
407
+ vantaloom login [--hub <url>] [--email <email>] [--password <pw>] [--prefix <dir>]
408
+ vantaloom logout [--prefix <dir>]
409
+
410
+ Hub login (for machines with no web access):
411
+ "vantaloom login" authenticates to the Hub from the terminal and joins this
412
+ machine to your workgroup — no browser needed. Email/password may be passed as
413
+ flags (for scripts) or entered interactively. The local runtime must be running
414
+ (run "vantaloom start" first).
1643
415
  `)
1644
416
  }