@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.
- package/cli.schema.json +1 -1
- package/console-app/assets/{index-CttM2Aiz.js → index-BY60UXB6.js} +132 -132
- package/console-app/index.html +2 -2
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +7 -12
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +16 -31
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +38 -53
- package/dist/.pikku/function/pikku-functions.gen.js +1 -3
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +2 -3
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +5 -7
- package/dist/.pikku/schemas/schemas/DevInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/ServeInput.schema.json +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/meta/allWorkflow.gen.json +6 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +4 -4
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +2 -2
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/bin/pikku.js +1 -1
- package/dist/src/cli.wiring.js +16 -31
- package/dist/src/functions/commands/dev.d.ts +0 -3
- package/dist/src/functions/commands/dev.js +8 -6
- package/dist/src/functions/commands/{user-flow.d.ts → scenario.d.ts} +2 -2
- package/dist/src/functions/commands/{user-flow.js → scenario.js} +21 -21
- package/dist/src/functions/commands/serve-console.d.ts +2 -3
- package/dist/src/functions/commands/serve-console.js +5 -51
- package/dist/src/functions/commands/serve.d.ts +3 -3
- package/dist/src/functions/commands/serve.js +11 -6
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +6 -5
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +8 -0
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +43 -0
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +4 -4
- package/dist/src/functions/workflows/all.workflow.js +17 -0
- package/dist/src/utils/pikku-cli-config.js +2 -2
- package/dist/src/utils/tsc-check.d.ts +46 -0
- package/dist/src/utils/tsc-check.js +141 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/skills/pikku-i18n/SKILL.md +58 -185
- package/skills/pikku-kysely/SKILL.md +68 -2
- package/dist/.pikku/schemas/schemas/ConsoleCommandInput.schema.json +0 -1
- package/dist/src/functions/commands/console.d.ts +0 -13
- package/dist/src/functions/commands/console.js +0 -122
- package/dist/src/functions/wirings/workflow/serialize-user-flow-actors.d.ts +0 -8
- package/dist/src/functions/wirings/workflow/serialize-user-flow-actors.js +0 -36
- /package/dist/.pikku/schemas/schemas/{UserFlowListInput.schema.json → ScenarioListInput.schema.json} +0 -0
- /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,
|
|
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
|
|
7
|
-
const
|
|
8
|
-
.filter(([, 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
|
|
15
|
+
export const scenarioList = pikkuSessionlessFunc({
|
|
16
16
|
func: async ({ logger, getInspectorState }) => {
|
|
17
17
|
const state = await getInspectorState();
|
|
18
|
-
const flows =
|
|
18
|
+
const flows = listScenarios(state);
|
|
19
19
|
if (flows.length === 0) {
|
|
20
|
-
logger.info('No
|
|
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
|
|
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.
|
|
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
|
|
40
|
+
throw new Error(`Unknown scenario environment '${environment}'. ` +
|
|
41
41
|
(known.length
|
|
42
42
|
? `Configured environments: ${known.join(', ')}`
|
|
43
|
-
: `Add
|
|
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 =
|
|
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
|
|
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
|
|
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('
|
|
65
|
+
const secret = await variables.get('SCENARIO_ACTOR_SECRET');
|
|
66
66
|
if (!secret) {
|
|
67
|
-
throw new Error('
|
|
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 =
|
|
70
|
+
const actors = createHttpScenarioActors({
|
|
71
71
|
apiUrl: env.apiUrl,
|
|
72
72
|
secret,
|
|
73
|
-
actors: config.
|
|
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(`
|
|
91
|
-
`in a
|
|
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}
|
|
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 {
|
|
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
|
|
3
|
+
export declare function resolveConsoleMount(): Promise<StaticMount | undefined>;
|
|
@@ -1,62 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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?:
|
|
3
|
+
console?: boolean;
|
|
4
4
|
}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
|
|
5
5
|
port?: string;
|
|
6
|
-
console?:
|
|
6
|
+
console?: boolean;
|
|
7
7
|
}, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{
|
|
8
8
|
port?: string;
|
|
9
|
-
console?:
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 {
|
|
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
|
|
67
|
-
if (
|
|
68
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
310
|
-
result.
|
|
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
|
+
};
|