@vantaloom/runtime-linux-x64 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.
- package/VERSION +1 -1
- package/bin/vantaloom-agent +0 -0
- package/bin/vantaloom-api +0 -0
- package/bin/vantaloom-browser +0 -0
- package/bin/vantaloomctl +0 -0
- package/cli/package.json +1 -1
- package/cli/src/cli.mjs +460 -2100
- package/cli/src/lib/auth.mjs +155 -0
- package/cli/src/lib/constants.mjs +34 -0
- package/cli/src/lib/install.mjs +627 -0
- package/cli/src/lib/lifecycle.mjs +187 -0
- package/cli/src/lib/mesh.mjs +236 -0
- package/cli/src/lib/package.mjs +206 -0
- package/cli/src/lib/platform.mjs +165 -0
- package/cli/src/lib/registry.mjs +237 -0
- package/manifest.json +2 -2
- package/package.json +1 -1
- package/web/404.html +1 -1
- package/web/__next.__PAGE__.txt +2 -2
- package/web/__next._full.txt +2 -2
- package/web/__next._head.txt +1 -1
- package/web/__next._index.txt +1 -1
- package/web/__next._tree.txt +1 -1
- package/web/_next/static/chunks/{c45c5fae4a957e2f.js → fb49ea3bfe8faeb0.js} +1 -1
- package/web/_not-found/__next._full.txt +1 -1
- package/web/_not-found/__next._head.txt +1 -1
- package/web/_not-found/__next._index.txt +1 -1
- package/web/_not-found/__next._not-found/__PAGE__.txt +1 -1
- package/web/_not-found/__next._not-found.txt +1 -1
- package/web/_not-found/__next._tree.txt +1 -1
- package/web/_not-found.html +1 -1
- package/web/_not-found.txt +1 -1
- package/web/index.html +1 -1
- package/web/index.txt +2 -2
- /package/web/_next/static/{RpkdljcHezHulenQJbyKT → fdvtf4murB4YOO6QLnYED}/_buildManifest.js +0 -0
- /package/web/_next/static/{RpkdljcHezHulenQJbyKT → fdvtf4murB4YOO6QLnYED}/_clientMiddlewareManifest.json +0 -0
- /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
|
+
}
|