@pikku/skills 0.12.2 → 0.12.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/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +56 -29
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +80 -34
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +82 -10
- package/skills/pikku-concepts/references/concept-mapping.md +2 -2
- package/skills/pikku-config/SKILL.md +134 -52
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +30 -5
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +12 -7
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-knowledge/SKILL.md +3 -3
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +285 -50
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-software-archaeology/README.md +16 -6
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +35 -1
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- package/skills/pikku-ws/SKILL.md +44 -8
|
@@ -36,22 +36,55 @@ yarn add @pikku/schedule
|
|
|
36
36
|
```typescript
|
|
37
37
|
import { InMemorySchedulerService } from '@pikku/schedule'
|
|
38
38
|
|
|
39
|
-
const
|
|
39
|
+
const schedulerService = new InMemorySchedulerService()
|
|
40
|
+
await schedulerService.start() // registers a CronJob per wired scheduled task
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
It implements core's `SchedulerService` on two mechanisms: `cron` for the
|
|
44
|
+
recurring tasks you declared with `wireScheduler` (see `pikku-cron`), and
|
|
45
|
+
`setTimeout` for one-off delayed RPCs. Both live in process memory, so nothing
|
|
46
|
+
survives a restart and nothing is shared between instances — fine for
|
|
47
|
+
development and a single-instance deployment, wrong for anything else.
|
|
48
|
+
|
|
49
|
+
`PikkuTaskScheduler` is a deprecated alias for the same class.
|
|
50
|
+
|
|
51
|
+
### Scheduling a one-off RPC
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const taskId = await schedulerService.scheduleRPC('5m', 'sendReminder', data, session)
|
|
55
|
+
await schedulerService.getTask(taskId) // { rpcName, scheduledFor, status, … } | null
|
|
56
|
+
await schedulerService.getAllTasks() // pending one-offs only
|
|
57
|
+
await schedulerService.unschedule(taskId) // true when it was still pending
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The delay is milliseconds or a duration string (`'30s'`, `'5m'`, `'2h'`). This is
|
|
61
|
+
also the mechanism a workflow's delayed steps use, which is why a workflow that
|
|
62
|
+
sleeps needs a `schedulerService` registered.
|
|
43
63
|
|
|
44
64
|
## Usage Patterns
|
|
45
65
|
|
|
46
66
|
### Basic Setup
|
|
47
67
|
|
|
68
|
+
The scheduler is a singleton service under the name **`schedulerService`**, and
|
|
69
|
+
it is started in your server bootstrap — declaring it without calling `start()`
|
|
70
|
+
registers no cron jobs, so nothing ever fires:
|
|
71
|
+
|
|
48
72
|
```typescript
|
|
73
|
+
// start.ts
|
|
49
74
|
import { InMemorySchedulerService } from '@pikku/schedule'
|
|
50
75
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
const schedulerService = new InMemorySchedulerService()
|
|
77
|
+
const singletonServices = await createSingletonServices(config, {
|
|
78
|
+
schedulerService,
|
|
54
79
|
})
|
|
80
|
+
|
|
81
|
+
await appServer.start()
|
|
82
|
+
await schedulerService.start()
|
|
55
83
|
```
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
Call `close()` on shutdown — it stops every cron job and clears pending timers.
|
|
86
|
+
|
|
87
|
+
For distributed or persistent scheduling, take the scheduler service off the
|
|
88
|
+
queue factory instead (`bullFactory.getSchedulerService()`,
|
|
89
|
+
`pgBossFactory.getSchedulerService()`) and register it under the same name. See
|
|
90
|
+
`pikku-queue`.
|
|
@@ -40,10 +40,32 @@ const schema = new AjvSchemaService(logger: Logger)
|
|
|
40
40
|
|
|
41
41
|
**Methods:**
|
|
42
42
|
|
|
43
|
-
- `compileSchema(
|
|
43
|
+
- `compileSchema(name: string, schema: any): void` — Compile and register a JSON schema under `name`
|
|
44
44
|
- `validateSchema(schemaName: string, json: any): void` — Validate data against a compiled schema (throws on failure)
|
|
45
45
|
- `getSchemaNames(): Set<string>` — Get all registered schema names
|
|
46
|
-
- `getSchemaKeys(schemaName: string): string[]` —
|
|
46
|
+
- `getSchemaKeys(schemaName: string): string[]` — Top-level property keys, or `[]` if the schema has no `properties`
|
|
47
|
+
|
|
48
|
+
The first argument is the **name**, the second the schema — the parameter is
|
|
49
|
+
called `schema` in the source, which reads backwards.
|
|
50
|
+
|
|
51
|
+
### Behaviour that matters
|
|
52
|
+
|
|
53
|
+
- **Registration is name-keyed and never re-compiles.** A second
|
|
54
|
+
`compileSchema('X', …)` with a different schema is a no-op; the first one wins
|
|
55
|
+
for the process lifetime. `@pikku/schema-cfworker` *does* recompile on a
|
|
56
|
+
changed value, so a dev hot-reload after codegen picks up a changed schema
|
|
57
|
+
there but not here — restart the process instead.
|
|
58
|
+
- **AJV is a module-level singleton**, shared by every `AjvSchemaService` you
|
|
59
|
+
construct, so compiled schema names are global to the process.
|
|
60
|
+
- **`useDefaults: true` mutates the validated object**, filling in schema
|
|
61
|
+
defaults in place. `coerceTypes: false`, so a query-string `"1"` will not
|
|
62
|
+
become `1` — the wiring layer is what coerces, not this service.
|
|
63
|
+
- `ajv-formats` is registered, so `format` keywords (`email`, `uuid`, `date-time`)
|
|
64
|
+
are enforced.
|
|
65
|
+
- A failed validation throws `UnprocessableContentError` (a 422). A *missing*
|
|
66
|
+
schema throws a bare string, `Missing validator for <name>` — not an `Error`,
|
|
67
|
+
so `catch (e) { e.message }` reads `undefined`. That normally means codegen
|
|
68
|
+
didn't run.
|
|
47
69
|
|
|
48
70
|
## Usage Patterns
|
|
49
71
|
|
|
@@ -41,10 +41,30 @@ const schema = new CFWorkerSchemaService(logger: Logger)
|
|
|
41
41
|
|
|
42
42
|
**Methods:**
|
|
43
43
|
|
|
44
|
-
- `compileSchema(
|
|
44
|
+
- `compileSchema(name: string, schema: any): void` — Compile and register a JSON schema under `name`
|
|
45
45
|
- `validateSchema(schemaName: string, json: any): void` — Validate data against a compiled schema (throws on failure)
|
|
46
46
|
- `getSchemaNames(): Set<string>` — Get all registered schema names
|
|
47
|
-
- `getSchemaKeys(schemaName: string): string[]` —
|
|
47
|
+
- `getSchemaKeys(schemaName: string): string[]` — Top-level property keys, or `[]` if the schema has no `properties`
|
|
48
|
+
|
|
49
|
+
### Where it differs from AJV
|
|
50
|
+
|
|
51
|
+
These two are not drop-in equivalents, and the differences are the kind that
|
|
52
|
+
surface as behaviour changes rather than compile errors:
|
|
53
|
+
|
|
54
|
+
- **No `useDefaults`.** AJV fills schema defaults into the validated object in
|
|
55
|
+
place; this validator does not. A field you relied on being defaulted arrives
|
|
56
|
+
`undefined` on Workers.
|
|
57
|
+
- **It re-compiles when the schema value changes.** AJV caches by name forever;
|
|
58
|
+
here a `compileSchema` with a different value for the same name replaces the
|
|
59
|
+
validator, which is what lets a dev hot-reload pick up regenerated schemas.
|
|
60
|
+
- **Each validator gets a deep clone of the schema** (`@cfworker/json-schema`
|
|
61
|
+
mutates what it is given, which throws on a frozen generated object).
|
|
62
|
+
- A compile failure throws `Error('Failed to compile schema: <name>')` with the
|
|
63
|
+
underlying cause swallowed — check the schema by hand when you see it.
|
|
64
|
+
|
|
65
|
+
A failed validation throws `UnprocessableContentError` (422) with the validator
|
|
66
|
+
errors joined; a *missing* schema throws a bare string, `Missing validator for
|
|
67
|
+
<name>`, not an `Error`.
|
|
48
68
|
|
|
49
69
|
## Usage Patterns
|
|
50
70
|
|
|
@@ -23,6 +23,10 @@ For **permissions** (pikkuPermission, pikkuAuth, per-function authorization) see
|
|
|
23
23
|
|
|
24
24
|
## Session Management
|
|
25
25
|
|
|
26
|
+
`session`, `setSession` and `clearSession` live on the **wire** — the function's
|
|
27
|
+
third argument — not on services. `setSession`/`clearSession` may be async
|
|
28
|
+
(cookie and session-store backends write on the way out), so await them.
|
|
29
|
+
|
|
26
30
|
```typescript
|
|
27
31
|
// Read session in pikkuFunc (session guaranteed to exist)
|
|
28
32
|
const getProfile = pikkuFunc({
|
|
@@ -36,7 +40,7 @@ const login = pikkuFunc({
|
|
|
36
40
|
auth: false,
|
|
37
41
|
func: async ({ jwt, db }, { email, password }, { setSession }) => {
|
|
38
42
|
const user = await db.verifyCredentials(email, password)
|
|
39
|
-
setSession({ userId: user.id })
|
|
43
|
+
await setSession({ userId: user.id })
|
|
40
44
|
return { token: jwt.sign({ userId: user.id }) }
|
|
41
45
|
},
|
|
42
46
|
})
|
|
@@ -44,28 +48,31 @@ const login = pikkuFunc({
|
|
|
44
48
|
// Clear session (logout)
|
|
45
49
|
const logout = pikkuFunc({
|
|
46
50
|
func: async ({}, _data, { clearSession }) => {
|
|
47
|
-
clearSession()
|
|
51
|
+
await clearSession()
|
|
48
52
|
},
|
|
49
53
|
})
|
|
50
54
|
```
|
|
51
55
|
|
|
56
|
+
`login` is `auth: false` because the caller has no session yet — a `pikkuFunc`
|
|
57
|
+
with the default `auth` would be rejected before its body ever ran.
|
|
58
|
+
|
|
52
59
|
## Built-in Auth Strategies
|
|
53
60
|
|
|
54
61
|
Apply these via `addHTTPMiddleware` in a wirings file:
|
|
55
62
|
|
|
56
63
|
```typescript
|
|
57
64
|
import { authBearer, authCookie, authAPIKey } from '@pikku/core/middleware'
|
|
58
|
-
import { addHTTPMiddleware } from '
|
|
65
|
+
import { addHTTPMiddleware } from '#pikku'
|
|
59
66
|
|
|
60
67
|
// JWT bearer token — reads Authorization header
|
|
61
68
|
addHTTPMiddleware('*', [authBearer()])
|
|
62
69
|
|
|
63
|
-
// Cookie-based sessions —
|
|
70
|
+
// Cookie-based sessions — re-issues the cookie when the session changes
|
|
64
71
|
addHTTPMiddleware('*', [
|
|
65
72
|
authCookie({
|
|
66
73
|
name: 'session',
|
|
67
74
|
expiresIn: { value: 30, unit: 'day' },
|
|
68
|
-
options: {
|
|
75
|
+
options: { sameSite: 'strict' },
|
|
69
76
|
}),
|
|
70
77
|
])
|
|
71
78
|
|
|
@@ -73,6 +80,40 @@ addHTTPMiddleware('*', [
|
|
|
73
80
|
addHTTPMiddleware('*', [authAPIKey({ source: 'all' })])
|
|
74
81
|
```
|
|
75
82
|
|
|
83
|
+
All three share the same escape hatch: they do nothing when there is no HTTP
|
|
84
|
+
request, or when a session is already set. That is what lets you stack several —
|
|
85
|
+
whichever runs first and finds a credential wins, and the rest step aside — and
|
|
86
|
+
it is also why none of them authenticate a queue job, a scheduled task or a
|
|
87
|
+
channel message. Those need a session set another way.
|
|
88
|
+
|
|
89
|
+
Each decodes its credential with the `jwt` service; without one registered, they
|
|
90
|
+
silently authenticate nobody.
|
|
91
|
+
|
|
92
|
+
**`authBearer` in static-token mode.** Passing `token` switches it from decoding
|
|
93
|
+
a JWT to comparing (in constant time) against a fixed value — the shape to use
|
|
94
|
+
for a service-to-service caller or a demo:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
authBearer({
|
|
98
|
+
token: {
|
|
99
|
+
secretId: 'AGENT_DEMO_TOKEN', // or: value: 'literal-token'
|
|
100
|
+
userSession: { userId: 'demo-user' },
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
An unset secret leaves the middleware inert rather than erroring, so a template
|
|
106
|
+
that ships this is safe until someone provides the secret. A malformed
|
|
107
|
+
`Authorization` header (no `Bearer ` scheme) throws `InvalidSessionError` in
|
|
108
|
+
either mode.
|
|
109
|
+
|
|
110
|
+
**`authCookie` options.** `name`, `expiresIn` and `options` are all part of the
|
|
111
|
+
config; `options` merges over the defaults `{ httpOnly: true, secure: true,
|
|
112
|
+
sameSite: 'lax', path: '/' }`, so only override what you need. The cookie is
|
|
113
|
+
re-issued after the request only when the session actually changed, which is how
|
|
114
|
+
a rolling session extends itself without writing a `Set-Cookie` on every
|
|
115
|
+
response.
|
|
116
|
+
|
|
76
117
|
## Complete Example
|
|
77
118
|
|
|
78
119
|
```typescript
|
|
@@ -84,10 +125,14 @@ export const isVerified = pikkuAuth(async (_services, session) => !!session?.ema
|
|
|
84
125
|
|
|
85
126
|
// wirings/auth.wiring.ts
|
|
86
127
|
import { authCookie } from '@pikku/core/middleware'
|
|
87
|
-
import { addHTTPMiddleware } from '
|
|
128
|
+
import { addHTTPMiddleware } from '#pikku'
|
|
88
129
|
|
|
89
130
|
addHTTPMiddleware('*', [
|
|
90
|
-
authCookie({
|
|
131
|
+
authCookie({
|
|
132
|
+
name: 'session',
|
|
133
|
+
expiresIn: { value: 30, unit: 'day' },
|
|
134
|
+
options: {},
|
|
135
|
+
}),
|
|
91
136
|
])
|
|
92
137
|
|
|
93
138
|
// functions/auth.functions.ts
|
|
@@ -95,14 +140,14 @@ export const login = pikkuFunc({
|
|
|
95
140
|
auth: false,
|
|
96
141
|
func: async ({ jwt, db }, { email, password }, { setSession }) => {
|
|
97
142
|
const user = await db.verifyCredentials(email, password)
|
|
98
|
-
setSession({ userId: user.id })
|
|
143
|
+
await setSession({ userId: user.id })
|
|
99
144
|
return { token: jwt.sign({ userId: user.id }) }
|
|
100
145
|
},
|
|
101
146
|
})
|
|
102
147
|
|
|
103
148
|
export const logout = pikkuFunc({
|
|
104
149
|
func: async ({}, _data, { clearSession }) => {
|
|
105
|
-
clearSession()
|
|
150
|
+
await clearSession()
|
|
106
151
|
},
|
|
107
152
|
})
|
|
108
153
|
```
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
name: pikku-services
|
|
3
3
|
description: >-
|
|
4
4
|
Use when setting up dependency injection, creating custom services, or configuring the service
|
|
5
|
-
layer in a Pikku app. Covers pikkuServices (singleton), pikkuWireServices (per-request),
|
|
6
|
-
typing, built-in services, and
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
layer in a Pikku app. Covers pikkuServices (singleton), pikkuWireServices (per-request),
|
|
6
|
+
pikkuServerLifecycle (startup/shutdown hooks), service typing, built-in services, and
|
|
7
|
+
tree-shaking. TRIGGER when: code uses pikkuServices/pikkuWireServices/pikkuServerLifecycle, user
|
|
8
|
+
asks about services.ts, lifecycle.ts, dependency injection, service factories, startup or
|
|
9
|
+
shutdown work, or built-in services (ConsoleLogger, JoseJWTService). DO NOT TRIGGER when: user asks
|
|
10
|
+
about middleware (use pikku-middleware), auth strategies or sessions (use pikku-security),
|
|
11
|
+
permissions (use pikku-permissions), or secrets/variables (use pikku-config).
|
|
10
12
|
installGroups: [core]
|
|
11
13
|
---
|
|
12
14
|
|
|
@@ -74,6 +76,39 @@ export const createWireServices = pikkuWireServices(
|
|
|
74
76
|
)
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
### `pikkuServerLifecycle(hooks)` — startup and shutdown work
|
|
80
|
+
|
|
81
|
+
A service factory should **construct** services, not run startup side effects. Seeding a database, warming a cache, starting a background consumer or draining a queue belongs in lifecycle hooks, which receive the singleton services after they are built:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// src/lifecycle.ts
|
|
85
|
+
import { pikkuServerLifecycle } from '@pikku/core'
|
|
86
|
+
import type { SingletonServices } from '../types/application-types.js'
|
|
87
|
+
|
|
88
|
+
export const lifecycle = pikkuServerLifecycle<SingletonServices>({
|
|
89
|
+
beforeStart: async ({ kysely }) => {
|
|
90
|
+
await runMigrations(kysely) // before the port opens
|
|
91
|
+
},
|
|
92
|
+
afterStart: async (services) => {
|
|
93
|
+
await seedDevData(services) // server is accepting traffic
|
|
94
|
+
},
|
|
95
|
+
beforeStop: async ({ queueService }) => {
|
|
96
|
+
await queueService.drain() // services are still alive here
|
|
97
|
+
},
|
|
98
|
+
afterStop: async () => {
|
|
99
|
+
await releaseExternalLock() // services are ALREADY stopped
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Every hook is optional. Order is `beforeStart` → server starts → `afterStart`, then on SIGINT `beforeStop` → services stopped → server stopped → `afterStop`.
|
|
105
|
+
|
|
106
|
+
**`afterStop` runs after the singleton services have been stopped.** It still receives the services object, but the services inside it are shut down — using one there is a use-after-close bug. Anything that needs a live service goes in `beforeStop`.
|
|
107
|
+
|
|
108
|
+
Export **exactly one** `pikkuServerLifecycle` from anywhere in `srcDirectories`; the inspector finds it by the wrapper call, so the filename is free (`src/lifecycle.ts` by convention). It must be an exported `const` initialized with a direct call to `pikkuServerLifecycle` — a re-export or a conditional wrapper is invisible to the inspector.
|
|
109
|
+
|
|
110
|
+
**Only `pikku dev` and `pikku serve` run these hooks.** If you bootstrap your own server (Express, Fastify, uWS, Lambda, Cloudflare, Next.js), no runtime adapter invokes them — put the work in your entrypoint instead.
|
|
111
|
+
|
|
77
112
|
### Auto-Generated Service Manifest
|
|
78
113
|
|
|
79
114
|
After `npx pikku all`, Pikku generates `.pikku/pikku-services.gen.ts`, a manifest of which services are actually used by wired functions:
|
|
@@ -97,7 +132,7 @@ export type RequiredSingletonServices = Pick<
|
|
|
97
132
|
|
|
98
133
|
### Using Services in Functions
|
|
99
134
|
|
|
100
|
-
**Every service must be declared in `SingletonServices` (or `Services`) in `application-types.d.ts`.** Never access a service via a body-level cast (`services as typeof services & { myService: MyService }`) — that means the type is missing. Add the import and the field to `SingletonServices`, then destructure inline in the function signature. The inspector emits `SERVICES_NOT_DESTRUCTURED` and tree-shaking breaks when the first param is a plain identifier rather than an object pattern. Never `new` a service inside a function — services arrive only via injection.
|
|
135
|
+
**Every service must be declared in `SingletonServices` (or `Services`) in `application-types.d.ts`.** Never access a service via a body-level cast (`services as typeof services & { myService: MyService }`) — that means the type is missing. Add the import and the field to `SingletonServices`, then destructure inline in the function signature. The inspector emits `SERVICES_NOT_DESTRUCTURED` (`PKU410`) and tree-shaking breaks when the first param is a plain identifier rather than an object pattern. Never `new` a service inside a function — services arrive only via injection.
|
|
101
136
|
|
|
102
137
|
```typescript
|
|
103
138
|
// ✅ Correct — inline destructure, no cast
|
|
@@ -122,15 +157,20 @@ const getUser = pikkuFunc({
|
|
|
122
157
|
|
|
123
158
|
**Never write a `if (!service) throw ...` existence guard in a function body.** It is dead code, and it defeats the platform.
|
|
124
159
|
|
|
125
|
-
Optionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means *"this may not be created"*, not *"this may be missing at call time"*. A service is optional precisely because **nothing destructures it**, and `
|
|
160
|
+
Optionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means *"this may not be created"*, not *"this may be missing at call time"*. A service is optional precisely because **nothing destructures it**, and the generated `requiredSingletonServices` manifest therefore never marks it for creation. The moment any wired function destructures it, Pikku creates it and guarantees it is there.
|
|
126
161
|
|
|
127
162
|
The types enforce this rather than merely documenting it. The inspector records the services destructured by every wired `func`, `permissions` **and** `middleware`, and emits them as `RequiredSingletonServices`. The generated function types then default their service parameter to:
|
|
128
163
|
|
|
129
164
|
```typescript
|
|
130
165
|
export type WiredSingletonServices = RequiredSingletonServices & SingletonServices
|
|
131
|
-
export type WiredServices = RequiredSingletonServices & Services
|
|
166
|
+
export type WiredServices = SecretlessServices<RequiredSingletonServices & Services>
|
|
132
167
|
```
|
|
133
168
|
|
|
169
|
+
The `SecretlessServices<...>` wrapper is why `secrets` never appears in a
|
|
170
|
+
function's services: it is stripped at the type level, not merely omitted by
|
|
171
|
+
convention. Read secrets in a service factory or middleware and hand the value
|
|
172
|
+
to a service instead.
|
|
173
|
+
|
|
134
174
|
so a service that is `foo?: Foo` in `SingletonServices` arrives as a non-optional `Foo` in every function, permission and middleware that uses it. There is nothing to guard against.
|
|
135
175
|
|
|
136
176
|
```typescript
|
|
@@ -232,7 +272,7 @@ export const createSingletonServices = pikkuServices(async (config) => {
|
|
|
232
272
|
|
|
233
273
|
export const createWireServices = pikkuWireServices(
|
|
234
274
|
async (singletonServices, wire) => ({
|
|
235
|
-
scopedLogger: new ScopedLogger(wire.session?.
|
|
275
|
+
scopedLogger: new ScopedLogger(wire.session?.userId),
|
|
236
276
|
})
|
|
237
277
|
)
|
|
238
278
|
|
|
@@ -23,7 +23,8 @@ The `audit` wire service is typed as `AuditLog` (from `@pikku/core`). Functions
|
|
|
23
23
|
```typescript
|
|
24
24
|
const deleteUser = pikkuFunc({
|
|
25
25
|
func: async ({ audit }, { userId }) => {
|
|
26
|
-
|
|
26
|
+
// The user identity comes from the wire session — the payload is metadata.
|
|
27
|
+
await audit.write({ type: 'user.deleted', source: 'explicit', metadata: { userId } })
|
|
27
28
|
// ...
|
|
28
29
|
},
|
|
29
30
|
})
|
|
@@ -6,7 +6,7 @@ Reverse-engineers an existing repository into a **Product Blueprint**: the produ
|
|
|
6
6
|
Existing Repository → pikku-software-archaeology → .knowledge/ blueprint → new Pikku application
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
This is **not** a code indexer or doc generator. It extracts
|
|
9
|
+
This is **not** a code indexer or doc generator. It extracts _intent over implementation_: `POST /api/users/:id/status` becomes the command `ActivateUser`; three scattered `if (inv.user_id !== req.user.id)` checks become one `InvoiceOwnerOnly` policy with three `enforcedAt` citations.
|
|
10
10
|
|
|
11
11
|
## Design decision: the AI is the parser
|
|
12
12
|
|
|
@@ -19,8 +19,9 @@ In Claude Code, from (or pointing at) the target repo:
|
|
|
19
19
|
> Use the pikku-software-archaeology skill to extract a product blueprint from /path/to/repo
|
|
20
20
|
|
|
21
21
|
The agent then:
|
|
22
|
+
|
|
22
23
|
1. **Surveys** the repo (manifests, entry points, routes, jobs, webhooks, schema, config, TODO/HACK markers) — facts only.
|
|
23
|
-
2. **Excavates the test suite** — `describe`/`it` names become workflow scenarios; assertions confirm policies and upgrade confidence; rules that exist
|
|
24
|
+
2. **Excavates the test suite** — `describe`/`it` names become workflow scenarios; assertions confirm policies and upgrade confidence; rules that exist _only_ in tests are captured.
|
|
24
25
|
3. **Extracts** through twelve lenses (domains, entities, commands, …) per the pipeline in `SKILL.md`. Large repos fan out subagents per lens and merge.
|
|
25
26
|
4. **Cross-checks and validates**:
|
|
26
27
|
```bash
|
|
@@ -37,15 +38,23 @@ Concept names are the stable IDs. On re-run after code changes, re-extract only
|
|
|
37
38
|
|
|
38
39
|
## How Pikku consumes the blueprint
|
|
39
40
|
|
|
40
|
-
Full mapping table in `references/pikku-mapping.md`. Summary: entities → Kysely migrations + Zod schemas; commands/queries → `pikkuFunc`s; api surfaces → `wireHTTP`; policies → shared permission functions (collapsing duplicated legacy checks); system workflows → `wireScheduler`/`wireQueueWorker`/`pikkuWorkflowFunc`; integrations → injected services with `
|
|
41
|
+
Full mapping table in `references/pikku-mapping.md`. Summary: entities → Kysely migrations + Zod schemas; commands/queries → `pikkuFunc`s; api surfaces → `wireHTTP`; policies → shared permission functions (collapsing duplicated legacy checks); system workflows → `wireScheduler`/`wireQueueWorker`/`pikkuWorkflowFunc`; integrations → injected services with `defineSecret`/`defineCredential`; test-derived scenarios → `pikkuUserFlow` stories / e2e tests. Humans resolve `migration.json.decisionsNeeded` before any generation starts.
|
|
41
42
|
|
|
42
43
|
## How uncertainty is represented
|
|
43
44
|
|
|
44
45
|
Every extracted concept carries:
|
|
45
46
|
|
|
46
47
|
```json
|
|
47
|
-
{
|
|
48
|
-
"
|
|
48
|
+
{
|
|
49
|
+
"evidence": [
|
|
50
|
+
{
|
|
51
|
+
"file": "controllers/invoices.js",
|
|
52
|
+
"lines": "52",
|
|
53
|
+
"note": "guard: only drafts editable"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"confidence": "high"
|
|
57
|
+
}
|
|
49
58
|
```
|
|
50
59
|
|
|
51
60
|
- **high** — the behavior itself is in the cited code/schema/test. Generates directly.
|
|
@@ -53,7 +62,8 @@ Every extracted concept carries:
|
|
|
53
62
|
- **low** — plausible reconstruction. Never auto-generated; surfaced for human review.
|
|
54
63
|
|
|
55
64
|
Two further distinctions keep facts and guesses separate:
|
|
56
|
-
|
|
65
|
+
|
|
66
|
+
- `events[].explicit: false` — the event was _reconstructed_ from side-effect clusters (email + status flip), not emitted by the code.
|
|
57
67
|
- Comments/docs vs code: comments describe intent, code describes behavior. Disagreements are recorded as the code's behavior plus a `gaps.json` entry.
|
|
58
68
|
|
|
59
69
|
## Repo layout
|
|
@@ -2,36 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
The `.knowledge/` blueprint is designed so each concept maps onto exactly one Pikku primitive. A generator (or an agent following `pikku-feature`) walks the JSON files in this order:
|
|
4
4
|
|
|
5
|
-
| Blueprint source
|
|
6
|
-
|
|
7
|
-
| `entities.json` attributes + relationships + constraints
|
|
8
|
-
| `entities.json` states/transitions
|
|
9
|
-
| `commands.json`
|
|
10
|
-
| `queries.json`
|
|
11
|
-
| `events.json`
|
|
12
|
-
| `policies.json` (authorization)
|
|
13
|
-
| `policies.json` (validation)
|
|
14
|
-
| `workflows.json` kind=user
|
|
15
|
-
| `workflows.json` kind=system, with `schedule`
|
|
16
|
-
| `workflows.json` multi-step / checkpointing
|
|
17
|
-
| `workflows.json` `scenarios[]`
|
|
18
|
-
| `api.json`
|
|
19
|
-
| `api.json` kind=webhook-in
|
|
20
|
-
| `integrations.json`
|
|
21
|
-
| `architecture.json` notes
|
|
22
|
-
| `invariants.json` enforcedBy=db-constraint
|
|
23
|
-
| `invariants.json` enforcedBy=code-guard/nothing
|
|
24
|
-
| `gaps.json`
|
|
25
|
-
| `migration.json.mappings`
|
|
26
|
-
| `interfaces.json` kind=cli
|
|
27
|
-
| `interfaces.json` kind=mcp
|
|
28
|
-
| `interfaces.json` kind=openapi-rest / sdk
|
|
29
|
-
| `interfaces.json` kind=websocket-realtime
|
|
30
|
-
| `frontend.json`
|
|
31
|
-
| `frontend-routes.json`
|
|
32
|
-
| `frontend-components.json` rebuild=`mantine-standard`/`mantine-composition` | components in `packages/components` composed from `@pikku/mantine` — the trivial/straightforward bulk
|
|
33
|
-
| `frontend-components.json` rebuild=`custom-logic`
|
|
34
|
-
| `frontend-components.json` rebuild=`custom-style`
|
|
5
|
+
| Blueprint source | Pikku target |
|
|
6
|
+
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `entities.json` attributes + relationships + constraints | Kysely migrations + generated `DB` types; Zod schemas per entity |
|
|
8
|
+
| `entities.json` states/transitions | a `status` column + transition guards inside the owning commands (or a state-machine helper) |
|
|
9
|
+
| `commands.json` | `pikkuFunc` / `pikkuSessionlessFunc` with `input:` Zod schema built from `input[]`; `preconditions` become guard clauses; name is the camelCased command name (`SendInvoice` → `sendInvoice`) |
|
|
10
|
+
| `queries.json` | `pikkuFunc` reads; `scoping` becomes the mandatory `WHERE` / session filter |
|
|
11
|
+
| `events.json` | EventHub topics (realtime) or queue messages; `consumedBy` become `wireQueueWorker` handlers — implicit events (`explicit: false`) get promoted to real emissions |
|
|
12
|
+
| `policies.json` (authorization) | Pikku `permissions` / middleware; one policy = one named permission function, wired everywhere `enforcedAt` listed — this collapses duplicated legacy checks into a single definition |
|
|
13
|
+
| `policies.json` (validation) | Zod schema refinements on the command's `input` |
|
|
14
|
+
| `workflows.json` kind=user | frontend flows + the commands they chain |
|
|
15
|
+
| `workflows.json` kind=system, with `schedule` | `wireScheduler` entries |
|
|
16
|
+
| `workflows.json` multi-step / checkpointing | `pikkuWorkflowFunc` with one `workflow.do(...)` step per blueprint step |
|
|
17
|
+
| `workflows.json` `scenarios[]` | **`pikkuUserFlow` stories — this is the canonical target.** Each scenario's given/when/outcome maps 1:1 onto a user-flow step sequence; group scenarios by their workflow into one flow per journey. Only scenarios with no user-facing surface (pure system workflows: cron sweeps, webhook ingest) fall back to API/e2e tests |
|
|
18
|
+
| `api.json` | `wireHTTP` routes: keep `path`+`method` for compatibility, point at the mapped command/query func; `auth: none`/capability-URL surfaces get `auth: false` |
|
|
19
|
+
| `api.json` kind=webhook-in | `wireHTTP` with `auth: false` + signature-verification middleware from the integration |
|
|
20
|
+
| `integrations.json` | services in `services.ts` (constructor-injected classes); `configVia` env vars become `defineSecret` / config; per-user credentials become `defineCredential` |
|
|
21
|
+
| `architecture.json` notes | deployment config (ports, raw-body routes, proxy expectations) |
|
|
22
|
+
| `invariants.json` enforcedBy=db-constraint | migration constraints (UNIQUE, CHECK, FK) |
|
|
23
|
+
| `invariants.json` enforcedBy=code-guard/nothing | guard clauses + a test each; `atRiskBecause` entries get a hardening task |
|
|
24
|
+
| `gaps.json` | excluded from generation; `open-product-decision` + `migration.json.decisionsNeeded` go to a human BEFORE generation starts |
|
|
25
|
+
| `migration.json.mappings` | the work plan: one mapping = one migration slice |
|
|
26
|
+
| `interfaces.json` kind=cli | `wireCLI` entrypoints — the CLI commands are the same funcs the routes expose |
|
|
27
|
+
| `interfaces.json` kind=mcp | `wireMCP` — each MCP tool IS a `pikkuFunc` (reuse the command/query funcs; don't author tool duplicates) |
|
|
28
|
+
| `interfaces.json` kind=openapi-rest / sdk | generated, not hand-written: the OpenAPI spec + typed client SDK fall out of the `wireHTTP` routes + codegen |
|
|
29
|
+
| `interfaces.json` kind=websocket-realtime | `pikku-realtime` EventHub topics / channels |
|
|
30
|
+
| `frontend.json` | `apps/app` (TanStack Start) shell: router, `@pikku/mantine` theme, `pikku-react-query` data layer, `better-auth` client — the target stack the legacy UI is rebuilt onto |
|
|
31
|
+
| `frontend-routes.json` | TanStack Router routes under `apps/app/src/routes/**` (thin data containers calling `usePikkuQuery`); `dataFrom` names become the generated hooks; subpath routes for rich detail views |
|
|
32
|
+
| `frontend-components.json` rebuild=`mantine-standard`/`mantine-composition` | components in `packages/components` composed from `@pikku/mantine` — the trivial/straightforward bulk |
|
|
33
|
+
| `frontend-components.json` rebuild=`custom-logic` | the PORT list — each becomes a `packages/components` component that reimplements the bespoke behavior (chart/table/editor); its `dependencies` inform whether the lib is kept or replaced. These are the frontend's real work items |
|
|
34
|
+
| `frontend-components.json` rebuild=`custom-style` | normalize to Mantine/theme tokens; usually deleted-and-recomposed, not ported |
|
|
35
35
|
|
|
36
36
|
## Order of generation
|
|
37
37
|
|
|
@@ -18,11 +18,16 @@ commit, separate from any feature work.
|
|
|
18
18
|
_template_, not the user's project — leaving it in place is misleading.
|
|
19
19
|
Either delete it (`git rm README.md`) or rewrite it with the new project's
|
|
20
20
|
name and purpose. Never ship a clone with the generic template README.
|
|
21
|
-
2. **Keep the lockfile committed.**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
2. **Keep the lockfile committed.** Do NOT re-add `yarn.lock` to `.gitignore`.
|
|
22
|
+
A real project commits its lockfile for reproducible installs. The correct
|
|
23
|
+
pattern is `yarn.lock` followed by `!/yarn.lock`, which commits the root
|
|
24
|
+
lockfile while keeping generated per-unit lockfiles under `.deploy/` (and
|
|
25
|
+
`e2e/`) ignored.
|
|
26
|
+
|
|
27
|
+
`create-pikku` keeps only the chosen package manager's lockfile and deletes
|
|
28
|
+
the other, and for yarn it may have written an **empty** `yarn.lock` as a
|
|
29
|
+
marker. Commit the lockfile *after* the first install has filled it in —
|
|
30
|
+
committing the empty placeholder pins nothing.
|
|
26
31
|
3. **Rename template identifiers.** Update `name` in the root `package.json`
|
|
27
32
|
(and any `@project/*` or other placeholder names) to the real project.
|
|
28
33
|
4. **Drop template-only artifacts.** Remove any `TEMPLATE.md`, demo docs, or
|
|
@@ -35,16 +35,23 @@ See `pikku-concepts` for the core mental model.
|
|
|
35
35
|
|
|
36
36
|
## API Reference
|
|
37
37
|
|
|
38
|
+
All three come from `#pikku`. A trigger is deliberately split in two: the
|
|
39
|
+
**source** owns the connection to the outside world and the **trigger** names the
|
|
40
|
+
function to run, so one source can be swapped (Redis → PG) without touching the
|
|
41
|
+
handler, and a handler can exist before any source is wired.
|
|
42
|
+
|
|
38
43
|
### `wireTrigger(config)`
|
|
39
44
|
|
|
40
45
|
Define the target function that handles trigger events:
|
|
41
46
|
|
|
42
47
|
```typescript
|
|
43
|
-
import { wireTrigger } from '
|
|
48
|
+
import { wireTrigger } from '#pikku'
|
|
44
49
|
|
|
45
50
|
wireTrigger({
|
|
46
51
|
name: string, // Trigger name (matches source)
|
|
47
52
|
func: PikkuFunc, // Function to call when event fires
|
|
53
|
+
description?: string,
|
|
54
|
+
tags?: string[],
|
|
48
55
|
})
|
|
49
56
|
```
|
|
50
57
|
|
|
@@ -53,18 +60,24 @@ wireTrigger({
|
|
|
53
60
|
Define the event source that fires triggers:
|
|
54
61
|
|
|
55
62
|
```typescript
|
|
56
|
-
import { wireTriggerSource } from '
|
|
63
|
+
import { wireTriggerSource } from '#pikku'
|
|
57
64
|
|
|
58
65
|
wireTriggerSource({
|
|
59
|
-
name: string, // Must match wireTrigger name
|
|
60
|
-
func: PikkuTriggerFunc, // Source function (sets up listener)
|
|
61
|
-
input: object, // Configuration
|
|
66
|
+
name: string, // Must match a wireTrigger name
|
|
67
|
+
func: PikkuTriggerFunc, // Source function (sets up the listener)
|
|
68
|
+
input: object, // Configuration handed to the source
|
|
62
69
|
})
|
|
63
70
|
```
|
|
64
71
|
|
|
72
|
+
`input` is required whenever the source function declares an input type, and the
|
|
73
|
+
name must be unique — wiring the same source name twice throws
|
|
74
|
+
`Trigger source already exists`.
|
|
75
|
+
|
|
65
76
|
### `pikkuTriggerFunc<TInput, TEvent>`
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
A trigger source function runs **once at startup**, not once per event. It sets
|
|
79
|
+
up a listener, calls `trigger.invoke(...)` for each event it sees, and returns a
|
|
80
|
+
teardown function:
|
|
68
81
|
|
|
69
82
|
```typescript
|
|
70
83
|
import { pikkuTriggerFunc } from '#pikku'
|
|
@@ -83,6 +96,37 @@ const source = pikkuTriggerFunc<
|
|
|
83
96
|
})
|
|
84
97
|
```
|
|
85
98
|
|
|
99
|
+
It receives **singleton services only** — there is no session, no request and no
|
|
100
|
+
per-wire services, because a listener outlives every event it will ever emit.
|
|
101
|
+
The config-object form (`pikkuTriggerFunc({ func, title, description, tags,
|
|
102
|
+
input, output })`) is also accepted when you want schemas or metadata on the
|
|
103
|
+
source.
|
|
104
|
+
|
|
105
|
+
## Starting triggers
|
|
106
|
+
|
|
107
|
+
Nothing fires until a `TriggerService` is started. For a single process,
|
|
108
|
+
`InMemoryTriggerService` walks every wired source that has at least one matching
|
|
109
|
+
target and sets it up:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { InMemoryTriggerService } from '@pikku/core/services'
|
|
113
|
+
|
|
114
|
+
const triggerService = new InMemoryTriggerService()
|
|
115
|
+
await triggerService.start()
|
|
116
|
+
// on shutdown
|
|
117
|
+
await triggerService.stop() // runs every source's teardown
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A source with no matching `wireTrigger` is logged and skipped rather than
|
|
121
|
+
erroring — the two halves are wired independently, so a half-wired trigger is a
|
|
122
|
+
normal intermediate state.
|
|
123
|
+
|
|
124
|
+
**If a wiring is silently skipped**, look for
|
|
125
|
+
`Skipping trigger … metadata not found` in the logs. Both wirings read metadata
|
|
126
|
+
generated by the inspector, and it warns rather than throwing; the usual fix is
|
|
127
|
+
the one the warning suggests — move the wiring into its own file so codegen
|
|
128
|
+
picks it up.
|
|
129
|
+
|
|
86
130
|
## Usage Patterns
|
|
87
131
|
|
|
88
132
|
### Redis Pub/Sub Source
|