create-stitchkit 0.3.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ is declared in the template root catalog.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.3] — 2026-08-18
10
+
11
+ ### Changed
12
+
13
+ - **Generated applications bind loopback by default.** Both processes listen on
14
+ `BIND_HOST` (default `127.0.0.1`) instead of a hardcoded `0.0.0.0` in the
15
+ backend server and both PM2 configs. Exposing the app to the network is now a
16
+ single conscious opt-in (`BIND_HOST=0.0.0.0` in `.env`) rather than the state
17
+ a forgotten edit leaves behind. Reported from a production deployment of a
18
+ generated app.
19
+ - **The template targets Stitchkit `^0.52.0`** — new applications get
20
+ `implement.declare`, keyed registries and hook-derived scope maps out of the
21
+ box. Purely additive relative to 0.50.
22
+ - **`bun run dev` reports honest URLs and fails fast on occupied ports.** The
23
+ final `Web:`/`API:` lines are rendered from the validated environment instead
24
+ of hardcoded ports, and before starting fresh PM2 processes the script probes
25
+ `API_PORT`/`WEB_PORT` and names the offending variable when a foreign process
26
+ holds one. Reloads of the app's own processes are unaffected.
27
+ - **`start` without a build says what to do.** A missing `dist/index.js` now
28
+ fails with “run `bun run build` first” (also preflighted in `pm2:prod`)
29
+ instead of a bare module-resolution error.
30
+ - **README and AGENTS.md pin the Prisma entry point.** Database commands go
31
+ through the root `bun run db:*` scripts; the `prisma` CLI invoked directly has
32
+ no datasource URL by design.
33
+
9
34
  ## [0.3.2] — 2026-08-17
10
35
 
11
36
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-stitchkit",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Create a production-shaped Stitchkit application",
5
5
  "license": "MIT",
6
6
  "author": "Max Listov <maxlistov@gmail.com>",
@@ -29,6 +29,9 @@ Shared never imports an application runtime package.
29
29
  endpoint URLs, query keys or error envelopes by hand.
30
30
  - Use Socket.IO through the shared realtime contract and Stitchkit wrappers.
31
31
  Authentication, authorization and room membership remain application policy.
32
+ - Drive Prisma only through the root `bun run db:*` scripts; the `prisma` CLI
33
+ invoked directly has no datasource URL. Keep `BIND_HOST` at its loopback
34
+ default unless network exposure is an explicit requirement.
32
35
  - Extend runtime smoke with an explicit typed probe for operations whose handler
33
36
  behavior matters. Generic OpenAPI/MCP discovery checks are already derived.
34
37
 
@@ -75,4 +75,12 @@ PostgreSQL is external infrastructure in development and production. The
75
75
  application owns its schema and migrations; the environment owns the database
76
76
  process and supplies its connection through `DATABASE_URL`.
77
77
 
78
+ Both processes bind `BIND_HOST` (default `127.0.0.1`, loopback only). Set
79
+ `BIND_HOST=0.0.0.0` in `.env` to expose the app on every network interface —
80
+ that is a conscious opt-in, typically behind a reverse proxy or firewall.
81
+
82
+ Always drive Prisma through the root `bun run db:*` scripts — invoking the
83
+ `prisma` CLI directly fails because the datasource URL is wired through the
84
+ `@app/db` package environment, not a static config.
85
+
78
86
  `bun run dev` and `bun run pm2:dev` use the same direct PM2 development path.
@@ -1,5 +1,7 @@
1
1
  NODE_ENV=development
2
2
  DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter
3
+ # 0.0.0.0 exposes the app to every network interface — opt in consciously.
4
+ BIND_HOST=127.0.0.1
3
5
  API_PORT=3211
4
6
  WEB_PORT=3210
5
7
  NEXT_PUBLIC_API_URL=http://127.0.0.1:3211
package/template/bun.lock CHANGED
@@ -138,7 +138,7 @@
138
138
  },
139
139
  },
