@pikku/cli 0.12.90 → 0.12.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/LICENSE +106 -0
  2. package/README.md +25 -2
  3. package/console-app/assets/{index-CSzCJzBb.css → index-D0HG8q0B.css} +1 -1
  4. package/console-app/assets/{index-C5Bd44e4.js → index-DDpIMCpy.js} +149 -149
  5. package/console-app/index.html +2 -2
  6. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  7. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  8. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
  10. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  11. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +15 -0
  15. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  16. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  17. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  18. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +43 -0
  19. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  20. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  21. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  22. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  23. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  24. package/dist/.pikku/function/pikku-function-types.gen.d.ts +4 -4
  25. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  26. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  27. package/dist/.pikku/function/pikku-functions-meta.gen.json +39 -44
  28. package/dist/.pikku/function/pikku-functions.gen.js +1 -3
  29. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  30. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  31. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  32. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  33. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +9 -0
  34. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +9 -0
  35. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  37. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  39. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  40. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  41. package/dist/.pikku/pikku-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  43. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  45. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  46. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  47. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  48. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -1
  49. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.d.ts +1 -0
  50. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +10 -0
  51. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.json +1 -0
  52. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +4 -0
  53. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.js +1 -0
  54. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.d.ts +1 -0
  55. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +10 -0
  56. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +4 -0
  57. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.js +1 -0
  58. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +4 -0
  59. package/dist/.pikku/scenarios/schemas/register.gen.js +4 -0
  60. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  61. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  62. package/dist/.pikku/schemas/register.gen.js +5 -3
  63. package/dist/.pikku/schemas/schemas/FabricSecretsListOutput.schema.json +1 -1
  64. package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -0
  65. package/dist/.pikku/schemas/schemas/FabricSecretsRotateOutput.schema.json +1 -0
  66. package/dist/.pikku/schemas/schemas/FabricSecretsSetOutput.schema.json +1 -1
  67. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  68. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +1 -1
  69. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  70. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  71. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  72. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  73. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  74. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  75. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  76. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  77. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  78. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  79. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  80. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  81. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  82. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +2 -8
  83. package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +19 -0
  84. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +17 -0
  85. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +152 -3
  86. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +39 -1
  87. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  88. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  89. package/dist/bin/pikku-bin.mjs +2 -2
  90. package/dist/src/cli.wiring.js +28 -0
  91. package/dist/src/deploy/analyzer/analyzer.js +22 -5
  92. package/dist/src/deploy/build-pipeline.js +5 -1
  93. package/dist/src/fabric/fabric-commands.d.ts +37 -9
  94. package/dist/src/fabric/fabric-commands.js +12 -0
  95. package/dist/src/fabric/functions/domains-add.function.d.ts +4 -4
  96. package/dist/src/fabric/functions/secrets-list.function.d.ts +16 -4
  97. package/dist/src/fabric/functions/secrets-list.function.js +12 -10
  98. package/dist/src/fabric/functions/secrets-rotate.function.d.ts +24 -0
  99. package/dist/src/fabric/functions/secrets-rotate.function.js +36 -0
  100. package/dist/src/fabric/functions/secrets-set.function.d.ts +8 -4
  101. package/dist/src/fabric/functions/secrets-set.function.js +18 -7
  102. package/dist/src/fabric/lib/http.d.ts +7 -1
  103. package/dist/src/fabric/lib/sealed-box.d.ts +16 -0
  104. package/dist/src/fabric/lib/sealed-box.js +72 -0
  105. package/dist/src/functions/commands/dev.js +11 -0
  106. package/dist/src/functions/commands/load-user-project.d.ts +7 -0
  107. package/dist/src/functions/commands/load-user-project.js +21 -0
  108. package/dist/src/functions/commands/pikku-command-bootstrap.js +13 -0
  109. package/dist/src/functions/commands/scenario-browser.d.ts +84 -0
  110. package/dist/src/functions/commands/scenario-browser.js +68 -0
  111. package/dist/src/functions/commands/scenario-environment.d.ts +32 -0
  112. package/dist/src/functions/commands/scenario-environment.js +61 -0
  113. package/dist/src/functions/commands/scenario-formatter.d.ts +81 -0
  114. package/dist/src/functions/commands/scenario-formatter.js +120 -0
  115. package/dist/src/functions/commands/scenario-ladder.d.ts +66 -0
  116. package/dist/src/functions/commands/scenario-ladder.js +132 -0
  117. package/dist/src/functions/commands/scenario-plan.d.ts +61 -0
  118. package/dist/src/functions/commands/scenario-plan.js +106 -0
  119. package/dist/src/functions/commands/scenario.d.ts +24 -0
  120. package/dist/src/functions/commands/scenario.js +322 -50
  121. package/dist/src/functions/commands/serve.js +2 -0
  122. package/dist/src/functions/commands/skills.js +27 -61
  123. package/dist/src/functions/db/db-codegen.d.ts +6 -0
  124. package/dist/src/functions/db/db-codegen.js +9 -0
  125. package/dist/src/functions/db/db-migrator.js +14 -0
  126. package/dist/src/functions/db/local-db.d.ts +8 -0
  127. package/dist/src/functions/db/local-db.js +24 -2
  128. package/dist/src/functions/db/migration-identifiers.d.ts +58 -0
  129. package/dist/src/functions/db/migration-identifiers.js +262 -0
  130. package/dist/src/functions/db/migration-provenance.d.ts +35 -0
  131. package/dist/src/functions/db/migration-provenance.js +80 -0
  132. package/dist/src/functions/db/schema-sql.d.ts +43 -0
  133. package/dist/src/functions/db/schema-sql.js +135 -0
  134. package/dist/src/functions/db/sqlite/sqlite-kysely.js +22 -2
  135. package/dist/src/functions/wirings/functions/pikku-command-functions.js +26 -8
  136. package/dist/src/functions/wirings/functions/schemas.js +13 -1
  137. package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -3
  138. package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +6 -1
  139. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +10 -0
  140. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +102 -0
  141. package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +42 -0
  142. package/dist/src/functions/wirings/scenarios/scenario-partition.js +71 -0
  143. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.d.ts +33 -0
  144. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.js +49 -0
  145. package/dist/src/functions/wirings/scenarios/serialize-feature-meta.d.ts +14 -0
  146. package/dist/src/functions/wirings/scenarios/serialize-feature-meta.js +29 -0
  147. package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.d.ts +12 -0
  148. package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.js +56 -0
  149. package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.d.ts +7 -0
  150. package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.js +40 -0
  151. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +43 -8
  152. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +1 -1
  153. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +13 -2
  154. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.d.ts +4 -0
  155. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +62 -0
  156. package/dist/src/functions/wirings/workflow/serialize-workflow-bootstrap-map.js +10 -3
  157. package/dist/src/functions/wirings/workflow/serialize-workflow-map.js +10 -3
  158. package/dist/src/functions/wirings/workflow/serialize-workflow-meta.js +4 -4
  159. package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
  160. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +232 -3
  161. package/dist/src/functions/workflows/all.workflow.js +2 -7
  162. package/dist/src/server/server-ready.d.ts +12 -0
  163. package/dist/src/server/server-ready.js +12 -0
  164. package/dist/src/server/spawn-dev-server.d.ts +50 -0
  165. package/dist/src/server/spawn-dev-server.js +112 -0
  166. package/dist/src/services.js +12 -1
  167. package/dist/src/utils/file-writer.js +14 -2
  168. package/dist/src/utils/meta-diff.js +5 -2
  169. package/dist/src/utils/pikku-cli-config.d.ts +14 -0
  170. package/dist/src/utils/pikku-cli-config.js +60 -0
  171. package/dist/src/utils/remove-legacy-scaffold-file.d.ts +14 -0
  172. package/dist/src/utils/remove-legacy-scaffold-file.js +23 -0
  173. package/dist/src/utils/resolve-scenario-actors.d.ts +23 -0
  174. package/dist/src/utils/resolve-scenario-actors.js +74 -0
  175. package/dist/src/utils/serialize-schemas.d.ts +3 -1
  176. package/dist/src/utils/serialize-schemas.js +39 -4
  177. package/dist/tsconfig.tsbuildinfo +1 -1
  178. package/package.json +18 -8
  179. package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +0 -1
  180. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +0 -1
  181. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +0 -31
  182. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +0 -10
  183. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +0 -104
  184. package/skills/pikku-addon/SKILL.md +0 -243
  185. package/skills/pikku-addon/references/addon-package-manifest.md +0 -63
  186. package/skills/pikku-ai-agent/SKILL.md +0 -231
  187. package/skills/pikku-ai-vercel/SKILL.md +0 -82
  188. package/skills/pikku-ai-voice/SKILL.md +0 -88
  189. package/skills/pikku-audit/SKILL.md +0 -175
  190. package/skills/pikku-aws/SKILL.md +0 -111
  191. package/skills/pikku-backblaze/SKILL.md +0 -71
  192. package/skills/pikku-better-auth/SKILL.md +0 -298
  193. package/skills/pikku-cli/SKILL.md +0 -198
  194. package/skills/pikku-cli/references/complete-example.md +0 -82
  195. package/skills/pikku-concepts/SKILL.md +0 -250
  196. package/skills/pikku-concepts/references/concept-mapping.md +0 -556
  197. package/skills/pikku-concepts/references/packages.md +0 -29
  198. package/skills/pikku-config/SKILL.md +0 -212
  199. package/skills/pikku-cron/SKILL.md +0 -214
  200. package/skills/pikku-deploy-azure/SKILL.md +0 -71
  201. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -82
  202. package/skills/pikku-deploy-express/SKILL.md +0 -86
  203. package/skills/pikku-deploy-fastify/SKILL.md +0 -73
  204. package/skills/pikku-deploy-lambda/SKILL.md +0 -110
  205. package/skills/pikku-deploy-nextjs/SKILL.md +0 -78
  206. package/skills/pikku-deploy-uws/SKILL.md +0 -88
  207. package/skills/pikku-deps/SKILL.md +0 -98
  208. package/skills/pikku-emails/SKILL.md +0 -163
  209. package/skills/pikku-fabric/SKILL.md +0 -318
  210. package/skills/pikku-fabric-debug/SKILL.md +0 -112
  211. package/skills/pikku-feature/SKILL.md +0 -258
  212. package/skills/pikku-gateway-slack/SKILL.md +0 -115
  213. package/skills/pikku-http/SKILL.md +0 -220
  214. package/skills/pikku-http/references/http-options.md +0 -55
  215. package/skills/pikku-i18n/SKILL.md +0 -137
  216. package/skills/pikku-info/SKILL.md +0 -100
  217. package/skills/pikku-jose/SKILL.md +0 -105
  218. package/skills/pikku-kysely/SKILL.md +0 -219
  219. package/skills/pikku-machine-auth/SKILL.md +0 -183
  220. package/skills/pikku-mcp/SKILL.md +0 -241
  221. package/skills/pikku-middleware/SKILL.md +0 -231
  222. package/skills/pikku-middleware/references/middleware-patterns.md +0 -61
  223. package/skills/pikku-mongodb/SKILL.md +0 -105
  224. package/skills/pikku-n8n-import/SKILL.md +0 -109
  225. package/skills/pikku-n8n-import/SPEC.md +0 -84
  226. package/skills/pikku-n8n-import/references/addon-mapping.md +0 -121
  227. package/skills/pikku-n8n-import/references/code-translation.md +0 -121
  228. package/skills/pikku-n8n-import/references/loops-and-control.md +0 -87
  229. package/skills/pikku-paraglide/SKILL.md +0 -117
  230. package/skills/pikku-permissions/SKILL.md +0 -192
  231. package/skills/pikku-pino/SKILL.md +0 -79
  232. package/skills/pikku-product-second-opinion/README.md +0 -43
  233. package/skills/pikku-product-second-opinion/SKILL.md +0 -158
  234. package/skills/pikku-product-second-opinion/example/sample-report.md +0 -90
  235. package/skills/pikku-product-second-opinion/references/report-template.md +0 -73
  236. package/skills/pikku-queue/SKILL.md +0 -240
  237. package/skills/pikku-react/SKILL.md +0 -212
  238. package/skills/pikku-react-query/SKILL.md +0 -242
  239. package/skills/pikku-realtime/SKILL.md +0 -236
  240. package/skills/pikku-realtime/references/other-routes.md +0 -23
  241. package/skills/pikku-redis/SKILL.md +0 -90
  242. package/skills/pikku-rpc/SKILL.md +0 -171
  243. package/skills/pikku-rtl/SKILL.md +0 -219
  244. package/skills/pikku-scenario/SKILL.md +0 -215
  245. package/skills/pikku-schedule/SKILL.md +0 -57
  246. package/skills/pikku-schema-ajv/SKILL.md +0 -62
  247. package/skills/pikku-schema-cfworker/SKILL.md +0 -63
  248. package/skills/pikku-security/SKILL.md +0 -108
  249. package/skills/pikku-services/SKILL.md +0 -248
  250. package/skills/pikku-services/references/audit-wire-service.md +0 -34
  251. package/skills/pikku-software-archaeology/README.md +0 -70
  252. package/skills/pikku-software-archaeology/SKILL.md +0 -186
  253. package/skills/pikku-software-archaeology/references/blueprint.schema.json +0 -625
  254. package/skills/pikku-software-archaeology/references/pikku-mapping.md +0 -49
  255. package/skills/pikku-software-archaeology/scripts/validate.mjs +0 -173
  256. package/skills/pikku-tag-middleware/SKILL.md +0 -13
  257. package/skills/pikku-template-clone/SKILL.md +0 -40
  258. package/skills/pikku-trigger/SKILL.md +0 -181
  259. package/skills/pikku-versioning/SKILL.md +0 -173
  260. package/skills/pikku-websocket/SKILL.md +0 -243
  261. package/skills/pikku-workflow/SKILL.md +0 -172
  262. package/skills/pikku-workflow/references/workflow-reference.md +0 -63
  263. package/skills/pikku-workflows-client/SKILL.md +0 -150
  264. package/skills/pikku-ws/SKILL.md +0 -47
