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 +25 -0
- package/package.json +1 -1
- package/template/AGENTS.md +3 -0
- package/template/README.md +8 -0
- package/template/_env.example +2 -0
- package/template/bun.lock +2 -2
- package/template/ecosystem.config.cjs +7 -1
- package/template/ecosystem.dev.config.cjs +7 -1
- package/template/package.json +2 -2
- package/template/packages/backend/package.json +1 -1
- package/template/packages/backend/scripts/ensure-built.ts +7 -0
- package/template/packages/backend/src/index.ts +2 -2
- package/template/packages/backend/tsconfig.json +1 -1
- package/template/packages/config/src/server.ts +3 -0
- package/template/scripts/dev.ts +50 -2
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
package/template/AGENTS.md
CHANGED
|
@@ -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
|
|
package/template/README.md
CHANGED
|
@@ -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.
|
package/template/_env.example
CHANGED
|
@@ -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.
|
|
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.
|
|
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: [
|
|
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 = [
|
|
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: [
|
package/template/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"packages/*"
|
|
8
8
|
],
|
|
9
9
|
"catalog": {
|
|
10
|
-
"stitchkit": "^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:
|
|
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
|
|
63
|
+
console.log(`API listening on http://${env.BIND_HOST}:${env.API_PORT}`);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
main().catch((error: unknown) => {
|
|
@@ -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(),
|
package/template/scripts/dev.ts
CHANGED
|
@@ -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(
|
|
61
|
-
console.log(
|
|
108
|
+
console.log(`Web: ${environment.NEXT_PUBLIC_WEB_URL}/en`);
|
|
109
|
+
console.log(`API: ${environment.NEXT_PUBLIC_API_URL}`);
|
|
62
110
|
}
|