@pikku/skills 0.12.37 → 0.12.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.37",
3
+ "version": "0.12.39",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -359,6 +359,36 @@ export const myFunc = pikkuFunc({
359
359
  })
360
360
  ```
361
361
 
362
+ ### Wrap an addon function only to reshape it
363
+
364
+ A screen that shows the addon's data as the addon returns it calls the addon
365
+ function itself: name it in `wireAddon({ expose: ['listTodos'], auth: true })`
366
+ and the frontend calls `rpc.invoke('todos:listTodos', …)` (over HTTP,
367
+ `POST /rpc/todos:listTodos` with `{ "data": … }`), still behind the session.
368
+ Use `ref('todos:listTodos')` on an HTTP or MCP wiring only when the addon needs
369
+ a route of its own. Don't write an app function that calls `rpc.invoke` and
370
+ returns the result unchanged: it's a second name and a second schema for the
371
+ same thing, and it drifts. `expose: true` exposes only what the addon itself
372
+ declared `expose: true` — an OpenAPI-generated addon declares none, so list
373
+ the names.
374
+
375
+ Write your own function when the app needs the data narrowed, typed, or
376
+ combined (a flag the upstream sends as `"0"`, a total summed from several
377
+ calls, one field out of fifty), or when the app adds a permission of its own.
378
+ `wireAddon`'s `scopes` gate every function in the addon at once, and a wiring
379
+ carries middleware, not permissions, so a rule on one addon function lives in
380
+ the `permissions` of an app function that calls it. An addon called with the
381
+ user's own credential is already limited upstream to what that user may do, so
382
+ a data-aware `pikkuPermission` repeating that check (may they read *this*
383
+ invoice?) adds nothing. A session-only `pikkuAuth` (a role, a tier) is still
384
+ worth it: it can be checked before any input exists, so the functions a user
385
+ can't call drop out of the tools an MCP client, an agent or a workflow is
386
+ offered, instead of failing upstream when called. Name it for what the screen means
387
+ (`getMyProfile`), not after the upstream operation (`usersRetrieveInfo`), and
388
+ give it an `output:` schema of only what the app uses. For an OpenAPI-generated
389
+ addon this matters more: its outputs mirror the upstream's loose, oversized
390
+ payloads, and the wrapper is where they become the app's own shape.
391
+
362
392
  ### Wire to HTTP
363
393
 
364
394
  ```typescript
@@ -13,67 +13,81 @@ A JSON or YAML document with a top-level `openapi` key (3.x) or `swagger` key
13
13
  (2.0), and a `paths` object. A URL ending in `openapi.json`, `swagger.json` or
14
14
  `.yaml` is almost always one; open it and check the key before generating.
15
15
 
16
- ## 1 — Put the spec in the repo
16
+ ## 1 — Look at the spec first
17
17
 
