@pikku/cli 0.12.68 → 0.12.70

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 (106) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/{index-CttM2Aiz.js → index-BY60UXB6.js} +132 -132
  3. package/console-app/index.html +2 -2
  4. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-channel.js +7 -12
  8. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  9. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +23 -30
  16. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  18. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  20. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  21. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  23. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.json +39 -54
  25. package/dist/.pikku/function/pikku-functions.gen.js +1 -3
  26. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  27. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  28. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  29. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  30. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  31. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  32. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  37. package/dist/.pikku/pikku-services.gen.d.ts +4 -2
  38. package/dist/.pikku/pikku-services.gen.js +2 -0
  39. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  40. package/dist/.pikku/pikku-types.gen.js +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  42. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  45. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  47. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +2 -3
  48. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  49. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  50. package/dist/.pikku/schemas/register.gen.js +5 -7
  51. package/dist/.pikku/schemas/schemas/DevInput.schema.json +1 -1
  52. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  53. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +1 -0
  54. package/dist/.pikku/schemas/schemas/ServeInput.schema.json +1 -1
  55. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  56. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  57. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  58. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  59. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  60. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  61. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  62. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  63. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  64. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  65. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +4 -4
  66. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +2 -2
  67. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  68. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  69. package/dist/bin/pikku-bin.mjs +2 -2
  70. package/dist/bin/pikku.js +1 -1
  71. package/dist/src/cli.wiring.js +23 -30
  72. package/dist/src/functions/commands/all.js +14 -1
  73. package/dist/src/functions/commands/bun-coverage-loader.d.ts +11 -0
  74. package/dist/src/functions/commands/bun-coverage-loader.js +42 -0
  75. package/dist/src/functions/commands/dev.d.ts +6 -3
  76. package/dist/src/functions/commands/dev.js +26 -9
  77. package/dist/src/functions/commands/{user-flow.d.ts → scenario.d.ts} +5 -2
  78. package/dist/src/functions/commands/{user-flow.js → scenario.js} +73 -32
  79. package/dist/src/functions/commands/serve-console.d.ts +2 -3
  80. package/dist/src/functions/commands/serve-console.js +5 -51
  81. package/dist/src/functions/commands/serve.d.ts +3 -3
  82. package/dist/src/functions/commands/serve.js +15 -7
  83. package/dist/src/functions/commands/start-coverage.d.ts +13 -0
  84. package/dist/src/functions/commands/start-coverage.js +26 -0
  85. package/dist/src/functions/commands/watch.js +2 -1
  86. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +6 -5
  87. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +2 -0
  88. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +29 -0
  89. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +4 -4
  90. package/dist/src/services.js +14 -1
  91. package/dist/src/utils/file-writer.d.ts +1 -0
  92. package/dist/src/utils/file-writer.js +9 -0
  93. package/dist/src/utils/meta-diff.d.ts +32 -0
  94. package/dist/src/utils/meta-diff.js +144 -0
  95. package/dist/src/utils/pikku-cli-config.js +2 -2
  96. package/dist/src/utils/serialize-schemas.js +1 -1
  97. package/dist/src/utils/strip-verbose-meta.js +3 -0
  98. package/dist/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +8 -6
  100. package/dist/.pikku/schemas/schemas/ConsoleCommandInput.schema.json +0 -1
  101. package/dist/.pikku/schemas/schemas/UserFlowRunInput.schema.json +0 -1
  102. package/dist/src/functions/commands/console.d.ts +0 -13
  103. package/dist/src/functions/commands/console.js +0 -122
  104. package/dist/src/functions/wirings/workflow/serialize-user-flow-actors.d.ts +0 -8
  105. package/dist/src/functions/wirings/workflow/serialize-user-flow-actors.js +0 -36
  106. /package/dist/.pikku/schemas/schemas/{UserFlowListInput.schema.json → ScenarioListInput.schema.json} +0 -0
