vercel-vm-factory 0.6.8 → 0.8.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 +9 -0
- package/deploy-vm.mjs +132 -46
- 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,14 @@ 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
|
+
|
|
66
|
+
Choosing bash or zsh adds the matching package to the generated Dockerfile when the VM image does not already include it.
|
|
67
|
+
|
|
59
68
|
Custom VM image:
|
|
60
69
|
|
|
61
70
|
```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 = {
|
|
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,12 @@ async function main() {
|
|
|
107
113
|
".generated",
|
|
108
114
|
project,
|
|
109
115
|
);
|
|
110
|
-
const dockerfile = makeDockerfile({
|
|
116
|
+
const dockerfile = makeDockerfile({
|
|
117
|
+
shell,
|
|
118
|
+
vmImage,
|
|
119
|
+
vmImageName,
|
|
120
|
+
wsShellImage,
|
|
121
|
+
});
|
|
111
122
|
|
|
112
123
|
await rm(appDir, { recursive: true, force: true });
|
|
113
124
|
await mkdir(appDir, { recursive: true });
|
|
@@ -118,6 +129,7 @@ async function main() {
|
|
|
118
129
|
project,
|
|
119
130
|
scope: scope || "default",
|
|
120
131
|
source: wsShellImage,
|
|
132
|
+
shell,
|
|
121
133
|
auth: authMode,
|
|
122
134
|
...(usesGitHubAuth(authMode) ? { callback: oauthRedirectUrl } : {}),
|
|
123
135
|
dockerfile: path.join(appDir, "Dockerfile.vercel"),
|
|
@@ -173,6 +185,7 @@ async function main() {
|
|
|
173
185
|
scope: scope || undefined,
|
|
174
186
|
project,
|
|
175
187
|
from: wsShellImage,
|
|
188
|
+
shell,
|
|
176
189
|
"auth-mode": authMode,
|
|
177
190
|
"auth-user": authUsername,
|
|
178
191
|
"auth-password": authPassword,
|
|
@@ -272,15 +285,12 @@ async function installVercelCli() {
|
|
|
272
285
|
);
|
|
273
286
|
|
|
274
287
|
const install = await choosePackageInstall();
|
|
275
|
-
const rl = createInterface({ input, output });
|
|
276
288
|
const answer = (
|
|
277
|
-
await
|
|
278
|
-
`Vercel CLI is not installed. Install
|
|
289
|
+
await askText(
|
|
290
|
+
`Vercel CLI is not installed. Install with ${install.command} ${install.args.join(" ")}?`,
|
|
291
|
+
"N",
|
|
279
292
|
)
|
|
280
|
-
)
|
|
281
|
-
.trim()
|
|
282
|
-
.toLowerCase();
|
|
283
|
-
rl.close();
|
|
293
|
+
).toLowerCase();
|
|
284
294
|
|
|
285
295
|
if (answer !== "y" && answer !== "yes")
|
|
286
296
|
throw new Error("Vercel CLI is required. Exiting.");
|
|
@@ -319,6 +329,7 @@ async function doctor() {
|
|
|
319
329
|
printKeyValue("project", defaults.project || "not set");
|
|
320
330
|
printKeyValue("scope", defaults.scope || "not set");
|
|
321
331
|
printKeyValue("source image", defaults.from || defaultWsShellImage);
|
|
332
|
+
printKeyValue("shell", defaults.shell || "/bin/sh");
|
|
322
333
|
printKeyValue("auth mode", defaults["auth-mode"] || "not set");
|
|
323
334
|
printKeyValue(
|
|
324
335
|
"auth user",
|
|
@@ -342,33 +353,56 @@ async function doctor() {
|
|
|
342
353
|
);
|
|
343
354
|
}
|
|
344
355
|
|
|
345
|
-
function makeDockerfile({ vmImage, wsShellImage }) {
|
|
356
|
+
function makeDockerfile({ shell, vmImage, vmImageName, wsShellImage }) {
|
|
357
|
+
const shellInstall = makeShellInstall({ shell, vmImageName });
|
|
346
358
|
return `ARG WS_SHELL_IMAGE=${wsShellImage}
|
|
347
359
|
ARG VM_IMAGE=${vmImage}
|
|
348
360
|
|
|
349
361
|
FROM \${WS_SHELL_IMAGE} AS ws-shell
|
|
350
362
|
FROM \${VM_IMAGE} AS vm
|
|
351
|
-
|
|
363
|
+
${shellInstall ? `\n${shellInstall}\n` : ""}
|
|
352
364
|
# wsterm already embeds the web UI; runtime config comes from environment variables.
|
|
353
365
|
COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
|
|
354
366
|
|
|
355
367
|
WORKDIR /app
|
|
356
368
|
ENV ENABLE_SSL=false
|
|
357
369
|
EXPOSE 80
|
|
358
|
-
CMD ["/app/bin/wsterm","-bind",":80","-fork","
|
|
370
|
+
CMD ["/app/bin/wsterm","-bind",":80","-fork","${shell}"]
|
|
359
371
|
`;
|
|
360
372
|
}
|
|
361
373
|
|
|
374
|
+
function makeShellInstall({ shell, vmImageName }) {
|
|
375
|
+
const packageName = path.basename(shell);
|
|
376
|
+
if (packageName === "sh") return "";
|
|
377
|
+
|
|
378
|
+
if (vmImageName === "alpine")
|
|
379
|
+
return `RUN apk add --no-cache ${packageName}`;
|
|
380
|
+
|
|
381
|
+
if (vmImageName === "ubuntu" || vmImageName === "debian")
|
|
382
|
+
return `RUN apt-get update \\
|
|
383
|
+
&& apt-get install -y --no-install-recommends ${packageName} \\
|
|
384
|
+
&& rm -rf /var/lib/apt/lists/*`;
|
|
385
|
+
|
|
386
|
+
return `RUN if command -v ${shell} >/dev/null 2>&1; then \\
|
|
387
|
+
true; \\
|
|
388
|
+
elif command -v apk >/dev/null 2>&1; then \\
|
|
389
|
+
apk add --no-cache ${packageName}; \\
|
|
390
|
+
elif command -v apt-get >/dev/null 2>&1; then \\
|
|
391
|
+
apt-get update \\
|
|
392
|
+
&& apt-get install -y --no-install-recommends ${packageName} \\
|
|
393
|
+
&& rm -rf /var/lib/apt/lists/*; \\
|
|
394
|
+
else \\
|
|
395
|
+
echo "Cannot install ${packageName}: unsupported VM image package manager" >&2; \\
|
|
396
|
+
exit 1; \\
|
|
397
|
+
fi`;
|
|
398
|
+
}
|
|
399
|
+
|
|
362
400
|
async function value(name, question, fallback) {
|
|
363
401
|
const current = args[name] ?? fallback;
|
|
364
402
|
if (args[name]) return args[name];
|
|
365
403
|
if (!input.isTTY) return current || "";
|
|
366
404
|
|
|
367
|
-
const
|
|
368
|
-
const answer = (
|
|
369
|
-
await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)
|
|
370
|
-
).trim();
|
|
371
|
-
rl.close();
|
|
405
|
+
const answer = await askText(question, fallback);
|
|
372
406
|
return answer || current || "";
|
|
373
407
|
}
|
|
374
408
|
|
|
@@ -376,10 +410,8 @@ async function secret(name, question, fallback) {
|
|
|
376
410
|
if (args[name]) return args[name];
|
|
377
411
|
if (!input.isTTY) return fallback || "";
|
|
378
412
|
|
|
379
|
-
const rl = createInterface({ input, output });
|
|
380
413
|
const placeholder = fallback ? mask(fallback) : "skip";
|
|
381
|
-
const answer =
|
|
382
|
-
rl.close();
|
|
414
|
+
const answer = await askText(question, placeholder);
|
|
383
415
|
return answer || fallback || "";
|
|
384
416
|
}
|
|
385
417
|
|
|
@@ -387,10 +419,10 @@ async function optionalValue(name, question, fallback) {
|
|
|
387
419
|
if (args[name] !== undefined) return args[name];
|
|
388
420
|
if (!input.isTTY) return "";
|
|
389
421
|
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
422
|
+
const answer = await askText(
|
|
423
|
+
question,
|
|
424
|
+
fallback ? `${fallback}; Enter to skip` : "skip",
|
|
425
|
+
);
|
|
394
426
|
return answer;
|
|
395
427
|
}
|
|
396
428
|
|
|
@@ -424,15 +456,14 @@ async function chooseAuthMode(fallback) {
|
|
|
424
456
|
}
|
|
425
457
|
if (!input.isTTY) return modes.has(fallback) ? fallback : "basic";
|
|
426
458
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
459
|
+
printChoices("Authentication", [
|
|
460
|
+
["1", "basic", "username/password"],
|
|
461
|
+
["2", "github", "GitHub OAuth"],
|
|
462
|
+
["3", "both", "basic + GitHub OAuth"],
|
|
463
|
+
["4", "none", "no app auth"],
|
|
464
|
+
]);
|
|
432
465
|
|
|
433
|
-
const
|
|
434
|
-
const answer = (await rl.question(`Authentication [${fallback}]: `)).trim();
|
|
435
|
-
rl.close();
|
|
466
|
+
const answer = await askText("Authentication", fallback);
|
|
436
467
|
|
|
437
468
|
if (!answer) return modes.has(fallback) ? fallback : "basic";
|
|
438
469
|
if (modes.has(answer)) return answer;
|
|
@@ -457,17 +488,16 @@ async function chooseVmImage(fallback) {
|
|
|
457
488
|
if (!input.isTTY) return fallback;
|
|
458
489
|
|
|
459
490
|
const names = Object.keys(vmImages);
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
491
|
+
printChoices(
|
|
492
|
+
"VM image",
|
|
493
|
+
names.map((name, index) => [
|
|
494
|
+
String(index + 1),
|
|
495
|
+
name,
|
|
496
|
+
vmImages[name],
|
|
497
|
+
]).concat([["4", "custom", "enter a full image name"]]),
|
|
465
498
|
);
|
|
466
|
-
console.log(` ${color.cyan("4")}. custom image`);
|
|
467
499
|
|
|
468
|
-
const
|
|
469
|
-
const answer = (await rl.question(`VM image [${fallback}]: `)).trim();
|
|
470
|
-
rl.close();
|
|
500
|
+
const answer = await askText("VM image", fallback);
|
|
471
501
|
|
|
472
502
|
if (!answer) return fallback;
|
|
473
503
|
if (vmImages[answer]) return answer;
|
|
@@ -481,6 +511,27 @@ async function chooseVmImage(fallback) {
|
|
|
481
511
|
return answer;
|
|
482
512
|
}
|
|
483
513
|
|
|
514
|
+
async function chooseShell(fallback) {
|
|
515
|
+
if (args.shell) return args.shell;
|
|
516
|
+
if (!input.isTTY) return fallback;
|
|
517
|
+
|
|
518
|
+
printChoices(
|
|
519
|
+
"Shell",
|
|
520
|
+
shells.map((shell, index) => [
|
|
521
|
+
String(index + 1),
|
|
522
|
+
shell,
|
|
523
|
+
index === shells.length - 1 ? "default" : "",
|
|
524
|
+
]),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const answer = await askText("Shell", fallback);
|
|
528
|
+
|
|
529
|
+
if (!answer) return fallback;
|
|
530
|
+
if (shells.includes(answer)) return answer;
|
|
531
|
+
if (/^[1-3]$/.test(answer)) return shells[Number(answer) - 1];
|
|
532
|
+
return answer;
|
|
533
|
+
}
|
|
534
|
+
|
|
484
535
|
async function readDefaults(file) {
|
|
485
536
|
try {
|
|
486
537
|
return JSON.parse(await readFile(file, "utf8"));
|
|
@@ -638,18 +689,30 @@ function findLastUrl(text) {
|
|
|
638
689
|
}
|
|
639
690
|
|
|
640
691
|
function printHeader() {
|
|
641
|
-
console.log(
|
|
642
|
-
console.log(
|
|
643
|
-
|
|
644
|
-
);
|
|
692
|
+
console.log("");
|
|
693
|
+
console.log(color.cyan(" __ ____ __ _____ _ "));
|
|
694
|
+
console.log(color.cyan(" \\ \\ / / \\/ | | ___|_ _ ___| |_ ___ _ __ _ _ "));
|
|
695
|
+
console.log(color.cyan(" \\ \\ / /| |\\/| | | |_ / _` |/ __| __/ _ \\| '__| | | |"));
|
|
696
|
+
console.log(color.cyan(" \\ V / | | | | | _| (_| | (__| || (_) | | | |_| |"));
|
|
697
|
+
console.log(color.cyan(" \\_/ |_| |_| |_| \\__,_|\\___|\\__\\___/|_| \\__, |"));
|
|
698
|
+
console.log(color.cyan(" |___/ "));
|
|
699
|
+
console.log(color.dim(" Vercel Container VM deployment helper"));
|
|
645
700
|
console.log("");
|
|
646
701
|
}
|
|
647
702
|
|
|
648
703
|
function printSummary(items) {
|
|
704
|
+
const entries = Object.entries(items);
|
|
705
|
+
const keyWidth = Math.max(...entries.map(([key]) => key.length), 1);
|
|
706
|
+
const valueWidth = Math.max(...entries.map(([, value]) => String(value).length), 1);
|
|
707
|
+
const width = keyWidth + valueWidth + 5;
|
|
708
|
+
|
|
649
709
|
console.log(color.bold("Deployment plan"));
|
|
710
|
+
console.log(color.cyan(`+${"-".repeat(width)}+`));
|
|
650
711
|
for (const [key, value] of Object.entries(items)) {
|
|
651
|
-
|
|
712
|
+
const line = `${key.padEnd(keyWidth)} ${String(value).padEnd(valueWidth)}`;
|
|
713
|
+
console.log(`${color.cyan("|")} ${line} ${color.cyan("|")}`);
|
|
652
714
|
}
|
|
715
|
+
console.log(color.cyan(`+${"-".repeat(width)}+`));
|
|
653
716
|
console.log("");
|
|
654
717
|
}
|
|
655
718
|
|
|
@@ -676,6 +739,28 @@ function printKeyValue(key, value) {
|
|
|
676
739
|
console.log(`${color.dim(`${key.padEnd(14)} `)}${value}`);
|
|
677
740
|
}
|
|
678
741
|
|
|
742
|
+
async function askText(question, fallback = "") {
|
|
743
|
+
const suffix = fallback ? color.dim(` [${fallback}]`) : "";
|
|
744
|
+
const rl = createInterface({ input, output });
|
|
745
|
+
const answer = (
|
|
746
|
+
await rl.question(
|
|
747
|
+
`${color.cyan("?")} ${color.bold(question)}${suffix}\n${color.dim("> ")} `,
|
|
748
|
+
)
|
|
749
|
+
).trim();
|
|
750
|
+
rl.close();
|
|
751
|
+
return answer;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function printChoices(title, choices) {
|
|
755
|
+
console.log("");
|
|
756
|
+
console.log(color.bold(title));
|
|
757
|
+
for (const [key, label, detail] of choices) {
|
|
758
|
+
const hint = detail ? ` ${color.dim(detail)}` : "";
|
|
759
|
+
console.log(` ${color.cyan(key)} ${label}${hint}`);
|
|
760
|
+
}
|
|
761
|
+
console.log("");
|
|
762
|
+
}
|
|
763
|
+
|
|
679
764
|
function step(text) {
|
|
680
765
|
console.log(`${color.cyan("->")} ${text}`);
|
|
681
766
|
}
|
|
@@ -706,6 +791,7 @@ Options:
|
|
|
706
791
|
--project NAME Vercel project name
|
|
707
792
|
--scope SLUG Optional Vercel team/user scope slug
|
|
708
793
|
--from IMAGE Source image for /app/bin/wsterm
|
|
794
|
+
--shell PATH /bin/bash, /bin/zsh, or /bin/sh
|
|
709
795
|
--auth-mode MODE basic, github, both, or none
|
|
710
796
|
--auth-user VALUE Username/password auth user
|
|
711
797
|
--auth-password VAL Username/password auth password
|