@pikku/cli 0.12.24 → 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 +163 -99
  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 +17 -14
  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 +15 -5
  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 +11 -6
  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 +12 -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-BDOqBctb.js +0 -232
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.24
2
+ * This file was generated by @pikku/cli@0.12.25
3
3
  */
4
4
  import { addWorkflow } from '@pikku/core/workflow';
5
5
  import './pikku-workflow-wirings-meta.gen.js';
@@ -11,8 +11,8 @@ async function checkForUpdate() {
11
11
  })
12
12
  if (!res.ok) return
13
13
  const { version: latest } = await res.json()
14
- if (latest !== '0.12.24') {
15
- process.stderr.write(`\n Update available 0.12.24 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
14
+ if (latest !== '0.12.25') {
15
+ process.stderr.write(`\n Update available 0.12.25 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
16
16
  }
17
17
  } catch {}
18
18
  }
@@ -7,6 +7,7 @@ import { pikkuTanStackStart } from './functions/runtimes/tanstack-start/pikku-co
7
7
  import { pikkuQueueService } from './functions/wirings/queue/pikku-command-queue-service.js';
8
8
  import { pikkuOpenAPI } from './functions/wirings/http/pikku-command-openapi.js';
9
9
  import { pikkuNext } from './functions/runtimes/nextjs/pikku-command-nextjs.js';
10
+ import { pikkuEmails } from './functions/wirings/emails/pikku-command-emails.js';
10
11
  import { pikkuCLICommand, wireCLI } from '../.pikku/cli/pikku-cli-types.gen.js';
11
12
  import { fabricCommands } from './fabric/fabric-commands.js';
12
13
  import { all } from './functions/commands/all.js';
@@ -17,8 +18,10 @@ import { dev } from './functions/commands/dev.js';
17
18
  import { dbMigrate } from './functions/commands/db-migrate.js';
18
19
  import { dbSeed } from './functions/commands/db-seed.js';
19
20
  import { dbReset } from './functions/commands/db-reset.js';
21
+ import { workspaceValidate, renderWorkspaceValidate, } from './functions/commands/workspace-validate.js';
20
22
  import { pikkuVersionsInit } from './functions/commands/versions-init.js';
21
23
  import { pikkuTestsInit } from './functions/commands/tests-init.js';
24
+ import { pikkuEmailsInit } from './functions/commands/emails-init.js';
22
25
  import { pikkuTestsCoverage } from './functions/commands/tests-coverage.js';
23
26
  import { pikkuVersionsCheck } from './functions/commands/versions-check.js';
24
27
  import { pikkuVersionsUpdate } from './functions/commands/versions-update.js';
@@ -194,6 +197,24 @@ wireCLI({
194
197
  func: pikkuSchemas,
195
198
  description: 'Generate JSON schemas for function input/output types',
196
199
  }),
200
+ emails: {
201
+ description: 'Email template generation commands',
202
+ subcommands: {
203
+ init: pikkuCLICommand({
204
+ func: pikkuEmailsInit,
205
+ description: 'Scaffold emailTemplatesDir with starter locales, theme, partials, and a hello-world email',
206
+ options: {
207
+ force: {
208
+ description: 'Overwrite an existing email scaffold',
209
+ },
210
+ },
211
+ }),
212
+ generate: pikkuCLICommand({
213
+ func: pikkuEmails,
214
+ description: 'Generate typed email renderers and metadata from emailTemplatesDir in pikku.config.json',
215
+ }),
216
+ },
217
+ },
197
218
  db: {
198
219
  description: 'Local development database commands',
199
220
  subcommands: {
@@ -211,6 +232,16 @@ wireCLI({
211
232
  }),
212
233
  },
213
234
  },
235
+ workspace: {
236
+ description: 'Workspace-level validation and maintenance commands',
237
+ subcommands: {
238
+ validate: pikkuCLICommand({
239
+ func: workspaceValidate,
240
+ render: renderWorkspaceValidate,
241
+ description: 'Check the project structure for Pikku workspace compatibility',
242
+ }),
243
+ },
244
+ },
214
245
  fabric: {
215
246
  description: 'PikkuFabric commands (login, link, deploy, domains, secrets, logs, …)',
216
247
  subcommands: fabricCommands,
@@ -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'),
@@ -151,8 +154,10 @@ export const dev = pikkuSessionlessFunc({
151
154
  // single instance under both names so addons like @pikku/addon-console
152
155
  // can read runs in dev without projects having to wire their own backing
153
156
  // store.
157
+ const devLogger = new ConsoleLogger();
154
158
  const inMemoryServices = {
155
- logger: new ConsoleLogger(),
159
+ logger: devLogger,
160
+ emailService: new LocalEmailService(),
156
161
  metaService: new LocalMetaService(pikkuDir),
157
162
  schedulerService,
158
163
  queueService: new InMemoryQueueService(),
@@ -205,20 +210,20 @@ export const dev = pikkuSessionlessFunc({
205
210
  });
206
211
  if (enableHmr) {
207
212
  await pikkuDevReloader({
208
- srcDirectories: config.srcDirectories,
213
+ srcDirectories: watchDirectories,
209
214
  logger,
210
215
  });
211
216
  }
212
217
  if (enableWatch) {
213
218
  const genIgnore = /\.gen\.tsx?$/;
214
- configWatcher = chokidar.watch(config.srcDirectories, {
219
+ configWatcher = chokidar.watch(watchDirectories, {
215
220
  ignoreInitial: true,
216
221
  ignored: genIgnore,
217
222
  });
218
223
  const generatorWatcher = () => {
219
224
  watcher?.close();
220
- logger.info(`• Watching directories: \n - ${config.srcDirectories.join('\n - ')}`);
221
- watcher = chokidar.watch(config.srcDirectories, {
225
+ logger.info(`• Watching directories: \n - ${watchDirectories.join('\n - ')}`);
226
+ watcher = chokidar.watch(watchDirectories, {
222
227
  ignoreInitial: true,
223
228
  ignored: genIgnore,
224
229
  });