@pikku/cli 0.12.69 → 0.12.71

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 (115) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/{index-BY60UXB6.js → index-Bn7IPpsQ.js} +140 -141
  3. package/console-app/index.html +1 -1
  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 +6 -11
  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 +27 -31
  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 +48 -43
  25. package/dist/.pikku/function/pikku-functions.gen.js +3 -1
  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 -2
  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 +3 -5
  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/PikkuScenarioFunctionsOutput.schema.json +1 -0
  54. package/dist/.pikku/schemas/schemas/ScenarioRunInput.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/meta/allWorkflow.gen.json +8 -2
  66. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  68. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  69. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  70. package/dist/bin/pikku-bin.mjs +2 -2
  71. package/dist/src/cli.wiring.js +27 -29
  72. package/dist/src/fabric/fabric-commands.d.ts +3 -3
  73. package/dist/src/fabric/functions/llm-key.function.d.ts +4 -4
  74. package/dist/src/fabric/functions/validate.function.js +54 -15
  75. package/dist/src/functions/commands/all.js +14 -1
  76. package/dist/src/functions/commands/bun-coverage-loader.d.ts +11 -0
  77. package/dist/src/functions/commands/bun-coverage-loader.js +42 -0
  78. package/dist/src/functions/commands/dev.d.ts +6 -0
  79. package/dist/src/functions/commands/dev.js +19 -4
  80. package/dist/src/functions/commands/enable.d.ts +1 -0
  81. package/dist/src/functions/commands/enable.js +9 -1
  82. package/dist/src/functions/commands/scenario.d.ts +3 -0
  83. package/dist/src/functions/commands/scenario.js +52 -11
  84. package/dist/src/functions/commands/serve.js +4 -1
  85. package/dist/src/functions/commands/start-coverage.d.ts +13 -0
  86. package/dist/src/functions/commands/start-coverage.js +42 -0
  87. package/dist/src/functions/commands/watch.js +2 -1
  88. package/dist/src/functions/validate/workspace-validate.js +0 -4
  89. package/dist/src/functions/wirings/console/serialize-console-functions.js +0 -6
  90. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +1 -0
  91. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +25 -0
  92. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +6 -0
  93. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +98 -0
  94. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +4 -5
  95. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +0 -6
  96. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +1 -15
  97. package/dist/src/functions/workflows/all.workflow.js +6 -0
  98. package/dist/src/services.js +14 -1
  99. package/dist/src/utils/file-writer.d.ts +1 -0
  100. package/dist/src/utils/file-writer.js +9 -0
  101. package/dist/src/utils/meta-diff.d.ts +32 -0
  102. package/dist/src/utils/meta-diff.js +144 -0
  103. package/dist/src/utils/pikku-cli-config.js +3 -0
  104. package/dist/src/utils/precise-coverage-map.d.ts +4 -0
  105. package/dist/src/utils/precise-coverage-map.js +176 -0
  106. package/dist/src/utils/serialize-schemas.js +1 -1
  107. package/dist/src/utils/strip-verbose-meta.js +3 -0
  108. package/dist/tsconfig.tsbuildinfo +1 -1
  109. package/package.json +9 -3
  110. package/dist/.pikku/schemas/schemas/PikkuTestsCoverageInput.schema.json +0 -1
  111. package/dist/.pikku/schemas/schemas/PikkuTestsInitInput.schema.json +0 -1
  112. package/dist/src/functions/commands/tests-coverage.d.ts +0 -10
  113. package/dist/src/functions/commands/tests-coverage.js +0 -273
  114. package/dist/src/functions/commands/tests-init.d.ts +0 -7
  115. package/dist/src/functions/commands/tests-init.js +0 -274
@@ -78,8 +78,17 @@ async function listSourceFiles(dir) {
78
78
  }
79
79
  // Heavy/generated dirs pruned during a source walk.
80
80
  const SKIP_WALK_DIRS = new Set([
81
- 'node_modules', '.git', '.next', 'dist', 'build', '.pikku',
82
- '.pikku-runtime', '.vite', '.tanstack', '.turbo', '.reports',
81
+ 'node_modules',
82
+ '.git',
83
+ '.next',
84
+ 'dist',
85
+ 'build',
86
+ '.pikku',
87
+ '.pikku-runtime',
88
+ '.vite',
89
+ '.tanstack',
90
+ '.turbo',
91
+ '.reports',
83
92
  ]);
