@pikku/skills 0.12.22 → 0.12.25
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 +101 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +2 -2
- package/skills/pikku-agent/SKILL.md +67 -316
- package/skills/pikku-agent/references/agents.md +299 -0
- package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
- package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
- package/skills/pikku-architect/SKILL.md +264 -0
- package/skills/pikku-auth/SKILL.md +89 -0
- package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +1 -27
- package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
- package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
- package/skills/{pikku-permissions/SKILL.md → pikku-auth/references/permissions.md} +1 -20
- package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
- package/skills/pikku-build/SKILL.md +87 -0
- package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +75 -23
- package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
- package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +1 -1
- package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
- package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
- package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
- package/skills/{pikku-build-app → pikku-build}/references/ship.md +7 -1
- package/skills/pikku-concepts/SKILL.md +72 -7
- package/skills/pikku-concepts/references/concept-mapping.md +8 -8
- package/skills/pikku-deploy/SKILL.md +158 -0
- package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
- package/skills/pikku-deploy/references/cloudflare.md +104 -0
- package/skills/pikku-deploy/references/express.md +92 -0
- package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
- package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
- package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
- package/skills/pikku-deploy/references/uws.md +72 -0
- package/skills/pikku-deploy/references/ws.md +75 -0
- package/skills/pikku-emails/SKILL.md +3 -2
- package/skills/pikku-fabric/SKILL.md +12 -2
- package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
- package/skills/pikku-i18n/SKILL.md +60 -207
- package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
- package/skills/pikku-i18n/references/messages.md +218 -0
- package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
- package/skills/pikku-knowledge/SKILL.md +14 -0
- package/skills/pikku-kysely/SKILL.md +13 -13
- package/skills/pikku-meta/SKILL.md +58 -130
- package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
- package/skills/pikku-meta/references/meta.md +114 -0
- package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-n8n-import/SKILL.md +0 -1
- package/skills/pikku-react/SKILL.md +50 -298
- package/skills/pikku-react/references/client.md +293 -0
- package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
- package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
- package/skills/pikku-scenario/SKILL.md +60 -45
- package/skills/pikku-scenario/references/persona-run.md +148 -0
- package/skills/pikku-service-backends/SKILL.md +154 -0
- package/skills/pikku-service-backends/references/aws.md +106 -0
- package/skills/pikku-service-backends/references/backblaze.md +57 -0
- package/skills/pikku-service-backends/references/mongodb.md +90 -0
- package/skills/pikku-service-backends/references/redis.md +75 -0
- package/skills/pikku-service-backends/references/schema.md +63 -0
- package/skills/pikku-services/SKILL.md +68 -291
- package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
- package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
- package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
- package/skills/pikku-services/references/services.md +272 -0
- package/skills/pikku-software-archaeology/README.md +5 -1
- package/skills/pikku-software-archaeology/SKILL.md +15 -2
- package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
- package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
- package/skills/pikku-webhook/SKILL.md +199 -0
- package/skills/pikku-wiring/SKILL.md +180 -0
- package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
- package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
- package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
- package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
- package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
- package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
- package/skills/{pikku-realtime/SKILL.md → pikku-wiring/references/realtime.md} +2 -25
- package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
- package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
- package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
- package/skills/pikku-workflow/SKILL.md +2 -2
- package/skills/pikku-aws/SKILL.md +0 -161
- package/skills/pikku-backblaze/SKILL.md +0 -104
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
- package/skills/pikku-deploy-express/SKILL.md +0 -122
- package/skills/pikku-deploy-uws/SKILL.md +0 -144
- package/skills/pikku-mongodb/SKILL.md +0 -113
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-redis/SKILL.md +0 -99
- package/skills/pikku-schema-ajv/SKILL.md +0 -83
- package/skills/pikku-schema-cfworker/SKILL.md +0 -82
- package/skills/pikku-ws/SKILL.md +0 -87
- /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
- /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
- /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
- /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
- /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
|
@@ -1,313 +1,65 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-react
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Use when a React frontend talks to a Pikku backend — PikkuProvider and createPikku at the app
|
|
5
|
+
root, the generated React Query hooks (usePikkuQuery, usePikkuMutation, usePikkuInfiniteQuery),
|
|
6
|
+
direct usePikkuRPC / usePikkuFetch calls, realtime subscriptions, agent and workflow hooks, and
|
|
7
|
+
the dev actor switcher. TRIGGER when: writing a React component that fetches or mutates backend
|
|
8
|
+
data, wiring PikkuProvider, paginating, running or tracking a workflow from the client, or
|
|
9
|
+
asking about useDevActors / VITE_DEV_ACTORS / quick login. DO NOT TRIGGER when: working on the
|
|
10
|
+
backend (use pikku-wiring), defining the workflow itself (use pikku-workflow), or writing
|
|
11
|
+
user-facing copy (use pikku-i18n).
|
|
4
12
|
installGroups: [client]
|
|
5
13
|
---
|
|
6
14
|
|
|
7
15
|
# Pikku React
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
The hook names and their argument types come from your generated `api.gen.ts` —
|
|
18
|
+
read it for what this app actually exposes. This skill is the part it cannot
|
|
19
|
+
tell you: which hook a given need calls for, and where the generated client
|
|
20
|
+
stops.
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
## Pick the reference
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
| You are… | Read |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| Wiring the app root, resolving the server URL, authenticating, or subscribing to realtime | `references/client.md` |
|
|
27
|
+
| Fetching, mutating or paginating data | `references/react-query.md` |
|
|
28
|
+
| Starting a workflow and showing its progress | `references/workflows.md` |
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
two hooks. It does **not** depend on React Query — that's a separate
|
|
21
|
-
opt-in via the generated `api.gen.ts`. Use this skill when setting up the
|
|
22
|
-
provider or making direct RPC calls.
|
|
30
|
+
## Reach for what
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
| Need | Use |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| Render data, dedupe and cache | `usePikkuQuery` |
|
|
35
|
+
| Trigger a write and wait for the result | `usePikkuMutation` |
|
|
36
|
+
| Paginate | `usePikkuInfiniteQuery` |
|
|
37
|
+
| One-off call from an event handler | `usePikkuRPC()` |
|
|
38
|
+
| Hit a REST endpoint rather than an RPC | `usePikkuFetch()` |
|
|
39
|
+
| Talk to one named AI agent | `usePikkuAgent(name)` → `.run` / `.stream` / `.approve` |
|
|
40
|
+
| Run one named workflow | `usePikkuWorkflow(name)` → `.start` / `.run` / `.status` |
|
|
41
|
+
| A workflow long enough to need progress UI | `references/workflows.md` |
|
|
42
|
+
| Subscribe to events, SSE or a channel | `usePikkuRealtime()` |
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
PikkuProvider,
|
|
29
|
-
createPikku,
|
|
30
|
-
usePikkuFetch,
|
|
31
|
-
usePikkuRPC,
|
|
32
|
-
usePikkuRealtime,
|
|
33
|
-
usePikkuAgent,
|
|
34
|
-
usePikkuWorkflow,
|
|
35
|
-
asI18n,
|
|
36
|
-
} from '@pikku/react'
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
`usePikkuRealtime` is only valid when you wired a `PikkuRealtime` class via
|
|
40
|
-
`createPikku` — see the setup section. `usePikkuAgent` and `usePikkuWorkflow`
|
|
41
|
-
are thin bindings over the RPC client that pin one agent/workflow name, so a
|
|
42
|
-
component never repeats it. `asI18n` is the i18n brand (see **pikku-i18n**).
|
|
43
|
-
|
|
44
|
-
## Resolving the server URL
|
|
45
|
-
|
|
46
|
-
Every client (`createPikku`, realtime, the auth client) resolves its base
|
|
47
|
-
through one shared helper in `src/lib/env.ts`. Write this once:
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
// Endpoints come from env, never hardcoded.
|
|
51
|
-
export function apiUrl(): string {
|
|
52
|
-
// SSR: the client hooks only run in the browser, so a placeholder is fine.
|
|
53
|
-
if (import.meta.env.SSR) {
|
|
54
|
-
return import.meta.env.VITE_API_URL ?? '/__api'
|
|
55
|
-
}
|
|
56
|
-
return import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**Never fall back to `http://localhost:3000`.** `import.meta.env.VITE_API_URL`
|
|
61
|
-
is substituted by Vite at _build_ time, so any deploy that supplies the URL as
|
|
62
|
-
a _runtime_ env var or platform binding leaves it `undefined` in the shipped
|
|
63
|
-
bundle — the fallback is then the only branch that ever runs in the browser. A
|
|
64
|
-
localhost fallback means every request from a deployed app goes to the user's
|
|
65
|
-
own machine. `origin + '/api'` is same-origin, needs no build-time knowledge of
|
|
66
|
-
the domain, and is correct wherever the app is served from.
|
|
67
|
-
|
|
68
|
-
For local dev, set `VITE_API_URL`, or proxy `/api` → your backend in
|
|
69
|
-
`vite.config.ts` under `server.proxy`. One `/api` entry also covers
|
|
70
|
-
`/api/auth/*`; only add more entries for root-level routes outside `/api`.
|
|
71
|
-
|
|
72
|
-
## Setup at the app root
|
|
73
|
-
|
|
74
|
-
```tsx
|
|
75
|
-
import { createPikku, PikkuProvider } from '@pikku/react'
|
|
76
|
-
import { PikkuFetch } from './pikku/pikku-fetch.gen'
|
|
77
|
-
import { PikkuRPC } from './pikku/pikku-rpc.gen'
|
|
78
|
-
import { apiUrl } from './lib/env'
|
|
79
|
-
|
|
80
|
-
const pikku = createPikku(PikkuFetch, PikkuRPC, {
|
|
81
|
-
serverUrl: apiUrl(),
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
createRoot(document.getElementById('root')!).render(
|
|
85
|
-
<PikkuProvider pikku={pikku}>
|
|
86
|
-
<App />
|
|
87
|
-
</PikkuProvider>
|
|
88
|
-
)
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
If the project also exposes realtime events (see **pikku-realtime**), pass
|
|
92
|
-
the `PikkuRealtime` class as the third argument and the instance gets a
|
|
93
|
-
`realtime` field too:
|
|
94
|
-
|
|
95
|
-
```tsx
|
|
96
|
-
import { PikkuRealtime } from './pikku/realtime.gen'
|
|
97
|
-
|
|
98
|
-
const pikku = createPikku(PikkuFetch, PikkuRPC, PikkuRealtime, {
|
|
99
|
-
serverUrl: apiUrl(),
|
|
100
|
-
})
|
|
101
|
-
// pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch
|
|
102
|
-
// (server URL + auth configured once).
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The generated classes come from your `pikku.config.json`:
|
|
106
|
-
|
|
107
|
-
| config field | generated file |
|
|
108
|
-
| ---------------------------- | ----------------------------------------------------- |
|
|
109
|
-
| `clientFiles.fetchFile` | typed HTTP client (`PikkuFetch` class) |
|
|
110
|
-
| `clientFiles.rpcWiringsFile` | RPC client (`PikkuRPC` class) calling all exposed fns |
|
|
111
|
-
| `clientFiles.realtimeFile` | `PikkuRealtime` (websocket events + SSE + channels) |
|
|
112
|
-
|
|
113
|
-
If a file isn't being generated, that field is missing from the config —
|
|
114
|
-
add it and re-run `pikku all`.
|
|
115
|
-
|
|
116
|
-
`createPikku(...)` accepts the same `CorePikkuFetchOptions` as `PikkuFetch`
|
|
117
|
-
plus `serverUrl`. Auth headers, request interceptors, etc. are configured
|
|
118
|
-
on the fetch instance — RPC and realtime inherit them automatically.
|
|
119
|
-
|
|
120
|
-
## Calling an RPC directly (no React Query)
|
|
121
|
-
|
|
122
|
-
Inside a component:
|
|
123
|
-
|
|
124
|
-
```tsx
|
|
125
|
-
import { usePikkuRPC } from '@pikku/react'
|
|
126
|
-
|
|
127
|
-
function Logout() {
|
|
128
|
-
const rpc = usePikkuRPC()
|
|
129
|
-
return <button onClick={() => rpc.invoke('logoutUser', {})}>Sign out</button>
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
`rpc.invoke(name, data)` is typed against `FlattenedRPCMap` — `name` must
|
|
134
|
-
be an exposed function id, `data` matches the input schema, return value
|
|
135
|
-
matches the output schema.
|
|
136
|
-
|
|
137
|
-
You also have `rpc.<funcName>(data)` if the generated RPC client builds
|
|
138
|
-
direct methods (project-dependent).
|
|
139
|
-
|
|
140
|
-
## Calling fetch directly
|
|
141
|
-
|
|
142
|
-
```tsx
|
|
143
|
-
const fetch = usePikkuFetch()
|
|
144
|
-
const data = await fetch.get('/some-rest-route', { searchParams: {...} })
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Use this only when the function is wired via HTTP (REST shape) and you
|
|
148
|
-
need a path-style call. For RPC calls, `usePikkuRPC()` is cleaner.
|
|
149
|
-
|
|
150
|
-
## Realtime subscriptions
|
|
151
|
-
|
|
152
|
-
If you wired a `PikkuRealtime` class into `createPikku`, use
|
|
153
|
-
`usePikkuRealtime()` to grab the shared instance:
|
|
154
|
-
|
|
155
|
-
```tsx
|
|
156
|
-
import { usePikkuRealtime } from '@pikku/react'
|
|
157
|
-
import type { PikkuRealtime } from './pikku/realtime.gen'
|
|
158
|
-
|
|
159
|
-
function TodoList() {
|
|
160
|
-
const realtime = usePikkuRealtime<PikkuRealtime>()
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
return realtime.subscribe('todo-created', ({ todo }) => {
|
|
163
|
-
/* ... */
|
|
164
|
-
})
|
|
165
|
-
}, [realtime])
|
|
166
|
-
// ...
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
The hook throws if no `PikkuRealtime` was wired — that's how you know to
|
|
171
|
-
add it to `createPikku(...)`. Full event-hub setup, publishing, and SSE
|
|
172
|
-
helpers live in **pikku-realtime**.
|
|
173
|
-
|
|
174
|
-
## When to reach for what
|
|
175
|
-
|
|
176
|
-
| Need | Use |
|
|
177
|
-
| ----------------------------------- | -------------------------------------------------- |
|
|
178
|
-
| Render data, dedupe + cache | **usePikkuQuery** (react-query) |
|
|
179
|
-
| Trigger a write, wait for result | **usePikkuMutation** (react-query) |
|
|
180
|
-
| Paginate | **usePikkuInfiniteQuery** (react-query) |
|
|
181
|
-
| One-off call from an event handler | `usePikkuRPC()` direct |
|
|
182
|
-
| Hit a REST endpoint (not RPC) | `usePikkuFetch()` |
|
|
183
|
-
| Run one named workflow | `usePikkuWorkflow('name')` → `.start/.run/.status` |
|
|
184
|
-
| Talk to one named AI agent | `usePikkuAgent('name')` → `.run/.stream/.approve` |
|
|
185
|
-
| Longer-running workflow UX | **pikku-workflows-client** |
|
|
186
|
-
| Subscribe to events / SSE / channel | `usePikkuRealtime()` (see **pikku-realtime**) |
|
|
187
|
-
|
|
188
|
-
The first three live in your generated `api.gen.ts` (see the
|
|
189
|
-
**pikku-react-query** skill). This skill covers the rest.
|
|
190
|
-
|
|
191
|
-
`usePikkuAgent` and `usePikkuWorkflow` bind the name once and hand back the
|
|
192
|
-
call methods with it already applied:
|
|
193
|
-
|
|
194
|
-
```tsx
|
|
195
|
-
const agent = usePikkuAgent('todo-agent')
|
|
196
|
-
const { text } = await agent.run({ message, threadId })
|
|
197
|
-
|
|
198
|
-
const workflow = usePikkuWorkflow('onboardUser')
|
|
199
|
-
const { runId } = await workflow.start({ email })
|
|
200
|
-
const state = await workflow.status(runId)
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## Authentication
|
|
204
|
-
|
|
205
|
-
Auth is handled at the `PikkuFetch` layer, and `createPikku`'s options object
|
|
206
|
-
_is_ `CorePikkuFetchOptions` plus `serverUrl` — flat, not nested under a
|
|
207
|
-
`fetchOptions` key:
|
|
208
|
-
|
|
209
|
-
```tsx
|
|
210
|
-
const pikku = createPikku(PikkuFetch, PikkuRPC, {
|
|
211
|
-
serverUrl: apiUrl(),
|
|
212
|
-
credentials: 'include', // cookie sessions
|
|
213
|
-
authHeaders: { jwt: token }, // or { apiKey }
|
|
214
|
-
transformDate: true,
|
|
215
|
-
})
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
There is no request-interceptor hook. For a token that changes after startup,
|
|
219
|
-
call the setter on the shared instance — RPC and realtime pick it up because
|
|
220
|
-
they hold the same fetch:
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
pikku.fetch.setAuthorizationJWT(token) // null clears it
|
|
224
|
-
pikku.fetch.setAPIKey(key)
|
|
225
|
-
pikku.fetch.setHeader('x-tenant', tenantId)
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
`authHeaders.jwt` becomes `Authorization: Bearer …` and `authHeaders.apiKey`
|
|
229
|
-
becomes `X-API-KEY`; setting a JWT takes precedence over an API key.
|
|
230
|
-
|
|
231
|
-
### Dev actor sign-in (`useDevActors`)
|
|
232
|
-
|
|
233
|
-
The dev-only "Sign in as …" control: one click signs in as a declared scenario
|
|
234
|
-
persona with no password, so the app can be reviewed as each kind of user.
|
|
235
|
-
`pikku fabric validate` **requires** any frontend with a login screen to ship one
|
|
236
|
-
(`app-missing-actor-quick-login-<app>`) — without it a reviewer is locked out of
|
|
237
|
-
their own sandbox.
|
|
238
|
-
|
|
239
|
-
```tsx
|
|
240
|
-
import { useDevActors } from '@pikku/react'
|
|
241
|
-
|
|
242
|
-
const { actors, signInAs, pendingEmail, isPending, error } = useDevActors({
|
|
243
|
-
// Gate both reads on the bundler's dev flag so no credential can reach a
|
|
244
|
-
// production bundle. The sandbox dev server bakes them from your personas.
|
|
245
|
-
actors: import.meta.env.DEV ? import.meta.env.VITE_DEV_ACTORS : undefined,
|
|
246
|
-
secrets: import.meta.env.DEV
|
|
247
|
-
? import.meta.env.VITE_DEV_ACTOR_SECRETS
|
|
248
|
-
: undefined,
|
|
249
|
-
apiUrl: apiUrl(),
|
|
250
|
-
onSignedIn: () => navigate({ to: '/' }),
|
|
251
|
-
})
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
- **It is UI-free**, so render it however you like. For the default rendering use
|
|
255
|
-
`<DevActorSwitcher />` from `@pikku/mantine/dev` — a separate entry point from
|
|
256
|
-
`@pikku/mantine/core`, whose contract is "drop-in alias for `@mantine/core`"
|
|
257
|
-
and so must not export components Mantine has no counterpart for.
|
|
258
|
-
- **`secrets` is `{ address: credential }`, not one shared value** — a
|
|
259
|
-
credential opens the one persona it was minted for (see
|
|
260
|
-
**pikku-better-auth**). `actors` is empty unless the host supplied both a list
|
|
261
|
-
and the credentials for it, and an actor with no credential is not offered, so
|
|
262
|
-
a production build renders nothing without you testing for it.
|
|
263
|
-
- **It takes `onSignedIn` rather than a router**, and takes the env values rather
|
|
264
|
-
than reading them, because how env is spelled is a bundler fact
|
|
265
|
-
(`import.meta.env.VITE_*` vs `process.env.NEXT_PUBLIC_*`).
|
|
266
|
-
- The underlying `signInAsActor()` and `parseDevActors()` are exported too, for a
|
|
267
|
-
non-React caller. The endpoint only accepts rows flagged `actor: true`, so it
|
|
268
|
-
can never impersonate a real user — see **pikku-better-auth**.
|
|
269
|
-
|
|
270
|
-
Do not hand-write the `devActors()` / `signInAsActor()` pair per app; that
|
|
271
|
-
copy-paste, including the `import.meta.env.DEV` gate, is exactly what this
|
|
272
|
-
replaced.
|
|
273
|
-
|
|
274
|
-
### Linking from a Mantine element: `renderRoot`, not `component`
|
|
275
|
-
|
|
276
|
-
Handing TanStack's `Link` to a Mantine element as `component={Link}` compiles,
|
|
277
|
-
renders, and navigates — and silently unties the type. Mantine's polymorphic
|
|
278
|
-
`component` prop widens the router generic to `AnyRouter`, so `to` and
|
|
279
|
-
`params` stop being checked against your actual routes. Renaming a route then
|
|
280
|
-
breaks the running app instead of the build, which is the one thing the typed
|
|
281
|
-
router exists to prevent.
|
|
282
|
-
|
|
283
|
-
Wrap the typed `Link` once and reach it through `renderRoot`, which passes the
|
|
284
|
-
props through without re-typing the element:
|
|
285
|
-
|
|
286
|
-
```tsx
|
|
287
|
-
// components/links.tsx — one wrapper the whole app links through
|
|
288
|
-
import { Link } from '@tanstack/react-router'
|
|
289
|
-
|
|
290
|
-
export const AssessmentLink = (props: { assessmentId: string; children: React.ReactNode }) => (
|
|
291
|
-
<Link to="/assessments/$assessmentId" params={{ assessmentId: props.assessmentId }}>
|
|
292
|
-
{props.children}
|
|
293
|
-
</Link>
|
|
294
|
-
)
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
```tsx
|
|
298
|
-
<Button renderRoot={(p) => <AssessmentLink assessmentId={id} {...p} />}>
|
|
299
|
-
Open
|
|
300
|
-
</Button>
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
The wrapper is where `to` and `params` are checked, and it is checked once.
|
|
44
|
+
A workflow that finishes in a moment can be awaited; one that does not needs
|
|
45
|
+
start-plus-observe, or the component holds a pending state with nothing to show.
|
|
304
46
|
|
|
305
47
|
## What NOT to do
|
|
306
48
|
|
|
307
|
-
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
49
|
+
- **Do not write a client.** The generated one covers every exposed function
|
|
50
|
+
with full types; a hand-rolled RPC client or a hand-written
|
|
51
|
+
`useQuery({ queryKey, queryFn })` reimplements it worse.
|
|
52
|
+
- **Do not instantiate `PikkuFetch`/`PikkuRPC` in a component.** `createPikku`
|
|
53
|
+
runs once at the app root and the instance flows through context — and
|
|
54
|
+
`usePikkuRPC()` outside `<PikkuProvider>` throws.
|
|
55
|
+
- **Do not call the RPC client inside a `useEffect`.** The hooks handle
|
|
56
|
+
deduplication, caching and unmounting; a manual effect handles none of them.
|
|
57
|
+
- **Do not construct a hook name at runtime.** Hook names are the RPC names known
|
|
58
|
+
at generation time, and a computed one is not type-checked.
|
|
59
|
+
- **Do not poll a workflow with `setInterval`.** `useWorkflowStatus` with a
|
|
60
|
+
`refetchInterval` callback dedupes across components and stops on a terminal
|
|
61
|
+
state in one place.
|
|
62
|
+
- **Do not reach for `as any` when a hook's types disagree with you.** The
|
|
63
|
+
mismatch is the backend's input/output schema; fix it there.
|
|
64
|
+
- **Do not hardcode a user-facing string.** Every display string goes through an
|
|
65
|
+
i18n message — see `pikku-i18n`.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Pikku React
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## What ships
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import {
|
|
8
|
+
PikkuProvider,
|
|
9
|
+
createPikku,
|
|
10
|
+
usePikkuFetch,
|
|
11
|
+
usePikkuRPC,
|
|
12
|
+
usePikkuRealtime,
|
|
13
|
+
usePikkuAgent,
|
|
14
|
+
usePikkuWorkflow,
|
|
15
|
+
asI18n,
|
|
16
|
+
} from '@pikku/react'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`usePikkuRealtime` is only valid when you wired a `PikkuRealtime` class via
|
|
20
|
+
`createPikku` — see the setup section. `usePikkuAgent` and `usePikkuWorkflow`
|
|
21
|
+
are thin bindings over the RPC client that pin one agent/workflow name, so a
|
|
22
|
+
component never repeats it. `asI18n` is the i18n brand (see **pikku-i18n**).
|
|
23
|
+
|
|
24
|
+
## Resolving the server URL
|
|
25
|
+
|
|
26
|
+
Every client (`createPikku`, realtime, the auth client) resolves its base
|
|
27
|
+
through one shared helper in `src/lib/env.ts`. Write this once:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// Endpoints come from env, never hardcoded.
|
|
31
|
+
export function apiUrl(): string {
|
|
32
|
+
// SSR: the client hooks only run in the browser, so a placeholder is fine.
|
|
33
|
+
if (import.meta.env.SSR) {
|
|
34
|
+
return import.meta.env.VITE_API_URL ?? '/__api'
|
|
35
|
+
}
|
|
36
|
+
return import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Never fall back to `http://localhost:3000`.** `import.meta.env.VITE_API_URL`
|
|
41
|
+
is substituted by Vite at _build_ time, so any deploy that supplies the URL as
|
|
42
|
+
a _runtime_ env var or platform binding leaves it `undefined` in the shipped
|
|
43
|
+
bundle — the fallback is then the only branch that ever runs in the browser. A
|
|
44
|
+
localhost fallback means every request from a deployed app goes to the user's
|
|
45
|
+
own machine. `origin + '/api'` is same-origin, needs no build-time knowledge of
|
|
46
|
+
the domain, and is correct wherever the app is served from.
|
|
47
|
+
|
|
48
|
+
For local dev, set `VITE_API_URL`, or proxy `/api` → your backend in
|
|
49
|
+
`vite.config.ts` under `server.proxy`. One `/api` entry also covers
|
|
50
|
+
`/api/auth/*`; only add more entries for root-level routes outside `/api`.
|
|
51
|
+
|
|
52
|
+
## Setup at the app root
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { createPikku, PikkuProvider } from '@pikku/react'
|
|
56
|
+
import { PikkuFetch } from './pikku/pikku-fetch.gen'
|
|
57
|
+
import { PikkuRPC } from './pikku/pikku-rpc.gen'
|
|
58
|
+
import { apiUrl } from './lib/env'
|
|
59
|
+
|
|
60
|
+
const pikku = createPikku(PikkuFetch, PikkuRPC, {
|
|
61
|
+
serverUrl: apiUrl(),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
createRoot(document.getElementById('root')!).render(
|
|
65
|
+
<PikkuProvider pikku={pikku}>
|
|
66
|
+
<App />
|
|
67
|
+
</PikkuProvider>
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the project also exposes realtime events (see **pikku-wiring**), pass
|
|
72
|
+
the `PikkuRealtime` class as the third argument and the instance gets a
|
|
73
|
+
`realtime` field too:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { PikkuRealtime } from './pikku/realtime.gen'
|
|
77
|
+
|
|
78
|
+
const pikku = createPikku(PikkuFetch, PikkuRPC, PikkuRealtime, {
|
|
79
|
+
serverUrl: apiUrl(),
|
|
80
|
+
})
|
|
81
|
+
// pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch
|
|
82
|
+
// (server URL + auth configured once).
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The generated classes come from your `pikku.config.json`:
|
|
86
|
+
|
|
87
|
+
| config field | generated file |
|
|
88
|
+
| ---------------------------- | ----------------------------------------------------- |
|
|
89
|
+
| `clientFiles.fetchFile` | typed HTTP client (`PikkuFetch` class) |
|
|
90
|
+
| `clientFiles.rpcWiringsFile` | RPC client (`PikkuRPC` class) calling all exposed fns |
|
|
91
|
+
| `clientFiles.realtimeFile` | `PikkuRealtime` (websocket events + SSE + channels) |
|
|
92
|
+
|
|
93
|
+
If a file isn't being generated, that field is missing from the config —
|
|
94
|
+
add it and re-run `pikku all`.
|
|
95
|
+
|
|
96
|
+
`createPikku(...)` accepts the same `CorePikkuFetchOptions` as `PikkuFetch`
|
|
97
|
+
plus `serverUrl`. Auth headers, request interceptors, etc. are configured
|
|
98
|
+
on the fetch instance — RPC and realtime inherit them automatically.
|
|
99
|
+
|
|
100
|
+
## Calling an RPC directly (no React Query)
|
|
101
|
+
|
|
102
|
+
Inside a component:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { usePikkuRPC } from '@pikku/react'
|
|
106
|
+
|
|
107
|
+
function Logout() {
|
|
108
|
+
const rpc = usePikkuRPC()
|
|
109
|
+
return <button onClick={() => rpc.invoke('logoutUser', {})}>Sign out</button>
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`rpc.invoke(name, data)` is typed against `FlattenedRPCMap` — `name` must
|
|
114
|
+
be an exposed function id, `data` matches the input schema, return value
|
|
115
|
+
matches the output schema.
|
|
116
|
+
|
|
117
|
+
You also have `rpc.<funcName>(data)` if the generated RPC client builds
|
|
118
|
+
direct methods (project-dependent).
|
|
119
|
+
|
|
120
|
+
## Calling fetch directly
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
const fetch = usePikkuFetch()
|
|
124
|
+
const data = await fetch.get('/some-rest-route', { searchParams: {...} })
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Use this only when the function is wired via HTTP (REST shape) and you
|
|
128
|
+
need a path-style call. For RPC calls, `usePikkuRPC()` is cleaner.
|
|
129
|
+
|
|
130
|
+
## Realtime subscriptions
|
|
131
|
+
|
|
132
|
+
If you wired a `PikkuRealtime` class into `createPikku`, use
|
|
133
|
+
`usePikkuRealtime()` to grab the shared instance:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { usePikkuRealtime } from '@pikku/react'
|
|
137
|
+
import type { PikkuRealtime } from './pikku/realtime.gen'
|
|
138
|
+
|
|
139
|
+
function TodoList() {
|
|
140
|
+
const realtime = usePikkuRealtime<PikkuRealtime>()
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
return realtime.subscribe('todo-created', ({ todo }) => {
|
|
143
|
+
/* ... */
|
|
144
|
+
})
|
|
145
|
+
}, [realtime])
|
|
146
|
+
// ...
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The hook throws if no `PikkuRealtime` was wired — that's how you know to
|
|
151
|
+
add it to `createPikku(...)`. Full event-hub setup, publishing, and SSE
|
|
152
|
+
helpers live in **pikku-wiring**.
|
|
153
|
+
|
|
154
|
+
## When to reach for what
|
|
155
|
+
|
|
156
|
+
| Need | Use |
|
|
157
|
+
| ----------------------------------- | -------------------------------------------------- |
|
|
158
|
+
| Render data, dedupe + cache | **usePikkuQuery** (react-query) |
|
|
159
|
+
| Trigger a write, wait for result | **usePikkuMutation** (react-query) |
|
|
160
|
+
| Paginate | **usePikkuInfiniteQuery** (react-query) |
|
|
161
|
+
| One-off call from an event handler | `usePikkuRPC()` direct |
|
|
162
|
+
| Hit a REST endpoint (not RPC) | `usePikkuFetch()` |
|
|
163
|
+
| Run one named workflow | `usePikkuWorkflow('name')` → `.start/.run/.status` |
|
|
164
|
+
| Talk to one named AI agent | `usePikkuAgent('name')` → `.run/.stream/.approve` |
|
|
165
|
+
| Longer-running workflow UX | `references/workflows.md` |
|
|
166
|
+
| Subscribe to events / SSE / channel | `usePikkuRealtime()` (see **pikku-wiring**) |
|
|
167
|
+
|
|
168
|
+
The first three live in your generated `api.gen.ts` (see the
|
|
169
|
+
`references/react-query.md`). This reference covers the rest.
|
|
170
|
+
|
|
171
|
+
`usePikkuAgent` and `usePikkuWorkflow` bind the name once and hand back the
|
|
172
|
+
call methods with it already applied:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
const agent = usePikkuAgent('todo-agent')
|
|
176
|
+
const { text } = await agent.run({ message, threadId })
|
|
177
|
+
|
|
178
|
+
const workflow = usePikkuWorkflow('onboardUser')
|
|
179
|
+
const { runId } = await workflow.start({ email })
|
|
180
|
+
const state = await workflow.status(runId)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Authentication
|
|
184
|
+
|
|
185
|
+
Auth is handled at the `PikkuFetch` layer, and `createPikku`'s options object
|
|
186
|
+
_is_ `CorePikkuFetchOptions` plus `serverUrl` — flat, not nested under a
|
|
187
|
+
`fetchOptions` key:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
const pikku = createPikku(PikkuFetch, PikkuRPC, {
|
|
191
|
+
serverUrl: apiUrl(),
|
|
192
|
+
credentials: 'include', // cookie sessions
|
|
193
|
+
authHeaders: { jwt: token }, // or { apiKey }
|
|
194
|
+
transformDate: true,
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
There is no request-interceptor hook. For a token that changes after startup,
|
|
199
|
+
call the setter on the shared instance — RPC and realtime pick it up because
|
|
200
|
+
they hold the same fetch:
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
pikku.fetch.setAuthorizationJWT(token) // null clears it
|
|
204
|
+
pikku.fetch.setAPIKey(key)
|
|
205
|
+
pikku.fetch.setHeader('x-tenant', tenantId)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`authHeaders.jwt` becomes `Authorization: Bearer …` and `authHeaders.apiKey`
|
|
209
|
+
becomes `X-API-KEY`; setting a JWT takes precedence over an API key.
|
|
210
|
+
|
|
211
|
+
### Dev actor sign-in (`useDevActors`)
|
|
212
|
+
|
|
213
|
+
The dev-only "Sign in as …" control: one click signs in as a declared scenario
|
|
214
|
+
persona with no password, so the app can be reviewed as each kind of user.
|
|
215
|
+
`pikku fabric validate` **requires** any frontend with a login screen to ship one
|
|
216
|
+
(`app-missing-actor-quick-login-<app>`) — without it a reviewer is locked out of
|
|
217
|
+
their own sandbox.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
import { useDevActors } from '@pikku/react'
|
|
221
|
+
|
|
222
|
+
const { actors, signInAs, pendingEmail, isPending, error } = useDevActors({
|
|
223
|
+
// Gate both reads on the bundler's dev flag so no credential can reach a
|
|
224
|
+
// production bundle. The sandbox dev server bakes them from your personas.
|
|
225
|
+
actors: import.meta.env.DEV ? import.meta.env.VITE_DEV_ACTORS : undefined,
|
|
226
|
+
secrets: import.meta.env.DEV
|
|
227
|
+
? import.meta.env.VITE_DEV_ACTOR_SECRETS
|
|
228
|
+
: undefined,
|
|
229
|
+
apiUrl: apiUrl(),
|
|
230
|
+
onSignedIn: () => navigate({ to: '/' }),
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- **It is UI-free**, so render it however you like. For the default rendering use
|
|
235
|
+
`<DevActorSwitcher />` from `@pikku/mantine/dev` — a separate entry point from
|
|
236
|
+
`@pikku/mantine/core`, whose contract is "drop-in alias for `@mantine/core`"
|
|
237
|
+
and so must not export components Mantine has no counterpart for.
|
|
238
|
+
- **`secrets` is `{ address: credential }`, not one shared value** — a
|
|
239
|
+
credential opens the one persona it was minted for (see
|
|
240
|
+
**pikku-auth**). `actors` is empty unless the host supplied both a list
|
|
241
|
+
and the credentials for it, and an actor with no credential is not offered, so
|
|
242
|
+
a production build renders nothing without you testing for it.
|
|
243
|
+
- **It takes `onSignedIn` rather than a router**, and takes the env values rather
|
|
244
|
+
than reading them, because how env is spelled is a bundler fact
|
|
245
|
+
(`import.meta.env.VITE_*` vs `process.env.NEXT_PUBLIC_*`).
|
|
246
|
+
- The underlying `signInAsActor()` and `parseDevActors()` are exported too, for a
|
|
247
|
+
non-React caller. The endpoint only accepts rows flagged `actor: true`, so it
|
|
248
|
+
can never impersonate a real user — see **pikku-auth**.
|
|
249
|
+
|
|
250
|
+
Do not hand-write the `devActors()` / `signInAsActor()` pair per app; that
|
|
251
|
+
copy-paste, including the `import.meta.env.DEV` gate, is exactly what this
|
|
252
|
+
replaced.
|
|
253
|
+
|
|
254
|
+
### Linking from a Mantine element: `renderRoot`, not `component`
|
|
255
|
+
|
|
256
|
+
Handing TanStack's `Link` to a Mantine element as `component={Link}` compiles,
|
|
257
|
+
renders, and navigates — and silently unties the type. Mantine's polymorphic
|
|
258
|
+
`component` prop widens the router generic to `AnyRouter`, so `to` and
|
|
259
|
+
`params` stop being checked against your actual routes. Renaming a route then
|
|
260
|
+
breaks the running app instead of the build, which is the one thing the typed
|
|
261
|
+
router exists to prevent.
|
|
262
|
+
|
|
263
|
+
Wrap the typed `Link` once and reach it through `renderRoot`, which passes the
|
|
264
|
+
props through without re-typing the element:
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
// components/links.tsx — one wrapper the whole app links through
|
|
268
|
+
import { Link } from '@tanstack/react-router'
|
|
269
|
+
|
|
270
|
+
export const AssessmentLink = (props: { assessmentId: string; children: React.ReactNode }) => (
|
|
271
|
+
<Link to="/assessments/$assessmentId" params={{ assessmentId: props.assessmentId }}>
|
|
272
|
+
{props.children}
|
|
273
|
+
</Link>
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
<Button renderRoot={(p) => <AssessmentLink assessmentId={id} {...p} />}>
|
|
279
|
+
Open
|
|
280
|
+
</Button>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The wrapper is where `to` and `params` are checked, and it is checked once.
|
|
284
|
+
|
|
285
|
+
## What NOT to do
|
|
286
|
+
|
|
287
|
+
- Don't instantiate `PikkuFetch`/`PikkuRPC` inside a component — `createPikku`
|
|
288
|
+
goes once at the app root, the instance flows through Context.
|
|
289
|
+
- Don't call `usePikkuRPC()` outside a `<PikkuProvider>` — it throws.
|
|
290
|
+
- Don't write a custom RPC client. The generated one already covers every
|
|
291
|
+
exposed function with full types.
|
|
292
|
+
- Don't hardcode user-facing strings. Every display string goes through an
|
|
293
|
+
i18n token — see **pikku-i18n** for the setup (it's English-only by default).
|