@pikku/inspector 0.12.31 → 0.12.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
1
+ ## 0.12.33
2
+
3
+ ### Patch Changes
4
+
5
+ - 4c17f7e: user flows: actors move onto the workflow wire + `pikku userflow` command
6
+ - Actors are no longer a singleton service: `startWorkflow(..., { actors })`
7
+ registers them per run and they arrive on the wire —
8
+ `func: async ({ logger }, input, { workflow, actors })`.
9
+ - Inspector enforces user flows are pure remote stories (PKU673): a
10
+ pikkuUserFlow func may only destructure `logger`/`config` from services.
11
+ - New `pikku userflow run <environment> [--flows a,b] [--tags x,y]` runs flows
12
+ against `userFlows.environments` from pikku.config.json (secret from
13
+ USER_FLOW_ACTOR_SECRET env), refusing internal (non-actor) steps so runs
14
+ against staging/production never touch local services; non-zero exit on
15
+ failure. `pikku userflow list` prints names, descriptions and tags.
16
+ - Workflow meta now carries `title` (parity with HTTP routes/functions).
17
+
18
+ - Updated dependencies [4c17f7e]
19
+ - @pikku/core@0.12.49
20
+
21
+ ## 0.12.32
22
+
23
+ ### Patch Changes
24
+
25
+ - 8dfddc3: pikkuUserFlow: user flows as workflows. A complex workflow whose steps can run
26
+ as actors over the real transport — `workflow.do(step, rpc, data, { actor:
27
+ actors.yasser })` — plus `workflow.expectEventually(...)` for polling async
28
+ effects. Actor steps never queue and never dispatch internally, so auth
29
+ middleware/permissions are exercised end-to-end; flows double as e2e tests and
30
+ staged/production health checks. Ships UserFlowActor types +
31
+ createHttpUserFlowActors (lazy sign-in via `/auth/sign-in/actor` with a
32
+ server-held secret), inspector source `'user-flow'`, and a console badge.
33
+ - Updated dependencies [5f2c566]
34
+ - Updated dependencies [8dfddc3]
35
+ - @pikku/core@0.12.48
36
+
1
37
  ## 0.12.31
2
38
 
3
39
  ### Patch Changes
@@ -250,9 +250,11 @@ export const addFunctions = (logger, node, checker, state, options) => {
250
250
  if (!ts.isIdentifier(expression)) {
251
251
  return;
252
252
  }
253
- // Match identifiers that contain both "pikku" and "func" (case insensitive)
253
+ // Match identifiers that contain both "pikku" and "func" (case insensitive),
254
+ // plus pikkuUserFlow (a workflow wrapper without "func" in its name)
254
255
  const pikkuFuncPattern = /pikku.*func/i;
255
- if (!pikkuFuncPattern.test(expression.text)) {
256
+ if (!pikkuFuncPattern.test(expression.text) &&
257
+ expression.text !== 'pikkuUserFlow') {
256
258
  return;
257
259
  }
258
260
  // only handle calls like pikkuFunc(...)
@@ -5,7 +5,7 @@ import type { WorkflowStepMeta } from '@pikku/core/workflow';
5
5
  */
6
6
  export declare function collectInvokedRPCs(steps: WorkflowStepMeta[], rpcs: Set<string>): void;
7
7
  /**
8
- * Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
9
- * Detects workflow registration and extracts metadata
8
+ * Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
9
+ * pikkuUserFlow() calls. Detects workflow registration and extracts metadata.
10
10
  */
11
11
  export declare const addWorkflow: AddWiring;
@@ -156,8 +156,8 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
156
156
  });
157
157
  }
158
158
  /**
159
- * Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
160
- * Detects workflow registration and extracts metadata
159
+ * Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
160
+ * pikkuUserFlow() calls. Detects workflow registration and extracts metadata.
161
161
  */
