@pikku/skills 0.12.10 → 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.
Files changed (60) hide show
  1. package/CHANGELOG.md +768 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +4 -4
  4. package/skills/pikku-addon/SKILL.md +17 -11
  5. package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +3 -3
  7. package/skills/pikku-audit/SKILL.md +28 -13
  8. package/skills/pikku-aws/SKILL.md +2 -2
  9. package/skills/pikku-better-auth/SKILL.md +97 -17
  10. package/skills/pikku-build-app/SKILL.md +621 -0
  11. package/skills/pikku-build-app/references/multi-app.md +117 -0
  12. package/skills/pikku-build-app/references/ship.md +98 -0
  13. package/skills/pikku-build-app/references/theming.md +70 -0
  14. package/skills/pikku-build-platform/SKILL.md +239 -0
  15. package/skills/pikku-build-quick/SKILL.md +238 -0
  16. package/skills/pikku-cli/SKILL.md +7 -7
  17. package/skills/pikku-cli/references/complete-example.md +1 -1
  18. package/skills/pikku-concepts/SKILL.md +5 -2
  19. package/skills/pikku-concepts/references/concept-mapping.md +1 -1
  20. package/skills/pikku-config/SKILL.md +5 -3
  21. package/skills/pikku-deploy-azure/SKILL.md +5 -4
  22. package/skills/pikku-emails/SKILL.md +5 -5
  23. package/skills/pikku-fabric/SKILL.md +27 -3
  24. package/skills/pikku-fabric-debug/SKILL.md +1 -1
  25. package/skills/pikku-feature/SKILL.md +5 -4
  26. package/skills/pikku-http/SKILL.md +4 -4
  27. package/skills/pikku-http/references/http-options.md +13 -13
  28. package/skills/pikku-i18n/SKILL.md +2 -1
  29. package/skills/pikku-info/SKILL.md +1 -1
  30. package/skills/pikku-knowledge/SKILL.md +13 -13
  31. package/skills/pikku-mcp/SKILL.md +4 -4
  32. package/skills/pikku-middleware/SKILL.md +5 -5
  33. package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
  34. package/skills/pikku-n8n-import/SKILL.md +12 -12
  35. package/skills/pikku-n8n-import/SPEC.md +3 -0
  36. package/skills/pikku-n8n-import/references/code-translation.md +26 -22
  37. package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
  38. package/skills/pikku-paraglide/SKILL.md +11 -6
  39. package/skills/pikku-permissions/SKILL.md +19 -15
  40. package/skills/pikku-product-second-opinion/README.md +3 -3
  41. package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
  42. package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
  43. package/skills/pikku-queue/SKILL.md +1 -1
  44. package/skills/pikku-react/SKILL.md +53 -13
  45. package/skills/pikku-realtime/SKILL.md +51 -19
  46. package/skills/pikku-rtl/SKILL.md +1 -1
  47. package/skills/pikku-scenario/SKILL.md +126 -14
  48. package/skills/pikku-schedule/SKILL.md +6 -1
  49. package/skills/pikku-schema-ajv/SKILL.md +2 -2
  50. package/skills/pikku-schema-cfworker/SKILL.md +1 -1
  51. package/skills/pikku-security/SKILL.md +9 -5
  52. package/skills/pikku-services/SKILL.md +27 -18
  53. package/skills/pikku-services/references/audit-wire-service.md +14 -8
  54. package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
  55. package/skills/pikku-tag-middleware/SKILL.md +1 -0
  56. package/skills/pikku-template-clone/SKILL.md +2 -1
  57. package/skills/pikku-trigger/SKILL.md +3 -3
  58. package/skills/pikku-websocket/SKILL.md +4 -3
  59. package/skills/pikku-workflow/SKILL.md +2 -2
  60. package/skills/pikku-workflow/references/workflow-reference.md +24 -10
@@ -9,13 +9,15 @@ Pair with `createAuditedKysely` to auto-capture every Kysely query as an audit e
9
9
  import { createInvocationAudit } from '@pikku/core/services'
10
10
  import { createAuditedKysely } from '@pikku/kysely'
11
11
 
