@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
@@ -0,0 +1,227 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { added, changed, dim, removed } from '../lib/output.js';
6
+ import { WorkspaceValidateOutput, readJsonSafe, readTextSafe, runWorkspaceValidate, } from '../../functions/validate/workspace-validate.js';
7
+ export const FabricValidateInput = z.object({});
8
+ export const FabricValidateOutput = WorkspaceValidateOutput;
9
+ const POSTGRES_SQL_PATTERNS = [
10
+ {
11
+ re: /\b(?:SMALL|BIG)?SERIAL\b/i,
12
+ label: 'SERIAL / BIGSERIAL / SMALLSERIAL',
13
+ },
14
+ { re: /\bJSONB\b/i, label: 'JSONB' },
15
+ { re: /\bCREATE\s+SEQUENCE\b/i, label: 'CREATE SEQUENCE' },
16
+ { re: /\bgen_random_uuid\s*\(\s*\)/i, label: 'gen_random_uuid()' },
17
+ { re: /::[a-z_]+/i, label: ':: type cast' },
18
+ { re: /\bTSVECTOR\b/i, label: 'TSVECTOR' },
19
+ { re: /\bARRAY\s*\[/i, label: 'ARRAY[…]' },
20
+ ];
21
+ export async function runFabricValidate(startDir = process.cwd()) {
22
+ const workspaceResult = await runWorkspaceValidate(startDir);
23
+ const root = workspaceResult.root;
24
+ const findings = [...workspaceResult.findings];
25
+ const e = (id, message, path, fixHint) => {
26
+ findings.push({ id, severity: 'error', message, path, fixHint });
27
+ };
28
+ const w = (id, message, path, fixHint) => {
29
+ findings.push({ id, severity: 'warn', message, path, fixHint });
30
+ };
31
+ const info = (id, message, path, fixHint) => {
32
+ findings.push({ id, severity: 'info', message, path, fixHint });
33
+ };
34
+ const fabricConfigPath = join(root, 'fabric.config.json');
35
+ const fabricConfig = await readJsonSafe(fabricConfigPath);
36
+ if (!fabricConfig) {
37
+ info('fabric-config-missing', 'fabric.config.json not found — project has not been linked to fabric yet', fabricConfigPath, 'Run `pikku fabric link` to create it, or create manually: {"projectId": "__PROJECT_ID__"}');
38
+ }
39
+ else if (!fabricConfig.projectId) {
40
+ info('fabric-config-no-project-id', 'fabric.config.json is missing "projectId"', fabricConfigPath, 'Add "projectId": "<your-project-id>" to fabric.config.json, or run `pikku fabric link`');
41
+ }
42
+ else if (fabricConfig.projectId === '__PROJECT_ID__') {
43
+ info('fabric-config-placeholder-project-id', 'fabric.config.json has a placeholder projectId ("__PROJECT_ID__") — project is not linked', fabricConfigPath, 'Run `pikku fabric link` to replace the placeholder with a real project ID');
44
+ }
45
+ const rootPkgPath = join(root, 'package.json');
46
+ const rootPkg = await readJsonSafe(rootPkgPath);
47
+ if (rootPkg) {
48
+ const allDeps = {
49
+ ...rootPkg.dependencies,
50
+ ...rootPkg.devDependencies,
51
+ };
52
+ if (!allDeps['@pikku/fabric-cli']) {
53
+ info('missing-fabric-cli', '@pikku/fabric-cli not in devDependencies — fabric CLI commands (validate, deploy) will not be available', rootPkgPath, 'Add "@pikku/fabric-cli" to devDependencies: use "file:./vendor/pikku-fabric-cli.tgz" (bundled release) or "portal:/path/to/pikku/packages/fabric-cli" (local dev)');
54
+ }
55
+ }
56
+ const fnDir = join(root, 'packages', 'functions');
57
+ const functionsSdkPkgName = (await readJsonSafe(join(root, 'packages', 'functions-sdk', 'package.json')))?.name;
58
+ const themePkgName = (await readJsonSafe(join(root, 'packages', 'theme', 'package.json')))?.name;
59
+ const componentsPkgName = (await readJsonSafe(join(root, 'packages', 'components', 'package.json')))?.name;
60
+ if (existsSync(fnDir)) {
61
+ const fnPkgPath = join(fnDir, 'package.json');
62
+ const fnPkg = await readJsonSafe(fnPkgPath);
63
+ if (fnPkg) {
64
+ const fnAllDeps = {
65
+ ...fnPkg.dependencies,
66
+ ...fnPkg.devDependencies,
67
+ ...fnPkg.peerDependencies,
68
+ };
69
+ if (fnAllDeps['@pikku/kysely-postgres']) {
70
+ e('fn-pkg-postgres-dep', '@pikku/kysely-postgres is in packages/functions dependencies — Fabric uses SQLite/libSQL (Turso), not PostgreSQL', fnPkgPath, 'Remove @pikku/kysely-postgres and use @pikku/kysely-sqlite with LibsqlWebDialect instead');
71
+ }
72
+ }
73
+ const servicesPath = join(fnDir, 'src', 'services.ts');
74
+ const servicesText = await readTextSafe(servicesPath);
75
+ if (servicesText) {
76
+ const usesKysely = /\bKysely\b/.test(servicesText);
77
+ const usesLibsql = servicesText.includes('@pikku/kysely-sqlite') ||
78
+ servicesText.includes('LibsqlWebDialect');
79
+ const usesProcessEnv = /\bprocess\.env\.[A-Z_]/.test(servicesText);
80
+ if (usesKysely && !usesLibsql) {
81
+ e('services-wrong-db-adapter', 'services.ts uses Kysely but not LibsqlWebDialect — Fabric injects a Turso/libSQL DATABASE_URL at runtime, not a PostgreSQL URL', servicesPath, 'Import LibsqlWebDialect from @pikku/kysely-sqlite and replace the dialect: new Kysely({ dialect: new LibsqlWebDialect({ url: databaseUrl }) })');
82
+ }
83
+ if (usesProcessEnv) {
84
+ info('services-process-env', 'services.ts reads process.env directly — prefer variables.get() for portable secret/variable access', servicesPath, 'Replace process.env.SOME_VAR with await variables.get("SOME_VAR") — declare the binding with wireVariable/wireSecret; process.env is fine for optional/non-secret config');
85
+ }
86
+ if (usesLibsql &&
87
+ rootPkg &&
88
+ !rootPkg.dependencies?.['@pikku/kysely-sqlite'] &&
89
+ !rootPkg.devDependencies?.['@pikku/kysely-sqlite']) {
90
+ e('missing-kysely-sqlite', 'services.ts imports @pikku/kysely-sqlite but it is not in root package.json', rootPkgPath, 'Add "@pikku/kysely-sqlite": "file:./vendor/pikku-kysely-sqlite.tgz" to dependencies');
91
+ }
92
+ }
93
+ // db/migrations/ — presence, numbering and SQL dialect
94
+ const migrationsDir = join(fnDir, 'db', 'migrations');
95
+ if (!existsSync(migrationsDir)) {
96
+ e('migrations-dir-missing', 'packages/functions/db/migrations/ not found', migrationsDir, 'Create db/migrations/ and add numbered .sql files (e.g. 0001-init.sql) using SQLite-compatible syntax');
97
+ }
98
+ else {
99
+ try {
100
+ const files = (await readdir(migrationsDir)).filter((f) => f.endsWith('.sql'));
101
+ const nums = [];
102
+ for (const f of files) {
103
+ const m = f.match(/^(\d+)/);
104
+ if (m)
105
+ nums.push(parseInt(m[1], 10));
106
+ }
107
+ for (let idx = 1; idx < nums.length; idx++) {
108
+ if (nums[idx] !== nums[idx - 1] + 1) {
109
+ const missing = `${nums[idx - 1] + 1}..${nums[idx] - 1}`;
110
+ e('migration-gap', `Migration numbering gap: IDs ${missing} are missing`, migrationsDir, 'Migrations must be consecutive. Add the missing .sql file or renumber if not yet applied.');
111
+ break;
112
+ }
113
+ }
114
+ for (const f of files) {
115
+ const sql = await readTextSafe(join(migrationsDir, f));
116
+ if (!sql)
117
+ continue;
118
+ const hits = POSTGRES_SQL_PATTERNS.filter(({ re }) => re.test(sql)).map(({ label }) => label);
119
+ if (hits.length > 0) {
120
+ e(`migration-postgres-sql-${f.replace(/[^a-z0-9]/gi, '-')}`, `${f} contains PostgreSQL syntax (${hits.join(', ')}) — Fabric uses SQLite/libSQL (Turso)`, join(migrationsDir, f), "Rewrite the migration using SQLite-compatible syntax: TEXT instead of JSONB, INTEGER PRIMARY KEY for auto-increment, datetime('now') instead of NOW(), no :: casts");
121
+ }
122
+ }
123
+ }
124
+ catch {
125
+ // readdir failure — skip
126
+ }
127
+ }
128
+ // db/seed.sql
129
+ const seedPath = join(fnDir, 'db', 'seed.sql');
130
+ if (!existsSync(seedPath)) {
131
+ e('seed-sql-missing', 'packages/functions/db/seed.sql not found', seedPath, 'Create db/seed.sql with idempotent INSERT OR IGNORE statements for demo/test data');
132
+ }
133
+ }
134
+ const appsDir = join(root, 'apps');
135
+ if (existsSync(appsDir)) {
136
+ if (fabricConfig) {
137
+ const frontends = (fabricConfig.frontends ?? {});
138
+ for (const [slug, fe] of Object.entries(frontends)) {
139
+ const cwd = fe.cwd?.replace(/^\.\//, '');
140
+ if (cwd && !existsSync(join(root, cwd))) {
141
+ e(`frontend-cwd-missing-${slug}`, `fabric.config.json frontend "${slug}" declares cwd "${cwd}" but that directory does not exist`, join(root, cwd), 'Create the directory or update the cwd in fabric.config.json');
142
+ }
143
+ }
144
+ }
145
+ let appEntries = [];
146
+ try {
147
+ appEntries = (await readdir(appsDir, { withFileTypes: true }))
148
+ .filter((e) => e.isDirectory())
149
+ .map((e) => e.name);
150
+ }
151
+ catch {
152
+ // ignore
153
+ }
154
+ const declaredCwds = fabricConfig
155
+ ? new Set(Object.values((fabricConfig.frontends ?? {})).map((f) => f.cwd?.replace(/^\.\//, '') ?? ''))
156
+ : null;
157
+ for (const name of appEntries) {
158
+ const appPath = join(appsDir, name);
159
+ const cwd = `apps/${name}`;
160
+ if (declaredCwds && !declaredCwds.has(cwd)) {
161
+ w(`app-not-declared-${name}`, `apps/${name} is not declared in fabric.config.json frontends`, appPath, `Add an entry to fabric.config.json: { "frontends": { "${name}": { "cwd": "${cwd}", "kind": "ssr" } } }`);
162
+ }
163
+ const appPkg = await readJsonSafe(join(appPath, 'package.json'));
164
+ if (!appPkg)
165
+ continue;
166
+ const appDeps = { ...appPkg.dependencies };
167
+ if (functionsSdkPkgName && !appDeps[functionsSdkPkgName]) {
168
+ info(`app-missing-functions-sdk-${name}`, `apps/${name} does not depend on ${functionsSdkPkgName} — the generated RPC client and React Query hooks`, join(appPath, 'package.json'), `Add "${functionsSdkPkgName}: workspace:*" to apps/${name}/package.json dependencies`);
169
+ }
170
+ if (themePkgName && !appDeps[themePkgName]) {
171
+ info(`app-missing-theme-${name}`, `apps/${name} does not depend on ${themePkgName}`, join(appPath, 'package.json'), `Add "${themePkgName}: workspace:*" to apps/${name}/package.json dependencies`);
172
+ }
173
+ if (componentsPkgName && !appDeps[componentsPkgName]) {
174
+ info(`app-missing-components-${name}`, `apps/${name} does not depend on ${componentsPkgName}`, join(appPath, 'package.json'), `Add "${componentsPkgName}: workspace:*" to apps/${name}/package.json dependencies`);
175
+ }
176
+ }
177
+ }
178
+ const designDocUrl = 'https://pikkufabric.dev/docs/design';
179
+ if (!existsSync(join(root, 'packages', 'theme'))) {
180
+ info('theme-missing', 'packages/theme/ not found — Fabric design features require a theme package', join(root, 'packages', 'theme'), `Create packages/theme/ with your Mantine theme tokens. See ${designDocUrl}`);
181
+ }
182
+ if (!existsSync(join(root, 'packages', 'components'))) {
183
+ info('components-missing', 'packages/components/ not found — Fabric design features require a components package', join(root, 'packages', 'components'), `Create packages/components/ with your shared UI components. See ${designDocUrl}`);
184
+ }
185
+ const ok = !findings.some((f) => f.severity === 'error');
186
+ return { ok, root, findings };
187
+ }
188
+ export const runValidate = runFabricValidate;
189
+ export const renderValidate = (_s, { ok, root, findings }) => {
190
+ if (findings.length === 0) {
191
+ console.log(added('✓ All checks passed — project is fabric-compatible'));
192
+ return;
193
+ }
194
+ const relPath = (p) => p.startsWith(root + '/') || p.startsWith(root + '\\')
195
+ ? p.slice(root.length + 1)
196
+ : p;
197
+ const errors = findings.filter((f) => f.severity === 'error');
198
+ const warns = findings.filter((f) => f.severity === 'warn');
199
+ const infos = findings.filter((f) => f.severity === 'info');
200
+ for (const f of [...errors, ...warns, ...infos]) {
201
+ const icon = f.severity === 'error'
202
+ ? removed('✗')
203
+ : f.severity === 'warn'
204
+ ? changed('⚠')
205
+ : dim('ℹ');
206
+ console.log(`${icon} ${f.message}`);
207
+ console.log(` ${dim('path:')} ${relPath(f.path)}`);
208
+ console.log(` ${dim('fix:')} ${f.fixHint}`);
209
+ console.log();
210
+ }
211
+ const counts = [];
212
+ if (errors.length) {
213
+ counts.push(removed(`${errors.length} error${errors.length !== 1 ? 's' : ''}`));
214
+ }
215
+ if (warns.length) {
216
+ counts.push(changed(`${warns.length} warning${warns.length !== 1 ? 's' : ''}`));
217
+ }
218
+ if (infos.length) {
219
+ counts.push(dim(`${infos.length} info${infos.length !== 1 ? 's' : ''}`));
220
+ }
221
+ console.log('─'.repeat(40));
222
+ console.log(counts.join(' '));
223
+ if (ok) {
224
+ console.log();
225
+ console.log(added('✓') + ' ' + dim('no errors — project can be linked to fabric'));
226
+ }
227
+ };
@@ -194,9 +194,12 @@ export async function runValidate(startDir = process.cwd()) {
194
194
  e('missing-kysely-sqlite', 'services.ts imports @pikku/kysely-sqlite but it is not in root package.json', rootPkgPath, 'Add "@pikku/kysely-sqlite": "file:./vendor/pikku-kysely-sqlite.tgz" to dependencies');
195
195
  }
196
196
  }
197
- // db/migrations/ — numbering and SQL dialect
197
+ // db/migrations/ — presence, numbering and SQL dialect
198
198
  const migrationsDir = join(fnDir, 'db', 'migrations');
199
- if (existsSync(migrationsDir)) {
199
+ if (!existsSync(migrationsDir)) {
200
+ e('migrations-dir-missing', 'packages/functions/db/migrations/ not found', migrationsDir, 'Create db/migrations/ and add numbered .sql files (e.g. 0001-init.sql) using SQLite-compatible syntax');
201
+ }
202
+ else {
200
203
  try {
201
204
  const files = (await readdir(migrationsDir))
202
205
  .filter((f) => f.endsWith('.sql'))
@@ -210,7 +213,7 @@ export async function runValidate(startDir = process.cwd()) {
210
213
  for (let idx = 1; idx < nums.length; idx++) {
211
214
  if (nums[idx] !== nums[idx - 1] + 1) {
212
215
  const missing = `${nums[idx - 1] + 1}..${nums[idx] - 1}`;
213
- e('migration-gap', `Migration numbering gap: IDs ${missing} are missing`, migrationsDir, 'Migrations must be consecutive (postgres-migrations refuses gaps). Add the missing .sql file or renumber if not yet applied.');
216
+ e('migration-gap', `Migration numbering gap: IDs ${missing} are missing`, migrationsDir, 'Migrations must be consecutive. Add the missing .sql file or renumber if not yet applied.');
214
217
  break;
215
218
  }
216
219
  }
@@ -229,6 +232,11 @@ export async function runValidate(startDir = process.cwd()) {
229
232
  // readdir failure — skip
230
233
  }
231
234
  }
235
+ // db/seed.sql
236
+ const seedPath = join(fnDir, 'db', 'seed.sql');
237
+ if (!existsSync(seedPath)) {
238
+ e('seed-sql-missing', 'packages/functions/db/seed.sql not found', seedPath, 'Create db/seed.sql with idempotent INSERT OR IGNORE statements for demo/test data');
239
+ }
232
240
  // db.types.ts — should only re-export from .pikku
233
241
  const dbTypesPath = join(fnDir, 'src', 'types', 'db.types.ts');
234
242
  const dbTypesText = await readTextSafe(dbTypesPath);
@@ -248,7 +256,7 @@ export async function runValidate(startDir = process.cwd()) {
248
256
  info('wirings-dir-missing', 'packages/functions/src/wirings/ not found', join(fnDir, 'src', 'wirings'), 'Create src/wirings/ for transport bindings: *.http.ts, *.queue.ts, *.schedule.ts, *.channel.ts, *.cli.ts');
249
257
  }
250
258
  if (!existsSync(join(fnDir, 'src', 'config.ts'))) {
251
- info('config-missing', 'packages/functions/src/config.ts not found', join(fnDir, 'src', 'config.ts'), 'Create src/config.ts: export const createConfig = pikkuConfig(async () => ({ dev: { db: true } }))');
259
+ info('config-missing', 'packages/functions/src/config.ts not found', join(fnDir, 'src', 'config.ts'), "Create src/config.ts: export const createConfig = pikkuConfig(async () => ({ sqliteDb: '.pikku-runtime/dev.db' }))");
252
260
  }
253
261
  }
254
262
  // ── apps/ vs fabric.config.json frontends ─────────────────────────────
@@ -3,14 +3,14 @@ export const bootstrap = pikkuVoidFunc({
3
3
  remote: true,
4
4
  func: async ({ logger, getInspectorState }, _data, { rpc }) => {
5
5
  await getInspectorState(false, true, true);
6
- await rpc.invoke('pikkuFunctionTypesSplit');
6
+ await rpc.invoke('pikkuFunctionTypesSplit', { bootstrap: true });
7
7
  await rpc.invoke('pikkuFunctionTypes');
8
8
  await rpc.invoke('pikkuHTTPTypes');
9
9
  await rpc.invoke('pikkuChannelTypes');
10
10
  await rpc.invoke('pikkuSchedulerTypes');
11
11
  await rpc.invoke('pikkuQueueTypes');
12
12
  await rpc.invoke('pikkuWorkflow', { bootstrap: true });
13
- await rpc.invoke('pikkuTriggerTypes');
13
+ await rpc.invoke('pikkuTriggerTypes', { bootstrap: true });
14
14
  await rpc.invoke('pikkuMCPTypes');
15
15
  await rpc.invoke('pikkuAIAgentTypes');
16
16
  await rpc.invoke('pikkuNodeTypes');
@@ -23,6 +23,9 @@ const MIME_TYPES = {
23
23
  export const consoleCommand = pikkuSessionlessFunc({
24
24
  remote: true,
25
25
  func: async ({ logger, config }, { port, open: openBrowser, hmr }, { rpc }) => {
26
+ const watchDirectories = [
27
+ ...new Set([config.emailTemplatesDir, ...config.srcDirectories].filter(Boolean)),
28
+ ];
26
29
  if (!config.scaffold?.console) {
27
30
  logger.error('Console is not enabled. Add { "scaffold": { "console": "no-auth" } } to your pikku.config.json');
28
31
  return;
@@ -68,19 +71,19 @@ export const consoleCommand = pikkuSessionlessFunc({
68
71
  });
69
72
  if (hmr) {
70
73
  await pikkuDevReloader({
71
- srcDirectories: config.srcDirectories,
74
+ srcDirectories: watchDirectories,
72
75
  logger,
73
76
  });
74
77
  }
75
- const configWatcher = chokidar.watch(config.srcDirectories, {
78
+ const configWatcher = chokidar.watch(watchDirectories, {
76
79
  ignoreInitial: true,
77
80
  ignored: /.*\.gen\.tsx?/,
78
81
  });
79
82
  let watcher = new chokidar.FSWatcher({});
80
83
  const generatorWatcher = () => {
81
84
  watcher.close();
82
- logger.info(`• Watching directories: \n - ${config.srcDirectories.join('\n - ')}`);
83
- watcher = chokidar.watch(config.srcDirectories, {
85
+ logger.info(`• Watching directories: \n - ${watchDirectories.join('\n - ')}`);
86
+ watcher = chokidar.watch(watchDirectories, {
84
87
  ignoreInitial: true,
85
88
  ignored: /.*\.gen\.ts/,
86
89
  });
@@ -0,0 +1 @@
1
+ export declare const dbAudit: import("#pikku").PikkuFunctionConfig<{}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{}, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{}, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
@@ -0,0 +1,67 @@
1
+ import { pikkuSessionlessFunc } from '#pikku';
2
+ import { resolveDb } from '../db/local-db.js';
3
+ import { loadUserConfigForDb } from './db-shared.js';
4
+ export const dbAudit = pikkuSessionlessFunc({
5
+ remote: true,
6
+ func: async ({ logger, config }) => {
7
+ const userConfig = await loadUserConfigForDb({ config, logger });
8
+ if (!userConfig)
9
+ return;
10
+ const resolved = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
11
+ if (!resolved) {
12
+ logger.error('pikku db audit: no database configured — set sqliteDb or postgresUrl in your createConfig.');
13
+ throw new Error('no database configured');
14
+ }
15
+ let manifest;
16
+ try {
17
+ const mod = await import(resolved.manifestFile);
18
+ manifest = mod.classificationManifest;
19
+ }
20
+ catch {
21
+ logger.error(`pikku db audit: classification manifest not found at ${resolved.manifestFile}.\n` +
22
+ ` Run \`pikku db migrate\` to generate it.`);
23
+ throw new Error('classification manifest not found');
24
+ }
25
+ let publicCount = 0;
26
+ let privateCount = 0;
27
+ let secretCount = 0;
28
+ const noStrategyColumns = [];
29
+ const secretColumns = [];
30
+ logger.info('Classification audit:');
31
+ for (const [table, cols] of Object.entries(manifest.tables)) {
32
+ logger.info(` ${table}:`);
33
+ for (const [col, info] of Object.entries(cols)) {
34
+ const { classification, anonymize_strategy } = info;
35
+ const strategyLabel = anonymize_strategy ?? '(null → will be nulled on clone)';
36
+ if (classification === 'public') {
37
+ publicCount++;
38
+ logger.info(` ${col.padEnd(30)} public`);
39
+ }
40
+ else if (classification === 'secret') {
41
+ secretCount++;
42
+ secretColumns.push(`${table}.${col}`);
43
+ if (!anonymize_strategy)
44
+ noStrategyColumns.push(`${table}.${col}`);
45
+ logger.info(` ${col.padEnd(30)} secret ${strategyLabel}`);
46
+ }
47
+ else {
48
+ privateCount++;
49
+ if (!anonymize_strategy)
50
+ noStrategyColumns.push(`${table}.${col}`);
51
+ logger.info(` ${col.padEnd(30)} private ${strategyLabel}`);
52
+ }
53
+ }
54
+ }
55
+ const total = publicCount + privateCount + secretCount;
56
+ logger.info('');
57
+ logger.info(`Summary: ${total} columns total — ` +
58
+ `${publicCount} public, ${privateCount} private, ${secretCount} secret`);
59
+ if (secretColumns.length > 0) {
60
+ logger.info(`Secret columns (extra-sensitive): ${secretColumns.join(', ')}`);
61
+ }
62
+ if (noStrategyColumns.length > 0) {
63
+ logger.warn(`${noStrategyColumns.length} private/secret column(s) have no anonymize strategy ` +
64
+ `and will be NULLed on clone: ${noStrategyColumns.join(', ')}`);
65
+ }
66
+ },
67
+ });
@@ -1,22 +1,18 @@
1
1
  import { pikkuSessionlessFunc } from '#pikku';
2
- import { resolveLocalDb, migrateAndCodegen } from '../db/local-db.js';
2
+ import { resolveDb, migrateAndCodegen } from '../db/local-db.js';
3
3
  import { loadUserConfigForDb } from './db-shared.js';
4
4
  export const dbMigrate = pikkuSessionlessFunc({
5
5
  remote: true,
6
- func: async ({ logger, config, getInspectorState }) => {
7
- const userConfig = await loadUserConfigForDb({
8
- config,
9
- getInspectorState,
10
- logger,
11
- });
6
+ func: async ({ logger, config }) => {
7
+ const userConfig = await loadUserConfigForDb({ config, logger });
12
8
  if (!userConfig)
13
9
  return;
14
- const resolved = resolveLocalDb(userConfig.dev?.db, config.rootDir, config.outDir, config.runtimeDir);
10
+ const resolved = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
15
11
  if (!resolved) {
16
- logger.error('pikku db migrate: dev.db is not configured in your pikku config.');
17
- throw new Error('dev.db not configured');
12
+ logger.error('pikku db migrate: no database configured — set sqliteDb or postgresUrl in your createConfig.');
13
+ throw new Error('no database configured');
18
14
  }
19
- const { migrate, codegen, zod } = migrateAndCodegen(resolved);
15
+ const { migrate, codegen, zod } = await migrateAndCodegen(resolved);
20
16
  if (migrate.applied.length === 0) {
21
17
  logger.info(`db migrate: no pending migrations (${migrate.skipped.length} already applied)`);
22
18
  }
@@ -1,24 +1,24 @@
1
1
  import { pikkuSessionlessFunc } from '#pikku';
2
- import { resolveLocalDb, reset, migrateAndCodegen, seed, } from '../db/local-db.js';
2
+ import { resolveDb, reset, migrateAndCodegen, seed } from '../db/local-db.js';
3
3
  import { loadUserConfigForDb } from './db-shared.js';
4
4
  export const dbReset = pikkuSessionlessFunc({
5
5
  remote: true,
6
- func: async ({ logger, config, getInspectorState }) => {
7
- const userConfig = await loadUserConfigForDb({
8
- config,
9
- getInspectorState,
10
- logger,
11
- });
6
+ func: async ({ logger, config }) => {
7
+ const userConfig = await loadUserConfigForDb({ config, logger });
12
8
  if (!userConfig)
13
9
  return;
14
- const resolved = resolveLocalDb(userConfig.dev?.db, config.rootDir, config.outDir, config.runtimeDir);
10
+ const resolved = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
15
11
  if (!resolved) {
16
- logger.error('pikku db reset: dev.db is not configured in your pikku config.');
17
- throw new Error('dev.db not configured');
12
+ logger.error('pikku db reset: no database configured — set sqliteDb in your createConfig.');
13
+ throw new Error('no database configured');
14
+ }
15
+ if (resolved.dialect !== 'sqlite') {
16
+ logger.error('pikku db reset: reset is only supported for SQLite databases.');
17
+ throw new Error('reset not supported for postgres');
18
18
  }
19
19
  reset(resolved, config.rootDir);
20
20
  logger.info(`db reset: removed ${resolved.dbFile}`);
21
- const { migrate, codegen, zod } = migrateAndCodegen(resolved);
21
+ const { migrate, codegen, zod } = await migrateAndCodegen(resolved);
22
22
  for (const name of migrate.applied) {
23
23
  logger.info(`db reset: applied ${name}`);
24
24
  }
@@ -28,7 +28,7 @@ export const dbReset = pikkuSessionlessFunc({
28
28
  logger.info(zod.written
29
29
  ? `db reset: regenerated ${zod.outFile} (${zod.tables.length} tables)`
30
30
  : `db reset: ${zod.outFile} unchanged`);
31
- const seedResult = seed(resolved);
31
+ const seedResult = await seed(resolved);
32
32
  if (seedResult.applied) {
33
33
  logger.info(`db reset: seeded ${resolved.seedFile} (${seedResult.bytes} bytes)`);
34
34
  }
@@ -1,22 +1,22 @@
1
1
  import { pikkuSessionlessFunc } from '#pikku';
2
- import { resolveLocalDb, seed } from '../db/local-db.js';
2
+ import { resolveDb, seed } from '../db/local-db.js';
3
3
  import { loadUserConfigForDb } from './db-shared.js';
4
4
  export const dbSeed = pikkuSessionlessFunc({
5
5
  remote: true,
6
- func: async ({ logger, config, getInspectorState }) => {
7
- const userConfig = await loadUserConfigForDb({
8
- config,
9
- getInspectorState,
10
- logger,
11
- });
6
+ func: async ({ logger, config }) => {
7
+ const userConfig = await loadUserConfigForDb({ config, logger });
12
8
  if (!userConfig)
13
9
  return;
14
- const resolved = resolveLocalDb(userConfig.dev?.db, config.rootDir, config.outDir, config.runtimeDir);
10
+ const resolved = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
15
11
  if (!resolved) {
16
- logger.error('pikku db seed: dev.db is not configured in your pikku config.');
17
- throw new Error('dev.db not configured');
12
+ logger.error('pikku db seed: no database configured — set sqliteDb in your createConfig.');
13
+ throw new Error('no database configured');
18
14
  }
19
- const result = seed(resolved);
15
+ if (resolved.dialect !== 'sqlite') {
16
+ logger.error('pikku db seed: seed is only supported for SQLite databases.');
17
+ throw new Error('seed not supported for postgres');
18
+ }
19
+ const result = await seed(resolved);
20
20
  if (!result.applied) {
21
21
  logger.info(`db seed: no ${resolved.seedFile} found, nothing to do`);
22
22
  }
@@ -1,32 +1,17 @@
1
- import type { DevDbConfig } from '../db/local-db.js';
2
1
  export interface UserConfigShape {
3
- dev?: {
4
- db?: DevDbConfig;
5
- };
2
+ sqliteDb?: string;
3
+ postgresUrl?: string;
6
4
  [key: string]: unknown;
7
5
  }
8
6
  interface LoadOptions {
9
7
  config: {
10
8
  rootDir: string;
11
- outDir: string;
9
+ srcDirectories: string[];
12
10
  };
13
- getInspectorState: (refresh: boolean) => Promise<{
14
- filesAndMethods: {
15
- pikkuConfigFactory?: {
16
- file: string;
17
- variable: string;
18
- };
19
- };
20
- }>;
21
11
  logger: {
22
12
  error: (msg: string) => void;
13
+ warn: (msg: string) => void;
23
14
  };
24
15
  }
25
- /**
26
- * Load the user's pikkuConfig the same way `dev.ts` does — through the
27
- * inspector state, then by importing the user's config factory file.
28
- * Returns `null` (and logs the error) if the project hasn't defined a
29
- * pikkuConfigFactory, so the caller can early-exit.
30
- */
31
16
  export declare function loadUserConfigForDb(options: LoadOptions): Promise<UserConfigShape | null>;
32
17
  export {};
@@ -1,25 +1,61 @@
1
- import { resolve } from 'path';
2
- import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
3
- /**
4
- * Load the user's pikkuConfig the same way `dev.ts` does — through the
5
- * inspector state, then by importing the user's config factory file.
6
- * Returns `null` (and logs the error) if the project hasn't defined a
7
- * pikkuConfigFactory, so the caller can early-exit.
8
- */
1
+ import { existsSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { loadUserModule } from './load-user-project.js';
4
+ function findUserConfigFactoryFile(rootDir, srcDirectories) {
5
+ for (const srcDir of srcDirectories) {
6
+ for (const name of ['config.ts', 'config.js']) {
7
+ const candidate = resolve(rootDir, srcDir, name);
8
+ if (existsSync(candidate))
9
+ return candidate;
10
+ }
11
+ }
12
+ for (const name of ['config.ts', 'config.js']) {
13
+ const candidate = join(rootDir, name);
14
+ if (existsSync(candidate))
15
+ return candidate;
16
+ }
17
+ return null;
18
+ }
9
19
  export async function loadUserConfigForDb(options) {
10
- const { config, getInspectorState, logger } = options;
11
- const inspectorState = await getInspectorState(true);
12
- const { pikkuConfigFactory } = inspectorState.filesAndMethods;
13
- if (!pikkuConfigFactory) {
20
+ const { config, logger } = options;
21
+ const hasSqliteDbAssets = existsSync(join(config.rootDir, 'db', 'sqlite'));
22
+ const hasPostgresDbAssets = existsSync(join(config.rootDir, 'db', 'postgres'));
23
+ const hasConventionalDbAssets = hasSqliteDbAssets || hasPostgresDbAssets;
24
+ const getFallbackConfig = () => {
25
+ if (hasSqliteDbAssets)
26
+ return { sqliteDb: '.pikku-runtime/dev.db' };
27
+ if (hasPostgresDbAssets) {
28
+ logger.error('Postgres assets detected but postgresUrl is not configured in createConfig.');
29
+ return null;
30
+ }
31
+ return null;
32
+ };
33
+ const configFactoryFile = findUserConfigFactoryFile(config.rootDir, config.srcDirectories);
34
+ if (!configFactoryFile) {
35
+ if (hasConventionalDbAssets) {
36
+ return getFallbackConfig();
37
+ }
14
38
  logger.error('createConfig must be defined in your project');
15
39
  return null;
16
40
  }
17
- const pikkuDir = resolve(config.rootDir, config.outDir);
18
- await loadUserBootstrap(pikkuDir);
19
- const configModule = await loadUserModule(pikkuConfigFactory.file);
20
- const userCreateConfig = configModule[pikkuConfigFactory.variable];
41
+ let configModule;
42
+ try {
43
+ configModule = await loadUserModule(configFactoryFile);
44
+ }
45
+ catch (error) {
46
+ if (hasConventionalDbAssets) {
47
+ logger.warn(`Falling back to default local db config because '${configFactoryFile}' could not be loaded: ${error.message}`);
48
+ return getFallbackConfig();
49
+ }
50
+ throw error;
51
+ }
52
+ const userCreateConfig = configModule.createConfig;
21
53
  if (typeof userCreateConfig !== 'function') {
22
- logger.error(`Expected '${pikkuConfigFactory.variable}' in '${pikkuConfigFactory.file}' to be a function`);
54
+ if (hasConventionalDbAssets) {
55
+ logger.warn(`Falling back to default local db config because '${configFactoryFile}' does not export createConfig`);
56
+ return getFallbackConfig();
57
+ }
58
+ logger.error(`Expected 'createConfig' in '${configFactoryFile}' to be a function`);
23
59
  return null;
24
60
  }
25
61
  return (await userCreateConfig());