@pikku/cli 0.12.43 → 0.12.44

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 (96) 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-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-meta.gen.json +55 -0
  15. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  17. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  18. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  19. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  20. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  21. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  22. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  23. package/dist/.pikku/function/pikku-functions-meta.gen.json +102 -64
  24. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  25. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  26. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  27. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  28. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  29. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  31. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  32. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  34. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  36. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  37. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-types.gen.js +1 -1
  39. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  44. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  45. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +6 -4
  46. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  47. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  48. package/dist/.pikku/schemas/register.gen.js +11 -3
  49. package/dist/.pikku/schemas/schemas/FabricAddInput.schema.json +1 -0
  50. package/dist/.pikku/schemas/schemas/FabricAddOutput.schema.json +1 -0
  51. package/dist/.pikku/schemas/schemas/FabricPublishInput.schema.json +1 -0
  52. package/dist/.pikku/schemas/schemas/FabricPublishOutput.schema.json +1 -0
  53. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  54. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  55. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  56. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  57. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  58. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  59. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  60. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  61. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  62. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  63. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  64. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  65. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  66. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  68. package/dist/bin/pikku-bin.mjs +2 -2
  69. package/dist/src/cli.wiring.js +12 -0
  70. package/dist/src/fabric/fabric-commands.d.ts +61 -3
  71. package/dist/src/fabric/fabric-commands.js +27 -1
  72. package/dist/src/fabric/functions/add.function.d.ts +50 -0
  73. package/dist/src/fabric/functions/add.function.js +144 -0
  74. package/dist/src/fabric/functions/publish.function.d.ts +45 -0
  75. package/dist/src/fabric/functions/publish.function.js +85 -0
  76. package/dist/src/fabric/functions/validate-core.d.ts +1 -1
  77. package/dist/src/fabric/functions/validate.function.d.ts +4 -4
  78. package/dist/src/fabric/functions/validate.function.js +119 -0
  79. package/dist/src/functions/commands/pikku-command-summary.js +3 -2
  80. package/dist/src/functions/commands/versions-update.js +10 -4
  81. package/dist/src/functions/commands/workspace-validate.d.ts +3 -3
  82. package/dist/src/functions/db/better-auth-schema.js +15 -2
  83. package/dist/src/functions/db/sqlite/sqlite-kysely.js +11 -3
  84. package/dist/src/functions/validate/workspace-validate.d.ts +2 -2
  85. package/dist/src/functions/validate/workspace-validate.js +4 -0
  86. package/dist/src/functions/wirings/emails/pikku-command-emails.js +48 -19
  87. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  88. package/dist/src/services/cli-logger-forwarder.service.d.ts +4 -1
  89. package/dist/src/services/cli-logger-forwarder.service.js +20 -2
  90. package/dist/src/services/cli-logger.service.d.ts +16 -2
  91. package/dist/src/services/cli-logger.service.js +33 -5
  92. package/dist/src/services.js +7 -0
  93. package/dist/tsconfig.tsbuildinfo +1 -1
  94. package/package.json +3 -3
  95. package/skills/pikku-emails/SKILL.md +157 -0
  96. package/console-app/assets/index-AwGnKyWe.js +0 -254
