blumenjs 0.1.3 → 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.
@@ -84,6 +84,24 @@ async function select(question, options) {
84
84
  });
85
85
  });
86
86
  }
87
+ async function confirm(question, defaultYes = true) {
88
+ const readline = await import("readline");
89
+ const rl = readline.createInterface({
90
+ input: process.stdin,
91
+ output: process.stdout
92
+ });
93
+ const hint = defaultYes ? "Y/n" : "y/N";
94
+ return new Promise((resolve2) => {
95
+ rl.question(` ${c.bold}${question}${c.reset} ${c.dim}(${hint})${c.reset} `, (answer) => {
96
+ rl.close();
97
+ const a = answer.trim().toLowerCase();
98
+ if (a === "")
99
+ resolve2(defaultYes);
100
+ else
101
+ resolve2(a === "y" || a === "yes");
102
+ });
103
+ });
104
+ }
87
105
 
88
106
  // cli/commands/dev.ts
89
107
  async function dev() {
@@ -555,12 +573,96 @@ export function Link({ href, children, onClick, target, ...rest }: LinkProps) {
555
573
 
556
574
  export default Link;
557
575
  `;
576
+ var DOCKERFILE = `# \u2500\u2500 Stage 1: Build Go server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
577
+ FROM golang:1.22-alpine AS go-builder
578
+ WORKDIR /app
579
+ COPY go-server/ go-server/
580
+ WORKDIR /app/go-server
581
+ RUN go build -o /app/blumen-server main.go
582
+
583
+ # \u2500\u2500 Stage 2: Build Node SSR + client bundle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
584
+ FROM node:20-alpine AS node-builder
585
+ WORKDIR /app
586
+ COPY package.json tsconfig.json webpack.config.js ./
587
+ COPY app/ app/
588
+ COPY node-ssr/ node-ssr/
589
+ COPY scripts/ scripts/
590
+ RUN npm install --production=false
591
+ RUN npx tsx scripts/generate-routes.ts
592
+ RUN npx webpack --mode production
593
+ RUN npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm \\
594
+ --outfile=dist/ssr-server.js --external:react --external:react-dom
595
+
596
+ # \u2500\u2500 Stage 3: Production image \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
597
+ FROM node:20-alpine
598
+ RUN apk add --no-cache libc6-compat
599
+ WORKDIR /app
600
+
601
+ # Copy Go binary
602
+ COPY --from=go-builder /app/blumen-server ./blumen-server
603
+
604
+ # Copy Node SSR server + production deps
605
+ COPY --from=node-builder /app/dist/ ./dist/
606
+ COPY --from=node-builder /app/static/ ./static/
607
+ COPY --from=node-builder /app/node_modules/ ./node_modules/
608
+ COPY --from=node-builder /app/package.json ./
609
+
610
+ EXPOSE 3000 4000
611
+
612
+ # Start both services (Go on :3000, Node SSR on :4000)
613
+ CMD sh -c './blumen-server & NODE_ENV=production node dist/ssr-server.js'
614
+ `;
615
+ var DOCKER_COMPOSE = `services:
616
+ app:
617
+ build: .
618
+ ports:
619
+ - "3000:3000"
620
+ environment:
621
+ - NODE_ENV=production
622
+ restart: unless-stopped
623
+ healthcheck:
624
+ test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
625
+ interval: 30s
626
+ timeout: 5s
627
+ retries: 3
628
+ start_period: 10s
629
+ `;
630
+ var DOCKERIGNORE = `node_modules
631
+ dist
632
+ static/js/bundle.js
633
+ .git
634
+ *.log
635
+ .DS_Store
636
+ `;
558
637
  function writeFile(base, relPath, content) {
559
638
  const fullPath = path2.join(base, relPath);
560
639
  fs3.mkdirSync(path2.dirname(fullPath), { recursive: true });
561
640
  fs3.writeFileSync(fullPath, content, "utf-8");
562
641
  }
563
- function getTemplateFiles(projectName) {
642
+ var TEMPLATE_MAP = {
643
+ starter: {
644
+ file: "app/pages/BlumenStarter.tsx",
645
+ label: "Starter",
646
+ desc: "Premium landing page with feature cards"
647
+ },
648
+ empty: {
649
+ file: "app/pages/templates/BlumenEmpty.tsx",
650
+ label: "Empty",
651
+ desc: "Minimal blank project \u2014 just a centered greeting"
652
+ },
653
+ dashboard: {
654
+ file: "app/pages/templates/BlumenDashboard.tsx",
655
+ label: "Dashboard",
656
+ desc: "Admin dashboard with sidebar, stats, and activity feed"
657
+ },
658
+ api: {
659
+ file: "app/pages/templates/BlumenApi.tsx",
660
+ label: "API",
661
+ desc: "API explorer with endpoint list and response preview"
662
+ }
663
+ };
664
+ function getTemplateFiles(projectName, template) {
665
+ const tpl = TEMPLATE_MAP[template] || TEMPLATE_MAP.starter;
564
666
  return [
565
667
  // Generated config
566
668
  ["package.json", pkgJson(projectName)],
@@ -571,8 +673,9 @@ function getTemplateFiles(projectName) {
571
673
  ["app/shared/DefaultApp.tsx", DEFAULT_APP],
572
674
  ["app/shared/DefaultDocument.tsx", DEFAULT_DOCUMENT],
573
675
  ["app/shared/Link.tsx", LINK_TSX],
574
- // Complex filescopied from the framework source (avoids escaping hell)
575
- ["app/pages/Home.tsx", readProjectFile("app/pages/BlumenStarter.tsx")],
676
+ // Home pagedetermined by template choice
677
+ ["app/pages/Home.tsx", readProjectFile(tpl.file)],
678
+ // Complex files — copied from the framework source
576
679
  ["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
577
680
  ["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
578
681
  ["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
@@ -580,7 +683,11 @@ function getTemplateFiles(projectName) {
580
683
  ["go-server/main.go", readProjectFile("go-server/main.go")],
581
684
  ["scripts/generate-routes.ts", readProjectFile("scripts/generate-routes.ts")],
582
685
  // Placeholder
583
- ["static/js/.gitkeep", ""]
686
+ ["static/js/.gitkeep", ""],
687
+ // Docker support (production deployment)
688
+ ["Dockerfile", DOCKERFILE],
689
+ ["docker-compose.yml", DOCKER_COMPOSE],
690
+ [".dockerignore", DOCKERIGNORE]
584
691
  ];
585
692
  }
586
693
  async function create(projectName) {
@@ -590,7 +697,7 @@ async function create(projectName) {
590
697
  log.error("Please provide a project name.");
591
698
  console.log(
592
699
  `
593
- ${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset}
700
+ ${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset} ${c.dim}[--template starter|empty|dashboard|api]${c.reset}
594
701
  `
595
702
  );
596
703
  process.exit(1);
@@ -600,6 +707,25 @@ async function create(projectName) {
600
707
  log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
601
708
  process.exit(1);
602
709
  }
710
+ let template = "";
711
+ const templateFlagIdx = process.argv.indexOf("--template");
712
+ if (templateFlagIdx !== -1 && process.argv[templateFlagIdx + 1]) {
713
+ template = process.argv[templateFlagIdx + 1];
714
+ if (!TEMPLATE_MAP[template]) {
715
+ log.error(`Unknown template: ${c.bold}${template}${c.reset}`);
716
+ log.info(`Available: ${Object.keys(TEMPLATE_MAP).join(", ")}`);
717
+ process.exit(1);
718
+ }
719
+ }
720
+ if (!template) {
721
+ template = await select(
722
+ "Which template do you want to use?",
723
+ Object.entries(TEMPLATE_MAP).map(
724
+ ([key, val]) => `${key} \u2014 ${val.desc}`
725
+ )
726
+ );
727
+ template = template.split(" \u2014 ")[0];
728
+ }
603
729
  const pkgManager = await select("Which package manager do you want to use?", [
604
730
  "npm",
605
731
  "yarn",
@@ -609,8 +735,8 @@ async function create(projectName) {
609
735
  log.blank();
610
736
  divider();
611
737
  log.blank();
612
- log.step(`Creating project in ${c.cyan}${projectName}${c.reset}...`);
613
- const files = getTemplateFiles(projectName);
738
+ log.step(`Creating project in ${c.cyan}${projectName}${c.reset} with ${c.magenta}${template}${c.reset} template...`);
739
+ const files = getTemplateFiles(projectName, template);
614
740
  for (const [relPath, content] of files) {
615
741
  writeFile(projectDir, relPath, content);
616
742
  }
@@ -636,7 +762,7 @@ async function create(projectName) {
636
762
  log.blank();
637
763
  divider();
638
764
  log.blank();
639
- log.success(`${c.bold}Project created!${c.reset}`);
765
+ log.success(`${c.bold}Project created!${c.reset} (template: ${c.magenta}${template}${c.reset})`);
640
766
  log.blank();
641
767
  console.log(` ${c.dim}Next steps:${c.reset}`);
642
768
  console.log(` cd ${projectName}`);
@@ -644,6 +770,271 @@ async function create(projectName) {
644
770
  log.blank();
645
771
  }
646
772
 
773
+ // cli/commands/deploy.ts
774
+ import { execSync as execSync4, spawnSync } from "child_process";
775
+ import * as fs4 from "fs";
776
+ import * as path3 from "path";
777
+ var PLATFORMS = [
778
+ { name: "Docker (any cloud)", ok: true, note: "Best option \u2014 multi-stage Dockerfile included" },
779
+ { name: "Railway", ok: true, note: "Auto-detects Dockerfile, deploy with railway up" },
780
+ { name: "Fly.io", ok: true, note: "Docker-based deploys via fly deploy" },
781
+ { name: "Render", ok: true, note: "Docker support, auto-deploy from Git" },
782
+ { name: "AWS EC2 / GCP / Azure", ok: true, note: "Full control, any runtime" },
783
+ { name: "DigitalOcean", ok: true, note: "App Platform supports Docker" },
784
+ { name: "Vercel", ok: false, note: "Node-only, no Go runtime" },
785
+ { name: "Netlify", ok: false, note: "Static/serverless only, no Go" }
786
+ ];
787
+ function hasCommand(cmd) {
788
+ try {
789
+ execSync4(`which ${cmd}`, { stdio: "pipe" });
790
+ return true;
791
+ } catch {
792
+ return false;
793
+ }
794
+ }
795
+ function ensureDockerfile() {
796
+ if (!fs4.existsSync("Dockerfile")) {
797
+ log.error("No Dockerfile found in the current directory.");
798
+ log.info(
799
+ `Run ${c.bold}blumen create${c.reset} to scaffold a project with Docker support.`
800
+ );
801
+ process.exit(1);
802
+ }
803
+ }
804
+ async function deployDocker() {
805
+ log.info("Docker Deployment\n");
806
+ ensureDockerfile();
807
+ if (!hasCommand("docker")) {
808
+ log.error("Docker is not installed.");
809
+ log.info(
810
+ `Install Docker: ${c.cyan}https://docs.docker.com/get-docker/${c.reset}`
811
+ );
812
+ process.exit(1);
813
+ }
814
+ const projectName = path3.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
815
+ const imageName = `blumen-${projectName}`;
816
+ log.step(`Building image: ${c.bold}${imageName}${c.reset}...`);
817
+ divider();
818
+ log.blank();
819
+ const buildResult = spawnSync("docker", ["build", "-t", imageName, "."], {
820
+ cwd: process.cwd(),
821
+ stdio: "inherit"
822
+ });
823
+ log.blank();
824
+ divider();
825
+ if (buildResult.status !== 0) {
826
+ log.error("Docker build failed.");
827
+ process.exit(1);
828
+ }
829
+ log.success(`Image ${c.bold}${imageName}${c.reset} built successfully!`);
830
+ log.blank();
831
+ const shouldRun = await confirm("Run the container now?");
832
+ if (shouldRun) {
833
+ log.step(`Starting ${c.bold}${imageName}${c.reset} on port 3000...`);
834
+ log.blank();
835
+ console.log(
836
+ ` ${c.dim}\u279C${c.reset} ${c.bold}App${c.reset}: ${c.cyan}http://localhost:3000${c.reset}`
837
+ );
838
+ console.log(
839
+ ` ${c.dim}Press ${c.bold}Ctrl+C${c.reset}${c.dim} to stop.${c.reset}`
840
+ );
841
+ log.blank();
842
+ spawnSync("docker", ["run", "--rm", "-p", "3000:3000", imageName], {
843
+ cwd: process.cwd(),
844
+ stdio: "inherit"
845
+ });
846
+ } else {
847
+ log.blank();
848
+ console.log(` ${c.dim}Run manually:${c.reset}`);
849
+ console.log(` docker run -p 3000:3000 ${imageName}`);
850
+ log.blank();
851
+ }
852
+ }
853
+ async function deployFly() {
854
+ log.info("Fly.io Deployment\n");
855
+ ensureDockerfile();
856
+ const projectName = path3.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
857
+ if (!fs4.existsSync("fly.toml")) {
858
+ log.step("Generating fly.toml...");
859
+ const flyConfig = `# Fly.io configuration for Blumen app
860
+ # Deploy with: fly deploy
861
+
862
+ app = "${projectName}"
863
+ primary_region = "iad"
864
+
865
+ [build]
866
+
867
+ [http_service]
868
+ internal_port = 3000
869
+ force_https = true
870
+ auto_stop_machines = "stop"
871
+ auto_start_machines = true
872
+ min_machines_running = 0
873
+
874
+ [checks]
875
+ [checks.health]
876
+ type = "http"
877
+ port = 3000
878
+ path = "/"
879
+ interval = "30s"
880
+ timeout = "5s"
881
+
882
+ [[vm]]
883
+ memory = "512mb"
884
+ cpu_kind = "shared"
885
+ cpus = 1
886
+ `;
887
+ fs4.writeFileSync("fly.toml", flyConfig);
888
+ log.success("fly.toml created");
889
+ } else {
890
+ log.info("fly.toml already exists, skipping generation.");
891
+ }
892
+ log.blank();
893
+ if (!hasCommand("fly") && !hasCommand("flyctl")) {
894
+ log.warn("Fly CLI is not installed.");
895
+ log.blank();
896
+ console.log(` ${c.dim}Install:${c.reset}`);
897
+ console.log(` curl -L https://fly.io/install.sh | sh`);
898
+ log.blank();
899
+ console.log(` ${c.dim}Then deploy:${c.reset}`);
900
+ console.log(` fly launch`);
901
+ console.log(` fly deploy`);
902
+ log.blank();
903
+ return;
904
+ }
905
+ const shouldDeploy = await confirm("Deploy to Fly.io now?");
906
+ if (shouldDeploy) {
907
+ log.step("Deploying to Fly.io...");
908
+ divider();
909
+ log.blank();
910
+ const flyCmd = hasCommand("fly") ? "fly" : "flyctl";
911
+ spawnSync(flyCmd, ["deploy"], {
912
+ cwd: process.cwd(),
913
+ stdio: "inherit"
914
+ });
915
+ log.blank();
916
+ divider();
917
+ log.success("Deployment complete!");
918
+ } else {
919
+ log.blank();
920
+ console.log(` ${c.dim}Deploy later:${c.reset}`);
921
+ console.log(` fly deploy`);
922
+ log.blank();
923
+ }
924
+ }
925
+ async function deployRailway() {
926
+ log.info("Railway Deployment\n");
927
+ ensureDockerfile();
928
+ if (!fs4.existsSync("railway.toml")) {
929
+ log.step("Generating railway.toml...");
930
+ const railwayConfig = `# Railway configuration for Blumen app
931
+ # Deploy with: railway up
932
+
933
+ [build]
934
+ builder = "DOCKERFILE"
935
+ dockerfilePath = "Dockerfile"
936
+
937
+ [deploy]
938
+ startCommand = ""
939
+ healthcheckPath = "/"
940
+ healthcheckTimeout = 30
941
+ restartPolicyType = "ON_FAILURE"
942
+ restartPolicyMaxRetries = 5
943
+ `;
944
+ fs4.writeFileSync("railway.toml", railwayConfig);
945
+ log.success("railway.toml created");
946
+ } else {
947
+ log.info("railway.toml already exists, skipping generation.");
948
+ }
949
+ log.blank();
950
+ if (!hasCommand("railway")) {
951
+ log.warn("Railway CLI is not installed.");
952
+ log.blank();
953
+ console.log(` ${c.dim}Install:${c.reset}`);
954
+ console.log(` npm install -g @railway/cli`);
955
+ log.blank();
956
+ console.log(` ${c.dim}Then deploy:${c.reset}`);
957
+ console.log(` railway login`);
958
+ console.log(` railway up`);
959
+ log.blank();
960
+ return;
961
+ }
962
+ const shouldDeploy = await confirm("Deploy to Railway now?");
963
+ if (shouldDeploy) {
964
+ log.step("Deploying to Railway...");
965
+ divider();
966
+ log.blank();
967
+ spawnSync("railway", ["up"], {
968
+ cwd: process.cwd(),
969
+ stdio: "inherit"
970
+ });
971
+ log.blank();
972
+ divider();
973
+ log.success("Deployment complete!");
974
+ } else {
975
+ log.blank();
976
+ console.log(` ${c.dim}Deploy later:${c.reset}`);
977
+ console.log(` railway up`);
978
+ log.blank();
979
+ }
980
+ }
981
+ function showInfo() {
982
+ log.info("Hosting Compatibility\n");
983
+ console.log(` Blumen apps require ${c.bold}Go${c.reset} + ${c.bold}Node.js${c.reset} \u2014 use Docker-compatible platforms.
984
+ `);
985
+ const maxName = Math.max(...PLATFORMS.map((p) => p.name.length));
986
+ for (const p of PLATFORMS) {
987
+ const icon = p.ok ? `${c.green}\u2713${c.reset}` : `${c.red}\u2717${c.reset}`;
988
+ const name = p.name.padEnd(maxName + 2);
989
+ const note = `${c.dim}${p.note}${c.reset}`;
990
+ console.log(` ${icon} ${name} ${note}`);
991
+ }
992
+ log.blank();
993
+ divider();
994
+ log.blank();
995
+ console.log(` ${c.bold}Quick start:${c.reset}`);
996
+ log.blank();
997
+ console.log(` ${c.dim}Docker (local):${c.reset} docker compose up`);
998
+ console.log(` ${c.dim}Fly.io:${c.reset} blumen deploy fly`);
999
+ console.log(` ${c.dim}Railway:${c.reset} blumen deploy railway`);
1000
+ log.blank();
1001
+ }
1002
+ async function deploy(subcommand) {
1003
+ banner();
1004
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
1005
+ log.info("Deploy your Blumen app\n");
1006
+ console.log(` ${c.bold}Usage${c.reset} blumen deploy ${c.dim}<target>${c.reset}
1007
+ `);
1008
+ console.log(` ${c.bold}Targets${c.reset}`);
1009
+ console.log(` docker Build Docker image and optionally run`);
1010
+ console.log(` fly Generate config and deploy to Fly.io`);
1011
+ console.log(` railway Generate config and deploy to Railway`);
1012
+ console.log(` info Show hosting compatibility matrix`);
1013
+ console.log("");
1014
+ return;
1015
+ }
1016
+ switch (subcommand) {
1017
+ case "docker":
1018
+ await deployDocker();
1019
+ break;
1020
+ case "fly":
1021
+ await deployFly();
1022
+ break;
1023
+ case "railway":
1024
+ await deployRailway();
1025
+ break;
1026
+ case "info":
1027
+ showInfo();
1028
+ break;
1029
+ default:
1030
+ log.error(`Unknown deploy target: ${c.bold}${subcommand}${c.reset}`);
1031
+ log.info(
1032
+ `Run ${c.bold}blumen deploy --help${c.reset} for available targets.`
1033
+ );
1034
+ process.exit(1);
1035
+ }
1036
+ }
1037
+
647
1038
  // cli/blumen.ts
