@pikku/cli 0.12.45 → 0.12.46

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 (72) hide show
  1. package/console-app/assets/index-ByiKUJ11.js +254 -0
  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 +110 -110
  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 -0
  64. package/dist/src/deploy/bundler/bundler.d.ts +1 -0
  65. package/dist/src/deploy/bundler/bundler.js +36 -5
  66. package/dist/src/deploy/provider-adapter.d.ts +7 -0
  67. package/dist/src/fabric/functions/validate.function.js +185 -0
  68. package/dist/src/functions/wirings/auth/pikku-command-auth.js +10 -1
  69. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  70. package/package.json +3 -3
  71. package/skills/pikku-better-auth/SKILL.md +19 -3
  72. package/console-app/assets/index-CRLT8CXr.js +0 -254
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  import { addSchema } from '@pikku/core/schema';
5
5
  import * as PikkuSchemasOutput from './schemas/PikkuSchemasOutput.schema.json' with { type: 'json' };
@@ -236,14 +236,14 @@ import * as PikkuQueueOutput from './schemas/PikkuQueueOutput.schema.json' with
236
236
  addSchema('PikkuQueueOutput', PikkuQueueOutput);
237
237
  import * as PikkuEventsScaffoldOutput from './schemas/PikkuEventsScaffoldOutput.schema.json' with { type: 'json' };
238
238
  addSchema('PikkuEventsScaffoldOutput', PikkuEventsScaffoldOutput);
239
- import * as PikkuSchedulerOutput from './schemas/PikkuSchedulerOutput.schema.json' with { type: 'json' };
240
- addSchema('PikkuSchedulerOutput', PikkuSchedulerOutput);
241
239
  import * as PikkuPublicRPCOutput from './schemas/PikkuPublicRPCOutput.schema.json' with { type: 'json' };
242
240
  addSchema('PikkuPublicRPCOutput', PikkuPublicRPCOutput);
243
241
  import * as PikkuRemoteRPCOutput from './schemas/PikkuRemoteRPCOutput.schema.json' with { type: 'json' };
244
242
  addSchema('PikkuRemoteRPCOutput', PikkuRemoteRPCOutput);
245
243
  import * as PikkuRPCOutput from './schemas/PikkuRPCOutput.schema.json' with { type: 'json' };
246
244
  addSchema('PikkuRPCOutput', PikkuRPCOutput);
245
+ import * as PikkuSchedulerOutput from './schemas/PikkuSchedulerOutput.schema.json' with { type: 'json' };
246
+ addSchema('PikkuSchedulerOutput', PikkuSchedulerOutput);
247
247
  import * as PikkuTriggerTypesInput from './schemas/PikkuTriggerTypesInput.schema.json' with { type: 'json' };
248
248
  addSchema('PikkuTriggerTypesInput', PikkuTriggerTypesInput);
249
249
  import * as PikkuTriggerOutput from './schemas/PikkuTriggerOutput.schema.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  export { wireSecret } from '@pikku/core/secret';
5
5
  export type { CoreSecret, SecretDefinitionMeta, SecretDefinitionsMeta } from '@pikku/core/secret';
@@ -1,4 +1,4 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  export { wireSecret } from '@pikku/core/secret';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  import { TypedSecretService as CoreTypedSecretService } from '@pikku/core/services';
5
5
  import type { SecretService } from '@pikku/core/services';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  import { TypedSecretService as CoreTypedSecretService } from '@pikku/core/services';
