@pikku/skills 0.12.4 → 0.12.6

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 (64) hide show
  1. package/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +56 -29
  4. package/skills/pikku-ai-agent/SKILL.md +197 -105
  5. package/skills/pikku-ai-vercel/SKILL.md +57 -18
  6. package/skills/pikku-ai-voice/SKILL.md +126 -52
  7. package/skills/pikku-audit/SKILL.md +35 -13
  8. package/skills/pikku-aws/SKILL.md +66 -16
  9. package/skills/pikku-backblaze/SKILL.md +44 -11
  10. package/skills/pikku-better-auth/SKILL.md +45 -10
  11. package/skills/pikku-cli/SKILL.md +67 -18
  12. package/skills/pikku-cli/references/complete-example.md +2 -0
  13. package/skills/pikku-concepts/SKILL.md +75 -10
  14. package/skills/pikku-config/SKILL.md +56 -14
  15. package/skills/pikku-cron/SKILL.md +13 -6
  16. package/skills/pikku-deploy-azure/SKILL.md +83 -28
  17. package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
  18. package/skills/pikku-deploy-express/SKILL.md +40 -4
  19. package/skills/pikku-deploy-fastify/SKILL.md +22 -1
  20. package/skills/pikku-deploy-lambda/SKILL.md +99 -19
  21. package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
  22. package/skills/pikku-deploy-uws/SKILL.md +54 -1
  23. package/skills/pikku-deps/SKILL.md +29 -8
  24. package/skills/pikku-emails/SKILL.md +36 -5
  25. package/skills/pikku-fabric/SKILL.md +27 -2
  26. package/skills/pikku-fabric-debug/SKILL.md +5 -1
  27. package/skills/pikku-feature/SKILL.md +6 -1
  28. package/skills/pikku-gateway-slack/SKILL.md +72 -11
  29. package/skills/pikku-http/SKILL.md +18 -5
  30. package/skills/pikku-http/references/http-options.md +10 -5
  31. package/skills/pikku-i18n/SKILL.md +18 -7
  32. package/skills/pikku-info/SKILL.md +18 -8
  33. package/skills/pikku-jose/SKILL.md +35 -6
  34. package/skills/pikku-kysely/SKILL.md +78 -15
  35. package/skills/pikku-machine-auth/SKILL.md +36 -1
  36. package/skills/pikku-mcp/SKILL.md +159 -149
  37. package/skills/pikku-middleware/SKILL.md +17 -5
  38. package/skills/pikku-mongodb/SKILL.md +10 -2
  39. package/skills/pikku-n8n-import/SKILL.md +14 -6
  40. package/skills/pikku-permissions/SKILL.md +102 -22
  41. package/skills/pikku-pino/SKILL.md +12 -4
  42. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  43. package/skills/pikku-queue/SKILL.md +45 -16
  44. package/skills/pikku-react/SKILL.md +41 -14
  45. package/skills/pikku-react-query/SKILL.md +14 -10
  46. package/skills/pikku-realtime/SKILL.md +44 -22
  47. package/skills/pikku-redis/SKILL.md +12 -3
  48. package/skills/pikku-rpc/SKILL.md +23 -12
  49. package/skills/pikku-rtl/SKILL.md +21 -17
  50. package/skills/pikku-scenario/SKILL.md +108 -75
  51. package/skills/pikku-schedule/SKILL.md +39 -6
  52. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  53. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  54. package/skills/pikku-security/SKILL.md +54 -9
  55. package/skills/pikku-services/SKILL.md +49 -9
  56. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  57. package/skills/pikku-template-clone/SKILL.md +10 -5
  58. package/skills/pikku-trigger/SKILL.md +50 -6
  59. package/skills/pikku-versioning/SKILL.md +46 -17
  60. package/skills/pikku-websocket/SKILL.md +72 -44
  61. package/skills/pikku-workflow/SKILL.md +35 -1
  62. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  63. package/skills/pikku-workflows-client/SKILL.md +13 -6
  64. package/skills/pikku-ws/SKILL.md +44 -8
@@ -36,19 +36,23 @@ See `pikku-concepts` for the core mental model.
36
36
 
37
37
  ### `wireCLI(config)`
38
38
 
