blumenjs 0.1.4 → 0.1.6

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.
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Blumen — postinstall check
4
+ *
5
+ * Runs after `npm install -g blumenjs` to verify Go is available.
6
+ * If not, prints install instructions for the user's platform.
7
+ */
8
+
9
+ const { execSync } = require("child_process");
10
+ const os = require("os");
11
+
12
+ const c = {
13
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
14
+ red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m",
15
+ cyan: "\x1b[36m", magenta: "\x1b[35m",
16
+ };
17
+
18
+ function hasGo() {
19
+ try {
20
+ execSync("go version", { stdio: "pipe" });
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function getGoVersion() {
28
+ try {
29
+ const out = execSync("go version", { encoding: "utf-8" }).trim();
30
+ const match = out.match(/go(\d+\.\d+(\.\d+)?)/);
31
+ return match ? match[1] : out;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ console.log("");
38
+ console.log(` ${c.magenta}${c.bold}🌸 Blumen${c.reset} ${c.dim}installed successfully!${c.reset}`);
39
+ console.log(` ${c.dim}The React framework powered by Go${c.reset}`);
40
+ console.log("");
41
+
42
+ if (hasGo()) {
43
+ const ver = getGoVersion();
44
+ console.log(` ${c.green}✓${c.reset} Go ${c.bold}${ver}${c.reset} detected`);
45
+ console.log(` ${c.green}✓${c.reset} Ready to go! Run ${c.cyan}blumen create my-app${c.reset} to start.`);
46
+ } else {
47
+ console.log(` ${c.yellow}⚠${c.reset} ${c.bold}Go is not installed${c.reset}`);
48
+ console.log(` ${c.dim}Blumen uses Go for its high-performance server layer.${c.reset}`);
49
+ console.log("");
50
+
51
+ const platform = os.platform();
52
+
53
+ if (platform === "darwin") {
54
+ console.log(` ${c.bold}Install Go (macOS):${c.reset}`);
55
+ console.log(` ${c.cyan}brew install go${c.reset}`);
56
+ console.log(` ${c.dim}or download from https://go.dev/dl/${c.reset}`);
57
+ } else if (platform === "linux") {
58
+ console.log(` ${c.bold}Install Go (Linux):${c.reset}`);
59
+ console.log(` ${c.cyan}sudo apt install golang-go${c.reset} ${c.dim}(Debian/Ubuntu)${c.reset}`);
60
+ console.log(` ${c.cyan}sudo dnf install golang${c.reset} ${c.dim}(Fedora)${c.reset}`);
61
+ console.log(` ${c.dim}or download from https://go.dev/dl/${c.reset}`);
62
+ } else if (platform === "win32") {
63
+ console.log(` ${c.bold}Install Go (Windows):${c.reset}`);
64
+ console.log(` ${c.cyan}winget install GoLang.Go${c.reset}`);
65
+ console.log(` ${c.dim}or download from https://go.dev/dl/${c.reset}`);
66
+ } else {
67
+ console.log(` ${c.bold}Install Go:${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
68
+ }
69
+
70
+ console.log("");
71
+ console.log(` ${c.dim}After installing Go, run:${c.reset} ${c.cyan}blumen create my-app${c.reset}`);
72
+ }
73
+
74
+ console.log("");
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // cli/commands/dev.ts
3
- import { spawn, execSync } from "child_process";
3
+ import { spawn, execSync as execSync2 } from "child_process";
4
4
 
5
5
  // cli/utils.ts
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
+ import * as os from "os";
8
9
  import { fileURLToPath } from "url";
10
+ import { execSync } from "child_process";
9
11
  var c = {
10
12
  reset: "\x1B[0m",
11
13
  bold: "\x1B[1m",
@@ -84,13 +86,129 @@ async function select(question, options) {
84
86
  });
85
87
  });
86
88
  }
89
+ async function confirm(question, defaultYes = true) {
90
+ const readline = await import("readline");
91
+ const rl = readline.createInterface({
92
+ input: process.stdin,
93
+ output: process.stdout
94
+ });
95
+ const hint = defaultYes ? "Y/n" : "y/N";
96
+ return new Promise((resolve2) => {
97
+ rl.question(` ${c.bold}${question}${c.reset} ${c.dim}(${hint})${c.reset} `, (answer) => {
98
+ rl.close();
99
+ const a = answer.trim().toLowerCase();
100
+ if (a === "")
101
+ resolve2(defaultYes);
102
+ else
103
+ resolve2(a === "y" || a === "yes");
104
+ });
105
+ });
106
+ }
107
+ function checkGoInstalled() {
108
+ try {
109
+ execSync("go version", { stdio: "pipe" });
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+ function getGoVersion() {
116
+ try {
117
+ const out = execSync("go version", { encoding: "utf-8" }).trim();
118
+ const match = out.match(/go(\d+\.\d+(\.\d+)?)/);
119
+ return match ? match[1] : out;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+ async function ensureGo() {
125
+ if (checkGoInstalled()) {
126
+ const ver = getGoVersion();
127
+ log.success(`Go ${c.bold}${ver}${c.reset} detected`);
128
+ return true;
129
+ }
130
+ log.blank();
131
+ log.warn(`${c.bold}Go is not installed${c.reset}`);
132
+ log.info("Blumen uses Go for its high-performance HTTP server layer.");
133
+ log.blank();
134
+ const platform2 = os.platform();
135
+ if (platform2 === "darwin") {
136
+ let hasBrew = false;
137
+ try {
138
+ execSync("brew --version", { stdio: "pipe" });
139
+ hasBrew = true;
140
+ } catch {
141
+ }
142
+ if (hasBrew) {
143
+ const doInstall = await confirm("Install Go via Homebrew? (brew install go)");
144
+ if (doInstall) {
145
+ log.step("Installing Go via Homebrew...");
146
+ try {
147
+ execSync("brew install go", { stdio: "inherit" });
148
+ log.success("Go installed successfully!");
149
+ return true;
150
+ } catch {
151
+ log.error("Homebrew install failed. Please install manually.");
152
+ console.log(`
153
+ ${c.cyan}https://go.dev/dl/${c.reset}
154
+ `);
155
+ return false;
156
+ }
157
+ }
158
+ }
159
+ console.log(` ${c.bold}Install Go manually:${c.reset}`);
160
+ console.log(` ${c.cyan}brew install go${c.reset} ${c.dim}(if you install Homebrew first)${c.reset}`);
161
+ console.log(` ${c.dim}or download from${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
162
+ } else if (platform2 === "linux") {
163
+ let hasApt = false;
164
+ try {
165
+ execSync("apt --version", { stdio: "pipe" });
166
+ hasApt = true;
167
+ } catch {
168
+ }
169
+ if (hasApt) {
170
+ const doInstall = await confirm("Install Go via apt? (sudo apt install golang-go)");
171
+ if (doInstall) {
172
+ log.step("Installing Go via apt...");
173
+ try {
174
+ execSync("sudo apt update && sudo apt install -y golang-go", { stdio: "inherit" });
175
+ log.success("Go installed successfully!");
176
+ return true;
177
+ } catch {
178
+ log.error("apt install failed. Please install manually.");
179
+ console.log(`
180
+ ${c.cyan}https://go.dev/dl/${c.reset}
181
+ `);
182
+ return false;
183
+ }
184
+ }
185
+ }
186
+ console.log(` ${c.bold}Install Go:${c.reset}`);
187
+ console.log(` ${c.cyan}sudo apt install golang-go${c.reset} ${c.dim}(Debian/Ubuntu)${c.reset}`);
188
+ console.log(` ${c.cyan}sudo dnf install golang${c.reset} ${c.dim}(Fedora)${c.reset}`);
189
+ console.log(` ${c.dim}or download from${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
190
+ } else if (platform2 === "win32") {
191
+ console.log(` ${c.bold}Install Go (Windows):${c.reset}`);
192
+ console.log(` ${c.cyan}winget install GoLang.Go${c.reset}`);
193
+ console.log(` ${c.dim}or download from${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
194
+ } else {
195
+ console.log(` ${c.bold}Install Go:${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
196
+ }
197
+ log.blank();
198
+ log.info("After installing Go, restart your terminal and try again.");
199
+ return false;
200
+ }
87
201
 
88
202
  // cli/commands/dev.ts
89
203
  async function dev() {
90
204
  banner();
205
+ const goReady = await ensureGo();
206
+ if (!goReady) {
207
+ process.exit(1);
208
+ }
91
209
  log.step("Generating routes...");
92
210
  try {
93
- execSync("npx tsx scripts/generate-routes.ts", {
211
+ execSync2("npx tsx scripts/generate-routes.ts", {
94
212
  stdio: "pipe",
95
213
  cwd: process.cwd()
96
214
  });
@@ -202,7 +320,7 @@ async function dev() {
202
320
  }
203
321
 
204
322
  // cli/commands/build.ts
205
- import { execSync as execSync2 } from "child_process";
323
+ import { execSync as execSync3 } from "child_process";
206
324
  async function build() {
207
325
  banner();
208
326
  log.info("Creating production build...");
@@ -226,7 +344,7 @@ async function build() {
226
344
  const step = steps[i];
227
345
  log.step(`[${i + 1}/${steps.length}] ${step.label}...`);
228
346
  try {
229
- execSync2(step.cmd, { stdio: "inherit", cwd: process.cwd() });
347
+ execSync3(step.cmd, { stdio: "inherit", cwd: process.cwd() });
230
348
  log.success(step.label);
231
349
  } catch {
232
350
  log.error(`Failed: ${step.label}`);
@@ -338,7 +456,7 @@ async function start() {
338
456
  // cli/commands/create.ts
339
457
  import * as fs3 from "fs";
340
458
  import * as path2 from "path";
341
- import { execSync as execSync3 } from "child_process";
459
+ import { execSync as execSync4 } from "child_process";
342
460
  function getFrameworkRoot() {
343
461
  const cliEntry = fs3.realpathSync(process.argv[1]);
344
462
  const cliDir = path2.dirname(cliEntry);
@@ -621,7 +739,30 @@ function writeFile(base, relPath, content) {
621
739
  fs3.mkdirSync(path2.dirname(fullPath), { recursive: true });
622
740
  fs3.writeFileSync(fullPath, content, "utf-8");
623
741
  }
624
- function getTemplateFiles(projectName) {
742
+ var TEMPLATE_MAP = {
743
+ starter: {
744
+ file: "app/pages/BlumenStarter.tsx",
745
+ label: "Starter",
746
+ desc: "Premium landing page with feature cards"
747
+ },
748
+ empty: {
749
+ file: "app/pages/templates/BlumenEmpty.tsx",
750
+ label: "Empty",
751
+ desc: "Minimal blank project \u2014 just a centered greeting"
752
+ },
753
+ dashboard: {
754
+ file: "app/pages/templates/BlumenDashboard.tsx",
755
+ label: "Dashboard",
756
+ desc: "Admin dashboard with sidebar, stats, and activity feed"
757
+ },
758
+ api: {
759
+ file: "app/pages/templates/BlumenApi.tsx",
760
+ label: "API",
761
+ desc: "API explorer with endpoint list and response preview"
762
+ }
763
+ };
764
+ function getTemplateFiles(projectName, template) {
765
+ const tpl = TEMPLATE_MAP[template] || TEMPLATE_MAP.starter;
625
766
  return [
626
767
  // Generated config
627
768
  ["package.json", pkgJson(projectName)],
@@ -632,8 +773,9 @@ function getTemplateFiles(projectName) {
632
773
  ["app/shared/DefaultApp.tsx", DEFAULT_APP],
633
774
  ["app/shared/DefaultDocument.tsx", DEFAULT_DOCUMENT],
634
775
  ["app/shared/Link.tsx", LINK_TSX],
635
- // Complex filescopied from the framework source (avoids escaping hell)
636
- ["app/pages/Home.tsx", readProjectFile("app/pages/BlumenStarter.tsx")],
776
+ // Home pagedetermined by template choice
777
+ ["app/pages/Home.tsx", readProjectFile(tpl.file)],
778
+ // Complex files — copied from the framework source
637
779
  ["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
638
780
  ["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
639
781
  ["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
@@ -651,11 +793,15 @@ function getTemplateFiles(projectName) {
651
793
  async function create(projectName) {
652
794
  banner();
653
795
  log.info("Create a new Blumen project\n");
796
+ const goReady = await ensureGo();
797
+ if (!goReady) {
798
+ process.exit(1);
799
+ }
654
800
  if (!projectName) {
655
801
  log.error("Please provide a project name.");
656
802
  console.log(
657
803
  `
658
- ${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset}
804
+ ${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset} ${c.dim}[--template starter|empty|dashboard|api]${c.reset}
659
805
  `
660
806
  );
661
807
  process.exit(1);
@@ -665,6 +811,25 @@ async function create(projectName) {
665
811
  log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
666
812
  process.exit(1);
667
813
  }
814
+ let template = "";
815
+ const templateFlagIdx = process.argv.indexOf("--template");
816
+ if (templateFlagIdx !== -1 && process.argv[templateFlagIdx + 1]) {
817
+ template = process.argv[templateFlagIdx + 1];
818
+ if (!TEMPLATE_MAP[template]) {
819
+ log.error(`Unknown template: ${c.bold}${template}${c.reset}`);
820
+ log.info(`Available: ${Object.keys(TEMPLATE_MAP).join(", ")}`);
821
+ process.exit(1);
822
+ }
823
+ }
824
+ if (!template) {
825
+ template = await select(
826
+ "Which template do you want to use?",
827
+ Object.entries(TEMPLATE_MAP).map(
828
+ ([key, val]) => `${key} \u2014 ${val.desc}`
829
+ )
830
+ );
831
+ template = template.split(" \u2014 ")[0];
832
+ }
668
833
  const pkgManager = await select("Which package manager do you want to use?", [
669
834
  "npm",
670
835
  "yarn",
@@ -674,8 +839,8 @@ async function create(projectName) {
674
839
  log.blank();
675
840
  divider();
676
841
  log.blank();
677
- log.step(`Creating project in ${c.cyan}${projectName}${c.reset}...`);
678
- const files = getTemplateFiles(projectName);
842
+ log.step(`Creating project in ${c.cyan}${projectName}${c.reset} with ${c.magenta}${template}${c.reset} template...`);
843
+ const files = getTemplateFiles(projectName, template);
679
844
  for (const [relPath, content] of files) {
680
845
  writeFile(projectDir, relPath, content);
681
846
  }
@@ -688,7 +853,7 @@ async function create(projectName) {
688
853
  bun: "bun install"
689
854
  };
690
855
  try {
691
- execSync3(installCmd[pkgManager], {
856
+ execSync4(installCmd[pkgManager], {
692
857
  cwd: projectDir,
693
858
  stdio: "inherit"
694
859
  });
@@ -701,7 +866,7 @@ async function create(projectName) {
701
866
  log.blank();
702
867
  divider();
703
868
  log.blank();
704
- log.success(`${c.bold}Project created!${c.reset}`);
869
+ log.success(`${c.bold}Project created!${c.reset} (template: ${c.magenta}${template}${c.reset})`);
705
870
  log.blank();
706
871
  console.log(` ${c.dim}Next steps:${c.reset}`);
707
872
  console.log(` cd ${projectName}`);
@@ -709,6 +874,271 @@ async function create(projectName) {
709
874
  log.blank();
710
875
  }
711
876
 
877
+ // cli/commands/deploy.ts
878
+ import { execSync as execSync5, spawnSync } from "child_process";
879
+ import * as fs4 from "fs";
880
+ import * as path3 from "path";
881
+ var PLATFORMS = [
882
+ { name: "Docker (any cloud)", ok: true, note: "Best option \u2014 multi-stage Dockerfile included" },
883
+ { name: "Railway", ok: true, note: "Auto-detects Dockerfile, deploy with railway up" },
884
+ { name: "Fly.io", ok: true, note: "Docker-based deploys via fly deploy" },
885
+ { name: "Render", ok: true, note: "Docker support, auto-deploy from Git" },
886
+ { name: "AWS EC2 / GCP / Azure", ok: true, note: "Full control, any runtime" },
887
+ { name: "DigitalOcean", ok: true, note: "App Platform supports Docker" },
888
+ { name: "Vercel", ok: false, note: "Node-only, no Go runtime" },
889
+ { name: "Netlify", ok: false, note: "Static/serverless only, no Go" }
890
+ ];
891
+ function hasCommand(cmd) {
892
+ try {
893
+ execSync5(`which ${cmd}`, { stdio: "pipe" });
894
+ return true;
895
+ } catch {
896
+ return false;
897
+ }
898
+ }
899
+ function ensureDockerfile() {
900
+ if (!fs4.existsSync("Dockerfile")) {
901
+ log.error("No Dockerfile found in the current directory.");
902
+ log.info(
903
+ `Run ${c.bold}blumen create${c.reset} to scaffold a project with Docker support.`
904
+ );
905
+ process.exit(1);
906
+ }
907
+ }
908
+ async function deployDocker() {
909
+ log.info("Docker Deployment\n");
910
+ ensureDockerfile();
911
+ if (!hasCommand("docker")) {
912
+ log.error("Docker is not installed.");
913
+ log.info(
914
+ `Install Docker: ${c.cyan}https://docs.docker.com/get-docker/${c.reset}`
915
+ );
916
+ process.exit(1);
917
+ }
918
+ const projectName = path3.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
919
+ const imageName = `blumen-${projectName}`;
920
+ log.step(`Building image: ${c.bold}${imageName}${c.reset}...`);
921
+ divider();
922
+ log.blank();
923
+ const buildResult = spawnSync("docker", ["build", "-t", imageName, "."], {
924
+ cwd: process.cwd(),
925
+ stdio: "inherit"
926
+ });
927
+ log.blank();
928
+ divider();
929
+ if (buildResult.status !== 0) {
930
+ log.error("Docker build failed.");
931
+ process.exit(1);
932
+ }
933
+ log.success(`Image ${c.bold}${imageName}${c.reset} built successfully!`);
934
+ log.blank();
935
+ const shouldRun = await confirm("Run the container now?");
936
+ if (shouldRun) {
937
+ log.step(`Starting ${c.bold}${imageName}${c.reset} on port 3000...`);
938
+ log.blank();
939
+ console.log(
940
+ ` ${c.dim}\u279C${c.reset} ${c.bold}App${c.reset}: ${c.cyan}http://localhost:3000${c.reset}`
941
+ );
942
+ console.log(
943
+ ` ${c.dim}Press ${c.bold}Ctrl+C${c.reset}${c.dim} to stop.${c.reset}`
944
+ );
945
+ log.blank();
946
+ spawnSync("docker", ["run", "--rm", "-p", "3000:3000", imageName], {
947
+ cwd: process.cwd(),
948
+ stdio: "inherit"
949
+ });
950
+ } else {
951
+ log.blank();
952
+ console.log(` ${c.dim}Run manually:${c.reset}`);
953
+ console.log(` docker run -p 3000:3000 ${imageName}`);
954
+ log.blank();
955
+ }
956
+ }
957
+ async function deployFly() {
958
+ log.info("Fly.io Deployment\n");
959
+ ensureDockerfile();
960
+ const projectName = path3.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
961
+ if (!fs4.existsSync("fly.toml")) {
962
+ log.step("Generating fly.toml...");
963
+ const flyConfig = `# Fly.io configuration for Blumen app
964
+ # Deploy with: fly deploy
965
+
966
+ app = "${projectName}"
967
+ primary_region = "iad"
968
+
969
+ [build]
970
+
971
+ [http_service]
972
+ internal_port = 3000
973
+ force_https = true
974
+ auto_stop_machines = "stop"
975
+ auto_start_machines = true
976
+ min_machines_running = 0
977
+
978
+ [checks]
979
+ [checks.health]
980
+ type = "http"
981
+ port = 3000
982
+ path = "/"
983
+ interval = "30s"
984
+ timeout = "5s"
985
+
986
+ [[vm]]
987
+ memory = "512mb"
988
+ cpu_kind = "shared"
989
+ cpus = 1
990
+ `;
991
+ fs4.writeFileSync("fly.toml", flyConfig);
992
+ log.success("fly.toml created");
993
+ } else {
994
+ log.info("fly.toml already exists, skipping generation.");
995
+ }
996
+ log.blank();
997
+ if (!hasCommand("fly") && !hasCommand("flyctl")) {
998
+ log.warn("Fly CLI is not installed.");
999
+ log.blank();
1000
+ console.log(` ${c.dim}Install:${c.reset}`);
1001
+ console.log(` curl -L https://fly.io/install.sh | sh`);
1002
+ log.blank();
1003
+ console.log(` ${c.dim}Then deploy:${c.reset}`);
1004
+ console.log(` fly launch`);
1005
+ console.log(` fly deploy`);
1006
+ log.blank();
1007
+ return;
1008
+ }
1009
+ const shouldDeploy = await confirm("Deploy to Fly.io now?");
1010
+ if (shouldDeploy) {
1011
+ log.step("Deploying to Fly.io...");
1012
+ divider();
1013
+ log.blank();
1014
+ const flyCmd = hasCommand("fly") ? "fly" : "flyctl";
1015
+ spawnSync(flyCmd, ["deploy"], {
1016
+ cwd: process.cwd(),
1017
+ stdio: "inherit"
1018
+ });
1019
+ log.blank();
1020
+ divider();
1021
+ log.success("Deployment complete!");
1022
+ } else {
1023
+ log.blank();
1024
+ console.log(` ${c.dim}Deploy later:${c.reset}`);
1025
+ console.log(` fly deploy`);
1026
+ log.blank();
1027
+ }
1028
+ }
1029
+ async function deployRailway() {
1030
+ log.info("Railway Deployment\n");
1031
+ ensureDockerfile();
1032
+ if (!fs4.existsSync("railway.toml")) {
1033
+ log.step("Generating railway.toml...");
1034
+ const railwayConfig = `# Railway configuration for Blumen app
1035
+ # Deploy with: railway up
1036
+
1037
+ [build]
1038
+ builder = "DOCKERFILE"
1039
+ dockerfilePath = "Dockerfile"
1040
+
1041
+ [deploy]
1042
+ startCommand = ""
1043
+ healthcheckPath = "/"
1044
+ healthcheckTimeout = 30
1045
+ restartPolicyType = "ON_FAILURE"
1046
+ restartPolicyMaxRetries = 5
1047
+ `;
1048
+ fs4.writeFileSync("railway.toml", railwayConfig);
1049
+ log.success("railway.toml created");
1050
+ } else {
1051
+ log.info("railway.toml already exists, skipping generation.");
1052
+ }
1053
+ log.blank();
1054
+ if (!hasCommand("railway")) {
1055
+ log.warn("Railway CLI is not installed.");
1056
+ log.blank();
1057
+ console.log(` ${c.dim}Install:${c.reset}`);
1058
+ console.log(` npm install -g @railway/cli`);
1059
+ log.blank();
1060
+ console.log(` ${c.dim}Then deploy:${c.reset}`);
1061
+ console.log(` railway login`);
1062
+ console.log(` railway up`);
1063
+ log.blank();
1064
+ return;
1065
+ }
1066
+ const shouldDeploy = await confirm("Deploy to Railway now?");
1067
+ if (shouldDeploy) {
1068
+ log.step("Deploying to Railway...");
1069
+ divider();
1070
+ log.blank();
1071
+ spawnSync("railway", ["up"], {
1072
+ cwd: process.cwd(),
1073
+ stdio: "inherit"
1074
+ });
1075
+ log.blank();
1076
+ divider();
1077
+ log.success("Deployment complete!");
1078
+ } else {
1079
+ log.blank();
1080
+ console.log(` ${c.dim}Deploy later:${c.reset}`);
1081
+ console.log(` railway up`);
1082
+ log.blank();
1083
+ }
1084
+ }
1085
+ function showInfo() {
1086
+ log.info("Hosting Compatibility\n");
1087
+ console.log(` Blumen apps require ${c.bold}Go${c.reset} + ${c.bold}Node.js${c.reset} \u2014 use Docker-compatible platforms.
1088
+ `);
1089
+ const maxName = Math.max(...PLATFORMS.map((p) => p.name.length));
1090
+ for (const p of PLATFORMS) {
1091
+ const icon = p.ok ? `${c.green}\u2713${c.reset}` : `${c.red}\u2717${c.reset}`;
1092
+ const name = p.name.padEnd(maxName + 2);
1093
+ const note = `${c.dim}${p.note}${c.reset}`;
1094
+ console.log(` ${icon} ${name} ${note}`);
1095
+ }
1096
+ log.blank();
1097
+ divider();
1098
+ log.blank();
1099
+ console.log(` ${c.bold}Quick start:${c.reset}`);
1100
+ log.blank();
1101
+ console.log(` ${c.dim}Docker (local):${c.reset} docker compose up`);
1102
+ console.log(` ${c.dim}Fly.io:${c.reset} blumen deploy fly`);
1103
+ console.log(` ${c.dim}Railway:${c.reset} blumen deploy railway`);
1104
+ log.blank();
1105
+ }
1106
+ async function deploy(subcommand) {
1107
+ banner();
1108
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
1109
+ log.info("Deploy your Blumen app\n");
1110
+ console.log(` ${c.bold}Usage${c.reset} blumen deploy ${c.dim}<target>${c.reset}
1111
+ `);
1112
+ console.log(` ${c.bold}Targets${c.reset}`);
1113
+ console.log(` docker Build Docker image and optionally run`);
1114
+ console.log(` fly Generate config and deploy to Fly.io`);
1115
+ console.log(` railway Generate config and deploy to Railway`);
1116
+ console.log(` info Show hosting compatibility matrix`);
1117
+ console.log("");
1118
+ return;
1119
+ }
1120
+ switch (subcommand) {
1121
+ case "docker":
1122
+ await deployDocker();
1123
+ break;
1124
+ case "fly":
1125
+ await deployFly();
1126
+ break;
1127
+ case "railway":
1128
+ await deployRailway();
1129
+ break;
1130
+ case "info":
1131
+ showInfo();
1132
+ break;
1133
+ default:
1134
+ log.error(`Unknown deploy target: ${c.bold}${subcommand}${c.reset}`);
1135
+ log.info(
1136
+ `Run ${c.bold}blumen deploy --help${c.reset} for available targets.`
1137
+ );
1138
+ process.exit(1);
1139
+ }
1140
+ }
1141
+
712
1142
  // cli/blumen.ts
713
1143
  async function main() {
714
1144
  const command = process.argv[2];
@@ -727,6 +1157,9 @@ async function main() {
727
1157
  console.log(
728
1158
  ` create Scaffold a new Blumen project`
729
1159
  );
1160
+ console.log(
1161
+ ` deploy Deploy to Docker, Fly.io, or Railway`
1162
+ );
730
1163
  console.log("");
731
1164
  console.log(` ${c.bold}Options${c.reset}`);
732
1165
  console.log(` --help Show this help message`);
@@ -751,6 +1184,9 @@ async function main() {
751
1184
  case "create":
752
1185
  await create(process.argv[3]);
753
1186
  break;
1187
+ case "deploy":
1188
+ await deploy(process.argv[3]);
1189
+ break;
754
1190
  default:
755
1191
  log.error(
756
1192
  `Unknown command: ${c.bold}${command}${c.reset}`