@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
@@ -0,0 +1,51 @@
1
+ class NodeSqliteStatement {
2
+ stmt;
3
+ reader;
4
+ constructor(stmt) {
5
+ this.stmt = stmt;
6
+ this.reader = Boolean(stmt.reader);
7
+ }
8
+ all(...parameters) {
9
+ return this.stmt.all(...parameters);
10
+ }
11
+ get(...parameters) {
12
+ return this.stmt.get(...parameters) ?? null;
13
+ }
14
+ iterate(...parameters) {
15
+ return this.stmt.iterate(...parameters);
16
+ }
17
+ run(...parameters) {
18
+ const result = this.stmt.run(...parameters);
19
+ return {
20
+ changes: result.changes,
21
+ lastInsertRowid: result.lastInsertRowid,
22
+ };
23
+ }
24
+ }
25
+ class NodeSqliteDatabase {
26
+ db;
27
+ constructor(db) {
28
+ this.db = db;
29
+ }
30
+ exec(sql) {
31
+ this.db.exec(sql);
32
+ }
33
+ prepare(sql) {
34
+ return new NodeSqliteStatement(this.db.prepare(sql));
35
+ }
36
+ close() {
37
+ this.db.close();
38
+ }
39
+ }
40
+ async function importNodeSqlite() {
41
+ const dynamicImport = new Function('return import("node:sqlite")');
42
+ return dynamicImport();
43
+ }
44
+ export async function createNodeSqliteRuntime() {
45
+ const { DatabaseSync } = await importNodeSqlite();
46
+ return {
47
+ open(filename) {
48
+ return new NodeSqliteDatabase(new DatabaseSync(filename));
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,20 @@
1
+ export interface SyncSqliteChanges {
2
+ changes: number | bigint;
3
+ lastInsertRowid: number | bigint;
4
+ }
5
+ export interface SyncSqliteStatement {
6
+ reader: boolean;
7
+ all(...parameters: unknown[]): unknown[];
8
+ get(...parameters: unknown[]): unknown | null;
9
+ iterate(...parameters: unknown[]): IterableIterator<unknown>;
10
+ run(...parameters: unknown[]): SyncSqliteChanges;
11
+ }
12
+ export interface SyncSqliteDatabase {
13
+ exec(sql: string): void;
14
+ prepare(sql: string): SyncSqliteStatement;
15
+ close(): void;
16
+ }
17
+ export interface SqliteRuntime {
18
+ open(filename: string): SyncSqliteDatabase;
19
+ }
20
+ export declare function loadSqliteRuntime(): Promise<SqliteRuntime>;
@@ -0,0 +1,13 @@
1
+ let runtimePromise;
2
+ export async function loadSqliteRuntime() {
3
+ runtimePromise ??= (async () => {
4
+ const isBunRuntime = typeof globalThis.Bun !== 'undefined';
5
+ if (isBunRuntime) {
6
+ const { bunSqliteRuntime } = await import('./sqlite-runtime-bun.js');
7
+ return bunSqliteRuntime;
8
+ }
9
+ const { createNodeSqliteRuntime } = await import('./sqlite-runtime-node.js');
10
+ return createNodeSqliteRuntime();
11
+ })();
12
+ return runtimePromise;
13
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export declare const FindingSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ severity: z.ZodEnum<{
5
+ info: "info";
6
+ warn: "warn";
7
+ error: "error";
8
+ }>;
9
+ message: z.ZodString;
10
+ path: z.ZodString;
11
+ fixHint: z.ZodString;
12
+ }, z.core.$strip>;
13
+ export type Finding = z.infer<typeof FindingSchema>;
14
+ export declare const WorkspaceValidateInput: z.ZodObject<{}, z.core.$strip>;
15
+ export declare const WorkspaceValidateOutput: z.ZodObject<{
16
+ ok: z.ZodBoolean;
17
+ root: z.ZodString;
18
+ findings: z.ZodArray<z.ZodObject<{
19
+ id: z.ZodString;
20
+ severity: z.ZodEnum<{
21
+ info: "info";
22
+ warn: "warn";
23
+ error: "error";
24
+ }>;
25
+ message: z.ZodString;
26
+ path: z.ZodString;
27
+ fixHint: z.ZodString;
28
+ }, z.core.$strip>>;
29
+ }, z.core.$strip>;
30
+ export declare function findProjectRoot(startDir: string): Promise<string>;
31
+ export declare function readJsonSafe<T>(path: string): Promise<T | null>;
32
+ export declare function readTextSafe(path: string): Promise<string | null>;
33
+ export declare function runWorkspaceValidate(startDir?: string): Promise<z.infer<typeof WorkspaceValidateOutput>>;
34
+ export declare const renderWorkspaceValidate: (_s: unknown, { ok, root, findings }: z.infer<typeof WorkspaceValidateOutput>) => void;
@@ -0,0 +1,258 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { added, changed, dim, removed } from '../../fabric/lib/output.js';
6
+ export const FindingSchema = z.object({
7
+ id: z.string(),
8
+ severity: z.enum(['error', 'warn', 'info']),
9
+ message: z.string(),
10
+ path: z.string(),
11
+ fixHint: z.string(),
12
+ });
13
+ export const WorkspaceValidateInput = z.object({});
14
+ export const WorkspaceValidateOutput = z.object({
15
+ ok: z.boolean(),
16
+ root: z.string(),
17
+ findings: z.array(FindingSchema),
18
+ });
19
+ export async function findProjectRoot(startDir) {
20
+ let dir = startDir;
21
+ while (true) {
22
+ if (existsSync(join(dir, 'package.json'))) {
23
+ try {
24
+ const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'));
25
+ if (pkg.workspaces)
26
+ return dir;
27
+ }
28
+ catch {
29
+ // ignore parse errors
30
+ }
31
+ }
32
+ const parent = dirname(dir);
33
+ if (parent === dir)
34
+ return startDir;
35
+ dir = parent;
36
+ }
37
+ }
38
+ export async function readJsonSafe(path) {
39
+ if (!existsSync(path))
40
+ return null;
41
+ try {
42
+ return JSON.parse(await readFile(path, 'utf8'));
43
+ }
44
+ catch (err) {
45
+ const message = err instanceof Error ? err.message : String(err);
46
+ throw new Error(`Invalid JSON in ${path}: ${message}`);
47
+ }
48
+ }
49
+ export async function readTextSafe(path) {
50
+ if (!existsSync(path))
51
+ return null;
52
+ try {
53
+ return await readFile(path, 'utf8');
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ async function hasAuthSessionMiddleware(fnDir) {
60
+ const metaPath = join(fnDir, '.pikku', 'middleware', 'pikku-middleware-groups-meta.gen.json');
61
+ const meta = await readJsonSafe(metaPath);
62
+ if (!meta?.instances)
63
+ return false;
64
+ return Object.values(meta.instances).some((instance) => instance.definitionId === 'authJsSession');
65
+ }
66
+ function migrationCreatesTable(sql, tableName) {
67
+ const escapedTable = tableName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
68
+ const re = new RegExp(`\\bcreate\\s+table\\s+(?:if\\s+not\\s+exists\\s+)?["'\`]?${escapedTable}["'\`]?\\b`, 'i');
69
+ return re.test(sql);
70
+ }
71
+ export async function runWorkspaceValidate(startDir = process.cwd()) {
72
+ const root = await findProjectRoot(startDir);
73
+ const findings = [];
74
+ const e = (id, message, path, fixHint) => {
75
+ findings.push({ id, severity: 'error', message, path, fixHint });
76
+ };
77
+ const w = (id, message, path, fixHint) => {
78
+ findings.push({ id, severity: 'warn', message, path, fixHint });
79
+ };
80
+ const info = (id, message, path, fixHint) => {
81
+ findings.push({ id, severity: 'info', message, path, fixHint });
82
+ };
83
+ const pikkuConfigPath = join(root, 'pikku.config.json');
84
+ const pikkuConfig = await readJsonSafe(pikkuConfigPath);
85
+ if (!pikkuConfig) {
86
+ e('pikku-config-missing', 'pikku.config.json not found at project root', pikkuConfigPath, 'Create pikku.config.json with srcDirectories pointing to packages/functions/src, outDir, and clientFiles');
87
+ }
88
+ else {
89
+ if (!pikkuConfig.srcDirectories) {
90
+ e('pikku-config-no-src-dirs', 'pikku.config.json missing "srcDirectories"', pikkuConfigPath, 'Add "srcDirectories": ["packages/functions/src"] to pikku.config.json');
91
+ }
92
+ if (!pikkuConfig.outDir) {
93
+ e('pikku-config-no-out-dir', 'pikku.config.json missing "outDir"', pikkuConfigPath, 'Add "outDir": "packages/functions/.pikku" to pikku.config.json');
94
+ }
95
+ if (!pikkuConfig.clientFiles) {
96
+ info('pikku-config-no-client-files', 'pikku.config.json missing "clientFiles" — no generated SDK or React Query hooks', pikkuConfigPath, 'Add clientFiles.rpcMapDeclarationFile and clientFiles.reactQueryFile pointing to packages/functions-sdk/src/pikku/');
97
+ }
98
+ }
99
+ const hasConfiguredDevDb = Boolean(pikkuConfig?.dev?.db);
100
+ const rootPkgPath = join(root, 'package.json');
101
+ const rootPkg = await readJsonSafe(rootPkgPath);
102
+ if (!rootPkg) {
103
+ e('root-package-missing', 'root package.json not found', rootPkgPath, 'Create a root package.json with workspaces: {"workspaces": ["packages/*", "apps/*"]}');
104
+ }
105
+ else {
106
+ if (!rootPkg.workspaces) {
107
+ w('root-package-no-workspaces', 'root package.json missing "workspaces"', rootPkgPath, 'Add "workspaces": ["packages/*", "apps/*"] to enable yarn workspaces');
108
+ }
109
+ const allDeps = {
110
+ ...rootPkg.dependencies,
111
+ ...rootPkg.devDependencies,
112
+ };
113
+ if (!allDeps['@pikku/core']) {
114
+ e('missing-core', '@pikku/core not in dependencies', rootPkgPath, 'Add "@pikku/core" to dependencies');
115
+ }
116
+ for (const [pkg, spec] of Object.entries(allDeps)) {
117
+ if (typeof spec !== 'string' || !spec.startsWith('file:'))
118
+ continue;
119
+ const relPath = spec.slice(5);
120
+ if (!relPath.includes('vendor'))
121
+ continue;
122
+ const absPath = join(root, relPath);
123
+ if (!existsSync(absPath)) {
124
+ w(`vendor-missing-${pkg.replace(/[@/]/g, '-')}`, `Vendor file missing for ${pkg}: ${relPath}`, absPath, `Restore or replace the missing vendor package at ${relPath}`);
125
+ }
126
+ }
127
+ }
128
+ const fnDir = join(root, 'packages', 'functions');
129
+ if (!existsSync(fnDir)) {
130
+ e('functions-pkg-missing', 'packages/functions/ directory not found', fnDir, 'Create packages/functions/ as a workspace containing src/, config.ts, and any local db assets you use');
131
+ }
132
+ else {
133
+ const fnPkgPath = join(fnDir, 'package.json');
134
+ const fnPkg = await readJsonSafe(fnPkgPath);
135
+ if (!fnPkg) {
136
+ e('functions-package-json-missing', 'packages/functions/package.json not found', fnPkgPath, 'Create packages/functions/package.json and declare the workspace package');
137
+ }
138
+ else if (fnPkg.type !== 'module') {
139
+ w('functions-pkg-no-esm', 'packages/functions/package.json is missing "type": "module"', fnPkgPath, 'Add "type": "module" to packages/functions/package.json');
140
+ }
141
+ const servicesPath = join(fnDir, 'src', 'services.ts');
142
+ const servicesText = await readTextSafe(servicesPath);
143
+ if (!servicesText) {
144
+ w('services-missing', 'packages/functions/src/services.ts not found', servicesPath, 'Create services.ts and export your service factory for the workspace');
145
+ }
146
+ const migrationsDir = join(fnDir, 'db', 'migrations');
147
+ const authEnabled = await hasAuthSessionMiddleware(fnDir);
148
+ let createsAppUser = false;
149
+ let createsAuthVerificationToken = false;
150
+ if (existsSync(migrationsDir)) {
151
+ try {
152
+ const files = (await readdir(migrationsDir))
153
+ .filter((f) => f.endsWith('.sql'))
154
+ .sort();
155
+ const nums = [];
156
+ for (const f of files) {
157
+ const m = f.match(/^(\d+)/);
158
+ if (m)
159
+ nums.push(parseInt(m[1], 10));
160
+ }
161
+ for (let idx = 1; idx < nums.length; idx++) {
162
+ if (nums[idx] !== nums[idx - 1] + 1) {
163
+ const missing = `${nums[idx - 1] + 1}..${nums[idx] - 1}`;
164
+ 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.');
165
+ break;
166
+ }
167
+ }
168
+ for (const f of files) {
169
+ const sql = await readTextSafe(join(migrationsDir, f));
170
+ if (!sql)
171
+ continue;
172
+ createsAppUser ||= migrationCreatesTable(sql, 'app_user');
173
+ createsAuthVerificationToken ||= migrationCreatesTable(sql, 'auth_verification_token');
174
+ }
175
+ }
176
+ catch {
177
+ // readdir failure — skip
178
+ }
179
+ }
180
+ if (authEnabled && !hasConfiguredDevDb) {
181
+ e('auth-dev-db-missing', 'Auth middleware is registered, but pikku.config.json is missing dev.db so local auth schema validation and db migrate cannot run', pikkuConfigPath, 'Add dev.db to pikku.config.json so `pikku db migrate` can create and validate the local auth schema');
182
+ }
183
+ if (authEnabled && !createsAppUser) {
184
+ e('auth-schema-missing-app-user', 'Auth middleware is registered, but no SQL migration creates the app_user table', migrationsDir, 'Add a migration that creates app_user before enabling auth');
185
+ }
186
+ if (authEnabled && !createsAuthVerificationToken) {
187
+ e('auth-schema-missing-verification-token', 'Auth middleware is registered, but no SQL migration creates the auth_verification_token table', migrationsDir, 'Add a migration that creates auth_verification_token before enabling auth');
188
+ }
189
+ const dbTypesPath = join(fnDir, 'src', 'types', 'db.types.ts');
190
+ const dbTypesText = await readTextSafe(dbTypesPath);
191
+ if (dbTypesText) {
192
+ const isReexport = dbTypesText.includes('.pikku/db/schema') ||
193
+ dbTypesText.includes('.pikku\\db\\schema');
194
+ const hasInlineTypes = /(?:^|\n)\s*(?:export\s+)?(?:interface\s+\w|type\s+\w+\s*=)/.test(dbTypesText);
195
+ if (hasInlineTypes && !isReexport) {
196
+ w('db-types-hand-edited', 'src/types/db.types.ts contains inline type definitions — it should only re-export from .pikku', dbTypesPath, "Replace the file with a single line: export type { DB } from '../../.pikku/db/schema.js' then regenerate");
197
+ }
198
+ }
199
+ if (!existsSync(join(fnDir, 'src', 'functions'))) {
200
+ info('functions-dir-missing', 'packages/functions/src/functions/ not found', join(fnDir, 'src', 'functions'), 'Create src/functions/ to hold pikkuSessionlessFunc definitions');
201
+ }
202
+ if (!existsSync(join(fnDir, 'src', 'wirings'))) {
203
+ info('wirings-dir-missing', 'packages/functions/src/wirings/ not found', join(fnDir, 'src', 'wirings'), 'Create src/wirings/ for transport bindings such as *.http.ts or *.queue.ts');
204
+ }
205
+ if (!existsSync(join(fnDir, 'src', 'config.ts'))) {
206
+ info('config-missing', 'packages/functions/src/config.ts not found', join(fnDir, 'src', 'config.ts'), 'Create src/config.ts and export your workspace config factory');
207
+ }
208
+ }
209
+ const testsDir = join(fnDir, 'tests');
210
+ if (!existsSync(testsDir)) {
211
+ info('tests-missing', 'packages/functions/tests/ not found — no function test harness', testsDir, 'Run `pikku tests init` to scaffold the function test harness');
212
+ }
213
+ const sdkDir = join(root, 'packages', 'functions-sdk');
214
+ if (!existsSync(sdkDir)) {
215
+ info('functions-sdk-missing', 'packages/functions-sdk/ not found — generated RPC client and React Query hooks will not be available', sdkDir, 'Create packages/functions-sdk/ as a workspace with src/pikku/ and point clientFiles there');
216
+ }
217
+ const ok = !findings.some((f) => f.severity === 'error');
218
+ return { ok, root, findings };
219
+ }
220
+ export const renderWorkspaceValidate = (_s, { ok, root, findings }) => {
221
+ if (findings.length === 0) {
222
+ console.log(added('✓ All checks passed — workspace is Pikku-compatible'));
223
+ return;
224
+ }
225
+ const relPath = (p) => p.startsWith(root + '/') || p.startsWith(root + '\\')
226
+ ? p.slice(root.length + 1)
227
+ : p;
228
+ const errors = findings.filter((f) => f.severity === 'error');
229
+ const warns = findings.filter((f) => f.severity === 'warn');
230
+ const infos = findings.filter((f) => f.severity === 'info');
231
+ for (const f of [...errors, ...warns, ...infos]) {
232
+ const icon = f.severity === 'error'
233
+ ? removed('✗')
234
+ : f.severity === 'warn'
235
+ ? changed('⚠')
236
+ : dim('ℹ');
237
+ console.log(`${icon} ${f.message}`);
238
+ console.log(` ${dim('path:')} ${relPath(f.path)}`);
239
+ console.log(` ${dim('fix:')} ${f.fixHint}`);
240
+ console.log();
241
+ }
242
+ const counts = [];
243
+ if (errors.length) {
244
+ counts.push(removed(`${errors.length} error${errors.length !== 1 ? 's' : ''}`));
245
+ }
246
+ if (warns.length) {
247
+ counts.push(changed(`${warns.length} warning${warns.length !== 1 ? 's' : ''}`));
248
+ }
249
+ if (infos.length) {
250
+ counts.push(dim(`${infos.length} info${infos.length !== 1 ? 's' : ''}`));
251
+ }
252
+ console.log('─'.repeat(40));
253
+ console.log(counts.join(' '));
254
+ if (ok) {
255
+ console.log();
256
+ console.log(added('✓') + ' ' + dim('no errors — workspace is structurally sound'));
257
+ }
258
+ };
@@ -12,7 +12,7 @@ export const pikkuCLITypes = pikkuSessionlessFunc({
12
12
  // services types) — skip schema generation. Full mode would dynamic-import
13
13
  // pikku-types.gen.ts, which re-exports the not-yet-written cli-types file.
14
14
  const visitState = bootstrap
15
- ? await getInspectorState(false, true, false)
15
+ ? await getInspectorState(false, true, true)
16
16
  : await getInspectorState();
17
17
  const functionTypesImportPath = getFileImportRelativePath(cliTypesFile, functionTypesFile, packageMappings);
18
18
  // Check for required types
@@ -0,0 +1,6 @@
1
+ import type { PikkuCLIConfig } from '../../../../types/config.js';
2
+ import type { CLILogger } from '../../../services/cli-logger.service.js';
3
+ export declare const pikkuEmails: import("#pikku").PikkuFunctionConfig<void, boolean | undefined, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, boolean | undefined, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<void, boolean | undefined, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
4
+ export declare function generateEmailsArtifacts(logger: CLILogger, config: Pick<PikkuCLIConfig, 'outDir'> & {
5
+ emailTemplatesDir?: string;
6
+ }): Promise<true | undefined>;
@@ -0,0 +1,172 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { basename, extname, join } from 'node:path';
4
+ import { pikkuSessionlessFunc } from '#pikku';
5
+ import { writeFileInDir } from '../../../utils/file-writer.js';
6
+ import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
7
+ import { serializeEmailsModule } from './serialize-emails.js';
8
+ function sha256(input) {
9
+ return createHash('sha256').update(input).digest('hex');
10
+ }
11
+ function extractVariables(...sources) {
12
+ const variables = new Set();
13
+ for (const source of sources) {
14
+ for (const match of source.matchAll(/\{\{\s*([^}]+?)\s*\}\}/g)) {
15
+ const key = String(match[1]).trim();
16
+ if (!key || key === 'content' || key.startsWith('>'))
17
+ continue;
18
+ const rootKey = key.split('.')[0];
19
+ if (rootKey === 't' || rootKey === 'theme' || rootKey === 'locale')
20
+ continue;
21
+ if (rootKey === 'subject')
22
+ continue;
23
+ variables.add(rootKey);
24
+ }
25
+ }
26
+ return [...variables].sort();
27
+ }
28
+ function stableStringify(value) {
29
+ if (Array.isArray(value)) {
30
+ return `[${value.map((item) => stableStringify(item)).join(',')}]`;
31
+ }
32
+ if (value && typeof value === 'object') {
33
+ const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
34
+ return `{${entries
35
+ .map(([key, nested]) => `${JSON.stringify(key)}:${stableStringify(nested)}`)
36
+ .join(',')}}`;
37
+ }
38
+ return JSON.stringify(value);
39
+ }
40
+ function collectStringLeaves(value) {
41
+ if (typeof value === 'string') {
42
+ return [value];
43
+ }
44
+ if (Array.isArray(value)) {
45
+ return value.flatMap((item) => collectStringLeaves(item));
46
+ }
47
+ if (value && typeof value === 'object') {
48
+ return Object.values(value).flatMap((nested) => collectStringLeaves(nested));
49
+ }
50
+ return [];
51
+ }
52
+ export const pikkuEmails = pikkuSessionlessFunc({
53
+ func: async ({ logger, config }) => {
54
+ return generateEmailsArtifacts(logger, config);
55
+ },
56
+ middleware: [
57
+ logCommandInfoAndTime({
58
+ commandStart: 'Generating emails',
59
+ commandEnd: 'Generated emails',
60
+ }),
61
+ ],
62
+ });
63
+ export async function generateEmailsArtifacts(logger, config) {
64
+ const emailDir = config.emailTemplatesDir;
65
+ if (!emailDir) {
66
+ logger.debug({
67
+ message: 'Skipping emails (set emailTemplatesDir in pikku.config.json to enable).',
68
+ type: 'skip',
69
+ });
70
+ return;
71
+ }
72
+ const typedOut = join(config.outDir, 'email', 'pikku-emails.gen.ts');
73
+ const metaOut = join(config.outDir, 'email', 'pikku-emails-meta.gen.json');
74
+ const [themeRaw, localeFiles, templateFiles, partialFiles] = await Promise.all([
75
+ readFile(join(emailDir, 'theme.json'), 'utf8').catch(() => '{}'),
76
+ readdir(join(emailDir, 'locales')).catch(() => []),
77
+ readdir(join(emailDir, 'templates')).catch(() => []),
78
+ readdir(join(emailDir, 'partials')).catch(() => []),
79
+ ]);
80
+ const theme = JSON.parse(themeRaw);
81
+ const locales = Object.fromEntries(await Promise.all(localeFiles
82
+ .filter((file) => extname(file) === '.json')
83
+ .map(async (file) => {
84
+ const locale = basename(file, '.json');
85
+ const raw = await readFile(join(emailDir, 'locales', file), 'utf8');
86
+ return [locale, JSON.parse(raw)];
87
+ })));
88
+ const partials = Object.fromEntries(await Promise.all(partialFiles
89
+ .filter((file) => extname(file) === '.html')
90
+ .map(async (file) => {
91
+ const name = basename(file, '.html');
92
+ const raw = await readFile(join(emailDir, 'partials', file), 'utf8');
93
+ return [name, raw];
94
+ })));
95
+ const templateNames = [
96
+ ...new Set(templateFiles
97
+ .filter((file) => file.endsWith('.html') ||
98
+ file.endsWith('.subject.txt') ||
99
+ file.endsWith('.text.txt'))
100
+ .map((file) => file.endsWith('.subject.txt')
101
+ ? file.slice(0, -'.subject.txt'.length)
102
+ : file.endsWith('.text.txt')
103
+ ? file.slice(0, -'.text.txt'.length)
104
+ : file.slice(0, -'.html'.length))),
105
+ ].sort();
106
+ if (templateNames.length === 0) {
107
+ logger.debug({
108
+ message: 'Skipping emails (no templates found).',
109
+ type: 'skip',
110
+ });
111
+ return;
112
+ }
113
+ const templates = Object.fromEntries(await Promise.all(templateNames.map(async (templateName) => {
114
+ const [html, subject, text] = await Promise.all([
115
+ readFile(join(emailDir, 'templates', `${templateName}.html`), 'utf8'),
116
+ readFile(join(emailDir, 'templates', `${templateName}.subject.txt`), 'utf8'),
117
+ readFile(join(emailDir, 'templates', `${templateName}.text.txt`), 'utf8').catch(() => ''),
118
+ ]);
119
+ const localeSources = Object.values(locales).flatMap((strings) => collectStringLeaves(strings));
120
+ const variables = extractVariables(html, subject, text, ...Object.values(partials), ...localeSources);
121
+ const hashes = Object.fromEntries(Object.entries(locales).map(([locale, strings]) => {
122
+ const localePayload = stableStringify({
123
+ templateName,
124
+ locale,
125
+ theme,
126
+ strings,
127
+ partials,
128
+ subject,
129
+ html,
130
+ text,
131
+ });
132
+ return [
133
+ locale,
134
+ {
135
+ contentHash: sha256(localePayload),
136
+ htmlHash: sha256(stableStringify({ html, partials, theme, strings })),
137
+ subjectHash: sha256(stableStringify({ subject, strings })),
138
+ textHash: sha256(stableStringify({ text, strings })),
139
+ },
140
+ ];
141
+ }));
142
+ return [
143
+ templateName,
144
+ {
145
+ html,
146
+ subject,
147
+ text,
148
+ variables,
149
+ hashes,
150
+ },
151
+ ];
152
+ })));
153
+ const meta = {
154
+ src: emailDir,
155
+ themeHash: sha256(stableStringify(theme)),
156
+ templates: Object.fromEntries(Object.entries(templates).map(([name, template]) => [
157
+ name,
158
+ {
159
+ variables: template.variables,
160
+ hasHtml: Boolean(template.html),
161
+ hasSubject: Boolean(template.subject),
162
+ hasText: Boolean(template.text),
163
+ locales: template.hashes,
164
+ },
165
+ ])),
166
+ };
167
+ await writeFileInDir(logger, typedOut, serializeEmailsModule({ theme, locales, partials, templates }));
168
+ await writeFileInDir(logger, metaOut, JSON.stringify(meta, null, 2), {
169
+ ignoreModifyComment: true,
170
+ });
171
+ return true;
172
+ }
@@ -0,0 +1,20 @@
1
+ type EmailTemplateAssets = {
2
+ html: string;
3
+ subject: string;
4
+ text: string;
5
+ variables: string[];
6
+ hashes: Record<string, {
7
+ contentHash: string;
8
+ htmlHash: string;
9
+ subjectHash: string;
10
+ textHash: string;
11
+ }>;
12
+ };
13
+ type SerializeEmailsInput = {
14
+ theme: Record<string, unknown>;
15
+ locales: Record<string, Record<string, unknown>>;
16
+ partials: Record<string, string>;
17
+ templates: Record<string, EmailTemplateAssets>;
18
+ };
19
+ export declare const serializeEmailsModule: ({ theme, locales, partials, templates, }: SerializeEmailsInput) => string;
20
+ export {};