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 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).
@@ -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
- const r = spawnSync(cmd, args, {
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
- function runStart(opts) {
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("uv", ["run", "alembic", "upgrade", "head"], path.join(stackRoot, "backend"), childEnv);
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
- const child = spawn("bun", ["run", "start"], {
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
- function shutdown(signal) {
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(signal);
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.on("SIGINT", () => shutdown("SIGINT"));
339
- process.on("SIGTERM", () => shutdown("SIGTERM"));
340
- child.on("exit", (code, sig) => {
341
- if (sig) process.exit(1);
342
- process.exit(code ?? 0);
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.0");
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.0",
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",