@vantaloom/runtime-darwin-arm64 0.12.48 → 0.12.50

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.
Files changed (37) hide show
  1. package/VERSION +1 -1
  2. package/bin/vantaloom-agent +0 -0
  3. package/bin/vantaloom-api +0 -0
  4. package/bin/vantaloom-browser +0 -0
  5. package/bin/vantaloomctl +0 -0
  6. package/cli/package.json +1 -1
  7. package/cli/src/cli.mjs +460 -2100
  8. package/cli/src/lib/auth.mjs +155 -0
  9. package/cli/src/lib/constants.mjs +34 -0
  10. package/cli/src/lib/install.mjs +627 -0
  11. package/cli/src/lib/lifecycle.mjs +187 -0
  12. package/cli/src/lib/mesh.mjs +236 -0
  13. package/cli/src/lib/package.mjs +206 -0
  14. package/cli/src/lib/platform.mjs +165 -0
  15. package/cli/src/lib/registry.mjs +237 -0
  16. package/manifest.json +2 -2
  17. package/package.json +1 -1
  18. package/web/404.html +1 -1
  19. package/web/__next.__PAGE__.txt +2 -2
  20. package/web/__next._full.txt +2 -2
  21. package/web/__next._head.txt +1 -1
  22. package/web/__next._index.txt +1 -1
  23. package/web/__next._tree.txt +1 -1
  24. package/web/_next/static/chunks/{c45c5fae4a957e2f.js → fb49ea3bfe8faeb0.js} +1 -1
  25. package/web/_not-found/__next._full.txt +1 -1
  26. package/web/_not-found/__next._head.txt +1 -1
  27. package/web/_not-found/__next._index.txt +1 -1
  28. package/web/_not-found/__next._not-found/__PAGE__.txt +1 -1
  29. package/web/_not-found/__next._not-found.txt +1 -1
  30. package/web/_not-found/__next._tree.txt +1 -1
  31. package/web/_not-found.html +1 -1
  32. package/web/_not-found.txt +1 -1
  33. package/web/index.html +1 -1
  34. package/web/index.txt +2 -2
  35. /package/web/_next/static/{RpkdljcHezHulenQJbyKT → fdvtf4murB4YOO6QLnYED}/_buildManifest.js +0 -0
  36. /package/web/_next/static/{RpkdljcHezHulenQJbyKT → fdvtf4murB4YOO6QLnYED}/_clientMiddlewareManifest.json +0 -0
  37. /package/web/_next/static/{RpkdljcHezHulenQJbyKT → fdvtf4murB4YOO6QLnYED}/_ssgManifest.js +0 -0
