vercel-vm-factory 0.3.8 → 0.6.8

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 +5 -5
  2. package/deploy-vm.mjs +82 -25
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Vercel VM Factory
2
2
 
3
- Create a tiny Vercel Container deployment: copy `wsterm` from `ghcr.io/v1xingyue/ws-shell:v1.8.alpine` into a selected base image, then deploy with Vercel CLI.
3
+ Create a tiny Vercel Container deployment: copy `wsterm` from `ghcr.io/v1xingyue/ws-shell:v1.8.alpine` into a selected VM image, then deploy with Vercel CLI.
4
4
 
5
5
  ```bash
6
6
  npx vercel-vm-factory create \
7
- --base ubuntu \
7
+ --vm-image ubuntu \
8
8
  --project ws-shell-ubuntu \
9
9
  --auth-mode basic \
10
10
  --auth-user admin \
@@ -50,16 +50,16 @@ CLI mapping:
50
50
  - Application Preset -> patched through Vercel API as `framework=container`
51
51
  - Root Directory -> generated project directory
52
52
 
53
- Base presets:
53
+ VM image presets:
54
54
 
55
55
  - `alpine` -> `alpine:3.23`
56
56
  - `ubuntu` -> `ubuntu:24.04`
57
57
  - `debian` -> `debian:13-slim`
58
58
 
59
- Custom base:
59
+ Custom VM image:
60
60
 
61
61
  ```bash
62
- npx vercel-vm-factory create --base fedora:42 --project ws-shell-fedora
62
+ npx vercel-vm-factory create --vm-image fedora:42 --project ws-shell-fedora
63
63
  ```
64
64
 
65
65
  Before deploying, set the GitHub OAuth callback URL to:
package/deploy-vm.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { spawn } from "node:child_process";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { stdin as input, stdout as output } from "node:process";
@@ -8,7 +8,7 @@ import path from "node:path";
8
8
 
9
9
  const defaultWsShellImage = "ghcr.io/v1xingyue/ws-shell:v1.8.alpine";
10
10
 
11
- const bases = {
11
+ const vmImages = {
12
12
  alpine: "alpine:3.23",
13
13
  ubuntu: "ubuntu:24.04",
14
14
  debian: "debian:13-slim",
@@ -20,8 +20,13 @@ const workspaceRoot = process.cwd();
20
20
  const stateRoot = path.join(homedir(), ".vercel-vm-factory");
21
21
  const defaultsPath = path.join(stateRoot, "defaults.json");
22
22
  const legacyDefaultsPath = path.join(scriptRoot, ".defaults.json");
23
- const defaults = {
23
+ const codeDefaults = { "vm-image": "alpine", from: defaultWsShellImage };
24
+ const packagedDefaults = {
24
25
  ...(await readDefaults(legacyDefaultsPath)),
26
+ ...codeDefaults,
27
+ };
28
+ let defaults = {
29
+ ...packagedDefaults,
25
30
  ...(await readDefaults(defaultsPath)),
26
31
  };
27
32
  const colorEnabled = output.isTTY && !process.env.NO_COLOR;
@@ -57,6 +62,7 @@ try {
57
62
 
58
63
  async function main() {
59
64
  printHeader();
65
+ defaults = await syncDefaults();
60
66
  await ensureVercelInstalled();
61
67
  await ensureVercelLogin();
62
68
 
@@ -65,8 +71,10 @@ async function main() {
65
71
  return;
66
72
  }
67
73
 
68
- const baseName = await chooseBase(args.base ?? defaults.base ?? "alpine");
69
- const baseImage = bases[baseName] ?? baseName;
74
+ const vmImageName = await chooseVmImage(
75
+ args["vm-image"] ?? args.base ?? defaults["vm-image"] ?? "alpine",
76
+ );
77
+ const vmImage = vmImages[vmImageName] ?? vmImageName;
70
78
  const scope = await optionalValue(
71
79
  "scope",
72
80
  "Vercel team/scope",
@@ -75,7 +83,7 @@ async function main() {
75
83
  const project = await value(
76
84
  "project",
77
85
  "Vercel project name",
78
- defaults.project ?? `ws-shell-${baseName}`,
86
+ defaults.project ?? `ws-shell-${vmImageName}`,
79
87
  );
80
88
  const wsShellImage =
81
89
  args.from ??
@@ -99,14 +107,14 @@ async function main() {
99
107
  ".generated",
100
108
  project,
101
109
  );
102
- const dockerfile = makeDockerfile({ baseImage, wsShellImage });
110
+ const dockerfile = makeDockerfile({ vmImage, wsShellImage });
103
111
 
104
112
  await rm(appDir, { recursive: true, force: true });
105
113
  await mkdir(appDir, { recursive: true });
106
114
  await writeFile(path.join(appDir, "Dockerfile.vercel"), dockerfile);
107
115
 
108
116
  printSummary({
109
- base: `${baseName} -> ${baseImage}`,
117
+ "vm image": `${vmImageName} -> ${vmImage}`,
110
118
  project,
111
119
  scope: scope || "default",
112
120
  source: wsShellImage,
@@ -161,7 +169,7 @@ async function main() {
161
169
 
162
170
  await writeDefaults(defaultsPath, {
163
171
  ...defaults,
164
- base: baseName,
172
+ "vm-image": vmImageName,
165
173
  scope: scope || undefined,
166
174
  project,
167
175
  from: wsShellImage,
@@ -307,7 +315,7 @@ async function doctor() {
307
315
  console.log("");
308
316
  console.log(color.bold("Saved defaults"));
309
317
  printKeyValue("defaults file", defaultsPath);
310
- printKeyValue("base", defaults.base || "not set");
318
+ printKeyValue("vm image", defaults["vm-image"] || "not set");
311
319
  printKeyValue("project", defaults.project || "not set");
312
320
  printKeyValue("scope", defaults.scope || "not set");
313
321
  printKeyValue("source image", defaults.from || defaultWsShellImage);
@@ -334,12 +342,12 @@ async function doctor() {
334
342
  );
335
343
  }
336
344
 
337
- function makeDockerfile({ baseImage, wsShellImage }) {
345
+ function makeDockerfile({ vmImage, wsShellImage }) {
338
346
  return `ARG WS_SHELL_IMAGE=${wsShellImage}