@@ -2,8 +2,17 @@ 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 { pikkuState } from '@pikku/core/internal';
6
- import { loadUserBootstrap } from './load-user-project.js';
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';
15
+ import { resolveScenarioEnvironment } from './scenario-environment.js';
7
16
  const isScenario = (wf) => wf?.scenario === true;
8
17
  const listScenarios = (state) => Object.entries(state.workflows?.meta ?? {})
9
18
  .filter(([, wf]) => isScenario(wf))
@@ -12,50 +21,140 @@ const listScenarios = (state) => Object.entries(state.workflows?.meta ?? {})
12
21
  name: wf.name ?? id,
13
22
  description: wf.description ?? wf.summary ?? wf.title ?? null,
14
23
  tags: wf.tags ?? [],
24
+ skip: wf.skip,
15
25
  }));
26
+ /**
27
+ * Features and scenario registrations, merged across the main package and any
28
+ * addon packages. A feature holds the very config objects the registrations
29
+ * hold, which is what makes membership resolvable by identity.
30
+ */
31
+ const collectRegisteredWirings = () => {
32
+ const features = new Map();
33
+ const registrations = new Map();
34
+ for (const [packageName] of getAllPackageStates()) {
35
+ const scope = packageName === '__main__' ? null : packageName;
36
+ for (const [id, feature] of pikkuState(scope, 'workflows', 'features')) {
37
+ features.set(id, feature);
38
+ }
39
+ for (const [name, registration] of pikkuState(scope, 'workflows', 'registrations')) {
40
+ registrations.set(name, registration);
41
+ }
42
+ }
43
+ return { features, registrations };
44
+ };
16
45
  export const scenarioList = pikkuSessionlessFunc({
17
- func: async ({ logger, getInspectorState }) => {
18
- const state = await getInspectorState();
46
+ func: async ({ logger, config, getInspectorState }) => {
47
+ const state = await getInspectorState(false, false, false, true);
19
48
  const flows = listScenarios(state);
20
49
  if (flows.length === 0) {
21
50
  logger.info('No scenarios found (pikkuScenario exports).');
22
51
  return;
23
52
  }
53
+ await loadScenarioBootstrap(resolve(config.rootDir, config.outDir));
54
+ const { features, registrations } = collectRegisteredWirings();
55
+ const { entries, unresolved } = resolveFeatureScenarios(features, registrations);
56
+ for (const { featureId, index } of unresolved) {
57
+ logger.error(`Feature '${featureId}' scenario #${index} is not a registered scenario — it will not run.`);
58
+ }
59
+ const describe = (name) => flows.find((flow) => flow.name === name)?.description;
60
+ for (const [featureId, feature] of features) {
61
+ const featureTags = feature.tags?.length
62
+ ? ` [${feature.tags.join(', ')}]`
63
+ : '';
64
+ logger.info(`${feature.name ?? featureId}${featureTags}`);
65
+ if (feature.description) {
66
+ logger.info(` ${feature.description}`);
67
+ }
68
+ for (const entry of entries.filter((e) => e.featureId === featureId)) {
69
+ const data = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
70
+ logger.info(` - ${entry.scenarioName}${data}`);
71
+ }
72
+ }
73
+ const inAFeature = new Set(entries.map((entry) => entry.scenarioName));
24
74
  for (const flow of flows) {
75
+ if (inAFeature.has(flow.name))
76
+ continue;
25
77
  const tags = flow.tags.length ? ` [${flow.tags.join(', ')}]` : '';
26
78
  logger.info(`${flow.name}${tags}`);
27
- if (flow.description) {
28
- logger.info(` ${flow.description}`);
79
+ const description = describe(flow.name);
80
+ if (description) {
81
+ logger.info(` ${description}`);
29
82
  }
30
83
  }
31
84
  },
32
85
  });
33
86
  export const scenarioRun = pikkuSessionlessFunc({
34
- func: async ({ logger, config, getInspectorState, variables }, { environment, flows, tags, coverage }) => {
35
- const state = await getInspectorState(true);
36
- const environments = config.scenarios?.environments ?? {};
37
- const env = environments[environment];
38
- if (!env) {
39
- const known = Object.keys(environments);
40
- throw new Error(`Unknown scenario environment '${environment}'. ` +
41
- (known.length
42
- ? `Configured environments: ${known.join(', ')}`
43
- : `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
44
- }
45
- let selected = listScenarios(state);
46
- if (flows) {
47
- const names = new Set(flows.split(',').map((f) => f.trim()));
48
- const unknown = [...names].filter((n) => !selected.some((f) => f.name === n));
49
- if (unknown.length) {
50
- throw new Error(`Unknown scenario(s): ${unknown.join(', ')}. Available: ${selected.map((f) => f.name).join(', ')}`);
51
- }
52
- selected = selected.filter((f) => names.has(f.name));
53
- }
54
- if (tags) {
55
- const wanted = tags.split(',').map((t) => t.trim());
56
- selected = selected.filter((f) => wanted.some((t) => f.tags.includes(t)));
57
- }
58
- if (selected.length === 0) {
87
+ func: async ({ logger, config, getInspectorState, variables }, { environment, flows, features, tags, excludeTags, coverage, browser = true, spawn = false, keepAlive = false, trace = false, apiUrl, appUrl, }) => {
88
+ const state = await getInspectorState(true, false, false, true);
89
+ // Resolved once, so actors, step env, the browser driver and any spawned
90
+ // server all target the same place — including when the target only exists
91
+ // at run time and arrives through --api-url/--app-url.
92
+ const env = resolveScenarioEnvironment({
93
+ environment,
94
+ environments: config.scenarios?.environments ?? {},
95
+ apiUrl,
96
+ appUrl,
97
+ spawn,
98
+ });
99
+ if (apiUrl || appUrl) {
100
+ logger.info(`Overriding '${environment}': apiUrl ${env.apiUrl}${env.appUrl ? `, appUrl ${env.appUrl}` : ''}`);
101
+ }
102
+ if (spawn) {
103
+ const { hostname, port } = new URL(env.apiUrl);
104
+ const resolvedPort = Number(port || 80);
105
+ logger.info(`Starting a server for '${environment}' on ${env.apiUrl}`);
106
+ const server = await spawnDevServer({
107
+ cwd: config.rootDir,
108
+ port: resolvedPort,
109
+ hostname,
110
+ coverage,
111
+ env: { API_URL: env.apiUrl },
112
+ onOutput: (text) => process.stdout.write(text),
113
+ });
114
+ // Registered rather than wrapped in a try/finally: the run below sets
115
+ // process.exitCode and can throw, and an exit handler covers both without
116
+ // the whole command body having to nest inside one block.
117
+ if (!keepAlive) {
118
+ process.once('exit', server.stop);
119
+ process.once('SIGINT', () => {
120
+ server.stop();
121
+ process.exit(1);
122
+ });
123
+ process.once('SIGTERM', () => {
124
+ server.stop();
125
+ process.exit(1);
126
+ });
127
+ }
128
+ await server.waitUntilReady();
129
+ }
130
+ // Features live in runtime state, not inspector meta — their scenario lists
131
+ // may be built by an ordinary loop — so the project has to be loaded before
132
+ // anything can be selected.
133
+ await loadScenarioBootstrap(resolve(config.rootDir, config.outDir));
134
+ const { features: registeredFeatures, registrations } = collectRegisteredWirings();
135
+ const split = (value) => value ? value.split(',').map((part) => part.trim()) : undefined;
136
+ let { groups, unresolved } = buildScenarioPlan({
137
+ scenarios: listScenarios(state).map(({ name, tags: flowTags, skip }) => ({
138
+ name,
139
+ tags: flowTags,
140
+ skip,
141
+ })),
142
+ features: registeredFeatures,
143
+ registrations,
144
+ flows: split(flows),
145
+ featureIds: split(features),
146
+ tags: split(tags),
147
+ excludeTags: split(excludeTags),
148
+ });
149
+ if (unresolved.length > 0) {
150
+ for (const { featureId, index } of unresolved) {
151
+ logger.error(`Feature '${featureId}' scenario #${index} is not a registered scenario. ` +
152
+ `A feature references scenarios by imported identifier — a scenario built inline inside a feature is never registered.`);
153
+ }
154
+ process.exitCode = 1;
155
+ return;
156
+ }
157
+ if (groups.length === 0) {
59
158
  logger.error('No scenarios matched.');
60
159
  process.exitCode = 1;
61
160
  return;
@@ -65,15 +164,57 @@ export const scenarioRun = pikkuSessionlessFunc({
65
164
  throw new Error('SCENARIO_ACTOR_SECRET is not set — scenario actors cannot sign in. ' +
66
165
  'Export it in the environment running this command (never put it in pikku.config.json).');
67
166
  }
167
+ // Declared actors plus one per persona nobody declared a body for. Resolved
168
+ // once: the HTTP actors and the Playwright provider below must see the same
169
+ // registry, and codegen resolved the same way to type `ScenarioActorName`.
170
+ const scenarioActors = resolveScenarioActors(config.scenarios);
68
171
  const actors = createHttpScenarioActors({
69
172
  apiUrl: env.apiUrl,
70
173
  secret,
71
- actors: config.scenarios?.actors ?? {},
174
+ actors: scenarioActors,
72
175
  signInPath: env.signInPath,
73
176
  rpcPath: env.rpcPath,
74
177
  });
75
- await loadUserBootstrap(resolve(config.rootDir, config.outDir));
178
+ const functionsMeta = state.functions?.meta ?? {};
179
+ const scenarioNames = new Set(groups.flatMap((group) => group.entries.map((e) => e.scenarioName)));
180
+ const browserStepsByFlow = new Map([...scenarioNames]
181
+ .map((name) => [
182
+ name,
183
+ scenarioBrowserSteps(state.workflows?.meta?.[name], functionsMeta),
184
+ ])
185
+ .filter(([, steps]) => steps.length > 0));
186
+ // Two reasons a scenario is held back, both reported as SKIP rather than
187
+ // failed. `--no-browser` is the direct replacement for cucumber's `@console`
188
+ // tag, so a standard run stays green on a machine with no browser; a `skip`
189
+ // on the scenario itself is the project quarantining it, and the plan has
190
+ // already cleared that reason for anything named directly with `--flows`.
191
+ const skipReasonFor = (entry) => {
192
+ if (entry.skip)
193
+ return entry.skip;
194
+ if (!browser && browserStepsByFlow.has(entry.scenarioName)) {
195
+ return 'browser steps, --no-browser';
196
+ }
197
+ return undefined;
198
+ };
199
+ const skipped = [];
200
+ groups = groups
201
+ .map((group) => ({
202
+ ...group,
203
+ entries: group.entries.filter((entry) => {
204
+ const reason = skipReasonFor(entry);
205
+ if (!reason)
206
+ return true;
207
+ skipped.push({ name: entry.scenarioName, reason });
208
+ return false;
209
+ }),
210
+ }))
211
+ .filter((group) => group.entries.length > 0);
76
212
  const workflowService = new InMemoryWorkflowService();
213
+ const scenarioService = workflowService.setRunExtension((engine) => new PikkuScenarioService(engine));
214
+ scenarioService.setScenarioEnvironment({
215
+ apiUrl: env.apiUrl,
216
+ appUrl: env.appUrl,
217
+ });
77
218
  pikkuState(null, 'package', 'singletonServices', {
78
219
  logger,
79
220
  workflowService,
@@ -86,7 +227,51 @@ export const scenarioRun = pikkuSessionlessFunc({
86
227
  `'${environment}' (${env.apiUrl}), not local services.`);
87
228
  },
88
229
  };
230
+ const needsBrowser = groups.some((group) => group.entries.some((entry) => browserStepsByFlow.has(entry.scenarioName)));
231
+ const failureDir = join(resolve(config.rootDir, config.outDir), 'scenario-failures');
232
+ const browserLifecycle = scenarioBrowserLifecycle(needsBrowser
233
+ ? await (async () => {
234
+ const provider = await resolveScenarioBrowserProvider({
235
+ environment,
236
+ apiUrl: env.apiUrl,
237
+ appUrl: env.appUrl,
238
+ secret,
239
+ actors: scenarioActors,
240
+ signInPath: env.signInPath,
241
+ failureDir,
242
+ browserScenarios: [...browserStepsByFlow.keys()],
243
+ driver: config.scenarios?.browserDriver,
244
+ });
245
+ scenarioService.setScenarioBrowserProvider(provider);
246
+ return provider;
247
+ })()
248
+ : undefined);
89
249
  const results = [];
250
+ /**
251
+ * The step ladder is read back off the recorded run, so it needs no live
252
+ * step events — it is the same data the console renders. Joining it to the
253
+ * declared prose happens here, where the inspector state is; laying it out
254
+ * is the formatter's job.
255
+ */
256
+ const readRunSteps = async (service, runId, flowName) => {
257
+ const prose = collectScenarioStepProse(state.workflows?.meta?.[flowName], functionsMeta);
258
+ const steps = (await service.getRunSteps(runId)).map((step) => ({
259
+ stepName: step.stepName,
260
+ status: step.status,
261
+ durationMs: step.succeededAt
262
+ ? step.succeededAt.getTime() - step.createdAt.getTime()
263
+ : undefined,
264
+ error: step.error?.message,
265
+ stack: step.error?.stack,
266
+ expected: step.error?.expected,
267
+ input: step.data,
268
+ stepFunc: step.rpcName,
269
+ }));
270
+ return {
271
+ rows: scenarioStepRows(steps, prose),
272
+ failure: scenarioFailureFromSteps(steps, prose),
273
+ };
274
+ };
90
275
  const coverageActor = coverage ? Object.values(actors)[0] : undefined;
91
276
  let coverageActive = Boolean(coverageActor);
92
277
  if (coverage && !coverageActor) {
@@ -106,8 +291,22 @@ export const scenarioRun = pikkuSessionlessFunc({
106
291
  return null;
107
292
  }
108
293
  };
109
- for (const flow of selected) {
294
+ /**
295
+ * A feature hook is not a pikku function and not a run — the feature is a
296
+ * grouping, not something durable. It gets the same three arguments a
297
+ * scenario body gets, the CLI's own singletons included, and its result is
298
+ * discarded.
299
+ */
300
+ const singletonServices = pikkuState(null, 'package', 'singletonServices');
301
+ const runFeatureHook = async (hook) => {
302
+ await hook(singletonServices, undefined, { actors });
303
+ };
304
+ const hookFailures = [];
305
+ const runEntry = async (label, scenarioName, data) => {
110
306
  const startedAt = Date.now();
307
+ // Before the scenario, not after it: the last scenario's window is left
308
+ // open for headed debugging, while this one still starts clean.
309
+ await browserLifecycle.reset();
111
310
  if (coverageActive) {
112
311
  const reset = await invokeCoverage('pikkuScenarioResetLiveCoverage');
113
312
  if (reset && reset.enabled === false) {
@@ -119,20 +318,27 @@ export const scenarioRun = pikkuSessionlessFunc({
119
318
  await coverageActor?.invoke('pikkuScenarioResetStubs', null);
120
319
  }
121
320
  catch { }
321
+ let runId;
322
+ let runError;
122
323
  try {
123
- const { runId } = await workflowService.startWorkflow(flow.name, undefined, { type: 'cli' }, guardRpc, { actors });
324
+ // The id comes back through the callback rather than the return value
325
+ // because a failing scenario throws instead of returning — and a failed
326
+ // run is exactly the one whose steps are worth reading.
327
+ ;
328
+ ({ runId } = await workflowService.startWorkflow(scenarioName, data, { type: 'cli' }, guardRpc, { actors, onRunCreated: (id) => (runId = id) }));
124
329
  const run = await workflowService.getRun(runId);
125
330
  if (run?.status === 'completed') {
126
331
  results.push({
127
- name: flow.name,
332
+ name: label,
128
333
  status: 'passed',
129
334
  durationMs: Date.now() - startedAt,
130
335
  output: run.output,
131
336
  });
132
337
  }
133
338
  else {
339
+ runError = run?.error;
134
340
  results.push({
135
- name: flow.name,
341
+ name: label,
136
342
  status: 'failed',
137
343
  durationMs: Date.now() - startedAt,
138
344
  error: run?.error?.message ?? `status: ${run?.status}`,
@@ -140,22 +346,90 @@ export const scenarioRun = pikkuSessionlessFunc({
140
346
  }
141
347
  }
142
348
  catch (e) {
349
+ runError = { stack: e?.stack };
143
350
  results.push({
144
- name: flow.name,
351
+ name: label,
145
352
  status: 'failed',
146
353
  durationMs: Date.now() - startedAt,
147
354
  error: e?.message ?? String(e),
148
355
  });
149
356
  }
357
+ const result = results[results.length - 1];
358
+ let stepFailure;
359
+ if (runId) {
360
+ const read = await readRunSteps(workflowService, runId, scenarioName);
361
+ result.steps = read.rows;
362
+ stepFailure = read.failure;
363
+ }
364
+ if (result.status === 'failed') {
365
+ result.failure = {
366
+ // A scenario can also fail outside any step — a hook, or the start
367
+ // itself — and then the run's own error is all there is to report.
368
+ ...(stepFailure ?? {
369
+ message: result.error ?? 'unknown failure',
370
+ stack: runError?.stack,
371
+ expected: runError?.expected,
372
+ }),
373
+ browser: await browserLifecycle.captureFailure(label),
374
+ };
375
+ }
150
376
  if (coverageActive) {
151
377
  const report = await invokeCoverage('pikkuScenarioTakeLiveCoverage');
152
378
  if (report) {
153
- scenarioCoverage[flow.name] = report;
379
+ scenarioCoverage[label] = report;
154
380
  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}'`);
381
+ logger.info(` coverage: ${covered?.length ?? 0}/${report.summary?.total ?? 0} functions exercised by '${label}'`);
382
+ }
383
+ }
384
+ };
385
+ for (const group of groups) {
386
+ const groupName = group.featureName ?? group.featureId;
387
+ const label = (entry) => {
388
+ const data = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
389
+ return groupName
390
+ ? `${groupName} › ${entry.scenarioName}${data}`
391
+ : entry.scenarioName;
392
+ };
393
+ let beforeError;
394
+ if (group.before) {
395
+ try {
396
+ await runFeatureHook(group.before);
397
+ }
398
+ catch (e) {
399
+ beforeError = e;
400
+ }
401
+ }
402
+ try {
403
+ if (beforeError) {
404
+ // Setup failed, so nothing in the group ran. Reporting them as failed
405
+ // rather than skipped is the honest reading: they did not pass.
406
+ for (const entry of group.entries) {
407
+ results.push({
408
+ name: label(entry),
409
+ status: 'failed',
410
+ durationMs: 0,
411
+ error: `feature '${groupName}' before hook failed: ${beforeError?.message ?? beforeError}`,
412
+ });
413
+ }
414
+ }
415
+ else {
416
+ for (const entry of group.entries) {
417
+ await runEntry(label(entry), entry.scenarioName, entry.data);
418
+ }
419
+ }
420
+ }
421
+ finally {
422
+ if (group.after) {
423
+ try {
424
+ await runFeatureHook(group.after);
425
+ }
426
+ catch (e) {
427
+ hookFailures.push(`feature '${groupName}' after hook failed: ${e?.message ?? e}`);
428
+ }
156
429
  }
157
430
  }
158
431
  }
432
+ await browserLifecycle.close();
159
433
  if (coverage && Object.keys(scenarioCoverage).length > 0) {
160
434
  const coverageDir = join(resolve(config.rootDir, config.outDir), 'coverage');
161
435
  mkdirSync(coverageDir, { recursive: true });
@@ -167,17 +441,15 @@ export const scenarioRun = pikkuSessionlessFunc({
167
441
  }, null, 2) + '\n');
168
442
  logger.info(`Scenario coverage → ${outFile}`);
169
443
  }
170
- const failed = results.filter((r) => r.status === 'failed');
171
- for (const r of results) {
172
- if (r.status === 'passed') {
173
- logger.info(`PASS ${r.name} (${r.durationMs}ms)${r.output !== undefined ? ` → ${JSON.stringify(r.output)}` : ''}`);
174
- }
175
- else {
176
- logger.error(`FAIL ${r.name} (${r.durationMs}ms): ${r.error}`);
177
- }
444
+ const report = { environment, results, skipped, hookFailures };
445
+ for (const { level, text } of formatScenarioReport(report, {
446
+ trace,
447
+ projectRoot: config.rootDir,
448
+ })) {
449
+ logger[level](text);
178
450
  }
179
- logger.info(`${results.length - failed.length}/${results.length} scenarios passed against '${environment}'`);
180
- if (failed.length > 0) {
451
+ const failed = results.filter((r) => r.status === 'failed');
452
+ if (failed.length > 0 || hookFailures.length > 0) {
181
453
  process.exitCode = 1;
182
454
  }
183
455
  },
@@ -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,49 +1,25 @@
1
- import { readdir, readFile, mkdir, writeFile } from 'fs/promises';
1
+ import { readdir, mkdir, writeFile } from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
- import { join, dirname, sep } from 'path';
4
- import { fileURLToPath } from 'url';
3
+ import { dirname, join, sep } from 'path';
4
+ import { listSkillFiles, listSkillNames, readSkillFile } from '@pikku/skills';
5
5
  import { pikkuSessionlessFunc } from '#pikku';
6
6
  /**
7
- * Walk up from this compiled file to find the package root (the directory
8
- * containing the @pikku/cli package.json), then resolve `<root>/skills/`.
9
- * Works whether the file is loaded from `dist/...` (published package) or
10
- * `src/...` (local dev via tsx/vitest).
7
+ * Write one skill's files out to `dest`, reading through @pikku/skills rather
8
+ * than off a directory we resolved ourselves. That package reads from its own
9
+ * `skills/` folder when there is one and from its embedded manifest otherwise,
10
+ * which is what makes this work inside the `bun --compile` binaries they carry
11
+ * no filesystem copy, so the old "walk up to the @pikku/cli package root"
12
+ * lookup threw on every Homebrew install.
11
13
  */
12
- async function findSkillsDir() {
13
- let dir = dirname(fileURLToPath(import.meta.url));
14
- for (let i = 0; i < 8; i++) {
15
- const pkgPath = join(dir, 'package.json');
16
- if (existsSync(pkgPath)) {
17
- try {
18
- const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
19
- if (pkg.name === '@pikku/cli') {
20
- const skills = join(dir, 'skills');
21
- if (existsSync(skills))
22
- return skills;
23
- }
24
- }
25
- catch { }
26
- }
27
- const parent = dirname(dir);
28
- if (parent === dir)
29
- break;
30
- dir = parent;
31
- }
32
- throw new Error('Could not locate bundled skills directory. Was @pikku/cli installed correctly?');
33
- }
34
- async function copyDir(src, dest) {
35
- await mkdir(dest, { recursive: true });
36
- const entries = await readdir(src, { withFileTypes: true });
37
- for (const entry of entries) {
38
- const s = join(src, entry.name);
39
- const d = join(dest, entry.name);
40
- if (entry.isDirectory()) {
41
- await copyDir(s, d);
42
- }
43
- else if (entry.isFile()) {
44
- const content = await readFile(s);
45
- await writeFile(d, content);
46
- }
14
+ async function writeSkill(name, dest) {
15
+ for (const path of await listSkillFiles(name)) {
16
+ const contents = await readSkillFile(path);
17
+ if (contents === null)
18
+ continue;
19
+ // Paths are relative to the skills root, so drop the leading skill name.
20
+ const target = join(dest, ...path.split('/').slice(1));
21
+ await mkdir(dirname(target), { recursive: true });
22
+ await writeFile(target, contents, 'utf-8');
47
23
  }
48
24
  }
49
25
  /**
@@ -82,17 +58,16 @@ function parseInstallGroups(frontmatter) {
82
58
  }
83
59
  return [];
84
60
  }
85
- async function readSkillInstallGroups(skillsDir, skillName) {
86
- const skillPath = join(skillsDir, skillName, 'SKILL.md');
87
- if (!existsSync(skillPath))
61
+ async function readSkillInstallGroups(skillName) {
62
+ const content = await readSkillFile(`${skillName}/SKILL.md`);
63
+ if (content === null)
88
64
  return [];
89
- const content = await readFile(skillPath, 'utf-8');
90
65
  const match = content.match(/^---\n([\s\S]*?)\n---/);
91
66
  if (!match)
92
67
  return [];
93
68
  return parseInstallGroups(match[1]);
94
69
  }
95
- async function getWantedSkills(skillsDir, allEntries, { only, core = false, fabric = false, }) {
70
+ async function getWantedSkills(allEntries, { only, core = false, fabric = false, }) {
96
71
  const wanted = new Set();
97
72
  if (only) {
98
73
  for (const name of only
@@ -108,7 +83,7 @@ async function getWantedSkills(skillsDir, allEntries, { only, core = false, fabr
108
83
  ];
109
84
  if (requestedGroups.length > 0) {
110
85
  for (const name of allEntries) {
111
- const groups = await readSkillInstallGroups(skillsDir, name);
86
+ const groups = await readSkillInstallGroups(name);
112
87
  if (requestedGroups.some((group) => groups.includes(group))) {
113
88
  wanted.add(name);
114
89
  }
@@ -118,11 +93,7 @@ async function getWantedSkills(skillsDir, allEntries, { only, core = false, fabr
118
93
  }
119
94
  export const pikkuSkillsList = pikkuSessionlessFunc({
120
95
  func: async ({ logger }) => {
121
- const skillsDir = await findSkillsDir();
122
- const entries = (await readdir(skillsDir, { withFileTypes: true }))
123
- .filter((e) => e.isDirectory())
124
- .map((e) => e.name)
125
- .sort();
96
+ const entries = await listSkillNames();
126
97
  const installed = new Set();
127
98
  for (const relative of Object.values(AGENT_SKILL_DIRS)) {
128
99
  const dir = join(process.cwd(), relative);
@@ -154,12 +125,8 @@ export const pikkuSkillsInstall = pikkuSessionlessFunc({
154
125
  process.exitCode = 1;
155
126
  return;
156
127
  }
157
- const skillsDir = await findSkillsDir();
158
- const allEntries = (await readdir(skillsDir, { withFileTypes: true }))
159
- .filter((e) => e.isDirectory())
160
- .map((e) => e.name)
161
- .sort();
162
- const wanted = await getWantedSkills(skillsDir, allEntries, {
128
+ const allEntries = await listSkillNames();
129
+ const wanted = await getWantedSkills(allEntries, {
163
130
  only,
164
131
  core,
165
132
  fabric,
@@ -175,13 +142,12 @@ export const pikkuSkillsInstall = pikkuSessionlessFunc({
175
142
  let installed = 0;
176
143
  let skipped = 0;
177
144
  for (const name of wanted) {
178
- const src = join(skillsDir, name);
179
145
  const dest = join(installRoot, name);
180
146
  if (existsSync(dest) && !update) {
181
147
  skipped++;
182
148
  continue;
183
149
  }
184
- await copyDir(src, dest);
150
+ await writeSkill(name, dest);
185
151
  installed++;
186
152
  }
187
153
  const destLabel = `${AGENT_SKILL_DIRS[agent].split(sep).join('/')}/`;
@@ -19,6 +19,12 @@ export interface CodegenOptions {
19
19
  rootDir?: string;
20
20
  /** DB dialect — drives real-type-aware date typing. Defaults to 'sqlite'. */
21
21
  dialect?: Dialect;
22
+ /**
23
+ * Where the project's migrations live. Only used to attribute each table in
24
+ * `schemaJsonFile` to whoever declared it; omitting it makes every table read
25
+ * as app-owned, which is what a project with no generated migrations is.
26
+ */
27
+ migrationsDir?: string;
22
28
  }
23
29
  export interface CodegenResult {
24
30
  outFile: string;