create-stitchkit 0.1.1 → 0.2.0
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 +23 -0
- package/README.md +11 -4
- package/dist/cli.js +99 -12
- package/examples/repository/_env.append +3 -0
- package/examples/repository/_env.example.append +4 -0
- package/examples/repository/e2e/repository.spec.ts +19 -0
- package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.ts +2 -1
- package/examples/repository/packages/backend/src/surface.ts +14 -0
- package/examples/repository/packages/config/src/features.ts +7 -0
- package/examples/repository/packages/db/schema.prisma +31 -0
- package/examples/repository/packages/frontend/src/app/[locale]/page.tsx +45 -0
- package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +151 -0
- package/examples/repository/packages/frontend/src/providers/index.tsx +18 -0
- package/examples/repository/packages/shared/src/index.ts +3 -0
- package/examples/repository/scripts/runtime-smoke.ts +91 -0
- package/package.json +10 -1
- package/template/AGENTS.md +50 -0
- package/template/README.md +18 -5
- package/template/_env +1 -3
- package/template/_env.example +1 -4
- package/template/app.config.json +9 -0
- package/template/bun.lock +4 -2
- package/template/docs/ADDING_A_FEATURE.md +101 -0
- package/template/docs/LAN_HTTPS.md +28 -0
- package/template/e2e/starter.spec.ts +17 -22
- package/template/ecosystem.config.cjs +3 -2
- package/template/ecosystem.dev.config.cjs +19 -4
- package/template/package.json +3 -1
- package/template/packages/backend/src/cli.ts +2 -1
- package/template/packages/backend/src/index.ts +19 -4
- package/template/packages/backend/src/surface-manifest.test.ts +62 -0
- package/template/packages/backend/src/surface-manifest.ts +117 -0
- package/template/packages/backend/src/surface.ts +3 -9
- package/template/packages/backend/src/transport/lan-onboarding.ts +29 -0
- package/template/packages/config/package.json +2 -1
- package/template/packages/config/src/features.ts +1 -0
- package/template/packages/config/src/identity.ts +18 -0
- package/template/packages/config/src/server.ts +13 -3
- package/template/packages/db/schema.prisma +0 -23
- package/template/packages/frontend/messages/en.json +0 -1
- package/template/packages/frontend/messages/ru.json +0 -1
- package/template/packages/frontend/package.json +1 -0
- package/template/packages/frontend/src/app/[locale]/page.tsx +8 -19
- package/template/packages/frontend/src/app/[locale]/starter-page.tsx +3 -4
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +3 -2
- package/template/packages/frontend/src/lib/seo/pages.ts +4 -5
- package/template/packages/frontend/src/providers/index.tsx +1 -4
- package/template/packages/frontend/src/theme/config.ts +2 -1
- package/template/packages/shared/src/index.ts +1 -3
- package/template/scripts/dev-lan.test.ts +58 -0
- package/template/scripts/dev-lan.ts +176 -0
- package/template/scripts/dev.ts +37 -7
- package/template/scripts/runtime-smoke.ts +13 -59
- package/template/scripts/surface-conformance.ts +105 -0
- /package/{template → examples/repository}/packages/backend/src/domain/errors.ts +0 -0
- /package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.test.ts +0 -0
- /package/{template → examples/repository}/packages/backend/src/transport/repository-service.ts +0 -0
- /package/{template → examples/repository}/packages/db/migrations/20260808000000_init/migration.sql +0 -0
- /package/{template → examples/repository}/packages/db/migrations/20260808170000_repository_visibility/migration.sql +0 -0
- /package/{template → examples/repository}/packages/frontend/src/components/repository-summary.tsx +0 -0
- /package/{template → examples/repository}/packages/frontend/src/lib/api/client.ts +0 -0
- /package/{template → examples/repository}/packages/frontend/src/lib/api/queries.ts +0 -0
- /package/{template → examples/repository}/packages/frontend/src/lib/realtime/repository.ts +0 -0
- /package/{template → examples/repository}/packages/frontend/src/providers/realtime.tsx +0 -0
- /package/{template → examples/repository}/packages/shared/src/contracts/repository.ts +0 -0
- /package/{template → examples/repository}/packages/shared/src/events/repository.ts +0 -0
- /package/{template → examples/repository}/packages/shared/src/schemas/repository.test.ts +0 -0
- /package/{template → examples/repository}/packages/shared/src/schemas/repository.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-stitchkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Create a production-shaped Stitchkit application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Max Listov <maxlistov@gmail.com>",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"template/**/*",
|
|
19
|
+
"examples/**/*",
|
|
19
20
|
"!template/**/.env",
|
|
20
21
|
"!template/**/node_modules/**",
|
|
21
22
|
"!template/**/.next/**",
|
|
@@ -26,6 +27,11 @@
|
|
|
26
27
|
"!template/**/src/generated/**",
|
|
27
28
|
"!template/**/*.log",
|
|
28
29
|
"!template/**/*.tsbuildinfo",
|
|
30
|
+
"!examples/**/node_modules/**",
|
|
31
|
+
"!examples/**/.next/**",
|
|
32
|
+
"!examples/**/dist/**",
|
|
33
|
+
"!examples/**/*.log",
|
|
34
|
+
"!examples/**/*.tsbuildinfo",
|
|
29
35
|
"README.md",
|
|
30
36
|
"CHANGELOG.md",
|
|
31
37
|
"LICENSE"
|
|
@@ -39,6 +45,9 @@
|
|
|
39
45
|
"build": "rm -rf dist && bun build src/cli.ts --outdir dist --target bun --packages external && chmod +x dist/cli.js",
|
|
40
46
|
"prepublishOnly": "bun run check && bun run test && bun run build"
|
|
41
47
|
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"zod": "^4.4.3"
|
|
50
|
+
},
|
|
42
51
|
"devDependencies": {
|
|
43
52
|
"@types/bun": "^1.3.14",
|
|
44
53
|
"typescript": "^7.0.2"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Application agent guide
|
|
2
|
+
|
|
3
|
+
This repository is a generated Stitchkit application. It is not the Stitchkit
|
|
4
|
+
framework source repository.
|
|
5
|
+
|
|
6
|
+
## Architecture
|
|
7
|
+
|
|
8
|
+
- `packages/shared` owns named Zod schemas, inferred DTO types, HTTP contracts
|
|
9
|
+
and realtime event definitions. It has no database, server or browser imports.
|
|
10
|
+
- `packages/db` owns Prisma schema, migrations and the generated client.
|
|
11
|
+
- `packages/backend` implements contracts, composes the registered surface and
|
|
12
|
+
owns application policy. Routes remain thin transport boundaries.
|
|
13
|
+
- `packages/frontend` owns Next.js pages, typed clients, query/mutation hooks and
|
|
14
|
+
cache reactions.
|
|
15
|
+
- `packages/config/src/server.ts` is the only server environment boundary.
|
|
16
|
+
`app.config.json` is the only application identity boundary.
|
|
17
|
+
|
|
18
|
+
Dependencies point inward: frontend/backend → shared; backend → db/config.
|
|
19
|
+
Shared never imports an application runtime package.
|
|
20
|
+
|
|
21
|
+
## Required patterns
|
|
22
|
+
|
|
23
|
+
- Define each runtime DTO as a named Zod schema in `packages/shared/src/schemas`.
|
|
24
|
+
- Reference schemas from a separate contract module; never inline `z.object()`
|
|
25
|
+
inside `defineContract`.
|
|
26
|
+
- Implement one service method per contract operation and register the service
|
|
27
|
+
once in `packages/backend/src/surface.ts`.
|
|
28
|
+
- Use Stitchkit's typed browser client and react-query-kit hooks; do not rebuild
|
|
29
|
+
endpoint URLs, query keys or error envelopes by hand.
|
|
30
|
+
- Use Socket.IO through the shared realtime contract and Stitchkit wrappers.
|
|
31
|
+
Authentication, authorization and room membership remain application policy.
|
|
32
|
+
- Extend runtime smoke with an explicit typed probe for operations whose handler
|
|
33
|
+
behavior matters. Generic OpenAPI/MCP discovery checks are already derived.
|
|
34
|
+
|
|
35
|
+
Do not duplicate DTOs, copy Stitchkit internals, add raw routes for operations a
|
|
36
|
+
contract can express, or create compatibility aliases. Fix framework gaps in
|
|
37
|
+
Stitchkit rather than copying its implementation into this application.
|
|
38
|
+
|
|
39
|
+
## Workflow
|
|
40
|
+
|
|
41
|
+
Follow [`docs/ADDING_A_FEATURE.md`](docs/ADDING_A_FEATURE.md) for the complete
|
|
42
|
+
vertical path. Before handing work off, run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun run check
|
|
46
|
+
bun run test
|
|
47
|
+
bun run build
|
|
48
|
+
bun run runtime:smoke
|
|
49
|
+
```
|
|
50
|
+
|
package/template/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Production-shaped application generated by `create-stitchkit`.
|
|
4
4
|
|
|
5
|
+
Application identity lives in [`app.config.json`](app.config.json). Change the
|
|
6
|
+
slug, display name, version and localized description there; package names,
|
|
7
|
+
process names, MCP/OpenAPI identity, UI copy and SEO derive from it.
|
|
8
|
+
|
|
5
9
|
## Start
|
|
6
10
|
|
|
7
11
|
Point `DATABASE_URL` in `.env` at an existing PostgreSQL database, then run:
|
|
@@ -19,11 +23,16 @@ checked-in migrations and launches:
|
|
|
19
23
|
- OpenAPI: <http://localhost:3211/openapi.json>
|
|
20
24
|
- MCP: <http://localhost:3211/mcp>
|
|
21
25
|
|
|
22
|
-
The home page is a compact Stitchkit Starter reference surface.
|
|
23
|
-
|
|
26
|
+
The home page is a compact, domain-free Stitchkit Starter reference surface.
|
|
27
|
+
Add application features as vertical schema → contract → service → client slices.
|
|
28
|
+
The exact workflow is in [`docs/ADDING_A_FEATURE.md`](docs/ADDING_A_FEATURE.md),
|
|
29
|
+
and root [`AGENTS.md`](AGENTS.md) gives coding agents the application boundaries.
|
|
30
|
+
|
|
31
|
+
When generated with `--example repository`, one configured GitHub repository
|
|
32
|
+
exercises the PostgreSQL cache → contract → typed client →
|
|
24
33
|
mutation invalidation → realtime path without adding a demo product domain.
|
|
25
34
|
Configure it with `GITHUB_REPOSITORY`; `GITHUB_TOKEN` is optional.
|
|
26
|
-
Repository visibility demonstrates the canonical Prisma enum → shared Zod schema
|
|
35
|
+
Repository visibility then demonstrates the canonical Prisma enum → shared Zod schema
|
|
27
36
|
→ HTTP/UI path without duplicating its allowed values.
|
|
28
37
|
|
|
29
38
|
The `/en/ui` catalogue is isolated under
|
|
@@ -47,6 +56,12 @@ bun run test
|
|
|
47
56
|
bun run build
|
|
48
57
|
```
|
|
49
58
|
|
|
59
|
+
## Physical-device HTTPS
|
|
60
|
+
|
|
61
|
+
`bun run dev:lan` is an explicit trusted-LAN mode powered by mkcert. It leaves
|
|
62
|
+
normal development and production unchanged. Setup and device trust steps are
|
|
63
|
+
in [`docs/LAN_HTTPS.md`](docs/LAN_HTTPS.md).
|
|
64
|
+
|
|
50
65
|
## Production
|
|
51
66
|
|
|
52
67
|
Provide a production `.env`, then:
|
|
@@ -65,5 +80,3 @@ application owns its schema and migrations; the environment owns the database
|
|
|
65
80
|
process and supplies its connection through `DATABASE_URL`.
|
|
66
81
|
|
|
67
82
|
`bun run dev` and `bun run pm2:dev` use the same direct PM2 development path.
|
|
68
|
-
Rename the neutral `stitchkit-starter` package and process names when adopting
|
|
69
|
-
the template for a product.
|
package/template/_env
CHANGED
package/template/_env.example
CHANGED
|
@@ -6,7 +6,4 @@ NEXT_PUBLIC_API_URL=http://127.0.0.1:3211
|
|
|
6
6
|
INTERNAL_API_URL=http://127.0.0.1:3211
|
|
7
7
|
NEXT_PUBLIC_WEB_URL=http://127.0.0.1:3210
|
|
8
8
|
LOG_FORMAT=pretty
|
|
9
|
-
|
|
10
|
-
GITHUB_CACHE_TTL_SECONDS=900
|
|
11
|
-
# Optional. Authenticated conditional requests have a higher GitHub rate limit.
|
|
12
|
-
# GITHUB_TOKEN=github_pat_...
|
|
9
|
+
CORS_ORIGIN=*
|
package/template/bun.lock
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"dotenv": "^17.4.2",
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
+
"@app/config": "workspace:*",
|
|
11
12
|
"@app/shared": "workspace:*",
|
|
12
13
|
"@axe-core/playwright": "^4.11.0",
|
|
13
14
|
"@biomejs/biome": "^2.5.7",
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"name": "@app/frontend",
|
|
73
74
|
"version": "0.1.0",
|
|
74
75
|
"dependencies": {
|
|
76
|
+
"@app/config": "workspace:*",
|
|
75
77
|
"@app/db": "workspace:*",
|
|
76
78
|
"@app/shared": "workspace:*",
|
|
77
79
|
"@radix-ui/react-alert-dialog": "^1.1.23",
|
|
@@ -136,7 +138,7 @@
|
|
|
136
138
|
},
|
|
137
139
|
},
|
|
138
140
|
"catalog": {
|
|
139
|
-
"stitchkit": "^0.
|
|
141
|
+
"stitchkit": "^0.45.0",
|
|
140
142
|
},
|
|
141
143
|
"packages": {
|
|
142
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=="],
|
|
@@ -1115,7 +1117,7 @@
|
|
|
1115
1117
|
|
|
1116
1118
|
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
|
1117
1119
|
|
|
1118
|
-
"stitchkit": ["stitchkit@0.
|
|
1120
|
+
"stitchkit": ["stitchkit@0.45.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-IYeet50iZK3J5/bPJApLfASxX1vb5KwliHvHLOwQsLx1QqIELmgeVBl3p5IbFd4sXKW0OIBPDZdtfN+xUsjT0A=="],
|
|
1119
1121
|
|
|
1120
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=="],
|
|
1121
1123
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Adding a vertical feature
|
|
2
|
+
|
|
3
|
+
This guide uses a small `status` resource to show the canonical path. The blank
|
|
4
|
+
scaffold starts with no application surface. In `--example repository` mode,
|
|
5
|
+
keep the repository slice and add the same files beside it.
|
|
6
|
+
|
|
7
|
+
## 1. Define the wire data
|
|
8
|
+
|
|
9
|
+
Create `packages/shared/src/schemas/status.ts`:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { z } from 'zod'
|
|
13
|
+
|
|
14
|
+
export const StatusSchema = z.object({ message: z.string().min(1) })
|
|
15
|
+
export const UpdateStatusInputSchema = StatusSchema
|
|
16
|
+
export type Status = z.infer<typeof StatusSchema>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Export it from the shared package. Do not introduce a second handwritten DTO.
|
|
20
|
+
|
|
21
|
+
## 2. Define the HTTP/tool contract separately
|
|
22
|
+
|
|
23
|
+
Create `packages/shared/src/contracts/status.ts` and import the named schemas:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineContract } from 'stitchkit'
|
|
27
|
+
import { StatusSchema, UpdateStatusInputSchema } from '../schemas/status'
|
|
28
|
+
|
|
29
|
+
export const statusContract = defineContract(
|
|
30
|
+
{ prefix: 'status', scope: 'public' },
|
|
31
|
+
{
|
|
32
|
+
read: { method: 'GET', path: '/', desc: 'Read status', output: StatusSchema },
|
|
33
|
+
update: {
|
|
34
|
+
method: 'PUT', path: '/', desc: 'Update status',
|
|
35
|
+
input: UpdateStatusInputSchema, output: StatusSchema,
|
|
36
|
+
expose: ['HTTP', 'MCP', 'AGENT', 'CLI'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The contract owns transport identity. Do not add a raw route or duplicate path.
|
|
43
|
+
|
|
44
|
+
## 3. Implement and register the service
|
|
45
|
+
|
|
46
|
+
Create `packages/backend/src/transport/status-service.ts` with `implement()`.
|
|
47
|
+
Keep persistence and business rules in a domain/service module; the contract
|
|
48
|
+
handler calls that module once. Add the returned service to the `services` array
|
|
49
|
+
in `packages/backend/src/surface.ts`. That one registration drives HTTP,
|
|
50
|
+
OpenAPI, MCP, agent tools and CLI discovery.
|
|
51
|
+
|
|
52
|
+
## 4. Add typed browser access
|
|
53
|
+
|
|
54
|
+
Export `statusContract` from `packages/shared/src/index.ts`. In
|
|
55
|
+
`packages/frontend/src/lib/api/client.ts`, create `statusApi` with the same
|
|
56
|
+
`createClient(statusContract, http)` pattern used by the application's other
|
|
57
|
+
contracts. Create the query key and react-query-kit query/mutation hooks in
|
|
58
|
+
`packages/frontend/src/lib/api/status.ts`. On mutation success, update or
|
|
59
|
+
invalidate that canonical key.
|
|
60
|
+
|
|
61
|
+
Render the hook from a feature component. Pages compose features; they do not
|
|
62
|
+
call `fetch`, construct `/api/status` or decode error bodies themselves.
|
|
63
|
+
|
|
64
|
+
## 5. Add realtime only when another client must observe the change
|
|
65
|
+
|
|
66
|
+
Declare the event in the shared realtime source and use a named Zod schema for
|
|
67
|
+
its tuple. The server emits after the domain change succeeds; the frontend cache
|
|
68
|
+
bridge reacts by updating or invalidating the status query. Keep handshake auth,
|
|
69
|
+
authorization and room membership in the application. Socket.IO delivery,
|
|
70
|
+
reconnection, retained subscriptions and validation belong to Stitchkit.
|
|
71
|
+
|
|
72
|
+
The starter advances its single `catalog.stitchkit` target only after the
|
|
73
|
+
required framework release exists. Do not copy a framework adapter into the
|
|
74
|
+
application or maintain a parallel event-map API.
|
|
75
|
+
|
|
76
|
+
## 6. Prove the surface and behavior
|
|
77
|
+
|
|
78
|
+
Generic smoke already derives expected HTTP operations and MCP tool names from
|
|
79
|
+
the registered services and compares them with live OpenAPI/MCP discovery. Add
|
|
80
|
+
an explicit probe only for handler behavior:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
defineSurfaceProbe({
|
|
84
|
+
name: 'status update lifecycle',
|
|
85
|
+
input: UpdateStatusInputSchema,
|
|
86
|
+
fixture: { message: 'Ready' },
|
|
87
|
+
output: StatusSchema,
|
|
88
|
+
run: (input) => statusClient.update(input),
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Add domain tests beside domain code, contract/schema tests in shared, and UI E2E
|
|
93
|
+
only for user-visible behavior. Finish with the commands in root `AGENTS.md`.
|
|
94
|
+
|
|
95
|
+
## Ownership boundary
|
|
96
|
+
|
|
97
|
+
Application-owned: domain policy, persistence, auth decisions, rooms, cache
|
|
98
|
+
semantics and presentation. Framework-owned: contract routing, validation,
|
|
99
|
+
normalized errors, transport lifecycle, tool discovery and Socket.IO wrappers.
|
|
100
|
+
When the framework-owned layer is missing a generic capability, fix Stitchkit
|
|
101
|
+
and upgrade this application's one catalog target after that release exists.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Trusted LAN HTTPS development
|
|
2
|
+
|
|
3
|
+
Use this optional mode to test microphone, camera, PWA and WebRTC behavior on a
|
|
4
|
+
physical device connected to the same private network:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
# Install the maintained local-CA tool first:
|
|
8
|
+
brew install mkcert
|
|
9
|
+
|
|
10
|
+
bun run dev:lan
|
|
11
|
+
# Multiple private interfaces:
|
|
12
|
+
bun run dev:lan --host 192.168.1.20
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The launcher uses mkcert, stores leaf certificates under
|
|
16
|
+
`~/.local/share/<app-slug>/lan-https`, and keeps the CA in mkcert's own CAROOT.
|
|
17
|
+
No certificate or machine address is written into the project. When the chosen
|
|
18
|
+
address changes, only the leaf certificate is renewed.
|
|
19
|
+
|
|
20
|
+
The command prints the HTTPS web/API URLs and a development-only onboarding URL.
|
|
21
|
+
Open that URL on the phone to download the **public** root certificate. On iOS,
|
|
22
|
+
install the profile and enable full trust under Certificate Trust Settings. On
|
|
23
|
+
Android, install it as a CA certificate; native development builds must opt into
|
|
24
|
+
user-installed roots. The private CA key is never served.
|
|
25
|
+
|
|
26
|
+
`bun run dev` remains plain localhost HTTP. Production never reads the LAN
|
|
27
|
+
certificate variables or mounts the onboarding routes.
|
|
28
|
+
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appIdentity } from '@app/config/identity';
|
|
1
2
|
import AxeBuilder from '@axe-core/playwright';
|
|
2
3
|
import { expect, test } from '@playwright/test';
|
|
3
4
|
|
|
@@ -6,7 +7,6 @@ test('renders the hydrated starter application and catalogue', async ({ page })
|
|
|
6
7
|
await expect(page.getByRole('heading', { level: 1 })).toContainText(
|
|
7
8
|
'Build the product, not the plumbing',
|
|
8
9
|
);
|
|
9
|
-
await expect(page.getByText('max-listov/stitchkit')).toBeVisible();
|
|
10
10
|
await page.getByRole('link', { name: /UI system/ }).click();
|
|
11
11
|
await expect(page).toHaveURL(/\/en\/ui\/components$/);
|
|
12
12
|
await expect(page.getByRole('heading', { level: 1 })).toContainText('UI components');
|
|
@@ -17,7 +17,7 @@ test('publishes complete page metadata and a reachable Open Graph card', async (
|
|
|
17
17
|
request,
|
|
18
18
|
}) => {
|
|
19
19
|
await page.goto('/en/ui/themes');
|
|
20
|
-
await expect(page).toHaveTitle(
|
|
20
|
+
await expect(page).toHaveTitle(`Theme system · ${appIdentity.name}`);
|
|
21
21
|
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
|
|
22
22
|
'href',
|
|
23
23
|
/\/en\/ui\/themes$/,
|
|
@@ -39,23 +39,6 @@ test('publishes complete page metadata and a reachable Open Graph card', async (
|
|
|
39
39
|
expect(await sitemapResponse.text()).toContain('/ru/ui/themes');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
test('spins the refresh icon in place while its action is pending', async ({ page }) => {
|
|
43
|
-
await page.route('**/api/repository/refresh', async (route) => {
|
|
44
|
-
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
45
|
-
await route.continue();
|
|
46
|
-
});
|
|
47
|
-
await page.goto('/en');
|
|
48
|
-
|
|
49
|
-
const refresh = page.getByRole('button', { name: 'Refresh repository data' });
|
|
50
|
-
await expect(refresh).toHaveCSS('height', '32px');
|
|
51
|
-
await expect(refresh).toHaveCSS('width', '32px');
|
|
52
|
-
await expect(refresh.locator('.tabler-icon-refresh')).toHaveCount(1);
|
|
53
|
-
await refresh.click();
|
|
54
|
-
await expect(refresh).toHaveAttribute('aria-busy', 'true');
|
|
55
|
-
await expect(refresh.locator('svg')).toHaveCount(1);
|
|
56
|
-
await expect(refresh.locator('.tabler-icon-refresh')).toHaveClass(/animate-spin/);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
42
|
test('switches catalogue sections and component tabs', async ({ page }) => {
|
|
60
43
|
await page.goto('/en/ui');
|
|
61
44
|
await expect(page).toHaveURL(/\/en\/ui\/components$/);
|
|
@@ -176,7 +159,11 @@ test('keeps long localized navigation labels inside the mobile drawer', async ({
|
|
|
176
159
|
expect(overflow.page).toBeLessThanOrEqual(1);
|
|
177
160
|
});
|
|
178
161
|
|
|
179
|
-
test('provides a server-first synchronized theme system', async ({
|
|
162
|
+
test('provides a server-first synchronized theme system', async ({
|
|
163
|
+
browserName,
|
|
164
|
+
context,
|
|
165
|
+
page,
|
|
166
|
+
}) => {
|
|
180
167
|
const consoleProblems: string[] = [];
|
|
181
168
|
page.on('console', (message) => {
|
|
182
169
|
if (message.type() === 'error' || message.type() === 'warning') {
|
|
@@ -210,17 +197,25 @@ test('provides a server-first synchronized theme system', async ({ context, page
|
|
|
210
197
|
const secondPage = await context.newPage();
|
|
211
198
|
await secondPage.goto('/en/ui/themes');
|
|
212
199
|
await expect(secondPage.locator('html')).toHaveClass(/dark/);
|
|
200
|
+
await expect(secondPage.getByTestId('theme-state-selected')).not.toContainText('hydrating');
|
|
201
|
+
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
213
202
|
await page.getByRole('button', { name: 'Light', exact: true }).click();
|
|
203
|
+
if (browserName === 'webkit') {
|
|
204
|
+
// Playwright WebKit shares localStorage but does not dispatch cross-page storage events.
|
|
205
|
+
await secondPage.reload();
|
|
206
|
+
}
|
|
214
207
|
await expect(secondPage.locator('html')).toHaveClass(/light/);
|
|
215
208
|
|
|
216
209
|
await page.getByRole('button', { name: 'System', exact: true }).click();
|
|
217
210
|
await expect(page.getByTestId('theme-state-selected')).toContainText('system');
|
|
218
211
|
const themeCookie = (await context.cookies()).find(
|
|
219
|
-
(cookie) => cookie.name ===
|
|
212
|
+
(cookie) => cookie.name === `${appIdentity.slug}-theme`,
|
|
220
213
|
);
|
|
221
214
|
expect(themeCookie?.value).toBe('system');
|
|
222
215
|
await page.reload();
|
|
223
|
-
|
|
216
|
+
const serverCookieState = page.getByTestId('theme-state-server-cookie');
|
|
217
|
+
await expect(serverCookieState).toHaveCount(1);
|
|
218
|
+
await expect(serverCookieState).toContainText('system');
|
|
224
219
|
expect(
|
|
225
220
|
consoleProblems.filter((message) =>
|
|
226
221
|
/hydration|useServerInsertedHTML|inline script/i.test(message),
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
2
|
const { config } = require('dotenv');
|
|
3
|
+
const identity = require('./app.config.json');
|
|
3
4
|
|
|
4
5
|
config({ path: path.join(__dirname, '.env'), quiet: true, override: true });
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
apps: [
|
|
8
9
|
{
|
|
9
|
-
name:
|
|
10
|
+
name: `${identity.slug}-backend`,
|
|
10
11
|
cwd: path.join(__dirname, 'packages/backend'),
|
|
11
12
|
script: 'dist/index.js',
|
|
12
13
|
interpreter: 'bun',
|
|
@@ -15,7 +16,7 @@ module.exports = {
|
|
|
15
16
|
env: { NODE_ENV: 'production' },
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
|
-
name:
|
|
19
|
+
name: `${identity.slug}-frontend`,
|
|
19
20
|
cwd: path.join(__dirname, 'packages/frontend'),
|
|
20
21
|
script: 'node_modules/.bin/next',
|
|
21
22
|
args: ['start', '--port', process.env.WEB_PORT, '--hostname', '0.0.0.0'],
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
2
|
const { config } = require('dotenv');
|
|
3
|
+
const identity = require('./app.config.json');
|
|
3
4
|
|
|
4
|
-
config({ path: path.join(__dirname, '.env'), quiet: true
|
|
5
|
+
config({ path: path.join(__dirname, '.env'), quiet: true });
|
|
6
|
+
|
|
7
|
+
const frontendArgs = ['dev', '--port', process.env.WEB_PORT, '--hostname', '0.0.0.0'];
|
|
8
|
+
if (process.env.DEV_HTTPS_CERT && process.env.DEV_HTTPS_KEY) {
|
|
9
|
+
frontendArgs.push(
|
|
10
|
+
'--experimental-https',
|
|
11
|
+
'--experimental-https-key',
|
|
12
|
+
process.env.DEV_HTTPS_KEY,
|
|
13
|
+
'--experimental-https-cert',
|
|
14
|
+
process.env.DEV_HTTPS_CERT,
|
|
15
|
+
);
|
|
16
|
+
if (process.env.DEV_HTTPS_CA) {
|
|
17
|
+
frontendArgs.push('--experimental-https-ca', process.env.DEV_HTTPS_CA);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
5
20
|
|
|
6
21
|
module.exports = {
|
|
7
22
|
apps: [
|
|
8
23
|
{
|
|
9
|
-
name:
|
|
24
|
+
name: `${identity.slug}-backend-dev`,
|
|
10
25
|
cwd: path.join(__dirname, 'packages/backend'),
|
|
11
26
|
script: 'src/index.ts',
|
|
12
27
|
interpreter: 'bun',
|
|
@@ -16,10 +31,10 @@ module.exports = {
|
|
|
16
31
|
env: { NODE_ENV: 'development' },
|
|
17
32
|
},
|
|
18
33
|
{
|
|
19
|
-
name:
|
|
34
|
+
name: `${identity.slug}-frontend-dev`,
|
|
20
35
|
cwd: path.join(__dirname, 'packages/frontend'),
|
|
21
36
|
script: 'node_modules/.bin/next',
|
|
22
|
-
args:
|
|
37
|
+
args: frontendArgs,
|
|
23
38
|
interpreter: 'bun',
|
|
24
39
|
autorestart: true,
|
|
25
40
|
kill_timeout: 10000,
|
package/template/package.json
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
"packages/*"
|
|
8
8
|
],
|
|
9
9
|
"catalog": {
|
|
10
|
-
"stitchkit": "^0.
|
|
10
|
+
"stitchkit": "^0.45.0"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"dev": "bun scripts/dev.ts",
|
|
14
|
+
"dev:lan": "bun scripts/dev-lan.ts",
|
|
14
15
|
"check": "bun run db:generate && bun run check:authored && bun run --filter '*' check",
|
|
15
16
|
"check:authored": "bun scripts/check-authored.ts",
|
|
16
17
|
"test": "bun run --filter '*' test",
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"dotenv": "^17.4.2"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
40
|
+
"@app/config": "workspace:*",
|
|
39
41
|
"@app/shared": "workspace:*",
|
|
40
42
|
"@axe-core/playwright": "^4.11.0",
|
|
41
43
|
"@biomejs/biome": "^2.5.7",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { appIdentity } from '@app/config/identity';
|
|
3
4
|
import { createCli } from 'stitchkit/cli';
|
|
4
5
|
import { createSurface } from './surface';
|
|
5
6
|
|
|
6
7
|
const { services, socket } = await createSurface();
|
|
7
8
|
|
|
8
9
|
try {
|
|
9
|
-
await createCli({ name:
|
|
10
|
+
await createCli({ name: appIdentity.slug, version: appIdentity.version, services });
|
|
10
11
|
} finally {
|
|
11
12
|
await socket.io.close();
|
|
12
13
|
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { env } from '@app/config';
|
|
2
|
+
import { appIdentity } from '@app/config/identity';
|
|
2
3
|
import { wrapInRequestContext } from 'stitchkit/observability';
|
|
3
4
|
import { createServer, generateOpenApiDocument, openApiRoute } from 'stitchkit/server';
|
|
4
5
|
import { createMcpHandler, createMcpHttpRoute } from 'stitchkit/tools';
|
|
5
6
|
import { prisma } from './lib/db';
|
|
6
7
|
import { createSurface } from './surface';
|
|
7
8
|
import { onError } from './transport/errors';
|
|
9
|
+
import { createLanOnboardingRoutes } from './transport/lan-onboarding';
|
|
8
10
|
|
|
9
11
|
async function main(): Promise<void> {
|
|
10
12
|
const { services, socket } = await createSurface();
|
|
11
13
|
const mcp = createMcpHandler({
|
|
12
|
-
serverInfo: { name:
|
|
14
|
+
serverInfo: { name: appIdentity.slug, version: appIdentity.version },
|
|
13
15
|
auth: () => ({ scope: 'public' }),
|
|
14
16
|
services,
|
|
15
17
|
});
|
|
16
18
|
const openApi = generateOpenApiDocument({
|
|
17
|
-
info: { title:
|
|
19
|
+
info: { title: `${appIdentity.name} API`, version: appIdentity.version },
|
|
18
20
|
groups: [{ pathPrefix: '/api', services }],
|
|
19
21
|
});
|
|
20
22
|
|
|
@@ -22,7 +24,7 @@ async function main(): Promise<void> {
|
|
|
22
24
|
groups: [{ pathPrefix: '/api', services }],
|
|
23
25
|
port: env.API_PORT,
|
|
24
26
|
hostname: '0.0.0.0',
|
|
25
|
-
cors: { origin:
|
|
27
|
+
cors: { origin: env.CORS_ORIGIN },
|
|
26
28
|
hooks: { onError },
|
|
27
29
|
logging: { format: env.LOG_FORMAT },
|
|
28
30
|
websocket: socket.websocket,
|
|
@@ -30,6 +32,7 @@ async function main(): Promise<void> {
|
|
|
30
32
|
socket.route,
|
|
31
33
|
openApiRoute('/openapi.json', openApi),
|
|
32
34
|
createMcpHttpRoute({ path: '/mcp', handler: mcp }),
|
|
35
|
+
...createLanOnboardingRoutes(),
|
|
33
36
|
{
|
|
34
37
|
method: 'GET',
|
|
35
38
|
path: '/health',
|
|
@@ -37,6 +40,16 @@ async function main(): Promise<void> {
|
|
|
37
40
|
},
|
|
38
41
|
],
|
|
39
42
|
wrapFetch: (fetch) => wrapInRequestContext(fetch),
|
|
43
|
+
bun:
|
|
44
|
+
env.DEV_HTTPS_CERT && env.DEV_HTTPS_KEY
|
|
45
|
+
? {
|
|
46
|
+
tls: {
|
|
47
|
+
cert: Bun.file(env.DEV_HTTPS_CERT),
|
|
48
|
+
key: Bun.file(env.DEV_HTTPS_KEY),
|
|
49
|
+
...(env.DEV_HTTPS_CA && { ca: Bun.file(env.DEV_HTTPS_CA) }),
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
: undefined,
|
|
40
53
|
});
|
|
41
54
|
|
|
42
55
|
async function shutdown(): Promise<void> {
|
|
@@ -48,7 +61,9 @@ async function main(): Promise<void> {
|
|
|
48
61
|
|
|
49
62
|
process.once('SIGTERM', shutdown);
|
|
50
63
|
process.once('SIGINT', shutdown);
|
|
51
|
-
console.log(
|
|
64
|
+
console.log(
|
|
65
|
+
`API listening on ${env.DEV_HTTPS_CERT ? 'https' : 'http'}://127.0.0.1:${env.API_PORT}`,
|
|
66
|
+
);
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
main().catch((error: unknown) => {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { defineContract } from 'stitchkit/contract';
|
|
3
|
+
import { generateOpenApiDocument, implement } from 'stitchkit/server';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { assertSurfaceConformance, buildSurfaceManifest } from './surface-manifest';
|
|
6
|
+
|
|
7
|
+
const service = implement(
|
|
8
|
+
defineContract(
|
|
9
|
+
{ prefix: 'notes' },
|
|
10
|
+
{
|
|
11
|
+
read: {
|
|
12
|
+
method: 'GET',
|
|
13
|
+
path: '/:id',
|
|
14
|
+
desc: 'Read a note',
|
|
15
|
+
params: z.object({ id: z.string() }),
|
|
16
|
+
output: z.object({ id: z.string() }),
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
),
|
|
20
|
+
{ read: ({ params }) => ({ id: params.id }) },
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
describe('surface conformance', () => {
|
|
24
|
+
test('derives matching HTTP and MCP discovery identities without calling handlers', () => {
|
|
25
|
+
const manifest = buildSurfaceManifest([service]);
|
|
26
|
+
const openApi = generateOpenApiDocument({
|
|
27
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
28
|
+
groups: [{ pathPrefix: '/api', services: [service] }],
|
|
29
|
+
});
|
|
30
|
+
expect(manifest).toEqual([
|
|
31
|
+
{
|
|
32
|
+
service: 'notes',
|
|
33
|
+
action: 'read',
|
|
34
|
+
http: [{ method: 'GET', path: '/api/notes/{id}' }],
|
|
35
|
+
tools: { AGENT: 'read_note', MCP: 'read_note' },
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
expect(() =>
|
|
39
|
+
assertSurfaceConformance({ manifest, openApi, mcpToolNames: ['read_note'] }),
|
|
40
|
+
).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('fails with transport-specific expected and actual diagnostics', () => {
|
|
44
|
+
const manifest = buildSurfaceManifest([service]);
|
|
45
|
+
expect(() =>
|
|
46
|
+
assertSurfaceConformance({ manifest, openApi: { paths: {} }, mcpToolNames: [] }),
|
|
47
|
+
).toThrow('HTTP/OpenAPI surface mismatch');
|
|
48
|
+
expect(() =>
|
|
49
|
+
assertSurfaceConformance({
|
|
50
|
+
manifest: manifest.map((operation) => ({ ...operation, http: [] })),
|
|
51
|
+
openApi: { paths: {} },
|
|
52
|
+
mcpToolNames: ['unexpected'],
|
|
53
|
+
}),
|
|
54
|
+
).toThrow('MCP discovery surface mismatch');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('fails first on duplicate contract identity', () => {
|
|
58
|
+
expect(() => buildSurfaceManifest([service, service])).toThrow(
|
|
59
|
+
'Duplicate operation identity notes.read',
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|