@@ -0,0 +1,627 @@
1
+ import {
2
+ appendFileSync,
3
+ chmodSync,
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ renameSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs"
13
+ import { cp, writeFile } from "node:fs/promises"
14
+ import { spawnSync } from "node:child_process"
15
+ import os from "node:os"
16
+ import path from "node:path"
17
+ import {
18
+ LOCK_ERROR_CODES,
19
+ cliRoot,
20
+ repoCandidate,
21
+ installedConfigPath,
22
+ defaultReleaseTag,
23
+ defaultRepo,
24
+ defaultNpmRegistry,
25
+ } from "./constants.mjs"
26
+ import {
27
+ binaryName,
28
+ platformId,
29
+ runtimePackageName,
30
+ parsePlatformId,
31
+ fileSha,
32
+ readJSONIfExists,
33
+ removeKnownPath,
34
+ writeText,
35
+ run,
36
+ runPnpm,
37
+ platformToGoEnv,
38
+ } from "./platform.mjs"
39
+ import {
40
+ meshBinarySet,
41
+ meshServiceRunning,
42
+ ensureMeshService,
43
+ ensureLinuxMeshCapabilities,
44
+ killTrayProcess,
45
+ } from "./mesh.mjs"
46
+ import {
47
+ enableRuntimeAutostart,
48
+ } from "./lifecycle.mjs"
49
+
50
+ // sleepSync blocks for ms milliseconds without async (install runs top-to-bottom
51
+ // and must not race the file copy against a process that is still releasing its
52
+ // handles). Uses Atomics.wait on a throwaway buffer — no busy spin.
53
+ export function sleepSync(ms) {
54
+ try {
55
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms))
56
+ } catch {
57
+ const end = Date.now() + ms
58
+ while (Date.now() < end) {} // fallback busy-wait if SAB is unavailable
59
+ }
60
+ }
61
+
62
+ // copyFileResilient overwrites dst with src, surviving the Windows case where dst
63
+ // is a running/locked executable. Windows refuses to OVERWRITE or DELETE an in-use
64
+ // .exe (EPERM/EBUSY) but DOES allow RENAMING it — the file handle tracks the file
65
+ // object, not its path. So on a lock error we move the locked file aside and copy
66
+ // the new one into the freed path; the runtime picks up the new binary on its next
67
+ // start, and the moved-aside `.old-*` file is reaped on a later install. This is
68
+ // the fix for the EPERM that blocked update/restart when a prior process (api,
69
+ // agent, or an orphaned easytier/mesh) still held a binary open.
70
+ export function copyFileResilient(src, dst) {
71
+ for (let attempt = 0; ; attempt += 1) {
72
+ try {
73
+ copyFileSync(src, dst)
74
+ return
75
+ } catch (err) {
76
+ if (!LOCK_ERROR_CODES.has(err.code) || !existsSync(dst)) {
77
+ if (attempt >= 4) throw err
78
+ sleepSync(250)
79
+ continue
80
+ }
81
+ try {
82
+ const aside = `${dst}.old-${process.pid}-${attempt}`
83
+ renameSync(dst, aside)
84
+ copyFileSync(src, dst)
85
+ try { rmSync(aside, { force: true }) } catch {} // best-effort; may still be locked
86
+ return
87
+ } catch (moveErr) {
88
+ if (attempt >= 4) throw moveErr
89
+ sleepSync(300)
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ // copyDirResilient recursively copies src→dst using copyFileResilient for every
96
+ // file, so a single locked binary can't abort the whole bin/ refresh. filter(srcPath)
97
+ // gates which entries are copied (mirrors fs.cp's filter).
98
+ export function copyDirResilient(src, dst, filter) {
99
+ mkdirSync(dst, { recursive: true })
100
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
101
+ const s = path.join(src, entry.name)
102
+ if (filter && !filter(s)) continue
103
+ const d = path.join(dst, entry.name)
104
+ if (entry.isDirectory()) {
105
+ copyDirResilient(s, d, filter)
106
+ } else {
107
+ copyFileResilient(s, d)
108
+ }
109
+ }
110
+ }
111
+
112
+ // cleanupStaleReplacements deletes the `.old-*` files left behind by a prior
113
+ // lock-safe replace once their handles have been released (best-effort).
114
+ export function cleanupStaleReplacements(dir) {
115
+ if (!existsSync(dir)) return
116
+ for (const entry of readdirSync(dir)) {
117
+ if (/\.old-\d+-\d+$/.test(entry)) {
118
+ try { rmSync(path.join(dir, entry), { force: true }) } catch {}
119
+ }
120
+ }
121
+ }
122
+
123
+ export async function applyPackage(packageRoot, prefix, options) {
124
+ assertRuntimePackage(packageRoot)
125
+
126
+ const existingConfig = readInstalledConfig(prefix)
127
+ const packageConfig = readJSONIfExists(path.join(packageRoot, "cli", "config.json"))
128
+ const mergedConfig = mergeRuntimeConfig(packageConfig, existingConfig, {
129
+ sourceRoot: options.sourceRoot,
130
+ })
131
+ const version = readVersion(packageRoot)
132
+
133
+ mkdirSync(prefix, { recursive: true })
134
+ const existingCtl = path.join(prefix, "bin", binaryName("vantaloomctl"))
135
+ if (existsSync(existingCtl)) {
136
+ spawnSync(existingCtl, ["stop", "--prefix", prefix], { stdio: "inherit", windowsHide: true })
137
+ }
138
+
139
+ // Kill lingering tray process that may hold locks on bin/ (older versions
140
+ // don't write tray.pid, so vantaloomctl stop won't find them).
141
+ killTrayProcess(prefix)
142
+
143
+ // Windows releases a stopped process's file handles asynchronously; copying
144
+ // bin/ the instant after `stop` can still hit the old exe's lock (EPERM). Give
145
+ // the OS a moment to close handles, and sweep any `.old-*` files a previous
146
+ // lock-safe replace left behind (now that those handles are likely released).
147
+ // copyFileResilient below is the real safety net if a handle is still open.
148
+ if (existsSync(path.join(prefix, "bin"))) {
149
+ sleepSync(600)
150
+ cleanupStaleReplacements(path.join(prefix, "bin"))
151
+ }
152
+
153
+ // On Windows the privileged mesh service holds its binaries open, so we can't
154
+ // wipe or overwrite them with a plain copy. Detect that up front: if the
155
+ // service is running, skip those files in the bin/ copy (the elevated `apply`
156
+ // swaps them after stopping the service).
157
+ const meshLocked =
158
+ process.platform === "win32" &&
159
+ meshServiceRunning(path.join(packageRoot, "bin"), "win32")
160
+
161
+ // Copy package contents to install prefix
162
+ const meshNames = meshBinarySet(process.platform)
163
+ // "adapters" carries the bundled Claude Code ACP adapter + reclaude shim. Older
164
+ // packages predate it, so its absence is tolerated (no warning) — bin/web/cli are
165
+ // still required.
166
+ const optionalDirs = new Set(["adapters"])
167
+ for (const name of ["bin", "web", "cli", "adapters"]) {
168
+ const src = path.join(packageRoot, name)
169
+ const dst = path.join(prefix, name)
170
+ if (!existsSync(src)) {
171
+ if (!optionalDirs.has(name)) {
172
+ console.error(` warning: package missing ${name}/ directory`)
173
+ }
174
+ continue
175
+ }
176
+ const copyOpts = { recursive: true, force: true, dereference: false }
177
+ if (name === "bin") {
178
+ // Never wipe bin/: that would drop the running mesh binaries (and, on Linux,
179
+ // clear easytier-core's setcap capability). Overwrite in place, but SKIP a
180
+ // mesh binary when the running service holds it open (Windows) OR it is
181
+ // byte-identical to what's installed. The upshot: an update where the mesh
182
+ // is unchanged leaves the privileged service entirely untouched, so it needs
183
+ // no UAC/sudo. Changed mesh binaries and all non-mesh files are copied.
184
+ copyOpts.filter = (s) => {
185
+ const base = path.basename(s)
186
+ if (!meshNames.includes(base)) return true
187
+ if (meshLocked) return false
188
+ const installed = path.join(dst, base)
189
+ return !(existsSync(installed) && fileSha(s) === fileSha(installed))
190
+ }
191
+ // bin/ holds the long-lived executables (api, agent, ctl, easytier, mesh).
192
+ // Copy each file with the lock-safe move-aside replace so a still-locked
193
+ // binary from a not-fully-exited prior process never aborts the update with
194
+ // EPERM (the bug that wedged update/restart).
195
+ copyDirResilient(src, dst, copyOpts.filter)
196
+ continue
197
+ } else {
198
+ removeKnownPath(dst, prefix)
199
+ }
200
+ await cp(src, dst, copyOpts)
201
+ }
202
+
203
+ // Ensure binaries are executable on Unix (cross-compiled from Windows they lose +x)
204
+ const binDir = path.join(prefix, "bin")
205
+ if (process.platform !== "win32" && existsSync(binDir)) {
206
+ for (const entry of readdirSync(binDir)) {
207
+ const binPath = path.join(binDir, entry)
208
+ try { chmodSync(binPath, 0o755) } catch {}
209
+ }
210
+ }
211
+
212
+ // Linux: grant easytier-core the TUN capability so the unprivileged runtime can
213
+ // bring up the mesh virtual network. This is the one privileged step of the
214
+ // install (a single sudo); the app itself stays unprivileged.
215
+ ensureLinuxMeshCapabilities(binDir)
216
+
217
+ // Windows/macOS: register (or lock-safely update) the privileged mesh service
218
+ // that runs easytier-core. This is the one elevation of the install on those
219
+ // platforms. Skipped for --no-start and gated to avoid prompting on every
220
+ // source rebuild (see ensureMeshService).
221
+ if (!options.noStart) {
222
+ ensureMeshService(packageRoot, prefix, {
223
+ sourceInstall: Boolean(options.sourceRoot),
224
+ withMesh: Boolean(options.withMesh),
225
+ skipMesh: Boolean(options.skipMesh),
226
+ })
227
+ }
228
+
229
+ // Make the runtime start at login/boot (per-user, no elevation). Idempotent.
230
+ if (!options.noStart && !options.skipAutostart) {
231
+ enableRuntimeAutostart(prefix)
232
+ }
233
+
234
+ // Verify vantaloomctl binary exists before trying to run it
235
+ const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
236
+ if (!existsSync(ctlBin)) {
237
+ const binContents = existsSync(binDir) ? readdirSync(binDir) : []
238
+ const srcBinContents = existsSync(path.join(packageRoot, "bin")) ? readdirSync(path.join(packageRoot, "bin")) : []
239
+ throw new Error(
240
+ `vantaloomctl binary not found at ${ctlBin}\n` +
241
+ ` installed bin/: [${binContents.join(", ")}]\n` +
242
+ ` package bin/: [${srcBinContents.join(", ")}]\n` +
243
+ ` platform: ${platformId()}\n` +
244
+ ` This may indicate a corrupt download. Try again or install from source.`
245
+ )
246
+ }
247
+
248
+ await writeLauncher(prefix)
249
+ await writeText(path.join(prefix, "cli", "config.json"), `${JSON.stringify(mergedConfig, null, 2)}\n`)
250
+ await writeText(path.join(prefix, "VERSION"), `${version}\n`)
251
+ await cp(path.join(packageRoot, "manifest.json"), path.join(prefix, "manifest.json"), {
252
+ force: true,
253
+ })
254
+
255
+ run(ctlBin, [
256
+ "install",
257
+ "--prefix",
258
+ prefix,
259
+ "--version",
260
+ version,
261
+ ])
262
+
263
+ if (!options.noStart) {
264
+ run(ctlBin, [
265
+ "start",
266
+ "--prefix",
267
+ prefix,
268
+ ])
269
+ }
270
+
271
+ return version
272
+ }
273
+
274
+ export function assertRuntimePackage(packageRoot) {
275
+ for (const name of ["bin", "web", "cli", "manifest.json"]) {
276
+ if (!existsSync(path.join(packageRoot, name))) {
277
+ throw new Error(`invalid Vantaloom package, missing ${name}: ${packageRoot}`)
278
+ }
279
+ }
280
+ }
281
+
282
+ export function readVersion(packageRoot) {
283
+ const versionPath = path.join(packageRoot, "VERSION")
284
+ if (existsSync(versionPath)) {
285
+ return readFileSync(versionPath, "utf8").trim() || "dev"
286
+ }
287
+ const manifest = readJSONIfExists(path.join(packageRoot, "manifest.json"))
288
+ return manifest.version ?? "dev"
289
+ }
290
+
291
+ export function readInstalledConfig(prefix) {
292
+ return readJSONIfExists(path.join(prefix, "cli", "config.json"))
293
+ }
294
+
295
+ export function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
296
+ const merged = { ...packageConfig }
297
+ for (const key of ["sourceRoot", "remote", "repo", "releaseTag", "runtimePackage", "runtimeVersion", "npmRegistry"]) {
298
+ if (!merged[key] && existingConfig[key]) {
299
+ merged[key] = existingConfig[key]
300
+ }
301
+ }
302
+ if (overrides.sourceRoot) {
303
+ merged.sourceRoot = overrides.sourceRoot
304
+ }
305
+ if (overrides.runtimePackage) {
306
+ merged.runtimePackage = overrides.runtimePackage
307
+ }
308
+ if (overrides.runtimeVersion) {
309
+ merged.runtimeVersion = overrides.runtimeVersion
310
+ }
311
+ if (overrides.npmRegistry) {
312
+ merged.npmRegistry = overrides.npmRegistry
313
+ }
314
+ if (!merged.repo) {
315
+ merged.repo = defaultRepo
316
+ }
317
+ if (!merged.releaseTag) {
318
+ merged.releaseTag = defaultReleaseTag
319
+ }
320
+ // Always force runtimePackage to match the running platform — a cross-compiled
321
+ // package may carry a config for a different platform (e.g. win32 inside darwin).
322
+ merged.runtimePackage = runtimePackageName(platformId())
323
+ if (!merged.runtimeVersion) {
324
+ merged.runtimeVersion = "latest"
325
+ }
326
+ if (!merged.npmRegistry) {
327
+ merged.npmRegistry = defaultNpmRegistry
328
+ }
329
+ return merged
330
+ }
331
+
332
+ export function runtimeConfigFromSource(sourceRoot) {
333
+ const remote = gitRemoteUrl(sourceRoot)
334
+ const repo = inferGitHubRepo(remote) || defaultRepo
335
+ // Inline registry detection to avoid circular dep with registry.mjs
336
+ let npmRegistry = ""
337
+ if (process.env.NPM_CONFIG_REGISTRY) {
338
+ npmRegistry = process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY
339
+ } else if (process.env.npm_config_registry) {
340
+ npmRegistry = process.env.npm_config_registry
341
+ } else {
342
+ try {
343
+ const npmrcPath = path.join(os.homedir(), ".npmrc")
344
+ if (existsSync(npmrcPath)) {
345
+ const content = readFileSync(npmrcPath, "utf8")
346
+ const match = content.match(/^\s*registry\s*=\s*(.+)/m)
347
+ if (match) npmRegistry = match[1].trim()
348
+ }
349
+ } catch {}
350
+ }
351
+ return {
352
+ ...(process.env.GITHUB_ACTIONS ? {} : { sourceRoot }),
353
+ ...(remote ? { remote } : {}),
354
+ repo,
355
+ releaseTag: defaultReleaseTag,
356
+ runtimePackage: runtimePackageName(platformId()),
357
+ runtimeVersion: "latest",
358
+ npmRegistry: npmRegistry || defaultNpmRegistry,
359
+ }
360
+ }
361
+
362
+ export function findSourceRoot(sourceOption) {
363
+ if (sourceOption) {
364
+ return assertSourceRoot(path.resolve(sourceOption))
365
+ }
366
+ if (process.env.VANTALOOM_SOURCE) {
367
+ return assertSourceRoot(path.resolve(process.env.VANTALOOM_SOURCE))
368
+ }
369
+ if (existsSync(installedConfigPath)) {
370
+ const config = JSON.parse(readFileSync(installedConfigPath, "utf8"))
371
+ if (config.sourceRoot) {
372
+ return assertSourceRoot(path.resolve(config.sourceRoot))
373
+ }
374
+ }
375
+ return assertSourceRoot(repoCandidate)
376
+ }
377
+
378
+ export function tryFindSourceRoot() {
379
+ try {
380
+ return findSourceRoot()
381
+ } catch {
382
+ return ""
383
+ }
384
+ }
385
+
386
+ export function tryAssertSourceRoot(sourceRoot) {
387
+ try {
388
+ return assertSourceRoot(sourceRoot)
389
+ } catch {
390
+ return ""
391
+ }
392
+ }
393
+
394
+ export function assertSourceRoot(sourceRoot) {
395
+ if (!existsSync(path.join(sourceRoot, "apps", "api", "go.mod"))) {
396
+ throw new Error(`not a Vantaloom source root: ${sourceRoot}`)
397
+ }
398
+ return sourceRoot
399
+ }
400
+
401
+ export function gitRemoteUrl(sourceRoot) {
402
+ const result = spawnSync("git", ["remote", "get-url", "origin"], {
403
+ cwd: sourceRoot,
404
+ encoding: "utf8",
405
+ windowsHide: true,
406
+ })
407
+ if (result.status === 0) {
408
+ return result.stdout.trim()
409
+ }
410
+ return ""
411
+ }
412
+
413
+ export function inferGitHubRepo(remote) {
414
+ if (!remote) {
415
+ return ""
416
+ }
417
+ const normalized = remote.replace(/\.git$/, "")
418
+ const httpsMatch = normalized.match(/github\.com[:/]([^/]+\/[^/]+)$/)
419
+ if (httpsMatch) {
420
+ return httpsMatch[1]
421
+ }
422
+ const sshMatch = normalized.match(/^[^:]+:([^/]+\/[^/]+)$/)
423
+ return sshMatch?.[1] ?? ""
424
+ }
425
+
426
+ export async function copyStaticWeb(sourceRoot, buildWeb) {
427
+ const exportRoot = path.join(sourceRoot, "apps", "vantaloom", "out")
428
+ if (!existsSync(exportRoot)) {
429
+ throw new Error(
430
+ "missing Next static export output; let GitHub CI run production build, or pass --build-web for a local one-off build"
431
+ )
432
+ }
433
+
434
+ removeKnownPath(buildWeb, path.dirname(buildWeb))
435
+ await copyDir(exportRoot, buildWeb)
436
+ }
437
+
438
+ export async function copyCliDirectory(target, sourceRoot, config) {
439
+ const sourceCliRoot = path.join(sourceRoot, "packages", "cli")
440
+ if (!existsSync(path.join(sourceCliRoot, "bin", "vantaloom.mjs"))) {
441
+ throw new Error(`missing source CLI package: ${sourceCliRoot}`)
442
+ }
443
+ removeKnownPath(target, path.dirname(target))
444
+ mkdirSync(target, { recursive: true })
445
+ await copyDir(sourceCliRoot, target)
446
+ await writeText(
447
+ path.join(target, "config.json"),
448
+ `${JSON.stringify(config, null, 2)}\n`
449
+ )
450
+ }
451
+
452
+ // writeLauncher writes the OS launcher script to the install prefix.
453
+ // NOTE: mirrored in Go at apps/desktop/internal/runtime/
454
+ export async function writeLauncher(prefix) {
455
+ if (process.platform === "win32") {
456
+ await writeText(
457
+ path.join(prefix, "vantaloom.cmd"),
458
+ `@echo off\r\nnode "%~dp0cli\\bin\\vantaloom.mjs" %*\r\n`
459
+ )
460
+ } else {
461
+ const launcher = `#!/usr/bin/env sh\nexec node "$(dirname "$0")/cli/bin/vantaloom.mjs" "$@"\n`
462
+ const launcherPath = path.join(prefix, "vantaloom")
463
+ await writeText(launcherPath, launcher)
464
+ chmodSync(launcherPath, 0o755)
465
+ }
466
+ }
467
+
468
+ export async function copyDir(source, destination, options = {}) {
469
+ if (!existsSync(source)) {
470
+ throw new Error(`missing source directory: ${source}`)
471
+ }
472
+ removeKnownPath(destination, path.dirname(destination))
473
+ mkdirSync(destination, { recursive: true })
474
+ await cp(source, destination, {
475
+ recursive: true,
476
+ force: true,
477
+ dereference: options.dereference ?? true,
478
+ })
479
+ }
480
+
481
+ export function writeBuildManifest(buildRoot, version, platform) {
482
+ const components = ["api", "agent", "web", "ctl"]
483
+ writeFileSync(path.join(buildRoot, "VERSION"), `${version}\n`)
484
+ writeFileSync(
485
+ path.join(buildRoot, "manifest.json"),
486
+ `${JSON.stringify(
487
+ {
488
+ name: "Vantaloom Local Runtime",
489
+ version,
490
+ platform,
491
+ updatedAt: new Date().toISOString(),
492
+ components,
493
+ },
494
+ null,
495
+ 2
496
+ )}\n`
497
+ )
498
+ }
499
+
500
+ export function writeRuntimePackageMetadata(packageRoot, sourceRoot, platform) {
501
+ const version = npmPackageVersion(sourceRoot)
502
+ const { os: runtimeOS, cpu } = parsePlatformId(platform)
503
+ const name = runtimePackageName(platform)
504
+ writeFileSync(
505
+ path.join(packageRoot, "package.json"),
506
+ `${JSON.stringify(
507
+ {
508
+ name,
509
+ version,
510
+ private: false,
511
+ description: `Vantaloom local runtime for ${platform}.`,
512
+ type: "module",
513
+ os: [runtimeOS],
514
+ cpu: [cpu],
515
+ files: ["bin", "web", "cli", "manifest.json", "VERSION", "README.md"],
516
+ publishConfig: {
517
+ access: "public",
518
+ },
519
+ engines: {
520
+ node: ">=20",
521
+ },
522
+ },
523
+ null,
524
+ 2
525
+ )}\n`
526
+ )
527
+ writeFileSync(
528
+ path.join(packageRoot, "README.md"),
529
+ `# ${name}\n\nPlatform runtime package for Vantaloom ${platform}. Install @vantaloom/cli instead of this package directly.\n`
530
+ )
531
+ }
532
+
533
+ export function npmPackageVersion(sourceRoot) {
534
+ const packageJSON = readJSONIfExists(path.join(sourceRoot, "packages", "cli", "package.json"))
535
+ return packageJSON.version ?? "0.0.0"
536
+ }
537
+
538
+ export function gitVersion(sourceRoot) {
539
+ const result = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
540
+ cwd: sourceRoot,
541
+ encoding: "utf8",
542
+ windowsHide: true,
543
+ })
544
+ if (result.status === 0) {
545
+ return result.stdout.trim() || "dev"
546
+ }
547
+ return "dev"
548
+ }
549
+
550
+ export function buildGo(sourceRoot, buildBin, name, targetPlatform) {
551
+ const goEnv = targetPlatform ? platformToGoEnv(targetPlatform) : {}
552
+ const isWindowsTarget = targetPlatform
553
+ ? targetPlatform.startsWith("win32")
554
+ : process.platform === "win32"
555
+ const ext = isWindowsTarget ? ".exe" : ""
556
+ let ldflags = "-s -w"
557
+ const args = ["build"]
558
+ // The privileged mesh sidecar is built REPRODUCIBLY: with no embedded git VCS
559
+ // stamp or build id, unchanged mesh source yields byte-identical output across
560
+ // releases. That lets an update where the mesh hasn't changed be detected as a
561
+ // no-op and SKIP the privileged service re-apply — so routine updates need no
562
+ // UAC (Windows) / sudo (Linux/macOS) and can run fully unattended/remotely.
563
+ if (name === "vantaloom-mesh") {
564
+ args.push("-trimpath", "-buildvcs=false")
565
+ ldflags += " -buildid="
566
+ }
567
+ args.push(
568
+ "-ldflags", ldflags,
569
+ "-o", path.join(buildBin, `${name}${ext}`),
570
+ `./apps/api/cmd/${name}`
571
+ )
572
+ run("go", args, { cwd: sourceRoot, env: { ...process.env, ...goEnv } })
573
+ }
574
+
575
+ // ensureInPath adds the Vantaloom install directory to the user's shell PATH
576
+ // on macOS and Linux, so `vantaloom` can be run directly after install.
577
+ // On Windows this is not needed (vantaloom.cmd is run by full path or added via installer).
578
+ export function ensureInPath(prefix) {
579
+ if (process.platform === "win32") return
580
+
581
+ // Check if already in PATH
582
+ const pathDirs = (process.env.PATH || "").split(":")
583
+ if (pathDirs.includes(prefix)) return
584
+
585
+ // Determine shell profile file
586
+ const home = os.homedir()
587
+ const shell = process.env.SHELL || ""
588
+ let profilePath
589
+ if (shell.endsWith("/zsh") || existsSync(path.join(home, ".zshrc"))) {
590
+ profilePath = path.join(home, ".zshrc")
591
+ } else if (shell.endsWith("/bash")) {
592
+ // On macOS, bash uses .bash_profile; on Linux, .bashrc
593
+ profilePath = process.platform === "darwin"
594
+ ? path.join(home, ".bash_profile")
595
+ : path.join(home, ".bashrc")
596
+ } else if (existsSync(path.join(home, ".profile"))) {
597
+ profilePath = path.join(home, ".profile")
598
+ } else {
599
+ profilePath = path.join(home, ".profile")
600
+ }
601
+
602
+ // Use $HOME-relative path for portability
603
+ const homeRelative = prefix.startsWith(home)
604
+ ? `$HOME${prefix.slice(home.length)}`
605
+ : prefix
606
+ const exportLine = `export PATH="${homeRelative}:$PATH"`
607
+ const marker = "# vantaloom"
608
+
609
+ // Check if already added to profile
610
+ try {
611
+ if (existsSync(profilePath)) {
612
+ const content = readFileSync(profilePath, "utf8")
613
+ if (content.includes("vantaloom") && content.includes("PATH")) return
614
+ }
615
+ } catch {}
616
+
617
+ // Append to profile
618
+ try {
619
+ const entry = `\n${marker}\n${exportLine}\n`
620
+ appendFileSync(profilePath, entry)
621
+ console.log(`PATH: added ${prefix} to ${profilePath}`)
622
+ console.log(` run: source ${profilePath} (or open a new terminal)`)
623
+ } catch (error) {
624
+ console.log(`PATH: could not update ${profilePath}: ${error.message}`)
625
+ console.log(` add manually: ${exportLine}`)
626
+ }
627
+ }