5
5
  const CREDENTIALS_META = {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  /**
5
5
  * Trigger-specific type definitions for tree-shaking optimization
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  /**
5
5
  * Trigger-specific type definitions for tree-shaking optimization
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  export { wireVariable } from '@pikku/core/variable';
5
5
  export type { CoreVariable, VariableDefinitionMeta, VariableDefinitionsMeta } from '@pikku/core/variable';
@@ -1,4 +1,4 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
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.45
2
+ * This file was generated by @pikku/cli@0.12.46
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.45
2
+ * This file was generated by @pikku/cli@0.12.46
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.45
2
+ * This file was generated by @pikku/cli@0.12.46
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.45
2
+ * This file was generated by @pikku/cli@0.12.46
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.45
2
+ * This file was generated by @pikku/cli@0.12.46
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.45
2
+ * This file was generated by @pikku/cli@0.12.46
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.45') {
15
- process.stderr.write(`\n Update available 0.12.45 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
14
+ if (latest !== '0.12.46') {
15
+ process.stderr.write(`\n Update available 0.12.46 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
16
16
  }
17
17
  } catch {}
18
18
  }
@@ -81,6 +81,7 @@ export async function runBuildPipeline(options) {
81
81
  entryFiles.set(unitName, entryPath);
82
82
  const bundleResult = await bundleUnits(projectDir, manifest, entryFiles, providerDir, {
83
83
  externals: provider.getExternals?.(),
84
+ stubModules: provider.getStubModules?.(),
84
85
  aliases: provider.getAliases?.(),
85
86
  define: provider.getDefine?.(),
86
87
  platform: provider.getPlatform?.(),
@@ -206,6 +207,7 @@ export async function runBuildPipeline(options) {
206
207
  };
207
208
  const result = await bundleUnits(projectDir, serverlessManifestForBundle, serverlessEntryFiles, providerDir, {
208
209
  externals: provider.getExternals?.(),
210
+ stubModules: provider.getStubModules?.(),
209
211
  aliases: provider.getAliases?.(),
210
212
  define: provider.getDefine?.(),
211
213
  platform: provider.getPlatform?.(),
@@ -30,5 +30,6 @@ export declare function bundleUnits(projectDir: string, manifest: DeploymentMani
30
30
  noRequireShim?: boolean;
31
31
  sourcemap?: boolean;
32
32
  emitMetafile?: boolean;
33
+ stubModules?: string[];
33
34
  resolveOutputDir?: (unit: DeploymentUnit, baseOutputDir: string) => string;
34
35
  }): Promise<BundleOutput>;
@@ -21,6 +21,21 @@ import { extractDependencies, generateMinimalPackageJson, } from './dep-extracto
21
21
  const SERVICE_GEN_FILE_MAP = {
22
22
  metaService: /pikku-meta-service\.gen/,
23
23
  };
24
+ /**
25
+ * Mapping of service name -> npm module patterns to stub when the service is
26
+ * NOT required by a deployment unit. Unlike SERVICE_GEN_FILE_MAP these are
27
+ * external packages, not gen files: a unit that doesn't wire the service never
28
+ * executes the code path that imports them, so replacing them with `export {}`
29
+ * keeps their (often large) trees out of the bundle.
30
+ *
31
+ * The AI SDKs (@pikku/ai-vercel + @ai-sdk/* + `ai`, ~3MB) are only constructed
32
+ * when `aiAgentRunner` is wired (agent units). Every non-agent unit stubs them.
33
+ * The shared services factory must guard the runner construction behind a
34
+ * defined-check on the dynamic import so a stubbed unit simply skips it.
35
+ */
36
+ const SERVICE_MODULE_MAP = {
37
+ aiAgentRunner: [/^@pikku\/ai-vercel/, /^@ai-sdk\//, /^ai$/],
38
+ };
24
39
  /**
25
40
  * Read the per-unit pikku-services.gen.ts and return the set of gen file
26
41
  * patterns that should be stubbed (because their service is not required).
@@ -34,8 +49,14 @@ async function getDeadGenFilePatterns(unitOutputDir) {
34
49
  if (match) {
35
50
  for (const line of match[1].split('\n')) {
36
51
  const kv = line.match(/'([^']+)':\s*false/);
37
- if (kv && SERVICE_GEN_FILE_MAP[kv[1]]) {
38
- patterns.push(SERVICE_GEN_FILE_MAP[kv[1]]);
52
+ if (!kv)
53
+ continue;
54
+ const service = kv[1];
55
+ if (SERVICE_GEN_FILE_MAP[service]) {
56
+ patterns.push(SERVICE_GEN_FILE_MAP[service]);
57
+ }
58
+ if (SERVICE_MODULE_MAP[service]) {
59
+ patterns.push(...SERVICE_MODULE_MAP[service]);
39
60
  }
40
61
  }
41
62
  }
@@ -81,14 +102,20 @@ const EXACT_DEPENDENCIES_FILENAME = 'exact-dependencies.json';
81
102
  * - package.json: Minimal manifest with only the external deps this unit needs
82
103
  */
83
104
  async function bundleUnit(options) {
84
- const { unit, entryPath, unitOutputDir, projectDir, externals, aliases, define, platform, format, sourcemap, emitMetafile, } = options;
105
+ const { unit, entryPath, unitOutputDir, projectDir, externals, aliases, define, platform, format, sourcemap, emitMetafile, stubModules, } = options;
85
106
  await mkdir(unitOutputDir, { recursive: true });
86
107
  const bundlePath = join(unitOutputDir, BUNDLE_FILENAME);
87
108
  const metafilePath = join(unitOutputDir, METAFILE_FILENAME);
88
109
  const packageJsonPath = join(unitOutputDir, PACKAGE_JSON_FILENAME);
89
110
  const exactDependenciesPath = join(unitOutputDir, EXACT_DEPENDENCIES_FILENAME);
90
- // Determine which gen files to stub based on per-unit service requirements
111
+ // Determine which gen files to stub based on per-unit service requirements,
112
+ // plus any provider-supplied module stubs (modules the provider's runtime
113
+ // never executes — e.g. the `postgres` driver on CF Workers, which use a
114
+ // libsql/Turso dialect; the postgres branch is URL-gated and never taken).
91
115
  const deadPatterns = await getDeadGenFilePatterns(unitOutputDir);
116
+ for (const source of stubModules ?? []) {
117
+ deadPatterns.push(new RegExp(source));
118
+ }
92
119
  // Run esbuild — inline everything into a self-contained bundle.
93
120
  // Only Node built-ins are kept external (CF Workers provides them).
94
121
  // The stub plugin replaces gen files for unused services with empty
@@ -132,7 +159,11 @@ async function bundleUnit(options) {
132
159
  metafile: true,
133
160
  target: 'es2022',
134
161
  outfile: bundlePath,
135
- minify: false,
162
+ // Minify every deploy bundle — esbuild output ships straight to the runtime
163
+ // (CF Workers / container), tsc is never the bundler. keepNames preserves
164
+ // Function.name / constructor.name so name-based reflection still works.
165
+ minify: true,
166
+ keepNames: true,
136
167
  sourcemap: sourcemap ?? false,
137
168
  logLevel: 'warning',
138
169
  loader: { '.ts': 'ts' },
@@ -74,6 +74,13 @@ export interface ProviderAdapter {
74
74
  * Defaults to ['node:*'] if not provided.
75
75
  */
76
76
  getExternals?(): string[];
77
+ /**
78
+ * Regex sources for modules to stub to `export {}` during bundling — modules
79
+ * this provider's runtime never executes (e.g. the `postgres` driver on CF
80
+ * Workers, which use a libsql/Turso dialect). Unlike `getExternals`, a stub
81
+ * removes the bytes entirely rather than leaving a runtime import to resolve.
82
+ */
83
+ getStubModules?(): string[];
77
84
  /**
78
85
  * Module aliases for esbuild bundling (e.g. { crypto: 'node:crypto' }).
79
86
  * Used to remap bare imports to platform-compatible paths.
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { readFile, readdir } from 'node:fs/promises';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
+ import { createRequire } from 'node:module';
5
6
  import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
6
7
  import { added, changed, removed, dim } from '../lib/output.js';
7
8
  const FindingSchema = z.object({
@@ -59,6 +60,35 @@ async function readTextSafe(path) {
59
60
  return null;
60
61
  }
61
62
  }
63
+ // List .ts/.tsx source files under a directory (skips node_modules). Used to
64
+ // scan an app for raw @mantine/core imports and i18n usage.
65
+ async function listSourceFiles(dir) {
66
+ if (!existsSync(dir))
67
+ return [];
68
+ try {
69
+ return (await readdir(dir, { recursive: true }))
70
+ .filter((f) => typeof f === 'string' &&
71
+ (f.endsWith('.ts') || f.endsWith('.tsx')) &&
72
+ !f.includes('node_modules'))
73
+ .map((f) => join(dir, f));
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ }
79
+ // Module-singleton-sensitive packages: a SECOND physical copy splits
80
+ // module-level state. The TanStack Start dev server registers its SSR
81
+ // middleware on one copy of @tanstack/start-plugin-core while the config hook
82
+ // reads another, so the frontend serves "Cannot GET /" (404). React/react-dom
83
+ // duplicates break hooks. This is a workspace-hoisting artifact, not a version
84
+ // mismatch — `resolutions` pins do NOT collapse it. Curated, not exhaustive:
85
+ // most duplicate deps are harmless, so only these are checked.
86
+ const SINGLETON_SENSITIVE_PKGS = [
87
+ 'vite',
88
+ '@tanstack/start-plugin-core',
89
+ 'react',
90
+ 'react-dom',
91
+ ];
62
92
  // Minimum @pikku/* versions Fabric requires. The pikku packages are versioned
63
93
  // independently (e.g. @pikku/cli moves faster than @pikku/core), so this is a
64
94
  // per-package floor map, not a single number. Only listed packages are
@@ -318,6 +348,46 @@ export async function runValidate(startDir = process.cwd()) {
318
348
  // readdir failure — skip
319
349
  }
320
350
  }
351
+ // ── better-auth stateless session (unit tree-shaking) ──────────────────
352
+ // Without `session.cookieCache`, the CLI wires the STATEFUL betterAuthSession
353
+ // bridge globally — every non-auth unit then bundles the full better-auth
354
+ // server (~2.5MB each), bloating bundles and the serial deploy uploads.
355
+ // Enabling cookieCache splits out a lean betterAuthStatelessSession that
356
+ // verifies the signed cookie, so only the auth unit carries the server. A
357
+ // hand-written global betterAuthSession defeats it the same way.
358
+ const fnSrcDir = join(fnDir, 'src');
359
+ if (existsSync(fnSrcDir)) {
360
+ try {
361
+ const srcFiles = (await readdir(fnSrcDir, { recursive: true })).filter((f) => typeof f === 'string' &&
362
+ (f.endsWith('.ts') || f.endsWith('.tsx')) &&
363
+ !f.endsWith('.gen.ts') &&
364
+ !f.includes('node_modules'));
365
+ for (const rel of srcFiles) {
366
+ const full = join(fnSrcDir, rel);
367
+ const text = await readTextSafe(full);
368
+ if (!text)
369
+ continue;
370
+ // 1) better-auth config without cookieCache enabled.
371
+ if (/\bpikkuBetterAuth\s*\(/.test(text) &&
372
+ /\bbetterAuth\s*\(/.test(text)) {
373
+ const cookieCacheDisabled = !/cookieCache/.test(text) ||
374
+ /cookieCache\s*:\s*\{[^}]*enabled\s*:\s*false/.test(text);
375
+ if (cookieCacheDisabled) {
376
+ w('better-auth-stateless-session-disabled', 'better-auth config does not enable session.cookieCache — every non-auth unit bundles the full better-auth server (~2.5MB each), bloating bundles and the serial deploy uploads', full, 'Add `session: { cookieCache: { enabled: true } }` to the betterAuth({...}) config so the CLI splits out betterAuthStatelessSession (pikku #737)');
377
+ }
378
+ }
379
+ // 2) hand-written global stateful betterAuthSession bridge.
380
+ if (/addHTTPMiddleware\s*\(\s*['"`]\*['"`]/.test(text) &&
381
+ /\bbetterAuthSession\s*\(/.test(text) &&
382
+ !/betterAuthStatelessSession/.test(text)) {
383
+ w('better-auth-stateful-session-global', 'a global addHTTPMiddleware registers the stateful betterAuthSession bridge — it pulls the full better-auth server into every unit, defeating stateless tree-shaking', full, 'Switch to betterAuthStatelessSession (requires session.cookieCache). A custom mapSession is currently pre-empted by the CLI-generated stateless middleware — see pikku #754');
384
+ }
385
+ }
386
+ }
387
+ catch {
388
+ // readdir failure — skip
389
+ }
390
+ }
321
391
  // Database layout is declared by pikku.config.json db.engine.
322
392
  const migrationsDir = join(root, 'db', dbEngine === 'postgres' ? 'postgres' : 'sqlite');
323
393
  if (!existsSync(migrationsDir)) {
@@ -449,6 +519,121 @@ export async function runValidate(startDir = process.cwd()) {
449
519
  if (componentsPkgName && !appDeps[componentsPkgName]) {
450
520
  info(`app-missing-components-${name}`, `apps/${name} does not depend on ${componentsPkgName}`, join(appPath, 'package.json'), `Add "${componentsPkgName}: workspace:*" to apps/${name}/package.json dependencies`);
451
521
  }
522
+ // The scaffolded dev vite config (generate-frontend-runtime) imports
523
+ // @babel/core to tag JSX with data-om-id for alt-click design editing.
524
+ // It resolves transitively via @vitejs/plugin-react, but that's a silent
525
+ // dependency — declare it explicitly so the resolution can't drift away.
526
+ if (!appPkg.devDependencies?.['@babel/core']) {
527
+ w(`app-missing-babel-core-${name}`, `apps/${name} does not declare @babel/core — the dev runtime needs it to instrument JSX (data-om-id) for design alt-click`, join(appPath, 'package.json'), `Add "@babel/core": "^7.26.0" to apps/${name}/package.json devDependencies`);
528
+ }
529
+ // ── i18n + @pikku/mantine convergence (React frontend apps) ──────────
530
+ // Every frontend converges onto the canonical starter-template stack:
531
+ // Paraglide JS (inlang) for translation + components imported from
532
+ // @pikku/mantine/core (whose I18nNode-typed props make untranslated
533
+ // strings a compile error). A raw @mantine/core import bypasses that gate.
534
+ // The i18next → Paraglide cutover is hard (no back-compat), so a residual
535
+ // i18next dep or useTranslation()/useI18n() call is an error.
536
+ const appAllDeps = {
537
+ ...appPkg.dependencies,
538
+ ...appPkg.devDependencies,
539
+ };
540
+ const isReactFrontend = !!(appAllDeps['@mantine/core'] ||
541
+ appAllDeps['@pikku/mantine'] ||
542
+ appAllDeps['react']);
543
+ if (isReactFrontend) {
544
+ const srcFiles = await listSourceFiles(join(appPath, 'src'));
545
+ let usesMessages = false;
546
+ const rawMantineFiles = [];
547
+ const legacyI18nFiles = [];
548
+ for (const file of srcFiles) {
549
+ const text = await readTextSafe(file);
550
+ if (!text)
551
+ continue;
552
+ const rel = file.slice(appPath.length + 1);
553
+ const norm = rel.replace(/\\/g, '/');
554
+ // Paraglide usage: the reactive useLocale() hook or an import from the
555
+ // local `@/i18n` scaffold (messages `m`, mKey/mList) — either means
556
+ // strings flow through compiled messages.
557
+ if (/\buseLocale\s*\(/.test(text) ||
558
+ /from\s+['"]@\/i18n(?:\/[\w-]+)?['"]/.test(text)) {
559
+ usesMessages = true;
560
+ }
561
+ // Legacy i18next/react-i18next/@pikku/react-i18n markers — removed by
562
+ // the cutover. The scaffold's own config.ts names these in comments,
563
+ // so skip src/i18n/ and match imports/hook calls, not bare words.
564
+ if (!/(?:^|\/)i18n\//.test(norm) &&
565
+ (/from\s+['"](?:react-i18next|i18next|@pikku\/react\/i18n)['"]/.test(text) ||
566
+ /\buseTranslation\s*\(/.test(text) ||
567
+ /\buseI18n\s*\(/.test(text))) {
568
+ legacyI18nFiles.push(rel);
569
+ }
570
+ // component import from @mantine/core — the trailing quote excludes
571
+ // the `@mantine/core/styles.css` side-effect import and @mantine/hooks
572
+ if (/from\s+['"]@mantine\/core['"]/.test(text)) {
573
+ rawMantineFiles.push(rel);
574
+ }
575
+ }
576
+ const hasParaglideDep = !!appAllDeps['@inlang/paraglide-js'];
577
+ const hasMessagesDir = existsSync(join(appPath, 'messages'));
578
+ const hasInlangProject = existsSync(join(appPath, 'project.inlang', 'settings.json'));
579
+ const hasLegacyI18nDeps = !!(appAllDeps['i18next'] || appAllDeps['react-i18next']);
580
+ // 1) i18next must be fully removed — hard cutover to Paraglide.
581
+ if (hasLegacyI18nDeps) {
582
+ e(`app-legacy-i18next-dep-${name}`, `apps/${name} still depends on i18next/react-i18next — Fabric migrated to Paraglide JS (inlang); the i18next stack must be removed`, join(appPath, 'package.json'), lines('Remove "i18next", "react-i18next" and "i18next-browser-languagedetector".', 'Add "@inlang/paraglide-js" (devDependencies) and the src/i18n scaffold.', 'Reference: templates/starter-template/apps/app.'));
583
+ }
584
+ if (legacyI18nFiles.length > 0) {
585
+ e(`app-legacy-i18n-usage-${name}`, `apps/${name} still calls useTranslation()/useI18n() or imports i18next in ${legacyI18nFiles.length} file(s) — these are removed by the Paraglide cutover`, join(appPath, 'src'), lines('Convert legacy i18n usage to Paraglide in:', ...legacyI18nFiles.slice(0, 10).map((f) => ` - ${f}`), ...(legacyI18nFiles.length > 10
586
+ ? [` …and ${legacyI18nFiles.length - 10} more`]
587
+ : []), "Replace `const { t } = useTranslation()` with `useLocale()` from '@/i18n/config',", "and `t('a.b')` with `m.a_b()` from '@/i18n/messages'."));
588
+ }
589
+ // 2) Paraglide must be present and wired (messages + inlang project).
590
+ if (!hasParaglideDep) {
591
+ e(`app-missing-paraglide-${name}`, `apps/${name} has no Paraglide i18n stack — every Fabric frontend must be translatable`, join(appPath, 'package.json'), lines('Add the canonical Paraglide stack:', '1. devDep: "@inlang/paraglide-js".', '2. messages/<locale>.json + project.inlang/settings.json (snake_case keys).', '3. src/i18n scaffold: config.ts (useLocale), messages.ts (branded `m`), ident.ts.', '4. vite.config: paraglideVitePlugin({ project: "./project.inlang", outdir: "./src/paraglide" }).', 'Route every user-visible string through `m.*()`; reference templates/starter-template/apps/app/src/i18n.'));
592
+ }
593
+ else if (!hasMessagesDir || !hasInlangProject) {
594
+ e(`app-paraglide-not-wired-${name}`, `apps/${name} declares @inlang/paraglide-js but is missing ${!hasMessagesDir ? 'messages/' : 'project.inlang/settings.json'} — Paraglide cannot compile`, appPath, lines('Paraglide compiles `messages/<locale>.json` against `project.inlang/settings.json`.', 'Create both (snake_case keys) — the generated src/paraglide/ output is gitignored.'));
595
+ }
596
+ else if (!usesMessages && srcFiles.length > 0) {
597
+ w(`app-i18n-unused-${name}`, `apps/${name} ships Paraglide but no component imports from @/i18n or calls useLocale() — strings are not actually translated`, appPath, "Route user-visible strings through `m.*()` from '@/i18n/messages' and subscribe via `useLocale()`.");
598
+ }
599
+ if (!appAllDeps['@pikku/mantine'] && appAllDeps['@mantine/core']) {
600
+ e(`app-missing-pikku-mantine-${name}`, `apps/${name} uses @mantine/core but not @pikku/mantine — components bypass the i18n-typed compile gate`, join(appPath, 'package.json'), 'Add "@pikku/mantine": "^0.12.5" and import components from "@pikku/mantine/core" (a drop-in for @mantine/core with I18nNode-typed string props).');
601
+ }
602
+ if (rawMantineFiles.length > 0) {
603
+ e(`app-raw-mantine-imports-${name}`, `apps/${name} imports components from "@mantine/core" directly in ${rawMantineFiles.length} file(s) — this bypasses the @pikku/mantine i18n gate, so untranslated strings compile silently`, join(appPath, 'src'), lines(`Swap 'from "@mantine/core"' → 'from "@pikku/mantine/core"' in:`, ...rawMantineFiles.slice(0, 10).map((f) => ` - ${f}`), ...(rawMantineFiles.length > 10
604
+ ? [` …and ${rawMantineFiles.length - 10} more`]
605
+ : []), 'Keep "@mantine/core/styles.css", @mantine/hooks and @mantine/notifications imports as-is.'));
606
+ }
607
+ }
608
+ }
609
+ // ── singleton-sensitive deps must resolve to ONE physical copy ─────────
610
+ // A second physical copy of a peer-virtualized lib (or React) splits
611
+ // module-level state and breaks TanStack Start dev SSR — the perauset
612
+ // "Cannot GET /" 404. Invariant: one resolved install dir per package
613
+ // across {app, root}. Best-effort: needs node_modules installed; anything
614
+ // unresolvable is skipped.
615
+ for (const name of appEntries) {
616
+ const appPath = join(appsDir, name);
617
+ if (!existsSync(join(appPath, 'package.json')))
618
+ continue;
619
+ for (const pkg of SINGLETON_SENSITIVE_PKGS) {
620
+ const installDirs = new Set();
621
+ for (const base of [appPath, root]) {
622
+ try {
623
+ const resolved = createRequire(join(base, 'package.json')).resolve(pkg);
624
+ const esc = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
625
+ const m = resolved.match(new RegExp(`^(.*[\\\\/]node_modules[\\\\/]${esc})[\\\\/]`));
626
+ if (m)
627
+ installDirs.add(m[1]);
628
+ }
629
+ catch {
630
+ // not resolvable from this base — skip
631
+ }
632
+ }
633
+ if (installDirs.size > 1) {
634
+ e(`dup-physical-copy-${name}-${pkg.replace(/[@/]/g, '-')}`, `apps/${name}: "${pkg}" resolves to ${installDirs.size} distinct physical copies — a module-singleton split (breaks TanStack Start dev SSR → frontend 404)`, appPath, lines(`"${pkg}" is installed more than once (e.g. one hoisted to the repo root and one nested under apps/${name}).`, `Declare "${pkg}" in exactly ONE workspace manifest (the root OR apps/${name}, not both), delete yarn.lock, and reinstall so it hoists to a single copy.`, '`resolutions` version-pins do NOT collapse a peer-virtualized duplicate.'));
635
+ }
636
+ }
452
637
  }
453
638
  }
454
639
  // ── packages/theme + packages/components ──────────────────────────────
@@ -1,4 +1,6 @@
1
1
  import { join, dirname } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { rm } from 'node:fs/promises';
2
4
  import { pikkuSessionlessFunc } from '#pikku';
3
5
  import { writeFileInDir } from '../../../utils/file-writer.js';
4
6
  import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
@@ -24,10 +26,17 @@ export const pikkuAuth = pikkuSessionlessFunc({
24
26
  await writeFileInDir(logger, authFile, wiring);
25
27
  await writeFileInDir(logger, secretsFile, secrets);
26
28
  // Stateless split: session middleware in its own file (see serializeAuthGen).
29
+ // Skip it when the project registers its own betterAuthStatelessSession — the
30
+ // generated default-map one would run first and pre-empt the user's custom
31
+ // mapSession (pikkujs/pikku#754). Remove a stale file so it can't linger and
32
+ // double-register.
27
33
  const middlewareFile = join(dirname(authFile), 'auth-middleware.gen.ts');
28
- if (middleware) {
34
+ if (middleware && !state.auth.userStatelessSession) {
29
35
  await writeFileInDir(logger, middlewareFile, middleware);
30
36
  }
37
+ else if (existsSync(middlewareFile)) {
38
+ await rm(middlewareFile, { force: true });
39
+ }
31
40
  // Static metadata of the enabled providers/plugins for the console SSO page,
32
41
  // following the `*-meta.gen.json` convention. Read at runtime by the console
33
42
  // getAuthProviders function instead of a runtime registry.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.46
3
3
  */
4
4
  /**
5
5
  * Auto-generated remote internal RPC queue worker and HTTP endpoint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/cli",
3
- "version": "0.12.45",
3
+ "version": "0.12.46",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "imports": {
@@ -29,9 +29,9 @@
29
29
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
30
30
  "@pikku/better-auth": "^0.12.9",
31
31
  "@pikku/core": "^0.12.35",
32
- "@pikku/deploy-cloudflare": "^0.12.3",
32
+ "@pikku/deploy-cloudflare": "^0.12.4",
33
33
  "@pikku/fetch": "^0.12.3",
34
- "@pikku/inspector": "^0.12.23",
34
+ "@pikku/inspector": "^0.12.24",
35
35
  "@pikku/kysely": "^0.12.16",
36
36
  "@pikku/kysely-node-sqlite": "^0.12.2",
37
37
  "@pikku/node-http-server": "^0.12.2",
@@ -46,9 +46,9 @@ yarn add @pikku/better-auth better-auth
46
46
  Better Auth owns its own HTTP surface, database tables, and session cookie. The Pikku integration is thin:
47
47
 
48
48
  1. **`pikkuBetterAuth(factory)`** — you export ONE `pikkuBetterAuth` call whose factory returns a configured `betterAuth({...})` instance. The pikku CLI inspects this export and generates everything else.
49
- 2. **Generated `auth.gen.ts`** — a catch-all `${basePath}{/*splat}` HTTP route per method (GET + POST) that forwards every request under the base path to better-auth's own internal router, plus `addHTTPMiddleware('*', [betterAuthSession({ auth })])`. The enabled providers and plugins are written to `auth/pikku-auth-meta.gen.json` (read by the console SSO page via `getAuthProviders`).
50
- 3. **Generated `auth-secrets.gen.ts`** a `wireSecret` for `BETTER_AUTH_SECRET` and for each social provider's OAuth credentials, plus a `wireVariable` for any non-secret provider config (e.g. `tenantId`).
51
- 4. **`betterAuthSession`** — middleware that reads better-auth's session on every request and populates the Pikku session object.
49
+ 2. **Generated `auth.gen.ts`** — a catch-all `${basePath}{/*splat}` HTTP route per method (GET + POST) that forwards every request under the base path to better-auth's own internal router. The enabled providers and plugins are written to `auth/pikku-auth-meta.gen.json` (read by the console SSO page via `getAuthProviders`).
50
+ 3. **Generated session middleware** — with `session.cookieCache` enabled (recommended), a separate `auth-middleware.gen.ts` adds the lean stateless `betterAuthStatelessSession()`; without it, `auth.gen.ts` adds the stateful `betterAuthSession()` that bundles the full server into every unit. See "Stateless session" below.
51
+ 4. **Generated `auth-secrets.gen.ts`** — a `wireSecret` for `BETTER_AUTH_SECRET` and for each social provider's OAuth credentials, plus a `wireVariable` for any non-secret provider config (e.g. `tenantId`).
52
52
 
53
53
  You do NOT hand-write routes, the session middleware, or the secret wiring — `pikkuBetterAuth` + the CLI generate all of it. Re-run `pikku auth` (or `pikku all`) to regenerate.
54
54
 
@@ -78,6 +78,8 @@ export const auth = pikkuBetterAuth(async ({ secrets }) => {
78
78
  // at runtime. Swap for the Kysely adapter in production (see below).
79
79
  database: memoryAdapter({ user: [], session: [], account: [], verification: [] }),
80
80
  emailAndPassword: { enabled: true },
81
+ // ALWAYS enable for deployed apps — see "Stateless session" below.
82
+ session: { cookieCache: { enabled: true } },
81
83
  socialProviders: {
82
84
  github: GITHUB_OAUTH,
83
85
  },
@@ -89,6 +91,19 @@ export const auth = pikkuBetterAuth(async ({ secrets }) => {
89
91
  - `socialProviders` keys must be string literals — the CLI reads them statically to emit a `wireSecret` per provider. Provider keys mirror better-auth's built-in ids exactly (e.g. `microsoft`, NOT `microsoft-entra-id`; `cognito`; `github`).
90
92
  - The factory runs lazily on the first auth request, so it pulls secrets/DB off the injected `services`.
91
93
  - The default `basePath` is `/api/auth`. Override it by passing `basePath` to `betterAuth`.
94
+ - **Enable `session: { cookieCache: { enabled: true } }`** so non-auth units tree-shake the better-auth server out (see below).
95
+
96
+ ## ⚠️ Stateless session — ALWAYS enable `cookieCache` for deployed apps
97
+
98
+ By default the CLI wires the **stateful** `betterAuthSession` bridge globally — it calls `services.auth()`, so EVERY unit/worker bundles the full better-auth server (~2.5MB each). On per-unit deploy targets (Fabric/Cloudflare) that bloats every bundle and the serial upload phase.
99
+
100
+ Enabling `session: { cookieCache: { enabled: true } }` makes the CLI split out a lean `betterAuthStatelessSession` (`src/scaffold/auth-middleware.gen.ts`) that verifies the signed session cookie using only `BETTER_AUTH_SECRET` — no `services.auth()`, no server bundled. Non-auth units drop from ~2.5MB to ~20KB. Only the auth unit carries the server. `pikku fabric validate` warns (`better-auth-stateless-session-disabled`) when it's off.
101
+
102
+ **Tradeoff:** server-side session revocation isn't seen until the cookie cache expires (sign-out is still immediate — it deletes the cookie).
103
+
104
+ **Do NOT also hand-write a global `addHTTPMiddleware('*', [betterAuthSession()])`** — that re-drags the stateful server into every unit and defeats the split (validate flags it as `better-auth-stateful-session-global`). The generated middleware is enough.
105
+
106
+ **Custom session fields (`role`, `locale`, …):** the generated stateless middleware uses the default map (`{ userId }` only). To use a custom map, register your own `betterAuthStatelessSession({ mapSession })` **globally** — `addHTTPMiddleware('*', [...])` or `addGlobalMiddleware([...])`. The CLI detects a global user registration and skips generating its own (pikkujs/pikku#754), so you keep cookieCache's lean bundles *and* your custom fields. A route-scoped registration (`addHTTPMiddleware('/some/path', [...])`) does not count — the generated global middleware is still emitted.
92
107
 
93
108
  ### 2. Production database adapter
94
109
 
@@ -105,6 +120,7 @@ export const auth = pikkuBetterAuth(async ({ secrets, kysely }) => {
105
120
  secret: BETTER_AUTH_SECRET,
106
121
  database: kyselyAdapter(kysely, { type: 'postgres' }),
107
122
  emailAndPassword: { enabled: true },
123
+ session: { cookieCache: { enabled: true } },
108
124
  })
109
125
  })
110
126
  ```