@pikku/skills 0.12.9 → 0.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +768 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +4 -4
- package/skills/pikku-addon/SKILL.md +20 -14
- package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
- package/skills/{pikku-ai-agent → pikku-agent}/SKILL.md +24 -24
- package/skills/pikku-ai-vercel/SKILL.md +18 -18
- package/skills/pikku-ai-voice/SKILL.md +15 -15
- package/skills/pikku-audit/SKILL.md +28 -13
- package/skills/pikku-aws/SKILL.md +2 -2
- package/skills/pikku-better-auth/SKILL.md +97 -17
- package/skills/pikku-build-app/SKILL.md +621 -0
- package/skills/pikku-build-app/references/multi-app.md +117 -0
- package/skills/pikku-build-app/references/ship.md +98 -0
- package/skills/pikku-build-app/references/theming.md +70 -0
- package/skills/pikku-build-platform/SKILL.md +239 -0
- package/skills/pikku-build-quick/SKILL.md +238 -0
- package/skills/pikku-cli/SKILL.md +7 -7
- package/skills/pikku-cli/references/complete-example.md +1 -1
- package/skills/pikku-concepts/SKILL.md +10 -7
- package/skills/pikku-concepts/references/concept-mapping.md +1 -1
- package/skills/pikku-config/SKILL.md +5 -3
- package/skills/pikku-deploy-azure/SKILL.md +5 -4
- package/skills/pikku-deploy-cloudflare/SKILL.md +9 -9
- package/skills/pikku-deploy-uws/SKILL.md +5 -2
- package/skills/pikku-deps/SKILL.md +42 -3
- package/skills/pikku-emails/SKILL.md +5 -5
- package/skills/pikku-fabric/SKILL.md +27 -3
- package/skills/pikku-fabric-debug/SKILL.md +1 -1
- package/skills/pikku-feature/SKILL.md +5 -4
- package/skills/pikku-http/SKILL.md +4 -4
- package/skills/pikku-http/references/http-options.md +13 -13
- package/skills/pikku-i18n/SKILL.md +2 -1
- package/skills/pikku-info/SKILL.md +1 -1
- package/skills/pikku-knowledge/SKILL.md +13 -13
- package/skills/pikku-kysely/SKILL.md +68 -41
- package/skills/pikku-machine-auth/SKILL.md +10 -10
- package/skills/pikku-mcp/SKILL.md +23 -20
- package/skills/pikku-middleware/SKILL.md +19 -12
- package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
- package/skills/pikku-mongodb/SKILL.md +11 -11
- package/skills/pikku-n8n-import/SKILL.md +12 -12
- package/skills/pikku-n8n-import/SPEC.md +3 -0
- package/skills/pikku-n8n-import/references/addon-mapping.md +14 -8
- package/skills/pikku-n8n-import/references/code-translation.md +26 -22
- package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
- package/skills/pikku-paraglide/SKILL.md +11 -6
- package/skills/pikku-permissions/SKILL.md +19 -15
- package/skills/pikku-product-second-opinion/README.md +3 -3
- package/skills/pikku-product-second-opinion/SKILL.md +83 -73
- package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
- package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
- package/skills/pikku-queue/SKILL.md +1 -1
- package/skills/pikku-react/SKILL.md +53 -13
- package/skills/pikku-realtime/SKILL.md +51 -19
- package/skills/pikku-rpc/SKILL.md +1 -1
- package/skills/pikku-rtl/SKILL.md +1 -1
- package/skills/pikku-scenario/SKILL.md +164 -44
- package/skills/pikku-schedule/SKILL.md +6 -1
- package/skills/pikku-schema-ajv/SKILL.md +2 -2
- package/skills/pikku-schema-cfworker/SKILL.md +1 -1
- package/skills/pikku-security/SKILL.md +9 -5
- package/skills/pikku-services/SKILL.md +27 -18
- package/skills/pikku-services/references/audit-wire-service.md +14 -8
- package/skills/pikku-software-archaeology/SKILL.md +27 -23
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +580 -102
- package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
- package/skills/pikku-tag-middleware/SKILL.md +1 -0
- package/skills/pikku-template-clone/SKILL.md +2 -1
- package/skills/pikku-trigger/SKILL.md +3 -3
- package/skills/pikku-versioning/SKILL.md +87 -3
- package/skills/pikku-websocket/SKILL.md +4 -3
- package/skills/pikku-workflow/SKILL.md +2 -2
- package/skills/pikku-workflow/references/workflow-reference.md +24 -10
- package/skills/pikku-ws/SKILL.md +5 -2
|
@@ -49,17 +49,17 @@ await mongo.close()
|
|
|
49
49
|
|
|
50
50
|
### Available Services
|
|
51
51
|
|
|
52
|
-
| Service
|
|
53
|
-
|
|
|
54
|
-
| `MongoDBChannelStore`
|
|
55
|
-
| `MongoDBEventHubStore`
|
|
56
|
-
| `MongoDBWorkflowService`
|
|
57
|
-
| `MongoDBWorkflowRunService`
|
|
58
|
-
| `MongoDBDeploymentService`
|
|
59
|
-
| `
|
|
60
|
-
| `MongoDBAgentRunService`
|
|
61
|
-
| `MongoDBSecretService`
|
|
62
|
-
| `MongoDBSessionStore`
|
|
52
|
+
| Service | Interface | Purpose |
|
|
53
|
+
| ---------------------------- | ------------------------------------------- | ---------------------------------------------- |
|
|
54
|
+
| `MongoDBChannelStore` | `ChannelStore` | WebSocket channel state persistence |
|
|
55
|
+
| `MongoDBEventHubStore` | `EventHubStore` | Event hub state persistence |
|
|
56
|
+
| `MongoDBWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
|
|
57
|
+
| `MongoDBWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
|
|
58
|
+
| `MongoDBDeploymentService` | `DeploymentService` | Deployment state management |
|
|
59
|
+
| `MongoDBAgentStorageService` | `AgentStorageService, AgentRunStateService` | AI conversation/run storage |
|
|
60
|
+
| `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |
|
|
61
|
+
| `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
|
|
62
|
+
| `MongoDBSessionStore` | `SessionStore` | Persisted user sessions |
|
|
63
63
|
|
|
64
64
|
All services take a `Db` instance in their constructor and have an `init()` method that creates collections/indexes.
|
|
65
65
|
|
|
@@ -59,13 +59,13 @@ than the exit code to know what landed. Relay every skipped workflow to the user
|
|
|
59
59
|
Every unmapped node is a stub that throws `… — implement me`. Classify each by its
|
|
60
60
|
JSDoc marker and route to the matching reference:
|
|
61
61
|
|
|
62
|
-
| Stub marker / signal
|
|
63
|
-
|
|
64
|
-
| `STUB — generated from n8n node "…" (type "n8n-nodes-base.<svc>…")`
|
|
65
|
-
| `STUB — generated from n8n Code node "…"`
|
|
66
|
-
| A `control` stub — Loop Over Items / **splitInBatches**, Switch expr-mode | `references/loops-and-control.md`
|
|
67
|
-
| `STUB — … vector-store … #902`
|
|
68
|
-
| Importer `diagnostics` (already exited 1)
|
|
62
|
+
| Stub marker / signal | Handle via |
|
|
63
|
+
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| `STUB — generated from n8n node "…" (type "n8n-nodes-base.<svc>…")` | `references/addon-mapping.md` |
|
|
65
|
+
| `STUB — generated from n8n Code node "…"` | `references/code-translation.md` |
|
|
66
|
+
| A `control` stub — Loop Over Items / **splitInBatches**, Switch expr-mode | `references/loops-and-control.md` |
|
|
67
|
+
| `STUB — … vector-store … #902` | rare now (RAG ships as `<store>:query`/`:ingest`); a residual one = an unmapped store → report it, don't guess |
|
|
68
|
+
| Importer `diagnostics` (already exited 1) | explain the reason; the workflow is un-importable as-is |
|
|
69
69
|
|
|
70
70
|
Read a reference file only when you actually hit that stub class.
|
|
71
71
|
|
|
@@ -99,11 +99,11 @@ Missing integrations — install these or the nodes stay stubs:
|
|
|
99
99
|
|
|
100
100
|
## References
|
|
101
101
|
|
|
102
|
-
| Open when you need to…
|
|
103
|
-
|
|
104
|
-
| map an integration stub (gmailTool, slackTool, googleSheets, plain action nodes) to an installed addon `ref(...)` | `references/addon-mapping.md`
|
|
105
|
-
| translate an n8n Code node body into a Pikku function body
|
|
106
|
-
| lower a Loop Over Items / splitInBatches loop, or a Switch that stayed a stub
|
|
102
|
+
| Open when you need to… | Read |
|
|
103
|
+
| ----------------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
|
104
|
+
| map an integration stub (gmailTool, slackTool, googleSheets, plain action nodes) to an installed addon `ref(...)` | `references/addon-mapping.md` |
|
|
105
|
+
| translate an n8n Code node body into a Pikku function body | `references/code-translation.md` |
|
|
106
|
+
| lower a Loop Over Items / splitInBatches loop, or a Switch that stayed a stub | `references/loops-and-control.md` |
|
|
107
107
|
|
|
108
108
|
## Final summary
|
|
109
109
|
|
|
@@ -11,6 +11,7 @@ remainder with judgment, report gaps that need a human decision, and verify.
|
|
|
11
11
|
## Scope
|
|
12
12
|
|
|
13
13
|
In scope:
|
|
14
|
+
|
|
14
15
|
- Running `pikku import n8n` and triaging its output.
|
|
15
16
|
- Filling integration stubs (→ addon refs), Code stubs (→ function bodies), and
|
|
16
17
|
loop/control stubs (→ `graph:map`/reduce/branch).
|
|
@@ -18,6 +19,7 @@ In scope:
|
|
|
18
19
|
- Verifying via `pikku all` + `tsc` + a zero-surviving-stub check.
|
|
19
20
|
|
|
20
21
|
Out of scope:
|
|
22
|
+
|
|
21
23
|
- Extending `@pikku/n8n-import` itself (it is frozen; do not add per-service tables
|
|
22
24
|
or new compiler rules to it).
|
|
23
25
|
- Authoring workflows from scratch (`pikku-workflow`) or hand-written addon wiring
|
|
@@ -47,6 +49,7 @@ Out of scope:
|
|
|
47
49
|
## Source And Evidence Model
|
|
48
50
|
|
|
49
51
|
Authoritative sources:
|
|
52
|
+
|
|
50
53
|
- `@pikku/n8n-import` codegen (stub markers, manifest shape, `import-n8n` command).
|
|
51
54
|
- `@pikku/addon-graph` function contracts (`graph:map`/`fanout`, `branch`).
|
|
52
55
|
- Installed `@pikku/addon-*` source (function names verified by grep, never guessed).
|
|
@@ -18,9 +18,11 @@ rewrite the stub.
|
|
|
18
18
|
"n8nType": "n8n-nodes-base.gmailTool",
|
|
19
19
|
"n8nName": "Send a message in Gmail",
|
|
20
20
|
"parameters": { "sendTo": "...", "message": "...", "subject": "..." },
|
|
21
|
-
"credentials": {
|
|
21
|
+
"credentials": {
|
|
22
|
+
"gmailOAuth2": { "id": "...", "name": "Personal Gmail" },
|
|
23
|
+
},
|
|
22
24
|
"isAgentTool": true,
|
|
23
|
-
"agentName": "Inbox Assistant"
|
|
25
|
+
"agentName": "Inbox Assistant",
|
|
24
26
|
}
|
|
25
27
|
```
|
|
26
28
|
2. **Installed addons** — `@pikku/addon-*` in the project's `package.json`
|
|
@@ -35,12 +37,12 @@ rewrite the stub.
|
|
|
35
37
|
Map `n8nType` to a package by reading its source. Common shapes (**guesses, not
|
|
36
38
|
authoritative** — always verify against installed source):
|
|
37
39
|
|
|
38
|
-
| n8n type prefix
|
|
39
|
-
|
|
40
|
-
| `n8n-nodes-base.gmail` / `gmailTool`
|
|
41
|
-
| `n8n-nodes-base.slack` / `slackTool`
|
|
42
|
-
| `n8n-nodes-base.googleSheets` / `…Tool`
|
|
43
|
-
| `n8n-nodes-base.notion` / `notionTool`
|
|
40
|
+
| n8n type prefix | typical addon candidate |
|
|
41
|
+
| ------------------------------------------ | ---------------------------- |
|
|
42
|
+
| `n8n-nodes-base.gmail` / `gmailTool` | `@pikku/addon-email-gmail` |
|
|
43
|
+
| `n8n-nodes-base.slack` / `slackTool` | `@pikku/addon-chat-slack` |
|
|
44
|
+
| `n8n-nodes-base.googleSheets` / `…Tool` | `@pikku/addon-sheets-google` |
|
|
45
|
+
| `n8n-nodes-base.notion` / `notionTool` | `@pikku/addon-docs-notion` |
|
|
44
46
|
| `n8n-nodes-base.telegram` / `telegramTool` | `@pikku/addon-chat-telegram` |
|
|
45
47
|
|
|
46
48
|
If no installed addon plausibly covers the n8n type, stop and report it — do not
|
|
@@ -74,6 +76,7 @@ over guessing.
|
|
|
74
76
|
Two outcomes, by `isAgentTool`:
|
|
75
77
|
|
|
76
78
|
**A) `isAgentTool: true`** — the stub is an agent tool referenced via `ref()`:
|
|
79
|
+
|
|
77
80
|
1. Delete the stub file.
|
|
78
81
|
2. In the agent file, replace `ref('agentGmailtool__sendAMessageInGmail')` in
|
|
79
82
|
`tools: [...]` with `ref('messageSend')` (the resolved addon function).
|
|
@@ -81,13 +84,16 @@ Two outcomes, by `isAgentTool`:
|
|
|
81
84
|
`node_modules/@pikku/addon-*`).
|
|
82
85
|
|
|
83
86
|
If you can't delete safely, leave a one-line re-export instead of a stub:
|
|
87
|
+
|
|
84
88
|
```ts
|
|
85
89
|
import { messageSend } from '@pikku/addon-email-gmail'
|
|
86
90
|
export const agentGmailtool__sendAMessageInGmail = messageSend
|
|
87
91
|
```
|
|
92
|
+
|
|
88
93
|
Default is delete + retarget; wrappers add maintenance burden.
|
|
89
94
|
|
|
90
95
|
**B) `isAgentTool: false`** — the stub is a graph node:
|
|
96
|
+
|
|
91
97
|
1. Open `<workflow>.graph.ts`.
|
|
92
98
|
2. In `nodes: { … }` find the entry whose value is the stub rpc name.
|
|
93
99
|
3. Replace it with the addon function name (`'messageSend'`).
|
|
@@ -20,39 +20,39 @@ refactor, add error handling, or invent fields.
|
|
|
20
20
|
3. **Apply the rubric.**
|
|
21
21
|
4. **Edit only the function body.** Leave imports, schemas, JSDoc, name, description, refs untouched unless step 5 forces it.
|
|
22
22
|
5. **If the schemas are wrong** (code reads `$json.userId: string` but input is `items: z.array(z.unknown())`), tighten with the smallest change. Prefer `z.unknown()` over `z.any()`. Never widen output to `z.any()`.
|
|
23
|
-
6. **Add one comment** at the top of the body noting the mode: `// translated from n8n Code node, mode: runOnceForAllItems`. This is the
|
|
23
|
+
6. **Add one comment** at the top of the body noting the mode: `// translated from n8n Code node, mode: runOnceForAllItems`. This is the _only_ comment you may add.
|
|
24
24
|
7. **Typecheck** (`yarn tsc` from the package root); fix errors with the smallest change.
|
|
25
25
|
|
|
26
26
|
## Rubric
|
|
27
27
|
|
|
28
28
|
### Envelope unwrapping — all-items mode
|
|
29
29
|
|
|
30
|
-
| n8n
|
|
31
|
-
|
|
32
|
-
| `items`
|
|
33
|
-
| `items[i].json.X`
|
|
34
|
-
| `items[i].json`
|
|
35
|
-
| `items[i].binary`
|
|
36
|
-
| `items.length`
|
|
37
|
-
| `items.map(i => i.json.X)` | `items.map((i: any) => i.X)`
|
|
30
|
+
| n8n | Pikku |
|
|
31
|
+
| -------------------------- | ------------------------------------------------- |
|
|
32
|
+
| `items` | `(data.items ?? []) as any[]` (or typed if known) |
|
|
33
|
+
| `items[i].json.X` | `items[i].X` |
|
|
34
|
+
| `items[i].json` | `items[i]` |
|
|
35
|
+
| `items[i].binary` | **NOT supported** — leave a TODO and explain |
|
|
36
|
+
| `items.length` | `items.length` |
|
|
37
|
+
| `items.map(i => i.json.X)` | `items.map((i: any) => i.X)` |
|
|
38
38
|
|
|
39
39
|
### Envelope unwrapping — each-item mode
|
|
40
40
|
|
|
41
|
-
| n8n
|
|
42
|
-
|
|
43
|
-
| `$json.X` / `$input.item.json.X` | `data.X` (input is the item itself)
|
|
44
|
-
| `$input.item.json`
|
|
45
|
-
| `$input.all()`
|
|
41
|
+
| n8n | Pikku |
|
|
42
|
+
| -------------------------------- | ------------------------------------------------- |
|
|
43
|
+
| `$json.X` / `$input.item.json.X` | `data.X` (input is the item itself) |
|
|
44
|
+
| `$input.item.json` | `data` |
|
|
45
|
+
| `$input.all()` | not available per-item — change to all-items mode |
|
|
46
46
|
|
|
47
47
|
### Return statement
|
|
48
48
|
|
|
49
|
-
| n8n
|
|
50
|
-
|
|
51
|
-
| `return [{ json: X }]`
|
|
52
|
-
| `return items.map(i => ({ json: ... }))` | `return { items: items.map(...) }`
|
|
53
|
-
| `return [{ json: X }, { json: Y }]`
|
|
54
|
-
| `return { json: X }` (each-item)
|
|
55
|
-
| `return [...]` (already plain)
|
|
49
|
+
| n8n | Pikku |
|
|
50
|
+
| ---------------------------------------- | --------------------------------------------------------------- |
|
|
51
|
+
| `return [{ json: X }]` | `return { items: [X] }` |
|
|
52
|
+
| `return items.map(i => ({ json: ... }))` | `return { items: items.map(...) }` |
|
|
53
|
+
| `return [{ json: X }, { json: Y }]` | `return { items: [X, Y] }` |
|
|
54
|
+
| `return { json: X }` (each-item) | `return X` |
|
|
55
|
+
| `return [...]` (already plain) | wrap in `{ items: [...] }` only if the output schema expects it |
|
|
56
56
|
|
|
57
57
|
### Built-ins — do NOT auto-translate
|
|
58
58
|
|
|
@@ -80,6 +80,7 @@ the first param).
|
|
|
80
80
|
## Example
|
|
81
81
|
|
|
82
82
|
Before (stub):
|
|
83
|
+
|
|
83
84
|
```ts
|
|
84
85
|
/**
|
|
85
86
|
* STUB — generated from n8n Code node "Custom Code".
|
|
@@ -90,12 +91,15 @@ export const codeStubCustomCode = pikkuSessionlessFunc({
|
|
|
90
91
|
input: CodeStubCustomCodeInput,
|
|
91
92
|
output: CodeStubCustomCodeOutput,
|
|
92
93
|
func: async (_services, _data) => {
|
|
93
|
-
throw new Error(
|
|
94
|
+
throw new Error(
|
|
95
|
+
'Stub: ported from n8n Code node "Custom Code" — implement me'
|
|
96
|
+
)
|
|
94
97
|
},
|
|
95
98
|
})
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
After:
|
|
102
|
+
|
|
99
103
|
```ts
|
|
100
104
|
export const codeStubCustomCode = pikkuSessionlessFunc({
|
|
101
105
|
description: 'Ported from n8n Code node "Custom Code"',
|
|
@@ -32,11 +32,11 @@ predecessor and any `$('<loop node>')` become `$item`.
|
|
|
32
32
|
|
|
33
33
|
### Decide the shape first
|
|
34
34
|
|
|
35
|
-
| Loop body does…
|
|
36
|
-
|
|
37
|
-
| transform each item independently (enrich, format, call one thing) | `graph:map` — child = the body
|
|
38
|
-
| accumulate across items (running total, build one object/array)
|
|
39
|
-
| pure side-effect per item, nothing downstream consumes results
|
|
35
|
+
| Loop body does… | Emit |
|
|
36
|
+
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| transform each item independently (enrich, format, call one thing) | `graph:map` — child = the body |
|
|
38
|
+
| accumulate across items (running total, build one object/array) | a **reduce**: a single generated function over the whole array, not a map — `graph:map` collects per-item results and _loses_ the accumulator |
|
|
39
|
+
| pure side-effect per item, nothing downstream consumes results | `graph:map` with **no `next`** (done branch empty) — the safest, unambiguous case |
|
|
40
40
|
|
|
41
41
|
### Child arity
|
|
42
42
|
|
|
@@ -49,8 +49,8 @@ predecessor and any `$('<loop node>')` become `$item`.
|
|
|
49
49
|
|
|
50
50
|
### Done-branch semantics (ask if it matters)
|
|
51
51
|
|
|
52
|
-
n8n's done output is version-dependent: it may carry the
|
|
53
|
-
|
|
52
|
+
n8n's done output is version-dependent: it may carry the _original_ items or the
|
|
53
|
+
_accumulated_ results. `graph:map`'s `next` receives the array of child results.
|
|
54
54
|
If a downstream node reads that array's shape and the distinction matters, add:
|
|
55
55
|
|
|
56
56
|
```ts
|
|
@@ -18,7 +18,7 @@ Use this as an execution checklist, not reference material.
|
|
|
18
18
|
## The rules that don't change
|
|
19
19
|
|
|
20
20
|
- **Never resolve an enum key dynamically.** No `mKey('status.' + value)`, no `m['enum__status__' + value]()`, no `mExists`/`mList` helpers. Dynamic keys can't be type-checked or tree-shaken. Everything is a static `m.<literal>()` reference, generated into the map.
|
|
21
|
-
- **The `enum__<group>__<member>` namespace.** `__` separates the prefix / group / member segments; a single `_` joins words
|
|
21
|
+
- **The `enum__<group>__<member>` namespace.** `__` separates the prefix / group / member segments; a single `_` joins words _within_ a segment (`enum__booking_status__form_received`). The prefix (`enum`) and separator (`__`) are configurable but leave them at the defaults.
|
|
22
22
|
- **Members must be valid JS identifiers.** Spell out leading digits — `two_guests`, not `2_guests`. The generator quotes an invalid member as a fallback but warns you to rename it.
|
|
23
23
|
- **`asI18n(...)` is only for opaque server data** (names, slugs, ids returned from the API). Never `asI18n()` a hardcoded English string or an enum value — an enum value goes through its label map.
|
|
24
24
|
|
|
@@ -35,8 +35,8 @@ export type I18nMessage = () => I18nString
|
|
|
35
35
|
export type EnumLabel<E extends string> = Record<E, I18nMessage>
|
|
36
36
|
|
|
37
37
|
export const bookingStatus = {
|
|
38
|
-
enquiry:
|
|
39
|
-
reserved:
|
|
38
|
+
enquiry: m.enum__booking_status__enquiry,
|
|
39
|
+
reserved: m.enum__booking_status__reserved,
|
|
40
40
|
confirmed: m.enum__booking_status__confirmed,
|
|
41
41
|
} satisfies EnumLabel<BookingStatus>
|
|
42
42
|
export type BookingStatusKey = keyof typeof bookingStatus
|
|
@@ -61,18 +61,20 @@ const items = [{ label: m.common__nav__items__dashboard /* ← reference */ }]
|
|
|
61
61
|
The DB column is the real source of truth for what an enum can be. The pikku CLI's db codegen emits a bare unions module — `.pikku/db/enums.gen.ts` — covering **both** Postgres native enums and SQLite `CHECK (col IN ('a','b',…))` constraints:
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
|
-
export type BookingStatus =
|
|
64
|
+
export type BookingStatus =
|
|
65
|
+
'enquiry' | 'reserved' | 'confirmed' | 'ended' | 'cancelled'
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
Point `@pikku/paraglide` at that file (`enumsFile`) and each catalog group whose member set **exactly matches** a DB enum is typed `satisfies EnumLabel<DbEnum>`. The label map then **is** the reconciliation — no separate assertion:
|
|
68
69
|
|
|
69
70
|
- catalog drops a DB member, or `en.json` is missing the key → `m.enum__…` doesn't exist / `Record<DbEnum,…>` isn't exhaustive → **`tsc` error naming the gap**.
|
|
70
71
|
- a DB enum with **no** catalog group → `unmatchedDbEnums: 'emit'` (default) generates a label map referencing `enum__<table>_<column>__<member>` keys, so `tsc` tells you exactly which keys to add; `'warn'` only reports it.
|
|
71
|
-
- a group with a member the DB lacks (a
|
|
72
|
+
- a group with a member the DB lacks (a _derived_ UI state, e.g. a `waitlisted` view of a `pending` row) → a drift warning. Make that a **standalone `m.<key>()` message**, not an enum member — the enum group must mirror the DB column exactly.
|
|
72
73
|
|
|
73
74
|
Labelling an enum that's never rendered costs nothing: Paraglide compiles only the messages actually referenced, so unused labels are tree-shaken away. So label every DB enum; don't add an opt-out.
|
|
74
75
|
|
|
75
76
|
**To make a column an enum**, give it a closed domain in the migration so codegen can see it:
|
|
77
|
+
|
|
76
78
|
- SQLite: `status TEXT NOT NULL CHECK (status IN ('enquiry','reserved','confirmed'))`
|
|
77
79
|
- Postgres: a native `CREATE TYPE … AS ENUM (…)` column.
|
|
78
80
|
|
|
@@ -88,7 +90,10 @@ import { paraglideEnums } from '@pikku/paraglide/vite'
|
|
|
88
90
|
|
|
89
91
|
export default defineConfig({
|
|
90
92
|
plugins: [
|
|
91
|
-
paraglideVitePlugin({
|
|
93
|
+
paraglideVitePlugin({
|
|
94
|
+
project: './project.inlang',
|
|
95
|
+
outdir: './src/paraglide',
|
|
96
|
+
}),
|
|
92
97
|
paraglideEnums({
|
|
93
98
|
catalog: './messages/en.json',
|
|
94
99
|
outFile: './src/i18n/i18n-enum.gen.ts',
|
|
@@ -26,14 +26,14 @@ export const deleteBook = pikkuFunc({
|
|
|
26
26
|
await db.deleteBook(bookId)
|
|
27
27
|
},
|
|
28
28
|
permissions: {
|
|
29
|
-
owner: isBookOwner,
|
|
29
|
+
owner: isBookOwner, // ← authorization here
|
|
30
30
|
},
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
// WRONG — permission check inside func body
|
|
34
34
|
export const deleteBook = pikkuFunc({
|
|
35
35
|
func: async ({ db }, { bookId }, { session }) => {
|
|
36
|
-
if (!session) throw new UnauthorizedError()
|
|
36
|
+
if (!session) throw new UnauthorizedError() // ← never do this
|
|
37
37
|
await db.deleteBook(bookId)
|
|
38
38
|
},
|
|
39
39
|
})
|
|
@@ -54,7 +54,7 @@ Use for checks that read the session but need no request data — and that asser
|
|
|
54
54
|
something **beyond** merely having a session (a flag, a tier, a claim).
|
|
55
55
|
|
|
56
56
|
```typescript
|
|
57
|
-
import { pikkuAuth } from '#pikku'
|
|
57
|
+
import { pikkuAuth } from '#pikku/function'
|
|
58
58
|
|
|
59
59
|
// Good: a real gate on the session's contents, not just its existence.
|
|
60
60
|
export const isVerified = pikkuAuth(
|
|
@@ -71,21 +71,21 @@ function already enforces. A function that needs a signed-in user sets
|
|
|
71
71
|
```typescript
|
|
72
72
|
// WRONG — redundant with auth: true; adds a permission that gates nothing.
|
|
73
73
|
export const isSignedIn = pikkuAuth(async (_s, session) => !!session)
|
|
74
|
-
pikkuFunc({ auth: true, permissions: { signedIn: isSignedIn }
|
|
74
|
+
pikkuFunc({ auth: true, permissions: { signedIn: isSignedIn } /* ... */ })
|
|
75
75
|
|
|
76
76
|
// RIGHT — auth: true already requires the session; permissions are for capability.
|
|
77
|
-
pikkuFunc({ auth: true
|
|
77
|
+
pikkuFunc({ auth: true /* ... */ })
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
A permission answers "
|
|
81
|
-
"
|
|
80
|
+
A permission answers "_may this user do this?_" (role, ownership, tier) — never
|
|
81
|
+
"_is there a session?_".
|
|
82
82
|
|
|
83
83
|
### `pikkuPermission(fn)` — Data-Aware Checks
|
|
84
84
|
|
|
85
85
|
Use when authorization depends on the actual request data (e.g., resource ownership).
|
|
86
86
|
|
|
87
87
|
```typescript
|
|
88
|
-
import { pikkuPermission } from '#pikku'
|
|
88
|
+
import { pikkuPermission } from '#pikku/function'
|
|
89
89
|
|
|
90
90
|
export const isBookOwner = pikkuPermission(
|
|
91
91
|
async ({ db }, { bookId }, { session }) => {
|
|
@@ -132,24 +132,24 @@ export const deleteBook = pikkuFunc({
|
|
|
132
132
|
|
|
133
133
|
### Global (`addGlobalPermission`) — App-Wide AND Gate
|
|
134
134
|
|
|
135
|
-
A global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever
|
|
135
|
+
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.
|
|
136
136
|
|
|
137
137
|
```typescript
|
|
138
|
-
import { addGlobalPermission } from '#pikku'
|
|
138
|
+
import { addGlobalPermission } from '#pikku/function'
|
|
139
139
|
|
|
140
140
|
addGlobalPermission([isEmployee]) // every function now also requires an employee session
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
Multiple `addGlobalPermission` calls accumulate and are AND'd together.
|
|
144
144
|
|
|
145
|
-
> Wire-, tag-, and HTTP-route-level permissions (`addHTTPPermission`, `addTagPermission`, and a `permissions` field on HTTP/channel/MCP wirings) were **removed in #972**. Permissions now live only on the function definition, plus the optional global gate. Tags are organizational only — use tag/HTTP
|
|
145
|
+
> Wire-, tag-, and HTTP-route-level permissions (`addHTTPPermission`, `addTagPermission`, and a `permissions` field on HTTP/channel/MCP wirings) were **removed in #972**. Permissions now live only on the function definition, plus the optional global gate. Tags are organizational only — use tag/HTTP _middleware_ (`addTagMiddleware`, `addHTTPMiddleware`) for cross-cutting request handling, not authorization.
|
|
146
146
|
|
|
147
147
|
## Scopes — the AND Gate Above Permissions
|
|
148
148
|
|
|
149
149
|
Scopes answer "what was this session granted?" before permissions ask "may this
|
|
150
150
|
user do this to this resource?". They are AND-ed: every scope listed must be
|
|
151
151
|
held. Because they are checked first and fail closed, a scope can only ever
|
|
152
|
-
|
|
152
|
+
_narrow_ access — it never grants what `permissions` would deny.
|
|
153
153
|
|
|
154
154
|
Declare the scope tree once with `defineScope`. The body is a no-op that
|
|
155
155
|
tree-shakes away; the CLI reads the call by AST and generates a `ScopeId` union,
|
|
@@ -158,7 +158,7 @@ gating on nothing.
|
|
|
158
158
|
|
|
159
159
|
```typescript
|
|
160
160
|
// src/scopes.ts
|
|
161
|
-
import { defineScope } from '#pikku'
|
|
161
|
+
import { defineScope } from '#pikku/scopes'
|
|
162
162
|
|
|
163
163
|
defineScope({
|
|
164
164
|
admin: {
|
|
@@ -222,7 +222,11 @@ export const handleStripeWebhook = pikkuSessionlessFunc({
|
|
|
222
222
|
permissionsInBody: true,
|
|
223
223
|
auth: false,
|
|
224
224
|
func: async ({ stripe }, data, { http }) => {
|
|
225
|
-
stripe.webhooks.constructEvent(
|
|
225
|
+
stripe.webhooks.constructEvent(
|
|
226
|
+
data.raw,
|
|
227
|
+
http.request.header('stripe-signature'),
|
|
228
|
+
secret
|
|
229
|
+
)
|
|
226
230
|
// ...
|
|
227
231
|
},
|
|
228
232
|
})
|
|
@@ -240,7 +244,7 @@ declared, inspectable, and reusable.
|
|
|
240
244
|
|
|
241
245
|
```typescript
|
|
242
246
|
// src/permissions.ts
|
|
243
|
-
import { pikkuAuth, pikkuPermission } from '#pikku'
|
|
247
|
+
import { pikkuAuth, pikkuPermission } from '#pikku/function'
|
|
244
248
|
|
|
245
249
|
export const isVerified = pikkuAuth(
|
|
246
250
|
async (_services, session) => !!session?.emailVerified
|
|
@@ -11,8 +11,8 @@ Existing repo → pikku-software-archaeology → .knowledge/ blueprint → pikku
|
|
|
11
11
|
|
|
12
12
|
## The split from pikku-software-archaeology
|
|
13
13
|
|
|
14
|
-
- **pikku-software-archaeology** extracts
|
|
15
|
-
- **pikku-product-second-opinion** reads that blueprint and writes an
|
|
14
|
+
- **pikku-software-archaeology** extracts _facts_ into `.knowledge/` for a machine (Pikku) to rebuild from. Engineer/generator audience.
|
|
15
|
+
- **pikku-product-second-opinion** reads that blueprint and writes an _opinionated report_ for a human to decide from. Non-technical audience.
|
|
16
16
|
|
|
17
17
|
One extracts; one advises. This skill consumes the other's output — it doesn't re-read the code.
|
|
18
18
|
|
|
@@ -30,7 +30,7 @@ One extracts; one advises. This skill consumes the other's output — it doesn't
|
|
|
30
30
|
4. **Argue improvements in business outcomes** (more reliable / faster / cheaper / safer / easier to hand off), and say whether each is a cheap **rewire** or an expensive **rebuild** — never recommend a rewrite just because the code is messy.
|
|
31
31
|
5. **Mark confidence.** Certain and "I'd need to check" are different sentences.
|
|
32
32
|
6. **Cover the frontend and the other ways the app is used** when the blueprint has them — walk the screens as a journey, call out consistency, and flag the custom-logic pieces (charts/tables/editors) as the real work vs the cheap standard pieces. Name the ways the product can be driven (people/web, developers/API+SDK, AI agents/MCP, power users/CLI) — often a genuine strength.
|
|
33
|
-
7. **Give honest technology tradeoffs — both sides.** Every stack bet (framework, auth, hosting, key libraries) gets what-it-buys AND what-it-costs in business terms, tied to the founder's stage/goals. Don't cheerlead, don't trash, and **don't soften the disadvantages**. The app's own bets are derived from the blueprint (`architecture.json`/`integrations.json`/`frontend.json` + the repo's manifest) — never from a list in the skill, because a verdict you could write before reading the blueprint isn't a second opinion. Separately, and only when a rebuild is actually being recommended, the target stack (Pikku, Better Auth, TanStack Start) gets the
|
|
33
|
+
7. **Give honest technology tradeoffs — both sides.** Every stack bet (framework, auth, hosting, key libraries) gets what-it-buys AND what-it-costs in business terms, tied to the founder's stage/goals. Don't cheerlead, don't trash, and **don't soften the disadvantages**. The app's own bets are derived from the blueprint (`architecture.json`/`integrations.json`/`frontend.json` + the repo's manifest) — never from a list in the skill, because a verdict you could write before reading the blueprint isn't a second opinion. Separately, and only when a rebuild is actually being recommended, the target stack (Pikku, Better Auth, TanStack Start) gets the _same_ both-sides treatment with cons first-class — pinning someone's dependency for being pre-1.0 while staying quiet about the replacement being pre-1.0 too is a pitch, not an opinion.
|
|
34
34
|
|
|
35
35
|
## Files
|
|
36
36
|
|