39
+ All three factories come from `#pikku` (the generated types re-export
40
+ `cli/pikku-cli-types.gen.js`). Importing them from `@pikku/core/cli` compiles but
41
+ loses your project's service and middleware types.
42
+
39
43
  ```typescript
40
- import { wireCLI } from '@pikku/core/cli'
44
+ import { wireCLI } from '#pikku'
41
45
 
42
46
  wireCLI({
43
47
  program: string, // Program name (e.g. 'todos')
44
- options?: { // Global options
45
- [key: string]: {
46
- description: string,
47
- short?: string, // Single char alias (e.g. 'v')
48
- default?: any,
49
- }
50
- },
48
+ description?: string,
49
+ summary?: string,
50
+ options?: CLIOptions, // Global options — see below
51
51
  render?: PikkuCLIRender, // Default renderer for all commands
52
+ middleware?: PikkuMiddleware[],
53
+ tags?: string[], // Targets tag middleware
54
+ errors?: string[],
55
+ auth?: boolean, // Only affects the websocket backend, not local runs
52
56
  commands: {
53
57
  [name: string]: PikkuCLICommand | {
54
58
  description: string,
@@ -65,24 +69,49 @@ import { pikkuCLICommand } from '#pikku'
65
69
 
66
70
  pikkuCLICommand({
67
71
  parameters?: string, // Positional args (e.g. '<text>', '<username> <email>')
68
- func: PikkuFunc, // Business logic function
72
+ func?: PikkuFunc, // Business logic function — omit on a pure command group
73
+ title?: string,
69
74
  description?: string,
70
75
  render?: PikkuCLIRender, // Custom output renderer
71
- options?: {
72
- [key: string]: {
73
- description: string,
74
- short?: string,
75
- default?: any,
76
- choices?: string[], // Restrict to values
77
- }
78
- },
76
+ options?: CLIOptions,
77
+ subcommands?: { [name: string]: PikkuCLICommand }, // nests to any depth
78
+ middleware?: PikkuMiddleware[],
79
+ permissions?: PermissionGroup,
80
+ auth?: boolean,
81
+ isDefault?: boolean, // Runs when the group is invoked with no subcommand
79
82
  })
80
83
  ```
81
84
 
85
+ `parameters` is checked against the func's input at compile time — a name that is
86
+ not a key of the input makes the type `never`, so a typo'd positional fails to
87
+ build rather than arriving as `undefined`.
88
+
89
+ ### Options
90
+
91
+ ```typescript
92
+ {
93
+ description: string,
94
+ short?: string, // Single char alias (e.g. 'v')
95
+ default?: any,
96
+ choices?: any[], // Restrict to these values
97
+ array?: boolean, // Collect every value up to the next flag
98
+ required?: boolean,
99
+ }
100
+ ```
101
+
102
+ How the parser reads them, which is worth knowing before you name one:
103
+
104
+ - **Flag names are camel-cased**, so `--api-url` and `--apiUrl` both fill `apiUrl`.
105
+ - **`--no-x` negation only works when `x` has a boolean `default`.** Without one,
106
+ `--no-x` parses as an option literally named `noX` — which is why boolean flags
107
+ should always declare their default.
108
+ - Short flags cluster (`-abc`), and only the last in a cluster may take a value.
109
+ - An unknown `--flag` warns rather than throwing.
110
+
82
111
  ### `pikkuCLIRender(fn)`
83
112
 
84
113
  ```typescript
85
- import { pikkuCLIRender } from '@pikku/core/cli'
114
+ import { pikkuCLIRender } from '#pikku'
86
115
 
87
116
  const renderer = pikkuCLIRender<OutputType>((services, data) => {
88
117
  // Format and print output to terminal
@@ -90,6 +119,15 @@ const renderer = pikkuCLIRender<OutputType>((services, data) => {
90
119
  })
91
120
  ```
92
121
 
122
+ ### Wire object (`wire.cli`)
123
+
124
+ ```typescript
125
+ wire.cli.program // program name
126
+ wire.cli.command // string[] — the resolved command path
127
+ wire.cli.data // all positionals and options, merged
128
+ wire.cli.channel // the channel when served remotely (see below)
129
+ ```
130
+
93
131
  ## Usage Patterns
94
132
 
95
133
  ### Basic Commands
