@pikku/cli 0.12.88 → 0.12.90

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 (102) hide show
  1. package/console-app/assets/{index-C3FPwdl7.js → index-C5Bd44e4.js} +149 -149
  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 +21 -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-contracts-meta.gen.json +6 -1
  12. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +30 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  18. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  20. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  21. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  23. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.json +64 -4
  25. package/dist/.pikku/function/pikku-functions.gen.js +9 -1
  26. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  27. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  28. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  30. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  31. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  32. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  34. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  35. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-types.gen.js +1 -1
  37. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  38. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  39. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  42. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  43. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +4 -0
  44. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  46. package/dist/.pikku/schemas/register.gen.js +9 -1
  47. package/dist/.pikku/schemas/schemas/DbBaselineInput.schema.json +1 -0
  48. package/dist/.pikku/schemas/schemas/DbCheckInput.schema.json +1 -0
  49. package/dist/.pikku/schemas/schemas/DbCodegenInput.schema.json +1 -0
  50. package/dist/.pikku/schemas/schemas/DbExportInput.schema.json +1 -0
  51. package/dist/.pikku/schemas/schemas/FabricValidateInput.schema.json +1 -1
  52. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  53. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  54. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  55. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  56. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  57. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  58. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  59. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  60. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  61. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  62. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  63. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  64. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  65. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  66. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  68. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  69. package/dist/bin/pikku-bin.mjs +2 -2
  70. package/dist/bin/pikku.js +16 -1
  71. package/dist/src/cli.wiring.js +20 -0
  72. package/dist/src/fabric/fabric-commands.d.ts +9 -3
  73. package/dist/src/fabric/fabric-commands.js +6 -0
  74. package/dist/src/fabric/functions/validate.function.d.ts +15 -5
  75. package/dist/src/fabric/functions/validate.function.js +69 -16
  76. package/dist/src/fabric/lib/config.d.ts +11 -1
  77. package/dist/src/fabric/lib/config.js +39 -1
  78. package/dist/src/fabric/lib/frontend-typecheck.d.ts +46 -0
  79. package/dist/src/fabric/lib/frontend-typecheck.js +139 -0
  80. package/dist/src/functions/commands/db-baseline.d.ts +13 -0
  81. package/dist/src/functions/commands/db-baseline.js +48 -0
  82. package/dist/src/functions/commands/db-check.d.ts +12 -0
  83. package/dist/src/functions/commands/db-check.js +64 -0
  84. package/dist/src/functions/commands/db-codegen.d.ts +11 -0
  85. package/dist/src/functions/commands/db-codegen.js +39 -0
  86. package/dist/src/functions/commands/db-export.d.ts +12 -0
  87. package/dist/src/functions/commands/db-export.js +31 -0
  88. package/dist/src/functions/commands/db-generate.js +19 -26
  89. package/dist/src/functions/db/db-migrator.d.ts +35 -4
  90. package/dist/src/functions/db/db-migrator.js +67 -10
  91. package/dist/src/functions/db/local-db.d.ts +235 -13
  92. package/dist/src/functions/db/local-db.js +430 -45
  93. package/dist/src/functions/db/postgres/postgres-introspector.js +19 -1
  94. package/dist/src/functions/db/postgres/postgres-migrator.d.ts +1 -0
  95. package/dist/src/functions/db/postgres/postgres-migrator.js +4 -1
  96. package/dist/src/functions/db/sqlite/sqlite-introspector.js +2 -1
  97. package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +1 -0
  98. package/dist/src/functions/db/sqlite/sqlite-migrator.js +6 -1
  99. package/dist/src/functions/db/sqlite/sqlite-runtime-node.js +14 -1
  100. package/dist/tsconfig.tsbuildinfo +1 -1
  101. package/package.json +3 -3
  102. package/skills/pikku-i18n/SKILL.md +53 -6
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
3
3
  */
