@pikku/cli 0.12.43 → 0.12.45

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 (114) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/index-CRLT8CXr.js +254 -0
  3. package/console-app/assets/{index-VleHndkw.css → index-DwyRdRuZ.css} +1 -1
  4. package/console-app/index.html +2 -2
  5. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  7. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  8. package/dist/.pikku/cli/pikku-cli-channel.js +11 -1
  9. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +5 -0
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +5 -0
  13. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +474 -0
  14. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  15. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  17. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +55 -0
  18. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  19. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  20. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  21. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  22. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  23. package/dist/.pikku/function/pikku-function-types.gen.d.ts +14 -1
  24. package/dist/.pikku/function/pikku-function-types.gen.js +17 -1
  25. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  26. package/dist/.pikku/function/pikku-functions-meta.gen.json +174 -136
  27. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  28. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  29. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  30. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  31. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  32. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  33. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  34. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  35. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  37. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  39. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  40. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  41. package/dist/.pikku/pikku-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  43. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  45. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.json +8 -8
  46. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  47. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  48. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  49. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +6 -4
  50. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  51. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  52. package/dist/.pikku/schemas/register.gen.js +13 -5
  53. package/dist/.pikku/schemas/schemas/FabricAddInput.schema.json +1 -0
  54. package/dist/.pikku/schemas/schemas/FabricAddOutput.schema.json +1 -0
  55. package/dist/.pikku/schemas/schemas/FabricPublishInput.schema.json +1 -0
  56. package/dist/.pikku/schemas/schemas/FabricPublishOutput.schema.json +1 -0
  57. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  58. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  59. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  60. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  61. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  62. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  63. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  64. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  65. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  66. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  67. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  68. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +15 -15
  69. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  70. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  71. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  72. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  73. package/dist/bin/pikku-bin.mjs +2 -2
  74. package/dist/src/cli.wiring.js +12 -0
  75. package/dist/src/fabric/fabric-commands.d.ts +61 -3
  76. package/dist/src/fabric/fabric-commands.js +27 -1
  77. package/dist/src/fabric/functions/add.function.d.ts +50 -0
  78. package/dist/src/fabric/functions/add.function.js +144 -0
  79. package/dist/src/fabric/functions/publish.function.d.ts +45 -0
  80. package/dist/src/fabric/functions/publish.function.js +85 -0
  81. package/dist/src/fabric/functions/validate-core.d.ts +1 -1
  82. package/dist/src/fabric/functions/validate.function.d.ts +4 -4
  83. package/dist/src/fabric/functions/validate.function.js +119 -0
  84. package/dist/src/functions/commands/pikku-command-summary.js +3 -2
  85. package/dist/src/functions/commands/versions-update.js +10 -4
  86. package/dist/src/functions/commands/workspace-validate.d.ts +3 -3
  87. package/dist/src/functions/db/better-auth-schema.js +15 -2
  88. package/dist/src/functions/db/sqlite/sqlite-kysely.js +11 -3
  89. package/dist/src/functions/validate/workspace-validate.d.ts +2 -2
  90. package/dist/src/functions/validate/workspace-validate.js +4 -0
  91. package/dist/src/functions/wirings/channels/pikku-channels.js +20 -11
  92. package/dist/src/functions/wirings/channels/pikku-command-channels-map.js +2 -2
  93. package/dist/src/functions/wirings/channels/pikku-command-channels.js +20 -6
  94. package/dist/src/functions/wirings/channels/serialize-typed-channel-map.d.ts +1 -1
  95. package/dist/src/functions/wirings/channels/serialize-typed-channel-map.js +59 -11
  96. package/dist/src/functions/wirings/cli/pikku-command-cli.js +20 -6
  97. package/dist/src/functions/wirings/emails/pikku-command-emails.js +48 -19
  98. package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +7 -1
  99. package/dist/src/functions/wirings/functions/serialize-addon-refs.d.ts +14 -0
  100. package/dist/src/functions/wirings/functions/serialize-addon-refs.js +129 -0
  101. package/dist/src/functions/wirings/http/pikku-command-http-routes.js +21 -6
  102. package/dist/src/functions/wirings/http/serialize-typed-http-map.js +12 -0
  103. package/dist/src/functions/workflows/all.workflow.js +7 -0
  104. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  105. package/dist/src/services/cli-logger-forwarder.service.d.ts +4 -1
  106. package/dist/src/services/cli-logger-forwarder.service.js +20 -2
  107. package/dist/src/services/cli-logger.service.d.ts +16 -2
  108. package/dist/src/services/cli-logger.service.js +33 -5
  109. package/dist/src/services.js +7 -0
  110. package/dist/src/utils/pikku-cli-config.js +18 -0
  111. package/dist/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +3 -3
  113. package/skills/pikku-emails/SKILL.md +157 -0
  114. package/console-app/assets/index-AwGnKyWe.js +0 -254
@@ -59,6 +59,41 @@ async function readTextSafe(path) {
59
59
  return null;
60
60
  }