@@ -0,0 +1,42 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ const shouldInstrument = (path) => !path.includes('/node_modules/') &&
4
+ !path.includes('/.pikku/') &&
5
+ !/\.(gen|test|d)\.tsx?$/.test(path);
6
+ /**
7
+ * Registers a Bun loader plugin that runs istanbul-lib-instrument over the
8
+ * project's TypeScript sources so `__coverage__` counters exist at runtime.
9
+ * Bun has no V8 precise coverage (`node:inspector` throws "Coverage APIs are
10
+ * not supported"), so instrumentation at load time is the coverage backend
11
+ * there. Must be called before the user bootstrap is imported.
12
+ */
13
+ export async function registerBunCoverageLoader({ rootDir, }) {
14
+ const bun = globalThis.Bun;
15
+ if (!bun) {
16
+ throw new Error('registerBunCoverageLoader called outside Bun');
17
+ }
18
+ const { createInstrumenter } = await import('istanbul-lib-instrument');
19
+ const instrumenter = createInstrumenter({
20
+ esModules: true,
21
+ compact: false,
22
+ parserPlugins: [
23
+ 'typescript',
24
+ 'importAttributes',
25
+ 'topLevelAwait',
26
+ 'decorators-legacy',
27
+ ],
28
+ });
29
+ const filter = new RegExp(`^${escapeRegExp(rootDir)}/.*\\.tsx?$`);
30
+ bun.plugin({
31
+ name: 'pikku-istanbul-coverage',
32
+ setup(build) {
33
+ build.onLoad({ filter }, async (args) => {
34
+ if (!shouldInstrument(args.path))
35
+ return undefined;
36
+ const source = await readFile(args.path, 'utf-8');
37
+ const contents = instrumenter.instrumentSync(source, args.path);
38
+ return { contents, loader: args.path.endsWith('.tsx') ? 'tsx' : 'ts' };
39
+ });
40
+ },
41
+ });
42
+ }
@@ -2,15 +2,18 @@ export declare const dev: import("#pikku").PikkuFunctionConfig<{
2
2
  port?: string;
3
3
  watch?: boolean;
4
4
  hmr?: boolean;
5
- console?: string | boolean;
5
+ coverage?: boolean;
6
+ test?: boolean;
6
7
  }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
7
8
  port?: string;
8
9
  watch?: boolean;
9
10
  hmr?: boolean;
10
- console?: string | boolean;
11
+ coverage?: boolean;
12
+ test?: boolean;
11
13
  }, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{
12
14
  port?: string;
13
15
  watch?: boolean;
14
16
  hmr?: boolean;
15
- console?: string | boolean;
17
+ coverage?: boolean;
18
+ test?: boolean;
16
19
  }, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
@@ -2,7 +2,7 @@ import { join, resolve } from 'path';
2
2
  import { pikkuSessionlessFunc } from '#pikku';
3
3
  import chokidar from 'chokidar';
4
4
  import { pikkuDevReloader } from '@pikku/core/dev';
5
- import { ConsoleLogger, LocalEmailService, InMemoryQueueService, InMemoryWorkflowService, InMemoryTriggerService, InMemoryAIRunStateService, } from '@pikku/core/services';
5
+ import { ConsoleLogger, LocalEmailService, spy, InMemoryQueueService, InMemoryWorkflowService, InMemoryTriggerService, InMemoryAIRunStateService, } from '@pikku/core/services';
6
6
  import { KyselyAIStorageService, KyselyAIRunStateService, KyselyAgentRunService, } from '@pikku/kysely';
7
7
  import { stopSingletonServices } from '@pikku/core';
8
8
  import { pikkuState } from '@pikku/core/internal';
@@ -11,11 +11,19 @@ import { LocalContent, } from '@pikku/core/services/local-content';
11
11
  import { InMemorySchedulerService } from '@pikku/schedule';
12
12
  import { resolveDb, createKysely, parseDatabaseUrl, } from '../db/local-db.js';
13
13
  import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
14
+ import { startCoverageService } from './start-coverage.js';
14
15
  import { createDevAIAgentRunner } from './dev-ai-runner.js';
