@pikku/cli 0.12.24 → 0.12.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/index-Ba9K10XZ.js +232 -0
  3. package/console-app/index.html +1 -1
  4. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-channel.js +21 -1
  8. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  9. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +50 -0
  12. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  15. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  16. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  17. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  18. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  19. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  20. package/dist/.pikku/function/pikku-functions-meta.gen.json +183 -104
  21. package/dist/.pikku/function/pikku-functions.gen.js +3 -1
  22. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  23. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  24. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  25. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  26. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  27. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  28. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  29. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  30. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  31. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  33. package/dist/.pikku/pikku-services.gen.d.ts +3 -1
  34. package/dist/.pikku/pikku-services.gen.js +2 -0
  35. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-types.gen.js +1 -1
  37. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  38. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  39. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  42. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  43. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +13 -9
  44. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  46. package/dist/.pikku/schemas/register.gen.js +17 -5
  47. package/dist/.pikku/schemas/schemas/DbAuditInput.schema.json +1 -0
  48. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  49. package/dist/.pikku/schemas/schemas/PikkuEmailsOutput.schema.json +1 -0
  50. package/dist/.pikku/schemas/schemas/PikkuFunctionTypesSplitInput.schema.json +1 -0
  51. package/dist/.pikku/schemas/schemas/PikkuTestsCoverageInput.schema.json +1 -1
  52. package/dist/.pikku/schemas/schemas/PikkuTriggerTypesInput.schema.json +1 -0
  53. package/dist/.pikku/schemas/schemas/WorkspaceValidateInput.schema.json +1 -0
  54. package/dist/.pikku/schemas/schemas/WorkspaceValidateOutput.schema.json +1 -0
  55. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  56. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  57. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  58. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  59. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  60. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  61. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  62. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  63. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  64. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  65. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +5 -5
  66. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  68. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  69. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  70. package/dist/bin/pikku-bin.mjs +2 -2
  71. package/dist/src/cli.wiring.js +39 -0
  72. package/dist/src/fabric/functions/validate-core.d.ts +20 -0
  73. package/dist/src/fabric/functions/validate-core.js +227 -0
  74. package/dist/src/fabric/functions/validate.function.js +12 -4
  75. package/dist/src/functions/commands/bootstrap.js +2 -2
  76. package/dist/src/functions/commands/console.js +7 -4
  77. package/dist/src/functions/commands/db-audit.d.ts +1 -0
  78. package/dist/src/functions/commands/db-audit.js +67 -0
  79. package/dist/src/functions/commands/db-migrate.js +7 -11
  80. package/dist/src/functions/commands/db-reset.js +12 -12
  81. package/dist/src/functions/commands/db-seed.js +11 -11
  82. package/dist/src/functions/commands/db-shared.d.ts +4 -19
  83. package/dist/src/functions/commands/db-shared.js +53 -17
  84. package/dist/src/functions/commands/dev.js +25 -14
  85. package/dist/src/functions/commands/emails-init.d.ts +5 -0
  86. package/dist/src/functions/commands/emails-init.js +162 -0
  87. package/dist/src/functions/commands/load-user-project.js +12 -3
  88. package/dist/src/functions/commands/new-addon.js +2 -2
  89. package/dist/src/functions/commands/tests-coverage.d.ts +3 -0
  90. package/dist/src/functions/commands/tests-coverage.js +34 -0
  91. package/dist/src/functions/commands/watch.js +7 -4
  92. package/dist/src/functions/commands/workspace-validate.d.ts +33 -0
  93. package/dist/src/functions/commands/workspace-validate.js +9 -0
  94. package/dist/src/functions/db/annotation-parser.d.ts +31 -0
  95. package/dist/src/functions/db/annotation-parser.js +93 -0
  96. package/dist/src/functions/db/coercion-plugin.d.ts +7 -0
  97. package/dist/src/functions/db/coercion-plugin.js +99 -0
  98. package/dist/src/functions/db/db-codegen.d.ts +24 -0
  99. package/dist/src/functions/db/db-codegen.js +276 -0
  100. package/dist/src/functions/db/db-introspector.d.ts +15 -0
  101. package/dist/src/functions/db/db-introspector.js +1 -0
  102. package/dist/src/functions/db/db-migrator.d.ts +32 -0
  103. package/dist/src/functions/db/db-migrator.js +65 -0
  104. package/dist/src/functions/db/local-db.d.ts +27 -33
  105. package/dist/src/functions/db/local-db.js +108 -57
  106. package/dist/src/functions/db/postgres/postgres-introspector.d.ts +10 -0
  107. package/dist/src/functions/db/postgres/postgres-introspector.js +54 -0
  108. package/dist/src/functions/db/postgres/postgres-migrator.d.ts +9 -0
  109. package/dist/src/functions/db/postgres/postgres-migrator.js +32 -0
  110. package/dist/src/functions/db/{seed.d.ts → sqlite/seed.d.ts} +2 -2
  111. package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +9 -0
  112. package/dist/src/functions/db/sqlite/sqlite-introspector.js +35 -0
  113. package/dist/src/functions/db/sqlite/sqlite-kysely.d.ts +8 -0
  114. package/dist/src/functions/db/sqlite/sqlite-kysely.js +62 -0
  115. package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +10 -0
  116. package/dist/src/functions/db/sqlite/sqlite-migrator.js +36 -0
  117. package/dist/src/functions/db/sqlite/sqlite-runtime-bun.d.ts +2 -0
  118. package/dist/src/functions/db/sqlite/sqlite-runtime-bun.js +52 -0
  119. package/dist/src/functions/db/sqlite/sqlite-runtime-node.d.ts +2 -0
  120. package/dist/src/functions/db/sqlite/sqlite-runtime-node.js +51 -0
  121. package/dist/src/functions/db/sqlite/sqlite-runtime.d.ts +20 -0
  122. package/dist/src/functions/db/sqlite/sqlite-runtime.js +13 -0
  123. package/dist/src/functions/validate/workspace-validate.d.ts +34 -0
  124. package/dist/src/functions/validate/workspace-validate.js +259 -0
  125. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +2 -1
  126. package/dist/src/functions/wirings/cli/pikku-command-cli-types.js +1 -1
  127. package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -4
  128. package/dist/src/functions/wirings/emails/pikku-command-emails.d.ts +6 -0
  129. package/dist/src/functions/wirings/emails/pikku-command-emails.js +172 -0
  130. package/dist/src/functions/wirings/emails/serialize-emails.d.ts +20 -0
  131. package/dist/src/functions/wirings/emails/serialize-emails.js +168 -0
  132. package/dist/src/functions/wirings/functions/pikku-command-function-types-split.d.ts +7 -1
  133. package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +2 -2
  134. package/dist/src/functions/wirings/functions/serialize-addon-types.js +1 -1
  135. package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.d.ts +7 -1
  136. package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.js +2 -2
  137. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +1 -1
  138. package/dist/src/functions/workflows/all.workflow.js +12 -7
  139. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  140. package/dist/src/services.js +2 -0
  141. package/dist/src/utils/pikku-cli-config.js +6 -0
  142. package/dist/tsconfig.tsbuildinfo +1 -1
  143. package/package.json +6 -4
  144. package/skills/pikku-auth-js/SKILL.md +271 -58
  145. package/skills/pikku-testing/SKILL.md +208 -0
  146. package/console-app/assets/index-BDOqBctb.js +0 -232
  147. package/dist/src/functions/db/sql-migrator.d.ts +0 -26
  148. package/dist/src/functions/db/sql-migrator.js +0 -104
  149. package/dist/src/functions/db/sqlite-codegen.d.ts +0 -45
  150. package/dist/src/functions/db/sqlite-codegen.js +0 -294
  151. /package/dist/src/functions/db/{seed.js → sqlite/seed.js} +0 -0
