@vantaloom/cli 0.14.17 → 0.15.30

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/bin/vantaloom.mjs CHANGED
@@ -5,5 +5,9 @@ import { main } from "../src/cli.mjs"
5
5
  main(process.argv.slice(2)).catch((error) => {
6
6
  const message = error instanceof Error ? error.message : String(error)
7
7
  console.error(`vantaloom: ${message}`)
8
- process.exit(1)
8
+ // Set the code and let the loop drain instead of process.exit(): after any
9
+ // registry fetch, undici still has sockets mid-close, and killing the process
10
+ // then trips a libuv assertion on Windows ("UV_HANDLE_CLOSING"), which buries
11
+ // the real error under a crash dump and returns 0xC0000409 instead of 1.
12
+ process.exitCode = 1
9
13
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantaloom/cli",
3
- "version": "0.14.17",
3
+ "version": "0.15.30",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Vantaloom local runtime manager.",
package/src/cli.mjs CHANGED
@@ -10,8 +10,6 @@ import path from "node:path"
10
10
  import {
11
11
  cliRoot,
12
12
  defaultNpmRegistry,
13
- defaultRepo,
14
- defaultReleaseTag,
15
13
  fallbackNpmRegistries,
16
14
  } from "./lib/constants.mjs"
17
15
  import {
@@ -32,20 +30,15 @@ import {
32
30
  findSourceRoot,
33
31
  tryFindSourceRoot,
34
32
  tryAssertSourceRoot,
35
- gitRemoteUrl,
36
- inferGitHubRepo,
37
33
  readInstalledConfig,
38
34
  writeRuntimePackageMetadata,
39
35
  ensureInPath,
40
36
  } from "./lib/install.mjs"
41
37
  import { buildRuntimePackage } from "./lib/package.mjs"
42
38
  import {
43
- detectNpmRegistry,
44
39
  resolveNpmPackageWithFallback,
45
40
  downloadNpmTarball,
46
- downloadReleaseAsset,
47
41
  findExtractedNpmPackage,
48
- findExtractedPackage,
49
42
  } from "./lib/registry.mjs"
50
43
  import {
51
44
  runCtl,
@@ -97,8 +90,6 @@ export async function main(argv) {
97
90
  await installFromPackage({ ...options, update: false })
98
91
  } else if (shouldUseSourceInstall(options)) {
99
92
  await installFromSource(options)
100
- } else if (shouldUseReleaseSync(options)) {
101
- await syncFromRelease(options, "install")
102
93
  } else {
103
94
  await syncFromNpmRegistry(options, "install")
104
95
  }
@@ -108,8 +99,6 @@ export async function main(argv) {
108
99
  await installFromPackage({ ...options, update: true })
109
100
  } else if (shouldUseSourceInstall(options)) {
110
101
  await installFromSource({ ...options, update: true })
111
- } else if (shouldUseReleaseSync(options)) {
112
- await syncFromRelease(options, "update")
113
102
  } else {
114
103
  await syncFromNpmRegistry(options, "update")
115
104
  }
@@ -227,7 +216,12 @@ async function syncFromNpmRegistry(options, action) {
227
216
  const runtimePackage = options.runtimePackage || runtimePackageName(platformId())
228
217
  const runtimeVersion = options.runtimeVersion || installedConfig.runtimeVersion || "latest"
229
218
  const explicitRegistry = options.npmRegistry || installedConfig.npmRegistry
230
- const registry = normalizeRegistry(explicitRegistry || detectNpmRegistry() || defaultNpmRegistry)
219
+ // Resolve against npmjs.org — the registry the runtime packages are published
220
+ // to. Ambient mirror config (npm_config_registry / .npmrc) is deliberately NOT
221
+ // adopted: mirrors lag on dist-tags (npmmirror still served "latest" = 0.6.12
222
+ // months after 0.15.x shipped), and trusting one turns an update into a silent
223
+ // rollback. Mirrors stay in the fallback chain for unreachable-network cases.
224
+ const registry = normalizeRegistry(explicitRegistry || defaultNpmRegistry)
231
225
  const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-npm-"))
232
226
 
233
227
  try {
@@ -240,6 +234,7 @@ async function syncFromNpmRegistry(options, action) {
240
234
  name: runtimePackage,
241
235
  version: runtimeVersion,
242
236
  })
237
+ assertNotDowngrade({ prefix, resolved, runtimePackage, options, action })
243
238
  await downloadNpmTarball({
244
239
  tarballUrl: resolved.tarball,
245
240
  target: archive,
@@ -268,56 +263,63 @@ async function syncFromNpmRegistry(options, action) {
268
263
  }
269
264
  }
270
265
 
271
- async function syncFromRelease(options, action) {
272
- const prefix = safeDirectory(options.prefix ?? defaultPrefix())
273
- const installedConfig = readInstalledConfig(prefix)
274
- const sourceRoot = installedConfig.sourceRoot
275
- ? tryAssertSourceRoot(installedConfig.sourceRoot)
276
- : tryFindSourceRoot()
277
- const sourceRemote = sourceRoot ? gitRemoteUrl(sourceRoot) : ""
278
- const repo = options.repo || installedConfig.repo || inferGitHubRepo(options.remote ?? installedConfig.remote ?? sourceRemote) || defaultRepo
279
- const releaseTag = options.releaseTag || installedConfig.releaseTag || defaultReleaseTag
280
- const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-update-"))
266
+ // A registry mirror can answer "latest" with a version far older than the one
267
+ // already installed (npmmirror did exactly that: 0.6.12 long after 0.15.x
268
+ // shipped), turning an update into a silent rollback. Refuse it unless the
269
+ // caller asked for that exact version or opted in explicitly.
270
+ function assertNotDowngrade({ prefix, resolved, runtimePackage, options, action }) {
271
+ if (action !== "update" || options.runtimeVersion || options.allowDowngrade) {
272
+ return
273
+ }
274
+ const installed = readRuntimeVersion(prefix)
275
+ if (!isOlderVersion(resolved.version, installed)) {
276
+ return
277
+ }
278
+ throw new Error(
279
+ `${resolved.registry} offers ${runtimePackage}@${resolved.version}, older than the installed ${installed}.\n` +
280
+ ` A mirror whose dist-tags lag behind npmjs.org is the usual cause.\n` +
281
+ ` Retry with --npm-registry https://registry.npmjs.org/, or pass --allow-downgrade to install it anyway.`
282
+ )
283
+ }
281
284
 
285
+ // Installed runtime version ("" when absent/unreadable — never block on it).
286
+ function readRuntimeVersion(prefix) {
282
287
  try {
283
- const extractRoot = path.join(tempRoot, "extract")
284
- const archive = path.join(tempRoot, `vantaloom-${platformId()}.tar.gz`)
285
- mkdirSync(extractRoot, { recursive: true })
286
-
287
- await downloadReleaseAsset({
288
- repo,
289
- releaseTag,
290
- assetName: `vantaloom-${platformId()}.tar.gz`,
291
- target: archive,
292
- token: options.githubToken,
293
- })
294
- run("tar", ["-xzf", archive, "-C", extractRoot])
295
-
296
- const packageRoot = findExtractedPackage(extractRoot, platformId())
297
- const version = await applyPackage(packageRoot, prefix, {
298
- noStart: options.noStart,
299
- sourceRoot: installedConfig.sourceRoot,
300
- update: action === "update",
301
- })
288
+ return readFileSync(path.join(prefix, "VERSION"), "utf8").trim()
289
+ } catch {
290
+ return ""
291
+ }
292
+ }
302
293
 
303
- console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
304
- console.log(`version: ${version}`)
305
- console.log(`source: ${repo}@${releaseTag}`)
306
- ensureInPath(prefix)
307
- console.log(`run: ${displayCommand(prefix)} status`)
308
- } finally {
309
- removeKnownPath(tempRoot, os.tmpdir())
294
+ // candidate < installed on the X.Y.Z triple (a -/+ suffix on patch is ignored).
295
+ // Non-semver on either side → false: never block a locally built or unversioned
296
+ // install from being replaced.
297
+ function isOlderVersion(candidate, installed) {
298
+ const parse = (value) => {
299
+ const parts = String(value).split(".")
300
+ if (parts.length < 3) {
301
+ return null
302
+ }
303
+ const triple = [parts[0], parts[1], parts.slice(2).join(".").split(/[-+]/)[0]].map(Number)
304
+ return triple.every(Number.isInteger) ? triple : null
305
+ }
306
+ const found = parse(candidate)
307
+ const current = parse(installed)
308
+ if (!found || !current) {
309
+ return false
310
+ }
311
+ for (let index = 0; index < 3; index += 1) {
312
+ if (found[index] !== current[index]) {
313
+ return found[index] < current[index]
314
+ }
310
315
  }
316
+ return false
311
317
  }
312
318
 
313
319
  function shouldUseSourceInstall(options) {
314
320
  return Boolean(options.local || options.source || options.buildWeb)
315
321
  }
316
322
 
317
- function shouldUseReleaseSync(options) {
318
- return Boolean(options.repo || options.remote || options.releaseTag || options.githubToken)
319
- }
320
-
321
323
  function printPaths(options) {
322
324
  const sourceRoot = findSourceRoot(options.source)
323
325
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
@@ -338,10 +340,6 @@ function parseOptions(args) {
338
340
  case "component":
339
341
  case "package":
340
342
  case "output":
341
- case "repo":
342
- case "remote":
343
- case "release-tag":
344
- case "github-token":
345
343
  case "runtime-package":
346
344
  case "runtime-version":
347
345
  case "npm-registry":
@@ -380,6 +378,9 @@ function parseOptions(args) {
380
378
  case "no-strict-ssl":
381
379
  options.noStrictSsl = true
382
380
  break
381
+ case "allow-downgrade":
382
+ options.allowDowngrade = true
383
+ break
383
384
  default:
384
385
  throw new Error(`unknown option --${key}`)
385
386
  }
@@ -393,7 +394,7 @@ function printHelp() {
393
394
  Usage:
394
395
  vantaloom install [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start]
395
396
  vantaloom install --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
396
- vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start]
397
+ vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start] [--allow-downgrade]
397
398
  vantaloom update --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
398
399
  vantaloom uninstall [--prefix <dir>]
399
400
  vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package] [--target <platform>]
@@ -4,8 +4,9 @@ import { fileURLToPath } from "node:url"
4
4
  export const cliRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..", "..")
5
5
  export const repoCandidate = path.resolve(cliRoot, "..", "..")
6
6
  export const installedConfigPath = path.join(cliRoot, "config.json")
7
- export const defaultReleaseTag = "runtime-latest"
8
- export const defaultRepo = "Timefiles404/Vantaloom-next"
7
+ // Runtime packages are published to npmjs.org (see docs/deployment.md). The
8
+ // mirror is a LAST-RESORT network fallback only — its dist-tags lag, so it must
9
+ // never be the primary source for resolving "latest".
9
10
  export const defaultNpmRegistry = "https://registry.npmjs.org"
10
11
  export const fallbackNpmRegistries = [
11
12
  "https://registry.npmmirror.com",
@@ -10,7 +10,7 @@ import {
10
10
  rmSync,
11
11
  writeFileSync,
12
12
  } from "node:fs"
13
- import { cp, writeFile } from "node:fs/promises"
13
+ import { cp, readdir, rm, writeFile } from "node:fs/promises"
14
14
  import { spawnSync } from "node:child_process"
15
15
  import os from "node:os"
16
16
  import path from "node:path"
@@ -19,8 +19,6 @@ import {
19
19
  cliRoot,
20
20
  repoCandidate,
21
21
  installedConfigPath,
22
- defaultReleaseTag,
23
- defaultRepo,
24
22
  defaultNpmRegistry,
25
23
  } from "./constants.mjs"
26
24
  import {
@@ -36,6 +34,7 @@ import {
36
34
  platformToGoEnv,
37
35
  } from "./platform.mjs"
38
36
  import {
37
+ killBrowserSidecarProcess,
39
38
  killTrayProcess,
40
39
  uninstallLegacyMeshOnce,
41
40
  } from "./legacy-cleanup.mjs"
@@ -135,6 +134,10 @@ export async function applyPackage(packageRoot, prefix, options) {
135
134
  // Kill lingering tray process that may hold locks on bin/ (older versions
136
135
  // don't write tray.pid, so vantaloomctl stop won't find them).
137
136
  killTrayProcess(prefix)
137
+ // 0.14.26: the browser moved into the official plugin — stop a pre-plugin
138
+ // install's orphaned vantaloom-browser sidecar + obscura engine and delete
139
+ // their stale bin/ binaries (nothing manages them anymore).
140
+ killBrowserSidecarProcess(prefix)
138
141
 
139
142
  // Windows releases a stopped process's file handles asynchronously; copying
140
143
  // bin/ the instant after `stop` can still hit the old exe's lock (EPERM). Give
@@ -262,7 +265,7 @@ export function readInstalledConfig(prefix) {
262
265
 
263
266
  export function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
264
267
  const merged = { ...packageConfig }
265
- for (const key of ["sourceRoot", "remote", "repo", "releaseTag", "runtimePackage", "runtimeVersion", "npmRegistry"]) {
268
+ for (const key of ["sourceRoot", "runtimePackage", "runtimeVersion", "npmRegistry"]) {
266
269
  if (!merged[key] && existingConfig[key]) {
267
270
  merged[key] = existingConfig[key]
268
271
  }
@@ -279,12 +282,6 @@ export function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
279
282
  if (overrides.npmRegistry) {
280
283
  merged.npmRegistry = overrides.npmRegistry
281
284
  }
282
- if (!merged.repo) {
283
- merged.repo = defaultRepo
284
- }
285
- if (!merged.releaseTag) {
286
- merged.releaseTag = defaultReleaseTag
287
- }
288
285
  // Always force runtimePackage to match the running platform — a cross-compiled
289
286
  // package may carry a config for a different platform (e.g. win32 inside darwin).
290
287
  merged.runtimePackage = runtimePackageName(platformId())
@@ -294,36 +291,25 @@ export function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
294
291
  if (!merged.npmRegistry) {
295
292
  merged.npmRegistry = defaultNpmRegistry
296
293
  }
294
+ // Drop keys left by the removed GitHub-release channel (0.15.29). Nothing
295
+ // reads them any more; leaving them in config.json only invites the next
296
+ // reader to believe there is a second update channel.
297
+ for (const legacy of ["repo", "releaseTag", "remote"]) {
298
+ delete merged[legacy]
299
+ }
297
300
  return merged
298
301
  }
299
302
 
303
+ // npmRegistry is pinned to npmjs.org rather than sniffed from the ambient npm
304
+ // config: baking a mirror in here made every later `vantaloom update` treat it
305
+ // as an explicit choice (skipping the fallback chain) and resolve "latest" from
306
+ // dist-tags that lag months behind. Only --npm-registry should change it.
300
307
  export function runtimeConfigFromSource(sourceRoot) {
301
- const remote = gitRemoteUrl(sourceRoot)
302
- const repo = inferGitHubRepo(remote) || defaultRepo
303
- // Inline registry detection to avoid circular dep with registry.mjs
304
- let npmRegistry = ""
305
- if (process.env.NPM_CONFIG_REGISTRY) {
306
- npmRegistry = process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY
307
- } else if (process.env.npm_config_registry) {
308
- npmRegistry = process.env.npm_config_registry
309
- } else {
310
- try {
311
- const npmrcPath = path.join(os.homedir(), ".npmrc")
312
- if (existsSync(npmrcPath)) {
313
- const content = readFileSync(npmrcPath, "utf8")
314
- const match = content.match(/^\s*registry\s*=\s*(.+)/m)
315
- if (match) npmRegistry = match[1].trim()
316
- }
317
- } catch {}
318
- }
319
308
  return {
320
309
  ...(process.env.GITHUB_ACTIONS ? {} : { sourceRoot }),
321
- ...(remote ? { remote } : {}),
322
- repo,
323
- releaseTag: defaultReleaseTag,
324
310
  runtimePackage: runtimePackageName(platformId()),
325
311
  runtimeVersion: "latest",
326
- npmRegistry: npmRegistry || defaultNpmRegistry,
312
+ npmRegistry: defaultNpmRegistry,
327
313
  }
328
314
  }
329
315
 
@@ -366,31 +352,6 @@ export function assertSourceRoot(sourceRoot) {
366
352
  return sourceRoot
367
353
  }
368
354
 
369
- export function gitRemoteUrl(sourceRoot) {
370
- const result = spawnSync("git", ["remote", "get-url", "origin"], {
371
- cwd: sourceRoot,
372
- encoding: "utf8",
373
- windowsHide: true,
374
- })
375
- if (result.status === 0) {
376
- return result.stdout.trim()
377
- }
378
- return ""
379
- }
380
-
381
- export function inferGitHubRepo(remote) {
382
- if (!remote) {
383
- return ""
384
- }
385
- const normalized = remote.replace(/\.git$/, "")
386
- const httpsMatch = normalized.match(/github\.com[:/]([^/]+\/[^/]+)$/)
387
- if (httpsMatch) {
388
- return httpsMatch[1]
389
- }
390
- const sshMatch = normalized.match(/^[^:]+:([^/]+\/[^/]+)$/)
391
- return sshMatch?.[1] ?? ""
392
- }
393
-
394
355
  export async function copyStaticWeb(sourceRoot, buildWeb) {
395
356
  const exportRoot = path.join(sourceRoot, "apps", "vantaloom", "out")
396
357
  if (!existsSync(exportRoot)) {
@@ -401,6 +362,25 @@ export async function copyStaticWeb(sourceRoot, buildWeb) {
401
362
 
402
363
  removeKnownPath(buildWeb, path.dirname(buildWeb))
403
364
  await copyDir(exportRoot, buildWeb)
365
+ // Source maps never ship in a published runtime package. Next 不产浏览器
366
+ // source map,但它自带的 polyfill chunk 会漏一个 .map 进静态导出(0.15.16
367
+ // 实测:a6dad97d…js.map,内容是 Next 自己的 polyfill-nomodule)。那一个碰巧
368
+ // 只含第三方代码,但「碰巧无害」不是标准——公开 APK 的 web/ 门禁早就规定
369
+ // source map 一律不当无害编译产物(sourcesContent 能内嵌完整源码),npm 包
370
+ // 与 Docker 镜像理应同一条纪律。在打包出口统一剥掉,整类问题终结。
371
+ await removeFilesByExtension(buildWeb, ".map")
372
+ }
373
+
374
+ async function removeFilesByExtension(root, extension) {
375
+ const entries = await readdir(root, { withFileTypes: true })
376
+ for (const entry of entries) {
377
+ const full = path.join(root, entry.name)
378
+ if (entry.isDirectory()) {
379
+ await removeFilesByExtension(full, extension)
380
+ } else if (entry.name.endsWith(extension)) {
381
+ await rm(full, { force: true })
382
+ }
383
+ }
404
384
  }
405
385
 
406
386
  export async function copyCliDirectory(target, sourceRoot, config) {
@@ -528,6 +508,10 @@ export function buildGo(sourceRoot, buildBin, name, targetPlatform) {
528
508
  let ldflags = "-s -w"
529
509
  const args = ["build"]
530
510
  args.push(
511
+ // -trimpath:不把构建机的绝对路径写进二进制。没有它,每个发布出去的
512
+ // 二进制里都是上千处 D:/Projects/…(0.15.16 实测 1954 处)——纯元数据
513
+ // 泄露(构建机目录结构),且路径逐台机器不同也破坏可复现构建。
514
+ "-trimpath",
531
515
  "-ldflags", ldflags,
532
516
  "-o", path.join(buildBin, `${name}${ext}`),
533
517
  `./apps/api/cmd/${name}`
@@ -51,6 +51,54 @@ export function killTrayProcess(prefix) {
51
51
  }
52
52
  }
53
53
 
54
+ // killBrowserSidecarProcess (0.14.26) stops a pre-plugin install's
55
+ // vantaloom-browser HTTP sidecar (port 8782) + its obscura engine processes,
56
+ // and deletes the stale bin/ binaries the runtime no longer ships. The browser
57
+ // moved into the official PLUGIN (@vantaloom/browser-plugin-*); without this,
58
+ // an updated runtime would leave the orphaned sidecar+engine running until
59
+ // reboot (nothing manages them anymore) and ~170MB of dead binaries in bin/.
60
+ // Best-effort + idempotent, mirrors killTrayProcess.
61
+ export function killBrowserSidecarProcess(prefix) {
62
+ if (process.platform === "win32") {
63
+ const pidFile = path.join(prefix, "runtime", "browser.pid")
64
+ if (existsSync(pidFile)) {
65
+ const pid = readFileSync(pidFile, "utf8").trim()
66
+ if (pid) {
67
+ spawnSync("taskkill", ["/T", "/F", "/PID", pid], { stdio: "ignore", windowsHide: true })
68
+ }
69
+ try { rmSync(pidFile, { force: true }) } catch {}
70
+ }
71
+ for (const image of ["vantaloom-browser.exe", "obscura.exe", "obscura-worker.exe"]) {
72
+ const result = spawnSync("tasklist", ["/FI", `IMAGENAME eq ${image}`, "/FO", "CSV", "/NH"], {
73
+ encoding: "utf8",
74
+ windowsHide: true,
75
+ })
76
+ if (result.stdout) {
77
+ for (const line of result.stdout.split("\n")) {
78
+ const match = line.match(/"[^"]+","(\d+)"/)
79
+ if (match) {
80
+ spawnSync("taskkill", ["/T", "/F", "/PID", match[1]], { stdio: "ignore", windowsHide: true })
81
+ }
82
+ }
83
+ }
84
+ }
85
+ } else {
86
+ spawnSync("pkill", ["-f", "vantaloom-browser"], { stdio: "ignore", windowsHide: true })
87
+ spawnSync("pkill", ["-f", "obscura"], { stdio: "ignore", windowsHide: true })
88
+ }
89
+ // Stale files: the sidecar/engine binaries + its port file (bin/ copies are
90
+ // overlay-only on update, they'd linger forever otherwise).
91
+ const binDir = path.join(prefix, "bin")
92
+ for (const name of [
93
+ "vantaloom-browser.exe", "vantaloom-browser",
94
+ "obscura.exe", "obscura",
95
+ "obscura-worker.exe", "obscura-worker",
96
+ ]) {
97
+ try { rmSync(path.join(binDir, name), { force: true }) } catch {}
98
+ }
99
+ try { rmSync(path.join(prefix, "runtime", "browser.port"), { force: true }) } catch {}
100
+ }
101
+
54
102
  // meshServiceRunningOrInstalled reports whether the Windows service exists at
55
103
  // all (running or stopped), so removeMeshService only prompts for elevation
56
104
  // when there is actually something to remove.
@@ -21,6 +21,7 @@ import {
21
21
  } from "./platform.mjs"
22
22
  import {
23
23
  removeMeshService,
24
+ killBrowserSidecarProcess,
24
25
  killTrayProcess,
25
26
  } from "./legacy-cleanup.mjs"
26
27
 
@@ -151,6 +152,7 @@ export async function uninstallRuntime(options) {
151
152
  spawnSync(ctlBin, ["stop", "--prefix", prefix], { stdio: "inherit", windowsHide: true })
152
153
  }
153
154
  killTrayProcess(prefix)
155
+ killBrowserSidecarProcess(prefix)
154
156
 
155
157
  // 2. Remove a legacy privileged mesh service if this install predates 0.13
156
158
  // (releases the TUN adapter + file locks). A no-op on any install that
@@ -1,98 +1,56 @@
1
- import { existsSync, mkdirSync } from "node:fs"
2
- import { cp } from "node:fs/promises"
3
- import path from "node:path"
4
- import { platformId, removeKnownPath, runPnpm } from "./platform.mjs"
5
- import {
6
- buildGo,
7
- copyStaticWeb,
8
- copyCliDirectory,
9
- writeBuildManifest,
10
- writeRuntimePackageMetadata,
11
- runtimeConfigFromSource,
12
- gitVersion,
13
- npmPackageVersion,
14
- } from "./install.mjs"
15
-
16
- export async function buildRuntimePackage(sourceRoot, packageRoot, options) {
17
- // Every build npm release AND local/dev stamps VERSION/manifest with the
18
- // npm package version. There is exactly ONE product version (the npm semver);
19
- // the git hash is recorded separately as manifest `commit` for diagnostics.
20
- // History: pre-0.13.5 local builds stamped the git hash into VERSION, which
21
- // split the version universe in two (the desktop shell read VERSION, the
22
- // settings page read cli/package.json) and broke update prompts both ways.
23
- const version = npmPackageVersion(sourceRoot)
24
- const commit = gitVersion(sourceRoot)
25
- const platform = options.target ?? platformId()
26
- const buildBin = path.join(packageRoot, "bin")
27
- const buildWeb = path.join(packageRoot, "web")
28
-
29
- removeKnownPath(packageRoot, path.dirname(packageRoot))
30
- mkdirSync(buildBin, { recursive: true })
31
-
32
- buildGo(sourceRoot, buildBin, "vantaloom-api", platform)
33
- buildGo(sourceRoot, buildBin, "vantaloom-agent", platform)
34
- buildGo(sourceRoot, buildBin, "vantaloomctl", platform)
35
- // Official "Vantaloom 浏览器" extension sidecar. Pure-Go (chromedp/CDP, no
36
- // CGO), cross-compiles for every target. Ships dormant: it's only started by
37
- // the api when the user installs+enables the browser extension (the heavy
38
- // Chromium binary is downloaded on demand, NOT bundled here).
39
- buildGo(sourceRoot, buildBin, "vantaloom-browser", platform)
40
- // The Windows system-tray app (vantaloom-tray) was removed — it crash-looped
41
- // on some Windows 11 builds and got respawned until OOM. No longer built.
42
-
43
- // Bundle the Obscura headless browser (the official "Vantaloom 浏览器" engine,
44
- // driven by the vantaloom-browser sidecar over CDP) for the target platform.
45
- await copyObscura(sourceRoot, buildBin, platform)
46
-
47
- if (options.buildWeb) {
48
- runPnpm(["--filter", "vantaloom-app", "build"], { cwd: sourceRoot })
49
- }
50
-
51
- await copyStaticWeb(sourceRoot, buildWeb)
52
- await copyCliDirectory(path.join(packageRoot, "cli"), sourceRoot, runtimeConfigFromSource(sourceRoot))
53
- writeBuildManifest(packageRoot, version, platform, commit)
54
-
55
- return { platform, version }
56
- }
57
-
58
- // copyObscura bundles the vendored Obscura headless-browser binaries
59
- // (obscura + obscura-worker, .exe on Windows) for the target platform into the
60
- // package bin/ dir. Obscura is the official "Vantaloom 浏览器" engine: a
61
- // CDP-compatible, V8-backed headless browser the vantaloom-browser sidecar
62
- // launches via `obscura serve` and drives over CDP. Warns + skips if the
63
- // vendor dir is absent (a cleaned vendor/ silently ships a runtime with no
64
- // browser engine).
65
- export async function copyObscura(sourceRoot, buildBin, platform) {
66
- const vendorMap = {
67
- "win32-x64": "windows-x86_64",
68
- "darwin-arm64": "macos-aarch64",
69
- "linux-x64": "linux-x86_64",
70
- }
71
- const vendorName = vendorMap[platform]
72
- if (!vendorName) {
73
- console.warn(` warning: no Obscura mapping for ${platform}; browser engine disabled for this platform`)
74
- return
75
- }
76
- const srcDir = path.join(sourceRoot, "vendor", "obscura", vendorName)
77
- if (!existsSync(srcDir)) {
78
- console.warn(` warning: Obscura binaries not found at ${srcDir}; run vendor download first`)
79
- return
80
- }
81
- const isWin = platform.startsWith("win32")
82
- const exe = isWin ? ".exe" : ""
83
- // Both binaries MUST ship together: obscura is the CLI/CDP server, obscura-worker
84
- // is the V8 render worker it spawns. Without the worker, `obscura serve` cannot
85
- // open pages.
86
- const files = [`obscura${exe}`, `obscura-worker${exe}`]
87
- let copied = 0
88
- for (const f of files) {
89
- const src = path.join(srcDir, f)
90
- if (!existsSync(src)) {
91
- console.warn(` warning: Obscura file missing: ${f}`)
92
- continue
93
- }
94
- await cp(src, path.join(buildBin, f), { force: true })
95
- copied++
96
- }
97
- console.log(` bundled Obscura ${vendorName} (${copied} files)`)
98
- }
1
+ import { mkdirSync } from "node:fs"
2
+ import path from "node:path"
3
+ import { platformId, removeKnownPath, runPnpm } from "./platform.mjs"
4
+ import {
5
+ buildGo,
6
+ copyStaticWeb,
7
+ copyCliDirectory,
8
+ writeBuildManifest,
9
+ writeRuntimePackageMetadata,
10
+ runtimeConfigFromSource,
11
+ gitVersion,
12
+ npmPackageVersion,
13
+ } from "./install.mjs"
14
+
15
+ export async function buildRuntimePackage(sourceRoot, packageRoot, options) {
16
+ // Every build npm release AND local/dev — stamps VERSION/manifest with the
17
+ // npm package version. There is exactly ONE product version (the npm semver);
18
+ // the git hash is recorded separately as manifest `commit` for diagnostics.
19
+ // History: pre-0.13.5 local builds stamped the git hash into VERSION, which
20
+ // split the version universe in two (the desktop shell read VERSION, the
21
+ // settings page read cli/package.json) and broke update prompts both ways.
22
+ const version = npmPackageVersion(sourceRoot)
23
+ const commit = gitVersion(sourceRoot)
24
+ const platform = options.target ?? platformId()
25
+ const buildBin = path.join(packageRoot, "bin")
26
+ const buildWeb = path.join(packageRoot, "web")
27
+
28
+ removeKnownPath(packageRoot, path.dirname(packageRoot))
29
+ mkdirSync(buildBin, { recursive: true })
30
+
31
+ buildGo(sourceRoot, buildBin, "vantaloom-api", platform)
32
+ buildGo(sourceRoot, buildBin, "vantaloom-agent", platform)
33
+ buildGo(sourceRoot, buildBin, "vantaloomctl", platform)
34
+ // vantaloom-mcp:跨机执行信道的 MCP 服务端(agent-bridge)。随 runtime bin 分发,
35
+ // 外部 agent(Claude Code / Codex / Cursor)配置指向它即可驱动整个组网。
36
+ buildGo(sourceRoot, buildBin, "vantaloom-mcp", platform)
37
+ // 0.14.26: the browser moved into the official optional PLUGIN
38
+ // (@vantaloom/browser-plugin-<platform>, built by scripts/build-browser-plugin.ps1)
39
+ // the runtime no longer builds the vantaloom-browser sidecar nor bundles the
40
+ // Obscura engine. The Windows system-tray app (vantaloom-tray) was removed
41
+ // earlier — it crash-looped on some Windows 11 builds. Neither is built here.
42
+
43
+ if (options.buildWeb) {
44
+ runPnpm(["--filter", "vantaloom-app", "build"], { cwd: sourceRoot })
45
+ }
46
+
47
+ await copyStaticWeb(sourceRoot, buildWeb)
48
+ await copyCliDirectory(path.join(packageRoot, "cli"), sourceRoot, runtimeConfigFromSource(sourceRoot))
49
+ writeBuildManifest(packageRoot, version, platform, commit)
50
+
51
+ return { platform, version }
52
+ }
53
+
54
+ // copyObscura was removed in 0.14.26: the Obscura engine ships inside the
55
+ // official browser PLUGIN packages (scripts/build-browser-plugin.ps1), not the
56
+ // runtime.
@@ -1,48 +1,10 @@
1
1
  import {
2
2
  existsSync,
3
- readFileSync,
4
3
  readdirSync,
5
4
  } from "node:fs"
6
5
  import { writeFile } from "node:fs/promises"
7
- import os from "node:os"
8
6
  import path from "node:path"
9
7
 
10
- export function detectNpmRegistry() {
11
- // 1. NPM_CONFIG_REGISTRY env var (highest priority, set by npm/npx when running)
12
- if (process.env.NPM_CONFIG_REGISTRY) {
13
- return process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY
14
- }
15
- // npm also sets the lowercase variant
16
- if (process.env.npm_config_registry) {
17
- return process.env.npm_config_registry
18
- }
19
-
20
- // 2. Read user .npmrc
21
- try {
22
- const npmrcPaths = [
23
- path.join(os.homedir(), ".npmrc"),
24
- ]
25
- // Also check project-level .npmrc
26
- const localNpmrc = path.resolve(".npmrc")
27
- if (localNpmrc !== npmrcPaths[0]) {
28
- npmrcPaths.unshift(localNpmrc)
29
- }
30
- for (const npmrcPath of npmrcPaths) {
31
- if (existsSync(npmrcPath)) {
32
- const content = readFileSync(npmrcPath, "utf8")
33
- const match = content.match(/^\s*registry\s*=\s*(.+)/m)
34
- if (match) {
35
- return match[1].trim()
36
- }
37
- }
38
- }
39
- } catch {
40
- // Ignore .npmrc read errors
41
- }
42
-
43
- return ""
44
- }
45
-
46
8
  export async function resolveNpmPackageWithFallback({ registries, name, version }) {
47
9
  const errors = []
48
10
  for (const registry of registries) {
@@ -133,91 +95,51 @@ export async function resolveNpmPackage({ registry, name, version }) {
133
95
  }
134
96
  }
135
97
 
98
+ // The runtime tarball is tens of MB (Go binaries + the exported web bundle), so
99
+ // the budget has to cover a slow link end to end — the old 120s cap aborted
100
+ // mid-body on ordinary connections and made updates fail for anyone far from
101
+ // npmjs.org.
102
+ const TARBALL_DOWNLOAD_TIMEOUT_MS = 15 * 60 * 1000
103
+
136
104
  export async function downloadNpmTarball({ tarballUrl, target, packageName, version }) {
137
105
  console.log(` downloading ${packageName}@${version}...`)
138
- let response
106
+ // One deadline for headers AND body: the abort fires during arrayBuffer() far
107
+ // more often than during fetch(), so both live inside the same try — otherwise
108
+ // the failure escapes as a bare "The operation was aborted due to timeout"
109
+ // with no hint of what was being downloaded or what to do about it.
139
110
  try {
140
- response = await fetch(tarballUrl, {
111
+ const response = await fetch(tarballUrl, {
141
112
  headers: {
142
113
  "User-Agent": "vantaloom-cli",
143
114
  },
144
- signal: AbortSignal.timeout(120000),
115
+ signal: AbortSignal.timeout(TARBALL_DOWNLOAD_TIMEOUT_MS),
145
116
  })
117
+ if (!response.ok) {
118
+ const detail = await response.text().catch(() => "")
119
+ throw new Error(`failed to download ${packageName}@${version}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
120
+ }
121
+ const buffer = Buffer.from(await response.arrayBuffer())
122
+ await writeFile(target, buffer)
123
+ console.log(` downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`)
146
124
  } catch (error) {
125
+ if (error instanceof Error && error.message.startsWith("failed to download")) {
126
+ throw error
127
+ }
147
128
  const causeCode = error?.cause?.code || ""
148
- const causeMsg = causeCode || error?.cause?.message || error.message || ""
129
+ const causeMsg = causeCode || error?.cause?.message || error?.message || ""
149
130
  const isSsl = causeCode.includes("CERT") || causeCode === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
150
- const hint = isSsl ? `\n Fix: run with --no-strict-ssl` : ""
131
+ const isTimeout = error?.name === "TimeoutError" || /aborted due to timeout/i.test(String(error?.message))
132
+ let hint = ""
133
+ if (isSsl) {
134
+ hint = `\n Fix: run with --no-strict-ssl`
135
+ } else if (isTimeout) {
136
+ hint =
137
+ `\n The transfer exceeded ${TARBALL_DOWNLOAD_TIMEOUT_MS / 60000} minutes.` +
138
+ `\n Retry, or fetch the same version from a closer mirror:` +
139
+ `\n vantaloom update --runtime-version ${version} --npm-registry https://registry.npmmirror.com/`
140
+ }
151
141
  throw new Error(`failed to download ${packageName}@${version} from ${tarballUrl}: ${causeMsg}${hint}`)
152
142
  }
153
- if (!response.ok) {
154
- const detail = await response.text().catch(() => "")
155
- throw new Error(`failed to download ${packageName}@${version}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
156
- }
157
-
158
- const buffer = Buffer.from(await response.arrayBuffer())
159
- await writeFile(target, buffer)
160
- console.log(` downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`)
161
- }
162
-
163
- export async function downloadReleaseAsset({ repo, releaseTag, assetName, target, token }) {
164
- const releaseUrl = `https://api.github.com/repos/${repo}/releases/tags/${releaseTag}`
165
- const headers = {
166
- "User-Agent": "vantaloom-cli",
167
- }
168
- const githubToken = token || process.env.VANTALOOM_GITHUB_TOKEN || process.env.GITHUB_TOKEN
169
- if (githubToken) {
170
- headers.Authorization = `Bearer ${githubToken}`
171
- }
172
-
173
- const releaseResponse = await fetch(releaseUrl, { headers })
174
- if (!releaseResponse.ok) {
175
- const detail = await releaseResponse.text().catch(() => "")
176
- const authHint = releaseResponse.status === 404 || releaseResponse.status === 403
177
- ? " If the repository is private, set VANTALOOM_GITHUB_TOKEN to a GitHub token that can read releases."
178
- : ""
179
- throw new Error(`failed to inspect ${repo}@${releaseTag}: HTTP ${releaseResponse.status}.${authHint}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
180
- }
181
-
182
- const release = await releaseResponse.json()
183
- const asset = release.assets?.find((asset) => asset.name === assetName)
184
- if (!asset) {
185
- const names = release.assets?.map((asset) => asset.name).join(", ") || "none"
186
- throw new Error(`missing release asset ${assetName} in ${repo}@${releaseTag}; available assets: ${names}`)
187
- }
188
-
189
- const response = await fetch(asset.url, {
190
- headers: {
191
- ...headers,
192
- Accept: "application/octet-stream",
193
- },
194
- })
195
- if (!response.ok) {
196
- const detail = await response.text().catch(() => "")
197
- const authHint = response.status === 404 || response.status === 403
198
- ? " If the repository is private, set VANTALOOM_GITHUB_TOKEN to a GitHub token that can read releases."
199
- : ""
200
- throw new Error(`failed to download ${assetName} from ${repo}@${releaseTag}: HTTP ${response.status}.${authHint}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
201
- }
202
-
203
- const buffer = Buffer.from(await response.arrayBuffer())
204
- await writeFile(target, buffer)
205
- }
206
-
207
- export function findExtractedPackage(extractRoot, platform) {
208
- const expected = path.join(extractRoot, `vantaloom-${platform}`)
209
- if (existsSync(expected)) {
210
- return expected
211
- }
212
-
213
- const directories = readdirSync(extractRoot, { withFileTypes: true })
214
- .filter((entry) => entry.isDirectory())
215
- .map((entry) => path.join(extractRoot, entry.name))
216
- if (directories.length === 1) {
217
- return directories[0]
218
- }
219
-
220
- throw new Error(`could not find extracted Vantaloom package in ${extractRoot}`)
221
143
  }
222
144
 
223
145
  export function findExtractedNpmPackage(extractRoot) {