15
- import { startConsoleServer } from './serve-console.js';
16
+ import { resolveConsoleMount } from './serve-console.js';
16
17
  export const dev = pikkuSessionlessFunc({
17
18
  remote: true,
18
- func: async ({ logger, config, getInspectorState, variables, devServerRunner }, { port, watch, hmr, console: consoleFlag }, { rpc }) => {
19
+ func: async ({ logger, config, getInspectorState, invalidateInspectorState, variables, devServerRunner, }, { port, watch, hmr, coverage, test }, { rpc }) => {
20
+ process.env.PIKKU_DEV_QUICK_LOGIN ??= 'true';
21
+ if (test) {
22
+ process.env.PIKKU_TEST_RUN = 'true';
23
+ }
24
+ const coverageService = coverage
25
+ ? await startCoverageService(logger, config.rootDir)
26
+ : undefined;
19
27
  const resolvedPort = parseInt(port || '3000', 10);
20
28
  const hostname = 'localhost';
21
29
  // Bind on IPv4 loopback explicitly. Under Bun, hostname 'localhost' resolves
@@ -177,8 +185,11 @@ export const dev = pikkuSessionlessFunc({
177
185
  const inMemoryServices = {
178
186
  logger: devLogger,
179
187
  ...(aiAgentRunner ? { aiAgentRunner } : {}),
180
- emailService: new LocalEmailService(),
188
+ emailService: test
189
+ ? spy('emailService', new LocalEmailService())
190
+ : new LocalEmailService(),
181
191
  metaService: new LocalMetaService(pikkuDir),
192
+ ...(coverageService ? { coverageService } : {}),
182
193
  schedulerService,
183
194
  queueService: new InMemoryQueueService(),
184
195
  workflowService,
@@ -195,7 +206,11 @@ export const dev = pikkuSessionlessFunc({
195
206
  ...inMemoryServices,
196
207
  getInspectorState,
197
208
  });
198
- const resolvedServices = { ...singletonServices, getInspectorState };
209
+ const resolvedServices = {
210
+ ...singletonServices,
211
+ getInspectorState,
212
+ ...(coverageService ? { coverageService } : {}),
213
+ };
199
214
  pikkuState(null, 'package', 'singletonServices', resolvedServices);
200
215
  resolvedServices.workflowService?.wireQueueWorkers?.();
201
216
  const { serverLifecycleFactory } = inspectorState.filesAndMethods;
@@ -205,20 +220,22 @@ export const dev = pikkuSessionlessFunc({
205
220
  const m = await loadUserModule(serverLifecycleFactory.file);
206
221
  return m[serverLifecycleFactory.variable];
207
222
  };
223
+ const consoleMount = await resolveConsoleMount();
208
224
  const pikkuServer = devServerRunner.createServer({
209
225
  ...userConfig,
210
226
  hostname: bindHostname,
211
227
  port: resolvedPort,
212
228
  content: localContentConfig,
229
+ ...(consoleMount ? { staticMounts: [consoleMount] } : {}),
213
230
  }, logger);
214
231
  const lifecycle = await loadLifecycle();
215
232
  await pikkuServer.init();
216
233
  await lifecycle?.beforeStart?.(resolvedServices);
217
234
  await pikkuServer.start();
218
235
  await lifecycle?.afterStart?.(resolvedServices);
219
- const consoleServer = consoleFlag
220
- ? await startConsoleServer(consoleFlag, hostname, logger)
221
- : undefined;
236
+ if (consoleMount) {
237
+ logger.info(`Pikku Console available at http://${hostname}:${resolvedPort}${consoleMount.urlPrefix}`);
238
+ }
222
239
  let configWatcher;
223
240
  let watcher;
224
241
  process.once('SIGINT', async () => {
@@ -229,7 +246,6 @@ export const dev = pikkuSessionlessFunc({
229
246
  await configWatcher?.close();
230
247
  await watcher?.close();
231
248
  await pikkuServer.stop();
232
- consoleServer?.close();
233
249
  await lifecycle?.afterStop?.(resolvedServices);
234
250
  }
235
251
  finally {
@@ -259,6 +275,7 @@ export const dev = pikkuSessionlessFunc({
259
275
  const handle = async () => {
260
276
  try {
261
277
  const start = Date.now();
278
+ invalidateInspectorState();
262
279
  await runAllWithCommandState();
263
280
  workflowService.wireQueueWorkers();
264
281
  logger.info({
@@ -1,14 +1,17 @@
1
- export declare const userFlowList: import("#pikku").PikkuFunctionConfig<{}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{}, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{}, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
2
- export declare const userFlowRun: import("#pikku").PikkuFunctionConfig<{
1
+ export declare const scenarioList: import("#pikku").PikkuFunctionConfig<{}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{}, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{}, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
2
+ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
3
3
  environment: string;
4
4
  flows?: string;
5
5
  tags?: string;
6
+ coverage?: boolean;
6
7
  }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
7
8
  environment: string;
8
9
  flows?: string;
9
10
  tags?: string;
11
+ coverage?: boolean;
10
12
  }, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{
11
13
  environment: string;
12
14
  flows?: string;
13
15
  tags?: string;
16
+ coverage?: boolean;
14
17
  }, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
@@ -1,23 +1,24 @@
1
- import { resolve } from 'path';
1
+ import { resolve, join } from 'node:path';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
3
  import { pikkuSessionlessFunc } from '#pikku';
3
- import { InMemoryWorkflowService, createHttpUserFlowActors, } from '@pikku/core/services';
4
+ import { InMemoryWorkflowService, createHttpScenarioActors, } from '@pikku/core/services';
4
5
  import { pikkuState } from '@pikku/core/internal';
5
6
  import { loadUserBootstrap } from './load-user-project.js';
6
- const isUserFlow = (wf) => wf?.userFlow === true;
7
- const listUserFlows = (state) => Object.entries(state.workflows?.meta ?? {})
8
- .filter(([, wf]) => isUserFlow(wf))
7
+ const isScenario = (wf) => wf?.scenario === true;
8
+ const listScenarios = (state) => Object.entries(state.workflows?.meta ?? {})
9
+ .filter(([, wf]) => isScenario(wf))
9
10
  .map(([id, wf]) => ({
10
11
  id,
11
12
  name: wf.name ?? id,
12
13
  description: wf.description ?? wf.summary ?? wf.title ?? null,
13
14
  tags: wf.tags ?? [],
14
15
  }));
15
- export const userFlowList = pikkuSessionlessFunc({
16
+ export const scenarioList = pikkuSessionlessFunc({
16
17
  func: async ({ logger, getInspectorState }) => {
17
18
  const state = await getInspectorState();
18
- const flows = listUserFlows(state);
19
+ const flows = listScenarios(state);
19
20
  if (flows.length === 0) {
20
- logger.info('No user flows found (pikkuUserFlow exports).');
21
+ logger.info('No scenarios found (pikkuScenario exports).');
21
22
  return;
22
23
  }
23
24
  for (const flow of flows) {
@@ -29,26 +30,24 @@ export const userFlowList = pikkuSessionlessFunc({
29
30
  }
30
31
  },
31
32
  });
32
- export const userFlowRun = pikkuSessionlessFunc({
33
- func: async ({ logger, config, getInspectorState, variables }, { environment, flows, tags }) => {
33
+ export const scenarioRun = pikkuSessionlessFunc({
34
+ func: async ({ logger, config, getInspectorState, variables }, { environment, flows, tags, coverage }) => {
34
35
  const state = await getInspectorState(true);
35
- // --- resolve the target environment ---
36
- const environments = config.userFlows?.environments ?? {};
36
+ const environments = config.scenarios?.environments ?? {};
37
37
  const env = environments[environment];
38
38
  if (!env) {
39
39
  const known = Object.keys(environments);
40
- throw new Error(`Unknown user-flow environment '${environment}'. ` +
40
+ throw new Error(`Unknown scenario environment '${environment}'. ` +
41
41
  (known.length
42
42
  ? `Configured environments: ${known.join(', ')}`
43
- : `Add userFlows.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
43
+ : `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
44
44
  }
45
- // --- select flows ---
46
- let selected = listUserFlows(state);
45
+ let selected = listScenarios(state);
47
46
  if (flows) {
48
47
  const names = new Set(flows.split(',').map((f) => f.trim()));
49
48
  const unknown = [...names].filter((n) => !selected.some((f) => f.name === n));
50
49
  if (unknown.length) {
51
- throw new Error(`Unknown user flow(s): ${unknown.join(', ')}. Available: ${selected.map((f) => f.name).join(', ')}`);
50
+ throw new Error(`Unknown scenario(s): ${unknown.join(', ')}. Available: ${selected.map((f) => f.name).join(', ')}`);
52
51
  }
53
52
  selected = selected.filter((f) => names.has(f.name));
54
53
  }
@@ -57,27 +56,22 @@ export const userFlowRun = pikkuSessionlessFunc({
57
56
  selected = selected.filter((f) => wanted.some((t) => f.tags.includes(t)));
58
57
  }
59
58
  if (selected.length === 0) {
60
- logger.error('No user flows matched.');
59
+ logger.error('No scenarios matched.');
61
60
  process.exitCode = 1;
62
61
  return;
63
62
  }
64
- // --- actors: registry from config, secret STRICTLY from env ---
65
- const secret = await variables.get('USER_FLOW_ACTOR_SECRET');
63
+ const secret = await variables.get('SCENARIO_ACTOR_SECRET');
66
64
  if (!secret) {
67
- throw new Error('USER_FLOW_ACTOR_SECRET is not set — user-flow actors cannot sign in. ' +
65
+ throw new Error('SCENARIO_ACTOR_SECRET is not set — scenario actors cannot sign in. ' +
68
66
  'Export it in the environment running this command (never put it in pikku.config.json).');
69
67
  }
70
- const actors = createHttpUserFlowActors({
68
+ const actors = createHttpScenarioActors({
71
69
  apiUrl: env.apiUrl,
72
70
  secret,
73
- actors: config.userFlows?.actors ?? {},
71
+ actors: config.scenarios?.actors ?? {},
74
72
  signInPath: env.signInPath,
75
73
  rpcPath: env.rpcPath,
76
74
  });
77
- // --- load the project's generated bootstrap so flows are registered.
78
- // Deliberately NO user services: flows may only use logger/config
79
- // (inspector-enforced), and internal rpc dispatch is refused below so a
80
- // run against staging/production can never touch local services.
81
75
  await loadUserBootstrap(resolve(config.rootDir, config.outDir));
82
76
  const workflowService = new InMemoryWorkflowService();
83
77
  pikkuState(null, 'package', 'singletonServices', {
@@ -87,15 +81,44 @@ export const userFlowRun = pikkuSessionlessFunc({
87
81
  });
88
82
  const guardRpc = {
89
83
  rpcWithWire: async (rpcName) => {
90
- throw new Error(`User flow tried to run '${rpcName}' as an internal step. Every workflow.do ` +
91
- `in a user flow must carry { actor: actors.x } so it executes against ` +
84
+ throw new Error(`Scenario tried to run '${rpcName}' as an internal step. Every workflow.do ` +
85
+ `in a scenario must carry { actor: actors.x } so it executes against ` +
92
86
  `'${environment}' (${env.apiUrl}), not local services.`);
93
87
  },
94
88
  };
95
- // --- run sequentially: flows are stories; parallel actors would share cookie jars ---
96
89
  const results = [];
90
+ const coverageActor = coverage ? Object.values(actors)[0] : undefined;
91
+ let coverageActive = Boolean(coverageActor);
92
+ if (coverage && !coverageActor) {
93
+ logger.warn('--coverage requires at least one configured actor — skipping coverage.');
94
+ }
95
+ const scenarioCoverage = {};
96
+ const invokeCoverage = async (rpcName) => {
97
+ if (!coverageActive || !coverageActor)
98
+ return null;
99
+ try {
100
+ return await coverageActor.invoke(rpcName, null);
101
+ }
102
+ catch (e) {
103
+ coverageActive = false;
104
+ logger.warn(`Coverage disabled — '${rpcName}' failed against '${environment}': ${e?.message ?? e}. ` +
105
+ `Is the server running with --coverage and the console addon wired?`);
106
+ return null;
107
+ }
108
+ };
97
109
  for (const flow of selected) {
98
110
  const startedAt = Date.now();
111
+ if (coverageActive) {
112
+ const reset = await invokeCoverage('console:resetLiveCoverage');
113
+ if (reset && reset.enabled === false) {
114
+ coverageActive = false;
115
+ logger.warn(`Coverage disabled — '${environment}' is not collecting (start the server with --coverage).`);
116
+ }
117
+ }
118
+ try {
119
+ await coverageActor?.invoke('console:resetStubs', null);
120
+ }
121
+ catch { }
99
122
  try {
100
123
  const { runId } = await workflowService.startWorkflow(flow.name, undefined, { type: 'cli' }, guardRpc, { actors });
101
124
  const run = await workflowService.getRun(runId);
@@ -124,8 +147,26 @@ export const userFlowRun = pikkuSessionlessFunc({
124
147
  error: e?.message ?? String(e),
125
148
  });
126
149
  }
150
+ if (coverageActive) {
151
+ const report = await invokeCoverage('console:takeLiveCoverage');
152
+ if (report) {
153
+ scenarioCoverage[flow.name] = report;
154
+ const covered = report.functions?.filter((f) => f.status === 'covered' || f.status === 'partial');
155
+ logger.info(` coverage: ${covered?.length ?? 0}/${report.summary?.total ?? 0} functions exercised by '${flow.name}'`);
156
+ }
157
+ }
158
+ }
159
+ if (coverage && Object.keys(scenarioCoverage).length > 0) {
160
+ const coverageDir = join(resolve(config.rootDir, config.outDir), 'coverage');
161
+ mkdirSync(coverageDir, { recursive: true });
162
+ const outFile = join(coverageDir, 'scenario-coverage.json');
163
+ writeFileSync(outFile, JSON.stringify({
164
+ generatedAt: new Date().toISOString(),
165
+ environment,
166
+ scenarios: scenarioCoverage,
167
+ }, null, 2) + '\n');
168
+ logger.info(`Scenario coverage → ${outFile}`);
127
169
  }
128
- // --- report ---
129
170
  const failed = results.filter((r) => r.status === 'failed');
130
171
  for (const r of results) {
131
172
  if (r.status === 'passed') {
@@ -135,7 +176,7 @@ export const userFlowRun = pikkuSessionlessFunc({
135
176
  logger.error(`FAIL ${r.name} (${r.durationMs}ms): ${r.error}`);
136
177
  }
137
178
  }
138
- logger.info(`${results.length - failed.length}/${results.length} user flows passed against '${environment}'`);
179
+ logger.info(`${results.length - failed.length}/${results.length} scenarios passed against '${environment}'`);
139
180
  if (failed.length > 0) {
140
181
  process.exitCode = 1;
141
182
  }
@@ -1,4 +1,3 @@
1
- import { createServer } from 'http';
2
- import type { Logger } from '@pikku/core/services';
1
+ import type { StaticMount } from '@pikku/node-http-server';
3
2
  export declare function resolveConsoleDir(): string;
4
- export declare function startConsoleServer(consoleFlag: string | boolean, hostname: string, logger: Logger): Promise<ReturnType<typeof createServer> | undefined>;
3
+ export declare function resolveConsoleMount(): Promise<StaticMount | undefined>;
@@ -1,62 +1,16 @@
1
- import { createServer } from 'http';
2
- import { readFile, stat } from 'fs/promises';
3
- import { join, extname } from 'path';
1
+ import { stat } from 'fs/promises';
2
+ import { join } from 'path';
4
3
  import { fileURLToPath } from 'url';
5
- const MIME_TYPES = {
6
- '.html': 'text/html',
7
- '.js': 'application/javascript',
8
- '.css': 'text/css',
9
- '.json': 'application/json',
10
- '.png': 'image/png',
11
- '.svg': 'image/svg+xml',
12
- '.ico': 'image/x-icon',
13
- '.webmanifest': 'application/manifest+json',
14
- '.woff': 'font/woff',
15
- '.woff2': 'font/woff2',
16
- '.ttf': 'font/ttf',
17
- '.map': 'application/json',
18
- };
19
4
  export function resolveConsoleDir() {
20
5
  return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', '..', 'console-app');
21
6
  }
22
- export async function startConsoleServer(consoleFlag, hostname, logger) {
7
+ export async function resolveConsoleMount() {
23
8
  const consoleDir = resolveConsoleDir();
24
- const consolePort = typeof consoleFlag === 'string' ? parseInt(consoleFlag, 10) : 51442;
25
9
  try {
26
- await stat(consoleDir);
10
+ await stat(join(consoleDir, 'index.html'));
27
11
  }
28
12
  catch {
29
- logger.error('Console app not found. Please rebuild @pikku/cli with the console app bundled.');
30
13
  return undefined;
31
14
  }
32
- const server = createServer(async (req, res) => {
33
- const url = new URL(req.url || '/', `http://localhost:${consolePort}`);
34
- let filePath = join(consoleDir, url.pathname);
35
- try {
36
- const s = await stat(filePath);
37
- if (s.isDirectory())
38
- filePath = join(filePath, 'index.html');
39
- }
40
- catch {
41
- filePath = join(consoleDir, 'index.html');
42
- }
43
- try {
44
- const content = await readFile(filePath);
45
- const contentType = MIME_TYPES[extname(filePath)] || 'application/octet-stream';
46
- res.writeHead(200, { 'Content-Type': contentType });
47
- res.end(content);
48
- }
49
- catch {
50
- res.writeHead(404, { 'Content-Type': 'text/plain' });
51
- res.end('Not Found');
52
- }
53
- });
54
- await new Promise((resolve, reject) => {
55
- server.listen(consolePort, () => {
56
- logger.info(`Pikku Console running at http://${hostname}:${consolePort}`);
57
- resolve();
58
- });
59
- server.on('error', reject);
60
- });
61
- return server;
15
+ return { urlPrefix: '/console', directory: consoleDir, spaFallback: true };
62
16
  }
@@ -1,10 +1,10 @@
1
1
  export declare const serve: import("#pikku").PikkuFunctionConfig<{
2
2
  port?: string;
3
- console?: string | boolean;
3
+ console?: boolean;
4
4
  }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
5
5
  port?: string;
6
- console?: string | boolean;
6
+ console?: boolean;
7
7
  }, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{
8
8
  port?: string;
9
- console?: string | boolean;
9
+ console?: boolean;
10
10
  }, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
@@ -10,10 +10,11 @@ import { InMemorySchedulerService } from '@pikku/schedule';
10
10
  import { resolveDb, createKysely, parseDatabaseUrl, } from '../db/local-db.js';
11
11
  import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
12
12
  import { createDevAIAgentRunner } from './dev-ai-runner.js';
13
- import { startConsoleServer } from './serve-console.js';
13
+ import { resolveConsoleMount } from './serve-console.js';
14
14
  export const serve = pikkuSessionlessFunc({
15
15
  remote: true,
16
- func: async ({ logger, config, getInspectorState, variables, devServerRunner }, { port, console: consoleFlag }) => {
16
+ func: async ({ logger, config, getInspectorState, variables, devServerRunner }, { port, console: serveConsole }) => {
17
+ process.env.PIKKU_DEV_QUICK_LOGIN ??= 'true';
17
18
  const resolvedPort = parseInt(port || '3000', 10);
18
19
  const hostname = 'localhost';
19
20
  const bindHostname = '127.0.0.1';
@@ -101,7 +102,10 @@ export const serve = pikkuSessionlessFunc({
101
102
  ...inMemoryServices,
102
103
  getInspectorState,
103
104
  });
104
- const resolvedServices = { ...singletonServices, getInspectorState };
105
+ const resolvedServices = {
106
+ ...singletonServices,
107
+ getInspectorState,
108
+ };
105
109
  pikkuState(null, 'package', 'singletonServices', resolvedServices);
106
110
  resolvedServices.workflowService?.wireQueueWorkers?.();
107
111
  const { serverLifecycleFactory } = inspectorState.filesAndMethods;
@@ -111,27 +115,31 @@ export const serve = pikkuSessionlessFunc({
111
115
  const m = await loadUserModule(serverLifecycleFactory.file);
112
116
  return m[serverLifecycleFactory.variable];
113
117
  };
118
+ const consoleMount = serveConsole ? await resolveConsoleMount() : undefined;
119
+ if (serveConsole && !consoleMount) {
120
+ logger.warn('Console app not found. Please rebuild @pikku/cli with the console app bundled.');
121
+ }
114
122
  const pikkuServer = devServerRunner.createServer({
115
123
  ...userConfig,
116
124
  hostname: bindHostname,
117
125
  port: resolvedPort,
118
126
  content: localContentConfig,
127
+ ...(consoleMount ? { staticMounts: [consoleMount] } : {}),
119
128
  }, logger);
120
129
  const lifecycle = await loadLifecycle();
121
130
  await pikkuServer.init();
122
131
  await lifecycle?.beforeStart?.(resolvedServices);
123
132
  await pikkuServer.start();
124
133
  await lifecycle?.afterStart?.(resolvedServices);
125
- const consoleServer = consoleFlag
126
- ? await startConsoleServer(consoleFlag, hostname, logger)
127
- : undefined;
134
+ if (consoleMount) {
135
+ logger.info(`Pikku Console available at http://${hostname}:${resolvedPort}${consoleMount.urlPrefix}`);
136
+ }
128
137
  process.once('SIGINT', async () => {
129
138
  logger.info('Stopping server...');
130
139
  try {
131
140
  await lifecycle?.beforeStop?.(resolvedServices);
132
141
  await stopSingletonServices();
133
142
  await pikkuServer.stop();
134
- consoleServer?.close();
135
143
  await lifecycle?.afterStop?.(resolvedServices);
136
144
  }
137
145
  finally {
@@ -0,0 +1,13 @@
1
+ import { type CoverageService } from '@pikku/core/services';
2
+ interface CoverageLogger {
3
+ info(message: string): void;
4
+ warn(message: string): void;
5
+ }
6
+ /**
7
+ * Starts the coverage backend for `pikku dev --coverage`: V8 precise coverage
8
+ * on Node, istanbul load-time instrumentation on Bun. Must run before the
9
+ * user bootstrap is imported so the Bun loader plugin sees every module.
10
+ * Returns undefined (with a warning) when no backend is available.
11
+ */
12
+ export declare function startCoverageService(logger: CoverageLogger, rootDir: string): Promise<CoverageService | undefined>;
13
+ export {};
@@ -0,0 +1,26 @@
1
+ import { V8CoverageService, IstanbulCoverageService, } from '@pikku/core/services';
2
+ import { registerBunCoverageLoader } from './bun-coverage-loader.js';
3
+ const isBunRuntime = () => typeof globalThis.Bun !== 'undefined';
4
+ /**
5
+ * Starts the coverage backend for `pikku dev --coverage`: V8 precise coverage
6
+ * on Node, istanbul load-time instrumentation on Bun. Must run before the
7
+ * user bootstrap is imported so the Bun loader plugin sees every module.
8
+ * Returns undefined (with a warning) when no backend is available.
9
+ */
10
+ export async function startCoverageService(logger, rootDir) {
11
+ if (isBunRuntime()) {
12
+ await registerBunCoverageLoader({ rootDir });
13
+ logger.info('Istanbul coverage enabled (Bun) — snapshot via console:takeLiveCoverage, reset via console:resetLiveCoverage');
14
+ return new IstanbulCoverageService();
15
+ }
16
+ const coverageService = new V8CoverageService();
17
+ try {
18
+ await coverageService.start();
19
+ logger.info('V8 precise coverage enabled — snapshot via console:takeLiveCoverage, reset via console:resetLiveCoverage');
20
+ return coverageService;
21
+ }
22
+ catch (e) {
23
+ logger.warn(`V8 precise coverage is not supported on this runtime — continuing without coverage: ${e instanceof Error ? e.message : e}`);
24
+ return undefined;
25
+ }
26
+ }
@@ -3,7 +3,7 @@ import chokidar from 'chokidar';
3
3
  import { pikkuDevReloader } from '@pikku/core/dev';
4
4
  export const watch = pikkuSessionlessFunc({
5
5
  remote: true,
6
- func: async ({ logger, config }, { hmr }, { rpc }) => {
6
+ func: async ({ logger, config, invalidateInspectorState }, { hmr }, { rpc }) => {
7
7
  const watchDirectories = [
8
8
  ...new Set([config.emailTemplatesDir, ...config.srcDirectories].filter(Boolean)),
9
9
  ];
@@ -29,6 +29,7 @@ export const watch = pikkuSessionlessFunc({
29
29
  const handle = async () => {
30
30
  try {
31
31
  const start = Date.now();
32
+ invalidateInspectorState();
32
33
  await rpc.invoke('all');
33
34
  logger.info({
34
35
  message: `✓ Generated in ${Date.now() - start}ms`,
@@ -3,7 +3,7 @@ import { ErrorCode } from '@pikku/inspector';
3
3
  import { writeFileInDir } from '../../../utils/file-writer.js';
4
4
  import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
5
5
  import { serializeWorkflowTypes } from './serialize-workflow-types.js';
6
- import { serializeUserFlowActors } from './serialize-user-flow-actors.js';
6
+ import { serializeScenarioActors } from './serialize-scenario-actors.js';
7
7
  import { serializeWorkflowRegistration } from './serialize-workflow-registration.js';
8
8
  import { serializeWorkflowMap } from './serialize-workflow-map.js';
9
9
  import { serializeWorkflowBootstrapMap } from './serialize-workflow-bootstrap-map.js';
@@ -63,13 +63,14 @@ export const pikkuWorkflow = pikkuSessionlessFunc({
63
63
  const rpcMapImportPath = getFileImportRelativePath(workflowTypesFile, config.rpcInternalMapDeclarationFile, packageMappings);
64
64
  const workflowMapImportPath = getFileImportRelativePath(workflowTypesFile, workflowMapDeclarationFile, packageMappings);
65
65
  await writeFileInDir(logger, workflowTypesFile, serializeWorkflowTypes(functionTypesImportPath, rpcMapImportPath, workflowMapImportPath));
66
- const userFlowActors = config.userFlows?.actors;
67
- if (userFlowActors && Object.keys(userFlowActors).length > 0) {
68
- await writeFileInDir(logger, config.userFlowActorsFile, serializeUserFlowActors(userFlowActors));
66
+ const scenarioActors = config.scenarios?.actors;
67
+ if (scenarioActors && Object.keys(scenarioActors).length > 0) {
68
+ const agentMapImportPath = getFileImportRelativePath(config.scenarioActorsFile, config.agentMapDeclarationFile, packageMappings);
69
+ await writeFileInDir(logger, config.scenarioActorsFile, serializeScenarioActors(scenarioActors, agentMapImportPath));
69
70
  // JSON twin for the runtime meta service (console personas view). Lives
70
71
  // next to workflow/meta but NOT inside it — getWorkflowMeta() treats
71
72
  // every workflow/meta/*.gen.json as a workflow.
72
- await writeFileInDir(logger, join(dirname(config.userFlowActorsFile), 'user-flow-actors.gen.json'), JSON.stringify(userFlowActors, null, 2));
73
+ await writeFileInDir(logger, join(dirname(config.scenarioActorsFile), 'scenario-actors.gen.json'), JSON.stringify(scenarioActors, null, 2));
73
74
  }
74
75
  await writeFileInDir(logger, workflowMapDeclarationFile, bootstrap
75
76
  ? serializeWorkflowBootstrapMap(workflows.meta, workflows.graphMeta)
@@ -0,0 +1,2 @@
1
+ import type { PikkuCLIConfig } from '../../../../types/config.js';
2
+ export declare const serializeScenarioActors: (actors: NonNullable<NonNullable<PikkuCLIConfig["scenarios"]>["actors"]>, agentMapImportPath: string) => string;
@@ -0,0 +1,29 @@
1
+ export const serializeScenarioActors = (actors, agentMapImportPath) => {
2
+ return `/** Scenario actors declared in pikku.config.json (\`scenarios.actors\`) */
3
+ import {
4
+ createHttpScenarioActors,
5
+ type HttpScenarioActorsConfig,
6
+ type ScenarioActor,
7
+ type ScenarioActorConfig,
8
+ } from '@pikku/core/services'
9
+ import type { AgentMap } from '${agentMapImportPath}'
10
+
11
+ export const scenarioActorConfigs = ${JSON.stringify(actors, null, 2)} as const satisfies Record<string, ScenarioActorConfig>
12
+
13
+ export type ScenarioActorName = keyof typeof scenarioActorConfigs
14
+
15
+ export type AgentName = keyof AgentMap & string
16
+ export type TypedScenarioActors = Record<
17
+ ScenarioActorName,
18
+ ScenarioActor<AgentName>
19
+ >
20
+
21
+ export const createScenarioActors = (
22
+ options: Omit<HttpScenarioActorsConfig, 'actors'>
23
+ ): TypedScenarioActors =>
24
+ createHttpScenarioActors({
25
+ ...options,
26
+ actors: scenarioActorConfigs,
27
+ }) as TypedScenarioActors
28
+ `;
29
+ };