create-kuckit-app 0.1.1 → 0.2.1
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/dist/bin.js +1 -1
- package/dist/{create-project-DTm05G7D.js → create-project-CP-h4Ygi.js} +7 -5
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/templates/base/.claude/CLAUDE.md +44 -0
- package/templates/base/.claude/agents/daidalos.md +76 -0
- package/templates/base/.claude/agents/episteme.md +79 -0
- package/templates/base/.claude/agents/librarian.md +132 -0
- package/templates/base/.claude/agents/oracle.md +210 -0
- package/templates/base/.claude/commands/create-plan.md +159 -0
- package/templates/base/.claude/commands/file-beads.md +98 -0
- package/templates/base/.claude/commands/review-beads.md +161 -0
- package/templates/base/.claude/settings.json +11 -0
- package/templates/base/.claude/skills/kuckit/SKILL.md +436 -0
- package/templates/base/.claude/skills/kuckit/references/ARCHITECTURE.md +388 -0
- package/templates/base/.claude/skills/kuckit/references/CLI-COMMANDS.md +365 -0
- package/templates/base/.claude/skills/kuckit/references/MODULE-DEVELOPMENT.md +581 -0
- package/templates/base/.claude/skills/kuckit/references/PACKAGES.md +112 -0
- package/templates/base/.claude/skills/kuckit/references/PUBLISHING.md +231 -0
- package/templates/base/.env.example +13 -0
- package/templates/base/.github/workflows/ci.yml +28 -0
- package/templates/base/.husky/pre-commit +1 -0
- package/templates/base/.prettierignore +5 -0
- package/templates/base/.prettierrc +8 -0
- package/templates/base/AGENTS.md +351 -0
- package/templates/base/apps/server/.env.example +18 -0
- package/templates/base/apps/server/AGENTS.md +93 -0
- package/templates/base/apps/server/package.json +13 -2
- package/templates/base/apps/server/src/app.ts +20 -0
- package/templates/base/apps/server/src/auth.ts +10 -0
- package/templates/base/apps/server/src/config/modules.ts +22 -0
- package/templates/base/apps/server/src/container.ts +81 -0
- package/templates/base/apps/server/src/health.ts +27 -0
- package/templates/base/apps/server/src/middleware/container.ts +41 -0
- package/templates/base/apps/server/src/rpc-router-registry.ts +26 -0
- package/templates/base/apps/server/src/rpc.ts +31 -0
- package/templates/base/apps/server/src/server.ts +42 -14
- package/templates/base/apps/web/.env.example +4 -0
- package/templates/base/apps/web/AGENTS.md +127 -0
- package/templates/base/apps/web/index.html +1 -1
- package/templates/base/apps/web/package.json +15 -2
- package/templates/base/apps/web/src/components/KuckitModuleRoute.tsx +82 -0
- package/templates/base/apps/web/src/lib/kuckit-router.ts +42 -0
- package/templates/base/apps/web/src/main.tsx +26 -14
- package/templates/base/apps/web/src/modules.client.ts +4 -3
- package/templates/base/apps/web/src/providers/KuckitProvider.tsx +147 -0
- package/templates/base/apps/web/src/providers/ServicesProvider.tsx +47 -0
- package/templates/base/apps/web/src/routeTree.gen.ts +91 -0
- package/templates/base/apps/web/src/routes/$.tsx +14 -0
- package/templates/base/apps/web/src/routes/__root.tsx +31 -0
- package/templates/base/apps/web/src/routes/index.tsx +46 -0
- package/templates/base/apps/web/src/routes/login.tsx +108 -0
- package/templates/base/apps/web/src/services/auth-client.ts +12 -0
- package/templates/base/apps/web/src/services/index.ts +3 -0
- package/templates/base/apps/web/src/services/rpc.ts +29 -0
- package/templates/base/apps/web/src/services/types.ts +14 -0
- package/templates/base/apps/web/tsconfig.json +5 -1
- package/templates/base/apps/web/vite.config.ts +8 -1
- package/templates/base/docker-compose.yml +23 -0
- package/templates/base/eslint.config.js +18 -0
- package/templates/base/package.json +32 -2
- package/templates/base/packages/api/AGENTS.md +66 -0
- package/templates/base/packages/api/package.json +35 -0
- package/templates/base/packages/api/src/context.ts +48 -0
- package/templates/base/packages/api/src/index.ts +22 -0
- package/templates/base/packages/api/tsconfig.json +8 -0
- package/templates/base/packages/auth/AGENTS.md +61 -0
- package/templates/base/packages/auth/package.json +27 -0
- package/templates/base/packages/auth/src/index.ts +22 -0
- package/templates/base/packages/auth/tsconfig.json +8 -0
- package/templates/base/packages/db/AGENTS.md +74 -0
- package/templates/base/packages/db/drizzle.config.ts +19 -0
- package/templates/base/packages/db/package.json +36 -0
- package/templates/base/packages/db/src/connection.ts +40 -0
- package/templates/base/packages/db/src/index.ts +4 -0
- package/templates/base/packages/db/src/migrations/0000_init.sql +54 -0
- package/templates/base/packages/db/src/migrations/meta/_journal.json +13 -0
- package/templates/base/packages/db/src/schema/auth.ts +51 -0
- package/templates/base/packages/db/tsconfig.json +8 -0
- package/templates/base/packages/items-module/AGENTS.md +210 -0
- package/templates/base/packages/items-module/package.json +32 -0
- package/templates/base/packages/items-module/src/adapters/item.drizzle.ts +66 -0
- package/templates/base/packages/items-module/src/api/items.router.ts +47 -0
- package/templates/base/packages/items-module/src/client-module.ts +39 -0
- package/templates/base/packages/items-module/src/domain/item.entity.ts +36 -0
- package/templates/base/packages/items-module/src/index.ts +15 -0
- package/templates/base/packages/items-module/src/module.ts +53 -0
- package/templates/base/packages/items-module/src/ports/item.repository.ts +13 -0
- package/templates/base/packages/items-module/src/ui/ItemsPage.tsx +144 -0
- package/templates/base/packages/items-module/src/usecases/create-item.ts +25 -0
- package/templates/base/packages/items-module/src/usecases/delete-item.ts +18 -0
- package/templates/base/packages/items-module/src/usecases/get-item.ts +19 -0
- package/templates/base/packages/items-module/src/usecases/list-items.ts +21 -0
- package/templates/base/packages/items-module/tsconfig.json +9 -0
- package/templates/base/turbo.json +13 -1
- package/templates/base/apps/web/src/App.tsx +0 -16
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# AGENTS.md - Server App
|
|
2
|
+
|
|
3
|
+
> See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Express backend hosting oRPC API, Better-Auth authentication, and the Kuckit module system.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
| ------------------------ | -------------------------- |
|
|
13
|
+
| `server.ts` | Entry point, bootstrap |
|
|
14
|
+
| `container.ts` | DI container setup |
|
|
15
|
+
| `config/modules.ts` | Module registration |
|
|
16
|
+
| `rpc.ts` | oRPC handler setup |
|
|
17
|
+
| `rpc-router-registry.ts` | Mutable router for modules |
|
|
18
|
+
| `auth.ts` | Better-Auth configuration |
|
|
19
|
+
|
|
20
|
+
## Module Loading Sequence
|
|
21
|
+
|
|
22
|
+
The server bootstraps in this order:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
1. createKuckitContainer() - Core services (db, logger, cache, etc.)
|
|
26
|
+
2. loadKuckitModules() with onApiRegistrations callback:
|
|
27
|
+
├─► register() hooks run (DI bindings)
|
|
28
|
+
├─► registerApi() hooks run (routers collected)
|
|
29
|
+
├─► onApiRegistrations() - Wire routers to rootRpcRouter
|
|
30
|
+
└─► onBootstrap() hooks run (startup logic)
|
|
31
|
+
3. setupRPC() - Create RPCHandler AFTER modules loaded
|
|
32
|
+
4. Express listens
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Router Registry Pattern
|
|
36
|
+
|
|
37
|
+
oRPC's `RPCHandler` captures the router at construction time. Modules wire their routers into a **mutable object** before the handler is created:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// rpc-router-registry.ts
|
|
41
|
+
export const rootRpcRouter = { ...appRouter }
|
|
42
|
+
|
|
43
|
+
export const wireModuleRpcRouters = (registrations: ApiRegistration[]) => {
|
|
44
|
+
for (const reg of registrations) {
|
|
45
|
+
if (reg.type === 'rpc-router') {
|
|
46
|
+
rootRpcRouter[reg.name] = reg.router
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// server.ts
|
|
54
|
+
await loadKuckitModules({
|
|
55
|
+
container,
|
|
56
|
+
modules: getModuleSpecs(),
|
|
57
|
+
onApiRegistrations: (registrations) => {
|
|
58
|
+
wireModuleRpcRouters(registrations) // Wire BEFORE setupRPC()
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
setupRPC(app) // Now create handler with fully-wired router
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Per-Request Scoping
|
|
66
|
+
|
|
67
|
+
Each HTTP request gets a scoped container with:
|
|
68
|
+
|
|
69
|
+
- `requestId` - Unique request identifier
|
|
70
|
+
- `requestLogger` - Logger with request context
|
|
71
|
+
- `session` - Current user session (if authenticated)
|
|
72
|
+
|
|
73
|
+
Access scoped services in routers via `context.di.cradle`.
|
|
74
|
+
|
|
75
|
+
## Adding New Modules
|
|
76
|
+
|
|
77
|
+
1. Install the module package
|
|
78
|
+
2. Add to `config/modules.ts`:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { kuckitModule as myModule } from '@__APP_NAME_KEBAB__/my-module'
|
|
82
|
+
|
|
83
|
+
export const getModuleSpecs = () => [
|
|
84
|
+
{ module: itemsModule },
|
|
85
|
+
{ module: myModule }, // Add here
|
|
86
|
+
]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
3. Restart the server
|
|
90
|
+
|
|
91
|
+
## Environment Variables
|
|
92
|
+
|
|
93
|
+
See `.env.example` in this directory.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "__APP_NAME_KEBAB__
|
|
2
|
+
"name": "@__APP_NAME_KEBAB__/server",
|
|
3
|
+
"private": true,
|
|
3
4
|
"type": "module",
|
|
4
5
|
"scripts": {
|
|
5
6
|
"build": "tsdown",
|
|
@@ -10,11 +11,21 @@
|
|
|
10
11
|
"express": "^5.1.0",
|
|
11
12
|
"cors": "^2.8.5",
|
|
12
13
|
"dotenv": "^17.2.2",
|
|
13
|
-
"awilix": "^12.0.5"
|
|
14
|
+
"awilix": "^12.0.5",
|
|
15
|
+
"pg": "^8.14.1",
|
|
16
|
+
"@orpc/server": "^1.10.0",
|
|
17
|
+
"@orpc/zod": "^1.10.0",
|
|
18
|
+
"better-auth": "^1.3.28",
|
|
19
|
+
"zod": "^4.1.11",
|
|
20
|
+
"@kuckit/sdk": "^1.0.0",
|
|
21
|
+
"@__APP_NAME_KEBAB__/api": "workspace:*",
|
|
22
|
+
"@__APP_NAME_KEBAB__/auth": "workspace:*",
|
|
23
|
+
"@__APP_NAME_KEBAB__/db": "workspace:*"
|
|
14
24
|
},
|
|
15
25
|
"devDependencies": {
|
|
16
26
|
"@types/express": "^5.0.1",
|
|
17
27
|
"@types/cors": "^2.8.17",
|
|
28
|
+
"@types/pg": "^8.11.11",
|
|
18
29
|
"typescript": "^5.8.2",
|
|
19
30
|
"tsdown": "^0.15.5"
|
|
20
31
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import express, { type Express } from 'express'
|
|
2
|
+
import cors from 'cors'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create and configure Express app
|
|
6
|
+
*/
|
|
7
|
+
export const createApp = (): Express => {
|
|
8
|
+
const app = express()
|
|
9
|
+
|
|
10
|
+
app.use(
|
|
11
|
+
cors({
|
|
12
|
+
origin: process.env.CORS_ORIGIN || 'http://localhost:3001',
|
|
13
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
|
14
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
15
|
+
credentials: true,
|
|
16
|
+
})
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return app
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { auth } from '@__APP_NAME_KEBAB__/auth'
|
|
2
|
+
import { toNodeHandler } from 'better-auth/node'
|
|
3
|
+
import type { Express } from 'express'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Setup Better-Auth routes
|
|
7
|
+
*/
|
|
8
|
+
export const setupAuth = (app: Express) => {
|
|
9
|
+
app.all('/api/auth{/*path}', toNodeHandler(auth))
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModuleSpec } from '@kuckit/sdk'
|
|
2
|
+
|
|
3
|
+
// Import your modules here
|
|
4
|
+
// import { kuckitModule as itemsModule } from '@__APP_NAME_KEBAB__/items-module'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Module specifications for the server application
|
|
8
|
+
*
|
|
9
|
+
* Add modules here to configure the server's functionality.
|
|
10
|
+
* Modules are loaded in order, with dependencies resolved automatically.
|
|
11
|
+
*/
|
|
12
|
+
export const getModuleSpecs = (): ModuleSpec[] => {
|
|
13
|
+
const modules: ModuleSpec[] = [
|
|
14
|
+
// Add your modules here:
|
|
15
|
+
// {
|
|
16
|
+
// module: itemsModule,
|
|
17
|
+
// config: { ... },
|
|
18
|
+
// },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
return modules
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import {
|
|
3
|
+
createKuckitContainer,
|
|
4
|
+
loadKuckitModules,
|
|
5
|
+
type CoreConfig,
|
|
6
|
+
type CoreCradle,
|
|
7
|
+
} from '@kuckit/sdk'
|
|
8
|
+
import type { Pool } from 'pg'
|
|
9
|
+
import { ensureDatabaseUrl } from '@__APP_NAME_KEBAB__/db/connection'
|
|
10
|
+
import { getModuleSpecs } from './config/modules'
|
|
11
|
+
import { wireModuleRpcRouters } from './rpc-router-registry'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Server configuration
|
|
15
|
+
*/
|
|
16
|
+
export interface ServerConfig extends CoreConfig {
|
|
17
|
+
port: number
|
|
18
|
+
corsOrigin: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Server DI container cradle
|
|
23
|
+
*/
|
|
24
|
+
export interface ServerCradle extends CoreCradle {
|
|
25
|
+
config: ServerConfig
|
|
26
|
+
dbPool: Pool
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type AppContainer = AwilixContainer<ServerCradle>
|
|
30
|
+
export type { ServerConfig as Config, ServerCradle as Cradle }
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load configuration from environment
|
|
34
|
+
*/
|
|
35
|
+
const loadConfig = (): ServerConfig => {
|
|
36
|
+
const databaseUrl = ensureDatabaseUrl()
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
databaseUrl,
|
|
40
|
+
enableFileLogging: process.env.ENABLE_FILE_LOGGING === 'true',
|
|
41
|
+
logDir: process.env.LOG_DIR || './logs',
|
|
42
|
+
logLevel: (process.env.LOG_LEVEL || 'INFO') as 'DEBUG' | 'INFO' | 'WARN' | 'ERROR',
|
|
43
|
+
env: process.env.NODE_ENV || 'development',
|
|
44
|
+
port: parseInt(process.env.PORT || '3000', 10),
|
|
45
|
+
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3001',
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build root container with all modules
|
|
51
|
+
*/
|
|
52
|
+
export const buildRootContainer = async (): Promise<AppContainer> => {
|
|
53
|
+
const config = loadConfig()
|
|
54
|
+
|
|
55
|
+
const container = await createKuckitContainer({ config })
|
|
56
|
+
|
|
57
|
+
await loadKuckitModules({
|
|
58
|
+
container,
|
|
59
|
+
env: config.env,
|
|
60
|
+
modules: getModuleSpecs(),
|
|
61
|
+
onApiRegistrations: (registrations) => {
|
|
62
|
+
wireModuleRpcRouters(registrations)
|
|
63
|
+
console.log(`Loaded ${registrations.length} API registrations from modules`)
|
|
64
|
+
},
|
|
65
|
+
onComplete: () => {
|
|
66
|
+
console.log('All modules loaded successfully')
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return container as AppContainer
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Cleanup container resources
|
|
75
|
+
*/
|
|
76
|
+
export const disposeContainer = async (container: AppContainer): Promise<void> => {
|
|
77
|
+
const { dbPool } = container.cradle
|
|
78
|
+
if (dbPool && typeof dbPool.end === 'function') {
|
|
79
|
+
await dbPool.end()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Express } from 'express'
|
|
2
|
+
import type { AppContainer } from './container'
|
|
3
|
+
|
|
4
|
+
export const setupHealth = (app: Express, container: AppContainer) => {
|
|
5
|
+
app.get('/health', async (_req, res) => {
|
|
6
|
+
const { dbPool } = container.cradle
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const client = await dbPool.connect()
|
|
10
|
+
await client.query('SELECT 1')
|
|
11
|
+
client.release()
|
|
12
|
+
|
|
13
|
+
res.status(200).json({
|
|
14
|
+
status: 'healthy',
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
checks: { database: 'ok' },
|
|
17
|
+
})
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('[Health] Database check failed:', error)
|
|
20
|
+
res.status(503).json({
|
|
21
|
+
status: 'unhealthy',
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
checks: { database: 'failed' },
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Express, Request, Response, NextFunction } from 'express'
|
|
2
|
+
import { asValue } from 'awilix'
|
|
3
|
+
import { buildRootContainer, type AppContainer } from '../container'
|
|
4
|
+
|
|
5
|
+
let rootContainer: AppContainer
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Initialize and setup container middleware
|
|
9
|
+
* Must be awaited before using the container
|
|
10
|
+
*/
|
|
11
|
+
export const setupContainerMiddleware = async (app: Express): Promise<void> => {
|
|
12
|
+
rootContainer = await buildRootContainer()
|
|
13
|
+
|
|
14
|
+
app.use((req: Request, res: Response, next: NextFunction) => {
|
|
15
|
+
req.scope = rootContainer.createScope()
|
|
16
|
+
|
|
17
|
+
req.scope.register({
|
|
18
|
+
requestId: asValue(crypto.randomUUID()),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
res.on('finish', () => {
|
|
22
|
+
req.scope?.dispose()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
res.on('close', () => {
|
|
26
|
+
req.scope?.dispose()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
next()
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get root container (for graceful shutdown)
|
|
35
|
+
*/
|
|
36
|
+
export const getRootContainer = (): AppContainer => {
|
|
37
|
+
if (!rootContainer) {
|
|
38
|
+
throw new Error('Container not initialized')
|
|
39
|
+
}
|
|
40
|
+
return rootContainer
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ApiRegistration } from '@kuckit/sdk'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Central mutable RPC router object used by RPCHandler.
|
|
5
|
+
*
|
|
6
|
+
* Module routers are wired into this object during the SDK loader's
|
|
7
|
+
* "wire" phase, before RPCHandler is instantiated.
|
|
8
|
+
*/
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export const rootRpcRouter: Record<string, any> = {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wire module RPC routers into the central router object.
|
|
14
|
+
* Called from the SDK loader's onApiRegistrations callback.
|
|
15
|
+
*/
|
|
16
|
+
export const wireModuleRpcRouters = (registrations: ApiRegistration[]): void => {
|
|
17
|
+
for (const reg of registrations) {
|
|
18
|
+
if (reg.type !== 'rpc-router') continue
|
|
19
|
+
|
|
20
|
+
if (rootRpcRouter[reg.name]) {
|
|
21
|
+
throw new Error(`Duplicate RPC router name: "${reg.name}"`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
rootRpcRouter[reg.name] = reg.router
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RPCHandler } from '@orpc/server/node'
|
|
2
|
+
import { onError } from '@orpc/server'
|
|
3
|
+
import { createContext } from '@__APP_NAME_KEBAB__/api'
|
|
4
|
+
import type { Express } from 'express'
|
|
5
|
+
import { rootRpcRouter } from './rpc-router-registry'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Setup oRPC handler
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: This must be called AFTER setupContainerMiddleware() has completed,
|
|
11
|
+
* which ensures all module routers have been wired into rootRpcRouter.
|
|
12
|
+
*/
|
|
13
|
+
export const setupRPC = (app: Express) => {
|
|
14
|
+
const rpcHandler = new RPCHandler(rootRpcRouter, {
|
|
15
|
+
interceptors: [
|
|
16
|
+
onError((error) => {
|
|
17
|
+
console.error('[RPC Error]', error)
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
app.use(async (req, res, next) => {
|
|
23
|
+
const result = await rpcHandler.handle(req, res, {
|
|
24
|
+
prefix: '/rpc',
|
|
25
|
+
context: await createContext({ req }),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (result.matched) return
|
|
29
|
+
next()
|
|
30
|
+
})
|
|
31
|
+
}
|
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
import 'dotenv/config'
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { createApp } from './app'
|
|
3
|
+
import { setupContainerMiddleware, getRootContainer } from './middleware/container'
|
|
4
|
+
import { setupAuth } from './auth'
|
|
5
|
+
import { setupRPC } from './rpc'
|
|
6
|
+
import { setupHealth } from './health'
|
|
7
|
+
import { disposeContainer } from './container'
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Bootstrap and start server
|
|
11
|
+
*/
|
|
12
|
+
const bootstrap = async () => {
|
|
13
|
+
const app = createApp()
|
|
7
14
|
|
|
8
|
-
app
|
|
9
|
-
|
|
15
|
+
await setupContainerMiddleware(app)
|
|
16
|
+
const rootContainer = getRootContainer()
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
app
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
setupAuth(app)
|
|
19
|
+
setupRPC(app)
|
|
20
|
+
setupHealth(app, rootContainer)
|
|
21
|
+
|
|
22
|
+
const port = process.env.PORT || 3000
|
|
23
|
+
const server = app.listen(port, () => {
|
|
24
|
+
console.log(`Server is running on port ${port}`)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const shutdown = async () => {
|
|
28
|
+
console.log('Shutting down gracefully...')
|
|
29
|
+
|
|
30
|
+
server.close(async () => {
|
|
31
|
+
await disposeContainer(rootContainer)
|
|
32
|
+
console.log('Server closed')
|
|
33
|
+
process.exit(0)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
console.error('Forced shutdown')
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}, 10000)
|
|
40
|
+
}
|
|
15
41
|
|
|
16
|
-
|
|
17
|
-
|
|
42
|
+
process.on('SIGTERM', shutdown)
|
|
43
|
+
process.on('SIGINT', shutdown)
|
|
44
|
+
}
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
console.
|
|
46
|
+
bootstrap().catch((error) => {
|
|
47
|
+
console.error('Failed to start server:', error)
|
|
48
|
+
process.exit(1)
|
|
21
49
|
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# AGENTS.md - Web App
|
|
2
|
+
|
|
3
|
+
> See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
React frontend using TanStack Router, TanStack Query, and Kuckit client modules.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
| ---------------------------------- | --------------------------------- |
|
|
13
|
+
| `main.tsx` | App entry point |
|
|
14
|
+
| `modules.client.ts` | Client module registration |
|
|
15
|
+
| `providers/KuckitProvider.tsx` | Kuckit context setup |
|
|
16
|
+
| `providers/ServicesProvider.tsx` | Services context (RPC, auth) |
|
|
17
|
+
| `routes/` | TanStack Router file-based routes |
|
|
18
|
+
| `routes/$.tsx` | Catch-all for module routes |
|
|
19
|
+
| `components/KuckitModuleRoute.tsx` | Renders module-registered routes |
|
|
20
|
+
|
|
21
|
+
## Adding Routes
|
|
22
|
+
|
|
23
|
+
Create files in `src/routes/`:
|
|
24
|
+
|
|
25
|
+
- `src/routes/about.tsx` → `/about`
|
|
26
|
+
- `src/routes/users/$id.tsx` → `/users/:id`
|
|
27
|
+
- `src/routes/settings/index.tsx` → `/settings`
|
|
28
|
+
|
|
29
|
+
**File-based routes take precedence** over module-registered routes.
|
|
30
|
+
|
|
31
|
+
## Module Route Integration
|
|
32
|
+
|
|
33
|
+
Modules register routes via `defineKuckitClientModule`. These are rendered via a catch-all pattern:
|
|
34
|
+
|
|
35
|
+
1. Module registers route in `routes` array
|
|
36
|
+
2. `RouteRegistry` collects routes during `loadKuckitClientModules()`
|
|
37
|
+
3. Catch-all route (`$.tsx`) matches any unhandled path
|
|
38
|
+
4. `KuckitModuleRoute` looks up path in registry and renders the component
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// routes/$.tsx - Catches module routes
|
|
42
|
+
import { KuckitModuleRoute } from '@/components/KuckitModuleRoute'
|
|
43
|
+
|
|
44
|
+
export function Route() {
|
|
45
|
+
return <KuckitModuleRoute />
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Context Providers
|
|
50
|
+
|
|
51
|
+
The app wraps content in Kuckit providers:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<ServicesProvider>
|
|
55
|
+
{' '}
|
|
56
|
+
{/* RPC client, auth */}
|
|
57
|
+
<KuckitProvider>
|
|
58
|
+
{' '}
|
|
59
|
+
{/* Nav, slots, routes registries */}
|
|
60
|
+
<KuckitRpcProvider>
|
|
61
|
+
{' '}
|
|
62
|
+
{/* RPC context for modules */}
|
|
63
|
+
{children}
|
|
64
|
+
</KuckitRpcProvider>
|
|
65
|
+
</KuckitProvider>
|
|
66
|
+
</ServicesProvider>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Available hooks:**
|
|
70
|
+
|
|
71
|
+
- `useRpc<T>()` - Get typed RPC client for API calls
|
|
72
|
+
- `useNavItems()` - Flat navigation items list
|
|
73
|
+
- `useNavTree()` - Hierarchical navigation tree
|
|
74
|
+
- `useSlot(name)` - Get components for a slot
|
|
75
|
+
- `useHasSlot(name)` - Check if slot has content
|
|
76
|
+
|
|
77
|
+
## useRpc Hook
|
|
78
|
+
|
|
79
|
+
Module components must use `useRpc()` to access the API:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { useRpc } from '@kuckit/sdk-react'
|
|
83
|
+
import { useQuery } from '@tanstack/react-query'
|
|
84
|
+
|
|
85
|
+
interface MyRpc {
|
|
86
|
+
items: { list: (input: {}) => Promise<Item[]> }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function MyComponent() {
|
|
90
|
+
const rpc = useRpc<MyRpc>()
|
|
91
|
+
|
|
92
|
+
const { data } = useQuery({
|
|
93
|
+
queryKey: ['items'],
|
|
94
|
+
queryFn: () => rpc.items.list({}),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Important**: Never use `import.meta.env.VITE_SERVER_URL` directly in module components - it's unavailable in external packages. The `useRpc` hook provides the correctly-configured client.
|
|
100
|
+
|
|
101
|
+
## Using Module UI Components
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { ItemsPage } from '@__APP_NAME_KEBAB__/items-module/ui'
|
|
105
|
+
|
|
106
|
+
// In your route component
|
|
107
|
+
export function Route() {
|
|
108
|
+
return <ItemsPage />
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Adding Client Modules
|
|
113
|
+
|
|
114
|
+
1. Import in `modules.client.ts`:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { kuckitClientModule as myClientModule } from '@__APP_NAME_KEBAB__/my-module/client'
|
|
118
|
+
|
|
119
|
+
export const clientModules = [
|
|
120
|
+
itemsClientModule,
|
|
121
|
+
myClientModule, // Add here
|
|
122
|
+
]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Environment Variables
|
|
126
|
+
|
|
127
|
+
See `.env.example` in this directory.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "__APP_NAME_KEBAB__
|
|
2
|
+
"name": "@__APP_NAME_KEBAB__/web",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
3
5
|
"type": "module",
|
|
4
6
|
"scripts": {
|
|
5
7
|
"dev": "vite --port=3001",
|
|
@@ -7,11 +9,22 @@
|
|
|
7
9
|
"check-types": "tsc --noEmit"
|
|
8
10
|
},
|
|
9
11
|
"dependencies": {
|
|
12
|
+
"@kuckit/sdk-react": "^1.0.0",
|
|
13
|
+
"@orpc/client": "^1.10.0",
|
|
14
|
+
"@orpc/tanstack-query": "^1.10.0",
|
|
10
15
|
"@tanstack/react-query": "^5.85.5",
|
|
16
|
+
"@tanstack/react-router": "^1.114.25",
|
|
17
|
+
"better-auth": "^1.3.28",
|
|
11
18
|
"react": "^19.1.0",
|
|
12
|
-
"react-dom": "^19.1.0"
|
|
19
|
+
"react-dom": "^19.1.0",
|
|
20
|
+
"zod": "^4.1.11",
|
|
21
|
+
"@__APP_NAME_KEBAB__/auth": "workspace:*"
|
|
13
22
|
},
|
|
14
23
|
"devDependencies": {
|
|
24
|
+
"@tanstack/react-router-devtools": "^1.114.27",
|
|
25
|
+
"@tanstack/router-plugin": "^1.114.27",
|
|
26
|
+
"@tanstack/react-query-devtools": "^5.85.5",
|
|
27
|
+
"@types/node": "^22.13.13",
|
|
15
28
|
"@types/react": "~19.1.10",
|
|
16
29
|
"@types/react-dom": "^19.0.4",
|
|
17
30
|
"@vitejs/plugin-react": "^4.3.4",
|