@vantaloom/cli 0.15.35 → 0.15.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.mjs CHANGED
@@ -1,495 +1,528 @@
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
- import {
10
- cliRoot,
11
- defaultNpmRegistry,
12
- fallbackNpmRegistries,
13
- } from "./lib/constants.mjs"
14
- import {
15
- platformId,
16
- runtimePackageName,
17
- safeDirectory,
18
- defaultPrefix,
19
- displayCommand,
20
- readJSONIfExists,
21
- normalizeRegistry,
22
- packageBasename,
23
- toCamel,
24
- removeKnownPath,
25
- run,
26
- } from "./lib/platform.mjs"
27
- import {
28
- applyPackage,
29
- findSourceRoot,
30
- tryFindSourceRoot,
31
- tryAssertSourceRoot,
32
- readInstalledConfig,
33
- writeRuntimePackageMetadata,
34
- ensureInPath,
35
- } from "./lib/install.mjs"
36
- import { buildRuntimePackage } from "./lib/package.mjs"
37
- import {
38
- resolveNpmPackageWithFallback,
39
- downloadNpmTarball,
40
- findExtractedNpmPackage,
41
- } from "./lib/registry.mjs"
42
- import {
43
- runCtl,
44
- uninstallRuntime,
45
- } from "./lib/lifecycle.mjs"
46
- import {
47
- runLogin,
48
- runLogout,
49
- localApiBase,
50
- } from "./lib/auth.mjs"
51
-
52
- // Inherit strict-ssl=false from npm/npx config, or respect NODE_TLS_REJECT_UNAUTHORIZED.
53
- // When npm runs us via npx with strict-ssl disabled, it sets npm_config_strict_ssl="false".
54
- // Also check user .npmrc for strict-ssl=false (common in China behind proxies).
55
- function shouldDisableTLS() {
56
- if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") return true
57
- if (process.env.npm_config_strict_ssl === "false") return true
58
- if (process.env.npm_config_strict_ssl === "") return true
59
- try {
60
- const npmrcPath = path.join(os.homedir(), ".npmrc")
61
- if (existsSync(npmrcPath)) {
62
- const content = readFileSync(npmrcPath, "utf8")
63
- if (/^\s*strict-ssl\s*=\s*false/m.test(content)) return true
64
- }
65
- } catch {}
66
- return false
67
- }
68
- if (shouldDisableTLS()) {
69
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
70
- }
71
-
72
- const cliVersion = readJSONIfExists(path.join(cliRoot, "package.json")).version ?? "unknown"
73
-
74
- export async function main(argv) {
75
- const command = argv[0] ?? "help"
76
- const options = parseOptions(argv.slice(1))
77
-
78
- // --no-strict-ssl flag disables TLS certificate verification for fetch calls
79
- if (options.noStrictSsl) {
80
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
81
- }
82
-
83
- if (command === "install" || command === "update") {
84
- console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
85
- }
86
-
87
- switch (command) {
88
- case "install":
89
- if (options.package) {
90
- await installFromPackage({ ...options, update: false })
91
- } else if (shouldUseSourceInstall(options)) {
92
- await installFromSource(options)
93
- } else {
94
- await syncFromNpmRegistry(options, "install")
95
- }
96
- return
97
- case "update":
98
- if (options.package) {
99
- await installFromPackage({ ...options, update: true })
100
- } else if (shouldUseSourceInstall(options)) {
101
- await installFromSource({ ...options, update: true })
102
- } else {
103
- await syncFromNpmRegistry(options, "update")
104
- }
105
- return
106
- case "package":
107
- await packageRuntime(options)
108
- return
109
- case "platform":
110
- console.log(platformId())
111
- return
112
- case "start":
113
- case "stop":
114
- case "restart":
115
- case "status":
116
- case "ports":
117
- runCtl(command, options)
118
- return
119
- case "path":
120
- printPaths(options)
121
- return
122
- case "uninstall":
123
- await uninstallRuntime(options)
124
- return
125
- case "login":
126
- await runLogin(options)
127
- return
128
- case "logout":
129
- await runLogout(options)
130
- return
131
- case "bootstrap":
132
- await runBootstrap(options)
133
- return
134
- case "help":
135
- case "-h":
136
- case "--help":
137
- printHelp()
138
- return
139
- default:
140
- throw new Error(`unknown command "${command}"`)
141
- }
142
- }
143
-
144
- async function installFromSource(options) {
145
- const sourceRoot = findSourceRoot(options.source)
146
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
147
- const buildRoot = path.join(sourceRoot, "artifacts", "local-install", platformId())
148
-
149
- const { version } = await buildRuntimePackage(sourceRoot, buildRoot, {
150
- buildWeb: options.buildWeb,
151
- })
152
-
153
- await applyPackage(buildRoot, prefix, {
154
- noStart: options.noStart,
155
- sourceRoot,
156
- update: options.update,
157
- })
158
-
159
- console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
160
- console.log(`version: ${version}`)
161
- ensureInPath(prefix)
162
- console.log(`run: ${displayCommand(prefix)} status`)
163
- }
164
-
165
- async function installFromPackage(options) {
166
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
167
- const packageRoot = safeDirectory(options.package)
168
- const version = await applyPackage(packageRoot, prefix, {
169
- noStart: options.noStart,
170
- update: options.update,
171
- })
172
-
173
- console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
174
- console.log(`version: ${version}`)
175
- ensureInPath(prefix)
176
- console.log(`run: ${displayCommand(prefix)} status`)
177
- }
178
-
179
- async function packageRuntime(options) {
180
- const sourceRoot = findSourceRoot(options.source)
181
- const targetPlatform = options.target ?? platformId()
182
- const packageRoot = safeDirectory(
183
- options.output ?? path.join(sourceRoot, "artifacts", "packages", `vantaloom-${targetPlatform}`)
184
- )
185
- const { platform: builtPlatform, version } = await buildRuntimePackage(sourceRoot, packageRoot, {
186
- buildWeb: options.buildWeb,
187
- target: targetPlatform,
188
- npmPackage: options.npmPackage,
189
- })
190
-
191
- if (options.npmPackage) {
192
- writeRuntimePackageMetadata(packageRoot, sourceRoot, builtPlatform)
193
- }
194
-
195
- if (options.archive) {
196
- const archivePath = `${packageRoot}.tar.gz`
197
- removeKnownPath(archivePath, path.dirname(archivePath))
198
- run("tar", [
199
- "-czf",
200
- archivePath,
201
- "-C",
202
- path.dirname(packageRoot),
203
- path.basename(packageRoot),
204
- ])
205
- console.log(`archive: ${archivePath}`)
206
- }
207
-
208
- console.log(`packaged Vantaloom: ${packageRoot}`)
209
- console.log(`platform: ${builtPlatform}`)
210
- console.log(`version: ${version}`)
211
- }
212
-
213
- async function syncFromNpmRegistry(options, action) {
214
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
215
- const installedConfig = readInstalledConfig(prefix)
216
- // Always derive runtimePackage from current platform — never trust stale config
217
- // from a different platform (e.g. win32 config baked into a darwin package).
218
- // Only explicit --runtime-package flag can override.
219
- const runtimePackage = options.runtimePackage || runtimePackageName(platformId())
220
- const runtimeVersion = options.runtimeVersion || installedConfig.runtimeVersion || "latest"
221
- const explicitRegistry = options.npmRegistry || installedConfig.npmRegistry
222
- // Resolve against npmjs.org — the registry the runtime packages are published
223
- // to. Ambient mirror config (npm_config_registry / .npmrc) is deliberately NOT
224
- // adopted: mirrors lag on dist-tags (npmmirror still served "latest" = 0.6.12
225
- // months after 0.15.x shipped), and trusting one turns an update into a silent
226
- // rollback. Mirrors stay in the fallback chain for unreachable-network cases.
227
- const registry = normalizeRegistry(explicitRegistry || defaultNpmRegistry)
228
- const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-npm-"))
229
-
230
- try {
231
- const extractRoot = path.join(tempRoot, "extract")
232
- const archive = path.join(tempRoot, `${packageBasename(runtimePackage)}-${runtimeVersion}.tgz`)
233
- mkdirSync(extractRoot, { recursive: true })
234
-
235
- const resolved = await resolveNpmPackageWithFallback({
236
- registries: explicitRegistry ? [registry] : [registry, ...fallbackNpmRegistries.map(normalizeRegistry)],
237
- name: runtimePackage,
238
- version: runtimeVersion,
239
- })
240
- assertNotDowngrade({ prefix, resolved, runtimePackage, options, action })
241
- await downloadNpmTarball({
242
- tarballUrl: resolved.tarball,
243
- target: archive,
244
- packageName: runtimePackage,
245
- version: resolved.version,
246
- })
247
- run("tar", ["-xzf", archive, "-C", extractRoot])
248
-
249
- const packageRoot = findExtractedNpmPackage(extractRoot)
250
- const version = await applyPackage(packageRoot, prefix, {
251
- noStart: options.noStart,
252
- runtimePackage,
253
- runtimeVersion: options.runtimeVersion ? resolved.version : "latest",
254
- npmRegistry: resolved.registry,
255
- update: action === "update",
256
- })
257
-
258
- console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
259
- console.log(`version: ${version}`)
260
- console.log(`source: ${runtimePackage}@${resolved.version}`)
261
- console.log(`registry: ${resolved.registry}`)
262
- ensureInPath(prefix)
263
- console.log(`run: ${displayCommand(prefix)} status`)
264
- } finally {
265
- removeKnownPath(tempRoot, os.tmpdir())
266
- }
267
- }
268
-
269
- // A registry mirror can answer "latest" with a version far older than the one
270
- // already installed (npmmirror did exactly that: 0.6.12 long after 0.15.x
271
- // shipped), turning an update into a silent rollback. Refuse it unless the
272
- // caller asked for that exact version or opted in explicitly.
273
- function assertNotDowngrade({ prefix, resolved, runtimePackage, options, action }) {
274
- if (action !== "update" || options.runtimeVersion || options.allowDowngrade) {
275
- return
276
- }
277
- const installed = readRuntimeVersion(prefix)
278
- if (!isOlderVersion(resolved.version, installed)) {
279
- return
280
- }
281
- throw new Error(
282
- `${resolved.registry} offers ${runtimePackage}@${resolved.version}, older than the installed ${installed}.\n` +
283
- ` A mirror whose dist-tags lag behind npmjs.org is the usual cause.\n` +
284
- ` Retry with --npm-registry https://registry.npmjs.org/, or pass --allow-downgrade to install it anyway.`
285
- )
286
- }
287
-
288
- // Installed runtime version ("" when absent/unreadable never block on it).
289
- function readRuntimeVersion(prefix) {
290
- try {
291
- return readFileSync(path.join(prefix, "VERSION"), "utf8").trim()
292
- } catch {
293
- return ""
294
- }
295
- }
296
-
297
- // candidate < installed on the X.Y.Z triple (a -/+ suffix on patch is ignored).
298
- // Non-semver on either side false: never block a locally built or unversioned
299
- // install from being replaced.
300
- function isOlderVersion(candidate, installed) {
301
- const parse = (value) => {
302
- const parts = String(value).split(".")
303
- if (parts.length < 3) {
304
- return null
305
- }
306
- const triple = [parts[0], parts[1], parts.slice(2).join(".").split(/[-+]/)[0]].map(Number)
307
- return triple.every(Number.isInteger) ? triple : null
308
- }
309
- const found = parse(candidate)
310
- const current = parse(installed)
311
- if (!found || !current) {
312
- return false
313
- }
314
- for (let index = 0; index < 3; index += 1) {
315
- if (found[index] !== current[index]) {
316
- return found[index] < current[index]
317
- }
318
- }
319
- return false
320
- }
321
-
322
- function shouldUseSourceInstall(options) {
323
- return Boolean(options.local || options.source || options.buildWeb)
324
- }
325
-
326
- function printPaths(options) {
327
- const sourceRoot = findSourceRoot(options.source)
328
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
329
- console.log(JSON.stringify({ sourceRoot, prefix }, null, 2))
330
- }
331
-
332
- function parseOptions(args) {
333
- const options = {}
334
- for (let index = 0; index < args.length; index += 1) {
335
- const arg = args[index]
336
- if (!arg.startsWith("--")) {
337
- throw new Error(`unexpected argument "${arg}"`)
338
- }
339
- const [key, inlineValue] = arg.slice(2).split("=", 2)
340
- switch (key) {
341
- case "prefix":
342
- case "source":
343
- case "component":
344
- case "package":
345
- case "output":
346
- case "runtime-package":
347
- case "runtime-version":
348
- case "npm-registry":
349
- case "hub":
350
- case "email":
351
- case "password":
352
- options[toCamel(key)] = inlineValue ?? args[++index]
353
- if (!options[toCamel(key)]) {
354
- throw new Error(`missing value for --${key}`)
355
- }
356
- break
357
- case "build-web":
358
- options.buildWeb = true
359
- break
360
- case "no-start":
361
- options.noStart = true
362
- break
363
- case "archive":
364
- options.archive = true
365
- break
366
- case "npm-package":
367
- options.npmPackage = true
368
- break
369
- case "target":
370
- options.target = inlineValue ?? args[++index]
371
- if (!options.target) {
372
- throw new Error("missing value for --target")
373
- }
374
- break
375
- case "local":
376
- options.local = true
377
- break
378
- case "skip-autostart":
379
- options.skipAutostart = true
380
- break
381
- case "no-strict-ssl":
382
- options.noStrictSsl = true
383
- break
384
- case "allow-downgrade":
385
- options.allowDowngrade = true
386
- break
387
- default:
388
- throw new Error(`unknown option --${key}`)
389
- }
390
- }
391
- return options
392
- }
393
-
394
- function printHelp() {
395
- console.log(`Vantaloom CLI
396
-
397
- Usage:
398
- vantaloom install [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start]
399
- vantaloom install --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
400
- vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start] [--allow-downgrade]
401
- vantaloom update --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
402
- vantaloom uninstall [--prefix <dir>]
403
- vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package] [--target <platform>]
404
- vantaloom start [--prefix <dir>] [--component all|api|agent|web]
405
- vantaloom stop [--prefix <dir>] [--component all|api|agent|web]
406
- vantaloom restart [--prefix <dir>] [--component all|api|agent|web]
407
- vantaloom status [--prefix <dir>]
408
- vantaloom ports [--prefix <dir>]
409
- vantaloom path [--prefix <dir>] [--source <repo>]
410
- vantaloom platform
411
- vantaloom login [--hub <url>] [--email <email>] [--password <pw>] [--prefix <dir>]
412
- vantaloom logout [--prefix <dir>]
413
- vantaloom bootstrap [--hub <url>] --email <email> --password <pw> [--prefix <dir>] [--runtime-version <ver>] [--npm-registry <url>]
414
-
415
- Hub login (for machines with no web access):
416
- "vantaloom login" authenticates to the Hub from the terminal and joins this
417
- machine to your workgroup — no browser needed. Email/password may be passed as
418
- flags (for scripts) or entered interactively. The local runtime must be running
419
- (run "vantaloom start" first).
420
-
421
- Unattended bootstrap (one command for fresh hosts):
422
- "vantaloom bootstrap" installs the runtime from npm, starts it, waits for it
423
- to be ready, then logs in to the Hub — all in one step. Designed for ssh/scp
424
- deployment scripts and cloud-init. Requires --email/--password (or env
425
- VANTALOOM_HUB_EMAIL/VANTALOOM_HUB_PASSWORD) and --hub (or env
426
- VANTALOOM_HUB_URL). The runtime auto-starts and reconnects on reboot.
427
- `)
428
- }
429
-
430
- // runBootstrap = install runtime (from npm) + wait ready + login Hub,一条命令完成无人值守部署。
431
- // 面向 ssh/scp 远程部署脚本和 cloud-init 场景;用户在远程主机 npm i -g @vantaloom/cli 后
432
- // 跑这条命令即可把机器加入 workgroup,无需进 UI。
433
- async function runBootstrap(options) {
434
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
435
- const hubUrl = options.hub || process.env.VANTALOOM_HUB_URL || process.env.NEXT_PUBLIC_HUB_URL
436
- const email = options.email || process.env.VANTALOOM_HUB_EMAIL
437
- const password = options.password || process.env.VANTALOOM_HUB_PASSWORD
438
-
439
- if (!hubUrl) {
440
- throw new Error("bootstrap 需要 --hub <url>(或设置 VANTALOOM_HUB_URL 环境变量)")
441
- }
442
- if (!email || !password) {
443
- throw new Error("bootstrap 需要 --email --password(或设置 VANTALOOM_HUB_EMAIL/VANTALOOM_HUB_PASSWORD 环境变量)")
444
- }
445
-
446
- // 1. 安装 runtime(从 npm 拉取,自动 start)
447
- console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
448
- console.log(`\n▸ 安装 runtime(从 npm)`)
449
- const installOpts = { ...options, prefix }
450
- // bootstrap 不允许 --no-start,runtime 必须起来才能 login
451
- delete installOpts.noStart
452
- await syncFromNpmRegistry(installOpts, "install")
453
-
454
- // 2. 等待 runtime 就绪(轮询 /v1/hub/status)
455
- const localApi = localApiBase(prefix)
456
- console.log(`\n▸ 等待 runtime 就绪(${localApi})`)
457
- let ready = false
458
- for (let i = 0; i < 30; i++) {
459
- await sleep(2000)
460
- try {
461
- const res = await fetch(`${localApi}/v1/hub/status`)
462
- if (res.ok) {
463
- ready = true
464
- console.log(` ✓ runtime 就绪(第 ${i + 1} 次探测)`)
465
- break
466
- }
467
- } catch {}
468
- if (i % 5 === 0) {
469
- console.log(` 等待中...(第 ${i + 1} 次)`)
470
- }
471
- }
472
- if (!ready) {
473
- throw new Error(`runtime 60s 内未就绪(${localApi} 不可达)。请手动检查 "vantaloom status" 后重试 "vantaloom login"。`)
474
- }
475
-
476
- // 3. 登录 Hub + 注册机器 + 连接
477
- console.log(`\n▸ 登录 Hub 并连接`)
478
- await runLogin({
479
- hub: hubUrl,
480
- email,
481
- password,
482
- prefix,
483
- })
484
-
485
- console.log(`\n════════════════════════════════════════`)
486
- console.log(` 无人值守部署完成!`)
487
- console.log(` 安装目录: ${prefix}`)
488
- console.log(` Hub: ${hubUrl}`)
489
- console.log(` runtime 已启动并会在重启后自动恢复。`)
490
- console.log(`════════════════════════════════════════`)
491
- }
492
-
493
- function sleep(ms) {
494
- return new Promise(resolve => setTimeout(resolve, ms))
495
- }
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
+ import {
10
+ cliRoot,
11
+ defaultNpmRegistry,
12
+ fallbackNpmRegistries,
13
+ } from "./lib/constants.mjs"
14
+ import {
15
+ platformId,
16
+ runtimePackageName,
17
+ safeDirectory,
18
+ defaultPrefix,
19
+ displayCommand,
20
+ readJSONIfExists,
21
+ normalizeRegistry,
22
+ packageBasename,
23
+ toCamel,
24
+ removeKnownPath,
25
+ run,
26
+ } from "./lib/platform.mjs"
27
+ import {
28
+ applyPackage,
29
+ findSourceRoot,
30
+ tryFindSourceRoot,
31
+ tryAssertSourceRoot,
32
+ readInstalledConfig,
33
+ writeRuntimePackageMetadata,
34
+ ensureInPath,
35
+ } from "./lib/install.mjs"
36
+ import { buildRuntimePackage } from "./lib/package.mjs"
37
+ import {
38
+ resolveNpmPackageWithFallback,
39
+ downloadNpmTarball,
40
+ findExtractedNpmPackage,
41
+ } from "./lib/registry.mjs"
42
+ import {
43
+ runCtl,
44
+ uninstallRuntime,
45
+ } from "./lib/lifecycle.mjs"
46
+ import {
47
+ runLogin,
48
+ runLogout,
49
+ localApiBase,
50
+ } from "./lib/auth.mjs"
51
+ import { runMcp, mcpHint } from "./lib/mcp.mjs"
52
+
53
+ // Inherit strict-ssl=false from npm/npx config, or respect NODE_TLS_REJECT_UNAUTHORIZED.
54
+ // When npm runs us via npx with strict-ssl disabled, it sets npm_config_strict_ssl="false".
55
+ // Also check user .npmrc for strict-ssl=false (common in China behind proxies).
56
+ function shouldDisableTLS() {
57
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") return true
58
+ if (process.env.npm_config_strict_ssl === "false") return true
59
+ if (process.env.npm_config_strict_ssl === "") return true
60
+ try {
61
+ const npmrcPath = path.join(os.homedir(), ".npmrc")
62
+ if (existsSync(npmrcPath)) {
63
+ const content = readFileSync(npmrcPath, "utf8")
64
+ if (/^\s*strict-ssl\s*=\s*false/m.test(content)) return true
65
+ }
66
+ } catch {}
67
+ return false
68
+ }
69
+ if (shouldDisableTLS()) {
70
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
71
+ }
72
+
73
+ const cliVersion = readJSONIfExists(path.join(cliRoot, "package.json")).version ?? "unknown"
74
+
75
+ export async function main(argv) {
76
+ const command = argv[0] ?? "help"
77
+ // "mcp" 带子命令(status/path/print/install);其余命令的参数全是 --flag。
78
+ let rest = argv.slice(1)
79
+ let subcommand
80
+ if (command === "mcp" && rest[0] && !rest[0].startsWith("--")) {
81
+ subcommand = rest[0]
82
+ rest = rest.slice(1)
83
+ }
84
+ const options = parseOptions(rest)
85
+
86
+ // --no-strict-ssl flag disables TLS certificate verification for fetch calls
87
+ if (options.noStrictSsl) {
88
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
89
+ }
90
+
91
+ if (command === "install" || command === "update") {
92
+ console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
93
+ }
94
+
95
+ switch (command) {
96
+ case "install":
97
+ if (options.package) {
98
+ await installFromPackage({ ...options, update: false })
99
+ } else if (shouldUseSourceInstall(options)) {
100
+ await installFromSource(options)
101
+ } else {
102
+ await syncFromNpmRegistry(options, "install")
103
+ }
104
+ return
105
+ case "update":
106
+ if (options.package) {
107
+ await installFromPackage({ ...options, update: true })
108
+ } else if (shouldUseSourceInstall(options)) {
109
+ await installFromSource({ ...options, update: true })
110
+ } else {
111
+ await syncFromNpmRegistry(options, "update")
112
+ }
113
+ return
114
+ case "package":
115
+ await packageRuntime(options)
116
+ return
117
+ case "platform":
118
+ console.log(platformId())
119
+ return
120
+ case "start":
121
+ case "stop":
122
+ case "restart":
123
+ case "status":
124
+ case "ports":
125
+ runCtl(command, options)
126
+ return
127
+ case "path":
128
+ printPaths(options)
129
+ return
130
+ case "uninstall":
131
+ await uninstallRuntime(options)
132
+ return
133
+ case "login":
134
+ await runLogin(options)
135
+ return
136
+ case "logout":
137
+ await runLogout(options)
138
+ return
139
+ case "bootstrap":
140
+ await runBootstrap(options)
141
+ return
142
+ case "mcp":
143
+ await runMcp(subcommand, options)
144
+ return
145
+ case "help":
146
+ case "-h":
147
+ case "--help":
148
+ printHelp()
149
+ return
150
+ default:
151
+ throw new Error(`unknown command "${command}"`)
152
+ }
153
+ }
154
+
155
+ async function installFromSource(options) {
156
+ const sourceRoot = findSourceRoot(options.source)
157
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
158
+ const buildRoot = path.join(sourceRoot, "artifacts", "local-install", platformId())
159
+
160
+ const { version } = await buildRuntimePackage(sourceRoot, buildRoot, {
161
+ buildWeb: options.buildWeb,
162
+ })
163
+
164
+ await applyPackage(buildRoot, prefix, {
165
+ noStart: options.noStart,
166
+ sourceRoot,
167
+ update: options.update,
168
+ })
169
+
170
+ console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
171
+ console.log(mcpHint(prefix))
172
+ console.log(`version: ${version}`)
173
+ ensureInPath(prefix)
174
+ console.log(`run: ${displayCommand(prefix)} status`)
175
+ }
176
+
177
+ async function installFromPackage(options) {
178
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
179
+ const packageRoot = safeDirectory(options.package)
180
+ const version = await applyPackage(packageRoot, prefix, {
181
+ noStart: options.noStart,
182
+ update: options.update,
183
+ })
184
+
185
+ console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
186
+ console.log(mcpHint(prefix))
187
+ console.log(`version: ${version}`)
188
+ ensureInPath(prefix)
189
+ console.log(`run: ${displayCommand(prefix)} status`)
190
+ }
191
+
192
+ async function packageRuntime(options) {
193
+ const sourceRoot = findSourceRoot(options.source)
194
+ const targetPlatform = options.target ?? platformId()
195
+ const packageRoot = safeDirectory(
196
+ options.output ?? path.join(sourceRoot, "artifacts", "packages", `vantaloom-${targetPlatform}`)
197
+ )
198
+ const { platform: builtPlatform, version } = await buildRuntimePackage(sourceRoot, packageRoot, {
199
+ buildWeb: options.buildWeb,
200
+ target: targetPlatform,
201
+ npmPackage: options.npmPackage,
202
+ })
203
+
204
+ if (options.npmPackage) {
205
+ writeRuntimePackageMetadata(packageRoot, sourceRoot, builtPlatform)
206
+ }
207
+
208
+ if (options.archive) {
209
+ const archivePath = `${packageRoot}.tar.gz`
210
+ removeKnownPath(archivePath, path.dirname(archivePath))
211
+ run("tar", [
212
+ "-czf",
213
+ archivePath,
214
+ "-C",
215
+ path.dirname(packageRoot),
216
+ path.basename(packageRoot),
217
+ ])
218
+ console.log(`archive: ${archivePath}`)
219
+ }
220
+
221
+ console.log(`packaged Vantaloom: ${packageRoot}`)
222
+ console.log(`platform: ${builtPlatform}`)
223
+ console.log(`version: ${version}`)
224
+ }
225
+
226
+ async function syncFromNpmRegistry(options, action) {
227
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
228
+ const installedConfig = readInstalledConfig(prefix)
229
+ // Always derive runtimePackage from current platform — never trust stale config
230
+ // from a different platform (e.g. win32 config baked into a darwin package).
231
+ // Only explicit --runtime-package flag can override.
232
+ const runtimePackage = options.runtimePackage || runtimePackageName(platformId())
233
+ const runtimeVersion = options.runtimeVersion || installedConfig.runtimeVersion || "latest"
234
+ const explicitRegistry = options.npmRegistry || installedConfig.npmRegistry
235
+ // Resolve against npmjs.org — the registry the runtime packages are published
236
+ // to. Ambient mirror config (npm_config_registry / .npmrc) is deliberately NOT
237
+ // adopted: mirrors lag on dist-tags (npmmirror still served "latest" = 0.6.12
238
+ // months after 0.15.x shipped), and trusting one turns an update into a silent
239
+ // rollback. Mirrors stay in the fallback chain for unreachable-network cases.
240
+ const registry = normalizeRegistry(explicitRegistry || defaultNpmRegistry)
241
+ const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-npm-"))
242
+
243
+ try {
244
+ const extractRoot = path.join(tempRoot, "extract")
245
+ const archive = path.join(tempRoot, `${packageBasename(runtimePackage)}-${runtimeVersion}.tgz`)
246
+ mkdirSync(extractRoot, { recursive: true })
247
+
248
+ const resolved = await resolveNpmPackageWithFallback({
249
+ registries: explicitRegistry ? [registry] : [registry, ...fallbackNpmRegistries.map(normalizeRegistry)],
250
+ name: runtimePackage,
251
+ version: runtimeVersion,
252
+ })
253
+ assertNotDowngrade({ prefix, resolved, runtimePackage, options, action })
254
+ await downloadNpmTarball({
255
+ tarballUrl: resolved.tarball,
256
+ target: archive,
257
+ packageName: runtimePackage,
258
+ version: resolved.version,
259
+ })
260
+ run("tar", ["-xzf", archive, "-C", extractRoot])
261
+
262
+ const packageRoot = findExtractedNpmPackage(extractRoot)
263
+ const version = await applyPackage(packageRoot, prefix, {
264
+ noStart: options.noStart,
265
+ runtimePackage,
266
+ runtimeVersion: options.runtimeVersion ? resolved.version : "latest",
267
+ npmRegistry: resolved.registry,
268
+ update: action === "update",
269
+ })
270
+
271
+ console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
272
+ console.log(mcpHint(prefix))
273
+ console.log(`version: ${version}`)
274
+ console.log(`source: ${runtimePackage}@${resolved.version}`)
275
+ console.log(`registry: ${resolved.registry}`)
276
+ ensureInPath(prefix)
277
+ console.log(`run: ${displayCommand(prefix)} status`)
278
+ } finally {
279
+ removeKnownPath(tempRoot, os.tmpdir())
280
+ }
281
+ }
282
+
283
+ // A registry mirror can answer "latest" with a version far older than the one
284
+ // already installed (npmmirror did exactly that: 0.6.12 long after 0.15.x
285
+ // shipped), turning an update into a silent rollback. Refuse it unless the
286
+ // caller asked for that exact version or opted in explicitly.
287
+ function assertNotDowngrade({ prefix, resolved, runtimePackage, options, action }) {
288
+ if (action !== "update" || options.runtimeVersion || options.allowDowngrade) {
289
+ return
290
+ }
291
+ const installed = readRuntimeVersion(prefix)
292
+ if (!isOlderVersion(resolved.version, installed)) {
293
+ return
294
+ }
295
+ throw new Error(
296
+ `${resolved.registry} offers ${runtimePackage}@${resolved.version}, older than the installed ${installed}.\n` +
297
+ ` A mirror whose dist-tags lag behind npmjs.org is the usual cause.\n` +
298
+ ` Retry with --npm-registry https://registry.npmjs.org/, or pass --allow-downgrade to install it anyway.`
299
+ )
300
+ }
301
+
302
+ // Installed runtime version ("" when absent/unreadable — never block on it).
303
+ function readRuntimeVersion(prefix) {
304
+ try {
305
+ return readFileSync(path.join(prefix, "VERSION"), "utf8").trim()
306
+ } catch {
307
+ return ""
308
+ }
309
+ }
310
+
311
+ // candidate < installed on the X.Y.Z triple (a -/+ suffix on patch is ignored).
312
+ // Non-semver on either side → false: never block a locally built or unversioned
313
+ // install from being replaced.
314
+ function isOlderVersion(candidate, installed) {
315
+ const parse = (value) => {
316
+ const parts = String(value).split(".")
317
+ if (parts.length < 3) {
318
+ return null
319
+ }
320
+ const triple = [parts[0], parts[1], parts.slice(2).join(".").split(/[-+]/)[0]].map(Number)
321
+ return triple.every(Number.isInteger) ? triple : null
322
+ }
323
+ const found = parse(candidate)
324
+ const current = parse(installed)
325
+ if (!found || !current) {
326
+ return false
327
+ }
328
+ for (let index = 0; index < 3; index += 1) {
329
+ if (found[index] !== current[index]) {
330
+ return found[index] < current[index]
331
+ }
332
+ }
333
+ return false
334
+ }
335
+
336
+ function shouldUseSourceInstall(options) {
337
+ return Boolean(options.local || options.source || options.buildWeb)
338
+ }
339
+
340
+ function printPaths(options) {
341
+ const sourceRoot = findSourceRoot(options.source)
342
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
343
+ console.log(JSON.stringify({ sourceRoot, prefix }, null, 2))
344
+ }
345
+
346
+ function parseOptions(args) {
347
+ const options = {}
348
+ for (let index = 0; index < args.length; index += 1) {
349
+ const arg = args[index]
350
+ if (!arg.startsWith("--")) {
351
+ throw new Error(`unexpected argument "${arg}"`)
352
+ }
353
+ const [key, inlineValue] = arg.slice(2).split("=", 2)
354
+ switch (key) {
355
+ case "prefix":
356
+ case "source":
357
+ case "component":
358
+ case "package":
359
+ case "output":
360
+ case "runtime-package":
361
+ case "runtime-version":
362
+ case "npm-registry":
363
+ case "hub":
364
+ case "email":
365
+ case "password":
366
+ case "client":
367
+ case "api":
368
+ options[toCamel(key)] = inlineValue ?? args[++index]
369
+ if (!options[toCamel(key)]) {
370
+ throw new Error(`missing value for --${key}`)
371
+ }
372
+ break
373
+ case "build-web":
374
+ options.buildWeb = true
375
+ break
376
+ case "no-start":
377
+ options.noStart = true
378
+ break
379
+ case "archive":
380
+ options.archive = true
381
+ break
382
+ case "npm-package":
383
+ options.npmPackage = true
384
+ break
385
+ case "target":
386
+ options.target = inlineValue ?? args[++index]
387
+ if (!options.target) {
388
+ throw new Error("missing value for --target")
389
+ }
390
+ break
391
+ case "local":
392
+ options.local = true
393
+ break
394
+ case "skip-autostart":
395
+ options.skipAutostart = true
396
+ break
397
+ case "no-strict-ssl":
398
+ options.noStrictSsl = true
399
+ break
400
+ case "allow-downgrade":
401
+ options.allowDowngrade = true
402
+ break
403
+ case "force":
404
+ options.force = true
405
+ break
406
+ case "no-skill":
407
+ options.skill = false
408
+ break
409
+ default:
410
+ throw new Error(`unknown option --${key}`)
411
+ }
412
+ }
413
+ return options
414
+ }
415
+
416
+ function printHelp() {
417
+ console.log(`Vantaloom CLI
418
+
419
+ Usage:
420
+ vantaloom install [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start]
421
+ vantaloom install --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
422
+ vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start] [--allow-downgrade]
423
+ vantaloom update --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
424
+ vantaloom uninstall [--prefix <dir>]
425
+ vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package] [--target <platform>]
426
+ vantaloom start [--prefix <dir>] [--component all|api|agent|web]
427
+ vantaloom stop [--prefix <dir>] [--component all|api|agent|web]
428
+ vantaloom restart [--prefix <dir>] [--component all|api|agent|web]
429
+ vantaloom status [--prefix <dir>]
430
+ vantaloom ports [--prefix <dir>]
431
+ vantaloom path [--prefix <dir>] [--source <repo>]
432
+ vantaloom platform
433
+ vantaloom login [--hub <url>] [--email <email>] [--password <pw>] [--prefix <dir>]
434
+ vantaloom logout [--prefix <dir>]
435
+ vantaloom bootstrap [--hub <url>] --email <email> --password <pw> [--prefix <dir>] [--runtime-version <ver>] [--npm-registry <url>]
436
+ vantaloom mcp [status|path|print|install] [--client claude|cursor|codex|all] [--api <url>] [--force] [--no-skill] [--prefix <dir>]
437
+
438
+ Let another agent drive this machine (MCP bridge):
439
+ "vantaloom mcp install" registers the bundled MCP server (<prefix>/bin/vantaloom-mcp)
440
+ with Claude Code / Cursor / Codex and installs the vantaloom-fleet skill, so an
441
+ external agent can list your machines, run commands, open terminals and move
442
+ files across the mesh — a keyless SSH replacement. "vantaloom mcp" alone prints
443
+ the binary path and per-client registration status; "print" emits the config
444
+ snippets if you would rather paste them yourself. The MCP server is launched by
445
+ the agent client (independent of the Vantaloom runtime lifecycle) but needs the
446
+ local runtime to be running.
447
+
448
+ Hub login (for machines with no web access):
449
+ "vantaloom login" authenticates to the Hub from the terminal and joins this
450
+ machine to your workgroup — no browser needed. Email/password may be passed as
451
+ flags (for scripts) or entered interactively. The local runtime must be running
452
+ (run "vantaloom start" first).
453
+
454
+ Unattended bootstrap (one command for fresh hosts):
455
+ "vantaloom bootstrap" installs the runtime from npm, starts it, waits for it
456
+ to be ready, then logs in to the Hub — all in one step. Designed for ssh/scp
457
+ deployment scripts and cloud-init. Requires --email/--password (or env
458
+ VANTALOOM_HUB_EMAIL/VANTALOOM_HUB_PASSWORD) and --hub (or env
459
+ VANTALOOM_HUB_URL). The runtime auto-starts and reconnects on reboot.
460
+ `)
461
+ }
462
+
463
+ // runBootstrap = install runtime (from npm) + wait ready + login Hub,一条命令完成无人值守部署。
464
+ // 面向 ssh/scp 远程部署脚本和 cloud-init 场景;用户在远程主机 npm i -g @vantaloom/cli
465
+ // 跑这条命令即可把机器加入 workgroup,无需进 UI。
466
+ async function runBootstrap(options) {
467
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
468
+ const hubUrl = options.hub || process.env.VANTALOOM_HUB_URL || process.env.NEXT_PUBLIC_HUB_URL
469
+ const email = options.email || process.env.VANTALOOM_HUB_EMAIL
470
+ const password = options.password || process.env.VANTALOOM_HUB_PASSWORD
471
+
472
+ if (!hubUrl) {
473
+ throw new Error("bootstrap 需要 --hub <url>(或设置 VANTALOOM_HUB_URL 环境变量)")
474
+ }
475
+ if (!email || !password) {
476
+ throw new Error("bootstrap 需要 --email --password(或设置 VANTALOOM_HUB_EMAIL/VANTALOOM_HUB_PASSWORD 环境变量)")
477
+ }
478
+
479
+ // 1. 安装 runtime(从 npm 拉取,自动 start)
480
+ console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
481
+ console.log(`\n▸ 安装 runtime(从 npm)`)
482
+ const installOpts = { ...options, prefix }
483
+ // bootstrap 不允许 --no-start,runtime 必须起来才能 login
484
+ delete installOpts.noStart
485
+ await syncFromNpmRegistry(installOpts, "install")
486
+
487
+ // 2. 等待 runtime 就绪(轮询 /v1/hub/status)
488
+ const localApi = localApiBase(prefix)
489
+ console.log(`\n▸ 等待 runtime 就绪(${localApi})`)
490
+ let ready = false
491
+ for (let i = 0; i < 30; i++) {
492
+ await sleep(2000)
493
+ try {
494
+ const res = await fetch(`${localApi}/v1/hub/status`)
495
+ if (res.ok) {
496
+ ready = true
497
+ console.log(` ✓ runtime 就绪(第 ${i + 1} 次探测)`)
498
+ break
499
+ }
500
+ } catch {}
501
+ if (i % 5 === 0) {
502
+ console.log(` 等待中...(第 ${i + 1} 次)`)
503
+ }
504
+ }
505
+ if (!ready) {
506
+ throw new Error(`runtime 60s 内未就绪(${localApi} 不可达)。请手动检查 "vantaloom status" 后重试 "vantaloom login"。`)
507
+ }
508
+
509
+ // 3. 登录 Hub + 注册机器 + 连接
510
+ console.log(`\n▸ 登录 Hub 并连接`)
511
+ await runLogin({
512
+ hub: hubUrl,
513
+ email,
514
+ password,
515
+ prefix,
516
+ })
517
+
518
+ console.log(`\n════════════════════════════════════════`)
519
+ console.log(` 无人值守部署完成!`)
520
+ console.log(` 安装目录: ${prefix}`)
521
+ console.log(` Hub: ${hubUrl}`)
522
+ console.log(` runtime 已启动并会在重启后自动恢复。`)
523
+ console.log(`════════════════════════════════════════`)
524
+ }
525
+
526
+ function sleep(ms) {
527
+ return new Promise(resolve => setTimeout(resolve, ms))
528
+ }