vercel-vm-factory 0.8.8 → 0.9.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 +34 -2
  2. package/deploy-vm.mjs +97 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,7 @@ Create a tiny Vercel Container deployment: copy `wsterm` from `ghcr.io/v1xingyue
6
6
  npx vercel-vm-factory create \
7
7
  --vm-image ubuntu \
8
8
  --shell /bin/bash \
9
+ --tools nodejs,codex \
9
10
  --project ws-shell-ubuntu \
10
11
  --auth-mode basic \
11
12
  --auth-user admin \
@@ -28,7 +29,7 @@ Run without flags for prompts:
28
29
  npx vercel-vm-factory create
29
30
  ```
30
31
 
31
- The prompt asks for authentication first: `basic`, `github`, `both`, or `none`, then only asks for the fields that mode needs.
32
+ The prompt walks through VM image, project, shell, optional preinstalled tools, and authentication. For list prompts, enter either names or numbers; tool choices can be comma-separated, for example `1,3` or `nodejs,claude-code`.
32
33
 
33
34
  Check local setup:
34
35
 
@@ -40,6 +41,16 @@ The script checks `vercel --version` and `vercel whoami`; if you are not logged
40
41
 
41
42
  Use `--help` to show all flags.
42
43
 
44
+ Common flags:
45
+
46
+ - `--vm-image alpine|ubuntu|debian|IMAGE`
47
+ - `--shell /bin/bash|/bin/zsh|/bin/sh`
48
+ - `--tools nodejs,codex,claude-code`
49
+ - `--project NAME`
50
+ - `--scope TEAM_SLUG`
51
+ - `--auth-mode basic|github|both|none`
52
+ - `--dry-run`
53
+
43
54
  Entered auth values are reused from `~/.vercel-vm-factory/defaults.json`; press Enter to keep the placeholder value or skip an empty one.
44
55
 
45
56
  The generated project contains only `Dockerfile.vercel`.
@@ -63,7 +74,28 @@ Shell options:
63
74
  - `/bin/zsh`
64
75
  - `/bin/sh`
65
76
 
66
- Choosing bash or zsh adds the matching package to the generated Dockerfile when the VM image does not already include it.
77
+ Choosing bash or zsh adds the matching package to the generated Dockerfile when the VM image does not already include it. Choosing zsh also installs oh-my-zsh.
78
+
79
+ Generated shell setup examples:
80
+
81
+ - Alpine + `/bin/zsh`: installs `zsh curl git`, then installs oh-my-zsh unattended.
82
+ - Ubuntu/Debian + `/bin/bash`: installs `bash` with `apt-get`.
83
+ - `/bin/sh`: no extra shell package is installed.
84
+
85
+ Preinstall tools:
86
+
87
+ - `nodejs`
88
+ - `codex`
89
+ - `claude-code`
90
+
91
+ Choosing codex or claude-code also installs Node.js/npm.
92
+
93
+ Generated tool setup examples:
94
+
95
+ - `--tools nodejs`: installs `nodejs npm`
96
+ - `--tools codex`: installs `nodejs npm`, then `npm install -g @openai/codex`
97
+ - `--tools claude-code`: installs `nodejs npm`, then `npm install -g @anthropic-ai/claude-code`
98
+ - `--tools codex,claude-code`: installs both CLIs
67
99
 
68
100
  Custom VM image:
69
101
 
package/deploy-vm.mjs CHANGED
@@ -14,6 +14,11 @@ const vmImages = {
14
14
  debian: "debian:13-slim",
15
15
  };
16
16
  const shells = ["/bin/bash", "/bin/zsh", "/bin/sh"];
17
+ const toolChoices = {
18
+ nodejs: "Node.js + npm",
19
+ codex: "OpenAI Codex CLI",
20
+ "claude-code": "Claude Code",
21
+ };
17
22
 
18
23
  const { command, args } = parseCommand(process.argv.slice(2));
19
24
  const scriptRoot = path.resolve(import.meta.dirname);
