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.
- package/README.md +35 -1
- package/deploy-vm.mjs +126 -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
|
|
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({
|
|
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
|