18
- `--openapi` reads a local path, not a URL. Download it to `specs/`, where it
19
- stays as the record of what the addon was generated from:
18
+ `--openapi` takes a path or a URL. A spec published only to signed-in callers
19
+ takes the key the way the API reads it: `--openapi-header "NAME: value"`
20
+ (repeatable), or in the URL's query string when the API reads it there
21
+ (Dolibarr's explorer takes `?DOLAPIKEY=`). A 401 while fetching says which.
20
22
 
21
- ```bash
22
- mkdir -p specs
23
- curl -fsSL <url> -o specs/<name>.openapi.json
24
- ```
23
+ The generator warns loudly when the spec has fewer than five operations, or only
24
+ auth routes. That is almost always the public half of a spec that shows more
25
+ to an authenticated caller — fetch it again with the key, don't build on it.
25
26
 
26
- ## 2 — Generate
27
+ Keep a copy in `specs/` as the record of what the addon was generated from.
27
28
 
28
- Inside an app, the addon is a workspace package under `packages/`:
29
+ ## 2 — Generate, from the app's root
29
30
 
30
31
  ```bash
31
- bunx --bun pikku new addon <name> --openapi specs/<name>.openapi.json --dir packages
32
+ bunx --bun pikku new addon <name> --openapi <path-or-url>
32
33
  ```
33
34
 
34
- This writes `packages/addon-<name>` as `@pikku/addon-<name>`, installs it and
35
- builds it. Inside a workspace the generated test app depends on it as
36
- `workspace:*`, so nothing is published.
37
-
38
- | Flag | When |
39
- | ----------------------------- | ----------------------------------------------------------------------------------------- |
40
- | `--credential apikey\|bearer` | The spec's `securitySchemes` is an API key or a bearer token — each user brings their own |
41
- | `--credential oauth2` | The spec's `securitySchemes` is OAuth2 |
42
- | `--auth-config <file>` | The spec gets auth wrong or leaves it out: a custom header, a delegated login endpoint |
43
- | `--mcp` | The operations should also be MCP tools |
44
- | `--camel-case` | The API's property names are snake_case and the app's are not |
45
-
46
- Read the spec's `securitySchemes` to pick the credential. Leave the flag off
47
- only when the API really takes no auth.
35
+ One command. Inside an app it writes `packages/addon-<name>` as
36
+ `@pikku/addon-<name>`, then installs it into the app:
37
+
38
+ - the dependency in the root and the functions `package.json`
39
+ - `src/addons/<name>.addon.ts` — `wireAddon` with `auth: true` and an explicit
40
+ `expose` list (every operation in per-user modes, only the `GET`s behind a
41
+ shared secret)
42
+ - the auth wiring in `src/auth.ts` for the chosen mode
43
+ - `<NAME>_BASE_URL` in `.env`
44
+
45
+ and then runs install and the addon's build. `--no-install` generates the
46
+ package alone.
47
+
48
+ | Flag | When |
49
+ | -------------------------------------------- | -------------------------------------------------------------------------------- |
50
+ | `--auth user` (default) | Each user brings their own credential |
51
+ | `--auth shared` | One secret behind every user; locally it goes in `.env` |
52
+ | `--auth none` | The API really takes no auth |
53
+ | `--credential apikey\|bearer\|basic\|oauth2` | Override what the spec's `securitySchemes` declares |
54
+ | `--auth-config <file>` | Users sign in with their upstream login, or the spec gets auth wrong |
55
+ | `--tags a,b` / `--include` / `--exclude` | Keep part of a huge spec: tags, or globs on operationId, `/path`, `METHOD /path` |
56
+ | `--mcp` | The operations should also be MCP tools |
57
+ | `--camel-case` | The API's property names are snake_case and the app's are not |
58
+
59
+ The mode comes from the spec unless a flag says otherwise. A spec with no
60
+ machine-readable auth is refused rather than guessed: pass one of the flags the
61
+ error names. Which mode fits, and the auth-config format, are in the
62
+ `pikku-build` skill's `references/openapi.md`.
48
63
 
49
64
  ## 3 — Check what was generated
50
65
 
51
66
  ```
52
67
  packages/addon-<name>/
68
+ ├── <name>.svg # placeholder icon; replace with the real one
53
69
  ├── src/<name>-api.service.ts # one fetch wrapper, reads <NAME>_BASE_URL
54
- ├── src/<name>.variable.ts # <NAME>_BASE_URL, an enum of the spec's servers
70
+ ├── src/<name>.variable.ts # <NAME>_BASE_URL: z.string().url(), the first server as default
55
71
  ├── src/functions/<op>.function.ts
56
72
  ├── src/functions/<op>.schemas.ts # the op's zod schemas — never import #pikku here
57
73
  └── src/index.ts # re-exports every function
58
74
  ```
59
75
 
60
- `<NAME>_BASE_URL` is an enum of the spec's `servers`. When those are
61
- placeholders or a per-tenant host (`https://{tenant}.example.com`, or a server
62
- list that is only an example), change its schema in `src/<name>.variable.ts`
63
- to `z.string().url()` so each deployment sets its own.
64
-
65
- ## 4 — Wire it into the app
76
+ An operation whose spec gives no response, or one too vague to validate
77
+ against, outputs `z.unknown()`. Tighten it in `<op>.schemas.ts` once §6 shows
78
+ what the API really returns.
66
79
 
67
- ```typescript
68
- // src/addons.ts
69
- import { wireAddon } from '#pikku/addon'
80
+ An upstream 401 on a per-user credential throws `CredentialRejectedError`
81
+ (403, `reauth: 'sign-in' | 'connect'`); a UI shows the matching screen again
82
+ rather than a generic error.
70
83
 
71
- wireAddon({ name: '<name>', package: '@pikku/addon-<name>' })
72
- ```
84
+ ## 4 — Call it from the app
73
85
 
74
- Then call operations by reference — `ref('<name>:<operationFn>')` in a
86
+ Operations are reached by reference — `ref('<name>:<operationFn>')` in a
75
87
  workflow, agent tool or HTTP wiring — the same way as any other addon. See
76
- "Consuming an Addon" in the skill.
88
+ "Consuming an Addon" in the skill. An exposed operation is also callable from
89
+ the frontend at `POST /rpc/<name>:<operationFn>` with a body of
90
+ `{ "data": { … } }`, as the signed-in user.
77
91
 
78
92
  ## 5 — Verify
79
93
 
@@ -92,6 +106,23 @@ another shared package. Codegen then reads the app's
92
106
  schemas with the wrong copy and fails on schemas that are correct. Pin one
93
107
  version for the whole install, as the finding says, and reinstall.
94
108
 
109
+ ## 6 — Check the spec against the real API
110
+
111
+ Specs are often wrong, and the generated schemas repeat every mistake. Before
112
+ building on the addon:
113
+
114
+ - **Call the reads you can.** The `GET`s the credential can reach, following ids
115
+ from lists into retrieves. Writes only if the user opts in.
116
+ - **Fix the addon, not the app**: the schema in the op's `<op>.schemas.ts`, or the
117
+ request shape in `src/<name>-api.service.ts`. Then rebuild it.
118
+ - **List each mismatch in `packages/addon-<name>/SPEC-ISSUES.md`**: a title and a
119
+ short description. No credentials or customer data.
120
+
121
+ Then tell the user in one line: "FYI, the spec deviates from the real API in
122
+ N ways: [SPEC-ISSUES.md](…)". Add to the file whenever a later call disagrees
123
+ with its schema. Ask before sending it to the API's maintainers, because an issue
124
+ on their tracker is a public post.
125
+
95
126
  ## Then
96
127
 
97
128
  Go back to the mode you were building in (`pikku-build`). The addon is a
@@ -307,7 +307,7 @@ singleton a 403 that leaves no platform user behind.
307
307
 
308
308
  ```typescript
309
309
  pikkuDelegatedAuth({
310
- authenticate: async ({ email, password, apiKey }) => upstream.login(...),
310
+ authenticate: async ({ login, email, password, apiKey }) => upstream.login(...),
311
311
  storeCredential: (userId, identity) =>
312
312
  credentialService.set('acme', identity.credential, userId),
313
313
  defaultRole: 'member',
@@ -318,7 +318,10 @@ pikkuDelegatedAuth({
318
318
  ```
319
319
 
320
320
  `POST /sign-in/delegated` forwards the credentials the user already has to
321
- `authenticate`. On success it JIT-provisions a real user row (email-keyed and
321
+ `authenticate`. The body takes `email`, `login` or `username` with `password`
322
+ (or `apiKey`); whichever identifier was sent reaches `authenticate` as
323
+ `credentials.login`, and `email` as well when it was one. Upstreams that sign in
324
+ with a username — most ERPs — need nothing more. On success it JIT-provisions a real user row (email-keyed and
322
325
  `emailVerified` — the upstream just verified them), links it via an `account`
323
326
  row (`providerId: 'delegated'`, `accountId: externalId`), persists the upstream
324
327
  token **before** minting the session, and returns a normal session cookie.
@@ -333,6 +336,18 @@ a warning and the user still gets in.
333
336
  `storeCredential` failing, by contrast, **fails the sign-in**: every proxied
334
337
  call would be dead anyway.
335
338
 
339
+ An upstream user with no email gets one made up from the login, and the
340
+ identity says so with `syntheticEmail: true`. A made-up address never links to
341
+ an existing user row, so it cannot take over someone else's account.
342
+
343
+ For an addon generated from an OpenAPI spec, none of this is written by hand:
344
+ `pikku new addon --openapi … --auth-config <file>` generates
345
+ `authenticate<Name>Upstream` in the addon and wires this plugin, the stored
346
+ credential and the actor credentials into `src/auth.ts`. The config format is
347
+ in the `pikku-build` skill's `references/openapi.md`. When the upstream later
348
+ refuses the stored token, the addon throws `CredentialRejectedError` (403,
349
+ `reauth: 'sign-in'`): the UI shows the sign-in again.
350
+
336
351
  #### `pikkuFabric()` — control-plane operator sign-in
337
352
 
338
353
  ```typescript
@@ -44,12 +44,14 @@ plus more effort" — it is App plus a deliberate surface checklist, so read the
44
44
  base first and follow it in full rather than blending the two into one plan.
45
45
 
46
46
  The supporting references belong to whichever mode sends you to them:
47
- `references/multi-app.md` (a second frontend), `references/design.md` (offering
48
- to mock the screens first, committing to a design direction, and judging whether
49
- the screens realise it — read before the first screen is built, not after the
50
- last), `references/theming.md`
47
+ `references/multi-app.md` (a second frontend), `references/design.md` (showing
48
+ a picture of the screens first, committing to a design direction, and judging
49
+ whether the screens realise it — read before the first screen is built, not
50
+ after the last), `references/theming.md`
51
51
  (authoring the theme),
52
- `references/ship.md` (deploying, and the Fabric-readiness contract).
52
+ `references/ship.md` (deploying, and the Fabric-readiness contract),
53
+ `references/openapi.md` (an app on an OpenAPI spec: the auth mode, the
54
+ auth-config format, and the sign-in or connect screen it implies).
53
55
 
54
56
  ## Bootstrap before anything else
55
57
 
@@ -67,10 +69,10 @@ while still planning. Those failures look alarming and are nothing but this.
67
69
  When the request comes with a file or a URL, look at it before planning
68
70
  anything. Two kinds are converted first and then built on:
69
71
 
70
- | Handed | Say, then do |
71
- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
72
- | An **OpenAPI / Swagger spec** — top-level `openapi` or `swagger` key, a `paths` object | "This is an OpenAPI spec — I'll turn it into an addon first." Follow the `pikku-addon` skill's OpenAPI reference. |
73
- | An **n8n export** — an object with `nodes` and `connections`, an array of them, or a `{ workflows: [...] }` wrapper | "This is an n8n workflow — I'll import it first." Follow `pikku-n8n-import`. |
72
+ | Handed | Say, then do |
73
+ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | An **OpenAPI / Swagger spec** — top-level `openapi` or `swagger` key, a `paths` object | "This is an OpenAPI spec — I'll turn it into an addon first." Pick the auth mode in `references/openapi.md`, then follow the `pikku-addon` skill's OpenAPI reference. |
75
+ | An **n8n export** — an object with `nodes` and `connections`, an array of them, or a `{ workflows: [...] }` wrapper | "This is an n8n workflow — I'll import it first." Follow `pikku-n8n-import`. |
74
76
 
75
77
  Say it at once, in one line, and start: this is the obvious first move, not a
76
78
  question for the user. Generate the whole spec, however large.
@@ -103,6 +105,75 @@ generated functions through `ref()`.
103
105
  `pikku knowledge plan progress` measures the build against it from the
104
106
  generated meta. You plan it and you build it; what you never do is edit the
105
107
  plan afterwards to match what you built — that is grading yourself.
108
+ - **Print the links whenever the stack comes up, and in every hand-over.** Full,
109
+ clickable URLs, with the ports taken from what `bun run dev` actually printed:
110
+ - **App** — the frontend's URL (`http://localhost:7104` in the template; each
111
+ frontend in `pikkufabric.config.json` has its own port)
112
+ - **API** — `http://localhost:3000`
113
+ - **Console** — `http://localhost:3000/console`, plus a deep link to each
114
+ page that shows what this turn produced (the paths are below)
115
+
116
+ A person who has to go hunting for the port assumes the app did not start.
117
+
118
+ ## Keep a BUILD-REPORT.md
119
+
120
+ Whenever pikku or a skill costs you time — a command that failed on a fresh
121
+ tree, a skill that described a flag the CLI does not have, generated code you
122
+ had to fix by hand — add an entry to `BUILD-REPORT.md` at the repo root as it
123
+ happens: what you ran, what you expected, what happened, and the workaround.
124
+ Leave out secrets, tokens and customer data.
125
+
126
+ At hand-over, show the file and ask the user whether to send it. Only with
127
+ their okay, send each entry with `pikku fabric report --stdin` (JSON on stdin;
128
+ `"kind": "product"` when pikku behaved wrongly, `"kind": "harness"` with
129
+ `"skill"` and `"passage"` when a skill misled you). The `pikku-report` skill
130
+ has the fields. When the CLI is not signed in to Fabric, the report is queued
131
+ locally rather than sent: say so, and that `pikku fabric findings flush` sends
132
+ the queue once they sign in. Do not retry or file it twice.
133
+
134
+ ## Who you are talking to
135
+
136
+ The prompt asks first how technical the person is: **not technical**,
137
+ **technical, no code**, or **developer** (the default when unsaid). Whenever the
138
+ build makes or changes something the console can show, give the
139
+ `http://localhost:<port>/console/...` link instead of describing it.
140
+
141
+ Until the app is deployed that is the local open-source console, on the port
142
+ `pikku dev` printed. Once it is on Fabric, link the Fabric console for the stage
143
+ you are talking about instead; the `pikku-fabric` skill says which.
144
+
145
+ | Level | Links | Code in the conversation |
146
+ | ------------------ | --------------------------- | ------------------------ |
147
+ | Not technical | Product pages only | Never |
148
+ | Technical, no code | Product and technical pages | Never |
149
+ | Developer | Product and technical pages | As normal |
150
+
151
+ "Never" includes snippets and command lines; say what changed in the person's
152
+ words and link to where they can see it.
153
+
154
+ **Product pages** — the only ones a non-technical person gets:
155
+
156
+ | Shows | Path |
157
+ | --------------------- | -------------------------------------------------------------------------- |
158
+ | Knowledge, plans | `/console/knowledge`, `/console/knowledge?id=<note path>` |
159
+ | Personas | `/console/personas`, `/console/virtual-users?persona=<id>` |
160
+ | Roles and permissions | `/console/roles`, `/console/scopes` (only with `@pikku/addon-admin` wired) |
161
+ | Scenarios and runs | `/console/scenarios?id=<id>`, `/console/scenarios?view=runs&run=<run id>` |
162
+ | Workflows | `/console/workflow?id=<id>` |
163
+ | Agents | `/console/agents`, `/console/agents/playground?id=<agent id>` |
164
+
165
+ **Technical pages** — never for a non-technical person: `/console/overview`,
166
+ `/console/functions`, `/console/surface`, `/console/database`,
167
+ `/console/changes`, `/console/wires/http`, `/console/wires/channel`,
168
+ `/console/wires/mcp`, `/console/wires/cli`, `/console/wires/gateway`,
169
+ `/console/async/scheduler`, `/console/async/queue`, `/console/async/trigger`,
170
+ `/console/runtime`, `/console/emails`, `/console/webhooks`, `/console/secrets`,
171
+ `/console/variables`, `/console/security`, `/console/auth-providers`,
172
+ `/console/addons`, `/console/analytics`, `/console/credentials`,
173
+ `/console/users`, `/console/audit`, `/console/flags`, `/console/scorers`.
174
+
175
+ These come from `packages/console/src/App.tsx`; do not link a path that is not
176
+ listed here.
106
177
 
107
178
  ## What NOT to do
108
179
 
@@ -24,6 +24,9 @@ project shaped so `pikku fabric init` later adopts it with zero rework.
24
24
  wirings, schemas or generated clients may have changed.
25
25
  4. If validation fails, fix the source cause and rerun. Do not paper over
26
26
  generated errors by editing generated files.
27
+ 5. Report at the person's level (SKILL.md, "Who you are talking to"): a console
28
+ link for everything the console can show, and no code unless they are a
29
+ developer.
27
30
 
28
31
  ## 0. Bootstrap, before anything else
29
32
 
@@ -62,11 +65,18 @@ in one message. Then stop; do not interview the user.
62
65
  a reference (brand guide, screenshots, a site whose register they want); or
63
66
  their own design agent/prompt, whose output you take as the direction.
64
67
  - **Do they want to see the screens before you build them?** Offer it here, in
65
- this same round, as a question and not a gate: one HTML page mocking the main
66
- screens, a few minutes, far cheaper to change than built screens. If they say
67
- yes, `references/design.md` owns what to make and what it then binds — the
68
+ this same round, with yes marked recommended, in the words of
69
+ `references/design.md` — "a picture of the main screens so you can say 'yes,
70
+ like that' or 'no, move this'", never "mock" or "wireframe". Behind it is one
71
+ HTML page mocking the main screens, a few minutes of work. On a yes, or no
72
+ answer at all, `references/design.md` owns what to make and what it then binds — the
68
73
  approved page becomes source of truth for the screens, and the theme is written
69
- before it so what they approve is what ships. If they say no, build.
74
+ before it so what they approve is what ships. Only an explicit no skips it.
75
+ - **May I write test records into the system it talks to?** Ask only when the
76
+ app reads a live system through an addon (an ERP, a CRM) and a milestone needs
77
+ data that isn't there yet: an unpaid invoice, a closed ticket. Say what you
78
+ would create and that it will be marked "Test". A no means building those
79
+ screens against their empty states.
70
80
  - **What language should the app speak, and what language does the team work
71
81
  in?** Two answers, not one — see §1a, which is where they go. Ask only if the
72
82
  request is not obviously English; a brief written in English about an English
@@ -343,8 +353,9 @@ What a milestone is:
343
353
  persona. If you cannot write the gherkin, you cannot build it yet — that is a
344
354
  `questions/` note, not a milestone.
345
355
 
346
- If §1's screen mock was made and approved, the milestones are read off it: every
347
- screen on that page belongs to some milestone, and a screen no milestone builds
356
+ If §1's screen mock was made — approved, or drawn because nobody answered — the
357
+ milestones are read off it: every screen on that page belongs to some milestone,
358
+ and a screen no milestone builds
348
359
  is a hole in this plan. Say which milestone covers which screen.
349
360
 
350
361
  How to order them:
@@ -363,8 +374,17 @@ How to order them:
363
374
  Number the files (`01-…`, `02-…`) so the order is visible in the tree. Then
364
375
  `knowledge index && knowledge validate` before you write a line of code.
365
376
 
366
- **Show the user the list before building.** This is the last cheap moment to
367
- reorder — after §6 the migrations are numbered and the order is concrete.
377
+ **One approval, then build to the end.** Show the picture of the screens and
378
+ the milestone list together, in one message, as the plan: which milestone builds
379
+ which screen, in what order. That is the only approval you ask for. It is the
380
+ last cheap moment to reorder: after §6 the migrations are numbered and the order
381
+ is concrete.
382
+
383
+ Once they approve it, or don't answer, build every milestone in order without
384
+ stopping to ask between them. Post one line as each milestone closes, with its
385
+ console links, and carry on. Stop only for what is theirs to decide: a
386
+ credential you don't have, spending money, posting in public, deleting or
387
+ overwriting their data, or a finding that changes the plan.
368
388
 
369
389
  ## 5a. The technical plan — one milestone at a time, before you build it
370
390
 
@@ -399,6 +419,8 @@ no plan, and everything after the current milestone is still allowed to move.
399
419
 
400
420
  ## 6. Implement milestones, one at a time
401
421
 
422
+ All of them, one after another, on the one approval from §5.
423
+
402
424
  **Per milestone** — plan it (§5a), set its note to `status: dispatched`, do the
403
425
  six steps, close it out (§6a), set it to `built`. Do not start the next one
404
426
  until §6a passes, §7 is green for this one _and §7a shows its functions
@@ -498,8 +520,8 @@ Rules that are not optional:
498
520
  `pikkuSessionlessFunc`. `pikkuFunc` with `auth: false` still answers
499
521
  `MissingSessionError` over `/rpc` to a caller with no session.
500
522
  - Better Auth already owns the `user`, `session`, `account` and `verification`
501
- tables. A domain table with one of those names — a class *session*, a drop-in
502
- *session* — collides in the migration. Name it for the domain instead
523
+ tables. A domain table with one of those names — a class _session_, a drop-in
524
+ _session_ — collides in the migration. Name it for the domain instead
503
525
  (`evening`, `class_meeting`) and keep the word in the UI copy.
504
526
  - The template's `/` redirects to `/app`, so the login screen — and its "Sign in
505
527
  as …" switcher — is what a signed-out visitor sees first. Replace `/` with a
@@ -544,7 +566,7 @@ start the frontend on its own (say :3000 is taken by another project), you owe
544
566
  it three things: the two `VITE_DEV_*` values the dev script would have computed,
545
567
  and `VITE_API_PROXY` pointing at your API — the dev proxy defaults to
546
568
  `http://localhost:3000`, so beside another project's server your sign-ins go to
547
- *its* API and come back `401 Invalid actor secret`, which reads like a bad
569
+ _its_ API and come back `401 Invalid actor secret`, which reads like a bad
548
570
  credential rather than the wrong server.
549
571
 
550
572
  The `--bun` in `bunx --bun pikku …` is load-bearing — keep it. Without it the
@@ -54,13 +54,18 @@ allowed is landing on one because it was nearest to hand.
54
54
  ## Offer to draw the screens before you build them
55
55
 
56
56
  Before the first milestone, **ask** whether they want to see the screens first.
57
- One question, in §1's round, not a gate of its own:
57
+ One question, in §1's round, not a gate of its own, with yes marked
58
+ recommended every time:
58
59
 
59
- > Want me to mock the main screens as a page you can look at before I build
60
- > anything? It takes a few minutes and it is much cheaper to change a picture
61
- > than a built screen.
60
+ > Before I build, shall I show you a picture of the main screens so you can say
61
+ > "yes, like that" or "no, move this"? (Recommended: it takes a few minutes and
62
+ > changing a picture is much cheaper than changing a built app.)
62
63
 
63
- If they decline, build; the direction in words is enough to be accountable to.
64
+ Never say "mock", "mockup" or "wireframe" to the person; most people do not
65
+ know the words. They stay the technical terms in this file only.
66
+
67
+ If they do not answer, draw it anyway and build from it. Only an explicit no
68
+ skips it; then build, and the direction in words is enough to be accountable to.
64
69
  If they accept, this is the cheapest decision in the project — a picture of eight
65
70
  screens costs a fraction of eight built screens, and it is the only point where
66
71
  "that is not what I meant" is free.
@@ -105,6 +110,12 @@ first. Whatever your host offers for showing a page is how you show it: an
105
110
  Artifact, a file they open, a preview server. The page is the deliverable; how it
106
111
  gets in front of them is not this file's business.
107
112
 
113
+ **Say it is a picture, on the page and in the message.** A well-drawn screen
114
+ reads as a finished app, and a person who thinks it is already built asks why
115
+ nothing works. The page opens with a banner that stays in view: "A picture of
116
+ the planned screens. Nothing is built yet." The message that shows it says the
117
+ same, then says what happens next: "Once you're happy with it, I'll build it."
118
+
108
119
  Write it to `knowledge/decisions/design/screens.html` and treat it as **source of
109
120
  truth for the screens** once they approve it. That has consequences worth
110
121
  stating:
@@ -63,86 +63,61 @@ Worth a scenario each, because they are two different claims: that a mechanic ca
63
63
  *see* the invoices nav item, and that their call to an invoices RPC is *refused*. The
64
64
  second is the one that catches a `permissions` field nobody wired.
65
65
 
66
- ## The clone
66
+ ## The second app
67
67
 
68
68
  ```bash
69
- cp -R apps/app apps/admin
70
- rm -rf apps/admin/node_modules apps/admin/src/paraglide
69
+ pikku new app admin --serves staff --personas manager,mechanic
71
70
  ```
72
71
 
73
- `src/paraglide` is compiled from `messages/` by the Vite plugin on first run;
74
- copying it forward ships one app's compiled strings inside another.
75
-
76
- Then, in order:
77
-
78
- ### 1. `apps/admin/package.json`
79
-
80
- - `name` → `@project/admin`
81
- - `dev` and `preview` ports → `7105`. Every frontend needs its own, or the second
82
- one fails to bind and the dev runner looks like it hung.
83
- - the `--tsBuildInfoFile` path inside the **`tsc` script** → `admin-tsc.tsbuildinfo`.
84
- In this template it is a CLI flag on that script (`tsc --noEmit --incremental
85
- --tsBuildInfoFile node_modules/.cache/app-tsc.tsbuildinfo`), not a
86
- `compilerOptions` entry — `tsconfig.json` needs no change. Left alone, the two
87
- apps fight over one incremental cache and you get type errors that vanish on a
88
- clean build: an hour of debugging for a one-word edit.
89
-
90
- ### 2. `pikkufabric.config.json`
91
-
92
- This file is the source of truth for what apps exist, and it is read whether or
93
- not you ever deploy to Fabric.
94
-
95
- ```json
96
- {
97
- "projectId": "__PROJECT_ID__",
98
- "frontends": {
99
- "app": {
100
- "cwd": "apps/app", "primary": true, "deploy": true, "kind": "ssr",
101
- "dev": { "command": ["bun", "run", "dev"], "port": 7104, "healthPath": "/" },
102
- "serves": "tenant",
103
- "personas": ["visitor", "chidi"]
104
- },
105
- "admin": {
106
- "cwd": "apps/admin", "primary": false, "deploy": true, "kind": "ssr",
107
- "dev": { "command": ["bun", "run", "dev"], "port": 7105, "healthPath": "/" },
108
- "serves": "owner",
109
- "personas": ["amina", "bilal"]
110
- }
111
- }
112
- }
113
- ```
114
-
115
- Two things to fix while you are in here, not just add:
116
-
117
- - **The shipped `app` entry may say `["yarn", "dev"]`** while the rest of the
118
- project is driven with bun. Correct it. A frontend that starts under a package
119
- manager the project does not use is a failure that only appears on someone
120
- else's machine.
121
- - **`serves` and `personas` name real personas.** Every persona should appear
122
- under exactly one frontend. A persona listed nowhere is a person with no way
123
- in, and that is a design bug worth seeing now rather than at review.
124
-
125
- ### 3. The dev runner
126
-
127
- `dev.mjs`, under the project's scripts directory, spawns `@project/app` **by
128
- name** and will silently never start your second app — the frontend simply is not
129
- there, with no error to explain it.
130
-
131
- Make it read the `frontends` map and spawn one child per entry, rather than
132
- adding a second hardcoded line. Two sources of truth for "which apps exist" is
133
- the drift this whole file is trying to avoid.
134
-
135
- ### 4. `pikku.config.json` → `environments`
136
-
137
- `local.appUrl` points at one app. Add an environment per frontend (`local`,
138
- `local-admin`) so the browser scenario pass can drive either one. A browser
139
- scenario run against the wrong `appUrl` fails on a missing element and reads like
140
- a UI bug rather than a config one.
141
-
142
- ### 5. Re-run `bun install`
143
-
144
- `apps/*` is already globbed in the root workspaces, so this just links the new
145
- one.
72
+ One command does every step this section used to list by hand: it fetches
73
+ `pikkujs/starter-template`'s `apps/app`, re-points its `package.json` at the
74
+ new name, its own dev/preview port and its own `--tsBuildInfoFile`, stamps
75
+ `app: '<slug>'` onto each named persona in `definePersonas({…})`, adds the
76
+ `frontends` entry, and re-runs `bun install`.
77
+
78
+ **It scaffolds from the starter template, not from the app you already have.**
79
+ Copying the working app drags its screens, routes and nav into an audience that
80
+ never asked for them, and the first hour in the new app goes on deleting
81
+ someone else's product.
82
+
83
+ `--template <source>` scaffolds from something else — any giget source, or a
84
+ path inside the repo for an offline or vendored copy. `--install false` skips
85
+ the install when you are batching several.
86
+
87
+ **The `--tsBuildInfoFile` edit is the one that used to bite.** Two apps sharing
88
+ one incremental cache produce type errors that vanish on a clean build: an hour
89
+ of debugging for a one-word edit. It is handled now, but it is why you should
90
+ not copy by hand.
91
+
92
+ ### What it refuses, and why that is the valuable part
93
+
94
+ The scaffolding is five file edits. Getting the audience wrong is a whole
95
+ second app nobody needed, so the command will not create one when:
96
+
97
+ - **`--serves` names a surface.** `dashboard`, `portal`, `admin`, `console`,
98
+ `ui` and friends say nothing — every frontend is an app. Name the people in
99
+ their own word: staff, customer, supplier, patient.
100
+ - **An existing app already serves that audience.** People sharing an audience
101
+ share ONE app and differ by nav and permitted actions. A new app is for a
102
+ group the first app is not for.
103
+ - **A named persona already signs into another app.** A person signs into one
104
+ app; move them out first if they really belong here.
105
+ - **The slug is what the plan calls an app that already exists.** The plan's
106
+ FIRST app is the one the project starts with — only the apps after it get
107
+ created.
108
+ - **A persona is not in `definePersonas({…})`.** The app is built around who
109
+ signs into it, so it is not created for people who do not exist yet.
110
+
111
+ It also repairs its own half-states: a run that died between writing the
112
+ directory and writing the config entry leaves one without the other, and
113
+ neither survives alone, so the next run clears the remains and carries on
114
+ rather than sending you in to do the surgery by hand.
115
+
116
+ ### What it does NOT do
117
+
118
+ It stops after `bun install`. Serving the new app — a reverse proxy, a
119
+ supervisor, a dev runner, a deploy target — belongs to whatever is hosting it.
120
+ On a plain checkout, `bun --filter @project/<slug> dev` is enough.
146
121
 
147
122
  ## Sessions across two origins
148
123