@@ -96,6 +101,7 @@ async function main() {
96
101
  defaults.from ??
97
102
  defaultWsShellImage;
98
103
  const shell = await chooseShell(args.shell ?? defaults.shell ?? "/bin/sh");
104
+ const tools = await chooseTools(args.tools ?? defaults.tools ?? "");
99
105
  const prod = args.prod !== "false";
100
106
  const dryRun = Boolean(args["dry-run"]);
101
107
  const skipLink = Boolean(args["skip-link"]);
@@ -115,6 +121,7 @@ async function main() {
115
121
  );
116
122
  const dockerfile = makeDockerfile({
117
123
  shell,
124
+ tools,
118
125
  vmImage,
119
126
  vmImageName,
120
127
  wsShellImage,
@@ -130,6 +137,7 @@ async function main() {
130
137
  scope: scope || "default",
131
138
  source: wsShellImage,
132
139
  shell,
140
+ tools: tools || "none",
133
141
  auth: authMode,
134
142
  ...(usesGitHubAuth(authMode) ? { callback: oauthRedirectUrl } : {}),
135
143
  dockerfile: path.join(appDir, "Dockerfile.vercel"),
@@ -186,6 +194,7 @@ async function main() {
186
194
  project,
187
195
  from: wsShellImage,
188
196
  shell,
197
+ tools,
189
198
  "auth-mode": authMode,
190
199
  "auth-user": authUsername,
191
200
  "auth-password": authPassword,
@@ -330,6 +339,7 @@ async function doctor() {
330
339
  printKeyValue("scope", defaults.scope || "not set");
331
340
  printKeyValue("source image", defaults.from || defaultWsShellImage);
332
341
  printKeyValue("shell", defaults.shell || "/bin/sh");
342
+ printKeyValue("tools", defaults.tools || "none");
333
343
  printKeyValue("auth mode", defaults["auth-mode"] || "not set");
334
344
  printKeyValue(
335
345
  "auth user",
@@ -353,14 +363,22 @@ async function doctor() {
353
363
  );
354
364
  }
355
365
 
356
- function makeDockerfile({ shell, vmImage, vmImageName, wsShellImage }) {
366
+ function makeDockerfile({ shell, tools, vmImage, vmImageName, wsShellImage }) {
357
367
  const shellInstall = makeShellInstall({ shell, vmImageName });
368
+ const ohMyZshInstall =
369
+ path.basename(shell) === "zsh"
370
+ ? `RUN RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended`
371
+ : "";
372
+ const toolInstall = makeToolInstall({ tools, vmImageName });
373
+ const shellSetup = [shellInstall, ohMyZshInstall, toolInstall]
374
+ .filter(Boolean)
375
+ .join("\n");
358
376
  return `ARG WS_SHELL_IMAGE=${wsShellImage}
359
377
  ARG VM_IMAGE=${vmImage}
360
378
 
361
379
  FROM \${WS_SHELL_IMAGE} AS ws-shell
362
380
  FROM \${VM_IMAGE} AS vm
363
- ${shellInstall ? `\n${shellInstall}\n` : ""}
381
+ ${shellSetup ? `\n${shellSetup}\n` : ""}
364
382
  # wsterm already embeds the web UI; runtime config comes from environment variables.
365
383
  COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
366
384
 
@@ -371,25 +389,64 @@ CMD ["/app/bin/wsterm","-bind",":80","-fork","${shell}"]
371
389
  `;
372
390
  }
373
391
 
392
+ function makeToolInstall({ tools, vmImageName }) {
393
+ const selected = new Set(parseTools(tools));
394
+ if (selected.has("codex") || selected.has("claude-code"))
395
+ selected.add("nodejs");
396
+ if (!selected.size) return "";
397
+
398
+ const packages = [];
399
+ if (selected.has("nodejs")) packages.push("nodejs", "npm");
400
+ const npmPackages = [];
401
+ if (selected.has("codex")) npmPackages.push("@openai/codex");
402
+ if (selected.has("claude-code")) npmPackages.push("@anthropic-ai/claude-code");
403
+
404
+ const installNode =
405
+ packages.length && vmImageName === "alpine"
406
+ ? `RUN apk add --no-cache ${packages.join(" ")}`
407
+ : packages.length && (vmImageName === "ubuntu" || vmImageName === "debian")
408
+ ? `RUN apt-get update \\
409
+ && apt-get install -y --no-install-recommends ${packages.join(" ")} \\
410
+ && rm -rf /var/lib/apt/lists/*`
411
+ : packages.length
412
+ ? `RUN if command -v apk >/dev/null 2>&1; then \\
413
+ apk add --no-cache ${packages.join(" ")}; \\
414
+ elif command -v apt-get >/dev/null 2>&1; then \\
415
+ apt-get update \\
416
+ && apt-get install -y --no-install-recommends ${packages.join(" ")} \\
417
+ && rm -rf /var/lib/apt/lists/*; \\
418
+ else \\
419
+ echo "Cannot install nodejs/npm: unsupported VM image package manager" >&2; \\
420
+ exit 1; \\
421
+ fi`
422
+ : "";
423
+
424
+ const installCli = npmPackages.length
425
+ ? `RUN npm install -g ${npmPackages.join(" ")}`
426
+ : "";
427
+ return [installNode, installCli].filter(Boolean).join("\n");
428
+ }
429
+
374
430
  function makeShellInstall({ shell, vmImageName }) {
375
431
  const packageName = path.basename(shell);
376
432
  if (packageName === "sh") return "";
433
+ const packages = packageName === "zsh" ? "zsh curl git" : packageName;
377
434
 
378
435
  if (vmImageName === "alpine")
379
- return `RUN apk add --no-cache ${packageName}`;
436
+ return `RUN apk add --no-cache ${packages}`;
380
437
 
381
438
  if (vmImageName === "ubuntu" || vmImageName === "debian")
382
439
  return `RUN apt-get update \\
383
- && apt-get install -y --no-install-recommends ${packageName} \\
440
+ && apt-get install -y --no-install-recommends ${packages} \\
384
441
  && rm -rf /var/lib/apt/lists/*`;
385
442
 
386
443
  return `RUN if command -v ${shell} >/dev/null 2>&1; then \\
387
444
  true; \\
388
445
  elif command -v apk >/dev/null 2>&1; then \\
389
- apk add --no-cache ${packageName}; \\
446
+ apk add --no-cache ${packages}; \\
390
447
  elif command -v apt-get >/dev/null 2>&1; then \\
391
448
  apt-get update \\
392
- && apt-get install -y --no-install-recommends ${packageName} \\
449
+ && apt-get install -y --no-install-recommends ${packages} \\
393
450
  && rm -rf /var/lib/apt/lists/*; \\
394
451
  else \\
395
452
  echo "Cannot install ${packageName}: unsupported VM image package manager" >&2; \\
@@ -532,6 +589,39 @@ async function chooseShell(fallback) {
532
589
  return answer;
533
590
  }
534
591
 
592
+ async function chooseTools(fallback) {
593
+ if (args.tools !== undefined) return parseTools(args.tools).join(",");
594
+ if (!input.isTTY) return parseTools(fallback).join(",");
595
+
596
+ const names = Object.keys(toolChoices);
597
+ printChoices(
598
+ "Preinstall tools",
599
+ names.map((name, index) => [
600
+ String(index + 1),
601
+ name,
602
+ toolChoices[name],
603
+ ]).concat([["0", "none", "default"]]),
604
+ );
605
+
606
+ const answer = await askText("Tools (comma separated)", fallback || "none");
607
+ return parseTools(answer || fallback).join(",");
608
+ }
609
+
610
+ function parseTools(value) {
611
+ const names = Object.keys(toolChoices);
612
+ const selected = String(value || "")
613
+ .split(",")
614
+ .map((item) => item.trim())
615
+ .filter(Boolean)
616
+ .flatMap((item) => {
617
+ if (item === "0" || item === "none") return [];
618
+ if (/^[1-3]$/.test(item)) return [names[Number(item) - 1]];
619
+ return [item];
620
+ })
621
+ .filter((item) => names.includes(item));
622
+ return [...new Set(selected)];
623
+ }
624
+
535
625
  async function readDefaults(file) {
536
626
  try {
537
627
  return JSON.parse(await readFile(file, "utf8"));
@@ -792,6 +882,7 @@ Options:
792
882
  --scope SLUG Optional Vercel team/user scope slug
793
883
  --from IMAGE Source image for /app/bin/wsterm
794
884
  --shell PATH /bin/bash, /bin/zsh, or /bin/sh
885
+ --tools LIST nodejs,codex,claude-code
795
886
  --auth-mode MODE basic, github, both, or none
796
887
  --auth-user VALUE Username/password auth user
797
888
  --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.8.8",
3
+ "version": "0.9.8",
4
4
  "description": "Create Vercel Container deployments for ws-shell from selectable VM images.",
5
5
  "license": "MIT",
6
6
  "type": "module",