vercel-vm-factory 0.3.8 → 0.6.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 +5 -5
- package/deploy-vm.mjs +82 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
8
|
--project ws-shell-ubuntu \
|
|
9
9
|
--auth-mode basic \
|
|
10
10
|
--auth-user admin \
|
|
@@ -50,16 +50,16 @@ CLI mapping:
|
|
|
50
50
|
- Application Preset -> patched through Vercel API as `framework=container`
|
|
51
51
|
- Root Directory -> generated project directory
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
VM image presets:
|
|
54
54
|
|
|
55
55
|
- `alpine` -> `alpine:3.23`
|
|
56
56
|
- `ubuntu` -> `ubuntu:24.04`
|
|
57
57
|
- `debian` -> `debian:13-slim`
|
|
58
58
|
|
|
59
|
-
Custom
|
|
59
|
+
Custom VM image:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
npx vercel-vm-factory create --
|
|
62
|
+
npx vercel-vm-factory create --vm-image fedora:42 --project ws-shell-fedora
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Before deploying, set the GitHub OAuth callback URL to:
|
package/deploy-vm.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -8,7 +8,7 @@ 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",
|
|
@@ -20,8 +20,13 @@ const workspaceRoot = process.cwd();
|
|
|
20
20
|
const stateRoot = path.join(homedir(), ".vercel-vm-factory");
|
|
21
21
|
const defaultsPath = path.join(stateRoot, "defaults.json");
|
|
22
22
|
const legacyDefaultsPath = path.join(scriptRoot, ".defaults.json");
|
|
23
|
-
const
|
|
23
|
+
const codeDefaults = { "vm-image": "alpine", from: defaultWsShellImage };
|
|
24
|
+
const packagedDefaults = {
|
|
24
25
|
...(await readDefaults(legacyDefaultsPath)),
|
|
26
|
+
...codeDefaults,
|
|
27
|
+
};
|
|
28
|
+
let defaults = {
|
|
29
|
+
...packagedDefaults,
|
|
25
30
|
...(await readDefaults(defaultsPath)),
|
|
26
31
|
};
|
|
27
32
|
const colorEnabled = output.isTTY && !process.env.NO_COLOR;
|
|
@@ -57,6 +62,7 @@ try {
|
|
|
57
62
|
|
|
58
63
|
async function main() {
|
|
59
64
|
printHeader();
|
|
65
|
+
defaults = await syncDefaults();
|
|
60
66
|
await ensureVercelInstalled();
|
|
61
67
|
await ensureVercelLogin();
|
|
62
68
|
|
|
@@ -65,8 +71,10 @@ async function main() {
|
|
|
65
71
|
return;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
const
|
|
69
|
-
|
|
74
|
+
const vmImageName = await chooseVmImage(
|
|
75
|
+
args["vm-image"] ?? args.base ?? defaults["vm-image"] ?? "alpine",
|
|
76
|
+
);
|
|
77
|
+
const vmImage = vmImages[vmImageName] ?? vmImageName;
|
|
70
78
|
const scope = await optionalValue(
|
|
71
79
|
"scope",
|
|
72
80
|
"Vercel team/scope",
|
|
@@ -75,7 +83,7 @@ async function main() {
|
|
|
75
83
|
const project = await value(
|
|
76
84
|
"project",
|
|
77
85
|
"Vercel project name",
|
|
78
|
-
defaults.project ?? `ws-shell-${
|
|
86
|
+
defaults.project ?? `ws-shell-${vmImageName}`,
|
|
79
87
|
);
|
|
80
88
|
const wsShellImage =
|
|
81
89
|
args.from ??
|
|
@@ -99,14 +107,14 @@ async function main() {
|
|
|
99
107
|
".generated",
|
|
100
108
|
project,
|
|
101
109
|
);
|
|
102
|
-
const dockerfile = makeDockerfile({
|
|
110
|
+
const dockerfile = makeDockerfile({ vmImage, wsShellImage });
|
|
103
111
|
|
|
104
112
|
await rm(appDir, { recursive: true, force: true });
|
|
105
113
|
await mkdir(appDir, { recursive: true });
|
|
106
114
|
await writeFile(path.join(appDir, "Dockerfile.vercel"), dockerfile);
|
|
107
115
|
|
|
108
116
|
printSummary({
|
|
109
|
-
|
|
117
|
+
"vm image": `${vmImageName} -> ${vmImage}`,
|
|
110
118
|
project,
|
|
111
119
|
scope: scope || "default",
|
|
112
120
|
source: wsShellImage,
|
|
@@ -161,7 +169,7 @@ async function main() {
|
|
|
161
169
|
|
|
162
170
|
await writeDefaults(defaultsPath, {
|
|
163
171
|
...defaults,
|
|
164
|
-
|
|
172
|
+
"vm-image": vmImageName,
|
|
165
173
|
scope: scope || undefined,
|
|
166
174
|
project,
|
|
167
175
|
from: wsShellImage,
|
|
@@ -307,7 +315,7 @@ async function doctor() {
|
|
|
307
315
|
console.log("");
|
|
308
316
|
console.log(color.bold("Saved defaults"));
|
|
309
317
|
printKeyValue("defaults file", defaultsPath);
|
|
310
|
-
printKeyValue("
|
|
318
|
+
printKeyValue("vm image", defaults["vm-image"] || "not set");
|
|
311
319
|
printKeyValue("project", defaults.project || "not set");
|
|
312
320
|
printKeyValue("scope", defaults.scope || "not set");
|
|
313
321
|
printKeyValue("source image", defaults.from || defaultWsShellImage);
|
|
@@ -334,12 +342,12 @@ async function doctor() {
|
|
|
334
342
|
);
|
|
335
343
|
}
|
|
336
344
|
|
|
337
|
-
function makeDockerfile({
|
|
345
|
+
function makeDockerfile({ vmImage, wsShellImage }) {
|
|
338
346
|
return `ARG WS_SHELL_IMAGE=${wsShellImage}
|
|
339
|
-
ARG
|
|
347
|
+
ARG VM_IMAGE=${vmImage}
|
|
340
348
|
|
|
341
349
|
FROM \${WS_SHELL_IMAGE} AS ws-shell
|
|
342
|
-
FROM \${
|
|
350
|
+
FROM \${VM_IMAGE} AS vm
|
|
343
351
|
|
|
344
352
|
# wsterm already embeds the web UI; runtime config comes from environment variables.
|
|
345
353
|
COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
|
|
@@ -443,28 +451,33 @@ function usesGitHubAuth(mode) {
|
|
|
443
451
|
return mode === "github" || mode === "both";
|
|
444
452
|
}
|
|
445
453
|
|
|
446
|
-
async function
|
|
454
|
+
async function chooseVmImage(fallback) {
|
|
455
|
+
if (args["vm-image"]) return args["vm-image"];
|
|
447
456
|
if (args.base) return args.base;
|
|
448
457
|
if (!input.isTTY) return fallback;
|
|
449
458
|
|
|
450
|
-
const names = Object.keys(
|
|
451
|
-
console.log(color.bold("Choose
|
|
459
|
+
const names = Object.keys(vmImages);
|
|
460
|
+
console.log(color.bold("Choose VM image"));
|
|
452
461
|
names.forEach((name, index) =>
|
|
453
462
|
console.log(
|
|
454
|
-
` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${
|
|
463
|
+
` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${vmImages[name]})`)}`,
|
|
455
464
|
),
|
|
456
465
|
);
|
|
457
466
|
console.log(` ${color.cyan("4")}. custom image`);
|
|
458
467
|
|
|
459
468
|
const rl = createInterface({ input, output });
|
|
460
|
-
const answer = (await rl.question(`
|
|
469
|
+
const answer = (await rl.question(`VM image [${fallback}]: `)).trim();
|
|
461
470
|
rl.close();
|
|
462
471
|
|
|
463
472
|
if (!answer) return fallback;
|
|
464
|
-
if (
|
|
473
|
+
if (vmImages[answer]) return answer;
|
|
465
474
|
if (/^[1-3]$/.test(answer)) return names[Number(answer) - 1];
|
|
466
475
|
if (answer === "4")
|
|
467
|
-
return value(
|
|
476
|
+
return value(
|
|
477
|
+
"custom-vm-image",
|
|
478
|
+
"Custom VM image",
|
|
479
|
+
defaults["custom-vm-image"],
|
|
480
|
+
);
|
|
468
481
|
return answer;
|
|
469
482
|
}
|
|
470
483
|
|
|
@@ -476,14 +489,57 @@ async function readDefaults(file) {
|
|
|
476
489
|
}
|
|
477
490
|
}
|
|
478
491
|
|
|
479
|
-
async function
|
|
480
|
-
const
|
|
481
|
-
|
|
492
|
+
async function syncDefaults() {
|
|
493
|
+
const current = await readDefaults(defaultsPath);
|
|
494
|
+
const codeMtime = Math.max(
|
|
495
|
+
await readMtime(import.meta.filename),
|
|
496
|
+
await readMtime(legacyDefaultsPath),
|
|
482
497
|
);
|
|
498
|
+
const homeMtime = await readMtime(defaultsPath);
|
|
499
|
+
const latest =
|
|
500
|
+
homeMtime > codeMtime
|
|
501
|
+
? { ...packagedDefaults, ...current }
|
|
502
|
+
: { ...current, ...packagedDefaults };
|
|
503
|
+
|
|
504
|
+
if (
|
|
505
|
+
JSON.stringify(cleanDefaults(current)) !==
|
|
506
|
+
JSON.stringify(cleanDefaults(latest))
|
|
507
|
+
)
|
|
508
|
+
await writeDefaults(defaultsPath, latest);
|
|
509
|
+
return cleanDefaults(latest);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function readMtime(file) {
|
|
513
|
+
try {
|
|
514
|
+
return (await stat(file)).mtimeMs;
|
|
515
|
+
} catch {
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function writeDefaults(file, data) {
|
|
521
|
+
const clean = cleanDefaults(data);
|
|
483
522
|
await mkdir(path.dirname(file), { recursive: true });
|
|
484
523
|
await writeFile(file, `${JSON.stringify(clean, null, 2)}\n`);
|
|
485
524
|
}
|
|
486
525
|
|
|
526
|
+
function cleanDefaults(data) {
|
|
527
|
+
const migrated = {
|
|
528
|
+
...data,
|
|
529
|
+
"vm-image": data["vm-image"] ?? data.base,
|
|
530
|
+
"custom-vm-image": data["custom-vm-image"] ?? data["custom-base"],
|
|
531
|
+
};
|
|
532
|
+
delete migrated.base;
|
|
533
|
+
delete migrated["custom-base"];
|
|
534
|
+
|
|
535
|
+
const clean = Object.fromEntries(
|
|
536
|
+
Object.entries(migrated).filter(([, value]) => value),
|
|
537
|
+
);
|
|
538
|
+
return Object.fromEntries(
|
|
539
|
+
Object.entries(clean).sort(([a], [b]) => a.localeCompare(b)),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
487
543
|
function mask(value) {
|
|
488
544
|
if (value.length <= 8) return "********";
|
|
489
545
|
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
@@ -640,12 +696,13 @@ function printHelp() {
|
|
|
640
696
|
printHeader();
|
|
641
697
|
console.log(`Usage:
|
|
642
698
|
vercel-vm-factory create
|
|
643
|
-
vercel-vm-factory create --
|
|
699
|
+
vercel-vm-factory create --vm-image ubuntu --project x-shell
|
|
644
700
|
vercel-vm-factory doctor
|
|
645
701
|
npx vercel-vm-factory create
|
|
646
702
|
|
|
647
703
|
Options:
|
|
648
|
-
--
|
|
704
|
+
--vm-image NAME alpine, ubuntu, debian, or a custom VM image
|
|
705
|
+
--base NAME Alias for --vm-image
|
|
649
706
|
--project NAME Vercel project name
|
|
650
707
|
--scope SLUG Optional Vercel team/user scope slug
|
|
651
708
|
--from IMAGE Source image for /app/bin/wsterm
|
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.6.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": {
|