vercel-vm-factory 0.5.8 → 0.7.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 +12 -5
  2. package/deploy-vm.mjs +131 -61
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,10 +1,11 @@
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
+ --shell /bin/bash \
8
9
  --project ws-shell-ubuntu \
9
10
  --auth-mode basic \
10
11
  --auth-user admin \
@@ -50,16 +51,22 @@ CLI mapping:
50
51
  - Application Preset -> patched through Vercel API as `framework=container`
51
52
  - Root Directory -> generated project directory
52
53
 
53
- Base presets:
54
+ VM image presets:
54
55
 
55
56
  - `alpine` -> `alpine:3.23`
56
57
  - `ubuntu` -> `ubuntu:24.04`
57
58
  - `debian` -> `debian:13-slim`
58
59
 
59
- Custom base:
60
+ Shell options:
61
+
62
+ - `/bin/bash`
63
+ - `/bin/zsh`
64
+ - `/bin/sh`
65
+
66
+ Custom VM image:
60
67
 
61
68
  ```bash
62
- npx vercel-vm-factory create --base fedora:42 --project ws-shell-fedora
69
+ npx vercel-vm-factory create --vm-image fedora:42 --project ws-shell-fedora
63
70
  ```
64
71
 
65
72
  Before deploying, set the GitHub OAuth callback URL to:
package/deploy-vm.mjs CHANGED
@@ -8,11 +8,12 @@ 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",
15
15
  };
16
+ const shells = ["/bin/bash", "/bin/zsh", "/bin/sh"];
16
17
 
17
18
  const { command, args } = parseCommand(process.argv.slice(2));
18
19
  const scriptRoot = path.resolve(import.meta.dirname);
@@ -20,7 +21,11 @@ const workspaceRoot = process.cwd();
20
21
  const stateRoot = path.join(homedir(), ".vercel-vm-factory");
21
22
  const defaultsPath = path.join(stateRoot, "defaults.json");
22
23
  const legacyDefaultsPath = path.join(scriptRoot, ".defaults.json");
