vercel-vm-factory 0.6.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 +7 -0
  2. package/deploy-vm.mjs +99 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,6 +5,7 @@ Create a tiny Vercel Container deployment: copy `wsterm` from `ghcr.io/v1xingyue
5
5
  ```bash
6
6
  npx vercel-vm-factory create \
7
7
  --vm-image ubuntu \
8
+ --shell /bin/bash \
8
9
  --project ws-shell-ubuntu \
9
10
  --auth-mode basic \
10
11
  --auth-user admin \
@@ -56,6 +57,12 @@ VM image presets:
56
57
  - `ubuntu` -> `ubuntu:24.04`
57
58
  - `debian` -> `debian:13-slim`
58
59
 
60
+ Shell options:
61
+
62
+ - `/bin/bash`
63
+ - `/bin/zsh`
64
+ - `/bin/sh`
65
+
59
66
  Custom VM image:
60
67
 
61
68
  ```bash
package/deploy-vm.mjs CHANGED
@@ -13,6 +13,7 @@ const vmImages = {
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 = { "vm-image": "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,
@@ -90,6 +95,7 @@ async function main() {
90
95
  process.env.WS_SHELL_IMAGE ??
91
96
  defaults.from ??
92
97
  defaultWsShellImage;
98
+ const shell = await chooseShell(args.shell ?? defaults.shell ?? "/bin/sh");
93
99
  const prod = args.prod !== "false";
94
100
  const dryRun = Boolean(args["dry-run"]);
95
101
  const skipLink = Boolean(args["skip-link"]);
@@ -107,7 +113,7 @@ async function main() {
107
113
  ".generated",
108
114
  project,
109
115
  );
110
- const dockerfile = makeDockerfile({ vmImage, wsShellImage });
116
+ const dockerfile = makeDockerfile({ shell, vmImage, wsShellImage });
111
117
 
112
118
  await rm(appDir, { recursive: true, force: true });
113
119
  await mkdir(appDir, { recursive: true });
@@ -118,6 +124,7 @@ async function main() {
118
124
  project,
119
125
  scope: scope || "default",
120
126
  source: wsShellImage,
127
+ shell,
121
128
  auth: authMode,
122
129
  ...(usesGitHubAuth(authMode) ? { callback: oauthRedirectUrl } : {}),
123
130
  dockerfile: path.join(appDir, "Dockerfile.vercel"),
@@ -173,6 +180,7 @@ async function main() {
173
180
  scope: scope || undefined,
174
181
  project,
175
182
  from: wsShellImage,
183
+ shell,
176
184
  "auth-mode": authMode,
177
185
  "auth-user": authUsername,
178
186
  "auth-password": authPassword,
@@ -272,15 +280,12 @@ async function installVercelCli() {
272
280
  );
273
281
 
274
282
  const install = await choosePackageInstall();
275
- const rl = createInterface({ input, output });
276
283
  const answer = (
277
- await rl.question(
278
- `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",
279
287
  )
280
- )
281
- .trim()
282
- .toLowerCase();
283
- rl.close();
288
+ ).toLowerCase();
284
289
 
285
290
  if (answer !== "y" && answer !== "yes")
286
291
  throw new Error("Vercel CLI is required. Exiting.");
@@ -319,6 +324,7 @@ async function doctor() {
319
324
  printKeyValue("project", defaults.project || "not set");
320
325
  printKeyValue("scope", defaults.scope || "not set");
321
326
  printKeyValue("source image", defaults.from || defaultWsShellImage);
327
+ printKeyValue("shell", defaults.shell || "/bin/sh");
322
328
  printKeyValue("auth mode", defaults["auth-mode"] || "not set");
323
329
  printKeyValue(
324
330
  "auth user",
@@ -342,7 +348,7 @@ async function doctor() {
342
348
  );
343
349
  }
344
350
 
345
- function makeDockerfile({ vmImage, wsShellImage }) {
351
+ function makeDockerfile({ shell, vmImage, wsShellImage }) {
346
352
  return `ARG WS_SHELL_IMAGE=${wsShellImage}
347
353
  ARG VM_IMAGE=${vmImage}
348
354
 
@@ -355,7 +361,7 @@ COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
355
361
  WORKDIR /app
356
362
  ENV ENABLE_SSL=false
357
363
  EXPOSE 80
358
- CMD ["/app/bin/wsterm","-bind",":80","-fork","/bin/sh"]
364
+ CMD ["/app/bin/wsterm","-bind",":80","-fork","${shell}"]
359
365
  `;
360
366
  }
361
367
 
@@ -364,11 +370,7 @@ async function value(name, question, fallback) {
364
370
  if (args[name]) return args[name];
365
371
  if (!input.isTTY) return current || "";
366
372
 
367
- const rl = createInterface({ input, output });
368
- const answer = (
369
- await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)
370
- ).trim();
371
- rl.close();
373
+ const answer = await askText(question, fallback);
372
374
  return answer || current || "";
373
375
  }
374
376
 
@@ -376,10 +378,8 @@ async function secret(name, question, fallback) {
376
378
  if (args[name]) return args[name];
377
379
  if (!input.isTTY) return fallback || "";
378
380
 
379
- const rl = createInterface({ input, output });
380
381
  const placeholder = fallback ? mask(fallback) : "skip";
381
- const answer = (await rl.question(`${question} [${placeholder}]: `)).trim();
382
- rl.close();
382
+ const answer = await askText(question, placeholder);
383
383
  return answer || fallback || "";
384
384
  }
385
385
 
@@ -387,10 +387,10 @@ async function optionalValue(name, question, fallback) {
387
387
  if (args[name] !== undefined) return args[name];
388
388
  if (!input.isTTY) return "";
389
389
 
390
- const rl = createInterface({ input, output });
391
- const suffix = fallback ? ` [${fallback}; Enter to skip]` : " [skip]";
392
- const answer = (await rl.question(`${question}${suffix}: `)).trim();
393
- rl.close();
390
+ const answer = await askText(
391
+ question,
392
+ fallback ? `${fallback}; Enter to skip` : "skip",
393
+ );
394
394
  return answer;
395
395
  }
396
396
 
@@ -424,15 +424,14 @@ async function chooseAuthMode(fallback) {
424
424
  }
425
425
  if (!input.isTTY) return modes.has(fallback) ? fallback : "basic";
426
426
 
427
- console.log(color.bold("Choose authentication"));
428
- console.log(` ${color.cyan("1")}. basic username/password`);
429
- console.log(` ${color.cyan("2")}. GitHub OAuth`);
430
- console.log(` ${color.cyan("3")}. both`);
431
- 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
+ ]);
432
433
 
