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.
- package/README.md +3 -1
- package/deploy-vm.mjs +174 -119
- 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`,
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
213
|
-
"
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
299
|
-
`Vercel CLI is not installed. Install with ${install.command} ${install.args.join(" ")}?`,
|
|
300
|
-
|
|
301
|
-
)
|
|
302
|
-
)
|
|
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
|
|
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=
|
|
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
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
shell,
|
|
580
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
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
|
|
725
|
-
|
|
726
|
-
|
|
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
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|