@pikku/cli 0.12.89 → 0.12.91
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/README.md +25 -2
- package/console-app/assets/{index-BaOGIr2O.js → index-CpkvmAGb.js} +148 -148
- package/console-app/assets/{index-CSzCJzBb.css → index-D0HG8q0B.css} +1 -1
- 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 +21 -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-contracts-meta.gen.json +6 -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 +52 -1
- 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 +4 -4
- 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 +73 -13
- package/dist/.pikku/function/pikku-functions.gen.js +9 -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/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +8 -0
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +8 -0
- 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 +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.d.ts +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +10 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.json +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.js +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.d.ts +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +10 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.js +1 -0
- 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 +9 -1
- package/dist/.pikku/schemas/schemas/DbBaselineInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbCheckInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbCodegenInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbExportInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricValidateInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +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/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-scenario-actors.gen.d.ts +19 -0
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +17 -0
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +152 -3
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +39 -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 +42 -0
- package/dist/src/fabric/fabric-commands.d.ts +9 -3
- package/dist/src/fabric/fabric-commands.js +6 -0
- package/dist/src/fabric/functions/validate.function.d.ts +15 -5
- package/dist/src/fabric/functions/validate.function.js +69 -16
- package/dist/src/fabric/lib/config.d.ts +11 -1
- package/dist/src/fabric/lib/config.js +39 -1
- package/dist/src/fabric/lib/frontend-typecheck.d.ts +46 -0
- package/dist/src/fabric/lib/frontend-typecheck.js +139 -0
- package/dist/src/functions/commands/db-baseline.d.ts +13 -0
- package/dist/src/functions/commands/db-baseline.js +48 -0
- package/dist/src/functions/commands/db-check.d.ts +12 -0
- package/dist/src/functions/commands/db-check.js +64 -0
- package/dist/src/functions/commands/db-codegen.d.ts +11 -0
- package/dist/src/functions/commands/db-codegen.js +39 -0
- package/dist/src/functions/commands/db-export.d.ts +12 -0
- package/dist/src/functions/commands/db-export.js +31 -0
- package/dist/src/functions/commands/db-generate.js +19 -26
- package/dist/src/functions/commands/dev.js +2 -0
- package/dist/src/functions/commands/load-user-project.d.ts +7 -0
- package/dist/src/functions/commands/load-user-project.js +21 -0
- package/dist/src/functions/commands/pikku-command-bootstrap.js +12 -0
- package/dist/src/functions/commands/scenario-browser.d.ts +69 -0
- package/dist/src/functions/commands/scenario-browser.js +60 -0
- package/dist/src/functions/commands/scenario-formatter.d.ts +81 -0
- package/dist/src/functions/commands/scenario-formatter.js +120 -0
- package/dist/src/functions/commands/scenario-ladder.d.ts +66 -0
- package/dist/src/functions/commands/scenario-ladder.js +132 -0
- package/dist/src/functions/commands/scenario-plan.d.ts +61 -0
- package/dist/src/functions/commands/scenario-plan.js +106 -0
- package/dist/src/functions/commands/scenario.d.ts +18 -0
- package/dist/src/functions/commands/scenario.js +305 -38
- package/dist/src/functions/commands/serve.js +2 -0
- package/dist/src/functions/db/db-migrator.d.ts +35 -4
- package/dist/src/functions/db/db-migrator.js +67 -10
- package/dist/src/functions/db/local-db.d.ts +235 -13
- package/dist/src/functions/db/local-db.js +430 -45
- package/dist/src/functions/db/postgres/postgres-introspector.js +19 -1
- package/dist/src/functions/db/postgres/postgres-migrator.d.ts +1 -0
- package/dist/src/functions/db/postgres/postgres-migrator.js +4 -1
- package/dist/src/functions/db/sqlite/sqlite-introspector.js +2 -1
- package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +1 -0
- package/dist/src/functions/db/sqlite/sqlite-migrator.js +6 -1
- package/dist/src/functions/wirings/functions/pikku-command-functions.js +26 -8
- package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -3
- package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +29 -0
- package/dist/src/functions/wirings/scenarios/scenario-partition.js +58 -0
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.d.ts +14 -0
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.js +29 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.d.ts +12 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.js +56 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.d.ts +7 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.js +40 -0
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +43 -8
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +13 -2
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.d.ts +4 -0
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +62 -0
- package/dist/src/functions/wirings/workflow/serialize-workflow-bootstrap-map.js +10 -3
- package/dist/src/functions/wirings/workflow/serialize-workflow-map.js +10 -3
- package/dist/src/functions/wirings/workflow/serialize-workflow-meta.js +4 -4
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +232 -3
- package/dist/src/server/server-ready.d.ts +12 -0
- package/dist/src/server/server-ready.js +12 -0
- package/dist/src/server/spawn-dev-server.d.ts +50 -0
- package/dist/src/server/spawn-dev-server.js +112 -0
- package/dist/src/services.js +12 -1
- package/dist/src/utils/meta-diff.js +5 -2
- package/dist/src/utils/pikku-cli-config.js +26 -0
- package/dist/src/utils/resolve-scenario-actors.d.ts +23 -0
- package/dist/src/utils/resolve-scenario-actors.js +74 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -6
- package/skills/pikku-i18n/SKILL.md +53 -6
- package/skills/pikku-scenario/SKILL.md +196 -29
|
@@ -2,8 +2,16 @@ import { resolve, join } from 'node:path';
|
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { pikkuSessionlessFunc } from '#pikku';
|
|
4
4
|
import { InMemoryWorkflowService, createHttpScenarioActors, } from '@pikku/core/services';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { PikkuScenarioService } from '@pikku/core/scenario';
|
|
6
|
+
import { pikkuState, getAllPackageStates } from '@pikku/core/internal';
|
|
7
|
+
import { resolveFeatureScenarios } from '@pikku/core/workflow';
|
|
8
|
+
import { loadScenarioBootstrap } from './load-user-project.js';
|
|
9
|
+
import { collectScenarioStepProse, scenarioBrowserSteps, scenarioFailureFromSteps, scenarioStepRows, } from './scenario-ladder.js';
|
|
10
|
+
import { formatScenarioReport } from './scenario-formatter.js';
|
|
11
|
+
import { resolveScenarioBrowserProvider, scenarioBrowserLifecycle, } from './scenario-browser.js';
|
|
12
|
+
import { resolveScenarioActors } from '../../utils/resolve-scenario-actors.js';
|
|
13
|
+
import { spawnDevServer } from '../../server/spawn-dev-server.js';
|
|
14
|
+
import { buildScenarioPlan } from './scenario-plan.js';
|
|
7
15
|
const isScenario = (wf) => wf?.scenario === true;
|
|
8
16
|
const listScenarios = (state) => Object.entries(state.workflows?.meta ?? {})
|
|
9
17
|
.filter(([, wf]) => isScenario(wf))
|
|
@@ -12,27 +20,71 @@ const listScenarios = (state) => Object.entries(state.workflows?.meta ?? {})
|
|
|
12
20
|
name: wf.name ?? id,
|
|
13
21
|
description: wf.description ?? wf.summary ?? wf.title ?? null,
|
|
14
22
|
tags: wf.tags ?? [],
|
|
23
|
+
skip: wf.skip,
|
|
15
24
|
}));
|
|
25
|
+
/**
|
|
26
|
+
* Features and scenario registrations, merged across the main package and any
|
|
27
|
+
* addon packages. A feature holds the very config objects the registrations
|
|
28
|
+
* hold, which is what makes membership resolvable by identity.
|
|
29
|
+
*/
|
|
30
|
+
const collectRegisteredWirings = () => {
|
|
31
|
+
const features = new Map();
|
|
32
|
+
const registrations = new Map();
|
|
33
|
+
for (const [packageName] of getAllPackageStates()) {
|
|
34
|
+
const scope = packageName === '__main__' ? null : packageName;
|
|
35
|
+
for (const [id, feature] of pikkuState(scope, 'workflows', 'features')) {
|
|
36
|
+
features.set(id, feature);
|
|
37
|
+
}
|
|
38
|
+
for (const [name, registration] of pikkuState(scope, 'workflows', 'registrations')) {
|
|
39
|
+
registrations.set(name, registration);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { features, registrations };
|
|
43
|
+
};
|
|
16
44
|
export const scenarioList = pikkuSessionlessFunc({
|
|
17
|
-
func: async ({ logger, getInspectorState }) => {
|
|
18
|
-
const state = await getInspectorState();
|
|
45
|
+
func: async ({ logger, config, getInspectorState }) => {
|
|
46
|
+
const state = await getInspectorState(false, false, false, true);
|
|
19
47
|
const flows = listScenarios(state);
|
|
20
48
|
if (flows.length === 0) {
|
|
21
49
|
logger.info('No scenarios found (pikkuScenario exports).');
|
|
22
50
|
return;
|
|
23
51
|
}
|
|
52
|
+
await loadScenarioBootstrap(resolve(config.rootDir, config.outDir));
|
|
53
|
+
const { features, registrations } = collectRegisteredWirings();
|
|
54
|
+
const { entries, unresolved } = resolveFeatureScenarios(features, registrations);
|
|
55
|
+
for (const { featureId, index } of unresolved) {
|
|
56
|
+
logger.error(`Feature '${featureId}' scenario #${index} is not a registered scenario — it will not run.`);
|
|
57
|
+
}
|
|
58
|
+
const describe = (name) => flows.find((flow) => flow.name === name)?.description;
|
|
59
|
+
for (const [featureId, feature] of features) {
|
|
60
|
+
const featureTags = feature.tags?.length
|
|
61
|
+
? ` [${feature.tags.join(', ')}]`
|
|
62
|
+
: '';
|
|
63
|
+
logger.info(`${feature.name ?? featureId}${featureTags}`);
|
|
64
|
+
if (feature.description) {
|
|
65
|
+
logger.info(` ${feature.description}`);
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries.filter((e) => e.featureId === featureId)) {
|
|
68
|
+
const data = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
|
|
69
|
+
logger.info(` - ${entry.scenarioName}${data}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const inAFeature = new Set(entries.map((entry) => entry.scenarioName));
|
|
24
73
|
for (const flow of flows) {
|
|
74
|
+
if (inAFeature.has(flow.name))
|
|
75
|
+
continue;
|
|
25
76
|
const tags = flow.tags.length ? ` [${flow.tags.join(', ')}]` : '';
|
|
26
77
|
logger.info(`${flow.name}${tags}`);
|
|
27
|
-
|
|
28
|
-
|
|
78
|
+
const description = describe(flow.name);
|
|
79
|
+
if (description) {
|
|
80
|
+
logger.info(` ${description}`);
|
|
29
81
|
}
|
|
30
82
|
}
|
|
31
83
|
},
|
|
32
84
|
});
|
|
33
85
|
export const scenarioRun = pikkuSessionlessFunc({
|
|
34
|
-
func: async ({ logger, config, getInspectorState, variables }, { environment, flows, tags, coverage }) => {
|
|
35
|
-
const state = await getInspectorState(true);
|
|
86
|
+
func: async ({ logger, config, getInspectorState, variables }, { environment, flows, features, tags, excludeTags, coverage, browser = true, spawn = false, keepAlive = false, trace = false, }) => {
|
|
87
|
+
const state = await getInspectorState(true, false, false, true);
|
|
36
88
|
const environments = config.scenarios?.environments ?? {};
|
|
37
89
|
const env = environments[environment];
|
|
38
90
|
if (!env) {
|
|
@@ -42,20 +94,62 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
42
94
|
? `Configured environments: ${known.join(', ')}`
|
|
43
95
|
: `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
|
|
44
96
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
97
|
+
if (spawn) {
|
|
98
|
+
const { hostname, port } = new URL(env.apiUrl);
|
|
99
|
+
const resolvedPort = Number(port || 80);
|
|
100
|
+
logger.info(`Starting a server for '${environment}' on ${env.apiUrl}`);
|
|
101
|
+
const server = await spawnDevServer({
|
|
102
|
+
cwd: config.rootDir,
|
|
103
|
+
port: resolvedPort,
|
|
104
|
+
hostname,
|
|
105
|
+
coverage,
|
|
106
|
+
env: { API_URL: env.apiUrl },
|
|
107
|
+
onOutput: (text) => process.stdout.write(text),
|
|
108
|
+
});
|
|
109
|
+
// Registered rather than wrapped in a try/finally: the run below sets
|
|
110
|
+
// process.exitCode and can throw, and an exit handler covers both without
|
|
111
|
+
// the whole command body having to nest inside one block.
|
|
112
|
+
if (!keepAlive) {
|
|
113
|
+
process.once('exit', server.stop);
|
|
114
|
+
process.once('SIGINT', () => {
|
|
115
|
+
server.stop();
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
118
|
+
process.once('SIGTERM', () => {
|
|
119
|
+
server.stop();
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
51
122
|
}
|
|
52
|
-
|
|
123
|
+
await server.waitUntilReady();
|
|
53
124
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
125
|
+
// Features live in runtime state, not inspector meta — their scenario lists
|
|
126
|
+
// may be built by an ordinary loop — so the project has to be loaded before
|
|
127
|
+
// anything can be selected.
|
|
128
|
+
await loadScenarioBootstrap(resolve(config.rootDir, config.outDir));
|
|
129
|
+
const { features: registeredFeatures, registrations } = collectRegisteredWirings();
|
|
130
|
+
const split = (value) => value ? value.split(',').map((part) => part.trim()) : undefined;
|
|
131
|
+
let { groups, unresolved } = buildScenarioPlan({
|
|
132
|
+
scenarios: listScenarios(state).map(({ name, tags: flowTags, skip }) => ({
|
|
133
|
+
name,
|
|
134
|
+
tags: flowTags,
|
|
135
|
+
skip,
|
|
136
|
+
})),
|
|
137
|
+
features: registeredFeatures,
|
|
138
|
+
registrations,
|
|
139
|
+
flows: split(flows),
|
|
140
|
+
featureIds: split(features),
|
|
141
|
+
tags: split(tags),
|
|
142
|
+
excludeTags: split(excludeTags),
|
|
143
|
+
});
|
|
144
|
+
if (unresolved.length > 0) {
|
|
145
|
+
for (const { featureId, index } of unresolved) {
|
|
146
|
+
logger.error(`Feature '${featureId}' scenario #${index} is not a registered scenario. ` +
|
|
147
|
+
`A feature references scenarios by imported identifier — a scenario built inline inside a feature is never registered.`);
|
|
148
|
+
}
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
57
151
|
}
|
|
58
|
-
if (
|
|
152
|
+
if (groups.length === 0) {
|
|
59
153
|
logger.error('No scenarios matched.');
|
|
60
154
|
process.exitCode = 1;
|
|
61
155
|
return;
|
|
@@ -65,15 +159,57 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
65
159
|
throw new Error('SCENARIO_ACTOR_SECRET is not set — scenario actors cannot sign in. ' +
|
|
66
160
|
'Export it in the environment running this command (never put it in pikku.config.json).');
|
|
67
161
|
}
|
|
162
|
+
// Declared actors plus one per persona nobody declared a body for. Resolved
|
|
163
|
+
// once: the HTTP actors and the Playwright provider below must see the same
|
|
164
|
+
// registry, and codegen resolved the same way to type `ScenarioActorName`.
|
|
165
|
+
const scenarioActors = resolveScenarioActors(config.scenarios);
|
|
68
166
|
const actors = createHttpScenarioActors({
|
|
69
167
|
apiUrl: env.apiUrl,
|
|
70
168
|
secret,
|
|
71
|
-
actors:
|
|
169
|
+
actors: scenarioActors,
|
|
72
170
|
signInPath: env.signInPath,
|
|
73
171
|
rpcPath: env.rpcPath,
|
|
74
172
|
});
|
|
75
|
-
|
|
173
|
+
const functionsMeta = state.functions?.meta ?? {};
|
|
174
|
+
const scenarioNames = new Set(groups.flatMap((group) => group.entries.map((e) => e.scenarioName)));
|
|
175
|
+
const browserStepsByFlow = new Map([...scenarioNames]
|
|
176
|
+
.map((name) => [
|
|
177
|
+
name,
|
|
178
|
+
scenarioBrowserSteps(state.workflows?.meta?.[name], functionsMeta),
|
|
179
|
+
])
|
|
180
|
+
.filter(([, steps]) => steps.length > 0));
|
|
181
|
+
// Two reasons a scenario is held back, both reported as SKIP rather than
|
|
182
|
+
// failed. `--no-browser` is the direct replacement for cucumber's `@console`
|
|
183
|
+
// tag, so a standard run stays green on a machine with no browser; a `skip`
|
|
184
|
+
// on the scenario itself is the project quarantining it, and the plan has
|
|
185
|
+
// already cleared that reason for anything named directly with `--flows`.
|
|
186
|
+
const skipReasonFor = (entry) => {
|
|
187
|
+
if (entry.skip)
|
|
188
|
+
return entry.skip;
|
|
189
|
+
if (!browser && browserStepsByFlow.has(entry.scenarioName)) {
|
|
190
|
+
return 'browser steps, --no-browser';
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
};
|
|
194
|
+
const skipped = [];
|
|
195
|
+
groups = groups
|
|
196
|
+
.map((group) => ({
|
|
197
|
+
...group,
|
|
198
|
+
entries: group.entries.filter((entry) => {
|
|
199
|
+
const reason = skipReasonFor(entry);
|
|
200
|
+
if (!reason)
|
|
201
|
+
return true;
|
|
202
|
+
skipped.push({ name: entry.scenarioName, reason });
|
|
203
|
+
return false;
|
|
204
|
+
}),
|
|
205
|
+
}))
|
|
206
|
+
.filter((group) => group.entries.length > 0);
|
|
76
207
|
const workflowService = new InMemoryWorkflowService();
|
|
208
|
+
const scenarioService = workflowService.setRunExtension((engine) => new PikkuScenarioService(engine));
|
|
209
|
+
scenarioService.setScenarioEnvironment({
|
|
210
|
+
apiUrl: env.apiUrl,
|
|
211
|
+
appUrl: env.appUrl,
|
|
212
|
+
});
|
|
77
213
|
pikkuState(null, 'package', 'singletonServices', {
|
|
78
214
|
logger,
|
|
79
215
|
workflowService,
|
|
@@ -86,7 +222,51 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
86
222
|
`'${environment}' (${env.apiUrl}), not local services.`);
|
|
87
223
|
},
|
|
88
224
|
};
|
|
225
|
+
const needsBrowser = groups.some((group) => group.entries.some((entry) => browserStepsByFlow.has(entry.scenarioName)));
|
|
226
|
+
const failureDir = join(resolve(config.rootDir, config.outDir), 'scenario-failures');
|
|
227
|
+
const browserLifecycle = scenarioBrowserLifecycle(needsBrowser
|
|
228
|
+
? await (async () => {
|
|
229
|
+
const provider = await resolveScenarioBrowserProvider({
|
|
230
|
+
environment,
|
|
231
|
+
apiUrl: env.apiUrl,
|
|
232
|
+
appUrl: env.appUrl,
|
|
233
|
+
secret,
|
|
234
|
+
actors: scenarioActors,
|
|
235
|
+
signInPath: env.signInPath,
|
|
236
|
+
failureDir,
|
|
237
|
+
browserScenarios: [...browserStepsByFlow.keys()],
|
|
238
|
+
driver: config.scenarios?.browserDriver,
|
|
239
|
+
});
|
|
240
|
+
scenarioService.setScenarioBrowserProvider(provider);
|
|
241
|
+
return provider;
|
|
242
|
+
})()
|
|
243
|
+
: undefined);
|
|
89
244
|
const results = [];
|
|
245
|
+
/**
|
|
246
|
+
* The step ladder is read back off the recorded run, so it needs no live
|
|
247
|
+
* step events — it is the same data the console renders. Joining it to the
|
|
248
|
+
* declared prose happens here, where the inspector state is; laying it out
|
|
249
|
+
* is the formatter's job.
|
|
250
|
+
*/
|
|
251
|
+
const readRunSteps = async (service, runId, flowName) => {
|
|
252
|
+
const prose = collectScenarioStepProse(state.workflows?.meta?.[flowName], functionsMeta);
|
|
253
|
+
const steps = (await service.getRunSteps(runId)).map((step) => ({
|
|
254
|
+
stepName: step.stepName,
|
|
255
|
+
status: step.status,
|
|
256
|
+
durationMs: step.succeededAt
|
|
257
|
+
? step.succeededAt.getTime() - step.createdAt.getTime()
|
|
258
|
+
: undefined,
|
|
259
|
+
error: step.error?.message,
|
|
260
|
+
stack: step.error?.stack,
|
|
261
|
+
expected: step.error?.expected,
|
|
262
|
+
input: step.data,
|
|
263
|
+
stepFunc: step.rpcName,
|
|
264
|
+
}));
|
|
265
|
+
return {
|
|
266
|
+
rows: scenarioStepRows(steps, prose),
|
|
267
|
+
failure: scenarioFailureFromSteps(steps, prose),
|
|
268
|
+
};
|
|
269
|
+
};
|
|
90
270
|
const coverageActor = coverage ? Object.values(actors)[0] : undefined;
|
|
91
271
|
let coverageActive = Boolean(coverageActor);
|
|
92
272
|
if (coverage && !coverageActor) {
|
|
@@ -106,8 +286,22 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
106
286
|
return null;
|
|
107
287
|
}
|
|
108
288
|
};
|
|
109
|
-
|
|
289
|
+
/**
|
|
290
|
+
* A feature hook is not a pikku function and not a run — the feature is a
|
|
291
|
+
* grouping, not something durable. It gets the same three arguments a
|
|
292
|
+
* scenario body gets, the CLI's own singletons included, and its result is
|
|
293
|
+
* discarded.
|
|
294
|
+
*/
|
|
295
|
+
const singletonServices = pikkuState(null, 'package', 'singletonServices');
|
|
296
|
+
const runFeatureHook = async (hook) => {
|
|
297
|
+
await hook(singletonServices, undefined, { actors });
|
|
298
|
+
};
|
|
299
|
+
const hookFailures = [];
|
|
300
|
+
const runEntry = async (label, scenarioName, data) => {
|
|
110
301
|
const startedAt = Date.now();
|
|
302
|
+
// Before the scenario, not after it: the last scenario's window is left
|
|
303
|
+
// open for headed debugging, while this one still starts clean.
|
|
304
|
+
await browserLifecycle.reset();
|
|
111
305
|
if (coverageActive) {
|
|
112
306
|
const reset = await invokeCoverage('pikkuScenarioResetLiveCoverage');
|
|
113
307
|
if (reset && reset.enabled === false) {
|
|
@@ -119,20 +313,27 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
119
313
|
await coverageActor?.invoke('pikkuScenarioResetStubs', null);
|
|
120
314
|
}
|
|
121
315
|
catch { }
|
|
316
|
+
let runId;
|
|
317
|
+
let runError;
|
|
122
318
|
try {
|
|
123
|
-
|
|
319
|
+
// The id comes back through the callback rather than the return value
|
|
320
|
+
// because a failing scenario throws instead of returning — and a failed
|
|
321
|
+
// run is exactly the one whose steps are worth reading.
|
|
322
|
+
;
|
|
323
|
+
({ runId } = await workflowService.startWorkflow(scenarioName, data, { type: 'cli' }, guardRpc, { actors, onRunCreated: (id) => (runId = id) }));
|
|
124
324
|
const run = await workflowService.getRun(runId);
|
|
125
325
|
if (run?.status === 'completed') {
|
|
126
326
|
results.push({
|
|
127
|
-
name:
|
|
327
|
+
name: label,
|
|
128
328
|
status: 'passed',
|
|
129
329
|
durationMs: Date.now() - startedAt,
|
|
130
330
|
output: run.output,
|
|
131
331
|
});
|
|
132
332
|
}
|
|
133
333
|
else {
|
|
334
|
+
runError = run?.error;
|
|
134
335
|
results.push({
|
|
135
|
-
name:
|
|
336
|
+
name: label,
|
|
136
337
|
status: 'failed',
|
|
137
338
|
durationMs: Date.now() - startedAt,
|
|
138
339
|
error: run?.error?.message ?? `status: ${run?.status}`,
|
|
@@ -140,22 +341,90 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
140
341
|
}
|
|
141
342
|
}
|
|
142
343
|
catch (e) {
|
|
344
|
+
runError = { stack: e?.stack };
|
|
143
345
|
results.push({
|
|
144
|
-
name:
|
|
346
|
+
name: label,
|
|
145
347
|
status: 'failed',
|
|
146
348
|
durationMs: Date.now() - startedAt,
|
|
147
349
|
error: e?.message ?? String(e),
|
|
148
350
|
});
|
|
149
351
|
}
|
|
352
|
+
const result = results[results.length - 1];
|
|
353
|
+
let stepFailure;
|
|
354
|
+
if (runId) {
|
|
355
|
+
const read = await readRunSteps(workflowService, runId, scenarioName);
|
|
356
|
+
result.steps = read.rows;
|
|
357
|
+
stepFailure = read.failure;
|
|
358
|
+
}
|
|
359
|
+
if (result.status === 'failed') {
|
|
360
|
+
result.failure = {
|
|
361
|
+
// A scenario can also fail outside any step — a hook, or the start
|
|
362
|
+
// itself — and then the run's own error is all there is to report.
|
|
363
|
+
...(stepFailure ?? {
|
|
364
|
+
message: result.error ?? 'unknown failure',
|
|
365
|
+
stack: runError?.stack,
|
|
366
|
+
expected: runError?.expected,
|
|
367
|
+
}),
|
|
368
|
+
browser: await browserLifecycle.captureFailure(label),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
150
371
|
if (coverageActive) {
|
|
151
372
|
const report = await invokeCoverage('pikkuScenarioTakeLiveCoverage');
|
|
152
373
|
if (report) {
|
|
153
|
-
scenarioCoverage[
|
|
374
|
+
scenarioCoverage[label] = report;
|
|
154
375
|
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 '${
|
|
376
|
+
logger.info(` coverage: ${covered?.length ?? 0}/${report.summary?.total ?? 0} functions exercised by '${label}'`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
for (const group of groups) {
|
|
381
|
+
const groupName = group.featureName ?? group.featureId;
|
|
382
|
+
const label = (entry) => {
|
|
383
|
+
const data = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
|
|
384
|
+
return groupName
|
|
385
|
+
? `${groupName} › ${entry.scenarioName}${data}`
|
|
386
|
+
: entry.scenarioName;
|
|
387
|
+
};
|
|
388
|
+
let beforeError;
|
|
389
|
+
if (group.before) {
|
|
390
|
+
try {
|
|
391
|
+
await runFeatureHook(group.before);
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
beforeError = e;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
if (beforeError) {
|
|
399
|
+
// Setup failed, so nothing in the group ran. Reporting them as failed
|
|
400
|
+
// rather than skipped is the honest reading: they did not pass.
|
|
401
|
+
for (const entry of group.entries) {
|
|
402
|
+
results.push({
|
|
403
|
+
name: label(entry),
|
|
404
|
+
status: 'failed',
|
|
405
|
+
durationMs: 0,
|
|
406
|
+
error: `feature '${groupName}' before hook failed: ${beforeError?.message ?? beforeError}`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
for (const entry of group.entries) {
|
|
412
|
+
await runEntry(label(entry), entry.scenarioName, entry.data);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
if (group.after) {
|
|
418
|
+
try {
|
|
419
|
+
await runFeatureHook(group.after);
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
hookFailures.push(`feature '${groupName}' after hook failed: ${e?.message ?? e}`);
|
|
423
|
+
}
|
|
156
424
|
}
|
|
157
425
|
}
|
|
158
426
|
}
|
|
427
|
+
await browserLifecycle.close();
|
|
159
428
|
if (coverage && Object.keys(scenarioCoverage).length > 0) {
|
|
160
429
|
const coverageDir = join(resolve(config.rootDir, config.outDir), 'coverage');
|
|
161
430
|
mkdirSync(coverageDir, { recursive: true });
|
|
@@ -167,17 +436,15 @@ export const scenarioRun = pikkuSessionlessFunc({
|
|
|
167
436
|
}, null, 2) + '\n');
|
|
168
437
|
logger.info(`Scenario coverage → ${outFile}`);
|
|
169
438
|
}
|
|
170
|
-
const
|
|
171
|
-
for (const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
logger.error(`FAIL ${r.name} (${r.durationMs}ms): ${r.error}`);
|
|
177
|
-
}
|
|
439
|
+
const report = { environment, results, skipped, hookFailures };
|
|
440
|
+
for (const { level, text } of formatScenarioReport(report, {
|
|
441
|
+
trace,
|
|
442
|
+
projectRoot: config.rootDir,
|
|
443
|
+
})) {
|
|
444
|
+
logger[level](text);
|
|
178
445
|
}
|
|
179
|
-
|
|
180
|
-
if (failed.length > 0) {
|
|
446
|
+
const failed = results.filter((r) => r.status === 'failed');
|
|
447
|
+
if (failed.length > 0 || hookFailures.length > 0) {
|
|
181
448
|
process.exitCode = 1;
|
|
182
449
|
}
|
|
183
450
|
},
|
|
@@ -11,6 +11,7 @@ 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
13
|
import { resolveConsoleMount } from './serve-console.js';
|
|
14
|
+
import { serverReadyLine } from '../../server/server-ready.js';
|
|
14
15
|
export const serve = pikkuSessionlessFunc({
|
|
15
16
|
remote: true,
|
|
16
17
|
func: async ({ logger, config, getInspectorState, variables, devServerRunner }, { port, console: serveConsole }) => {
|
|
@@ -136,6 +137,7 @@ export const serve = pikkuSessionlessFunc({
|
|
|
136
137
|
if (consoleMount) {
|
|
137
138
|
logger.info(`Pikku Console available at http://${hostname}:${resolvedPort}${consoleMount.urlPrefix}`);
|
|
138
139
|
}
|
|
140
|
+
logger.info(serverReadyLine(hostname, resolvedPort));
|
|
139
141
|
process.once('SIGINT', async () => {
|
|
140
142
|
logger.info('Stopping server...');
|
|
141
143
|
try {
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The migrator's own bookkeeping table, which belongs to no dialect and to no
|
|
3
|
+
* project's schema.
|
|
4
|
+
*
|
|
5
|
+
* Every introspector hides it for that reason. Leaving it visible on one
|
|
6
|
+
* dialect and not the other is not cosmetic: a schema source exported from a
|
|
7
|
+
* database that has been migrated would publish `sql_migrations` as one of its
|
|
8
|
+
* own tables, and the consumer — which also has it — would then read the source
|
|
9
|
+
* as partially covered and emit column deltas instead of the source's own SQL,
|
|
10
|
+
* silently dropping its primary keys, indexes and constraints.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MIGRATION_TRACKING_TABLE = "sql_migrations";
|
|
1
13
|
export declare class MigrationDriftError extends Error {
|
|
2
14
|
readonly file: string;
|
|
3
15
|
readonly recordedHash: string;
|
|
@@ -23,10 +35,29 @@ export interface MigrationExecutor {
|
|
|
23
35
|
ensureTrackingTable(): Promise<void>;
|
|
24
36
|
getApplied(): Promise<AppliedMigration[]>;
|
|
25
37
|
runMigration(sql: string, name: string, hash: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Record a migration as applied without running its SQL.
|
|
40
|
+
*
|
|
41
|
+
* For a database that already contains what the migration describes, because
|
|
42
|
+
* something created those tables before anyone wrote them down. Only ever
|
|
43
|
+
* called once the caller has confirmed that is actually true — recording a
|
|
44
|
+
* migration whose tables are absent leaves a database permanently behind with
|
|
45
|
+
* no pending migration to reveal it.
|
|
46
|
+
*/
|
|
47
|
+
recordMigration(name: string, hash: string): Promise<void>;
|
|
26
48
|
}
|
|
49
|
+
export declare function migrate(executor: MigrationExecutor, migrationsDir: string): Promise<MigrateResult>;
|
|
27
50
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
51
|
+
* Record every pending migration as applied, without running any of it.
|
|
52
|
+
*
|
|
53
|
+
* The escape hatch for a database that already has the tables a migration
|
|
54
|
+
* creates — the shape you get when a runtime bootstrapped its own schema at
|
|
55
|
+
* boot and the migration writing it down was authored afterwards. Running that
|
|
56
|
+
* migration would fail on every existing deployment; skipping it forever would
|
|
57
|
+
* leave the history lying. Recording it says what is true.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately unconditional here. Whether the database really does match is a
|
|
60
|
+
* question about schemas, not migration files, so the caller answers it first
|
|
61
|
+
* and this only runs once it has.
|
|
31
62
|
*/
|
|
32
|
-
export declare function
|
|
63
|
+
export declare function baselineMigrations(executor: MigrationExecutor, migrationsDir: string): Promise<string[]>;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* The migrator's own bookkeeping table, which belongs to no dialect and to no
|
|
6
|
+
* project's schema.
|
|
7
|
+
*
|
|
8
|
+
* Every introspector hides it for that reason. Leaving it visible on one
|
|
9
|
+
* dialect and not the other is not cosmetic: a schema source exported from a
|
|
10
|
+
* database that has been migrated would publish `sql_migrations` as one of its
|
|
11
|
+
* own tables, and the consumer — which also has it — would then read the source
|
|
12
|
+
* as partially covered and emit column deltas instead of the source's own SQL,
|
|
13
|
+
* silently dropping its primary keys, indexes and constraints.
|
|
14
|
+
*/
|
|
15
|
+
export const MIGRATION_TRACKING_TABLE = 'sql_migrations';
|
|
4
16
|
export class MigrationDriftError extends Error {
|
|
5
17
|
file;
|
|
6
18
|
recordedHash;
|
|
@@ -31,9 +43,25 @@ function sha256(bytes) {
|
|
|
31
43
|
* executor. Hashes raw file bytes on apply; subsequent runs re-hash and bail
|
|
32
44
|
* with `MigrationDriftError` if any applied file has changed on disk.
|
|
33
45
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The migrations on disk, or none.
|
|
48
|
+
*
|
|
49
|
+
* A project that has never generated a migration has no directory to read, and
|
|
50
|
+
* that is the ordinary first-run state rather than a failure — it is precisely
|
|
51
|
+
* the project `db generate` exists to serve.
|
|
52
|
+
*/
|
|
53
|
+
const migrationFiles = (migrationsDir) => existsSync(migrationsDir)
|
|
54
|
+
? readdirSync(migrationsDir)
|
|
55
|
+
.filter((f) => f.endsWith('.sql'))
|
|
56
|
+
.sort()
|
|
57
|
+
: [];
|
|
58
|
+
/**
|
|
59
|
+
* Re-hash every applied migration and bail if one has changed on disk.
|
|
60
|
+
*
|
|
61
|
+
* Applies to baselining as much as to migrating: recording a file as applied
|
|
62
|
+
* only means anything if the file is still the one that was applied.
|
|
63
|
+
*/
|
|
64
|
+
function assertNoDrift(applied, migrationsDir) {
|
|
37
65
|
for (const row of applied) {
|
|
38
66
|
let currentHash = null;
|
|
39
67
|
try {
|
|
@@ -46,13 +74,15 @@ export async function migrate(executor, migrationsDir) {
|
|
|
46
74
|
throw new MigrationDriftError(row.name, row.hash, currentHash, row.applied_at, migrationsDir);
|
|
47
75
|
}
|
|
48
76
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
}
|
|
78
|
+
export async function migrate(executor, migrationsDir) {
|
|
79
|
+
await executor.ensureTrackingTable();
|
|
80
|
+
const applied = await executor.getApplied();
|
|
81
|
+
assertNoDrift(applied, migrationsDir);
|
|
82
|
+
const appliedNames = new Set(applied.map((r) => r.name));
|
|
53
83
|
const result = { applied: [], skipped: [] };
|
|
54
|
-
for (const name of
|
|
55
|
-
if (
|
|
84
|
+
for (const name of migrationFiles(migrationsDir)) {
|
|
85
|
+
if (appliedNames.has(name)) {
|
|
56
86
|
result.skipped.push(name);
|
|
57
87
|
continue;
|
|
58
88
|
}
|
|
@@ -63,3 +93,30 @@ export async function migrate(executor, migrationsDir) {
|
|
|
63
93
|
}
|
|
64
94
|
return result;
|
|
65
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Record every pending migration as applied, without running any of it.
|
|
98
|
+
*
|
|
99
|
+
* The escape hatch for a database that already has the tables a migration
|
|
100
|
+
* creates — the shape you get when a runtime bootstrapped its own schema at
|
|
101
|
+
* boot and the migration writing it down was authored afterwards. Running that
|
|
102
|
+
* migration would fail on every existing deployment; skipping it forever would
|
|
103
|
+
* leave the history lying. Recording it says what is true.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately unconditional here. Whether the database really does match is a
|
|
106
|
+
* question about schemas, not migration files, so the caller answers it first
|
|
107
|
+
* and this only runs once it has.
|
|
108
|
+
*/
|
|
109
|
+
export async function baselineMigrations(executor, migrationsDir) {
|
|
110
|
+
await executor.ensureTrackingTable();
|
|
111
|
+
const applied = await executor.getApplied();
|
|
112
|
+
assertNoDrift(applied, migrationsDir);
|
|
113
|
+
const appliedNames = new Set(applied.map((r) => r.name));
|
|
114
|
+
const recorded = [];
|
|
115
|
+
for (const name of migrationFiles(migrationsDir)) {
|
|
116
|
+
if (appliedNames.has(name))
|
|
117
|
+
continue;
|
|
118
|
+
await executor.recordMigration(name, sha256(readFileSync(join(migrationsDir, name))));
|
|
119
|
+
recorded.push(name);
|
|
120
|
+
}
|
|
121
|
+
return recorded;
|
|
122
|
+
}
|