140
140
  "catalog": {
141
- "stitchkit": "^0.50.0",
141
+ "stitchkit": "^0.52.0",
142
142
  },
143
143
  "packages": {
144
144
  "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.25", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LIAO6kAG8fpXQb9L0iwPk1FIbXftvqnyC56v5NEAzeWTeL8fUsy/Hx86VPBTWEDFdwbVprjWifJOAqS6AOj3mA=="],
@@ -1117,7 +1117,7 @@
1117
1117
 
1118
1118
  "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
1119
1119
 
1120
- "stitchkit": ["stitchkit@0.50.0", "", { "dependencies": { "ky": "^2.0.2" }, "peerDependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2", "@modelcontextprotocol/server": "^2.0.0", "@socket.io/bun-engine": "^0.1.1", "@socket.io/component-emitter": "^3.1.2", "@tanstack/react-query": ">=5", "@types/bun": "^1.3.14", "ai": "^7.0.0", "react": ">=18", "react-query-kit": "^3.3.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "srvx": "^0.12.5", "zod": "^4.4.3" }, "optionalPeers": ["@modelcontextprotocol/ext-apps", "@modelcontextprotocol/server", "@socket.io/bun-engine", "@socket.io/component-emitter", "@tanstack/react-query", "@types/bun", "ai", "react", "react-query-kit", "socket.io", "socket.io-client", "srvx"] }, "sha512-lMadqgQ9aryAhULyA0biVjx9bn86HOUhcUj809TyHy2MuUZSnL9qI4fiByQbVAqlxW6i1a1RLAeguEJeu6jwWg=="],
1120
+ "stitchkit": ["stitchkit@0.52.0", "", { "dependencies": { "ky": "^2.0.2" }, "peerDependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2", "@modelcontextprotocol/server": "^2.0.0", "@socket.io/bun-engine": "^0.1.1", "@socket.io/component-emitter": "^3.1.2", "@tanstack/react-query": ">=5", "@types/bun": "^1.3.14", "ai": "^7.0.0", "react": ">=18", "react-query-kit": "^3.3.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "srvx": "^0.12.5", "zod": "^4.4.3" }, "optionalPeers": ["@modelcontextprotocol/ext-apps", "@modelcontextprotocol/server", "@socket.io/bun-engine", "@socket.io/component-emitter", "@tanstack/react-query", "@types/bun", "ai", "react", "react-query-kit", "socket.io", "socket.io-client", "srvx"] }, "sha512-sqtbZUbndsInagUr8/QoD1Hkc8nQMy1TiHu0Txo0XcRMSu4b73aGDdbyk8g4lnhEk0jOqxPXoGQZS/m7XMnnbQ=="],
1121
1121
 
1122
1122
  "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
1123
1123
 
@@ -19,7 +19,13 @@ module.exports = {
19
19
  name: `${identity.slug}-frontend`,
20
20
  cwd: path.join(__dirname, 'packages/frontend'),
21
21
  script: 'node_modules/.bin/next',
22
- args: ['start', '--port', process.env.WEB_PORT, '--hostname', '0.0.0.0'],
22
+ args: [
23
+ 'start',
24
+ '--port',
25
+ process.env.WEB_PORT,
26
+ '--hostname',
27
+ process.env.BIND_HOST ?? '127.0.0.1',
28
+ ],
23
29
  interpreter: 'bun',
24
30
  autorestart: true,
25
31
  kill_timeout: 15000,
@@ -4,7 +4,13 @@ const identity = require('./app.config.json');
4
4
 
5
5
  config({ path: path.join(__dirname, '.env'), quiet: true });
6
6
 
7
- const frontendArgs = ['dev', '--port', process.env.WEB_PORT, '--hostname', '0.0.0.0'];
7
+ const frontendArgs = [
8
+ 'dev',
9
+ '--port',
10
+ process.env.WEB_PORT,
11
+ '--hostname',
12
+ process.env.BIND_HOST ?? '127.0.0.1',
13
+ ];
8
14
 
9
15
  module.exports = {
10
16
  apps: [
@@ -7,7 +7,7 @@
7
7
  "packages/*"
8
8
  ],
9
9
  "catalog": {
10
- "stitchkit": "^0.50.0"
10
+ "stitchkit": "^0.52.0"
11
11
  },
12
12
  "scripts": {
13
13
  "dev": "bun scripts/dev.ts",
@@ -31,7 +31,7 @@
31
31
  "lint": "biome check --error-on-warnings .",
32
32
  "lint:fix": "biome check --write .",
33
33
  "pm2:dev": "bun scripts/dev.ts",
34
- "pm2:prod": "bun run db:deploy && pm2 startOrReload ecosystem.config.cjs --update-env"
34
+ "pm2:prod": "bun run db:deploy && bun packages/backend/scripts/ensure-built.ts && pm2 startOrReload ecosystem.config.cjs --update-env"
35
35
  },
36
36
  "dependencies": {
37
37
  "dotenv": "^17.4.2"
@@ -6,7 +6,7 @@
6
6
  "scripts": {
7
7
  "dev": "bun --watch src/index.ts",
8
8
  "build": "rm -rf dist && bun build src/index.ts src/cli.ts src/tools.ts --outdir dist --target bun --packages external",
9
- "start": "bun dist/index.js",
9
+ "start": "bun scripts/ensure-built.ts && bun dist/index.js",
10
10
  "check": "bun x tsc --noEmit",
11
11
  "test": "bun test --pass-with-no-tests",
12
12
  "cli": "bun src/cli.ts",
@@ -0,0 +1,7 @@
1
+ // Preflight for `start` / `pm2:prod`: a missing build otherwise surfaces as a
2
+ // bare `Module not found "dist/index.js"` with no hint at the obvious fix.
3
+ const entry = new URL('../dist/index.js', import.meta.url);
4
+ if (!(await Bun.file(entry).exists())) {
5
+ console.error('dist/index.js not found — run `bun run build` first.');
6
+ process.exit(1);
7
+ }
@@ -27,7 +27,7 @@ async function main(): Promise<void> {
27
27
  const server = createServer({
28
28
  groups: [{ pathPrefix: '/api', services }],
29
29
  port: env.API_PORT,
30
- hostname: '0.0.0.0',
30
+ hostname: env.BIND_HOST,
31
31
  cors: { origin: env.CORS_ORIGIN },
32
32
  hooks: { onError },
33
33
  logging: { format: env.LOG_FORMAT },
@@ -60,7 +60,7 @@ async function main(): Promise<void> {
60
60
  },
61
61
  });
62
62
 
63
- console.log(`API listening on http://127.0.0.1:${env.API_PORT}`);
63
+ console.log(`API listening on http://${env.BIND_HOST}:${env.API_PORT}`);
64
64
  }
65
65
 
66
66
  main().catch((error: unknown) => {
@@ -4,5 +4,5 @@
4
4
  "types": ["bun"],
5
5
  "paths": { "@/*": ["./src/*"] }
6
6
  },
7
- "include": ["src"]
7
+ "include": ["src", "scripts"]
8
8
  }
@@ -10,6 +10,9 @@ export const env = createEnv({
10
10
  server: {
11
11
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
12
12
  DATABASE_URL: z.url(),
13
+ // Loopback by default — exposing the app to the network is an explicit
14
+ // opt-in (`BIND_HOST=0.0.0.0`), never something a forgotten edit causes.
15
+ BIND_HOST: z.string().min(1).default('127.0.0.1'),
13
16
  API_PORT: z.coerce.number().int().positive(),
14
17
  WEB_PORT: z.coerce.number().int().positive(),
15
18
  NEXT_PUBLIC_API_URL: z.url(),
@@ -1,4 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
+ import { z } from 'zod';
2
3
  import { appIdentity } from '../packages/config/src/identity';
3
4
  import { ensureLocalEnvironment } from './local-env';
4
5
  import { inheritToolingEnvironment } from './tooling-env';
@@ -26,6 +27,7 @@ export async function runDevelopment(environment?: Record<string, string>): Prom
26
27
  'DATABASE_URL still contains the starter placeholder. Create a PostgreSQL database, update DATABASE_URL in .env, then rerun `bun run dev`.',
27
28
  );
28
29
  }
30
+ await assertPortsAvailable(environmentForRun);
29
31
  await run(['bun', 'run', 'db:setup'], environmentForRun);
30
32
  await run(
31
33
  ['pm2', 'startOrReload', 'ecosystem.dev.config.cjs', '--update-env'],
@@ -33,6 +35,50 @@ export async function runDevelopment(environment?: Record<string, string>): Prom
33
35
  );
34
36
  }
35
37
 
38
+ /**
39
+ * Fail fast with the offending variable when a port is held by a FOREIGN
40
+ * process. A rerun of `bun run dev` reloads this app's own PM2 processes while
41
+ * they still hold the ports, so the probe is skipped once they are registered.
42
+ */
43
+ async function assertPortsAvailable(environment: Record<string, string>): Promise<void> {
44
+ const registered = await registeredPm2Names();
45
+ const managed = [`${appIdentity.slug}-backend-dev`, `${appIdentity.slug}-frontend-dev`];
46
+ if (managed.some((name) => registered.has(name))) return;
47
+ assertPortFree(Number(environment.API_PORT), 'API_PORT');
48
+ assertPortFree(Number(environment.WEB_PORT), 'WEB_PORT');
49
+ }
50
+
51
+ async function registeredPm2Names(): Promise<Set<string>> {
52
+ const child = Bun.spawn(['pm2', 'jlist'], { cwd: root, stdout: 'pipe', stderr: 'ignore' });
53
+ const output = await new Response(child.stdout).text();
54
+ await child.exited;
55
+ const parsed = z.array(z.object({ name: z.string() })).safeParse(safeJsonParse(output));
56
+ return new Set(parsed.success ? parsed.data.map((entry) => entry.name) : []);
57
+ }
58
+
59
+ function safeJsonParse(text: string): unknown {
60
+ try {
61
+ return JSON.parse(text);
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ }
66
+
67
+ function assertPortFree(port: number, variable: string): void {
68
+ try {
69
+ const listener = Bun.listen({
70
+ hostname: '127.0.0.1',
71
+ port,
72
+ socket: { data: () => undefined },
73
+ });
74
+ listener.stop(true);
75
+ } catch {
76
+ throw new Error(
77
+ `Port ${port} (${variable}) is already in use by another process. Pick a free port in .env and update the URLs that embed it.`,
78
+ );
79
+ }
80
+ }
81
+
36
82
  function assertToolAvailable(command: string, instruction: string): void {
37
83
  if (!Bun.which(command)) throw new Error(`${command} is required. ${instruction}`);
38
84
  }
@@ -43,6 +89,7 @@ export async function developmentEnvironment(
43
89
  const { env } = await import('../packages/config/src/server');
44
90
  return {
45
91
  DATABASE_URL: env.DATABASE_URL,
92
+ BIND_HOST: env.BIND_HOST,
46
93
  API_PORT: String(env.API_PORT),
47
94
  WEB_PORT: String(env.WEB_PORT),
48
95
  CORS_ORIGIN: env.CORS_ORIGIN,
@@ -56,7 +103,8 @@ export async function developmentEnvironment(
56
103
  if (import.meta.main) {
57
104
  await runDevelopment();
58
105
 
106
+ const environment = await developmentEnvironment();
59
107
  console.log(`${appIdentity.name} development processes are running`);
60
- console.log('Web: http://localhost:3210/en');
61
- console.log('API: http://localhost:3211');
108
+ console.log(`Web: ${environment.NEXT_PUBLIC_WEB_URL}/en`);
109
+ console.log(`API: ${environment.NEXT_PUBLIC_API_URL}`);
62
110
  }