@pikku/cli 0.12.23 → 0.12.25

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 (120) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/index-D4DgafuS.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 +16 -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 +41 -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 +130 -66
  21. package/dist/.pikku/function/pikku-functions.gen.js +1 -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 +20 -17
  44. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  46. package/dist/.pikku/schemas/register.gen.js +13 -3
  47. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  48. package/dist/.pikku/schemas/schemas/PikkuEmailsOutput.schema.json +1 -0
  49. package/dist/.pikku/schemas/schemas/PikkuFunctionTypesSplitInput.schema.json +1 -0
  50. package/dist/.pikku/schemas/schemas/PikkuTriggerTypesInput.schema.json +1 -0
  51. package/dist/.pikku/schemas/schemas/WorkspaceValidateInput.schema.json +1 -0
  52. package/dist/.pikku/schemas/schemas/WorkspaceValidateOutput.schema.json +1 -0
  53. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  54. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  55. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  56. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  57. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  58. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  59. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  60. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  61. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  62. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  63. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +5 -5
  64. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  65. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  66. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  68. package/dist/bin/pikku-bin.mjs +2 -2
  69. package/dist/src/cli.wiring.js +31 -0
  70. package/dist/src/fabric/functions/validate-core.d.ts +20 -0
  71. package/dist/src/fabric/functions/validate-core.js +227 -0
  72. package/dist/src/fabric/functions/validate.function.js +11 -3
  73. package/dist/src/functions/commands/bootstrap.js +2 -2
  74. package/dist/src/functions/commands/console.js +7 -4
  75. package/dist/src/functions/commands/db-migrate.js +2 -3
  76. package/dist/src/functions/commands/db-reset.js +3 -4
  77. package/dist/src/functions/commands/db-seed.js +2 -3
  78. package/dist/src/functions/commands/db-shared.d.ts +2 -15
  79. package/dist/src/functions/commands/db-shared.js +43 -17
  80. package/dist/src/functions/commands/dev.js +28 -8
  81. package/dist/src/functions/commands/emails-init.d.ts +5 -0
  82. package/dist/src/functions/commands/emails-init.js +162 -0
  83. package/dist/src/functions/commands/load-user-project.js +12 -3
  84. package/dist/src/functions/commands/watch.js +7 -4
  85. package/dist/src/functions/commands/workspace-validate.d.ts +33 -0
  86. package/dist/src/functions/commands/workspace-validate.js +9 -0
  87. package/dist/src/functions/db/coercion-plugin.d.ts +7 -0
  88. package/dist/src/functions/db/coercion-plugin.js +99 -0
  89. package/dist/src/functions/db/local-db.d.ts +2 -2
  90. package/dist/src/functions/db/local-db.js +13 -8
  91. package/dist/src/functions/db/seed.d.ts +2 -2
  92. package/dist/src/functions/db/sql-migrator.d.ts +3 -3
  93. package/dist/src/functions/db/sqlite-codegen.d.ts +3 -3
  94. package/dist/src/functions/db/sqlite-kysely.d.ts +8 -0
  95. package/dist/src/functions/db/sqlite-kysely.js +62 -0
  96. package/dist/src/functions/db/sqlite-runtime-bun.d.ts +2 -0
  97. package/dist/src/functions/db/sqlite-runtime-bun.js +52 -0
  98. package/dist/src/functions/db/sqlite-runtime-node.d.ts +2 -0
  99. package/dist/src/functions/db/sqlite-runtime-node.js +51 -0
  100. package/dist/src/functions/db/sqlite-runtime.d.ts +20 -0
  101. package/dist/src/functions/db/sqlite-runtime.js +13 -0
  102. package/dist/src/functions/validate/workspace-validate.d.ts +34 -0
  103. package/dist/src/functions/validate/workspace-validate.js +258 -0
  104. package/dist/src/functions/wirings/cli/pikku-command-cli-types.js +1 -1
  105. package/dist/src/functions/wirings/emails/pikku-command-emails.d.ts +6 -0
  106. package/dist/src/functions/wirings/emails/pikku-command-emails.js +172 -0
  107. package/dist/src/functions/wirings/emails/serialize-emails.d.ts +20 -0
  108. package/dist/src/functions/wirings/emails/serialize-emails.js +168 -0
  109. package/dist/src/functions/wirings/functions/pikku-command-function-types-split.d.ts +7 -1
  110. package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +2 -2
  111. package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.d.ts +7 -1
  112. package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.js +2 -2
  113. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +1 -1
  114. package/dist/src/functions/workflows/all.workflow.js +12 -7
  115. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  116. package/dist/src/utils/pikku-cli-config.js +6 -0
  117. package/dist/tsconfig.tsbuildinfo +1 -1
  118. package/package.json +3 -3
  119. package/skills/pikku-auth-js/SKILL.md +271 -58
  120. package/console-app/assets/index-CAk106ji.js +0 -232
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ export declare const FabricValidateInput: z.ZodObject<{}, z.core.$strip>;
3
+ export declare const FabricValidateOutput: z.ZodObject<{
4
+ ok: z.ZodBoolean;
5
+ root: z.ZodString;
6
+ findings: z.ZodArray<z.ZodObject<{
7
+ id: z.ZodString;
8
+ severity: z.ZodEnum<{
9
+ info: "info";
10
+ warn: "warn";
11
+ error: "error";
12
+ }>;
13
+ message: z.ZodString;
14
+ path: z.ZodString;
15
+ fixHint: z.ZodString;
16
+ }, z.core.$strip>>;
17
+ }, z.core.$strip>;
18
+ export declare function runFabricValidate(startDir?: string): Promise<z.infer<typeof FabricValidateOutput>>;
19
+ export declare const runValidate: typeof runFabricValidate;
20
+ export declare const renderValidate: (_s: unknown, { ok, root, findings }: z.infer<typeof FabricValidateOutput>) => void;
@@ -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);
@@ -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
  });