433
- const rl = createInterface({ input, output });
434
- const answer = (await rl.question(`Authentication [${fallback}]: `)).trim();
435
- rl.close();
434
+ const answer = await askText("Authentication", fallback);
436
435
 
437
436
  if (!answer) return modes.has(fallback) ? fallback : "basic";
438
437
  if (modes.has(answer)) return answer;
@@ -457,17 +456,16 @@ async function chooseVmImage(fallback) {
457
456
  if (!input.isTTY) return fallback;
458
457
 
459
458
  const names = Object.keys(vmImages);
460
- console.log(color.bold("Choose VM image"));
461
- names.forEach((name, index) =>
462
- console.log(
463
- ` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${vmImages[name]})`)}`,
464
- ),
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"]]),
465
466
  );
466
- console.log(` ${color.cyan("4")}. custom image`);
467
467
 
468
- const rl = createInterface({ input, output });
469
- const answer = (await rl.question(`VM image [${fallback}]: `)).trim();
470
- rl.close();
468
+ const answer = await askText("VM image", fallback);
471
469
 
472
470
  if (!answer) return fallback;
473
471
  if (vmImages[answer]) return answer;
@@ -481,6 +479,27 @@ async function chooseVmImage(fallback) {
481
479
  return answer;
482
480
  }
483
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];
500
+ return answer;
501
+ }
502
+
484
503
  async function readDefaults(file) {
485
504
  try {
486
505
  return JSON.parse(await readFile(file, "utf8"));
@@ -638,18 +657,30 @@ function findLastUrl(text) {
638
657
  }
639
658
 
640
659
  function printHeader() {
641
- console.log(color.bold(color.cyan("Vercel VM Factory")));
642
- console.log(
643
- color.dim("Build a Container deployment from a tiny Dockerfile.vercel"),
644
- );
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"));
645
668
  console.log("");
646
669
  }
647
670
 
648
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
+
649
677
  console.log(color.bold("Deployment plan"));
678
+ console.log(color.cyan(`+${"-".repeat(width)}+`));
650
679
  for (const [key, value] of Object.entries(items)) {
651
- printKeyValue(key, value);
680
+ const line = `${key.padEnd(keyWidth)} ${String(value).padEnd(valueWidth)}`;
681
+ console.log(`${color.cyan("|")} ${line} ${color.cyan("|")}`);
652
682
  }
683
+ console.log(color.cyan(`+${"-".repeat(width)}+`));
653
684
  console.log("");
654
685
  }
655
686
 
@@ -676,6 +707,28 @@ function printKeyValue(key, value) {
676
707
  console.log(`${color.dim(`${key.padEnd(14)} `)}${value}`);
677
708
  }
678
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
+
679
732
  function step(text) {
680
733
  console.log(`${color.cyan("->")} ${text}`);
681
734
  }
@@ -706,6 +759,7 @@ Options:
706
759
  --project NAME Vercel project name
707
760
  --scope SLUG Optional Vercel team/user scope slug
708
761
  --from IMAGE Source image for /app/bin/wsterm
762
+ --shell PATH /bin/bash, /bin/zsh, or /bin/sh
709
763
  --auth-mode MODE basic, github, both, or none
710
764
  --auth-user VALUE Username/password auth user
711
765
  --auth-password VAL Username/password auth password
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vercel-vm-factory",
3
- "version": "0.6.8",
3
+ "version": "0.7.8",
4
4
  "description": "Create Vercel Container deployments for ws-shell from selectable VM images.",
5
5
  "license": "MIT",
6
6
  "type": "module",