@pikku/skills 0.12.10 → 0.12.12

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +819 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +4 -4
  4. package/skills/pikku-addon/SKILL.md +17 -11
  5. package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +4 -5
  7. package/skills/pikku-audit/SKILL.md +28 -13
  8. package/skills/pikku-aws/SKILL.md +2 -2
  9. package/skills/pikku-better-auth/SKILL.md +97 -17
  10. package/skills/pikku-build-app/SKILL.md +621 -0
  11. package/skills/pikku-build-app/references/multi-app.md +117 -0
  12. package/skills/pikku-build-app/references/ship.md +98 -0
  13. package/skills/pikku-build-app/references/theming.md +70 -0
  14. package/skills/pikku-build-platform/SKILL.md +239 -0
  15. package/skills/pikku-build-quick/SKILL.md +238 -0
  16. package/skills/pikku-cli/SKILL.md +7 -7
  17. package/skills/pikku-cli/references/complete-example.md +1 -1
  18. package/skills/pikku-concepts/SKILL.md +5 -2
  19. package/skills/pikku-concepts/references/concept-mapping.md +1 -1
  20. package/skills/pikku-config/SKILL.md +5 -3
  21. package/skills/pikku-deploy-azure/SKILL.md +5 -4
  22. package/skills/pikku-emails/SKILL.md +28 -7
  23. package/skills/pikku-fabric/SKILL.md +27 -3
  24. package/skills/pikku-fabric-debug/SKILL.md +1 -1
  25. package/skills/pikku-feature/SKILL.md +5 -4
  26. package/skills/pikku-http/SKILL.md +4 -4
  27. package/skills/pikku-http/references/http-options.md +13 -13
  28. package/skills/pikku-i18n/SKILL.md +2 -1
  29. package/skills/pikku-info/SKILL.md +1 -1
  30. package/skills/pikku-knowledge/SKILL.md +13 -13
  31. package/skills/pikku-mcp/SKILL.md +4 -4
  32. package/skills/pikku-middleware/SKILL.md +5 -5
  33. package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
  34. package/skills/pikku-n8n-import/SKILL.md +12 -12
  35. package/skills/pikku-n8n-import/SPEC.md +3 -0
  36. package/skills/pikku-n8n-import/references/code-translation.md +26 -22
  37. package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
  38. package/skills/pikku-paraglide/SKILL.md +11 -6
  39. package/skills/pikku-permissions/SKILL.md +19 -15
  40. package/skills/pikku-product-second-opinion/README.md +3 -3
  41. package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
  42. package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
  43. package/skills/pikku-queue/SKILL.md +1 -1
  44. package/skills/pikku-react/SKILL.md +53 -13
  45. package/skills/pikku-realtime/SKILL.md +52 -21
  46. package/skills/pikku-rpc/SKILL.md +4 -2
  47. package/skills/pikku-rtl/SKILL.md +1 -1
  48. package/skills/pikku-scenario/SKILL.md +131 -20
  49. package/skills/pikku-schedule/SKILL.md +6 -1
  50. package/skills/pikku-schema-ajv/SKILL.md +2 -2
  51. package/skills/pikku-schema-cfworker/SKILL.md +1 -1
  52. package/skills/pikku-security/SKILL.md +9 -5
  53. package/skills/pikku-services/SKILL.md +27 -18
  54. package/skills/pikku-services/references/audit-wire-service.md +14 -8
  55. package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
  56. package/skills/pikku-tag-middleware/SKILL.md +1 -0
  57. package/skills/pikku-template-clone/SKILL.md +2 -1
  58. package/skills/pikku-trigger/SKILL.md +3 -3
  59. package/skills/pikku-websocket/SKILL.md +4 -3
  60. package/skills/pikku-workflow/SKILL.md +2 -2
  61. package/skills/pikku-workflow/references/workflow-reference.md +24 -10
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: pikku-build-quick
3
+ description: >-
4
+ Build a working app on open-source Pikku fast — scaffold to running screens, skipping the
5
+ knowledge base and the milestone ladder. For spikes, throwaway demos, and ideas nobody has
6
+ committed to yet. TRIGGER when: the user asked for something quick, a prototype, a spike, a
7
+ demo of an idea, or "just get it running", or picked "Quick" from the build-mode question. DO
8
+ NOT TRIGGER when: the request is an unqualified "build me an X on Pikku" with no signal of
9
+ speed or throwaway-ness — App is the default and small or toy-sounding apps do not change that
10
+ (use pikku-build-app); the user wants a real product someone else will pick up (use
11
+ pikku-build-app); the user wants a demo of Pikku itself — one that shows off surfaces like
12
+ workflows, queues, realtime or i18n (use pikku-build-platform); or the user is adding a feature
13
+ to an app that already exists rather than building one from a fresh scaffold (use
14
+ pikku-feature).
15
+ installGroups: [core]
16
+ ---
17
+
18
+ # Build an app on Pikku, fast
19
+
20
+ You have a scaffolded project with skills installed. Get it to working, seeded,
21
+ signed-in screens in as few steps as possible.
22
+
23
+ **What this mode deliberately skips**, and what that costs:
24
+
25
+ | Skipped | Cost |
26
+ |---|---|
27
+ | `knowledge/` | Another agent — or you next week — cannot resume this. Nothing records *why*. |
28
+ | Milestone planning | No build order, no per-piece proof. Fine at this size, painful past it. |
29
+ | Design direction | It will look like the template. |
30
+ | Refusal scenarios | Access control is asserted, not proven. |
31
+
32
+ **Say this out loud to the user, once, when you finish.** A quick build that gets
33
+ mistaken for a real one is the only way this mode does damage. §6 is the way out.
34
+
35
+ ## Agent Operating Procedure
36
+
37
+ 1. Read `AGENTS.md` at the project root before your first screen — routing slots,
38
+ `useNavItems()`, and the shipped component kit.
39
+ 2. Keep generated files generated. Never hand-edit `.pikku/`, `*.gen.*`, or the SDK.
40
+ 3. Run `pikku all` after touching functions, wirings or schemas. It is the gate,
41
+ and its criticals are real.
42
+
43
+ ## 1. One question, then build
44
+
45
+ Ask **one** thing, and only if the original request left it open: **who uses
46
+ it — one kind of person, or several?** Everything else you decide yourself.
47
+
48
+ - **One kind** — no roles to declare. The rule is ownership: you see yours, not
49
+ theirs.
50
+ - **Several** — declare a role each in §2 and keep the count honest. An invented
51
+ role becomes invented screens.
52
+
53
+ Do not ask about design, deployment, or scope. This is the quick mode; the
54
+ defaults are the point.
55
+
56
+ ## 2. Personas — 60 seconds, not optional
57
+
58
+ `packages/functions/src/personas.ts` ships with a `visitor`. Add one persona per
59
+ kind of person, plus **a second one of the primary kind** — that is what makes
60
+ "you see yours, not theirs" observable when you click around.
61
+
62
+ **One kind of person** — no `defineSystemRole` at all. Ownership is the only
63
+ rule, and it lives in each function's `permissions`, not in a role:
64
+
65
+ ```typescript
66
+ import { definePersonas } from '#pikku/scopes/pikku-personas.gen.js'
67
+
68
+ definePersonas({
69
+ visitor: { name: 'Visitor', jobTitle: 'Synthetic health-check user', account: {} },
70
+ amina: { name: 'Amina', jobTitle: 'Gardener', account: {} },
71
+ bilal: { name: 'Bilal', jobTitle: 'Gardener', account: {} },
72
+ })
73
+ ```
74
+
75
+ **Several kinds** — one role each, and only for the kinds the user actually
76
+ named:
77
+
78
+ ```typescript
79
+ import { definePersonas } from '#pikku/scopes/pikku-personas.gen.js'
80
+ import { defineSystemRole } from '#pikku'
81
+
82
+ defineSystemRole({
83
+ owner: { displayName: 'Owner', description: 'Sees only their own rows', scopes: [] },
84
+ tenant: { displayName: 'Tenant', description: 'Sees only their own tenancy', scopes: [] },
85
+ })
86
+
87
+ definePersonas({
88
+ visitor: { name: 'Visitor', jobTitle: 'Synthetic health-check user', account: {} },
89
+ amina: { name: 'Amina', jobTitle: 'Owner', roles: ['owner'], account: {} },
90
+ bilal: { name: 'Bilal', jobTitle: 'Owner', roles: ['owner'], account: {} },
91
+ chidi: { name: 'Chidi', jobTitle: 'Tenant', roles: ['tenant'], account: {} },
92
+ })
93
+ ```
94
+
95
+ Two owners in both examples, deliberately: one owner cannot demonstrate that
96
+ owners are separated from each other.
97
+
98
+ - **Keep `visitor`.** The shipped scenarios name `actors.visitor`; removing it
99
+ fails `pikku all` and nothing you write after that registers.
100
+ - **One `definePersonas` call for the whole project.**
101
+ - **Never write an email address** — each is derived from the persona id and
102
+ `scenarios.emailDomain` in `pikku.config.json`.
103
+ - `roles` is typechecked against `defineSystemRole`; an undeclared role is a
104
+ build error.
105
+
106
+ ## 3. Build
107
+
108
+ Run `bunx --bun pikku bootstrap` once first. It wires the `#pikku` import alias
109
+ codegen depends on; without it your first `db migrate` fails with
110
+ `Cannot find package '#pikku'`.
111
+
112
+ Then, in this order — it is the order codegen depends on:
113
+
114
+ 1. **Migration** — SQL in `db/sqlite/`, numbered on from what is there. Apply
115
+ with `bunx --bun pikku db migrate`, which regenerates the Kysely types.
116
+ 2. **Seed** — rows in `db/sqlite-dev-seed.sql`. There is no seed command:
117
+ `bunx --bun pikku db reset` wipes, migrates and seeds in one go, and is the
118
+ only thing that applies the file. It always starts from a wiped database, so
119
+ the file is plain `INSERT`s — no `ON CONFLICT DO NOTHING`. **Be generous, and
120
+ seed rows for both personas.** An empty app demos badly, and you cannot see a
121
+ layout break against zero rows.
122
+ 3. **Functions** — one `pikkuFunc` per `*.function.ts`, `expose: true`. Pikku
123
+ generates the typed RPC client and React Query hooks; you do NOT write HTTP
124
+ routes. `wireHTTP` only for a real REST shape (a third-party webhook).
125
+ 4. `bunx --bun pikku all`
126
+ 5. **UI** — pages in `apps/app/src/pages/`, one route file each in
127
+ `apps/app/src/routes/`, calling `usePikkuQuery` / `usePikkuMutation` from
128
+ `@project/functions-sdk/pikku/api.gen`. Compose `@/components/<Name>` —
129
+ `PageHeader`, `Panel`, `StatGrid`, `DataTable` — rather than hand-rolling.
130
+ Register each screen in `useNavItems()`; that one file feeds the desktop
131
+ sidebar and the phone navigation.
132
+
133
+ **Aim for two or three real entities and three screens** — a working surface, a
134
+ detail view, and somewhere to land. One table with a form on it is not an app,
135
+ and it is not faster to build.
136
+
137
+ Rules that stay non-negotiable even here, because breaking them costs more time
138
+ than they save:
139
+
140
+ - Input and output types come from `input:`/`output:` zod schemas. Never generic
141
+ type params, never an inline return type. The schema is the type.
142
+ - Permission checks go in the `permissions` field, never the function body. An
143
+ exposed function with no session and no permission is reachable by anyone over
144
+ `POST /rpc/:rpcName` (PKU574).
145
+ - No `process.env` inside a function — use the injected `variables` / `secrets`
146
+ services.
147
+ - A `z.date()` **input** arrives over RPC as an ISO string, not a `Date`.
148
+ `new Date(value)` before calling date methods, or it throws
149
+ `.getTime is not a function` at runtime.
150
+ - On SQLite, `db/annotations.ts` is where a `DATETIME` becomes a `Date` and a
151
+ `JSON` column becomes a typed object. Without an entry they are `string` and
152
+ `unknown` (PKU481). Add the annotation rather than casting.
153
+ - Surface errors inline next to the control that failed. No empty catch.
154
+ - Every user-facing string is a translation key, not a literal. It is one extra
155
+ keystroke now and a rewrite later.
156
+ - Never hardcode a host or port — the API base resolves to same-origin `/api`.
157
+
158
+ Then run it:
159
+
160
+ ```sh
161
+ bun run prebuild && bun run dev
162
+ ```
163
+
164
+ API on :3000, app on the port vite prints. A frontend against a dead API looks
165
+ exactly like an app bug, so if every request fails, check both came up.
166
+
167
+ ## 4. Look at it — actually
168
+
169
+ Sign up, click every screen. **HTTP 200 is not evidence:** pages are
170
+ client-rendered, so the server returns 200 with an empty shell and a page whose
171
+ component throws still looks fine to `curl`. Open a browser, or drive it
172
+ headlessly and assert on rendered text.
173
+
174
+ **Screenshot at 390px too.** A layout that is fine at 1440 routinely breaks on a
175
+ phone — an overflowing table, a row of buttons wrapped into a pile, a modal
176
+ taller than the viewport. It is the most likely width your demo gets opened at.
177
+
178
+ If you have five spare minutes, `npx impeccable install` (Node 22.18+) scores
179
+ each screen against interaction heuristics and names what is wrong. Feed it
180
+ screenshots, not source. It will polish the default look; it will not give the
181
+ app a look — that is `pikku-build-app` §8a.
182
+
183
+ ## 5. One smoke scenario
184
+
185
+ Not the full ladder — one journey, end to end, as a real persona, so the app has
186
+ at least one thing that stays true.
187
+
188
+ ```typescript
189
+ import { pikkuScenario } from '#pikku/workflow/pikku-workflow-types.gen.js'
190
+
191
+ export const ownerCreatesAndSeesItScenario = pikkuScenario<void, { id: string }>({
192
+ title: 'An owner creates a thing and sees it',
193
+ tags: ['scenario', 'smoke'],
194
+ func: async (_services, _data, { scenario, actors }) => {
195
+ const row = await scenario.do('creates', 'createThing', { name: 'first' }, { actor: actors.amina })
196
+ await scenario.then('sees it listed', 'thingShowsInList', { id: row.id }, { actor: actors.amina })
197
+ return { id: row.id }
198
+ },
199
+ })
200
+ ```
201
+
202
+ - **`do` takes an RPC name; `given`/`when`/`then` take a declared
203
+ `pikkuScenarioStep`.** An RPC name in a `then` will not resolve.
204
+ - **Every scenario must assert.** A ladder with no `then` is a PKU680 critical —
205
+ it fails `pikku all`, stopping codegen rather than a test.
206
+ - **Add `SCENARIO_ACTOR_SECRET` to `.env`.** `bun run dev` writes that file with
207
+ only a `BETTER_AUTH_SECRET`; without the actor secret
208
+ `/api/auth/sign-in/actor` is disabled and every scenario fails at sign-in, for
209
+ a reason that reads like an auth bug.
210
+ - **There is no state reset** — scope what you create to unique ids.
211
+
212
+ Keep the three shipped scenarios in `packages/functions/test/scenarios/` green.
213
+
214
+ ```sh
215
+ bunx --bun pikku scenario run local --spawn
216
+ ```
217
+
218
+ ## 6. Hand it over honestly
219
+
220
+ Tell the user, in one short paragraph: what runs, what it is seeded with, and
221
+ that this is a quick build — no knowledge base, no milestones, no design pass,
222
+ access control clicked-through rather than proven.
223
+
224
+ **Upgrading to a real build is additive, not a rewrite.** If they want it, switch
225
+ to `pikku-build-app` and do this, in order:
226
+
227
+ 1. Write `knowledge/` for what already exists — `entities/` for what you built,
228
+ `decisions/` for what you chose silently, `questions/` for what you guessed
229
+ at. Then `pikku knowledge index && pikku knowledge validate`.
230
+ 2. Backfill a milestone note per screen you built, at `status: built`, each with
231
+ its gherkin block.
232
+ 3. Write the refusal scenarios — the ones proving one persona cannot reach
233
+ another's rows. This is the gap that matters most.
234
+ 4. Then pick up `pikku-build-app` at its §4 (apps) or §5 (milestones) for
235
+ anything new.
236
+
237
+ Nothing built here has to be thrown away to do that — which is the whole reason
238
+ this mode is allowed to skip those steps in the first place.
@@ -41,7 +41,7 @@ All three factories come from `#pikku` (the generated types re-export
41
41
  loses your project's service and middleware types.
42
42
 
43
43
  ```typescript
44
- import { wireCLI } from '#pikku'
44
+ import { wireCLI } from '#pikku/cli'
45
45
 
46
46
  wireCLI({
47
47
  program: string, // Program name (e.g. 'todos')
@@ -65,7 +65,7 @@ wireCLI({
65
65
  ### `pikkuCLICommand(config)`
66
66
 
67
67
  ```typescript
68
- import { pikkuCLICommand } from '#pikku'
68
+ import { pikkuCLICommand } from '#pikku/cli'
69
69
 
70
70
  pikkuCLICommand({
71
71
  parameters?: string, // Positional args (e.g. '<text>', '<username> <email>')
@@ -111,7 +111,7 @@ How the parser reads them, which is worth knowing before you name one:
111
111
  ### `pikkuCLIRender(fn)`
112
112
 
113
113
  ```typescript
114
- import { pikkuCLIRender } from '#pikku'
114
+ import { pikkuCLIRender } from '#pikku/cli'
115
115
 
116
116
  const renderer = pikkuCLIRender<OutputType>((services, data) => {
117
117
  // Format and print output to terminal
@@ -122,10 +122,10 @@ const renderer = pikkuCLIRender<OutputType>((services, data) => {
122
122
  ### Wire object (`wire.cli`)
123
123
 
124
124
  ```typescript
125
- wire.cli.program // program name
126
- wire.cli.command // string[] — the resolved command path
127
- wire.cli.data // all positionals and options, merged
128
- wire.cli.channel // the channel when served remotely (see below)
125
+ wire.cli.program // program name
126
+ wire.cli.command // string[] — the resolved command path
127
+ wire.cli.data // all positionals and options, merged
128
+ wire.cli.channel // the channel when served remotely (see below)
129
129
  ```
130
130
 
131
131
  ## Usage Patterns
@@ -32,7 +32,7 @@ export const deleteUser = pikkuFunc({
32
32
  })
33
33
 
34
34
  // wirings/cli.wiring.ts
35
- import { wireCLI, pikkuCLICommand, pikkuCLIRender } from '#pikku'
35
+ import { wireCLI, pikkuCLICommand, pikkuCLIRender } from '#pikku/cli'
36
36
 
37
37
  const userRenderer = pikkuCLIRender<{ user: User }>((_services, { user }) => {
38
38
  console.log(`Created user: ${user.username} (${user.email}) [${user.role}]`)
@@ -236,7 +236,9 @@ await server.start()
236
236
 
237
237
  Run `npx pikku all` to generate:
238
238
 
239
- - `pikku-types.gen.ts` — Typed function factories and wiring functions
239
+ - one directory per wiring (`function/`, `http/`, `workflow/`, …), each with an
240
+ `index.ts` reached as `#pikku/<name>` — typed function factories and wiring
241
+ functions, split so an app pulls in only the wirings it uses
240
242
  - `pikku-fetch.gen.ts` — Type-safe HTTP client
241
243
  - `pikku-websocket.gen.ts` — Type-safe WebSocket client
242
244
  - `pikku-bootstrap.gen.ts` — Runtime initialization (auto-imports all wirings)
@@ -271,7 +273,8 @@ src/
271
273
  ├── middleware.ts # Middleware definitions (see pikku-security)
272
274
  ├── permissions.ts # Permission definitions (see pikku-security)
273
275
  └── .pikku/ # Generated (gitignored)
274
- ├── pikku-types.gen.ts
276
+ ├── function/ # #pikku/function
277
+ ├── http/ # #pikku/http
275
278
  ├── pikku-fetch.gen.ts
276
279
  └── pikku-bootstrap.gen.ts
277
280
  ```
@@ -472,7 +472,7 @@ if (!canEdit(user, todo)) {
472
472
  **Pikku:**
473
473
 
474
474
  ```typescript
475
- import { NotFoundError, ForbiddenError } from '@pikku/core/errors'
475
+ import { NotFoundError, ForbiddenError } from '#pikku/error'
476
476
 
477
477
  const updateTodo = pikkuFunc(async (services, { id, title }, wire) => {
478
478
  const todo = services.todoStore.getTodo(id)
@@ -75,7 +75,9 @@ greppable. Call it at the point the value reaches the thing that needs it:
75
75
  ```typescript
76
76
  // services.ts — allowed
77
77
  const createSingletonServices = pikkuServices(async (config, { secrets }) => ({
78
- stripe: new StripeService((await secrets.getSecret('STRIPE_CONFIG')).reveal()),
78
+ stripe: new StripeService(
79
+ (await secrets.getSecret('STRIPE_CONFIG')).reveal()
80
+ ),
79
81
  }))
80
82
 
81
83
  // functions/*.ts — ask the service, never the secret store
@@ -162,7 +164,7 @@ defineCredential({
162
164
 
163
165
  ### Usage
164
166
 
165
- ```typescript
167
+ ````typescript
166
168
  // Per-user API key — no oauth2 block
167
169
  defineCredential({
168
170
  name: 'stripe',
@@ -208,7 +210,7 @@ export const createWireServices = pikkuWireServices(async (_services, wire) => {
208
210
  export const postMessage = pikkuFunc({
209
211
  func: async ({ slack }, { channel, text }) => slack.postMessage(channel, text),
210
212
  })
211
- ```
213
+ ````
212
214
 
213
215
  A `wire` credential resolves per user, so an unconnected user hits
214
216
  `MissingCredentialError` rather than silently acting as someone else; a
@@ -59,10 +59,11 @@ import { createAzureHandler } from '@pikku/azure-functions'
59
59
  import { createConfig, createSingletonServices } from './services.js'
60
60
  import './.pikku/pikku-bootstrap.gen.js'
61
61
 
62
- const handlers = createAzureHandler(
63
- { createConfig, createSingletonServices },
64
- ['fetch', 'queue', 'scheduled']
65
- )
62
+ const handlers = createAzureHandler({ createConfig, createSingletonServices }, [
63
+ 'fetch',
64
+ 'queue',
65
+ 'scheduled',
66
+ ])
66
67
 
67
68
  app.http('api', {
68
69
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
@@ -36,8 +36,8 @@ generated output is never edited by hand.
36
36
  ```jsonc
37
37
  // pikku.config.json
38
38
  {
39
- "emailTemplatesDir": "emails", // relative to rootDir; omit to disable emails
40
- "outDir": ".pikku" // gen lands in <outDir>/email/
39
+ "emailTemplatesDir": "emails", // relative to rootDir; omit to disable emails
40
+ "outDir": ".pikku", // gen lands in <outDir>/email/
41
41
  }
42
42
  ```
43
43
 
@@ -82,10 +82,31 @@ Placeholders are `{{ ... }}`. Resolution order inside a template:
82
82
  - `{{> footer}}` — include a partial from `partials/`.
83
83
  - `{{content}}` / `{{subject}}` — only meaningful inside `partials/layout.html`
84
84
  (the rendered body and subject). `layout.html` wraps every template if present.
85
+ - `{{{verifyUrl}}}` — the same value **unescaped**. See below.
85
86
 
86
87
  Locale strings may themselves contain variables and partial-free placeholders, e.g.
87
- `"subject": "{{inviterName}} invited you to join {{organizationName}}"`. These are
88
- resolved in the same pass, so a subject of `{{t.invitation.subject}}` expands fully.
88
+ `"subject": "{{inviterName}} invited you to join {{organizationName}}"`. Locale files
89
+ and `theme.json` ship alongside the templates, so they are expanded first and a subject
90
+ of `{{t.invitation.subject}}` expands fully.
91
+
92
+ ## Escaping
93
+
94
+ Values are HTML-escaped (`& < > " '`) on the way into `.html` output, so a URL, a
95
+ display name or a font stack containing quotes lands inside its attribute instead of
96
+ breaking out of it. `.subject.txt` and `.text.txt` are plain text and are never escaped.
97
+
98
+ Rendering is **layered by trust**, and the layers do not leak into each other:
99
+
100
+ - Partials are inlined first — a `data` value that happens to contain `{{> footer}}`
101
+ is not an include.
102
+ - `theme.*` and `t.*` are template-author input: expanded next, escaped, and allowed
103
+ to contain further placeholders (up to 5 levels).
104
+ - Everything else is caller data: substituted in **one pass**, escaped, and never
105
+ rescanned — a `data` value containing `{{...}}` renders as those literal characters.
106
+
107
+ `{{content}}` and partials are template-authored markup and stay raw. For a value you
108
+ genuinely want inserted as markup, use the explicit `{{{value}}}` form — it is opt-in,
109
+ it bypasses escaping entirely, and it is only safe for HTML you control.
89
110
 
90
111
  ## Typed variables (per template)
91
112
 
@@ -121,9 +142,9 @@ render with sample data and read the result rather than trusting that it compile
121
142
 
122
143
  ```ts
123
144
  const rendered = renderEmailTemplate({
124
- name: 'verify-email', // EmailTemplateName (autocompleted)
125
- locale: 'en', // optional, defaults to 'en'
126
- data: { verifyUrl: url }, // EmailTemplateVariables<'verify-email'>
145
+ name: 'verify-email', // EmailTemplateName (autocompleted)
146
+ locale: 'en', // optional, defaults to 'en'
147
+ data: { verifyUrl: url }, // EmailTemplateVariables<'verify-email'>
127
148
  })
128
149
  // rendered: { name, locale, subject, html, text?, variables, hash }
129
150
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pikku-fabric
3
- description: 'Build and convert apps for the Pikku Fabric platform. Covers SQLite/libSQL database setup with Kysely, fabric project layout, deploy provider config, `fabric.config.json`, and the pikku-verify workflow. TRIGGER when: user is working on a Fabric-hosted Pikku project, converting an app to Fabric format, or asking about Fabric deployment, database, or project conventions. DO NOT TRIGGER when: user is working on a generic (non-Fabric) Pikku deployment — use pikku-deploy-cloudflare, pikku-deploy-fastify, etc. instead.'
3
+ description: 'Build and convert apps for the Pikku Fabric platform. Covers SQLite/libSQL database setup with Kysely, fabric project layout, deploy provider config, `fabric.config.json`, and the pikku-verify workflow. TRIGGER when: user is working on a Fabric-hosted Pikku project, converting an app to Fabric format, or asking about Fabric deployment, database, or project conventions. TRIGGER when: user asks about a `pikku fabric validate` finding, including app-missing-actor-quick-login. DO NOT TRIGGER when: user is working on a generic (non-Fabric) Pikku deployment — use pikku-deploy-cloudflare, pikku-deploy-fastify, etc. instead.'
4
4
  installGroups: [fabric]
5
5
  ---
6
6
 
@@ -115,7 +115,7 @@ and refuses a database outside the runtime directory. Anything a real environmen
115
115
  needs — accounts, role grants — is provisioning, not seeding, and belongs in
116
116
  `pikku persona sync` or a migration.
117
117
 
118
- A Better Auth app has a second constraint: the plugins you enable (`admin()`,
118
+ A Better Auth app has a second constraint: the plugins you enable (`ban()`,
119
119
  `actor()`, …) each declare columns, and `pikku db migrate` refuses to run while
120
120
  the applied schema is missing any of them. `pikku db generate` writes the
121
121
  migration that closes the gap.
@@ -311,13 +311,37 @@ Always call the `pikku-verify` tool after modifying functions, wirings, or schem
311
311
 
312
312
  The output card shows whether any breaking changes were detected.
313
313
 
314
+ ### `app-missing-actor-quick-login-<app>`
315
+
316
+ The `fabric validate` finding people most often misread. It fires when an app has
317
+ a **login screen** but no dev actor switcher, and it is not a style nit: a sandbox
318
+ reviewer has no seed password, so without the control they are locked out of the
319
+ app they were asked to look at.
320
+
321
+ Satisfy it with `<DevActorSwitcher />` from `@pikku/mantine/dev`, or with your
322
+ own UI built on `useDevActors()` from `@pikku/react` — validate accepts either
323
+ call site as evidence, so custom rendering passes. See **pikku-react** for the
324
+ props and **pikku-scenario** for where the actor list comes from.
325
+
326
+ The validator also accepts the shapes that predate the package — a hand-rolled
327
+ `signInAsActor()` or a literal `POST /auth/sign-in/actor` — so an older app does
328
+ not fail the build. **Treat that as a grace period, not the target: migrate those
329
+ to `<DevActorSwitcher />`.** The hand-copied version is exactly the duplication
330
+ the package exists to remove, and the copies drift — the ones that prompted this
331
+ had already diverged on the `import.meta.env.DEV` gate that keeps the shared
332
+ secret out of production bundles.
333
+
334
+ Do **not** satisfy it with Better Auth's `/dev/quick-login`. That is a different
335
+ endpoint with a different purpose — one fixed admin, not the declared personas —
336
+ and it does not clear this rule.
337
+
314
338
  ## Hard rules
315
339
 
316
340
  These apply in every Fabric app:
317
341
 
318
342
  - **No `process.env`** — use `variables.get('NAME')` and `secrets.getSecret('NAME')`. Declare with `defineVariable` / `defineSecret`.
319
343
  - **No `as any`** — fix types properly.
320
- - **No generic `Error`** — throw `NotFoundError`, `ConflictError`, `BadRequestError`, `UnauthorizedError` from `@pikku/core/errors`.
344
+ - **No generic `Error`** — throw `NotFoundError`, `ConflictError`, `BadRequestError`, `UnauthorizedError` from `#pikku/error`.
321
345
  - **No auth checks in function bodies** — use `permissions:` field on the function config with a `pikkuPermission` factory.
322
346
  - **No hand-editing `.pikku/db/schema.gen.ts`** — write a migration and re-run `pikku db migrate`.
323
347
  - **One runtime unit per file** — never define multiple functions/workflows in a single source file.
@@ -87,7 +87,7 @@ without it, even though the flag reads as optional.
87
87
  pikku fabric status # active + in-flight deployment, per stage, with gitSha
88
88
  ```
89
89
 
90
- Check this *before* deep-diving. A stage still serving an older `gitSha`, or a
90
+ Check this _before_ deep-diving. A stage still serving an older `gitSha`, or a
91
91
  deploy stuck in flight, explains a whole class of "my fix did nothing".
92
92
 
93
93
  ## Known gaps — do not misread these as bugs in your app
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pikku-feature
3
- description: 'Drive create-a-feature work for a Pikku project: discover project context, work on a feature branch, implement + verify + commit, and ask the user to review via the diff. TRIGGER when: the user asks to "create a feature", "build a todo app", "add X to my Pikku project", "wire up a new endpoint", or anything that implies turning a natural-language request into Pikku functions/wirings/migrations. DO NOT TRIGGER when: the user asks for a one-off code edit in an existing function, or asks about Pikku concepts (use pikku-concepts).'
3
+ description: 'Drive create-a-feature work inside a Pikku project that already exists: discover project context, work on a feature branch, implement + verify + commit, and ask the user to review via the diff. TRIGGER when: the user asks to "create a feature", "add X to my Pikku project", "wire up a new endpoint", or anything that implies turning a natural-language request into Pikku functions/wirings/migrations within a working app. DO NOT TRIGGER when: the user asks for a one-off code edit in an existing function, asks about Pikku concepts (use pikku-concepts), or is building a whole app from a fresh scaffold rather than extending one (use pikku-build-app, or pikku-build-quick / pikku-build-platform).'
4
4
  installGroups: [core]
5
5
  allowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku all *), Bash(yarn tsc), Bash(git status *), Bash(git diff *), Bash(git switch *), Bash(git checkout *), Bash(git checkout -b *), Bash(git add *), Bash(git commit *), Bash(git log *), Bash(git branch *)
6
6
  argument-hint: '<feature description>'
@@ -121,7 +121,7 @@ in-app features don't.
121
121
  queue-dispatched.
122
122
  - **Auth checks belong on the function or wiring**, not in function bodies.
123
123
  Use the `permissions` field with a `pikkuPermission` factory.
124
- - **Throw typed errors** from `@pikku/core/errors` — `NotFoundError`,
124
+ - **Throw typed errors** from `#pikku/error` — `NotFoundError`,
125
125
  `ConflictError`, `BadRequestError`. Never bare `Error`.
126
126
  - **Migrations are inline SQL files** in the project's migrations dir
127
127
  (typically `sql/`). Use a numbered prefix matching existing files.
@@ -141,8 +141,9 @@ Some patterns vary by project; **read a neighbour file before writing**:
141
141
  `CreateTodoOutput`) passed to `input`/`output` on the func config — vs
142
142
  generic-typed config. Schema name **must match codegen expectations** (the
143
143
  exported const name = the schema name in generated `.gen.json`).
144
- - **Imports**: usually `'#pikku'` for `pikkuFunc` / `pikkuSessionlessFunc`
145
- etc. Copy what neighbours do.
144
+ - **Imports**: `#pikku` is a namespace, not a module — one subpath per wiring.
145
+ `pikkuFunc` / `pikkuSessionlessFunc` come from `'#pikku/function'`, `wireHTTP`
146
+ from `'#pikku/http'`. Copy what neighbours do.
146
147
  - **Service usage**: e.g. `kysely`, `redis`. Look at how an existing function
147
148
  destructures services from its first arg. **Check `application-types.d.ts`**
148
149
  to see whether services like `kysely` are typed (`Kysely<DB>`) or untyped
@@ -38,7 +38,7 @@ Follow existing patterns you find (naming, tag usage, file organization). See `p
38
38
 
39
39
  ## API Reference
40
40
 
41
- All three come from `#pikku` (the generated `.pikku/pikku-types.gen.js`), which
41
+ All three come from `#pikku/http` (the generated `.pikku/http/index.ts`), which
42
42
  binds them to your project's service, session and middleware types. The
43
43
  `@pikku/core/http` versions are the unbound generics — they compile, but you
44
44
  lose the typing that makes the wiring worth having.
@@ -59,7 +59,7 @@ addHTTPMiddleware('*', [authBearer()]) // All routes
59
59
  addHTTPMiddleware('/api/*', [rateLimit()]) // Pattern match
60
60
  ```
61
61
 
62
- > HTTP-route-level permissions (`addHTTPPermission`, a `permissions` field on the wiring) were removed in #972. Declare authorization on the function definition (`pikkuFunc({ permissions })`, see `pikku-permissions`), or app-wide via `addGlobalPermission`. Tags/patterns are for *middleware* only now.
62
+ > HTTP-route-level permissions (`addHTTPPermission`, a `permissions` field on the wiring) were removed in #972. Declare authorization on the function definition (`pikkuFunc({ permissions })`, see `pikku-permissions`), or app-wide via `addGlobalPermission`. Tags/patterns are for _middleware_ only now.
63
63
 
64
64
  ## Data Flow
65
65
 
@@ -211,7 +211,7 @@ Functions live in their own files (one per file) and supply behavior + `permissi
211
211
 
212
212
  ```typescript
213
213
  // functions/books.functions.ts
214
- import { pikkuFunc, pikkuSessionlessFunc } from '#pikku'
214
+ import { pikkuFunc, pikkuSessionlessFunc } from '#pikku/function'
215
215
 
216
216
  export const listBooks = pikkuSessionlessFunc({
217
217
  title: 'List Books',
@@ -226,7 +226,7 @@ export const getBook = pikkuFunc({
226
226
  })
227
227
 
228
228
  // wirings/books.http.ts — same defineHTTPRoutes/wireHTTPRoutes shape as the Route Groups example above
229
- import { addHTTPMiddleware } from '#pikku'
229
+ import { addHTTPMiddleware } from '#pikku/http'
230
230
  import { cors, authBearer } from '@pikku/core/middleware'
231
231
 
232
232
  addHTTPMiddleware('*', [cors(), authBearer()])
@@ -4,19 +4,19 @@
4
4
 
5
5
  Wire a single function to an HTTP endpoint. Import from `#pikku`.
6
6
 
7
- | Option | Type | Notes |
8
- | --- | --- | --- |
9
- | `method` | `'get' \| 'post' \| 'put' \| 'patch' \| 'delete' \| 'head' \| 'options'` | HTTP verb |
10
- | `route` | `string` | e.g. `/books/:bookId` — `:params` become `data` fields |
11
- | `func` | `PikkuFunc` | The function to call |
12
- | `auth?` | `boolean` | Override default auth (`true` = require session) |
13
- | `tags?` | `string[]` | For grouping, middleware targeting |
14
- | `middleware?` | `PikkuMiddleware[]` | Per-route middleware |
15
- | `sse?` | `boolean` | Enable Server-Sent Events — **`method: 'get'` only** |
16
- | `query?` | `Array<keyof In>` | **`method: 'post'` only** — input fields also read from the query string |
17
- | `contentType?` | `'xml' \| 'json'` | Response content type |
18
- | `timeout?` | `number` | Request timeout in ms |
19
- | `headers?` | `HTTPHeadersSchema` | Expected headers schema |
7
+ | Option | Type | Notes |
8
+ | -------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
9
+ | `method` | `'get' \| 'post' \| 'put' \| 'patch' \| 'delete' \| 'head' \| 'options'` | HTTP verb |
10
+ | `route` | `string` | e.g. `/books/:bookId` — `:params` become `data` fields |
11
+ | `func` | `PikkuFunc` | The function to call |
12
+ | `auth?` | `boolean` | Override default auth (`true` = require session) |
13
+ | `tags?` | `string[]` | For grouping, middleware targeting |
14
+ | `middleware?` | `PikkuMiddleware[]` | Per-route middleware |
15
+ | `sse?` | `boolean` | Enable Server-Sent Events — **`method: 'get'` only** |
16
+ | `query?` | `Array<keyof In>` | **`method: 'post'` only** — input fields also read from the query string |
17
+ | `contentType?` | `'xml' \| 'json'` | Response content type |
18
+ | `timeout?` | `number` | Request timeout in ms |
19
+ | `headers?` | `HTTPHeadersSchema` | Expected headers schema |
20
20
 
21
21
  `sse` and `query` are constrained by the config union rather than by a runtime
22
22
  check, so a `sse: true` on a `post` fails to typecheck rather than silently
@@ -142,7 +142,8 @@ The wrapper alternative — a module that walks the namespace and pipes each mes
142
142
 
143
143
  `packages/console` is the one place in this repo that still wraps it, in `src/i18n/messages.ts`, to keep the debug mask (`█`) it carried over from i18next. That wrapper is a leftover, not a pattern — the generated-locale approach above is how a new app gets the same masking without touching every export. Don't copy it.
144
144
 
145
- The `mKey`/`mList` runtime resolvers that used to live beside it are **gone**, and must not come back. `mList` resolved indexed `prefix.0` keys that no longer exist; `mKey` took a computed string, which is exactly the type safety Paraglide exists to provide. Where a key really is dynamic, map the discriminant to a message *function* and call it — the map is type-checked, a string is not.
145
+ The `mKey`/`mList` runtime resolvers that used to live beside it are **gone**, and must not come back. `mList` resolved indexed `prefix.0` keys that no longer exist; `mKey` took a computed string, which is exactly the type safety Paraglide exists to provide. Where a key really is dynamic, map the discriminant to a message _function_ and call it — the map is type-checked, a string is not.
146
+
146
147
  - Don't re-resolve messages by string key or re-implement `{param}` interpolation. A key-string resolver turns a missing key back into silent runtime text, surrendering the type safety that is the entire reason to use Paraglide.
147
148
  - Don't reach for i18next/react-i18next or a runtime-fetch translation loader — Paraglide's compiled functions are the whole delivery mechanism.
148
149
  - Don't tokenize backend error messages or logs here — those are not frontend display strings.
@@ -29,7 +29,7 @@ Use the `pikku info` CLI commands to inspect this Pikku project. Run the command
29
29
 
30
30
  There are exactly four subcommands — `functions`, `tags`, `middleware`,
31
31
  `permissions`. Routes, channels, schedulers and queues are not separate
32
- subcommands; they show up as the *transport* column of `info functions --verbose`.
32
+ subcommands; they show up as the _transport_ column of `info functions --verbose`.
33
33
 
34
34
  ## Available Commands
35
35