@@ -0,0 +1,144 @@
1
+ import { z } from 'zod';
2
+ import { readFile, mkdir, rm, writeFile, rename } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { dirname, isAbsolute, join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
8
+ import { resolveApiContext } from '../lib/config.js';
9
+ export const FabricAddInput = z.object({
10
+ id: z.string(),
11
+ dir: z.string().optional(),
12
+ apiUrl: z.string().optional(),
13
+ });
14
+ export const FabricAddOutput = z.object({
15
+ id: z.string(),
16
+ name: z.string(),
17
+ version: z.string(),
18
+ path: z.string(),
19
+ });
20
+ /** Walk up from cwd to find the project root (the dir with package.json). */
21
+ function resolveProjectRoot() {
22
+ let dir = process.cwd();
23
+ while (true) {
24
+ if (existsSync(join(dir, 'package.json')))
25
+ return dir;
26
+ const parent = dirname(dir);
27
+ if (parent === dir)
28
+ return process.cwd();
29
+ dir = parent;
30
+ }
31
+ }
32
+ /** Read `addons.addonDir` from pikku.config.json if present (json only). */
33
+ async function readAddonDirFromConfig(root) {
34
+ const cfgPath = join(root, 'pikku.config.json');
35
+ if (!existsSync(cfgPath))
36
+ return undefined;
37
+ try {
38
+ const cfg = JSON.parse(await readFile(cfgPath, 'utf8'));
39
+ return cfg.addons?.addonDir;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ /** Ensure the root package.json `workspaces` glob covers `<addonDir>/*`, so a
46
+ * later `yarn install` symlinks the addon into node_modules and `wireAddon`
47
+ * resolves it by package name. */
48
+ async function ensureWorkspaceGlob(root, addonDir) {
49
+ const pkgPath = join(root, 'package.json');
50
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
51
+ const glob = `${addonDir}/*`;
52
+ const objectForm = !Array.isArray(pkg.workspaces) && pkg.workspaces != null;
53
+ const list = Array.isArray(pkg.workspaces)
54
+ ? pkg.workspaces
55
+ : (pkg.workspaces?.packages ?? []);
56
+ if (list.includes(glob))
57
+ return;
58
+ list.push(glob);
59
+ if (objectForm)
60
+ pkg.workspaces.packages = list;
61
+ else
62
+ pkg.workspaces = list;
63
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
64
+ }
65
+ /** Record install provenance in pikku-addons.json — CLI-owned, so we know which
66
+ * registry package + version a folder came from even after the user forks it. */
67
+ async function recordInstall(root, name, rec) {
68
+ const p = join(root, 'pikku-addons.json');
69
+ let data = {};
70
+ if (existsSync(p)) {
71
+ try {
72
+ data = JSON.parse(await readFile(p, 'utf8'));
73
+ }
74
+ catch {
75
+ data = {};
76
+ }
77
+ }
78
+ data[name] = rec;
79
+ await writeFile(p, JSON.stringify(data, null, 2) + '\n');
80
+ }
81
+ /**
82
+ * Install a community-registry addon shadcn-style: the source is copied into
83
+ * `<addonDir>/<name>/` (default `addons/`, top-level so it sits outside the
84
+ * app's TS scan and never collides with the project's own CoreConfig). The dir
85
+ * is registered as a yarn workspace, so `yarn install` symlinks it into
86
+ * node_modules and `wireAddon({ package })` resolves it by name unchanged.
87
+ *
88
+ * Provenance (registry id + version) is recorded in pikku-addons.json, which is
89
+ * CLI-owned and survives the user editing/forking the copied source.
90
+ */
91
+ export const FabricAdd = pikkuSessionlessFunc({
92
+ description: 'Install an addon from the Fabric community registry into addons/ (shadcn-style).',
93
+ input: FabricAddInput,
94
+ output: FabricAddOutput,
95
+ func: async (_services, { id, dir, apiUrl: apiUrlOverride }) => {
96
+ const ctx = await resolveApiContext({ apiUrlOverride });
97
+ // 1. resolve a presigned download URL (public read)
98
+ const metaRes = await fetch(`${ctx.apiUrl}/registry/packages/${encodeURIComponent(id)}/download`);
99
+ if (!metaRes.ok)
100
+ throw new Error(`download lookup failed → ${metaRes.status}: ${await metaRes.text()}`);
101
+ const { url } = (await metaRes.json());
102
+ // 2. fetch the artifact
103
+ const dl = await fetch(url);
104
+ if (!dl.ok)
105
+ throw new Error(`artifact fetch failed → ${dl.status}`);
106
+ const artifact = Buffer.from(await dl.arrayBuffer());
107
+ const root = resolveProjectRoot();
108
+ const addonDir = dir ?? (await readAddonDirFromConfig(root)) ?? 'addons';
109
+ const addonRoot = isAbsolute(addonDir) ? addonDir : join(root, addonDir);
110
+ // 3. stage inside addonRoot (same filesystem — no EXDEV on the final move)
111
+ // and strip npm-pack's `package/` prefix. The dir name isn't known until
112
+ // we read the artifact's package.json.
113
+ await mkdir(addonRoot, { recursive: true });
114
+ const staging = join(addonRoot, `.pikku-add-${id}-${Date.now()}`);
115
+ await mkdir(staging, { recursive: true });
116
+ const tmp = join(tmpdir(), `pikku-add-${id}.tgz`);
117
+ await writeFile(tmp, artifact);
118
+ try {
119
+ execFileSync('tar', ['-xzf', tmp, '-C', staging, '--strip-components=1']);
120
+ await rm(tmp, { force: true });
121
+ const pkg = JSON.parse(await readFile(join(staging, 'package.json'), 'utf8'));
122
+ if (!pkg.name)
123
+ throw new Error('artifact package.json is missing a "name" field');
124
+ const version = pkg.version ?? '0.0.0';
125
+ // shadcn copy: folder is the last segment of the (scoped) package name
126
+ const folder = pkg.name.split('/').pop();
127
+ const target = join(addonRoot, folder);
128
+ await rm(target, { recursive: true, force: true });
129
+ await rename(staging, target);
130
+ // 4. register the workspace glob + record provenance (skip glob for an
131
+ // absolute --dir override — it can't be a relative workspace pattern)
132
+ if (!isAbsolute(addonDir))
133
+ await ensureWorkspaceGlob(root, addonDir);
134
+ await recordInstall(root, pkg.name, { id, version });
135
+ console.log(`[fabric] installed ${pkg.name}@${version} → ${target}`);
136
+ console.log('[fabric] run `yarn install` to link it into node_modules');
137
+ return { id, name: pkg.name, version, path: target };
138
+ }
139
+ finally {
140
+ await rm(staging, { recursive: true, force: true });
141
+ await rm(tmp, { force: true });
142
+ }
143
+ },
144
+ });
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+ export declare const FabricPublishInput: z.ZodObject<{
3
+ dir: z.ZodOptional<z.ZodString>;
4
+ apiUrl: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$strip>;
6
+ export declare const FabricPublishOutput: z.ZodObject<{
7
+ id: z.ZodString;
8
+ name: z.ZodString;
9
+ version: z.ZodString;
10
+ publisher: z.ZodNullable<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ /**
13
+ * Publish a package to the Fabric community registry. Packs the directory into
14
+ * a gzipped tar, requests a short-lived presigned upload URL, PUTs the artifact
15
+ * to R2, then finalizes the publish so the catalogue indexes it. Authenticated
16
+ * as the logged-in user (the package is attributed to their org or person).
17
+ *
18
+ * Generating the package contents (`.pikku/` meta etc.) is a separate step;
19
+ * this command only packages + uploads what's already in the directory.
20
+ */
21
+ export declare const FabricPublish: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
22
+ dir?: string | undefined;
23
+ apiUrl?: string | undefined;
24
+ }, {
25
+ id: string;
26
+ name: string;
27
+ version: string;
28
+ publisher: string | null;
29
+ }, "session" | "rpc", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
30
+ dir?: string | undefined;
31
+ apiUrl?: string | undefined;
32
+ }, {
33
+ id: string;
34
+ name: string;
35
+ version: string;
36
+ publisher: string | null;
37
+ }, "session" | "rpc", import("../../../types/application-types.js").Services> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<{
38
+ dir?: string | undefined;
39
+ apiUrl?: string | undefined;
40
+ }, {
41
+ id: string;
42
+ name: string;
43
+ version: string;
44
+ publisher: string | null;
45
+ }, "session" | "rpc", import("../../../types/application-types.js").Services>, undefined, undefined>;
@@ -0,0 +1,85 @@
1
+ import { z } from 'zod';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { existsSync, readFileSync, mkdirSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
8
+ import { resolveApiContext } from '../lib/config.js';
9
+ export const FabricPublishInput = z.object({
10
+ dir: z.string().optional(),
11
+ apiUrl: z.string().optional(),
12
+ });
13
+ export const FabricPublishOutput = z.object({
14
+ id: z.string(),
15
+ name: z.string(),
16
+ version: z.string(),
17
+ publisher: z.string().nullable(),
18
+ });
19
+ /**
20
+ * Publish a package to the Fabric community registry. Packs the directory into
21
+ * a gzipped tar, requests a short-lived presigned upload URL, PUTs the artifact
22
+ * to R2, then finalizes the publish so the catalogue indexes it. Authenticated
23
+ * as the logged-in user (the package is attributed to their org or person).
24
+ *
25
+ * Generating the package contents (`.pikku/` meta etc.) is a separate step;
26
+ * this command only packages + uploads what's already in the directory.
27
+ */
28
+ export const FabricPublish = pikkuSessionlessFunc({
29
+ description: 'Publish a package directory to the Fabric community registry.',
30
+ input: FabricPublishInput,
31
+ output: FabricPublishOutput,
32
+ func: async (_services, { dir, apiUrl: apiUrlOverride }) => {
33
+ const ctx = await resolveApiContext({ apiUrlOverride });
34
+ if (!ctx.token)
35
+ throw new Error('Not logged in. Run `pikku fabric login` first.');
36
+ const packageDir = dir ?? process.cwd();
37
+ const pkgPath = join(packageDir, 'package.json');
38
+ if (!existsSync(pkgPath))
39
+ throw new Error(`No package.json found in ${packageDir}`);
40
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
41
+ if (!pkg.name || !pkg.version)
42
+ throw new Error('package.json must have a name and version');
43
+ // Pack via `npm pack` so the artifact honours the package's `files` field
44
+ // (ship src/.pikku/types, not build/VCS noise) and matches the layout a
45
+ // normal install produces. npm nests contents under `package/`; the
46
+ // registry ingestion and `pikku fabric add` both handle that prefix.
47
+ const packDir = join(tmpdir(), `pikku-publish-${Date.now()}`);
48
+ mkdirSync(packDir, { recursive: true });
49
+ const packOut = execFileSync('npm', ['pack', '--json', '--pack-destination', packDir], { cwd: packageDir, encoding: 'utf8' });
50
+ const tgzName = JSON.parse(packOut)[0].filename;
51
+ const artifact = readFileSync(join(packDir, tgzName));
52
+ const headers = {
53
+ authorization: `Bearer ${ctx.token}`,
54
+ 'content-type': 'application/json',
55
+ };
56
+ const post = async (path, body) => {
57
+ const r = await fetch(`${ctx.apiUrl}${path}`, {
58
+ method: 'POST',
59
+ headers,
60
+ body: JSON.stringify(body),
61
+ });
62
+ if (!r.ok)
63
+ throw new Error(`POST ${path} → ${r.status}: ${await r.text()}`);
64
+ return r.json();
65
+ };
66
+ // 1. presigned upload URL (short-lived)
67
+ const { uploadUrl, artifactKey } = await post('/registry/packages/publish-url', { packageName: pkg.name, version: pkg.version });
68
+ // 2. PUT the artifact to the exact signed URL (no extra headers — the URL
69
+ // is signed over host only; mismatched headers break the signature).
70
+ const put = await fetch(uploadUrl, { method: 'PUT', body: artifact });
71
+ if (!put.ok)
72
+ throw new Error(`upload failed → ${put.status}: ${await put.text()}`);
73
+ // 3. finalize — server reads the artifact back, extracts meta, indexes it
74
+ const entry = await post('/registry/packages/publish', { artifactKey });
75
+ const publisher = entry.publisher?.name ?? null;
76
+ console.log(`[fabric] published ${entry.name}@${entry.version} (id=${entry.id})` +
77
+ (publisher ? ` as ${publisher}` : ''));
78
+ return {
79
+ id: entry.id,
80
+ name: entry.name,
81
+ version: entry.version,
82
+ publisher,
83
+ };
84
+ },
85
+ });
@@ -6,9 +6,9 @@ export declare const FabricValidateOutput: z.ZodObject<{
6
6
  findings: z.ZodArray<z.ZodObject<{
7
7
  id: z.ZodString;
8
8
  severity: z.ZodEnum<{
9
- info: "info";
10
9
  warn: "warn";
11
10
  error: "error";
11
+ info: "info";
12
12
  }>;
13
13
  message: z.ZodString;
14
14
  path: z.ZodString;
@@ -6,9 +6,9 @@ export declare const FabricValidateOutput: z.ZodObject<{
6
6
  findings: z.ZodArray<z.ZodObject<{
7
7
  id: z.ZodString;
8
8
  severity: z.ZodEnum<{
9
- info: "info";
10
9
  warn: "warn";
11
10
  error: "error";
11
+ info: "info";
12
12
  }>;
13
13
  message: z.ZodString;
14
14
  path: z.ZodString;
@@ -21,7 +21,7 @@ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js"
21
21
  root: string;
22
22
  findings: {
23
23
  id: string;
24
- severity: "info" | "warn" | "error";
24
+ severity: "warn" | "error" | "info";
25
25
  message: string;
26
26
  path: string;
27
27
  fixHint: string;
@@ -31,7 +31,7 @@ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js"
31
31
  root: string;
32
32
  findings: {
33
33
  id: string;
34
- severity: "info" | "warn" | "error";
34
+ severity: "warn" | "error" | "info";
35
35
  message: string;
36
36
  path: string;
37
37
  fixHint: string;
@@ -41,7 +41,7 @@ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js"
41
41
  root: string;
42
42
  findings: {
43
43
  id: string;
44
- severity: "info" | "warn" | "error";
44
+ severity: "warn" | "error" | "info";
45
45
  message: string;
46
46
  path: string;
47
47
  fixHint: string;
@@ -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);