23
- const codeDefaults = { base: "alpine", from: defaultWsShellImage };
24
+ const codeDefaults = {
25
+ "vm-image": "alpine",
26
+ from: defaultWsShellImage,
27
+ shell: "/bin/sh",
28
+ };
24
29
  const packagedDefaults = {
25
30
  ...(await readDefaults(legacyDefaultsPath)),
26
31
  ...codeDefaults,
@@ -71,8 +76,10 @@ async function main() {
71
76
  return;
72
77
  }
73
78
 
74
- const baseName = await chooseBase(args.base ?? defaults.base ?? "alpine");
75
- const baseImage = bases[baseName] ?? baseName;
79
+ const vmImageName = await chooseVmImage(
80
+ args["vm-image"] ?? args.base ?? defaults["vm-image"] ?? "alpine",
81
+ );
82
+ const vmImage = vmImages[vmImageName] ?? vmImageName;
76
83
  const scope = await optionalValue(
77
84
  "scope",
78
85
  "Vercel team/scope",
@@ -81,13 +88,14 @@ async function main() {
81
88
  const project = await value(
82
89
  "project",
83
90
  "Vercel project name",
84
- defaults.project ?? `ws-shell-${baseName}`,
91
+ defaults.project ?? `ws-shell-${vmImageName}`,
85
92
  );
86
93
  const wsShellImage =
87
94
  args.from ??
88
95
  process.env.WS_SHELL_IMAGE ??
89
96
  defaults.from ??
90
97
  defaultWsShellImage;
98
+ const shell = await chooseShell(args.shell ?? defaults.shell ?? "/bin/sh");
91
99
  const prod = args.prod !== "false";
92
100
  const dryRun = Boolean(args["dry-run"]);
93
101
  const skipLink = Boolean(args["skip-link"]);
@@ -105,17 +113,18 @@ async function main() {
105
113
  ".generated",
106
114
  project,
107
115
  );
108
- const dockerfile = makeDockerfile({ baseImage, wsShellImage });
116
+ const dockerfile = makeDockerfile({ shell, vmImage, wsShellImage });
109
117
 
110
118
  await rm(appDir, { recursive: true, force: true });
111
119
  await mkdir(appDir, { recursive: true });
112
120
  await writeFile(path.join(appDir, "Dockerfile.vercel"), dockerfile);
113
121
 
114
122
  printSummary({
115
- base: `${baseName} -> ${baseImage}`,
123
+ "vm image": `${vmImageName} -> ${vmImage}`,
116
124
  project,
117
125
  scope: scope || "default",
118
126
  source: wsShellImage,
127
+ shell,
119
128
  auth: authMode,
120
129
  ...(usesGitHubAuth(authMode) ? { callback: oauthRedirectUrl } : {}),
121
130
  dockerfile: path.join(appDir, "Dockerfile.vercel"),
@@ -167,10 +176,11 @@ async function main() {
167
176
 
168
177
  await writeDefaults(defaultsPath, {
169
178
  ...defaults,
170
- base: baseName,
179
+ "vm-image": vmImageName,
171
180
  scope: scope || undefined,
172
181
  project,
173
182
  from: wsShellImage,
183
+ shell,
174
184
  "auth-mode": authMode,
175
185
  "auth-user": authUsername,
176
186
  "auth-password": authPassword,
@@ -270,15 +280,12 @@ async function installVercelCli() {
270
280
  );
271
281
 
272
282
  const install = await choosePackageInstall();
273
- const rl = createInterface({ input, output });
274
283
  const answer = (
275
- await rl.question(
276
- `Vercel CLI is not installed. Install it with "${install.command} ${install.args.join(" ")}"? [y/N]: `,
284
+ await askText(
285
+ `Vercel CLI is not installed. Install with ${install.command} ${install.args.join(" ")}?`,
286
+ "N",
277
287
  )
278
- )
279
- .trim()
280
- .toLowerCase();
281
- rl.close();
288
+ ).toLowerCase();
282
289
 
283
290
  if (answer !== "y" && answer !== "yes")
284
291
  throw new Error("Vercel CLI is required. Exiting.");
@@ -313,10 +320,11 @@ async function doctor() {
313
320
  console.log("");
314
321
  console.log(color.bold("Saved defaults"));
315
322
  printKeyValue("defaults file", defaultsPath);
316
- printKeyValue("base", defaults.base || "not set");
323
+ printKeyValue("vm image", defaults["vm-image"] || "not set");
317
324
  printKeyValue("project", defaults.project || "not set");
318
325
  printKeyValue("scope", defaults.scope || "not set");
319
326
  printKeyValue("source image", defaults.from || defaultWsShellImage);
327
+ printKeyValue("shell", defaults.shell || "/bin/sh");
320
328
  printKeyValue("auth mode", defaults["auth-mode"] || "not set");
321
329
  printKeyValue(
322
330
  "auth user",
@@ -340,12 +348,12 @@ async function doctor() {
340
348
  );
341
349
  }
342
350
 
343
- function makeDockerfile({ baseImage, wsShellImage }) {
351
+ function makeDockerfile({ shell, vmImage, wsShellImage }) {
344
352
  return `ARG WS_SHELL_IMAGE=${wsShellImage}
345
- ARG BASE_IMAGE=${baseImage}
353
+ ARG VM_IMAGE=${vmImage}
346
354
 
347
355
  FROM \${WS_SHELL_IMAGE} AS ws-shell
348
- FROM \${BASE_IMAGE} AS base
356
+ FROM \${VM_IMAGE} AS vm
349
357
 
350
358
  # wsterm already embeds the web UI; runtime config comes from environment variables.
351
359
  COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
@@ -353,7 +361,7 @@ COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
353
361
  WORKDIR /app
354
362
  ENV ENABLE_SSL=false
355
363
  EXPOSE 80
356
- CMD ["/app/bin/wsterm","-bind",":80","-fork","/bin/sh"]
364
+ CMD ["/app/bin/wsterm","-bind",":80","-fork","${shell}"]
357
365
  `;
358
366
  }
359
367
 
@@ -362,11 +370,7 @@ async function value(name, question, fallback) {
362
370
  if (args[name]) return args[name];
363
371
  if (!input.isTTY) return current || "";
364
372
 
365
- const rl = createInterface({ input, output });
366
- const answer = (
367
- await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)
368
- ).trim();
369
- rl.close();
373
+ const answer = await askText(question, fallback);
370
374
  return answer || current || "";
371
375
  }
372
376
 
@@ -374,10 +378,8 @@ async function secret(name, question, fallback) {
374
378
  if (args[name]) return args[name];
375
379
  if (!input.isTTY) return fallback || "";
376
380
 
377
- const rl = createInterface({ input, output });
378
381
  const placeholder = fallback ? mask(fallback) : "skip";
379
- const answer = (await rl.question(`${question} [${placeholder}]: `)).trim();
380
- rl.close();
382
+ const answer = await askText(question, placeholder);
381
383
  return answer || fallback || "";
382
384
  }
383
385
 
@@ -385,10 +387,10 @@ async function optionalValue(name, question, fallback) {
385
387
  if (args[name] !== undefined) return args[name];
386
388
  if (!input.isTTY) return "";
387
389
 
388
- const rl = createInterface({ input, output });
389
- const suffix = fallback ? ` [${fallback}; Enter to skip]` : " [skip]";
390
- const answer = (await rl.question(`${question}${suffix}: `)).trim();
391
- rl.close();
390
+ const answer = await askText(
391
+ question,
392
+ fallback ? `${fallback}; Enter to skip` : "skip",
393
+ );
392
394
  return answer;
393
395
  }
394
396
 
@@ -422,15 +424,14 @@ async function chooseAuthMode(fallback) {
422
424
  }
423
425
  if (!input.isTTY) return modes.has(fallback) ? fallback : "basic";
424
426
 
425
- console.log(color.bold("Choose authentication"));
426
- console.log(` ${color.cyan("1")}. basic username/password`);
427
- console.log(` ${color.cyan("2")}. GitHub OAuth`);
428
- console.log(` ${color.cyan("3")}. both`);
429
- console.log(` ${color.cyan("4")}. none`);
427
+ printChoices("Authentication", [
428
+ ["1", "basic", "username/password"],
429
+ ["2", "github", "GitHub OAuth"],
430
+ ["3", "both", "basic + GitHub OAuth"],
431
+ ["4", "none", "no app auth"],
432
+ ]);
430
433
 
431
- const rl = createInterface({ input, output });
432
- const answer = (await rl.question(`Authentication [${fallback}]: `)).trim();
433
- rl.close();
434
+ const answer = await askText("Authentication", fallback);
434
435
 
435
436
  if (!answer) return modes.has(fallback) ? fallback : "basic";
436
437
  if (modes.has(answer)) return answer;
@@ -449,28 +450,53 @@ function usesGitHubAuth(mode) {
449
450
  return mode === "github" || mode === "both";
450
451
  }
451
452
 
452
- async function chooseBase(fallback) {
453
+ async function chooseVmImage(fallback) {
454
+ if (args["vm-image"]) return args["vm-image"];
453
455
  if (args.base) return args.base;
454
456
  if (!input.isTTY) return fallback;
455
457
 
456
- const names = Object.keys(bases);
457
- console.log(color.bold("Choose base image"));
458
- names.forEach((name, index) =>
459
- console.log(
460
- ` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${bases[name]})`)}`,
461
- ),
458
+ const names = Object.keys(vmImages);
459
+ printChoices(
460
+ "VM image",
461
+ names.map((name, index) => [
462
+ String(index + 1),
463
+ name,
464
+ vmImages[name],
465
+ ]).concat([["4", "custom", "enter a full image name"]]),
462
466
  );
463
- console.log(` ${color.cyan("4")}. custom image`);
464
467
 
465
- const rl = createInterface({ input, output });
466
- const answer = (await rl.question(`Base [${fallback}]: `)).trim();
467
- rl.close();
468
+ const answer = await askText("VM image", fallback);
468
469
 
469
470
  if (!answer) return fallback;
470
- if (bases[answer]) return answer;
471
+ if (vmImages[answer]) return answer;
471
472
  if (/^[1-3]$/.test(answer)) return names[Number(answer) - 1];
472
473
  if (answer === "4")
473
- return value("custom-base", "Custom base image", defaults["custom-base"]);
474
+ return value(
475
+ "custom-vm-image",
476
+ "Custom VM image",
477
+ defaults["custom-vm-image"],
478
+ );
479
+ return answer;
480
+ }
481
+
482
+ async function chooseShell(fallback) {
483
+ if (args.shell) return args.shell;
484
+ if (!input.isTTY) return fallback;
485
+
486
+ printChoices(
487
+ "Shell",
488
+ shells.map((shell, index) => [
489
+ String(index + 1),
490
+ shell,
491
+ index === shells.length - 1 ? "default" : "",
492
+ ]),
493
+ );
494
+
495
+ const answer = await askText("Shell", fallback);
496
+
497
+ if (!answer) return fallback;
498
+ if (shells.includes(answer)) return answer;
499
+ if (/^[1-3]$/.test(answer)) return shells[Number(answer) - 1];
474
500
  return answer;
475
501
  }
476
502
 
@@ -517,8 +543,16 @@ async function writeDefaults(file, data) {
517
543
  }
518
544
 
519
545
  function cleanDefaults(data) {
546
+ const migrated = {
547
+ ...data,
548
+ "vm-image": data["vm-image"] ?? data.base,
549
+ "custom-vm-image": data["custom-vm-image"] ?? data["custom-base"],
550
+ };
551
+ delete migrated.base;
552
+ delete migrated["custom-base"];
553
+
520
554
  const clean = Object.fromEntries(
521
- Object.entries(data).filter(([, value]) => value),
555
+ Object.entries(migrated).filter(([, value]) => value),
522
556
  );
523
557
  return Object.fromEntries(
524
558
  Object.entries(clean).sort(([a], [b]) => a.localeCompare(b)),
@@ -623,18 +657,30 @@ function findLastUrl(text) {
623
657
  }
624
658
 
625
659
  function printHeader() {
626
- console.log(color.bold(color.cyan("Vercel VM Factory")));
627
- console.log(
628
- color.dim("Build a Container deployment from a tiny Dockerfile.vercel"),
629
- );
660
+ console.log("");
661
+ console.log(color.cyan(" __ ____ __ _____ _ "));
662
+ console.log(color.cyan(" \\ \\ / / \\/ | | ___|_ _ ___| |_ ___ _ __ _ _ "));
663
+ console.log(color.cyan(" \\ \\ / /| |\\/| | | |_ / _` |/ __| __/ _ \\| '__| | | |"));
664
+ console.log(color.cyan(" \\ V / | | | | | _| (_| | (__| || (_) | | | |_| |"));
665
+ console.log(color.cyan(" \\_/ |_| |_| |_| \\__,_|\\___|\\__\\___/|_| \\__, |"));
666
+ console.log(color.cyan(" |___/ "));
667
+ console.log(color.dim(" Vercel Container VM deployment helper"));
630
668
  console.log("");
631
669
  }
632
670
 
633
671
  function printSummary(items) {
672
+ const entries = Object.entries(items);
673
+ const keyWidth = Math.max(...entries.map(([key]) => key.length), 1);
674
+ const valueWidth = Math.max(...entries.map(([, value]) => String(value).length), 1);
675
+ const width = keyWidth + valueWidth + 5;
676
+
634
677
  console.log(color.bold("Deployment plan"));
678
+ console.log(color.cyan(`+${"-".repeat(width)}+`));
635
679
  for (const [key, value] of Object.entries(items)) {
636
- printKeyValue(key, value);
680
+ const line = `${key.padEnd(keyWidth)} ${String(value).padEnd(valueWidth)}`;
681
+ console.log(`${color.cyan("|")} ${line} ${color.cyan("|")}`);
637
682
  }
683
+ console.log(color.cyan(`+${"-".repeat(width)}+`));
638
684
  console.log("");
639
685
  }
640
686
 
@@ -661,6 +707,28 @@ function printKeyValue(key, value) {
661
707
  console.log(`${color.dim(`${key.padEnd(14)} `)}${value}`);
662
708
  }
663
709
 
710
+ async function askText(question, fallback = "") {
711
+ const suffix = fallback ? color.dim(` [${fallback}]`) : "";
712
+ const rl = createInterface({ input, output });
713
+ const answer = (
714
+ await rl.question(
715
+ `${color.cyan("?")} ${color.bold(question)}${suffix}\n${color.dim("> ")} `,
716
+ )
717
+ ).trim();
718
+ rl.close();
719
+ return answer;
720
+ }
721
+
722
+ function printChoices(title, choices) {
723
+ console.log("");
724
+ console.log(color.bold(title));
725
+ for (const [key, label, detail] of choices) {
726
+ const hint = detail ? ` ${color.dim(detail)}` : "";
727
+ console.log(` ${color.cyan(key)} ${label}${hint}`);
728
+ }
729
+ console.log("");
730
+ }
731
+
664
732
  function step(text) {
665
733
  console.log(`${color.cyan("->")} ${text}`);
666
734
  }
@@ -681,15 +749,17 @@ function printHelp() {
681
749
  printHeader();
682
750
  console.log(`Usage:
683
751
  vercel-vm-factory create
684
- vercel-vm-factory create --base ubuntu --project x-shell
752
+ vercel-vm-factory create --vm-image ubuntu --project x-shell
685
753
  vercel-vm-factory doctor
686
754
  npx vercel-vm-factory create
687
755
 
688
756
  Options:
689
- --base NAME alpine, ubuntu, debian, or a custom image
757
+ --vm-image NAME alpine, ubuntu, debian, or a custom VM image
758
+ --base NAME Alias for --vm-image
690
759
  --project NAME Vercel project name
691
760
  --scope SLUG Optional Vercel team/user scope slug
692
761
  --from IMAGE Source image for /app/bin/wsterm
762
+ --shell PATH /bin/bash, /bin/zsh, or /bin/sh
693
763
  --auth-mode MODE basic, github, both, or none
694
764
  --auth-user VALUE Username/password auth user
695
765
  --auth-password VAL Username/password auth password
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vercel-vm-factory",
3
- "version": "0.5.8",
4
- "description": "Create Vercel Container deployments for ws-shell from selectable base images.",
3
+ "version": "0.7.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": {