business-stack 0.1.3 → 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/README.md CHANGED
@@ -8,8 +8,9 @@ Install these on your machine and ensure they are on your `PATH`:
8
8
 
9
9
  | Tool | Purpose |
10
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 |
11
+ | [Node.js](https://nodejs.org/) 20+ | Runs the `business-stack` CLI |
12
+ | **npm** (comes with Node) | `business-stack start` / `dev` use **`npm install`** and **`npm run`** for workspaces and Turborepo |
13
+ | *(optional)* [Bun](https://bun.sh) | Only if you develop the **full monorepo** from git (`bun run dev` at repo root); **not** required for the published `business-stack` package |
13
14
  | [uv](https://docs.astral.sh/uv/) | Installs and runs the Python backend |
14
15
  | Python **3.12** | Required by the backend (managed via `uv`) |
15
16
 
@@ -61,7 +62,7 @@ npx business-stack start
61
62
  ```
62
63
 
63
64
  - **`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
+ - **`start`** runs **`npm install`** (workspaces), **`uv sync`**, **`npm run build`**, Alembic migrations, then **`npm run start`** (Turborepo: Next, gateway, backend). First run can take several minutes.
65
66
 
66
67
  Optional flags for `start`: `--skip-install`, `--skip-build`, `--skip-migrate`.
67
68
 
@@ -111,23 +111,92 @@ 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;
114
+ function getCliPackageVersion() {
115
+ try {
116
+ const p = path.join(__dirname, "..", "package.json");
117
+ return JSON.parse(fs.readFileSync(p, "utf8")).version;
118
+ } catch {
119
+ return "?";
118
120
  }
121
+ }
122
+
123
+ function logPhase(msg) {
124
+ const line = `business-stack: ${msg}`;
125
+ console.log(line);
126
+ console.error(line);
127
+ }
128
+
129
+ /** Microsoft Store / WindowsApps shims often exit 0 immediately with no real Bun — skip them. */
130
+ function isWindowsAppsStub(exePath) {
131
+ const n = exePath.toLowerCase().replace(/\//g, "\\");
132
+ return (
133
+ n.includes("\\windowsapps\\") ||
134
+ n.includes("microsoft\\windowsapps") ||
135
+ n.endsWith("appinstaller.exe")
136
+ );
137
+ }
138
+
139
+ function whereAll(cmd) {
119
140
  const r = spawnSync("where.exe", [cmd], {
120
141
  encoding: "utf8",
121
142
  windowsHide: true,
122
143
  });
123
144
  if (r.status !== 0 || !r.stdout) {
124
- return cmd;
145
+ return [];
125
146
  }
126
- const first = r.stdout
147
+ return r.stdout
127
148
  .split(/\r?\n/)
128
149
  .map((s) => s.trim())
129
- .find(Boolean);
130
- return first || cmd;
150
+ .filter(Boolean);
151
+ }
152
+
153
+ function executableResponds(exe, args) {
154
+ const r = spawnSync(exe, args, {
155
+ encoding: "utf8",
156
+ windowsHide: true,
157
+ stdio: ["ignore", "pipe", "pipe"],
158
+ timeout: 20_000,
159
+ });
160
+ if (r.error) {
161
+ return false;
162
+ }
163
+ if (r.status !== 0) {
164
+ return false;
165
+ }
166
+ const out = `${String(r.stdout || "")}${String(r.stderr || "")}`.trim();
167
+ return out.length >= 2;
168
+ }
169
+
170
+ /**
171
+ * Resolve `npm` / `bun` / `uv` on Windows: `where` can list a useless WindowsApps stub first.
172
+ * Pick a path that successfully runs `--version`.
173
+ */
174
+ function resolveExecutable(cmd) {
175
+ if (process.platform !== "win32") {
176
+ return cmd;
177
+ }
178
+ const all = whereAll(cmd);
179
+ if (all.length === 0) {
180
+ return cmd;
181
+ }
182
+ const preferred = all.filter((p) => !isWindowsAppsStub(p));
183
+ const order = preferred.length > 0 ? preferred : all;
184
+ for (let i = order.length - 1; i >= 0; i--) {
185
+ const exe = order[i];
186
+ if (isWindowsAppsStub(exe)) {
187
+ continue;
188
+ }
189
+ if (executableResponds(exe, ["--version"])) {
190
+ return exe;
191
+ }
192
+ }
193
+ for (let i = order.length - 1; i >= 0; i--) {
194
+ const exe = order[i];
195
+ if (executableResponds(exe, ["--version"])) {
196
+ return exe;
197
+ }
198
+ }
199
+ return order[order.length - 1] || cmd;
131
200
  }
132
201
 
133
202
  function cmdExists(cmd) {
@@ -140,8 +209,10 @@ function cmdExists(cmd) {
140
209
  function runDoctor() {
141
210
  const ok = [];
142
211
  const bad = [];
143
- if (cmdExists("bun")) ok.push("bun");
144
- else bad.push("bun (https://bun.sh)");
212
+ if (cmdExists("node")) ok.push("node");
213
+ else bad.push("node (https://nodejs.org/)");
214
+ if (cmdExists("npm")) ok.push("npm");
215
+ else bad.push("npm (ships with Node.js)");
145
216
  if (cmdExists("uv")) ok.push("uv");
146
217
  else bad.push("uv (https://docs.astral.sh/uv/)");
147
218
  if (cmdExists("python") || cmdExists("python3")) ok.push("python");
@@ -155,6 +226,9 @@ function runDoctor() {
155
226
  return;
156
227
  }
157
228
  console.log("\nAll common toolchain binaries are on PATH.");
229
+ console.log(
230
+ "\nThe gateway runs on **Node** (tsx + better-sqlite3). Bun is optional for monorepo contributors only.",
231
+ );
158
232
  }
159
233
 
160
234
  async function promptDefaults(rl, defaults) {
@@ -283,19 +357,33 @@ BACKEND_GATEWAY_SECRET=${backendGatewaySecret}
283
357
  }
284
358
 
285
359
  function envFileLooksConfigured(stackRoot) {
286
- const g = path.join(stackRoot, "gateway", ".env");
287
- if (fs.existsSync(g)) {
288
- const t = fs.readFileSync(g, "utf8");
289
- if (/BETTER_AUTH_SECRET=\S+/.test(t) || /INTEGRATIONS_ENCRYPTION_KEY=\S+/.test(t)) {
290
- return true;
360
+ try {
361
+ const g = path.join(stackRoot, "gateway", ".env");
362
+ if (fs.existsSync(g)) {
363
+ const t = fs.readFileSync(g, "utf8");
364
+ if (
365
+ /^BETTER_AUTH_SECRET=./m.test(t) ||
366
+ /^INTEGRATIONS_ENCRYPTION_KEY=./m.test(t)
367
+ ) {
368
+ return true;
369
+ }
291
370
  }
371
+ return hasExistingSecrets(stackRoot);
372
+ } catch (e) {
373
+ console.error(
374
+ "business-stack: could not read setup state:",
375
+ e instanceof Error ? e.message : e,
376
+ );
377
+ return false;
292
378
  }
293
- return hasExistingSecrets(stackRoot);
294
379
  }
295
380
 
296
381
  function runCmd(label, cmd, args, cwd, extraEnv) {
297
- console.error(`business-stack: ${label}…`);
382
+ logPhase(`${label}…`);
298
383
  const exe = resolveExecutable(cmd);
384
+ if (process.platform === "win32") {
385
+ logPhase(`using ${cmd} → ${exe}`);
386
+ }
299
387
  const r = spawnSync(exe, args, {
300
388
  cwd,
301
389
  stdio: "inherit",
@@ -333,7 +421,7 @@ function waitForChild(child, label) {
333
421
 
334
422
  child.on("error", (err) => {
335
423
  console.error(`business-stack: ${label} — failed to spawn process:`, err.message);
336
- console.error(" Is Bun installed and on PATH? Run: business-stack doctor");
424
+ console.error(" Run: business-stack doctor");
337
425
  finish(1);
338
426
  });
339
427
 
@@ -357,8 +445,9 @@ async function runStart(opts) {
357
445
  process.exit(1);
358
446
  }
359
447
 
360
- console.error("business-stack: starting (install → sync → build → migrate → servers)");
361
- console.error(`business-stack: stack root ${stackRoot}`);
448
+ logPhase("starting (install → sync → build → migrate → servers)");
449
+ logPhase(`stack root ${stackRoot}`);
450
+ logPhase(`CLI package v${getCliPackageVersion()} — if there is no output below, run: npm i -g business-stack@latest`);
362
451
 
363
452
  const fromDb = readStackSecretsEnv(stackRoot);
364
453
  const childEnv = { ...process.env };
@@ -369,11 +458,17 @@ async function runStart(opts) {
369
458
  }
370
459
 
371
460
  if (!opts.skipInstall) {
372
- runCmd("bun install (workspaces)", "bun", ["install"], stackRoot, childEnv);
461
+ runCmd(
462
+ "npm install (workspaces)",
463
+ "npm",
464
+ ["install", "--no-fund", "--no-audit"],
465
+ stackRoot,
466
+ childEnv,
467
+ );
373
468
  }
374
469
  runCmd("uv sync (backend)", "uv", ["sync"], path.join(stackRoot, "backend"), childEnv);
375
470
  if (!opts.skipBuild) {
376
- runCmd("bun run build", "bun", ["run", "build"], stackRoot, childEnv);
471
+ runCmd("npm run build", "npm", ["run", "build"], stackRoot, childEnv);
377
472
  }
378
473
  if (!opts.skipMigrate) {
379
474
  runCmd(
@@ -385,10 +480,12 @@ async function runStart(opts) {
385
480
  );
386
481
  }
387
482
 
388
- console.error("business-stack: launching production servers (turbo start)");
389
-
390
- const bunExe = resolveExecutable("bun");
391
- const child = spawn(bunExe, ["run", "start"], {
483
+ logPhase("launching production servers (turbo start via npm)");
484
+ const npmExe = resolveExecutable("npm");
485
+ if (process.platform === "win32") {
486
+ logPhase(`using npm ${npmExe}`);
487
+ }
488
+ const child = spawn(npmExe, ["run", "start"], {
392
489
  cwd: stackRoot,
393
490
  stdio: "inherit",
394
491
  env: childEnv,
@@ -427,17 +524,17 @@ function runDev() {
427
524
  childEnv[k] = String(v);
428
525
  }
429
526
  }
430
- runCmd("bun run dev", "bun", ["run", "dev"], stackRoot, childEnv);
527
+ runCmd("npm run dev", "npm", ["run", "dev"], stackRoot, childEnv);
431
528
  }
432
529
 
433
530
  program
434
531
  .name("business-stack")
435
532
  .description("Run the business-stack monorepo (Next + Hono gateway + FastAPI)")
436
- .version("0.1.3");
533
+ .version("0.1.6");
437
534
 
438
535
  program
439
536
  .command("doctor")
440
- .description("Check for bun, uv, and python on PATH")
537
+ .description("Check for node, npm, uv, and python on PATH")
441
538
  .action(runDoctor);
442
539
 
443
540
  program
@@ -457,7 +554,7 @@ program
457
554
  program
458
555
  .command("start")
459
556
  .description("Install deps, build, migrate, run production stack (turbo start)")
460
- .option("--skip-install", "Skip bun install")
557
+ .option("--skip-install", "Skip npm install")
461
558
  .option("--skip-build", "Skip turbo build")
462
559
  .option("--skip-migrate", "Skip alembic upgrade")
463
560
  .action(async (o) => {
package/gateway/README.md CHANGED
@@ -1,13 +1,23 @@
1
- To install dependencies:
1
+ Install dependencies from the **repo root** (workspaces) or in this folder:
2
+
2
3
  ```sh
3
- bun install
4
+ npm install
4
5
  ```
5
6
 
6
- To run:
7
+ Run (development with reload):
8
+
7
9
  ```sh
8
- bun run dev
10
+ npm run dev
11
+ ```
12
+
13
+ Production-style (no watch):
14
+
15
+ ```sh
16
+ npm run start
9
17
  ```
10
18
 
11
19
  The gateway listens on **port 3001** by default (`PORT`). Open `http://127.0.0.1:3001` (or set `PORT`).
12
20
 
21
+ Stack: **Hono**, **Node** via **tsx**, **better-sqlite3** for auth + integration settings, **Better Auth**.
22
+
13
23
  Better Auth needs a public **base URL** (where `/api/auth` is reached in the browser). With the Next.js app proxying auth, set `BETTER_AUTH_URL` to that origin (e.g. `http://localhost:3000`). See `.env.example`. If unset, the gateway defaults to `http://localhost:3000`.
@@ -1,24 +1,27 @@
1
1
  {
2
2
  "name": "@business-stack/gateway",
3
3
  "private": true,
4
- "packageManager": "bun@1.3.1",
5
4
  "scripts": {
6
- "dev": "bun run --hot src/index.ts",
7
- "start": "bun run src/index.ts",
8
- "migrate": "bun x @better-auth/cli@latest migrate --yes",
9
- "build": "bun -e \"require('fs').mkdirSync('dist',{recursive:true}); require('fs').writeFileSync('dist/.buildstamp','')\"",
5
+ "dev": "tsx watch src/index.ts",
6
+ "start": "tsx src/index.ts",
7
+ "migrate": "npx @better-auth/cli@latest migrate --yes",
8
+ "build": "node -e \"require('fs').mkdirSync('dist',{recursive:true}); require('fs').writeFileSync('dist/.buildstamp','')\"",
10
9
  "lint": "biome check .",
11
10
  "lint:fix": "biome check --write .",
12
11
  "typecheck": "tsc --noEmit",
13
- "test": "bun -e \"process.exit(0)\"",
14
- "clean": "bun x rimraf@6 dist"
12
+ "test": "node -e \"process.exit(0)\"",
13
+ "clean": "npx rimraf@6 dist"
15
14
  },
16
15
  "dependencies": {
16
+ "@hono/node-server": "^1.19.9",
17
17
  "better-auth": "1.5.6",
18
- "hono": "^4.12.10"
18
+ "better-sqlite3": "^11.7.0",
19
+ "hono": "^4.12.10",
20
+ "tsx": "^4.19.3"
19
21
  },
20
22
  "devDependencies": {
21
- "@types/bun": "latest",
23
+ "@types/better-sqlite3": "^7.6.12",
24
+ "@types/node": "^20",
22
25
  "typescript": "^5.8.3"
23
26
  }
24
27
  }
@@ -1,11 +1,11 @@
1
- import { Database } from "bun:sqlite";
2
1
  import { betterAuth } from "better-auth";
2
+ import Database from "better-sqlite3";
3
3
  import { loadStackSecretsIntoEnv } from "./stack-secrets";
4
4
 
5
5
  loadStackSecretsIntoEnv();
6
6
 
7
7
  const dbPath = process.env.BETTER_AUTH_DATABASE_PATH ?? "auth.sqlite";
8
- const sqlite = new Database(dbPath, { create: true });
8
+ const sqlite = new Database(dbPath);
9
9
 
10
10
  function parseTrustedOrigins(): string[] {
11
11
  const raw = process.env.BETTER_AUTH_TRUSTED_ORIGINS;
@@ -1,3 +1,4 @@
1
+ import { serve } from "@hono/node-server";
1
2
  import { Hono } from "hono";
2
3
  import { cors } from "hono/cors";
3
4
  import { proxy } from "hono/proxy";
@@ -130,12 +131,14 @@ app.get("/session", (c) => {
130
131
  });
131
132
 
132
133
  const port = Number(process.env.PORT ?? 3001);
134
+ const hostname = process.env.HOST?.trim() || "127.0.0.1";
133
135
 
134
- export default {
135
- port,
136
+ serve({
136
137
  fetch: app.fetch,
137
- };
138
+ port,
139
+ hostname,
140
+ });
138
141
 
139
142
  console.log(
140
- `Gateway listening on http://localhost:${port} (backend ${backendOrigin}, prefix ${apiGatewayPrefix})`,
143
+ `Gateway listening on http://${hostname}:${port} (backend ${backendOrigin}, prefix ${apiGatewayPrefix})`,
141
144
  );
@@ -1,15 +1,15 @@
1
- import { Database } from "bun:sqlite";
1
+ import Database from "better-sqlite3";
2
2
  import { decryptIntegrationValue, encryptIntegrationValue } from "./crypto";
3
3
  import { INTEGRATION_DB_KEYS, type IntegrationDbKey } from "./keys";
4
4
 
5
5
  const dbPath = process.env.BETTER_AUTH_DATABASE_PATH ?? "auth.sqlite";
6
6
 
7
- let _db: Database | null = null;
7
+ let _db: Database.Database | null = null;
8
8
 
9
- function getDb(): Database {
9
+ function getDb(): Database.Database {
10
10
  if (!_db) {
11
- _db = new Database(dbPath, { create: true });
12
- _db.run(`
11
+ _db = new Database(dbPath);
12
+ _db.exec(`
13
13
  CREATE TABLE IF NOT EXISTS integration_settings (
14
14
  key TEXT PRIMARY KEY NOT NULL,
15
15
  ciphertext TEXT NOT NULL,
@@ -22,8 +22,8 @@ function getDb(): Database {
22
22
 
23
23
  export function getEncryptedRow(key: IntegrationDbKey): string | null {
24
24
  const row = getDb()
25
- .query("SELECT ciphertext FROM integration_settings WHERE key = ?")
26
- .get(key) as { ciphertext: string } | null;
25
+ .prepare("SELECT ciphertext FROM integration_settings WHERE key = ?")
26
+ .get(key) as { ciphertext: string } | undefined;
27
27
  return row?.ciphertext ?? null;
28
28
  }
29
29
 
@@ -33,16 +33,17 @@ export function setEncryptedRow(
33
33
  ): void {
34
34
  const ciphertext = encryptIntegrationValue(plaintext);
35
35
  const updatedAt = new Date().toISOString();
36
- getDb().run(
37
- `INSERT INTO integration_settings (key, ciphertext, updated_at)
38
- VALUES (?, ?, ?)
39
- ON CONFLICT(key) DO UPDATE SET ciphertext = excluded.ciphertext, updated_at = excluded.updated_at`,
40
- [key, ciphertext, updatedAt],
41
- );
36
+ getDb()
37
+ .prepare(
38
+ `INSERT INTO integration_settings (key, ciphertext, updated_at)
39
+ VALUES (?, ?, ?)
40
+ ON CONFLICT(key) DO UPDATE SET ciphertext = excluded.ciphertext, updated_at = excluded.updated_at`,
41
+ )
42
+ .run(key, ciphertext, updatedAt);
42
43
  }
43
44
 
44
45
  export function deleteRow(key: IntegrationDbKey): void {
45
- getDb().run("DELETE FROM integration_settings WHERE key = ?", [key]);
46
+ getDb().prepare("DELETE FROM integration_settings WHERE key = ?").run(key);
46
47
  }
47
48
 
48
49
  export function getPlaintext(key: IntegrationDbKey): string | null {
@@ -1,4 +1,4 @@
1
- import { Database } from "bun:sqlite";
1
+ import Database from "better-sqlite3";
2
2
 
3
3
  /**
4
4
  * If an env var is unset or whitespace-only, fill it from `stack_secrets` in the auth DB.
@@ -6,20 +6,20 @@ import { Database } from "bun:sqlite";
6
6
  */
7
7
  export function loadStackSecretsIntoEnv(): void {
8
8
  const dbPath = process.env.BETTER_AUTH_DATABASE_PATH ?? "auth.sqlite";
9
- let db: Database;
9
+ let db: Database.Database;
10
10
  try {
11
- db = new Database(dbPath, { create: true });
11
+ db = new Database(dbPath);
12
12
  } catch {
13
13
  return;
14
14
  }
15
15
  try {
16
- db.run(`
16
+ db.exec(`
17
17
  CREATE TABLE IF NOT EXISTS stack_secrets (
18
18
  key TEXT PRIMARY KEY NOT NULL,
19
19
  value TEXT NOT NULL
20
20
  )
21
21
  `);
22
- const rows = db.query("SELECT key, value FROM stack_secrets").all() as {
22
+ const rows = db.prepare("SELECT key, value FROM stack_secrets").all() as {
23
23
  key: string;
24
24
  value: string;
25
25
  }[];
@@ -5,7 +5,7 @@
5
5
  "moduleResolution": "bundler",
6
6
  "target": "ES2022",
7
7
  "lib": ["ES2022"],
8
- "types": ["bun-types"],
8
+ "types": ["node"],
9
9
  "skipLibCheck": true,
10
10
  "noEmit": true
11
11
  },
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "business-stack",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Next.js + Hono gateway + FastAPI monorepo",
5
5
  "license": "UNLICENSED",
6
- "packageManager": "bun@1.3.1",
7
6
  "workspaces": ["frontend/web", "gateway", "backend"],
8
7
  "scripts": {
9
8
  "prepublishOnly": "node ../scripts/sync-npm-package.cjs",