@pikku/cli 0.12.24 → 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 (151) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/index-Ba9K10XZ.js +232 -0
  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 +21 -1
  8. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  9. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +50 -0
  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 +183 -104
  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 +3 -1
  34. package/dist/.pikku/pikku-services.gen.js +2 -0
  35. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-types.gen.js +1 -1
  37. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  38. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  39. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  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 +13 -9
  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 +17 -5
  47. package/dist/.pikku/schemas/schemas/DbAuditInput.schema.json +1 -0
  48. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  49. package/dist/.pikku/schemas/schemas/PikkuEmailsOutput.schema.json +1 -0
  50. package/dist/.pikku/schemas/schemas/PikkuFunctionTypesSplitInput.schema.json +1 -0
  51. package/dist/.pikku/schemas/schemas/PikkuTestsCoverageInput.schema.json +1 -1
  52. package/dist/.pikku/schemas/schemas/PikkuTriggerTypesInput.schema.json +1 -0
  53. package/dist/.pikku/schemas/schemas/WorkspaceValidateInput.schema.json +1 -0
  54. package/dist/.pikku/schemas/schemas/WorkspaceValidateOutput.schema.json +1 -0
  55. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  56. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  57. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  58. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  59. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  60. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  61. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  62. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  63. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  64. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  65. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +5 -5
  66. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  68. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  69. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  70. package/dist/bin/pikku-bin.mjs +2 -2
  71. package/dist/src/cli.wiring.js +39 -0
  72. package/dist/src/fabric/functions/validate-core.d.ts +20 -0
  73. package/dist/src/fabric/functions/validate-core.js +227 -0
  74. package/dist/src/fabric/functions/validate.function.js +12 -4
  75. package/dist/src/functions/commands/bootstrap.js +2 -2
  76. package/dist/src/functions/commands/console.js +7 -4
  77. package/dist/src/functions/commands/db-audit.d.ts +1 -0
  78. package/dist/src/functions/commands/db-audit.js +67 -0
  79. package/dist/src/functions/commands/db-migrate.js +7 -11
  80. package/dist/src/functions/commands/db-reset.js +12 -12
  81. package/dist/src/functions/commands/db-seed.js +11 -11
  82. package/dist/src/functions/commands/db-shared.d.ts +4 -19
  83. package/dist/src/functions/commands/db-shared.js +53 -17
  84. package/dist/src/functions/commands/dev.js +25 -14
  85. package/dist/src/functions/commands/emails-init.d.ts +5 -0
  86. package/dist/src/functions/commands/emails-init.js +162 -0
  87. package/dist/src/functions/commands/load-user-project.js +12 -3
  88. package/dist/src/functions/commands/new-addon.js +2 -2
  89. package/dist/src/functions/commands/tests-coverage.d.ts +3 -0
  90. package/dist/src/functions/commands/tests-coverage.js +34 -0
  91. package/dist/src/functions/commands/watch.js +7 -4
  92. package/dist/src/functions/commands/workspace-validate.d.ts +33 -0
  93. package/dist/src/functions/commands/workspace-validate.js +9 -0
  94. package/dist/src/functions/db/annotation-parser.d.ts +31 -0
  95. package/dist/src/functions/db/annotation-parser.js +93 -0
  96. package/dist/src/functions/db/coercion-plugin.d.ts +7 -0
  97. package/dist/src/functions/db/coercion-plugin.js +99 -0
  98. package/dist/src/functions/db/db-codegen.d.ts +24 -0
  99. package/dist/src/functions/db/db-codegen.js +276 -0
  100. package/dist/src/functions/db/db-introspector.d.ts +15 -0
  101. package/dist/src/functions/db/db-introspector.js +1 -0
  102. package/dist/src/functions/db/db-migrator.d.ts +32 -0
  103. package/dist/src/functions/db/db-migrator.js +65 -0
  104. package/dist/src/functions/db/local-db.d.ts +27 -33
  105. package/dist/src/functions/db/local-db.js +108 -57
  106. package/dist/src/functions/db/postgres/postgres-introspector.d.ts +10 -0
  107. package/dist/src/functions/db/postgres/postgres-introspector.js +54 -0
  108. package/dist/src/functions/db/postgres/postgres-migrator.d.ts +9 -0
  109. package/dist/src/functions/db/postgres/postgres-migrator.js +32 -0
  110. package/dist/src/functions/db/{seed.d.ts → sqlite/seed.d.ts} +2 -2
  111. package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +9 -0
  112. package/dist/src/functions/db/sqlite/sqlite-introspector.js +35 -0
  113. package/dist/src/functions/db/sqlite/sqlite-kysely.d.ts +8 -0
  114. package/dist/src/functions/db/sqlite/sqlite-kysely.js +62 -0
  115. package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +10 -0
  116. package/dist/src/functions/db/sqlite/sqlite-migrator.js +36 -0
  117. package/dist/src/functions/db/sqlite/sqlite-runtime-bun.d.ts +2 -0
  118. package/dist/src/functions/db/sqlite/sqlite-runtime-bun.js +52 -0
  119. package/dist/src/functions/db/sqlite/sqlite-runtime-node.d.ts +2 -0
  120. package/dist/src/functions/db/sqlite/sqlite-runtime-node.js +51 -0
  121. package/dist/src/functions/db/sqlite/sqlite-runtime.d.ts +20 -0
  122. package/dist/src/functions/db/sqlite/sqlite-runtime.js +13 -0
  123. package/dist/src/functions/validate/workspace-validate.d.ts +34 -0
  124. package/dist/src/functions/validate/workspace-validate.js +259 -0
  125. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +2 -1
  126. package/dist/src/functions/wirings/cli/pikku-command-cli-types.js +1 -1
  127. package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -4
  128. package/dist/src/functions/wirings/emails/pikku-command-emails.d.ts +6 -0
  129. package/dist/src/functions/wirings/emails/pikku-command-emails.js +172 -0
  130. package/dist/src/functions/wirings/emails/serialize-emails.d.ts +20 -0
  131. package/dist/src/functions/wirings/emails/serialize-emails.js +168 -0
  132. package/dist/src/functions/wirings/functions/pikku-command-function-types-split.d.ts +7 -1
  133. package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +2 -2
  134. package/dist/src/functions/wirings/functions/serialize-addon-types.js +1 -1
  135. package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.d.ts +7 -1
  136. package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.js +2 -2
  137. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +1 -1
  138. package/dist/src/functions/workflows/all.workflow.js +12 -7
  139. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  140. package/dist/src/services.js +2 -0
  141. package/dist/src/utils/pikku-cli-config.js +6 -0
  142. package/dist/tsconfig.tsbuildinfo +1 -1
  143. package/package.json +6 -4
  144. package/skills/pikku-auth-js/SKILL.md +271 -58
  145. package/skills/pikku-testing/SKILL.md +208 -0
  146. package/console-app/assets/index-BDOqBctb.js +0 -232
  147. package/dist/src/functions/db/sql-migrator.d.ts +0 -26
  148. package/dist/src/functions/db/sql-migrator.js +0 -104
  149. package/dist/src/functions/db/sqlite-codegen.d.ts +0 -45
  150. package/dist/src/functions/db/sqlite-codegen.js +0 -294
  151. /package/dist/src/functions/db/{seed.js → sqlite/seed.js} +0 -0
