blumenjs 0.1.4 → 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() {
@@ -621,7 +639,30 @@ function writeFile(base, relPath, content) {
621
639
  fs3.mkdirSync(path2.dirname(fullPath), { recursive: true });
622
640
  fs3.writeFileSync(fullPath, content, "utf-8");
623
641
  }
624
- 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;
625
666
  return [
626
667
  // Generated config
627
668
  ["package.json", pkgJson(projectName)],
@@ -632,8 +673,9 @@ function getTemplateFiles(projectName) {
632
673
  ["app/shared/DefaultApp.tsx", DEFAULT_APP],
633
674
  ["app/shared/DefaultDocument.tsx", DEFAULT_DOCUMENT],
634
675
  ["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")],
676
+ // Home pagedetermined by template choice
677
+ ["app/pages/Home.tsx", readProjectFile(tpl.file)],
678
+ // Complex files — copied from the framework source
637
679
  ["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
638
680
  ["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
639
681
  ["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
@@ -655,7 +697,7 @@ async function create(projectName) {
655
697
  log.error("Please provide a project name.");
656
698
  console.log(
657
699
  `
658
- ${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}
659
701
  `
660
702
  );
661
703
  process.exit(1);
@@ -665,6 +707,25 @@ async function create(projectName) {
665
707
  log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
666
708
  process.exit(1);
667
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
+ }
668
729
  const pkgManager = await select("Which package manager do you want to use?", [
669
730
  "npm",
670
731
  "yarn",
@@ -674,8 +735,8 @@ async function create(projectName) {
674
735
  log.blank();
675
736
  divider();
676
737
  log.blank();
677
- log.step(`Creating project in ${c.cyan}${projectName}${c.reset}...`);
678
- 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);
679
740
  for (const [relPath, content] of files) {
680
741
  writeFile(projectDir, relPath, content);
681
742
  }
@@ -701,7 +762,7 @@ async function create(projectName) {
701
762
  log.blank();
702
763
  divider();
703
764
  log.blank();
704
- log.success(`${c.bold}Project created!${c.reset}`);
765
+ log.success(`${c.bold}Project created!${c.reset} (template: ${c.magenta}${template}${c.reset})`);
705
766
  log.blank();
706
767
  console.log(` ${c.dim}Next steps:${c.reset}`);
707
768
  console.log(` cd ${projectName}`);
@@ -709,6 +770,271 @@ async function create(projectName) {
709
770
  log.blank();
710
771
  }
711
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
+
712
1038
  // cli/blumen.ts
713
1039
  async function main() {
714
1040
  const command = process.argv[2];
@@ -727,6 +1053,9 @@ async function main() {
727
1053
  console.log(
728
1054
  ` create Scaffold a new Blumen project`
729
1055
  );
1056
+ console.log(
1057
+ ` deploy Deploy to Docker, Fly.io, or Railway`
1058
+ );
730
1059
  console.log("");
731
1060
  console.log(` ${c.bold}Options${c.reset}`);
732
1061
  console.log(` --help Show this help message`);
@@ -751,6 +1080,9 @@ async function main() {
751
1080
  case "create":
752
1081
  await create(process.argv[3]);
753
1082
  break;
1083
+ case "deploy":
1084
+ await deploy(process.argv[3]);
1085
+ break;
754
1086
  default:
755
1087
  log.error(
756
1088
  `Unknown command: ${c.bold}${command}${c.reset}`
@@ -369,7 +369,30 @@ function writeFile(base, relPath, content) {
369
369
  fs2.mkdirSync(path2.dirname(fullPath), { recursive: true });
370
370
  fs2.writeFileSync(fullPath, content, "utf-8");
371
371
  }
372
- 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;
373
396
  return [
374
397
  // Generated config
375
398
  ["package.json", pkgJson(projectName)],
@@ -380,8 +403,9 @@ function getTemplateFiles(projectName) {
380
403
  ["app/shared/DefaultApp.tsx", DEFAULT_APP],
381
404
  ["app/shared/DefaultDocument.tsx", DEFAULT_DOCUMENT],
382
405
  ["app/shared/Link.tsx", LINK_TSX],
383
- // Complex filescopied from the framework source (avoids escaping hell)
384
- ["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
385
409
  ["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
386
410
  ["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
387
411
  ["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
@@ -403,7 +427,7 @@ async function create(projectName) {
403
427
  log.error("Please provide a project name.");
404
428
  console.log(
405
429
  `
406
- ${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}
407
431
  `
408
432
  );
409
433
  process.exit(1);
@@ -413,6 +437,25 @@ async function create(projectName) {
413
437
  log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
414
438
  process.exit(1);
415
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
+ }
416
459
  const pkgManager = await select("Which package manager do you want to use?", [
417
460
  "npm",
418
461
  "yarn",
@@ -422,8 +465,8 @@ async function create(projectName) {
422
465
  log.blank();
423
466
  divider();
424
467
  log.blank();
425
- log.step(`Creating project in ${c.cyan}${projectName}${c.reset}...`);
426
- 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);
427
470
  for (const [relPath, content] of files) {
428
471
  writeFile(projectDir, relPath, content);
429
472
  }
@@ -449,7 +492,7 @@ async function create(projectName) {
449
492
  log.blank();
450
493
  divider();
451
494
  log.blank();
452
- log.success(`${c.bold}Project created!${c.reset}`);
495
+ log.success(`${c.bold}Project created!${c.reset} (template: ${c.magenta}${template}${c.reset})`);
453
496
  log.blank();
454
497
  console.log(` ${c.dim}Next steps:${c.reset}`);
455
498
  console.log(` cd ${projectName}`);