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