vercel-vm-factory 0.1.0
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 +59 -0
- package/bin/vercel-vm-factory.js +2 -0
- package/deploy-vm.mjs +427 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Vercel VM Factory
|
|
2
|
+
|
|
3
|
+
Create a tiny Vercel Container deployment: copy `wsterm` from `ghcr.io/v1xingyue/ws-shell:v1.1.alpine` into a selected base image, then deploy with Vercel CLI.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx vercel-vm-factory create \
|
|
7
|
+
--base ubuntu \
|
|
8
|
+
--project ws-shell-ubuntu \
|
|
9
|
+
--client-id YOUR_GITHUB_CLIENT_ID \
|
|
10
|
+
--client-secret YOUR_GITHUB_CLIENT_SECRET \
|
|
11
|
+
--github-userid 12345678
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Run without flags for prompts:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx vercel-vm-factory create
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Check local setup:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx vercel-vm-factory doctor
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The script checks `vercel --version` and `vercel whoami`; if you are not logged in, it runs `vercel login`.
|
|
27
|
+
|
|
28
|
+
Use `--help` to show all flags.
|
|
29
|
+
|
|
30
|
+
Entered GitHub values are reused from `~/.vercel-vm-factory/defaults.json`; press Enter to keep the placeholder value or skip an empty one.
|
|
31
|
+
|
|
32
|
+
The generated project contains only `Dockerfile.vercel`.
|
|
33
|
+
|
|
34
|
+
CLI mapping:
|
|
35
|
+
|
|
36
|
+
- Vercel Team -> `--scope TEAM_SLUG` when needed; omit it to use the CLI default scope
|
|
37
|
+
- Project Name -> `--project x-shell`
|
|
38
|
+
- Application Preset -> patched through Vercel API as `framework=container`
|
|
39
|
+
- Root Directory -> generated project directory
|
|
40
|
+
|
|
41
|
+
Base presets:
|
|
42
|
+
|
|
43
|
+
- `alpine` -> `alpine:3.23`
|
|
44
|
+
- `ubuntu` -> `ubuntu:24.04`
|
|
45
|
+
- `debian` -> `debian:13-slim`
|
|
46
|
+
|
|
47
|
+
Custom base:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx vercel-vm-factory create --base fedora:42 --project ws-shell-fedora
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Before deploying, set the GitHub OAuth callback URL to:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
https://PROJECT.vercel.app/auth/github/callback
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use `--dry-run` to generate files without deploying.
|
package/deploy-vm.mjs
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
const bases = {
|
|
10
|
+
alpine: "alpine:3.23",
|
|
11
|
+
ubuntu: "ubuntu:24.04",
|
|
12
|
+
debian: "debian:13-slim",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const { command, args } = parseCommand(process.argv.slice(2));
|
|
16
|
+
const scriptRoot = path.resolve(import.meta.dirname);
|
|
17
|
+
const workspaceRoot = process.cwd();
|
|
18
|
+
const stateRoot = path.join(homedir(), ".vercel-vm-factory");
|
|
19
|
+
const defaultsPath = path.join(stateRoot, "defaults.json");
|
|
20
|
+
const legacyDefaultsPath = path.join(scriptRoot, ".defaults.json");
|
|
21
|
+
const defaults = { ...(await readDefaults(legacyDefaultsPath)), ...(await readDefaults(defaultsPath)) };
|
|
22
|
+
const colorEnabled = output.isTTY && !process.env.NO_COLOR;
|
|
23
|
+
const color = {
|
|
24
|
+
dim: (text) => paint(text, "2"),
|
|
25
|
+
cyan: (text) => paint(text, "36"),
|
|
26
|
+
green: (text) => paint(text, "32"),
|
|
27
|
+
yellow: (text) => paint(text, "33"),
|
|
28
|
+
red: (text) => paint(text, "31"),
|
|
29
|
+
bold: (text) => paint(text, "1"),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
if (args.help || args.h || command === "help") {
|
|
34
|
+
printHelp();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
if (!["create", "doctor"].includes(command)) {
|
|
38
|
+
throw new Error(`Unknown command: ${command}. Run vercel-vm-factory help`);
|
|
39
|
+
}
|
|
40
|
+
await main();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
if (message.includes("scope does not exist")) {
|
|
44
|
+
console.error(`\nScope not found. Leave scope empty, or set the real CLI slug with --scope.`);
|
|
45
|
+
console.error(`If it was saved before, edit or delete: ${defaultsPath}`);
|
|
46
|
+
}
|
|
47
|
+
console.error(message);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
printHeader();
|
|
53
|
+
|
|
54
|
+
if (args.doctor || command === "doctor") {
|
|
55
|
+
await doctor();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const baseName = await chooseBase(args.base ?? defaults.base ?? "alpine");
|
|
60
|
+
const baseImage = bases[baseName] ?? baseName;
|
|
61
|
+
const scope = await optionalValue("scope", "Vercel team/scope", defaults.scope);
|
|
62
|
+
const project = await value("project", "Vercel project name", defaults.project ?? `ws-shell-${baseName}`);
|
|
63
|
+
const wsShellImage = args.from ?? process.env.WS_SHELL_IMAGE ?? defaults.from ?? "ghcr.io/v1xingyue/ws-shell:v1.1.alpine";
|
|
64
|
+
const prod = args.prod !== "false";
|
|
65
|
+
const dryRun = Boolean(args["dry-run"]);
|
|
66
|
+
const skipLink = Boolean(args["skip-link"]);
|
|
67
|
+
|
|
68
|
+
const oauthRedirectUrl =
|
|
69
|
+
args["redirect-url"] ??
|
|
70
|
+
process.env.OAUTH_REDIRECT_URL ??
|
|
71
|
+
defaults["redirect-url"] ??
|
|
72
|
+
`https://${project}.vercel.app/auth/github/callback`;
|
|
73
|
+
|
|
74
|
+
const appDir = path.join(workspaceRoot, ".vercel-vm-factory", ".generated", project);
|
|
75
|
+
const dockerfile = makeDockerfile({ baseImage, wsShellImage });
|
|
76
|
+
|
|
77
|
+
await rm(appDir, { recursive: true, force: true });
|
|
78
|
+
await mkdir(appDir, { recursive: true });
|
|
79
|
+
await writeFile(path.join(appDir, "Dockerfile.vercel"), dockerfile);
|
|
80
|
+
|
|
81
|
+
printSummary({
|
|
82
|
+
base: `${baseName} -> ${baseImage}`,
|
|
83
|
+
project,
|
|
84
|
+
scope: scope || "default",
|
|
85
|
+
source: wsShellImage,
|
|
86
|
+
callback: oauthRedirectUrl,
|
|
87
|
+
dockerfile: path.join(appDir, "Dockerfile.vercel"),
|
|
88
|
+
target: prod ? "production" : "preview",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (dryRun) {
|
|
92
|
+
ok("dry run: skipped vercel deploy");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await ensureVercelReady();
|
|
97
|
+
|
|
98
|
+
const githubClientId = await secret("client-id", "GitHub client id", process.env.GITHUB_CLIENT_ID ?? defaults["client-id"]);
|
|
99
|
+
const githubClientSecret = await secret(
|
|
100
|
+
"client-secret",
|
|
101
|
+
"GitHub client secret",
|
|
102
|
+
process.env.GITHUB_CLIENT_SECRET ?? defaults["client-secret"],
|
|
103
|
+
);
|
|
104
|
+
const allowedUserIds = await secret("github-userid", "Allowed GitHub numeric user id(s)", process.env.ALLOWED_USER_IDS ?? defaults["github-userid"]);
|
|
105
|
+
|
|
106
|
+
await writeDefaults(defaultsPath, {
|
|
107
|
+
...defaults,
|
|
108
|
+
base: baseName,
|
|
109
|
+
scope: scope || undefined,
|
|
110
|
+
project,
|
|
111
|
+
from: wsShellImage,
|
|
112
|
+
"client-id": githubClientId,
|
|
113
|
+
"client-secret": githubClientSecret,
|
|
114
|
+
"github-userid": allowedUserIds,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const commonArgs = [];
|
|
118
|
+
if (args.token) commonArgs.push("--token", args.token);
|
|
119
|
+
if (scope) commonArgs.push("--scope", scope);
|
|
120
|
+
|
|
121
|
+
if (!skipLink) {
|
|
122
|
+
step("Linking Vercel project");
|
|
123
|
+
await runNoUrl("vercel", ["link", "--yes", "--project", project, "--cwd", appDir, ...commonArgs]);
|
|
124
|
+
} else {
|
|
125
|
+
warn("skip-link enabled; using existing .vercel/project.json");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
step("Setting project framework=container");
|
|
129
|
+
await setContainerFramework(appDir, commonArgs);
|
|
130
|
+
|
|
131
|
+
const vercelArgs = [
|
|
132
|
+
"deploy",
|
|
133
|
+
appDir,
|
|
134
|
+
"--yes",
|
|
135
|
+
"--logs",
|
|
136
|
+
"--env",
|
|
137
|
+
`OAUTH_REDIRECT_URL=${oauthRedirectUrl}`,
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
if (githubClientId) vercelArgs.push("--env", `GITHUB_CLIENT_ID=${githubClientId}`);
|
|
141
|
+
if (githubClientSecret) vercelArgs.push("--env", `GITHUB_CLIENT_SECRET=${githubClientSecret}`);
|
|
142
|
+
if (allowedUserIds) vercelArgs.push("--env", `ALLOWED_USER_IDS=${allowedUserIds}`);
|
|
143
|
+
if (prod) vercelArgs.push("--prod");
|
|
144
|
+
vercelArgs.push(...commonArgs);
|
|
145
|
+
|
|
146
|
+
step("Deploying");
|
|
147
|
+
const deploymentUrl = await run("vercel", vercelArgs);
|
|
148
|
+
console.log(`\n${color.green("Deployment URL:")} ${color.bold(deploymentUrl)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function setContainerFramework(appDir, commonArgs) {
|
|
152
|
+
const projectFile = path.join(appDir, ".vercel", "project.json");
|
|
153
|
+
const projectConfig = JSON.parse(await readFile(projectFile, "utf8"));
|
|
154
|
+
if (!projectConfig.projectId) throw new Error(`Missing projectId in ${projectFile}`);
|
|
155
|
+
|
|
156
|
+
await runNoUrl("vercel", [
|
|
157
|
+
"api",
|
|
158
|
+
`/v10/projects/${projectConfig.projectId}`,
|
|
159
|
+
"-X",
|
|
160
|
+
"PATCH",
|
|
161
|
+
"-F",
|
|
162
|
+
"framework=container",
|
|
163
|
+
...commonArgs,
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function ensureVercelReady() {
|
|
168
|
+
step("Checking Vercel CLI");
|
|
169
|
+
try {
|
|
170
|
+
const version = await runCapture("vercel", ["--version"]);
|
|
171
|
+
ok(version.split("\n").filter(Boolean).at(-1) || "vercel installed");
|
|
172
|
+
} catch {
|
|
173
|
+
throw new Error("Vercel CLI is not installed. Install it with: pnpm add -g vercel");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
step("Checking Vercel login");
|
|
177
|
+
try {
|
|
178
|
+
const whoami = await runCapture("vercel", ["whoami"]);
|
|
179
|
+
ok(`logged in as ${whoami.trim()}`);
|
|
180
|
+
} catch {
|
|
181
|
+
warn("Vercel CLI is not logged in. Starting vercel login...");
|
|
182
|
+
await runNoUrl("vercel", ["login"]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function doctor() {
|
|
187
|
+
await ensureVercelReady();
|
|
188
|
+
console.log("");
|
|
189
|
+
console.log(color.bold("Saved defaults"));
|
|
190
|
+
printKeyValue("defaults file", defaultsPath);
|
|
191
|
+
printKeyValue("base", defaults.base || "not set");
|
|
192
|
+
printKeyValue("project", defaults.project || "not set");
|
|
193
|
+
printKeyValue("scope", defaults.scope || "not set");
|
|
194
|
+
printKeyValue("source image", defaults.from || "ghcr.io/v1xingyue/ws-shell:v1.1.alpine");
|
|
195
|
+
printKeyValue("client id", defaults["client-id"] ? mask(defaults["client-id"]) : "not set");
|
|
196
|
+
printKeyValue("client secret", defaults["client-secret"] ? mask(defaults["client-secret"]) : "not set");
|
|
197
|
+
printKeyValue("github userid", defaults["github-userid"] ? mask(defaults["github-userid"]) : "not set");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function makeDockerfile({ baseImage, wsShellImage }) {
|
|
201
|
+
return `ARG WS_SHELL_IMAGE=${wsShellImage}
|
|
202
|
+
ARG BASE_IMAGE=${baseImage}
|
|
203
|
+
|
|
204
|
+
FROM \${WS_SHELL_IMAGE} AS ws-shell
|
|
205
|
+
FROM \${BASE_IMAGE} AS base
|
|
206
|
+
|
|
207
|
+
# wsterm already embeds the web UI; runtime config comes from environment variables.
|
|
208
|
+
COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
|
|
209
|
+
|
|
210
|
+
WORKDIR /app
|
|
211
|
+
ENV ENABLE_SSL=false
|
|
212
|
+
EXPOSE 80
|
|
213
|
+
CMD ["/app/bin/wsterm","-bind",":80","-fork","/bin/sh"]
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function value(name, question, fallback) {
|
|
218
|
+
const current = args[name] ?? fallback;
|
|
219
|
+
if (args[name]) return args[name];
|
|
220
|
+
if (!input.isTTY) return current || "";
|
|
221
|
+
|
|
222
|
+
const rl = createInterface({ input, output });
|
|
223
|
+
const answer = (await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)).trim();
|
|
224
|
+
rl.close();
|
|
225
|
+
return answer || current || "";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function secret(name, question, fallback) {
|
|
229
|
+
if (args[name]) return args[name];
|
|
230
|
+
if (!input.isTTY) return fallback || "";
|
|
231
|
+
|
|
232
|
+
const rl = createInterface({ input, output });
|
|
233
|
+
const placeholder = fallback ? mask(fallback) : "skip";
|
|
234
|
+
const answer = (await rl.question(`${question} [${placeholder}]: `)).trim();
|
|
235
|
+
rl.close();
|
|
236
|
+
return answer || fallback || "";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function optionalValue(name, question, fallback) {
|
|
240
|
+
if (args[name] !== undefined) return args[name];
|
|
241
|
+
if (!input.isTTY) return "";
|
|
242
|
+
|
|
243
|
+
const rl = createInterface({ input, output });
|
|
244
|
+
const suffix = fallback ? ` [${fallback}; Enter to skip]` : " [skip]";
|
|
245
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
246
|
+
rl.close();
|
|
247
|
+
return answer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function chooseBase(fallback) {
|
|
251
|
+
if (args.base) return args.base;
|
|
252
|
+
if (!input.isTTY) return fallback;
|
|
253
|
+
|
|
254
|
+
const names = Object.keys(bases);
|
|
255
|
+
console.log(color.bold("Choose base image"));
|
|
256
|
+
names.forEach((name, index) => console.log(` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${bases[name]})`)}`));
|
|
257
|
+
console.log(` ${color.cyan("4")}. custom image`);
|
|
258
|
+
|
|
259
|
+
const rl = createInterface({ input, output });
|
|
260
|
+
const answer = (await rl.question(`Base [${fallback}]: `)).trim();
|
|
261
|
+
rl.close();
|
|
262
|
+
|
|
263
|
+
if (!answer) return fallback;
|
|
264
|
+
if (bases[answer]) return answer;
|
|
265
|
+
if (/^[1-3]$/.test(answer)) return names[Number(answer) - 1];
|
|
266
|
+
if (answer === "4") return value("custom-base", "Custom base image", defaults["custom-base"]);
|
|
267
|
+
return answer;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function readDefaults(file) {
|
|
271
|
+
try {
|
|
272
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
273
|
+
} catch {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function writeDefaults(file, data) {
|
|
279
|
+
const clean = Object.fromEntries(Object.entries(data).filter(([, value]) => value));
|
|
280
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
281
|
+
await writeFile(file, `${JSON.stringify(clean, null, 2)}\n`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function mask(value) {
|
|
285
|
+
if (value.length <= 8) return "********";
|
|
286
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseArgs(argv) {
|
|
290
|
+
const out = {};
|
|
291
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
292
|
+
const arg = argv[i];
|
|
293
|
+
if (!arg.startsWith("--")) continue;
|
|
294
|
+
|
|
295
|
+
const [rawKey, rawValue] = arg.slice(2).split("=", 2);
|
|
296
|
+
out[rawKey] = rawValue ?? (argv[i + 1]?.startsWith("--") ? true : argv[i + 1]) ?? true;
|
|
297
|
+
if (rawValue === undefined && argv[i + 1] && !argv[i + 1].startsWith("--")) i += 1;
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseCommand(argv) {
|
|
303
|
+
const known = new Set(["create", "doctor", "help"]);
|
|
304
|
+
const first = argv[0];
|
|
305
|
+
if (first && !first.startsWith("--")) {
|
|
306
|
+
return { command: known.has(first) ? first : first, args: parseArgs(argv.slice(1)) };
|
|
307
|
+
}
|
|
308
|
+
return { command: "create", args: parseArgs(argv) };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function run(command, commandArgs) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
let seenUrl = "";
|
|
314
|
+
const child = spawn(command, commandArgs, { stdio: ["inherit", "pipe", "pipe"] });
|
|
315
|
+
|
|
316
|
+
child.stdout.on("data", (chunk) => {
|
|
317
|
+
const text = chunk.toString();
|
|
318
|
+
output.write(text);
|
|
319
|
+
seenUrl = findLastUrl(text) || seenUrl;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
child.stderr.on("data", (chunk) => {
|
|
323
|
+
const text = chunk.toString();
|
|
324
|
+
output.write(text);
|
|
325
|
+
seenUrl = findLastUrl(text) || seenUrl;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
child.on("error", reject);
|
|
329
|
+
child.on("close", (code) => {
|
|
330
|
+
if (code === 0 && seenUrl) resolve(seenUrl);
|
|
331
|
+
else if (code === 0) reject(new Error("vercel finished but no deployment url was found"));
|
|
332
|
+
else reject(new Error(`vercel exited with code ${code}`));
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function runCapture(command, commandArgs) {
|
|
338
|
+
return new Promise((resolve, reject) => {
|
|
339
|
+
let text = "";
|
|
340
|
+
const child = spawn(command, commandArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
341
|
+
child.stdout.on("data", (chunk) => {
|
|
342
|
+
text += chunk.toString();
|
|
343
|
+
});
|
|
344
|
+
child.stderr.on("data", (chunk) => {
|
|
345
|
+
text += chunk.toString();
|
|
346
|
+
});
|
|
347
|
+
child.on("error", reject);
|
|
348
|
+
child.on("close", (code) => {
|
|
349
|
+
if (code === 0) resolve(text.trim());
|
|
350
|
+
else reject(new Error(text.trim() || `${command} exited with code ${code}`));
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function runNoUrl(command, commandArgs) {
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
const child = spawn(command, commandArgs, { stdio: "inherit" });
|
|
358
|
+
child.on("error", reject);
|
|
359
|
+
child.on("close", (code) => {
|
|
360
|
+
if (code === 0) resolve();
|
|
361
|
+
else reject(new Error(`${command} exited with code ${code}`));
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function findLastUrl(text) {
|
|
367
|
+
return text.match(/https:\/\/[^\s]+\.vercel\.app/g)?.at(-1) ?? "";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function printHeader() {
|
|
371
|
+
console.log(color.bold(color.cyan("Vercel VM Factory")));
|
|
372
|
+
console.log(color.dim("Build a Container deployment from a tiny Dockerfile.vercel"));
|
|
373
|
+
console.log("");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function printSummary(items) {
|
|
377
|
+
console.log(color.bold("Deployment plan"));
|
|
378
|
+
for (const [key, value] of Object.entries(items)) {
|
|
379
|
+
printKeyValue(key, value);
|
|
380
|
+
}
|
|
381
|
+
console.log("");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function printKeyValue(key, value) {
|
|
385
|
+
console.log(`${color.dim(`${key.padEnd(14)} `)}${value}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function step(text) {
|
|
389
|
+
console.log(`${color.cyan("->")} ${text}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function ok(text) {
|
|
393
|
+
console.log(`${color.green("OK")} ${text}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function warn(text) {
|
|
397
|
+
console.log(`${color.yellow("!")} ${text}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function paint(text, code) {
|
|
401
|
+
return colorEnabled ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function printHelp() {
|
|
405
|
+
printHeader();
|
|
406
|
+
console.log(`Usage:
|
|
407
|
+
vercel-vm-factory create
|
|
408
|
+
vercel-vm-factory create --base ubuntu --project x-shell
|
|
409
|
+
vercel-vm-factory doctor
|
|
410
|
+
npx vercel-vm-factory create
|
|
411
|
+
|
|
412
|
+
Options:
|
|
413
|
+
--base NAME alpine, ubuntu, debian, or a custom image
|
|
414
|
+
--project NAME Vercel project name
|
|
415
|
+
--scope SLUG Optional Vercel team/user scope slug
|
|
416
|
+
--from IMAGE Source image for /app/bin/wsterm
|
|
417
|
+
--client-id VALUE GitHub OAuth client id
|
|
418
|
+
--client-secret VAL GitHub OAuth client secret
|
|
419
|
+
--github-userid VAL Allowed GitHub numeric user id(s)
|
|
420
|
+
--redirect-url URL OAuth redirect URL
|
|
421
|
+
--prod=false Deploy preview instead of production
|
|
422
|
+
--skip-link Reuse existing generated .vercel/project.json
|
|
423
|
+
--dry-run Generate Dockerfile.vercel only
|
|
424
|
+
--doctor Check Vercel CLI/login and saved defaults
|
|
425
|
+
--help Show this help
|
|
426
|
+
`);
|
|
427
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vercel-vm-factory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create Vercel Container deployments for ws-shell from selectable base images.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vercel-vm-factory": "bin/vercel-vm-factory.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"deploy-vm.mjs",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"deploy": "node deploy-vm.mjs create",
|
|
17
|
+
"doctor": "node deploy-vm.mjs doctor",
|
|
18
|
+
"pack:check": "npm pack --dry-run"
|
|
19
|
+
}
|
|
20
|
+
}
|