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