12
- export const createWireServices = pikkuWireServices(async (singletonServices, wire) => {
13
- const audit = createInvocationAudit(singletonServices.audit, wire)
14
- const kysely = singletonServices.kysely
15
- ? createAuditedKysely(singletonServices.kysely, { audit })
16
- : undefined
17
- return { audit, ...(kysely ? { kysely } : {}) }
18
- })
12
+ export const createWireServices = pikkuWireServices(
13
+ async (singletonServices, wire) => {
14
+ const audit = createInvocationAudit(singletonServices.audit, wire)
15
+ const kysely = singletonServices.kysely
16
+ ? createAuditedKysely(singletonServices.kysely, { audit })
17
+ : undefined
18
+ return { audit, ...(kysely ? { kysely } : {}) }
19
+ }
20
+ )
19
21
  ```
20
22
 
21
23
  The `audit` wire service is typed as `AuditLog` (from `@pikku/core`). Functions that emit custom events use it directly:
@@ -24,7 +26,11 @@ The `audit` wire service is typed as `AuditLog` (from `@pikku/core`). Functions
24
26
  const deleteUser = pikkuFunc({
25
27
  func: async ({ audit }, { userId }) => {
26
28
  // The user identity comes from the wire session — the payload is metadata.
27
- await audit.write({ type: 'user.deleted', source: 'explicit', metadata: { userId } })
29
+ await audit.write({
30
+ type: 'user.deleted',
31
+ source: 'explicit',
32
+ metadata: { userId },
33
+ })
28
34
  // ...
29
35
  },
30
36
  })
@@ -5,118 +5,182 @@
5
5
  //
6
6
  // Usage: node validate.mjs <path-to-.knowledge-dir>
7
7
 
8
- import { readFileSync, existsSync } from 'node:fs';
9
- import { join, dirname } from 'node:path';
10
- import { fileURLToPath } from 'node:url';
8
+ import { readFileSync, existsSync } from 'node:fs'
9
+ import { join, dirname } from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
11
 
12
- const here = dirname(fileURLToPath(import.meta.url));
13
- const schemaDoc = JSON.parse(readFileSync(join(here, '..', 'references', 'blueprint.schema.json'), 'utf8'));
12
+ const here = dirname(fileURLToPath(import.meta.url))
13
+ const schemaDoc = JSON.parse(
14
+ readFileSync(join(here, '..', 'references', 'blueprint.schema.json'), 'utf8')
15
+ )
14
16
 
15
- const dir = process.argv[2];
16
- if (!dir) { console.error('usage: node validate.mjs <.knowledge dir>'); process.exit(2); }
17
+ const dir = process.argv[2]
18
+ if (!dir) {
19
+ console.error('usage: node validate.mjs <.knowledge dir>')
20
+ process.exit(2)
21
+ }
17
22
 
18
- const errors = [];
19
- const warnings = [];
23
+ const errors = []
24
+ const warnings = []
20
25
 
21
26
  // --- minimal JSON-Schema-subset validator (type, required, properties, items, enum, minItems, pattern, $ref -> $defs) ---
22
27
  function resolveRef(ref) {
23
- const m = /^#\/\$defs\/(\w+)$/.exec(ref);
24
- if (!m || !schemaDoc.$defs[m[1]]) throw new Error(`unresolvable $ref ${ref}`);
25
- return schemaDoc.$defs[m[1]];
28
+ const m = /^#\/\$defs\/(\w+)$/.exec(ref)
29
+ if (!m || !schemaDoc.$defs[m[1]]) throw new Error(`unresolvable $ref ${ref}`)
30
+ return schemaDoc.$defs[m[1]]
26
31
  }
27
32
 
28
33
  function check(value, schema, path) {
29
- if (schema.$ref) schema = { ...resolveRef(schema.$ref), ...schema, $ref: undefined };
34
+ if (schema.$ref)
35
+ schema = { ...resolveRef(schema.$ref), ...schema, $ref: undefined }
30
36
  if (schema.enum && !schema.enum.includes(value)) {
31
- errors.push(`${path}: expected one of [${schema.enum.join(', ')}], got ${JSON.stringify(value)}`);
32
- return;
37
+ errors.push(
38
+ `${path}: expected one of [${schema.enum.join(', ')}], got ${JSON.stringify(value)}`
39
+ )
40
+ return
33
41
  }
34
- const t = schema.type;
42
+ const t = schema.type
35
43
  if (t === 'object') {
36
44
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
37
- errors.push(`${path}: expected object`); return;
45
+ errors.push(`${path}: expected object`)
46
+ return
38
47
  }
39
48
  for (const req of schema.required || []) {
40
- if (!(req in value)) errors.push(`${path}: missing required field "${req}"`);
49
+ if (!(req in value))
50
+ errors.push(`${path}: missing required field "${req}"`)
41
51
  }
42
52
  for (const [k, v] of Object.entries(value)) {
43
- if (schema.properties?.[k]) check(v, schema.properties[k], `${path}.${k}`);
53
+ if (schema.properties?.[k]) check(v, schema.properties[k], `${path}.${k}`)
44
54
  }
45
55
  } else if (t === 'array') {
46
- if (!Array.isArray(value)) { errors.push(`${path}: expected array`); return; }
56
+ if (!Array.isArray(value)) {
57
+ errors.push(`${path}: expected array`)
58
+ return
59
+ }
47
60
  if (schema.minItems && value.length < schema.minItems) {
48
- errors.push(`${path}: needs at least ${schema.minItems} item(s), has ${value.length}`);
61
+ errors.push(
62
+ `${path}: needs at least ${schema.minItems} item(s), has ${value.length}`
63
+ )
49
64
  }
50
- if (schema.items) value.forEach((v, i) => check(v, schema.items, `${path}[${i}]`));
65
+ if (schema.items)
66
+ value.forEach((v, i) => check(v, schema.items, `${path}[${i}]`))
51
67
  } else if (t === 'string') {
52
- if (typeof value !== 'string') { errors.push(`${path}: expected string`); return; }
68
+ if (typeof value !== 'string') {
69
+ errors.push(`${path}: expected string`)
70
+ return
71
+ }
53
72
  if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
54
- errors.push(`${path}: "${value}" does not match ${schema.pattern}`);
73
+ errors.push(`${path}: "${value}" does not match ${schema.pattern}`)
55
74
  }
56
75
  } else if (t === 'boolean' && typeof value !== 'boolean') {
57
- errors.push(`${path}: expected boolean`);
76
+ errors.push(`${path}: expected boolean`)
58
77
  } else if (t === 'number' && typeof value !== 'number') {
59
- errors.push(`${path}: expected number`);
78
+ errors.push(`${path}: expected number`)
60
79
  }
61
80
  }
62
81
 
63
82
  // --- load + per-file validation ---
64
83
  // Files marked `x-optional` (the frontend layer) only validate when present, so a
65
84
  // backend-only repo does not fail for lacking them.
66
- const docs = {};
85
+ const docs = {}
67
86
  for (const [filename, fileSchema] of Object.entries(schemaDoc.files)) {
68
- const p = join(dir, filename);
87
+ const p = join(dir, filename)
69
88
  if (!existsSync(p)) {
70
- if (!fileSchema['x-optional']) errors.push(`${filename}: missing`);
71
- continue;
89
+ if (!fileSchema['x-optional']) errors.push(`${filename}: missing`)
90
+ continue
72
91
  }
73
92
  try {
74
- docs[filename] = JSON.parse(readFileSync(p, 'utf8'));
93
+ docs[filename] = JSON.parse(readFileSync(p, 'utf8'))
75
94
  } catch (e) {
76
- errors.push(`${filename}: invalid JSON (${e.message})`); continue;
95
+ errors.push(`${filename}: invalid JSON (${e.message})`)
96
+ continue
77
97
  }
78
- check(docs[filename], fileSchema, filename);
98
+ check(docs[filename], fileSchema, filename)
79
99
  }
80
- if (!existsSync(join(dir, 'blueprint.md'))) errors.push('blueprint.md: missing');
100
+ if (!existsSync(join(dir, 'blueprint.md'))) errors.push('blueprint.md: missing')
81
101
 
82
102
  // --- cross-file referential checks ---
83
103
  if (docs['domains.json'] && docs['commands.json']) {
84
- const domains = new Set((docs['domains.json'].domains || []).map((d) => d.name));
85
- const commandNames = new Set((docs['commands.json'].commands || []).map((c) => c.name));
86
- const queryNames = new Set((docs['queries.json']?.queries || []).map((q) => q.name));
87
- const eventNames = new Set((docs['events.json']?.events || []).map((e) => e.name));
104
+ const domains = new Set(
105
+ (docs['domains.json'].domains || []).map((d) => d.name)
106
+ )
107
+ const commandNames = new Set(
108
+ (docs['commands.json'].commands || []).map((c) => c.name)
109
+ )
110
+ const queryNames = new Set(
111
+ (docs['queries.json']?.queries || []).map((q) => q.name)
112
+ )
113
+ const eventNames = new Set(
114
+ (docs['events.json']?.events || []).map((e) => e.name)
115
+ )
88
116
 
89
117
  const wantDomain = (owner, d) => {
90
- if (d && !domains.has(d)) errors.push(`${owner}: domain "${d}" not defined in domains.json`);
91
- };
118
+ if (d && !domains.has(d))
119
+ errors.push(`${owner}: domain "${d}" not defined in domains.json`)
120
+ }
92
121
  for (const c of docs['commands.json'].commands || []) {
93
- wantDomain(`commands.json:${c.name}`, c.domain);
122
+ wantDomain(`commands.json:${c.name}`, c.domain)
94
123
  for (const ev of c.eventsProduced || []) {
95
- if (!eventNames.has(ev)) warnings.push(`commands.json:${c.name} produces "${ev}" which is not in events.json`);
124
+ if (!eventNames.has(ev))
125
+ warnings.push(
126
+ `commands.json:${c.name} produces "${ev}" which is not in events.json`
127
+ )
96
128
  }
97
129
  }
98
- for (const q of docs['queries.json']?.queries || []) wantDomain(`queries.json:${q.name}`, q.domain);
99
- for (const e of docs['entities.json']?.entities || []) wantDomain(`entities.json:${e.name}`, e.domain);
100
- for (const ev of docs['events.json']?.events || []) wantDomain(`events.json:${ev.name}`, ev.domain);
130
+ for (const q of docs['queries.json']?.queries || [])
131
+ wantDomain(`queries.json:${q.name}`, q.domain)
132
+ for (const e of docs['entities.json']?.entities || [])
133
+ wantDomain(`entities.json:${e.name}`, e.domain)
134
+ for (const ev of docs['events.json']?.events || [])
135
+ wantDomain(`events.json:${ev.name}`, ev.domain)
101
136
 
102
137
  for (const s of docs['api.json']?.surfaces || []) {
103
- const { type, name } = s.mapsTo || {};
104
- if (type === 'command' && !commandNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: maps to unknown command "${name}"`);
105
- if (type === 'query' && !queryNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: maps to unknown query "${name}"`);
106
- if (type === 'event-ingress' && !eventNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: event-ingress maps to unknown event "${name}" (state-changing webhooks should map to a command instead)`);
138
+ const { type, name } = s.mapsTo || {}
139
+ if (type === 'command' && !commandNames.has(name))
140
+ errors.push(
141
+ `api.json:${s.method || ''} ${s.path}: maps to unknown command "${name}"`
142
+ )
143
+ if (type === 'query' && !queryNames.has(name))
144
+ errors.push(
145
+ `api.json:${s.method || ''} ${s.path}: maps to unknown query "${name}"`
146
+ )
147
+ if (type === 'event-ingress' && !eventNames.has(name))
148
+ errors.push(
149
+ `api.json:${s.method || ''} ${s.path}: event-ingress maps to unknown event "${name}" (state-changing webhooks should map to a command instead)`
150
+ )
107
151
  }