84
93
  // Recursively list handwritten .ts/.tsx files under a dir, pruning heavy dirs
85
94
  // (node_modules, build output, generated) DURING traversal and skipping .gen.*
@@ -430,14 +439,48 @@ export async function runValidate(startDir = process.cwd()) {
430
439
  // need to bun install?" — aborting the deploy. Catch that class here.
431
440
  {
432
441
  const NODE_BUILTINS = new Set([
433
- 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
434
- 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http',
435
- 'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks',
436
- 'process', 'punycode', 'querystring', 'readline', 'repl', 'stream',
437
- 'string_decoder', 'timers', 'tls', 'tty', 'url', 'util', 'v8', 'vm',
438
- 'worker_threads', 'zlib',
442
+ 'assert',
443
+ 'async_hooks',
444
+ 'buffer',
445
+ 'child_process',
446
+ 'cluster',
447
+ 'console',
448
+ 'constants',
449
+ 'crypto',
450
+ 'dgram',
451
+ 'dns',
452
+ 'domain',
453
+ 'events',
454
+ 'fs',
455
+ 'http',
456
+ 'http2',
457
+ 'https',
458
+ 'inspector',
459
+ 'module',
460
+ 'net',
461
+ 'os',
462
+ 'path',
463
+ 'perf_hooks',
464
+ 'process',
465
+ 'punycode',
466
+ 'querystring',
467
+ 'readline',
468
+ 'repl',
469
+ 'stream',
470
+ 'string_decoder',
471
+ 'timers',
472
+ 'tls',
473
+ 'tty',
474
+ 'url',
475
+ 'util',
476
+ 'v8',
477
+ 'vm',
478
+ 'worker_threads',
479
+ 'zlib',
439
480
  ]);
440
- const pkgNameOf = (spec) => spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
481
+ const pkgNameOf = (spec) => spec.startsWith('@')
482
+ ? spec.split('/').slice(0, 2).join('/')
483
+ : spec.split('/')[0];
441
484
  const wsDirs = [];
442
485
  for (const group of ['packages', 'apps', 'backends']) {
443
486
  const groupDir = join(root, group);
@@ -445,7 +488,8 @@ export async function runValidate(startDir = process.cwd()) {
445
488
  continue;
446
489
  try {
447
490
  for (const d of await readdir(groupDir, { withFileTypes: true })) {
448
- if (d.isDirectory() && existsSync(join(groupDir, d.name, 'package.json'))) {
491
+ if (d.isDirectory() &&
492
+ existsSync(join(groupDir, d.name, 'package.json'))) {
449
493
  wsDirs.push(join(groupDir, d.name));
450
494
  }
451
495
  }
@@ -1020,11 +1064,6 @@ export async function runValidate(startDir = process.cwd()) {
1020
1064
  if (!existsSync(join(root, 'packages', 'components'))) {
1021
1065
  info('components-missing', 'packages/components/ not found — Fabric design features require a components package', join(root, 'packages', 'components'), `Create packages/components/ with your shared UI components. See ${designDocUrl}`);
1022
1066
  }
1023
- // ── packages/functions/tests/ ─────────────────────────────────────────
1024
- const testsDir = join(fnDir, 'tests');
1025
- if (!existsSync(testsDir)) {
1026
- info('tests-missing', 'packages/functions/tests/ not found — no function test harness', testsDir, lines('Run `pikku tests init` from the repo root.', 'That should scaffold `packages/functions/tests/` with the generated function test harness.', 'Expected contents include feature files, step definitions, and support files.', 'Commit the scaffolded files after generation.'));
1027
- }
1028
1067
  // ── packages/functions-sdk/ ───────────────────────────────────────────
1029
1068
  const sdkDir = join(root, 'packages', 'functions-sdk');
1030
1069
  if (!existsSync(sdkDir)) {
@@ -1,12 +1,25 @@
1
1
  import { pikkuVoidFunc } from '#pikku';
2
+ import { computeMetaDiff, readMetaSnapshot } from '../../utils/meta-diff.js';
2
3
  export const all = pikkuVoidFunc({
3
4
  remote: true,
4
- func: async ({ workflowService, logger }, _data, { rpc }) => {
5
+ func: async ({ workflowService, logger, config }, _data, { rpc }) => {
6
+ // --diff: snapshot the meta BEFORE codegen overwrites it, so we can report
7
+ // exactly what this run changes. Cheap (a few small JSON reads), skipped
8
+ // unless requested.
9
+ const beforeMeta = config.diff ? readMetaSnapshot(config.outDir) : null;
5
10
  // Drive the run directly (the CLI is always inline — no queueService) so we
6
11
  // can read back the per-step durations already recorded on each step's
7
12
  // state. startWorkflow runs the job to completion before returning in inline
8
13
  // mode and re-throws on failure, matching runToCompletion's behaviour.
9
14
  const { runId } = await workflowService.startWorkflow('allWorkflow', {}, { type: 'internal' }, rpc, { inline: true });
15
+ // Reached only on success (startWorkflow re-throws → non-zero exit above),
16
+ // so the diff never emits on a failed codegen. Print it as a single marker
17
+ // line on stdout for the sandbox "what changed" build card to consume.
18
+ if (beforeMeta) {
19
+ const diff = computeMetaDiff(beforeMeta, readMetaSnapshot(config.outDir));
20
+ // eslint-disable-next-line no-console
21
+ console.log(`PIKKU_DIFF ${JSON.stringify(diff)}`);
22
+ }
10
23
  // Per-step timing breakdown for debugging slow runs (e.g. which inspector
11
24
  // re-runs dominate). Opt-in via PIKKU_TIMING so normal runs stay quiet.
12
25
  if (typeof process !== 'undefined' && process.env?.PIKKU_TIMING) {
@@ -0,0 +1,11 @@
1
+ export interface BunCoverageLoaderOptions {
2
+ rootDir: string;
3
+ }
4
+ /**
5
+ * Registers a Bun loader plugin that runs istanbul-lib-instrument over the
6
+ * project's TypeScript sources so `__coverage__` counters exist at runtime.
7
+ * Bun has no V8 precise coverage (`node:inspector` throws "Coverage APIs are
8
+ * not supported"), so instrumentation at load time is the coverage backend
9
+ * there. Must be called before the user bootstrap is imported.
10
+ */
11
+ export declare function registerBunCoverageLoader({ rootDir, }: BunCoverageLoaderOptions): Promise<void>;
@@ -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,12 +2,18 @@ export declare const dev: import("#pikku").PikkuFunctionConfig<{
2
2
  port?: string;
3
3
  watch?: boolean;
4
4
  hmr?: boolean;
5
+ coverage?: boolean;
6
+ test?: boolean;
5
7
  }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
6
8
  port?: string;
7
9
  watch?: boolean;
8
10
  hmr?: boolean;
11
+ coverage?: boolean;
12
+ test?: boolean;
9
13
  }, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{
10
14
  port?: string;
11
15
  watch?: boolean;
12
16
  hmr?: boolean;
17
+ coverage?: boolean;
18
+ test?: boolean;
13
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,12 +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
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 }, { rpc }) => {
19
+ func: async ({ logger, config, getInspectorState, invalidateInspectorState, variables, devServerRunner, }, { port, watch, hmr, coverage, test }, { rpc }) => {
19
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;
20
27
  const resolvedPort = parseInt(port || '3000', 10);
21
28
  const hostname = 'localhost';
22
29
  // Bind on IPv4 loopback explicitly. Under Bun, hostname 'localhost' resolves
@@ -178,8 +185,11 @@ export const dev = pikkuSessionlessFunc({
178
185
  const inMemoryServices = {
179
186
  logger: devLogger,
180
187
  ...(aiAgentRunner ? { aiAgentRunner } : {}),
181
- emailService: new LocalEmailService(),
188
+ emailService: test
189
+ ? spy('emailService', new LocalEmailService())
190
+ : new LocalEmailService(),
182
191
  metaService: new LocalMetaService(pikkuDir),
192
+ ...(coverageService ? { coverageService } : {}),
183
193
  schedulerService,
184
194
  queueService: new InMemoryQueueService(),
185
195
  workflowService,
@@ -196,7 +206,11 @@ export const dev = pikkuSessionlessFunc({
196
206
  ...inMemoryServices,
197
207
  getInspectorState,
198
208
  });
199
- const resolvedServices = { ...singletonServices, getInspectorState };
209
+ const resolvedServices = {
210
+ ...singletonServices,
211
+ getInspectorState,
212
+ ...(coverageService ? { coverageService } : {}),
213
+ };
200
214
  pikkuState(null, 'package', 'singletonServices', resolvedServices);
201
215
  resolvedServices.workflowService?.wireQueueWorkers?.();
202
216
  const { serverLifecycleFactory } = inspectorState.filesAndMethods;
@@ -261,6 +275,7 @@ export const dev = pikkuSessionlessFunc({
261
275
  const handle = async () => {
262
276
  try {
263
277
  const start = Date.now();
278
+ invalidateInspectorState();
264
279
  await runAllWithCommandState();
265
280
  workflowService.wireQueueWorkers();
266
281
  logger.info({
@@ -1,5 +1,6 @@
1
1
  export declare const enableRpc: import("#pikku").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<void, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
2
2
  export declare const enableConsole: import("#pikku").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<void, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
3
+ export declare const enableScenarios: import("#pikku").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<void, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
3
4
  export declare const enableAgent: import("#pikku").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<void, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
4
5
  export declare const enableWorkflow: import("#pikku").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<void, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
5
6
  export declare const enableEvents: import("#pikku").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<void, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
@@ -5,6 +5,7 @@ const FEATURE_DEFAULTS = {
5
5
  rpc: 'auth',
6
6
  agent: 'auth',
7
7
  console: 'auth',
8
+ scenarios: 'auth',
8
9
  workflow: 'auth',
9
10
  events: 'auth',
10
11
  remoteRpc: 'no-auth',
@@ -22,7 +23,11 @@ async function enableFeature(feature, logger, config, data) {
22
23
  }
23
24
  // The console is an admin surface — every RPC requires a session, so it can
24
25
  // never be scaffolded no-auth (the --no-auth flag is ignored for it).
25
- const value = feature === 'console' ? 'auth' : noAuth ? 'no-auth' : FEATURE_DEFAULTS[feature];
26
+ const value = feature === 'console'
27
+ ? 'auth'
28
+ : noAuth
29
+ ? 'no-auth'
30
+ : FEATURE_DEFAULTS[feature];
26
31
  json.scaffold[feature] = value;
27
32
  await writeFile(configPath, JSON.stringify(json, null, 2) + '\n', 'utf-8');
28
33
  logger.info(`Enabled scaffold.${feature} = '${value}' in ${configPath}`);
@@ -33,6 +38,9 @@ export const enableRpc = pikkuVoidFunc({
33
38
  export const enableConsole = pikkuVoidFunc({
34
39
  func: async ({ logger, config }, data) => enableFeature('console', logger, config, data),
35
40
  });
41
+ export const enableScenarios = pikkuVoidFunc({
42
+ func: async ({ logger, config }, data) => enableFeature('scenarios', logger, config, data),
43
+ });
36
44
  export const enableAgent = pikkuVoidFunc({
37
45
  func: async ({ logger, config }, data) => enableFeature('agent', logger, config, data),
38
46
  });
@@ -3,12 +3,15 @@ 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,4 +1,5 @@
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
4
  import { InMemoryWorkflowService, createHttpScenarioActors, } from '@pikku/core/services';
4
5
  import { pikkuState } from '@pikku/core/internal';
@@ -30,9 +31,8 @@ export const scenarioList = pikkuSessionlessFunc({
30
31
  },
31
32
  });
32
33
  export const scenarioRun = pikkuSessionlessFunc({
33
- func: async ({ logger, config, getInspectorState, variables }, { environment, flows, tags }) => {
34
+ func: async ({ logger, config, getInspectorState, variables }, { environment, flows, tags, coverage }) => {
34
35
  const state = await getInspectorState(true);
35
- // --- resolve the target environment ---
36
36
  const environments = config.scenarios?.environments ?? {};
37
37
  const env = environments[environment];
38
38
  if (!env) {
@@ -42,7 +42,6 @@ export const scenarioRun = pikkuSessionlessFunc({
42
42
  ? `Configured environments: ${known.join(', ')}`
43
43
  : `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
44
44
  }
45
- // --- select flows ---
46
45
  let selected = listScenarios(state);
47
46
  if (flows) {
48
47
  const names = new Set(flows.split(',').map((f) => f.trim()));
@@ -61,7 +60,6 @@ export const scenarioRun = pikkuSessionlessFunc({
61
60
  process.exitCode = 1;
62
61
  return;
63
62
  }
64
- // --- actors: registry from config, secret STRICTLY from env ---
65
63
  const secret = await variables.get('SCENARIO_ACTOR_SECRET');
66
64
  if (!secret) {
67
65
  throw new Error('SCENARIO_ACTOR_SECRET is not set — scenario actors cannot sign in. ' +
@@ -74,10 +72,6 @@ export const scenarioRun = pikkuSessionlessFunc({
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', {
@@ -92,10 +86,39 @@ export const scenarioRun = pikkuSessionlessFunc({
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 "scaffold.scenarios" enabled in pikku.config.json?`);
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('pikkuScenarioResetLiveCoverage');
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('pikkuScenarioResetStubs', 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 scenarioRun = pikkuSessionlessFunc({
124
147
  error: e?.message ?? String(e),
125
148
  });
126
149
  }
150
+ if (coverageActive) {
151
+ const report = await invokeCoverage('pikkuScenarioTakeLiveCoverage');
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') {
@@ -102,7 +102,10 @@ export const serve = pikkuSessionlessFunc({
102
102
  ...inMemoryServices,
103
103
  getInspectorState,
104
104
  });
105
- const resolvedServices = { ...singletonServices, getInspectorState };
105
+ const resolvedServices = {
106
+ ...singletonServices,
107
+ getInspectorState,
108
+ };
106
109
  pikkuState(null, 'package', 'singletonServices', resolvedServices);
107
110
  resolvedServices.workflowService?.wireQueueWorkers?.();
108
111
  const { serverLifecycleFactory } = inspectorState.filesAndMethods;
@@ -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,42 @@
1
+ import { V8CoverageService } from '@pikku/core/services/v8-coverage';
2
+ import { IstanbulCoverageService } from '@pikku/core/services/istanbul-coverage';
3
+ import { registerBunCoverageLoader } from './bun-coverage-loader.js';
4
+ import { mapPreciseCoverage, mapLineHitsToReport, } from '../../utils/precise-coverage-map.js';
5
+ const isBunRuntime = () => typeof globalThis.Bun !== 'undefined';
6
+ // The source-map-aware span mapping (and its @jridgewell/trace-mapping
7
+ // dependency) lives in the CLI, not @pikku/core — the scaffolded
8
+ // pikkuScenarioTakeLiveCoverage function reaches it through the optional
9
+ // CoverageService.takeReport, which only the CLI-booted service provides.
10
+ const withTakeReport = (service) => {
11
+ service.takeReport = async (functionsMeta) => {
12
+ const snapshot = await service.takeCoverage();
13
+ if (snapshot.kind === 'line-hits') {
14
+ return mapLineHitsToReport(snapshot.lineHits, functionsMeta);
15
+ }
16
+ return mapPreciseCoverage(snapshot.scripts, snapshot.getScriptSource, functionsMeta);
17
+ };
18
+ return service;
19
+ };
20
+ /**
21
+ * Starts the coverage backend for `pikku dev --coverage`: V8 precise coverage
22
+ * on Node, istanbul load-time instrumentation on Bun. Must run before the
23
+ * user bootstrap is imported so the Bun loader plugin sees every module.
24
+ * Returns undefined (with a warning) when no backend is available.
25
+ */
26
+ export async function startCoverageService(logger, rootDir) {
27
+ if (isBunRuntime()) {
28
+ await registerBunCoverageLoader({ rootDir });
29
+ logger.info('Istanbul coverage enabled (Bun) — snapshot via pikkuScenarioTakeLiveCoverage, reset via pikkuScenarioResetLiveCoverage');
30
+ return withTakeReport(new IstanbulCoverageService());
31
+ }
32
+ const coverageService = new V8CoverageService();
33
+ try {
34
+ await coverageService.start();
35
+ logger.info('V8 precise coverage enabled — snapshot via pikkuScenarioTakeLiveCoverage, reset via pikkuScenarioResetLiveCoverage');
36
+ return withTakeReport(coverageService);
37
+ }
38
+ catch (e) {
39
+ logger.warn(`V8 precise coverage is not supported on this runtime — continuing without coverage: ${e instanceof Error ? e.message : e}`);
40
+ return undefined;
41
+ }
42
+ }
@@ -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`,
@@ -228,10 +228,6 @@ export async function runWorkspaceValidate(startDir = process.cwd()) {
228
228
  info('config-missing', 'packages/functions/src/config.ts not found', join(fnDir, 'src', 'config.ts'), 'Create src/config.ts and export your workspace config factory');
229
229
  }
230
230
  }
231
- const testsDir = join(fnDir, 'tests');
232
- if (!existsSync(testsDir)) {
233
- info('tests-missing', 'packages/functions/tests/ not found — no function test harness', testsDir, 'Run `pikku tests init` to scaffold the generated function test harness under packages/functions/tests/ (feature files, step defs, and support files)');
234
- }
235
231
  const sdkDir = join(root, 'packages', 'functions-sdk');
236
232
  if (!existsSync(sdkDir)) {
237
233
  info('functions-sdk-missing', 'packages/functions-sdk/ not found — generated RPC client and React Query hooks will not be available', sdkDir, 'Create packages/functions-sdk/ as a workspace with src/pikku/ as the generated output root; point clientFiles.rpcMapDeclarationFile and clientFiles.reactQueryFile there');
@@ -94,12 +94,6 @@ export const consoleRoutes = defineHTTPRoutes({
94
94
  sse: true,
95
95
  func: ref('console:streamWorkflowRun'),
96
96
  },
97
- functionTestsStream: {
98
- route: '/function-tests/stream',
99
- method: 'get',
100
- sse: true,
101
- func: ref('console:streamFunctionTests'),
102
- },
103
97
  },
104
98
  })
105
99
 
@@ -0,0 +1 @@
1
+ export declare const pikkuScenarioFunctions: import("#pikku").PikkuFunctionConfig<void, boolean, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, boolean, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<void, boolean, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
@@ -0,0 +1,25 @@
1
+ import { pikkuSessionlessFunc } from '#pikku';
2
+ import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
3
+ import { writeFileInDir } from '../../../utils/file-writer.js';
4
+ import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
5
+ import { serializeScenarioFunctions } from './serialize-scenario-functions.js';
6
+ export const pikkuScenarioFunctions = pikkuSessionlessFunc({
7
+ func: async ({ logger, config, variables }) => {
8
+ const deployCodegenFlag = await variables.get('PIKKU_DEPLOY_CODEGEN');
9
+ if (deployCodegenFlag === '1') {
10
+ return false;
11
+ }
12
+ if (config.scaffold?.scenarios && config.scenariosFunctionsFile) {
13
+ const pathToPikkuTypes = getFileImportRelativePath(config.scenariosFunctionsFile, config.typesDeclarationFile, config.packageMappings);
14
+ await writeFileInDir(logger, config.scenariosFunctionsFile, serializeScenarioFunctions(pathToPikkuTypes, config.scaffold.scenarios === 'auth'));
15
+ return true;
16
+ }
17
+ return false;
18
+ },
19
+ middleware: [
20
+ logCommandInfoAndTime({
21
+ commandStart: 'Generating Scenario functions',
22
+ commandEnd: 'Generated Scenario functions',
23
+ }),
24
+ ],
25
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generate the scenario instrumentation functions (`pikku scenario` coverage
3
+ * snapshots and stub-call inspection) into the project scaffold, so scenario
4
+ * runs work against any server without requiring the console addon.
5
+ */
6
+ export declare const serializeScenarioFunctions: (pathToPikkuTypes: string, requireAuth?: boolean) => string;