339
- ARG BASE_IMAGE=${baseImage}
347
+ ARG VM_IMAGE=${vmImage}
340
348
 
341
349
  FROM \${WS_SHELL_IMAGE} AS ws-shell
342
- FROM \${BASE_IMAGE} AS base
350
+ FROM \${VM_IMAGE} AS vm
343
351
 
344
352
  # wsterm already embeds the web UI; runtime config comes from environment variables.
345
353
  COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
@@ -443,28 +451,33 @@ function usesGitHubAuth(mode) {
443
451
  return mode === "github" || mode === "both";
444
452
  }
445
453
 
446
- async function chooseBase(fallback) {
454
+ async function chooseVmImage(fallback) {
455
+ if (args["vm-image"]) return args["vm-image"];
447
456
  if (args.base) return args.base;
448
457
  if (!input.isTTY) return fallback;
449
458
 
450
- const names = Object.keys(bases);
451
- console.log(color.bold("Choose base image"));
459
+ const names = Object.keys(vmImages);
460
+ console.log(color.bold("Choose VM image"));
452
461
  names.forEach((name, index) =>
453
462
  console.log(
454
- ` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${bases[name]})`)}`,
463
+ ` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${vmImages[name]})`)}`,
455
464
  ),
456
465
  );
457
466
  console.log(` ${color.cyan("4")}. custom image`);
458
467
 
459
468
  const rl = createInterface({ input, output });
460
- const answer = (await rl.question(`Base [${fallback}]: `)).trim();
469
+ const answer = (await rl.question(`VM image [${fallback}]: `)).trim();
461
470
  rl.close();
462
471
 
