@pikku/cli 0.12.26 → 0.12.28

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 (91) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/index-Ca6xJwNm.js +229 -0
  3. package/console-app/assets/{index-C52h1B_L.css → index-DwUzVI5k.css} +1 -1
  4. package/console-app/index.html +2 -2
  5. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  7. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  8. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  15. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  16. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  17. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  18. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  19. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  20. package/dist/.pikku/function/pikku-functions-meta.gen.json +192 -170
  21. package/dist/.pikku/function/pikku-functions.gen.js +3 -1
  22. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  23. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  24. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  25. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  26. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  27. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  28. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  29. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  30. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  31. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  33. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  35. package/dist/.pikku/pikku-types.gen.js +1 -1
  36. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  37. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  38. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  39. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.json +4 -0
  40. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  42. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  43. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +11 -10
  44. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  46. package/dist/.pikku/schemas/register.gen.js +13 -13
  47. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  48. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  49. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  50. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  51. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  52. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  53. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  54. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  55. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  56. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  57. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  58. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +8 -2
  59. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  60. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  61. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  62. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  63. package/dist/bin/pikku-bin.mjs +2 -2
  64. package/dist/src/fabric/functions/validate-core.js +6 -6
  65. package/dist/src/fabric/functions/validate.function.js +23 -7
  66. package/dist/src/functions/commands/tests-coverage.js +4 -2
  67. package/dist/src/functions/db/annotation-parser.d.ts +7 -7
  68. package/dist/src/functions/db/annotation-parser.js +61 -11
  69. package/dist/src/functions/db/db-codegen.d.ts +4 -0
  70. package/dist/src/functions/db/db-codegen.js +117 -15
  71. package/dist/src/functions/db/local-db.d.ts +6 -0
  72. package/dist/src/functions/db/local-db.js +134 -34
  73. package/dist/src/functions/db/postgres/postgres-introspector.d.ts +8 -2
  74. package/dist/src/functions/db/postgres/postgres-introspector.js +26 -14
  75. package/dist/src/functions/validate/workspace-validate.js +4 -1
  76. package/dist/src/functions/wirings/auth/pikku-command-auth.d.ts +1 -0
  77. package/dist/src/functions/wirings/auth/pikku-command-auth.js +22 -0
  78. package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +1 -0
  79. package/dist/src/functions/wirings/auth/serialize-auth-gen.js +115 -0
  80. package/dist/src/functions/workflows/all.workflow.js +1 -0
  81. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  82. package/dist/src/utils/pikku-cli-config.js +3 -0
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/package.json +7 -4
  85. package/skills/pikku-auth-js/SKILL.md +137 -117
  86. package/skills/pikku-middleware/SKILL.md +283 -0
  87. package/skills/pikku-permissions/SKILL.md +165 -0
  88. package/skills/pikku-security/SKILL.md +38 -177
  89. package/skills/pikku-services/SKILL.md +44 -7
  90. package/skills/pikku-tag-middleware/SKILL.md +13 -0
  91. package/console-app/assets/index-Ba9K10XZ.js +0 -232
