vercel-vm-factory 0.1.2 → 0.1.5

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.
Files changed (2) hide show
  1. package/deploy-vm.mjs +167 -49
  2. 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.3.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 = { ...(await readDefaults(legacyDefaultsPath)), ...(await readDefaults(defaultsPath)) };
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(`\nScope not found. Leave scope empty, or set the real CLI slug with --scope.`);
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("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";
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(workspaceRoot, ".vercel-vm-factory", ".generated", project);
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("auth-user", "Basic auth username", process.env.AUTH_USERNAME ?? defaults["auth-user"])
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("auth-password", "Basic auth password", process.env.AUTH_PASSWORD ?? defaults["auth-password"])
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("client-id", "GitHub client id", process.env.GITHUB_CLIENT_ID ?? defaults["client-id"])
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("client-secret", "GitHub client secret", process.env.GITHUB_CLIENT_SECRET ?? defaults["client-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("github-userid", "Allowed GitHub numeric user id(s)", process.env.ALLOWED_USER_IDS ?? defaults["github-userid"])
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", ["link", "--yes", "--project", project, "--cwd", appDir, ...commonArgs]);
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)) vercelArgs.push("--env", `OAUTH_REDIRECT_URL=${oauthRedirectUrl}`);
155
- if (githubClientId) vercelArgs.push("--env", `GITHUB_CLIENT_ID=${githubClientId}`);
156
- if (githubClientSecret) vercelArgs.push("--env", `GITHUB_CLIENT_SECRET=${githubClientSecret}`);
157
- if (allowedUserIds) vercelArgs.push("--env", `ALLOWED_USER_IDS=${allowedUserIds}`);
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(`\n${color.green("Deployment URL:")} ${color.bold(deploymentUrl)}`);
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) throw new Error(`Missing projectId in ${projectFile}`);
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("Vercel CLI is not installed. Install it with: pnpm add -g vercel");
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("source image", defaults.from || "ghcr.io/v1xingyue/ws-shell:v1.1.alpine");
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("auth user", defaults["auth-user"] ? mask(defaults["auth-user"]) : "not set");
212
- printKeyValue("auth password", defaults["auth-password"] ? mask(defaults["auth-password"]) : "not set");
213
- printKeyValue("client id", defaults["client-id"] ? mask(defaults["client-id"]) : "not set");
214
- printKeyValue("client secret", defaults["client-secret"] ? mask(defaults["client-secret"]) : "not set");
215
- printKeyValue("github userid", defaults["github-userid"] ? mask(defaults["github-userid"]) : "not set");
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 = (await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)).trim();
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(args["auth-user"] || args["auth-password"] || process.env.AUTH_USERNAME || process.env.AUTH_PASSWORD);
271
- const hasGitHub = Boolean(args["client-id"] || args["client-secret"] || args["github-userid"] || process.env.GITHUB_CLIENT_ID || process.env.GITHUB_CLIENT_SECRET);
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"])) throw new Error("--auth-mode must be basic, github, both, or none");
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) => console.log(` ${color.cyan(String(index + 1))}. ${name} ${color.dim(`(${bases[name]})`)}`));
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") return value("custom-base", "Custom base image", defaults["custom-base"]);
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(Object.entries(data).filter(([, value]) => value));
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] = rawValue ?? (argv[i + 1]?.startsWith("--") ? true : argv[i + 1]) ?? true;
360
- if (rawValue === undefined && argv[i + 1] && !argv[i + 1].startsWith("--")) i += 1;
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 { command: known.has(first) ? first : first, args: parseArgs(argv.slice(1)) };
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, { stdio: ["inherit", "pipe", "pipe"] });
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) reject(new Error("vercel finished but no deployment url was found"));
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, { stdio: ["ignore", "pipe", "pipe"] });
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 reject(new Error(text.trim() || `${command} exited with code ${code}`));
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(color.dim("Build a Container deployment from a tiny Dockerfile.vercel"));
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(`${color.yellow("!")} Set this callback URL in your GitHub OAuth App before deploying:`);
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(color.dim(" GitHub: Settings -> Developer settings -> OAuth Apps -> Authorization callback URL"));
452
- console.log(color.dim(" User ID: open https://api.github.com/users/YOUR_LOGIN and copy the numeric id"));
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vercel-vm-factory",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Create Vercel Container deployments for ws-shell from selectable base images.",
5
5
  "license": "MIT",
6
6
  "type": "module",