@pikku/cli 0.12.49 → 0.12.51

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 (83) hide show
  1. package/console-app/assets/{index-DybYeIfd.js → index-92DoVRHq.js} +142 -139
  2. package/console-app/index.html +1 -1
  3. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  4. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  6. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  8. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  17. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  18. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  19. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  20. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  21. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  22. package/dist/.pikku/function/pikku-functions-meta.gen.json +70 -70
  23. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  24. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  25. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  26. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  27. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  28. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  31. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  33. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  35. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  37. package/dist/.pikku/pikku-types.gen.js +1 -1
  38. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  39. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  43. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  44. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  46. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  47. package/dist/.pikku/schemas/register.gen.js +3 -3
  48. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  49. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  50. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  51. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  52. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  53. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  54. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  55. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  56. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  57. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  58. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  59. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  60. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  61. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  62. package/dist/bin/pikku-bin.mjs +2 -2
  63. package/dist/src/deploy/build-pipeline.js +2 -1
  64. package/dist/src/deploy/provider-adapter.d.ts +13 -0
  65. package/dist/src/deploy/server-entry.d.ts +1 -1
  66. package/dist/src/deploy/server-entry.js +3 -1
  67. package/dist/src/fabric/functions/validate.function.js +6 -0
  68. package/dist/src/fabric/lib/config.js +1 -1
  69. package/dist/src/functions/commands/dev-ai-runner.d.ts +19 -0
  70. package/dist/src/functions/commands/dev-ai-runner.js +70 -0
  71. package/dist/src/functions/commands/dev.js +16 -0
  72. package/dist/src/functions/commands/tests-coverage.js +6 -0
  73. package/dist/src/functions/db/db-codegen.d.ts +9 -6
  74. package/dist/src/functions/db/db-codegen.js +62 -5
  75. package/dist/src/functions/db/db-introspector.d.ts +6 -0
  76. package/dist/src/functions/db/local-db.d.ts +1 -0
  77. package/dist/src/functions/db/local-db.js +3 -0
  78. package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +8 -0
  79. package/dist/src/functions/db/sqlite/sqlite-introspector.js +27 -0
  80. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  81. package/dist/tsconfig.tsbuildinfo +1 -1
  82. package/package.json +4 -4
  83. package/skills/pikku-paraglide/SKILL.md +117 -0
@@ -1,4 +1,4 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.49
2
+ * This file was generated by @pikku/cli@0.12.51
3
3
  */
4
4
  export { wireVariable } from '@pikku/core/variable';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.49
2
+ * This file was generated by @pikku/cli@0.12.51
3
3
  */
4
4
  import { TypedVariablesService as CoreTypedVariablesService } from '@pikku/core/services';
5
5
  import type { VariablesService } from '@pikku/core/services';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.49
2
+ * This file was generated by @pikku/cli@0.12.51
3
3
  */
4
4
  import { TypedVariablesService as CoreTypedVariablesService } from '@pikku/core/services';
5
5
  const VARIABLES_META = {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.49
2
+ * This file was generated by @pikku/cli@0.12.51
3
3
  */
4
4
  import { WorkflowCancelledException } from '@pikku/core/workflow';
5
5
  import { template } from '@pikku/core/workflow';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.49
2
+ * This file was generated by @pikku/cli@0.12.51
3
3
  */
4
4
  import { WorkflowCancelledException } from '@pikku/core/workflow';
5
5
  import { template } from '@pikku/core/workflow';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.49
2
+ * This file was generated by @pikku/cli@0.12.51
3
3
  */
4
4
  import { pikkuState } from '@pikku/core/internal';
5
5
  import allWorkflowMeta from './meta/allWorkflow.gen.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.49
2
+ * This file was generated by @pikku/cli@0.12.51
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.49') {
15
- process.stderr.write(`\n Update available 0.12.49 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
14
+ if (latest !== '0.12.51') {
15
+ process.stderr.write(`\n Update available 0.12.51 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
16
16
  }
17
17
  } catch {}
18
18
  }
