@pikku/cli 0.12.25 → 0.12.26

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 (115) hide show
  1. package/console-app/assets/{index-D4DgafuS.js → index-Ba9K10XZ.js} +4 -4
  2. package/console-app/index.html +1 -1
  3. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  4. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  6. package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
  7. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  8. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +9 -0
  11. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  14. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  15. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  16. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  17. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  18. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  19. package/dist/.pikku/function/pikku-functions-meta.gen.json +116 -101
  20. package/dist/.pikku/function/pikku-functions.gen.js +3 -1
  21. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  22. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  23. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  24. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  25. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  26. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  27. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  28. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  29. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  30. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  31. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  32. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-types.gen.js +1 -1
  35. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  36. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  37. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  38. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  39. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  40. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  41. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +10 -9
  42. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  43. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  44. package/dist/.pikku/schemas/register.gen.js +7 -5
  45. package/dist/.pikku/schemas/schemas/DbAuditInput.schema.json +1 -0
  46. package/dist/.pikku/schemas/schemas/PikkuTestsCoverageInput.schema.json +1 -1
  47. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  48. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  49. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  50. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  51. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  52. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  53. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  54. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  55. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  56. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  57. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  58. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  59. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  60. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  61. package/dist/bin/pikku-bin.mjs +2 -2
  62. package/dist/src/cli.wiring.js +8 -0
  63. package/dist/src/fabric/functions/validate.function.js +1 -1
  64. package/dist/src/functions/commands/db-audit.d.ts +1 -0
  65. package/dist/src/functions/commands/db-audit.js +67 -0
  66. package/dist/src/functions/commands/db-migrate.js +5 -8
  67. package/dist/src/functions/commands/db-reset.js +9 -8
  68. package/dist/src/functions/commands/db-seed.js +9 -8
  69. package/dist/src/functions/commands/db-shared.d.ts +2 -4
  70. package/dist/src/functions/commands/db-shared.js +15 -5
  71. package/dist/src/functions/commands/dev.js +14 -8
  72. package/dist/src/functions/commands/new-addon.js +2 -2
  73. package/dist/src/functions/commands/tests-coverage.d.ts +3 -0
  74. package/dist/src/functions/commands/tests-coverage.js +34 -0
  75. package/dist/src/functions/db/annotation-parser.d.ts +31 -0
  76. package/dist/src/functions/db/annotation-parser.js +93 -0
  77. package/dist/src/functions/db/db-codegen.d.ts +24 -0
  78. package/dist/src/functions/db/db-codegen.js +276 -0
  79. package/dist/src/functions/db/db-introspector.d.ts +15 -0
  80. package/dist/src/functions/db/db-introspector.js +1 -0
  81. package/dist/src/functions/db/db-migrator.d.ts +32 -0
  82. package/dist/src/functions/db/db-migrator.js +65 -0
  83. package/dist/src/functions/db/local-db.d.ts +27 -33
  84. package/dist/src/functions/db/local-db.js +100 -53
  85. package/dist/src/functions/db/postgres/postgres-introspector.d.ts +10 -0
  86. package/dist/src/functions/db/postgres/postgres-introspector.js +54 -0
  87. package/dist/src/functions/db/postgres/postgres-migrator.d.ts +9 -0
  88. package/dist/src/functions/db/postgres/postgres-migrator.js +32 -0
  89. package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +9 -0
  90. package/dist/src/functions/db/sqlite/sqlite-introspector.js +35 -0
  91. package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +10 -0
  92. package/dist/src/functions/db/sqlite/sqlite-migrator.js +36 -0
  93. package/dist/src/functions/validate/workspace-validate.js +3 -2
  94. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +2 -1
  95. package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -4
  96. package/dist/src/functions/wirings/functions/serialize-addon-types.js +1 -1
  97. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  98. package/dist/src/services.js +2 -0
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +5 -3
  101. package/skills/pikku-testing/SKILL.md +208 -0
  102. package/dist/src/functions/db/sql-migrator.d.ts +0 -26
  103. package/dist/src/functions/db/sql-migrator.js +0 -104
  104. package/dist/src/functions/db/sqlite-codegen.d.ts +0 -45
  105. package/dist/src/functions/db/sqlite-codegen.js +0 -294
  106. /package/dist/src/functions/db/{seed.d.ts → sqlite/seed.d.ts} +0 -0
  107. /package/dist/src/functions/db/{seed.js → sqlite/seed.js} +0 -0
  108. /package/dist/src/functions/db/{sqlite-kysely.d.ts → sqlite/sqlite-kysely.d.ts} +0 -0
  109. /package/dist/src/functions/db/{sqlite-kysely.js → sqlite/sqlite-kysely.js} +0 -0
  110. /package/dist/src/functions/db/{sqlite-runtime-bun.d.ts → sqlite/sqlite-runtime-bun.d.ts} +0 -0
  111. /package/dist/src/functions/db/{sqlite-runtime-bun.js → sqlite/sqlite-runtime-bun.js} +0 -0
  112. /package/dist/src/functions/db/{sqlite-runtime-node.d.ts → sqlite/sqlite-runtime-node.d.ts} +0 -0
  113. /package/dist/src/functions/db/{sqlite-runtime-node.js → sqlite/sqlite-runtime-node.js} +0 -0
  114. /package/dist/src/functions/db/{sqlite-runtime.d.ts → sqlite/sqlite-runtime.d.ts} +0 -0
  115. /package/dist/src/functions/db/{sqlite-runtime.js → sqlite/sqlite-runtime.js} +0 -0
