@vantaloom/cli 0.6.0 → 0.13.6
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/package.json +2 -1
- package/src/cli.mjs +71 -1299
- package/src/lib/auth.mjs +155 -0
- package/src/lib/constants.mjs +29 -0
- package/src/lib/install.mjs +590 -0
- package/src/lib/legacy-cleanup.mjs +148 -0
- package/src/lib/lifecycle.mjs +189 -0
- package/src/lib/package.mjs +98 -0
- package/src/lib/platform.mjs +158 -0
- package/src/lib/registry.mjs +237 -0
|
@@ -0,0 +1,590 @@
|
|
|
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
|
+
readJSONIfExists,
|
|
32
|
+
removeKnownPath,
|
|
33
|
+
writeText,
|
|
34
|
+
run,
|
|
35
|
+
runPnpm,
|
|
36
|
+
platformToGoEnv,
|
|
37
|
+
} from "./platform.mjs"
|
|
38
|
+
import {
|
|
39
|
+
killTrayProcess,
|
|
40
|
+
uninstallLegacyMeshOnce,
|
|
41
|
+
} from "./legacy-cleanup.mjs"
|
|
42
|
+
import {
|
|
43
|
+
enableRuntimeAutostart,
|
|
44
|
+
} from "./lifecycle.mjs"
|
|
45
|
+
|
|
46
|
+
// sleepSync blocks for ms milliseconds without async (install runs top-to-bottom
|
|
47
|
+
// and must not race the file copy against a process that is still releasing its
|
|
48
|
+
// handles). Uses Atomics.wait on a throwaway buffer — no busy spin.
|
|
49
|
+
export function sleepSync(ms) {
|
|
50
|
+
try {
|
|
51
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms))
|
|
52
|
+
} catch {
|
|
53
|
+
const end = Date.now() + ms
|
|
54
|
+
while (Date.now() < end) {} // fallback busy-wait if SAB is unavailable
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// copyFileResilient overwrites dst with src, surviving the Windows case where dst
|
|
59
|
+
// is a running/locked executable. Windows refuses to OVERWRITE or DELETE an in-use
|
|
60
|
+
// .exe (EPERM/EBUSY) but DOES allow RENAMING it — the file handle tracks the file
|
|
61
|
+
// object, not its path. So on a lock error we move the locked file aside and copy
|
|
62
|
+
// the new one into the freed path; the runtime picks up the new binary on its next
|
|
63
|
+
// start, and the moved-aside `.old-*` file is reaped on a later install. This is
|
|
64
|
+
// the fix for the EPERM that blocked update/restart when a prior process (api,
|
|
65
|
+
// agent, or an orphaned easytier/mesh) still held a binary open.
|
|
66
|
+
export function copyFileResilient(src, dst) {
|
|
67
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
68
|
+
try {
|
|
69
|
+
copyFileSync(src, dst)
|
|
70
|
+
return
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (!LOCK_ERROR_CODES.has(err.code) || !existsSync(dst)) {
|
|
73
|
+
if (attempt >= 4) throw err
|
|
74
|
+
sleepSync(250)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const aside = `${dst}.old-${process.pid}-${attempt}`
|
|
79
|
+
renameSync(dst, aside)
|
|
80
|
+
copyFileSync(src, dst)
|
|
81
|
+
try { rmSync(aside, { force: true }) } catch {} // best-effort; may still be locked
|
|
82
|
+
return
|
|
83
|
+
} catch (moveErr) {
|
|
84
|
+
if (attempt >= 4) throw moveErr
|
|
85
|
+
sleepSync(300)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// copyDirResilient recursively copies src→dst using copyFileResilient for every
|
|
92
|
+
// file, so a single locked binary can't abort the whole bin/ refresh. filter(srcPath)
|
|
93
|
+
// gates which entries are copied (mirrors fs.cp's filter).
|
|
94
|
+
export function copyDirResilient(src, dst, filter) {
|
|
95
|
+
mkdirSync(dst, { recursive: true })
|
|
96
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
97
|
+
const s = path.join(src, entry.name)
|
|
98
|
+
if (filter && !filter(s)) continue
|
|
99
|
+
const d = path.join(dst, entry.name)
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
copyDirResilient(s, d, filter)
|
|
102
|
+
} else {
|
|
103
|
+
copyFileResilient(s, d)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// cleanupStaleReplacements deletes the `.old-*` files left behind by a prior
|
|
109
|
+
// lock-safe replace once their handles have been released (best-effort).
|
|
110
|
+
export function cleanupStaleReplacements(dir) {
|
|
111
|
+
if (!existsSync(dir)) return
|
|
112
|
+
for (const entry of readdirSync(dir)) {
|
|
113
|
+
if (/\.old-\d+-\d+$/.test(entry)) {
|
|
114
|
+
try { rmSync(path.join(dir, entry), { force: true }) } catch {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function applyPackage(packageRoot, prefix, options) {
|
|
120
|
+
assertRuntimePackage(packageRoot)
|
|
121
|
+
|
|
122
|
+
const existingConfig = readInstalledConfig(prefix)
|
|
123
|
+
const packageConfig = readJSONIfExists(path.join(packageRoot, "cli", "config.json"))
|
|
124
|
+
const mergedConfig = mergeRuntimeConfig(packageConfig, existingConfig, {
|
|
125
|
+
sourceRoot: options.sourceRoot,
|
|
126
|
+
})
|
|
127
|
+
const version = readVersion(packageRoot)
|
|
128
|
+
|
|
129
|
+
mkdirSync(prefix, { recursive: true })
|
|
130
|
+
const existingCtl = path.join(prefix, "bin", binaryName("vantaloomctl"))
|
|
131
|
+
if (existsSync(existingCtl)) {
|
|
132
|
+
spawnSync(existingCtl, ["stop", "--prefix", prefix], { stdio: "inherit", windowsHide: true })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Kill lingering tray process that may hold locks on bin/ (older versions
|
|
136
|
+
// don't write tray.pid, so vantaloomctl stop won't find them).
|
|
137
|
+
killTrayProcess(prefix)
|
|
138
|
+
|
|
139
|
+
// Windows releases a stopped process's file handles asynchronously; copying
|
|
140
|
+
// bin/ the instant after `stop` can still hit the old exe's lock (EPERM). Give
|
|
141
|
+
// the OS a moment to close handles, and sweep any `.old-*` files a previous
|
|
142
|
+
// lock-safe replace left behind (now that those handles are likely released).
|
|
143
|
+
// copyFileResilient below is the real safety net if a handle is still open.
|
|
144
|
+
if (existsSync(path.join(prefix, "bin"))) {
|
|
145
|
+
sleepSync(600)
|
|
146
|
+
cleanupStaleReplacements(path.join(prefix, "bin"))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Copy package contents to install prefix
|
|
150
|
+
const optionalDirs = new Set()
|
|
151
|
+
for (const name of ["bin", "web", "cli"]) {
|
|
152
|
+
const src = path.join(packageRoot, name)
|
|
153
|
+
const dst = path.join(prefix, name)
|
|
154
|
+
if (!existsSync(src)) {
|
|
155
|
+
if (!optionalDirs.has(name)) {
|
|
156
|
+
console.error(` warning: package missing ${name}/ directory`)
|
|
157
|
+
}
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
const copyOpts = { recursive: true, force: true, dereference: false }
|
|
161
|
+
if (name === "bin") {
|
|
162
|
+
// Never wipe bin/: overwrite in place (unconditional overlay). bin/ holds
|
|
163
|
+
// the long-lived executables (api, agent, ctl, browser). Copy each file
|
|
164
|
+
// with the lock-safe move-aside replace so a still-locked binary from a
|
|
165
|
+
// not-fully-exited prior process never aborts the update with EPERM (the
|
|
166
|
+
// bug that wedged update/restart).
|
|
167
|
+
copyDirResilient(src, dst)
|
|
168
|
+
continue
|
|
169
|
+
} else {
|
|
170
|
+
removeKnownPath(dst, prefix)
|
|
171
|
+
}
|
|
172
|
+
await cp(src, dst, copyOpts)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Ensure binaries are executable on Unix (cross-compiled from Windows they lose +x)
|
|
176
|
+
const binDir = path.join(prefix, "bin")
|
|
177
|
+
if (process.platform !== "win32" && existsSync(binDir)) {
|
|
178
|
+
for (const entry of readdirSync(binDir)) {
|
|
179
|
+
const binPath = path.join(binDir, entry)
|
|
180
|
+
try { chmodSync(binPath, 0o755) } catch {}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// One-time migration: an install/update from before 0.13 may still have the
|
|
185
|
+
// privileged VantaloomMesh service (EasyTier P2P sidecar) registered on
|
|
186
|
+
// disk, plus its binaries in bin/. The new package no longer bundles
|
|
187
|
+
// vantaloom-mesh/easytier-core/the support DLLs, and the bin/ copy above
|
|
188
|
+
// only OVERLAYS files (never deletes) — so without this step the old
|
|
189
|
+
// service + stale binaries would linger forever and could even
|
|
190
|
+
// restart-loop easytier-core. Idempotent (a done-marker skips all future
|
|
191
|
+
// installs/updates) and never blocks install on a declined/failed
|
|
192
|
+
// elevation — see uninstallLegacyMeshOnce.
|
|
193
|
+
if (!options.noStart) {
|
|
194
|
+
await uninstallLegacyMeshOnce(prefix, options)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Make the runtime start at login/boot (per-user, no elevation). Idempotent.
|
|
198
|
+
if (!options.noStart && !options.skipAutostart) {
|
|
199
|
+
enableRuntimeAutostart(prefix)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Verify vantaloomctl binary exists before trying to run it
|
|
203
|
+
const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
|
|
204
|
+
if (!existsSync(ctlBin)) {
|
|
205
|
+
const binContents = existsSync(binDir) ? readdirSync(binDir) : []
|
|
206
|
+
const srcBinContents = existsSync(path.join(packageRoot, "bin")) ? readdirSync(path.join(packageRoot, "bin")) : []
|
|
207
|
+
throw new Error(
|
|
208
|
+
`vantaloomctl binary not found at ${ctlBin}\n` +
|
|
209
|
+
` installed bin/: [${binContents.join(", ")}]\n` +
|
|
210
|
+
` package bin/: [${srcBinContents.join(", ")}]\n` +
|
|
211
|
+
` platform: ${platformId()}\n` +
|
|
212
|
+
` This may indicate a corrupt download. Try again or install from source.`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await writeLauncher(prefix)
|
|
217
|
+
await writeText(path.join(prefix, "cli", "config.json"), `${JSON.stringify(mergedConfig, null, 2)}\n`)
|
|
218
|
+
await writeText(path.join(prefix, "VERSION"), `${version}\n`)
|
|
219
|
+
await cp(path.join(packageRoot, "manifest.json"), path.join(prefix, "manifest.json"), {
|
|
220
|
+
force: true,
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
run(ctlBin, [
|
|
224
|
+
"install",
|
|
225
|
+
"--prefix",
|
|
226
|
+
prefix,
|
|
227
|
+
"--version",
|
|
228
|
+
version,
|
|
229
|
+
])
|
|
230
|
+
|
|
231
|
+
if (!options.noStart) {
|
|
232
|
+
run(ctlBin, [
|
|
233
|
+
"start",
|
|
234
|
+
"--prefix",
|
|
235
|
+
prefix,
|
|
236
|
+
])
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return version
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function assertRuntimePackage(packageRoot) {
|
|
243
|
+
for (const name of ["bin", "web", "cli", "manifest.json"]) {
|
|
244
|
+
if (!existsSync(path.join(packageRoot, name))) {
|
|
245
|
+
throw new Error(`invalid Vantaloom package, missing ${name}: ${packageRoot}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function readVersion(packageRoot) {
|
|
251
|
+
const versionPath = path.join(packageRoot, "VERSION")
|
|
252
|
+
if (existsSync(versionPath)) {
|
|
253
|
+
return readFileSync(versionPath, "utf8").trim() || "dev"
|
|
254
|
+
}
|
|
255
|
+
const manifest = readJSONIfExists(path.join(packageRoot, "manifest.json"))
|
|
256
|
+
return manifest.version ?? "dev"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function readInstalledConfig(prefix) {
|
|
260
|
+
return readJSONIfExists(path.join(prefix, "cli", "config.json"))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
|
|
264
|
+
const merged = { ...packageConfig }
|
|
265
|
+
for (const key of ["sourceRoot", "remote", "repo", "releaseTag", "runtimePackage", "runtimeVersion", "npmRegistry"]) {
|
|
266
|
+
if (!merged[key] && existingConfig[key]) {
|
|
267
|
+
merged[key] = existingConfig[key]
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (overrides.sourceRoot) {
|
|
271
|
+
merged.sourceRoot = overrides.sourceRoot
|
|
272
|
+
}
|
|
273
|
+
if (overrides.runtimePackage) {
|
|
274
|
+
merged.runtimePackage = overrides.runtimePackage
|
|
275
|
+
}
|
|
276
|
+
if (overrides.runtimeVersion) {
|
|
277
|
+
merged.runtimeVersion = overrides.runtimeVersion
|
|
278
|
+
}
|
|
279
|
+
if (overrides.npmRegistry) {
|
|
280
|
+
merged.npmRegistry = overrides.npmRegistry
|
|
281
|
+
}
|
|
282
|
+
if (!merged.repo) {
|
|
283
|
+
merged.repo = defaultRepo
|
|
284
|
+
}
|
|
285
|
+
if (!merged.releaseTag) {
|
|
286
|
+
merged.releaseTag = defaultReleaseTag
|
|
287
|
+
}
|
|
288
|
+
// Always force runtimePackage to match the running platform — a cross-compiled
|
|
289
|
+
// package may carry a config for a different platform (e.g. win32 inside darwin).
|
|
290
|
+
merged.runtimePackage = runtimePackageName(platformId())
|
|
291
|
+
if (!merged.runtimeVersion) {
|
|
292
|
+
merged.runtimeVersion = "latest"
|
|
293
|
+
}
|
|
294
|
+
if (!merged.npmRegistry) {
|
|
295
|
+
merged.npmRegistry = defaultNpmRegistry
|
|
296
|
+
}
|
|
297
|
+
return merged
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
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
|
+
return {
|
|
320
|
+
...(process.env.GITHUB_ACTIONS ? {} : { sourceRoot }),
|
|
321
|
+
...(remote ? { remote } : {}),
|
|
322
|
+
repo,
|
|
323
|
+
releaseTag: defaultReleaseTag,
|
|
324
|
+
runtimePackage: runtimePackageName(platformId()),
|
|
325
|
+
runtimeVersion: "latest",
|
|
326
|
+
npmRegistry: npmRegistry || defaultNpmRegistry,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function findSourceRoot(sourceOption) {
|
|
331
|
+
if (sourceOption) {
|
|
332
|
+
return assertSourceRoot(path.resolve(sourceOption))
|
|
333
|
+
}
|
|
334
|
+
if (process.env.VANTALOOM_SOURCE) {
|
|
335
|
+
return assertSourceRoot(path.resolve(process.env.VANTALOOM_SOURCE))
|
|
336
|
+
}
|
|
337
|
+
if (existsSync(installedConfigPath)) {
|
|
338
|
+
const config = JSON.parse(readFileSync(installedConfigPath, "utf8"))
|
|
339
|
+
if (config.sourceRoot) {
|
|
340
|
+
return assertSourceRoot(path.resolve(config.sourceRoot))
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return assertSourceRoot(repoCandidate)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function tryFindSourceRoot() {
|
|
347
|
+
try {
|
|
348
|
+
return findSourceRoot()
|
|
349
|
+
} catch {
|
|
350
|
+
return ""
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function tryAssertSourceRoot(sourceRoot) {
|
|
355
|
+
try {
|
|
356
|
+
return assertSourceRoot(sourceRoot)
|
|
357
|
+
} catch {
|
|
358
|
+
return ""
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function assertSourceRoot(sourceRoot) {
|
|
363
|
+
if (!existsSync(path.join(sourceRoot, "apps", "api", "go.mod"))) {
|
|
364
|
+
throw new Error(`not a Vantaloom source root: ${sourceRoot}`)
|
|
365
|
+
}
|
|
366
|
+
return sourceRoot
|
|
367
|
+
}
|
|
368
|
+
|
|
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
|
+
export async function copyStaticWeb(sourceRoot, buildWeb) {
|
|
395
|
+
const exportRoot = path.join(sourceRoot, "apps", "vantaloom", "out")
|
|
396
|
+
if (!existsSync(exportRoot)) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
"missing Next static export output; let GitHub CI run production build, or pass --build-web for a local one-off build"
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
removeKnownPath(buildWeb, path.dirname(buildWeb))
|
|
403
|
+
await copyDir(exportRoot, buildWeb)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function copyCliDirectory(target, sourceRoot, config) {
|
|
407
|
+
const sourceCliRoot = path.join(sourceRoot, "packages", "cli")
|
|
408
|
+
if (!existsSync(path.join(sourceCliRoot, "bin", "vantaloom.mjs"))) {
|
|
409
|
+
throw new Error(`missing source CLI package: ${sourceCliRoot}`)
|
|
410
|
+
}
|
|
411
|
+
removeKnownPath(target, path.dirname(target))
|
|
412
|
+
mkdirSync(target, { recursive: true })
|
|
413
|
+
await copyDir(sourceCliRoot, target)
|
|
414
|
+
await writeText(
|
|
415
|
+
path.join(target, "config.json"),
|
|
416
|
+
`${JSON.stringify(config, null, 2)}\n`
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// writeLauncher writes the OS launcher script to the install prefix.
|
|
421
|
+
// NOTE: mirrored in Go at apps/desktop/internal/runtime/
|
|
422
|
+
export async function writeLauncher(prefix) {
|
|
423
|
+
if (process.platform === "win32") {
|
|
424
|
+
await writeText(
|
|
425
|
+
path.join(prefix, "vantaloom.cmd"),
|
|
426
|
+
`@echo off\r\nnode "%~dp0cli\\bin\\vantaloom.mjs" %*\r\n`
|
|
427
|
+
)
|
|
428
|
+
} else {
|
|
429
|
+
const launcher = `#!/usr/bin/env sh\nexec node "$(dirname "$0")/cli/bin/vantaloom.mjs" "$@"\n`
|
|
430
|
+
const launcherPath = path.join(prefix, "vantaloom")
|
|
431
|
+
await writeText(launcherPath, launcher)
|
|
432
|
+
chmodSync(launcherPath, 0o755)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export async function copyDir(source, destination, options = {}) {
|
|
437
|
+
if (!existsSync(source)) {
|
|
438
|
+
throw new Error(`missing source directory: ${source}`)
|
|
439
|
+
}
|
|
440
|
+
removeKnownPath(destination, path.dirname(destination))
|
|
441
|
+
mkdirSync(destination, { recursive: true })
|
|
442
|
+
await cp(source, destination, {
|
|
443
|
+
recursive: true,
|
|
444
|
+
force: true,
|
|
445
|
+
dereference: options.dereference ?? true,
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function writeBuildManifest(buildRoot, version, platform, commit) {
|
|
450
|
+
const components = ["api", "agent", "web", "ctl"]
|
|
451
|
+
writeFileSync(path.join(buildRoot, "VERSION"), `${version}\n`)
|
|
452
|
+
writeFileSync(
|
|
453
|
+
path.join(buildRoot, "manifest.json"),
|
|
454
|
+
`${JSON.stringify(
|
|
455
|
+
{
|
|
456
|
+
name: "Vantaloom Local Runtime",
|
|
457
|
+
// The ONE product version: always the npm semver. The source git hash
|
|
458
|
+
// rides along as `commit` for diagnostics — it must never leak into
|
|
459
|
+
// `version` (pre-0.13.5 it did, splitting the update checks).
|
|
460
|
+
version,
|
|
461
|
+
platform,
|
|
462
|
+
...(commit ? { commit } : {}),
|
|
463
|
+
updatedAt: new Date().toISOString(),
|
|
464
|
+
components,
|
|
465
|
+
},
|
|
466
|
+
null,
|
|
467
|
+
2
|
|
468
|
+
)}\n`
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function writeRuntimePackageMetadata(packageRoot, sourceRoot, platform) {
|
|
473
|
+
const version = npmPackageVersion(sourceRoot)
|
|
474
|
+
const { os: runtimeOS, cpu } = parsePlatformId(platform)
|
|
475
|
+
const name = runtimePackageName(platform)
|
|
476
|
+
writeFileSync(
|
|
477
|
+
path.join(packageRoot, "package.json"),
|
|
478
|
+
`${JSON.stringify(
|
|
479
|
+
{
|
|
480
|
+
name,
|
|
481
|
+
version,
|
|
482
|
+
private: false,
|
|
483
|
+
description: `Vantaloom local runtime for ${platform}.`,
|
|
484
|
+
type: "module",
|
|
485
|
+
os: [runtimeOS],
|
|
486
|
+
cpu: [cpu],
|
|
487
|
+
files: ["bin", "web", "cli", "manifest.json", "VERSION", "README.md"],
|
|
488
|
+
publishConfig: {
|
|
489
|
+
access: "public",
|
|
490
|
+
},
|
|
491
|
+
engines: {
|
|
492
|
+
node: ">=20",
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
null,
|
|
496
|
+
2
|
|
497
|
+
)}\n`
|
|
498
|
+
)
|
|
499
|
+
writeFileSync(
|
|
500
|
+
path.join(packageRoot, "README.md"),
|
|
501
|
+
`# ${name}\n\nPlatform runtime package for Vantaloom ${platform}. Install @vantaloom/cli instead of this package directly.\n`
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function npmPackageVersion(sourceRoot) {
|
|
506
|
+
const packageJSON = readJSONIfExists(path.join(sourceRoot, "packages", "cli", "package.json"))
|
|
507
|
+
return packageJSON.version ?? "0.0.0"
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function gitVersion(sourceRoot) {
|
|
511
|
+
const result = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
|
|
512
|
+
cwd: sourceRoot,
|
|
513
|
+
encoding: "utf8",
|
|
514
|
+
windowsHide: true,
|
|
515
|
+
})
|
|
516
|
+
if (result.status === 0) {
|
|
517
|
+
return result.stdout.trim() || "dev"
|
|
518
|
+
}
|
|
519
|
+
return "dev"
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function buildGo(sourceRoot, buildBin, name, targetPlatform) {
|
|
523
|
+
const goEnv = targetPlatform ? platformToGoEnv(targetPlatform) : {}
|
|
524
|
+
const isWindowsTarget = targetPlatform
|
|
525
|
+
? targetPlatform.startsWith("win32")
|
|
526
|
+
: process.platform === "win32"
|
|
527
|
+
const ext = isWindowsTarget ? ".exe" : ""
|
|
528
|
+
let ldflags = "-s -w"
|
|
529
|
+
const args = ["build"]
|
|
530
|
+
args.push(
|
|
531
|
+
"-ldflags", ldflags,
|
|
532
|
+
"-o", path.join(buildBin, `${name}${ext}`),
|
|
533
|
+
`./apps/api/cmd/${name}`
|
|
534
|
+
)
|
|
535
|
+
run("go", args, { cwd: sourceRoot, env: { ...process.env, ...goEnv } })
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ensureInPath adds the Vantaloom install directory to the user's shell PATH
|
|
539
|
+
// on macOS and Linux, so `vantaloom` can be run directly after install.
|
|
540
|
+
// On Windows this is not needed (vantaloom.cmd is run by full path or added via installer).
|
|
541
|
+
export function ensureInPath(prefix) {
|
|
542
|
+
if (process.platform === "win32") return
|
|
543
|
+
|
|
544
|
+
// Check if already in PATH
|
|
545
|
+
const pathDirs = (process.env.PATH || "").split(":")
|
|
546
|
+
if (pathDirs.includes(prefix)) return
|
|
547
|
+
|
|
548
|
+
// Determine shell profile file
|
|
549
|
+
const home = os.homedir()
|
|
550
|
+
const shell = process.env.SHELL || ""
|
|
551
|
+
let profilePath
|
|
552
|
+
if (shell.endsWith("/zsh") || existsSync(path.join(home, ".zshrc"))) {
|
|
553
|
+
profilePath = path.join(home, ".zshrc")
|
|
554
|
+
} else if (shell.endsWith("/bash")) {
|
|
555
|
+
// On macOS, bash uses .bash_profile; on Linux, .bashrc
|
|
556
|
+
profilePath = process.platform === "darwin"
|
|
557
|
+
? path.join(home, ".bash_profile")
|
|
558
|
+
: path.join(home, ".bashrc")
|
|
559
|
+
} else if (existsSync(path.join(home, ".profile"))) {
|
|
560
|
+
profilePath = path.join(home, ".profile")
|
|
561
|
+
} else {
|
|
562
|
+
profilePath = path.join(home, ".profile")
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Use $HOME-relative path for portability
|
|
566
|
+
const homeRelative = prefix.startsWith(home)
|
|
567
|
+
? `$HOME${prefix.slice(home.length)}`
|
|
568
|
+
: prefix
|
|
569
|
+
const exportLine = `export PATH="${homeRelative}:$PATH"`
|
|
570
|
+
const marker = "# vantaloom"
|
|
571
|
+
|
|
572
|
+
// Check if already added to profile
|
|
573
|
+
try {
|
|
574
|
+
if (existsSync(profilePath)) {
|
|
575
|
+
const content = readFileSync(profilePath, "utf8")
|
|
576
|
+
if (content.includes("vantaloom") && content.includes("PATH")) return
|
|
577
|
+
}
|
|
578
|
+
} catch {}
|
|
579
|
+
|
|
580
|
+
// Append to profile
|
|
581
|
+
try {
|
|
582
|
+
const entry = `\n${marker}\n${exportLine}\n`
|
|
583
|
+
appendFileSync(profilePath, entry)
|
|
584
|
+
console.log(`PATH: added ${prefix} to ${profilePath}`)
|
|
585
|
+
console.log(` run: source ${profilePath} (or open a new terminal)`)
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.log(`PATH: could not update ${profilePath}: ${error.message}`)
|
|
588
|
+
console.log(` add manually: ${exportLine}`)
|
|
589
|
+
}
|
|
590
|
+
}
|