vercel-vm-factory 0.1.2 → 0.1.3
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/deploy-vm.mjs +167 -49
- package/package.json +1 -1
package/deploy-vm.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
|
|
9
|
+
const defaultWsShellImage = "ghcr.io/v1xingyue/ws-shell:v1.2.alpine";
|
|
10
|
+
|
|
9
11
|
const bases = {
|
|
10
12
|
alpine: "alpine:3.23",
|
|
11
13
|
ubuntu: "ubuntu:24.04",
|
|
@@ -18,7 +20,10 @@ const workspaceRoot = process.cwd();
|
|
|
18
20
|
const stateRoot = path.join(homedir(), ".vercel-vm-factory");
|
|
19
21
|
const defaultsPath = path.join(stateRoot, "defaults.json");
|
|
20
22
|
const legacyDefaultsPath = path.join(scriptRoot, ".defaults.json");
|
|
21
|
-
const defaults = {
|
|
23
|
+
const defaults = {
|
|
24
|
+
...(await readDefaults(legacyDefaultsPath)),
|
|
25
|
+
...(await readDefaults(defaultsPath)),
|
|
26
|
+
};
|
|
22
27
|
const colorEnabled = output.isTTY && !process.env.NO_COLOR;
|
|
23
28
|
const color = {
|
|
24
29
|
dim: (text) => paint(text, "2"),
|
|
@@ -41,7 +46,9 @@ try {
|
|
|
41
46
|
} catch (error) {
|
|
42
47
|
const message = error instanceof Error ? error.message : String(error);
|
|
43
48
|
if (message.includes("scope does not exist")) {
|
|
44
|
-
console.error(
|
|
49
|
+
console.error(
|
|
50
|
+
`\nScope not found. Leave scope empty, or set the real CLI slug with --scope.`,
|
|
51
|
+
);
|
|
45
52
|
console.error(`If it was saved before, edit or delete: ${defaultsPath}`);
|
|
46
53
|
}
|
|
47
54
|
console.error(message);
|
|
@@ -58,9 +65,21 @@ async function main() {
|
|
|
58
65
|
|
|
59
66
|
const baseName = await chooseBase(args.base ?? defaults.base ?? "alpine");
|
|
60
67
|
const baseImage = bases[baseName] ?? baseName;
|
|
61
|
-
const scope = await optionalValue(
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
const scope = await optionalValue(
|
|
69
|
+
"scope",
|
|
70
|
+
"Vercel team/scope",
|
|
71
|
+
defaults.scope,
|
|
72
|
+
);
|
|
73
|
+
const project = await value(
|
|
74
|
+
"project",
|
|
75
|
+
"Vercel project name",
|
|
76
|
+
defaults.project ?? `ws-shell-${baseName}`,
|
|
77
|
+
);
|
|
78
|
+
const wsShellImage =
|
|
79
|
+
args.from ??
|
|
80
|
+
process.env.WS_SHELL_IMAGE ??
|
|
81
|
+
defaults.from ??
|
|
82
|
+
defaultWsShellImage;
|
|
64
83
|
const prod = args.prod !== "false";
|
|
65
84
|
const dryRun = Boolean(args["dry-run"]);
|
|
66
85
|
const skipLink = Boolean(args["skip-link"]);
|
|
@@ -72,7 +91,12 @@ async function main() {
|
|
|
72
91
|
defaults["redirect-url"] ??
|
|
73
92
|
`https://${project}.vercel.app/auth/github/callback`;
|
|
74
93
|
|
|
75
|
-
const appDir = path.join(
|
|
94
|
+
const appDir = path.join(
|
|
95
|
+
workspaceRoot,
|
|
96
|
+
".vercel-vm-factory",
|
|
97
|
+
".generated",
|
|
98
|
+
project,
|
|
99
|
+
);
|
|
76
100
|
const dockerfile = makeDockerfile({ baseImage, wsShellImage });
|
|
77
101
|
|
|
78
102
|
await rm(appDir, { recursive: true, force: true });
|
|
@@ -99,19 +123,39 @@ async function main() {
|
|
|
99
123
|
if (usesGitHubAuth(authMode)) printOAuthGuide(oauthRedirectUrl);
|
|
100
124
|
|
|
101
125
|
const authUsername = usesBasicAuth(authMode)
|
|
102
|
-
? await secret(
|
|
126
|
+
? await secret(
|
|
127
|
+
"auth-user",
|
|
128
|
+
"Basic auth username",
|
|
129
|
+
process.env.AUTH_USERNAME ?? defaults["auth-user"],
|
|
130
|
+
)
|
|
103
131
|
: "";
|
|
104
132
|
const authPassword = usesBasicAuth(authMode)
|
|
105
|
-
? await secret(
|
|
133
|
+
? await secret(
|
|
134
|
+
"auth-password",
|
|
135
|
+
"Basic auth password",
|
|
136
|
+
process.env.AUTH_PASSWORD ?? defaults["auth-password"],
|
|
137
|
+
)
|
|
106
138
|
: "";
|
|
107
139
|
const githubClientId = usesGitHubAuth(authMode)
|
|
108
|
-
? await secret(
|
|
140
|
+
? await secret(
|
|
141
|
+
"client-id",
|
|
142
|
+
"GitHub client id",
|
|
143
|
+
process.env.GITHUB_CLIENT_ID ?? defaults["client-id"],
|
|
144
|
+
)
|
|
109
145
|
: "";
|
|
110
146
|
const githubClientSecret = usesGitHubAuth(authMode)
|
|
111
|
-
? await secret(
|
|
147
|
+
? await secret(
|
|
148
|
+
"client-secret",
|
|
149
|
+
"GitHub client secret",
|
|
150
|
+
process.env.GITHUB_CLIENT_SECRET ?? defaults["client-secret"],
|
|
151
|
+
)
|
|
112
152
|
: "";
|
|
113
153
|
const allowedUserIds = usesGitHubAuth(authMode)
|
|
114
|
-
? await secret(
|
|
154
|
+
? await secret(
|
|
155
|
+
"github-userid",
|
|
156
|
+
"Allowed GitHub numeric user id(s)",
|
|
157
|
+
process.env.ALLOWED_USER_IDS ?? defaults["github-userid"],
|
|
158
|
+
)
|
|
115
159
|
: "";
|
|
116
160
|
|
|
117
161
|
await writeDefaults(defaultsPath, {
|
|
@@ -134,7 +178,15 @@ async function main() {
|
|
|
134
178
|
|
|
135
179
|
if (!skipLink) {
|
|
136
180
|
step("Linking Vercel project");
|
|
137
|
-
await runNoUrl("vercel", [
|
|
181
|
+
await runNoUrl("vercel", [
|
|
182
|
+
"link",
|
|
183
|
+
"--yes",
|
|
184
|
+
"--project",
|
|
185
|
+
project,
|
|
186
|
+
"--cwd",
|
|
187
|
+
appDir,
|
|
188
|
+
...commonArgs,
|
|
189
|
+
]);
|
|
138
190
|
} else {
|
|
139
191
|
warn("skip-link enabled; using existing .vercel/project.json");
|
|
140
192
|
}
|
|
@@ -142,31 +194,33 @@ async function main() {
|
|
|
142
194
|
step("Setting project framework=container");
|
|
143
195
|
await setContainerFramework(appDir, commonArgs);
|
|
144
196
|
|
|
145
|
-
const vercelArgs = [
|
|
146
|
-
"deploy",
|
|
147
|
-
appDir,
|
|
148
|
-
"--yes",
|
|
149
|
-
"--logs",
|
|
150
|
-
];
|
|
197
|
+
const vercelArgs = ["deploy", appDir, "--yes", "--logs"];
|
|
151
198
|
|
|
152
199
|
if (authUsername) vercelArgs.push("--env", `AUTH_USERNAME=${authUsername}`);
|
|
153
200
|
if (authPassword) vercelArgs.push("--env", `AUTH_PASSWORD=${authPassword}`);
|
|
154
|
-
if (usesGitHubAuth(authMode))
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
201
|
+
if (usesGitHubAuth(authMode))
|
|
202
|
+
vercelArgs.push("--env", `OAUTH_REDIRECT_URL=${oauthRedirectUrl}`);
|
|
203
|
+
if (githubClientId)
|
|
204
|
+
vercelArgs.push("--env", `GITHUB_CLIENT_ID=${githubClientId}`);
|
|
205
|
+
if (githubClientSecret)
|
|
206
|
+
vercelArgs.push("--env", `GITHUB_CLIENT_SECRET=${githubClientSecret}`);
|
|
207
|
+
if (allowedUserIds)
|
|
208
|
+
vercelArgs.push("--env", `ALLOWED_USER_IDS=${allowedUserIds}`);
|
|
158
209
|
if (prod) vercelArgs.push("--prod");
|
|
159
210
|
vercelArgs.push(...commonArgs);
|
|
160
211
|
|
|
161
212
|
step("Deploying");
|
|
162
213
|
const deploymentUrl = await run("vercel", vercelArgs);
|
|
163
|
-
console.log(
|
|
214
|
+
console.log(
|
|
215
|
+
`\n${color.green("Deployment URL:")} ${color.bold(deploymentUrl)}`,
|
|
216
|
+
);
|
|
164
217
|
}
|
|
165
218
|
|
|
166
219
|
async function setContainerFramework(appDir, commonArgs) {
|
|
167
220
|
const projectFile = path.join(appDir, ".vercel", "project.json");
|
|
168
221
|
const projectConfig = JSON.parse(await readFile(projectFile, "utf8"));
|
|
169
|
-
if (!projectConfig.projectId)
|
|
222
|
+
if (!projectConfig.projectId)
|
|
223
|
+
throw new Error(`Missing projectId in ${projectFile}`);
|
|
170
224
|
|
|
171
225
|
await runNoUrl("vercel", [
|
|
172
226
|
"api",
|
|
@@ -185,7 +239,9 @@ async function ensureVercelReady() {
|
|
|
185
239
|
const version = await runCapture("vercel", ["--version"]);
|
|
186
240
|
ok(version.split("\n").filter(Boolean).at(-1) || "vercel installed");
|
|
187
241
|
} catch {
|
|
188
|
-
throw new Error(
|
|
242
|
+
throw new Error(
|
|
243
|
+
"Vercel CLI is not installed. Install it with: pnpm add -g vercel",
|
|
244
|
+
);
|
|
189
245
|
}
|
|
190
246
|
|
|
191
247
|
step("Checking Vercel login");
|
|
@@ -206,13 +262,31 @@ async function doctor() {
|
|
|
206
262
|
printKeyValue("base", defaults.base || "not set");
|
|
207
263
|
printKeyValue("project", defaults.project || "not set");
|
|
208
264
|
printKeyValue("scope", defaults.scope || "not set");
|
|
209
|
-
printKeyValue(
|
|
265
|
+
printKeyValue(
|
|
266
|
+
"source image",
|
|
267
|
+
defaults.from || "ghcr.io/v1xingyue/ws-shell:v1.1.alpine",
|
|
268
|
+
);
|
|
210
269
|
printKeyValue("auth mode", defaults["auth-mode"] || "not set");
|
|
211
|
-
printKeyValue(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
printKeyValue(
|
|
270
|
+
printKeyValue(
|
|
271
|
+
"auth user",
|
|
272
|
+
defaults["auth-user"] ? mask(defaults["auth-user"]) : "not set",
|
|
273
|
+
);
|
|
274
|
+
printKeyValue(
|
|
275
|
+
"auth password",
|
|
276
|
+
defaults["auth-password"] ? mask(defaults["auth-password"]) : "not set",
|
|
277
|
+
);
|
|
278
|
+
printKeyValue(
|
|
279
|
+
"client id",
|
|
280
|
+
defaults["client-id"] ? mask(defaults["client-id"]) : "not set",
|
|
281
|
+
);
|
|
282
|
+
printKeyValue(
|
|
283
|
+
"client secret",
|
|
284
|
+
defaults["client-secret"] ? mask(defaults["client-secret"]) : "not set",
|
|
285
|
+
);
|
|
286
|
+
printKeyValue(
|
|
287
|
+
"github userid",
|
|
288
|
+
defaults["github-userid"] ? mask(defaults["github-userid"]) : "not set",
|
|
289
|
+
);
|
|
216
290
|
}
|
|
217
291
|
|
|
218
292
|
function makeDockerfile({ baseImage, wsShellImage }) {
|
|
@@ -238,7 +312,9 @@ async function value(name, question, fallback) {
|
|
|
238
312
|
if (!input.isTTY) return current || "";
|
|
239
313
|
|
|
240
314
|
const rl = createInterface({ input, output });
|
|
241
|
-
const answer = (
|
|
315
|
+
const answer = (
|
|
316
|
+
await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)
|
|
317
|
+
).trim();
|
|
242
318
|
rl.close();
|
|
243
319
|
return answer || current || "";
|
|
244
320
|
}
|
|
@@ -267,8 +343,19 @@ async function optionalValue(name, question, fallback) {
|
|
|
267
343
|
|
|
268
344
|
function defaultAuthMode() {
|
|
269
345
|
if (args["auth-mode"]) return args["auth-mode"];
|
|
270
|
-
const hasBasic = Boolean(
|
|
271
|
-
|
|
346
|
+
const hasBasic = Boolean(
|
|
347
|
+
args["auth-user"] ||
|
|
348
|
+
args["auth-password"] ||
|
|
349
|
+
process.env.AUTH_USERNAME ||
|
|
350
|
+
process.env.AUTH_PASSWORD,
|
|
351
|
+
);
|
|
352
|
+
const hasGitHub = Boolean(
|
|
353
|
+
args["client-id"] ||
|
|
354
|
+
args["client-secret"] ||
|
|
355
|
+
args["github-userid"] ||
|
|
356
|
+
process.env.GITHUB_CLIENT_ID ||
|
|
357
|
+
process.env.GITHUB_CLIENT_SECRET,
|
|
358
|
+
);
|
|
272
359
|
if (hasBasic && hasGitHub) return "both";
|
|
273
360
|
if (hasBasic) return "basic";
|
|
274
361
|
if (hasGitHub) return "github";
|
|
@@ -278,7 +365,8 @@ function defaultAuthMode() {
|
|
|
278
365
|
async function chooseAuthMode(fallback) {
|
|
279
366
|
const modes = new Set(["basic", "github", "both", "none"]);
|
|
280
367
|
if (args["auth-mode"]) {
|
|
281
|
-
if (!modes.has(args["auth-mode"]))
|
|
368
|
+
if (!modes.has(args["auth-mode"]))
|
|
369
|
+
throw new Error("--auth-mode must be basic, github, both, or none");
|
|
282
370
|
return args["auth-mode"];
|
|
283
371
|
}
|
|
284
372
|
if (!input.isTTY) return modes.has(fallback) ? fallback : "basic";
|
|
@@ -316,7 +404,11 @@ async function chooseBase(fallback) {
|
|
|
316
404
|
|
|
317
405
|
const names = Object.keys(bases);
|
|
318
406
|
console.log(color.bold("Choose base image"));
|
|
319
|
-
names.forEach((name, index) =>
|
|
407
|
+
names.forEach((name, index) =>
|
|
408
|
+
console.log(
|
|
409
|
+
` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${bases[name]})`)}`,
|
|
410
|
+
),
|
|
411
|
+
);
|
|
320
412
|
console.log(` ${color.cyan("4")}. custom image`);
|
|
321
413
|
|
|
322
414
|
const rl = createInterface({ input, output });
|
|
@@ -326,7 +418,8 @@ async function chooseBase(fallback) {
|
|
|
326
418
|
if (!answer) return fallback;
|
|
327
419
|
if (bases[answer]) return answer;
|
|
328
420
|
if (/^[1-3]$/.test(answer)) return names[Number(answer) - 1];
|
|
329
|
-
if (answer === "4")
|
|
421
|
+
if (answer === "4")
|
|
422
|
+
return value("custom-base", "Custom base image", defaults["custom-base"]);
|
|
330
423
|
return answer;
|
|
331
424
|
}
|
|
332
425
|
|
|
@@ -339,7 +432,9 @@ async function readDefaults(file) {
|
|
|
339
432
|
}
|
|
340
433
|
|
|
341
434
|
async function writeDefaults(file, data) {
|
|
342
|
-
const clean = Object.fromEntries(
|
|
435
|
+
const clean = Object.fromEntries(
|
|
436
|
+
Object.entries(data).filter(([, value]) => value),
|
|
437
|
+
);
|
|
343
438
|
await mkdir(path.dirname(file), { recursive: true });
|
|
344
439
|
await writeFile(file, `${JSON.stringify(clean, null, 2)}\n`);
|
|
345
440
|
}
|
|
@@ -356,8 +451,10 @@ function parseArgs(argv) {
|
|
|
356
451
|
if (!arg.startsWith("--")) continue;
|
|
357
452
|
|
|
358
453
|
const [rawKey, rawValue] = arg.slice(2).split("=", 2);
|
|
359
|
-
out[rawKey] =
|
|
360
|
-
|
|
454
|
+
out[rawKey] =
|
|
455
|
+
rawValue ?? (argv[i + 1]?.startsWith("--") ? true : argv[i + 1]) ?? true;
|
|
456
|
+
if (rawValue === undefined && argv[i + 1] && !argv[i + 1].startsWith("--"))
|
|
457
|
+
i += 1;
|
|
361
458
|
}
|
|
362
459
|
return out;
|
|
363
460
|
}
|
|
@@ -366,7 +463,10 @@ function parseCommand(argv) {
|
|
|
366
463
|
const known = new Set(["create", "doctor", "help"]);
|
|
367
464
|
const first = argv[0];
|
|
368
465
|
if (first && !first.startsWith("--")) {
|
|
369
|
-
return {
|
|
466
|
+
return {
|
|
467
|
+
command: known.has(first) ? first : first,
|
|
468
|
+
args: parseArgs(argv.slice(1)),
|
|
469
|
+
};
|
|
370
470
|
}
|
|
371
471
|
return { command: "create", args: parseArgs(argv) };
|
|
372
472
|
}
|
|
@@ -374,7 +474,9 @@ function parseCommand(argv) {
|
|
|
374
474
|
function run(command, commandArgs) {
|
|
375
475
|
return new Promise((resolve, reject) => {
|
|
376
476
|
let seenUrl = "";
|
|
377
|
-
const child = spawn(command, commandArgs, {
|
|
477
|
+
const child = spawn(command, commandArgs, {
|
|
478
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
479
|
+
});
|
|
378
480
|
|
|
379
481
|
child.stdout.on("data", (chunk) => {
|
|
380
482
|
const text = chunk.toString();
|
|
@@ -391,7 +493,8 @@ function run(command, commandArgs) {
|
|
|
391
493
|
child.on("error", reject);
|
|
392
494
|
child.on("close", (code) => {
|
|
393
495
|
if (code === 0 && seenUrl) resolve(seenUrl);
|
|
394
|
-
else if (code === 0)
|
|
496
|
+
else if (code === 0)
|
|
497
|
+
reject(new Error("vercel finished but no deployment url was found"));
|
|
395
498
|
else reject(new Error(`vercel exited with code ${code}`));
|
|
396
499
|
});
|
|
397
500
|
});
|
|
@@ -400,7 +503,9 @@ function run(command, commandArgs) {
|
|
|
400
503
|
function runCapture(command, commandArgs) {
|
|
401
504
|
return new Promise((resolve, reject) => {
|
|
402
505
|
let text = "";
|
|
403
|
-
const child = spawn(command, commandArgs, {
|
|
506
|
+
const child = spawn(command, commandArgs, {
|
|
507
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
508
|
+
});
|
|
404
509
|
child.stdout.on("data", (chunk) => {
|
|
405
510
|
text += chunk.toString();
|
|
406
511
|
});
|
|
@@ -410,7 +515,8 @@ function runCapture(command, commandArgs) {
|
|
|
410
515
|
child.on("error", reject);
|
|
411
516
|
child.on("close", (code) => {
|
|
412
517
|
if (code === 0) resolve(text.trim());
|
|
413
|
-
else
|
|
518
|
+
else
|
|
519
|
+
reject(new Error(text.trim() || `${command} exited with code ${code}`));
|
|
414
520
|
});
|
|
415
521
|
});
|
|
416
522
|
}
|
|
@@ -432,7 +538,9 @@ function findLastUrl(text) {
|
|
|
432
538
|
|
|
433
539
|
function printHeader() {
|
|
434
540
|
console.log(color.bold(color.cyan("Vercel VM Factory")));
|
|
435
|
-
console.log(
|
|
541
|
+
console.log(
|
|
542
|
+
color.dim("Build a Container deployment from a tiny Dockerfile.vercel"),
|
|
543
|
+
);
|
|
436
544
|
console.log("");
|
|
437
545
|
}
|
|
438
546
|
|
|
@@ -446,10 +554,20 @@ function printSummary(items) {
|
|
|
446
554
|
|
|
447
555
|
function printOAuthGuide(callbackUrl) {
|
|
448
556
|
console.log(color.bold("GitHub OAuth"));
|
|
449
|
-
console.log(
|
|
557
|
+
console.log(
|
|
558
|
+
`${color.yellow("!")} Set this callback URL in your GitHub OAuth App before deploying:`,
|
|
559
|
+
);
|
|
450
560
|
console.log(` ${color.bold(callbackUrl)}`);
|
|
451
|
-
console.log(
|
|
452
|
-
|
|
561
|
+
console.log(
|
|
562
|
+
color.dim(
|
|
563
|
+
" GitHub: Settings -> Developer settings -> OAuth Apps -> Authorization callback URL",
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
console.log(
|
|
567
|
+
color.dim(
|
|
568
|
+
" User ID: open https://api.github.com/users/YOUR_LOGIN and copy the numeric id",
|
|
569
|
+
),
|
|
570
|
+
);
|
|
453
571
|
console.log("");
|
|
454
572
|
}
|
|
455
573
|
|