@@ -1,26 +0,0 @@
1
- import type { DatabaseSync } from 'node:sqlite';
2
- export declare class MigrationDriftError extends Error {
3
- readonly file: string;
4
- readonly recordedHash: string;
5
- readonly currentHash: string | null;
6
- readonly appliedAt: string;
7
- constructor(file: string, recordedHash: string, currentHash: string | null, appliedAt: string);
8
- }
9
- export interface MigrateResult {
10
- applied: string[];
11
- skipped: string[];
12
- }
13
- /**
14
- * Apply pending migrations from <migrationsDir>/*.sql against the open db.
15
- * Hashes raw bytes on apply; subsequent runs re-hash and bail with
16
- * MigrationDriftError if any applied file has changed on disk.
17
- *
18
- * The same bytes that get hashed are the bytes passed to db.exec — no
19
- * splitting, trimming, or normalization. See docs/dev-builtin-sqlite.md.
20
- */
21
- export declare function migrate(db: DatabaseSync, migrationsDir: string): MigrateResult;
22
- /**
23
- * Wipe the tracking table. Used by `pikku db reset` after the DB file is
24
- * removed (calling this on its own does NOT drop user tables).
25
- */
26
- export declare function dropTrackingTable(db: DatabaseSync): void;
@@ -1,104 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFileSync, readdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- const TRACKING_TABLE = 'sql_migrations';
5
- export class MigrationDriftError extends Error {
6
- file;
7
- recordedHash;
8
- currentHash;
9
- appliedAt;
10
- constructor(file, recordedHash, currentHash, appliedAt) {
11
- const onDisk = currentHash === null
12
- ? 'file missing on disk'
13
- : `sha256:${currentHash.slice(0, 8)}…`;
14
- super(`[PKU-DB-DRIFT] db/migrations/${file}\n\n` +
15
- `Migration content has changed since it was applied.\n` +
16
- ` recorded: sha256:${recordedHash.slice(0, 8)}… applied ${appliedAt}\n` +
17
- ` on disk: ${onDisk}\n\n` +
18
- `If this edit was intentional, run \`pikku db reset\` to rebuild the dev\n` +
19
- `database from scratch. Production migrations are immutable.`);
20
- this.file = file;
21
- this.recordedHash = recordedHash;
22
- this.currentHash = currentHash;
23
- this.appliedAt = appliedAt;
24
- this.name = 'MigrationDriftError';
25
- }
26
- }
27
- function sha256(bytes) {
28
- return createHash('sha256').update(bytes).digest('hex');
29
- }
30
- function ensureTrackingTable(db) {
31
- db.exec(`CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE} (
32
- name TEXT PRIMARY KEY,
33
- hash TEXT NOT NULL,
34
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
35
- )`);
36
- }
37
- function listMigrationFiles(migrationsDir) {
38
- return readdirSync(migrationsDir)
39
- .filter((f) => f.endsWith('.sql'))
40
- .sort();
41
- }
42
- function readApplied(db) {
43
- return db
44
- .prepare(`SELECT name, hash, applied_at FROM ${TRACKING_TABLE} ORDER BY name`)
45
- .all();
46
- }
47
- function checkDrift(applied, migrationsDir) {
48
- for (const row of applied) {
49
- let currentHash = null;
50
- try {
51
- currentHash = sha256(readFileSync(join(migrationsDir, row.name)));
52
- }
53
- catch {
54
- currentHash = null;
55
- }
56
- if (currentHash !== row.hash) {
57
- throw new MigrationDriftError(row.name, row.hash, currentHash, row.applied_at);
58
- }
59
- }
60
- }
61
- /**
62
- * Apply pending migrations from <migrationsDir>/*.sql against the open db.
63
- * Hashes raw bytes on apply; subsequent runs re-hash and bail with
64
- * MigrationDriftError if any applied file has changed on disk.
65
- *
66
- * The same bytes that get hashed are the bytes passed to db.exec — no
67
- * splitting, trimming, or normalization. See docs/dev-builtin-sqlite.md.
68
- */
69
- export function migrate(db, migrationsDir) {
70
- ensureTrackingTable(db);
71
- const applied = readApplied(db);
72
- checkDrift(applied, migrationsDir);
73
- const appliedSet = new Set(applied.map((r) => r.name));
74
- const files = listMigrationFiles(migrationsDir);
75
- const result = { applied: [], skipped: [] };
76
- const insertStmt = db.prepare(`INSERT INTO ${TRACKING_TABLE} (name, hash) VALUES (?, ?)`);
77
- for (const name of files) {
78
- if (appliedSet.has(name)) {
79
- result.skipped.push(name);
80
- continue;
81
- }
82
- const raw = readFileSync(join(migrationsDir, name));
83
- const hash = sha256(raw);
84
- db.exec('BEGIN');
85
- try {
86
- db.exec(raw.toString('utf8'));
87
- insertStmt.run(name, hash);
88
- db.exec('COMMIT');
89
- }
90
- catch (err) {
91
- db.exec('ROLLBACK');
92
- throw err;
93
- }
94
- result.applied.push(name);
95
- }
96
- return result;
97
- }
98
- /**
99
- * Wipe the tracking table. Used by `pikku db reset` after the DB file is
100
- * removed (calling this on its own does NOT drop user tables).
101
- */
102
- export function dropTrackingTable(db) {
103
- db.exec(`DROP TABLE IF EXISTS ${TRACKING_TABLE}`);
104
- }
@@ -1,45 +0,0 @@
1
- import type { DatabaseSync } from 'node:sqlite';
2
- import type { ColumnKind } from '@pikku/kysely-node-sqlite';
3
- /** Internal annotation: column kind plus an optional TS type string (for @json). */
4
- interface ColAnnotation {
5
- kind: ColumnKind;
6
- /** TypeScript type string, e.g. `string[]` or `Record<string, number>`. Only set for @json. */
7
- tsType?: string;
8
- }
9
- /** Internal annotation map used during codegen. */
10
- type AnnotationMap = Record<string, Record<string, ColAnnotation>>;
11
- /**
12
- * Parse `-- @bool | @date | @json [TypescriptType]` inline annotations from
13
- * migration SQL files. The TypeScript type is optional and only meaningful for
14
- * `@json` — it controls the generated TypeScript type (e.g. `string[]`,
15
- * `Record<string, number>`).
16
- *
17
- * Returns an AnnotationMap: { table_name: { col_name: ColAnnotation } }.
18
- */
19
- export declare function parseAnnotations(migrationsDir: string): AnnotationMap;
20
- export interface CodegenOptions {
21
- outFile: string;
22
- coercionFile: string;
23
- camelCase?: boolean;
24
- migrationsDir?: string;
25
- }
26
- export interface CodegenResult {
27
- outFile: string;
28
- coercionFile: string;
29
- written: boolean;
30
- coercionWritten: boolean;
31
- tables: string[];
32
- }
33
- /**
34
- * Introspect the open SQLite database and emit a Kysely DB type to outFile
35
- * plus a CoercionMap to coercionFile.
36
- *
37
- * Columns are annotated via:
38
- * 1. Naming conventions (_at/_on → date; is_/has_/can_ → bool)
39
- * 2. Inline SQL comments: `col_name TYPE ... -- @bool|@date|@json [TsType]`
40
- * For @json, an optional TypeScript type string controls the generated type.
41
- *
42
- * Returns `written: false` if the on-disk file already matches.
43
- */
44
- export declare function generateSchemaTypes(db: DatabaseSync, options: CodegenOptions): CodegenResult;
45
- export {};
@@ -1,294 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- const SKIP_TABLES = new Set(['sqlite_sequence', 'sql_migrations']);
4
- function snakeToPascal(name) {
5
- return name
6
- .split('_')
7
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
8
- .join('');
9
- }
10
- function snakeToCamel(name) {
11
- return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
12
- }
13
- function mapType(sqlType) {
14
- const upper = sqlType.toUpperCase();
15
- if (upper.includes('INT'))
16
- return 'number';
17
- if (upper.includes('CHAR') ||
18
- upper.includes('CLOB') ||
19
- upper.includes('TEXT')) {
20
- return 'string';
21
- }
22
- if (upper.includes('BLOB'))
23
- return 'Buffer';
24
- if (upper.includes('REAL') ||
25
- upper.includes('FLOA') ||
26
- upper.includes('DOUB')) {
27
- return 'number';
28
- }
29
- if (upper.includes('NUMERIC') || upper.includes('DECIMAL'))
30
- return 'number';
31
- if (upper.includes('BOOL'))
32
- return 'number';
33
- return 'string';
34
- }
35
- function listTables(db) {
36
- const tableRows = db
37
- .prepare(`SELECT name FROM sqlite_master
38
- WHERE type = 'table'
39
- AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'
40
- ORDER BY name`)
41
- .all();
42
- return tableRows
43
- .filter((t) => !SKIP_TABLES.has(t.name))
44
- .map((t) => {
45
- const allColumns = db
46
- .prepare(`PRAGMA table_xinfo(${escapeIdentifier(t.name)})`)
47
- .all();
48
- // hidden: 0=regular, 2=virtual generated, 3=stored generated — include all; skip 1 (vtab hidden)
49
- const columns = allColumns.filter((c) => c.hidden !== 1);
50
- return { name: t.name, columns };
51
- });
52
- }
53
- function escapeIdentifier(name) {
54
- return `"${name.replace(/"/g, '""')}"`;
55
- }
56
- // ─── Annotation parsing ──────────────────────────────────────────────────────
57
- /**
58
- * Determine column kind from naming conventions:
59
- * *_at / *_on → date
60
- * is_* / has_* / can_* → bool
61
- */
62
- function annotationFromName(colName) {
63
- if (/_at$|_on$/.test(colName))
64
- return { kind: 'date' };
65
- if (/^is_|^has_|^can_/.test(colName))
66
- return { kind: 'bool' };
67
- return null;
68
- }
69
- /**
70
- * Parse `-- @bool | @date | @json [TypescriptType]` inline annotations from
71
- * migration SQL files. The TypeScript type is optional and only meaningful for
72
- * `@json` — it controls the generated TypeScript type (e.g. `string[]`,
73
- * `Record<string, number>`).
74
- *
75
- * Returns an AnnotationMap: { table_name: { col_name: ColAnnotation } }.
76
- */
77
- export function parseAnnotations(migrationsDir) {
78
- let files;
79
- try {
80
- files = readdirSync(migrationsDir)
81
- .filter((f) => f.endsWith('.sql'))
82
- .sort();
83
- }
84
- catch {
85
- return {};
86
- }
87
- const result = {};
88
- for (const file of files) {
89
- const content = readFileSync(join(migrationsDir, file), 'utf8');
90
- const createTablePattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"?(\w+)"?\s*\(([^;]+)\)/gis;
91
- let tableMatch;
92
- while ((tableMatch = createTablePattern.exec(content)) !== null) {
93
- const tableName = tableMatch[1].toLowerCase();
94
- const body = tableMatch[2];
95
- for (const line of body.split('\n')) {
96
- const trimmed = line.trim();
97
- if (/^(PRIMARY|UNIQUE|CHECK|FOREIGN|CONSTRAINT)/i.test(trimmed))
98
- continue;
99
- // Match: col_name TYPE ... -- @kind [optional ts type]
100
- const annotationMatch = trimmed.match(/^(\w+)\s+\w.*?--\s*@(bool|date|json)(?:\s+(.+?))?$/i);
101
- if (annotationMatch) {
102
- const colName = annotationMatch[1].toLowerCase();
103
- const kind = annotationMatch[2].toLowerCase();
104
- const tsType = annotationMatch[3]?.trim() || undefined;
105
- if (!result[tableName])
106
- result[tableName] = {};
107
- result[tableName][colName] = {
108
- kind,
109
- tsType: kind === 'json' ? tsType : undefined,
110
- };
111
- }
112
- }
113
- }
114
- }
115
- return result;
116
- }
117
- /**
118
- * Merge explicit SQL annotations with naming-convention-based ones.
119
- * Explicit annotations take precedence.
120
- */
121
- function buildAnnotationMap(tables, explicit) {
122
- const merged = {};
123
- for (const table of tables) {
124
- const explicitCols = explicit[table.name] ?? {};
125
- for (const col of table.columns) {
126
- const annotation = explicitCols[col.name] ?? annotationFromName(col.name);
127
- if (annotation) {
128
- if (!merged[table.name])
129
- merged[table.name] = {};
130
- merged[table.name][col.name] = annotation;
131
- }
132
- }
133
- }
134
- return merged;
135
- }
136
- // ─── Type expression ─────────────────────────────────────────────────────────
137
- function columnTypeExpression(col, annotation) {
138
- const nullable = !col.notnull && col.pk === 0;
139
- const hasDefault = col.dflt_value !== null && col.dflt_value !== undefined;
140
- const isAutoInt = col.pk === 1 && mapType(col.type) === 'number';
141
- const isGenerated = col.hidden === 2 || col.hidden === 3;
142
- const wrap = (inner) => hasDefault || isAutoInt || isGenerated ? `Generated<${inner}>` : inner;
143
- if (annotation?.kind === 'bool') {
144
- const base = nullable ? 'boolean | null' : 'boolean';
145
- const rw = nullable ? 'boolean | number | null' : 'boolean | number';
146
- return wrap(`ColumnType<${base}, ${rw}, ${rw}>`);
147
- }
148
- if (annotation?.kind === 'date') {
149
- const base = nullable ? 'Date | null' : 'Date';
150
- const rw = nullable ? 'Date | string | null' : 'Date | string';
151
- return wrap(`ColumnType<${base}, ${rw}, ${rw}>`);
152
- }
153
- if (annotation?.kind === 'json') {
154
- const base = annotation.tsType
155
- ? nullable
156
- ? `${annotation.tsType} | null`
157
- : annotation.tsType
158
- : nullable
159
- ? 'unknown | null'
160
- : 'unknown';
161
- return wrap(base);
162
- }
163
- // Default: plain SQL-mapped type
164
- const base = mapType(col.type);
165
- if (isAutoInt)
166
- return `Generated<${base}>`;
167
- if (hasDefault || isGenerated)
168
- return `Generated<${base}${nullable ? ' | null' : ''}>`;
169
- return nullable ? `${base} | null` : base;
170
- }
171
- // ─── Interface emitter ───────────────────────────────────────────────────────
172
- function emitInterface(table, camelCase, annotations) {
173
- const ifaceName = snakeToPascal(table.name);
174
- const tableCols = annotations[table.name] ?? {};
175
- const fields = table.columns
176
- .map((col) => {
177
- const fieldName = camelCase ? snakeToCamel(col.name) : col.name;
178
- const annotation = tableCols[col.name] ?? null;
179
- const type = columnTypeExpression(col, annotation);
180
- const safeName = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldName)
181
- ? fieldName
182
- : JSON.stringify(fieldName);
183
- return ` ${safeName}: ${type}`;
184
- })
185
- .join('\n');
186
- return `export interface ${ifaceName} {\n${fields}\n}`;
187
- }
188
- /**
189
- * Introspect the open SQLite database and emit a Kysely DB type to outFile
190
- * plus a CoercionMap to coercionFile.
191
- *
192
- * Columns are annotated via:
193
- * 1. Naming conventions (_at/_on → date; is_/has_/can_ → bool)
194
- * 2. Inline SQL comments: `col_name TYPE ... -- @bool|@date|@json [TsType]`
195
- * For @json, an optional TypeScript type string controls the generated type.
196
- *
197
- * Returns `written: false` if the on-disk file already matches.
198
- */
199
- export function generateSchemaTypes(db, options) {
200
- const camelCase = options.camelCase ?? true;
201
- const tables = listTables(db);
202
- const explicitAnnotations = options.migrationsDir
203
- ? parseAnnotations(options.migrationsDir)
204
- : {};
205
- const annotations = buildAnnotationMap(tables, explicitAnnotations);
206
- // ── schema.d.ts ──
207
- const interfaces = tables
208
- .map((t) => emitInterface(t, camelCase, annotations))
209
- .join('\n\n');
210
- const dbEntries = tables
211
- .map((t) => {
212
- const tableKey = camelCase ? snakeToCamel(t.name) : t.name;
213
- const safe = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableKey)
214
- ? tableKey
215
- : JSON.stringify(tableKey);
216
- return ` ${safe}: ${snakeToPascal(t.name)}`;
217
- })
218
- .join('\n');
219
- const schemaBody = [
220
- `// Generated by @pikku/cli — do not edit by hand.`,
221
- `// Run \`pikku db migrate\` to refresh.`,
222
- ``,
223
- `import type { ColumnType } from 'kysely'`,
224
- ``,
225
- `export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>`,
226
- ` ? ColumnType<S, I | undefined, U>`,
227
- ` : ColumnType<T, T | undefined, T>`,
228
- ``,
229
- interfaces,
230
- ``,
231
- `export interface DB {`,
232
- dbEntries,
233
- `}`,
234
- ``,
235
- ].join('\n');
236
- // ── coercion.gen.ts (runtime map — only kind, not tsType) ──
237
- const runtimeMap = {};
238
- for (const [table, cols] of Object.entries(annotations)) {
239
- for (const [col, ann] of Object.entries(cols)) {
240
- if (!runtimeMap[table])
241
- runtimeMap[table] = {};
242
- runtimeMap[table][col] = ann.kind;
243
- }
244
- }
245
- const coercionEntries = Object.entries(runtimeMap)
246
- .map(([table, cols]) => {
247
- const colEntries = Object.entries(cols)
248
- .map(([col, kind]) => ` "${col}": "${kind}"`)
249
- .join(',\n');
250
- return ` "${table}": {\n${colEntries}\n }`;
251
- })
252
- .join(',\n');
253
- const coercionBody = [
254
- `// Generated by @pikku/cli — do not edit by hand.`,
255
- `// Run \`pikku db migrate\` to refresh.`,
256
- ``,
257
- `export const coercionMap = {`,
258
- coercionEntries,
259
- `} as const`,
260
- ``,
261
- ].join('\n');
262
- // ── write files ──
263
- let existingSchema = null;
264
- let existingCoercion = null;
265
- try {
266
- existingSchema = readFileSync(options.outFile, 'utf8');
267
- }
268
- catch {
269
- /* ok */
270
- }
271
- try {
272
- existingCoercion = readFileSync(options.coercionFile, 'utf8');
273
- }
274
- catch {
275
- /* ok */
276
- }
277
- const schemaChanged = existingSchema !== schemaBody;
278
- const coercionChanged = existingCoercion !== coercionBody;
279
- if (schemaChanged) {
280
- mkdirSync(dirname(options.outFile), { recursive: true });
281
- writeFileSync(options.outFile, schemaBody, 'utf8');
282
- }
283
- if (coercionChanged) {
284
- mkdirSync(dirname(options.coercionFile), { recursive: true });
285
- writeFileSync(options.coercionFile, coercionBody, 'utf8');
286
- }
287
- return {
288
- outFile: options.outFile,
289
- coercionFile: options.coercionFile,
290
- written: schemaChanged,
291
- coercionWritten: coercionChanged,
292
- tables: tables.map((t) => t.name),
293
- };
294
- }