108
152
  // every domain's listed concepts should exist
109
153
  for (const d of docs['domains.json'].domains || []) {
110
- for (const c of d.commands || []) if (!commandNames.has(c)) warnings.push(`domains.json:${d.name}: lists command "${c}" not in commands.json`);
111
- for (const q of d.queries || []) if (!queryNames.has(q)) warnings.push(`domains.json:${d.name}: lists query "${q}" not in queries.json`);
112
- for (const e of d.events || []) if (!eventNames.has(e)) warnings.push(`domains.json:${d.name}: lists event "${e}" not in events.json`);
113
- const policyNames = new Set((docs['policies.json']?.policies || []).map((p) => p.name));
114
- for (const p of d.policies || []) if (!policyNames.has(p)) warnings.push(`domains.json:${d.name}: lists policy "${p}" not in policies.json`);
154
+ for (const c of d.commands || [])
155
+ if (!commandNames.has(c))
156
+ warnings.push(
157
+ `domains.json:${d.name}: lists command "${c}" not in commands.json`
158
+ )
159
+ for (const q of d.queries || [])
160
+ if (!queryNames.has(q))
161
+ warnings.push(
162
+ `domains.json:${d.name}: lists query "${q}" not in queries.json`
163
+ )
164
+ for (const e of d.events || [])
165
+ if (!eventNames.has(e))
166
+ warnings.push(
167
+ `domains.json:${d.name}: lists event "${e}" not in events.json`
168
+ )
169
+ const policyNames = new Set(
170
+ (docs['policies.json']?.policies || []).map((p) => p.name)
171
+ )
172
+ for (const p of d.policies || [])
173
+ if (!policyNames.has(p))
174
+ warnings.push(
175
+ `domains.json:${d.name}: lists policy "${p}" not in policies.json`
176
+ )
115
177
  }
