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.
- package/dist/cli/blumen.js +339 -7
- package/dist/cli/commands/create.js +50 -7
- package/dist/cli/commands/deploy.js +346 -0
- package/dist/templates/app/pages/templates/BlumenApi.tsx +489 -0
- package/dist/templates/app/pages/templates/BlumenDashboard.tsx +546 -0
- package/dist/templates/app/pages/templates/BlumenEmpty.tsx +128 -0
- package/package.json +1 -1
package/dist/cli/blumen.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
636
|
-
["app/pages/Home.tsx", readProjectFile(
|
|
676
|
+
// Home page — determined 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
|
-
|
|
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
|
-
//
|
|
384
|
-
["app/pages/Home.tsx", readProjectFile(
|
|
406
|
+
// Home page — determined 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}`);
|