@pikku/skills 0.12.16 → 0.12.18
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 +51 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-build-app/SKILL.md +62 -1
- package/skills/pikku-build-platform/SKILL.md +7 -0
- package/skills/pikku-build-quick/SKILL.md +9 -0
- package/skills/pikku-concepts/SKILL.md +91 -1
- package/skills/pikku-fabric/SKILL.md +8 -2
- package/skills/pikku-feature/SKILL.md +7 -0
- package/skills/pikku-i18n/SKILL.md +77 -2
- package/skills/pikku-middleware/SKILL.md +76 -1
- package/skills/pikku-permissions/SKILL.md +4 -0
- package/skills/pikku-scenario/SKILL.md +51 -0
- package/skills/pikku-schedule/SKILL.md +23 -0
package/package.json
CHANGED
|
@@ -28,7 +28,10 @@ project shaped so `pikku fabric init` later adopts it with zero rework.
|
|
|
28
28
|
## Agent Operating Procedure
|
|
29
29
|
|
|
30
30
|
1. Discover before editing. Run `pikku info functions --verbose --silent` and
|
|
31
|
-
read `AGENTS.md` before your first change.
|
|
31
|
+
read `AGENTS.md` before your first change. Read `locale` in
|
|
32
|
+
`pikku.config.json` too: it is the language every `description`, `title` and
|
|
33
|
+
step `template` you write must be in (§1a). Identifiers stay English whatever
|
|
34
|
+
it says.
|
|
32
35
|
2. Make the smallest source change that satisfies the task. Keep generated files
|
|
33
36
|
generated — never hand-edit `.pikku/`, `*.gen.*`, or the SDK.
|
|
34
37
|
3. Validate with the narrowest relevant command, then `pikku all` when functions,
|
|
@@ -72,10 +75,68 @@ in one message. Then stop; do not interview the user.
|
|
|
72
75
|
Neutral (fine for an internal tool, but say so out loud); a direction in words;
|
|
73
76
|
a reference (brand guide, screenshots, a site whose register they want); or
|
|
74
77
|
their own design agent/prompt, whose output you take as the direction.
|
|
78
|
+
- **What language should the app speak, and what language does the team work
|
|
79
|
+
in?** Two answers, not one — see §1a, which is where they go. Ask only if the
|
|
80
|
+
request is not obviously English; a brief written in English about an English
|
|
81
|
+
product answers both.
|
|
75
82
|
|
|
76
83
|
Skip anything you can decide yourself. If nobody answers, assume one app with
|
|
77
84
|
paths, the roles implied by the request, Neutral, English — and say so.
|
|
78
85
|
|
|
86
|
+
## 1a. Three languages, and you must not collapse them
|
|
87
|
+
|
|
88
|
+
A brief saying "the entire UI is German" is about **one** of these. Getting this
|
|
89
|
+
wrong has already shipped a project that can never add a second language, so
|
|
90
|
+
settle all three explicitly before you write code.
|
|
91
|
+
|
|
92
|
+
| Axis | What it covers | Where it goes |
|
|
93
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
94
|
+
| **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages. | Nowhere — **always English**, no setting, not negotiable |
|
|
95
|
+
| **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions | `locale` in `pikku.config.json`, default `en` |
|
|
96
|
+
| **Product UI** | Every string the app shows a user | `messages/<locale>.json`, and `defaultLocale` for what a first-time visitor opens in |
|
|
97
|
+
|
|
98
|
+
**Identifiers are English.** The product's market does not change this and
|
|
99
|
+
neither does `locale`. Identifiers are the surface the generated `#pikku/*`
|
|
100
|
+
clients, `pikku info`, the typed RPC map and the Kysely types all bind to, and
|
|
101
|
+
unlike a string an identifier cannot be translated later — renaming one is a
|
|
102
|
+
migration. A German practice management tool gets `getWorklist`, `case`,
|
|
103
|
+
`event`, not `getUebersicht`, `vorgang`, `ereignis`.
|
|
104
|
+
|
|
105
|
+
**Meta follows `locale`.** Write the team's answer into `pikku.config.json` in
|
|
106
|
+
this phase, before there is any meta to be wrong:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "locale": "de" }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
It exists for the Pikku Console. Meta is the one part of a project the Console
|
|
113
|
+
renders back to a human, so a team working in German reads their own functions,
|
|
114
|
+
features and scenario reports in German. Default `en` and do not ask when the
|
|
115
|
+
project is obviously English. **On every later run, read this field first and
|
|
116
|
+
author descriptions, titles and step templates in it** — a project whose
|
|
117
|
+
`locale` you ignored reports half in one language and half in another.
|
|
118
|
+
|
|
119
|
+
**Product UI is the message catalogue.** `messages/<locale>.json` via
|
|
120
|
+
`pikku-i18n`, with `defaultLocale` deciding what a visitor opens in.
|
|
121
|
+
`baseLocale` in `project.inlang/settings.json` **stays `en`**: it names the
|
|
122
|
+
message source, the catalogue every other language is cloned from, so a project
|
|
123
|
+
that repoints it has nothing to translate from and `--add-locale` is broken
|
|
124
|
+
forever.
|
|
125
|
+
|
|
126
|
+
A German medical portal, correctly:
|
|
127
|
+
|
|
128
|
+
```jsonc
|
|
129
|
+
// project.inlang/settings.json
|
|
130
|
+
{ "baseLocale": "en", "locales": ["en", "de"] }
|
|
131
|
+
// apps/app/src/i18n/active.json (or: fabric i18n --default-locale de)
|
|
132
|
+
{ "defaultLocale": "de" }
|
|
133
|
+
// pikku.config.json
|
|
134
|
+
{ "locale": "de" }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Record the two non-obvious answers as a `decisions/` note in §2 — neither is
|
|
138
|
+
discoverable from code, and the next agent will otherwise re-derive them wrong.
|
|
139
|
+
|
|
79
140
|
---
|
|
80
141
|
|
|
81
142
|
## PHASE 1 — What the app is
|
|
@@ -154,6 +154,13 @@ icons, hardcoded `marginLeft`, a nav that opens on the wrong side.
|
|
|
154
154
|
Adding a language means adding a locale file. If it means touching components,
|
|
155
155
|
that is the finding.
|
|
156
156
|
|
|
157
|
+
`baseLocale` stays `en` through all of this — it names the message source that
|
|
158
|
+
every added locale is cloned from, so three locales is `locales: ["en", …]` and
|
|
159
|
+
never a repointed base. Shipping locales is also not a reason for anything in
|
|
160
|
+
the code to stop being English: identifiers are English in every project, and
|
|
161
|
+
the language of `description`/`title`/`template` is `locale` in
|
|
162
|
+
`pikku.config.json`. See `pikku-build-app` §1a.
|
|
163
|
+
|
|
157
164
|
### Emails — `pikku-emails`
|
|
158
165
|
|
|
159
166
|
Templates in `emails/`, rendered and sent through the injected `email` service,
|
|
@@ -52,6 +52,15 @@ it — one kind of person, or several?** Everything else you decide yourself.
|
|
|
52
52
|
Do not ask about design, deployment, or scope. This is the quick mode; the
|
|
53
53
|
defaults are the point.
|
|
54
54
|
|
|
55
|
+
Do not ask about language either — take the defaults and note them in §6.
|
|
56
|
+
Identifiers are English in every project, whatever the product's market;
|
|
57
|
+
`locale` in `pikku.config.json` (the language of `description`/`title`/step
|
|
58
|
+
`template`, which the Console renders) stays `en` unless the user already told
|
|
59
|
+
you otherwise. If the request says the app's UI is not English, that is the
|
|
60
|
+
message catalogue only: add the locale and set `defaultLocale`, and leave
|
|
61
|
+
`baseLocale` at `en`. `pikku-build-app` §1a has the three axes in full; getting
|
|
62
|
+
them confused is how a project ends up unable to add a second language.
|
|
63
|
+
|
|
55
64
|
## 2. Personas — 60 seconds, not optional
|
|
56
65
|
|
|
57
66
|
`packages/functions/src/personas.ts` ships with a `visitor`. Add one persona per
|
|
@@ -138,7 +138,9 @@ Services can be destructured inline in the `func` signature (e.g. `async ({ logg
|
|
|
138
138
|
|
|
139
139
|
```typescript
|
|
140
140
|
pikkuFunc({
|
|
141
|
-
// Identity and documentation
|
|
141
|
+
// Identity and documentation — prose, so it follows `locale` in
|
|
142
|
+
// pikku.config.json (default `en`). The identifier does not; see
|
|
143
|
+
// "What Language You Write In".
|
|
142
144
|
title?: string, // Human-readable name
|
|
143
145
|
description?: string, // What the function does
|
|
144
146
|
version?: number, // Contract version (see pikku-versioning)
|
|
@@ -321,6 +323,94 @@ src/
|
|
|
321
323
|
└── pikku-bootstrap.gen.ts
|
|
322
324
|
```
|
|
323
325
|
|
|
326
|
+
## What Language You Write In
|
|
327
|
+
|
|
328
|
+
Three different things in a Pikku project have a human language, and they are
|
|
329
|
+
**not** the same language. Collapsing them is the mistake this section exists to
|
|
330
|
+
prevent, and it has already shipped in a real product — the failure is at the
|
|
331
|
+
bottom.
|
|
332
|
+
|
|
333
|
+
| Axis | What it covers | What decides it |
|
|
334
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
335
|
+
| **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. | `locale` in `pikku.config.json`. Defaults to `en`. |
|
|
337
|
+
| **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
|
+
|
|
339
|
+
### Identifiers are English, and nothing changes that
|
|
340
|
+
|
|
341
|
+
Not the product's market, not the team's working language, and **not `locale`**.
|
|
342
|
+
A German medical practice, an Arabic marketplace and a Japanese logistics tool
|
|
343
|
+
all get `getOverview`, `AttentionStripe`, `case`, `event`.
|
|
344
|
+
|
|
345
|
+
This is not linguistic preference, it is mechanics. Identifiers are the surface
|
|
346
|
+
every other tool binds to: the generated `#pikku/*` clients, `pikku info` and
|
|
347
|
+
`pikku meta`, the RPC map a scenario's `actor.invoke` is typed over, the
|
|
348
|
+
generated SQL types, every skill and every agent that ever picks the project up.
|
|
349
|
+
A `vorgang` table types as `Vorgang` in Kysely and reads as noise to everyone who
|
|
350
|
+
did not name it, and unlike a string it cannot be translated later — renaming an
|
|
351
|
+
identifier is a migration, not an edit.
|
|
352
|
+
|
|
353
|
+
### Meta follows `locale`, and that is what the field is for
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{ "locale": "de" }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Meta is the one part of a project the **Pikku Console** renders back to a human.
|
|
360
|
+
A team reviewing their own functions, features and scenario reports in the
|
|
361
|
+
Console is reading meta and nothing else, so a team whose working language is
|
|
362
|
+
German should be able to read their Console in German. That is the entire reason
|
|
363
|
+
the field exists.
|
|
364
|
+
|
|
365
|
+
Read it before you author meta, and write descriptions, titles and templates in
|
|
366
|
+
it. Absent, it is `en`. It is a BCP-47 tag (`en`, `de`, `pt-BR` — a hyphen, not
|
|
367
|
+
an underscore), and the CLI rejects anything else by name.
|
|
368
|
+
|
|
369
|
+
`locale` is **not** licence to rename anything. `locale: "de"` buys a German
|
|
370
|
+
`description: 'Zeigt die Arbeitsliste'` on a function still called
|
|
371
|
+
`getWorklist`.
|
|
372
|
+
|
|
373
|
+
### Product UI language lives in the catalogue, and only there
|
|
374
|
+
|
|
375
|
+
What the app says to its users is a translation concern, not a code concern. It
|
|
376
|
+
belongs in `messages/<locale>.json`; `pikku-i18n` owns the details. The one rule
|
|
377
|
+
worth repeating here: **`baseLocale` in `project.inlang/settings.json` stays
|
|
378
|
+
`en`.** It names the message _source_ — the catalogue every other language is
|
|
379
|
+
cloned from and translated against — so a project that sets it to anything else
|
|
380
|
+
has no English catalogue to translate from and can never gain a second language
|
|
381
|
+
without re-authoring every key.
|
|
382
|
+
|
|
383
|
+
### The failure this comes from
|
|
384
|
+
|
|
385
|
+
An agent was asked to build a doctor's portal for a German practice. The brief
|
|
386
|
+
said "the entire UI is German, no English strings visible anywhere". The agent
|
|
387
|
+
read one sentence about the product's users as an instruction about the
|
|
388
|
+
codebase, and produced:
|
|
389
|
+
|
|
390
|
+
- `project.inlang/settings.json` with `baseLocale: "de"` and `locales: ["de"]`,
|
|
391
|
+
no `en.json` at all — which silently broke `--add-locale` forever
|
|
392
|
+
- RPC functions `getUebersicht` and `getPatientendetail`
|
|
393
|
+
- React components `Zeitstrahl` and `AufmerksamkeitStreifen`
|
|
394
|
+
- database tables `vorgang` and `ereignis`, with German columns
|
|
395
|
+
|
|
396
|
+
Every one of those is wrong, and the brief was satisfied by none of them: a
|
|
397
|
+
German UI needs German _messages_. What that project actually wanted was three
|
|
398
|
+
settings, each on its own axis:
|
|
399
|
+
|
|
400
|
+
```jsonc
|
|
401
|
+
// project.inlang/settings.json — the message source stays English
|
|
402
|
+
{ "baseLocale": "en", "locales": ["en", "de"] }
|
|
403
|
+
|
|
404
|
+
// apps/app/src/i18n/active.json — what a first-time visitor opens in
|
|
405
|
+
{ "defaultLocale": "de" }
|
|
406
|
+
|
|
407
|
+
// pikku.config.json — the language the team reads their Console in
|
|
408
|
+
{ "locale": "de" }
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Identifiers stay English throughout. When a brief tells you the product speaks a
|
|
412
|
+
language, it is telling you about axis three and nothing else.
|
|
413
|
+
|
|
324
414
|
## Environment Variables
|
|
325
415
|
|
|
326
416
|
Never use `process.env` inside Pikku functions. Use the `variables` service (see `pikku-config`):
|
|
@@ -322,7 +322,12 @@ Why it parked is the whole story, and it is `statusReason`, not `status`:
|
|
|
322
322
|
accepts them for that deploy.
|
|
323
323
|
- `needs_config` — a declared secret or variable has no value covering the
|
|
324
324
|
stage. The CLI names them. `--auto-approve` will **not** force this through;
|
|
325
|
-
set the values
|
|
325
|
+
set the values and re-attach — `pikku fabric secrets set <name>` for a
|
|
326
|
+
declared secret, `pikku fabric variables set <name> --value <v>` for a declared
|
|
327
|
+
variable. They are separate stores: a secret is sealed to the stage and cannot
|
|
328
|
+
be read back, a variable is stored plainly and can (`variables get`). `set`
|
|
329
|
+
reads the value as JSON when it parses, so `--value true` is the boolean on a
|
|
330
|
+
stage exactly as it is from `.env`, and `--value '"true"'` is the string.
|
|
326
331
|
- `needs_attention` — the plan is red. Nothing to approve.
|
|
327
332
|
|
|
328
333
|
`--sync` defaults to a 900s ceiling; `--timeout <seconds>` moves it. On timeout
|
|
@@ -411,6 +416,7 @@ These apply in every Fabric app:
|
|
|
411
416
|
- **No hand-editing `.pikku/db/schema.gen.ts`** — write a migration and re-run `pikku db migrate`.
|
|
412
417
|
- **One runtime unit per file** — never define multiple functions/workflows in a single source file.
|
|
413
418
|
- **Workflow steps don't need manual wiring** — `pikkuSessionlessFunc` step functions in `*.steps.ts` files are auto-discovered by codegen.
|
|
419
|
+
- **Identifiers are English** — functions, components, types, files, database tables and columns, in every app whatever market it serves. The team's language is `locale` in `pikku.config.json` and reaches `description`/`title`/`template` only; the app's language is the message catalogue, where `baseLocale` stays `en` and `defaultLocale` decides what a visitor opens in. `pikku fabric validate` warns (`app-base-locale-not-english-<app>`) when an app repoints its base.
|
|
414
420
|
|
|
415
421
|
## Converting an existing app to Fabric format
|
|
416
422
|
|
|
@@ -426,7 +432,7 @@ Fix every `error` and `warn` in the output before continuing. Then:
|
|
|
426
432
|
2. **Replace route handlers with pikkuFuncs**: extract business logic into `pikkuFunc`/`pikkuSessionlessFunc`, add `wireHTTP` or `expose: true` for transport.
|
|
427
433
|
3. **Replace DI/IoC with pikkuServices**: move service construction to `createSingletonServices` in `services.ts`.
|
|
428
434
|
4. **Replace `process.env` calls**: plain config becomes `defineVariable` + `variables.get()`, anything sensitive becomes `defineSecret` + `secrets.getSecret()`.
|
|
429
|
-
5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles
|
|
435
|
+
5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles` — plus `locale` if the team does not work in English, which is the language every `description`, `title` and step `template` is then authored in.
|
|
430
436
|
6. **Add `pikkufabric.config.json`** at project root with `projectId`, `production.domain`, and `frontends` (production is always `main`, so there is no `production.branch`).
|
|
431
437
|
7. **Run `pikku all`** — verify codegen succeeds and there are no type errors.
|
|
432
438
|
8. **Run `pikku fabric validate`** once more to confirm no structural issues remain.
|
|
@@ -41,6 +41,13 @@ schemas (`yarn pikku meta functions get <id>`) or workflow steps
|
|
|
41
41
|
**Capability rule:** do not introduce new wires of a type whose
|
|
42
42
|
`capabilities.<type>` is `false` unless the user explicitly asked for it.
|
|
43
43
|
|
|
44
|
+
**Language rule:** read `locale` in `pikku.config.json` (default `en`). It is
|
|
45
|
+
the language of every `description`, `title` and step `template` you author —
|
|
46
|
+
the meta the Pikku Console renders back to the team. It renames nothing:
|
|
47
|
+
functions, components, types, files, tables and columns are English in every
|
|
48
|
+
project, and what the app says to its users is the message catalogue. See
|
|
49
|
+
`pikku-concepts` → _What Language You Write In_.
|
|
50
|
+
|
|
44
51
|
## Stage 2 — State intent in plain English (BEFORE writing code)
|
|
45
52
|
|
|
46
53
|
Before touching any files, give the user one paragraph stating exactly what
|
|
@@ -11,10 +11,81 @@ installGroups: [client, fabric]
|
|
|
11
11
|
Use this skill as an execution checklist, not reference material.
|
|
12
12
|
|
|
13
13
|
1. Every user-facing string in a frontend is a message. Never hardcode display text — add a key to `messages/en.json` and render `m.the__key()`. This holds even when the app ships only English; the messages are the seam a second language slots into later.
|
|
14
|
-
2. One `messages/<locale>.json` per language at the app root (NOT under `src/`), declared in `project.inlang/settings.json`. English (`en`) is `baseLocale` and the only locale until someone adds another.
|
|
14
|
+
2. One `messages/<locale>.json` per language at the app root (NOT under `src/`), declared in `project.inlang/settings.json`. English (`en`) is `baseLocale` and the only locale until someone adds another. **`baseLocale` stays `en` whatever language the product speaks** — see [The product's language is not the code's language](#the-products-language-is-not-the-codes-language), which is the first thing to read if the brief says the app is not in English.
|
|
15
15
|
3. Messages compile to typed ESM functions in `src/paraglide/` (generated, self-gitignored — never edit or commit it). The Vite plugin compiles during `dev`/`build` with HMR on message edits; run the CLI compile only when you need `tsc` before Vite has ever run.
|
|
16
16
|
4. Validate with the app's own `tsc` then its `build`. The deploy pipeline compiles Paraglide and runs each frontend's `tsc` before building it — an i18n mistake blocks the deploy.
|
|
17
17
|
|
|
18
|
+
## The product's language is not the code's language
|
|
19
|
+
|
|
20
|
+
A brief that says "the entire UI is German, no English strings visible anywhere"
|
|
21
|
+
is a statement about **one** of three separate things, and reading it as a
|
|
22
|
+
statement about the codebase is the single most expensive mistake available in
|
|
23
|
+
this skill. Three axes:
|
|
24
|
+
|
|
25
|
+
| Axis | What it covers | What sets it |
|
|
26
|
+
| --------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
27
|
+
| **Identifiers** | Function, component, type and file names; database tables and columns | Nothing — always English, no setting |
|
|
28
|
+
| **Meta** | `description` / `name` / `title` / `template` authored inside the code, which the Pikku Console renders | `locale` in `pikku.config.json`, default `en` |
|
|
29
|
+
| **Product UI** | Every string the app shows a user | `messages/<locale>.json` + `defaultLocale` — **this axis only** |
|
|
30
|
+
|
|
31
|
+
A non-English product moves the third row and nothing else.
|
|
32
|
+
|
|
33
|
+
### `baseLocale` stays `en`
|
|
34
|
+
|
|
35
|
+
`baseLocale` in `project.inlang/settings.json` does not mean "the language the
|
|
36
|
+
app is in". It names the message **source** — the catalogue every other locale is
|
|
37
|
+
cloned from and translated against. Setting it to the product's language looks
|
|
38
|
+
like it works, because the app does come up in that language, and then:
|
|
39
|
+
|
|
40
|
+
- there is no `en.json`, so `--add-locale` has no catalogue to translate from
|
|
41
|
+
- the app can never gain a second language without re-authoring every key
|
|
42
|
+
- a message missing from a locale falls back to a catalogue nobody wrote
|
|
43
|
+
|
|
44
|
+
The setting that actually decides what a first-time visitor sees is
|
|
45
|
+
`defaultLocale`, held in `apps/app/src/i18n/active.json` in the Fabric app
|
|
46
|
+
template and read by `src/i18n/config.ts`. It is deliberately a separate file
|
|
47
|
+
from `settings.json` for exactly this reason — the source language and the
|
|
48
|
+
served language are different questions.
|
|
49
|
+
|
|
50
|
+
So a German medical portal is **three** settings, not one:
|
|
51
|
+
|
|
52
|
+
```jsonc
|
|
53
|
+
// project.inlang/settings.json — the source catalogue is English
|
|
54
|
+
{ "baseLocale": "en", "locales": ["en", "de"] }
|
|
55
|
+
|
|
56
|
+
// apps/app/src/i18n/active.json — what a visitor opens in
|
|
57
|
+
{ "defaultLocale": "de" }
|
|
58
|
+
|
|
59
|
+
// pikku.config.json — the language the team reads their Console in
|
|
60
|
+
{ "locale": "de" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
In the Fabric template both of the first two have a command, so you rarely edit
|
|
64
|
+
them by hand:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
fabric i18n --add-locale de # adds "de" to locales, seeds messages/de.json from en.json
|
|
68
|
+
fabric i18n --default-locale de # writes active.json — the app now OPENS in German
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### The failure this is written from
|
|
72
|
+
|
|
73
|
+
A real build, from this template. The brief said the UI was German; the agent
|
|
74
|
+
set `baseLocale: "de"` with `locales: ["de"]` and no `en.json`, then carried the
|
|
75
|
+
same reading into the code — RPC functions `getUebersicht` and
|
|
76
|
+
`getPatientendetail`, components `Zeitstrahl` and `AufmerksamkeitStreifen`,
|
|
77
|
+
helpers `datumDeutsch` and `voraussichtlichFertig`, database tables `vorgang`
|
|
78
|
+
and `ereignis` with German columns.
|
|
79
|
+
|
|
80
|
+
The German UI it was asked for needed none of that. It needed German **values**
|
|
81
|
+
in a catalogue whose keys and source stayed English. What it got instead was a
|
|
82
|
+
project that cannot add a second language and cannot be picked up by anyone who
|
|
83
|
+
does not read German.
|
|
84
|
+
|
|
85
|
+
If you find a project in this state, say so plainly rather than working around
|
|
86
|
+
it: `baseLocale` cannot be repointed without re-keying every message, so it is a
|
|
87
|
+
migration someone has to agree to, not a fix to slip in.
|
|
88
|
+
|
|
18
89
|
## The moving parts (starter-template layout)
|
|
19
90
|
|
|
20
91
|
- `messages/en.json` — flat keys, `{param}` interpolation, inlang message-format:
|
|
@@ -109,9 +180,10 @@ The gate catches _invalid_ messages but not _inlined_ strings. The `@pikku/manti
|
|
|
109
180
|
## Adding a second language
|
|
110
181
|
|
|
111
182
|
1. `messages/fr.json` mirroring `en.json`'s keys (translate the values, keep `{param}` names identical).
|
|
112
|
-
2. Add `"fr"` to `locales` in `project.inlang/settings.json`.
|
|
183
|
+
2. Add `"fr"` to `locales` in `project.inlang/settings.json`. **Leave `baseLocale` at `en`** — step 1 only works because there is an English catalogue to mirror.
|
|
113
184
|
3. Recompile (restart/`vite dev` or the CLI compile). A locale file missing keys falls back to the base locale per message.
|
|
114
185
|
4. Content is reachable via the `/<lang>` URL prefix (`detectLocale` already resolves it); the base locale needs no prefix. Expose the switcher via `useLocale().setLocale`.
|
|
186
|
+
5. Only if the app should **open** in the new language rather than merely offer it: set `defaultLocale` (`active.json` / `fabric i18n --default-locale fr`). Adding a locale and changing the default are different asks — do the second only when asked.
|
|
115
187
|
|
|
116
188
|
## i18n debug mode (find inlined strings)
|
|
117
189
|
|
|
@@ -137,6 +209,9 @@ The wrapper alternative — a module that walks the namespace and pipes each mes
|
|
|
137
209
|
## What NOT to do
|
|
138
210
|
|
|
139
211
|
- Don't hardcode display strings "just for now" — the message is the work.
|
|
212
|
+
- Don't set `baseLocale` to anything but `en`, whatever language the product speaks. It names the source catalogue, and a project without one can never add a language. Set `defaultLocale` instead.
|
|
213
|
+
- Don't let a non-English UI reach the identifiers. Functions, components, types, files, tables and columns are English in every project; the product's language lives in `messages/*.json` and nowhere else.
|
|
214
|
+
- Don't translate message **keys**. `auth__login__title` stays English in `de.json`; only the value changes.
|
|
140
215
|
- Don't edit or commit anything under `src/paraglide/` — it's regenerated; change `messages/*.json` instead.
|
|
141
216
|
- **Don't wrap `m`.** No re-export module, no branding layer, no resolver. Components import `m` from `../paraglide/messages.js` and call it. `@pikku/react`'s `I18nString` is declared as `string & { readonly __brand: 'LocalizedString' }` — deliberately identical to Paraglide's own `LocalizedString` — so `m.some__key()` satisfies the `@pikku/mantine` `I18nNode` gate natively. A wrapper adds nothing and costs per-message tree-shaking.
|
|
142
217
|
|
|
@@ -184,7 +184,82 @@ const earlyMiddleware = pikkuMiddleware({
|
|
|
184
184
|
|
|
185
185
|
Within the same priority level, the collection order above is preserved. Use priority when a middleware must run before/after others regardless of where it was registered (e.g. telemetry wrapping everything, session extraction before auth checks).
|
|
186
186
|
|
|
187
|
-
##
|
|
187
|
+
## ⛔ MACHINE AUTH: THE TOKEN BECOMES A SESSION. ⛔
|
|
188
|
+
|
|
189
|
+
**A caller that has an identity — a sandbox, a deployed stage, a pool host, a device — is authenticated ONCE, in middleware, which calls `setSession`. The function is then an ordinary `pikkuFunc` gated with `scopes`. It reads `session`. It NEVER re-derives who the caller is.**
|
|
190
|
+
|
|
191
|
+
Either a function is sessionless (genuinely public) or it has a session. Anything in between — a token verified inside `func`, a token verified in a `permissions` check that returns `true`, an identity passed in the input schema, the same resolver memoised per request so N functions can each call it — is the anti-pattern this section exists to kill.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// middleware.ts — resolve the bearer ONCE, for every route
|
|
195
|
+
const sandboxBearerAuth = pikkuMiddleware<SingletonServices>(
|
|
196
|
+
async ({ kysely, auth }, { http, getSession, setSession }, next) => {
|
|
197
|
+
if (await getSession?.()) return next()
|
|
198
|
+
const header = http?.request?.header?.('authorization')
|
|
199
|
+
if (!header?.startsWith('Bearer ')) return next()
|
|
200
|
+
const sandbox = await resolveSandboxSession(kysely, auth, header.slice(7).trim())
|
|
201
|
+
if (sandbox) {
|
|
202
|
+
setSession?.({ userId: sandbox.createdByUserId ?? sandbox.sandboxInstanceId,
|
|
203
|
+
orgId: sandbox.organizationId, scopes: ['machine:sandbox'], sandbox } as UserSession)
|
|
204
|
+
}
|
|
205
|
+
return next()
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
addHTTPMiddleware('*', [cors(...), betterAuthSession(), apiBearerAuth, sandboxBearerAuth as any])
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// functions/report-something.function.ts
|
|
214
|
+
export const reportSomething = pikkuFunc({
|
|
215
|
+
expose: true,
|
|
216
|
+
scopes: ['machine:sandbox'], // ← the gate. Enforced by the runner, seen by the inspector.
|
|
217
|
+
input: ReportSomethingInput,
|
|
218
|
+
output: ReportSomethingOutput,
|
|
219
|
+
func: async ({ kysely }, input, { session }) => {
|
|
220
|
+
const sandbox = sandboxOf(session) // ← narrowing only, no verification
|
|
221
|
+
...
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
An unresolved token leaves the session unset and the function throws `MissingSessionError` — 401, for free. Declare the scope tree once with `defineScope` (see `pikku-permissions`).
|
|
227
|
+
|
|
228
|
+
### It MUST be `addHTTPMiddleware`, never `addTagMiddleware`
|
|
229
|
+
|
|
230
|
+
**A session set in tag middleware is invisible to the function when the call arrives over `POST /rpc/:rpcName`.** Tag middleware runs inside `runPikkuFunc`, and the RPC dispatch calls it without a `sessionService`, so `invocationWire.session` is never re-read after your `setSession` — the function sees the session the OUTER wire had, which is none. `addHTTPMiddleware('*')` runs on the `/rpc` route itself, before its handler, and that session is the one the dispatched function inherits. Tag middleware is still right for a gate that only says yes/no.
|
|
231
|
+
|
|
232
|
+
### A cron is a machine identity too — set it in the task's own middleware
|
|
233
|
+
|
|
234
|
+
A scheduled task has no caller and no header, but it is still a machine principal, and without a session it cannot invoke a gated RPC or be attributed in anything it writes. Give it one the same way, in the task's own `middleware`:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const cronSession = pikkuMiddleware(async (_services, { scheduledTask, setSession }, next) => {
|
|
238
|
+
setSession?.({ userId: `cron:${scheduledTask?.name}`, scopes: ['machine:cron'] } as UserSession)
|
|
239
|
+
return next()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
wireScheduler({
|
|
243
|
+
name: 'tickVirtualUserSchedules',
|
|
244
|
+
schedule: '*/15 * * * *',
|
|
245
|
+
middleware: [cronSession],
|
|
246
|
+
func: tickVirtualUserSchedules,
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
One `const`, not a `machineSession(name)` factory: the inspector rejects a bare `pikkuMiddleware()` that is not assigned to a variable or object property, and the task name is on the wire anyway. Parameterised middleware goes through `pikkuMiddlewareFactory`.
|
|
251
|
+
|
|
252
|
+
The task can then be a thin `rpc.invoke('someGatedRpc')` against the same entry point a person calls, instead of factoring the logic into a `lib/` helper purely to route around the missing identity.
|
|
253
|
+
|
|
254
|
+
Unlike tag middleware over `/rpc`, this works: `runScheduledTask` builds its wire with a `sessionService`, so the session set here is the one the function is frozen with. And unlike a person, a cron is **not** a user row — inventing a seeded account for it buys a phantom member in every list, seat count and bill, and a per-org membership that a cross-org sweep has to ignore anyway. Platform-wide authority is a scope, not a membership.
|
|
255
|
+
|
|
256
|
+
### The one sessionless exception: bootstrap
|
|
257
|
+
|
|
258
|
+
An endpoint that runs BEFORE the caller has an identity — registering a new host with a shared bootstrap key, a login, a device-code request — has no session to set. That one stays `pikkuSessionlessFunc` and declares its gate in `permissions` (see `pikku-permissions`).
|
|
259
|
+
|
|
260
|
+
## Service-to-Service Bearer Auth (gate-only pattern)
|
|
261
|
+
|
|
262
|
+
Use this when the callee needs to know only THAT the caller is trusted, not WHICH caller it is. If it needs to know which, use the session pattern above.
|
|
188
263
|
|
|
189
264
|
A server that exposes RPCs only to a trusted caller (e.g. an API calling a machine-agent). Auth lives in a tag middleware — NOT in the function body. Authorization/permission checks belong in the `permissions` field (see `pikku-permissions`), never inside `func`.
|
|
190
265
|
|
|
@@ -13,6 +13,10 @@ installGroups: [core]
|
|
|
13
13
|
|
|
14
14
|
# Pikku Permissions
|
|
15
15
|
|
|
16
|
+
## ⛔ FIRST: is the caller a machine with a token? ⛔
|
|
17
|
+
|
|
18
|
+
**Then this is NOT a permissions problem.** Resolve the token in `addHTTPMiddleware('*')` middleware that calls `setSession`, make the function a `pikkuFunc`, and gate it with `scopes`. A `permissions` check that verifies a bearer token and returns `true` is authentication wearing an authorization hat — and it leaves the function sessionless, so every body still has to work out who called it. See the machine-auth section of `pikku-middleware`. The only exception is a bootstrap endpoint whose caller has no identity yet (a shared-secret registration, a login): that one is sessionless and declares its gate here.
|
|
19
|
+
|
|
16
20
|
## The Rule
|
|
17
21
|
|
|
18
22
|
**ALWAYS put authorization checks in the `permissions` field of `pikkuFunc` or `pikkuSessionlessFunc` — NEVER inside the `func` body.**
|
|
@@ -356,6 +356,56 @@ const order = await scenario.do(
|
|
|
356
356
|
|
|
357
357
|
Reach for a `pikkuScenarioStep` on the non-browser side only when one intent genuinely spans several RPCs, or when the step asserts something the RPC result alone does not say.
|
|
358
358
|
|
|
359
|
+
### What language the prose is in
|
|
360
|
+
|
|
361
|
+
A scenario carries two kinds of text, and they do not share a language.
|
|
362
|
+
|
|
363
|
+
**Identifiers are English.** The exported const (`buysAnApple`,
|
|
364
|
+
`credentialFeature`), the step's `name` — which is its `pikkuFuncId`, the typed
|
|
365
|
+
string the generated step map is keyed by — the file name, and every helper in
|
|
366
|
+
`*.browser.ts`. These bind to generated code and to `pikku scenario list`; they
|
|
367
|
+
are English in every project regardless of who the product is for or what
|
|
368
|
+
language the team speaks. There is no setting that changes this.
|
|
369
|
+
|
|
370
|
+
**Prose follows `locale` in `pikku.config.json`** (default `en`). That is a
|
|
371
|
+
step's `description` and `template`, a feature's `name` and `description`, a
|
|
372
|
+
scenario's `title`, and the positional step names passed to
|
|
373
|
+
`scenario.given/when/then`. Read the field before you write any of them.
|
|
374
|
+
|
|
375
|
+
This split is the same one the feature table already states — _the export
|
|
376
|
+
identifier is the feature's id; `name` is the human-readable label_ — applied to
|
|
377
|
+
language. The report is the deliverable, and it is read by the team; the
|
|
378
|
+
identifier is an API, and it is read by the toolchain.
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// pikku.config.json: { "locale": "de" }
|
|
382
|
+
export const buysAnApple = pikkuScenarioStep<{ qty: number }, { orderId: string }>({
|
|
383
|
+
name: 'buysAnApple', // identifier — English, always
|
|
384
|
+
description: 'kauft einen Apfel', // prose — follows locale
|
|
385
|
+
template: 'kauft {qty} Äpfel', // prose — follows locale
|
|
386
|
+
actor: true,
|
|
387
|
+
default: async (_services, { qty }, { actor }) =>
|
|
388
|
+
await actor.invoke('placeOrder', { qty }),
|
|
389
|
+
})
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Note what does **not** change: `placeOrder` is still `placeOrder`, and the file
|
|
393
|
+
is still `apple.scenario.ts`.
|
|
394
|
+
|
|
395
|
+
A product with a non-English UI is not on its own a reason to set `locale` — that
|
|
396
|
+
is the app's language, not the team's. Ask, or leave it `en`.
|
|
397
|
+
|
|
398
|
+
**Where a non-`en` `locale` still shows English, today.** The reporter composes a
|
|
399
|
+
sentence as `<Keyword> the <actor> <template>` (`composeStepProse`), and both the
|
|
400
|
+
keyword and the article `the` are English literals. The Console translates the
|
|
401
|
+
Given/When/Then keywords into its own UI language; the CLI reporter does not, and
|
|
402
|
+
nothing translates `the`. So `locale: "de"` gives you German step prose inside an
|
|
403
|
+
English frame — `Given the shopper kauft 1 Äpfel`. Write templates that read
|
|
404
|
+
acceptably in that frame rather than trying to defeat it. A second gap: where a
|
|
405
|
+
function or scenario declares no `title`, the Console falls back to splitting the
|
|
406
|
+
**identifier** into an English-looking label (`toEnglishName`), so under a
|
|
407
|
+
non-`en` `locale` meta is worth authoring rather than leaving to the fallback.
|
|
408
|
+
|
|
359
409
|
### `then` bindings are witnesses, not alternatives
|
|
360
410
|
|
|
361
411
|
This is the one place the surface bindings do **not** behave like a switch, and it is the part worth reading twice.
|
|
@@ -783,6 +833,7 @@ Services are plain objects — a Pikku function is pure business logic, so a moc
|
|
|
783
833
|
| Assuming a clean database | There is no state reset — it may be a staging server. Scope what you create. |
|
|
784
834
|
| `sleep()` before asserting | Use `expectEventually`. |
|
|
785
835
|
| A step named `clicksAddToBasket` / `opensThePage` | That is an action, not an intent. Name the step for what the actor wanted; put the clicking in a utility. |
|
|
836
|
+
| A step named `kauftEinenApfel` / a `vorgang` table | Identifiers are English in every project. The German belongs in `description` / `template`, and only when `pikku.config.json` sets `locale`. |
|
|
786
837
|
| A browser step that assumes it is already on a page | It can then only run mid-flow. Arrive first — check the URL, navigate if needed. |
|
|
787
838
|
| `getByLabel('Full Name')` in a translated app | Passes only in the base locale, and a copy edit breaks it as an unexplained timeout. Locate by message key. |
|
|
788
839
|
| A `browser` binding guarding `if (!browser)` | The binding guarantees it. The guard hides the real error, which is a missing actor (`PKU677`). |
|
|
@@ -50,6 +50,29 @@ wireScheduler({
|
|
|
50
50
|
})
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Giving a cron an identity
|
|
54
|
+
|
|
55
|
+
A cron has no caller, so it runs with **no session at all**: it cannot invoke a
|
|
56
|
+
permission- or scope-gated RPC, and nothing it writes can be attributed. A
|
|
57
|
+
scheduled task is a machine principal — give it a session in the task's own
|
|
58
|
+
`middleware`, exactly as a bearer-authenticated caller gets one:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
wireScheduler({
|
|
62
|
+
name: 'bookingLifecycleDaily',
|
|
63
|
+
schedule: '0 3 * * *',
|
|
64
|
+
middleware: [cronSession],
|
|
65
|
+
func: bookingLifecycleDaily,
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`runScheduledTask` builds its wire with a `sessionService`, so a `setSession`
|
|
70
|
+
here is the session the function is frozen with. See the machine-auth section of
|
|
71
|
+
`pikku-middleware` for the factory and for why a cron is not a user row.
|
|
72
|
+
|
|
73
|
+
A scheduler service running a task on someone's behalf can pass a session
|
|
74
|
+
directly instead: `runScheduledTask({ name, session })`.
|
|
75
|
+
|
|
53
76
|
### Wire Object (`wire.scheduledTask`)
|
|
54
77
|
|
|
55
78
|
Inside scheduled functions:
|