@pikku/cli 0.12.42 → 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.
- package/cli.schema.json +1 -1
- package/console-app/assets/index-CRLT8CXr.js +254 -0
- package/console-app/assets/index-DwyRdRuZ.css +1 -0
- package/console-app/index.html +2 -2
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +11 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +10 -0
- package/dist/.pikku/cli/pikku-cli-client.gen.js +73 -0
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +56 -26
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +2 -2
- package/dist/.pikku/function/pikku-function-types.gen.js +3 -2
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +134 -96
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +2 -2
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +4 -2
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +13 -5
- package/dist/.pikku/schemas/schemas/DeployApplyInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/DeployPlanInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricAddInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricAddOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricPublishInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricPublishOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/cli.wiring.js +12 -0
- package/dist/src/deploy/build-pipeline.d.ts +2 -0
- package/dist/src/deploy/build-pipeline.js +7 -1
- package/dist/src/deploy/bundler/bundler.d.ts +2 -0
- package/dist/src/deploy/bundler/bundler.js +8 -5
- package/dist/src/fabric/fabric-commands.d.ts +61 -3
- package/dist/src/fabric/fabric-commands.js +27 -1
- package/dist/src/fabric/functions/add.function.d.ts +50 -0
- package/dist/src/fabric/functions/add.function.js +144 -0
- package/dist/src/fabric/functions/publish.function.d.ts +45 -0
- package/dist/src/fabric/functions/publish.function.js +85 -0
- package/dist/src/fabric/functions/validate-core.d.ts +1 -1
- package/dist/src/fabric/functions/validate.function.d.ts +4 -4
- package/dist/src/fabric/functions/validate.function.js +119 -0
- package/dist/src/functions/commands/deploy-apply.d.ts +3 -0
- package/dist/src/functions/commands/deploy-apply.js +1 -0
- package/dist/src/functions/commands/deploy-plan.d.ts +3 -0
- package/dist/src/functions/commands/deploy-plan.js +1 -0
- package/dist/src/functions/commands/pikku-command-summary.js +3 -2
- package/dist/src/functions/commands/versions-update.js +10 -4
- package/dist/src/functions/commands/workspace-validate.d.ts +3 -3
- package/dist/src/functions/db/better-auth-schema.js +15 -2
- package/dist/src/functions/db/sqlite/sqlite-kysely.js +11 -3
- package/dist/src/functions/validate/workspace-validate.d.ts +2 -2
- package/dist/src/functions/validate/workspace-validate.js +4 -0
- package/dist/src/functions/wirings/auth/pikku-command-auth.js +6 -1
- package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +4 -1
- package/dist/src/functions/wirings/auth/serialize-auth-gen.js +36 -12
- package/dist/src/functions/wirings/emails/pikku-command-emails.js +48 -19
- package/dist/src/functions/wirings/functions/pikku-command-services.js +5 -4
- package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -2
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/services/cli-logger-forwarder.service.d.ts +4 -1
- package/dist/src/services/cli-logger-forwarder.service.js +20 -2
- package/dist/src/services/cli-logger.service.d.ts +16 -2
- package/dist/src/services/cli-logger.service.js +33 -5
- package/dist/src/services.d.ts +6 -1
- package/dist/src/services.js +13 -12
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/skills/pikku-emails/SKILL.md +157 -0
- package/console-app/assets/index-D9Z9rySK.js +0 -233
- package/console-app/assets/index-DwUzVI5k.css +0 -1
|
@@ -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;
|
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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)) {
|
|
@@ -11,12 +11,15 @@ export declare const deployApply: import("#pikku").PikkuFunctionConfig<{
|
|
|
11
11
|
fromPlan?: boolean;
|
|
12
12
|
provider?: string;
|
|
13
13
|
resultFile?: string;
|
|
14
|
+
debugArtifacts?: boolean;
|
|
14
15
|
}, void, "session" | "rpc", import("#pikku").PikkuFunctionSessionless<{
|
|
15
16
|
fromPlan?: boolean;
|
|
16
17
|
provider?: string;
|
|
17
18
|
resultFile?: string;
|
|
19
|
+
debugArtifacts?: boolean;
|
|
18
20
|
}, void, "session" | "rpc", import("#pikku").Services> | import("#pikku").PikkuFunction<{
|
|
19
21
|
fromPlan?: boolean;
|
|
20
22
|
provider?: string;
|
|
21
23
|
resultFile?: string;
|
|
24
|
+
debugArtifacts?: boolean;
|
|
22
25
|
}, void, "session" | "rpc", import("#pikku").Services>, undefined, undefined>;
|
|
@@ -180,6 +180,7 @@ export const deployApply = pikkuSessionlessFunc({
|
|
|
180
180
|
serverlessIncompatible: config.deploy?.serverlessIncompatible,
|
|
181
181
|
getEntryContext,
|
|
182
182
|
outDir: config.outDir,
|
|
183
|
+
debugArtifacts: data?.debugArtifacts ?? false,
|
|
183
184
|
logger,
|
|
184
185
|
});
|
|
185
186
|
if (buildResult.manifest.units.length === 0) {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export declare const deployPlan: import("#pikku").PikkuFunctionConfig<{
|
|
2
2
|
resultFile?: string;
|
|
3
3
|
provider?: string;
|
|
4
|
+
debugArtifacts?: boolean;
|
|
4
5
|
}, void, "session" | "rpc", import("#pikku").PikkuFunctionSessionless<{
|
|
5
6
|
resultFile?: string;
|
|
6
7
|
provider?: string;
|
|
8
|
+
debugArtifacts?: boolean;
|
|
7
9
|
}, void, "session" | "rpc", import("#pikku").Services> | import("#pikku").PikkuFunction<{
|
|
8
10
|
resultFile?: string;
|
|
9
11
|
provider?: string;
|
|
12
|
+
debugArtifacts?: boolean;
|
|
10
13
|
}, void, "session" | "rpc", import("#pikku").Services>, undefined, undefined>;
|
|
@@ -56,6 +56,7 @@ export const deployPlan = pikkuSessionlessFunc({
|
|
|
56
56
|
serverlessIncompatible: config.deploy?.serverlessIncompatible,
|
|
57
57
|
getEntryContext,
|
|
58
58
|
outDir: config.outDir,
|
|
59
|
+
debugArtifacts: data?.debugArtifacts ?? false,
|
|
59
60
|
logger,
|
|
60
61
|
});
|
|
61
62
|
if (result.manifest.units.length === 0) {
|
|
@@ -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.
|
|
58
|
-
|
|
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.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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();
|