@pikku/cli 0.12.67 → 0.12.69

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 (97) 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 +16 -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 +38 -53
  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 +1 -1
  38. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  39. package/dist/.pikku/pikku-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  45. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +2 -3
  47. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  48. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  49. package/dist/.pikku/schemas/register.gen.js +5 -7
  50. package/dist/.pikku/schemas/schemas/DevInput.schema.json +1 -1
  51. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  52. package/dist/.pikku/schemas/schemas/ServeInput.schema.json +1 -1
  53. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  54. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  55. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  56. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  57. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  58. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  59. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  60. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  61. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  62. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  63. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +6 -1
  64. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +4 -4
  65. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +2 -2
  66. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  68. package/dist/bin/pikku-bin.mjs +2 -2
  69. package/dist/bin/pikku.js +1 -1
  70. package/dist/src/cli.wiring.js +16 -31
  71. package/dist/src/functions/commands/dev.d.ts +0 -3
  72. package/dist/src/functions/commands/dev.js +8 -6
  73. package/dist/src/functions/commands/{user-flow.d.ts → scenario.d.ts} +2 -2
  74. package/dist/src/functions/commands/{user-flow.js → scenario.js} +21 -21
  75. package/dist/src/functions/commands/serve-console.d.ts +2 -3
  76. package/dist/src/functions/commands/serve-console.js +5 -51
  77. package/dist/src/functions/commands/serve.d.ts +3 -3
  78. package/dist/src/functions/commands/serve.js +11 -6
  79. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +6 -5
  80. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +8 -0
  81. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +43 -0
  82. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +4 -4
  83. package/dist/src/functions/workflows/all.workflow.js +17 -0
  84. package/dist/src/utils/pikku-cli-config.js +2 -2
  85. package/dist/src/utils/tsc-check.d.ts +46 -0
  86. package/dist/src/utils/tsc-check.js +141 -0
  87. package/dist/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +6 -6
  89. package/skills/pikku-i18n/SKILL.md +58 -185
  90. package/skills/pikku-kysely/SKILL.md +68 -2
  91. package/dist/.pikku/schemas/schemas/ConsoleCommandInput.schema.json +0 -1
  92. package/dist/src/functions/commands/console.d.ts +0 -13
  93. package/dist/src/functions/commands/console.js +0 -122
  94. package/dist/src/functions/wirings/workflow/serialize-user-flow-actors.d.ts +0 -8
  95. package/dist/src/functions/wirings/workflow/serialize-user-flow-actors.js +0 -36
  96. /package/dist/.pikku/schemas/schemas/{UserFlowListInput.schema.json → ScenarioListInput.schema.json} +0 -0
  97. /package/dist/.pikku/schemas/schemas/{UserFlowRunInput.schema.json → ScenarioRunInput.schema.json} +0 -0
@@ -1,23 +1,23 @@
1
1
  import { resolve } from 'path';
2
2
  import { pikkuSessionlessFunc } from '#pikku';
3
- import { InMemoryWorkflowService, createHttpUserFlowActors, } from '@pikku/core/services';
3
+ import { InMemoryWorkflowService, createHttpScenarioActors, } from '@pikku/core/services';
4
4
  import { pikkuState } from '@pikku/core/internal';
5
5
  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))
6
+ const isScenario = (wf) => wf?.scenario === true;
7
+ const listScenarios = (state) => Object.entries(state.workflows?.meta ?? {})
8
+ .filter(([, wf]) => isScenario(wf))
9
9
  .map(([id, wf]) => ({
10
10
  id,
11
11
  name: wf.name ?? id,
12
12
  description: wf.description ?? wf.summary ?? wf.title ?? null,
13
13
  tags: wf.tags ?? [],
14
14
  }));