61
61
  }
62
+ // Minimum @pikku/* versions Fabric requires. The pikku packages are versioned
63
+ // independently (e.g. @pikku/cli moves faster than @pikku/core), so this is a
64
+ // per-package floor map, not a single number. Only listed packages are
65
+ // enforced — others are skipped to avoid false positives on packages with
66
+ // their own (lower) version lines. Bump these as the supported floor moves.
67
+ // - @pikku/cli < 0.12.43 ships a `pikku dev` that hangs without ever
68
+ // listening (the sandbox never serves routes).
69
+ // - @pikku/core mismatches split pikkuState into duplicate copies, so app
70
+ // and console routes 404; pin the floor that matches the runtime.
71
+ const PIKKU_MIN_VERSIONS = {
72
+ '@pikku/cli': '0.12.43',
73
+ '@pikku/core': '0.12.34',
74
+ };
75
+ // Pull major.minor.patch from a spec, ignoring range prefixes (^ ~ >=),
76
+ // npm: aliases, and pre-release/build suffixes. null if no semver is present
77
+ // (file:, workspace:, *, latest — resolved only at install time).
78
+ function parseSemver(spec) {
79
+ const m = spec.match(/(\d+)\.(\d+)\.(\d+)/);
80
+ if (!m)
81
+ return null;
82
+ return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
83
+ }
84
+ function semverLt(a, b) {
85
+ for (let i = 0; i < 3; i++) {
86
+ if (a[i] !== b[i])
87
+ return a[i] < b[i];
88
+ }
89
+ return false;
90
+ }
91
+ // Fall back to the installed version when the spec carries no semver
92
+ // (file:/workspace:/* deps resolve to a concrete version on disk).
93
+ async function installedSemver(root, pkg) {
94
+ const j = await readJsonSafe(join(root, 'node_modules', pkg, 'package.json'));
95
+ return j?.version ? parseSemver(j.version) : null;
96
+ }
62
97
  // PostgreSQL-specific syntax that won't work on SQLite/libSQL (Turso)
63
98
  const POSTGRES_SQL_PATTERNS = [
64
99
  {
@@ -146,6 +181,59 @@ export async function runValidate(startDir = process.cwd()) {
146
181
  }
147
182
  }
148
183
  }
184
+ // ── @pikku/* minimum versions ──────────────────────────────────────────
185
+ // Scan every workspace manifest for @pikku/* deps below the required floor.
186
+ // A stale @pikku/cli hangs `pikku dev`; a stale @pikku/core duplicates
187
+ // pikkuState and 404s every route — both are hard blockers, so error.
188
+ {
189
+ const manifestPaths = [rootPkgPath];
190
+ for (const group of ['packages', 'apps']) {
191
+ const groupDir = join(root, group);
192
+ if (!existsSync(groupDir))
193
+ continue;
194
+ try {
195
+ for (const d of await readdir(groupDir, { withFileTypes: true })) {
196
+ if (d.isDirectory()) {
197
+ manifestPaths.push(join(groupDir, d.name, 'package.json'));
198
+ }
199
+ }
200
+ }
201
+ catch {
202
+ // ignore
203
+ }
204
+ }
205
+ const lowestByPkg = new Map();
206
+ for (const mPath of manifestPaths) {
207
+ const m = await readJsonSafe(mPath);
208
+ if (!m)
209
+ continue;
210
+ const deps = {
211
+ ...m.dependencies,
212
+ ...m.devDependencies,
213
+ ...m.peerDependencies,
214
+ };
215
+ for (const [pkg, spec] of Object.entries(deps)) {
216
+ if (!pkg.startsWith('@pikku/') || !(pkg in PIKKU_MIN_VERSIONS))
217
+ continue;
218
+ if (typeof spec !== 'string')
219
+ continue;
220
+ const version = parseSemver(spec) ?? (await installedSemver(root, pkg));
221
+ if (!version)
222
+ continue;
223
+ const prev = lowestByPkg.get(pkg);
224
+ if (!prev || semverLt(version, prev.version)) {
225
+ lowestByPkg.set(pkg, { version, manifest: mPath, spec });
226
+ }
227
+ }
228
+ }
229
+ for (const [pkg, seen] of lowestByPkg) {
230
+ const floorStr = PIKKU_MIN_VERSIONS[pkg];
231
+ const floor = parseSemver(floorStr);
232
+ if (floor && semverLt(seen.version, floor)) {
233
+ e(`pikku-version-below-min-${pkg.replace(/[@/]/g, '-')}`, `${pkg} is ${seen.version.join('.')} (spec "${seen.spec}") — Fabric requires >= ${floorStr}`, seen.manifest, lines(`Bump ${pkg} to ^${floorStr} (or newer) and reinstall:`, ` yarn up ${pkg}@^${floorStr}`, 'Then run `yarn install` and re-run `pikku fabric validate`.'));
234
+ }
235
+ }
236
+ }
149
237
  // ── packages/functions/ ────────────────────────────────────────────────