648
1039
  async function main() {
649
1040
  const command = process.argv[2];
@@ -662,6 +1053,9 @@ async function main() {
662
1053
  console.log(
663
1054
  ` create Scaffold a new Blumen project`
664
1055
  );
1056
+ console.log(
1057
+ ` deploy Deploy to Docker, Fly.io, or Railway`
1058
+ );
665
1059
  console.log("");
666
1060
  console.log(` ${c.bold}Options${c.reset}`);
667
1061
  console.log(` --help Show this help message`);
@@ -686,6 +1080,9 @@ async function main() {
686
1080
  case "create":
687
1081
  await create(process.argv[3]);
688
1082
  break;
1083
+ case "deploy":
1084
+ await deploy(process.argv[3]);
1085
+ break;
689
1086
  default:
690
1087
  log.error(
691
1088
  `Unknown command: ${c.bold}${command}${c.reset}`
@@ -303,12 +303,96 @@ export function Link({ href, children, onClick, target, ...rest }: LinkProps) {
303
303
 
304
304
  export default Link;
305
305
  `;
306
+ var DOCKERFILE = `# \u2500\u2500 Stage 1: Build Go server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
307
+ FROM golang:1.22-alpine AS go-builder
308
+ WORKDIR /app
309
+ COPY go-server/ go-server/
310
+ WORKDIR /app/go-server
311
+ RUN go build -o /app/blumen-server main.go
312
+
313
+ # \u2500\u2500 Stage 2: Build Node SSR + client bundle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
314
+ FROM node:20-alpine AS node-builder
315
+ WORKDIR /app
316
+ COPY package.json tsconfig.json webpack.config.js ./
317
+ COPY app/ app/
318
+ COPY node-ssr/ node-ssr/
319
+ COPY scripts/ scripts/
320
+ RUN npm install --production=false
321
+ RUN npx tsx scripts/generate-routes.ts
322
+ RUN npx webpack --mode production
323
+ RUN npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm \\
324
+ --outfile=dist/ssr-server.js --external:react --external:react-dom
325
+
326
+ # \u2500\u2500 Stage 3: Production image \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
327
+ FROM node:20-alpine
328
+ RUN apk add --no-cache libc6-compat
329
+ WORKDIR /app
330
+
331
+ # Copy Go binary
332
+ COPY --from=go-builder /app/blumen-server ./blumen-server
333
+
334
+ # Copy Node SSR server + production deps
335
+ COPY --from=node-builder /app/dist/ ./dist/
336
+ COPY --from=node-builder /app/static/ ./static/
337
+ COPY --from=node-builder /app/node_modules/ ./node_modules/
338
+ COPY --from=node-builder /app/package.json ./
339
+
340
+ EXPOSE 3000 4000
341
+
342
+ # Start both services (Go on :3000, Node SSR on :4000)
343
+ CMD sh -c './blumen-server & NODE_ENV=production node dist/ssr-server.js'
344
+ `;
345
+ var DOCKER_COMPOSE = `services:
346
+ app:
347
+ build: .
348
+ ports:
349
+ - "3000:3000"
350
+ environment:
351
+ - NODE_ENV=production
352
+ restart: unless-stopped
353
+ healthcheck:
354
+ test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
355
+ interval: 30s
356
+ timeout: 5s
357
+ retries: 3
358
+ start_period: 10s
359
+ `;
360
+ var DOCKERIGNORE = `node_modules
361
+ dist
362
+ static/js/bundle.js
363
+ .git
364
+ *.log
365
+ .DS_Store
366
+ `;
306
367
  function writeFile(base, relPath, content) {
307
368
  const fullPath = path2.join(base, relPath);
308
369
  fs2.mkdirSync(path2.dirname(fullPath), { recursive: true });
309
370
  fs2.writeFileSync(fullPath, content, "utf-8");
310
371
  }
311
- function getTemplateFiles(projectName) {
372
+ var TEMPLATE_MAP = {
373
+ starter: {
374
+ file: "app/pages/BlumenStarter.tsx",
375
+ label: "Starter",
376
+ desc: "Premium landing page with feature cards"
377
+ },
378
+ empty: {
379
+ file: "app/pages/templates/BlumenEmpty.tsx",
380
+ label: "Empty",
381
+ desc: "Minimal blank project \u2014 just a centered greeting"
382
+ },
383
+ dashboard: {
384
+ file: "app/pages/templates/BlumenDashboard.tsx",
385
+ label: "Dashboard",
386
+ desc: "Admin dashboard with sidebar, stats, and activity feed"
387
+ },
388
+ api: {
389
+ file: "app/pages/templates/BlumenApi.tsx",
390
+ label: "API",
391
+ desc: "API explorer with endpoint list and response preview"
392
+ }
393
+ };
394
+ function getTemplateFiles(projectName, template) {
395
+ const tpl = TEMPLATE_MAP[template] || TEMPLATE_MAP.starter;
312
396
  return [
313
397
  // Generated config
314
398
  ["package.json", pkgJson(projectName)],
@@ -319,8 +403,9 @@ function getTemplateFiles(projectName) {
319
403
  ["app/shared/DefaultApp.tsx", DEFAULT_APP],
320
404
  ["app/shared/DefaultDocument.tsx", DEFAULT_DOCUMENT],
321
405
  ["app/shared/Link.tsx", LINK_TSX],
322
- // Complex filescopied from the framework source (avoids escaping hell)
323
- ["app/pages/Home.tsx", readProjectFile("app/pages/BlumenStarter.tsx")],
406
+ // Home pagedetermined by template choice
407
+ ["app/pages/Home.tsx", readProjectFile(tpl.file)],
408
+ // Complex files — copied from the framework source
324
409
  ["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
325
410
  ["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
326
411
  ["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
@@ -328,7 +413,11 @@ function getTemplateFiles(projectName) {
328
413
  ["go-server/main.go", readProjectFile("go-server/main.go")],
329
414
  ["scripts/generate-routes.ts", readProjectFile("scripts/generate-routes.ts")],
330
415
  // Placeholder
331
- ["static/js/.gitkeep", ""]
416
+ ["static/js/.gitkeep", ""],
417
+ // Docker support (production deployment)
418
+ ["Dockerfile", DOCKERFILE],
419
+ ["docker-compose.yml", DOCKER_COMPOSE],
420
+ [".dockerignore", DOCKERIGNORE]
332
421
  ];
333
422
  }
334
423
  async function create(projectName) {
@@ -338,7 +427,7 @@ async function create(projectName) {
338
427
  log.error("Please provide a project name.");
339
428
  console.log(
340
429
  `
341
- ${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset}
430
+ ${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset} ${c.dim}[--template starter|empty|dashboard|api]${c.reset}
342
431
  `
343
432
  );
344
433
  process.exit(1);
@@ -348,6 +437,25 @@ async function create(projectName) {
348
437
  log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
349
438
  process.exit(1);
350
439
  }
440
+ let template = "";
441
+ const templateFlagIdx = process.argv.indexOf("--template");
442
+ if (templateFlagIdx !== -1 && process.argv[templateFlagIdx + 1]) {
443
+ template = process.argv[templateFlagIdx + 1];
444
+ if (!TEMPLATE_MAP[template]) {
445
+ log.error(`Unknown template: ${c.bold}${template}${c.reset}`);
446
+ log.info(`Available: ${Object.keys(TEMPLATE_MAP).join(", ")}`);
447
+ process.exit(1);
448
+ }
449
+ }
450
+ if (!template) {
451
+ template = await select(
452
+ "Which template do you want to use?",
453
+ Object.entries(TEMPLATE_MAP).map(
454
+ ([key, val]) => `${key} \u2014 ${val.desc}`
455
+ )
456
+ );
457
+ template = template.split(" \u2014 ")[0];
458
+ }
351
459
  const pkgManager = await select("Which package manager do you want to use?", [
352
460
  "npm",
353
461
  "yarn",
@@ -357,8 +465,8 @@ async function create(projectName) {
357
465
  log.blank();
358
466
  divider();
359
467
  log.blank();
360
- log.step(`Creating project in ${c.cyan}${projectName}${c.reset}...`);
361
- const files = getTemplateFiles(projectName);
468
+ log.step(`Creating project in ${c.cyan}${projectName}${c.reset} with ${c.magenta}${template}${c.reset} template...`);
469
+ const files = getTemplateFiles(projectName, template);
362
470
  for (const [relPath, content] of files) {
363
471
  writeFile(projectDir, relPath, content);
364
472
  }
@@ -384,7 +492,7 @@ async function create(projectName) {
384
492
  log.blank();
385
493
  divider();
386
494
  log.blank();
387
- log.success(`${c.bold}Project created!${c.reset}`);
495
+ log.success(`${c.bold}Project created!${c.reset} (template: ${c.magenta}${template}${c.reset})`);
388
496
  log.blank();
389
497
  console.log(` ${c.dim}Next steps:${c.reset}`);
390
498
  console.log(` cd ${projectName}`);