@pikku/skills 0.12.20 → 0.12.22
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 +60 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +8 -7
- package/skills/pikku-better-auth/SKILL.md +67 -40
- package/skills/pikku-build-app/SKILL.md +2 -2
- package/skills/pikku-build-quick/SKILL.md +2 -2
- package/skills/pikku-concepts/SKILL.md +3 -1
- package/skills/pikku-fabric/SKILL.md +10 -10
- package/skills/pikku-feature/SKILL.md +104 -2
- package/skills/pikku-http/SKILL.md +1 -1
- package/skills/pikku-middleware/SKILL.md +3 -3
- package/skills/pikku-permissions/SKILL.md +4 -4
- package/skills/pikku-scenario/SKILL.md +4 -4
- package/skills/pikku-security/SKILL.md +3 -3
- package/skills/pikku-services/SKILL.md +3 -3
- package/skills/pikku-workflow/SKILL.md +1 -1
package/package.json
CHANGED
|
@@ -146,7 +146,7 @@ second argument is always present — an addon never falls back to its own logge
|
|
|
146
146
|
variables or secrets; the consuming app supplies them:
|
|
147
147
|
|
|
148
148
|
```typescript
|
|
149
|
-
import { pikkuAddonServices } from '#pikku/
|
|
149
|
+
import { pikkuAddonServices } from '#pikku/setup'
|
|
150
150
|
|
|
151
151
|
export const createSingletonServices = pikkuAddonServices(
|
|
152
152
|
async (config, { secrets, logger }) => {
|
|
@@ -167,7 +167,7 @@ config object.
|
|
|
167
167
|
Define per-request services for an addon package (created fresh per HTTP request, queue job, etc.):
|
|
168
168
|
|
|
169
169
|
```typescript
|
|
170
|
-
import { pikkuAddonWireServices } from '#pikku/
|
|
170
|
+
import { pikkuAddonWireServices } from '#pikku/setup'
|
|
171
171
|
|
|
172
172
|
export const createWireServices = pikkuAddonWireServices(
|
|
173
173
|
async (singletonServices, wire) => {
|
|
@@ -195,7 +195,7 @@ This generates `package.json` (exports `.pikku/*` + `dist/`), `pikku.config.json
|
|
|
195
195
|
|
|
196
196
|
```typescript
|
|
197
197
|
// src/services.ts
|
|
198
|
-
import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/
|
|
198
|
+
import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/setup'
|
|
199
199
|
import { TodoStore } from './todo-store.service.js'
|
|
200
200
|
|
|
201
201
|
export const createSingletonServices = pikkuAddonServices(async () => {
|
|
@@ -213,14 +213,15 @@ export const createWireServices = pikkuAddonWireServices(
|
|
|
213
213
|
|
|
214
214
|
### Functions
|
|
215
215
|
|
|
216
|
-
An addon
|
|
217
|
-
`#pikku
|
|
218
|
-
|
|
216
|
+
An addon's generated tree roots at `.pikku/addon/`, but its `imports` map points
|
|
217
|
+
`#pikku/*` there, so it authors against the same subpaths an application does —
|
|
218
|
+
`#pikku/function`, `#pikku/http`. The `addon` segment is the package's own
|
|
219
|
+
business, never part of a specifier.
|
|
219
220
|
|
|
220
221
|
```typescript
|
|
221
222
|
// src/functions/addTodo.function.ts
|
|
222
223
|
import { z } from 'zod'
|
|
223
|
-
import { pikkuSessionlessFunc } from '#pikku/
|
|
224
|
+
import { pikkuSessionlessFunc } from '#pikku/function'
|
|
224
225
|
|
|
225
226
|
const AddTodoInput = z.object({ title: z.string() })
|
|
226
227
|
const AddTodoOutput = z.object({ id: z.string(), title: z.string() })
|
|
@@ -237,12 +237,12 @@ Banning is the one capability with a schema requirement, and it has its own
|
|
|
237
237
|
small plugin:
|
|
238
238
|
|
|
239
239
|
```typescript
|
|
240
|
-
import {
|
|
240
|
+
import { pikkuBan } from '@pikku/better-auth'
|
|
241
241
|
|
|
242
|
-
betterAuth({ plugins: [
|
|
242
|
+
betterAuth({ plugins: [pikkuBan()] })
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
`
|
|
245
|
+
`pikkuBan()` adds `banned`, `banReason` and `banExpires` to `user` and refuses to
|
|
246
246
|
create a session for a banned user, lapsing an expired ban as it goes. It makes
|
|
247
247
|
no authorization decision — who may ban is decided by `admin:users:ban` — so it
|
|
248
248
|
never needs to know about scopes or roles.
|
|
@@ -252,22 +252,28 @@ never needs to know about scopes or roles.
|
|
|
252
252
|
Five, all imported from the package root and passed to `betterAuth({ plugins })`
|
|
253
253
|
like any other. None is automatic — an app wires the ones it needs.
|
|
254
254
|
|
|
255
|
-
| Plugin
|
|
256
|
-
|
|
|
257
|
-
| `
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
255
|
+
| Plugin | Plugin `id` | Adds | Use it when |
|
|
256
|
+
| ------------------------ | ------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
257
|
+
| `pikkuBan()` | `pikku-ban` | `user.banned/banReason/banExpires` | You ban users (the schema + enforcement half of the above) |
|
|
258
|
+
| `pikkuActor()` | `actor` | `POST /sign-in/actor`, `user.actor` | Scenarios or a dev switcher sign in as a persona |
|
|
259
|
+
| `pikkuCredentialOAuth()` | `credential-oauth` | `POST /credential-oauth/link`, `/credential-oauth/callback/:providerId` | An app links OAuth2 **API credentials** for a user |
|
|
260
|
+
| `pikkuDelegatedAuth()` | `delegated-auth` | `POST /sign-in/delegated` | An imported upstream API is the system of record for identity |
|
|
261
|
+
| `pikkuFabric()` | `fabric` | `POST /sign-in/fabric` | A Fabric-deployed app lets a control-plane operator in |
|
|
262
|
+
|
|
263
|
+
Every one carries a `pikku` prefix, because a `plugins: [...]` array mixes these
|
|
264
|
+
with better-auth's own and a bare `actor()` next to `organization()` says
|
|
265
|
+
nothing about where it came from. The unprefixed names — `ban`, `actor`,
|
|
266
|
+
`credentialOAuth`, `delegatedAuth`, `fabric` — are still exported as deprecated
|
|
267
|
+
aliases, so existing apps keep working.
|
|
262
268
|
|
|
263
269
|
The plugin's `id` is what better-auth stores; the **export name** is what the
|
|
264
270
|
inspector reads off your `plugins` array and what generated metadata is keyed
|
|
265
|
-
by, so the two differ for
|
|
271
|
+
by, so the two differ for every one of them.
|
|
266
272
|
|
|
267
|
-
#### `
|
|
273
|
+
#### `pikkuCredentialOAuth()` — link API credentials, not identities
|
|
268
274
|
|
|
269
275
|
```typescript
|
|
270
|
-
|
|
276
|
+
pikkuCredentialOAuth({
|
|
271
277
|
config: [
|
|
272
278
|
{
|
|
273
279
|
providerId: 'github',
|
|
@@ -308,10 +314,10 @@ by construction. Tokens land in better-auth's `account` table, so
|
|
|
308
314
|
An undeclared `providerId` is a 404; an anonymous caller a 401; a refused
|
|
309
315
|
singleton a 403 that leaves no platform user behind.
|
|
310
316
|
|
|
311
|
-
#### `
|
|
317
|
+
#### `pikkuDelegatedAuth()` — the upstream API is the identity provider
|
|
312
318
|
|
|
313
319
|
```typescript
|
|
314
|
-
|
|
320
|
+
pikkuDelegatedAuth({
|
|
315
321
|
authenticate: async ({ email, password, apiKey }) => upstream.login(...),
|
|
316
322
|
storeCredential: (userId, identity) =>
|
|
317
323
|
credentialService.set('acme', identity.credential, userId),
|
|
@@ -338,10 +344,10 @@ a warning and the user still gets in.
|
|
|
338
344
|
`storeCredential` failing, by contrast, **fails the sign-in**: every proxied
|
|
339
345
|
call would be dead anyway.
|
|
340
346
|
|
|
341
|
-
#### `
|
|
347
|
+
#### `pikkuFabric()` — control-plane operator sign-in
|
|
342
348
|
|
|
343
349
|
```typescript
|
|
344
|
-
|
|
350
|
+
pikkuFabric({ publicKey: FABRIC_AUTH_PUBLIC_KEY, scopeService, logger })
|
|
345
351
|
```
|
|
346
352
|
|
|
347
353
|
`POST /sign-in/fabric` verifies a short-lived RS256 token that the Fabric
|
|
@@ -468,9 +474,9 @@ someone" means **a particular kind of user** rather than one fixed admin.
|
|
|
468
474
|
Register it explicitly — it is not automatic:
|
|
469
475
|
|
|
470
476
|
```typescript
|
|
471
|
-
import {
|
|
477
|
+
import { pikkuActor } from '@pikku/better-auth'
|
|
472
478
|
|
|
473
|
-
plugins: [
|
|
479
|
+
plugins: [pikkuActor({ secret: SCENARIO_ACTOR_SECRET })]
|
|
474
480
|
```
|
|
475
481
|
|
|
476
482
|
`POST ${basePath}/sign-in/actor` `{ email, secret, name? }` → 200 + the normal
|
|
@@ -510,9 +516,9 @@ an `actor: true` row only under `pikku dev`. With the opt-in set, a stage signs
|
|
|
510
516
|
in as the personas the deployment provisioned when it started and refuses
|
|
511
517
|
everything else (`No actor account exists for that address`), so holding the
|
|
512
518
|
secret on such a stage does not let anyone invent identities. Those rows are
|
|
513
|
-
written by
|
|
514
|
-
|
|
515
|
-
endpoint is shut.
|
|
519
|
+
written by the fabric plugin when an operator asks to act as an address the
|
|
520
|
+
stage has no account for, so provisioning needs no actor secret and works on a
|
|
521
|
+
stage whose endpoint is shut.
|
|
516
522
|
|
|
517
523
|
**`SCENARIO_ACTOR_SECRET` is a credential as powerful as the most privileged
|
|
518
524
|
persona.** Provisioning grants declared roles to actor accounts, so an
|
|
@@ -538,30 +544,46 @@ This is the endpoint `pikku scenario` signs its actors in through, and the one
|
|
|
538
544
|
the frontend dev switcher posts to — see `pikku-scenario` for declaring the
|
|
539
545
|
actors and `pikku-react` for `useDevActors()`.
|
|
540
546
|
|
|
541
|
-
### Provisioning personas
|
|
547
|
+
### Provisioning personas
|
|
542
548
|
|
|
543
|
-
Anywhere but `pikku dev`, the accounts have to exist before anyone signs in.
|
|
544
|
-
|
|
549
|
+
Anywhere but `pikku dev`, the accounts have to exist before anyone signs in. The
|
|
550
|
+
stage creates them itself, from the personas you hand `pikkuFabric`:
|
|
545
551
|
|
|
546
552
|
```ts
|
|
547
|
-
import {
|
|
553
|
+
import { pikkuFabric } from '@pikku/better-auth'
|
|
548
554
|
import {
|
|
549
555
|
personaConfigs,
|
|
550
556
|
personaEnvironments,
|
|
551
557
|
} from '#pikku/pikku-personas.gen.js'
|
|
552
558
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
559
|
+
pikkuFabric({
|
|
560
|
+
publicKey,
|
|
561
|
+
audience,
|
|
562
|
+
scopeService,
|
|
563
|
+
personas: {
|
|
564
|
+
personas: personaConfigs,
|
|
565
|
+
environments: personaEnvironments,
|
|
566
|
+
},
|
|
556
567
|
})
|
|
557
568
|
```
|
|
558
569
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
570
|
+
There is nothing else to call and nothing to schedule. The plugin's operator
|
|
571
|
+
endpoint resolves the address the caller wants to act as; a miss provisions the
|
|
572
|
+
declaration and looks again. On a stage that already holds the persona that is
|
|
573
|
+
one query, and the pass only runs when there is genuinely something absent to
|
|
574
|
+
create.
|
|
575
|
+
|
|
576
|
+
**Do not reach for `pikkuServerLifecycle`'s `afterStart` for this.** That hook is
|
|
577
|
+
invoked by `pikku serve` and `pikku dev` and by nothing else — no deploy runtime
|
|
578
|
+
calls it — so a stage on Workers or a serverless target that provisioned from
|
|
579
|
+
`afterStart` provisioned nothing, and every persona signed in holding no roles.
|
|
580
|
+
|
|
581
|
+
Provisioning runs where the database already is, which is the point: the CLI has
|
|
582
|
+
no connection to a deployed environment's database — it resolves one from the
|
|
583
|
+
local project config — so a `pikku persona sync staging` that wrote rows would
|
|
584
|
+
write them to whatever database the checkout happened to point at. `pikku persona
|
|
585
|
+
sync <environment>` still exists, and reports who that environment will provision
|
|
586
|
+
and why anyone was skipped, which is what you run _before_ the deploy.
|
|
565
587
|
|
|
566
588
|
It creates missing accounts as `actor: true`, applies the roles each persona
|
|
567
589
|
declares, and is additive — it never revokes. `PIKKU_ENV` (or an explicit
|
|
@@ -577,15 +599,20 @@ open. By default provisioning warns about those accounts and changes nothing.
|
|
|
577
599
|
`orphans: 'ban'` shuts them:
|
|
578
600
|
|
|
579
601
|
```ts
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
602
|
+
pikkuFabric({
|
|
603
|
+
publicKey,
|
|
604
|
+
audience,
|
|
605
|
+
scopeService,
|
|
606
|
+
personas: {
|
|
607
|
+
personas: personaConfigs,
|
|
608
|
+
environments: personaEnvironments,
|
|
609
|
+
orphans: 'ban',
|
|
610
|
+
},
|
|
584
611
|
})
|
|
585
612
|
```
|
|
586
613
|
|
|
587
614
|
It writes the same `banned` column the console's ban RPC writes (so it needs the
|
|
588
|
-
`
|
|
615
|
+
`pikkuBan()` plugin wired, and says so if it isn't), revokes the account's sessions,
|
|
589
616
|
and leaves the row, its grants and its history intact — provisioning lifts the
|
|
590
617
|
ban again by itself if the persona comes back. Deleting is deliberately not
|
|
591
618
|
offered: an actor row is referenced by whatever those scenarios did while it
|
|
@@ -210,7 +210,7 @@ people the user named, and the roles they imply.
|
|
|
210
210
|
|
|
211
211
|
```typescript
|
|
212
212
|
import { definePersonas } from '#pikku/scopes/pikku-personas.gen.js'
|
|
213
|
-
import { defineSystemRole } from '#pikku'
|
|
213
|
+
import { defineSystemRole } from '#pikku/scopes'
|
|
214
214
|
|
|
215
215
|
defineSystemRole({
|
|
216
216
|
owner: {
|
|
@@ -484,7 +484,7 @@ experiences it. Three ship in `packages/functions/test/scenarios/` — keep them
|
|
|
484
484
|
green — and every milestone's gherkin block from §5 becomes one more.
|
|
485
485
|
|
|
486
486
|
```typescript
|
|
487
|
-
import { pikkuScenario } from '#pikku/
|
|
487
|
+
import { pikkuScenario } from '#pikku/scenarios'
|
|
488
488
|
|
|
489
489
|
export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
|
|
490
490
|
title: 'A tenant reports a fault and the owner sees it',
|
|
@@ -85,7 +85,7 @@ named:
|
|
|
85
85
|
|
|
86
86
|
```typescript
|
|
87
87
|
import { definePersonas } from '#pikku/scopes/pikku-personas.gen.js'
|
|
88
|
-
import { defineSystemRole } from '#pikku'
|
|
88
|
+
import { defineSystemRole } from '#pikku/scopes'
|
|
89
89
|
|
|
90
90
|
defineSystemRole({
|
|
91
91
|
owner: { displayName: 'Owner', description: 'Sees only their own rows', scopes: [] },
|
|
@@ -199,7 +199,7 @@ Not the full ladder — one journey, end to end, as a real persona, so the app h
|
|
|
199
199
|
at least one thing that stays true.
|
|
200
200
|
|
|
201
201
|
```typescript
|
|
202
|
-
import { pikkuScenario } from '#pikku/
|
|
202
|
+
import { pikkuScenario } from '#pikku/scenarios'
|
|
203
203
|
|
|
204
204
|
export const ownerCreatesAndSeesItScenario = pikkuScenario<void, { id: string }>({
|
|
205
205
|
title: 'An owner creates a thing and sees it',
|
|
@@ -247,6 +247,8 @@ export const lifecycle = pikkuServerLifecycle<SingletonServices>({
|
|
|
247
247
|
|
|
248
248
|
Export exactly one `pikkuServerLifecycle` from anywhere in `srcDirectories` — the inspector finds it by the wrapper call. Every hook is optional and receives the already-created singleton services. See pikku-services for the ordering and the `afterStop` caveat.
|
|
249
249
|
|
|
250
|
+
**Only `pikku dev` and `pikku serve` invoke these hooks.** No deploy runtime does, so anything a Workers or serverless stage needs done cannot live here — put it on the request path that needs it, guarded by a cheap check.
|
|
251
|
+
|
|
250
252
|
**2. Bootstrap it yourself (required for a specific runtime)**
|
|
251
253
|
|
|
252
254
|
Express, Fastify, uWS, Lambda, Cloudflare and Next.js need their own entrypoint, because Pikku is embedded in a server you own:
|
|
@@ -333,7 +335,7 @@ bottom.
|
|
|
333
335
|
| Axis | What it covers | What decides it |
|
|
334
336
|
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
335
337
|
| **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Branch names and commit messages. | Nothing. **Always English.** There is no setting. |
|
|
336
|
-
| **Meta** | The prose authored _inside_ the code: `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role/persona descriptions. | `metaLocale` in `pikku.config.json`. Defaults to `en`.
|
|
338
|
+
| **Meta** | The prose authored _inside_ the code: `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role/persona descriptions. | `metaLocale` in `pikku.config.json`. Defaults to `en`. |
|
|
337
339
|
| **Product UI** | Every string the app shows a user. | `messages/<locale>.json`, with `active.json`'s `defaultLocale` choosing what a first-time visitor opens in. |
|
|
338
340
|
|
|
339
341
|
### Identifiers are English, and nothing changes that
|
|
@@ -119,7 +119,7 @@ without it in production?"** If yes, it is configuration and belongs in a
|
|
|
119
119
|
migration, however much it looks like sample data. A venue and its rooms, a
|
|
120
120
|
product catalogue, a tenant, a country list, the organization the whole
|
|
121
121
|
deployment hangs off — all configuration. Accounts and role grants are
|
|
122
|
-
provisioning:
|
|
122
|
+
provisioning: the fabric plugin's `personas`, or a migration. What is left
|
|
123
123
|
over is the seed's job — the bookings, orders and messages a demo needs and a real
|
|
124
124
|
environment starts without.
|
|
125
125
|
|
|
@@ -129,8 +129,8 @@ a stage whose pages return 200 — the shell renders fine — while its first da
|
|
|
129
129
|
read throws `no result` or a foreign-key violation on a row the seed was
|
|
130
130
|
silently supplying.
|
|
131
131
|
|
|
132
|
-
A Better Auth app has a second constraint: the plugins you enable (`
|
|
133
|
-
`
|
|
132
|
+
A Better Auth app has a second constraint: the plugins you enable (`pikkuBan()`,
|
|
133
|
+
`pikkuActor()`, …) each declare columns, and `pikku db migrate` refuses to run while
|
|
134
134
|
the applied schema is missing any of them. `pikku db generate` writes the
|
|
135
135
|
migration that closes the gap.
|
|
136
136
|
|
|
@@ -306,13 +306,13 @@ tells you nothing about whether it worked. `--sync` waits for a terminal state
|
|
|
306
306
|
and exits non-zero unless the deployment went live, which is the only form worth
|
|
307
307
|
running in CI:
|
|
308
308
|
|
|
309
|
-
| exit | meaning
|
|
310
|
-
| ---- |
|
|
311
|
-
| 0
|
|
312
|
-
| 1
|
|
313
|
-
| 2
|
|
314
|
-
| 3
|
|
315
|
-
| 4
|
|
309
|
+
| exit | meaning |
|
|
310
|
+
| ---- | ----------------------------------------------------------------------- |
|
|
311
|
+
| 0 | live (or queued, without `--sync`) |
|
|
312
|
+
| 1 | the command could not run — not logged in, unsafe git state, bad flags |
|
|
313
|
+
| 2 | the deployment failed, errored, timed out server-side, or was cancelled |
|
|
314
|
+
| 3 | the deployment is blocked and nothing the CLI can do will unblock it |
|
|
315
|
+
| 4 | the wait hit `--timeout` with the deployment still in flight |
|
|
316
316
|
|
|
317
317
|
Fabric parks every deploy at a gate after the plan phase (`status: suspended`).
|
|
318
318
|
Why it parked is the whole story, and it is `statusReason`, not `status`:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-feature
|
|
3
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
|
-
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 *)
|
|
4
|
+
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 *), Bash(yarn pikku fabric report *), Bash(npx --no pikku fabric report *)
|
|
5
5
|
argument-hint: '<feature description>'
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -16,6 +16,7 @@ Use this skill as an execution checklist, not reference material.
|
|
|
16
16
|
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
17
17
|
4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
|
|
18
18
|
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
19
|
+
6. Report anything about pikku itself that cost you time, the moment it happens — see **Report what fought you**.
|
|
19
20
|
|
|
20
21
|
End-to-end flow: **discover → state intent → branch → implement → verify → commit → hand to reviewer**.
|
|
21
22
|
|
|
@@ -246,6 +247,106 @@ feature/<slug>` and surface the PR-create URL.
|
|
|
246
247
|
|
|
247
248
|
Do not push without explicit confirmation. Do not merge.
|
|
248
249
|
|
|
250
|
+
## Report what fought you
|
|
251
|
+
|
|
252
|
+
When pikku itself is what cost you time, report it with `pikku fabric report`.
|
|
253
|
+
Nothing is written to the repo; the finding goes to the linked fabric project
|
|
254
|
+
and the terminal shows you exactly what was sent.
|
|
255
|
+
|
|
256
|
+
**Report at the moment it happens**, not at the end from memory — a run that
|
|
257
|
+
falls over never reaches its end. One finding per thing that fought you.
|
|
258
|
+
|
|
259
|
+
### The ladder
|
|
260
|
+
|
|
261
|
+
1. **Find the quicker workaround.** The user is paying for their feature, not
|
|
262
|
+
for pikku's health.
|
|
263
|
+
2. **Investigate** only when there is no workaround, or when the user asks why
|
|
264
|
+
something is slow or wrong.
|
|
265
|
+
3. **Report at the depth you already reached.** Never spend extra effort to
|
|
266
|
+
file; never throw away effort you already spent. If the investigation took
|
|
267
|
+
you to the mechanism, the finding says so — named file, named function, what
|
|
268
|
+
is actually happening, and what pikku should do instead.
|
|
269
|
+
|
|
270
|
+
**Never fix pikku itself.** Not a patch in `node_modules`, not a linked
|
|
271
|
+
checkout, not a branch in the framework repo. Many agents each patching pikku to
|
|
272
|
+
unblock themselves is many divergent copies and a merge problem nobody signed up
|
|
273
|
+
for. Work around it in the app, report it, and let the fix happen once.
|
|
274
|
+
|
|
275
|
+
### What counts
|
|
276
|
+
|
|
277
|
+
Anything that cost you time and would cost the next person the same. Most of
|
|
278
|
+
these never produce an error: output that is quietly wrong, a generated type
|
|
279
|
+
that disagrees with the runtime, a check that passes when it should not, a
|
|
280
|
+
narrowing you had to write by hand because the framework should have written it.
|
|
281
|
+
**Having to write code the framework should have written for you is a finding.**
|
|
282
|
+
|
|
283
|
+
So is anything that only shows up in one place — invisible locally, fatal
|
|
284
|
+
deployed, or the reverse. Say which, with `--surface`.
|
|
285
|
+
|
|
286
|
+
Not a finding: a preference, a thing you would have designed differently, or
|
|
287
|
+
baseline noise that was already failing before you started.
|
|
288
|
+
|
|
289
|
+
### Two kinds
|
|
290
|
+
|
|
291
|
+
- `--kind product` — pikku behaved wrongly. Fixing it is a change to the
|
|
292
|
+
framework.
|
|
293
|
+
- `--kind harness` — a skill misled you: it told you to run something that does
|
|
294
|
+
not exist, described a flag that is spelled differently, or contradicted what
|
|
295
|
+
the CLI actually did. Pass `--skill <name>` and `--passage "<the line or
|
|
296
|
+
section>"`. This is the most useful kind to file, because it is fixable
|
|
297
|
+
immediately — so file it even when the cost was small.
|
|
298
|
+
|
|
299
|
+
### When there was no workaround
|
|
300
|
+
|
|
301
|
+
Report it anyway with `--unresolved`, and put what you tried and how each
|
|
302
|
+
attempt failed in `--tried`. That is what stops the next person walking the same
|
|
303
|
+
dead ends. Tell the user what you did instead — abandoned it, shipped something
|
|
304
|
+
degraded, or stopped.
|
|
305
|
+
|
|
306
|
+
`--unresolved` means **no workaround was found**. It does not mean the
|
|
307
|
+
workaround was unpleasant.
|
|
308
|
+
|
|
309
|
+
### The command
|
|
310
|
+
|
|
311
|
+
Send it as JSON on stdin. Most of a finding is prose, and prose carries
|
|
312
|
+
apostrophes, quotes, backticks and newlines — each one a shell metacharacter
|
|
313
|
+
before it is a character in your sentence. A stack trace passed to `--error`
|
|
314
|
+
breaks the command at its first newline; a backtick in `--actual` runs whatever
|
|
315
|
+
follows it. Quote the heredoc delimiter (`<<'EOF'`, never `<<EOF`) so the shell
|
|
316
|
+
leaves the body alone.
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
pikku fabric report --stdin <<'EOF'
|
|
320
|
+
{
|
|
321
|
+
"title": "<one-line title>",
|
|
322
|
+
"kind": "product",
|
|
323
|
+
"model": "<the model you are>",
|
|
324
|
+
"expected": "<what you expected pikku to do>",
|
|
325
|
+
"actual": "<what it did instead>",
|
|
326
|
+
"command": "<the command you ran>",
|
|
327
|
+
"workaround": "<what you did instead, inside the app>"
|
|
328
|
+
}
|
|
329
|
+
EOF
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Add whichever of these you actually have: `error` (the error's message line,
|
|
333
|
+
verbatim), `repro` (the shortest way to reach it again), `proposal` (what pikku
|
|
334
|
+
should do), `area`, `surface` (`local`, `deployed` or `both`), `cost` (measured
|
|
335
|
+
if you measured it — "98s vs 20s steady" ranks; "slow" does not), `run` (an id
|
|
336
|
+
shared by every finding from this build), `deployTarget`.
|
|
337
|
+
|
|
338
|
+
The same fields exist as flags — `--kind`, `--expected` and so on — for a
|
|
339
|
+
finding short enough to type. Anything with a newline or a quote in it goes
|
|
340
|
+
through `--stdin`.
|
|
341
|
+
|
|
342
|
+
Versions, platform and package manager are read off the installed tree for you.
|
|
343
|
+
Do not pass them and do not ask the user for them.
|
|
344
|
+
|
|
345
|
+
Reporting never fails a build. A finding that cannot be sent — logged out, or
|
|
346
|
+
fabric unreachable — is held on the machine and goes out with the next report
|
|
347
|
+
that succeeds, so nothing you file is lost. If it says the finding was queued,
|
|
348
|
+
carry on with the feature; do not try to fix it, and do not file it again.
|
|
349
|
+
|
|
249
350
|
## Hard constraints
|
|
250
351
|
|
|
251
352
|
The skill's `allowed-tools` does **not** permit:
|
|
@@ -254,7 +355,8 @@ The skill's `allowed-tools` does **not** permit:
|
|
|
254
355
|
- `yarn dbmigrate` (never run migrations against the real DB during planning)
|
|
255
356
|
- `pikku deploy apply` (never deploy)
|
|
256
357
|
- secret writes
|
|
257
|
-
- network calls beyond what the implementation requires
|
|
358
|
+
- network calls beyond what the implementation requires, except
|
|
359
|
+
`pikku fabric report`, which is explicitly permitted
|
|
258
360
|
|
|
259
361
|
If the feature genuinely needs any of these, **stop and ask** with a clear
|
|
260
362
|
explanation of why and what would change.
|
|
@@ -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/middleware'
|
|
230
230
|
import { cors, authBearer } from '@pikku/core/middleware'
|
|
231
231
|
|
|
232
232
|
addHTTPMiddleware('*', [cors(), authBearer()])
|
|
@@ -23,7 +23,7 @@ installGroups: [core]
|
|
|
23
23
|
## The `pikkuMiddleware` Factory
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
|
-
import { pikkuMiddleware } from '#pikku/
|
|
26
|
+
import { pikkuMiddleware } from '#pikku/middleware'
|
|
27
27
|
|
|
28
28
|
// Simple: just a function
|
|
29
29
|
const myMiddleware = pikkuMiddleware(async (services, wire, next) => {
|
|
@@ -139,7 +139,7 @@ Tags from the function definition and the wire object are merged — middleware
|
|
|
139
139
|
### Registering Tag Middleware
|
|
140
140
|
|
|
141
141
|
```typescript
|
|
142
|
-
import { addTagMiddleware } from '#pikku/
|
|
142
|
+
import { addTagMiddleware } from '#pikku/middleware'
|
|
143
143
|
|
|
144
144
|
addTagMiddleware('machine-agent', [machineAgentBearerAuth])
|
|
145
145
|
```
|
|
@@ -277,7 +277,7 @@ export const getToken = () => _token
|
|
|
277
277
|
```typescript
|
|
278
278
|
// wirings/http.wiring.ts
|
|
279
279
|
import { timingSafeEqual } from 'node:crypto'
|
|
280
|
-
import { addTagMiddleware, pikkuMiddleware } from '#pikku/
|
|
280
|
+
import { addTagMiddleware, pikkuMiddleware } from '#pikku/middleware'
|
|
281
281
|
import { UnauthorizedError } from '#pikku/error'
|
|
282
282
|
import { getToken } from '../lib/host-token.js'
|
|
283
283
|
|
|
@@ -58,7 +58,7 @@ Use for checks that read the session but need no request data — and that asser
|
|
|
58
58
|
something **beyond** merely having a session (a flag, a tier, a claim).
|
|
59
59
|
|
|
60
60
|
```typescript
|
|
61
|
-
import { pikkuAuth } from '#pikku/
|
|
61
|
+
import { pikkuAuth } from '#pikku/auth'
|
|
62
62
|
|
|
63
63
|
// Good: a real gate on the session's contents, not just its existence.
|
|
64
64
|
export const isVerified = pikkuAuth(
|
|
@@ -89,7 +89,7 @@ A permission answers "_may this user do this?_" (role, ownership, tier) — neve
|
|
|
89
89
|
Use when authorization depends on the actual request data (e.g., resource ownership).
|
|
90
90
|
|
|
91
91
|
```typescript
|
|
92
|
-
import { pikkuPermission } from '#pikku/
|
|
92
|
+
import { pikkuPermission } from '#pikku/auth'
|
|
93
93
|
|
|
94
94
|
export const isBookOwner = pikkuPermission(
|
|
95
95
|
async ({ db }, { bookId }, { session }) => {
|
|
@@ -139,7 +139,7 @@ export const deleteBook = pikkuFunc({
|
|
|
139
139
|
A global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever _narrow_ access — it never grants access a function's own `permissions` would deny.
|
|
140
140
|
|
|
141
141
|
```typescript
|
|
142
|
-
import { addGlobalPermission } from '#pikku/
|
|
142
|
+
import { addGlobalPermission } from '#pikku/auth'
|
|
143
143
|
|
|
144
144
|
addGlobalPermission([isEmployee]) // every function now also requires an employee session
|
|
145
145
|
```
|
|
@@ -248,7 +248,7 @@ declared, inspectable, and reusable.
|
|
|
248
248
|
|
|
249
249
|
```typescript
|
|
250
250
|
// src/permissions.ts
|
|
251
|
-
import { pikkuAuth, pikkuPermission } from '#pikku/
|
|
251
|
+
import { pikkuAuth, pikkuPermission } from '#pikku/auth'
|
|
252
252
|
|
|
253
253
|
export const isVerified = pikkuAuth(
|
|
254
254
|
async (_services, session) => !!session?.emailVerified
|
|
@@ -46,7 +46,7 @@ Scenarios live in `srcDirectories` like any other function — by convention `*.
|
|
|
46
46
|
`pikkuScenario` comes from the **generated** workflow types, not `@pikku/core`:
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
|
-
import { pikkuScenario } from '#pikku/
|
|
49
|
+
import { pikkuScenario } from '#pikku/scenarios'
|
|
50
50
|
|
|
51
51
|
export const orderSupportScenario = pikkuScenario<
|
|
52
52
|
{ value?: number },
|
|
@@ -175,7 +175,7 @@ Hooks are scenario-only. A `before`/`after` on a `pikkuWorkflowFunc` never runs
|
|
|
175
175
|
`pikkuFeature` groups scenarios the way gherkin's `Feature:` groups `Scenario:`. Scenarios are referenced by **imported identifier**, so a renamed or deleted scenario is a compile error rather than a silent skip:
|
|
176
176
|
|
|
177
177
|
```typescript
|
|
178
|
-
import { pikkuFeature } from '#pikku/
|
|
178
|
+
import { pikkuFeature } from '#pikku/scenarios'
|
|
179
179
|
import {
|
|
180
180
|
credentialLazyLoadScenario,
|
|
181
181
|
credentialRoundTripScenario,
|
|
@@ -240,7 +240,7 @@ Utilities are **not steps**. They are plain exported functions, they take the br
|
|
|
240
240
|
|
|
241
241
|
```typescript
|
|
242
242
|
// shop.browser.ts — shared actions. Not steps: nothing here is an intent.
|
|
243
|
-
import type { PikkuBrowserWire } from '#pikku/
|
|
243
|
+
import type { PikkuBrowserWire } from '#pikku/scenarios'
|
|
244
244
|
import type {} from '@pikku/playwright'
|
|
245
245
|
|
|
246
246
|
/** Arrive on the shop, from wherever the browser happens to be. */
|
|
@@ -462,7 +462,7 @@ Assertions with no possible browser witness are a different thing and should not
|
|
|
462
462
|
`scenario.do` can only name an RPC. A **step** is a named, typed unit of scenario behaviour whose body is an ordinary pikku function — so it can call several RPCs as its actor, assert, or drive a browser.
|
|
463
463
|
|
|
464
464
|
```typescript
|
|
465
|
-
import { pikkuScenarioStep } from '#pikku/
|
|
465
|
+
import { pikkuScenarioStep } from '#pikku/scenarios'
|
|
466
466
|
|
|
467
467
|
export const buysAnApple = pikkuScenarioStep<
|
|
468
468
|
{ qty: number },
|
|
@@ -62,7 +62,7 @@ Apply these via `addHTTPMiddleware` in a wirings file:
|
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
64
|
import { authBearer, authCookie, authAPIKey } from '#pikku/middleware'
|
|
65
|
-
import { addHTTPMiddleware } from '#pikku/
|
|
65
|
+
import { addHTTPMiddleware } from '#pikku/middleware'
|
|
66
66
|
|
|
67
67
|
// JWT bearer token — reads Authorization header
|
|
68
68
|
addHTTPMiddleware('*', [authBearer()])
|
|
@@ -118,7 +118,7 @@ response.
|
|
|
118
118
|
|
|
119
119
|
```typescript
|
|
120
120
|
// permissions.ts
|
|
121
|
-
import { pikkuAuth, pikkuPermission } from '#pikku/
|
|
121
|
+
import { pikkuAuth, pikkuPermission } from '#pikku/auth'
|
|
122
122
|
|
|
123
123
|
export const isAuthenticated = pikkuAuth(
|
|
124
124
|
async (_services, session) => !!session
|
|
@@ -129,7 +129,7 @@ export const isVerified = pikkuAuth(
|
|
|
129
129
|
|
|
130
130
|
// wirings/auth.wiring.ts
|
|
131
131
|
import { authCookie } from '#pikku/middleware'
|
|
132
|
-
import { addHTTPMiddleware } from '#pikku/
|
|
132
|
+
import { addHTTPMiddleware } from '#pikku/middleware'
|
|
133
133
|
|
|
134
134
|
addHTTPMiddleware('*', [
|
|
135
135
|
authCookie({
|
|
@@ -38,7 +38,7 @@ pikku info tags --verbose # Understand project organization
|
|
|
38
38
|
### `pikkuServices(factory)` — singleton services (created once at startup)
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
import { pikkuServices } from '#pikku/
|
|
41
|
+
import { pikkuServices } from '#pikku/setup'
|
|
42
42
|
import { ConsoleLogger } from '@pikku/core/services'
|
|
43
43
|
import { JoseJWTService } from '@pikku/jose'
|
|
44
44
|
|
|
@@ -61,7 +61,7 @@ export const createSingletonServices = pikkuServices(
|
|
|
61
61
|
### `pikkuWireServices(factory)` — per-request services (fresh per HTTP request, queue job, CLI command, etc.)
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
|
-
import { pikkuWireServices } from '#pikku/
|
|
64
|
+
import { pikkuWireServices } from '#pikku/setup'
|
|
65
65
|
|
|
66
66
|
export const createWireServices = pikkuWireServices(
|
|
67
67
|
async (singletonServices, wire) => {
|
|
@@ -240,7 +240,7 @@ const createSingletonServices = pikkuServices(async (config) => {
|
|
|
240
240
|
|
|
241
241
|
```typescript
|
|
242
242
|
// services.ts
|
|
243
|
-
import { pikkuServices, pikkuWireServices } from '#pikku/
|
|
243
|
+
import { pikkuServices, pikkuWireServices } from '#pikku/setup'
|
|
244
244
|
import { ConsoleLogger } from '@pikku/core/services'
|
|
245
245
|
import { JoseJWTService } from '@pikku/jose'
|
|
246
246
|
|