@pikku/skills 0.12.10 → 0.12.11
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 +768 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +4 -4
- package/skills/pikku-addon/SKILL.md +17 -11
- package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
- package/skills/pikku-agent/SKILL.md +3 -3
- package/skills/pikku-audit/SKILL.md +28 -13
- package/skills/pikku-aws/SKILL.md +2 -2
- package/skills/pikku-better-auth/SKILL.md +97 -17
- package/skills/pikku-build-app/SKILL.md +621 -0
- package/skills/pikku-build-app/references/multi-app.md +117 -0
- package/skills/pikku-build-app/references/ship.md +98 -0
- package/skills/pikku-build-app/references/theming.md +70 -0
- package/skills/pikku-build-platform/SKILL.md +239 -0
- package/skills/pikku-build-quick/SKILL.md +238 -0
- package/skills/pikku-cli/SKILL.md +7 -7
- package/skills/pikku-cli/references/complete-example.md +1 -1
- package/skills/pikku-concepts/SKILL.md +5 -2
- package/skills/pikku-concepts/references/concept-mapping.md +1 -1
- package/skills/pikku-config/SKILL.md +5 -3
- package/skills/pikku-deploy-azure/SKILL.md +5 -4
- package/skills/pikku-emails/SKILL.md +5 -5
- package/skills/pikku-fabric/SKILL.md +27 -3
- package/skills/pikku-fabric-debug/SKILL.md +1 -1
- package/skills/pikku-feature/SKILL.md +5 -4
- package/skills/pikku-http/SKILL.md +4 -4
- package/skills/pikku-http/references/http-options.md +13 -13
- package/skills/pikku-i18n/SKILL.md +2 -1
- package/skills/pikku-info/SKILL.md +1 -1
- package/skills/pikku-knowledge/SKILL.md +13 -13
- package/skills/pikku-mcp/SKILL.md +4 -4
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
- package/skills/pikku-n8n-import/SKILL.md +12 -12
- package/skills/pikku-n8n-import/SPEC.md +3 -0
- package/skills/pikku-n8n-import/references/code-translation.md +26 -22
- package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
- package/skills/pikku-paraglide/SKILL.md +11 -6
- package/skills/pikku-permissions/SKILL.md +19 -15
- package/skills/pikku-product-second-opinion/README.md +3 -3
- package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
- package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
- package/skills/pikku-queue/SKILL.md +1 -1
- package/skills/pikku-react/SKILL.md +53 -13
- package/skills/pikku-realtime/SKILL.md +51 -19
- package/skills/pikku-rtl/SKILL.md +1 -1
- package/skills/pikku-scenario/SKILL.md +126 -14
- package/skills/pikku-schedule/SKILL.md +6 -1
- package/skills/pikku-schema-ajv/SKILL.md +2 -2
- package/skills/pikku-schema-cfworker/SKILL.md +1 -1
- package/skills/pikku-security/SKILL.md +9 -5
- package/skills/pikku-services/SKILL.md +27 -18
- package/skills/pikku-services/references/audit-wire-service.md +14 -8
- package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
- package/skills/pikku-tag-middleware/SKILL.md +1 -0
- package/skills/pikku-template-clone/SKILL.md +2 -1
- package/skills/pikku-trigger/SKILL.md +3 -3
- package/skills/pikku-websocket/SKILL.md +4 -3
- package/skills/pikku-workflow/SKILL.md +2 -2
- 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
|
|
126
|
-
wire.cli.command
|
|
127
|
-
wire.cli.data
|
|
128
|
-
wire.cli.channel
|
|
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
|
-
-
|
|
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
|
|
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 '
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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",
|
|
40
|
-
"outDir": ".pikku"
|
|
39
|
+
"emailTemplatesDir": "emails", // relative to rootDir; omit to disable emails
|
|
40
|
+
"outDir": ".pikku", // gen lands in <outDir>/email/
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
@@ -121,9 +121,9 @@ render with sample data and read the result rather than trusting that it compile
|
|
|
121
121
|
|
|
122
122
|
```ts
|
|
123
123
|
const rendered = renderEmailTemplate({
|
|
124
|
-
name: 'verify-email',
|
|
125
|
-
locale: 'en',
|
|
126
|
-
data: { verifyUrl: url },
|
|
124
|
+
name: 'verify-email', // EmailTemplateName (autocompleted)
|
|
125
|
+
locale: 'en', // optional, defaults to 'en'
|
|
126
|
+
data: { verifyUrl: url }, // EmailTemplateVariables<'verify-email'>
|
|
127
127
|
})
|
|
128
128
|
// rendered: { name, locale, subject, html, text?, variables, hash }
|
|
129
129
|
```
|
|
@@ -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 (`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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**:
|
|
145
|
-
|
|
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/
|
|
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
|
|
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
|
|
8
|
-
|
|
|
9
|
-
| `method`
|
|
10
|
-
| `route`
|
|
11
|
-
| `func`
|
|
12
|
-
| `auth?`
|
|
13
|
-
| `tags?`
|
|
14
|
-
| `middleware?`
|
|
15
|
-
| `sse?`
|
|
16
|
-
| `query?`
|
|
17
|
-
| `contentType?` | `'xml' \| 'json'`
|
|
18
|
-
| `timeout?`
|
|
19
|
-
| `headers?`
|
|
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
|
|
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
|
|
32
|
+
subcommands; they show up as the _transport_ column of `info functions --verbose`.
|
|
33
33
|
|
|
34
34
|
## Available Commands
|
|
35
35
|
|
|
@@ -66,7 +66,7 @@ Frontmatter fields:
|
|
|
66
66
|
|
|
67
67
|
| Field | Meaning |
|
|
68
68
|
| ------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
-
| `type` | **The only required field.** `slice`, `entity`, `decision`, `note`, `overview`. Lowercase — gates compare it literally. |
|
|
69
|
+
| `type` | **The only required field.** `slice` — or `milestone`, when the project's own `knowledge/index.md` names the section that way; `validate` accepts both, so follow the scaffold rather than this list. Then `entity`, `decision`, `note`, `overview`. Lowercase — gates compare it literally. |
|
|
70
70
|
| `title` | What to call the note in a listing. Falls back to the first heading, then the filename. |
|
|
71
71
|
| `description` | One line, used as the note's subtitle in a section index. |
|
|
72
72
|
| `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below. |
|
|
@@ -193,19 +193,19 @@ It is a **summary, not the note** — the argument continues in prose underneath
|
|
|
193
193
|
|
|
194
194
|
**Every kind resolves.** That is the whole design: a kind that cannot be checked lets notes accumulate references nothing validates, and the graph rots into fiction exactly where it looks most authoritative.
|
|
195
195
|
|
|
196
|
-
| Kind | An id is | Where it resolves
|
|
197
|
-
| ----------- | -------------------------------------------------- |
|
|
198
|
-
| `func:` | a function id | generated function meta
|
|
199
|
-
| `workflow:` | a workflow name | generated workflow meta
|
|
200
|
-
| `schema:` | a schema name | generated schemas
|
|
201
|
-
| `http:` | a route, `method:route`, or the function behind it | generated http wirings
|
|
202
|
-
| `queue:` | a queue name | generated queue wirings
|
|
203
|
-
| `cron:` | a scheduled task name | generated scheduler wirings
|
|
204
|
-
| `channel:` | a channel name | generated channel meta
|
|
205
|
-
| `table:` | a table name | the generated db schema
|
|
206
|
-
| `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency
|
|
196
|
+
| Kind | An id is | Where it resolves |
|
|
197
|
+
| ----------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
198
|
+
| `func:` | a function id | generated function meta |
|
|
199
|
+
| `workflow:` | a workflow name | generated workflow meta |
|
|
200
|
+
| `schema:` | a schema name | generated schemas |
|
|
201
|
+
| `http:` | a route, `method:route`, or the function behind it | generated http wirings |
|
|
202
|
+
| `queue:` | a queue name | generated queue wirings |
|
|
203
|
+
| `cron:` | a scheduled task name | generated scheduler wirings |
|
|
204
|
+
| `channel:` | a channel name | generated channel meta |
|
|
205
|
+
| `table:` | a table name | the generated db schema |
|
|
206
|
+
| `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency |
|
|
207
207
|
| `scope:` | a scope name | the `scopes:` a function gates itself with, plus the scopes a `defineSystemRole()` confers |
|
|
208
|
-
| `persona:` | a persona name | `definePersonas()`
|
|
208
|
+
| `persona:` | a persona name | `definePersonas()` |
|
|
209
209
|
|
|
210
210
|
Ids are case-sensitive: `createEntry` is not `createentry`.
|
|
211
211
|
|