463
472
  if (!answer) return fallback;
464
- if (bases[answer]) return answer;
473
+ if (vmImages[answer]) return answer;
465
474
  if (/^[1-3]$/.test(answer)) return names[Number(answer) - 1];
466
475
  if (answer === "4")
467
- return value("custom-base", "Custom base image", defaults["custom-base"]);
476
+ return value(
477
+ "custom-vm-image",
478
+ "Custom VM image",
479
+ defaults["custom-vm-image"],
480
+ );
468
481
  return answer;
469
482
  }
470
483
 
@@ -476,14 +489,57 @@ async function readDefaults(file) {
476
489
  }
477
490
  }
478
491
 
479
- async function writeDefaults(file, data) {
480
- const clean = Object.fromEntries(
481
- Object.entries(data).filter(([, value]) => value),
492
+ async function syncDefaults() {
493
+ const current = await readDefaults(defaultsPath);
494
+ const codeMtime = Math.max(
495
+ await readMtime(import.meta.filename),
496
+ await readMtime(legacyDefaultsPath),
482
497
  );
498
+ const homeMtime = await readMtime(defaultsPath);
499
+ const latest =
500
+ homeMtime > codeMtime
501
+ ? { ...packagedDefaults, ...current }
502
+ : { ...current, ...packagedDefaults };
503
+
504
+ if (
505
+ JSON.stringify(cleanDefaults(current)) !==
506
+ JSON.stringify(cleanDefaults(latest))
507
+ )
508
+ await writeDefaults(defaultsPath, latest);
509
+ return cleanDefaults(latest);
510
+ }
511
+
512
+ async function readMtime(file) {
513
+ try {
514
+ return (await stat(file)).mtimeMs;
515
+ } catch {
516
+ return 0;
517
+ }
518
+ }
519
+
520
+ async function writeDefaults(file, data) {
521
+ const clean = cleanDefaults(data);
483
522
  await mkdir(path.dirname(file), { recursive: true });
484
523
  await writeFile(file, `${JSON.stringify(clean, null, 2)}\n`);
485
524
  }
486
525
 
526
+ function cleanDefaults(data) {
527
+ const migrated = {
528
+ ...data,
529
+ "vm-image": data["vm-image"] ?? data.base,
530
+ "custom-vm-image": data["custom-vm-image"] ?? data["custom-base"],
531
+ };
532
+ delete migrated.base;
533
+ delete migrated["custom-base"];
534
+
535
+ const clean = Object.fromEntries(
536
+ Object.entries(migrated).filter(([, value]) => value),
537
+ );
538
+ return Object.fromEntries(
539
+ Object.entries(clean).sort(([a], [b]) => a.localeCompare(b)),
540
+ );
541
+ }
542
+
487
543
  function mask(value) {
488
544
  if (value.length <= 8) return "********";
489
545
  return `${value.slice(0, 4)}...${value.slice(-4)}`;
@@ -640,12 +696,13 @@ function printHelp() {
640
696
  printHeader();
641
697
  console.log(`Usage:
642
698
  vercel-vm-factory create
643
- vercel-vm-factory create --base ubuntu --project x-shell
699
+ vercel-vm-factory create --vm-image ubuntu --project x-shell
644
700
  vercel-vm-factory doctor
645
701
  npx vercel-vm-factory create
646
702
 
647
703
  Options:
648
- --base NAME alpine, ubuntu, debian, or a custom image
704
+ --vm-image NAME alpine, ubuntu, debian, or a custom VM image
705
+ --base NAME Alias for --vm-image
649
706
  --project NAME Vercel project name
650
707
  --scope SLUG Optional Vercel team/user scope slug
651
708
  --from IMAGE Source image for /app/bin/wsterm
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vercel-vm-factory",
3
- "version": "0.3.8",
4
- "description": "Create Vercel Container deployments for ws-shell from selectable base images.",
3
+ "version": "0.6.8",
4
+ "description": "Create Vercel Container deployments for ws-shell from selectable VM images.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {