vercel-vm-factory 0.9.8 → 0.11.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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/deploy-vm.mjs +174 -119
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -78,10 +78,12 @@ Choosing bash or zsh adds the matching package to the generated Dockerfile when
78
78
 
79
79
  Generated shell setup examples:
80
80
 
81
- - Alpine + `/bin/zsh`: installs `zsh curl git`, then installs oh-my-zsh unattended.
81
+ - Alpine + `/bin/zsh`: installs `zsh curl git`, installs oh-my-zsh unattended, and writes `/root/.zshrc`.
82
82
  - Ubuntu/Debian + `/bin/bash`: installs `bash` with `apt-get`.
83
83
  - `/bin/sh`: no extra shell package is installed.
84
84
 
85
+ The generated Dockerfile sets `HOME=/root` and `SHELL` to the selected shell path.
86
+
85
87
  Preinstall tools:
86
88
 
87
89
  - `nodejs`
package/deploy-vm.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { spawn } from "node:child_process";
4
- import { createInterface } from "node:readline/promises";
5
4
  import { stdin as input, stdout as output } from "node:process";
6
5
  import { homedir } from "node:os";
7
6
  import path from "node:path";
7
+ import * as p from "@clack/prompts";
8
8
 
9
9
  const defaultWsShellImage = "ghcr.io/v1xingyue/ws-shell:v1.8.alpine";
10
10
 
@@ -187,43 +187,30 @@ async function main() {
187
187
  )
188
188
  : "";
189
189
 
190
- await writeDefaults(defaultsPath, {
191
- ...defaults,
192
- "vm-image": vmImageName,
193
- scope: scope || undefined,
194
- project,
195
- from: wsShellImage,
196
- shell,
197
- tools,
198
- "auth-mode": authMode,
199
- "auth-user": authUsername,
200
- "auth-password": authPassword,
201
- "client-id": githubClientId,
202
- "client-secret": githubClientSecret,
203
- "github-userid": allowedUserIds,
204
- });
205
-
206
- const commonArgs = [];
207
- if (args.token) commonArgs.push("--token", args.token);
208
- if (scope) commonArgs.push("--scope", scope);
190
+ let activeScope = scope;
191
+ const commonArgs = () => makeCommonArgs(activeScope);
209
192
 
210
193
  if (!skipLink) {
211
194
  step("Linking Vercel project");
212
- await runNoUrl("vercel", [
213
- "link",
214
- "--yes",
215
- "--project",
216
- project,
217
- "--cwd",
218
- appDir,
219
- ...commonArgs,
220
- ]);
195
+ await withScopeFallback(activeScope, async () =>
196
+ runNoUrl("vercel", [
197
+ "link",
198
+ "--yes",
199
+ "--project",
200
+ project,
201
+ "--cwd",
202
+ appDir,
203
+ ...commonArgs(),
204
+ ]),
205
+ );
221
206
  } else {
222
207
  warn("skip-link enabled; using existing .vercel/project.json");
223
208
  }
224
209
 
225
210
  step("Setting project framework=container");
226
- await setContainerFramework(appDir, commonArgs);
211
+ await withScopeFallback(activeScope, async () =>
212
+ setContainerFramework(appDir, commonArgs()),
213
+ );
227
214
 
228
215
  const vercelArgs = ["deploy", appDir, "--yes", "--logs"];
229
216
 
@@ -238,13 +225,40 @@ async function main() {
238
225
  if (allowedUserIds)
239
226
  vercelArgs.push("--env", `ALLOWED_USER_IDS=${allowedUserIds}`);
240
227
  if (prod) vercelArgs.push("--prod");
241
- vercelArgs.push(...commonArgs);
242
228
 
243
229
  step("Deploying");
244
- const deploymentUrl = await run("vercel", vercelArgs);
230
+ const deploymentUrl = await withScopeFallback(activeScope, async () =>
231
+ run("vercel", [...vercelArgs, ...commonArgs()]),
232
+ );
233
+ await writeDefaults(defaultsPath, {
234
+ ...defaults,
235
+ "vm-image": vmImageName,
236
+ scope: activeScope || undefined,
237
+ project,
238
+ from: wsShellImage,
239
+ shell,
240
+ tools,
241
+ "auth-mode": authMode,
242
+ "auth-user": authUsername,
243
+ "auth-password": authPassword,
244
+ "client-id": githubClientId,
245
+ "client-secret": githubClientSecret,
246
+ "github-userid": allowedUserIds,
247
+ });
245
248
  console.log(
246
249
  `\n${color.green("Deployment URL:")} ${color.bold(deploymentUrl)}`,
247
250
  );
251
+
252
+ async function withScopeFallback(scopeValue, action) {
253
+ try {
254
+ return await action();
255
+ } catch (error) {
256
+ if (!scopeValue || !isMissingScope(error)) throw error;
257
+ warn(`scope "${scopeValue}" not found; retrying with default Vercel scope`);
258
+ activeScope = "";
259
+ return action();
260
+ }
261
+ }
248
262
  }
249
263
 
250
264
  async function setContainerFramework(appDir, commonArgs) {
@@ -294,15 +308,14 @@ async function installVercelCli() {
294
308
  );
295
309
 
296
310
  const install = await choosePackageInstall();
297
- const answer = (
298
- await askText(
299
- `Vercel CLI is not installed. Install with ${install.command} ${install.args.join(" ")}?`,
300
- "N",
301
- )
302
- ).toLowerCase();
311
+ const answer = await promptResult(
312
+ p.confirm({
313
+ message: `Vercel CLI is not installed. Install with ${install.command} ${install.args.join(" ")}?`,
314
+ initialValue: false,
315
+ }),
316
+ );
303
317
 
304
- if (answer !== "y" && answer !== "yes")
305
- throw new Error("Vercel CLI is required. Exiting.");
318
+ if (!answer) throw new Error("Vercel CLI is required. Exiting.");
306
319
 
307
320
  step(`Installing Vercel CLI with ${install.command}`);
308
321
  await runNoUrl(install.command, install.args);
@@ -367,7 +380,13 @@ function makeDockerfile({ shell, tools, vmImage, vmImageName, wsShellImage }) {
367
380
  const shellInstall = makeShellInstall({ shell, vmImageName });
368
381
  const ohMyZshInstall =
369
382
  path.basename(shell) === "zsh"
370
- ? `RUN RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended`
383
+ ? `RUN RUNZSH=no CHSH=no KEEP_ZSHRC=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended \\
384
+ && printf '%s\\n' \\
385
+ 'export ZSH="$HOME/.oh-my-zsh"' \\
386
+ 'ZSH_THEME="robbyrussell"' \\
387
+ 'plugins=(git)' \\
388
+ 'source "$ZSH/oh-my-zsh.sh"' \\
389
+ > /root/.zshrc`
371
390
  : "";
372
391
  const toolInstall = makeToolInstall({ tools, vmImageName });
373
392
  const shellSetup = [shellInstall, ohMyZshInstall, toolInstall]
@@ -384,6 +403,8 @@ COPY --from=ws-shell /app/bin/wsterm /app/bin/wsterm
384
403
 
385
404
  WORKDIR /app
386
405
  ENV ENABLE_SSL=false
406
+ ENV HOME=/root
407
+ ENV SHELL=${shell}
387
408
  EXPOSE 80
388
409
  CMD ["/app/bin/wsterm","-bind",":80","-fork","${shell}"]
389
410
  `;
@@ -468,7 +489,7 @@ async function secret(name, question, fallback) {
468
489
  if (!input.isTTY) return fallback || "";
469
490
 
470
491
  const placeholder = fallback ? mask(fallback) : "skip";
471
- const answer = await askText(question, placeholder);
492
+ const answer = await askSecret(question, placeholder);
472
493
  return answer || fallback || "";
473
494
  }
474
495
 
@@ -513,22 +534,18 @@ async function chooseAuthMode(fallback) {
513
534
  }
514
535
  if (!input.isTTY) return modes.has(fallback) ? fallback : "basic";
515
536
 
516
- printChoices("Authentication", [
517
- ["1", "basic", "username/password"],
518
- ["2", "github", "GitHub OAuth"],
519
- ["3", "both", "basic + GitHub OAuth"],
520
- ["4", "none", "no app auth"],
521
- ]);
522
-
523
- const answer = await askText("Authentication", fallback);
524
-
525
- if (!answer) return modes.has(fallback) ? fallback : "basic";
526
- if (modes.has(answer)) return answer;
527
- if (answer === "1") return "basic";
528
- if (answer === "2") return "github";
529
- if (answer === "3") return "both";
530
- if (answer === "4") return "none";
531
- throw new Error("Authentication must be basic, github, both, or none");
537
+ return promptResult(
538
+ p.select({
539
+ message: "Authentication",
540
+ initialValue: modes.has(fallback) ? fallback : "basic",
541
+ options: [
542
+ { value: "basic", label: "Basic", hint: "username/password" },
543
+ { value: "github", label: "GitHub OAuth" },
544
+ { value: "both", label: "Basic + GitHub OAuth" },
545
+ { value: "none", label: "None", hint: "no app auth" },
546
+ ],
547
+ }),
548
+ );
532
549
  }
533
550
 
534
551
  function usesBasicAuth(mode) {
@@ -545,25 +562,23 @@ async function chooseVmImage(fallback) {
545
562
  if (!input.isTTY) return fallback;
546
563
 
547
564
  const names = Object.keys(vmImages);
548
- printChoices(
549
- "VM image",
550
- names.map((name, index) => [
551
- String(index + 1),
552
- name,
553
- vmImages[name],
554
- ]).concat([["4", "custom", "enter a full image name"]]),
565
+ const answer = await promptResult(
566
+ p.select({
567
+ message: "VM image",
568
+ initialValue: vmImages[fallback] ? fallback : "custom",
569
+ options: names
570
+ .map((name) => ({ value: name, label: name, hint: vmImages[name] }))
571
+ .concat([
572
+ { value: "custom", label: "Custom", hint: "enter a full image name" },
573
+ ]),
574
+ }),
555
575
  );
556
576
 
557
- const answer = await askText("VM image", fallback);
558
-
559
- if (!answer) return fallback;
560
- if (vmImages[answer]) return answer;
561
- if (/^[1-3]$/.test(answer)) return names[Number(answer) - 1];
562
- if (answer === "4")
577
+ if (answer === "custom")
563
578
  return value(
564
579
  "custom-vm-image",
565
580
  "Custom VM image",
566
- defaults["custom-vm-image"],
581
+ vmImages[fallback] ? defaults["custom-vm-image"] : fallback,
567
582
  );
568
583
  return answer;
569
584
  }
@@ -572,21 +587,17 @@ async function chooseShell(fallback) {
572
587
  if (args.shell) return args.shell;
573
588
  if (!input.isTTY) return fallback;
574
589
 
575
- printChoices(
576
- "Shell",
577
- shells.map((shell, index) => [
578
- String(index + 1),
579
- shell,
580
- index === shells.length - 1 ? "default" : "",
581
- ]),
590
+ return promptResult(
591
+ p.select({
592
+ message: "Shell",
593
+ initialValue: shells.includes(fallback) ? fallback : "/bin/sh",
594
+ options: shells.map((shell, index) => ({
595
+ value: shell,
596
+ label: shell,
597
+ hint: index === shells.length - 1 ? "default" : undefined,
598
+ })),
599
+ }),
582
600
  );
583
-
584
- const answer = await askText("Shell", fallback);
585
-
586
- if (!answer) return fallback;
587
- if (shells.includes(answer)) return answer;
588
- if (/^[1-3]$/.test(answer)) return shells[Number(answer) - 1];
589
- return answer;
590
601
  }
591
602
 
592
603
  async function chooseTools(fallback) {
@@ -594,17 +605,20 @@ async function chooseTools(fallback) {
594
605
  if (!input.isTTY) return parseTools(fallback).join(",");
595
606
 
596
607
  const names = Object.keys(toolChoices);
597
- printChoices(
598
- "Preinstall tools",
599
- names.map((name, index) => [
600
- String(index + 1),
601
- name,
602
- toolChoices[name],
603
- ]).concat([["0", "none", "default"]]),
608
+ const selected = await promptResult(
609
+ p.multiselect({
610
+ message: "Preinstall tools",
611
+ required: false,
612
+ initialValues: parseTools(fallback),
613
+ options: names.map((name) => ({
614
+ value: name,
615
+ label: name,
616
+ hint: toolChoices[name],
617
+ })),
618
+ }),
604
619
  );
605
620
 
606
- const answer = await askText("Tools (comma separated)", fallback || "none");
607
- return parseTools(answer || fallback).join(",");
621
+ return selected.join(",");
608
622
  }
609
623
 
610
624
  function parseTools(value) {
@@ -713,23 +727,37 @@ function parseCommand(argv) {
713
727
  return { command: "create", args: parseArgs(argv) };
714
728
  }
715
729
 
730
+ function makeCommonArgs(scope) {
731
+ const commonArgs = [];
732
+ if (args.token) commonArgs.push("--token", args.token);
733
+ if (scope) commonArgs.push("--scope", scope);
734
+ return commonArgs;
735
+ }
736
+
737
+ function isMissingScope(error) {
738
+ return String(error?.message || error).includes("scope does not exist");
739
+ }
740
+
716
741
  function run(command, commandArgs) {
717
742
  return new Promise((resolve, reject) => {
718
743
  let seenUrl = "";
744
+ let text = "";
719
745
  const child = spawn(command, commandArgs, {
720
746
  stdio: ["inherit", "pipe", "pipe"],
721
747
  });
722
748
 
723
749
  child.stdout.on("data", (chunk) => {
724
- const text = chunk.toString();
725
- output.write(text);
726
- seenUrl = findLastUrl(text) || seenUrl;
750
+ const chunkText = chunk.toString();
751
+ text += chunkText;
752
+ output.write(chunkText);
753
+ seenUrl = findLastUrl(chunkText) || seenUrl;
727
754
  });
728
755
 
729
756
  child.stderr.on("data", (chunk) => {
730
- const text = chunk.toString();
731
- output.write(text);
732
- seenUrl = findLastUrl(text) || seenUrl;
757
+ const chunkText = chunk.toString();
758
+ text += chunkText;
759
+ output.write(chunkText);
760
+ seenUrl = findLastUrl(chunkText) || seenUrl;
733
761
  });
734
762
 
735
763
  child.on("error", reject);
@@ -737,7 +765,7 @@ function run(command, commandArgs) {
737
765
  if (code === 0 && seenUrl) resolve(seenUrl);
738
766
  else if (code === 0)
739
767
  reject(new Error("vercel finished but no deployment url was found"));
740
- else reject(new Error(`vercel exited with code ${code}`));
768
+ else reject(new Error(text.trim() || `vercel exited with code ${code}`));
741
769
  });
742
770
  });
743
771
  }
@@ -765,11 +793,24 @@ function runCapture(command, commandArgs) {
765
793
 
766
794
  function runNoUrl(command, commandArgs) {
767
795
  return new Promise((resolve, reject) => {
768
- const child = spawn(command, commandArgs, { stdio: "inherit" });
796
+ let text = "";
797
+ const child = spawn(command, commandArgs, {
798
+ stdio: ["inherit", "pipe", "pipe"],
799
+ });
800
+ child.stdout.on("data", (chunk) => {
801
+ const chunkText = chunk.toString();
802
+ text += chunkText;
803
+ output.write(chunkText);
804
+ });
805
+ child.stderr.on("data", (chunk) => {
806
+ const chunkText = chunk.toString();
807
+ text += chunkText;
808
+ output.write(chunkText);
809
+ });
769
810
  child.on("error", reject);
770
811
  child.on("close", (code) => {
771
812
  if (code === 0) resolve();
772
- else reject(new Error(`${command} exited with code ${code}`));
813
+ else reject(new Error(text.trim() || `${command} exited with code ${code}`));
773
814
  });
774
815
  });
775
816
  }
@@ -779,6 +820,10 @@ function findLastUrl(text) {
779
820
  }
780
821
 
781
822
  function printHeader() {
823
+ if (input.isTTY) {
824
+ p.intro("Vercel VM Factory");
825
+ return;
826
+ }
782
827
  console.log("");
783
828
  console.log(color.cyan(" __ ____ __ _____ _ "));
784
829
  console.log(color.cyan(" \\ \\ / / \\/ | | ___|_ _ ___| |_ ___ _ __ _ _ "));
@@ -830,25 +875,35 @@ function printKeyValue(key, value) {
830
875
  }
831
876
 
832
877
  async function askText(question, fallback = "") {
833
- const suffix = fallback ? color.dim(` [${fallback}]`) : "";
834
- const rl = createInterface({ input, output });
835
- const answer = (
836
- await rl.question(
837
- `${color.cyan("?")} ${color.bold(question)}${suffix}\n${color.dim("> ")} `,
838
- )
878
+ return String(
879
+ await promptResult(
880
+ p.text({
881
+ message: question,
882
+ placeholder: fallback || undefined,
883
+ }),
884
+ ),
839
885
  ).trim();
840
- rl.close();
841
- return answer;
842
886
  }
843
887
 
844
- function printChoices(title, choices) {
845
- console.log("");
846
- console.log(color.bold(title));
847
- for (const [key, label, detail] of choices) {
848
- const hint = detail ? ` ${color.dim(detail)}` : "";
849
- console.log(` ${color.cyan(key)} ${label}${hint}`);
888
+ async function askSecret(question, fallback = "") {
889
+ return String(
890
+ await promptResult(
891
+ p.password({
892
+ message: question,
893
+ mask: "*",
894
+ placeholder: fallback || undefined,
895
+ }),
896
+ ),
897
+ ).trim();
898
+ }
899
+
900
+ async function promptResult(resultPromise) {
901
+ const result = await resultPromise;
902
+ if (p.isCancel(result)) {
903
+ p.cancel("Operation cancelled.");
904
+ process.exit(0);
850
905
  }
851
- console.log("");
906
+ return result;
852
907
  }
853
908
 
854
909
  function step(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vercel-vm-factory",
3
- "version": "0.9.8",
3
+ "version": "0.11.8",
4
4
  "description": "Create Vercel Container deployments for ws-shell from selectable VM images.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,9 @@
12
12
  "deploy-vm.mjs",
13
13
  "README.md"
14
14
  ],
15
+ "dependencies": {
16
+ "@clack/prompts": "^0.11.0"
17
+ },
15
18
  "scripts": {
16
19
  "deploy": "node deploy-vm.mjs create",
17
20
  "doctor": "node deploy-vm.mjs doctor",