@vantaloom/runtime-darwin-x64 0.1.24 → 0.6.1
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/VERSION +1 -1
- package/bin/vantaloom-agent +0 -0
- package/bin/vantaloom-api +0 -0
- package/bin/vantaloom-mesh +0 -0
- package/bin/vantaloomctl +0 -0
- package/cli/config.json +3 -2
- package/cli/package.json +1 -1
- package/cli/src/cli.mjs +814 -48
- package/manifest.json +2 -2
- package/package.json +1 -1
- package/web/404.html +1 -1
- package/web/__next.__PAGE__.txt +2 -2
- package/web/__next._full.txt +3 -3
- package/web/__next._head.txt +1 -1
- package/web/__next._index.txt +2 -2
- package/web/__next._tree.txt +2 -2
- package/web/_next/static/chunks/3fe17d3e837dc30f.js +52 -0
- package/web/_next/static/chunks/670bd1a320370071.css +2 -0
- package/web/_not-found/__next._full.txt +2 -2
- package/web/_not-found/__next._head.txt +1 -1
- package/web/_not-found/__next._index.txt +2 -2
- package/web/_not-found/{__next._not-found.__PAGE__.txt → __next._not-found/__PAGE__.txt} +1 -1
- package/web/_not-found/__next._not-found.txt +1 -1
- package/web/_not-found/__next._tree.txt +2 -2
- package/web/_not-found.html +1 -1
- package/web/_not-found.txt +2 -2
- package/web/index.html +1 -31
- package/web/index.txt +3 -3
- package/web/_next/static/chunks/5922011684321db3.css +0 -2
- package/web/_next/static/chunks/bb819ea11a2befeb.js +0 -49
- /package/web/_next/static/{kDr_5ifTTP-NWy7O1R7JF → HVagxx3-CrF1tDnEJCTXD}/_buildManifest.js +0 -0
- /package/web/_next/static/{kDr_5ifTTP-NWy7O1R7JF → HVagxx3-CrF1tDnEJCTXD}/_clientMiddlewareManifest.json +0 -0
- /package/web/_next/static/{kDr_5ifTTP-NWy7O1R7JF → HVagxx3-CrF1tDnEJCTXD}/_ssgManifest.js +0 -0
package/cli/src/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process"
|
|
2
2
|
import {
|
|
3
|
+
appendFileSync,
|
|
3
4
|
chmodSync,
|
|
4
5
|
existsSync,
|
|
5
6
|
mkdirSync,
|
|
@@ -10,21 +11,61 @@ import {
|
|
|
10
11
|
writeFileSync,
|
|
11
12
|
} from "node:fs"
|
|
12
13
|
import { cp, writeFile } from "node:fs/promises"
|
|
14
|
+
import { createHash } from "node:crypto"
|
|
13
15
|
import os from "node:os"
|
|
14
16
|
import path from "node:path"
|
|
15
17
|
import { fileURLToPath } from "node:url"
|
|
16
18
|
|
|
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
|
+
|
|
17
24
|
const cliRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..")
|
|
18
25
|
const repoCandidate = path.resolve(cliRoot, "..", "..")
|
|
19
26
|
const installedConfigPath = path.join(cliRoot, "config.json")
|
|
20
27
|
const defaultReleaseTag = "runtime-latest"
|
|
21
28
|
const defaultRepo = "Timefiles404/Vantaloom-next"
|
|
22
29
|
const defaultNpmRegistry = "https://registry.npmjs.org"
|
|
30
|
+
const fallbackNpmRegistries = [
|
|
31
|
+
"https://registry.npmmirror.com",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// Inherit strict-ssl=false from npm/npx config, or respect NODE_TLS_REJECT_UNAUTHORIZED.
|
|
35
|
+
// When npm runs us via npx with strict-ssl disabled, it sets npm_config_strict_ssl="false".
|
|
36
|
+
// Also check user .npmrc for strict-ssl=false (common in China behind proxies).
|
|
37
|
+
function shouldDisableTLS() {
|
|
38
|
+
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") return true
|
|
39
|
+
if (process.env.npm_config_strict_ssl === "false") return true
|
|
40
|
+
if (process.env.npm_config_strict_ssl === "") return true
|
|
41
|
+
try {
|
|
42
|
+
const npmrcPath = path.join(os.homedir(), ".npmrc")
|
|
43
|
+
if (existsSync(npmrcPath)) {
|
|
44
|
+
const content = readFileSync(npmrcPath, "utf8")
|
|
45
|
+
if (/^\s*strict-ssl\s*=\s*false/m.test(content)) return true
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
if (shouldDisableTLS()) {
|
|
51
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const cliVersion = readJSONIfExists(path.join(cliRoot, "package.json")).version ?? "unknown"
|
|
23
55
|
|
|
24
56
|
export async function main(argv) {
|
|
25
57
|
const command = argv[0] ?? "help"
|
|
26
58
|
const options = parseOptions(argv.slice(1))
|
|
27
59
|
|
|
60
|
+
// --no-strict-ssl flag disables TLS certificate verification for fetch calls
|
|
61
|
+
if (options.noStrictSsl) {
|
|
62
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (command === "install" || command === "update") {
|
|
66
|
+
console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
|
|
67
|
+
}
|
|
68
|
+
|
|
28
69
|
switch (command) {
|
|
29
70
|
case "install":
|
|
30
71
|
if (options.package) {
|
|
@@ -64,6 +105,9 @@ export async function main(argv) {
|
|
|
64
105
|
case "path":
|
|
65
106
|
printPaths(options)
|
|
66
107
|
return
|
|
108
|
+
case "uninstall":
|
|
109
|
+
await uninstallRuntime(options)
|
|
110
|
+
return
|
|
67
111
|
case "help":
|
|
68
112
|
case "-h":
|
|
69
113
|
case "--help":
|
|
@@ -87,10 +131,13 @@ async function installFromSource(options) {
|
|
|
87
131
|
noStart: options.noStart,
|
|
88
132
|
sourceRoot,
|
|
89
133
|
update: options.update,
|
|
134
|
+
withMesh: options.withMesh,
|
|
135
|
+
skipMesh: options.skipMesh,
|
|
90
136
|
})
|
|
91
137
|
|
|
92
138
|
console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
|
|
93
139
|
console.log(`version: ${version}`)
|
|
140
|
+
ensureInPath(prefix)
|
|
94
141
|
console.log(`run: ${displayCommand(prefix)} status`)
|
|
95
142
|
}
|
|
96
143
|
|
|
@@ -100,20 +147,25 @@ async function installFromPackage(options) {
|
|
|
100
147
|
const version = await applyPackage(packageRoot, prefix, {
|
|
101
148
|
noStart: options.noStart,
|
|
102
149
|
update: options.update,
|
|
150
|
+
withMesh: options.withMesh,
|
|
151
|
+
skipMesh: options.skipMesh,
|
|
103
152
|
})
|
|
104
153
|
|
|
105
154
|
console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
|
|
106
155
|
console.log(`version: ${version}`)
|
|
156
|
+
ensureInPath(prefix)
|
|
107
157
|
console.log(`run: ${displayCommand(prefix)} status`)
|
|
108
158
|
}
|
|
109
159
|
|
|
110
160
|
async function packageRuntime(options) {
|
|
111
161
|
const sourceRoot = findSourceRoot(options.source)
|
|
162
|
+
const targetPlatform = options.target ?? platformId()
|
|
112
163
|
const packageRoot = safeDirectory(
|
|
113
|
-
options.output ?? path.join(sourceRoot, "artifacts", "packages", `vantaloom-${
|
|
164
|
+
options.output ?? path.join(sourceRoot, "artifacts", "packages", `vantaloom-${targetPlatform}`)
|
|
114
165
|
)
|
|
115
166
|
const { platform: builtPlatform, version } = await buildRuntimePackage(sourceRoot, packageRoot, {
|
|
116
167
|
buildWeb: options.buildWeb,
|
|
168
|
+
target: targetPlatform,
|
|
117
169
|
})
|
|
118
170
|
|
|
119
171
|
if (options.npmPackage) {
|
|
@@ -140,16 +192,28 @@ async function packageRuntime(options) {
|
|
|
140
192
|
|
|
141
193
|
async function buildRuntimePackage(sourceRoot, packageRoot, options) {
|
|
142
194
|
const version = gitVersion(sourceRoot)
|
|
143
|
-
const platform = platformId()
|
|
195
|
+
const platform = options.target ?? platformId()
|
|
144
196
|
const buildBin = path.join(packageRoot, "bin")
|
|
145
197
|
const buildWeb = path.join(packageRoot, "web")
|
|
146
198
|
|
|
147
199
|
removeKnownPath(packageRoot, path.dirname(packageRoot))
|
|
148
200
|
mkdirSync(buildBin, { recursive: true })
|
|
149
201
|
|
|
150
|
-
buildGo(sourceRoot, buildBin, "vantaloom-api")
|
|
151
|
-
buildGo(sourceRoot, buildBin, "vantaloom-agent")
|
|
152
|
-
buildGo(sourceRoot, buildBin, "vantaloomctl")
|
|
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)
|
|
153
217
|
|
|
154
218
|
if (options.buildWeb) {
|
|
155
219
|
runPnpm(["--filter", "vantaloom-app", "build"], { cwd: sourceRoot })
|
|
@@ -162,6 +226,45 @@ async function buildRuntimePackage(sourceRoot, packageRoot, options) {
|
|
|
162
226
|
return { platform, version }
|
|
163
227
|
}
|
|
164
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
|
+
|
|
165
268
|
async function applyPackage(packageRoot, prefix, options) {
|
|
166
269
|
assertRuntimePackage(packageRoot)
|
|
167
270
|
|
|
@@ -178,15 +281,82 @@ async function applyPackage(packageRoot, prefix, options) {
|
|
|
178
281
|
spawnSync(existingCtl, ["stop", "--prefix", prefix], { stdio: "inherit" })
|
|
179
282
|
}
|
|
180
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
|
|
181
297
|
for (const name of ["bin", "web", "cli"]) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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),
|
|
187
338
|
})
|
|
188
339
|
}
|
|
189
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
|
+
|
|
190
360
|
await writeLauncher(prefix)
|
|
191
361
|
await writeText(path.join(prefix, "cli", "config.json"), `${JSON.stringify(mergedConfig, null, 2)}\n`)
|
|
192
362
|
await writeText(path.join(prefix, "VERSION"), `${version}\n`)
|
|
@@ -194,7 +364,7 @@ async function applyPackage(packageRoot, prefix, options) {
|
|
|
194
364
|
force: true,
|
|
195
365
|
})
|
|
196
366
|
|
|
197
|
-
run(
|
|
367
|
+
run(ctlBin, [
|
|
198
368
|
"install",
|
|
199
369
|
"--prefix",
|
|
200
370
|
prefix,
|
|
@@ -203,7 +373,7 @@ async function applyPackage(packageRoot, prefix, options) {
|
|
|
203
373
|
])
|
|
204
374
|
|
|
205
375
|
if (!options.noStart) {
|
|
206
|
-
run(
|
|
376
|
+
run(ctlBin, [
|
|
207
377
|
"start",
|
|
208
378
|
"--prefix",
|
|
209
379
|
prefix,
|
|
@@ -213,12 +383,350 @@ async function applyPackage(packageRoot, prefix, options) {
|
|
|
213
383
|
return version
|
|
214
384
|
}
|
|
215
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
|
+
|
|
216
720
|
async function syncFromNpmRegistry(options, action) {
|
|
217
721
|
const prefix = safeDirectory(options.prefix ?? defaultPrefix())
|
|
218
722
|
const installedConfig = readInstalledConfig(prefix)
|
|
219
|
-
|
|
723
|
+
// Always derive runtimePackage from current platform — never trust stale config
|
|
724
|
+
// from a different platform (e.g. win32 config baked into a darwin package).
|
|
725
|
+
// Only explicit --runtime-package flag can override.
|
|
726
|
+
const runtimePackage = options.runtimePackage || runtimePackageName(platformId())
|
|
220
727
|
const runtimeVersion = options.runtimeVersion || installedConfig.runtimeVersion || "latest"
|
|
221
|
-
const
|
|
728
|
+
const explicitRegistry = options.npmRegistry || installedConfig.npmRegistry
|
|
729
|
+
const registry = normalizeRegistry(explicitRegistry || detectNpmRegistry() || defaultNpmRegistry)
|
|
222
730
|
const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-npm-"))
|
|
223
731
|
|
|
224
732
|
try {
|
|
@@ -226,7 +734,11 @@ async function syncFromNpmRegistry(options, action) {
|
|
|
226
734
|
const archive = path.join(tempRoot, `${packageBasename(runtimePackage)}-${runtimeVersion}.tgz`)
|
|
227
735
|
mkdirSync(extractRoot, { recursive: true })
|
|
228
736
|
|
|
229
|
-
const resolved = await
|
|
737
|
+
const resolved = await resolveNpmPackageWithFallback({
|
|
738
|
+
registries: explicitRegistry ? [registry] : [registry, ...fallbackNpmRegistries.map(normalizeRegistry)],
|
|
739
|
+
name: runtimePackage,
|
|
740
|
+
version: runtimeVersion,
|
|
741
|
+
})
|
|
230
742
|
await downloadNpmTarball({
|
|
231
743
|
tarballUrl: resolved.tarball,
|
|
232
744
|
target: archive,
|
|
@@ -240,13 +752,17 @@ async function syncFromNpmRegistry(options, action) {
|
|
|
240
752
|
noStart: options.noStart,
|
|
241
753
|
runtimePackage,
|
|
242
754
|
runtimeVersion: options.runtimeVersion ? resolved.version : "latest",
|
|
243
|
-
npmRegistry: registry,
|
|
755
|
+
npmRegistry: resolved.registry,
|
|
244
756
|
update: action === "update",
|
|
757
|
+
withMesh: options.withMesh,
|
|
758
|
+
skipMesh: options.skipMesh,
|
|
245
759
|
})
|
|
246
760
|
|
|
247
761
|
console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
|
|
248
762
|
console.log(`version: ${version}`)
|
|
249
763
|
console.log(`source: ${runtimePackage}@${resolved.version}`)
|
|
764
|
+
console.log(`registry: ${resolved.registry}`)
|
|
765
|
+
ensureInPath(prefix)
|
|
250
766
|
console.log(`run: ${displayCommand(prefix)} status`)
|
|
251
767
|
} finally {
|
|
252
768
|
removeKnownPath(tempRoot, os.tmpdir())
|
|
@@ -283,11 +799,14 @@ async function syncFromRelease(options, action) {
|
|
|
283
799
|
noStart: options.noStart,
|
|
284
800
|
sourceRoot: installedConfig.sourceRoot,
|
|
285
801
|
update: action === "update",
|
|
802
|
+
withMesh: options.withMesh,
|
|
803
|
+
skipMesh: options.skipMesh,
|
|
286
804
|
})
|
|
287
805
|
|
|
288
806
|
console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
|
|
289
807
|
console.log(`version: ${version}`)
|
|
290
808
|
console.log(`source: ${repo}@${releaseTag}`)
|
|
809
|
+
ensureInPath(prefix)
|
|
291
810
|
console.log(`run: ${displayCommand(prefix)} status`)
|
|
292
811
|
} finally {
|
|
293
812
|
removeKnownPath(tempRoot, os.tmpdir())
|
|
@@ -338,14 +857,114 @@ async function downloadReleaseAsset({ repo, releaseTag, assetName, target, token
|
|
|
338
857
|
await writeFile(target, buffer)
|
|
339
858
|
}
|
|
340
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
|
+
|
|
341
946
|
async function resolveNpmPackage({ registry, name, version }) {
|
|
342
947
|
const metadataUrl = `${registry}/${encodeURIComponent(name).replace("%2F", "%2f")}`
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
}
|
|
349
968
|
if (!response.ok) {
|
|
350
969
|
const detail = await response.text().catch(() => "")
|
|
351
970
|
throw new Error(`failed to inspect npm package ${name}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
|
|
@@ -365,11 +984,22 @@ async function resolveNpmPackage({ registry, name, version }) {
|
|
|
365
984
|
}
|
|
366
985
|
|
|
367
986
|
async function downloadNpmTarball({ tarballUrl, target, packageName, version }) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
}
|
|
373
1003
|
if (!response.ok) {
|
|
374
1004
|
const detail = await response.text().catch(() => "")
|
|
375
1005
|
throw new Error(`failed to download ${packageName}@${version}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
|
|
@@ -377,6 +1007,7 @@ async function downloadNpmTarball({ tarballUrl, target, packageName, version })
|
|
|
377
1007
|
|
|
378
1008
|
const buffer = Buffer.from(await response.arrayBuffer())
|
|
379
1009
|
await writeFile(target, buffer)
|
|
1010
|
+
console.log(` downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`)
|
|
380
1011
|
}
|
|
381
1012
|
|
|
382
1013
|
function shouldUseSourceInstall(options) {
|
|
@@ -440,9 +1071,9 @@ function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
|
|
|
440
1071
|
if (!merged.releaseTag) {
|
|
441
1072
|
merged.releaseTag = defaultReleaseTag
|
|
442
1073
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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())
|
|
446
1077
|
if (!merged.runtimeVersion) {
|
|
447
1078
|
merged.runtimeVersion = "latest"
|
|
448
1079
|
}
|
|
@@ -462,7 +1093,7 @@ function runtimeConfigFromSource(sourceRoot) {
|
|
|
462
1093
|
releaseTag: defaultReleaseTag,
|
|
463
1094
|
runtimePackage: runtimePackageName(platformId()),
|
|
464
1095
|
runtimeVersion: "latest",
|
|
465
|
-
npmRegistry: defaultNpmRegistry,
|
|
1096
|
+
npmRegistry: detectNpmRegistry() || defaultNpmRegistry,
|
|
466
1097
|
}
|
|
467
1098
|
}
|
|
468
1099
|
|
|
@@ -598,13 +1229,20 @@ function printPaths(options) {
|
|
|
598
1229
|
console.log(JSON.stringify({ sourceRoot, prefix }, null, 2))
|
|
599
1230
|
}
|
|
600
1231
|
|
|
601
|
-
function buildGo(sourceRoot, buildBin, name) {
|
|
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"
|
|
602
1239
|
run("go", [
|
|
603
1240
|
"build",
|
|
1241
|
+
"-ldflags", ldflags,
|
|
604
1242
|
"-o",
|
|
605
|
-
path.join(buildBin,
|
|
1243
|
+
path.join(buildBin, `${name}${ext}`),
|
|
606
1244
|
`./apps/api/cmd/${name}`,
|
|
607
|
-
], { cwd: sourceRoot })
|
|
1245
|
+
], { cwd: sourceRoot, env: { ...process.env, ...goEnv } })
|
|
608
1246
|
}
|
|
609
1247
|
|
|
610
1248
|
async function copyDir(source, destination, options = {}) {
|
|
@@ -621,6 +1259,10 @@ async function copyDir(source, destination, options = {}) {
|
|
|
621
1259
|
}
|
|
622
1260
|
|
|
623
1261
|
function writeBuildManifest(buildRoot, version, platform) {
|
|
1262
|
+
const components = ["api", "agent", "web", "ctl"]
|
|
1263
|
+
if (platform.startsWith("win32")) {
|
|
1264
|
+
components.push("tray")
|
|
1265
|
+
}
|
|
624
1266
|
writeFileSync(path.join(buildRoot, "VERSION"), `${version}\n`)
|
|
625
1267
|
writeFileSync(
|
|
626
1268
|
path.join(buildRoot, "manifest.json"),
|
|
@@ -630,7 +1272,7 @@ function writeBuildManifest(buildRoot, version, platform) {
|
|
|
630
1272
|
version,
|
|
631
1273
|
platform,
|
|
632
1274
|
updatedAt: new Date().toISOString(),
|
|
633
|
-
components
|
|
1275
|
+
components,
|
|
634
1276
|
},
|
|
635
1277
|
null,
|
|
636
1278
|
2
|
|
@@ -748,9 +1390,27 @@ function parseOptions(args) {
|
|
|
748
1390
|
case "npm-package":
|
|
749
1391
|
options.npmPackage = true
|
|
750
1392
|
break
|
|
1393
|
+
case "target":
|
|
1394
|
+
options.target = inlineValue ?? args[++index]
|
|
1395
|
+
if (!options.target) {
|
|
1396
|
+
throw new Error("missing value for --target")
|
|
1397
|
+
}
|
|
1398
|
+
break
|
|
751
1399
|
case "local":
|
|
752
1400
|
options.local = true
|
|
753
1401
|
break
|
|
1402
|
+
case "with-mesh":
|
|
1403
|
+
options.withMesh = true
|
|
1404
|
+
break
|
|
1405
|
+
case "skip-mesh":
|
|
1406
|
+
options.skipMesh = true
|
|
1407
|
+
break
|
|
1408
|
+
case "skip-autostart":
|
|
1409
|
+
options.skipAutostart = true
|
|
1410
|
+
break
|
|
1411
|
+
case "no-strict-ssl":
|
|
1412
|
+
options.noStrictSsl = true
|
|
1413
|
+
break
|
|
754
1414
|
default:
|
|
755
1415
|
throw new Error(`unknown option --${key}`)
|
|
756
1416
|
}
|
|
@@ -799,8 +1459,22 @@ function runPnpm(args, options = {}) {
|
|
|
799
1459
|
run("pnpm", args, options)
|
|
800
1460
|
}
|
|
801
1461
|
|
|
802
|
-
function binaryName(name) {
|
|
803
|
-
|
|
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
|
+
}
|
|
804
1478
|
}
|
|
805
1479
|
|
|
806
1480
|
function platformId() {
|
|
@@ -858,21 +1532,113 @@ async function writeText(filePath, content) {
|
|
|
858
1532
|
await writeFile(filePath, content)
|
|
859
1533
|
}
|
|
860
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
|
+
|
|
861
1621
|
function printHelp() {
|
|
862
1622
|
console.log(`Vantaloom CLI
|
|
863
1623
|
|
|
864
1624
|
Usage:
|
|
865
|
-
vantaloom install
|
|
866
|
-
vantaloom install
|
|
867
|
-
vantaloom update
|
|
868
|
-
vantaloom update
|
|
869
|
-
vantaloom
|
|
870
|
-
vantaloom
|
|
871
|
-
vantaloom
|
|
872
|
-
vantaloom
|
|
873
|
-
vantaloom
|
|
874
|
-
vantaloom
|
|
875
|
-
vantaloom
|
|
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]
|
|
1630
|
+
vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package] [--target <platform>]
|
|
1631
|
+
vantaloom start [--prefix <dir>] [--component all|api|agent|web]
|
|
1632
|
+
vantaloom stop [--prefix <dir>] [--component all|api|agent|web]
|
|
1633
|
+
vantaloom restart [--prefix <dir>] [--component all|api|agent|web]
|
|
1634
|
+
vantaloom status [--prefix <dir>]
|
|
1635
|
+
vantaloom ports [--prefix <dir>]
|
|
1636
|
+
vantaloom path [--prefix <dir>] [--source <repo>]
|
|
876
1637
|
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.
|
|
877
1643
|
`)
|
|
878
1644
|
}
|