business-stack 0.1.0 → 0.1.3
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/README.md +118 -0
- package/bin/business-stack.cjs +99 -24
- package/package.json +2 -1
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# business-stack
|
|
2
|
+
|
|
3
|
+
Single npm package bundling a **Next.js** app, **Hono** gateway (Better Auth, integrations, authenticated proxy to FastAPI), and **FastAPI** backend. Turborepo runs web, gateway, and backend together.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Install these on your machine and ensure they are on your `PATH`:
|
|
8
|
+
|
|
9
|
+
| Tool | Purpose |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| [Node.js](https://nodejs.org/) 20+ | Runs the `business-stack` CLI (`setup`, `start`, `doctor`) |
|
|
12
|
+
| [Bun](https://bun.sh) | Installs JS workspaces and runs the gateway |
|
|
13
|
+
| [uv](https://docs.astral.sh/uv/) | Installs and runs the Python backend |
|
|
14
|
+
| Python **3.12** | Required by the backend (managed via `uv`) |
|
|
15
|
+
|
|
16
|
+
Check your environment:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx business-stack doctor
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Using `npx` (no install, or one-off runs)
|
|
23
|
+
|
|
24
|
+
`npx` runs the same `business-stack` binary as a global or local install. Use a version tag so npm does not cache an old release:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx --yes business-stack@latest doctor
|
|
28
|
+
npx --yes business-stack@latest setup --yes
|
|
29
|
+
npx --yes business-stack@latest start
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
On **Windows Command Prompt**, use the same commands; if `npx` is slow the first time, it is downloading the package.
|
|
33
|
+
|
|
34
|
+
## Installation (recommended: project-level)
|
|
35
|
+
|
|
36
|
+
**Prefer a local (project) install** so the stack, generated `.env` files, and SQLite database live under your project’s `node_modules/business-stack/`, match npm’s default `npm i business-stack`, and avoid permission or PATH issues from global installs.
|
|
37
|
+
|
|
38
|
+
### 1. Create or use a project directory
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mkdir my-stack && cd my-stack
|
|
42
|
+
npm init -y
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Install the package locally
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install business-stack
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This installs the full app tree at `node_modules/business-stack/` (Next app, gateway, backend sources).
|
|
52
|
+
|
|
53
|
+
### 3. Run the CLI via `npx` (or package scripts)
|
|
54
|
+
|
|
55
|
+
From the **same directory** as your `package.json` (so npm resolves the local install):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx business-stack doctor
|
|
59
|
+
npx business-stack setup --yes
|
|
60
|
+
npx business-stack start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **`setup`** generates secrets, stores them in SQLite (`node_modules/business-stack/gateway/auth.sqlite`, table `stack_secrets`), and writes `gateway/.env`, `frontend/web/.env.local`, and `backend/.env` under that package path. You do not need to create `.env` files by hand.
|
|
64
|
+
- **`start`** runs `bun install`, `uv sync`, production build, Alembic migrations, then starts Next (`next start`), the gateway, and uvicorn. First run can take several minutes.
|
|
65
|
+
|
|
66
|
+
Optional flags for `start`: `--skip-install`, `--skip-build`, `--skip-migrate`.
|
|
67
|
+
|
|
68
|
+
Development servers (hot reload) instead of production:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx business-stack dev
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. Optional: npm scripts in your `package.json`
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"scripts": {
|
|
79
|
+
"stack:doctor": "business-stack doctor",
|
|
80
|
+
"stack:setup": "business-stack setup --yes",
|
|
81
|
+
"stack:start": "business-stack start",
|
|
82
|
+
"stack:dev": "business-stack dev"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
After a local install, npm adds `node_modules/.bin/business-stack`, so these scripts work without `npx`.
|
|
88
|
+
|
|
89
|
+
## Global install (optional)
|
|
90
|
+
|
|
91
|
+
If you want the `business-stack` command on your PATH for every shell:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install -g business-stack
|
|
95
|
+
business-stack doctor
|
|
96
|
+
business-stack setup --yes
|
|
97
|
+
business-stack start
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Files and secrets are then under your global npm prefix (for example `%AppData%\npm\node_modules\business-stack` on Windows). Use global install only if you are comfortable with that layout and upgrades overwriting the same tree.
|
|
101
|
+
|
|
102
|
+
## Default URLs
|
|
103
|
+
|
|
104
|
+
| Service | URL |
|
|
105
|
+
|---------|-----|
|
|
106
|
+
| Web (Next) | `http://127.0.0.1:3000` |
|
|
107
|
+
| Gateway | `http://127.0.0.1:3001` |
|
|
108
|
+
| FastAPI | `http://127.0.0.1:8000` |
|
|
109
|
+
|
|
110
|
+
`setup` uses these by default; interactive `business-stack setup` (without `--yes`) prompts for URLs.
|
|
111
|
+
|
|
112
|
+
## Security note
|
|
113
|
+
|
|
114
|
+
Secrets in `stack_secrets` and generated `.env` files are **plaintext at rest**, like a normal `.env`. Protect the install directory and do not commit `gateway/.env`, `frontend/web/.env.local`, `backend/.env`, or `*.sqlite`.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
See `package.json` (`license` field).
|
package/bin/business-stack.cjs
CHANGED
|
@@ -111,10 +111,29 @@ function writeEnvFile(filePath, content) {
|
|
|
111
111
|
fs.writeFileSync(filePath, `${content.trim()}\n`, "utf8");
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Resolve `bun` / `uv` to a concrete path on Windows (`where`); avoids spawn ENOENT for .cmd shims. */
|
|
115
|
+
function resolveExecutable(cmd) {
|
|
116
|
+
if (process.platform !== "win32") {
|
|
117
|
+
return cmd;
|
|
118
|
+
}
|
|
119
|
+
const r = spawnSync("where.exe", [cmd], {
|
|
120
|
+
encoding: "utf8",
|
|
121
|
+
windowsHide: true,
|
|
122
|
+
});
|
|
123
|
+
if (r.status !== 0 || !r.stdout) {
|
|
124
|
+
return cmd;
|
|
125
|
+
}
|
|
126
|
+
const first = r.stdout
|
|
127
|
+
.split(/\r?\n/)
|
|
128
|
+
.map((s) => s.trim())
|
|
129
|
+
.find(Boolean);
|
|
130
|
+
return first || cmd;
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
function cmdExists(cmd) {
|
|
115
134
|
const isWin = process.platform === "win32";
|
|
116
135
|
const which = isWin ? "where" : "which";
|
|
117
|
-
const r = spawnSync(which, [cmd], { encoding: "utf8" });
|
|
136
|
+
const r = spawnSync(which, [cmd], { encoding: "utf8", windowsHide: isWin });
|
|
118
137
|
return r.status === 0;
|
|
119
138
|
}
|
|
120
139
|
|
|
@@ -274,19 +293,62 @@ function envFileLooksConfigured(stackRoot) {
|
|
|
274
293
|
return hasExistingSecrets(stackRoot);
|
|
275
294
|
}
|
|
276
295
|
|
|
277
|
-
function runCmd(cmd, args, cwd, extraEnv) {
|
|
278
|
-
|
|
296
|
+
function runCmd(label, cmd, args, cwd, extraEnv) {
|
|
297
|
+
console.error(`business-stack: ${label}…`);
|
|
298
|
+
const exe = resolveExecutable(cmd);
|
|
299
|
+
const r = spawnSync(exe, args, {
|
|
279
300
|
cwd,
|
|
280
301
|
stdio: "inherit",
|
|
281
302
|
env: { ...process.env, ...extraEnv },
|
|
282
303
|
shell: false,
|
|
304
|
+
windowsHide: process.platform === "win32",
|
|
283
305
|
});
|
|
306
|
+
if (r.error) {
|
|
307
|
+
console.error(`business-stack: ${label} failed:`, r.error.message);
|
|
308
|
+
console.error(` Command: ${exe} ${args.join(" ")}`);
|
|
309
|
+
console.error(` cwd: ${cwd}`);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
if (r.signal) {
|
|
313
|
+
console.error(`business-stack: ${label} killed (${r.signal})`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
284
316
|
if (r.status !== 0) {
|
|
285
317
|
process.exit(r.status ?? 1);
|
|
286
318
|
}
|
|
287
319
|
}
|
|
288
320
|
|
|
289
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Wait for turbo + servers. Async so Commander keeps the Node event loop alive on Windows
|
|
323
|
+
* until the child exits (avoids exiting immediately with no output).
|
|
324
|
+
*/
|
|
325
|
+
function waitForChild(child, label) {
|
|
326
|
+
return new Promise((resolve) => {
|
|
327
|
+
let settled = false;
|
|
328
|
+
const finish = (code) => {
|
|
329
|
+
if (settled) return;
|
|
330
|
+
settled = true;
|
|
331
|
+
resolve(code ?? 0);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
child.on("error", (err) => {
|
|
335
|
+
console.error(`business-stack: ${label} — failed to spawn process:`, err.message);
|
|
336
|
+
console.error(" Is Bun installed and on PATH? Run: business-stack doctor");
|
|
337
|
+
finish(1);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
child.on("exit", (code, sig) => {
|
|
341
|
+
if (sig) {
|
|
342
|
+
console.error(`business-stack: ${label} — child exited with signal ${sig}`);
|
|
343
|
+
finish(1);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
finish(code ?? 0);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function runStart(opts) {
|
|
290
352
|
const stackRoot = getStackRoot();
|
|
291
353
|
if (!envFileLooksConfigured(stackRoot)) {
|
|
292
354
|
console.error(
|
|
@@ -295,6 +357,9 @@ function runStart(opts) {
|
|
|
295
357
|
process.exit(1);
|
|
296
358
|
}
|
|
297
359
|
|
|
360
|
+
console.error("business-stack: starting (install → sync → build → migrate → servers)…");
|
|
361
|
+
console.error(`business-stack: stack root ${stackRoot}`);
|
|
362
|
+
|
|
298
363
|
const fromDb = readStackSecretsEnv(stackRoot);
|
|
299
364
|
const childEnv = { ...process.env };
|
|
300
365
|
for (const [k, v] of Object.entries(fromDb)) {
|
|
@@ -304,43 +369,53 @@ function runStart(opts) {
|
|
|
304
369
|
}
|
|
305
370
|
|
|
306
371
|
if (!opts.skipInstall) {
|
|
307
|
-
runCmd("bun", ["install"], stackRoot, childEnv);
|
|
372
|
+
runCmd("bun install (workspaces)", "bun", ["install"], stackRoot, childEnv);
|
|
308
373
|
}
|
|
309
|
-
runCmd("uv", ["sync"], path.join(stackRoot, "backend"), childEnv);
|
|
374
|
+
runCmd("uv sync (backend)", "uv", ["sync"], path.join(stackRoot, "backend"), childEnv);
|
|
310
375
|
if (!opts.skipBuild) {
|
|
311
|
-
runCmd("bun", ["run", "build"], stackRoot, childEnv);
|
|
376
|
+
runCmd("bun run build", "bun", ["run", "build"], stackRoot, childEnv);
|
|
312
377
|
}
|
|
313
378
|
if (!opts.skipMigrate) {
|
|
314
|
-
runCmd(
|
|
379
|
+
runCmd(
|
|
380
|
+
"alembic upgrade head",
|
|
381
|
+
"uv",
|
|
382
|
+
["run", "alembic", "upgrade", "head"],
|
|
383
|
+
path.join(stackRoot, "backend"),
|
|
384
|
+
childEnv,
|
|
385
|
+
);
|
|
315
386
|
}
|
|
316
387
|
|
|
317
|
-
|
|
388
|
+
console.error("business-stack: launching production servers (turbo start)…");
|
|
389
|
+
|
|
390
|
+
const bunExe = resolveExecutable("bun");
|
|
391
|
+
const child = spawn(bunExe, ["run", "start"], {
|
|
318
392
|
cwd: stackRoot,
|
|
319
393
|
stdio: "inherit",
|
|
320
394
|
env: childEnv,
|
|
321
395
|
shell: false,
|
|
396
|
+
windowsHide: process.platform === "win32",
|
|
322
397
|
});
|
|
323
398
|
|
|
324
|
-
|
|
399
|
+
const shutdown = () => {
|
|
325
400
|
try {
|
|
326
|
-
if (process.platform === "win32") {
|
|
401
|
+
if (process.platform === "win32" && child.pid) {
|
|
327
402
|
spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
|
|
328
403
|
stdio: "ignore",
|
|
404
|
+
windowsHide: true,
|
|
329
405
|
});
|
|
330
|
-
} else {
|
|
331
|
-
child.kill(
|
|
406
|
+
} else if (child.pid) {
|
|
407
|
+
child.kill("SIGINT");
|
|
332
408
|
}
|
|
333
409
|
} catch {
|
|
334
410
|
/* ignore */
|
|
335
411
|
}
|
|
336
|
-
}
|
|
412
|
+
};
|
|
337
413
|
|
|
338
|
-
process.
|
|
339
|
-
process.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
});
|
|
414
|
+
process.once("SIGINT", shutdown);
|
|
415
|
+
process.once("SIGTERM", shutdown);
|
|
416
|
+
|
|
417
|
+
const code = await waitForChild(child, "turbo start");
|
|
418
|
+
process.exit(code);
|
|
344
419
|
}
|
|
345
420
|
|
|
346
421
|
function runDev() {
|
|
@@ -352,13 +427,13 @@ function runDev() {
|
|
|
352
427
|
childEnv[k] = String(v);
|
|
353
428
|
}
|
|
354
429
|
}
|
|
355
|
-
runCmd("bun", ["run", "dev"], stackRoot, childEnv);
|
|
430
|
+
runCmd("bun run dev", "bun", ["run", "dev"], stackRoot, childEnv);
|
|
356
431
|
}
|
|
357
432
|
|
|
358
433
|
program
|
|
359
434
|
.name("business-stack")
|
|
360
435
|
.description("Run the business-stack monorepo (Next + Hono gateway + FastAPI)")
|
|
361
|
-
.version("0.1.
|
|
436
|
+
.version("0.1.3");
|
|
362
437
|
|
|
363
438
|
program
|
|
364
439
|
.command("doctor")
|
|
@@ -385,9 +460,9 @@ program
|
|
|
385
460
|
.option("--skip-install", "Skip bun install")
|
|
386
461
|
.option("--skip-build", "Skip turbo build")
|
|
387
462
|
.option("--skip-migrate", "Skip alembic upgrade")
|
|
388
|
-
.action((o) => {
|
|
463
|
+
.action(async (o) => {
|
|
389
464
|
try {
|
|
390
|
-
runStart(o);
|
|
465
|
+
await runStart(o);
|
|
391
466
|
} catch (e) {
|
|
392
467
|
console.error(e instanceof Error ? e.message : e);
|
|
393
468
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "business-stack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Next.js + Hono gateway + FastAPI monorepo",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"packageManager": "bun@1.3.1",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"business-stack": "bin/business-stack.cjs"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
+
"README.md",
|
|
18
19
|
"bin",
|
|
19
20
|
"turbo.json",
|
|
20
21
|
".python-version",
|