@@ -111,8 +111,8 @@ export const pikkuTestsCoverage = pikkuSessionlessFunc({
111
111
  logger.error(`Verbose metadata not found at ${verboseMetaPath}. Run 'pikku all' first.`);
112
112
  process.exit(1);
113
113
  }
114
- const coverageFinal = join(functionsDir, 'coverage', 'coverage-final.json');
115
- const outDir = join(ftestDir, 'coverage');
114
+ const coverageFinal = join(functionsDir, '.coverage', 'coverage-final.json');
115
+ const outDir = join(ftestDir, '.coverage');
116
116
  const outFile = join(outDir, 'function-coverage.json');
117
117
  if (!noRun) {
118
118
  const findBin = (name, searchFrom) => {
@@ -153,6 +153,8 @@ export const pikkuTestsCoverage = pikkuSessionlessFunc({
153
153
  'src',
154
154
  '--include',
155
155
  'src/**',
156
+ '--report-dir',
157
+ '.coverage',
156
158
  '--reporter',
157
159
  'json',
158
160
  '--reporter',
@@ -1,5 +1,5 @@
1
1
  import type { ColumnKind } from './coercion-plugin.js';
2
- type Classification = 'public' | 'private' | 'secret';
2
+ type Classification = 'public' | 'private' | 'pii' | 'secret';
3
3
  type AnonymizeStrategy = 'fake:email' | 'fake:name' | 'hash' | 'keep' | null;
4
4
  export interface ColAnnotation {
5
5
  kind?: ColumnKind;
@@ -19,13 +19,13 @@ export declare function annotationFromName(colName: string): {
19
19
  kind: ColumnKind;
20
20
  } | null;
21
21
  /**
22
- * Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @secret[:strategy]`
22
+ * Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @pii[:strategy] | @secret[:strategy]`
23
23
  * inline annotations from migration SQL files in `migrationsDir`.
24
- *
25
- * Multiple annotations on the same comment line are supported, e.g.:
26
- * `deleted_at TIMESTAMP -- @date @private:keep`
27
- *
28
- * Covers both CREATE TABLE body lines and ALTER TABLE ... ADD [COLUMN] statements.
29
24
  */
30
25
  export declare function parseAnnotations(migrationsDir: string): AnnotationMap;
26
+ /**
27
+ * Load annotations for a project. Tries `db/annotations.ts` sidecar first;
28
+ * falls back to SQL comment parsing from `migrationsDir` if not found.
29
+ */
30
+ export declare function loadAnnotations(rootDir: string, migrationsDir?: string): AnnotationMap;
31
31
  export {};
@@ -1,4 +1,4 @@
1
- import { readFileSync, readdirSync } from 'node:fs';
1
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  /**
4
4
  * Determine column kind from naming conventions:
@@ -12,11 +12,51 @@ export function annotationFromName(colName) {
12
12
  return { kind: 'bool' };
13
13
  return null;
14
14
  }
15
+ // ── Load from db/annotations.gen.json sidecar ────────────────────────────────
16
+ /**
17
+ * Try to load annotations from a `db/annotations.gen.json` sidecar generated
18
+ * by `yarn db:types`. Returns null if the file doesn't exist.
19
+ *
20
+ * The JSON file uses snake_case keys (raw DB names) so it can be read
21
+ * directly without any conversion. It is emitted by bin/db-classify.ts.
22
+ */
23
+ function loadAnnotationsSidecar(rootDir) {
24
+ const jsonPath = join(rootDir, 'db', 'annotations.gen.json');
25
+ if (!existsSync(jsonPath))
26
+ return null;
27
+ try {
28
+ const raw = JSON.parse(readFileSync(jsonPath, 'utf8'));
29
+ const result = {};
30
+ for (const [table, cols] of Object.entries(raw)) {
31
+ result[table] = {};
32
+ for (const [col, ann] of Object.entries(cols)) {
33
+ if (!ann)
34
+ continue;
35
+ const entry = {};
36
+ if (ann.kind === 'bool' || ann.kind === 'date' || ann.kind === 'json')
37
+ entry.kind = ann.kind;
38
+ if (ann.tsType)
39
+ entry.tsType = ann.tsType;
40
+ const vis = ann.visibility;
41
+ if (vis === 'public' || vis === 'private' || vis === 'secret')
42
+ entry.classification = vis;
43
+ result[table][col] = entry;
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ // ── SQL comment parsing (fallback) ───────────────────────────────────────────
15
53
  function parseStrategy(s) {
16
54
  if (!s)
17
55
  return null;
18
56
  const valid = ['fake:email', 'fake:name', 'hash', 'keep'];
19
- return valid.includes(s) ? s : null;
57
+ return valid.includes(s)
58
+ ? s
59
+ : null;
20
60
  }
21
61
  function parseComment(comment) {
22
62
  const ann = {};
@@ -34,26 +74,25 @@ function parseComment(comment) {
34
74
  ann.tsType = jsonM[1].trim();
35
75
  }
36
76
  }
37
- const classM = comment.match(/@(public|private|secret)(?::([^\s@]+))?/i);
77
+ const classM = comment.match(/@(public|private|pii|secret)(?::([^\s@]+))?/i);
38
78
  if (classM) {
39
79
  ann.classification = classM[1].toLowerCase();
40
- ann.anonymize = parseStrategy(classM[2]);
80
+ const strategy = parseStrategy(classM[2]);
81
+ if (strategy !== null)
82
+ ann.anonymize = strategy;
41
83
  }
42
84
  return ann;
43
85
  }
44
86
  /**
45
- * Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @secret[:strategy]`
87
+ * Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @pii[:strategy] | @secret[:strategy]`
46
88
  * inline annotations from migration SQL files in `migrationsDir`.
47
- *
48
- * Multiple annotations on the same comment line are supported, e.g.:
49
- * `deleted_at TIMESTAMP -- @date @private:keep`
50
- *
51
- * Covers both CREATE TABLE body lines and ALTER TABLE ... ADD [COLUMN] statements.
52
89
  */
53
90
  export function parseAnnotations(migrationsDir) {
54
91
  let files;
55
92
  try {
56
- files = readdirSync(migrationsDir).filter((f) => f.endsWith('.sql')).sort();
93
+ files = readdirSync(migrationsDir)
94
+ .filter((f) => f.endsWith('.sql'))
95
+ .sort();
57
96
  }
58
97
  catch {
59
98
  return {};
@@ -91,3 +130,14 @@ export function parseAnnotations(migrationsDir) {
91
130
  }
92
131
  return result;
93
132
  }
133
+ // ── Public entry point ────────────────────────────────────────────────────────
134
+ /**
135
+ * Load annotations for a project. Tries `db/annotations.ts` sidecar first;
136
+ * falls back to SQL comment parsing from `migrationsDir` if not found.
137
+ */
138
+ export function loadAnnotations(rootDir, migrationsDir) {
139
+ const sidecar = loadAnnotationsSidecar(rootDir);
140
+ if (sidecar)
141
+ return sidecar;
142
+ return migrationsDir ? parseAnnotations(migrationsDir) : {};
143
+ }
@@ -3,16 +3,20 @@ export interface CodegenOptions {
3
3
  outFile: string;
4
4
  coercionFile: string;
5
5
  manifestFile?: string;
6
+ classificationMapFile?: string;
6
7
  camelCase?: boolean;
8
+ rootDir?: string;
7
9
  migrationsDir?: string;
8
10
  }
9
11
  export interface CodegenResult {
10
12
  outFile: string;
11
13
  coercionFile: string;
12
14
  manifestFile?: string;
15
+ classificationMapFile?: string;
13
16
  written: boolean;
14
17
  coercionWritten: boolean;
15
18
  manifestWritten: boolean;
19
+ classificationMapWritten: boolean;
16
20
  tables: string[];
17
21
  }
18
22
  /**
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
- import { parseAnnotations, annotationFromName, } from './annotation-parser.js';
3
+ import { loadAnnotations, parseAnnotations, annotationFromName, } from './annotation-parser.js';
4
4
  // ─── Name helpers ─────────────────────────────────────────────────────────────
5
5
  function snakeToPascal(name) {
6
6
  return name
@@ -23,7 +23,9 @@ function mapType(sqlType) {
23
23
  return 'string';
24
24
  if (upper.includes('BLOB') || upper === 'BYTEA')
25
25
  return 'Buffer';
26
- if (upper.includes('REAL') || upper.includes('FLOA') || upper.includes('DOUB'))
26
+ if (upper.includes('REAL') ||
27
+ upper.includes('FLOA') ||
28
+ upper.includes('DOUB'))
27
29
  return 'number';
28
30
  if (upper.includes('NUMERIC') || upper.includes('DECIMAL'))
29
31
  return 'number';
@@ -88,7 +90,11 @@ function columnTypeExpression(col, annotation, classification) {
88
90
  return `Generated<${base}${nullable ? ' | null' : ''}>`;
89
91
  return nullable ? `${base} | null` : base;
90
92
  }
91
- const B = classification === 'secret' ? 'Secret' : 'Private';
93
+ const B = classification === 'secret'
94
+ ? 'Secret'
95
+ : classification === 'pii'
96
+ ? 'Pii'
97
+ : 'Private';
92
98
  const sBase = selectBase(annotation, col);
93
99
  const iBase = insertBase(annotation, col);
94
100
  const selectT = nullable ? `${B}<${sBase}> | null` : `${B}<${sBase}>`;
@@ -98,9 +104,14 @@ function columnTypeExpression(col, annotation, classification) {
98
104
  const updateT = nullable ? `${iBase} | null` : iBase;
99
105
  return `ColumnType<${selectT}, ${insertT}, ${updateT}>`;
100
106
  }
107
+ /** Strip optional schema prefix (e.g. "app.user" → "user"). */
108
+ function bareTableName(name) {
109
+ const dot = name.indexOf('.');
110
+ return dot >= 0 ? name.slice(dot + 1) : name;
111
+ }
101
112
  function emitInterface(table, camelCase, explicitAnnotations) {
102
113
  const ifaceName = snakeToPascal(table.name);
103
- const tableCols = explicitAnnotations[table.name] ?? {};
114
+ const tableCols = explicitAnnotations[bareTableName(table.name)] ?? {};
104
115
  const fields = table.columns
105
116
  .map((col) => {
106
117
  const fieldName = camelCase ? snakeToCamel(col.name) : col.name;
@@ -122,7 +133,7 @@ function emitInterface(table, camelCase, explicitAnnotations) {
122
133
  function emitManifest(tables, explicitAnnotations) {
123
134
  const tableEntries = tables
124
135
  .map((table) => {
125
- const tableCols = explicitAnnotations[table.name] ?? {};
136
+ const tableCols = explicitAnnotations[bareTableName(table.name)] ?? {};
126
137
  const colEntries = table.columns
127
138
  .map((col) => {
128
139
  const ann = tableCols[col.name];
@@ -149,6 +160,63 @@ function emitManifest(tables, explicitAnnotations) {
149
160
  ``,
150
161
  ].join('\n');
151
162
  }
163
+ // ─── Classification map type emitter ─────────────────────────────────────────
164
+ /**
165
+ * Emits a `DbClassificationMap` type declaration that the developer's
166
+ * hand-authored `db/classifications.ts` must satisfy. Every table and column
167
+ * present in the current schema appears as a required key — TypeScript will
168
+ * flag added or removed columns.
169
+ */
170
+ function emitClassificationMap(tables) {
171
+ const colEntry = ` security: 'public' | 'private' | 'pii' | 'secret' | 'encrypted'\n classification?: 'fake:email' | 'fake:name' | 'hash' | 'keep'\n description?: string`;
172
+ // Group tables by schema (for postgres schema.table names)
173
+ const schemaMap = new Map();
174
+ for (const table of tables) {
175
+ const dot = table.name.indexOf('.');
176
+ const schema = dot >= 0 ? table.name.slice(0, dot) : '';
177
+ const bare = dot >= 0 ? table.name.slice(dot + 1) : table.name;
178
+ if (!schemaMap.has(schema))
179
+ schemaMap.set(schema, new Map());
180
+ schemaMap.get(schema).set(bare, table.columns.map((c) => c.name));
181
+ }
182
+ const lines = [
183
+ `// Generated by @pikku/cli — do not edit by hand.`,
184
+ `// Run \`pikku db migrate\` to refresh.`,
185
+ `// Use this type in db/classifications.ts:`,
186
+ `// import type { DbClassificationMap } from './.pikku/db/classification-map.gen.d.ts'`,
187
+ `// export const classifications = { ... } satisfies DbClassificationMap`,
188
+ ``,
189
+ `export type ColumnEntry = {`,
190
+ `${colEntry}`,
191
+ `}`,
192
+ ``,
193
+ `export type DbClassificationMap = {`,
194
+ ];
195
+ for (const [schema, tables] of schemaMap) {
196
+ if (schema) {
197
+ lines.push(` ${JSON.stringify(schema)}: {`);
198
+ for (const [table, cols] of tables) {
199
+ lines.push(` ${JSON.stringify(table)}: {`);
200
+ for (const col of cols) {
201
+ lines.push(` ${JSON.stringify(col)}: ColumnEntry`);
202
+ }
203
+ lines.push(` }`);
204
+ }
205
+ lines.push(` }`);
206
+ }
207
+ else {
208
+ for (const [table, cols] of tables) {
209
+ lines.push(` ${JSON.stringify(table)}: {`);
210
+ for (const col of cols) {
211
+ lines.push(` ${JSON.stringify(col)}: ColumnEntry`);
212
+ }
213
+ lines.push(` }`);
214
+ }
215
+ }
216
+ }
217
+ lines.push(`}`, ``);
218
+ return lines.join('\n');
219
+ }
152
220
  /**
153
221
  * Introspect `introspector` and emit:
154
222
  * - `schema.d.ts` Kysely DB type with classification brands
@@ -162,9 +230,11 @@ export async function generateSchemaTypes(introspector, options) {
162
230
  name,
163
231
  columns: await introspector.getColumns(name),
164
232
  })));
165
- const explicitAnnotations = options.migrationsDir
166
- ? parseAnnotations(options.migrationsDir)
167
- : {};
233
+ const explicitAnnotations = options.rootDir
234
+ ? loadAnnotations(options.rootDir, options.migrationsDir)
235
+ : options.migrationsDir
236
+ ? parseAnnotations(options.migrationsDir)
237
+ : {};
168
238
  // ── schema.d.ts ─────────────────────────────────────────────────────────────
169
239
  const interfaces = tables
170
240
  .map((t) => emitInterface(t, camelCase, explicitAnnotations))
@@ -188,8 +258,9 @@ export async function generateSchemaTypes(introspector, options) {
188
258
  ` ? ColumnType<S, I | undefined, U>`,
189
259
  ` : ColumnType<T, T | undefined, T>`,
190
260
  ``,
191
- `export type Private<T> = T & { readonly __pii__: 'private' }`,
192
- `export type Secret<T> = T & { readonly __pii__: 'secret' }`,
261
+ `export type Private<T> = T & { readonly __classification__: 'private' }`,
262
+ `export type Pii<T> = T & { readonly __classification__: 'pii' }`,
263
+ `export type Secret<T> = T & { readonly __classification__: 'secret' }`,
193
264
  ``,
194
265
  interfaces,
195
266
  ``,
@@ -201,7 +272,7 @@ export async function generateSchemaTypes(introspector, options) {
201
272
  // ── coercion.gen.ts ──────────────────────────────────────────────────────────
202
273
  const coercionMap = {};
203
274
  for (const table of tables) {
204
- const tableCols = explicitAnnotations[table.name] ?? {};
275
+ const tableCols = explicitAnnotations[bareTableName(table.name)] ?? {};
205
276
  for (const col of table.columns) {
206
277
  const sqlAnn = tableCols[col.name];
207
278
  const kind = sqlAnn?.kind ?? annotationFromName(col.name)?.kind;
@@ -230,28 +301,51 @@ export async function generateSchemaTypes(introspector, options) {
230
301
  ``,
231
302
  ].join('\n');
232
303
  // ── classification.gen.ts ───────────────────────────────────────────────────
233
- const manifestBody = options.manifestFile ? emitManifest(tables, explicitAnnotations) : null;
304
+ const manifestBody = options.manifestFile
305
+ ? emitManifest(tables, explicitAnnotations)
306
+ : null;
307
+ // ── classification-map.gen.d.ts ──────────────────────────────────────────────
308
+ const classificationMapBody = options.classificationMapFile
309
+ ? emitClassificationMap(tables)
310
+ : null;
234
311
  // ── write files ───────────────────────────────────────────────────────────────
235
312
  let existingSchema = null;
236
313
  let existingCoercion = null;
237
314
  let existingManifest = null;
315
+ let existingClassificationMap = null;
238
316
  try {
239
317
  existingSchema = readFileSync(options.outFile, 'utf8');
240
318
  }
241
- catch { /* ok */ }
319
+ catch {
320
+ /* ok */
321
+ }
242
322
  try {
243
323
  existingCoercion = readFileSync(options.coercionFile, 'utf8');
244
324
  }
245
- catch { /* ok */ }
325
+ catch {
326
+ /* ok */
327
+ }
246
328
  if (options.manifestFile) {
247
329
  try {
248
330
  existingManifest = readFileSync(options.manifestFile, 'utf8');
249
331
  }
250
- catch { /* ok */ }
332
+ catch {
333
+ /* ok */
334
+ }
335
+ }
336
+ if (options.classificationMapFile) {
337
+ try {
338
+ existingClassificationMap = readFileSync(options.classificationMapFile, 'utf8');
339
+ }
340
+ catch {
341
+ /* ok */
342
+ }
251
343
  }
252
344
  const schemaChanged = existingSchema !== schemaBody;
253
345
  const coercionChanged = existingCoercion !== coercionBody;
254
346
  const manifestChanged = manifestBody !== null && existingManifest !== manifestBody;
347
+ const classificationMapChanged = classificationMapBody !== null &&
348
+ existingClassificationMap !== classificationMapBody;
255
349
  if (schemaChanged) {
256
350
  mkdirSync(dirname(options.outFile), { recursive: true });
257
351
  writeFileSync(options.outFile, schemaBody, 'utf8');
@@ -264,13 +358,21 @@ export async function generateSchemaTypes(introspector, options) {
264
358
  mkdirSync(dirname(options.manifestFile), { recursive: true });
265
359
  writeFileSync(options.manifestFile, manifestBody, 'utf8');
266
360
  }
361
+ if (classificationMapChanged &&
362
+ options.classificationMapFile &&
363
+ classificationMapBody) {
364
+ mkdirSync(dirname(options.classificationMapFile), { recursive: true });
365
+ writeFileSync(options.classificationMapFile, classificationMapBody, 'utf8');
366
+ }
267
367
  return {
268
368
  outFile: options.outFile,
269
369
  coercionFile: options.coercionFile,
270
370
  manifestFile: options.manifestFile,
371
+ classificationMapFile: options.classificationMapFile,
271
372
  written: schemaChanged,
272
373
  coercionWritten: coercionChanged,
273
374
  manifestWritten: manifestChanged,
375
+ classificationMapWritten: classificationMapChanged,
274
376
  tables: tables.map((t) => t.name),
275
377
  };
276
378
  }
@@ -5,10 +5,14 @@ import { type ZodCodegenResult } from './zod-codegen.js';
5
5
  import { type SeedResult } from './sqlite/seed.js';
6
6
  import type { UserConfigShape } from '../commands/db-shared.js';
7
7
  interface ResolvedDbBase {
8
+ rootDir: string;
8
9
  migrationsDir: string;
9
10
  schemaFile: string;
10
11
  coercionFile: string;
11
12
  manifestFile: string;
13
+ classificationMapFile: string;
14
+ classificationsFile: string;
15
+ classificationsGenJsonFile: string;
12
16
  zodFile: string;
13
17
  camelCase: boolean;
14
18
  }
@@ -34,6 +38,8 @@ export interface MigrateAndCodegenOutcome {
34
38
  migrate: MigrateResult;
35
39
  codegen: CodegenResult;
36
40
  zod: ZodCodegenResult;
41
+ classificationsScaffolded: boolean;
42
+ classificationsJsonWritten: boolean;
37
43
  }
38
44
  export declare function migrateAndCodegen(resolved: ResolvedDb): Promise<MigrateAndCodegenOutcome>;
39
45
  export declare function seed(resolved: ResolvedSqliteDb): Promise<SeedResult>;
@@ -1,5 +1,8 @@
1
- import { existsSync, mkdirSync, rmSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, } from 'node:fs';
2
2
  import { resolve, isAbsolute, relative, dirname, join } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+ import { fileURLToPath } from 'node:url';
3
6
  import { migrate } from './db-migrator.js';
4
7
  import { generateSchemaTypes } from './db-codegen.js';
5
8
  import { generateZodTypes } from './zod-codegen.js';
@@ -18,10 +21,14 @@ import { PostgresIntrospector } from './postgres/postgres-introspector.js';
18
21
  */
19
22
  export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
20
23
  const base = (sub) => ({
24
+ rootDir,
21
25
  migrationsDir: resolveAgainst(rootDir, sub),
22
26
  schemaFile: join(outDir, 'db', 'schema.d.ts'),
23
27
  coercionFile: join(outDir, 'db', 'coercion.gen.ts'),
24
28
  manifestFile: join(outDir, 'db', 'classification.gen.ts'),
29
+ classificationMapFile: join(outDir, 'db', 'classification-map.gen.d.ts'),
30
+ classificationsFile: join(rootDir, 'db', 'annotations.ts'),
31
+ classificationsGenJsonFile: join(outDir, 'db', 'annotations.gen.json'),
25
32
  zodFile: join(outDir, 'db', 'zod.gen.ts'),
26
33
  camelCase: true,
27
34
  });
@@ -43,7 +50,7 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
43
50
  dialect: 'sqlite',
44
51
  dbFile: resolveAgainst(rootDir, userConfig.sqliteDb),
45
52
  runtimeDir: resolvedRuntimeDir,
46
- seedFile: resolveAgainst(rootDir, 'db/seed.sql'),
53
+ seedFile: resolveAgainst(rootDir, 'db/sqlite-seed.sql'),
47
54
  ...base('db/sqlite'),
48
55
  };
49
56
  }
@@ -60,61 +67,71 @@ function resolveAgainst(root, p) {
60
67
  return isAbsolute(p) ? p : resolve(root, p);
61
68
  }
62
69
  export async function migrateAndCodegen(resolved) {
70
+ let migrateResult;
71
+ let codegenResult;
63
72
  if (resolved.dialect === 'sqlite') {
64
73
  mkdirSync(dirname(resolved.dbFile), { recursive: true });
65
74
  const runtime = await loadSqliteRuntime();
66
75
  const db = runtime.open(resolved.dbFile);
67
76
  try {
68
77
  const executor = new SqliteMigrationExecutor(db);
69
- const migrateResult = await migrate(executor, resolved.migrationsDir);
78
+ migrateResult = await migrate(executor, resolved.migrationsDir);
70
79
  const introspector = new SqliteIntrospector(db);
71
- const codegenResult = await generateSchemaTypes(introspector, {
80
+ codegenResult = await generateSchemaTypes(introspector, {
72
81
  outFile: resolved.schemaFile,
73
82
  coercionFile: resolved.coercionFile,
74
83
  manifestFile: resolved.manifestFile,
84
+ classificationMapFile: resolved.classificationMapFile,
75
85
  camelCase: resolved.camelCase,
86
+ rootDir: resolved.rootDir,
76
87
  migrationsDir: resolved.migrationsDir,
77
88
  });
78
- const zodResult = generateZodTypes({
79
- schemaFile: resolved.schemaFile,
80
- outFile: resolved.zodFile,
81
- });
82
- return { migrate: migrateResult, codegen: codegenResult, zod: zodResult };
83
89
  }
84
90
  finally {
85
91
  db.close();
86
92
  }
87
93
  }
88
- // Postgres
89
- const introspector = new PostgresIntrospector(resolved.connectionString);
90
- await introspector.connect();
91
- try {
92
- const { Client } = await import('pg');
93
- const client = new Client({ connectionString: resolved.connectionString });
94
- await client.connect();
94
+ else {
95
+ // Postgres
96
+ const introspector = new PostgresIntrospector(resolved.connectionString);
97
+ await introspector.connect();
95
98
  try {
96
- const executor = new PostgresMigrationExecutor(client);
97
- const migrateResult = await migrate(executor, resolved.migrationsDir);
98
- const codegenResult = await generateSchemaTypes(introspector, {
99
- outFile: resolved.schemaFile,
100
- coercionFile: resolved.coercionFile,
101
- manifestFile: resolved.manifestFile,
102
- camelCase: resolved.camelCase,
103
- migrationsDir: resolved.migrationsDir,
104
- });
105
- const zodResult = generateZodTypes({
106
- schemaFile: resolved.schemaFile,
107
- outFile: resolved.zodFile,
108
- });
109
- return { migrate: migrateResult, codegen: codegenResult, zod: zodResult };
99
+ const { Client } = await import('pg');
100
+ const client = new Client({ connectionString: resolved.connectionString });
101
+ await client.connect();
102
+ try {
103
+ const executor = new PostgresMigrationExecutor(client);
104
+ migrateResult = await migrate(executor, resolved.migrationsDir);
105
+ codegenResult = await generateSchemaTypes(introspector, {
106
+ outFile: resolved.schemaFile,
107
+ coercionFile: resolved.coercionFile,
108
+ manifestFile: resolved.manifestFile,
109
+ classificationMapFile: resolved.classificationMapFile,
110
+ camelCase: resolved.camelCase,
111
+ migrationsDir: resolved.migrationsDir,
112
+ });
113
+ }
114
+ finally {
115
+ await client.end();
116
+ }
110
117
  }
111
118
  finally {
112
- await client.end();
119
+ await introspector.close();
113
120
  }
114
121
  }
115
- finally {
116
- await introspector.close();
117
- }
122
+ const zodResult = generateZodTypes({
123
+ schemaFile: resolved.schemaFile,
124
+ outFile: resolved.zodFile,
125
+ });
126
+ // ── Classifications step ──────────────────────────────────────────────────
127
+ const { scaffolded, jsonWritten } = syncClassifications(resolved.classificationsFile, resolved.classificationsGenJsonFile, codegenResult.tables);
128
+ return {
129
+ migrate: migrateResult,
130
+ codegen: codegenResult,
131
+ zod: zodResult,
132
+ classificationsScaffolded: scaffolded,
133
+ classificationsJsonWritten: jsonWritten,
134
+ };
118
135
  }
119
136
  // ─── SQLite-only operations ───────────────────────────────────────────────────
120
137
  export async function seed(resolved) {
@@ -139,6 +156,89 @@ export function reset(resolved, rootDir) {
139
156
  rmSync(resolved.dbFile);
140
157
  }
141
158
  }
159
+ // ── Classification sync ───────────────────────────────────────────────────────
160
+ /**
161
+ * Loads `db/classifications.ts` via tsx, serialises it to
162
+ * `.pikku/db/classifications.gen.json` for runtime consumption (console, etc.).
163
+ *
164
+ * If `db/classifications.ts` doesn't exist yet, writes a scaffold with every
165
+ * table defaulting to `private` so the developer has a starting point.
166
+ */
167
+ function syncClassifications(classificationsFile, genJsonFile, tableNames) {
168
+ let scaffolded = false;
169
+ if (!existsSync(classificationsFile)) {
170
+ const relMap = join(dirname(classificationsFile), '..', '.pikku', 'db', 'classification-map.gen.d.ts');
171
+ const relMapPosix = relMap.replace(/\\/g, '/');
172
+ const groups = new Map();
173
+ for (const name of tableNames) {
174
+ const dot = name.indexOf('.');
175
+ const schema = dot >= 0 ? name.slice(0, dot) : '';
176
+ const table = dot >= 0 ? name.slice(dot + 1) : name;
177
+ if (!groups.has(schema))
178
+ groups.set(schema, []);
179
+ groups.get(schema).push(table);
180
+ }
181
+ const bodyLines = [
182
+ `import type { DbClassificationMap } from '${relMapPosix}'`,
183
+ ``,
184
+ `export const classifications = {`,
185
+ ];
186
+ for (const [schema, tables] of groups) {
187
+ if (schema)
188
+ bodyLines.push(` ${JSON.stringify(schema)}: {`);
189
+ for (const table of tables) {
190
+ bodyLines.push(schema
191
+ ? ` ${JSON.stringify(table)}: {`
192
+ : ` ${JSON.stringify(table)}: {`);
193
+ bodyLines.push(schema ? ` },` : ` },`);
194
+ }
195
+ if (schema)
196
+ bodyLines.push(` },`);
197
+ }
198
+ bodyLines.push(`} satisfies DbClassificationMap`, ``);
199
+ mkdirSync(dirname(classificationsFile), { recursive: true });
200
+ writeFileSync(classificationsFile, bodyLines.join('\n'), 'utf8');
201
+ scaffolded = true;
202
+ }
203
+ // Resolve tsx from the CLI package's own node_modules so it works regardless
204
+ // of whether the user's project has tsx installed.
205
+ const _require = createRequire(fileURLToPath(import.meta.url));
206
+ let tsxEsmPath = null;
207
+ try {
208
+ tsxEsmPath = _require.resolve('tsx/esm');
209
+ }
210
+ catch {
211
+ // tsx not bundled with this CLI install — skip JSON emit
212
+ }
213
+ const script = [
214
+ `import * as mod from ${JSON.stringify(classificationsFile)}`,
215
+ `const val = Object.values(mod)[0]`,
216
+ `process.stdout.write(JSON.stringify(val))`,
217
+ ].join('\n');
218
+ let jsonWritten = false;
219
+ if (tsxEsmPath) {
220
+ try {
221
+ const json = execSync(`node --import ${JSON.stringify(tsxEsmPath)} --input-type=module`, {
222
+ input: script,
223
+ encoding: 'utf8',
224
+ stdio: ['pipe', 'pipe', 'pipe'],
225
+ });
226
+ const existing = existsSync(genJsonFile)
227
+ ? readFileSync(genJsonFile, 'utf8')
228
+ : null;
229
+ const next = JSON.stringify(JSON.parse(json), null, 2) + '\n';
230
+ if (existing !== next) {
231
+ mkdirSync(dirname(genJsonFile), { recursive: true });
232
+ writeFileSync(genJsonFile, next, 'utf8');
233
+ jsonWritten = true;
234
+ }
235
+ }
236
+ catch {
237
+ // annotations file has syntax errors — skip JSON emit
238
+ }
239
+ }
240
+ return { scaffolded, jsonWritten };
241
+ }
142
242
  export async function createKysely(resolved) {
143
243
  mkdirSync(dirname(resolved.dbFile), { recursive: true });
144
244
  const runtime = await loadSqliteRuntime();