@@ -193,6 +231,17 @@ wireCLI({
193
231
 
194
232
  The func's input is the positional `parameters` plus `options`, merged (e.g. `parameters: '<username> <email>'` + an `admin` option → func input `{ username, email, admin }`).
195
233
 
234
+ A renderer's full signature is `(services, data, session?)`. It returns nothing —
235
+ printing is its job.
236
+
237
+ ### Running the program over a websocket
238
+
239
+ Codegen emits a `<program>-channel.gen.ts` beside your wiring: a `wireChannel`
240
+ that serves the same commands remotely, so a local binary and a hosted session
241
+ run identical code. `auth` on `wireCLI` guards **that channel only** — a locally
242
+ executed CLI has no connection to authenticate, so it is not a way to require a
243
+ session for local runs. Don't hand-write or edit the generated channel file.
244
+
196
245
  ## Complete Example
197
246
 
198
247
  For a full functions + renderers + nested-subcommand wiring walkthrough, see `references/complete-example.md`.
@@ -32,6 +32,8 @@ export const deleteUser = pikkuFunc({
32
32
  })
33
33
 
34
34
  // wirings/cli.wiring.ts
35
+ import { wireCLI, pikkuCLICommand, pikkuCLIRender } from '#pikku'
36
+
35
37
  const userRenderer = pikkuCLIRender<{ user: User }>((_services, { user }) => {
36
38
  console.log(`Created user: ${user.username} (${user.email}) [${user.role}]`)
37
39
  })
@@ -28,7 +28,8 @@ Pikku is a TypeScript framework that separates business logic from transport mec
28
28
  For deep-dive on each topic, see the dedicated skills:
29
29
 
30
30
  - **Wiring**: `pikku-http`, `pikku-websocket`, `pikku-rpc`, `pikku-mcp`, `pikku-queue`, `pikku-cron`, `pikku-trigger`, `pikku-cli`, `pikku-ai-agent`, `pikku-workflow`
31
- - **Infrastructure**: `pikku-services`, `pikku-security`, `pikku-config`
31
+ - **Authorization**: `pikku-security` (authentication/sessions), `pikku-permissions` (permission checks, scopes), `pikku-middleware` (global/tag/route middleware)
32
+ - **Infrastructure**: `pikku-services`, `pikku-config`
32
33
  - **Project introspection**: `pikku-info`
33
34
 
34
35
  ## Core Mental Model
@@ -58,7 +59,7 @@ The function never imports Express, never reads `req.body`, never touches `ws.se
58
59
 
59
60
  ## Concept Mapping: Generic Backend → Pikku
60
61
 
61
- Controllers/routes → `pikkuFunc`; middleware/auth/permissions → `pikku-security`; DI → `pikku-services`; transports (HTTP/WS/queue/cron) → their `wire*` + skill. For the full Generic Backend → Pikku mapping table (with side-by-side code examples), read `references/concept-mapping.md`.
62
+ Controllers/routes → `pikkuFunc`; auth/sessions → `pikku-security`; authorization checks → `pikku-permissions`; request interception → `pikku-middleware`; DI → `pikku-services`; transports (HTTP/WS/queue/cron) → their `wire*` + skill. For the full Generic Backend → Pikku mapping table (with side-by-side code examples), read `references/concept-mapping.md`.
62
63
 
63
64
  ## Functions
64
65
 
@@ -95,22 +96,53 @@ Services can be destructured inline in the `func` signature (e.g. `async ({ logg
95
96
 
96
97
  ```typescript
97
98
  pikkuFunc({
99
+ // Identity and documentation
98
100
  title?: string, // Human-readable name
99
101
  description?: string, // What the function does
100
- version?: number, // Contract version (see pikku-config for versioning)
102
+ version?: number, // Contract version (see pikku-versioning)
103
+ override?: string, // Logical name override, so several exports share a versioned base
101
104
  tags?: string[], // For grouping and middleware targeting
105
+
106
+ // Contract
107
+ input?: ZodSchema, // Input validation schema
108
+ output?: ZodSchema, // Output validation schema
109
+ errors?: Array<typeof PikkuError>, // Errors this function may throw
110
+
111
+ // Reachability
102
112
  expose?: boolean, // Allow external RPC calls (see pikku-rpc)
103
113
  remote?: boolean, // Allow remote RPC calls
104
114
  mcp?: boolean, // Expose as MCP tool (see pikku-mcp)
115
+ readonly?: boolean, // Declares the function performs no writes
116
+ deploy?: 'serverless' | 'server' | 'auto',
117
+
118
+ // Authorization — see pikku-permissions
105
119
  auth?: boolean, // Override default auth requirement
106
- input?: ZodSchema, // Input validation schema
107
- output?: ZodSchema, // Output validation schema
108
- permissions?: PermissionGroup, // See pikku-security
109
- middleware?: PikkuMiddleware[], // See pikku-security
120
+ scopes?: ScopeId[], // AND-ed, checked before permissions; session required
121
+ permissions?: PermissionGroup, // OR-ed pool
122
+ permissionsInBody?: boolean, // Last resort; needs allow.permissionsInBody in config
123
+ middleware?: PikkuMiddleware[], // See pikku-middleware
124
+
125
+ // Agent tooling — see pikku-ai-agent
126
+ approvalRequired?: boolean,
127
+ approvalDescription?: (services, data) => Promise<string>,
128
+
129
+ // Workflow step behavior — see pikku-workflow
130
+ workflowQueued?: boolean, // Dispatch via queue instead of inline
131
+ workflowRetries?: number,
132
+ workflowTimeout?: string, // e.g. '30s', '5m'
133
+
134
+ audit?: boolean | { durability?: 'best-effort' | 'transactional' },
135
+
110
136
  func: async (services, data, wire) => { ... },
111
137
  })
112
138
  ```
113
139
 
140
+ `scopes` is the one option `pikkuSessionlessFunc` does not accept, and the
141
+ omission is deliberate: scopes are AND-ed and fail closed, so an anonymous
142
+ caller holds none and satisfies none — a sessionless function with scopes would
143
+ reject every caller it exists to serve. Gate those with `permissions`, which
144
+ receive the optional session and may pass anonymous.
145
+
114
146
  **Generics XOR `input`/`output` — never both.** A function's data and return
115
147
  types come from *one* source: either the `input`/`output` schemas (preferred —
116
148
  they double as runtime validation and OpenAPI) or type generics
@@ -145,7 +177,35 @@ Schemas serve triple duty: runtime validation, TypeScript types, and OpenAPI doc
145
177
 
146
178
  ## Server Bootstrap
147
179
 
148
- Every Pikku app follows the same bootstrap pattern regardless of runtime:
180
+ There are two ways to start a Pikku app. Pick based on whether you need to own the HTTP server.
181
+
182
+ **1. Let Pikku own the server (preferred when you don't need a specific runtime)**
183
+
184
+ `pikku dev` and `pikku serve` create the config and singleton services, start the server, and shut it down cleanly. You write no bootstrap code at all — startup and shutdown work goes in lifecycle hooks:
185
+
186
+ ```typescript
187
+ // src/lifecycle.ts
188
+ import { pikkuServerLifecycle } from '@pikku/core'
189
+ import type { SingletonServices } from '../types/application-types.js'
190
+
191
+ export const lifecycle = pikkuServerLifecycle<SingletonServices>({
192
+ beforeStart: async ({ kysely }) => {
193
+ await runMigrations(kysely)
194
+ },
195
+ afterStart: async ({ logger }) => {
196
+ logger.info('accepting traffic')
197
+ },
198
+ beforeStop: async ({ queueService }) => {
199
+ await queueService.drain()
200
+ },
201
+ })
202
+ ```
203
+
204
+ Export exactly one `pikkuServerLifecycle` from anywhere in `srcDirectories` — the inspector finds it by the wrapper call. Every hook is optional and receives the already-created singleton services. See pikku-services for the ordering and the `afterStop` caveat.
205
+
206
+ **2. Bootstrap it yourself (required for a specific runtime)**
207
+
208
+ Express, Fastify, uWS, Lambda, Cloudflare and Next.js need their own entrypoint, because Pikku is embedded in a server you own:
149
209
 
150
210
  ```typescript
151
211
  import '../../functions/.pikku/pikku-bootstrap.gen.js' // Generated — registers all wirings
@@ -168,6 +228,10 @@ await server.init()
168
228
  await server.start()
169
229
  ```
170
230
 
231
+ **Lifecycle hooks do not run on this path** — only `pikku dev` and `pikku serve` invoke them. Do your startup work directly in the entrypoint instead.
232
+
233
+ `pikku workspace validate` warns when a project starts a server by hand *and* depends on no runtime adapter, since that combination means path 1 was available and unused. Silence it with `"lint": { "customServerBootstrap": "off" }` in `pikku.config.json`.
234
+
171
235
  ## Code Generation
172
236
 
173
237
  Run `npx pikku all` to generate:
@@ -175,7 +239,7 @@ Run `npx pikku all` to generate:
175
239
  - `pikku-types.gen.ts` — Typed function factories and wiring functions
176
240
  - `pikku-fetch.gen.ts` — Type-safe HTTP client
177
241
  - `pikku-websocket.gen.ts` — Type-safe WebSocket client
178
- - `pikku-bootstrap.gen.js` — Runtime initialization (auto-imports all wirings)
242
+ - `pikku-bootstrap.gen.ts` — Runtime initialization (auto-imports all wirings)
179
243
  - `pikku-services.gen.ts` — Service factory types
180
244
 
181
245
  Config lives in `pikku.config.json`:
@@ -203,12 +267,13 @@ src/
203
267
  │ └── queue.wiring.ts
204
268
  ├── schemas.ts # Zod/Valibot schemas
205
269
  ├── services.ts # Service factories (see pikku-services)
270
+ ├── lifecycle.ts # Server lifecycle hooks (pikku dev/serve only)
206
271
  ├── middleware.ts # Middleware definitions (see pikku-security)
207
272
  ├── permissions.ts # Permission definitions (see pikku-security)
208
273
  └── .pikku/ # Generated (gitignored)
209
274
  ├── pikku-types.gen.ts
210
275
  ├── pikku-fetch.gen.ts
211
- └── pikku-bootstrap.gen.js
276
+ └── pikku-bootstrap.gen.ts
212
277
  ```
213
278
 
214
279
  ## Environment Variables
@@ -4,9 +4,10 @@ description: >-
4
4
  Use when managing secrets, environment variables, config, or OAuth2 credentials in a Pikku app.
5
5
  Covers defineSecret, defineVariable, defineCredential, and typed config access. TRIGGER when:
6
6
  code uses defineSecret/defineVariable/defineCredential, user asks about env vars, secrets,
7
- config, OAuth2, or "how do I access environment variables". DO NOT TRIGGER when: user asks about
8
- API versioning/breaking changes (use pikku-versioning), service factories (use pikku-services),
9
- or auth middleware (use pikku-security).
7
+ config, OAuth2, SecretValue/.reveal(), SecretCoercionError, or "how do I access environment
8
+ variables". DO NOT TRIGGER when: user asks about API versioning/breaking changes (use
9
+ pikku-versioning), service factories (use pikku-services), middleware (use pikku-middleware), or
10
+ auth strategies and sessions (use pikku-security).
10
11
  installGroups: [core]
11
12
  ---
12
13
 
@@ -64,10 +65,17 @@ or any wire** — it is removed from their services type and throws at runtime i
64
65
  reached through a cast. Read it where you wire the app and hand the value to a
65
66
  service:
66
67
 
68
+ `getSecret` returns a `SecretValue<T>`, not the bare value. It is nominal — not
69
+ assignable to `string`, so every concretely-typed sink rejects it — it serializes
70
+ to `[secret]` in logs and audits, and coercing it to a string (a template
71
+ literal, a concatenation) throws `SecretCoercionError`, because that is always a
72
+ leak. `.reveal()` is the one way out, which makes every disclosure deliberate and
73
+ greppable. Call it at the point the value reaches the thing that needs it:
74
+
67
75
  ```typescript
68
76
  // services.ts — allowed
69
77
  const createSingletonServices = pikkuServices(async (config, { secrets }) => ({
70
- stripe: new StripeService(await secrets.getSecret('STRIPE_CONFIG')),
78
+ stripe: new StripeService((await secrets.getSecret('STRIPE_CONFIG')).reveal()),
71
79
  }))
72
80
 
73
81
  // functions/*.ts — ask the service, never the secret store
@@ -113,7 +121,7 @@ defineSecret({
113
121
  })
114
122
 
115
123
  // In your services factory — fully typed
116
- const config = await secrets.getSecret('STRIPE_CONFIG')
124
+ const config = (await secrets.getSecret('STRIPE_CONFIG')).reveal()
117
125
  // config.apiKey → string (autocompleted)
118
126
  // config.webhookSecret → string (autocompleted)
119
127
 
@@ -178,17 +186,34 @@ defineCredential({
178
186
  },
179
187
  })
180
188
 
181
- // In your function — tokens refresh automatically
182
- const response = await slackOAuth.request(
183
- 'https://slack.com/api/chat.postMessage',
184
- {
185
- method: 'POST',
186
- body: JSON.stringify({ channel, text }),
189
+ ### Reading a Credential
190
+
191
+ A declared credential is resolved per invocation through `wire.getCredential(name)`,
192
+ so the natural place to read it is a wire service factory: build the client there
193
+ once and let functions ask the client, the same way they ask a service for a
194
+ secret-derived value. Tokens refresh automatically, so what arrives is already
195
+ valid.
196
+
197
+ ```typescript
198
+ export const createWireServices = pikkuWireServices(async (_services, wire) => {
199
+ const cred = await wire.getCredential?.<{ accessToken: string }>('slack')
200
+ if (!cred?.accessToken) {
201
+ // Tells the caller which credential to connect, and where.
202
+ throw new MissingCredentialError('slack', 'oauth2', '/credentials/slack/connect')
187
203
  }
188
- )
189
- const data = await response.json()
204
+ return { slack: new SlackClient(cred.accessToken) }
205
+ })
206
+
207
+ // functions/*.ts — ask the client, never the credential store
208
+ export const postMessage = pikkuFunc({
209
+ func: async ({ slack }, { channel, text }) => slack.postMessage(channel, text),
210
+ })
190
211
  ```
191
212
 
213
+ A `wire` credential resolves per user, so an unconnected user hits
214
+ `MissingCredentialError` rather than silently acting as someone else; a
215
+ `singleton` credential is platform-level and identical for every caller.
216
+
192
217
  ## Key Rule
193
218
 
194
219
  **Never use `process.env` inside Pikku functions.** Use the `variables` or `secrets` service:
@@ -201,7 +226,24 @@ const apiKey = process.env.API_KEY
201
226
  const apiKey = services.variables.get('API_KEY')
202
227
  ```
203
228
 
204
- `process.env` belongs only in server bootstrap code (`start.ts`).
229
+ `process.env` belongs only in server bootstrap code (`start.ts`). Under `pikku dev` / `pikku serve` there is no `start.ts` — startup work goes in a `pikkuServerLifecycle` export, and the hooks receive the singleton services, so read configuration through `variables` there too (see pikku-services).
230
+
231
+ ### Lint rules
232
+
233
+ `pikku.config.json` can set the severity of individual checks:
234
+
235
+ ```json
236
+ {
237
+ "lint": {
238
+ "servicesNotDestructured": "error",
239
+ "wiresNotDestructured": "error",
240
+ "functionDynamicImport": "warn",
241
+ "customServerBootstrap": "warn"
242
+ }
243
+ }
244
+ ```
245
+
246
+ `customServerBootstrap` is the one evaluated by `pikku workspace validate` rather than codegen: it warns when the root `start`/`dev` script boots a server without `pikku dev` / `pikku serve` and no runtime adapter is installed. Set it to `"off"` to keep a hand-rolled entrypoint, or `"error"` to enforce the hooks.
205
247
 
206
248
  ## Complete Example
207
249
 
@@ -6,6 +6,7 @@ description: >-
6
6
  when: code uses wireScheduler, user asks about cron, scheduled tasks, recurring jobs, or "run
7
7
  every X minutes/hours". DO NOT TRIGGER when: user asks about background jobs with retries (use
8
8
  pikku-queue) or event-driven triggers (use pikku-trigger).
9
+ installGroups: [core]
9
10
  ---
10
11
 
11
12
  # Pikku Cron/Scheduler Wiring
@@ -42,6 +43,7 @@ wireScheduler({
42
43
  name: string, // Unique scheduler name
43
44
  schedule: string, // Cron expression
44
45
  func: PikkuVoidFunc, // Must be pikkuVoidFunc (no input/output)
46
+ tags?: string[], // Targets tag middleware — see pikku-middleware
45
47
  middleware?: PikkuMiddleware[],
46
48
  })
47
49
  ```
@@ -53,10 +55,17 @@ Inside scheduled functions:
53
55
  ```typescript
54
56
  wire.scheduledTask.name // Scheduler name
55
57
  wire.scheduledTask.schedule // Cron expression string
56
- wire.scheduledTask.executionTime // When this execution was triggered
57
- wire.scheduledTask.skip(reason) // Skip this execution (no error)
58
+ wire.scheduledTask.executionTime // Date this execution was triggered
59
+ wire.scheduledTask.skip(reason?) // Abort this execution — THROWS, never returns
58
60
  ```
59
61
 
62
+ **`skip()` aborts by throwing.** It reads like an early return but it is not:
63
+ nothing after the call runs, so there is no need to `return` afterwards. The
64
+ consequence that bites is in middleware — a `try/catch` around `await next()`
65
+ will catch a skip and report it as a failure. If your middleware distinguishes
66
+ success from failure, let the skip pass through rather than logging it as an
67
+ error.
68
+
60
69
  ### Cron Expression Reference
61
70
 
62
71
  ```
@@ -113,8 +122,7 @@ const weeklyCleanup = pikkuVoidFunc({
113
122
 
114
123
  const staleCount = await db.countStaleTodos()
115
124
  if (staleCount === 0) {
116
- wire.scheduledTask.skip('No stale todos found')
117
- return
125
+ wire.scheduledTask.skip('No stale todos found') // throws — nothing below runs
118
126
  }
119
127
 
120
128
  await db.deleteCompletedTodos({ olderThan: '30d' })
@@ -178,8 +186,7 @@ export const cleanupExpired = pikkuVoidFunc({
178
186
  func: async ({ db, logger }, _input, wire) => {
179
187
  const count = await db.countExpiredSessions()
180
188
  if (count === 0) {
181
- wire.scheduledTask.skip('No expired sessions')
182
- return
189
+ wire.scheduledTask.skip('No expired sessions') // throws — nothing below runs
183
190
  }
184
191
  await db.deleteExpiredSessions()
185
192
  logger.info(`Cleaned ${count} expired sessions`)
@@ -1,10 +1,11 @@
1
1
  ---
2
2
  name: pikku-deploy-azure
3
3
  description: >-
4
- Use when deploying a Pikku app to Azure Functions. Covers PikkuAzFunctionsLogger and
5
- PikkuAzTimerRequest for Azure Functions runtime. TRIGGER when: user asks about Azure Functions,
6
- Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when: user asks about AWS Lambda
7
- (use pikku-deploy-lambda) or Cloudflare Workers (use pikku-deploy-cloudflare).
4
+ Use when deploying a Pikku app to Azure Functions. Covers createAzureHandler for HTTP, storage
5
+ queue and timer triggers, plus AzInvocationLogger and PikkuAZTimerRequest. TRIGGER when: user
6
+ asks about Azure Functions, Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when:
7
+ user asks about AWS Lambda (use pikku-deploy-lambda) or Cloudflare Workers (use
8
+ pikku-deploy-cloudflare).
8
9
  ---
9
10
 
10
11
  # Pikku Azure Functions Deployment
@@ -29,43 +30,97 @@ yarn add @pikku/azure-functions @azure/functions
29
30
 
30
31
  ## API Reference
31
32
 
32
- ### `PikkuAzFunctionsLogger`
33
-
34
- Logger implementation that integrates with Azure Functions' built-in logging context.
35
-
36
- ### `PikkuAzTimerRequest`
37
-
38
- Timer trigger request handler for running Pikku scheduled functions as Azure Timer Triggers.
33
+ Exported from `@pikku/azure-functions`:
34
+
35
+ - `createAzureHandler(factories, handlerTypes)` — the entry point. Returns
36
+ `{ http?, queue?, timer? }` for the handler types you ask for.
37
+ - `createAzureWorkerHandler(factories)` — `createAzureHandler(factories, ['fetch'])`.
38
+ - `createAzureWebSocketHandler(factories)` — **a stub**: its `negotiate` always
39
+ answers `501 WebSocket via Azure Web PubSub not yet implemented`. Channels do
40
+ not work on Azure yet; do not plan a deployment around it.
41
+ - `AzInvocationLogger` — the logger. Note the name: there is no
42
+ `PikkuAzFunctionsLogger`.
43
+ - `PikkuAZTimerRequest` — `new PikkuAZTimerRequest(context, data)`, a
44
+ `PikkuRequest` carrying the data. The context argument is accepted and
45
+ ignored.
46
+ - `AzureQueueService`, `AzureDeploymentService`.
47
+
48
+ `factories` is `{ createConfig, createSingletonServices, createPlatformServices? }`.
49
+ Services are built from `process.env` and cached in module scope across
50
+ invocations of the same instance.
39
51
 
40
52
  ## Usage Patterns
41
53
 
42
- ### HTTP Function
54
+ ### Registering handlers
43
55
 
44
56
  ```typescript
45
57
  import { app } from '@azure/functions'
46
- import { PikkuAzFunctionsLogger } from '@pikku/azure-functions'
58
+ import { createAzureHandler } from '@pikku/azure-functions'
59
+ import { createConfig, createSingletonServices } from './services.js'
60
+ import './.pikku/pikku-bootstrap.gen.js'
61
+
62
+ const handlers = createAzureHandler(
63
+ { createConfig, createSingletonServices },
64
+ ['fetch', 'queue', 'scheduled']
65
+ )
47
66
 
48
67
  app.http('api', {
49
- methods: ['GET', 'POST', 'PUT', 'DELETE'],
68
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
50
69
  route: '{*path}',
51
- handler: async (request, context) => {
52
- const logger = new PikkuAzFunctionsLogger(context)
53
- // Wire Pikku HTTP runner with Azure request/response
54
- },
70
+ handler: handlers.http as any,
55
71
  })
56
- ```
57
72
 
58
- ### Timer Trigger
59
-
60
- ```typescript
61
- import { app } from '@azure/functions'
62
- import { PikkuAzTimerRequest } from '@pikku/azure-functions'
73
+ app.storageQueue('queue', {
74
+ queueName: 'my-queue',
75
+ connection: 'AzureWebJobsStorage',
76
+ handler: handlers.queue as any,
77
+ })
63
78
 
64
79
  app.timer('scheduler', {
65
80
  schedule: '0 */5 * * * *',
66
- handler: async (timer, context) => {
67
- const request = new PikkuAzTimerRequest(timer)
68
- // Process scheduled Pikku functions
69
- },
81
+ handler: handlers.timer as any,
70
82
  })
71
83
  ```
84
+
85
+ Note the key names: `handlerTypes` uses **`scheduled`**, but the handler it
86
+ returns is **`timer`**.
87
+
88
+ ### HTTP
89
+
90
+ The handler buffers the whole body, converts to a standard `Request`, and
91
+ returns the response body as **text** — a streaming or binary response is
92
+ flattened. It takes no `RunHTTPWiringOptions`, so there is no `maxBodySize` or
93
+ `respondWith404` here; Azure's own request limits are the bound. A thrown error
94
+ is logged to `console.error` and whatever the response already holds is
95
+ returned.
96
+
97
+ ### Queue
98
+
99
+ The queue name comes from the message's own `queueName`, falling back to
100
+ `context.triggerMetadata.queueTrigger` and then `'unknown'` — a name that does
101
+ not match a wired queue means the job has no handler. `attemptsMade` is read
102
+ from `dequeueCount`, and `waitForCompletion` throws: Azure Storage Queues are
103
+ fire-and-forget. A failing job throws out of the handler, so retries and the
104
+ poison queue are governed by `host.json`, not by Pikku.
105
+
106
+ Producer side, `AzureQueueService(connectionString?)` falls back to
107
+ `AzureWebJobsStorage` and throws at construction if neither is set. Messages are
108
+ base64-encoded (Azure requires it), `delay` is milliseconds mapped to
109
+ `visibilityTimeout` in whole seconds capped at 7 days, `supportsResults` is
110
+ `false` and `getJob()` always throws. The queue name is remapped through
111
+ `AZURE_QUEUE_NAME_<SCREAMING_SNAKE>` when that variable exists, otherwise used
112
+ as-is.
113
+
114
+ ### Timer
115
+
116
+ The timer handler runs **every** scheduled task registered in the bundle,
117
+ ignoring both the `Timer` argument and each task's own cron expression. Unlike
118
+ the Lambda equivalent it does not catch per-task failures, so the first task
119
+ that throws aborts the ones after it — keep one schedule per function app, or
120
+ guard the task bodies yourself.
121
+
122
+ ### Logging
123
+
124
+ `new AzInvocationLogger(context)` forwards to the invocation context's
125
+ `info`/`warn`/`error`/`debug`/`trace`. `setLevel()` is a **no-op**: every level
126
+ is emitted and filtering has to be done in Azure's own logging configuration.