@@ -3,10 +3,9 @@ import { resolveLocalDb, 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 }) => {
6
+ func: async ({ logger, config }) => {
7
7
  const userConfig = await loadUserConfigForDb({
8
8
  config,
9
- getInspectorState,
10
9
  logger,
11
10
  });
12
11
  if (!userConfig)
@@ -16,7 +15,7 @@ export const dbMigrate = pikkuSessionlessFunc({
16
15
  logger.error('pikku db migrate: dev.db is not configured in your pikku config.');
17
16
  throw new Error('dev.db not configured');
18
17
  }
19
- const { migrate, codegen, zod } = migrateAndCodegen(resolved);
18
+ const { migrate, codegen, zod } = await migrateAndCodegen(resolved);
20
19
  if (migrate.applied.length === 0) {
21
20
  logger.info(`db migrate: no pending migrations (${migrate.skipped.length} already applied)`);
22
21
  }
@@ -3,10 +3,9 @@ import { resolveLocalDb, reset, migrateAndCodegen, seed, } from '../db/local-db.
3
3
  import { loadUserConfigForDb } from './db-shared.js';
4
4
  export const dbReset = pikkuSessionlessFunc({
5
5
  remote: true,
6
- func: async ({ logger, config, getInspectorState }) => {
6
+ func: async ({ logger, config }) => {
7
7
  const userConfig = await loadUserConfigForDb({
8
8
  config,
9
- getInspectorState,
10
9
  logger,
11
10
  });
12
11
  if (!userConfig)
@@ -18,7 +17,7 @@ export const dbReset = pikkuSessionlessFunc({
18
17
  }
19
18
  reset(resolved, config.rootDir);
20
19
  logger.info(`db reset: removed ${resolved.dbFile}`);
21
- const { migrate, codegen, zod } = migrateAndCodegen(resolved);
20
+ const { migrate, codegen, zod } = await migrateAndCodegen(resolved);
22
21
  for (const name of migrate.applied) {
23
22
  logger.info(`db reset: applied ${name}`);
24
23
  }
@@ -28,7 +27,7 @@ export const dbReset = pikkuSessionlessFunc({
28
27
  logger.info(zod.written
29
28
  ? `db reset: regenerated ${zod.outFile} (${zod.tables.length} tables)`
30
29
  : `db reset: ${zod.outFile} unchanged`);
31
- const seedResult = seed(resolved);
30
+ const seedResult = await seed(resolved);
32
31
  if (seedResult.applied) {
33
32
  logger.info(`db reset: seeded ${resolved.seedFile} (${seedResult.bytes} bytes)`);
34
33
  }
@@ -3,10 +3,9 @@ import { resolveLocalDb, 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 }) => {
6
+ func: async ({ logger, config }) => {
7
7
  const userConfig = await loadUserConfigForDb({
8
8
  config,
9
- getInspectorState,
10
9
  logger,
11
10
  });
12
11
  if (!userConfig)
@@ -16,7 +15,7 @@ export const dbSeed = pikkuSessionlessFunc({
16
15
  logger.error('pikku db seed: dev.db is not configured in your pikku config.');
17
16
  throw new Error('dev.db not configured');
18
17
  }
19
- const result = seed(resolved);
18
+ const result = await seed(resolved);
20
19
  if (!result.applied) {
21
20
  logger.info(`db seed: no ${resolved.seedFile} found, nothing to do`);
22
21
  }
@@ -8,25 +8,12 @@ export interface UserConfigShape {
8
8
  interface LoadOptions {
9
9
  config: {
10
10
  rootDir: string;
11
- outDir: string;
11
+ srcDirectories: string[];
12
12
  };
13
- getInspectorState: (refresh: boolean) => Promise<{
14
- filesAndMethods: {
15
- pikkuConfigFactory?: {
16
- file: string;
17
- variable: string;
18
- };
19
- };
20
- }>;
21
13
  logger: {
22
14
  error: (msg: string) => void;
15
+ warn: (msg: string) => void;
23
16
  };
24
17
  }
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
18
  export declare function loadUserConfigForDb(options: LoadOptions): Promise<UserConfigShape | null>;
32
19
  export {};
@@ -1,25 +1,51 @@
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 hasConventionalDbAssets = existsSync(join(config.rootDir, 'db', 'migrations')) ||
22
+ existsSync(join(config.rootDir, 'db', 'seed.sql'));
23
+ const configFactoryFile = findUserConfigFactoryFile(config.rootDir, config.srcDirectories);
24
+ if (!configFactoryFile) {
25
+ if (hasConventionalDbAssets) {
26
+ return { dev: { db: true } };
27
+ }
14
28
  logger.error('createConfig must be defined in your project');
15
29
  return null;
16
30
  }
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];
31
+ let configModule;
32
+ try {
33
+ configModule = await loadUserModule(configFactoryFile);
34
+ }
35
+ catch (error) {
36
+ if (hasConventionalDbAssets) {
37
+ logger.warn(`Falling back to default local db config because '${configFactoryFile}' could not be loaded: ${error.message}`);
38
+ return { dev: { db: true } };
39
+ }
40
+ throw error;
41
+ }
42
+ const userCreateConfig = configModule.createConfig;
21
43
  if (typeof userCreateConfig !== 'function') {
22
- logger.error(`Expected '${pikkuConfigFactory.variable}' in '${pikkuConfigFactory.file}' to be a function`);
44
+ if (hasConventionalDbAssets) {
45
+ logger.warn(`Falling back to default local db config because '${configFactoryFile}' does not export createConfig`);
46
+ return { dev: { db: true } };
47
+ }
48
+ logger.error(`Expected 'createConfig' in '${configFactoryFile}' to be a function`);
23
49
  return null;
24
50
  }
25
51
  return (await userCreateConfig());
@@ -3,7 +3,7 @@ import { join, resolve } from 'path';
3
3
  import { pikkuSessionlessFunc } from '#pikku';
4
4
  import chokidar from 'chokidar';
5
5
  import { pikkuDevReloader } from '@pikku/core/dev';
6
- import { ConsoleLogger, InMemoryQueueService, InMemoryWorkflowService, InMemoryTriggerService, InMemoryAIRunStateService, } from '@pikku/core/services';
6
+ import { ConsoleLogger, LocalEmailService, InMemoryQueueService, InMemoryWorkflowService, InMemoryTriggerService, InMemoryAIRunStateService, } from '@pikku/core/services';
7
7
  import { KyselyAIStorageService, KyselyAIRunStateService, KyselyAgentRunService, } from '@pikku/kysely';
8
8
  import { stopSingletonServices } from '@pikku/core';
9
9
  import { pikkuState } from '@pikku/core/internal';
@@ -23,6 +23,9 @@ export const dev = pikkuSessionlessFunc({
23
23
  const hostname = 'localhost';
24
24
  const enableWatch = watch !== false;
25
25
  const enableHmr = hmr !== false;
26
+ const watchDirectories = [
27
+ ...new Set([config.emailTemplatesDir, ...config.srcDirectories].filter(Boolean)),
28
+ ];
26
29
  const commandSingletonServices = pikkuState(null, 'package', 'singletonServices');
27
30
  const commandFunctionMeta = {
28
31
  ...pikkuState(null, 'function', 'meta'),
@@ -49,7 +52,22 @@ export const dev = pikkuSessionlessFunc({
49
52
  const previousRPCMeta = pikkuState(null, 'rpc', 'meta');
50
53
  const previousWorkflowsMeta = pikkuState(null, 'workflows', 'meta');
51
54
  const previousWorkflowRegistrations = pikkuState(null, 'workflows', 'registrations');
52
- pikkuState(null, 'package', 'singletonServices', commandSingletonServices);
55
+ // During hot-reload (when user services are already live), build a hybrid services
56
+ // object: user services (so in-flight requests keep kysely/content/etc.) overlaid
57
+ // with the CLI config (outDir, scaffold, schemaDirectory, etc. — required by
58
+ // allWorkflow for code generation paths). Replacing the entire services object with
59
+ // commandSingletonServices during hot-reload caused a race condition where concurrent
60
+ // auth requests saw CLI services (no kysely) and crashed.
61
+ const isHotReload = previousSingletonServices !== commandSingletonServices &&
62
+ !!previousSingletonServices;
63
+ const codegenServices = isHotReload
64
+ ? {
65
+ ...previousSingletonServices,
66
+ config: commandSingletonServices?.config ??
67
+ previousSingletonServices.config,
68
+ }
69
+ : commandSingletonServices;
70
+ pikkuState(null, 'package', 'singletonServices', codegenServices);
53
71
  pikkuState(null, 'function', 'functions', new Map([...previousFunctions.entries(), ...commandFunctions.entries()]));
54
72
  pikkuState(null, 'function', 'meta', {
55
73
  ...previousFunctionMeta,
@@ -98,7 +116,7 @@ export const dev = pikkuSessionlessFunc({
98
116
  const userCreateConfig = configModule[pikkuConfigFactory.variable];
99
117
  const userCreateSingletonServices = servicesModule[singletonServicesFactory.variable];
100
118
  const userConfig = await userCreateConfig();
101
- const resolvedLocalDb = resolveLocalDb(userConfig.dev?.db, config.rootDir, config.outDir, config.runtimeDir);
119
+ const resolvedLocalDb = resolveLocalDb(userConfig.dev?.db ?? true, config.rootDir, config.outDir, config.runtimeDir);
102
120
  const kysely = resolvedLocalDb
103
121
  ? await createKysely(resolvedLocalDb)
104
122
  : undefined;
@@ -136,8 +154,10 @@ export const dev = pikkuSessionlessFunc({
136
154
  // single instance under both names so addons like @pikku/addon-console
137
155
  // can read runs in dev without projects having to wire their own backing
138
156
  // store.
157
+ const devLogger = new ConsoleLogger();
139
158
  const inMemoryServices = {
140
- logger: new ConsoleLogger(),
159
+ logger: devLogger,
160
+ emailService: new LocalEmailService(),
141
161
  metaService: new LocalMetaService(pikkuDir),
142
162
  schedulerService,
143
163
  queueService: new InMemoryQueueService(),
@@ -190,20 +210,20 @@ export const dev = pikkuSessionlessFunc({
190
210
  });
191
211
  if (enableHmr) {
192
212
  await pikkuDevReloader({
193
- srcDirectories: config.srcDirectories,
213
+ srcDirectories: watchDirectories,
194
214
  logger,
195
215
  });
196
216
  }
197
217
  if (enableWatch) {
198
218
  const genIgnore = /\.gen\.tsx?$/;
199
- configWatcher = chokidar.watch(config.srcDirectories, {
219
+ configWatcher = chokidar.watch(watchDirectories, {
200
220
  ignoreInitial: true,
201
221
  ignored: genIgnore,
202
222
  });
203
223
  const generatorWatcher = () => {
204
224
  watcher?.close();
205
- logger.info(`• Watching directories: \n - ${config.srcDirectories.join('\n - ')}`);
206
- watcher = chokidar.watch(config.srcDirectories, {
225
+ logger.info(`• Watching directories: \n - ${watchDirectories.join('\n - ')}`);
226
+ watcher = chokidar.watch(watchDirectories, {
207
227
  ignoreInitial: true,
208
228
  ignored: genIgnore,
209
229
  });
@@ -0,0 +1,5 @@
1
+ type EmailsInitInput = {
2
+ force?: boolean;
3
+ };
4
+ export declare const pikkuEmailsInit: import("#pikku").PikkuFunctionConfig<EmailsInitInput, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<EmailsInitInput, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<EmailsInitInput, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
5
+ export {};