150
238
  const fnDir = join(root, 'packages', 'functions');
151
239
  const functionsSdkPkgName = (await readJsonSafe(join(root, 'packages', 'functions-sdk', 'package.json')))?.name;
@@ -199,6 +287,37 @@ export async function runValidate(startDir = process.cwd()) {
199
287
  e('missing-kysely-sqlite', 'services.ts imports @pikku/kysely-sqlite but it is not in root package.json', rootPkgPath, 'Add "@pikku/kysely-sqlite": "file:./vendor/pikku-kysely-sqlite.tgz" to dependencies');
200
288
  }
201
289
  }
290
+ // ── better-auth client baseURL must include the /auth segment ──────────
291
+ // The Fabric deploy edge keeps the /api prefix for the better-auth unit
292
+ // (it registers /api/auth/*) and strips /api only for the other units; the
293
+ // sandbox Caddy mirrors that with a non-stripping /api/auth/* handler. So
294
+ // the DEFAULT basePath (/api/auth) is the CORRECT server config — do NOT
295
+ // override it. The real footgun is the client: better-auth appends the
296
+ // endpoint to baseURL verbatim, so a bare /api baseURL yields
297
+ // /api/sign-in/email (no /auth) and 404s. The client baseURL must resolve
298
+ // to /api/auth.
299
+ const appsDir = join(root, 'apps');
300
+ if (existsSync(appsDir)) {
301
+ try {
302
+ const appFiles = (await readdir(appsDir, { recursive: true })).filter((f) => typeof f === 'string' &&
303
+ (f.endsWith('.ts') || f.endsWith('.tsx')) &&
304
+ !f.includes('node_modules'));
305
+ for (const rel of appFiles) {
306
+ const text = await readTextSafe(join(appsDir, rel));
307
+ if (!text || !/\bcreateAuthClient\s*\(/.test(text))
308
+ continue;
309
+ const baseURL = text.match(/createAuthClient\s*\([^)]*baseURL\s*:\s*([^,)\n]+)/)?.[1];
310
+ // Heuristic: flag a bare /api baseURL with no /auth segment anywhere
311
+ // near the client config.
312
+ if (baseURL && /['"`]\/api['"`]/.test(baseURL) && !/auth/i.test(baseURL)) {
313
+ w('better-auth-client-baseurl-missing-auth', `createAuthClient baseURL is ${baseURL.trim()} — it omits the /auth segment, so the client calls /api/sign-in/email instead of /api/auth/sign-in/email and auth 404s`, join(appsDir, rel), "Append the auth basePath: baseURL: `${apiUrl()}/auth` (resolving to /api/auth)");
314
+ }
315
+ }
316
+ }
317
+ catch {
318
+ // readdir failure — skip
319
+ }
320
+ }
202
321
  // Database layout is declared by pikku.config.json db.engine.
203
322
  const migrationsDir = join(root, 'db', dbEngine === 'postgres' ? 'postgres' : 'sqlite');
204
323
  if (!existsSync(migrationsDir)) {
@@ -54,8 +54,9 @@ export const pikkuSummary = pikkuSessionlessFunc({
54
54
  // stdout (which would break NDJSON consumers).
55
55
  logger.info({ message: summary.format(), type: 'summary' });
56
56
  }
57
- if (logger.hasCriticalErrors()) {
58
- throw new Error('Pikku inspection failed due to critical diagnostics');
57
+ if (logger.hasBlockingDiagnostics()) {
58
+ const severities = logger.blockingSeverities().join(', ');
59
+ throw new Error(`Pikku inspection failed due to ${severities} diagnostics`);
59
60
  }
60
61
  },
61
62
  });
@@ -12,11 +12,17 @@ export const pikkuVersionsUpdate = pikkuSessionlessFunc({
12
12
  }
13
13
  const immutabilityErrors = visitState.manifest.errors.filter((e) => e.code === ErrorCode.FUNCTION_VERSION_MODIFIED);
14
14
  if (immutabilityErrors.length > 0) {
15
+ // A published contract changed without a version bump. We must not save
16
+ // (that would overwrite an immutable record), but a contract drift should
17
+ // not crash `pikku all` / the dev server. Surface it as an `error`
18
+ // diagnostic: printed always, blocking only under `--fail-on-error`.
19
+ // `pikku versions check` remains the hard deploy gate.
15
20
  for (const e of immutabilityErrors) {
16
- logger.critical(ErrorCode.FUNCTION_VERSION_MODIFIED, e.message);
17
- }
18
- if (logger.hasCriticalErrors()) {
19
- process.exit(1);
21
+ logger.diagnostic({
22
+ severity: 'error',
23
+ code: ErrorCode.FUNCTION_VERSION_MODIFIED,
24
+ message: e.message,
25
+ });
20
26
  }
21
27
  return;
22
28
  }
@@ -4,7 +4,7 @@ export declare const workspaceValidate: import("#pikku").PikkuFunctionConfig<Rec
4
4
  root: string;
5
5
  findings: {
6
6
  id: string;
7
- severity: "info" | "warn" | "error";
7
+ severity: "warn" | "error" | "info";
8
8
  message: string;
9
9
  path: string;
10
10
  fixHint: string;
@@ -14,7 +14,7 @@ export declare const workspaceValidate: import("#pikku").PikkuFunctionConfig<Rec
14
14
  root: string;
15
15
  findings: {
16
16
  id: string;
17
- severity: "info" | "warn" | "error";
17
+ severity: "warn" | "error" | "info";
18
18
  message: string;
19
19
  path: string;
20
20
  fixHint: string;
@@ -24,7 +24,7 @@ export declare const workspaceValidate: import("#pikku").PikkuFunctionConfig<Rec
24
24
  root: string;
25
25
  findings: {
26
26
  id: string;
27
- severity: "info" | "warn" | "error";
27
+ severity: "warn" | "error" | "info";
28
28
  message: string;
29
29
  path: string;
30
30
  fixHint: string;
@@ -3,7 +3,7 @@ import { pathToFileURL } from 'node:url';
3
3
  import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
4
4
  import { join, extname, dirname } from 'node:path';
5
5
  import { PIKKU_BETTER_AUTH } from '@pikku/better-auth';
6
- import { LocalSecretService, LocalVariablesService } from '@pikku/core/services';
6
+ import { LocalVariablesService } from '@pikku/core/services';
7
7
  import { loadUserModule } from '../commands/load-user-project.js';
8
8
  let cachedGetMigrations = null;
9
9
  async function loadGetMigrations() {
@@ -87,9 +87,22 @@ async function loadAuthFactory(sourceFile) {
87
87
  }
88
88
  return null;
89
89
  }
90
+ // Schema-only auth introspection never executes auth — it just reads the Better
91
+ // Auth options to derive the table/column shape. Secret *values* don't affect the
92
+ // schema, so we hand the factory a fake secret service that resolves every key to
93
+ // a placeholder. This keeps `pikku db migrate`'s drift check from requiring the
94
+ // app's real secrets (BETTER_AUTH_SECRET etc.) to be present in the environment.
95
+ function fakeSecretService() {
96
+ const placeholder = 'schema-introspection-only';
97
+ return {
98
+ getSecret: async () => placeholder,
99
+ hasSecret: async () => true,
100
+ setSecret: async () => { },
101
+ };
102
+ }
90
103
  function schemaServicesStub(kysely, logger) {
91
104
  const variables = new LocalVariablesService();
92
- const secrets = new LocalSecretService(variables);
105
+ const secrets = fakeSecretService();
93
106
  const base = {
94
107
  kysely,
95
108
  logger,
@@ -12,12 +12,19 @@ function coerce(v) {
12
12
  return JSON.stringify(v);
13
13
  return v;
14
14
  }
15
+ // A statement returns rows when it is a SELECT or carries a RETURNING clause.
16
+ // node:sqlite's StatementSync has no `reader` flag (always undefined), so without
17
+ // this kysely would run INSERT ... RETURNING via `.run()` and drop the returned
18
+ // rows — which breaks better-auth sign-up (it inserts and expects the row back).
19
+ function isReaderSql(sql) {
20
+ return /^\s*select/i.test(sql) || /\breturning\b/i.test(sql);
21
+ }
15
22
  class RuntimeSqliteStatement {
16
23
  stmt;
17
24
  reader;
18
- constructor(stmt) {
25
+ constructor(stmt, reader) {
19
26
  this.stmt = stmt;
20
- this.reader = Boolean(stmt.reader);
27
+ this.reader = reader;
21
28
  }
22
29
  all(parameters) {
23
30
  return this.stmt.all(...parameters.map(coerce));
@@ -41,7 +48,8 @@ class RuntimeSqliteDatabase {
41
48
  this.db = db;
42
49
  }
43
50
  prepare(sql) {
44
- return new RuntimeSqliteStatement(this.db.prepare(sql));
51
+ const stmt = this.db.prepare(sql);
52
+ return new RuntimeSqliteStatement(stmt, Boolean(stmt.reader) || isReaderSql(sql));
45
53
  }
46
54
  close() {
47
55
  this.db.close();
@@ -2,9 +2,9 @@ import { z } from 'zod';
2
2
  export declare const FindingSchema: z.ZodObject<{
3
3
  id: z.ZodString;
4
4
  severity: z.ZodEnum<{
5
- info: "info";
6
5
  warn: "warn";
7
6
  error: "error";
7
+ info: "info";
8
8
  }>;
9
9
  message: z.ZodString;
10
10
  path: z.ZodString;
@@ -18,9 +18,9 @@ export declare const WorkspaceValidateOutput: z.ZodObject<{
18
18
  findings: z.ZodArray<z.ZodObject<{
19
19
  id: z.ZodString;
20
20
  severity: z.ZodEnum<{
21
- info: "info";
22
21
  warn: "warn";
23
22
  error: "error";
23
+ info: "info";
24
24
  }>;
25
25
  message: z.ZodString;
26
26
  path: z.ZodString;
@@ -95,6 +95,10 @@ export async function runWorkspaceValidate(startDir = process.cwd()) {
95
95
  if (!pikkuConfig.clientFiles) {
96
96
  info('pikku-config-no-client-files', 'pikku.config.json missing "clientFiles" — generated RPC client files and React Query hooks will not be written', pikkuConfigPath, 'Add clientFiles.rpcMapDeclarationFile and clientFiles.reactQueryFile pointing to packages/functions-sdk/src/pikku/ (for example: rpc-map.gen.d.ts and api.gen.ts)');
97
97
  }
98
+ const scaffold = pikkuConfig.scaffold;
99
+ if (!scaffold?.console) {
100
+ e('pikku-config-no-console-scaffold', 'pikku.config.json missing "scaffold.console" — Fabric cannot introspect the running app (console:getFunctionsMeta and friends 404), so the sandbox builder shows no functions', pikkuConfigPath, 'Add "console": "no-auth" under "scaffold" in pikku.config.json (use "auth" to require a session)');
101
+ }
98
102
  }
99
103
  const rootPkgPath = join(root, 'package.json');
100
104
  const rootPkg = await readJsonSafe(rootPkgPath);
@@ -6,19 +6,28 @@ import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
6
6
  export const pikkuChannels = pikkuVoidFunc({
7
7
  func: async ({ logger, config, getInspectorState }) => {
8
8
  const visitState = await getInspectorState();
9
- const { channelsWiringFile, channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings, schema, } = config;
10
- const { channels } = visitState;
11
- if (channels.files.size === 0 || Object.keys(channels.meta).length === 0) {
9
+ const { channelsWiringFile, channelsWiringMetaFile, channelsWiringMetaJsonFile, channelContractsMetaJsonFile, packageMappings, schema, } = config;
10
+ const { channels, exportedContracts } = visitState;
11
+ const hasChannelContracts = Object.keys(exportedContracts.channel).length > 0;
12
+ if ((channels.files.size === 0 || Object.keys(channels.meta).length === 0) &&
13
+ !hasChannelContracts) {
12
14
  return;
13
15
  }
14
- await writeFileInDir(logger, channelsWiringFile, serializeFileImports('addChannel', channelsWiringFile, channels.files, packageMappings));
15
- await writeFileInDir(logger, channelsWiringMetaJsonFile, JSON.stringify(channels.meta, null, 2));
16
- const jsonImportPath = getFileImportRelativePath(channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings);
17
- const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
18
- const importStatement = supportsImportAttributes
19
- ? `import metaData from '${jsonImportPath}' with { type: 'json' }`
20
- : `import metaData from '${jsonImportPath}'`;
21
- await writeFileInDir(logger, channelsWiringMetaFile, `import { pikkuState } from '@pikku/core/internal'\nimport { ChannelsMeta } from '@pikku/core/channel'\n${importStatement}\npikkuState(null, 'channel', 'meta', metaData as ChannelsMeta)`);
16
+ if (channels.files.size > 0 && Object.keys(channels.meta).length > 0) {
17
+ await writeFileInDir(logger, channelsWiringFile, serializeFileImports('addChannel', channelsWiringFile, channels.files, packageMappings));
18
+ }
19
+ if (Object.keys(channels.meta).length > 0) {
20
+ await writeFileInDir(logger, channelsWiringMetaJsonFile, JSON.stringify(channels.meta, null, 2));
21
+ }
22
+ await writeFileInDir(logger, channelContractsMetaJsonFile, JSON.stringify(exportedContracts.channel, null, 2));
23
+ if (Object.keys(channels.meta).length > 0) {
24
+ const jsonImportPath = getFileImportRelativePath(channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings);
25
+ const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
26
+ const importStatement = supportsImportAttributes
27
+ ? `import metaData from '${jsonImportPath}' with { type: 'json' }`
28
+ : `import metaData from '${jsonImportPath}'`;
29
+ await writeFileInDir(logger, channelsWiringMetaFile, `import { pikkuState } from '@pikku/core/internal'\nimport { ChannelsMeta } from '@pikku/core/channel'\n${importStatement}\npikkuState(null, 'channel', 'meta', metaData as ChannelsMeta)`);
30
+ }
22
31
  },
23
32
  middleware: [
24
33
  logCommandInfoAndTime({
@@ -5,8 +5,8 @@ import { serializeTypedChannelsMap } from './serialize-typed-channel-map.js';
5
5
  export const pikkuChannelsMap = pikkuSessionlessFunc({
6
6
  func: async ({ logger, config, getInspectorState }) => {
7
7
  const state = await getInspectorState();
8
- const { channelsMapDeclarationFile, packageMappings } = config;
9
- const content = serializeTypedChannelsMap(logger, channelsMapDeclarationFile, packageMappings, state.functions.typesMap, state.functions.meta, state.addonFunctions, state.channels.meta);
8
+ const { channelsMapDeclarationFile, packageMappings, rpcInternalMapDeclarationFile, } = config;
9
+ const content = serializeTypedChannelsMap(logger, channelsMapDeclarationFile, packageMappings, state.functions.typesMap, state.functions.meta, state.addonFunctions, state.channels.meta, rpcInternalMapDeclarationFile);
10
10
  await writeFileInDir(logger, channelsMapDeclarationFile, content);
11
11
  },
12
12
  middleware: [
@@ -7,21 +7,35 @@ import { stripVerboseFields, hasVerboseFields, } from '../../../utils/strip-verb
7
7
  export const pikkuCommandChannels = pikkuSessionlessFunc({
8
8
  func: async ({ logger, config, getInspectorState }) => {
9
9
  const visitState = await getInspectorState();
10
- const { channelsWiringFile, channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings, schema, } = config;
11
- const { channels } = visitState;
12
- if (channels.files.size === 0 || Object.keys(channels.meta).length === 0) {
10
+ const { channelsWiringFile, channelsWiringMetaFile, channelsWiringMetaJsonFile, channelContractsMetaJsonFile, channelContractsMetaFile, packageMappings, schema, } = config;
11
+ const { channels, exportedContracts } = visitState;
12
+ const hasChannelContracts = Object.keys(exportedContracts.channel).length > 0;
13
+ if ((channels.files.size === 0 || Object.keys(channels.meta).length === 0) &&
14
+ !hasChannelContracts) {
13
15
  return undefined;
14
16
  }
17
+ // The bootstrap imports channelsWiringFile and channelsWiringMetaFile
18
+ // whenever this command reports channels as active (truthy return), so both
19
+ // must always be written once past the guard above — including the
20
+ // contracts-only case where there are no local channel source files
21
+ // (channels.files is empty). Skipping either leaves the bootstrap importing
22
+ // a file that was never generated and the per-unit deploy bundle fails.
15
23
  await writeFileInDir(logger, channelsWiringFile, serializeFileImports('addChannel', channelsWiringFile, channels.files, packageMappings));
16
- // Write minimal JSON (runtime-only fields)
17
24
  const minimalMeta = stripVerboseFields(channels.meta);
18
25
  await writeFileInDir(logger, channelsWiringMetaJsonFile, JSON.stringify(minimalMeta, null, 2));
19
- // Write verbose JSON only if it has additional fields
20
26
  if (hasVerboseFields(channels.meta)) {
21
27
  const verbosePath = channelsWiringMetaJsonFile.replace(/\.gen\.json$/, '-verbose.gen.json');
22
28
  await writeFileInDir(logger, verbosePath, JSON.stringify(channels.meta, null, 2));
23
29
  }
24
- // Generate TypeScript file that imports the minimal JSON
30
+ await writeFileInDir(logger, channelContractsMetaJsonFile, JSON.stringify(exportedContracts.channel, null, 2));
31
+ if (hasChannelContracts) {
32
+ const contractsJsonImportPath = getFileImportRelativePath(channelContractsMetaFile, channelContractsMetaJsonFile, packageMappings);
33
+ const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
34
+ const contractsImportStatement = supportsImportAttributes
35
+ ? `import contractsMeta from '${contractsJsonImportPath}' with { type: 'json' }`
36
+ : `import contractsMeta from '${contractsJsonImportPath}'`;
37
+ await writeFileInDir(logger, channelContractsMetaFile, `${contractsImportStatement}\nexport default contractsMeta`);
38
+ }
25
39
  const jsonImportPath = getFileImportRelativePath(channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings);
26
40
  const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
27
41
  const importStatement = supportsImportAttributes
@@ -2,4 +2,4 @@ import type { ChannelsMeta } from '@pikku/core/channel';
2
2
  import type { TypesMap } from '@pikku/inspector';
3
3
  import type { FunctionsMeta } from '@pikku/core';
4
4
  import type { Logger } from '@pikku/core/services';
5
- export declare const serializeTypedChannelsMap: (logger: Logger, relativeToPath: string, packageMappings: Record<string, string>, typesMap: TypesMap, functionsMeta: FunctionsMeta, addonFunctions: Record<string, FunctionsMeta>, channelsMeta: ChannelsMeta) => string;
5
+ export declare const serializeTypedChannelsMap: (logger: Logger, relativeToPath: string, packageMappings: Record<string, string>, typesMap: TypesMap, functionsMeta: FunctionsMeta, addonFunctions: Record<string, FunctionsMeta>, channelsMeta: ChannelsMeta, rpcInternalMapDeclarationFile: string) => string;
@@ -1,7 +1,8 @@
1
1
  import { serializeImportMap } from '../../../utils/serialize-import-map.js';
2
+ import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
2
3
  import { generateCustomTypes, resolveFunctionMeta } from '@pikku/inspector';
3
- export const serializeTypedChannelsMap = (logger, relativeToPath, packageMappings, typesMap, functionsMeta, addonFunctions, channelsMeta) => {
4
- const { channels, requiredTypes } = generateChannels(functionsMeta, addonFunctions, channelsMeta);
4
+ export const serializeTypedChannelsMap = (logger, relativeToPath, packageMappings, typesMap, functionsMeta, addonFunctions, channelsMeta, rpcInternalMapDeclarationFile) => {
5
+ const { channels, requiredTypes } = generateChannels(logger, typesMap, functionsMeta, addonFunctions, channelsMeta);
5
6
  typesMap.customTypes.forEach(({ references }) => {
6
7
  for (const reference of references) {
7
8
  if (reference !== '__object' && !reference.startsWith('__object_')) {
@@ -9,13 +10,25 @@ export const serializeTypedChannelsMap = (logger, relativeToPath, packageMapping
9
10
  }
10
11
  }
11
12
  });
13
+ const needsFlattenedRPCMap = Array.from(requiredTypes).some((t) => t.includes('FlattenedRPCMap'));
14
+ if (needsFlattenedRPCMap) {
15
+ for (const t of Array.from(requiredTypes)) {
16
+ if (t.includes('FlattenedRPCMap')) {
17
+ requiredTypes.delete(t);
18
+ }
19
+ }
20
+ }
12
21
  const imports = serializeImportMap(logger, relativeToPath, packageMappings, typesMap, requiredTypes);
22
+ const rpcMapImport = needsFlattenedRPCMap
23
+ ? `import type { FlattenedRPCMap } from '${getFileImportRelativePath(relativeToPath, rpcInternalMapDeclarationFile, packageMappings)}'`
24
+ : '';
13
25
  const serializedCustomTypes = generateCustomTypes(typesMap, requiredTypes);
14
26
  return `/**
15
27
  * This provides the structure needed for TypeScript to be aware of channels
16
28
  */
17
29
 
18
30
  ${imports}
31
+ ${rpcMapImport}
19
32
  ${serializedCustomTypes}
20
33
 
21
34
  interface ChannelHandler<I, O> {
@@ -40,7 +53,7 @@ export type ChannelWiringHandlerOf<
40
53
  : never;
41
54
  `;
42
55
  };
43
- function generateChannels(functionsMeta, addonFunctions, channelsMeta) {
56
+ function generateChannels(logger, typesMap, functionsMeta, addonFunctions, channelsMeta) {
44
57
  const state = { functions: { meta: functionsMeta }, addonFunctions };
45
58
  const requiredTypes = new Set();
46
59
  const channelsObject = {};
@@ -57,17 +70,30 @@ function generateChannels(functionsMeta, addonFunctions, channelsMeta) {
57
70
  const inputTypes = func.inputs || null;
58
71
  const outputTypes = func.outputs || null;
59
72
  channelsObject[name].message = {
60
- inputs: inputTypes,
61
- outputs: outputTypes,
73
+ inputs: normalizeTypes(logger, typesMap, inputTypes),
74
+ outputs: normalizeTypes(logger, typesMap, outputTypes),
62
75
  };
63
- inputTypes?.forEach((type) => requiredTypes.add(type));
64
- outputTypes?.forEach((type) => requiredTypes.add(type));
76
+ channelsObject[name].message.inputs?.forEach((type) => requiredTypes.add(type));
77
+ channelsObject[name].message.outputs?.forEach((type) => requiredTypes.add(type));
65
78
  }
66
79
  for (const [key, route] of Object.entries(messageWirings)) {
67
80
  if (!channelsObject[name].routes[key]) {
68
81
  channelsObject[name].routes[key] = {};
69
82
  }
70
83
  for (const [method, { pikkuFuncId }] of Object.entries(route)) {
84
+ // Addon functions are namespaced ('ns:fn') and their types aren't in
85
+ // the consumer's local typesMap, but are reachable via FlattenedRPCMap.
86
+ if (pikkuFuncId.includes(':')) {
87
+ const inputType = `FlattenedRPCMap['${pikkuFuncId}']['input']`;
88
+ const outputType = `FlattenedRPCMap['${pikkuFuncId}']['output']`;
89
+ channelsObject[name].routes[key][method] = {
90
+ inputTypes: [inputType],
91
+ outputTypes: [outputType],
92
+ };
93
+ requiredTypes.add(inputType);
94
+ requiredTypes.add(outputType);
95
+ continue;
96
+ }
71
97
  const func = resolveFunctionMeta(state, pikkuFuncId);
72
98
  if (!func) {
73
99
  throw new Error(`Function ${pikkuFuncId} not found in functionsMeta for channel ${name}, route ${key}, method ${method}`);
@@ -75,11 +101,11 @@ function generateChannels(functionsMeta, addonFunctions, channelsMeta) {
75
101
  const inputTypes = func.inputs || null;
76
102
  const outputTypes = func.outputs || null;
77
103
  channelsObject[name].routes[key][method] = {
78
- inputTypes,
79
- outputTypes,
104
+ inputTypes: normalizeTypes(logger, typesMap, inputTypes),
105
+ outputTypes: normalizeTypes(logger, typesMap, outputTypes),
80
106
  };
81
- inputTypes?.forEach((type) => requiredTypes.add(type));
82
- outputTypes?.forEach((type) => requiredTypes.add(type));
107
+ channelsObject[name].routes[key][method].inputTypes?.forEach((type) => requiredTypes.add(type));
108
+ channelsObject[name].routes[key][method].outputTypes?.forEach((type) => requiredTypes.add(type));
83
109
  }
84
110
  }
85
111
  }
@@ -112,3 +138,25 @@ function generateChannels(functionsMeta, addonFunctions, channelsMeta) {
112
138
  function formatTypeArray(types) {
113
139
  return types ? types.join(' | ') : 'null';
114
140
  }
141
+ function normalizeTypes(logger, typesMap, types) {
142
+ if (!types || types.length === 0)
143
+ return types;
144
+ const resolved = types.filter((type) => hasType(typesMap, type));
145
+ if (resolved.length > 0) {
146
+ return resolved;
147
+ }
148
+ logger.warn(`Channel type '${types.join(' | ')}' not found in local typesMap, falling back to unknown`);
149
+ return ['unknown'];
150
+ }
151
+ function hasType(typesMap, type) {
152
+ if (['void', 'never', 'unknown', 'null', 'undefined'].includes(type)) {
153
+ return true;
154
+ }
155
+ try {
156
+ typesMap.getTypeMeta(type);
157
+ return true;
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
@@ -7,21 +7,35 @@ import { stripVerboseFields, hasVerboseFields, } from '../../../utils/strip-verb
7
7
  export const pikkuCLI = pikkuSessionlessFunc({
8
8
  func: async ({ logger, config, getInspectorState }) => {
9
9
  const visitState = await getInspectorState();
10
- const { cliWiringsFile, cliWiringMetaFile, cliWiringMetaJsonFile, packageMappings, schema, } = config;
11
- const { cli } = visitState;
12
- if (cli.files.size === 0 || Object.keys(cli.meta).length === 0) {
10
+ const { cliWiringsFile, cliWiringMetaFile, cliWiringMetaJsonFile, cliContractsMetaJsonFile, cliContractsMetaFile, packageMappings, schema, } = config;
11
+ const { cli, exportedContracts } = visitState;
12
+ const hasCLIContracts = Object.keys(exportedContracts.cli).length > 0;
13
+ if ((cli.files.size === 0 || Object.keys(cli.meta).length === 0) &&
14
+ !hasCLIContracts) {
13
15
  return undefined;
14
16
  }
15
- // Generate CLI wirings file
17
+ // The bootstrap imports cliWiringsFile and cliWiringMetaFile whenever this
18
+ // command reports CLI as active (truthy return), so both must always be
19
+ // written once past the guard above — including the contracts-only case
20
+ // where there are no local wireCLI source files (cli.files is empty).
21
+ // Skipping either leaves the bootstrap importing a file that was never
22
+ // generated and the per-unit deploy bundle fails.
16
23
  await writeFileInDir(logger, cliWiringsFile, serializeFileImports('wireCLI', cliWiringsFile, cli.files, packageMappings));
17
- // Write minimal JSON (runtime-only fields)
18
24
  const minimalMeta = stripVerboseFields(cli.meta);
19
25
  await writeFileInDir(logger, cliWiringMetaJsonFile, JSON.stringify(minimalMeta, null, 2));
20
- // Write verbose JSON only if it has additional fields
21
26
  if (hasVerboseFields(cli.meta)) {
22
27
  const verbosePath = cliWiringMetaJsonFile.replace(/\.gen\.json$/, '-verbose.gen.json');
23
28
  await writeFileInDir(logger, verbosePath, JSON.stringify(cli.meta, null, 2));
24
29
  }
30
+ await writeFileInDir(logger, cliContractsMetaJsonFile, JSON.stringify(exportedContracts.cli, null, 2));
31
+ if (hasCLIContracts) {
32
+ const contractsJsonImportPath = getFileImportRelativePath(cliContractsMetaFile, cliContractsMetaJsonFile, packageMappings);
33
+ const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
34
+ const contractsImportStatement = supportsImportAttributes
35
+ ? `import contractsMeta from '${contractsJsonImportPath}' with { type: 'json' }`
36
+ : `import contractsMeta from '${contractsJsonImportPath}'`;
37
+ await writeFileInDir(logger, cliContractsMetaFile, `${contractsImportStatement}\nexport default contractsMeta`);
38
+ }
25
39
  const jsonImportPath = getFileImportRelativePath(cliWiringMetaFile, cliWiringMetaJsonFile, packageMappings);
26
40
  const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
27
41
  const importStatement = supportsImportAttributes