@@ -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';
@@ -14,7 +14,7 @@ import { pikkuWebsocketHandler } from '@pikku/ws';
14
14
  import { PikkuNodeHTTPServer } from '@pikku/node-http-server';
15
15
  import { WebSocketServer } from 'ws';
16
16
  import { InMemorySchedulerService } from '@pikku/schedule';
17
- import { resolveLocalDb, createKysely } from '../db/local-db.js';
17
+ import { resolveDb, createKysely } from '../db/local-db.js';
18
18
  import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
19
19
  export const dev = pikkuSessionlessFunc({
20
20
  remote: true,
@@ -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'),
@@ -113,19 +116,25 @@ export const dev = pikkuSessionlessFunc({
113
116
  const userCreateConfig = configModule[pikkuConfigFactory.variable];
114
117
  const userCreateSingletonServices = servicesModule[singletonServicesFactory.variable];
115
118
  const userConfig = await userCreateConfig();
116
- const resolvedLocalDb = resolveLocalDb(userConfig.dev?.db ?? true, config.rootDir, config.outDir, config.runtimeDir);
119
+ const resolvedDb = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
120
+ const resolvedLocalDb = resolvedDb?.dialect === 'sqlite'
121
+ ? resolvedDb
122
+ : userConfig.sqliteDb
123
+ ? resolveDb({ sqliteDb: userConfig.sqliteDb }, config.rootDir, config.outDir, config.runtimeDir)
124
+ : undefined;
117
125
  const kysely = resolvedLocalDb
118
126
  ? await createKysely(resolvedLocalDb)
119
127
  : undefined;
120
128
  const resolvedRuntimeDir = config.runtimeDir ?? join(config.rootDir, '.pikku-runtime');
121
- const localContentConfig = userConfig.dev
122
- ?.content
129
+ const localContentConfig = userConfig.content
123
130
  ? {
124
- localFileUploadPath: join(resolvedRuntimeDir, 'content'),
125
- uploadUrlPrefix: '/upload',
126
- assetUrlPrefix: '/assets',
131
+ localFileUploadPath: userConfig.content.contentPath
132
+ ? resolve(config.rootDir, userConfig.content.contentPath)
133
+ : join(resolvedRuntimeDir, 'content'),
134
+ uploadUrlPrefix: userConfig.content.uploadUrlPrefix ?? '/upload',
135
+ assetUrlPrefix: userConfig.content.assetUrlPrefix ?? '/assets',
127
136
  server: `http://${hostname}:${resolvedPort}`,
128
- ...(userConfig.dev.content !== true ? userConfig.dev.content : {}),
137
+ sizeLimit: userConfig.content.sizeLimit,
129
138
  }
130
139
  : undefined;
131
140
  const localContent = localContentConfig
@@ -151,8 +160,10 @@ export const dev = pikkuSessionlessFunc({
151
160
  // single instance under both names so addons like @pikku/addon-console
152
161
  // can read runs in dev without projects having to wire their own backing
153
162
  // store.
163
+ const devLogger = new ConsoleLogger();
154
164
  const inMemoryServices = {
155
- logger: new ConsoleLogger(),
165
+ logger: devLogger,
166
+ emailService: new LocalEmailService(),
156
167
  metaService: new LocalMetaService(pikkuDir),
157
168
  schedulerService,
158
169
  queueService: new InMemoryQueueService(),
@@ -205,20 +216,20 @@ export const dev = pikkuSessionlessFunc({
205
216
  });
206
217
  if (enableHmr) {
207
218
  await pikkuDevReloader({
208
- srcDirectories: config.srcDirectories,
219
+ srcDirectories: watchDirectories,
209
220
  logger,
210
221
  });
211
222
  }
212
223
  if (enableWatch) {
213
224
  const genIgnore = /\.gen\.tsx?$/;
214
- configWatcher = chokidar.watch(config.srcDirectories, {
225
+ configWatcher = chokidar.watch(watchDirectories, {
215
226
  ignoreInitial: true,
216
227
  ignored: genIgnore,
217
228
  });
218
229
  const generatorWatcher = () => {
219
230
  watcher?.close();
220
- logger.info(`• Watching directories: \n - ${config.srcDirectories.join('\n - ')}`);
221
- watcher = chokidar.watch(config.srcDirectories, {
231
+ logger.info(`• Watching directories: \n - ${watchDirectories.join('\n - ')}`);
232
+ watcher = chokidar.watch(watchDirectories, {
222
233
  ignoreInitial: true,
223
234
  ignored: genIgnore,
224
235
  });
@@ -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 {};
@@ -0,0 +1,162 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { pikkuSessionlessFunc } from '#pikku';
5
+ import { generateEmailsArtifacts } from '../wirings/emails/pikku-command-emails.js';
6
+ const DEFAULT_EMAIL_DIR = 'emails';
7
+ const DEFAULT_THEME = {
8
+ appName: 'Pikku App',
9
+ previewText: 'Your app can render localized emails out of the box.',
10
+ colors: {
11
+ background: '#f5f7fb',
12
+ surface: '#ffffff',
13
+ text: '#101828',
14
+ muted: '#475467',
15
+ border: '#d0d5dd',
16
+ primary: '#7c3aed',
17
+ primaryText: '#ffffff',
18
+ footer: '#667085',
19
+ },
20
+ fonts: {
21
+ body: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
22
+ },
23
+ };
24
+ const DEFAULT_EN_LOCALE = {
25
+ helloWorld: {
26
+ subject: 'Hello from {{appName}}',
27
+ eyebrow: 'Email templates are live',
28
+ title: 'Your first Pikku email is ready',
29
+ intro: 'This starter template proves the pipeline works and gives you a place to shape your own visual language.',
30
+ body: 'Chat to your AI to create new emails, refine the theme, or localize every message for your product.',
31
+ cta: 'Open the email console',
32
+ footer: 'Built with Pikku email templates.',
33
+ },
34
+ };
35
+ const DEFAULT_DE_LOCALE = {
36
+ helloWorld: {
37
+ subject: 'Hallo von {{appName}}',
38
+ eyebrow: 'E-Mail-Vorlagen sind aktiv',
39
+ title: 'Deine erste Pikku-E-Mail ist bereit',
40
+ intro: 'Diese Startvorlage zeigt, dass die Pipeline funktioniert, und gibt dir einen Ausgangspunkt fuer dein eigenes Design.',
41
+ body: 'Sprich mit deiner KI, um neue E-Mails zu erstellen, das Theme zu verfeinern oder jede Nachricht zu lokalisieren.',
42
+ cta: 'E-Mail-Konsole oeffnen',
43
+ footer: 'Erstellt mit Pikku-E-Mail-Vorlagen.',
44
+ },
45
+ };
46
+ function layoutTemplate() {
47
+ return `<!doctype html>
48
+ <html lang="{{locale}}">
49
+ <head>
50
+ <meta charset="utf-8" />
51
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
52
+ <title>{{subject}}</title>
53
+ </head>
54
+ <body style="margin:0;padding:32px 16px;background:{{theme.colors.background}};font-family:{{theme.fonts.body}};color:{{theme.colors.text}};">
55
+ <div style="max-width:640px;margin:0 auto;background:{{theme.colors.surface}};border:1px solid {{theme.colors.border}};border-radius:20px;overflow:hidden;">
56
+ {{content}}
57
+ </div>
58
+ </body>
59
+ </html>
60
+ `;
61
+ }
62
+ function footerPartial() {
63
+ return `<div style="padding:24px 32px;border-top:1px solid {{theme.colors.border}};font-size:13px;line-height:1.6;color:{{theme.colors.footer}};">
64
+ <p style="margin:0;">{{t.helloWorld.footer}}</p>
65
+ </div>
66
+ `;
67
+ }
68
+ function helloWorldHtml() {
69
+ return `<div style="padding:32px;">
70
+ <p style="margin:0 0 16px;font-size:12px;letter-spacing:0.12em;text-transform:uppercase;color:{{theme.colors.primary}};">
71
+ {{t.helloWorld.eyebrow}}
72
+ </p>
73
+ <h1 style="margin:0 0 16px;font-size:32px;line-height:1.15;color:{{theme.colors.text}};">
74
+ {{t.helloWorld.title}}
75
+ </h1>
76
+ <p style="margin:0 0 16px;font-size:16px;line-height:1.7;color:{{theme.colors.muted}};">
77
+ Hello {{userName}}. {{t.helloWorld.intro}}
78
+ </p>
79
+ <p style="margin:0 0 24px;font-size:16px;line-height:1.7;color:{{theme.colors.muted}};">
80
+ {{t.helloWorld.body}}
81
+ </p>
82
+ <a
83
+ href="{{previewUrl}}"
84
+ style="display:inline-block;padding:14px 18px;border-radius:999px;background:{{theme.colors.primary}};color:{{theme.colors.primaryText}};font-weight:600;text-decoration:none;"
85
+ >
86
+ {{t.helloWorld.cta}}
87
+ </a>
88
+ </div>
89
+ {{> footer}}
90
+ `;
91
+ }
92
+ function helloWorldSubject() {
93
+ return `{{t.helloWorld.subject}}
94
+ `;
95
+ }
96
+ function helloWorldText() {
97
+ return `{{t.helloWorld.title}}
98
+
99
+ Hello {{userName}}.
100
+
101
+ {{t.helloWorld.intro}}
102
+
103
+ {{t.helloWorld.body}}
104
+
105
+ {{t.helloWorld.cta}}: {{previewUrl}}
106
+ `;
107
+ }
108
+ async function ensureFile(path, content) {
109
+ await mkdir(dirname(path), { recursive: true });
110
+ await writeFile(path, content, 'utf8');
111
+ }
112
+ async function updateJsonConfig(configDir, emailDir) {
113
+ const configPath = join(configDir, 'pikku.config.json');
114
+ if (!existsSync(configPath)) {
115
+ return false;
116
+ }
117
+ const raw = await readFile(configPath, 'utf8');
118
+ const parsed = JSON.parse(raw);
119
+ if (typeof parsed.emailTemplatesDir === 'string' && parsed.emailTemplatesDir) {
120
+ return false;
121
+ }
122
+ parsed.emailTemplatesDir = emailDir;
123
+ await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
124
+ return true;
125
+ }
126
+ export const pikkuEmailsInit = pikkuSessionlessFunc({
127
+ func: async ({ logger, config }, input) => {
128
+ const force = input?.force;
129
+ const configuredEmailDir = config.emailTemplatesDir ?? join(config.rootDir, DEFAULT_EMAIL_DIR);
130
+ const emailDir = config.emailTemplatesDir && config.emailTemplatesDir.length > 0
131
+ ? config.emailTemplatesDir
132
+ : join(config.rootDir, DEFAULT_EMAIL_DIR);
133
+ const files = [
134
+ [join(emailDir, 'theme.json'), `${JSON.stringify(DEFAULT_THEME, null, 2)}\n`],
135
+ [join(emailDir, 'locales', 'en.json'), `${JSON.stringify(DEFAULT_EN_LOCALE, null, 2)}\n`],
136
+ [join(emailDir, 'locales', 'de.json'), `${JSON.stringify(DEFAULT_DE_LOCALE, null, 2)}\n`],
137
+ [join(emailDir, 'partials', 'layout.html'), layoutTemplate()],
138
+ [join(emailDir, 'partials', 'footer.html'), footerPartial()],
139
+ [join(emailDir, 'templates', 'hello-world.html'), helloWorldHtml()],
140
+ [join(emailDir, 'templates', 'hello-world.subject.txt'), helloWorldSubject()],
141
+ [join(emailDir, 'templates', 'hello-world.text.txt'), helloWorldText()],
142
+ ];
143
+ const existing = !force
144
+ ? files.map(([path]) => path).filter((path) => existsSync(path))
145
+ : [];
146
+ if (existing.length > 0) {
147
+ logger.error(`Email scaffold already exists at ${existing[0]}. Use --force to overwrite.`);
148
+ process.exit(1);
149
+ }
150
+ await Promise.all(files.map(([path, content]) => ensureFile(path, content)));
151
+ const configUpdated = !config.emailTemplatesDir &&
152
+ (await updateJsonConfig(config.configDir, DEFAULT_EMAIL_DIR));
153
+ if (!config.emailTemplatesDir && !configUpdated) {
154
+ logger.warn('Unable to auto-update pikku.config.json. Add "emailTemplatesDir": "emails" manually.');
155
+ }
156
+ await generateEmailsArtifacts(logger, {
157
+ emailTemplatesDir: emailDir,
158
+ outDir: config.outDir,
159
+ });
160
+ logger.info(`Email templates initialized at ${configuredEmailDir}`);
161
+ },
162
+ });
@@ -1,7 +1,11 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { pathToFileURL } from 'url';
3
4
  import { register } from 'tsx/esm/api';
4
5
  let tsxRegistered = false;
6
+ function isBunRuntime() {
7
+ return typeof globalThis.Bun !== 'undefined';
8
+ }
5
9
  /**
6
10
  * Globally register tsx so subsequent `import()` calls go through Node's
7
11
  * normal resolver — yielding ONE module graph (and one shared pikkuState),
@@ -13,22 +17,27 @@ let tsxRegistered = false;
13
17
  * module state (e.g. `pikkuState` registrations from wireHTTPRoutes).
14
18
  */
15
19
  function ensureTsxRegistered() {
20
+ if (isBunRuntime())
21
+ return;
16
22
  if (tsxRegistered)
17
23
  return;
18
24
  register();
19
25
  tsxRegistered = true;
20
26
  }
27
+ async function importUserPath(filePath) {
28
+ return import(pathToFileURL(filePath).href);
29
+ }
21
30
  /**
22
31
  * Load the generated `pikku-bootstrap.gen.{ts,js}` from the user's project,
23
32
  * which in turn pulls in all wiring/meta files so they register into
24
33
  * `pikkuState`.
25
34
  */
26
35
  export async function loadUserBootstrap(pikkuDir) {
27
- ensureTsxRegistered();
28
36
  const bootstrapTs = join(pikkuDir, 'pikku-bootstrap.gen.ts');
29
37
  const bootstrapJs = join(pikkuDir, 'pikku-bootstrap.gen.js');
30
38
  const bootstrapPath = existsSync(bootstrapTs) ? bootstrapTs : bootstrapJs;
31
- await import(bootstrapPath);
39
+ ensureTsxRegistered();
40
+ await importUserPath(bootstrapPath);
32
41
  }
33
42
  /**
34
43
  * Import a user-source TypeScript file (e.g. their config or services
@@ -36,5 +45,5 @@ export async function loadUserBootstrap(pikkuDir) {
36
45
  */
37
46
  export async function loadUserModule(filePath) {
38
47
  ensureTsxRegistered();
39
- return import(filePath);
48
+ return importUserPath(filePath);
40
49
  }
@@ -219,7 +219,7 @@ export const createSingletonServices = pikkuAddonServices(async (
219
219
  config,
220
220
  { secrets, variables }
221
221
  ) => {
222
- const creds = await secrets.getSecretJSON<${pascalName}Secrets>('${screamingName}_CREDENTIALS')
222
+ const creds = await secrets.getSecret<${pascalName}Secrets>('${screamingName}_CREDENTIALS')
223
223
  const ${camelName} = new ${pascalName}Service(creds, variables)
224
224
 
225
225
  return { ${camelName} }
@@ -717,7 +717,7 @@ import { createSingletonServices } from './services.js'
717
717
  test('${name} addon', async () => {
718
718
  const secrets = new LocalSecretService()
719
719
  // Set up secrets for the service
720
- // await secrets.setSecretJSON('${screamingName}_CREDENTIALS', { ... })
720
+ // await secrets.setSecret('${screamingName}_CREDENTIALS', { ... })
721
721
 
722
722
  const singletonServices = await createSingletonServices({}, { secrets })
723
723
  const rpc = rpcService.getContextRPCService(singletonServices as any, {})
@@ -1,7 +1,10 @@
1
1
  export declare const pikkuTestsCoverage: import("#pikku").PikkuFunctionConfig<{
2
2
  noRun?: boolean;
3
+ aiOut?: string;
3
4
  }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
4
5
  noRun?: boolean;
6
+ aiOut?: string;
5
7
  }, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{
6
8
  noRun?: boolean;
9
+ aiOut?: string;
7
10
  }, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
@@ -92,6 +92,7 @@ function lineCoverage(fileCov, span) {
92
92
  export const pikkuTestsCoverage = pikkuSessionlessFunc({
93
93
  func: async ({ logger, config }, input) => {
94
94
  const noRun = input?.noRun ?? false;
95
+ const aiOut = input?.aiOut ?? null;
95
96
  const packageMappings = config.packageMappings ?? {};
96
97
  const srcDirs = config.srcDirectories ?? [];
97
98
  const functionsRelDir = Object.keys(packageMappings).find((key) => srcDirs.some((src) => src === key || src.startsWith(key + '/'))) ?? Object.keys(packageMappings)[0];
@@ -227,5 +228,38 @@ export const pikkuTestsCoverage = pikkuSessionlessFunc({
227
228
  console.log(` ${summary.covered} covered · ${summary.partial} partial · ${summary.uncovered} uncovered · ${summary.unknown} unknown (overall ${(overallRatio * 100).toFixed(1)}%)`);
228
229
  if (uncovered.length)
229
230
  console.log(` uncovered: ${uncovered.map((f) => f.name).join(', ')}`);
231
+ if (aiOut !== null) {
232
+ const generatedAt = new Date().toISOString();
233
+ const pct = Math.round(overallRatio * 100);
234
+ const needWork = functions.filter((f) => f.status !== 'covered');
235
+ const lines = [
236
+ `Coverage report — generated ${generatedAt}.`,
237
+ `Overall: ${pct}% (${summary.covered}/${summary.total} functions fully covered)`,
238
+ '',
239
+ needWork.length === 0
240
+ ? 'All functions are fully covered — nothing to do!'
241
+ : `Functions needing coverage (${needWork.length}):`,
242
+ ...needWork.flatMap((fn) => {
243
+ const ratio = Math.round(fn.ratio * 100);
244
+ const missed = fn.missedLines.length > 0 ? fn.missedLines.join(', ') : 'none';
245
+ return [
246
+ `- ${fn.name} [${fn.status}, ${ratio}% covered, ${fn.coveredLines}/${fn.totalLines} lines]`,
247
+ ` file: ${fn.sourceFile}`,
248
+ ` missed lines: ${missed}`,
249
+ ];
250
+ }),
251
+ ];
252
+ if (needWork.length > 0) {
253
+ lines.push('', 'Write @pikku/cucumber Gherkin scenarios (no custom steps) in tests/tests/features/ to cover the missed lines above.', 'Use pikku meta to get versioned RPC names and function schemas before writing.', 'Run pikku tests coverage after writing to verify coverage improved.');
254
+ }
255
+ const prompt = lines.join('\n') + '\n';
256
+ if (aiOut === '-') {
257
+ process.stdout.write(prompt);
258
+ }
259
+ else {
260
+ writeFileSync(aiOut, prompt, 'utf-8');
261
+ console.log(`AI prompt → ${relative(config.rootDir, aiOut)}`);
262
+ }
263
+ }
230
264
  },
231
265
  });
@@ -4,21 +4,24 @@ import { pikkuDevReloader } from '@pikku/core/dev';
4
4
  export const watch = pikkuSessionlessFunc({
5
5
  remote: true,
6
6
  func: async ({ logger, config }, { hmr }, { rpc }) => {
7
+ const watchDirectories = [
8
+ ...new Set([config.emailTemplatesDir, ...config.srcDirectories].filter(Boolean)),
9
+ ];
7
10
  if (hmr) {
8
11
  await pikkuDevReloader({
9
- srcDirectories: config.srcDirectories,
12
+ srcDirectories: watchDirectories,
10
13
  logger,
11
14
  });
12
15
  }
13
- const configWatcher = chokidar.watch(config.srcDirectories, {
16
+ const configWatcher = chokidar.watch(watchDirectories, {
14
17
  ignoreInitial: true,
15
18
  ignored: /.*\.gen\.tsx?/,
16
19
  });
17
20
  let watcher = new chokidar.FSWatcher({});
18
21
  const generatorWatcher = () => {
19
22
  watcher.close();
20
- logger.info(`• Watching directories: \n - ${config.srcDirectories.join('\n - ')}`);
21
- watcher = chokidar.watch(config.srcDirectories, {
23
+ logger.info(`• Watching directories: \n - ${watchDirectories.join('\n - ')}`);
24
+ watcher = chokidar.watch(watchDirectories, {
22
25
  ignoreInitial: true,
23
26
  ignored: /.*\.gen\.ts/,
24
27
  });
@@ -0,0 +1,33 @@
1
+ import { renderWorkspaceValidate } from '../validate/workspace-validate.js';
2
+ export declare const workspaceValidate: import("#pikku").PikkuFunctionConfig<Record<string, never>, {
3
+ ok: boolean;
4
+ root: string;
5
+ findings: {
6
+ id: string;
7
+ severity: "info" | "warn" | "error";
8
+ message: string;
9
+ path: string;
10
+ fixHint: string;
11
+ }[];
12
+ }, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<Record<string, never>, {
13
+ ok: boolean;
14
+ root: string;
15
+ findings: {
16
+ id: string;
17
+ severity: "info" | "warn" | "error";
18
+ message: string;
19
+ path: string;
20
+ fixHint: string;
21
+ }[];
22
+ }, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<Record<string, never>, {
23
+ ok: boolean;
24
+ root: string;
25
+ findings: {
26
+ id: string;
27
+ severity: "info" | "warn" | "error";
28
+ message: string;
29
+ path: string;
30
+ fixHint: string;
31
+ }[];
32
+ }, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
33
+ export { renderWorkspaceValidate };
@@ -0,0 +1,9 @@
1
+ import { pikkuSessionlessFunc } from '#pikku';
2
+ import { WorkspaceValidateInput, WorkspaceValidateOutput, renderWorkspaceValidate, runWorkspaceValidate, } from '../validate/workspace-validate.js';
3
+ export const workspaceValidate = pikkuSessionlessFunc({
4
+ description: 'Check the current Pikku workspace structure for compatibility. Prints all missing or misconfigured items with fix hints.',
5
+ input: WorkspaceValidateInput,
6
+ output: WorkspaceValidateOutput,
7
+ func: async () => runWorkspaceValidate(),
8
+ });
9
+ export { renderWorkspaceValidate };
@@ -0,0 +1,31 @@
1
+ import type { ColumnKind } from './coercion-plugin.js';
2
+ type Classification = 'public' | 'private' | 'secret';
3
+ type AnonymizeStrategy = 'fake:email' | 'fake:name' | 'hash' | 'keep' | null;
4
+ export interface ColAnnotation {
5
+ kind?: ColumnKind;
6
+ /** TypeScript type string for @json columns, e.g. `string[]`. */
7
+ tsType?: string;
8
+ classification?: Classification;
9
+ anonymize?: AnonymizeStrategy;
10
+ }
11
+ /** Per-table, per-column annotation map built from migration SQL comments. */
12
+ export type AnnotationMap = Record<string, Record<string, ColAnnotation>>;
13
+ /**
14
+ * Determine column kind from naming conventions:
15
+ * *_at / *_on → date
16
+ * is_* / has_* / can_* → bool
17
+ */
18
+ export declare function annotationFromName(colName: string): {
19
+ kind: ColumnKind;
20
+ } | null;
21
+ /**
22
+ * Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @secret[:strategy]`
23
+ * inline annotations from migration SQL files in `migrationsDir`.
24
+ *
25
+ * Multiple annotations on the same comment line are supported, e.g.:
26
+ * `deleted_at TIMESTAMP -- @date @private:keep`
27
+ *
28
+ * Covers both CREATE TABLE body lines and ALTER TABLE ... ADD [COLUMN] statements.
29
+ */
30
+ export declare function parseAnnotations(migrationsDir: string): AnnotationMap;
31
+ export {};
@@ -0,0 +1,93 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Determine column kind from naming conventions:
5
+ * *_at / *_on → date
6
+ * is_* / has_* / can_* → bool
7
+ */
8
+ export function annotationFromName(colName) {
9
+ if (/_at$|_on$/.test(colName))
10
+ return { kind: 'date' };
11
+ if (/^is_|^has_|^can_/.test(colName))
12
+ return { kind: 'bool' };
13
+ return null;
14
+ }
15
+ function parseStrategy(s) {
16
+ if (!s)
17
+ return null;
18
+ const valid = ['fake:email', 'fake:name', 'hash', 'keep'];
19
+ return valid.includes(s) ? s : null;
20
+ }
21
+ function parseComment(comment) {
22
+ const ann = {};
23
+ if (/@bool\b/i.test(comment)) {
24
+ ann.kind = 'bool';
25
+ }
26
+ else if (/@date\b/i.test(comment)) {
27
+ ann.kind = 'date';
28
+ }
29
+ else {
30
+ const jsonM = comment.match(/@json\b(?:\s+([^\s@]+))?/i);
31
+ if (jsonM) {
32
+ ann.kind = 'json';
33
+ if (jsonM[1])
34
+ ann.tsType = jsonM[1].trim();
35
+ }
36
+ }
37
+ const classM = comment.match(/@(public|private|secret)(?::([^\s@]+))?/i);
38
+ if (classM) {
39
+ ann.classification = classM[1].toLowerCase();
40
+ ann.anonymize = parseStrategy(classM[2]);
41
+ }
42
+ return ann;
43
+ }
44
+ /**
45
+ * Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @secret[:strategy]`
46
+ * inline annotations from migration SQL files in `migrationsDir`.
47
+ *
48
+ * Multiple annotations on the same comment line are supported, e.g.:
49
+ * `deleted_at TIMESTAMP -- @date @private:keep`
50
+ *
51
+ * Covers both CREATE TABLE body lines and ALTER TABLE ... ADD [COLUMN] statements.
52
+ */
53
+ export function parseAnnotations(migrationsDir) {
54
+ let files;
55
+ try {
56
+ files = readdirSync(migrationsDir).filter((f) => f.endsWith('.sql')).sort();
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ const result = {};
62
+ function merge(tableName, colName, partial) {
63
+ if (!partial.kind && partial.classification === undefined)
64
+ return;
65
+ if (!result[tableName])
66
+ result[tableName] = {};
67
+ result[tableName][colName] = { ...result[tableName][colName], ...partial };
68
+ }
69
+ for (const file of files) {
70
+ const content = readFileSync(join(migrationsDir, file), 'utf8');
71
+ const createTablePattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"?(\w+)"?\s*\(([^;]+)\)/gis;
72
+ let tableMatch;
73
+ while ((tableMatch = createTablePattern.exec(content)) !== null) {
74
+ const tableName = tableMatch[1].toLowerCase();
75
+ const body = tableMatch[2];
76
+ for (const line of body.split('\n')) {
77
+ const trimmed = line.trim();
78
+ if (/^(PRIMARY|UNIQUE|CHECK|FOREIGN|CONSTRAINT)/i.test(trimmed))
79
+ continue;
80
+ const lineMatch = trimmed.match(/^(\w+)\s+\w[^-]*--\s*(.+?)\s*,?\s*$/);
81
+ if (!lineMatch)
82
+ continue;
83
+ merge(tableName, lineMatch[1].toLowerCase(), parseComment(lineMatch[2]));
84
+ }
85
+ }
86
+ const alterPattern = /ALTER\s+TABLE\s+"?(\w+)"?\s+ADD\s+(?:COLUMN\s+)?"?(\w+)"?\s+\w[^;\n-]*(?:;\s*)?--\s*(.+?)(?:\r?\n|$)/gim;
87
+ let alterMatch;
88
+ while ((alterMatch = alterPattern.exec(content)) !== null) {
89
+ merge(alterMatch[1].toLowerCase(), alterMatch[2].toLowerCase(), parseComment(alterMatch[3]));
90
+ }
91
+ }
92
+ return result;
93
+ }
@@ -0,0 +1,7 @@
1
+ import type { KyselyPlugin } from 'kysely';
2
+ export type ColumnKind = 'date' | 'bool' | 'json';
3
+ export type CoercionMap = Record<string, Record<string, ColumnKind>>;
4
+ export interface CreateCoercionPluginOptions {
5
+ map: CoercionMap;
6
+ }
7
+ export declare function createCoercionPlugin(options: CreateCoercionPluginOptions): KyselyPlugin;