@@ -0,0 +1,31 @@
1
+ import type { ColumnKind } from './coercion-plugin.js';
2
+ type Classification = 'public' | 'private' | 'secret';
3
+ type AnonymizeStrategy = 'fake:email' | 'fake:name' | 'hash' | 'keep' | null;
4
+ export interface ColAnnotation {
5
+ kind?: ColumnKind;
6
+ /** TypeScript type string for @json columns, e.g. `string[]`. */
7
+ tsType?: string;
8
+ classification?: Classification;
9
+ anonymize?: AnonymizeStrategy;
10
+ }
11
+ /** Per-table, per-column annotation map built from migration SQL comments. */
12
+ export type AnnotationMap = Record<string, Record<string, ColAnnotation>>;
13
+ /**
14
+ * Determine column kind from naming conventions:
15
+ * *_at / *_on → date
16
+ * is_* / has_* / can_* → bool
17
+ */
18
+ export declare function annotationFromName(colName: string): {
19
+ kind: ColumnKind;
20
+ } | null;
21
+ /**
22
+ * Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @secret[:strategy]`
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
+ */
30
+ export declare function parseAnnotations(migrationsDir: string): AnnotationMap;
31
+ export {};
@@ -0,0 +1,93 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Determine column kind from naming conventions:
5
+ * *_at / *_on → date
6
+ * is_* / has_* / can_* → bool
7
+ */
8
+ export function annotationFromName(colName) {
9
+ if (/_at$|_on$/.test(colName))
10
+ return { kind: 'date' };
11
+ if (/^is_|^has_|^can_/.test(colName))
12
+ return { kind: 'bool' };
13
+ return null;
14
+ }
15
+ function parseStrategy(s) {
16
+ if (!s)
17
+ return null;
18
+ const valid = ['fake:email', 'fake:name', 'hash', 'keep'];
19
+ return valid.includes(s) ? s : null;
20
+ }
21
+ function parseComment(comment) {
22
+ const ann = {};
23
+ if (/@bool\b/i.test(comment)) {
24
+ ann.kind = 'bool';
25
+ }
26
+ else if (/@date\b/i.test(comment)) {
27
+ ann.kind = 'date';
28
+ }
29
+ else {
30
+ const jsonM = comment.match(/@json\b(?:\s+([^\s@]+))?/i);
31
+ if (jsonM) {
32
+ ann.kind = 'json';
33
+ if (jsonM[1])
34
+ ann.tsType = jsonM[1].trim();
35
+ }
36
+ }
37
+ const classM = comment.match(/@(public|private|secret)(?::([^\s@]+))?/i);
38
+ if (classM) {
39
+ ann.classification = classM[1].toLowerCase();
40
+ ann.anonymize = parseStrategy(classM[2]);
41
+ }
42
+ return ann;
43
+ }
44
+ /**
45
+ * Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @secret[:strategy]`
46
+ * 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
+ */
53
+ export function parseAnnotations(migrationsDir) {
54
+ let files;
55
+ try {
56
+ files = readdirSync(migrationsDir).filter((f) => f.endsWith('.sql')).sort();
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ const result = {};
62
+ function merge(tableName, colName, partial) {
63
+ if (!partial.kind && partial.classification === undefined)
64
+ return;
65
+ if (!result[tableName])
66
+ result[tableName] = {};
67
+ result[tableName][colName] = { ...result[tableName][colName], ...partial };
68
+ }
69
+ for (const file of files) {
70
+ const content = readFileSync(join(migrationsDir, file), 'utf8');
71
+ const createTablePattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"?(\w+)"?\s*\(([^;]+)\)/gis;
72
+ let tableMatch;
73
+ while ((tableMatch = createTablePattern.exec(content)) !== null) {
74
+ const tableName = tableMatch[1].toLowerCase();
75
+ const body = tableMatch[2];
76
+ for (const line of body.split('\n')) {
77
+ const trimmed = line.trim();
78
+ if (/^(PRIMARY|UNIQUE|CHECK|FOREIGN|CONSTRAINT)/i.test(trimmed))
79
+ continue;
80
+ const lineMatch = trimmed.match(/^(\w+)\s+\w[^-]*--\s*(.+?)\s*,?\s*$/);
81
+ if (!lineMatch)
82
+ continue;
83
+ merge(tableName, lineMatch[1].toLowerCase(), parseComment(lineMatch[2]));
84
+ }
85
+ }
86
+ const alterPattern = /ALTER\s+TABLE\s+"?(\w+)"?\s+ADD\s+(?:COLUMN\s+)?"?(\w+)"?\s+\w[^;\n-]*(?:;\s*)?--\s*(.+?)(?:\r?\n|$)/gim;
87
+ let alterMatch;
88
+ while ((alterMatch = alterPattern.exec(content)) !== null) {
89
+ merge(alterMatch[1].toLowerCase(), alterMatch[2].toLowerCase(), parseComment(alterMatch[3]));
90
+ }
91
+ }
92
+ return result;
93
+ }
@@ -0,0 +1,24 @@
1
+ import type { DbIntrospector } from './db-introspector.js';
2
+ export interface CodegenOptions {
3
+ outFile: string;
4
+ coercionFile: string;
5
+ manifestFile?: string;
6
+ camelCase?: boolean;
7
+ migrationsDir?: string;
8
+ }
9
+ export interface CodegenResult {
10
+ outFile: string;
11
+ coercionFile: string;
12
+ manifestFile?: string;
13
+ written: boolean;
14
+ coercionWritten: boolean;
15
+ manifestWritten: boolean;
16
+ tables: string[];
17
+ }
18
+ /**
19
+ * Introspect `introspector` and emit:
20
+ * - `schema.d.ts` Kysely DB type with classification brands
21
+ * - `coercion.gen.ts` Runtime CoercionMap for date/bool/json coercion
22
+ * - `classification.gen.ts` Data-classification manifest (when manifestFile set)
23
+ */
24
+ export declare function generateSchemaTypes(introspector: DbIntrospector, options: CodegenOptions): Promise<CodegenResult>;
@@ -0,0 +1,276 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { parseAnnotations, annotationFromName, } from './annotation-parser.js';
4
+ // ─── Name helpers ─────────────────────────────────────────────────────────────
5
+ function snakeToPascal(name) {
6
+ return name
7
+ .split('_')
8
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
9
+ .join('');
10
+ }
11
+ function snakeToCamel(name) {
12
+ return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
13
+ }
14
+ // ─── Type mapping ─────────────────────────────────────────────────────────────
15
+ function mapType(sqlType) {
16
+ const upper = sqlType.toUpperCase();
17
+ if (upper.includes('INT'))
18
+ return 'number';
19
+ if (upper.includes('CHAR') ||
20
+ upper.includes('CLOB') ||
21
+ upper.includes('TEXT') ||
22
+ upper === 'UUID')
23
+ return 'string';
24
+ if (upper.includes('BLOB') || upper === 'BYTEA')
25
+ return 'Buffer';
26
+ if (upper.includes('REAL') || upper.includes('FLOA') || upper.includes('DOUB'))
27
+ return 'number';
28
+ if (upper.includes('NUMERIC') || upper.includes('DECIMAL'))
29
+ return 'number';
30
+ // Postgres BOOLEAN type → boolean; SQLite BOOL (stores as int) → number
31
+ if (upper === 'BOOLEAN')
32
+ return 'boolean';
33
+ if (upper.includes('BOOL'))
34
+ return 'number';
35
+ if (upper.includes('JSON'))
36
+ return 'unknown';
37
+ return 'string';
38
+ }
39
+ // ─── Type expression ─────────────────────────────────────────────────────────
40
+ function selectBase(annotation, col) {
41
+ if (annotation?.kind === 'bool')
42
+ return 'boolean';
43
+ if (annotation?.kind === 'date')
44
+ return 'Date';
45
+ if (annotation?.kind === 'json')
46
+ return annotation.tsType ?? 'unknown';
47
+ return mapType(col.type);
48
+ }
49
+ function insertBase(annotation, col) {
50
+ if (annotation?.kind === 'bool')
51
+ return 'boolean | number';
52
+ if (annotation?.kind === 'date')
53
+ return 'Date | string';
54
+ if (annotation?.kind === 'json')
55
+ return annotation.tsType ?? 'unknown';
56
+ return mapType(col.type);
57
+ }
58
+ function columnTypeExpression(col, annotation, classification) {
59
+ const nullable = !col.notNull && !col.pk;
60
+ const isAutoInt = col.pk && mapType(col.type) === 'number';
61
+ const isOptionalInsert = col.defaultValue !== null || isAutoInt || Boolean(col.generated);
62
+ if (classification === 'public') {
63
+ const wrap = (inner) => isOptionalInsert ? `Generated<${inner}>` : inner;
64
+ if (annotation?.kind === 'bool') {
65
+ const base = nullable ? 'boolean | null' : 'boolean';
66
+ const rw = nullable ? 'boolean | number | null' : 'boolean | number';
67
+ return wrap(`ColumnType<${base}, ${rw}, ${rw}>`);
68
+ }
69
+ if (annotation?.kind === 'date') {
70
+ const base = nullable ? 'Date | null' : 'Date';
71
+ const rw = nullable ? 'Date | string | null' : 'Date | string';
72
+ return wrap(`ColumnType<${base}, ${rw}, ${rw}>`);
73
+ }
74
+ if (annotation?.kind === 'json') {
75
+ const base = annotation.tsType
76
+ ? nullable
77
+ ? `${annotation.tsType} | null`
78
+ : annotation.tsType
79
+ : nullable
80
+ ? 'unknown | null'
81
+ : 'unknown';
82
+ return wrap(base);
83
+ }
84
+ const base = mapType(col.type);
85
+ if (isAutoInt)
86
+ return `Generated<${base}>`;
87
+ if (col.defaultValue !== null || col.generated)
88
+ return `Generated<${base}${nullable ? ' | null' : ''}>`;
89
+ return nullable ? `${base} | null` : base;
90
+ }
91
+ const B = classification === 'secret' ? 'Secret' : 'Private';
92
+ const sBase = selectBase(annotation, col);
93
+ const iBase = insertBase(annotation, col);
94
+ const selectT = nullable ? `${B}<${sBase}> | null` : `${B}<${sBase}>`;
95
+ const insertT = nullable
96
+ ? `${iBase} | null${isOptionalInsert ? ' | undefined' : ''}`
97
+ : `${iBase}${isOptionalInsert ? ' | undefined' : ''}`;
98
+ const updateT = nullable ? `${iBase} | null` : iBase;
99
+ return `ColumnType<${selectT}, ${insertT}, ${updateT}>`;
100
+ }
101
+ function emitInterface(table, camelCase, explicitAnnotations) {
102
+ const ifaceName = snakeToPascal(table.name);
103
+ const tableCols = explicitAnnotations[table.name] ?? {};
104
+ const fields = table.columns
105
+ .map((col) => {
106
+ const fieldName = camelCase ? snakeToCamel(col.name) : col.name;
107
+ const sqlAnn = tableCols[col.name] ?? null;
108
+ const kindAnn = sqlAnn?.kind
109
+ ? { kind: sqlAnn.kind, tsType: sqlAnn.tsType }
110
+ : annotationFromName(col.name);
111
+ const classification = sqlAnn?.classification ?? 'private';
112
+ const type = columnTypeExpression(col, kindAnn, classification);
113
+ const safeName = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldName)
114
+ ? fieldName
115
+ : JSON.stringify(fieldName);
116
+ return ` ${safeName}: ${type}`;
117
+ })
118
+ .join('\n');
119
+ return `export interface ${ifaceName} {\n${fields}\n}`;
120
+ }
121
+ // ─── Manifest emitter ────────────────────────────────────────────────────────
122
+ function emitManifest(tables, explicitAnnotations) {
123
+ const tableEntries = tables
124
+ .map((table) => {
125
+ const tableCols = explicitAnnotations[table.name] ?? {};
126
+ const colEntries = table.columns
127
+ .map((col) => {
128
+ const ann = tableCols[col.name];
129
+ const classification = ann?.classification ?? 'private';
130
+ const strategy = ann?.anonymize ?? null;
131
+ const strategyLiteral = strategy === null ? 'null' : `'${strategy}'`;
132
+ return (` ${JSON.stringify(col.name)}: ` +
133
+ `{ classification: '${classification}', anonymize_strategy: ${strategyLiteral} }`);
134
+ })
135
+ .join(',\n');
136
+ return ` ${JSON.stringify(table.name)}: {\n${colEntries}\n }`;
137
+ })
138
+ .join(',\n');
139
+ return [
140
+ `// Generated by @pikku/cli — do not edit by hand.`,
141
+ `// Run \`pikku db migrate\` to refresh.`,
142
+ ``,
143
+ `export const classificationManifest = {`,
144
+ ` version: 1 as const,`,
145
+ ` tables: {`,
146
+ tableEntries,
147
+ ` },`,
148
+ `} as const`,
149
+ ``,
150
+ ].join('\n');
151
+ }
152
+ /**
153
+ * Introspect `introspector` and emit:
154
+ * - `schema.d.ts` Kysely DB type with classification brands
155
+ * - `coercion.gen.ts` Runtime CoercionMap for date/bool/json coercion
156
+ * - `classification.gen.ts` Data-classification manifest (when manifestFile set)
157
+ */
158
+ export async function generateSchemaTypes(introspector, options) {
159
+ const camelCase = options.camelCase ?? true;
160
+ const tableNames = await introspector.listTables();
161
+ const tables = await Promise.all(tableNames.map(async (name) => ({
162
+ name,
163
+ columns: await introspector.getColumns(name),
164
+ })));
165
+ const explicitAnnotations = options.migrationsDir
166
+ ? parseAnnotations(options.migrationsDir)
167
+ : {};
168
+ // ── schema.d.ts ─────────────────────────────────────────────────────────────
169
+ const interfaces = tables
170
+ .map((t) => emitInterface(t, camelCase, explicitAnnotations))
171
+ .join('\n\n');
172
+ const dbEntries = tables
173
+ .map((t) => {
174
+ const tableKey = camelCase ? snakeToCamel(t.name) : t.name;
175
+ const safe = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableKey)
176
+ ? tableKey
177
+ : JSON.stringify(tableKey);
178
+ return ` ${safe}: ${snakeToPascal(t.name)}`;
179
+ })
180
+ .join('\n');
181
+ const schemaBody = [
182
+ `// Generated by @pikku/cli — do not edit by hand.`,
183
+ `// Run \`pikku db migrate\` to refresh.`,
184
+ ``,
185
+ `import type { ColumnType } from 'kysely'`,
186
+ ``,
187
+ `export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>`,
188
+ ` ? ColumnType<S, I | undefined, U>`,
189
+ ` : ColumnType<T, T | undefined, T>`,
190
+ ``,
191
+ `export type Private<T> = T & { readonly __pii__: 'private' }`,
192
+ `export type Secret<T> = T & { readonly __pii__: 'secret' }`,
193
+ ``,
194
+ interfaces,
195
+ ``,
196
+ `export interface DB {`,
197
+ dbEntries,
198
+ `}`,
199
+ ``,
200
+ ].join('\n');
201
+ // ── coercion.gen.ts ──────────────────────────────────────────────────────────
202
+ const coercionMap = {};
203
+ for (const table of tables) {
204
+ const tableCols = explicitAnnotations[table.name] ?? {};
205
+ for (const col of table.columns) {
206
+ const sqlAnn = tableCols[col.name];
207
+ const kind = sqlAnn?.kind ?? annotationFromName(col.name)?.kind;
208
+ if (kind) {
209
+ if (!coercionMap[table.name])
210
+ coercionMap[table.name] = {};
211
+ coercionMap[table.name][col.name] = kind;
212
+ }
213
+ }
214
+ }
215
+ const coercionEntries = Object.entries(coercionMap)
216
+ .map(([table, cols]) => {
217
+ const colEntries = Object.entries(cols)
218
+ .map(([col, kind]) => ` "${col}": "${kind}"`)
219
+ .join(',\n');
220
+ return ` "${table}": {\n${colEntries}\n }`;
221
+ })
222
+ .join(',\n');
223
+ const coercionBody = [
224
+ `// Generated by @pikku/cli — do not edit by hand.`,
225
+ `// Run \`pikku db migrate\` to refresh.`,
226
+ ``,
227
+ `export const coercionMap = {`,
228
+ coercionEntries,
229
+ `} as const`,
230
+ ``,
231
+ ].join('\n');
232
+ // ── classification.gen.ts ───────────────────────────────────────────────────
233
+ const manifestBody = options.manifestFile ? emitManifest(tables, explicitAnnotations) : null;
234
+ // ── write files ───────────────────────────────────────────────────────────────
235
+ let existingSchema = null;
236
+ let existingCoercion = null;
237
+ let existingManifest = null;
238
+ try {
239
+ existingSchema = readFileSync(options.outFile, 'utf8');
240
+ }
241
+ catch { /* ok */ }
242
+ try {
243
+ existingCoercion = readFileSync(options.coercionFile, 'utf8');
244
+ }
245
+ catch { /* ok */ }
246
+ if (options.manifestFile) {
247
+ try {
248
+ existingManifest = readFileSync(options.manifestFile, 'utf8');
249
+ }
250
+ catch { /* ok */ }
251
+ }
252
+ const schemaChanged = existingSchema !== schemaBody;
253
+ const coercionChanged = existingCoercion !== coercionBody;
254
+ const manifestChanged = manifestBody !== null && existingManifest !== manifestBody;
255
+ if (schemaChanged) {
256
+ mkdirSync(dirname(options.outFile), { recursive: true });
257
+ writeFileSync(options.outFile, schemaBody, 'utf8');
258
+ }
259
+ if (coercionChanged) {
260
+ mkdirSync(dirname(options.coercionFile), { recursive: true });
261
+ writeFileSync(options.coercionFile, coercionBody, 'utf8');
262
+ }
263
+ if (manifestChanged && options.manifestFile && manifestBody) {
264
+ mkdirSync(dirname(options.manifestFile), { recursive: true });
265
+ writeFileSync(options.manifestFile, manifestBody, 'utf8');
266
+ }
267
+ return {
268
+ outFile: options.outFile,
269
+ coercionFile: options.coercionFile,
270
+ manifestFile: options.manifestFile,
271
+ written: schemaChanged,
272
+ coercionWritten: coercionChanged,
273
+ manifestWritten: manifestChanged,
274
+ tables: tables.map((t) => t.name),
275
+ };
276
+ }
@@ -0,0 +1,15 @@
1
+ export interface ColumnInfo {
2
+ name: string;
3
+ /** Raw SQL/DB type string, e.g. 'INTEGER', 'TEXT', 'boolean', 'timestamp without time zone' */
4
+ type: string;
5
+ notNull: boolean;
6
+ pk: boolean;
7
+ defaultValue: string | null;
8
+ /** True for virtual or stored generated columns — these are read-only and never inserted. */
9
+ generated?: boolean;
10
+ }
11
+ export interface DbIntrospector {
12
+ listTables(): Promise<string[]>;
13
+ getColumns(table: string): Promise<ColumnInfo[]>;
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ export declare class MigrationDriftError extends Error {
2
+ readonly file: string;
3
+ readonly recordedHash: string;
4
+ readonly currentHash: string | null;
5
+ readonly appliedAt: string;
6
+ constructor(file: string, recordedHash: string, currentHash: string | null, appliedAt: string, migrationsDir: string);
7
+ }
8
+ export interface MigrateResult {
9
+ applied: string[];
10
+ skipped: string[];
11
+ }
12
+ export interface AppliedMigration {
13
+ name: string;
14
+ hash: string;
15
+ applied_at: string;
16
+ }
17
+ /**
18
+ * Provider-agnostic migration executor. Implement this for each DB dialect.
19
+ * Each method maps to a single DB operation; all file I/O and hashing lives
20
+ * in the shared `migrate()` function above.
21
+ */
22
+ export interface MigrationExecutor {
23
+ ensureTrackingTable(): Promise<void>;
24
+ getApplied(): Promise<AppliedMigration[]>;
25
+ runMigration(sql: string, name: string, hash: string): Promise<void>;
26
+ }
27
+ /**
28
+ * Apply pending migrations from `migrationsDir/*.sql` using the supplied
29
+ * executor. Hashes raw file bytes on apply; subsequent runs re-hash and bail
30
+ * with `MigrationDriftError` if any applied file has changed on disk.
31
+ */
32
+ export declare function migrate(executor: MigrationExecutor, migrationsDir: string): Promise<MigrateResult>;
@@ -0,0 +1,65 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export class MigrationDriftError extends Error {
5
+ file;
6
+ recordedHash;
7
+ currentHash;
8
+ appliedAt;
9
+ constructor(file, recordedHash, currentHash, appliedAt, migrationsDir) {
10
+ const onDisk = currentHash === null
11
+ ? 'file missing on disk'
12
+ : `sha256:${currentHash.slice(0, 8)}…`;
13
+ super(`[PKU-DB-DRIFT] ${migrationsDir}/${file}\n\n` +
14
+ `Migration content has changed since it was applied.\n` +
15
+ ` recorded: sha256:${recordedHash.slice(0, 8)}… applied ${appliedAt}\n` +
16
+ ` on disk: ${onDisk}\n\n` +
17
+ `If this edit was intentional, write a new forward migration to revert the change.\n` +
18
+ `Production migrations are immutable.`);
19
+ this.file = file;
20
+ this.recordedHash = recordedHash;
21
+ this.currentHash = currentHash;
22
+ this.appliedAt = appliedAt;
23
+ this.name = 'MigrationDriftError';
24
+ }
25
+ }
26
+ function sha256(bytes) {
27
+ return createHash('sha256').update(bytes).digest('hex');
28
+ }
29
+ /**
30
+ * Apply pending migrations from `migrationsDir/*.sql` using the supplied
31
+ * executor. Hashes raw file bytes on apply; subsequent runs re-hash and bail
32
+ * with `MigrationDriftError` if any applied file has changed on disk.
33
+ */
34
+ export async function migrate(executor, migrationsDir) {
35
+ await executor.ensureTrackingTable();
36
+ const applied = await executor.getApplied();
37
+ for (const row of applied) {
38
+ let currentHash = null;
39
+ try {
40
+ currentHash = sha256(readFileSync(join(migrationsDir, row.name)));
41
+ }
42
+ catch {
43
+ currentHash = null;
44
+ }
45
+ if (currentHash !== row.hash) {
46
+ throw new MigrationDriftError(row.name, row.hash, currentHash, row.applied_at, migrationsDir);
47
+ }
48
+ }
49
+ const appliedSet = new Set(applied.map((r) => r.name));
50
+ const files = readdirSync(migrationsDir)
51
+ .filter((f) => f.endsWith('.sql'))
52
+ .sort();
53
+ const result = { applied: [], skipped: [] };
54
+ for (const name of files) {
55
+ if (appliedSet.has(name)) {
56
+ result.skipped.push(name);
57
+ continue;
58
+ }
59
+ const raw = readFileSync(join(migrationsDir, name));
60
+ const hash = sha256(raw);
61
+ await executor.runMigration(raw.toString('utf8'), name, hash);
62
+ result.applied.push(name);
63
+ }
64
+ return result;
65
+ }
@@ -1,48 +1,42 @@
1
1
  import type { Kysely } from 'kysely';
2
- import { type MigrateResult } from './sql-migrator.js';
3
- import { type CodegenResult } from './sqlite-codegen.js';
2
+ import { type MigrateResult } from './db-migrator.js';
3
+ import { type CodegenResult } from './db-codegen.js';
4
4
  import { type ZodCodegenResult } from './zod-codegen.js';
5
- import { type SeedResult } from './seed.js';
6
- export type DevDbConfig = true | {
7
- file?: string;
8
- };
9
- export interface ResolvedLocalDb {
10
- dbFile: string;
11
- runtimeDir: string;
5
+ import { type SeedResult } from './sqlite/seed.js';
6
+ import type { UserConfigShape } from '../commands/db-shared.js';
7
+ interface ResolvedDbBase {
12
8
  migrationsDir: string;
13
- seedFile: string;
14
9
  schemaFile: string;
15
10
  coercionFile: string;
11
+ manifestFile: string;
16
12
  zodFile: string;
17
13
  camelCase: boolean;
18
14
  }
15
+ export interface ResolvedSqliteDb extends ResolvedDbBase {
16
+ dialect: 'sqlite';
17
+ dbFile: string;
18
+ runtimeDir: string;
19
+ seedFile: string;
20
+ }
21
+ export interface ResolvedPostgresDb extends ResolvedDbBase {
22
+ dialect: 'postgres';
23
+ connectionString: string;
24
+ }
25
+ export type ResolvedDb = ResolvedSqliteDb | ResolvedPostgresDb;
19
26
  /**
20
- * Resolve a DevDbConfig into absolute paths.
21
- * - dbFile lives under runtimeDir (default: <rootDir>/.pikku-runtime)
22
- * - schema/coercion/zod are generated into outDir/db
23
- * - migrations and seed are authored source under rootDir/db
27
+ * Resolve a UserConfigShape into an absolute-path descriptor.
28
+ * Returns null when neither sqliteDb nor postgresUrl is configured.
24
29
  */
25
- export declare function resolveLocalDb(config: DevDbConfig | undefined, rootDir: string, outDir: string, runtimeDir?: string): ResolvedLocalDb | null;
30
+ export declare function resolveDb(userConfig: UserConfigShape, rootDir: string, outDir: string, runtimeDir?: string): ResolvedDb | null;
31
+ /** @deprecated Use resolveDb(userConfig, ...) instead. */
32
+ export declare function resolveLocalDb(sqliteDb: string | undefined, rootDir: string, outDir: string, runtimeDir?: string): ResolvedSqliteDb | null;
26
33
  export interface MigrateAndCodegenOutcome {
27
34
  migrate: MigrateResult;
28
35
  codegen: CodegenResult;
29
36
  zod: ZodCodegenResult;
30
37
  }
31
- /**
32
- * Run the migrate routine (open tracking-table → drift-check → apply →
33
- * codegen close). Used by both `pikku db migrate` and `pikku dev` boot.
34
- */
35
- export declare function migrateAndCodegen(resolved: ResolvedLocalDb): Promise<MigrateAndCodegenOutcome>;
36
- export declare function seed(resolved: ResolvedLocalDb): Promise<SeedResult>;
37
- /**
38
- * Delete the dev DB file. Refuses if NODE_ENV is 'production' or the
39
- * resolved file lives outside the project root (defensive against
40
- * misconfigured absolute paths).
41
- */
42
- export declare function reset(resolved: ResolvedLocalDb, rootDir: string): void;
43
- /**
44
- * Construct the user-facing Kysely instance for the dev DB. Used by
45
- * `pikku dev` to populate inMemoryServices.kysely.
46
- * Wires the coercion plugin when db/coercion.gen.ts exists.
47
- */
48
- export declare function createKysely<DB>(resolved: ResolvedLocalDb): Promise<Kysely<DB>>;
38
+ export declare function migrateAndCodegen(resolved: ResolvedDb): Promise<MigrateAndCodegenOutcome>;
39
+ export declare function seed(resolved: ResolvedSqliteDb): Promise<SeedResult>;
40
+ export declare function reset(resolved: ResolvedSqliteDb, rootDir: string): void;
41
+ export declare function createKysely<DB>(resolved: ResolvedSqliteDb): Promise<Kysely<DB>>;
42
+ export {};