@pikku/cli 0.12.91 → 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 (222) hide show
  1. package/LICENSE +106 -0
  2. package/console-app/assets/{index-CpkvmAGb.js → index-DDpIMCpy.js} +149 -149
  3. package/console-app/index.html +1 -1
  4. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
  8. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  9. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +15 -0
  13. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  14. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +21 -0
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  18. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  20. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  21. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  23. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  25. package/dist/.pikku/function/pikku-functions-meta.gen.json +35 -40
  26. package/dist/.pikku/function/pikku-functions.gen.js +1 -3
  27. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  28. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  31. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +2 -1
  32. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +2 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  37. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  39. package/dist/.pikku/pikku-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  45. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -1
  47. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
  48. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
  49. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
  50. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
  51. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +4 -0
  52. package/dist/.pikku/scenarios/schemas/register.gen.js +4 -0
  53. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  54. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  55. package/dist/.pikku/schemas/register.gen.js +5 -3
  56. package/dist/.pikku/schemas/schemas/FabricSecretsListOutput.schema.json +1 -1
  57. package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -0
  58. package/dist/.pikku/schemas/schemas/FabricSecretsRotateOutput.schema.json +1 -0
  59. package/dist/.pikku/schemas/schemas/FabricSecretsSetOutput.schema.json +1 -1
  60. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  61. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +1 -1
  62. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  63. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  64. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  65. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  66. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  67. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  68. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  69. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  70. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  71. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  72. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  73. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  74. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  75. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +2 -8
  76. package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +1 -1
  77. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +1 -1
  78. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  79. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  80. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  81. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  82. package/dist/bin/pikku-bin.mjs +2 -2
  83. package/dist/src/cli.wiring.js +6 -0
  84. package/dist/src/deploy/analyzer/analyzer.js +22 -5
  85. package/dist/src/deploy/build-pipeline.js +5 -1
  86. package/dist/src/fabric/fabric-commands.d.ts +37 -9
  87. package/dist/src/fabric/fabric-commands.js +12 -0
  88. package/dist/src/fabric/functions/domains-add.function.d.ts +4 -4
  89. package/dist/src/fabric/functions/secrets-list.function.d.ts +16 -4
  90. package/dist/src/fabric/functions/secrets-list.function.js +12 -10
  91. package/dist/src/fabric/functions/secrets-rotate.function.d.ts +24 -0
  92. package/dist/src/fabric/functions/secrets-rotate.function.js +36 -0
  93. package/dist/src/fabric/functions/secrets-set.function.d.ts +8 -4
  94. package/dist/src/fabric/functions/secrets-set.function.js +18 -7
  95. package/dist/src/fabric/lib/http.d.ts +7 -1
  96. package/dist/src/fabric/lib/sealed-box.d.ts +16 -0
  97. package/dist/src/fabric/lib/sealed-box.js +72 -0
  98. package/dist/src/functions/commands/dev.js +9 -0
  99. package/dist/src/functions/commands/pikku-command-bootstrap.js +1 -0
  100. package/dist/src/functions/commands/scenario-browser.d.ts +17 -2
  101. package/dist/src/functions/commands/scenario-browser.js +16 -8
  102. package/dist/src/functions/commands/scenario-environment.d.ts +32 -0
  103. package/dist/src/functions/commands/scenario-environment.js +61 -0
  104. package/dist/src/functions/commands/scenario.d.ts +6 -0
  105. package/dist/src/functions/commands/scenario.js +14 -9
  106. package/dist/src/functions/commands/skills.js +27 -61
  107. package/dist/src/functions/db/db-codegen.d.ts +6 -0
  108. package/dist/src/functions/db/db-codegen.js +9 -0
  109. package/dist/src/functions/db/db-migrator.js +14 -0
  110. package/dist/src/functions/db/local-db.d.ts +8 -0
  111. package/dist/src/functions/db/local-db.js +24 -2
  112. package/dist/src/functions/db/migration-identifiers.d.ts +58 -0
  113. package/dist/src/functions/db/migration-identifiers.js +262 -0
  114. package/dist/src/functions/db/migration-provenance.d.ts +35 -0
  115. package/dist/src/functions/db/migration-provenance.js +80 -0
  116. package/dist/src/functions/db/schema-sql.d.ts +43 -0
  117. package/dist/src/functions/db/schema-sql.js +135 -0
  118. package/dist/src/functions/db/sqlite/sqlite-kysely.js +22 -2
  119. package/dist/src/functions/wirings/functions/schemas.js +13 -1
  120. package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +6 -1
  121. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +10 -0
  122. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +102 -0
  123. package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +13 -0
  124. package/dist/src/functions/wirings/scenarios/scenario-partition.js +15 -2
  125. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.d.ts +33 -0
  126. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.js +49 -0
  127. package/dist/src/functions/workflows/all.workflow.js +2 -7
  128. package/dist/src/utils/file-writer.js +14 -2
  129. package/dist/src/utils/pikku-cli-config.d.ts +14 -0
  130. package/dist/src/utils/pikku-cli-config.js +34 -0
  131. package/dist/src/utils/remove-legacy-scaffold-file.d.ts +14 -0
  132. package/dist/src/utils/remove-legacy-scaffold-file.js +23 -0
  133. package/dist/src/utils/serialize-schemas.d.ts +3 -1
  134. package/dist/src/utils/serialize-schemas.js +39 -4
  135. package/dist/tsconfig.tsbuildinfo +1 -1
  136. package/package.json +9 -9
  137. package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +0 -1
  138. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +0 -1
  139. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +0 -31
  140. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +0 -10
  141. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +0 -104
  142. package/skills/pikku-addon/SKILL.md +0 -243
  143. package/skills/pikku-addon/references/addon-package-manifest.md +0 -63
  144. package/skills/pikku-ai-agent/SKILL.md +0 -231
  145. package/skills/pikku-ai-vercel/SKILL.md +0 -82
  146. package/skills/pikku-ai-voice/SKILL.md +0 -88
  147. package/skills/pikku-audit/SKILL.md +0 -175
  148. package/skills/pikku-aws/SKILL.md +0 -111
  149. package/skills/pikku-backblaze/SKILL.md +0 -71
  150. package/skills/pikku-better-auth/SKILL.md +0 -298
  151. package/skills/pikku-cli/SKILL.md +0 -198
  152. package/skills/pikku-cli/references/complete-example.md +0 -82
  153. package/skills/pikku-concepts/SKILL.md +0 -250
  154. package/skills/pikku-concepts/references/concept-mapping.md +0 -556
  155. package/skills/pikku-concepts/references/packages.md +0 -29
  156. package/skills/pikku-config/SKILL.md +0 -212
  157. package/skills/pikku-cron/SKILL.md +0 -214
  158. package/skills/pikku-deploy-azure/SKILL.md +0 -71
  159. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -82
  160. package/skills/pikku-deploy-express/SKILL.md +0 -86
  161. package/skills/pikku-deploy-fastify/SKILL.md +0 -73
  162. package/skills/pikku-deploy-lambda/SKILL.md +0 -110
  163. package/skills/pikku-deploy-nextjs/SKILL.md +0 -78
  164. package/skills/pikku-deploy-uws/SKILL.md +0 -88
  165. package/skills/pikku-deps/SKILL.md +0 -98
  166. package/skills/pikku-emails/SKILL.md +0 -163
  167. package/skills/pikku-fabric/SKILL.md +0 -318
  168. package/skills/pikku-fabric-debug/SKILL.md +0 -112
  169. package/skills/pikku-feature/SKILL.md +0 -258
  170. package/skills/pikku-gateway-slack/SKILL.md +0 -115
  171. package/skills/pikku-http/SKILL.md +0 -220
  172. package/skills/pikku-http/references/http-options.md +0 -55
  173. package/skills/pikku-i18n/SKILL.md +0 -137
  174. package/skills/pikku-info/SKILL.md +0 -100
  175. package/skills/pikku-jose/SKILL.md +0 -105
  176. package/skills/pikku-kysely/SKILL.md +0 -219
  177. package/skills/pikku-machine-auth/SKILL.md +0 -183
  178. package/skills/pikku-mcp/SKILL.md +0 -241
  179. package/skills/pikku-middleware/SKILL.md +0 -231
  180. package/skills/pikku-middleware/references/middleware-patterns.md +0 -61
  181. package/skills/pikku-mongodb/SKILL.md +0 -105
  182. package/skills/pikku-n8n-import/SKILL.md +0 -109
  183. package/skills/pikku-n8n-import/SPEC.md +0 -84
  184. package/skills/pikku-n8n-import/references/addon-mapping.md +0 -121
  185. package/skills/pikku-n8n-import/references/code-translation.md +0 -121
  186. package/skills/pikku-n8n-import/references/loops-and-control.md +0 -87
  187. package/skills/pikku-paraglide/SKILL.md +0 -117
  188. package/skills/pikku-permissions/SKILL.md +0 -192
  189. package/skills/pikku-pino/SKILL.md +0 -79
  190. package/skills/pikku-product-second-opinion/README.md +0 -43
  191. package/skills/pikku-product-second-opinion/SKILL.md +0 -158
  192. package/skills/pikku-product-second-opinion/example/sample-report.md +0 -90
  193. package/skills/pikku-product-second-opinion/references/report-template.md +0 -73
  194. package/skills/pikku-queue/SKILL.md +0 -240
  195. package/skills/pikku-react/SKILL.md +0 -212
  196. package/skills/pikku-react-query/SKILL.md +0 -242
  197. package/skills/pikku-realtime/SKILL.md +0 -236
  198. package/skills/pikku-realtime/references/other-routes.md +0 -23
  199. package/skills/pikku-redis/SKILL.md +0 -90
  200. package/skills/pikku-rpc/SKILL.md +0 -171
  201. package/skills/pikku-rtl/SKILL.md +0 -219
  202. package/skills/pikku-scenario/SKILL.md +0 -382
  203. package/skills/pikku-schedule/SKILL.md +0 -57
  204. package/skills/pikku-schema-ajv/SKILL.md +0 -62
  205. package/skills/pikku-schema-cfworker/SKILL.md +0 -63
  206. package/skills/pikku-security/SKILL.md +0 -108
  207. package/skills/pikku-services/SKILL.md +0 -248
  208. package/skills/pikku-services/references/audit-wire-service.md +0 -34
  209. package/skills/pikku-software-archaeology/README.md +0 -70
  210. package/skills/pikku-software-archaeology/SKILL.md +0 -186
  211. package/skills/pikku-software-archaeology/references/blueprint.schema.json +0 -625
  212. package/skills/pikku-software-archaeology/references/pikku-mapping.md +0 -49
  213. package/skills/pikku-software-archaeology/scripts/validate.mjs +0 -173
  214. package/skills/pikku-tag-middleware/SKILL.md +0 -13
  215. package/skills/pikku-template-clone/SKILL.md +0 -40
  216. package/skills/pikku-trigger/SKILL.md +0 -181
  217. package/skills/pikku-versioning/SKILL.md +0 -173
  218. package/skills/pikku-websocket/SKILL.md +0 -243
  219. package/skills/pikku-workflow/SKILL.md +0 -172
  220. package/skills/pikku-workflow/references/workflow-reference.md +0 -63
  221. package/skills/pikku-workflows-client/SKILL.md +0 -150
  222. package/skills/pikku-ws/SKILL.md +0 -47
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Catching camelCase columns at migrate time, before they can half-work.
3
+ *
4
+ * Pikku's Kysely runs with `CamelCasePlugin`, so `priceCents` in TypeScript is
5
+ * `price_cents` in SQL and nothing else. A migration that declares the column
6
+ * *as* `priceCents` is not merely unconventional — it is broken in the one way
7
+ * that hides itself: `.selectAll()` compiles to `SELECT *` and never names an
8
+ * identifier, so the table reads back perfectly, while the first query that
9
+ * names the column (`.select(['animal.priceCents'])`) asks for `price_cents`
10
+ * and gets `no such column`. The usual conclusion is that the plugin is broken,
11
+ * and the usual response is a raw `sql` template or a retreat to `.selectAll()`
12
+ * — both of which keep the real cause alive.
13
+ *
14
+ * There is no exception to escape. Even the Better Auth schema `db generate`
15
+ * writes is snake_case (`email_verified`, `user_id`), because Better Auth is
16
+ * handed the app's own Kysely and its camelCase field names compile the same
17
+ * way everyone else's do.
18
+ */
19
+ import { splitStatements } from './schema-sql.js';
20
+ export class CamelCaseIdentifierError extends Error {
21
+ offenders;
22
+ constructor(offenders) {
23
+ const lines = offenders.map((o) => ` ${o.file} ${o.column === null ? o.table : `${o.table}.${o.column}`} → ${o.suggestion}`);
24
+ super(`[PKU-DB-CAMEL] Migrations declare camelCase identifiers.\n\n` +
25
+ `Pikku's Kysely runs with CamelCasePlugin, which maps camelCase in TypeScript\n` +
26
+ `to snake_case in SQL. A camelCase column half-works: \`.selectAll()\` emits\n` +
27
+ `\`SELECT *\` so the table reads fine, but naming the column compiles it to\n` +
28
+ `snake_case and the database answers \`no such column\`.\n\n` +
29
+ `${lines.join('\n')}\n\n` +
30
+ `Fix the column definition in the migration file itself and replay your dev\n` +
31
+ `database from scratch. Do not write a RENAME COLUMN migration — that leaves\n` +
32
+ `the banned identifier in a SQL file forever.`);
33
+ this.offenders = offenders;
34
+ this.name = 'CamelCaseIdentifierError';
35
+ }
36
+ }
37
+ /**
38
+ * Strip comments so a word in prose can never be read as an identifier.
39
+ *
40
+ * `splitStatements` already skips over comments when it looks for a boundary,
41
+ * but it hands back the statement with them still in it, and `-- the userId
42
+ * column` would otherwise be flagged. Quoting rules are honoured for the same
43
+ * reason they are there: `'a -- b'` is a string, not a comment.
44
+ */
45
+ export function stripSqlComments(sql) {
46
+ let out = '';
47
+ let i = 0;
48
+ while (i < sql.length) {
49
+ const ch = sql[i];
50
+ if (ch === "'" || ch === '"' || ch === '`') {
51
+ const start = i;
52
+ i++;
53
+ while (i < sql.length) {
54
+ if (sql[i] === '\\' && ch === "'") {
55
+ i += 2;
56
+ continue;
57
+ }
58
+ if (sql[i] === ch) {
59
+ if (sql[i + 1] === ch) {
60
+ i += 2;
61
+ continue;
62
+ }
63
+ i++;
64
+ break;
65
+ }
66
+ i++;
67
+ }
68
+ out += sql.slice(start, i);
69
+ continue;
70
+ }
71
+ if (ch === '-' && sql[i + 1] === '-') {
72
+ const end = sql.indexOf('\n', i);
73
+ i = end === -1 ? sql.length : end;
74
+ continue;
75
+ }
76
+ if (ch === '/' && sql[i + 1] === '*') {
77
+ const end = sql.indexOf('*/', i + 2);
78
+ i = end === -1 ? sql.length : end + 2;
79
+ // A block comment can span lines, and removing it outright would join two
80
+ // statements onto one line. A space keeps the tokens either side apart.
81
+ out += ' ';
82
+ continue;
83
+ }
84
+ out += ch;
85
+ i++;
86
+ }
87
+ return out;
88
+ }
89
+ const IDENTIFIER = String.raw `"[^"]*"|\`[^\`]*\`|\[[^\]]*\]|[A-Za-z_][A-Za-z_0-9$]*`;
90
+ const CREATE_TABLE = new RegExp(String.raw `^CREATE\s+(?:TEMP(?:ORARY)?\s+|UNLOGGED\s+)*TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\S+?)\s*\(`, 'i');
91
+ const ALTER_TABLE = new RegExp(String.raw `^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?(\S+)`, 'i');
92
+ const ADD_COLUMN = new RegExp(String.raw `^\s*ADD\s+(?:COLUMN\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(${IDENTIFIER})`, 'i');
93
+ const COLUMN_NAME = new RegExp(String.raw `^\s*(${IDENTIFIER})`);
94
+ /**
95
+ * The words that open a table-level constraint rather than a column.
96
+ *
97
+ * Only consulted for an unquoted first token: `"check"` in quotes is a column
98
+ * named check, however unwise, and skipping it would let `"checkedAt"`'s
99
+ * neighbours hide behind it.
100
+ */
101
+ const CONSTRAINT_KEYWORDS = new Set([
102
+ 'constraint',
103
+ 'primary',
104
+ 'foreign',
105
+ 'unique',
106
+ 'check',
107
+ 'exclude',
108
+ 'index',
109
+ 'key',
110
+ 'like',
111
+ 'period',
112
+ 'fulltext',
113
+ 'spatial',
114
+ ]);
115
+ /** Drop the quoting a dialect happens to use, leaving the identifier itself. */
116
+ const unquote = (name) => /^["`[]/.test(name) ? name.slice(1, -1) : name;
117
+ const isCamelCase = (name) => /[a-z][A-Z]/.test(name);
118
+ const toSnakeCase = (name) => name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
119
+ /**
120
+ * Walk `sql` from `from` and return the offsets just inside and just outside
121
+ * the parenthesised group that starts there, or `null` if it never closes.
122
+ *
123
+ * Quote-aware, because a `)` inside `DEFAULT ')'` closes nothing.
124
+ */
125
+ function parenSpan(sql, from) {
126
+ let depth = 0;
127
+ let i = from;
128
+ while (i < sql.length) {
129
+ const ch = sql[i];
130
+ if (ch === "'" || ch === '"' || ch === '`') {
131
+ i++;
132
+ while (i < sql.length && sql[i] !== ch)
133
+ i++;
134
+ i++;
135
+ continue;
136
+ }
137
+ if (ch === '(') {
138
+ depth++;
139
+ if (depth === 1)
140
+ from = i + 1;
141
+ }
142
+ else if (ch === ')') {
143
+ depth--;
144
+ if (depth === 0)
145
+ return { start: from, end: i };
146
+ }
147
+ i++;
148
+ }
149
+ return null;
150
+ }
151
+ /**
152
+ * Split a definition list on the commas that separate its items.
153
+ *
154
+ * The commas inside `NUMERIC(10,2)`, a `CHECK (x IN ('a','b'))` or a compound
155
+ * `PRIMARY KEY (a, b)` belong to those and not to the list, so only depth zero
156
+ * counts.
157
+ */
158
+ function splitTopLevel(list) {
159
+ const items = [];
160
+ let depth = 0;
161
+ let start = 0;
162
+ let i = 0;
163
+ while (i < list.length) {
164
+ const ch = list[i];
165
+ if (ch === "'" || ch === '"' || ch === '`') {
166
+ i++;
167
+ while (i < list.length && list[i] !== ch)
168
+ i++;
169
+ i++;
170
+ continue;
171
+ }
172
+ if (ch === '(')
173
+ depth++;
174
+ else if (ch === ')')
175
+ depth--;
176
+ else if (ch === ',' && depth === 0) {
177
+ items.push(list.slice(start, i));
178
+ start = i + 1;
179
+ }
180
+ i++;
181
+ }
182
+ items.push(list.slice(start));
183
+ return items.filter((item) => item.trim().length > 0);
184
+ }
185
+ /**
186
+ * Every camelCase identifier one migration file declares.
187
+ *
188
+ * Only declarations are read — a `CREATE TABLE` column list and an `ALTER TABLE
189
+ * … ADD COLUMN`. Everywhere else an identifier merely refers to something that
190
+ * was declared somewhere, and reporting those would name the same mistake once
191
+ * per reference while adding nothing to the fix.
192
+ */
193
+ export function findCamelCaseIdentifiers(file, sql) {
194
+ const offenders = [];
195
+ for (const statement of splitStatements(stripSqlComments(sql))) {
196
+ const create = CREATE_TABLE.exec(statement);
197
+ if (create) {
198
+ const table = unquote(create[1].split('.').pop());
199
+ if (isCamelCase(table)) {
200
+ offenders.push({
201
+ file,
202
+ table,
203
+ column: null,
204
+ suggestion: toSnakeCase(table),
205
+ });
206
+ }
207
+ const span = parenSpan(statement, create.index + create[0].length - 1);
208
+ if (!span)
209
+ continue;
210
+ for (const item of splitTopLevel(statement.slice(span.start, span.end))) {
211
+ const name = COLUMN_NAME.exec(item)?.[1];
212
+ if (!name)
213
+ continue;
214
+ if (CONSTRAINT_KEYWORDS.has(name.toLowerCase()))
215
+ continue;
216
+ const column = unquote(name);
217
+ if (isCamelCase(column)) {
218
+ offenders.push({
219
+ file,
220
+ table,
221
+ column,
222
+ suggestion: toSnakeCase(column),
223
+ });
224
+ }
225
+ }
226
+ continue;
227
+ }
228
+ const alter = ALTER_TABLE.exec(statement);
229
+ if (!alter)
230
+ continue;
231
+ const table = unquote(alter[1].split('.').pop());
232
+ // Postgres lets one ALTER carry several actions, and only the ADDs declare.
233
+ for (const action of splitTopLevel(statement.slice(alter[0].length).replace(/;\s*$/, ''))) {
234
+ const name = ADD_COLUMN.exec(action)?.[1];
235
+ if (!name)
236
+ continue;
237
+ if (CONSTRAINT_KEYWORDS.has(name.toLowerCase()))
238
+ continue;
239
+ const column = unquote(name);
240
+ if (isCamelCase(column)) {
241
+ offenders.push({
242
+ file,
243
+ table,
244
+ column,
245
+ suggestion: toSnakeCase(column),
246
+ });
247
+ }
248
+ }
249
+ }
250
+ return offenders;
251
+ }
252
+ /**
253
+ * Bail unless every migration on disk is snake_case throughout.
254
+ *
255
+ * Reports all of them at once: a camelCase column is rarely alone, and a
256
+ * one-at-a-time failure turns a single edit into one migrate run per column.
257
+ */
258
+ export function assertSnakeCaseIdentifiers(migrations) {
259
+ const offenders = migrations.flatMap(({ name, sql }) => findCamelCaseIdentifiers(name, sql));
260
+ if (offenders.length > 0)
261
+ throw new CamelCaseIdentifierError(offenders);
262
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Who declared a table, and where the declaration came from.
3
+ *
4
+ * `source` is the stable identifier a consumer filters on — the same label
5
+ * `db generate` gives the migration it writes (`better-auth`, `pikku-runtime`,
6
+ * an addon's flattened package name). `origin` is the prose from the migration
7
+ * header, written for a human reading a tooltip.
8
+ */
9
+ export interface TableProvenance {
10
+ source: string;
11
+ origin: string;
12
+ }
13
+ /**
14
+ * Drop the schema qualifier and any quoting, so `"app"."user"` and `user` are
15
+ * the same key.
16
+ *
17
+ * Introspection reports a bare name on SQLite and a qualified one on Postgres,
18
+ * while a migration may write either — comparing on the bare, unquoted name is
19
+ * the only form both sides agree on.
20
+ */
21
+ export declare function bareUnquotedTableName(name: string): string;
22
+ /**
23
+ * Read the table→source map out of a project's migration directory.
24
+ *
25
+ * Provenance is decided at `db generate` time, but the schema JSON is written
26
+ * at `db migrate` time, and the two commands do not have to run together. The
27
+ * migration files are the durable record that survives between them: each
28
+ * generated one carries its origin in a header and its source in its filename,
29
+ * so re-reading them costs a directory scan and needs neither a Better Auth
30
+ * config load nor a live database.
31
+ *
32
+ * Tables absent from the result are app-owned — including every table when the
33
+ * project has no generated migrations at all.
34
+ */
35
+ export declare function readMigrationProvenance(migrationsDir: string): Map<string, TableProvenance>;
@@ -0,0 +1,80 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * The header `db generate` stamps on every migration it writes.
5
+ *
6
+ * Matching it is what separates a generated migration from one the project
7
+ * authored: a file without this line declares the project's own tables, and
8
+ * anything it creates is app-owned by definition.
9
+ */
10
+ const GENERATED_HEADER = /^-- Generated by `pikku db generate` from (.+)\.\s*$/m;
11
+ /** `0002-pikku-runtime.sql` → `pikku-runtime`. */
12
+ const MIGRATION_LABEL = /^\d+-(.+)\.sql$/;
13
+ /**
14
+ * Strip `--` line comments so a `CREATE TABLE` quoted inside a REVIEW note is
15
+ * not mistaken for one the migration actually runs.
16
+ */
17
+ const stripComments = (sql) => sql.replace(/--[^\n]*/g, '');
18
+ const CREATE_TABLE = /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(;]+)/gi;
19
+ /**
20
+ * Drop the schema qualifier and any quoting, so `"app"."user"` and `user` are
21
+ * the same key.
22
+ *
23
+ * Introspection reports a bare name on SQLite and a qualified one on Postgres,
24
+ * while a migration may write either — comparing on the bare, unquoted name is
25
+ * the only form both sides agree on.
26
+ */
27
+ export function bareUnquotedTableName(name) {
28
+ const last = name.split('.').pop() ?? name;
29
+ return last.replace(/^["'`[]|["'`\]]$/g, '');
30
+ }
31
+ /**
32
+ * Read the table→source map out of a project's migration directory.
33
+ *
34
+ * Provenance is decided at `db generate` time, but the schema JSON is written
35
+ * at `db migrate` time, and the two commands do not have to run together. The
36
+ * migration files are the durable record that survives between them: each
37
+ * generated one carries its origin in a header and its source in its filename,
38
+ * so re-reading them costs a directory scan and needs neither a Better Auth
39
+ * config load nor a live database.
40
+ *
41
+ * Tables absent from the result are app-owned — including every table when the
42
+ * project has no generated migrations at all.
43
+ */
44
+ export function readMigrationProvenance(migrationsDir) {
45
+ const provenance = new Map();
46
+ let files;
47
+ try {
48
+ files = readdirSync(migrationsDir);
49
+ }
50
+ catch {
51
+ // No migrations directory is the ordinary case for a project that has not
52
+ // run `db generate` yet — every table is app-owned.
53
+ return provenance;
54
+ }
55
+ for (const file of files.filter((f) => f.endsWith('.sql')).sort()) {
56
+ let sql;
57
+ try {
58
+ sql = readFileSync(join(migrationsDir, file), 'utf8');
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ const header = GENERATED_HEADER.exec(sql);
64
+ if (!header)
65
+ continue;
66
+ const label = MIGRATION_LABEL.exec(file);
67
+ if (!label)
68
+ continue;
69
+ const source = label[1];
70
+ const origin = header[1];
71
+ for (const match of stripComments(sql).matchAll(CREATE_TABLE)) {
72
+ const table = bareUnquotedTableName(match[1]);
73
+ // First writer wins: a later migration that recreates a table is fixing
74
+ // it, not claiming it.
75
+ if (!provenance.has(table))
76
+ provenance.set(table, { source, origin });
77
+ }
78
+ }
79
+ return provenance;
80
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Reading a schema source's own SQL back out, one table at a time.
3
+ *
4
+ * A source hands `db generate` both what must exist (`tables`) and what creates
5
+ * it (`sql`). The column map is a comparison surface — it answers "is this
6
+ * table there, and does it have these columns" — and it is deliberately lossy:
7
+ * primary keys, foreign keys, uniqueness, check constraints and indexes are all
8
+ * absent from it. So the moment the generator has to *create* something, the
9
+ * only honest source is the SQL, and this is what pulls the relevant part of it
10
+ * out.
11
+ */
12
+ /**
13
+ * Split a SQL script into its top-level statements, semicolons included.
14
+ *
15
+ * A naive `split(';')` is wrong in a way that only shows up later: a semicolon
16
+ * inside a string literal, a quoted identifier or a comment is not a statement
17
+ * boundary, and cutting there yields two fragments that are each valid-looking
18
+ * and neither of which does what the original did.
19
+ */
20
+ export declare function splitStatements(sql: string): string[];
21
+ /**
22
+ * Reduce a written table name to the form two sources can be compared on.
23
+ *
24
+ * The same table is `two_factor` to one writer, `"two_factor"` to Kysely and
25
+ * `public.two_factor` to Postgres introspection. Dropping the schema qualifier,
26
+ * the quoting and the case is the only shape all three agree on.
27
+ */
28
+ export declare function bareTableName(name: string): string;
29
+ /**
30
+ * Every statement in `sql` that builds `table`, in the order the source wrote
31
+ * them.
32
+ *
33
+ * The `CREATE TABLE` and its indexes, plus any `ALTER TABLE` the source uses to
34
+ * hang a constraint on it afterwards — which together are what the column list
35
+ * cannot express. Source order is preserved because it is load-bearing: an
36
+ * index cannot precede its table, and a table cannot precede one it references.
37
+ *
38
+ * An empty result means the source's SQL does not visibly create the table.
39
+ * That is a real answer rather than a failure — a source may create a table
40
+ * from something other than a literal `CREATE TABLE` — and the caller is
41
+ * expected to fall back rather than emit nothing.
42
+ */
43
+ export declare function tableCreationSql(sql: string, table: string): string[];
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Reading a schema source's own SQL back out, one table at a time.
3
+ *
4
+ * A source hands `db generate` both what must exist (`tables`) and what creates
5
+ * it (`sql`). The column map is a comparison surface — it answers "is this
6
+ * table there, and does it have these columns" — and it is deliberately lossy:
7
+ * primary keys, foreign keys, uniqueness, check constraints and indexes are all
8
+ * absent from it. So the moment the generator has to *create* something, the
9
+ * only honest source is the SQL, and this is what pulls the relevant part of it
10
+ * out.
11
+ */
12
+ /**
13
+ * Split a SQL script into its top-level statements, semicolons included.
14
+ *
15
+ * A naive `split(';')` is wrong in a way that only shows up later: a semicolon
16
+ * inside a string literal, a quoted identifier or a comment is not a statement
17
+ * boundary, and cutting there yields two fragments that are each valid-looking
18
+ * and neither of which does what the original did.
19
+ */
20
+ export function splitStatements(sql) {
21
+ const statements = [];
22
+ let start = 0;
23
+ let i = 0;
24
+ const closeQuote = (quote) => {
25
+ i++;
26
+ while (i < sql.length) {
27
+ if (sql[i] === '\\' && quote === "'") {
28
+ i += 2;
29
+ continue;
30
+ }
31
+ if (sql[i] === quote) {
32
+ // A doubled quote is an escaped one, not the end of the literal.
33
+ if (sql[i + 1] === quote) {
34
+ i += 2;
35
+ continue;
36
+ }
37
+ i++;
38
+ return;
39
+ }
40
+ i++;
41
+ }
42
+ };
43
+ while (i < sql.length) {
44
+ const ch = sql[i];
45
+ if (ch === "'" || ch === '"' || ch === '`') {
46
+ closeQuote(ch);
47
+ continue;
48
+ }
49
+ if (ch === '-' && sql[i + 1] === '-') {
50
+ const end = sql.indexOf('\n', i);
51
+ i = end === -1 ? sql.length : end + 1;
52
+ continue;
53
+ }
54
+ if (ch === '/' && sql[i + 1] === '*') {
55
+ const end = sql.indexOf('*/', i + 2);
56
+ i = end === -1 ? sql.length : end + 2;
57
+ continue;
58
+ }
59
+ // Postgres dollar quoting: everything between `$tag$` and its twin is a
60
+ // literal, and a function body written that way is full of semicolons.
61
+ if (ch === '$') {
62
+ const tag = /^\$[A-Za-z_0-9]*\$/.exec(sql.slice(i));
63
+ if (tag) {
64
+ const end = sql.indexOf(tag[0], i + tag[0].length);
65
+ i = end === -1 ? sql.length : end + tag[0].length;
66
+ continue;
67
+ }
68
+ }
69
+ if (ch === ';') {
70
+ const statement = sql.slice(start, i + 1).trim();
71
+ if (statement.length > 1)
72
+ statements.push(statement);
73
+ start = i + 1;
74
+ }
75
+ i++;
76
+ }
77
+ // A script whose last statement has no trailing semicolon still ran it.
78
+ const tail = sql.slice(start).trim();
79
+ if (tail.length > 0)
80
+ statements.push(tail);
81
+ return statements;
82
+ }
83
+ /**
84
+ * Reduce a written table name to the form two sources can be compared on.
85
+ *
86
+ * The same table is `two_factor` to one writer, `"two_factor"` to Kysely and
87
+ * `public.two_factor` to Postgres introspection. Dropping the schema qualifier,
88
+ * the quoting and the case is the only shape all three agree on.
89
+ */
90
+ export function bareTableName(name) {
91
+ const last = name.split('.').pop() ?? name;
92
+ return last.replace(/^["'`[]|["'`\]]$/g, '').toLowerCase();
93
+ }
94
+ const CREATE_TABLE = /^CREATE\s+(?:TEMP(?:ORARY)?\s+|UNLOGGED\s+)*TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)/i;
95
+ const CREATE_INDEX = /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?\S+\s+ON\s+(?:ONLY\s+)?([^\s(]+)/i;
96
+ const ALTER_TABLE = /^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?([^\s(]+)/i;
97
+ /**
98
+ * Every statement in `sql` that builds `table`, in the order the source wrote
99
+ * them.
100
+ *
101
+ * The `CREATE TABLE` and its indexes, plus any `ALTER TABLE` the source uses to
102
+ * hang a constraint on it afterwards — which together are what the column list
103
+ * cannot express. Source order is preserved because it is load-bearing: an
104
+ * index cannot precede its table, and a table cannot precede one it references.
105
+ *
106
+ * An empty result means the source's SQL does not visibly create the table.
107
+ * That is a real answer rather than a failure — a source may create a table
108
+ * from something other than a literal `CREATE TABLE` — and the caller is
109
+ * expected to fall back rather than emit nothing.
110
+ */
111
+ export function tableCreationSql(sql, table) {
112
+ const wanted = bareTableName(table);
113
+ const statements = [];
114
+ let creates = false;
115
+ for (const statement of splitStatements(sql)) {
116
+ const create = CREATE_TABLE.exec(statement);
117
+ if (create && bareTableName(create[1]) === wanted) {
118
+ creates = true;
119
+ statements.push(statement);
120
+ continue;
121
+ }
122
+ const index = CREATE_INDEX.exec(statement);
123
+ if (index && bareTableName(index[1]) === wanted) {
124
+ statements.push(statement);
125
+ continue;
126
+ }
127
+ const alter = ALTER_TABLE.exec(statement);
128
+ if (alter && bareTableName(alter[1]) === wanted) {
129
+ statements.push(statement);
130
+ }
131
+ }
132
+ // Indexes and alters without the table they belong to would fail on their own,
133
+ // and their presence says the table came from somewhere this cannot read.
134
+ return creates ? statements : [];
135
+ }
@@ -1,4 +1,21 @@
1
1
  import { Kysely, SqliteDialect, CamelCasePlugin, } from 'kysely';
2
+ /**
3
+ * Whether a value is a JSON column's worth of data rather than some other
4
+ * object that merely happens to be one. A `Map`, a `RegExp` or a class
5
+ * instance stringifies to `"{}"` or to a partial view of itself, which would
6
+ * silently persist an empty JSON blob where the caller meant something.
7
+ *
8
+ * Kept in lockstep with `isJsonEncodable` in `@pikku/kysely-sqlite`'s
9
+ * LibsqlWebDialect: whatever binds here under `pikku dev` has to bind the same
10
+ * way once the app is deployed, or the dev and deployed runtimes disagree
11
+ * about what a query may do.
12
+ */
13
+ function isJsonEncodable(v) {
14
+ if (Array.isArray(v))
15
+ return true;
16
+ const proto = Object.getPrototypeOf(v);
17
+ return proto === Object.prototype || proto === null;
18
+ }
2
19
  function coerce(v) {
3
20
  if (v === null || v === undefined)
4
21
  return null;
@@ -8,8 +25,11 @@ function coerce(v) {
8
25
  return v.toISOString();
9
26
  if (v instanceof Uint8Array)
10
27
  return v;
11
- if (typeof v === 'object')
12
- return JSON.stringify(v);
28
+ if (typeof v === 'object') {
29
+ if (isJsonEncodable(v))
30
+ return JSON.stringify(v);
31
+ throw new Error(`sqlite: unsupported argument type ${v.constructor?.name ?? 'object'}`);
32
+ }
13
33
  return v;
14
34
  }
15
35
  // A statement returns rows when it is a SELECT or carries a RETURNING clause.
@@ -1,6 +1,7 @@
1
1
  import { pikkuSessionlessFunc } from '#pikku';
2
2
  import { saveSchemas } from '../../../utils/serialize-schemas.js';
3
3
  import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
4
+ import { partitionRequiredSchemas } from '../scenarios/scenario-schema-partition.js';
4
5
  export const pikkuSchemas = pikkuSessionlessFunc({
5
6
  func: async ({ logger, config, getInspectorState }) => {
6
7
  const visitState = await getInspectorState();
@@ -16,7 +17,18 @@ export const pikkuSchemas = pikkuSessionlessFunc({
16
17
  Object.keys(visitState.schemas).length === 0) {
17
18
  return undefined;
18
19
  }
19
- await saveSchemas(logger, config.schemaDirectory, visitState.schemas, visitState.requiredSchemas, config.schema?.supportsImportAttributes ?? true, config.addonName || null);
20
+ const supportsImportAttributes = config.schema?.supportsImportAttributes ?? true;
21
+ const { appRequired, scenarioOnly } = partitionRequiredSchemas({
22
+ functionsMeta: visitState.functions.meta,
23
+ requiredSchemas: visitState.requiredSchemas,
24
+ getUniqueName: (name) => visitState.functions.typesMap.getUniqueName(name),
25
+ schemasFromTypes: config.schemasFromTypes,
26
+ });
27
+ await saveSchemas(logger, config.schemaDirectory, visitState.schemas, appRequired, supportsImportAttributes, config.addonName || null);
28
+ // Always written, even when empty — the scenario bootstrap imports it
29
+ // unconditionally, and a project that deleted its last scenario must stop
30
+ // registering the schemas it had.
31
+ await saveSchemas(logger, config.scenarioSchemaDirectory, visitState.schemas, scenarioOnly, supportsImportAttributes, config.addonName || null, 'scenario schemas');
20
32
  return true;
21
33
  },
22
34
  middleware: [
@@ -2,6 +2,7 @@ import { pikkuSessionlessFunc } from '#pikku';
2
2
  import { writeFileInDir } from '../../../utils/file-writer.js';
3
3
  import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
4
4
  import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
5
+ import { withoutScenarios } from '../scenarios/scenario-partition.js';
5
6
  export function filterInternalRPCMeta(internalMeta, functionsMeta) {
6
7
  const filteredInternalMeta = {};
7
8
  for (const [key, value] of Object.entries(internalMeta)) {
@@ -18,7 +19,11 @@ export const pikkuRPC = pikkuSessionlessFunc({
18
19
  // Keep direct function ids and unversioned aliases whose target function
19
20
  // survived filtering. Without the second check, aliases like `listCards`
20
21
  // get dropped even though `listCards@v2` still exists.
21
- const filteredInternalMeta = filterInternalRPCMeta(rpc.internalMeta, functions.meta);
22
+ // Scenarios are excluded along with the steps they call: a scenario name in
23
+ // the app's RPC meta is a name a deployed bundle carries and, in principle,
24
+ // dispatches. Scenario runs don't need the entry — the mapping is the
25
+ // identity, which is also the fallback when a name is absent.
26
+ const filteredInternalMeta = filterInternalRPCMeta(rpc.internalMeta, withoutScenarios(functions.meta));
22
27
  if (Object.keys(filteredInternalMeta).length > 0) {
23
28
  await writeFileInDir(logger, rpcInternalWiringMetaJsonFile, JSON.stringify(filteredInternalMeta, null, 2));
24
29
  const jsonImportPath = getFileImportRelativePath(rpcInternalWiringMetaFile, rpcInternalWiringMetaJsonFile, packageMappings);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Register the instrumentation into the running dev server's state, so the
3
+ * scenario runner can reach it over `/rpc/<name>` exactly as before.
4
+ *
5
+ * They are registered sessionless: `auth` decides whether a session is
6
+ * *required*, and a sessioned function would demand one regardless of the flag —
7
+ * which is what made `scaffold.scenarios: true` (as opposed to `'auth'`) refuse
8
+ * every call and log a warning about it.
9
+ */
10
+ export declare const registerScenarioInstrumentation: (requireAuth: boolean) => void;