@vantaloom/cli 0.1.3 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/cli.mjs +213 -119
package/README.md CHANGED
@@ -9,4 +9,4 @@ vantaloom status
9
9
  vantaloom update
10
10
  ```
11
11
 
12
- The CLI downloads platform-specific runtime packages produced by the Vantaloom GitHub CI pipeline.
12
+ The CLI downloads the current platform runtime from public npm packages such as `@vantaloom/runtime-win32-x64`, `@vantaloom/runtime-linux-x64`, and `@vantaloom/runtime-darwin-arm64`. No GitHub token is required for install or update.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantaloom/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Vantaloom local runtime manager.",
package/src/cli.mjs CHANGED
@@ -2,12 +2,10 @@ import { execFileSync, spawnSync } from "node:child_process"
2
2
  import {
3
3
  chmodSync,
4
4
  existsSync,
5
- lstatSync,
6
5
  mkdirSync,
7
6
  mkdtempSync,
8
7
  readdirSync,
9
8
  readFileSync,
10
- readlinkSync,
11
9
  rmSync,
12
10
  writeFileSync,
13
11
  } from "node:fs"
@@ -21,19 +19,7 @@ const repoCandidate = path.resolve(cliRoot, "..", "..")
21
19
  const installedConfigPath = path.join(cliRoot, "config.json")
22
20
  const defaultReleaseTag = "runtime-latest"
23
21
  const defaultRepo = "Timefiles404/Vantaloom-next"
24
-
25
- const runtimePackages = [
26
- "@next/env",
27
- "@swc/helpers",
28
- "baseline-browser-mapping",
29
- "caniuse-lite",
30
- "client-only",
31
- "postcss",
32
- "react",
33
- "react-dom",
34
- "scheduler",
35
- "styled-jsx",
36
- ]
22
+ const defaultNpmRegistry = "https://registry.npmjs.org"
37
23
 
38
24
  export async function main(argv) {
39
25
  const command = argv[0] ?? "help"
@@ -43,19 +29,23 @@ export async function main(argv) {
43
29
  case "install":
44
30
  if (options.package) {
45
31
  await installFromPackage({ ...options, update: false })
46
- } else if (shouldUseDistSync(options)) {
32
+ } else if (shouldUseSourceInstall(options)) {
33
+ await installFromSource(options)
34
+ } else if (shouldUseReleaseSync(options)) {
47
35
  await syncFromRelease(options, "install")
48
36
  } else {
49
- await installFromSource(options)
37
+ await syncFromNpmRegistry(options, "install")
50
38
  }
51
39
  return
52
40
  case "update":
53
41
  if (options.package) {
54
42
  await installFromPackage({ ...options, update: true })
55
- } else if (shouldUseDistSync(options)) {
43
+ } else if (shouldUseSourceInstall(options)) {
44
+ await installFromSource({ ...options, update: true })
45
+ } else if (shouldUseReleaseSync(options)) {
56
46
  await syncFromRelease(options, "update")
57
47
  } else {
58
- await installFromSource({ ...options, update: true })
48
+ await syncFromNpmRegistry(options, "update")
59
49
  }
60
50
  return
61
51
  case "package":
@@ -126,6 +116,10 @@ async function packageRuntime(options) {
126
116
  buildWeb: options.buildWeb,
127
117
  })
128
118
 
119
+ if (options.npmPackage) {
120
+ writeRuntimePackageMetadata(packageRoot, sourceRoot, builtPlatform)
121
+ }
122
+
129
123
  if (options.archive) {
130
124
  const archivePath = `${packageRoot}.tar.gz`
131
125
  removeKnownPath(archivePath, path.dirname(archivePath))
@@ -161,7 +155,7 @@ async function buildRuntimePackage(sourceRoot, packageRoot, options) {
161
155
  runPnpm(["--filter", "vantaloom-app", "build"], { cwd: sourceRoot })
162
156
  }
163
157
 
164
- await copyStandaloneWeb(sourceRoot, buildWeb)
158
+ await copyStaticWeb(sourceRoot, buildWeb)
165
159
  await copyCliDirectory(path.join(packageRoot, "cli"), sourceRoot, runtimeConfigFromSource(sourceRoot))
166
160
  writeBuildManifest(packageRoot, version, platform)
167
161
 
@@ -219,6 +213,46 @@ async function applyPackage(packageRoot, prefix, options) {
219
213
  return version
220
214
  }
221
215
 
216
+ async function syncFromNpmRegistry(options, action) {
217
+ const prefix = safeDirectory(options.prefix ?? defaultPrefix())
218
+ const installedConfig = readInstalledConfig(prefix)
219
+ const runtimePackage = options.runtimePackage || installedConfig.runtimePackage || runtimePackageName(platformId())
220
+ const runtimeVersion = options.runtimeVersion || installedConfig.runtimeVersion || "latest"
221
+ const registry = normalizeRegistry(options.npmRegistry || installedConfig.npmRegistry || defaultNpmRegistry)
222
+ const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-npm-"))
223
+
224
+ try {
225
+ const extractRoot = path.join(tempRoot, "extract")
226
+ const archive = path.join(tempRoot, `${packageBasename(runtimePackage)}-${runtimeVersion}.tgz`)
227
+ mkdirSync(extractRoot, { recursive: true })
228
+
229
+ const resolved = await resolveNpmPackage({ registry, name: runtimePackage, version: runtimeVersion })
230
+ await downloadNpmTarball({
231
+ tarballUrl: resolved.tarball,
232
+ target: archive,
233
+ packageName: runtimePackage,
234
+ version: resolved.version,
235
+ })
236
+ run("tar", ["-xzf", archive, "-C", extractRoot])
237
+
238
+ const packageRoot = findExtractedNpmPackage(extractRoot)
239
+ const version = await applyPackage(packageRoot, prefix, {
240
+ noStart: options.noStart,
241
+ runtimePackage,
242
+ runtimeVersion: options.runtimeVersion ? resolved.version : "latest",
243
+ npmRegistry: registry,
244
+ update: action === "update",
245
+ })
246
+
247
+ console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
248
+ console.log(`version: ${version}`)
249
+ console.log(`source: ${runtimePackage}@${resolved.version}`)
250
+ console.log(`run: ${displayCommand(prefix)} status`)
251
+ } finally {
252
+ removeKnownPath(tempRoot, os.tmpdir())
253
+ }
254
+ }
255
+
222
256
  async function syncFromRelease(options, action) {
223
257
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
224
258
  const installedConfig = readInstalledConfig(prefix)
@@ -304,8 +338,53 @@ async function downloadReleaseAsset({ repo, releaseTag, assetName, target, token
304
338
  await writeFile(target, buffer)
305
339
  }
306
340
 
307
- function shouldUseDistSync(options) {
308
- return !options.local && !options.source && !options.buildWeb
341
+ async function resolveNpmPackage({ registry, name, version }) {
342
+ const metadataUrl = `${registry}/${encodeURIComponent(name).replace("%2F", "%2f")}`
343
+ const response = await fetch(metadataUrl, {
344
+ headers: {
345
+ Accept: "application/vnd.npm.install-v1+json",
346
+ "User-Agent": "vantaloom-cli",
347
+ },
348
+ })
349
+ if (!response.ok) {
350
+ const detail = await response.text().catch(() => "")
351
+ throw new Error(`failed to inspect npm package ${name}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
352
+ }
353
+
354
+ const metadata = await response.json()
355
+ const selectedVersion = metadata["dist-tags"]?.[version] ?? version
356
+ const packageVersion = metadata.versions?.[selectedVersion]
357
+ if (!packageVersion?.dist?.tarball) {
358
+ const available = Object.keys(metadata.versions ?? {}).slice(-8).join(", ") || "none"
359
+ throw new Error(`missing npm package ${name}@${version}; available versions: ${available}`)
360
+ }
361
+ return {
362
+ version: selectedVersion,
363
+ tarball: packageVersion.dist.tarball,
364
+ }
365
+ }
366
+
367
+ async function downloadNpmTarball({ tarballUrl, target, packageName, version }) {
368
+ const response = await fetch(tarballUrl, {
369
+ headers: {
370
+ "User-Agent": "vantaloom-cli",
371
+ },
372
+ })
373
+ if (!response.ok) {
374
+ const detail = await response.text().catch(() => "")
375
+ throw new Error(`failed to download ${packageName}@${version}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
376
+ }
377
+
378
+ const buffer = Buffer.from(await response.arrayBuffer())
379
+ await writeFile(target, buffer)
380
+ }
381
+
382
+ function shouldUseSourceInstall(options) {
383
+ return Boolean(options.local || options.source || options.buildWeb)
384
+ }
385
+
386
+ function shouldUseReleaseSync(options) {
387
+ return Boolean(options.repo || options.remote || options.releaseTag || options.githubToken)
309
388
  }
310
389
 
311
390
  function assertRuntimePackage(packageRoot) {
@@ -338,7 +417,7 @@ function readJSONIfExists(filePath) {
338
417
 
339
418
  function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
340
419
  const merged = { ...packageConfig }
341
- for (const key of ["sourceRoot", "remote", "repo", "releaseTag"]) {
420
+ for (const key of ["sourceRoot", "remote", "repo", "releaseTag", "runtimePackage", "runtimeVersion", "npmRegistry"]) {
342
421
  if (!merged[key] && existingConfig[key]) {
343
422
  merged[key] = existingConfig[key]
344
423
  }
@@ -346,12 +425,30 @@ function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
346
425
  if (overrides.sourceRoot) {
347
426
  merged.sourceRoot = overrides.sourceRoot
348
427
  }
428
+ if (overrides.runtimePackage) {
429
+ merged.runtimePackage = overrides.runtimePackage
430
+ }
431
+ if (overrides.runtimeVersion) {
432
+ merged.runtimeVersion = overrides.runtimeVersion
433
+ }
434
+ if (overrides.npmRegistry) {
435
+ merged.npmRegistry = overrides.npmRegistry
436
+ }
349
437
  if (!merged.repo) {
350
438
  merged.repo = defaultRepo
351
439
  }
352
440
  if (!merged.releaseTag) {
353
441
  merged.releaseTag = defaultReleaseTag
354
442
  }
443
+ if (!merged.runtimePackage) {
444
+ merged.runtimePackage = runtimePackageName(platformId())
445
+ }
446
+ if (!merged.runtimeVersion) {
447
+ merged.runtimeVersion = "latest"
448
+ }
449
+ if (!merged.npmRegistry) {
450
+ merged.npmRegistry = defaultNpmRegistry
451
+ }
355
452
  return merged
356
453
  }
357
454
 
@@ -363,6 +460,9 @@ function runtimeConfigFromSource(sourceRoot) {
363
460
  ...(remote ? { remote } : {}),
364
461
  repo,
365
462
  releaseTag: defaultReleaseTag,
463
+ runtimePackage: runtimePackageName(platformId()),
464
+ runtimeVersion: "latest",
465
+ npmRegistry: defaultNpmRegistry,
366
466
  }
367
467
  }
368
468
 
@@ -382,6 +482,22 @@ function findExtractedPackage(extractRoot, platform) {
382
482
  throw new Error(`could not find extracted Vantaloom package in ${extractRoot}`)
383
483
  }
384
484
 
485
+ function findExtractedNpmPackage(extractRoot) {
486
+ const expected = path.join(extractRoot, "package")
487
+ if (existsSync(expected)) {
488
+ return expected
489
+ }
490
+
491
+ const directories = readdirSync(extractRoot, { withFileTypes: true })
492
+ .filter((entry) => entry.isDirectory())
493
+ .map((entry) => path.join(extractRoot, entry.name))
494
+ if (directories.length === 1) {
495
+ return directories[0]
496
+ }
497
+
498
+ throw new Error(`could not find extracted npm package in ${extractRoot}`)
499
+ }
500
+
385
501
  function tryFindSourceRoot() {
386
502
  try {
387
503
  return findSourceRoot()
@@ -422,29 +538,16 @@ function inferGitHubRepo(remote) {
422
538
  return sshMatch?.[1] ?? ""
423
539
  }
424
540
 
425
- async function copyStandaloneWeb(sourceRoot, buildWeb) {
426
- const standalone = path.join(sourceRoot, "apps", "vantaloom", ".next", "standalone")
427
- const staticSource = path.join(sourceRoot, "apps", "vantaloom", ".next", "static")
428
- const publicSource = path.join(sourceRoot, "apps", "vantaloom", "public")
429
- if (!existsSync(standalone)) {
541
+ async function copyStaticWeb(sourceRoot, buildWeb) {
542
+ const exportRoot = path.join(sourceRoot, "apps", "vantaloom", "out")
543
+ if (!existsSync(exportRoot)) {
430
544
  throw new Error(
431
- "missing Next standalone output; let GitHub CI run production build, or pass --build-web for a local one-off build"
545
+ "missing Next static export output; let GitHub CI run production build, or pass --build-web for a local one-off build"
432
546
  )
433
547
  }
434
548
 
435
549
  removeKnownPath(buildWeb, path.dirname(buildWeb))
436
- const preserveStandaloneLinks = process.platform !== "win32"
437
- await copyDir(standalone, buildWeb, { dereference: !preserveStandaloneLinks })
438
- if (preserveStandaloneLinks) {
439
- await copyStandaloneHoistedTargets(sourceRoot, buildWeb)
440
- }
441
- await copyDir(staticSource, path.join(buildWeb, "apps", "vantaloom", ".next", "static"))
442
- await copyDir(publicSource, path.join(buildWeb, "apps", "vantaloom", "public"))
443
-
444
- const appNodeModules = path.join(buildWeb, "apps", "vantaloom", "node_modules")
445
- for (const packageName of runtimePackages) {
446
- await copyPnpmRuntimePackage(sourceRoot, packageName, appNodeModules)
447
- }
550
+ await copyDir(exportRoot, buildWeb)
448
551
  }
449
552
 
450
553
  async function copyCliDirectory(target, sourceRoot, config) {
@@ -504,81 +607,6 @@ function buildGo(sourceRoot, buildBin, name) {
504
607
  ], { cwd: sourceRoot })
505
608
  }
506
609
 
507
- async function copyPnpmRuntimePackage(sourceRoot, packageName, destinationNodeModules) {
508
- const pnpmRoot = path.join(sourceRoot, "node_modules", ".pnpm")
509
- const storeName = packageName.replace("/", "+")
510
- const packageRoot = readdirSync(pnpmRoot, { withFileTypes: true })
511
- .filter((entry) => entry.isDirectory() && entry.name.startsWith(`${storeName}@`))
512
- .map((entry) => path.join(pnpmRoot, entry.name))
513
- .sort()
514
- .reverse()[0]
515
- if (!packageRoot) {
516
- throw new Error(`missing pnpm runtime package: ${packageName}`)
517
- }
518
-
519
- const source = path.join(packageRoot, "node_modules", ...packageName.split("/"))
520
- if (!existsSync(source)) {
521
- throw new Error(`missing pnpm runtime package source: ${source}`)
522
- }
523
-
524
- await copyDir(source, path.join(destinationNodeModules, ...packageName.split("/")))
525
- }
526
-
527
- async function copyStandaloneHoistedTargets(sourceRoot, buildWeb) {
528
- const sourceHoisted = path.join(sourceRoot, "node_modules", ".pnpm", "node_modules")
529
- const buildHoisted = path.join(buildWeb, "node_modules", ".pnpm", "node_modules")
530
- const packageNames = findHoistedSymlinkPackages(path.join(buildWeb, "node_modules"))
531
-
532
- for (const packageName of packageNames) {
533
- const source = path.join(sourceHoisted, ...packageName.split("/"))
534
- if (existsSync(source)) {
535
- await copyDir(source, path.join(buildHoisted, ...packageName.split("/")))
536
- }
537
- }
538
- }
539
-
540
- function findHoistedSymlinkPackages(nodeModules) {
541
- const packages = new Set()
542
- if (!existsSync(nodeModules)) {
543
- return packages
544
- }
545
-
546
- for (const entry of readdirSync(nodeModules, { withFileTypes: true })) {
547
- if (entry.name === ".pnpm" || entry.name.startsWith(".")) {
548
- continue
549
- }
550
-
551
- const entryPath = path.join(nodeModules, entry.name)
552
- if (entry.name.startsWith("@") && entry.isDirectory()) {
553
- for (const scopedEntry of readdirSync(entryPath, { withFileTypes: true })) {
554
- addHoistedSymlinkPackage(packages, path.join(entryPath, scopedEntry.name), `${entry.name}/${scopedEntry.name}`)
555
- }
556
- continue
557
- }
558
-
559
- addHoistedSymlinkPackage(packages, entryPath, entry.name)
560
- }
561
-
562
- return packages
563
- }
564
-
565
- function addHoistedSymlinkPackage(packages, packagePath, packageName) {
566
- let stat
567
- try {
568
- stat = lstatSync(packagePath)
569
- } catch {
570
- return
571
- }
572
- if (!stat.isSymbolicLink()) {
573
- return
574
- }
575
-
576
- const target = readlinkSync(packagePath).replaceAll("\\", "/")
577
- if (target.includes(".pnpm/node_modules/")) {
578
- packages.add(packageName)
579
- }
580
- }
581
-
582
610
  async function copyDir(source, destination, options = {}) {
583
611
  if (!existsSync(source)) {
584
612
  throw new Error(`missing source directory: ${source}`)
@@ -610,6 +638,39 @@ function writeBuildManifest(buildRoot, version, platform) {
610
638
  )
611
639
  }
612
640
 
641
+ function writeRuntimePackageMetadata(packageRoot, sourceRoot, platform) {
642
+ const version = npmPackageVersion(sourceRoot)
643
+ const { os: runtimeOS, cpu } = parsePlatformId(platform)
644
+ const name = runtimePackageName(platform)
645
+ writeFileSync(
646
+ path.join(packageRoot, "package.json"),
647
+ `${JSON.stringify(
648
+ {
649
+ name,
650
+ version,
651
+ private: false,
652
+ description: `Vantaloom local runtime for ${platform}.`,
653
+ type: "module",
654
+ os: [runtimeOS],
655
+ cpu: [cpu],
656
+ files: ["bin", "web", "cli", "manifest.json", "VERSION", "README.md"],
657
+ publishConfig: {
658
+ access: "public",
659
+ },
660
+ engines: {
661
+ node: ">=20",
662
+ },
663
+ },
664
+ null,
665
+ 2
666
+ )}\n`
667
+ )
668
+ writeFileSync(
669
+ path.join(packageRoot, "README.md"),
670
+ `# ${name}\n\nPlatform runtime package for Vantaloom ${platform}. Install @vantaloom/cli instead of this package directly.\n`
671
+ )
672
+ }
673
+
613
674
  function findSourceRoot(sourceOption) {
614
675
  if (sourceOption) {
615
676
  return assertSourceRoot(path.resolve(sourceOption))
@@ -626,6 +687,11 @@ function findSourceRoot(sourceOption) {
626
687
  return assertSourceRoot(repoCandidate)
627
688
  }
628
689
 
690
+ function npmPackageVersion(sourceRoot) {
691
+ const packageJSON = readJSONIfExists(path.join(sourceRoot, "packages", "cli", "package.json"))
692
+ return packageJSON.version ?? "0.0.0"
693
+ }
694
+
629
695
  function assertSourceRoot(sourceRoot) {
630
696
  if (!existsSync(path.join(sourceRoot, "apps", "api", "go.mod"))) {
631
697
  throw new Error(`not a Vantaloom source root: ${sourceRoot}`)
@@ -662,6 +728,9 @@ function parseOptions(args) {
662
728
  case "remote":
663
729
  case "release-tag":
664
730
  case "github-token":
731
+ case "runtime-package":
732
+ case "runtime-version":
733
+ case "npm-registry":
665
734
  options[toCamel(key)] = inlineValue ?? args[++index]
666
735
  if (!options[toCamel(key)]) {
667
736
  throw new Error(`missing value for --${key}`)
@@ -676,6 +745,9 @@ function parseOptions(args) {
676
745
  case "archive":
677
746
  options.archive = true
678
747
  break
748
+ case "npm-package":
749
+ options.npmPackage = true
750
+ break
679
751
  case "local":
680
752
  options.local = true
681
753
  break
@@ -735,6 +807,28 @@ function platformId() {
735
807
  return `${process.platform}-${process.arch}`
736
808
  }
737
809
 
810
+ function runtimePackageName(platform) {
811
+ return `@vantaloom/runtime-${platform}`
812
+ }
813
+
814
+ function parsePlatformId(platform) {
815
+ const parts = platform.split("-")
816
+ const cpu = parts.pop()
817
+ const runtimeOS = parts.join("-")
818
+ if (!runtimeOS || !cpu) {
819
+ throw new Error(`invalid platform id: ${platform}`)
820
+ }
821
+ return { os: runtimeOS, cpu }
822
+ }
823
+
824
+ function packageBasename(name) {
825
+ return name.split("/").pop() ?? name
826
+ }
827
+
828
+ function normalizeRegistry(value) {
829
+ return value.replace(/\/+$/, "")
830
+ }
831
+
738
832
  function defaultPrefix() {
739
833
  if (process.env.VANTALOOM_HOME) {
740
834
  return process.env.VANTALOOM_HOME
@@ -768,11 +862,11 @@ function printHelp() {
768
862
  console.log(`Vantaloom CLI
769
863
 
770
864
  Usage:
771
- vantaloom install [--prefix <dir>] [--repo <owner/name>] [--release-tag <tag>] [--github-token <token>] [--package <dir>] [--no-start]
865
+ vantaloom install [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start]
772
866
  vantaloom install --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
773
- vantaloom update [--prefix <dir>] [--repo <owner/name>] [--release-tag <tag>] [--github-token <token>] [--no-start]
867
+ vantaloom update [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start]
774
868
  vantaloom update --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start]
775
- vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive]
869
+ vantaloom package [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package]
776
870
  vantaloom start [--prefix <dir>] [--component all|api|agent|web]
777
871
  vantaloom stop [--prefix <dir>] [--component all|api|agent|web]
778
872
  vantaloom restart [--prefix <dir>] [--component all|api|agent|web]