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.
- package/cli/postinstall.cjs +74 -0
- package/dist/cli/blumen.js +449 -13
- package/dist/cli/commands/create.js +170 -9
- package/dist/cli/commands/deploy.js +346 -0
- package/dist/cli/commands/dev.js +120 -2
- package/dist/cli/utils.js +99 -0
- package/dist/templates/app/pages/templates/BlumenApi.tsx +489 -0
- package/dist/templates/app/pages/templates/BlumenDashboard.tsx +344 -0
- package/dist/templates/app/pages/templates/BlumenEmpty.tsx +128 -0
- package/package.json +3 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// cli/commands/create.ts
|
|
2
2
|
import * as fs2 from "fs";
|
|
3
3
|
import * as path2 from "path";
|
|
4
|
-
import { execSync } from "child_process";
|
|
4
|
+
import { execSync as execSync2 } from "child_process";
|
|
5
5
|
|
|
6
6
|
// cli/utils.ts
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import * as path from "path";
|
|
9
|
+
import * as os from "os";
|
|
9
10
|
import { fileURLToPath } from "url";
|
|
11
|
+
import { execSync } from "child_process";
|
|
10
12
|
var c = {
|
|
11
13
|
reset: "\x1B[0m",
|
|
12
14
|
bold: "\x1B[1m",
|
|
@@ -85,6 +87,118 @@ async function select(question, options) {
|
|
|
85
87
|
});
|
|
86
88
|
});
|
|
87
89
|
}
|
|
90
|
+
async function confirm(question, defaultYes = true) {
|
|
91
|
+
const readline = await import("readline");
|
|
92
|
+
const rl = readline.createInterface({
|
|
93
|
+
input: process.stdin,
|
|
94
|
+
output: process.stdout
|
|
95
|
+
});
|
|
96
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
97
|
+
return new Promise((resolve2) => {
|
|
98
|
+
rl.question(` ${c.bold}${question}${c.reset} ${c.dim}(${hint})${c.reset} `, (answer) => {
|
|
99
|
+
rl.close();
|
|
100
|
+
const a = answer.trim().toLowerCase();
|
|
101
|
+
if (a === "")
|
|
102
|
+
resolve2(defaultYes);
|
|
103
|
+
else
|
|
104
|
+
resolve2(a === "y" || a === "yes");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function checkGoInstalled() {
|
|
109
|
+
try {
|
|
110
|
+
execSync("go version", { stdio: "pipe" });
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function getGoVersion() {
|
|
117
|
+
try {
|
|
118
|
+
const out = execSync("go version", { encoding: "utf-8" }).trim();
|
|
119
|
+
const match = out.match(/go(\d+\.\d+(\.\d+)?)/);
|
|
120
|
+
return match ? match[1] : out;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function ensureGo() {
|
|
126
|
+
if (checkGoInstalled()) {
|
|
127
|
+
const ver = getGoVersion();
|
|
128
|
+
log.success(`Go ${c.bold}${ver}${c.reset} detected`);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
log.blank();
|
|
132
|
+
log.warn(`${c.bold}Go is not installed${c.reset}`);
|
|
133
|
+
log.info("Blumen uses Go for its high-performance HTTP server layer.");
|
|
134
|
+
log.blank();
|
|
135
|
+
const platform2 = os.platform();
|
|
136
|
+
if (platform2 === "darwin") {
|
|
137
|
+
let hasBrew = false;
|
|
138
|
+
try {
|
|
139
|
+
execSync("brew --version", { stdio: "pipe" });
|
|
140
|
+
hasBrew = true;
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
if (hasBrew) {
|
|
144
|
+
const doInstall = await confirm("Install Go via Homebrew? (brew install go)");
|
|
145
|
+
if (doInstall) {
|
|
146
|
+
log.step("Installing Go via Homebrew...");
|
|
147
|
+
try {
|
|
148
|
+
execSync("brew install go", { stdio: "inherit" });
|
|
149
|
+
log.success("Go installed successfully!");
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
log.error("Homebrew install failed. Please install manually.");
|
|
153
|
+
console.log(`
|
|
154
|
+
${c.cyan}https://go.dev/dl/${c.reset}
|
|
155
|
+
`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
console.log(` ${c.bold}Install Go manually:${c.reset}`);
|
|
161
|
+
console.log(` ${c.cyan}brew install go${c.reset} ${c.dim}(if you install Homebrew first)${c.reset}`);
|
|
162
|
+
console.log(` ${c.dim}or download from${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
|
|
163
|
+
} else if (platform2 === "linux") {
|
|
164
|
+
let hasApt = false;
|
|
165
|
+
try {
|
|
166
|
+
execSync("apt --version", { stdio: "pipe" });
|
|
167
|
+
hasApt = true;
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
if (hasApt) {
|
|
171
|
+
const doInstall = await confirm("Install Go via apt? (sudo apt install golang-go)");
|
|
172
|
+
if (doInstall) {
|
|
173
|
+
log.step("Installing Go via apt...");
|
|
174
|
+
try {
|
|
175
|
+
execSync("sudo apt update && sudo apt install -y golang-go", { stdio: "inherit" });
|
|
176
|
+
log.success("Go installed successfully!");
|
|
177
|
+
return true;
|
|
178
|
+
} catch {
|
|
179
|
+
log.error("apt install failed. Please install manually.");
|
|
180
|
+
console.log(`
|
|
181
|
+
${c.cyan}https://go.dev/dl/${c.reset}
|
|
182
|
+
`);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
console.log(` ${c.bold}Install Go:${c.reset}`);
|
|
188
|
+
console.log(` ${c.cyan}sudo apt install golang-go${c.reset} ${c.dim}(Debian/Ubuntu)${c.reset}`);
|
|
189
|
+
console.log(` ${c.cyan}sudo dnf install golang${c.reset} ${c.dim}(Fedora)${c.reset}`);
|
|
190
|
+
console.log(` ${c.dim}or download from${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
|
|
191
|
+
} else if (platform2 === "win32") {
|
|
192
|
+
console.log(` ${c.bold}Install Go (Windows):${c.reset}`);
|
|
193
|
+
console.log(` ${c.cyan}winget install GoLang.Go${c.reset}`);
|
|
194
|
+
console.log(` ${c.dim}or download from${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
|
|
195
|
+
} else {
|
|
196
|
+
console.log(` ${c.bold}Install Go:${c.reset} ${c.cyan}https://go.dev/dl/${c.reset}`);
|
|
197
|
+
}
|
|
198
|
+
log.blank();
|
|
199
|
+
log.info("After installing Go, restart your terminal and try again.");
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
88
202
|
|
|
89
203
|
// cli/commands/create.ts
|
|
90
204
|
function getFrameworkRoot() {
|
|
@@ -369,7 +483,30 @@ function writeFile(base, relPath, content) {
|
|
|
369
483
|
fs2.mkdirSync(path2.dirname(fullPath), { recursive: true });
|
|
370
484
|
fs2.writeFileSync(fullPath, content, "utf-8");
|
|
371
485
|
}
|
|
372
|
-
|
|
486
|
+
var TEMPLATE_MAP = {
|
|
487
|
+
starter: {
|
|
488
|
+
file: "app/pages/BlumenStarter.tsx",
|
|
489
|
+
label: "Starter",
|
|
490
|
+
desc: "Premium landing page with feature cards"
|
|
491
|
+
},
|
|
492
|
+
empty: {
|
|
493
|
+
file: "app/pages/templates/BlumenEmpty.tsx",
|
|
494
|
+
label: "Empty",
|
|
495
|
+
desc: "Minimal blank project \u2014 just a centered greeting"
|
|
496
|
+
},
|
|
497
|
+
dashboard: {
|
|
498
|
+
file: "app/pages/templates/BlumenDashboard.tsx",
|
|
499
|
+
label: "Dashboard",
|
|
500
|
+
desc: "Admin dashboard with sidebar, stats, and activity feed"
|
|
501
|
+
},
|
|
502
|
+
api: {
|
|
503
|
+
file: "app/pages/templates/BlumenApi.tsx",
|
|
504
|
+
label: "API",
|
|
505
|
+
desc: "API explorer with endpoint list and response preview"
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
function getTemplateFiles(projectName, template) {
|
|
509
|
+
const tpl = TEMPLATE_MAP[template] || TEMPLATE_MAP.starter;
|
|
373
510
|
return [
|
|
374
511
|
// Generated config
|
|
375
512
|
["package.json", pkgJson(projectName)],
|
|
@@ -380,8 +517,9 @@ function getTemplateFiles(projectName) {
|
|
|
380
517
|
["app/shared/DefaultApp.tsx", DEFAULT_APP],
|
|
381
518
|
["app/shared/DefaultDocument.tsx", DEFAULT_DOCUMENT],
|
|
382
519
|
["app/shared/Link.tsx", LINK_TSX],
|
|
383
|
-
//
|
|
384
|
-
["app/pages/Home.tsx", readProjectFile(
|
|
520
|
+
// Home page — determined by template choice
|
|
521
|
+
["app/pages/Home.tsx", readProjectFile(tpl.file)],
|
|
522
|
+
// Complex files — copied from the framework source
|
|
385
523
|
["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
|
|
386
524
|
["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
|
|
387
525
|
["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
|
|
@@ -399,11 +537,15 @@ function getTemplateFiles(projectName) {
|
|
|
399
537
|
async function create(projectName) {
|
|
400
538
|
banner();
|
|
401
539
|
log.info("Create a new Blumen project\n");
|
|
540
|
+
const goReady = await ensureGo();
|
|
541
|
+
if (!goReady) {
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
402
544
|
if (!projectName) {
|
|
403
545
|
log.error("Please provide a project name.");
|
|
404
546
|
console.log(
|
|
405
547
|
`
|
|
406
|
-
${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset}
|
|
548
|
+
${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset} ${c.dim}[--template starter|empty|dashboard|api]${c.reset}
|
|
407
549
|
`
|
|
408
550
|
);
|
|
409
551
|
process.exit(1);
|
|
@@ -413,6 +555,25 @@ async function create(projectName) {
|
|
|
413
555
|
log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
|
|
414
556
|
process.exit(1);
|
|
415
557
|
}
|
|
558
|
+
let template = "";
|
|
559
|
+
const templateFlagIdx = process.argv.indexOf("--template");
|
|
560
|
+
if (templateFlagIdx !== -1 && process.argv[templateFlagIdx + 1]) {
|
|
561
|
+
template = process.argv[templateFlagIdx + 1];
|
|
562
|
+
if (!TEMPLATE_MAP[template]) {
|
|
563
|
+
log.error(`Unknown template: ${c.bold}${template}${c.reset}`);
|
|
564
|
+
log.info(`Available: ${Object.keys(TEMPLATE_MAP).join(", ")}`);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!template) {
|
|
569
|
+
template = await select(
|
|
570
|
+
"Which template do you want to use?",
|
|
571
|
+
Object.entries(TEMPLATE_MAP).map(
|
|
572
|
+
([key, val]) => `${key} \u2014 ${val.desc}`
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
template = template.split(" \u2014 ")[0];
|
|
576
|
+
}
|
|
416
577
|
const pkgManager = await select("Which package manager do you want to use?", [
|
|
417
578
|
"npm",
|
|
418
579
|
"yarn",
|
|
@@ -422,8 +583,8 @@ async function create(projectName) {
|
|
|
422
583
|
log.blank();
|
|
423
584
|
divider();
|
|
424
585
|
log.blank();
|
|
425
|
-
log.step(`Creating project in ${c.cyan}${projectName}${c.reset}...`);
|
|
426
|
-
const files = getTemplateFiles(projectName);
|
|
586
|
+
log.step(`Creating project in ${c.cyan}${projectName}${c.reset} with ${c.magenta}${template}${c.reset} template...`);
|
|
587
|
+
const files = getTemplateFiles(projectName, template);
|
|
427
588
|
for (const [relPath, content] of files) {
|
|
428
589
|
writeFile(projectDir, relPath, content);
|
|
429
590
|
}
|
|
@@ -436,7 +597,7 @@ async function create(projectName) {
|
|
|
436
597
|
bun: "bun install"
|
|
437
598
|
};
|
|
438
599
|
try {
|
|
439
|
-
|
|
600
|
+
execSync2(installCmd[pkgManager], {
|
|
440
601
|
cwd: projectDir,
|
|
441
602
|
stdio: "inherit"
|
|
442
603
|
});
|
|
@@ -449,7 +610,7 @@ async function create(projectName) {
|
|
|
449
610
|
log.blank();
|
|
450
611
|
divider();
|
|
451
612
|
log.blank();
|
|
452
|
-
log.success(`${c.bold}Project created!${c.reset}`);
|
|
613
|
+
log.success(`${c.bold}Project created!${c.reset} (template: ${c.magenta}${template}${c.reset})`);
|
|
453
614
|
log.blank();
|
|
454
615
|
console.log(` ${c.dim}Next steps:${c.reset}`);
|
|
455
616
|
console.log(` cd ${projectName}`);
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// cli/commands/deploy.ts
|
|
2
|
+
import { execSync, spawnSync } from "child_process";
|
|
3
|
+
import * as fs2 from "fs";
|
|
4
|
+
import * as path2 from "path";
|
|
5
|
+
|
|
6
|
+
// cli/utils.ts
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var c = {
|
|
11
|
+
reset: "\x1B[0m",
|
|
12
|
+
bold: "\x1B[1m",
|
|
13
|
+
dim: "\x1B[2m",
|
|
14
|
+
red: "\x1B[31m",
|
|
15
|
+
green: "\x1B[32m",
|
|
16
|
+
yellow: "\x1B[33m",
|
|
17
|
+
blue: "\x1B[34m",
|
|
18
|
+
magenta: "\x1B[35m",
|
|
19
|
+
cyan: "\x1B[36m",
|
|
20
|
+
white: "\x1B[37m",
|
|
21
|
+
gray: "\x1B[90m"
|
|
22
|
+
};
|
|
23
|
+
var log = {
|
|
24
|
+
info: (msg) => console.log(` ${c.magenta}\u25CF${c.reset} ${msg}`),
|
|
25
|
+
success: (msg) => console.log(` ${c.green}\u2713${c.reset} ${msg}`),
|
|
26
|
+
error: (msg) => console.error(` ${c.red}\u2717${c.reset} ${msg}`),
|
|
27
|
+
warn: (msg) => console.log(` ${c.yellow}\u26A0${c.reset} ${msg}`),
|
|
28
|
+
step: (msg) => console.log(` ${c.dim}\u2192${c.reset} ${msg}`),
|
|
29
|
+
blank: () => console.log("")
|
|
30
|
+
};
|
|
31
|
+
function getVersion() {
|
|
32
|
+
try {
|
|
33
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
34
|
+
let dir = path.dirname(thisFile);
|
|
35
|
+
for (let i = 0; i < 5; i++) {
|
|
36
|
+
const pkgFile = path.join(dir, "package.json");
|
|
37
|
+
if (fs.existsSync(pkgFile)) {
|
|
38
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf-8"));
|
|
39
|
+
if (pkg.name === "blumenjs" || pkg.name === "go-react-ssr") {
|
|
40
|
+
return pkg.version;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
dir = path.dirname(dir);
|
|
44
|
+
}
|
|
45
|
+
return "0.0.0";
|
|
46
|
+
} catch {
|
|
47
|
+
return "0.0.0";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function banner() {
|
|
51
|
+
const version = getVersion();
|
|
52
|
+
console.log("");
|
|
53
|
+
console.log(
|
|
54
|
+
` ${c.magenta}${c.bold}\u{1F338} Blumen${c.reset} ${c.dim}v${version}${c.reset}`
|
|
55
|
+
);
|
|
56
|
+
console.log(
|
|
57
|
+
` ${c.dim}The React framework powered by Go${c.reset}`
|
|
58
|
+
);
|
|
59
|
+
console.log("");
|
|
60
|
+
}
|
|
61
|
+
function divider() {
|
|
62
|
+
console.log(` ${c.dim}${"\u2500".repeat(48)}${c.reset}`);
|
|
63
|
+
}
|
|
64
|
+
async function confirm(question, defaultYes = true) {
|
|
65
|
+
const readline = await import("readline");
|
|
66
|
+
const rl = readline.createInterface({
|
|
67
|
+
input: process.stdin,
|
|
68
|
+
output: process.stdout
|
|
69
|
+
});
|
|
70
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
rl.question(` ${c.bold}${question}${c.reset} ${c.dim}(${hint})${c.reset} `, (answer) => {
|
|
73
|
+
rl.close();
|
|
74
|
+
const a = answer.trim().toLowerCase();
|
|
75
|
+
if (a === "")
|
|
76
|
+
resolve(defaultYes);
|
|
77
|
+
else
|
|
78
|
+
resolve(a === "y" || a === "yes");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// cli/commands/deploy.ts
|
|
84
|
+
var PLATFORMS = [
|
|
85
|
+
{ name: "Docker (any cloud)", ok: true, note: "Best option \u2014 multi-stage Dockerfile included" },
|
|
86
|
+
{ name: "Railway", ok: true, note: "Auto-detects Dockerfile, deploy with railway up" },
|
|
87
|
+
{ name: "Fly.io", ok: true, note: "Docker-based deploys via fly deploy" },
|
|
88
|
+
{ name: "Render", ok: true, note: "Docker support, auto-deploy from Git" },
|
|
89
|
+
{ name: "AWS EC2 / GCP / Azure", ok: true, note: "Full control, any runtime" },
|
|
90
|
+
{ name: "DigitalOcean", ok: true, note: "App Platform supports Docker" },
|
|
91
|
+
{ name: "Vercel", ok: false, note: "Node-only, no Go runtime" },
|
|
92
|
+
{ name: "Netlify", ok: false, note: "Static/serverless only, no Go" }
|
|
93
|
+
];
|
|
94
|
+
function hasCommand(cmd) {
|
|
95
|
+
try {
|
|
96
|
+
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function ensureDockerfile() {
|
|
103
|
+
if (!fs2.existsSync("Dockerfile")) {
|
|
104
|
+
log.error("No Dockerfile found in the current directory.");
|
|
105
|
+
log.info(
|
|
106
|
+
`Run ${c.bold}blumen create${c.reset} to scaffold a project with Docker support.`
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function deployDocker() {
|
|
112
|
+
log.info("Docker Deployment\n");
|
|
113
|
+
ensureDockerfile();
|
|
114
|
+
if (!hasCommand("docker")) {
|
|
115
|
+
log.error("Docker is not installed.");
|
|
116
|
+
log.info(
|
|
117
|
+
`Install Docker: ${c.cyan}https://docs.docker.com/get-docker/${c.reset}`
|
|
118
|
+
);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const projectName = path2.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
122
|
+
const imageName = `blumen-${projectName}`;
|
|
123
|
+
log.step(`Building image: ${c.bold}${imageName}${c.reset}...`);
|
|
124
|
+
divider();
|
|
125
|
+
log.blank();
|
|
126
|
+
const buildResult = spawnSync("docker", ["build", "-t", imageName, "."], {
|
|
127
|
+
cwd: process.cwd(),
|
|
128
|
+
stdio: "inherit"
|
|
129
|
+
});
|
|
130
|
+
log.blank();
|
|
131
|
+
divider();
|
|
132
|
+
if (buildResult.status !== 0) {
|
|
133
|
+
log.error("Docker build failed.");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
log.success(`Image ${c.bold}${imageName}${c.reset} built successfully!`);
|
|
137
|
+
log.blank();
|
|
138
|
+
const shouldRun = await confirm("Run the container now?");
|
|
139
|
+
if (shouldRun) {
|
|
140
|
+
log.step(`Starting ${c.bold}${imageName}${c.reset} on port 3000...`);
|
|
141
|
+
log.blank();
|
|
142
|
+
console.log(
|
|
143
|
+
` ${c.dim}\u279C${c.reset} ${c.bold}App${c.reset}: ${c.cyan}http://localhost:3000${c.reset}`
|
|
144
|
+
);
|
|
145
|
+
console.log(
|
|
146
|
+
` ${c.dim}Press ${c.bold}Ctrl+C${c.reset}${c.dim} to stop.${c.reset}`
|
|
147
|
+
);
|
|
148
|
+
log.blank();
|
|
149
|
+
spawnSync("docker", ["run", "--rm", "-p", "3000:3000", imageName], {
|
|
150
|
+
cwd: process.cwd(),
|
|
151
|
+
stdio: "inherit"
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
log.blank();
|
|
155
|
+
console.log(` ${c.dim}Run manually:${c.reset}`);
|
|
156
|
+
console.log(` docker run -p 3000:3000 ${imageName}`);
|
|
157
|
+
log.blank();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function deployFly() {
|
|
161
|
+
log.info("Fly.io Deployment\n");
|
|
162
|
+
ensureDockerfile();
|
|
163
|
+
const projectName = path2.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
164
|
+
if (!fs2.existsSync("fly.toml")) {
|
|
165
|
+
log.step("Generating fly.toml...");
|
|
166
|
+
const flyConfig = `# Fly.io configuration for Blumen app
|
|
167
|
+
# Deploy with: fly deploy
|
|
168
|
+
|
|
169
|
+
app = "${projectName}"
|
|
170
|
+
primary_region = "iad"
|
|
171
|
+
|
|
172
|
+
[build]
|
|
173
|
+
|
|
174
|
+
[http_service]
|
|
175
|
+
internal_port = 3000
|
|
176
|
+
force_https = true
|
|
177
|
+
auto_stop_machines = "stop"
|
|
178
|
+
auto_start_machines = true
|
|
179
|
+
min_machines_running = 0
|
|
180
|
+
|
|
181
|
+
[checks]
|
|
182
|
+
[checks.health]
|
|
183
|
+
type = "http"
|
|
184
|
+
port = 3000
|
|
185
|
+
path = "/"
|
|
186
|
+
interval = "30s"
|
|
187
|
+
timeout = "5s"
|
|
188
|
+
|
|
189
|
+
[[vm]]
|
|
190
|
+
memory = "512mb"
|
|
191
|
+
cpu_kind = "shared"
|
|
192
|
+
cpus = 1
|
|
193
|
+
`;
|
|
194
|
+
fs2.writeFileSync("fly.toml", flyConfig);
|
|
195
|
+
log.success("fly.toml created");
|
|
196
|
+
} else {
|
|
197
|
+
log.info("fly.toml already exists, skipping generation.");
|
|
198
|
+
}
|
|
199
|
+
log.blank();
|
|
200
|
+
if (!hasCommand("fly") && !hasCommand("flyctl")) {
|
|
201
|
+
log.warn("Fly CLI is not installed.");
|
|
202
|
+
log.blank();
|
|
203
|
+
console.log(` ${c.dim}Install:${c.reset}`);
|
|
204
|
+
console.log(` curl -L https://fly.io/install.sh | sh`);
|
|
205
|
+
log.blank();
|
|
206
|
+
console.log(` ${c.dim}Then deploy:${c.reset}`);
|
|
207
|
+
console.log(` fly launch`);
|
|
208
|
+
console.log(` fly deploy`);
|
|
209
|
+
log.blank();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const shouldDeploy = await confirm("Deploy to Fly.io now?");
|
|
213
|
+
if (shouldDeploy) {
|
|
214
|
+
log.step("Deploying to Fly.io...");
|
|
215
|
+
divider();
|
|
216
|
+
log.blank();
|
|
217
|
+
const flyCmd = hasCommand("fly") ? "fly" : "flyctl";
|
|
218
|
+
spawnSync(flyCmd, ["deploy"], {
|
|
219
|
+
cwd: process.cwd(),
|
|
220
|
+
stdio: "inherit"
|
|
221
|
+
});
|
|
222
|
+
log.blank();
|
|
223
|
+
divider();
|
|
224
|
+
log.success("Deployment complete!");
|
|
225
|
+
} else {
|
|
226
|
+
log.blank();
|
|
227
|
+
console.log(` ${c.dim}Deploy later:${c.reset}`);
|
|
228
|
+
console.log(` fly deploy`);
|
|
229
|
+
log.blank();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function deployRailway() {
|
|
233
|
+
log.info("Railway Deployment\n");
|
|
234
|
+
ensureDockerfile();
|
|
235
|
+
if (!fs2.existsSync("railway.toml")) {
|
|
236
|
+
log.step("Generating railway.toml...");
|
|
237
|
+
const railwayConfig = `# Railway configuration for Blumen app
|
|
238
|
+
# Deploy with: railway up
|
|
239
|
+
|
|
240
|
+
[build]
|
|
241
|
+
builder = "DOCKERFILE"
|
|
242
|
+
dockerfilePath = "Dockerfile"
|
|
243
|
+
|
|
244
|
+
[deploy]
|
|
245
|
+
startCommand = ""
|
|
246
|
+
healthcheckPath = "/"
|
|
247
|
+
healthcheckTimeout = 30
|
|
248
|
+
restartPolicyType = "ON_FAILURE"
|
|
249
|
+
restartPolicyMaxRetries = 5
|
|
250
|
+
`;
|
|
251
|
+
fs2.writeFileSync("railway.toml", railwayConfig);
|
|
252
|
+
log.success("railway.toml created");
|
|
253
|
+
} else {
|
|
254
|
+
log.info("railway.toml already exists, skipping generation.");
|
|
255
|
+
}
|
|
256
|
+
log.blank();
|
|
257
|
+
if (!hasCommand("railway")) {
|
|
258
|
+
log.warn("Railway CLI is not installed.");
|
|
259
|
+
log.blank();
|
|
260
|
+
console.log(` ${c.dim}Install:${c.reset}`);
|
|
261
|
+
console.log(` npm install -g @railway/cli`);
|
|
262
|
+
log.blank();
|
|
263
|
+
console.log(` ${c.dim}Then deploy:${c.reset}`);
|
|
264
|
+
console.log(` railway login`);
|
|
265
|
+
console.log(` railway up`);
|
|
266
|
+
log.blank();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const shouldDeploy = await confirm("Deploy to Railway now?");
|
|
270
|
+
if (shouldDeploy) {
|
|
271
|
+
log.step("Deploying to Railway...");
|
|
272
|
+
divider();
|
|
273
|
+
log.blank();
|
|
274
|
+
spawnSync("railway", ["up"], {
|
|
275
|
+
cwd: process.cwd(),
|
|
276
|
+
stdio: "inherit"
|
|
277
|
+
});
|
|
278
|
+
log.blank();
|
|
279
|
+
divider();
|
|
280
|
+
log.success("Deployment complete!");
|
|
281
|
+
} else {
|
|
282
|
+
log.blank();
|
|
283
|
+
console.log(` ${c.dim}Deploy later:${c.reset}`);
|
|
284
|
+
console.log(` railway up`);
|
|
285
|
+
log.blank();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function showInfo() {
|
|
289
|
+
log.info("Hosting Compatibility\n");
|
|
290
|
+
console.log(` Blumen apps require ${c.bold}Go${c.reset} + ${c.bold}Node.js${c.reset} \u2014 use Docker-compatible platforms.
|
|
291
|
+
`);
|
|
292
|
+
const maxName = Math.max(...PLATFORMS.map((p) => p.name.length));
|
|
293
|
+
for (const p of PLATFORMS) {
|
|
294
|
+
const icon = p.ok ? `${c.green}\u2713${c.reset}` : `${c.red}\u2717${c.reset}`;
|
|
295
|
+
const name = p.name.padEnd(maxName + 2);
|
|
296
|
+
const note = `${c.dim}${p.note}${c.reset}`;
|
|
297
|
+
console.log(` ${icon} ${name} ${note}`);
|
|
298
|
+
}
|
|
299
|
+
log.blank();
|
|
300
|
+
divider();
|
|
301
|
+
log.blank();
|
|
302
|
+
console.log(` ${c.bold}Quick start:${c.reset}`);
|
|
303
|
+
log.blank();
|
|
304
|
+
console.log(` ${c.dim}Docker (local):${c.reset} docker compose up`);
|
|
305
|
+
console.log(` ${c.dim}Fly.io:${c.reset} blumen deploy fly`);
|
|
306
|
+
console.log(` ${c.dim}Railway:${c.reset} blumen deploy railway`);
|
|
307
|
+
log.blank();
|
|
308
|
+
}
|
|
309
|
+
async function deploy(subcommand) {
|
|
310
|
+
banner();
|
|
311
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
312
|
+
log.info("Deploy your Blumen app\n");
|
|
313
|
+
console.log(` ${c.bold}Usage${c.reset} blumen deploy ${c.dim}<target>${c.reset}
|
|
314
|
+
`);
|
|
315
|
+
console.log(` ${c.bold}Targets${c.reset}`);
|
|
316
|
+
console.log(` docker Build Docker image and optionally run`);
|
|
317
|
+
console.log(` fly Generate config and deploy to Fly.io`);
|
|
318
|
+
console.log(` railway Generate config and deploy to Railway`);
|
|
319
|
+
console.log(` info Show hosting compatibility matrix`);
|
|
320
|
+
console.log("");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
switch (subcommand) {
|
|
324
|
+
case "docker":
|
|
325
|
+
await deployDocker();
|
|
326
|
+
break;
|
|
327
|
+
case "fly":
|
|
328
|
+
await deployFly();
|
|
329
|
+
break;
|
|
330
|
+
case "railway":
|
|
331
|
+
await deployRailway();
|
|
332
|
+
break;
|
|
333
|
+
case "info":
|
|
334
|
+
showInfo();
|
|
335
|
+
break;
|
|
336
|
+
default:
|
|
337
|
+
log.error(`Unknown deploy target: ${c.bold}${subcommand}${c.reset}`);
|
|
338
|
+
log.info(
|
|
339
|
+
`Run ${c.bold}blumen deploy --help${c.reset} for available targets.`
|
|
340
|
+
);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
export {
|
|
345
|
+
deploy
|
|
346
|
+
};
|