@vantaloom/cli 0.6.0 → 0.13.6

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.
@@ -0,0 +1,148 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs"
8
+ import { spawnSync } from "node:child_process"
9
+ import path from "node:path"
10
+ import { binaryName, psQuote } from "./platform.mjs"
11
+
12
+ // This module holds process-cleanup helpers that used to live in mesh.mjs
13
+ // (removed in 0.13 along with the EasyTier mesh sidecar and the CLI's
14
+ // install-time mesh-service registration). Neither function below is
15
+ // mesh-specific to its live callers:
16
+ // - killTrayProcess predates the mesh sidecar entirely (kills a leftover
17
+ // vantaloom-tray.exe — a separate, already-removed feature).
18
+ // - removeMeshService is now a ONE-TIME migration primitive: the elevated
19
+ // uninstall of the legacy privileged `VantaloomMesh` service, invoked
20
+ // exactly once per install by uninstallLegacyMeshOnce below.
21
+
22
+ export function killTrayProcess(prefix) {
23
+ if (process.platform === "win32") {
24
+ // Try PID file first (new tray versions write runtime/tray.pid).
25
+ const pidFile = path.join(prefix, "runtime", "tray.pid")
26
+ if (existsSync(pidFile)) {
27
+ const pid = readFileSync(pidFile, "utf8").trim()
28
+ if (pid) {
29
+ spawnSync("taskkill", ["/PID", pid, "/F"], { stdio: "ignore", windowsHide: true })
30
+ try { rmSync(pidFile, { force: true }) } catch {}
31
+ }
32
+ }
33
+ // Fallback: kill by image name if the binary is inside our prefix.
34
+ const result = spawnSync("tasklist", ["/FI", "IMAGENAME eq vantaloom-tray.exe", "/FO", "CSV", "/NH"], {
35
+ encoding: "utf8",
36
+ windowsHide: true,
37
+ })
38
+ if (result.stdout) {
39
+ for (const line of result.stdout.split("\n")) {
40
+ const match = line.match(/"vantaloom-tray\.exe","(\d+)"/)
41
+ if (match) {
42
+ spawnSync("taskkill", ["/PID", match[1], "/F"], { stdio: "ignore", windowsHide: true })
43
+ }
44
+ }
45
+ }
46
+ // Brief pause to let file handles release.
47
+ spawnSync("timeout", ["/t", "1", "/nobreak"], { stdio: "ignore", windowsHide: true })
48
+ } else {
49
+ // Unix: pkill by name (best-effort).
50
+ spawnSync("pkill", ["-f", "vantaloom-tray"], { stdio: "ignore", windowsHide: true })
51
+ }
52
+ }
53
+
54
+ // meshServiceRunningOrInstalled reports whether the Windows service exists at
55
+ // all (running or stopped), so removeMeshService only prompts for elevation
56
+ // when there is actually something to remove.
57
+ function meshServiceRunningOrInstalled(binDir) {
58
+ const exe = path.join(binDir, "vantaloom-mesh.exe")
59
+ if (!existsSync(exe)) return false
60
+ const r = spawnSync(exe, ["status"], { encoding: "utf8", windowsHide: true })
61
+ if (r.error || typeof r.stdout !== "string") return false
62
+ return !r.stdout.toLowerCase().includes("not installed")
63
+ }
64
+
65
+ // removeMeshService elevates once to invoke the still-on-disk legacy
66
+ // vantaloom-mesh binary's own `uninstall` verb, which self-removes its SCM
67
+ // service (Windows) or LaunchDaemon (macOS). Linux never registered a service
68
+ // (it only used setcap) so there's nothing to remove there.
69
+ //
70
+ // Returns true when there was nothing to do, or removal succeeded; false when
71
+ // an elevated uninstall was attempted but declined/failed (never throws) — a
72
+ // caller that wants "retry next time" semantics acts on the return value (see
73
+ // uninstallLegacyMeshOnce).
74
+ export function removeMeshService(prefix, options) {
75
+ const platform = process.platform
76
+ if (platform !== "win32" && platform !== "darwin") return true // Linux: nothing registered
77
+ const exe = path.join(prefix, "bin", binaryName("vantaloom-mesh"))
78
+ if (!existsSync(exe)) return true
79
+ if (options.skipMesh) return true
80
+
81
+ if (platform === "win32") {
82
+ if (!meshServiceRunningOrInstalled(path.join(prefix, "bin"))) return true
83
+ console.log(" mesh: removing legacy privileged P2P service (UAC prompt) ...")
84
+ const ps = `$ErrorActionPreference='Stop'; $p = Start-Process -FilePath ${psQuote(exe)} -ArgumentList 'uninstall' -Verb RunAs -Wait -PassThru; exit $p.ExitCode`
85
+ const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "inherit", windowsHide: true })
86
+ if (r.error || r.status !== 0) {
87
+ console.warn(` mesh: could not remove legacy service; run manually (as Administrator): "${exe}" uninstall`)
88
+ return false
89
+ }
90
+ return true
91
+ }
92
+
93
+ console.log(" mesh: removing legacy privileged P2P service (sudo) ...")
94
+ const r = spawnSync("sudo", [exe, "uninstall"], { stdio: "inherit", windowsHide: true })
95
+ if (r.error || r.status !== 0) {
96
+ console.warn(` mesh: could not remove legacy service; run manually: sudo "${exe}" uninstall`)
97
+ return false
98
+ }
99
+ return true
100
+ }
101
+
102
+ // The bin/ files a pre-0.13 install's EasyTier mesh sidecar left behind
103
+ // (Windows and Unix names both listed — deleting a name absent on this
104
+ // platform is a harmless no-op via rmSync's force:true).
105
+ const LEGACY_MESH_FILE_NAMES = [
106
+ "vantaloom-mesh.exe", "vantaloom-mesh",
107
+ "easytier-core.exe", "easytier-core",
108
+ "easytier-cli.exe", "easytier-cli",
109
+ "wintun.dll", "Packet.dll", "WinDivert64.sys",
110
+ ]
111
+
112
+ // uninstallLegacyMeshOnce is the one-time migration that removes a pre-0.13
113
+ // install's privileged `VantaloomMesh` service (Windows SCM service / macOS
114
+ // LaunchDaemon; Linux never had one) plus its stale bin/ binaries. It exists
115
+ // because applyPackage's bin/ copy only OVERLAYS files — it never deletes —
116
+ // so a 0.13+ package (which no longer bundles vantaloom-mesh/easytier-core/
117
+ // the support DLLs) would otherwise leave the old service + binaries in place
118
+ // forever, and the orphaned service could even restart-loop easytier-core.
119
+ //
120
+ // Idempotent via a done-marker file (<prefix>/.mesh-removed): once cleanup has
121
+ // run, every future install/update returns immediately. Never blocks
122
+ // install/update — a failed or user-declined elevation prompt just logs a
123
+ // hint and leaves the marker UNwritten, so the very next install/update
124
+ // retries (design doc §10: "拒绝则留置、下次再试").
125
+ export async function uninstallLegacyMeshOnce(prefix, options) {
126
+ const markerPath = path.join(prefix, ".mesh-removed")
127
+ if (existsSync(markerPath)) return
128
+
129
+ try {
130
+ const removed = removeMeshService(prefix, options)
131
+ if (!removed) {
132
+ console.warn(" mesh: legacy service removal was declined or failed; will retry on the next install/update")
133
+ return // leave the marker unwritten so this retries
134
+ }
135
+
136
+ // Best-effort: delete the stale mesh binaries so nothing can linger or
137
+ // restart-loop even after the service registration itself is gone.
138
+ const binDir = path.join(prefix, "bin")
139
+ for (const name of LEGACY_MESH_FILE_NAMES) {
140
+ try { rmSync(path.join(binDir, name), { force: true }) } catch {}
141
+ }
142
+
143
+ mkdirSync(prefix, { recursive: true })
144
+ writeFileSync(markerPath, `${new Date().toISOString()}\n`)
145
+ } catch (error) {
146
+ console.warn(` mesh: legacy cleanup failed (${error.message}); will retry on the next install/update`)
147
+ }
148
+ }
@@ -0,0 +1,189 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ rmSync,
5
+ writeFileSync,
6
+ } from "node:fs"
7
+ import { spawnSync } from "node:child_process"
8
+ import os from "node:os"
9
+ import path from "node:path"
10
+ import {
11
+ AUTOSTART_LABEL,
12
+ WINDOWS_RUN_KEY,
13
+ WINDOWS_RUN_VALUE,
14
+ } from "./constants.mjs"
15
+ import {
16
+ binaryName,
17
+ safeDirectory,
18
+ defaultPrefix,
19
+ run,
20
+ removeKnownPath,
21
+ } from "./platform.mjs"
22
+ import {
23
+ removeMeshService,
24
+ killTrayProcess,
25
+ } from "./legacy-cleanup.mjs"
26
+
27
+ // ── Runtime login-autostart (no elevation) ──
28
+ // Start the local runtime (api/agent/web/tray) at login/boot using only
29
+ // per-user mechanisms — no UAC/sudo. This is separate from the privileged mesh
30
+ // service: the mesh sidecar autostarts via the OS service manager; this brings
31
+ // up the unprivileged runtime that joins the mesh and serves the local API.
32
+
33
+ export function enableRuntimeAutostart(prefix) {
34
+ try {
35
+ if (process.platform === "win32") {
36
+ // A .vbs launches the runtime with a hidden window (no console flash); the
37
+ // HKCU Run key runs it at login without elevation.
38
+ const launcher = path.join(prefix, "vantaloom.cmd")
39
+ const vbsPath = path.join(prefix, "autostart.vbs")
40
+ const vbs = `' Vantaloom runtime autostart (hidden)\r\nCreateObject("WScript.Shell").Run """${launcher}"" start", 0, False\r\n`
41
+ writeFileSync(vbsPath, vbs)
42
+ const result = spawnSync(
43
+ "powershell",
44
+ [
45
+ "-NoProfile",
46
+ "-NonInteractive",
47
+ "-Command",
48
+ `Set-ItemProperty -Path '${WINDOWS_RUN_KEY}' -Name '${WINDOWS_RUN_VALUE}' -Value 'wscript.exe "${vbsPath}"'`,
49
+ ],
50
+ { stdio: "ignore", windowsHide: true }
51
+ )
52
+ if (result.error || result.status !== 0) {
53
+ console.warn(" autostart: could not register login entry (HKCU Run)")
54
+ } else {
55
+ console.log(" autostart: enabled (login Run key)")
56
+ }
57
+ return
58
+ }
59
+ if (process.platform === "darwin") {
60
+ const launcher = path.join(prefix, "vantaloom")
61
+ const dir = path.join(os.homedir(), "Library", "LaunchAgents")
62
+ mkdirSync(dir, { recursive: true })
63
+ const plistPath = path.join(dir, `${AUTOSTART_LABEL}.plist`)
64
+ // RunAtLoad only (no KeepAlive): `vantaloom start` spawns detached and exits.
65
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
66
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
67
+ <plist version="1.0"><dict>
68
+ <key>Label</key><string>${AUTOSTART_LABEL}</string>
69
+ <key>ProgramArguments</key><array><string>${launcher}</string><string>start</string></array>
70
+ <key>RunAtLoad</key><true/>
71
+ </dict></plist>
72
+ `
73
+ writeFileSync(plistPath, plist)
74
+ spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore", windowsHide: true })
75
+ spawnSync("launchctl", ["load", "-w", plistPath], { stdio: "ignore", windowsHide: true })
76
+ console.log(" autostart: enabled (LaunchAgent)")
77
+ return
78
+ }
79
+ // Linux: user systemd unit + linger (no root).
80
+ const launcher = path.join(prefix, "vantaloom")
81
+ const dir = path.join(os.homedir(), ".config", "systemd", "user")
82
+ mkdirSync(dir, { recursive: true })
83
+ const unit = `[Unit]
84
+ Description=Vantaloom local runtime
85
+ After=network-online.target
86
+ Wants=network-online.target
87
+
88
+ [Service]
89
+ Type=oneshot
90
+ RemainAfterExit=yes
91
+ ExecStart=${launcher} start
92
+ ExecStop=${launcher} stop
93
+
94
+ [Install]
95
+ WantedBy=default.target
96
+ `
97
+ writeFileSync(path.join(dir, "vantaloom-runtime.service"), unit)
98
+ spawnSync("loginctl", ["enable-linger"], { stdio: "ignore", windowsHide: true })
99
+ spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore", windowsHide: true })
100
+ spawnSync("systemctl", ["--user", "enable", "vantaloom-runtime.service"], { stdio: "ignore", windowsHide: true })
101
+ console.log(" autostart: enabled (systemd user unit)")
102
+ } catch (error) {
103
+ console.warn(` autostart: could not enable (${error.message})`)
104
+ }
105
+ }
106
+
107
+ export function disableRuntimeAutostart() {
108
+ try {
109
+ if (process.platform === "win32") {
110
+ // Remove the Run-key entry; the autostart.vbs lives in the install dir and
111
+ // is deleted with it.
112
+ spawnSync(
113
+ "powershell",
114
+ ["-NoProfile", "-NonInteractive", "-Command", `Remove-ItemProperty -Path '${WINDOWS_RUN_KEY}' -Name '${WINDOWS_RUN_VALUE}' -ErrorAction SilentlyContinue`],
115
+ { stdio: "ignore", windowsHide: true }
116
+ )
117
+ return
118
+ }
119
+ if (process.platform === "darwin") {
120
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`)
121
+ if (existsSync(plistPath)) {
122
+ spawnSync("launchctl", ["unload", "-w", plistPath], { stdio: "ignore", windowsHide: true })
123
+ rmSync(plistPath, { force: true })
124
+ }
125
+ return
126
+ }
127
+ const unit = path.join(os.homedir(), ".config", "systemd", "user", "vantaloom-runtime.service")
128
+ if (existsSync(unit)) {
129
+ spawnSync("systemctl", ["--user", "disable", "vantaloom-runtime.service"], { stdio: "ignore", windowsHide: true })
130
+ rmSync(unit, { force: true })
131
+ spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore", windowsHide: true })
132
+ }
133
+ } catch (error) {
134
+ console.warn(` autostart: could not disable (${error.message})`)
135
+ }
136
+ }
137
+
138
+ // uninstallRuntime tears down a local install: stop the runtime, remove any
139
+ // legacy privileged mesh service (elevated), then delete the install directory.
140
+ export async function uninstallRuntime(options) {
141
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
142
+ if (!existsSync(prefix)) {
143
+ console.log(`Vantaloom is not installed at ${prefix}`)
144
+ return
145
+ }
146
+ console.log(`Uninstalling Vantaloom from ${prefix} ...`)
147
+
148
+ // 1. Stop the runtime (api/agent/web/tray). Best-effort.
149
+ const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
150
+ if (existsSync(ctlBin)) {
151
+ spawnSync(ctlBin, ["stop", "--prefix", prefix], { stdio: "inherit", windowsHide: true })
152
+ }
153
+ killTrayProcess(prefix)
154
+
155
+ // 2. Remove a legacy privileged mesh service if this install predates 0.13
156
+ // (releases the TUN adapter + file locks). A no-op on any install that
157
+ // never had one (see legacy-cleanup.mjs's removeMeshService).
158
+ removeMeshService(prefix, options)
159
+
160
+ // 2b. Remove the login-autostart entry (lives outside the install dir).
161
+ disableRuntimeAutostart()
162
+
163
+ // 3. Delete the install directory.
164
+ try {
165
+ const parent = path.dirname(prefix)
166
+ removeKnownPath(prefix, parent)
167
+ console.log(`removed ${prefix}`)
168
+ } catch (error) {
169
+ console.warn(` could not remove ${prefix}: ${error.message}`)
170
+ console.warn(" some files may still be locked; re-run after closing Vantaloom processes.")
171
+ }
172
+
173
+ console.log("Vantaloom uninstalled.")
174
+ console.log("note: the install dir was left out of PATH edits; remove the bin/ entry from your shell profile if you added it.")
175
+ }
176
+
177
+ export function runCtl(command, options) {
178
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
179
+ const ctl = path.join(prefix, "bin", binaryName("vantaloomctl"))
180
+ if (!existsSync(ctl)) {
181
+ throw new Error(`missing installed runtime at ${prefix}; run "vantaloom install" first`)
182
+ }
183
+
184
+ const args = [command, "--prefix", prefix]
185
+ if (options.component) {
186
+ args.push("--component", options.component)
187
+ }
188
+ run(ctl, args)
189
+ }
@@ -0,0 +1,98 @@
1
+ import { existsSync, mkdirSync } from "node:fs"
2
+ import { cp } from "node:fs/promises"
3
+ import path from "node:path"
4
+ import { platformId, removeKnownPath, runPnpm } from "./platform.mjs"
5
+ import {
6
+ buildGo,
7
+ copyStaticWeb,
8
+ copyCliDirectory,
9
+ writeBuildManifest,
10
+ writeRuntimePackageMetadata,
11
+ runtimeConfigFromSource,
12
+ gitVersion,
13
+ npmPackageVersion,
14
+ } from "./install.mjs"
15
+
16
+ export async function buildRuntimePackage(sourceRoot, packageRoot, options) {
17
+ // Every build — npm release AND local/dev — stamps VERSION/manifest with the
18
+ // npm package version. There is exactly ONE product version (the npm semver);
19
+ // the git hash is recorded separately as manifest `commit` for diagnostics.
20
+ // History: pre-0.13.5 local builds stamped the git hash into VERSION, which
21
+ // split the version universe in two (the desktop shell read VERSION, the
22
+ // settings page read cli/package.json) and broke update prompts both ways.
23
+ const version = npmPackageVersion(sourceRoot)
24
+ const commit = gitVersion(sourceRoot)
25
+ const platform = options.target ?? platformId()
26
+ const buildBin = path.join(packageRoot, "bin")
27
+ const buildWeb = path.join(packageRoot, "web")
28
+
29
+ removeKnownPath(packageRoot, path.dirname(packageRoot))
30
+ mkdirSync(buildBin, { recursive: true })
31
+
32
+ buildGo(sourceRoot, buildBin, "vantaloom-api", platform)
33
+ buildGo(sourceRoot, buildBin, "vantaloom-agent", platform)
34
+ buildGo(sourceRoot, buildBin, "vantaloomctl", platform)
35
+ // Official "Vantaloom 浏览器" extension sidecar. Pure-Go (chromedp/CDP, no
36
+ // CGO), cross-compiles for every target. Ships dormant: it's only started by
37
+ // the api when the user installs+enables the browser extension (the heavy
38
+ // Chromium binary is downloaded on demand, NOT bundled here).
39
+ buildGo(sourceRoot, buildBin, "vantaloom-browser", platform)
40
+ // The Windows system-tray app (vantaloom-tray) was removed — it crash-looped
41
+ // on some Windows 11 builds and got respawned until OOM. No longer built.
42
+
43
+ // Bundle the Obscura headless browser (the official "Vantaloom 浏览器" engine,
44
+ // driven by the vantaloom-browser sidecar over CDP) for the target platform.
45
+ await copyObscura(sourceRoot, buildBin, platform)
46
+
47
+ if (options.buildWeb) {
48
+ runPnpm(["--filter", "vantaloom-app", "build"], { cwd: sourceRoot })
49
+ }
50
+
51
+ await copyStaticWeb(sourceRoot, buildWeb)
52
+ await copyCliDirectory(path.join(packageRoot, "cli"), sourceRoot, runtimeConfigFromSource(sourceRoot))
53
+ writeBuildManifest(packageRoot, version, platform, commit)
54
+
55
+ return { platform, version }
56
+ }
57
+
58
+ // copyObscura bundles the vendored Obscura headless-browser binaries
59
+ // (obscura + obscura-worker, .exe on Windows) for the target platform into the
60
+ // package bin/ dir. Obscura is the official "Vantaloom 浏览器" engine: a
61
+ // CDP-compatible, V8-backed headless browser the vantaloom-browser sidecar
62
+ // launches via `obscura serve` and drives over CDP. Warns + skips if the
63
+ // vendor dir is absent (a cleaned vendor/ silently ships a runtime with no
64
+ // browser engine).
65
+ export async function copyObscura(sourceRoot, buildBin, platform) {
66
+ const vendorMap = {
67
+ "win32-x64": "windows-x86_64",
68
+ "darwin-arm64": "macos-aarch64",
69
+ "linux-x64": "linux-x86_64",
70
+ }
71
+ const vendorName = vendorMap[platform]
72
+ if (!vendorName) {
73
+ console.warn(` warning: no Obscura mapping for ${platform}; browser engine disabled for this platform`)
74
+ return
75
+ }
76
+ const srcDir = path.join(sourceRoot, "vendor", "obscura", vendorName)
77
+ if (!existsSync(srcDir)) {
78
+ console.warn(` warning: Obscura binaries not found at ${srcDir}; run vendor download first`)
79
+ return
80
+ }
81
+ const isWin = platform.startsWith("win32")
82
+ const exe = isWin ? ".exe" : ""
83
+ // Both binaries MUST ship together: obscura is the CLI/CDP server, obscura-worker
84
+ // is the V8 render worker it spawns. Without the worker, `obscura serve` cannot
85
+ // open pages.
86
+ const files = [`obscura${exe}`, `obscura-worker${exe}`]
87
+ let copied = 0
88
+ for (const f of files) {
89
+ const src = path.join(srcDir, f)
90
+ if (!existsSync(src)) {
91
+ console.warn(` warning: Obscura file missing: ${f}`)
92
+ continue
93
+ }
94
+ await cp(src, path.join(buildBin, f), { force: true })
95
+ copied++
96
+ }
97
+ console.log(` bundled Obscura ${vendorName} (${copied} files)`)
98
+ }
@@ -0,0 +1,158 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ } from "node:fs"
7
+ import { writeFile } from "node:fs/promises"
8
+ import { execFileSync } from "node:child_process"
9
+ import os from "node:os"
10
+ import path from "node:path"
11
+
12
+ // binaryName returns the OS-appropriate binary file name.
13
+ // NOTE: mirrored in Go at apps/desktop/internal/runtime/
14
+ export function binaryName(name, targetPlatform) {
15
+ const isWindows = targetPlatform
16
+ ? targetPlatform.startsWith("win32")
17
+ : process.platform === "win32"
18
+ return isWindows ? `${name}.exe` : name
19
+ }
20
+
21
+ // platformId returns the current platform identifier string.
22
+ // NOTE: mirrored in Go at apps/desktop/internal/runtime/
23
+ export function platformId() {
24
+ return `${process.platform}-${process.arch}`
25
+ }
26
+
27
+ // runtimePackageName returns the npm package name for a given platform.
28
+ // NOTE: mirrored in Go at apps/desktop/internal/runtime/
29
+ export function runtimePackageName(platform) {
30
+ return `@vantaloom/runtime-${platform}`
31
+ }
32
+
33
+ export function parsePlatformId(platform) {
34
+ const parts = platform.split("-")
35
+ const cpu = parts.pop()
36
+ const runtimeOS = parts.join("-")
37
+ if (!runtimeOS || !cpu) {
38
+ throw new Error(`invalid platform id: ${platform}`)
39
+ }
40
+ return { os: runtimeOS, cpu }
41
+ }
42
+
43
+ // psQuote wraps a value as a PowerShell single-quoted string literal.
44
+ // NOTE: mirrored in Go at apps/desktop/internal/runtime/
45
+ export function psQuote(value) {
46
+ return "'" + String(value).replace(/'/g, "''") + "'"
47
+ }
48
+
49
+ // defaultPrefix returns the default install prefix for the current platform.
50
+ // NOTE: mirrored in Go at apps/desktop/internal/runtime/
51
+ export function defaultPrefix() {
52
+ if (process.env.VANTALOOM_HOME) {
53
+ return process.env.VANTALOOM_HOME
54
+ }
55
+ if (process.platform === "win32") {
56
+ // Prefer D: (the historical default) but fall back to C: when this machine
57
+ // has no D: drive. Keep in sync with the desktop shell's DefaultPrefix().
58
+ return existsSync("D:\\") ? "D:\\Vantaloom" : "C:\\Vantaloom"
59
+ }
60
+ if (process.platform === "darwin") {
61
+ return path.join(os.homedir(), "Applications", "Vantaloom")
62
+ }
63
+ return path.join(os.homedir(), ".local", "vantaloom")
64
+ }
65
+
66
+ export function displayCommand(prefix) {
67
+ if (process.platform === "win32") {
68
+ return path.join(prefix, "vantaloom.cmd")
69
+ }
70
+ return path.join(prefix, "vantaloom")
71
+ }
72
+
73
+ export function readJSONIfExists(filePath) {
74
+ if (!existsSync(filePath)) {
75
+ return {}
76
+ }
77
+ return JSON.parse(readFileSync(filePath, "utf8"))
78
+ }
79
+
80
+ export function packageBasename(name) {
81
+ return name.split("/").pop() ?? name
82
+ }
83
+
84
+ export function normalizeRegistry(value) {
85
+ return value.replace(/\/+$/, "")
86
+ }
87
+
88
+ export function toCamel(value) {
89
+ return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
90
+ }
91
+
92
+ export function safeDirectory(value) {
93
+ const full = path.resolve(value)
94
+ const parsed = path.parse(full)
95
+ if (full === parsed.root) {
96
+ throw new Error(`refusing to operate on unsafe directory: ${value}`)
97
+ }
98
+ return full
99
+ }
100
+
101
+ export function removeKnownPath(target, expectedParent) {
102
+ if (!existsSync(target)) {
103
+ return
104
+ }
105
+ const full = path.resolve(target)
106
+ const parent = path.resolve(expectedParent)
107
+ const prefix = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`
108
+ if (!full.startsWith(prefix)) {
109
+ throw new Error(`refusing to remove path outside expected parent: ${full}`)
110
+ }
111
+ rmSync(full, { recursive: true, force: true })
112
+ }
113
+
114
+ export async function writeText(filePath, content) {
115
+ mkdirSync(path.dirname(filePath), { recursive: true })
116
+ await writeFile(filePath, content)
117
+ }
118
+
119
+ export function run(command, args, options = {}) {
120
+ try {
121
+ execFileSync(command, args, { stdio: "inherit", windowsHide: true, ...options })
122
+ } catch (error) {
123
+ if (error && typeof error.status === "number") {
124
+ throw new Error(`${command} exited with ${error.status}`)
125
+ }
126
+ throw error
127
+ }
128
+ }
129
+
130
+ export function runPnpm(args, options = {}) {
131
+ if (process.platform === "win32") {
132
+ run("cmd.exe", ["/d", "/s", "/c", "pnpm", ...args], options)
133
+ return
134
+ }
135
+ run("pnpm", args, options)
136
+ }
137
+
138
+ // runNpm invokes npm cross-platform. On Windows npm is `npm.cmd`, which
139
+ // execFileSync (used by run) can't resolve directly (ENOENT) — so wrap via
140
+ // cmd.exe, mirroring runPnpm.
141
+ export function runNpm(args, options = {}) {
142
+ if (process.platform === "win32") {
143
+ run("cmd.exe", ["/d", "/s", "/c", "npm", ...args], options)
144
+ return
145
+ }
146
+ run("npm", args, options)
147
+ }
148
+
149
+ export function platformToGoEnv(platform) {
150
+ const { os: runtimeOS, cpu } = parsePlatformId(platform)
151
+ const goosMap = { win32: "windows", linux: "linux", darwin: "darwin" }
152
+ const goarchMap = { x64: "amd64", arm64: "arm64" }
153
+ return {
154
+ GOOS: goosMap[runtimeOS] ?? runtimeOS,
155
+ GOARCH: goarchMap[cpu] ?? cpu,
156
+ CGO_ENABLED: "0",
157
+ }
158
+ }