4
4
  import { TypedSecretService as CoreTypedSecretService } from '@pikku/core/services';
5
5
  import secretsMeta from './pikku-secrets-meta.gen.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
3
3
  */
4
4
  import { TypedVariablesService as CoreTypedVariablesService } from '@pikku/core/services';
5
5
  import variablesMeta from './pikku-variables-meta.gen.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88
2
+ * This file was generated by @pikku/cli@0.12.90
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.88') {
15
- process.stderr.write(`\n Update available 0.12.88 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
14
+ if (latest !== '0.12.90') {
15
+ process.stderr.write(`\n Update available 0.12.90 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
16
16
  }
17
17
  } catch {}
18
18
  }
package/dist/bin/pikku.js CHANGED
@@ -19,13 +19,28 @@ const __dirname = dirname(__filename);
19
19
  * Cannot be left to the package manager: the CLI has a node shebang, so `bunx
20
20
  * pikku dev` execs node and bun's own .env injection never reaches the process.
21
21
  * Existing variables win — a real env var must always beat a checked-out file.
22
+ *
23
+ * `process.loadEnvFile` is node-only, and the shebang is not a guarantee: under
24
+ * `bunx --bun pikku …` (which projects use so the CLI gets bun's `node:sqlite`)
25
+ * the process is bun and that method does not exist. Parse the file ourselves in
26
+ * that case rather than losing every secret in it.
22
27
  */
23
28
  function loadEnvFile() {
24
29
  const envPath = join(process.cwd(), '.env');
25
30
  if (!existsSync(envPath))
26
31
  return;
27
32
  try {
28
- process.loadEnvFile(envPath);
33
+ if (typeof process.loadEnvFile === 'function') {
34
+ process.loadEnvFile(envPath);
35
+ return;
36
+ }
37
+ for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
38
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
39
+ if (!match)
40
+ continue;
41
+ const value = match[2].trim().replace(/^(['"])(.*)\1$/s, '$2');
42
+ process.env[match[1]] ??= value;
43
+ }
29
44
  }
30
45
  catch (error) {
31
46
  process.stderr.write(` Could not read .env: ${error.message}\n`);
@@ -18,6 +18,10 @@ import { dev } from './functions/commands/dev.js';
18
18
  import { serve } from './functions/commands/serve.js';
19
19
  import { dbMigrate } from './functions/commands/db-migrate.js';
20
20
  import { dbGenerate } from './functions/commands/db-generate.js';
21
+ import { dbCodegen } from './functions/commands/db-codegen.js';
22
+ import { dbCheck } from './functions/commands/db-check.js';
23
+ import { dbBaseline } from './functions/commands/db-baseline.js';
24
+ import { dbExport } from './functions/commands/db-export.js';
21
25
  import { dbSeed } from './functions/commands/db-seed.js';
22
26
  import { dbReset } from './functions/commands/db-reset.js';
23
27
  import { dbAudit } from './functions/commands/db-audit.js';
@@ -321,6 +325,22 @@ wireCLI({
321
325
  func: dbGenerate,
322
326
  description: 'Generate SQL migrations from detected schema changes',
323
327
  }),
328
+ check: pikkuCLICommand({
329
+ func: dbCheck,
330
+ description: 'Report how the configured database differs from the schema its migrations define',
331
+ }),
332
+ baseline: pikkuCLICommand({
333
+ func: dbBaseline,
334
+ description: 'Record pending migrations as applied, for a database that already has what they describe',
335
+ }),
336
+ codegen: pikkuCLICommand({
337
+ func: dbCodegen,
338
+ description: 'Regenerate db/schema.gen.ts and db/zod.gen.ts from the migration files, without connecting to a database',
339
+ }),
340
+ export: pikkuCLICommand({
341
+ func: dbExport,
342
+ description: "Publish this package's schema so projects consuming it as an addon can create its tables",
343
+ }),
324
344
  seed: pikkuCLICommand({
325
345
  func: dbSeed,
326
346
  description: 'Apply db/seed.sql to the dev database',
@@ -1,5 +1,7 @@
1
1
  export declare const fabricCommands: {
2
- validate: import("@pikku/core/cli").CoreCLICommandConfig<import("../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<Record<string, never>, {
2
+ validate: import("@pikku/core/cli").CoreCLICommandConfig<import("../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
3
+ skipTypecheck?: boolean | undefined;
4
+ }, {
3
5
  ok: boolean;
4
6
  root: string;
5
7
  findings: {
@@ -9,7 +11,9 @@ export declare const fabricCommands: {
9
11
  path: string;
10
12
  fixHint: string;
11
13
  }[];
12
- }, "rpc" | "session", import("../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<Record<string, never>, {
14
+ }, "rpc" | "session", import("../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
15
+ skipTypecheck?: boolean | undefined;
16
+ }, {
13
17
  ok: boolean;
14
18
  root: string;
15
19
  findings: {
@@ -19,7 +23,9 @@ export declare const fabricCommands: {
19
23
  path: string;
20
24
  fixHint: string;
21
25
  }[];
22
- }, "rpc" | "session", import("../../.pikku/pikku-types.gen.js").WiredServices> | import("../../.pikku/pikku-types.gen.js").PikkuFunction<Record<string, never>, {
26
+ }, "rpc" | "session", import("../../.pikku/pikku-types.gen.js").WiredServices> | import("../../.pikku/pikku-types.gen.js").PikkuFunction<{
27
+ skipTypecheck?: boolean | undefined;
28
+ }, {
23
29
  ok: boolean;
24
30
  root: string;
25
31
  findings: {
@@ -48,6 +48,12 @@ export const fabricCommands = defineCLICommands({
48
48
  func: FabricValidate,
49
49
  render: renderValidate,
50
50
  description: 'Check the project structure for fabric compatibility — prints all missing or misconfigured items with fix hints',
51
+ options: {
52
+ skipTypecheck: {
53
+ description: 'Skip the frontend type-check the build container runs (structural checks only)',
54
+ default: false,
55
+ },
56
+ },
51
57
  }),
52
58
  smoke: pikkuCLICommand({
53
59
  func: FabricSmoke,
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
- export declare const FabricValidateInput: z.ZodObject<{}, z.core.$strip>;
2
+ export declare const FabricValidateInput: z.ZodObject<{
3
+ skipTypecheck: z.ZodOptional<z.ZodBoolean>;
4
+ }, z.core.$strip>;
3
5
  export declare const FabricValidateOutput: z.ZodObject<{
4
6
  ok: z.ZodBoolean;
5
7
  root: z.ZodString;
@@ -15,8 +17,12 @@ export declare const FabricValidateOutput: z.ZodObject<{
15
17
  fixHint: z.ZodString;
16
18
  }, z.core.$strip>>;
17
19
  }, z.core.$strip>;
18
- export declare function runValidate(startDir?: string): Promise<z.infer<typeof FabricValidateOutput>>;
19
- export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<Record<string, never>, {
20
+ export declare function runValidate(startDir?: string, opts?: {
21
+ skipTypecheck?: boolean;
22
+ }): Promise<z.infer<typeof FabricValidateOutput>>;
23
+ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
24
+ skipTypecheck?: boolean | undefined;
25
+ }, {
20
26
  ok: boolean;
21
27
  root: string;
22
28
  findings: {
@@ -26,7 +32,9 @@ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js"
26
32
  path: string;
27
33
  fixHint: string;
28
34
  }[];
29
- }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<Record<string, never>, {
35
+ }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
36
+ skipTypecheck?: boolean | undefined;
37
+ }, {
30
38
  ok: boolean;
31
39
  root: string;
32
40
  findings: {
@@ -36,7 +44,9 @@ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js"
36
44
  path: string;
37
45
  fixHint: string;
38
46
  }[];
39
- }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<Record<string, never>, {
47
+ }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<{
48
+ skipTypecheck?: boolean | undefined;
49
+ }, {
40
50
  ok: boolean;
41
51
  root: string;
42
52
  findings: {
@@ -5,6 +5,7 @@ import { dirname, join } from 'node:path';
5
5
  import { createRequire } from 'node:module';
6
6
  import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
7
7
  import { added, changed, removed, dim } from '../lib/output.js';
8
+ import { typeCheckFrontends } from '../lib/frontend-typecheck.js';
8
9
  const FindingSchema = z.object({
9
10
  id: z.string(),
10
11
  severity: z.enum(['error', 'warn', 'info']),
@@ -12,7 +13,12 @@ const FindingSchema = z.object({
12
13
  path: z.string(),
13
14
  fixHint: z.string(),
14
15
  });
15
- export const FabricValidateInput = z.object({});
16
+ export const FabricValidateInput = z.object({
17
+ skipTypecheck: z
18
+ .boolean()
19
+ .optional()
20
+ .describe('Skip the frontend type-check stage (structural checks only, much faster)'),
21
+ });
16
22
  export const FabricValidateOutput = z.object({
17
23
  ok: z.boolean(),
18
24
  root: z.string(),
@@ -182,7 +188,7 @@ const POSTGRES_SQL_PATTERNS = [
182
188
  { re: /\bTSVECTOR\b/i, label: 'TSVECTOR' },
183
189
  { re: /\bARRAY\s*\[/i, label: 'ARRAY[…]' },
184
190
  ];
185
- export async function runValidate(startDir = process.cwd()) {
191
+ export async function runValidate(startDir = process.cwd(), opts = {}) {
186
192
  const root = await findProjectRoot(startDir);
187
193
  const findings = [];
188
194
  const e = (id, message, path, fixHint) => {
@@ -838,19 +844,47 @@ export async function runValidate(startDir = process.cwd()) {
838
844
  info('config-missing', 'packages/functions/src/config.ts not found', join(fnDir, 'src', 'config.ts'), "Create src/config.ts: export const createConfig = pikkuConfig(async () => ({ sqliteDb: '.pikku-runtime/dev.db' }))");
839
845
  }
840
846
  }
847
+ // ── declared frontends ────────────────────────────────────────────────
848
+ // pikkufabric.config.json is unvalidated JSON, so every field below is a
849
+ // claim, not a guarantee: a null entry or a non-string cwd used to throw on
850
+ // property access and take down the whole validation run — the one thing that
851
+ // was supposed to report the broken config. Shape-check the entries once here
852
+ // and let the apps/ and type-check passes consume the narrowed list.
853
+ const declaredFrontends = [];
854
+ /** every syntactically valid cwd, including ones whose directory is missing */
855
+ const declaredCwdList = [];
856
+ const rawFrontends = fabricConfig?.frontends;
857
+ if (rawFrontends !== undefined &&
858
+ (!rawFrontends || typeof rawFrontends !== 'object')) {
859
+ e('frontends-invalid', 'pikkufabric.config.json "frontends" is not an object — no frontend will be built or type-checked', fabricConfigPath, `Set "frontends" to an object keyed by slug: { "app": { "cwd": "apps/app", "kind": "ssr" } }`);
860
+ }
861
+ else if (rawFrontends) {
862
+ for (const [slug, entry] of Object.entries(rawFrontends)) {
863
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
864
+ e(`frontend-entry-invalid-${slug}`, `pikkufabric.config.json frontend "${slug}" is not an object`, fabricConfigPath, `Give "${slug}" an object value: { "cwd": "apps/${slug}", "kind": "ssr" }`);
865
+ continue;
866
+ }
867
+ const { cwd, deploy } = entry;
868
+ if (typeof cwd !== 'string' || cwd.trim() === '') {
869
+ e(`frontend-cwd-invalid-${slug}`, `pikkufabric.config.json frontend "${slug}" has no string "cwd" — the build container has nothing to build`, fabricConfigPath, `Set "cwd" to the app directory, e.g. { "${slug}": { "cwd": "apps/${slug}" } }`);
870
+ continue;
871
+ }
872
+ const rel = cwd.replace(/^\.\//, '');
873
+ declaredCwdList.push(rel);
874
+ const dir = join(root, rel);
875
+ if (!existsSync(dir)) {
876
+ // Reported here rather than inside the apps/ block below: a project
877
+ // without an apps/ directory used to validate cleanly while declaring a
878
+ // deployable frontend that does not exist.
879
+ e(`frontend-cwd-missing-${slug}`, `fabric.config.json frontend "${slug}" declares cwd "${rel}" but that directory does not exist`, dir, `Create the directory or update the cwd in fabric.config.json`);
880
+ continue;
881
+ }
882
+ declaredFrontends.push({ slug, cwd: rel, dir, deploy: deploy !== false });
883
+ }
884
+ }
841
885
  // ── apps/ vs fabric.config.json frontends ─────────────────────────────
842
886
  const appsDir = join(root, 'apps');
843
887
  if (existsSync(appsDir)) {
844
- // Check declared frontends have a real directory on disk
845
- if (fabricConfig) {
846
- const frontends = (fabricConfig.frontends ?? {});
847
- for (const [slug, fe] of Object.entries(frontends)) {
848
- const cwd = fe.cwd?.replace(/^\.\//, '');
849
- if (cwd && !existsSync(join(root, cwd))) {
850
- e(`frontend-cwd-missing-${slug}`, `fabric.config.json frontend "${slug}" declares cwd "${cwd}" but that directory does not exist`, join(root, cwd), `Create the directory or update the cwd in fabric.config.json`);
851
- }
852
- }
853
- }
854
888
  // Check each app/ subdir is declared and has correct local deps
855
889
  let appEntries = [];
856
890
  try {
@@ -861,9 +895,7 @@ export async function runValidate(startDir = process.cwd()) {
861
895
  catch {
862
896
  /* ignore */
863
897
  }
864
- const declaredCwds = fabricConfig
865
- ? new Set(Object.values((fabricConfig.frontends ?? {})).map((f) => f.cwd?.replace(/^\.\//, '') ?? ''))
866
- : null;
898
+ const declaredCwds = fabricConfig ? new Set(declaredCwdList) : null;
867
899
  for (const name of appEntries) {
868
900
  const appPath = join(appsDir, name);
869
901
  const cwd = `apps/${name}`;
@@ -1119,6 +1151,27 @@ export async function runValidate(startDir = process.cwd()) {
1119
1151
  if (!existsSync(sdkDir)) {
1120
1152
  info('functions-sdk-missing', 'packages/functions-sdk/ not found — generated RPC client and React Query hooks will not be available', sdkDir, lines('Create a workspace at `packages/functions-sdk/`.', 'Minimum structure:', '- packages/functions-sdk/package.json', '- packages/functions-sdk/src/pikku/', 'Point `pikku.config.json` clientFiles to:', '- packages/functions-sdk/src/pikku/rpc-map.gen.d.ts', '- packages/functions-sdk/src/pikku/api.gen.ts', 'This package is the home for generated client artifacts, not handwritten server code.'));
1121
1153
  }
1154
+ // ── frontend type-check (what the build container actually runs) ───────
1155
+ // Every check above is a heuristic standing in for this compile. The build
1156
+ // container type-checks each deployable frontend and aborts the deploy on a
1157
+ // non-zero exit, so running the same compile here is the difference between
1158
+ // a 20-second local failure and a burned deploy out of the daily 10.
1159
+ if (!opts.skipTypecheck) {
1160
+ const frontends = declaredFrontends
1161
+ .filter((fe) => fe.deploy)
1162
+ .map((fe) => ({ name: fe.slug, dir: fe.dir }));
1163
+ for (const result of await typeCheckFrontends(root, frontends)) {
1164
+ if (result.skipped) {
1165
+ w(`frontend-typecheck-skipped-${result.name}`, `could not type-check frontend "${result.name}" — ${result.skipped}`, result.dir, lines('The build container type-checks every deployable frontend and aborts the deploy if it fails.', 'Give the app a tsconfig.json and a "tsc" script so the same check runs locally.'));
1166
+ continue;
1167
+ }
1168
+ if (result.errors.length === 0)
1169
+ continue;
1170
+ const shown = result.errors.slice(0, 20);
1171
+ const extra = result.errors.length - shown.length;
1172
+ e(`frontend-typecheck-${result.name}`, `frontend "${result.name}" does not type-check — the build container aborts the deploy on this`, result.dir, lines(...shown, ...(extra > 0 ? [`… and ${extra} more`] : []), '', `Reproduce with: cd ${result.dir.slice(root.length + 1)} && ${result.command}`));
1173
+ }
1174
+ }
1122
1175
  const ok = !findings.some((f) => f.severity === 'error');
1123
1176
  return { ok, root, findings };
1124
1177
  }
@@ -1126,7 +1179,7 @@ export const FabricValidate = pikkuSessionlessFunc({
1126
1179
  description: 'Check the current project structure for fabric compatibility. Prints all missing or misconfigured items with fix hints so an AI agent or developer can resolve them.',
1127
1180
  input: FabricValidateInput,
1128
1181
  output: FabricValidateOutput,
1129
- func: async (_services) => runValidate(),
1182
+ func: async (_services, { skipTypecheck }) => runValidate(process.cwd(), { skipTypecheck }),
1130
1183
  });
1131
1184
  export const renderValidate = (_s, { ok, root, findings }) => {
1132
1185
  if (findings.length === 0) {
@@ -50,7 +50,17 @@ export declare function findProjectConfig(startDir?: string): Promise<{
50
50
  * placeholder to the API as if it were a real id.
51
51
  */
52
52
  export declare function isLinkedProjectId(projectId?: string | null): boolean;
53
- export declare function writeProjectConfig(cwd: string, config: ProjectConfig): Promise<string>;
53
+ /**
54
+ * Merge into the existing config rather than replacing it. `link` and `init`
55
+ * only know the projectId, so a plain write silently deleted every other key —
56
+ * including `frontends`, which is what tells the build container an app exists.
57
+ * The symptom is a deploy that succeeds having built no frontend at all, long
58
+ * after the link that caused it. Unknown keys are preserved too; nothing here
59
+ * has any business dropping config it does not understand.
60
+ */
61
+ export declare function writeProjectConfig(cwd: string, config: Partial<ProjectConfig> & {
62
+ projectId: string;
63
+ }): Promise<string>;
54
64
  export declare function readAuthFile(): Promise<AuthFile>;
55
65
  export declare function writeAuthFile(file: AuthFile): Promise<void>;
56
66
  export interface ResolvedApiContext {
@@ -31,10 +31,48 @@ export function isLinkedProjectId(projectId) {
31
31
  return false;
32
32
  return !/^__.*__$/.test(projectId);
33
33
  }
34
+ /**
35
+ * Merge into the existing config rather than replacing it. `link` and `init`
36
+ * only know the projectId, so a plain write silently deleted every other key —
37
+ * including `frontends`, which is what tells the build container an app exists.
38
+ * The symptom is a deploy that succeeds having built no frontend at all, long
39
+ * after the link that caused it. Unknown keys are preserved too; nothing here
40
+ * has any business dropping config it does not understand.
41
+ */
34
42
  export async function writeProjectConfig(cwd, config) {
35
43
  const path = join(cwd, projectConfigName);
44
+ let existing = {};
45
+ if (existsSync(path)) {
46
+ // A read that fails for any reason other than the file's contents —
47
+ // permissions, a transient I/O error — tells us nothing about what the file
48
+ // holds. Rewriting it then is the exact clobber this function exists to
49
+ // prevent, so refuse the write instead.
50
+ let raw;
51
+ try {
52
+ raw = await readFile(path, 'utf8');
53
+ }
54
+ catch (error) {
55
+ throw new Error(`Cannot read ${projectConfigName} at ${path} (${error.message}) — refusing to overwrite it`);
56
+ }
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
60
+ existing = parsed;
61
+ }
62
+ else {
63
+ // Valid JSON that is not an object holds nothing we can merge, but it is
64
+ // still the user's file — say so rather than dropping it silently.
65
+ console.warn(`[fabric] ${projectConfigName} is not a JSON object — rewriting it`);
66
+ }
67
+ }
68
+ catch (error) {
69
+ // An unparseable config is not a reason to lose the link; warn and start
70
+ // fresh rather than throwing away the command the user just ran.
71
+ console.warn(`[fabric] ${projectConfigName} is not valid JSON (${error.message}) — rewriting it`);
72
+ }
73
+ }
36
74
  await mkdir(dirname(path), { recursive: true });
37
- await writeFile(path, JSON.stringify(config, null, 2) + '\n', 'utf8');
75
+ await writeFile(path, JSON.stringify({ ...existing, ...config }, null, 2) + '\n', 'utf8');
38
76
  return path;
39
77
  }
40
78
  export async function readAuthFile() {
@@ -0,0 +1,46 @@
1
+ export interface FrontendTypeCheckResult {
2
+ /** fabric.config.json frontend key, e.g. "app" */
3
+ name: string;
4
+ /** absolute directory that was type-checked */
5
+ dir: string;
6
+ /**
7
+ * The invocation that actually ran, e.g. `yarn run tsc` or `npx tsc --noEmit`.
8
+ * A finding tells the user to reproduce it, so it has to be the command this
9
+ * check chose — not a guess at their package manager.
10
+ */
11
+ command: string;
12
+ /** raw `file(line,col): error TSxxxx: message` lines, in tsc's order */
13
+ errors: string[];
14
+ /** set when the frontend could not be checked at all (no tsconfig, tsc missing) */
15
+ skipped?: string;
16
+ }
17
+ /**
18
+ * Run each deployable frontend's type-check exactly as the Fabric build
19
+ * container does, so a type error fails `fabric validate` on the developer's
20
+ * machine instead of eight minutes into a deploy.
21
+ *
22
+ * The container runs the frontend's own `tsc` script (falling back to
23
+ * `tsc --noEmit`) and aborts the deploy on a non-zero exit; the deploy budget
24
+ * is 10/day, so spending one on a TS2353 is expensive. Everything else in
25
+ * validate is a structural heuristic *approximating* this compile — this is the
26
+ * compile.
27
+ */
28
+ export declare function typeCheckFrontends(root: string, frontendDirs: Array<{
29
+ name: string;
30
+ dir: string;
31
+ }>): Promise<FrontendTypeCheckResult[]>;
32
+ /**
33
+ * The frontend's own `tsc` script when it has one — that is what the build
34
+ * container runs — otherwise `tsc --noEmit` through the project's package
35
+ * manager, so the compiler resolved is the workspace-local one.
36
+ */
37
+ export declare function resolveTypeCheckCommand(root: string, dir: string): Promise<{
38
+ command: string;
39
+ args: string[];
40
+ }>;
41
+ /**
42
+ * Keep only tsc's diagnostic lines. The runner wraps the command with its own
43
+ * chatter ("$ tsc --noEmit", "error Command failed with exit code 2"), which is
44
+ * noise in a finding.
45
+ */
46
+ export declare function parseTscErrors(output: string): string[];