@pikku/cli 0.12.159 → 0.12.160
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/dist/.pikku/addon/index.d.ts +1 -1
- package/dist/.pikku/addon/index.js +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.js +1 -1
- package/dist/.pikku/agent/index.d.ts +1 -1
- package/dist/.pikku/agent/index.js +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/agent/pikku-model-aliases.gen.js +1 -1
- package/dist/.pikku/analytics/index.d.ts +1 -1
- package/dist/.pikku/analytics/index.js +1 -1
- package/dist/.pikku/analytics/pikku-analytics-types.gen.d.ts +1 -1
- package/dist/.pikku/analytics/pikku-analytics-types.gen.js +1 -1
- package/dist/.pikku/auth/index.d.ts +1 -1
- package/dist/.pikku/auth/index.js +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.d.ts +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.js +1 -1
- package/dist/.pikku/channel/index.d.ts +1 -1
- package/dist/.pikku/channel/index.js +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/index.d.ts +1 -1
- package/dist/.pikku/cli/index.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
- 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 +25 -0
- 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/error/index.d.ts +1 -1
- package/dist/.pikku/error/index.js +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.d.ts +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.js +1 -1
- package/dist/.pikku/function/index.d.ts +1 -1
- package/dist/.pikku/function/index.js +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 +20 -3
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/gateway/index.d.ts +1 -1
- package/dist/.pikku/gateway/index.js +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.d.ts +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.js +1 -1
- package/dist/.pikku/http/index.d.ts +1 -1
- package/dist/.pikku/http/index.js +1 -1
- 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/mcp/index.d.ts +1 -1
- package/dist/.pikku/mcp/index.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/middleware/index.d.ts +1 -1
- package/dist/.pikku/middleware/index.js +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.d.ts +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.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-services.gen.d.ts +1 -1
- package/dist/.pikku/queue/index.d.ts +1 -1
- package/dist/.pikku/queue/index.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 +1 -0
- package/dist/.pikku/scenarios/index.d.ts +1 -1
- package/dist/.pikku/scenarios/index.js +1 -1
- package/dist/.pikku/scenarios/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-types.gen.d.ts +7 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
- package/dist/.pikku/scheduler/index.d.ts +1 -1
- package/dist/.pikku/scheduler/index.js +1 -1
- 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 +3 -1
- package/dist/.pikku/schemas/schemas/ScenarioGuideInput.schema.json +1 -0
- package/dist/.pikku/scopes/index.d.ts +1 -1
- package/dist/.pikku/scopes/index.js +1 -1
- package/dist/.pikku/scopes/pikku-flags.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-roles.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/index.d.ts +1 -1
- package/dist/.pikku/secrets/index.js +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/services/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/setup/index.d.ts +1 -1
- package/dist/.pikku/setup/index.js +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.d.ts +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.js +1 -1
- package/dist/.pikku/trigger/index.d.ts +1 -1
- package/dist/.pikku/trigger/index.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/index.d.ts +1 -1
- package/dist/.pikku/variables/index.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/index.d.ts +1 -1
- package/dist/.pikku/workflow/index.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- 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/src/cli.wiring.js +25 -1
- package/dist/src/deploy/build-pipeline.d.ts +2 -0
- package/dist/src/deploy/build-pipeline.js +4 -3
- package/dist/src/functions/commands/deploy-apply.js +1 -0
- package/dist/src/functions/commands/deploy-plan.js +1 -0
- package/dist/src/functions/commands/scenario-guide.d.ts +184 -0
- package/dist/src/functions/commands/scenario-guide.js +313 -0
- package/dist/src/functions/commands/scenario-ladder.d.ts +3 -1
- package/dist/src/functions/commands/scenario-ladder.js +1 -0
- package/dist/src/functions/commands/scenario-plan.d.ts +17 -1
- package/dist/src/functions/commands/scenario-plan.js +15 -0
- package/dist/src/functions/commands/scenario.d.ts +30 -0
- package/dist/src/functions/commands/scenario.js +186 -22
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.js +1 -0
- package/dist/src/functions/wirings/workflow/serialize-scenario-types.js +6 -0
- package/dist/src/services.js +3 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/surface.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { resolve, join } from 'node:path';
|
|
3
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, join, dirname, relative, sep } from 'node:path';
|
|
3
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { glob } from 'tinyglobby';
|
|
4
5
|
import { pikkuSessionlessFunc } from '#pikku/function';
|
|
5
6
|
import { InMemoryWorkflowService } from '@pikku/core/services';
|
|
6
7
|
import { FileScenarioRunStore } from '@pikku/core/services';
|
|
@@ -14,7 +15,8 @@ import { resolveScenarioBrowserProvider, scenarioBrowserLifecycle, } from './sce
|
|
|
14
15
|
import { resolvePersonas } from '../../utils/resolve-personas.js';
|
|
15
16
|
import { resolvePersonaCredentials } from '../../utils/persona-credentials.js';
|
|
16
17
|
import { spawnDevServer } from '../../server/spawn-dev-server.js';
|
|
17
|
-
import {
|
|
18
|
+
import { checkGuideCoverage, featureEvidence, guideStep, parseGuideLock, parseGuidePage, renderGuideLock, renderGuidePage, } from './scenario-guide.js';
|
|
19
|
+
import { buildScenarioPlan, identifyScenarioResult } from './scenario-plan.js';
|
|
18
20
|
import { resolveEnvironment, isLocalUrl } from './environment.js';
|
|
19
21
|
import { readDevAddress } from './dev-address.js';
|
|
20
22
|
import { createDevAgentRunner } from './dev-agent-runner.js';
|
|
@@ -354,11 +356,18 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
354
356
|
const runStore = new FileScenarioRunStore({ dir: captureDir });
|
|
355
357
|
const startedAtIso = new Date().toISOString();
|
|
356
358
|
try {
|
|
359
|
+
const selection = {
|
|
360
|
+
...(split(flows) ? { flows: split(flows) } : {}),
|
|
361
|
+
...(split(features) ? { features: split(features) } : {}),
|
|
362
|
+
...(split(tags) ? { tags: split(tags) } : {}),
|
|
363
|
+
...(split(excludeTags) ? { excludeTags: split(excludeTags) } : {}),
|
|
364
|
+
};
|
|
357
365
|
await runStore.start({
|
|
358
366
|
runId: captureRunId,
|
|
359
367
|
environment,
|
|
360
368
|
surface: runSurface,
|
|
361
369
|
status: 'running',
|
|
370
|
+
...(Object.keys(selection).length > 0 ? { selection } : {}),
|
|
362
371
|
startedAt: startedAtIso,
|
|
363
372
|
results: [],
|
|
364
373
|
skipped,
|
|
@@ -371,6 +380,11 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
371
380
|
* is the formatter's job.
|
|
372
381
|
*/
|
|
373
382
|
const readRunSteps = async (service, runId, flowName) => {
|
|
383
|
+
// Taken from the run rather than derived from the ladder: the video
|
|
384
|
+
// clock starts when an actor's window opens, which is somewhere after
|
|
385
|
+
// step one, and every step that never touched a browser burns scenario
|
|
386
|
+
// time while the recording sits still.
|
|
387
|
+
const videoOffsets = scenarioService.takeStepVideoOffsets(runId);
|
|
374
388
|
const prose = collectScenarioStepProse(state.workflows?.meta?.[flowName], functionsMeta, state.personas?.definitions ?? []);
|
|
375
389
|
const steps = (await service.getRunSteps(runId)).map((step) => ({
|
|
376
390
|
stepName: step.stepName,
|
|
@@ -383,6 +397,7 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
383
397
|
expected: step.error?.expected,
|
|
384
398
|
input: step.data,
|
|
385
399
|
stepFunc: step.rpcName,
|
|
400
|
+
video: videoOffsets.get(step.stepName),
|
|
386
401
|
}));
|
|
387
402
|
return {
|
|
388
403
|
rows: scenarioStepRows(steps, prose),
|
|
@@ -427,33 +442,29 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
427
442
|
};
|
|
428
443
|
const hookFailures = [];
|
|
429
444
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* into the record because a run read back next week is describing a suite
|
|
445
|
+
* Which registration ran and which feature grouped it, snapshotted into
|
|
446
|
+
* the record because a run read back next week is describing a suite
|
|
433
447
|
* whose source has moved on.
|
|
434
448
|
*/
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
};
|
|
443
|
-
};
|
|
444
|
-
const runEntry = async (label, scenarioName, data, feature) => {
|
|
449
|
+
const identityOf = (scenarioName, group) => ({
|
|
450
|
+
scenarioName,
|
|
451
|
+
featureId: group?.featureId,
|
|
452
|
+
featureName: group?.featureName,
|
|
453
|
+
tags: state.workflows?.meta?.[scenarioName]?.tags,
|
|
454
|
+
});
|
|
455
|
+
const runEntry = async (label, scenarioName, data, identity) => {
|
|
445
456
|
const startedAt = Date.now();
|
|
446
457
|
if (databaseBaseline) {
|
|
447
458
|
try {
|
|
448
459
|
await databaseBaseline.restore();
|
|
449
460
|
}
|
|
450
461
|
catch (e) {
|
|
451
|
-
const result =
|
|
462
|
+
const result = identifyScenarioResult({
|
|
452
463
|
name: label,
|
|
453
464
|
status: 'failed',
|
|
454
465
|
durationMs: Date.now() - startedAt,
|
|
455
466
|
error: `database reset failed: ${e?.message ?? e}`,
|
|
456
|
-
},
|
|
467
|
+
}, identity);
|
|
457
468
|
results.push(result);
|
|
458
469
|
await runStore.recordScenario(captureRunId, result);
|
|
459
470
|
return;
|
|
@@ -534,7 +545,7 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
534
545
|
// Told here, acted on at the next scenario's reset — that is what closes
|
|
535
546
|
// these windows and finalises the video this outcome decides the fate of.
|
|
536
547
|
browserLifecycle.endScenario(result.status);
|
|
537
|
-
Object.assign(result,
|
|
548
|
+
Object.assign(result, identifyScenarioResult(result, identity));
|
|
538
549
|
await runStore.recordScenario(captureRunId, result);
|
|
539
550
|
if (coverageActive) {
|
|
540
551
|
const report = await invokeCoverage('pikkuScenarioTakeLiveCoverage');
|
|
@@ -582,19 +593,19 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
582
593
|
// Setup failed, so nothing in the group ran. Reporting them as failed
|
|
583
594
|
// rather than skipped is the honest reading: they did not pass.
|
|
584
595
|
for (const entry of group.entries) {
|
|
585
|
-
const result =
|
|
596
|
+
const result = identifyScenarioResult({
|
|
586
597
|
name: label(entry),
|
|
587
598
|
status: 'failed',
|
|
588
599
|
durationMs: 0,
|
|
589
600
|
error: `${groupName ? `feature '${groupName}' ` : ''}${beforeStage} failed: ${beforeError?.message ?? beforeError}`,
|
|
590
|
-
}, entry.scenarioName,
|
|
601
|
+
}, identityOf(entry.scenarioName, group));
|
|
591
602
|
results.push(result);
|
|
592
603
|
await runStore.recordScenario(captureRunId, result);
|
|
593
604
|
}
|
|
594
605
|
}
|
|
595
606
|
else {
|
|
596
607
|
for (const entry of group.entries) {
|
|
597
|
-
await runEntry(label(entry), entry.scenarioName, entry.data,
|
|
608
|
+
await runEntry(label(entry), entry.scenarioName, entry.data, identityOf(entry.scenarioName, group));
|
|
598
609
|
}
|
|
599
610
|
}
|
|
600
611
|
}
|
|
@@ -699,3 +710,156 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
699
710
|
}
|
|
700
711
|
},
|
|
701
712
|
});
|
|
713
|
+
/**
|
|
714
|
+
* `pikku scenario guide` — the suite, written out as markdown.
|
|
715
|
+
*
|
|
716
|
+
* Structure comes from the registry (which features exist, what their scenarios
|
|
717
|
+
* are called), and evidence from the latest run record (what the steps actually
|
|
718
|
+
* said, and what they filed). Editorial prose comes from the project's own
|
|
719
|
+
* `docs/` sources, which cite a feature by leaving the marker pair where its
|
|
720
|
+
* block belongs; this merges the three and writes one markdown file per source.
|
|
721
|
+
* It renders nothing, resolves no asset URLs and calls no model: the writing is
|
|
722
|
+
* somebody else's concern, and this is the compiler that keeps it honest.
|
|
723
|
+
*/
|
|
724
|
+
export const scenarioGuide = pikkuSessionlessFunc({
|
|
725
|
+
func: async ({ logger, config, getInspectorState }, { docs = 'docs', output, runId, allowUndocumented = false, artifactBase }) => {
|
|
726
|
+
const state = await getInspectorState(false, false, false, true);
|
|
727
|
+
const outDir = resolve(config.rootDir, config.outDir);
|
|
728
|
+
await loadScenarioBootstrap(outDir);
|
|
729
|
+
const { features: registeredFeatures } = collectRegisteredWirings();
|
|
730
|
+
const runsDir = join(outDir, 'scenario-runs');
|
|
731
|
+
const runStore = new FileScenarioRunStore({ dir: runsDir });
|
|
732
|
+
const latest = runId ?? (await runStore.list({ limit: 1 }))[0]?.runId;
|
|
733
|
+
const record = latest ? await runStore.get(latest) : undefined;
|
|
734
|
+
if (!record) {
|
|
735
|
+
logger.error(runId
|
|
736
|
+
? `No run '${runId}' under ${runsDir}.`
|
|
737
|
+
: `No scenario run under ${runsDir} — run \`pikku scenario run <environment> --screenshots\` first, since a guide is written out of what a run recorded.`);
|
|
738
|
+
process.exitCode = 1;
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (record.status !== 'passed') {
|
|
742
|
+
logger.error(`Run '${record.runId}' is ${record.status}. A guide is a claim that the product does what the page says, so it is only ever written out of a run that passed.`);
|
|
743
|
+
process.exitCode = 1;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (record.selection) {
|
|
747
|
+
const narrowed = Object.entries(record.selection)
|
|
748
|
+
.map(([flag, values]) => `--${flag} ${values.join(',')}`)
|
|
749
|
+
.join(' ');
|
|
750
|
+
logger.error(`Run '${record.runId}' was narrowed (${narrowed}), so it is missing scenarios the suite has. Guide pages would be written as though those flows do not exist — run the whole suite, or pass --run-id for one that was.`);
|
|
751
|
+
process.exitCode = 1;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const workflowsMeta = state.workflows?.meta ?? {};
|
|
755
|
+
const features = [...registeredFeatures].map(([id, feature]) => {
|
|
756
|
+
const name = feature.name ?? id;
|
|
757
|
+
return {
|
|
758
|
+
id,
|
|
759
|
+
name,
|
|
760
|
+
...(feature.description ? { description: feature.description } : {}),
|
|
761
|
+
document: feature.document !== false,
|
|
762
|
+
scenarios: record.results
|
|
763
|
+
// By id, never by the display name: a title is rewritten freely,
|
|
764
|
+
// and two features are allowed to share one.
|
|
765
|
+
.filter((result) => result.featureId === id ||
|
|
766
|
+
(result.featureId === undefined && result.feature === name))
|
|
767
|
+
.map((result) => {
|
|
768
|
+
const meta = result.scenarioName
|
|
769
|
+
? workflowsMeta[result.scenarioName]
|
|
770
|
+
: undefined;
|
|
771
|
+
return {
|
|
772
|
+
name: result.scenarioName ?? result.name,
|
|
773
|
+
title: meta?.title ?? result.name,
|
|
774
|
+
...(meta?.description ? { description: meta.description } : {}),
|
|
775
|
+
steps: (result.steps ?? []).map((step) => guideStep(step.sentence)),
|
|
776
|
+
screenshots: (result.artifacts ?? [])
|
|
777
|
+
.filter((artifact) => artifact.kind === 'screenshot')
|
|
778
|
+
.map((artifact) => ({
|
|
779
|
+
...(artifact.id ? { id: artifact.id } : {}),
|
|
780
|
+
...(artifact.name ? { name: artifact.name } : {}),
|
|
781
|
+
path: artifact.path,
|
|
782
|
+
})),
|
|
783
|
+
videos: (result.artifacts ?? [])
|
|
784
|
+
.filter((artifact) => artifact.kind === 'video')
|
|
785
|
+
.map((artifact) => ({
|
|
786
|
+
...(artifact.id ? { id: artifact.id } : {}),
|
|
787
|
+
...(artifact.actor ? { actor: artifact.actor } : {}),
|
|
788
|
+
path: artifact.path,
|
|
789
|
+
})),
|
|
790
|
+
};
|
|
791
|
+
}),
|
|
792
|
+
};
|
|
793
|
+
});
|
|
794
|
+
const docsDir = resolve(config.rootDir, docs);
|
|
795
|
+
const sources = (await glob('**/*.md', { cwd: docsDir, onlyFiles: true })).sort();
|
|
796
|
+
const pages = [];
|
|
797
|
+
for (const source of sources) {
|
|
798
|
+
pages.push(parseGuidePage(source, readFileSync(join(docsDir, source), 'utf-8')));
|
|
799
|
+
}
|
|
800
|
+
const lockPath = join(docsDir, '.guide.lock');
|
|
801
|
+
const lock = existsSync(lockPath)
|
|
802
|
+
? parseGuideLock(readFileSync(lockPath, 'utf-8'))
|
|
803
|
+
: {};
|
|
804
|
+
const coverage = checkGuideCoverage(features, pages, lock);
|
|
805
|
+
for (const { path, featureId } of coverage.unknown) {
|
|
806
|
+
logger.error(`${join(docs, path)} cites '${featureId}', which is not a registered feature — a page describing something that no longer exists.`);
|
|
807
|
+
}
|
|
808
|
+
for (const { path, featureId } of coverage.optedOut) {
|
|
809
|
+
logger.error(`${join(docs, path)} cites '${featureId}', which declares \`document: false\`.`);
|
|
810
|
+
}
|
|
811
|
+
for (const { path, featureId } of coverage.figureless) {
|
|
812
|
+
logger.warn(`${join(docs, path)} cites '${featureId}', whose run filed no screenshot — the block renders empty. Take one with \`actor.screenshot(...)\` in a scenario the feature owns.`);
|
|
813
|
+
}
|
|
814
|
+
for (const { path, featureId, locked, current } of coverage.stale) {
|
|
815
|
+
logger.warn(`${join(docs, path)} was written against '${featureId}' at ${locked}, which is now ${current} — the flow moved, so re-read the prose around that block.`);
|
|
816
|
+
}
|
|
817
|
+
for (const featureId of coverage.missing) {
|
|
818
|
+
const message = `Feature '${featureId}' is cited by no page. Place \`<!-- pikku:guide feature=${featureId} -->\` and \`<!-- /pikku:guide -->\` in a page under ${docs}/, or set \`document: false\` on the feature.`;
|
|
819
|
+
if (allowUndocumented) {
|
|
820
|
+
logger.warn(message);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
logger.error(message);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// A compiler that has found an error does not emit. Undocumented features
|
|
827
|
+
// are the one failure that can be downgraded, because a suite mid-way
|
|
828
|
+
// through being written still wants its pages built.
|
|
829
|
+
if (coverage.unknown.length > 0 ||
|
|
830
|
+
coverage.optedOut.length > 0 ||
|
|
831
|
+
(!allowUndocumented && coverage.missing.length > 0)) {
|
|
832
|
+
process.exitCode = 1;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const byId = new Map(features.map((feature) => [feature.id, feature]));
|
|
836
|
+
const outputDir = output
|
|
837
|
+
? resolve(config.rootDir, output)
|
|
838
|
+
: join(outDir, 'guide');
|
|
839
|
+
const artifactRoot = join(runsDir, record.runId);
|
|
840
|
+
for (const page of pages) {
|
|
841
|
+
const target = join(outputDir, page.path);
|
|
842
|
+
// Relative, forward-slashed and computed per page: a guide is markdown
|
|
843
|
+
// with ordinary image refs, and whoever consumes it rewrites the paths.
|
|
844
|
+
// `artifactBase` replaces it with one prefix for every page, for a host
|
|
845
|
+
// that serves the artifacts at a fixed address rather than beside the
|
|
846
|
+
// markdown — and it stays as given, since only the caller knows whether
|
|
847
|
+
// it is a path, a route or an origin.
|
|
848
|
+
const base = artifactBase
|
|
849
|
+
? artifactBase.endsWith('/')
|
|
850
|
+
? artifactBase
|
|
851
|
+
: `${artifactBase}/`
|
|
852
|
+
: `${relative(dirname(target), artifactRoot).split(sep).join('/')}/`;
|
|
853
|
+
const markdown = renderGuidePage(page, byId, base);
|
|
854
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
855
|
+
writeFileSync(target, markdown);
|
|
856
|
+
}
|
|
857
|
+
writeFileSync(lockPath, renderGuideLock(features, coverage.cited));
|
|
858
|
+
logger.info(`${pages.length} page(s) → ${outputDir} (run ${record.runId}, ${coverage.cited.length} documented feature(s))`);
|
|
859
|
+
for (const feature of features) {
|
|
860
|
+
if (feature.document) {
|
|
861
|
+
logger.debug(` ${feature.id} ${featureEvidence(feature)}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
});
|
|
@@ -17,6 +17,7 @@ export const buildFeaturesMeta = (featureFiles) => {
|
|
|
17
17
|
name: feature.name ?? id,
|
|
18
18
|
...(feature.description ? { description: feature.description } : {}),
|
|
19
19
|
tags: feature.tags ?? [],
|
|
20
|
+
...(feature.document === undefined ? {} : { document: feature.document }),
|
|
20
21
|
entries: feature.entries.map((entry) => entry.data === undefined
|
|
21
22
|
? { scenario: entry.scenario }
|
|
22
23
|
: { scenario: entry.scenario, data: entry.data }),
|
|
@@ -223,6 +223,12 @@ type PikkuFeatureConfig<Scenarios extends readonly unknown[]> = {
|
|
|
223
223
|
name: string
|
|
224
224
|
description?: string
|
|
225
225
|
tags?: string[]
|
|
226
|
+
/**
|
|
227
|
+
* Whether this feature is guide material, defaulting to true. \`pikku scenario
|
|
228
|
+
* guide\` requires every documented feature to be cited by a page; a feature
|
|
229
|
+
* that is pure plumbing sets this false rather than being written about.
|
|
230
|
+
*/
|
|
231
|
+
document?: boolean
|
|
226
232
|
scenarios: { [K in keyof Scenarios]: PikkuFeatureEntry<Scenarios[K]> }
|
|
227
233
|
/**
|
|
228
234
|
* Runs ONCE before the whole group — not before each scenario. Per-scenario
|
package/dist/src/services.js
CHANGED
|
@@ -383,6 +383,8 @@ export const createSingletonServices = async (config) => {
|
|
|
383
383
|
// Keeping the check in this single place avoids `typeof Bun` branches leaking
|
|
384
384
|
// into the deploy pipeline / dev command.
|
|
385
385
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
386
|
+
const deployBundler = config.deploy?.bundler ?? 'auto';
|
|
387
|
+
const bundleWithBun = deployBundler === 'bun' || (deployBundler === 'auto' && isBun);
|
|
386
388
|
return {
|
|
387
389
|
config,
|
|
388
390
|
logger,
|
|
@@ -392,7 +394,7 @@ export const createSingletonServices = async (config) => {
|
|
|
392
394
|
getInspectorState,
|
|
393
395
|
invalidateInspectorState,
|
|
394
396
|
workflowService,
|
|
395
|
-
bundler:
|
|
397
|
+
bundler: bundleWithBun ? new BunBundler() : new NodeBundler(),
|
|
396
398
|
devServerRunner: isBun
|
|
397
399
|
? new BunServerRunner()
|
|
398
400
|
: new NodeServerRunner(rootDir),
|