@@ -188,7 +188,8 @@ export async function runBuildPipeline(options) {
188
188
  await mkdir(unitDir, { recursive: true });
189
189
  const ctx = getEntryContext(unitDir, pikkuDir, unit, inspectorState);
190
190
  const source = unit.target === 'server'
191
- ? generateServerEntrySource(ctx)
191
+ ? (provider.generateServerEntrySource?.(ctx) ??
192
+ generateServerEntrySource(ctx))
192
193
  : provider.generateEntrySource(ctx);
193
194
  await writeFile(entryPath, source, 'utf-8');
194
195
  if (unit.target === 'server') {
@@ -59,6 +59,19 @@ export interface ProviderAdapter {
59
59
  * Called once per unit.
60
60
  */
61
61
  generateEntrySource(ctx: EntryGenerationContext): string;
62
+ /**
63
+ * Generate the entry file source for a `target: 'server'` (container) unit.
64
+ *
65
+ * Optional. When a provider implements it, the deploy pipeline uses it
66
+ * instead of the provider-agnostic `generateServerEntrySource`, so the
67
+ * provider can inject the SAME platform services (kysely, secrets, …) into
68
+ * the container that it injects into its serverless workers — sourcing the
69
+ * bindings from `process.env` instead of a runtime `env` object. When
70
+ * omitted, the pipeline falls back to the generic generator (no platform
71
+ * injection), which is correct for providers whose containers carry no
72
+ * platform services.
73
+ */
74
+ generateServerEntrySource?(ctx: EntryGenerationContext): string;
62
75
  /**
63
76
  * Generate provider-specific config files per unit (e.g. wrangler.toml).
64
77
  * Returns a map of filename → content to write into the unit directory.
@@ -12,5 +12,5 @@
12
12
  */
13
13
  import type { EntryGenerationContext } from './provider-adapter.js';
14
14
  export declare function generateServerEntrySource(ctx: EntryGenerationContext): string;
15
- export declare const SERVER_DOCKERFILE = "# Generated by @pikku/cli \u2014 do not edit\nFROM node:22-slim\n\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends tini ca-certificates \\\n && rm -rf /var/lib/apt/lists/*\n\nRUN groupadd -r pikku && useradd -r -g pikku -u 1001 pikku\n\nWORKDIR /app\n\n# Pikku bundles user code + first-party deps inline; only externalised deps\n# (typically empty for pure-JS apps) are listed in package.json. We ship no\n# lockfile because the bundle has already pinned everything via the\n# extracted exact-dependencies \u2014 `npm install --omit=dev` resolves the\n# leftover externals (if any) at build time.\nCOPY --chown=pikku:pikku package.json ./\nRUN npm install --omit=dev --no-audit --no-fund\n\nCOPY --chown=pikku:pikku bundle.js ./\n\nUSER pikku\nENV NODE_ENV=production\nENV PORT=8080\nEXPOSE 8080\n\nENTRYPOINT [\"/usr/bin/tini\", \"--\"]\nCMD [\"node\", \"bundle.js\"]\n";
15
+ export declare const SERVER_DOCKERFILE = "# Generated by @pikku/cli \u2014 do not edit\n# Full image (not -slim): carries the build toolchain so externalised deps with\n# native addons can compile from source at `npm install` time.\nFROM node:26\n\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends tini ca-certificates \\\n && rm -rf /var/lib/apt/lists/*\n\nRUN groupadd -r pikku && useradd -r -g pikku -u 1001 pikku\n\nWORKDIR /app\n\n# Pikku bundles user code + first-party deps inline; only externalised deps\n# (typically empty for pure-JS apps) are listed in package.json. We ship no\n# lockfile because the bundle has already pinned everything via the\n# extracted exact-dependencies \u2014 `npm install --omit=dev` resolves the\n# leftover externals (if any) at build time.\nCOPY --chown=pikku:pikku package.json ./\nRUN npm install --omit=dev --no-audit --no-fund\n\nCOPY --chown=pikku:pikku bundle.js ./\n\nUSER pikku\nENV NODE_ENV=production\nENV PORT=8080\nEXPOSE 8080\n\nENTRYPOINT [\"/usr/bin/tini\", \"--\"]\nCMD [\"node\", \"bundle.js\"]\n";
16
16
  export declare const SERVER_DOCKERIGNORE = "node_modules\n.git\n*.log\nmetafile.json\nexact-dependencies.json\n";
@@ -45,7 +45,9 @@ export function generateServerEntrySource(ctx) {
45
45
  return lines.join('\n');
46
46
  }
47
47
  export const SERVER_DOCKERFILE = `# Generated by @pikku/cli — do not edit
48
- FROM node:22-slim
48
+ # Full image (not -slim): carries the build toolchain so externalised deps with
49
+ # native addons can compile from source at \`npm install\` time.
50
+ FROM node:26
49
51
 
50
52
  RUN apt-get update \\
51
53
  && apt-get install -y --no-install-recommends tini ca-certificates \\
@@ -387,6 +387,12 @@ export async function runValidate(startDir = process.cwd()) {
387
387
  if (!fnPkg?.dependencies?.['@ai-sdk/openai-compatible']) {
388
388
  e('missing-ai-sdk-openai-compatible', 'Project declares agent units but @ai-sdk/openai-compatible is not in packages/functions dependencies', fnPkgPath, 'Run `yarn add @ai-sdk/openai-compatible` in packages/functions — must be in dependencies, not devDependencies');
389
389
  }
390
+ // `ai` is a peer dep of @pikku/ai-vercel — not auto-installed. Without it
391
+ // `pikku dev` can't construct the agent runner and agents 503 with
392
+ // AIProviderNotConfiguredError.
393
+ if (!fnPkg?.dependencies?.['ai']) {
394
+ e('missing-ai-sdk-core', 'Project declares agent units but `ai` (the Vercel AI SDK) is not in packages/functions dependencies — it is a peer dependency of @pikku/ai-vercel and is not installed automatically', fnPkgPath, 'Run `yarn add ai` in packages/functions — must be in dependencies, not devDependencies');
395
+ }
390
396
  }
391
397
  // services.ts
392
398
  const servicesPath = join(fnDir, 'src', 'services.ts');
@@ -2,7 +2,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
- const DEFAULT_API_URL = 'http://localhost:7103';
5
+ const DEFAULT_API_URL = 'https://api.pikkufabric.com';
6
6
  const projectConfigName = 'pikkufabric.config.json';
7
7
  const authFilePath = join(homedir(), '.fabric', 'auth.json');
8
8
  export async function findProjectConfig(startDir = process.cwd()) {
@@ -0,0 +1,19 @@
1
+ import type { Logger, VariablesService } from '@pikku/core/services';
2
+ import type { AIAgentRunnerService } from '@pikku/core/services';
3
+ /**
4
+ * Build the AI agent runner for `pikku dev` from env.
5
+ *
6
+ * Deployed agent units get their runner wired by the bundler; the dev server
7
+ * has no equivalent, so agents 503 with AIProviderNotConfiguredError unless we
8
+ * construct one here. When an OpenAI-compatible base URL + key are present
9
+ * (fabric injects LITELLM_PROXY_URL/LITELLM_API_KEY; the standard OPENAI_*
10
+ * vars are also honored) we point a single openai-compatible provider at it and
11
+ * register it under every common provider prefix. Returns undefined when no AI
12
+ * env is configured (agents stay disabled, with the clear downstream error) or
13
+ * when the AI SDK packages aren't installed in the project.
14
+ */
15
+ export declare function createDevAIAgentRunner({ logger, projectRoot, variables, }: {
16
+ logger: Logger;
17
+ projectRoot: string;
18
+ variables: VariablesService;
19
+ }): Promise<AIAgentRunnerService | undefined>;
@@ -0,0 +1,70 @@
1
+ import { createRequire } from 'module';
2
+ import { join } from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ // Provider prefixes a fabric/proxy baseURL fronts. Models are written as
5
+ // `openai/gpt-4o-mini`, `openai/deepseek-v4-flash`, etc. — the runner splits on
6
+ // the first `/`, so the prefix only selects the provider entry and the bare
7
+ // model name is forwarded to the OpenAI-compatible proxy, which routes it.
8
+ const PROXY_PROVIDER_NAMES = [
9
+ 'openai',
10
+ 'anthropic',
11
+ 'google',
12
+ 'gemini',
13
+ 'deepseek',
14
+ 'xai',
15
+ 'litellm',
16
+ ];
17
+ /**
18
+ * Build the AI agent runner for `pikku dev` from env.
19
+ *
20
+ * Deployed agent units get their runner wired by the bundler; the dev server
21
+ * has no equivalent, so agents 503 with AIProviderNotConfiguredError unless we
22
+ * construct one here. When an OpenAI-compatible base URL + key are present
23
+ * (fabric injects LITELLM_PROXY_URL/LITELLM_API_KEY; the standard OPENAI_*
24
+ * vars are also honored) we point a single openai-compatible provider at it and
25
+ * register it under every common provider prefix. Returns undefined when no AI
26
+ * env is configured (agents stay disabled, with the clear downstream error) or
27
+ * when the AI SDK packages aren't installed in the project.
28
+ */
29
+ export async function createDevAIAgentRunner({ logger, projectRoot, variables, }) {
30
+ // Pair the URL with its matching key — coalescing each var independently could
31
+ // combine an OPENAI_BASE_URL with a LITELLM_API_KEY (or vice versa) and
32
+ // misroute or 401 every call. Take a complete OpenAI pair first, else LiteLLM.
33
+ const openAIBaseURL = await variables.get('OPENAI_BASE_URL');
34
+ const openAIApiKey = await variables.get('OPENAI_API_KEY');
35
+ const liteLLMBaseURL = await variables.get('LITELLM_PROXY_URL');
36
+ const liteLLMApiKey = await variables.get('LITELLM_API_KEY');
37
+ const [baseURL, apiKey] = openAIBaseURL && openAIApiKey
38
+ ? [openAIBaseURL, openAIApiKey]
39
+ : liteLLMBaseURL && liteLLMApiKey
40
+ ? [liteLLMBaseURL, liteLLMApiKey]
41
+ : [undefined, undefined];
42
+ if (!baseURL || !apiKey) {
43
+ logger.debug('pikku dev: no AI provider env (OPENAI_BASE_URL/OPENAI_API_KEY or LITELLM_PROXY_URL/LITELLM_API_KEY) — AI agents disabled');
44
+ return undefined;
45
+ }
46
+ // Resolve from the project's node_modules — the AI SDK packages are the
47
+ // project's deps, not the CLI's, so they share the project's `ai` version.
48
+ const require = createRequire(pathToFileURL(join(projectRoot, 'package.json')).href);
49
+ let VercelAIAgentRunner;
50
+ let createOpenAICompatible;
51
+ try {
52
+ ;
53
+ ({ VercelAIAgentRunner } = await import(pathToFileURL(require.resolve('@pikku/ai-vercel')).href));
54
+ ({ createOpenAICompatible } = await import(pathToFileURL(require.resolve('@ai-sdk/openai-compatible')).href));
55
+ }
56
+ catch (error) {
57
+ logger.warn(`pikku dev: AI provider env is set but the AI SDK packages could not be loaded (install @pikku/ai-vercel, @ai-sdk/openai-compatible, and ai) — AI agents disabled: ${error instanceof Error ? error.message : String(error)}`);
58
+ return undefined;
59
+ }
60
+ const buildProviders = (key) => {
61
+ const provider = createOpenAICompatible({
62
+ name: 'pikku-dev',
63
+ baseURL,
64
+ apiKey: key,
65
+ });
66
+ return Object.fromEntries(PROXY_PROVIDER_NAMES.map((name) => [name, provider]));
67
+ };
68
+ logger.info(`pikku dev: AI agent runner wired to ${baseURL}`);
69
+ return new VercelAIAgentRunner(buildProviders(apiKey), buildProviders);
70
+ }
@@ -15,6 +15,7 @@ import { WebSocketServer } from 'ws';
15
15
  import { InMemorySchedulerService } from '@pikku/schedule';
16
16
  import { resolveDb, createKysely, parseDatabaseUrl, } from '../db/local-db.js';
17
17
  import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
18
+ import { createDevAIAgentRunner } from './dev-ai-runner.js';
18
19
  export const dev = pikkuSessionlessFunc({
19
20
  remote: true,
20
21
  func: async ({ logger, config, getInspectorState, variables }, { port, watch, hmr }, { rpc }) => {
@@ -153,8 +154,23 @@ export const dev = pikkuSessionlessFunc({
153
154
  // can read runs in dev without projects having to wire their own backing
154
155
  // store.
155
156
  const devLogger = new ConsoleLogger();
157
+ // Deployed agent units get their runner from the bundler; the dev server
158
+ // has no equivalent, so construct one from env or agents 503 with
159
+ // AIProviderNotConfiguredError. The template forwards injected services
160
+ // (`...existingServices`) so this reaches getSingletonServices().
161
+ // Only when the project declares agents — otherwise the runner's
162
+ // missing-SDK warning fires spuriously for projects with global AI env.
163
+ const hasAgents = Object.keys(inspectorState.agents.agentsMeta).length > 0;
164
+ const aiAgentRunner = hasAgents
165
+ ? await createDevAIAgentRunner({
166
+ logger,
167
+ projectRoot: config.rootDir,
168
+ variables,
169
+ })
170
+ : undefined;
156
171
  const inMemoryServices = {
157
172
  logger: devLogger,
173
+ ...(aiAgentRunner ? { aiAgentRunner } : {}),
158
174
  emailService: new LocalEmailService(),
159
175
  metaService: new LocalMetaService(pikkuDir),
160
176
  schedulerService,
@@ -165,6 +165,12 @@ export const pikkuTestsCoverage = pikkuSessionlessFunc({
165
165
  '--require',
166
166
  'tests/tests/support/**/*.ts',
167
167
  'tests/tests/features/**/*.feature',
168
+ '--format',
169
+ 'progress',
170
+ // Persist the HTML report so the console can parse scenarios back out
171
+ // of it (readAllMeta attaches them per function).
172
+ '--format',
173
+ 'html:tests/tests/reports/cucumber-report.html',
168
174
  ], { cwd: functionsDir, stdio: 'inherit', env: spawnEnv });
169
175
  if (res.status !== 0) {
170
176
  logger.error(`Test run failed (exit ${res.status})`);
@@ -7,6 +7,13 @@ export interface CodegenOptions {
7
7
  manifestFile?: string;
8
8
  classificationMapFile?: string;
9
9
  schemaJsonFile?: string;
10
+ /**
11
+ * When set, emit a standalone module of bare enum unions (one
12
+ * `export type <Table><Column>` per enum column) — independent of the wrapped
13
+ * DB interface, so callers import a clean union instead of unwrapping
14
+ * `ColumnType<Private<…>>`.
15
+ */
16
+ enumsFile?: string;
10
17
  camelCase?: boolean;
11
18
  rootDir?: string;
12
19
  /** DB dialect — drives real-type-aware date typing. Defaults to 'sqlite'. */
@@ -17,10 +24,12 @@ export interface CodegenResult {
17
24
  coercionFile: string;
18
25
  manifestFile?: string;
19
26
  classificationMapFile?: string;
27
+ enumsFile?: string;
20
28
  written: boolean;
21
29
  coercionWritten: boolean;
22
30
  manifestWritten: boolean;
23
31
  classificationMapWritten: boolean;
32
+ enumsWritten: boolean;
24
33
  tables: string[];
25
34
  /** Non-fatal codegen warnings (e.g. name looks like a date but unannotated). */
26
35
  warnings: string[];
@@ -31,11 +40,5 @@ export interface CodegenResult {
31
40
  */
32
41
  zodFormats: Record<string, Record<string, ZodFormat>>;
33
42
  }
34
- /**
35
- * Introspect `introspector` and emit:
36
- * - `schema.d.ts` Kysely DB type with classification brands
37
- * - `coercion.gen.ts` Runtime CoercionMap for date/bool/json coercion
38
- * - `classification.gen.ts` Data-classification manifest (when manifestFile set)
39
- */
40
43
  export declare function generateSchemaTypes(introspector: DbIntrospector, options: CodegenOptions): Promise<CodegenResult>;
41
44
  export {};
@@ -177,11 +177,13 @@ function emitInterface(table, camelCase, explicitAnnotations, dialect, enumByNam
177
177
  `is ${col.type} (${real}). If intentional, set its kind in db/annotations.ts.`);
178
178
  }
179
179
  }
180
- // A Postgres enum column reports `type` as 'USER-DEFINED'; its real values
181
- // come from `udtName`. Type it as a union of string literals — only when
182
- // no explicit `tsType`/`kind` overrides it. This reuses the `tsType`
183
- // plumbing, so it flows through both the public and classified branches.
184
- const enumValues = col.udtName ? enumByName.get(col.udtName) : undefined;
180
+ // Enum columns are typed as a union of string literals — only when no
181
+ // explicit `tsType`/`kind` overrides it. This reuses the `tsType` plumbing,
182
+ // so it flows through both the public and classified branches. Values come
183
+ // from a Postgres enum (`type` is 'USER-DEFINED', real values via `udtName`)
184
+ // or, on SQLite, from a `CHECK (col IN ())` constraint the introspector
185
+ // parsed onto `col.enumValues`.
186
+ const enumValues = col.enumValues ?? (col.udtName ? enumByName.get(col.udtName) : undefined);
185
187
  const enumUnion = enumValues && enumValues.length > 0
186
188
  ? enumValues.map((v) => `'${escapeTsString(v)}'`).join(' | ')
187
189
  : null;
@@ -322,6 +324,41 @@ function emitClassificationMap(tables) {
322
324
  * - `coercion.gen.ts` Runtime CoercionMap for date/bool/json coercion
323
325
  * - `classification.gen.ts` Data-classification manifest (when manifestFile set)
324
326
  */
327
+ /**
328
+ * Bare string-literal enum types, independent of the wrapped DB interface. One
329
+ * `export type <Table><Column>` per enum column — Postgres native enums (values
330
+ * via `udtName`/`enumByName`) and SQLite `CHECK (col IN (…))` (values on
331
+ * `col.enumValues`) alike. Lets app code and i18n reconciliation import a clean
332
+ * union instead of unwrapping `ColumnType<Private<…>>` from the DB interface.
333
+ */
334
+ function emitEnumsModule(tables, enumByName) {
335
+ const pascal = (s) => s.charAt(0).toUpperCase() + s.slice(1);
336
+ const seen = new Set();
337
+ const lines = [];
338
+ for (const t of tables) {
339
+ const tablePart = pascal(snakeToCamel(bareTableName(t.name)));
340
+ for (const col of t.columns) {
341
+ const values = col.enumValues ?? (col.udtName ? enumByName.get(col.udtName) : undefined);
342
+ if (!values || values.length === 0)
343
+ continue;
344
+ const name = `${tablePart}${pascal(snakeToCamel(col.name))}`;
345
+ if (seen.has(name))
346
+ continue;
347
+ seen.add(name);
348
+ const union = values.map((v) => `'${escapeTsString(v)}'`).join(' | ');
349
+ lines.push(`export type ${name} = ${union}`);
350
+ }
351
+ }
352
+ return [
353
+ `// Generated by @pikku/cli — do not edit by hand.`,
354
+ `// Run \`pikku db migrate\` to refresh.`,
355
+ `//`,
356
+ `// Bare enum unions from Postgres enums and SQLite CHECK (col IN (…)) constraints.`,
357
+ ``,
358
+ ...(lines.length ? lines : ['export {}']),
359
+ ``,
360
+ ].join('\n');
361
+ }
325
362
  export async function generateSchemaTypes(introspector, options) {
326
363
  const camelCase = options.camelCase ?? true;
327
364
  const dialect = options.dialect ?? 'sqlite';
@@ -456,6 +493,10 @@ export async function generateSchemaTypes(introspector, options) {
456
493
  schemaJsonBody =
457
494
  JSON.stringify({ tables: jsonTables, enums }, null, 2) + '\n';
458
495
  }
496
+ // ── enums.gen.ts ─────────────────────────────────────────────────────────────
497
+ const enumsBody = options.enumsFile
498
+ ? emitEnumsModule(tables, enumByName)
499
+ : null;
459
500
  // ── write files ───────────────────────────────────────────────────────────────
460
501
  let existingSchema = null;
461
502
  let existingCoercion = null;
@@ -498,12 +539,22 @@ export async function generateSchemaTypes(introspector, options) {
498
539
  /* ok */
499
540
  }
500
541
  }
542
+ let existingEnums = null;
543
+ if (options.enumsFile) {
544
+ try {
545
+ existingEnums = readFileSync(options.enumsFile, 'utf8');
546
+ }
547
+ catch {
548
+ /* ok */
549
+ }
550
+ }
501
551
  const schemaChanged = existingSchema !== schemaBody;
502
552
  const coercionChanged = existingCoercion !== coercionBody;
503
553
  const manifestChanged = manifestBody !== null && existingManifest !== manifestBody;
504
554
  const classificationMapChanged = classificationMapBody !== null &&
505
555
  existingClassificationMap !== classificationMapBody;
506
556
  const schemaJsonChanged = schemaJsonBody !== null && existingSchemaJson !== schemaJsonBody;
557
+ const enumsChanged = enumsBody !== null && existingEnums !== enumsBody;
507
558
  if (schemaChanged) {
508
559
  mkdirSync(dirname(options.outFile), { recursive: true });
509
560
  writeFileSync(options.outFile, schemaBody, 'utf8');
@@ -526,15 +577,21 @@ export async function generateSchemaTypes(introspector, options) {
526
577
  mkdirSync(dirname(options.schemaJsonFile), { recursive: true });
527
578
  writeFileSync(options.schemaJsonFile, schemaJsonBody, 'utf8');
528
579
  }
580
+ if (enumsChanged && options.enumsFile && enumsBody) {
581
+ mkdirSync(dirname(options.enumsFile), { recursive: true });
582
+ writeFileSync(options.enumsFile, enumsBody, 'utf8');
583
+ }
529
584
  return {
530
585
  outFile: options.outFile,
531
586
  coercionFile: options.coercionFile,
532
587
  manifestFile: options.manifestFile,
533
588
  classificationMapFile: options.classificationMapFile,
589
+ enumsFile: options.enumsFile,
534
590
  written: schemaChanged,
535
591
  coercionWritten: coercionChanged,
536
592
  manifestWritten: manifestChanged,
537
593
  classificationMapWritten: classificationMapChanged,
594
+ enumsWritten: enumsChanged,
538
595
  tables: tables.map((t) => t.name),
539
596
  warnings,
540
597
  zodFormats,
@@ -13,6 +13,12 @@ export interface ColumnInfo {
13
13
  * name used to resolve its values. Undefined on SQLite (no native enums).
14
14
  */
15
15
  udtName?: string;
16
+ /**
17
+ * String-literal enum values for this column, when known. On SQLite (no native
18
+ * enums) these come from a `CHECK (col IN ('a','b',…))` constraint — the CHECK
19
+ * *is* the enum definition. Typed as a union, mirroring a Postgres enum column.
20
+ */
21
+ enumValues?: string[];
16
22
  }
17
23
  export interface ForeignKeyInfo {
18
24
  column: string;
@@ -14,6 +14,7 @@ interface ResolvedDbBase {
14
14
  schemaJsonFile: string;
15
15
  classificationsFile: string;
16
16
  classificationsGenJsonFile: string;
17
+ enumsFile: string;
17
18
  zodFile: string;
18
19
  camelCase: boolean;
19
20
  }
@@ -47,6 +47,7 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
47
47
  manifestFile: join(outDir, 'db', 'classification.gen.ts'),
48
48
  classificationMapFile: join(outDir, 'db', 'classification-map.gen.d.ts'),
49
49
  schemaJsonFile: join(outDir, 'db', 'pikku-db-schema.gen.json'),
50
+ enumsFile: join(outDir, 'db', 'enums.gen.ts'),
50
51
  classificationsFile: join(rootDir, 'db', 'annotations.ts'),
51
52
  // Compiled sidecar lives beside the authored annotations.ts in db/ — this is
52
53
  // where both consumers read it: the codegen's loadAnnotations() and the
@@ -182,6 +183,7 @@ export async function migrateAndCodegen(resolved) {
182
183
  manifestFile: resolved.manifestFile,
183
184
  classificationMapFile: resolved.classificationMapFile,
184
185
  schemaJsonFile: resolved.schemaJsonFile,
186
+ enumsFile: resolved.enumsFile,
185
187
  camelCase: resolved.camelCase,
186
188
  rootDir: resolved.rootDir,
187
189
  dialect: 'sqlite',
@@ -204,6 +206,7 @@ export async function migrateAndCodegen(resolved) {
204
206
  manifestFile: resolved.manifestFile,
205
207
  classificationMapFile: resolved.classificationMapFile,
206
208
  schemaJsonFile: resolved.schemaJsonFile,
209
+ enumsFile: resolved.enumsFile,
207
210
  camelCase: resolved.camelCase,
208
211
  rootDir: resolved.rootDir,
209
212
  dialect: 'postgres',
@@ -5,6 +5,14 @@ export declare class SqliteIntrospector implements DbIntrospector {
5
5
  constructor(db: SyncSqliteDatabase);
6
6
  listTables(): Promise<string[]>;
7
7
  getColumns(table: string): Promise<ColumnInfo[]>;
8
+ /**
9
+ * SQLite has no native enums, but a `CHECK (col IN ('a','b',…))` constraint is
10
+ * an enum by another name. Parse the table's stored `CREATE TABLE` DDL and map
11
+ * each constrained column to its allowed values so codegen can type it as a
12
+ * union. Only the positive `col IN (…)` form is recognised (the convention);
13
+ * `NOT IN`, ranges, and boolean expressions are left as plain `string`.
14
+ */
15
+ private parseCheckEnums;
8
16
  getForeignKeys(table: string): Promise<ForeignKeyInfo[]>;
9
17
  listEnums(): Promise<EnumInfo[]>;
10
18
  close(): Promise<void>;
@@ -18,6 +18,7 @@ export class SqliteIntrospector {
18
18
  const rows = this.db
19
19
  .prepare(`PRAGMA table_xinfo(${escaped})`)
20
20
  .all();
21
+ const enumsByColumn = this.parseCheckEnums(table);
21
22
  return rows
22
23
  .filter((c) => c.hidden !== 1)
23
24
  .map((c) => ({
@@ -27,8 +28,34 @@ export class SqliteIntrospector {
27
28
  pk: c.pk > 0,
28
29
  defaultValue: c.dflt_value != null ? String(c.dflt_value) : null,
29
30
  generated: c.hidden === 2 || c.hidden === 3,
31
+ enumValues: enumsByColumn.get(c.name),
30
32
  }));
31
33
  }
34
+ /**
35
+ * SQLite has no native enums, but a `CHECK (col IN ('a','b',…))` constraint is
36
+ * an enum by another name. Parse the table's stored `CREATE TABLE` DDL and map
37
+ * each constrained column to its allowed values so codegen can type it as a
38
+ * union. Only the positive `col IN (…)` form is recognised (the convention);
39
+ * `NOT IN`, ranges, and boolean expressions are left as plain `string`.
40
+ */
41
+ parseCheckEnums(table) {
42
+ const out = new Map();
43
+ const row = this.db
44
+ .prepare(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`)
45
+ .get(table);
46
+ const ddl = row?.sql;
47
+ if (!ddl)
48
+ return out;
49
+ const checkIn = /CHECK\s*\(\s*"?(\w+)"?\s+IN\s*\(([^)]*)\)/gi;
50
+ let m;
51
+ while ((m = checkIn.exec(ddl))) {
52
+ const column = m[1];
53
+ const values = [...m[2].matchAll(/'((?:[^']|'')*)'/g)].map((q) => q[1].replace(/''/g, "'"));
54
+ if (values.length > 0)
55
+ out.set(column, values);
56
+ }
57
+ return out;
58
+ }
32
59
  async getForeignKeys(table) {
33
60
  const escaped = `"${table.replace(/"/g, '""')}"`;
34
61
  const rows = this.db
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.49
2
+ * This file was generated by @pikku/cli@0.12.51
3
3
  */
4
4
  /**
5
5
  * Auto-generated remote internal RPC queue worker and HTTP endpoint