@pikku/skills 0.12.35 → 0.12.37
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/README.md +9 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +9 -5
- package/dist/skills.gen.d.ts +1 -0
- package/dist/skills.gen.js +5 -3
- package/dist/snippets.d.ts +26 -0
- package/dist/snippets.js +148 -0
- package/package.json +2 -2
- package/skills/pikku-addon/SKILL.md +40 -24
- package/skills/pikku-addon/references/addon-package-manifest.md +9 -4
- package/skills/pikku-addon/references/openapi.md +99 -0
- package/skills/pikku-agent/references/agents.md +3 -1
- package/skills/pikku-auth/references/better-auth.md +16 -0
- package/skills/pikku-build/SKILL.md +18 -1
- package/skills/pikku-build/references/app.md +52 -4
- package/skills/pikku-build/references/feature.md +23 -96
- package/skills/pikku-build/references/quick.md +12 -3
- package/skills/pikku-changes/SKILL.md +172 -0
- package/skills/pikku-concepts/SKILL.md +33 -138
- package/skills/pikku-concepts/references/bootstrap.md +58 -0
- package/skills/pikku-concepts/references/concept-mapping.md +16 -0
- package/skills/pikku-concepts/references/language.md +87 -0
- package/skills/pikku-deploy/SKILL.md +1 -1
- package/skills/pikku-fabric/SKILL.md +13 -13
- package/skills/pikku-guide/SKILL.md +264 -0
- package/skills/pikku-kysely/SKILL.md +1 -1
- package/skills/pikku-n8n-import/SKILL.md +4 -3
- package/skills/pikku-react/references/client.md +12 -0
- package/skills/pikku-realtime/SKILL.md +6 -6
- package/skills/pikku-report/SKILL.md +143 -0
- package/skills/pikku-scenario/SKILL.md +71 -563
- package/skills/pikku-scenario/references/browser.md +59 -0
- package/skills/pikku-scenario/references/coverage.md +70 -0
- package/skills/pikku-scenario/references/personas.md +87 -0
- package/skills/pikku-scenario/references/steps.md +366 -0
- package/skills/pikku-service-backends/SKILL.md +1 -1
- package/skills/pikku-wiring/SKILL.md +1 -1
- package/skills/pikku-workflow/SKILL.md +7 -8
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class UnclosedSnippetError extends Error {
|
|
2
|
+
constructor(name: string, file: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class DuplicateSnippetError extends Error {
|
|
5
|
+
constructor(name: string, file: string, other: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class MissingSnippetError extends Error {
|
|
8
|
+
constructor(name: string, file: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The `// @snippet start <name>` regions in a project's source, so an example
|
|
12
|
+
* in a doc comment can name one instead of restating it. The same mechanism the
|
|
13
|
+
* website uses to pull its code blocks out of the shop template: the code that
|
|
14
|
+
* reaches the reader is the code that compiles, and it cannot drift.
|
|
15
|
+
*/
|
|
16
|
+
export declare const collectSnippets: (projectDir: string, into?: Map<string, string>, origins?: Map<string, string>) => Promise<Map<string, string>>;
|
|
17
|
+
export type Snippets = Record<string, string> | Map<string, string>;
|
|
18
|
+
/** Every region a markdown document shows, in order. */
|
|
19
|
+
export declare const snippetRegionsIn: (markdown: string) => string[];
|
|
20
|
+
/**
|
|
21
|
+
* A skill's text with every marked fence replaced by its region's code. Used
|
|
22
|
+
* twice, deliberately: `scripts/embed.ts` bakes the result into the manifest
|
|
23
|
+
* that ships inside the CLI binary, and `readSkillFile` applies it to a
|
|
24
|
+
* filesystem copy so an installed package shows the same code the binary does.
|
|
25
|
+
*/
|
|
26
|
+
export declare const expandSkillMarkdown: (markdown: string, snippets: Snippets, file?: string) => string;
|
package/dist/snippets.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
const START = /^\s*(?:\/\/|--)\s*@snippet start (\S+)\s*$/;
|
|
4
|
+
const END = /^\s*(?:\/\/|--)\s*@snippet end (\S+)\s*$/;
|
|
5
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.sql'];
|
|
6
|
+
const SKIPPED_DIRECTORIES = new Set(['node_modules', '.pikku', 'dist', '.git']);
|
|
7
|
+
export class UnclosedSnippetError extends Error {
|
|
8
|
+
constructor(name, file) {
|
|
9
|
+
super(`The snippet "${name}" opens in ${file} and never closes. Add "// @snippet end ${name}".`);
|
|
10
|
+
this.name = 'UnclosedSnippetError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class DuplicateSnippetError extends Error {
|
|
14
|
+
constructor(name, file, other) {
|
|
15
|
+
super(`The snippet "${name}" is defined twice, in ${other} and ${file}. Snippet names are the reference an example uses, so they have to be unique.`);
|
|
16
|
+
this.name = 'DuplicateSnippetError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class MissingSnippetError extends Error {
|
|
20
|
+
constructor(name, file) {
|
|
21
|
+
super(`${file} shows the snippet "${name}", which no @snippet region defines. ` +
|
|
22
|
+
`Add "// @snippet start ${name}" to a compiled example project, or fix the fence.`);
|
|
23
|
+
this.name = 'MissingSnippetError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const dedent = (lines) => {
|
|
27
|
+
const indents = lines
|
|
28
|
+
.filter((line) => line.trim().length > 0)
|
|
29
|
+
.map((line) => line.length - line.trimStart().length);
|
|
30
|
+
const shortest = indents.length > 0 ? Math.min(...indents) : 0;
|
|
31
|
+
return lines
|
|
32
|
+
.map((line) => line.slice(shortest))
|
|
33
|
+
.join('\n')
|
|
34
|
+
.trim();
|
|
35
|
+
};
|
|
36
|
+
const sourceFilesIn = async (directory, found = []) => {
|
|
37
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
38
|
+
const full = join(directory, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
if (SKIPPED_DIRECTORIES.has(entry.name) || entry.name.startsWith('.')) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
await sourceFilesIn(full, found);
|
|
44
|
+
}
|
|
45
|
+
else if (SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
|
|
46
|
+
found.push(full);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return found;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The `// @snippet start <name>` regions in a project's source, so an example
|
|
53
|
+
* in a doc comment can name one instead of restating it. The same mechanism the
|
|
54
|
+
* website uses to pull its code blocks out of the shop template: the code that
|
|
55
|
+
* reaches the reader is the code that compiles, and it cannot drift.
|
|
56
|
+
*/
|
|
57
|
+
export const collectSnippets = async (projectDir, into = new Map(), origins = new Map()) => {
|
|
58
|
+
for (const file of await sourceFilesIn(projectDir)) {
|
|
59
|
+
const where = relative(projectDir, file);
|
|
60
|
+
const lines = (await readFile(file, 'utf8')).split('\n');
|
|
61
|
+
const open = new Map();
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const end = line.match(END);
|
|
64
|
+
if (end?.[1] && open.has(end[1])) {
|
|
65
|
+
const name = end[1];
|
|
66
|
+
const existing = origins.get(name);
|
|
67
|
+
if (existing)
|
|
68
|
+
throw new DuplicateSnippetError(name, where, existing);
|
|
69
|
+
into.set(name, dedent(open.get(name)));
|
|
70
|
+
origins.set(name, where);
|
|
71
|
+
open.delete(name);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const start = line.match(START);
|
|
75
|
+
if (start?.[1]) {
|
|
76
|
+
if (!open.has(start[1]))
|
|
77
|
+
open.set(start[1], []);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (end)
|
|
81
|
+
continue;
|
|
82
|
+
for (const body of open.values())
|
|
83
|
+
body.push(line);
|
|
84
|
+
}
|
|
85
|
+
const [unclosed] = open.keys();
|
|
86
|
+
if (unclosed)
|
|
87
|
+
throw new UnclosedSnippetError(unclosed, where);
|
|
88
|
+
}
|
|
89
|
+
return into;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* A fence that names a region instead of carrying code:
|
|
93
|
+
*
|
|
94
|
+
* ```ts snippet:definePersonas
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* The language is optional and kept. Marked fences are replaced whole — both
|
|
98
|
+
* the placeholder form and a filled-in block, so a skill can be migrated
|
|
99
|
+
* without hand-deleting the old copy.
|
|
100
|
+
*/
|
|
101
|
+
const SNIPPET_FENCE = /^(\s*)```(\S*)\s+snippet:([A-Za-z0-9_.-]+)\s*$/;
|
|
102
|
+
const CLOSING_FENCE = /^\s*```\s*$/;
|
|
103
|
+
const replace = (markdown, lookup) => {
|
|
104
|
+
const lines = markdown.split('\n');
|
|
105
|
+
const out = [];
|
|
106
|
+
for (let i = 0; i < lines.length; i++) {
|
|
107
|
+
const line = lines[i];
|
|
108
|
+
const match = line.match(SNIPPET_FENCE);
|
|
109
|
+
if (!match) {
|
|
110
|
+
out.push(line);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const indent = match[1] ?? '';
|
|
114
|
+
const lang = match[2] ?? '';
|
|
115
|
+
const region = match[3];
|
|
116
|
+
const body = lookup(region);
|
|
117
|
+
out.push(`${indent}\`\`\`${lang}`);
|
|
118
|
+
for (const bodyLine of body.split('\n')) {
|
|
119
|
+
out.push(bodyLine.length > 0 ? indent + bodyLine : '');
|
|
120
|
+
}
|
|
121
|
+
out.push(`${indent}\`\`\``);
|
|
122
|
+
while (i + 1 < lines.length && !CLOSING_FENCE.test(lines[i + 1]))
|
|
123
|
+
i++;
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
return out.join('\n');
|
|
127
|
+
};
|
|
128
|
+
/** Every region a markdown document shows, in order. */
|
|
129
|
+
export const snippetRegionsIn = (markdown) => markdown
|
|
130
|
+
.split('\n')
|
|
131
|
+
.map((line) => line.match(SNIPPET_FENCE))
|
|
132
|
+
.filter((match) => match !== null)
|
|
133
|
+
.map((match) => match[3]);
|
|
134
|
+
/**
|
|
135
|
+
* A skill's text with every marked fence replaced by its region's code. Used
|
|
136
|
+
* twice, deliberately: `scripts/embed.ts` bakes the result into the manifest
|
|
137
|
+
* that ships inside the CLI binary, and `readSkillFile` applies it to a
|
|
138
|
+
* filesystem copy so an installed package shows the same code the binary does.
|
|
139
|
+
*/
|
|
140
|
+
export const expandSkillMarkdown = (markdown, snippets, file = 'skill') => {
|
|
141
|
+
const lookup = (name) => {
|
|
142
|
+
const body = snippets instanceof Map ? snippets.get(name) : snippets[name];
|
|
143
|
+
if (body === undefined)
|
|
144
|
+
throw new MissingSnippetError(name, file);
|
|
145
|
+
return body;
|
|
146
|
+
};
|
|
147
|
+
return replace(markdown, lookup);
|
|
148
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/skills",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.37",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"types": "dist/index.d.ts",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"scripts": {
|
|
17
|
-
"embed": "
|
|
17
|
+
"embed": "bun run scripts/embed.ts",
|
|
18
18
|
"tsc": "bun run embed && tsc",
|
|
19
19
|
"build": "bun run embed && tsc -b",
|
|
20
20
|
"ncu": "npx npm-check-updates",
|
|
@@ -3,8 +3,10 @@ name: pikku-addon
|
|
|
3
3
|
description: >-
|
|
4
4
|
Use when creating or consuming reusable function packages (addons) in Pikku. Covers wireAddon,
|
|
5
5
|
ref(), pikkuAddonServices, pikkuAddonWireServices, addon package structure, addons that ship
|
|
6
|
-
database tables (pikku db export),
|
|
7
|
-
|
|
6
|
+
database tables (pikku db export), generating an addon from an OpenAPI/Swagger spec, and
|
|
7
|
+
cross-project function sharing. TRIGGER when: code uses wireAddon/ref()/pikkuAddonServices, user
|
|
8
|
+
asks about addons, reusable function packages, cross-project sharing, or addon package structure,
|
|
9
|
+
or the user hands over an OpenAPI/Swagger spec (file or URL) to build on. DO NOT
|
|
8
10
|
TRIGGER when: user asks about internal function composition (use pikku-wiring) or general function
|
|
9
11
|
definitions (use pikku-concepts).
|
|
10
12
|
installGroups: [core]
|
|
@@ -19,7 +21,7 @@ Use this skill as an execution checklist, not reference material.
|
|
|
19
21
|
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
20
22
|
2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
|
|
21
23
|
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
22
|
-
4. Validate with the narrowest relevant command first, then run `pikku
|
|
24
|
+
4. Validate with the narrowest relevant command first, then run `pikku all` when functions, wirings, schemas, or generated clients may have changed.
|
|
23
25
|
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
24
26
|
|
|
25
27
|
Addons are reusable Pikku function packages that can be shared across projects. They bundle functions, services, secrets, and variables into a self-contained NPM package.
|
|
@@ -48,6 +50,7 @@ wireAddon({
|
|
|
48
50
|
rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution
|
|
49
51
|
auth?: boolean, // Require a session for every function in the addon
|
|
50
52
|
mcp?: boolean | string[], // true: every function the addon declared mcp: true; a list: the tools this app offers, typed against the addon's function names
|
|
53
|
+
expose?: boolean | string[], // what rpc.exposed / POST /rpc may reach: unset/true = the addon's own expose: true, false = none, a list = exactly those (typed; PKU343 on unknown names)
|
|
51
54
|
tags?: string[], // Tags applied to all addon functions
|
|
52
55
|
scopes?: string[], // Required of every function, on top of its own
|
|
53
56
|
secretOverrides?: Record<string, string>, // Remap secret names (and grant them)
|
|
@@ -146,7 +149,7 @@ second argument is always present — an addon never falls back to its own logge
|
|
|
146
149
|
variables or secrets; the consuming app supplies them:
|
|
147
150
|
|
|
148
151
|
```typescript
|
|
149
|
-
import { pikkuAddonServices } from '#pikku/setup'
|
|
152
|
+
import { pikkuAddonServices } from '#pikku/addon/setup'
|
|
150
153
|
|
|
151
154
|
export const createSingletonServices = pikkuAddonServices(
|
|
152
155
|
async (config, { secrets, logger }) => {
|
|
@@ -167,7 +170,7 @@ config object.
|
|
|
167
170
|
Define per-request services for an addon package (created fresh per HTTP request, queue job, etc.):
|
|
168
171
|
|
|
169
172
|
```typescript
|
|
170
|
-
import { pikkuAddonWireServices } from '#pikku/setup'
|
|
173
|
+
import { pikkuAddonWireServices } from '#pikku/addon/setup'
|
|
171
174
|
|
|
172
175
|
export const createWireServices = pikkuAddonWireServices(
|
|
173
176
|
async (singletonServices, wire) => {
|
|
@@ -189,13 +192,17 @@ npx pikku new addon <name> # name is a required positional
|
|
|
189
192
|
npx pikku new addon stripe --display-name Stripe --category Payments --dir addons
|
|
190
193
|
```
|
|
191
194
|
|
|
195
|
+
**Handed an OpenAPI or Swagger spec?** Don't hand-write the functions —
|
|
196
|
+
`pikku new addon <name> --openapi <spec>` generates one function per operation,
|
|
197
|
+
with its schemas, service and credential. Read `references/openapi.md`.
|
|
198
|
+
|
|
192
199
|
This generates `package.json` (exports `.pikku/*` + `dist/`), `pikku.config.json` (`addon: true`), `tsconfig.json` (`#pikku` path mapping), `src/services.ts`, `src/functions/`, and `types/application-types.d.ts`. For the full file contents/exports you rarely hand-edit, read `references/addon-package-manifest.md`.
|
|
193
200
|
|
|
194
201
|
### Services
|
|
195
202
|
|
|
196
203
|
```typescript
|
|
197
204
|
// src/services.ts
|
|
198
|
-
import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/setup'
|
|
205
|
+
import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/addon/setup'
|
|
199
206
|
import { TodoStore } from './todo-store.service.js'
|
|
200
207
|
|
|
201
208
|
export const createSingletonServices = pikkuAddonServices(async () => {
|
|
@@ -213,18 +220,30 @@ export const createWireServices = pikkuAddonWireServices(
|
|
|
213
220
|
|
|
214
221
|
### Functions
|
|
215
222
|
|
|
216
|
-
An addon's generated tree roots at `.pikku/addon/`,
|
|
217
|
-
|
|
218
|
-
`#pikku/function
|
|
219
|
-
|
|
223
|
+
An addon's generated tree roots at `.pikku/addon/`, and its own source reaches
|
|
224
|
+
it by that path — `#pikku/addon/function`, `#pikku/addon/setup`. An application
|
|
225
|
+
authors against `#pikku/function`; an addon never does, because inside the addon
|
|
226
|
+
`#pikku/function` names a leaf that does not exist.
|
|
227
|
+
|
|
228
|
+
**Declare zod schemas in a file that never imports `#pikku`.** `pikku all` loads
|
|
229
|
+
the file that declares each schema to convert it, and at runtime `#pikku`
|
|
230
|
+
resolves through the package's `imports` — into a `dist` the first build has not
|
|
231
|
+
written yet. Inline, every schema fails with `Could not convert Zod schema …
|
|
232
|
+
Cannot find module …/dist/.pikku/…` and the addon never builds from clean. A
|
|
233
|
+
sibling `<fn>.schemas.ts` is what `--openapi` generates:
|
|
220
234
|
|
|
221
235
|
```typescript
|
|
222
|
-
// src/functions/addTodo.
|
|
236
|
+
// src/functions/addTodo.schemas.ts
|
|
223
237
|
import { z } from 'zod'
|
|
224
|
-
import { pikkuSessionlessFunc } from '#pikku/function'
|
|
225
238
|
|
|
226
|
-
const AddTodoInput = z.object({ title: z.string() })
|
|
227
|
-
const AddTodoOutput = z.object({ id: z.string(), title: z.string() })
|
|
239
|
+
export const AddTodoInput = z.object({ title: z.string() })
|
|
240
|
+
export const AddTodoOutput = z.object({ id: z.string(), title: z.string() })
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// src/functions/addTodo.function.ts
|
|
245
|
+
import { pikkuSessionlessFunc } from '#pikku/addon/function'
|
|
246
|
+
import { AddTodoInput, AddTodoOutput } from './addTodo.schemas.js'
|
|
228
247
|
|
|
229
248
|
export const addTodo = pikkuSessionlessFunc({
|
|
230
249
|
description: 'Adds a new todo',
|
|
@@ -246,9 +265,7 @@ approvalDescription: async (_services, { title }) => `Add a todo called "${title
|
|
|
246
265
|
### Build
|
|
247
266
|
|
|
248
267
|
```bash
|
|
249
|
-
yarn pikku all
|
|
250
|
-
yarn tsc # Compile TypeScript
|
|
251
|
-
cp -r .pikku types dist/ # Ship the generated files and the types they import
|
|
268
|
+
yarn build # prebuild: pikku all, then tsc && pikku dist
|
|
252
269
|
yarn pikku validate # Check the published file set holds together
|
|
253
270
|
```
|
|
254
271
|
|
|
@@ -257,12 +274,11 @@ devDependency, and building it against a different CLI than it declares is how
|
|
|
257
274
|
generated output ends up disagreeing with the packaged one. `npx pikku new
|
|
258
275
|
addon` above is the exception — it runs before the addon, and its CLI, exist.
|
|
259
276
|
|
|
260
|
-
`
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
which is what `pikku validate` is there to catch before you publish.
|
|
277
|
+
`pikku dist` copies what `tsc` cannot emit — the generated `*.gen.json` meta and
|
|
278
|
+
the hand-written `types/*.d.ts` the generated files import — to where `tsc` put
|
|
279
|
+
everything else. Leave it out and the addon installs fine and fails to typecheck
|
|
280
|
+
in every app that depends on it, which is what `pikku validate` is there to
|
|
281
|
+
catch before you publish.
|
|
266
282
|
|
|
267
283
|
### Database tables
|
|
268
284
|
|
|
@@ -297,7 +313,7 @@ packed, or it never arrives:
|
|
|
297
313
|
**An unresolvable artifact stops `db generate`.** Because the file is
|
|
298
314
|
unconditional, absence means the package cannot say whether it ships tables —
|
|
299
315
|
either it was built with an older CLI, or `exports`/`files` do not carry it. The
|
|
300
|
-
error names both causes. An addon with genuinely no tables is
|
|
316
|
+
error names both causes. An addon with genuinely no tables is _not_ this case:
|
|
301
317
|
it publishes `{}` and is waved through.
|
|
302
318
|
|
|
303
319
|
Two more loud ones: a malformed artifact (missing the SQL for a dialect it
|
|
@@ -22,7 +22,7 @@ An addon's generated tree roots one level down, at `.pikku/addon/`, so its own
|
|
|
22
22
|
leaves are reached as `#pikku/addon/<leaf>` while an application's are
|
|
23
23
|
`#pikku/<leaf>`. `paths` are global to a tsx process rather than scoped to the
|
|
24
24
|
package that declared them, and the extra segment is what stops a linked addon's
|
|
25
|
-
`#pikku/function` from matching the
|
|
25
|
+
`#pikku/function` from matching the _host application's_ flat leaf.
|
|
26
26
|
|
|
27
27
|
## pikku.config.json
|
|
28
28
|
|
|
@@ -68,20 +68,25 @@ package that declared them, and the extra segment is what stops a linked addon's
|
|
|
68
68
|
"scripts": {
|
|
69
69
|
"prebuild": "pikku all",
|
|
70
70
|
"pikku": "pikku all",
|
|
71
|
-
"build": "tsc &&
|
|
71
|
+
"build": "tsc && pikku dist"
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
**`imports` names `dist`, never the source tree.** `files: ["dist"]` is the whole
|
|
77
|
-
published package, and `
|
|
77
|
+
published package, and `pikku dist` copies `.pikku` and `types` into it — so a
|
|
78
78
|
`#pikku/*` target under `./.pikku/` resolves for the author and for nobody else.
|
|
79
79
|
It is a silent break: the addon compiles, packs, installs and then throws
|
|
80
80
|
`Cannot find module '.../.pikku/addon/function/index.ts'` on first import in the
|
|
81
|
-
consuming app, out of a file the consumer never wrote. The addon's own
|
|
81
|
+
consuming app, out of a file the consumer never wrote. The addon's own `tsc`
|
|
82
82
|
does not read `imports` at all — tsconfig `paths` covers it, which is why the
|
|
83
83
|
two maps point at different trees.
|
|
84
84
|
|
|
85
|
+
`pikku all` does read `imports`, though: it loads each file that declares a zod
|
|
86
|
+
schema to convert it to JSON Schema, and at runtime `#pikku` resolves through
|
|
87
|
+
`imports`, into a `dist` the first build has not written yet. So a schema must
|
|
88
|
+
live in a file that never imports `#pikku` — see "Functions" in the skill.
|
|
89
|
+
|
|
85
90
|
**`exports` targets carry the `addon` segment; the subpaths do not.** A consumer
|
|
86
91
|
writes `@my-org/addon-todos/.pikku/rpc/...`, exactly as it would in an
|
|
87
92
|
application, and the leaf stays the package's own business.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# An addon from an OpenAPI spec
|
|
2
|
+
|
|
3
|
+
When you are handed an OpenAPI or Swagger spec — a file, or a URL to one — the
|
|
4
|
+
API it describes becomes an addon: one function per operation, each with its
|
|
5
|
+
input and output schemas, behind one service that makes the HTTP calls. Say so
|
|
6
|
+
before starting ("This is an OpenAPI spec — I'll turn it into an addon first"),
|
|
7
|
+
then generate it. Don't hand-write the functions, and don't ask which
|
|
8
|
+
operations to keep: generate the whole spec, however large.
|
|
9
|
+
|
|
10
|
+
## Recognising one
|
|
11
|
+
|
|
12
|
+
A JSON or YAML document with a top-level `openapi` key (3.x) or `swagger` key
|
|
13
|
+
(2.0), and a `paths` object. A URL ending in `openapi.json`, `swagger.json` or
|
|
14
|
+
`.yaml` is almost always one; open it and check the key before generating.
|
|
15
|
+
|
|
16
|
+
## 1 — Put the spec in the repo
|
|
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:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
mkdir -p specs
|
|
23
|
+
curl -fsSL <url> -o specs/<name>.openapi.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2 — Generate
|
|
27
|
+
|
|
28
|
+
Inside an app, the addon is a workspace package under `packages/`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bunx --bun pikku new addon <name> --openapi specs/<name>.openapi.json --dir packages
|
|
32
|
+
```
|
|
33
|
+
|
|
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.
|
|
48
|
+
|
|
49
|
+
## 3 — Check what was generated
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
packages/addon-<name>/
|
|
53
|
+
├── 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
|
|
55
|
+
├── src/functions/<op>.function.ts
|
|
56
|
+
├── src/functions/<op>.schemas.ts # the op's zod schemas — never import #pikku here
|
|
57
|
+
└── src/index.ts # re-exports every function
|
|
58
|
+
```
|
|
59
|
+
|
|
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
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// src/addons.ts
|
|
69
|
+
import { wireAddon } from '#pikku/addon'
|
|
70
|
+
|
|
71
|
+
wireAddon({ name: '<name>', package: '@pikku/addon-<name>' })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then call operations by reference — `ref('<name>:<operationFn>')` in a
|
|
75
|
+
workflow, agent tool or HTTP wiring — the same way as any other addon. See
|
|
76
|
+
"Consuming an Addon" in the skill.
|
|
77
|
+
|
|
78
|
+
## 5 — Verify
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bunx --bun pikku all
|
|
82
|
+
bunx --bun pikku validate
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`Could not convert Zod schema … Cannot find module …/dist/.pikku/…` means a
|
|
86
|
+
schema is declared in a file that imports `#pikku`. The generator never does
|
|
87
|
+
that, so a hand edit moved it — put it back in `<op>.schemas.ts`.
|
|
88
|
+
|
|
89
|
+
`pikku validate` reports a `skewed-type-identity` error (and `pikku all` warns
|
|
90
|
+
`[PKU719]`) when the CLI and the app resolve different copies of `zod` or
|
|
91
|
+
another shared package. Codegen then reads the app's
|
|
92
|
+
schemas with the wrong copy and fails on schemas that are correct. Pin one
|
|
93
|
+
version for the whole install, as the finding says, and reinstall.
|
|
94
|
+
|
|
95
|
+
## Then
|
|
96
|
+
|
|
97
|
+
Go back to the mode you were building in (`pikku-build`). The addon is a
|
|
98
|
+
dependency of the app, not the app: plan milestones around what the user wants
|
|
99
|
+
to do with the API, and reach the operations through `ref()`.
|
|
@@ -35,7 +35,9 @@ pikkuAgent({
|
|
|
35
35
|
temperature?: number,
|
|
36
36
|
providerOptions?: { // passed through untouched, keyed by provider
|
|
37
37
|
openai?: { reasoningEffort?: 'minimal' | ... },
|
|
38
|
-
},
|
|
38
|
+
}, // inline literals only — a computed value
|
|
39
|
+
// cannot be read into the generated metadata
|
|
40
|
+
// and is reported as PKU156
|
|
39
41
|
|
|
40
42
|
// --- capabilities: all three take ref() handles, not imported values ---
|
|
41
43
|
tools?: unknown[], // ref('todos:addTodo'), ref('graph:sleep'), …
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Pikku Better Auth Integration
|
|
2
2
|
|
|
3
|
+
Every section, in the order it is usually needed:
|
|
4
|
+
|
|
5
|
+
- [⚠️ MANDATORY RULE — READ FIRST](#mandatory-rule--read-first)
|
|
6
|
+
- [Installation](#installation)
|
|
7
|
+
- [Core Concepts](#core-concepts)
|
|
8
|
+
- [Standard Setup](#standard-setup)
|
|
9
|
+
- [⚠️ Stateless session — ALWAYS enable `cookieCache` for deployed apps](#stateless-session--always-enable-cookiecache-for-deployed-apps)
|
|
10
|
+
- [Social Providers needing extra config](#social-providers-needing-extra-config)
|
|
11
|
+
- [Auth-Protected Functions](#auth-protected-functions)
|
|
12
|
+
- [HTTP surface (call the real endpoints)](#http-surface-call-the-real-endpoints)
|
|
13
|
+
- [Secret Management](#secret-management)
|
|
14
|
+
- [`pikkuBetterAuth` API](#pikkubetterauth-api)
|
|
15
|
+
- [Post-signup side effects](#post-signup-side-effects)
|
|
16
|
+
- [Two-factor (2FA / MFA)](#two-factor-2fa--mfa)
|
|
17
|
+
- [Security hardening](#security-hardening)
|
|
18
|
+
|
|
3
19
|
## ⚠️ MANDATORY RULE — READ FIRST
|
|
4
20
|
|
|
5
21
|
**ALL authentication in Pikku apps MUST use `@pikku/better-auth`. No exceptions.**
|
|
@@ -24,7 +24,6 @@ agent:
|
|
|
24
24
|
command: pikku knowledge validate
|
|
25
25
|
- id: typechecks
|
|
26
26
|
command: pikku all --tsc-summary
|
|
27
|
-
|
|
28
27
|
---
|
|
29
28
|
|
|
30
29
|
# Build on Pikku
|
|
@@ -63,6 +62,24 @@ generated code depends on, and on a fresh scaffold **every command that touches
|
|
|
63
62
|
codegen fails until it has run**, including ones you would reasonably reach for
|
|
64
63
|
while still planning. Those failures look alarming and are nothing but this.
|
|
65
64
|
|
|
65
|
+
## Start from what you were handed
|
|
66
|
+
|
|
67
|
+
When the request comes with a file or a URL, look at it before planning
|
|
68
|
+
anything. Two kinds are converted first and then built on:
|
|
69
|
+
|
|
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`. |
|
|
74
|
+
|
|
75
|
+
Say it at once, in one line, and start: this is the obvious first move, not a
|
|
76
|
+
question for the user. Generate the whole spec, however large.
|
|
77
|
+
|
|
78
|
+
Neither is the app. When the conversion compiles, come back here and carry on
|
|
79
|
+
in the mode the request calls for — App by default — planning milestones around
|
|
80
|
+
what the user wants to do with the API or the workflow, and reaching the
|
|
81
|
+
generated functions through `ref()`.
|
|
82
|
+
|
|
66
83
|
## What holds in every mode
|
|
67
84
|
|
|
68
85
|
- **The branch and the diff are the contract.** A reviewer sees real, compiled,
|
|
@@ -494,6 +494,35 @@ Rules that are not optional:
|
|
|
494
494
|
render the failure inline next to the control that triggered it — not a toast.
|
|
495
495
|
- An exposed function with no session and no permission is reachable by anyone
|
|
496
496
|
over `POST /rpc/:rpcName` (PKU574). Either gate it or drop `expose: true`.
|
|
497
|
+
- A public, signed-out read (a homepage's programme, a price list) is a
|
|
498
|
+
`pikkuSessionlessFunc`. `pikkuFunc` with `auth: false` still answers
|
|
499
|
+
`MissingSessionError` over `/rpc` to a caller with no session.
|
|
500
|
+
- 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
|
|
503
|
+
(`evening`, `class_meeting`) and keep the word in the UI copy.
|
|
504
|
+
- The template's `/` redirects to `/app`, so the login screen — and its "Sign in
|
|
505
|
+
as …" switcher — is what a signed-out visitor sees first. Replace `/` with a
|
|
506
|
+
public homepage and that stops being true: mount `<DevActorSwitcher />` in the
|
|
507
|
+
public layout as well, or a reviewer lands on a site with no way in.
|
|
508
|
+
|
|
509
|
+
Before the first run, make sure `.env` at the project root holds the two
|
|
510
|
+
secrets the local stack needs. `bun run dev` appends whichever is missing, but
|
|
511
|
+
check anyway — a project scaffolded from an older template, or a `.env` copied
|
|
512
|
+
in from elsewhere, can lack one, and neither failure names the variable:
|
|
513
|
+
|
|
514
|
+
- `BETTER_AUTH_SECRET` — without it the first sign-up is a 500.
|
|
515
|
+
- `SCENARIO_ACTOR_SECRET` — without it `/api/auth/sign-in/actor` is disabled:
|
|
516
|
+
every scenario fails at sign-in before its first step, and the "Sign in as …"
|
|
517
|
+
switcher renders nothing.
|
|
518
|
+
|
|
519
|
+
```sh
|
|
520
|
+
grep -q '^BETTER_AUTH_SECRET=' .env 2>/dev/null || echo "BETTER_AUTH_SECRET=$(openssl rand -base64 32)" >> .env
|
|
521
|
+
grep -q '^SCENARIO_ACTOR_SECRET=' .env 2>/dev/null || echo "SCENARIO_ACTOR_SECRET=$(openssl rand -base64 32)" >> .env
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
`.env` is gitignored and local only — never commit it. A deployed stage gets its
|
|
525
|
+
secrets from the platform (`pikku fabric secrets`), not from this file.
|
|
497
526
|
|
|
498
527
|
Then run it:
|
|
499
528
|
|
|
@@ -505,6 +534,19 @@ That starts the API on :3000 and every frontend in `pikkufabric.config.json`. A
|
|
|
505
534
|
frontend running against a dead API looks exactly like an app bug, so if every
|
|
506
535
|
request fails, check that both halves came up.
|
|
507
536
|
|
|
537
|
+
**Start the stack through `bun run dev`, not by launching `vite` or `pikku dev`
|
|
538
|
+
yourself.** The dev script reads the personas, derives one credential per
|
|
539
|
+
persona from `SCENARIO_ACTOR_SECRET`, and hands both to the frontend as
|
|
540
|
+
`VITE_DEV_ACTORS` / `VITE_DEV_ACTOR_SECRETS`. Vite reads those once, at boot. A
|
|
541
|
+
frontend started any other way — or restarted by hand later — has an empty
|
|
542
|
+
actor list, and the switcher silently disappears from every page. If you do
|
|
543
|
+
start the frontend on its own (say :3000 is taken by another project), you owe
|
|
544
|
+
it three things: the two `VITE_DEV_*` values the dev script would have computed,
|
|
545
|
+
and `VITE_API_PROXY` pointing at your API — the dev proxy defaults to
|
|
546
|
+
`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
|
|
548
|
+
credential rather than the wrong server.
|
|
549
|
+
|
|
508
550
|
The `--bun` in `bunx --bun pikku …` is load-bearing — keep it. Without it the
|
|
509
551
|
CLI's `#!/usr/bin/env node` shebang hands the process to whatever Node is on
|
|
510
552
|
PATH, which fails below Node 24 with `ERR_UNKNOWN_BUILTIN_MODULE: No such
|
|
@@ -614,10 +656,11 @@ export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
|
|
|
614
656
|
- **Write the refusals.** The third step above is the whole point of §4: one
|
|
615
657
|
persona reaching for another's row has to be rejected, and that rejection is a
|
|
616
658
|
scenario. It is how you prove access control instead of asserting it.
|
|
617
|
-
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
659
|
+
- **`SCENARIO_ACTOR_SECRET` must be in `.env`** (§6, before the first run).
|
|
660
|
+
Without it `/api/auth/sign-in/actor` is disabled — every scenario then fails
|
|
661
|
+
at sign-in, before its first step, for a reason that reads like an auth bug.
|
|
662
|
+
`pikku scenario run` reads it from the environment, so source `.env` first
|
|
663
|
+
(`set -a && . ./.env && set +a`) when you run outside `bun run dev`.
|
|
621
664
|
- **There is no state reset.** A scenario runs against a live server: scope what
|
|
622
665
|
you create to your own rows and unique ids, and never assume a clean database.
|
|
623
666
|
|
|
@@ -786,6 +829,11 @@ standalone`, `cloudflare`, `aws`), how to serve several frontends behind one
|
|
|
786
829
|
API, the pre-release gate to run, and the contract that keeps `pikku fabric init`
|
|
787
830
|
a one-command import later rather than a migration.
|
|
788
831
|
|
|
832
|
+
The app is not handed over without its user guide. The scenarios you wrote are
|
|
833
|
+
already its skeleton: read **pikku-guide**, write one page per audience citing
|
|
834
|
+
every feature, and build it from a full, passing `--run browser --screenshots`
|
|
835
|
+
run.
|
|
836
|
+
|
|
789
837
|
Two things from it are worth knowing before you get there, because they are
|
|
790
838
|
cheaper to honour than to retrofit:
|
|
791
839
|
|