116
178
  // commands with no policies and no preconditions are suspicious for mutating ops
117
179
  for (const c of docs['commands.json'].commands || []) {
118
180
  if (!(c.policies || []).length && !(c.preconditions || []).length) {
119
- warnings.push(`commands.json:${c.name}: no policies or preconditions — really unguarded, or missed extraction?`);
181
+ warnings.push(
182
+ `commands.json:${c.name}: no policies or preconditions — really unguarded, or missed extraction?`
183
+ )
120
184
  }
121
185
  }
122
186
  }
@@ -124,13 +188,15 @@ if (docs['domains.json'] && docs['commands.json']) {
124
188
  // --- frontend layer cross-checks (only when the optional frontend files exist) ---
125
189
  if (docs['frontend-components.json']) {
126
190
  const componentNames = new Set(
127
- (docs['frontend-components.json'].components || []).map((c) => c.name),
128
- );
191
+ (docs['frontend-components.json'].components || []).map((c) => c.name)
192
+ )
129
193
  // routes should reference components that were actually inventoried
130
194
  for (const r of docs['frontend-routes.json']?.routes || []) {
131
195
  for (const c of r.usesComponents || []) {
132
196
  if (!componentNames.has(c)) {
133
- warnings.push(`frontend-routes.json:${r.path}: uses component "${c}" not in frontend-components.json`);
197
+ warnings.push(
198
+ `frontend-routes.json:${r.path}: uses component "${c}" not in frontend-components.json`
199
+ )
134
200
  }
135
201
  }
136
202
  }
@@ -138,7 +204,9 @@ if (docs['frontend-components.json']) {
138
204
  // port-risk is unactionable
139
205
  for (const c of docs['frontend-components.json'].components || []) {
140
206
  if (c.rebuild === 'custom-logic' && !c.customLogic) {
141
- warnings.push(`frontend-components.json:${c.name}: rebuild=custom-logic but no customLogic description — port risk is unactionable`);
207
+ warnings.push(
208
+ `frontend-components.json:${c.name}: rebuild=custom-logic but no customLogic description — port risk is unactionable`
209
+ )
142
210
  }
143
211
  }
144
212
  // data-fetching queries named on routes should resolve to a real query/command
@@ -146,11 +214,13 @@ if (docs['frontend-components.json']) {
146
214
  const known = new Set([
147
215
  ...(docs['queries.json']?.queries || []).map((q) => q.name),
148
216
  ...(docs['commands.json']?.commands || []).map((c) => c.name),
149
- ]);
217
+ ])
150
218
  for (const r of docs['frontend-routes.json']?.routes || []) {
151
219
  for (const d of r.dataFrom || []) {
152
220
  if (!known.has(d)) {
153
- warnings.push(`frontend-routes.json:${r.path}: reads "${d}" which is not a known query/command`);
221
+ warnings.push(
222
+ `frontend-routes.json:${r.path}: reads "${d}" which is not a known query/command`
223
+ )
154
224
  }
155
225
  }
156
226
  }
@@ -160,14 +230,21 @@ if (docs['frontend-components.json']) {
160
230
  // an inconsistent UI with no specific design findings = under-extraction
161
231
  // (guarded on frontend.json alone — independent of the component inventory)
162
232
  if (docs['frontend.json']) {
163
- const consistency = docs['frontend.json'].designSystemConsistency;
164
- const findingCount = (docs['frontend.json'].designFindings || []).length;
165
- if ((consistency === 'mixed' || consistency === 'ad-hoc') && findingCount === 0) {
166
- warnings.push(`frontend.json: designSystemConsistency="${consistency}" but designFindings is empty — name the specific broken patterns (interaction/theming/cross-page/…)`);
233
+ const consistency = docs['frontend.json'].designSystemConsistency
234
+ const findingCount = (docs['frontend.json'].designFindings || []).length
235
+ if (
236
+ (consistency === 'mixed' || consistency === 'ad-hoc') &&
237
+ findingCount === 0
238
+ ) {
239
+ warnings.push(
240
+ `frontend.json: designSystemConsistency="${consistency}" but designFindings is empty — name the specific broken patterns (interaction/theming/cross-page/…)`
241
+ )
167
242
  }
168
243
  }
169
244
 
170
- for (const w of warnings) console.log(`WARN ${w}`);
171
- for (const e of errors) console.log(`ERROR ${e}`);
172
- console.log(`\n${errors.length} error(s), ${warnings.length} warning(s) across ${Object.keys(docs).length} files`);
173
- process.exit(errors.length ? 1 : 0);
245
+ for (const w of warnings) console.log(`WARN ${w}`)
246
+ for (const e of errors) console.log(`ERROR ${e}`)
247
+ console.log(
248
+ `\n${errors.length} error(s), ${warnings.length} warning(s) across ${Object.keys(docs).length} files`
249
+ )
250
+ process.exit(errors.length ? 1 : 0)
@@ -6,6 +6,7 @@ description: 'Deprecated — use pikku-middleware instead. Tag middleware (addTa
6
6
  # Deprecated: use `pikku-middleware`
7
7
 
8
8
  Tag middleware is covered in the **`pikku-middleware`** skill, which also covers:
9
+
9
10
  - `addHTTPMiddleware` (global / prefix-based)
10
11
  - `addTagMiddleware` (tag-scoped)
11
12
  - Middleware execution order and priority
@@ -26,8 +26,9 @@ commit, separate from any feature work.
26
26
 
27
27
  `create-pikku` keeps only the chosen package manager's lockfile and deletes
28
28
  the other, and for yarn it may have written an **empty** `yarn.lock` as a
29
- marker. Commit the lockfile *after* the first install has filled it in —
29
+ marker. Commit the lockfile _after_ the first install has filled it in —
30
30
  committing the empty placeholder pins nothing.
31
+
31
32
  3. **Rename template identifiers.** Update `name` in the root `package.json`
32
33
  (and any `@project/*` or other placeholder names) to the real project.
33
34
  4. **Drop template-only artifacts.** Remove any `TEMPLATE.md`, demo docs, or
@@ -45,7 +45,7 @@ handler, and a handler can exist before any source is wired.
45
45
  Define the target function that handles trigger events:
46
46
 
47
47
  ```typescript
48
- import { wireTrigger } from '#pikku'
48
+ import { wireTrigger } from '#pikku/trigger'
49
49
 
50
50
  wireTrigger({
51
51
  name: string, // Trigger name (matches source)
@@ -60,7 +60,7 @@ wireTrigger({
60
60
  Define the event source that fires triggers:
61
61
 
62
62
  ```typescript
63
- import { wireTriggerSource } from '#pikku'
63
+ import { wireTriggerSource } from '#pikku/trigger'
64
64
 
65
65
  wireTriggerSource({
66
66
  name: string, // Must match a wireTrigger name
@@ -80,7 +80,7 @@ up a listener, calls `trigger.invoke(...)` for each event it sees, and returns a
80
80
  teardown function:
81
81
 
82
82
  ```typescript
83
- import { pikkuTriggerFunc } from '#pikku'
83
+ import { pikkuTriggerFunc } from '#pikku/trigger'
84
84
 
85
85
  const source = pikkuTriggerFunc<
86
86
  InputType, // Configuration input
@@ -94,8 +94,9 @@ wireChannel({
94
94
  name: 'todos',
95
95
  route: '/todos',
96
96
  onMessageWiring: {
97
- action: { // ← the field to route on
98
- create: { func: createTodo }, // ← its possible values
97
+ action: {
98
+ // ← the field to route on
99
+ create: { func: createTodo }, // ← its possible values
99
100
  list: { func: listTodos, auth: false },
100
101
  },
101
102
  },
@@ -129,7 +130,7 @@ wireChannel({
129
130
  onMessageWiring: {
130
131
  action: {
131
132
  authenticate: { func: authenticate, auth: false }, // No session required
132
- subscribe: { func: subscribeTodos }, // Session required
133
+ subscribe: { func: subscribeTodos }, // Session required
133
134
  create: { func: createTodo },
134
135
  },
135
136
  },
@@ -64,8 +64,8 @@ import {
64
64
  pikkuWorkflowComplexFunc,
65
65
  } from '#pikku/workflow/pikku-workflow-types.gen.js'
66
66
 
67
- // WRONG — '#pikku' does not re-export them (TS2305)
68
- import { pikkuWorkflowFunc } from '#pikku'
67
+ // WRONG — the function leaf does not re-export them (TS2305)
68
+ import { pikkuWorkflowFunc } from '#pikku/function'
69
69
  ```
70
70
 
71
71
  ## Defining a workflow
@@ -6,15 +6,15 @@ Whether a step runs **inline** (same process/session, no queue round-trip) or is
6
6
 
7
7
  - **Steps default to inline.** Most steps don't need their own worker; running them inline avoids a queue round-trip per step, so a normally-started workflow executes its whole chain in one orchestrator pass.
8
8
  - **`workflowQueued: true` opts a function out.** Set it on the **function config** (`pikkuFunc` / `pikkuSessionlessFunc`, same level as `auth`/`expose`) to dispatch that step via the queue — for expensive/long-running steps that deserve their own worker, retry isolation, and concurrency limits. `workflowRetries` and `workflowTimeout` sit alongside it.
9
- - **Run-level `inline` is separate** and only controls whether the *whole run* executes in-process without queue infrastructure (set automatically when there is no `queueService`, or via `startWorkflow(..., { inline: true })`). It governs sleep handling, not per-step dispatch.
9
+ - **Run-level `inline` is separate** and only controls whether the _whole run_ executes in-process without queue infrastructure (set automatically when there is no `queueService`, or via `startWorkflow(..., { inline: true })`). It governs sleep handling, not per-step dispatch.
10
10
 
11
11
  The rule (`dispatchStep`):
12
12
 
13
- | Function `workflowQueued` | `queueService` present? | Result |
14
- |---|---|---|
15
- | default / `false` | any | **inline** |
16
- | `true` | yes | **queued** (own worker) |
17
- | `true` | no | **throws** |
13
+ | Function `workflowQueued` | `queueService` present? | Result |
14
+ | ------------------------- | ----------------------- | ----------------------- |
15
+ | default / `false` | any | **inline** |
16
+ | `true` | yes | **queued** (own worker) |
17
+ | `true` | no | **throws** |
18
18
 
19
19
  ```typescript
20
20
  // Push this one expensive step onto the queue; every other step stays inline:
@@ -24,7 +24,9 @@ export const renderLargeReport = pikkuSessionlessFunc({
24
24
  workflowTimeout: '5m',
25
25
  input: ReportInput,
26
26
  output: ReportOutput,
27
- func: async (services, data) => { /* ... */ },
27
+ func: async (services, data) => {
28
+ /* ... */
29
+ },
28
30
  })
29
31
  ```
30
32
 
@@ -39,13 +41,25 @@ Usually auto-scaffolded via `scaffold.workflow`. To wire by hand:
39
41
 
40
42
  ```typescript
41
43
  // Start a workflow
42
- wireHTTP({ method: 'post', route: '/onboard', func: workflowStart('onboardUser') })
44
+ wireHTTP({
45
+ method: 'post',
46
+ route: '/onboard',
47
+ func: workflowStart('onboardUser'),
48
+ })
43
49
 
44
50
  // Execute workflow steps (called by the orchestrator)
45
- wireHTTP({ method: 'post', route: '/onboard/run', func: workflow('onboardUser') })
51
+ wireHTTP({
52
+ method: 'post',
53
+ route: '/onboard/run',
54
+ func: workflow('onboardUser'),
55
+ })
46
56
 
47
57
  // Check workflow status
48
- wireHTTP({ method: 'get', route: '/onboard/status/:runId', func: workflowStatus('onboardUser') })
58
+ wireHTTP({
59
+ method: 'get',
60
+ route: '/onboard/status/:runId',
61
+ func: workflowStatus('onboardUser'),
62
+ })
49
63
  ```
50
64
 
51
65
  ## Suspend / resume example