162
162
  export const addWorkflow = (logger, node, checker, state) => {
163
163
  if (!ts.isCallExpression(node)) {
@@ -176,6 +176,11 @@ export const addWorkflow = (logger, node, checker, state) => {
176
176
  else if (expression.text === 'pikkuWorkflowComplexFunc') {
177
177
  wrapperType = 'complex';
178
178
  }
179
+ else if (expression.text === 'pikkuUserFlow') {
180
+ // A user flow is a complex workflow whose steps run as actors over the
181
+ // real transport — same extraction rules as complex, distinct meta.
182
+ wrapperType = 'user-flow';
183
+ }
179
184
  else {
180
185
  return;
181
186
  }
@@ -193,6 +198,7 @@ export const addWorkflow = (logger, node, checker, state) => {
193
198
  const { funcNode, resolvedFunc } = extractFunctionNode(firstArg, checker);
194
199
  // Extract metadata if using object form
195
200
  let tags;
201
+ let title;
196
202
  let summary;
197
203
  let description;
198
204
  let errors;
@@ -202,6 +208,7 @@ export const addWorkflow = (logger, node, checker, state) => {
202
208
  if (metadata.disabled)
203
209
  return;
204
210
  tags = metadata.tags;
211
+ title = metadata.title;
205
212
  summary = metadata.summary;
206
213
  description = metadata.description;
207
214
  errors = metadata.errors;
@@ -230,7 +237,7 @@ export const addWorkflow = (logger, node, checker, state) => {
230
237
  // Try DSL workflow extraction first
231
238
  // Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
232
239
  const result = extractDSLWorkflow(node, checker, {
233
- allowInline: wrapperType === 'complex',
240
+ allowInline: wrapperType !== 'dsl',
234
241
  });
235
242
  if (result.status === 'ok' && result.steps) {
236
243
  // Extraction succeeded
@@ -275,7 +282,7 @@ export const addWorkflow = (logger, node, checker, state) => {
275
282
  // For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
276
283
  // patterns the DSL extractor doesn't handle (array+push, nested Promise.all
277
284
  // with identifier args, etc.) are still registered as invoked functions.
278
- if (wrapperType === 'complex') {
285
+ if (wrapperType !== 'dsl') {
279
286
  getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
280
287
  }
281
288
  state.workflows.meta[workflowName] = {
@@ -284,15 +291,31 @@ export const addWorkflow = (logger, node, checker, state) => {
284
291
  steps,
285
292
  context,
286
293
  dsl,
294
+ title,
287
295
  summary,
288
296
  description,
289
297
  errors,
290
298
  tags,
291
299
  expose,
300
+ userFlow: wrapperType === 'user-flow' ? true : undefined,
292
301
  };
302
+ // User flows are pure stories of remote RPCs (same rule as client-side CLI
303
+ // renderers): the func may only destructure logger/config — everything else
304
+ // must go through actor steps so the flow runs against the TARGET
305
+ // environment, never local services.
306
+ const funcMeta = state.functions.meta[pikkuFuncId];
307
+ if (wrapperType === 'user-flow' && funcMeta?.services) {
308
+ const disallowed = funcMeta.services.services.filter((svc) => svc !== 'logger' && svc !== 'config');
309
+ if (disallowed.length > 0) {
310
+ logger.critical(ErrorCode.USER_FLOW_HAS_SERVICES, `User flow '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
311
+ `User flows may only use 'logger'/'config' — drive everything else through ` +
312
+ `actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
313
+ `runs against the target environment.`);
314
+ return;
315
+ }
316
+ }
293
317
  // Workflow functions require platform services that aren't visible
294
318
  // through parameter destructuring (they're accessed via workflow.do/sleep)
295
- const funcMeta = state.functions.meta[pikkuFuncId];
296
319
  if (funcMeta?.services) {
297
320
  for (const svc of [
298
321
  'workflowService',
@@ -19,6 +19,7 @@ export declare enum ErrorCode {
19
19
  MISSING_QUEUE_NAME = "PKU384",
20
20
  MISSING_CHANNEL_NAME = "PKU400",
21
21
  CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
22
+ USER_FLOW_HAS_SERVICES = "PKU673",
22
23
  DYNAMIC_STEP_NAME = "PKU529",
23
24
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
24
25
  INVALID_DSL_WORKFLOW = "PKU641",
@@ -21,6 +21,7 @@ export var ErrorCode;
21
21
  ErrorCode["MISSING_QUEUE_NAME"] = "PKU384";
22
22
  ErrorCode["MISSING_CHANNEL_NAME"] = "PKU400";
23
23
  ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
24
+ ErrorCode["USER_FLOW_HAS_SERVICES"] = "PKU673";
24
25
  ErrorCode["DYNAMIC_STEP_NAME"] = "PKU529";
25
26
  ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
26
27
  ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
@@ -291,10 +291,15 @@ export function convertDslToGraph(workflowName, meta) {
291
291
  nodesRecord[node.nodeId] = node;
292
292
  }
293
293
  const entryNodeIds = nodes.length > 0 ? [nodes[0].nodeId] : [];
294
- // Determine source type based on dsl flag:
294
+ // Determine source type:
295
+ // - userFlow: complex workflow whose steps run as actors (user flow)
295
296
  // - dsl === true: pure DSL workflow, can be serialized
296
297
  // - dsl === false: complex workflow with inline steps, not serializable
297
- const source = meta.dsl === false ? 'complex' : 'dsl';
298
+ const source = meta.userFlow
299
+ ? 'user-flow'
300
+ : meta.dsl === false
301
+ ? 'complex'
302
+ : 'dsl';
298
303
  return {
299
304
  name: workflowName,
300
305
  pikkuFuncId: meta.pikkuFuncId,
@@ -123,8 +123,9 @@ export declare const isFlowNode: (node: SerializedGraphNode) => node is FlowNode
123
123
  * - 'dsl': Pure DSL workflow (pikkuWorkflowFunc) - can be round-tripped to code
124
124
  * - 'complex': Complex workflow (pikkuWorkflowComplexFunc) - contains inline steps, not serializable
125
125
  * - 'graph': Graph-based workflow (pikkuWorkflowGraph)
126
+ * - 'user-flow': User flow (pikkuUserFlow) - complex workflow whose steps run as actors
126
127
  */
127
- export type WorkflowSourceType = 'dsl' | 'complex' | 'graph';
128
+ export type WorkflowSourceType = 'dsl' | 'complex' | 'graph' | 'user-flow';
128
129
  /**
129
130
  * Serialized workflow graph - the canonical JSON format
130
131
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.31",
3
+ "version": "0.12.33",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.45",
38
+ "@pikku/core": "^0.12.49",
39
39
  "openapi-types": "^12.1.3",
40
40
  "path-to-regexp": "^8.3.0",
41
41
  "ts-json-schema-generator": "2.9.1-next.16",
@@ -357,9 +357,13 @@ export const addFunctions: AddWiring = (
357
357
  return
358
358
  }
359
359
 
360
- // Match identifiers that contain both "pikku" and "func" (case insensitive)
360
+ // Match identifiers that contain both "pikku" and "func" (case insensitive),
361
+ // plus pikkuUserFlow (a workflow wrapper without "func" in its name)
361
362
  const pikkuFuncPattern = /pikku.*func/i
362
- if (!pikkuFuncPattern.test(expression.text)) {
363
+ if (
364
+ !pikkuFuncPattern.test(expression.text) &&
365
+ expression.text !== 'pikkuUserFlow'
366
+ ) {
363
367
  return
364
368
  }
365
369
 
@@ -169,8 +169,8 @@ function getWorkflowInvocations(
169
169
  }
170
170
 
171
171
  /**
172
- * Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
173
- * Detects workflow registration and extracts metadata
172
+ * Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
173
+ * pikkuUserFlow() calls. Detects workflow registration and extracts metadata.
174
174
  */
175
175
  export const addWorkflow: AddWiring = (logger, node, checker, state) => {
176
176
  if (!ts.isCallExpression(node)) {
@@ -185,11 +185,15 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
185
185
  return
186
186
  }
187
187
 
188
- let wrapperType: 'dsl' | 'complex' | null = null
188
+ let wrapperType: 'dsl' | 'complex' | 'user-flow' | null = null
189
189
  if (expression.text === 'pikkuWorkflowFunc') {
190
190
  wrapperType = 'dsl'
191
191
  } else if (expression.text === 'pikkuWorkflowComplexFunc') {
192
192
  wrapperType = 'complex'
193
+ } else if (expression.text === 'pikkuUserFlow') {
194
+ // A user flow is a complex workflow whose steps run as actors over the
195
+ // real transport — same extraction rules as complex, distinct meta.
196
+ wrapperType = 'user-flow'
193
197
  } else {
194
198
  return
195
199
  }
@@ -220,6 +224,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
220
224
 
221
225
  // Extract metadata if using object form
222
226
  let tags: string[] | undefined
227
+ let title: string | undefined
223
228
  let summary: string | undefined
224
229
  let description: string | undefined
225
230
  let errors: string[] | undefined
@@ -235,6 +240,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
235
240
  )
236
241
  if (metadata.disabled) return
237
242
  tags = metadata.tags
243
+ title = metadata.title
238
244
  summary = metadata.summary
239
245
  description = metadata.description
240
246
  errors = metadata.errors
@@ -277,7 +283,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
277
283
  // Try DSL workflow extraction first
278
284
  // Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
279
285
  const result = extractDSLWorkflow(node, checker, {
280
- allowInline: wrapperType === 'complex',
286
+ allowInline: wrapperType !== 'dsl',
281
287
  })
282
288
 
283
289
  if (result.status === 'ok' && result.steps) {
@@ -331,7 +337,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
331
337
  // For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
332
338
  // patterns the DSL extractor doesn't handle (array+push, nested Promise.all
333
339
  // with identifier args, etc.) are still registered as invoked functions.
334
- if (wrapperType === 'complex') {
340
+ if (wrapperType !== 'dsl') {
335
341
  getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
336
342
  }
337
343
 
@@ -341,16 +347,38 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
341
347
  steps,
342
348
  context,
343
349
  dsl,
350
+ title,
344
351
  summary,
345
352
  description,
346
353
  errors,
347
354
  tags,
348
355
  expose,
356
+ userFlow: wrapperType === 'user-flow' ? true : undefined,
357
+ }
358
+
359
+ // User flows are pure stories of remote RPCs (same rule as client-side CLI
360
+ // renderers): the func may only destructure logger/config — everything else
361
+ // must go through actor steps so the flow runs against the TARGET
362
+ // environment, never local services.
363
+ const funcMeta = state.functions.meta[pikkuFuncId]
364
+ if (wrapperType === 'user-flow' && funcMeta?.services) {
365
+ const disallowed = funcMeta.services.services.filter(
366
+ (svc) => svc !== 'logger' && svc !== 'config'
367
+ )
368
+ if (disallowed.length > 0) {
369
+ logger.critical(
370
+ ErrorCode.USER_FLOW_HAS_SERVICES,
371
+ `User flow '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
372
+ `User flows may only use 'logger'/'config' — drive everything else through ` +
373
+ `actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
374
+ `runs against the target environment.`
375
+ )
376
+ return
377
+ }
349
378
  }
350
379
 
351
380
  // Workflow functions require platform services that aren't visible
352
381
  // through parameter destructuring (they're accessed via workflow.do/sleep)
353
- const funcMeta = state.functions.meta[pikkuFuncId]
354
382
  if (funcMeta?.services) {
355
383
  for (const svc of [
356
384
  'workflowService',
@@ -21,6 +21,7 @@ export enum ErrorCode {
21
21
  MISSING_QUEUE_NAME = 'PKU384',
22
22
  MISSING_CHANNEL_NAME = 'PKU400',
23
23
  CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = 'PKU672',
24
+ USER_FLOW_HAS_SERVICES = 'PKU673',
24
25
  DYNAMIC_STEP_NAME = 'PKU529',
25
26
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = 'PKU600',
26
27
  INVALID_DSL_WORKFLOW = 'PKU641',
@@ -389,10 +389,15 @@ export function convertDslToGraph(
389
389
 
390
390
  const entryNodeIds = nodes.length > 0 ? [nodes[0].nodeId] : []
391
391
 
392
- // Determine source type based on dsl flag:
392
+ // Determine source type:
393
+ // - userFlow: complex workflow whose steps run as actors (user flow)
393
394
  // - dsl === true: pure DSL workflow, can be serialized
394
395
  // - dsl === false: complex workflow with inline steps, not serializable
395
- const source = meta.dsl === false ? 'complex' : 'dsl'
396
+ const source = meta.userFlow
397
+ ? 'user-flow'
398
+ : meta.dsl === false
399
+ ? 'complex'
400
+ : 'dsl'
396
401
 
397
402
  return {
398
403
  name: workflowName,
@@ -177,8 +177,9 @@ export const isFlowNode = (node: SerializedGraphNode): node is FlowNode =>
177
177
  * - 'dsl': Pure DSL workflow (pikkuWorkflowFunc) - can be round-tripped to code
178
178
  * - 'complex': Complex workflow (pikkuWorkflowComplexFunc) - contains inline steps, not serializable
179
179
  * - 'graph': Graph-based workflow (pikkuWorkflowGraph)
180
+ * - 'user-flow': User flow (pikkuUserFlow) - complex workflow whose steps run as actors
180
181
  */
181
- export type WorkflowSourceType = 'dsl' | 'complex' | 'graph'
182
+ export type WorkflowSourceType = 'dsl' | 'complex' | 'graph' | 'user-flow'
182
183
 
183
184
  /**
184
185
  * Serialized workflow graph - the canonical JSON format