vercel-vm-factory 0.5.8 → 0.7.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 +12 -5
- package/deploy-vm.mjs +131 -61
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Vercel VM Factory
|
|
2
2
|
|
|
3
|
-
Create a tiny Vercel Container deployment: copy `wsterm` from `ghcr.io/v1xingyue/ws-shell:v1.8.alpine` into a selected
|
|
3
|
+
Create a tiny Vercel Container deployment: copy `wsterm` from `ghcr.io/v1xingyue/ws-shell:v1.8.alpine` into a selected VM image, then deploy with Vercel CLI.
|
|
4
4
|
|
|
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 \
|
|
@@ -50,16 +51,22 @@ CLI mapping:
|
|
|
50
51
|
- Application Preset -> patched through Vercel API as `framework=container`
|
|
51
52
|
- Root Directory -> generated project directory
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
VM image presets:
|
|
54
55
|
|
|
55
56
|
- `alpine` -> `alpine:3.23`
|
|
56
57
|
- `ubuntu` -> `ubuntu:24.04`
|
|
57
58
|
- `debian` -> `debian:13-slim`
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
Shell options:
|
|
61
|
+
|
|
62
|
+
- `/bin/bash`
|
|
63
|
+
- `/bin/zsh`
|
|
64
|
+
- `/bin/sh`
|
|
65
|
+
|
|
66
|
+
Custom VM image:
|
|
60
67
|
|
|
61
68
|
```bash
|
|
62
|
-
npx vercel-vm-factory create --
|
|
69
|
+
npx vercel-vm-factory create --vm-image fedora:42 --project ws-shell-fedora
|
|
63
70
|
```
|
|
64
71
|
|
|
65
72
|
Before deploying, set the GitHub OAuth callback URL to:
|
package/deploy-vm.mjs
CHANGED
|
@@ -8,11 +8,12 @@ import path from "node:path";
|
|
|
8
8
|
|
|
9
9
|
const defaultWsShellImage = "ghcr.io/v1xingyue/ws-shell:v1.8.alpine";
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const vmImages = {
|
|
12
12
|
alpine: "alpine:3.23",
|
|
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,
|
|
@@ -71,8 +76,10 @@ async function main() {
|
|
|
71
76
|
return;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
const
|
|
75
|
-
|
|
79
|
+
const vmImageName = await chooseVmImage(
|
|
80
|
+
args["vm-image"] ?? args.base ?? defaults["vm-image"] ?? "alpine",
|
|
81
|
+
);
|
|
82
|
+
const vmImage = vmImages[vmImageName] ?? vmImageName;
|
|
76
83
|
const scope = await optionalValue(
|
|
77
84
|
"scope",
|
|
78
85
|
"Vercel team/scope",
|
|
@@ -81,13 +88,14 @@ async function main() {
|
|
|
81
88
|
const project = await value(
|
|
82
89
|
"project",
|
|
83
90
|
"Vercel project name",
|
|
84
|
-
defaults.project ?? `ws-shell-${
|
|
91
|
+
defaults.project ?? `ws-shell-${vmImageName}`,
|
|
85
92
|
);
|
|
86
93
|
const wsShellImage =
|
|
87
94
|
args.from ??
|
|
88
95
|
process.env.WS_SHELL_IMAGE ??
|
|
89
96
|
defaults.from ??
|
|
90
97
|
defaultWsShellImage;
|
|
98
|
+
const shell = await chooseShell(args.shell ?? defaults.shell ?? "/bin/sh");
|
|
91
99
|
const prod = args.prod !== "false";
|
|
92
100
|
const dryRun = Boolean(args["dry-run"]);
|
|
93
101
|
const skipLink = Boolean(args["skip-link"]);
|
|
@@ -105,17 +113,18 @@ async function main() {
|
|
|
105
113
|
".generated",
|
|
106
114
|
project,
|
|
107
115
|
);
|
|
108
|
-
const dockerfile = makeDockerfile({
|
|
116
|
+
const dockerfile = makeDockerfile({ shell, vmImage, wsShellImage });
|
|
109
117
|
|
|
110
118
|
await rm(appDir, { recursive: true, force: true });
|
|
111
119
|
await mkdir(appDir, { recursive: true });
|
|
112
120
|
await writeFile(path.join(appDir, "Dockerfile.vercel"), dockerfile);
|
|
113
121
|
|
|
114
122
|
printSummary({
|
|
115
|
-
|
|
123
|
+
"vm image": `${vmImageName} -> ${vmImage}`,
|
|
116
124
|
project,
|
|
117
125
|
scope: scope || "default",
|
|
118
126
|
source: wsShellImage,
|
|
127
|
+
shell,
|
|
119
128
|
auth: authMode,
|
|
120
129
|
...(usesGitHubAuth(authMode) ? { callback: oauthRedirectUrl } : {}),
|
|
121
130
|
dockerfile: path.join(appDir, "Dockerfile.vercel"),
|
|
@@ -167,10 +176,11 @@ async function main() {
|
|
|
167
176
|
|
|
168
177
|
await writeDefaults(defaultsPath, {
|
|
169
178
|
...defaults,
|
|
170
|
-
|
|
179
|
+
"vm-image": vmImageName,
|
|
171
180
|
scope: scope || undefined,
|
|
172
181
|
project,
|
|
173
182
|
from: wsShellImage,
|
|
183
|
+
shell,
|
|
174
184
|
"auth-mode": authMode,
|
|
175
185
|
"auth-user": authUsername,
|
|
176
186
|
"auth-password": authPassword,
|
|
@@ -270,15 +280,12 @@ async function installVercelCli() {
|
|
|
270
280
|
);
|
|
271
281
|
|
|
272
282
|
const install = await choosePackageInstall();
|
|
273
|
-
const rl = createInterface({ input, output });
|
|
274
283
|
const answer = (
|
|
275
|
-
await
|
|
276
|
-
`Vercel CLI is not installed. Install
|
|
284
|
+
await askText(
|
|
285
|
+
`Vercel CLI is not installed. Install with ${install.command} ${install.args.join(" ")}?`,
|
|
286
|
+
"N",
|
|
277
287
|
)
|
|
278
|
-
)
|
|
279
|
-
.trim()
|
|
280
|
-
.toLowerCase();
|
|
281
|
-
rl.close();
|
|
288
|
+
).toLowerCase();
|
|
282
289
|
|
|
283
290
|
if (answer !== "y" && answer !== "yes")
|
|
284
291
|
throw new Error("Vercel CLI is required. Exiting.");
|
|
@@ -313,10 +320,11 @@ async function doctor() {
|
|
|
313
320
|
console.log("");
|
|
314
321
|
console.log(color.bold("Saved defaults"));
|
|
315
322
|
printKeyValue("defaults file", defaultsPath);
|
|
316
|
-
printKeyValue("
|
|
323
|
+
printKeyValue("vm image", defaults["vm-image"] || "not set");
|
|
317
324
|
printKeyValue("project", defaults.project || "not set");
|
|
318
325
|
printKeyValue("scope", defaults.scope || "not set");
|
|
319
326
|
printKeyValue("source image", defaults.from || defaultWsShellImage);
|
|
327
|
+
printKeyValue("shell", defaults.shell || "/bin/sh");
|
|
320
328
|
printKeyValue("auth mode", defaults["auth-mode"] || "not set");
|
|
321
329
|
printKeyValue(
|
|
322
330
|
"auth user",
|
|
@@ -340,12 +348,12 @@ async function doctor() {
|
|
|
340
348
|
);
|
|
341
349
|
}
|
|
342
350
|
|
|
343
|
-
function makeDockerfile({
|
|
351
|
+
function makeDockerfile({ shell, vmImage, wsShellImage }) {
|
|
344
352
|
return `ARG WS_SHELL_IMAGE=${wsShellImage}
|
|
345
|
-
ARG
|
|
353
|
+
ARG VM_IMAGE=${vmImage}
|
|
346
354
|
|
|
347
355
|
FROM \${WS_SHELL_IMAGE} AS ws-shell
|
|
348
|
-
FROM \${
|
|
356
|
+
FROM \${VM_IMAGE} AS vm
|
|
349
357
|
|
|
350
358
|
# wsterm already embeds the web UI; runtime config comes from environment variables.
|
|
351
359
|
COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
|
|
@@ -353,7 +361,7 @@ COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
|
|
|
353
361
|
WORKDIR /app
|
|
354
362
|
ENV ENABLE_SSL=false
|
|
355
363
|
EXPOSE 80
|
|
356
|
-
CMD ["/app/bin/wsterm","-bind",":80","-fork","
|
|
364
|
+
CMD ["/app/bin/wsterm","-bind",":80","-fork","${shell}"]
|
|
357
365
|
`;
|
|
358
366
|
}
|
|
359
367
|
|
|
@@ -362,11 +370,7 @@ async function value(name, question, fallback) {
|
|
|
362
370
|
if (args[name]) return args[name];
|
|
363
371
|
if (!input.isTTY) return current || "";
|
|
364
372
|
|
|
365
|
-
const
|
|
366
|
-
const answer = (
|
|
367
|
-
await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)
|
|
368
|
-
).trim();
|
|
369
|
-
rl.close();
|
|
373
|
+
const answer = await askText(question, fallback);
|
|
370
374
|
return answer || current || "";
|
|
371
375
|
}
|
|
372
376
|
|
|
@@ -374,10 +378,8 @@ async function secret(name, question, fallback) {
|
|
|
374
378
|
if (args[name]) return args[name];
|
|
375
379
|
if (!input.isTTY) return fallback || "";
|
|
376
380
|
|
|
377
|
-
const rl = createInterface({ input, output });
|
|
378
381
|
const placeholder = fallback ? mask(fallback) : "skip";
|
|
379
|
-
const answer =
|
|
380
|
-
rl.close();
|
|
382
|
+
const answer = await askText(question, placeholder);
|
|
381
383
|
return answer || fallback || "";
|
|
382
384
|
}
|
|
383
385
|
|
|
@@ -385,10 +387,10 @@ async function optionalValue(name, question, fallback) {
|
|
|
385
387
|
if (args[name] !== undefined) return args[name];
|
|
386
388
|
if (!input.isTTY) return "";
|
|
387
389
|
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
390
|
+
const answer = await askText(
|
|
391
|
+
question,
|
|
392
|
+
fallback ? `${fallback}; Enter to skip` : "skip",
|
|
393
|
+
);
|
|
392
394
|
return answer;
|
|
393
395
|
}
|
|
394
396
|
|
|
@@ -422,15 +424,14 @@ async function chooseAuthMode(fallback) {
|
|
|
422
424
|
}
|
|
423
425
|
if (!input.isTTY) return modes.has(fallback) ? fallback : "basic";
|
|
424
426
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
427
|
+
printChoices("Authentication", [
|
|
428
|
+
["1", "basic", "username/password"],
|
|
429
|
+
["2", "github", "GitHub OAuth"],
|
|
430
|
+
["3", "both", "basic + GitHub OAuth"],
|
|
431
|
+
["4", "none", "no app auth"],
|
|
432
|
+
]);
|
|
430
433
|
|
|
431
|
-
const
|
|
432
|
-
const answer = (await rl.question(`Authentication [${fallback}]: `)).trim();
|
|
433
|
-
rl.close();
|
|
434
|
+
const answer = await askText("Authentication", fallback);
|
|
434
435
|
|
|
435
436
|
if (!answer) return modes.has(fallback) ? fallback : "basic";
|
|
436
437
|
if (modes.has(answer)) return answer;
|
|
@@ -449,28 +450,53 @@ function usesGitHubAuth(mode) {
|
|
|
449
450
|
return mode === "github" || mode === "both";
|
|
450
451
|
}
|
|
451
452
|
|
|
452
|
-
async function
|
|
453
|
+
async function chooseVmImage(fallback) {
|
|
454
|
+
if (args["vm-image"]) return args["vm-image"];
|
|
453
455
|
if (args.base) return args.base;
|
|
454
456
|
if (!input.isTTY) return fallback;
|
|
455
457
|
|
|
456
|
-
const names = Object.keys(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
458
|
+
const names = Object.keys(vmImages);
|
|
459
|
+
printChoices(
|
|
460
|
+
"VM image",
|
|
461
|
+
names.map((name, index) => [
|
|
462
|
+
String(index + 1),
|
|
463
|
+
name,
|
|
464
|
+
vmImages[name],
|
|
465
|
+
]).concat([["4", "custom", "enter a full image name"]]),
|
|
462
466
|
);
|
|
463
|
-
console.log(` ${color.cyan("4")}. custom image`);
|
|
464
467
|
|
|
465
|
-
const
|
|
466
|
-
const answer = (await rl.question(`Base [${fallback}]: `)).trim();
|
|
467
|
-
rl.close();
|
|
468
|
+
const answer = await askText("VM image", fallback);
|
|
468
469
|
|
|
469
470
|
if (!answer) return fallback;
|
|
470
|
-
if (
|
|
471
|
+
if (vmImages[answer]) return answer;
|
|
471
472
|
if (/^[1-3]$/.test(answer)) return names[Number(answer) - 1];
|
|
472
473
|
if (answer === "4")
|
|
473
|
-
return value(
|
|
474
|
+
return value(
|
|
475
|
+
"custom-vm-image",
|
|
476
|
+
"Custom VM image",
|
|
477
|
+
defaults["custom-vm-image"],
|
|
478
|
+
);
|
|
479
|
+
return answer;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function chooseShell(fallback) {
|
|
483
|
+
if (args.shell) return args.shell;
|
|
484
|
+
if (!input.isTTY) return fallback;
|
|
485
|
+
|
|
486
|
+
printChoices(
|
|
487
|
+
"Shell",
|
|
488
|
+
shells.map((shell, index) => [
|
|
489
|
+
String(index + 1),
|
|
490
|
+
shell,
|
|
491
|
+
index === shells.length - 1 ? "default" : "",
|
|
492
|
+
]),
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
const answer = await askText("Shell", fallback);
|
|
496
|
+
|
|
497
|
+
if (!answer) return fallback;
|
|
498
|
+
if (shells.includes(answer)) return answer;
|
|
499
|
+
if (/^[1-3]$/.test(answer)) return shells[Number(answer) - 1];
|
|
474
500
|
return answer;
|
|
475
501
|
}
|
|
476
502
|
|
|
@@ -517,8 +543,16 @@ async function writeDefaults(file, data) {
|
|
|
517
543
|
}
|
|
518
544
|
|
|
519
545
|
function cleanDefaults(data) {
|
|
546
|
+
const migrated = {
|
|
547
|
+
...data,
|
|
548
|
+
"vm-image": data["vm-image"] ?? data.base,
|
|
549
|
+
"custom-vm-image": data["custom-vm-image"] ?? data["custom-base"],
|
|
550
|
+
};
|
|
551
|
+
delete migrated.base;
|
|
552
|
+
delete migrated["custom-base"];
|
|
553
|
+
|
|
520
554
|
const clean = Object.fromEntries(
|
|
521
|
-
Object.entries(
|
|
555
|
+
Object.entries(migrated).filter(([, value]) => value),
|
|
522
556
|
);
|
|
523
557
|
return Object.fromEntries(
|
|
524
558
|
Object.entries(clean).sort(([a], [b]) => a.localeCompare(b)),
|
|
@@ -623,18 +657,30 @@ function findLastUrl(text) {
|
|
|
623
657
|
}
|
|
624
658
|
|
|
625
659
|
function printHeader() {
|
|
626
|
-
console.log(
|
|
627
|
-
console.log(
|
|
628
|
-
|
|
629
|
-
);
|
|
660
|
+
console.log("");
|
|
661
|
+
console.log(color.cyan(" __ ____ __ _____ _ "));
|
|
662
|
+
console.log(color.cyan(" \\ \\ / / \\/ | | ___|_ _ ___| |_ ___ _ __ _ _ "));
|
|
663
|
+
console.log(color.cyan(" \\ \\ / /| |\\/| | | |_ / _` |/ __| __/ _ \\| '__| | | |"));
|
|
664
|
+
console.log(color.cyan(" \\ V / | | | | | _| (_| | (__| || (_) | | | |_| |"));
|
|
665
|
+
console.log(color.cyan(" \\_/ |_| |_| |_| \\__,_|\\___|\\__\\___/|_| \\__, |"));
|
|
666
|
+
console.log(color.cyan(" |___/ "));
|
|
667
|
+
console.log(color.dim(" Vercel Container VM deployment helper"));
|
|
630
668
|
console.log("");
|
|
631
669
|
}
|
|
632
670
|
|
|
633
671
|
function printSummary(items) {
|
|
672
|
+
const entries = Object.entries(items);
|
|
673
|
+
const keyWidth = Math.max(...entries.map(([key]) => key.length), 1);
|
|
674
|
+
const valueWidth = Math.max(...entries.map(([, value]) => String(value).length), 1);
|
|
675
|
+
const width = keyWidth + valueWidth + 5;
|
|
676
|
+
|
|
634
677
|
console.log(color.bold("Deployment plan"));
|
|
678
|
+
console.log(color.cyan(`+${"-".repeat(width)}+`));
|
|
635
679
|
for (const [key, value] of Object.entries(items)) {
|
|
636
|
-
|
|
680
|
+
const line = `${key.padEnd(keyWidth)} ${String(value).padEnd(valueWidth)}`;
|
|
681
|
+
console.log(`${color.cyan("|")} ${line} ${color.cyan("|")}`);
|
|
637
682
|
}
|
|
683
|
+
console.log(color.cyan(`+${"-".repeat(width)}+`));
|
|
638
684
|
console.log("");
|
|
639
685
|
}
|
|
640
686
|
|
|
@@ -661,6 +707,28 @@ function printKeyValue(key, value) {
|
|
|
661
707
|
console.log(`${color.dim(`${key.padEnd(14)} `)}${value}`);
|
|
662
708
|
}
|
|
663
709
|
|
|
710
|
+
async function askText(question, fallback = "") {
|
|
711
|
+
const suffix = fallback ? color.dim(` [${fallback}]`) : "";
|
|
712
|
+
const rl = createInterface({ input, output });
|
|
713
|
+
const answer = (
|
|
714
|
+
await rl.question(
|
|
715
|
+
`${color.cyan("?")} ${color.bold(question)}${suffix}\n${color.dim("> ")} `,
|
|
716
|
+
)
|
|
717
|
+
).trim();
|
|
718
|
+
rl.close();
|
|
719
|
+
return answer;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function printChoices(title, choices) {
|
|
723
|
+
console.log("");
|
|
724
|
+
console.log(color.bold(title));
|
|
725
|
+
for (const [key, label, detail] of choices) {
|
|
726
|
+
const hint = detail ? ` ${color.dim(detail)}` : "";
|
|
727
|
+
console.log(` ${color.cyan(key)} ${label}${hint}`);
|
|
728
|
+
}
|
|
729
|
+
console.log("");
|
|
730
|
+
}
|
|
731
|
+
|
|
664
732
|
function step(text) {
|
|
665
733
|
console.log(`${color.cyan("->")} ${text}`);
|
|
666
734
|
}
|
|
@@ -681,15 +749,17 @@ function printHelp() {
|
|
|
681
749
|
printHeader();
|
|
682
750
|
console.log(`Usage:
|
|
683
751
|
vercel-vm-factory create
|
|
684
|
-
vercel-vm-factory create --
|
|
752
|
+
vercel-vm-factory create --vm-image ubuntu --project x-shell
|
|
685
753
|
vercel-vm-factory doctor
|
|
686
754
|
npx vercel-vm-factory create
|
|
687
755
|
|
|
688
756
|
Options:
|
|
689
|
-
--
|
|
757
|
+
--vm-image NAME alpine, ubuntu, debian, or a custom VM image
|
|
758
|
+
--base NAME Alias for --vm-image
|
|
690
759
|
--project NAME Vercel project name
|
|
691
760
|
--scope SLUG Optional Vercel team/user scope slug
|
|
692
761
|
--from IMAGE Source image for /app/bin/wsterm
|
|
762
|
+
--shell PATH /bin/bash, /bin/zsh, or /bin/sh
|
|
693
763
|
--auth-mode MODE basic, github, both, or none
|
|
694
764
|
--auth-user VALUE Username/password auth user
|
|
695
765
|
--auth-password VAL Username/password auth password
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vercel-vm-factory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Create Vercel Container deployments for ws-shell from selectable
|
|
3
|
+
"version": "0.7.8",
|
|
4
|
+
"description": "Create Vercel Container deployments for ws-shell from selectable VM images.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|