15
- export const userFlowList = pikkuSessionlessFunc({
15
+ export const scenarioList = pikkuSessionlessFunc({
16
16
  func: async ({ logger, getInspectorState }) => {
17
17
  const state = await getInspectorState();
18
- const flows = listUserFlows(state);
18
+ const flows = listScenarios(state);
19
19
  if (flows.length === 0) {
20
- logger.info('No user flows found (pikkuUserFlow exports).');
20
+ logger.info('No scenarios found (pikkuScenario exports).');
21
21
  return;
22
22
  }
23
23
  for (const flow of flows) {
@@ -29,26 +29,26 @@ export const userFlowList = pikkuSessionlessFunc({
29
29
  }
30
30
  },
31
31
  });
32
- export const userFlowRun = pikkuSessionlessFunc({
32
+ export const scenarioRun = pikkuSessionlessFunc({
33
33
  func: async ({ logger, config, getInspectorState, variables }, { environment, flows, tags }) => {
34
34
  const state = await getInspectorState(true);
35
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
45
  // --- select flows ---
46
- let selected = listUserFlows(state);
46
+ let selected = listScenarios(state);
47
47
  if (flows) {
48
48
  const names = new Set(flows.split(',').map((f) => f.trim()));
49
49
  const unknown = [...names].filter((n) => !selected.some((f) => f.name === n));
50
50
  if (unknown.length) {
51
- throw new Error(`Unknown user flow(s): ${unknown.join(', ')}. Available: ${selected.map((f) => f.name).join(', ')}`);
51
+ throw new Error(`Unknown scenario(s): ${unknown.join(', ')}. Available: ${selected.map((f) => f.name).join(', ')}`);
52
52
  }
53
53
  selected = selected.filter((f) => names.has(f.name));
54
54
  }
@@ -57,20 +57,20 @@ export const userFlowRun = pikkuSessionlessFunc({
57
57
  selected = selected.filter((f) => wanted.some((t) => f.tags.includes(t)));
58
58
  }
59
59
  if (selected.length === 0) {
60
- logger.error('No user flows matched.');
60
+ logger.error('No scenarios matched.');
61
61
  process.exitCode = 1;
62
62
  return;
63
63
  }
64
64
  // --- actors: registry from config, secret STRICTLY from env ---
65
- const secret = await variables.get('USER_FLOW_ACTOR_SECRET');
65
+ const secret = await variables.get('SCENARIO_ACTOR_SECRET');
66
66
  if (!secret) {
67
- throw new Error('USER_FLOW_ACTOR_SECRET is not set — user-flow actors cannot sign in. ' +
67
+ throw new Error('SCENARIO_ACTOR_SECRET is not set — scenario actors cannot sign in. ' +
68
68
  'Export it in the environment running this command (never put it in pikku.config.json).');
69
69
  }
70
- const actors = createHttpUserFlowActors({
70
+ const actors = createHttpScenarioActors({
71
71
  apiUrl: env.apiUrl,
72
72
  secret,
73
- actors: config.userFlows?.actors ?? {},
73
+ actors: config.scenarios?.actors ?? {},
74
74
  signInPath: env.signInPath,
75
75
  rpcPath: env.rpcPath,
76
76
  });
@@ -87,8 +87,8 @@ export const userFlowRun = pikkuSessionlessFunc({
87
87
  });
88
88
  const guardRpc = {
89
89
  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 ` +
90
+ throw new Error(`Scenario tried to run '${rpcName}' as an internal step. Every workflow.do ` +
91
+ `in a scenario must carry { actor: actors.x } so it executes against ` +
92
92
  `'${environment}' (${env.apiUrl}), not local services.`);
93
93
  },
94
94
  };
@@ -135,7 +135,7 @@ export const userFlowRun = pikkuSessionlessFunc({
135
135
  logger.error(`FAIL ${r.name} (${r.durationMs}ms): ${r.error}`);
136
136
  }
137
137
  }
138
- logger.info(`${results.length - failed.length}/${results.length} user flows passed against '${environment}'`);
138
+ logger.info(`${results.length - failed.length}/${results.length} scenarios passed against '${environment}'`);
139
139
  if (failed.length > 0) {
140
140
  process.exitCode = 1;
141
141
  }
@@ -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';
@@ -111,27 +112,31 @@ export const serve = pikkuSessionlessFunc({
111
112
  const m = await loadUserModule(serverLifecycleFactory.file);
112
113
  return m[serverLifecycleFactory.variable];
113
114
  };
115
+ const consoleMount = serveConsole ? await resolveConsoleMount() : undefined;
116
+ if (serveConsole && !consoleMount) {
117
+ logger.warn('Console app not found. Please rebuild @pikku/cli with the console app bundled.');
118
+ }
114
119
  const pikkuServer = devServerRunner.createServer({
115
120
  ...userConfig,
116
121
  hostname: bindHostname,
117
122
  port: resolvedPort,
118
123
  content: localContentConfig,
124
+ ...(consoleMount ? { staticMounts: [consoleMount] } : {}),
119
125
  }, logger);
120
126
  const lifecycle = await loadLifecycle();
121
127
  await pikkuServer.init();
122
128
  await lifecycle?.beforeStart?.(resolvedServices);
123
129
  await pikkuServer.start();
124
130
  await lifecycle?.afterStart?.(resolvedServices);
125
- const consoleServer = consoleFlag
126
- ? await startConsoleServer(consoleFlag, hostname, logger)
127
- : undefined;
131
+ if (consoleMount) {
132
+ logger.info(`Pikku Console available at http://${hostname}:${resolvedPort}${consoleMount.urlPrefix}`);
133
+ }
128
134
  process.once('SIGINT', async () => {
129
135
  logger.info('Stopping server...');
130
136
  try {
131
137
  await lifecycle?.beforeStop?.(resolvedServices);
132
138
  await stopSingletonServices();
133
139
  await pikkuServer.stop();
134
- consoleServer?.close();
135
140
  await lifecycle?.afterStop?.(resolvedServices);
136
141
  }
137
142
  finally {
@@ -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,8 @@
1
+ import type { PikkuCLIConfig } from '../../../../types/config.js';
2
+ /**
3
+ * Generate the typed scenario actor registry from pikku.config.json's
4
+ * `scenarios.actors`. Apps wire the result as the `actors` singleton service:
5
+ *
6
+ * actors: createScenarioActors({ apiUrl, secret })
7
+ */
8
+ export declare const serializeScenarioActors: (actors: NonNullable<NonNullable<PikkuCLIConfig["scenarios"]>["actors"]>, agentMapImportPath: string) => string;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Generate the typed scenario actor registry from pikku.config.json's
3
+ * `scenarios.actors`. Apps wire the result as the `actors` singleton service:
4
+ *
5
+ * actors: createScenarioActors({ apiUrl, secret })
6
+ */
7
+ export const serializeScenarioActors = (actors, agentMapImportPath) => {
8
+ return `/**
9
+ * Scenario actors declared in pikku.config.json (\`scenarios.actors\`).
10
+ * Any scenario can impersonate any actor.
11
+ */
12
+ import {
13
+ createHttpScenarioActors,
14
+ type HttpScenarioActorsConfig,
15
+ type ScenarioActor,
16
+ type ScenarioActorConfig,
17
+ } from '@pikku/core/services'
18
+ import type { AgentMap } from '${agentMapImportPath}'
19
+
20
+ export const scenarioActorConfigs = ${JSON.stringify(actors, null, 2)} as const satisfies Record<string, ScenarioActorConfig>
21
+
22
+ export type ScenarioActorName = keyof typeof scenarioActorConfigs
23
+
24
+ /** \`actor.converse({ agent })\` is constrained to the project's agent names. */
25
+ export type AgentName = keyof AgentMap & string
26
+ export type TypedScenarioActors = Record<
27
+ ScenarioActorName,
28
+ ScenarioActor<AgentName>
29
+ >
30
+
31
+ /**
32
+ * Build the injected \`actors\` service: one lazy HTTP actor per registry
33
+ * entry, signing in via the Better Auth actor plugin on first invoke.
34
+ */
35
+ export const createScenarioActors = (
36
+ options: Omit<HttpScenarioActorsConfig, 'actors'>
37
+ ): TypedScenarioActors =>
38
+ createHttpScenarioActors({
39
+ ...options,
40
+ actors: scenarioActorConfigs,
41
+ }) as TypedScenarioActors
42
+ `;
43
+ };
@@ -108,23 +108,23 @@ export function pikkuWorkflowComplexFunc(func: any) {
108
108
  }
109
109
 
110
110
  /**
111
- * A user flow: a complex workflow that drives the app the way users do.
111
+ * A scenario: a complex workflow that drives the app the way users do.
112
112
  * Steps run as actors over the REAL transport — \`workflow.do(step, rpc,
113
113
  * data, { actor: actors.yasser })\` — so flows double as e2e tests and
114
114
  * staged/production health checks (no state reset; scope what you create).
115
115
  */
116
- export function pikkuUserFlow<
116
+ export function pikkuScenario<
117
117
  InputSchema extends StandardSchemaV1 | undefined = undefined,
118
118
  OutputSchema extends StandardSchemaV1 | undefined = undefined
119
119
  >(
120
120
  config: PikkuWorkflowConfigWithSchema<InputSchema, OutputSchema>
121
121
  ): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'workflow', PikkuFunctionWorkflow<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown>, InputSchema, OutputSchema>
122
- export function pikkuUserFlow<In, Out = unknown>(
122
+ export function pikkuScenario<In, Out = unknown>(
123
123
  func:
124
124
  | PikkuFunctionWorkflow<In, Out>
125
125
  | PikkuFunctionConfig<In, Out, 'workflow', PikkuFunctionWorkflow<In, Out>>
126
126
  ): PikkuFunctionConfig<In, Out, 'workflow'>
127
- export function pikkuUserFlow(func: any) {
127
+ export function pikkuScenario(func: any) {
128
128
  return typeof func === 'function' ? { func } : func
129
129
  }
130
130
 
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { pikkuWorkflowComplexFunc } from '#pikku/workflow/pikku-workflow-types.gen.js';
3
3
  import { assertSingleCoreVersion } from '../../utils/assert-single-core-version.js';
4
+ import { PikkuTypecheckFailedError, renderTscFull, renderTscSummary, runProjectTypecheck, } from '../../utils/tsc-check.js';
4
5
  const scaffoldFiles = (config) => {
5
6
  const files = [];
6
7
  if (config.scaffold?.rpc && config.publicRpcFile)
@@ -272,5 +273,21 @@ export const allWorkflow = pikkuWorkflowComplexFunc({
272
273
  await workflow.do('Versions update', 'pikkuVersionsUpdate', null);
273
274
  await workflow.do('Bootstrap', 'pikkuBootstrap', { allImports });
274
275
  await workflow.do('Summary', 'pikkuSummary', null);
276
+ // Opt-in type-check gate (--tsc / --tsc-summary). Runs last, over the
277
+ // post-codegen project, so generated .pikku files are included — matching a
278
+ // real build. Printed inside the step; the throw lives in the workflow body
279
+ // (like assertSingleCoreVersion) so it reliably fails the run.
280
+ if (config.tsc || config.tscSummary) {
281
+ const errorCount = await workflow.do('Type check', async () => {
282
+ const { result, diagnostics, formatHost } = runProjectTypecheck(config.tsconfig, config.rootDir);
283
+ logger.info(config.tsc
284
+ ? renderTscFull(diagnostics, config.rootDir, formatHost)
285
+ : renderTscSummary(result));
286
+ return result.errorCount;
287
+ });
288
+ if (errorCount > 0) {
289
+ throw new PikkuTypecheckFailedError(`Type check failed: ${errorCount} error${errorCount === 1 ? '' : 's'}`);
290
+ }
291
+ }
275
292
  },
276
293
  });
@@ -306,8 +306,8 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
306
306
  if (!result.workflowMetaDir) {
307
307
  result.workflowMetaDir = join(workflowDir, 'meta');
308
308
  }
309
- if (!result.userFlowActorsFile) {
310
- result.userFlowActorsFile = join(workflowDir, 'pikku-user-flow-actors.gen.ts');
309
+ if (!result.scenarioActorsFile) {
310
+ result.scenarioActorsFile = join(workflowDir, 'pikku-scenario-actors.gen.ts');
311
311
  }
312
312
  // Services
313
313
  if (!result.servicesFile) {
@@ -0,0 +1,46 @@
1
+ import ts from 'typescript';
2
+ import { PikkuError } from '@pikku/core';
3
+ export declare class PikkuTypecheckFailedError extends PikkuError {
4
+ }
5
+ export interface TscDiagnostic {
6
+ file: string;
7
+ line: number;
8
+ column: number;
9
+ code: number;
10
+ category: 'error' | 'warning' | 'suggestion' | 'message';
11
+ message: string;
12
+ }
13
+ export interface TscCheckResult {
14
+ errorCount: number;
15
+ warningCount: number;
16
+ fileCount: number;
17
+ diagnostics: TscDiagnostic[];
18
+ }
19
+ /**
20
+ * Turn raw tsc diagnostics into a filtered, structured result. Pure — it takes
21
+ * already-computed diagnostics so it can be unit-tested without a program.
22
+ * Anything under node_modules or outside the project root is dropped so the
23
+ * output stays focused on the user's own code (and not lib .d.ts noise).
24
+ */
25
+ export declare const collectTscDiagnostics: (diagnostics: readonly ts.Diagnostic[], rootDir: string) => TscCheckResult;
26
+ /**
27
+ * Compact, token-frugal render of a type-check result: a one-line header plus
28
+ * one line per diagnostic (no code frames), capped. Used by `--tsc-summary` so
29
+ * AI agents and CI logs get the signal without the flood.
30
+ */
31
+ export declare const renderTscSummary: (result: TscCheckResult, maxLines?: number) => string;
32
+ /**
33
+ * Full render: tsc's own formatter with code frames, filtered to project files.
34
+ */
35
+ export declare const renderTscFull: (diagnostics: readonly ts.Diagnostic[], rootDir: string, formatHost: ts.FormatDiagnosticsHost) => string;
36
+ /**
37
+ * Run a real `tsc --noEmit` over the project's own tsconfig — the correct
38
+ * source of truth (lib, paths, strict), unlike the inspector's stripped-down
39
+ * traversal program. Returns both a structured result and the raw diagnostics
40
+ * so the caller can pick the compact (`--tsc-summary`) or full (`--tsc`) render.
41
+ */
42
+ export declare const runProjectTypecheck: (tsconfigPath: string, rootDir: string) => {
43
+ result: TscCheckResult;
44
+ diagnostics: ts.Diagnostic[];
45
+ formatHost: ts.FormatDiagnosticsHost;
46
+ };
@@ -0,0 +1,141 @@
1
+ import ts from 'typescript';
2
+ import { dirname, isAbsolute, relative } from 'node:path';
3
+ import { PikkuError } from '@pikku/core';
4
+ // A type-check failure is expected (the --tsc gate did its job) — throw a
5
+ // PikkuError so the runner logs the message alone, not a stack trace.
6
+ export class PikkuTypecheckFailedError extends PikkuError {
7
+ }
8
+ const CATEGORY = {
9
+ [ts.DiagnosticCategory.Error]: 'error',
10
+ [ts.DiagnosticCategory.Warning]: 'warning',
11
+ [ts.DiagnosticCategory.Suggestion]: 'suggestion',
12
+ [ts.DiagnosticCategory.Message]: 'message',
13
+ };
14
+ const MAX_MESSAGE_LENGTH = 200;
15
+ const DEFAULT_MAX_LINES = 50;
16
+ const NO_FILE = '(project)';
17
+ const isProjectFile = (fileName, rootDir) => {
18
+ if (fileName.includes('/node_modules/'))
19
+ return false;
20
+ const rel = relative(rootDir, fileName);
21
+ return !rel.startsWith('..') && !isAbsolute(rel);
22
+ };
23
+ /**
24
+ * Turn raw tsc diagnostics into a filtered, structured result. Pure — it takes
25
+ * already-computed diagnostics so it can be unit-tested without a program.
26
+ * Anything under node_modules or outside the project root is dropped so the
27
+ * output stays focused on the user's own code (and not lib .d.ts noise).
28
+ */
29
+ export const collectTscDiagnostics = (diagnostics, rootDir) => {
30
+ const files = new Set();
31
+ const collected = [];
32
+ let errorCount = 0;
33
+ let warningCount = 0;
34
+ for (const d of diagnostics) {
35
+ let file = NO_FILE;
36
+ let line = 0;
37
+ let column = 0;
38
+ if (d.file) {
39
+ if (!isProjectFile(d.file.fileName, rootDir))
40
+ continue;
41
+ file = relative(rootDir, d.file.fileName);
42
+ const lc = d.file.getLineAndCharacterOfPosition(d.start ?? 0);
43
+ line = lc.line + 1;
44
+ column = lc.character + 1;
45
+ }
46
+ const category = CATEGORY[d.category];
47
+ if (category === 'error')
48
+ errorCount++;
49
+ else if (category === 'warning')
50
+ warningCount++;
51
+ let message = ts.flattenDiagnosticMessageText(d.messageText, ' ');
52
+ if (message.length > MAX_MESSAGE_LENGTH) {
53
+ message = `${message.slice(0, MAX_MESSAGE_LENGTH - 1)}…`;
54
+ }
55
+ files.add(file);
56
+ collected.push({ file, line, column, code: d.code, category, message });
57
+ }
58
+ collected.sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1);
59
+ return {
60
+ errorCount,
61
+ warningCount,
62
+ fileCount: files.size,
63
+ diagnostics: collected,
64
+ };
65
+ };
66
+ /**
67
+ * Compact, token-frugal render of a type-check result: a one-line header plus
68
+ * one line per diagnostic (no code frames), capped. Used by `--tsc-summary` so
69
+ * AI agents and CI logs get the signal without the flood.
70
+ */
71
+ export const renderTscSummary = (result, maxLines = DEFAULT_MAX_LINES) => {
72
+ if (result.errorCount === 0 && result.warningCount === 0) {
73
+ return 'Type check passed — no errors.';
74
+ }
75
+ const counts = [];
76
+ if (result.errorCount > 0) {
77
+ counts.push(`${result.errorCount} error${result.errorCount === 1 ? '' : 's'}`);
78
+ }
79
+ if (result.warningCount > 0) {
80
+ counts.push(`${result.warningCount} warning${result.warningCount === 1 ? '' : 's'}`);
81
+ }
82
+ const fileLabel = `${result.fileCount} file${result.fileCount === 1 ? '' : 's'}`;
83
+ const lines = [`Type check: ${counts.join(', ')} in ${fileLabel}`];
84
+ const shown = result.diagnostics.slice(0, maxLines);
85
+ for (const d of shown) {
86
+ const at = d.line > 0 ? `${d.file}:${d.line}:${d.column}` : d.file;
87
+ lines.push(` ${at} TS${d.code} ${d.message}`);
88
+ }
89
+ const remaining = result.diagnostics.length - shown.length;
90
+ if (remaining > 0) {
91
+ lines.push(` … and ${remaining} more (run \`pikku all --tsc\` for full output)`);
92
+ }
93
+ return lines.join('\n');
94
+ };
95
+ /**
96
+ * Full render: tsc's own formatter with code frames, filtered to project files.
97
+ */
98
+ export const renderTscFull = (diagnostics, rootDir, formatHost) => {
99
+ const filtered = diagnostics.filter((d) => !d.file || isProjectFile(d.file.fileName, rootDir));
100
+ if (filtered.length === 0)
101
+ return 'Type check passed — no errors.';
102
+ return ts.formatDiagnosticsWithColorAndContext(filtered, formatHost);
103
+ };
104
+ /**
105
+ * Run a real `tsc --noEmit` over the project's own tsconfig — the correct
106
+ * source of truth (lib, paths, strict), unlike the inspector's stripped-down
107
+ * traversal program. Returns both a structured result and the raw diagnostics
108
+ * so the caller can pick the compact (`--tsc-summary`) or full (`--tsc`) render.
109
+ */
110
+ export const runProjectTypecheck = (tsconfigPath, rootDir) => {
111
+ const formatHost = {
112
+ getCanonicalFileName: (f) => f,
113
+ getCurrentDirectory: () => rootDir,
114
+ getNewLine: () => ts.sys.newLine,
115
+ };
116
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
117
+ if (configFile.error) {
118
+ const diagnostics = [configFile.error];
119
+ return {
120
+ result: collectTscDiagnostics(diagnostics, rootDir),
121
+ diagnostics,
122
+ formatHost,
123
+ };
124
+ }
125
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, dirname(tsconfigPath));
126
+ const program = ts.createProgram({
127
+ rootNames: parsed.fileNames,
128
+ options: {
129
+ ...parsed.options,
130
+ noEmit: true,
131
+ incremental: false,
132
+ tsBuildInfoFile: undefined,
133
+ },
134
+ });
135
+ const diagnostics = [...parsed.errors, ...ts.getPreEmitDiagnostics(program)];
136
+ return {
137
+ result: collectTscDiagnostics(diagnostics, rootDir),
138
+ diagnostics,
139
+ formatHost,
140
+ };
141
+ };