vercel-vm-factory 0.7.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 +35 -1
  2. package/deploy-vm.mjs +126 -3
  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,6 +74,29 @@ Shell options:
63
74
  - `/bin/zsh`
64
75
  - `/bin/sh`
65
76
 
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
99
+
66
100
  Custom VM image:
67
101
 
68
102
  ```bash
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"]);
@@ -113,7 +119,13 @@ async function main() {
113
119
  ".generated",
114
120
  project,
115
121
  );
116
- const dockerfile = makeDockerfile({ shell, vmImage, wsShellImage });
122
+ const dockerfile = makeDockerfile({
123
+ shell,
124
+ tools,
125
+ vmImage,
126
+ vmImageName,
127
+ wsShellImage,
128
+ });
117
129
 
118
130
  await rm(appDir, { recursive: true, force: true });
119
131
  await mkdir(appDir, { recursive: true });
@@ -125,6 +137,7 @@ async function main() {
125
137
  scope: scope || "default",
126
138
  source: wsShellImage,
127
139
  shell,
140
+ tools: tools || "none",
128
141
  auth: authMode,
129
142
  ...(usesGitHubAuth(authMode) ? { callback: oauthRedirectUrl } : {}),
130
143
  dockerfile: path.join(appDir, "Dockerfile.vercel"),
@@ -181,6 +194,7 @@ async function main() {
181
194
  project,
182
195
  from: wsShellImage,
183
196
  shell,
197
+ tools,
184
198
  "auth-mode": authMode,
185
199
  "auth-user": authUsername,
186
200
  "auth-password": authPassword,
@@ -325,6 +339,7 @@ async function doctor() {
325
339
  printKeyValue("scope", defaults.scope || "not set");
326
340
  printKeyValue("source image", defaults.from || defaultWsShellImage);
327
341
  printKeyValue("shell", defaults.shell || "/bin/sh");
342
+ printKeyValue("tools", defaults.tools || "none");
328
343
  printKeyValue("auth mode", defaults["auth-mode"] || "not set");
329
344
  printKeyValue(
330
345
  "auth user",
@@ -348,13 +363,22 @@ async function doctor() {
348
363
  );
349
364
  }
350
365
 
351
- function makeDockerfile({ shell, vmImage, wsShellImage }) {
366
+ function makeDockerfile({ shell, tools, vmImage, vmImageName, wsShellImage }) {
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");
352
376
  return `ARG WS_SHELL_IMAGE=${wsShellImage}
353
377
  ARG VM_IMAGE=${vmImage}
354
378
 
355
379
  FROM \${WS_SHELL_IMAGE} AS ws-shell
356
380
  FROM \${VM_IMAGE} AS vm
357
-
381
+ ${shellSetup ? `\n${shellSetup}\n` : ""}
358
382
  # wsterm already embeds the web UI; runtime config comes from environment variables.
359
383
  COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
360
384
 
@@ -365,6 +389,71 @@ CMD ["/app/bin/wsterm","-bind",":80","-fork","${shell}"]
365
389
  `;
366
390
  }
367
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
+
430
+ function makeShellInstall({ shell, vmImageName }) {
431
+ const packageName = path.basename(shell);
432
+ if (packageName === "sh") return "";
433
+ const packages = packageName === "zsh" ? "zsh curl git" : packageName;
434
+
435
+ if (vmImageName === "alpine")
436
+ return `RUN apk add --no-cache ${packages}`;
437
+
438
+ if (vmImageName === "ubuntu" || vmImageName === "debian")
439
+ return `RUN apt-get update \\
440
+ && apt-get install -y --no-install-recommends ${packages} \\
441
+ && rm -rf /var/lib/apt/lists/*`;
442
+
443
+ return `RUN if command -v ${shell} >/dev/null 2>&1; then \\
444
+ true; \\
445
+ elif command -v apk >/dev/null 2>&1; then \\
446
+ apk add --no-cache ${packages}; \\
447
+ elif command -v apt-get >/dev/null 2>&1; then \\
448
+ apt-get update \\
449
+ && apt-get install -y --no-install-recommends ${packages} \\
450
+ && rm -rf /var/lib/apt/lists/*; \\
451
+ else \\
452
+ echo "Cannot install ${packageName}: unsupported VM image package manager" >&2; \\
453
+ exit 1; \\
454
+ fi`;
455
+ }
456
+
368
457
  async function value(name, question, fallback) {
369
458
  const current = args[name] ?? fallback;
370
459
  if (args[name]) return args[name];
@@ -500,6 +589,39 @@ async function chooseShell(fallback) {
500
589
  return answer;
501
590
  }
502
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
+
503
625
  async function readDefaults(file) {
504
626
  try {
505
627
  return JSON.parse(await readFile(file, "utf8"));
@@ -760,6 +882,7 @@ Options:
760
882
  --scope SLUG Optional Vercel team/user scope slug
761
883
  --from IMAGE Source image for /app/bin/wsterm
762
884
  --shell PATH /bin/bash, /bin/zsh, or /bin/sh
885
+ --tools LIST nodejs,codex,claude-code
763
886
  --auth-mode MODE basic, github, both, or none
764
887
  --auth-user VALUE Username/password auth user
765
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.7.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",