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.
- package/README.md +34 -2
- package/deploy-vm.mjs +97 -6
- 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
|
|
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
|
-
${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|