baysecms-cli 1.0.0
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/README.md +16 -0
- package/cli.mjs +590 -0
- package/package.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# BayseCMS CLI
|
|
2
|
+
|
|
3
|
+
CLI for BayseCMS schema push and frontend helper generation.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `baysecms schema push`
|
|
8
|
+
- `baysecms codegen`
|
|
9
|
+
|
|
10
|
+
## Example
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
BAYSECMS_API_URL="https://api.example.com" \
|
|
14
|
+
BAYSECMS_API_KEY="<api-key>" \
|
|
15
|
+
baysecms codegen --out src/lib/baysecms
|
|
16
|
+
```
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BayseCMS CLI — schema-as-code → cloud tenant.
|
|
4
|
+
* Usage: baysecms schema push [--config path] [--file path] [--review] [--publish]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { cwd } from "node:process";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONFIG = "baysecms.config.json";
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const args = { _: [], flags: {} };
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const a = argv[i];
|
|
17
|
+
if (a.startsWith("--")) {
|
|
18
|
+
const key = a.slice(2);
|
|
19
|
+
const next = argv[i + 1];
|
|
20
|
+
if (!next || next.startsWith("--")) {
|
|
21
|
+
args.flags[key] = true;
|
|
22
|
+
} else {
|
|
23
|
+
args.flags[key] = next;
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
args._.push(a);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function loadConfig(configPath) {
|
|
34
|
+
const raw = await readFile(configPath, "utf-8");
|
|
35
|
+
return JSON.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function headersFromConfig(cfg) {
|
|
39
|
+
const h = { "content-type": "application/json" };
|
|
40
|
+
const apiKey = process.env.BAYSECMS_API_KEY || cfg.apiKey;
|
|
41
|
+
if (apiKey) {
|
|
42
|
+
h["x-api-key"] = apiKey;
|
|
43
|
+
return h;
|
|
44
|
+
}
|
|
45
|
+
const token = process.env.BAYSECMS_TOKEN || cfg.token;
|
|
46
|
+
if (!token) {
|
|
47
|
+
console.error("Missing auth: set BAYSECMS_API_KEY or BAYSECMS_TOKEN (or apiKey/token in config).");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
h.authorization = `Bearer ${token}`;
|
|
51
|
+
const tenantId = process.env.BAYSECMS_TENANT_ID || cfg.tenantId;
|
|
52
|
+
const workspaceId = process.env.BAYSECMS_WORKSPACE_ID || cfg.workspaceId;
|
|
53
|
+
if (tenantId) h["x-tenant-id"] = tenantId;
|
|
54
|
+
if (workspaceId) h["x-workspace-id"] = workspaceId;
|
|
55
|
+
return h;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const toPascalCase = (value) =>
|
|
59
|
+
String(value)
|
|
60
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
61
|
+
.split(" ")
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
64
|
+
.join("");
|
|
65
|
+
|
|
66
|
+
const toCamelCase = (value) => {
|
|
67
|
+
const parts = String(value)
|
|
68
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
69
|
+
.trim()
|
|
70
|
+
.split(" ")
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
if (!parts.length) return "field";
|
|
73
|
+
return parts
|
|
74
|
+
.map((part, index) => {
|
|
75
|
+
const normalized = part.toLowerCase();
|
|
76
|
+
if (index === 0) return normalized;
|
|
77
|
+
return normalized[0].toUpperCase() + normalized.slice(1);
|
|
78
|
+
})
|
|
79
|
+
.join("");
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const toSafeIdentifier = (value) => {
|
|
83
|
+
const camel = toCamelCase(value).replace(/[^a-zA-Z0-9_$]/g, "");
|
|
84
|
+
if (!camel) return "field";
|
|
85
|
+
if (/^[0-9]/.test(camel)) return `field${camel}`;
|
|
86
|
+
return camel;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const fieldToTypeScript = (field) => {
|
|
90
|
+
switch (field.type) {
|
|
91
|
+
case "string":
|
|
92
|
+
case "text":
|
|
93
|
+
case "slug":
|
|
94
|
+
case "email":
|
|
95
|
+
case "url":
|
|
96
|
+
return "string";
|
|
97
|
+
case "number":
|
|
98
|
+
return "number";
|
|
99
|
+
case "boolean":
|
|
100
|
+
return "boolean";
|
|
101
|
+
case "image":
|
|
102
|
+
return "{ url: string; alt?: string | null }";
|
|
103
|
+
case "richText":
|
|
104
|
+
return "Array<{ text: string }>";
|
|
105
|
+
case "tags":
|
|
106
|
+
return "string[]";
|
|
107
|
+
case "object": {
|
|
108
|
+
const nested = (field.fields ?? [])
|
|
109
|
+
.map((f) => `${toSafeIdentifier(f.key)}${f.required ? "" : "?"}: ${fieldToTypeScript(f)};`)
|
|
110
|
+
.join(" ");
|
|
111
|
+
return `{ ${nested} }`;
|
|
112
|
+
}
|
|
113
|
+
case "array": {
|
|
114
|
+
const item = field.items ?? { type: "string", key: "item" };
|
|
115
|
+
return `${fieldToTypeScript(item)}[]`;
|
|
116
|
+
}
|
|
117
|
+
default:
|
|
118
|
+
return "unknown";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function buildGeneratedTypes(collections) {
|
|
123
|
+
const lines = [
|
|
124
|
+
"/** Auto-generated by `baysecms codegen`. Do not edit manually. */",
|
|
125
|
+
""
|
|
126
|
+
];
|
|
127
|
+
for (const collection of collections) {
|
|
128
|
+
const typeName = `${toPascalCase(collection.slug)}Item`;
|
|
129
|
+
lines.push(`export type ${typeName} = {`);
|
|
130
|
+
lines.push(" id: string;");
|
|
131
|
+
lines.push(" slug: string | null;");
|
|
132
|
+
lines.push(" createdAt: string;");
|
|
133
|
+
lines.push(" updatedAt: string;");
|
|
134
|
+
lines.push(" data: {");
|
|
135
|
+
for (const field of collection.fields ?? []) {
|
|
136
|
+
const tsType = fieldToTypeScript(field);
|
|
137
|
+
lines.push(` ${toSafeIdentifier(field.key)}${field.required ? "" : "?"}: ${tsType};`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(" };");
|
|
140
|
+
lines.push("};");
|
|
141
|
+
lines.push("");
|
|
142
|
+
}
|
|
143
|
+
return `${lines.join("\n").trim()}\n`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildGeneratedData(collections) {
|
|
147
|
+
const lines = [
|
|
148
|
+
"/** Auto-generated by `baysecms codegen`. Do not edit manually. */",
|
|
149
|
+
'import { createBayseCMSClient } from "baysecms/client";',
|
|
150
|
+
'import type {',
|
|
151
|
+
...collections.map((c) => ` ${toPascalCase(c.slug)}Item,`),
|
|
152
|
+
'} from "./types";',
|
|
153
|
+
"",
|
|
154
|
+
"const readEnv = (key: string): string => {",
|
|
155
|
+
" if (typeof process !== \"undefined\" && process?.env?.[key]) return String(process.env[key]);",
|
|
156
|
+
" const maybeRuntime = globalThis as { __BAYSECMS_RUNTIME__?: Record<string, string | undefined>; __VITE_ENV__?: Record<string, string | undefined> };",
|
|
157
|
+
" if (maybeRuntime.__BAYSECMS_RUNTIME__?.[key]) return String(maybeRuntime.__BAYSECMS_RUNTIME__[key]);",
|
|
158
|
+
" if (key === \"BAYSECMS_API_URL\" && maybeRuntime.__VITE_ENV__?.VITE_BAYSECMS_API_URL) return String(maybeRuntime.__VITE_ENV__.VITE_BAYSECMS_API_URL);",
|
|
159
|
+
" if (key === \"BAYSECMS_API_KEY\" && maybeRuntime.__VITE_ENV__?.VITE_BAYSECMS_API_KEY) return String(maybeRuntime.__VITE_ENV__.VITE_BAYSECMS_API_KEY);",
|
|
160
|
+
" return \"\";",
|
|
161
|
+
"};",
|
|
162
|
+
"",
|
|
163
|
+
"function getApiClient() {",
|
|
164
|
+
" const apiUrl = readEnv(\"BAYSECMS_API_URL\") || \"http://localhost:4000\";",
|
|
165
|
+
" const apiKey = readEnv(\"BAYSECMS_API_KEY\") || \"\";",
|
|
166
|
+
" return createBayseCMSClient({",
|
|
167
|
+
" baseUrl: apiUrl,",
|
|
168
|
+
" apiKey: apiKey || undefined",
|
|
169
|
+
" });",
|
|
170
|
+
"}",
|
|
171
|
+
"",
|
|
172
|
+
"const toCamelCase = (value: string): string => {",
|
|
173
|
+
" const parts = String(value)",
|
|
174
|
+
" .replace(/[^a-zA-Z0-9]+/g, \" \")",
|
|
175
|
+
" .trim()",
|
|
176
|
+
" .split(\" \")",
|
|
177
|
+
" .filter(Boolean);",
|
|
178
|
+
" if (!parts.length) return \"field\";",
|
|
179
|
+
" return parts",
|
|
180
|
+
" .map((part, index) => {",
|
|
181
|
+
" const normalized = part.toLowerCase();",
|
|
182
|
+
" return index === 0 ? normalized : normalized[0].toUpperCase() + normalized.slice(1);",
|
|
183
|
+
" })",
|
|
184
|
+
" .join(\"\");",
|
|
185
|
+
"};",
|
|
186
|
+
"",
|
|
187
|
+
"const normalizeKeys = (value: unknown): unknown => {",
|
|
188
|
+
" if (Array.isArray(value)) return value.map(normalizeKeys);",
|
|
189
|
+
" if (!value || typeof value !== \"object\") return value;",
|
|
190
|
+
" const output: Record<string, unknown> = {};",
|
|
191
|
+
" for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {",
|
|
192
|
+
" output[toCamelCase(key)] = normalizeKeys(nested);",
|
|
193
|
+
" }",
|
|
194
|
+
" return output;",
|
|
195
|
+
"};",
|
|
196
|
+
"",
|
|
197
|
+
"const mapItem = <T extends { data?: unknown }>(item: T): T => ({",
|
|
198
|
+
" ...item,",
|
|
199
|
+
" data: normalizeKeys(item.data)",
|
|
200
|
+
"});",
|
|
201
|
+
""
|
|
202
|
+
];
|
|
203
|
+
for (const collection of collections) {
|
|
204
|
+
const pascal = toPascalCase(collection.slug);
|
|
205
|
+
if (collection.isSingleton) {
|
|
206
|
+
lines.push(`export async function get${pascal}(): Promise<${pascal}Item> {`);
|
|
207
|
+
lines.push(" const client = getApiClient();");
|
|
208
|
+
lines.push(` const res = await client.get<{ item: ${pascal}Item }>(\"/v2/content/${collection.slug}/singleton\");`);
|
|
209
|
+
lines.push(` return mapItem(res.item as ${pascal}Item);`);
|
|
210
|
+
lines.push("}");
|
|
211
|
+
lines.push("");
|
|
212
|
+
} else {
|
|
213
|
+
lines.push(`export async function get${pascal}Items(): Promise<${pascal}Item[]> {`);
|
|
214
|
+
lines.push(" const client = getApiClient();");
|
|
215
|
+
lines.push(` const res = await client.getContent({ schema: "${collection.slug}", status: "published" });`);
|
|
216
|
+
lines.push(` return (res.items as ${pascal}Item[]).map((item) => mapItem(item));`);
|
|
217
|
+
lines.push("}");
|
|
218
|
+
lines.push("");
|
|
219
|
+
lines.push(`export async function get${pascal}ItemBySlug(slug: string): Promise<${pascal}Item | null> {`);
|
|
220
|
+
lines.push(" try {");
|
|
221
|
+
lines.push(" const client = getApiClient();");
|
|
222
|
+
lines.push(` const item = await client.getContentBySlug("${collection.slug}", slug);`);
|
|
223
|
+
lines.push(` return mapItem(item as ${pascal}Item);`);
|
|
224
|
+
lines.push(" } catch {");
|
|
225
|
+
lines.push(" return null;");
|
|
226
|
+
lines.push(" }");
|
|
227
|
+
lines.push("}");
|
|
228
|
+
lines.push("");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return `${lines.join("\n").trim()}\n`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildGeneratedHooks(collections) {
|
|
235
|
+
const lines = [
|
|
236
|
+
"/** Auto-generated by `baysecms codegen`. Do not edit manually. */",
|
|
237
|
+
'import { useEffect, useMemo, useState } from "react";',
|
|
238
|
+
'import { createBayseCMSClient } from "baysecms/client";',
|
|
239
|
+
'import type {',
|
|
240
|
+
...collections.map((c) => ` ${toPascalCase(c.slug)}Item,`),
|
|
241
|
+
'} from "./types";',
|
|
242
|
+
"",
|
|
243
|
+
"type QueryState<T> = {",
|
|
244
|
+
" data: T | null;",
|
|
245
|
+
" isLoading: boolean;",
|
|
246
|
+
" error: Error | null;",
|
|
247
|
+
" isStale: boolean;",
|
|
248
|
+
"};",
|
|
249
|
+
"",
|
|
250
|
+
"const queryCache = new Map<string, { data: unknown; timestamp: number }>();",
|
|
251
|
+
"const cacheGcTimers = new Map<string, ReturnType<typeof setTimeout>>();",
|
|
252
|
+
"",
|
|
253
|
+
"const readEnv = (key: string): string => {",
|
|
254
|
+
" if (typeof process !== \"undefined\" && process?.env?.[key]) return String(process.env[key]);",
|
|
255
|
+
" const maybeRuntime = globalThis as { __BAYSECMS_RUNTIME__?: Record<string, string | undefined>; __VITE_ENV__?: Record<string, string | undefined> };",
|
|
256
|
+
" if (maybeRuntime.__BAYSECMS_RUNTIME__?.[key]) return String(maybeRuntime.__BAYSECMS_RUNTIME__[key]);",
|
|
257
|
+
" if (key === \"BAYSECMS_API_URL\" && maybeRuntime.__VITE_ENV__?.VITE_BAYSECMS_API_URL) return String(maybeRuntime.__VITE_ENV__.VITE_BAYSECMS_API_URL);",
|
|
258
|
+
" if (key === \"BAYSECMS_API_KEY\" && maybeRuntime.__VITE_ENV__?.VITE_BAYSECMS_API_KEY) return String(maybeRuntime.__VITE_ENV__.VITE_BAYSECMS_API_KEY);",
|
|
259
|
+
" return \"\";",
|
|
260
|
+
"};",
|
|
261
|
+
"function getApiClient() {",
|
|
262
|
+
" const apiUrl = readEnv(\"BAYSECMS_API_URL\") || \"http://localhost:4000\";",
|
|
263
|
+
" const apiKey = readEnv(\"BAYSECMS_API_KEY\") || \"\";",
|
|
264
|
+
" return createBayseCMSClient({",
|
|
265
|
+
" baseUrl: apiUrl,",
|
|
266
|
+
" apiKey: apiKey || undefined",
|
|
267
|
+
" });",
|
|
268
|
+
"}",
|
|
269
|
+
"",
|
|
270
|
+
"export function useQuery<T>(config: {",
|
|
271
|
+
" queryKey: Array<string | null | number>;",
|
|
272
|
+
" queryFn: () => Promise<T>;",
|
|
273
|
+
" staleTime?: number;",
|
|
274
|
+
" gcTime?: number;",
|
|
275
|
+
"}): QueryState<T> {",
|
|
276
|
+
" const cacheKey = useMemo(() => JSON.stringify(config.queryKey), [config.queryKey]);",
|
|
277
|
+
" const staleTime = config.staleTime ?? 5 * 60 * 1000;",
|
|
278
|
+
" const gcTime = config.gcTime ?? 30 * 60 * 1000;",
|
|
279
|
+
" const cached = queryCache.get(cacheKey);",
|
|
280
|
+
" const stale = !cached || Date.now() - cached.timestamp > staleTime;",
|
|
281
|
+
"",
|
|
282
|
+
" const [state, setState] = useState<QueryState<T>>({",
|
|
283
|
+
" data: cached ? (cached.data as T) : null,",
|
|
284
|
+
" isLoading: stale,",
|
|
285
|
+
" error: null,",
|
|
286
|
+
" isStale: stale",
|
|
287
|
+
" });",
|
|
288
|
+
"",
|
|
289
|
+
" useEffect(() => {",
|
|
290
|
+
" if (!stale && cached) {",
|
|
291
|
+
" setState({",
|
|
292
|
+
" data: cached.data as T,",
|
|
293
|
+
" isLoading: false,",
|
|
294
|
+
" error: null,",
|
|
295
|
+
" isStale: false",
|
|
296
|
+
" });",
|
|
297
|
+
" return;",
|
|
298
|
+
" }",
|
|
299
|
+
"",
|
|
300
|
+
" let isMounted = true;",
|
|
301
|
+
" setState((prev) => ({ ...prev, isLoading: true, error: null, isStale: true }));",
|
|
302
|
+
"",
|
|
303
|
+
" config",
|
|
304
|
+
" .queryFn()",
|
|
305
|
+
" .then((data) => {",
|
|
306
|
+
" if (!isMounted) return;",
|
|
307
|
+
" queryCache.set(cacheKey, { data, timestamp: Date.now() });",
|
|
308
|
+
" const prevTimer = cacheGcTimers.get(cacheKey);",
|
|
309
|
+
" if (prevTimer) clearTimeout(prevTimer);",
|
|
310
|
+
" cacheGcTimers.set(",
|
|
311
|
+
" cacheKey,",
|
|
312
|
+
" setTimeout(() => {",
|
|
313
|
+
" queryCache.delete(cacheKey);",
|
|
314
|
+
" cacheGcTimers.delete(cacheKey);",
|
|
315
|
+
" }, gcTime)",
|
|
316
|
+
" );",
|
|
317
|
+
" setState({ data, isLoading: false, error: null, isStale: false });",
|
|
318
|
+
" })",
|
|
319
|
+
" .catch((error) => {",
|
|
320
|
+
" if (!isMounted) return;",
|
|
321
|
+
" setState((prev) => ({",
|
|
322
|
+
" ...prev,",
|
|
323
|
+
" isLoading: false,",
|
|
324
|
+
" error: error instanceof Error ? error : new Error(String(error))",
|
|
325
|
+
" }));",
|
|
326
|
+
" });",
|
|
327
|
+
"",
|
|
328
|
+
" return () => {",
|
|
329
|
+
" isMounted = false;",
|
|
330
|
+
" };",
|
|
331
|
+
" }, [cacheKey, stale, gcTime]);",
|
|
332
|
+
"",
|
|
333
|
+
" return state;",
|
|
334
|
+
"}",
|
|
335
|
+
""
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
for (const collection of collections) {
|
|
339
|
+
const pascal = toPascalCase(collection.slug);
|
|
340
|
+
const camel = toSafeIdentifier(collection.slug);
|
|
341
|
+
if (collection.isSingleton) {
|
|
342
|
+
lines.push(`export const ${camel}Queries = {`);
|
|
343
|
+
lines.push(` all: () => ["${collection.slug}"] as const,`);
|
|
344
|
+
lines.push(` singleton: () => [...${camel}Queries.all(), "singleton"] as const`);
|
|
345
|
+
lines.push("};");
|
|
346
|
+
lines.push("");
|
|
347
|
+
lines.push(`export function use${pascal}() {`);
|
|
348
|
+
lines.push(` return useQuery<${pascal}Item>({`);
|
|
349
|
+
lines.push(` queryKey: ${camel}Queries.singleton(),`);
|
|
350
|
+
lines.push(" queryFn: async () => {");
|
|
351
|
+
lines.push(" const client = getApiClient();");
|
|
352
|
+
lines.push(` const res = await client.get<{ item: ${pascal}Item }>(\"/v2/content/${collection.slug}/singleton\");`);
|
|
353
|
+
lines.push(` return res.item as ${pascal}Item;`);
|
|
354
|
+
lines.push(" },");
|
|
355
|
+
lines.push(" staleTime: 60 * 60 * 1000");
|
|
356
|
+
lines.push(" });");
|
|
357
|
+
lines.push("}");
|
|
358
|
+
lines.push("");
|
|
359
|
+
} else {
|
|
360
|
+
lines.push(`export const ${camel}Queries = {`);
|
|
361
|
+
lines.push(` all: () => ["${collection.slug}"] as const,`);
|
|
362
|
+
lines.push(` list: () => [...${camel}Queries.all(), "list"] as const,`);
|
|
363
|
+
lines.push(` bySlug: (slug: string) => [...${camel}Queries.all(), "slug", slug] as const`);
|
|
364
|
+
lines.push("};");
|
|
365
|
+
lines.push("");
|
|
366
|
+
lines.push(`export function use${pascal}Items() {`);
|
|
367
|
+
lines.push(` return useQuery<${pascal}Item[]>({`);
|
|
368
|
+
lines.push(` queryKey: ${camel}Queries.list(),`);
|
|
369
|
+
lines.push(" queryFn: async () => {");
|
|
370
|
+
lines.push(" const client = getApiClient();");
|
|
371
|
+
lines.push(
|
|
372
|
+
` const res = await client.getContent({ schema: "${collection.slug}", status: "published" });`
|
|
373
|
+
);
|
|
374
|
+
lines.push(` return (res.items as ${pascal}Item[]) ?? [];`);
|
|
375
|
+
lines.push(" },");
|
|
376
|
+
lines.push(" staleTime: 5 * 60 * 1000");
|
|
377
|
+
lines.push(" });");
|
|
378
|
+
lines.push("}");
|
|
379
|
+
lines.push("");
|
|
380
|
+
lines.push(`export function use${pascal}ItemBySlug(slug: string | null) {`);
|
|
381
|
+
lines.push(` return useQuery<${pascal}Item>({`);
|
|
382
|
+
lines.push(` queryKey: ${camel}Queries.bySlug(slug ?? ""),`);
|
|
383
|
+
lines.push(" queryFn: async () => {");
|
|
384
|
+
lines.push(' if (!slug) throw new Error("Slug required");');
|
|
385
|
+
lines.push(" const client = getApiClient();");
|
|
386
|
+
lines.push(
|
|
387
|
+
` return (await client.getContentBySlug("${collection.slug}", slug)) as ${pascal}Item;`
|
|
388
|
+
);
|
|
389
|
+
lines.push(" }");
|
|
390
|
+
lines.push(" });");
|
|
391
|
+
lines.push("}");
|
|
392
|
+
lines.push("");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return `${lines.join("\n").trim()}\n`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function buildGeneratedIndex(collections) {
|
|
400
|
+
const exports = collections.flatMap((c) => {
|
|
401
|
+
const pascal = toPascalCase(c.slug);
|
|
402
|
+
if (c.isSingleton) return [`get${pascal}`];
|
|
403
|
+
return [`get${pascal}Items`, `get${pascal}ItemBySlug`];
|
|
404
|
+
});
|
|
405
|
+
const hookExports = collections.flatMap((c) => {
|
|
406
|
+
const pascal = toPascalCase(c.slug);
|
|
407
|
+
const camel = toSafeIdentifier(c.slug);
|
|
408
|
+
if (c.isSingleton) return [`${camel}Queries`, `use${pascal}`];
|
|
409
|
+
return [`${camel}Queries`, `use${pascal}Items`, `use${pascal}ItemBySlug`];
|
|
410
|
+
});
|
|
411
|
+
return `/** Auto-generated by \`baysecms codegen\`. */\nexport * from "./types";\nexport { ${exports.join(", ")} } from "./data";\nexport { useQuery, ${hookExports.join(", ")} } from "./hooks";\n`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function fetchPublishedCollections(apiUrl, hdrs, schemaFilter) {
|
|
415
|
+
const payload = await request(apiUrl, "/schemas", "GET", undefined, hdrs);
|
|
416
|
+
const rawItems = Array.isArray(payload?.items) ? payload.items : [];
|
|
417
|
+
const latestPublishedBySchema = new Map();
|
|
418
|
+
for (const item of rawItems) {
|
|
419
|
+
if (item?.status !== "published" || !item?.schemaId || typeof item?.version !== "number") continue;
|
|
420
|
+
const current = latestPublishedBySchema.get(item.schemaId);
|
|
421
|
+
if (!current || item.version > current.version) {
|
|
422
|
+
latestPublishedBySchema.set(item.schemaId, item);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const collections = [];
|
|
427
|
+
for (const schema of latestPublishedBySchema.values()) {
|
|
428
|
+
for (const collection of schema.collections ?? []) {
|
|
429
|
+
if (!collection?.slug) continue;
|
|
430
|
+
if (schemaFilter && collection.slug !== schemaFilter) continue;
|
|
431
|
+
collections.push({
|
|
432
|
+
schemaId: schema.schemaId,
|
|
433
|
+
slug: collection.slug,
|
|
434
|
+
name: collection.name || collection.slug,
|
|
435
|
+
fields: Array.isArray(collection.fields) ? collection.fields : [],
|
|
436
|
+
isSingleton: Boolean(collection.isSingleton)
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return collections;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function runCodegen(apiUrl, hdrs, outDir, schemaFilter) {
|
|
444
|
+
const collections = await fetchPublishedCollections(apiUrl, hdrs, schemaFilter);
|
|
445
|
+
if (!collections.length) {
|
|
446
|
+
console.error("No published collections found for codegen.");
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
await mkdir(outDir, { recursive: true });
|
|
450
|
+
await writeFile(join(outDir, "types.ts"), buildGeneratedTypes(collections), "utf-8");
|
|
451
|
+
await writeFile(join(outDir, "data.ts"), buildGeneratedData(collections), "utf-8");
|
|
452
|
+
await writeFile(join(outDir, "hooks.ts"), buildGeneratedHooks(collections), "utf-8");
|
|
453
|
+
await writeFile(join(outDir, "index.ts"), buildGeneratedIndex(collections), "utf-8");
|
|
454
|
+
console.log(`Generated ${collections.length} collection helper(s) in ${outDir}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function request(apiUrl, path, method, body, hdrs) {
|
|
458
|
+
const url = `${apiUrl.replace(/\/$/, "")}${path}`;
|
|
459
|
+
const res = await fetch(url, {
|
|
460
|
+
method,
|
|
461
|
+
headers: hdrs,
|
|
462
|
+
body: body ? JSON.stringify(body) : undefined
|
|
463
|
+
});
|
|
464
|
+
const text = await res.text();
|
|
465
|
+
let data;
|
|
466
|
+
try {
|
|
467
|
+
data = JSON.parse(text);
|
|
468
|
+
} catch {
|
|
469
|
+
data = text;
|
|
470
|
+
}
|
|
471
|
+
if (!res.ok) {
|
|
472
|
+
console.error(`HTTP ${res.status}`, data);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
return data;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function main() {
|
|
479
|
+
const argv = process.argv.slice(2);
|
|
480
|
+
if (
|
|
481
|
+
argv.length === 0 ||
|
|
482
|
+
argv[0] === "--help" ||
|
|
483
|
+
argv[0] === "-h" ||
|
|
484
|
+
argv.includes("--help") ||
|
|
485
|
+
argv.includes("-h")
|
|
486
|
+
) {
|
|
487
|
+
console.log(`
|
|
488
|
+
BayseCMS CLI
|
|
489
|
+
|
|
490
|
+
baysecms schema push [options]
|
|
491
|
+
baysecms codegen [options]
|
|
492
|
+
|
|
493
|
+
Options:
|
|
494
|
+
--config <path> Default: ./baysecms.config.json
|
|
495
|
+
--file <path> Schema JSON file (overrides config.schemaFile)
|
|
496
|
+
--review After draft, POST .../review
|
|
497
|
+
--publish After review, POST .../publish (implies --review)
|
|
498
|
+
--out <path> For codegen. Default: ./src/lib/baysecms
|
|
499
|
+
--schema <slug> For codegen. Generate only one collection slug
|
|
500
|
+
--no-codegen With schema push --publish, skip auto helper generation
|
|
501
|
+
|
|
502
|
+
Env:
|
|
503
|
+
BAYSECMS_API_URL Override config.apiUrl
|
|
504
|
+
BAYSECMS_API_KEY API key auth (recommended for codegen)
|
|
505
|
+
BAYSECMS_TOKEN Bearer JWT alternative
|
|
506
|
+
BAYSECMS_TENANT_ID Optional; Cognito custom claim usually covers this
|
|
507
|
+
BAYSECMS_WORKSPACE_ID
|
|
508
|
+
`);
|
|
509
|
+
process.exit(0);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const parsed = parseArgs(argv);
|
|
513
|
+
const [cmd, sub] = parsed._;
|
|
514
|
+
|
|
515
|
+
const configPath = resolve(cwd(), parsed.flags.config || DEFAULT_CONFIG);
|
|
516
|
+
let cfg;
|
|
517
|
+
try {
|
|
518
|
+
cfg = await loadConfig(configPath);
|
|
519
|
+
} catch (e) {
|
|
520
|
+
console.error(`Cannot read config: ${configPath}`, e.message);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const apiUrl = process.env.BAYSECMS_API_URL || cfg.apiUrl;
|
|
525
|
+
if (!apiUrl) {
|
|
526
|
+
console.error("Missing apiUrl in config or BAYSECMS_API_URL");
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const hdrs = headersFromConfig(cfg);
|
|
531
|
+
|
|
532
|
+
if (cmd === "schema" && sub === "push") {
|
|
533
|
+
const schemaPath = resolve(cwd(), parsed.flags.file || cfg.schemaFile || "baysecms.schema.json");
|
|
534
|
+
let schemaBody;
|
|
535
|
+
try {
|
|
536
|
+
schemaBody = JSON.parse(await readFile(schemaPath, "utf-8"));
|
|
537
|
+
} catch (e) {
|
|
538
|
+
console.error(`Cannot read schema file: ${schemaPath}`, e.message);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log(`Pushing schema draft to ${apiUrl} ...`);
|
|
543
|
+
await request(apiUrl, "/schemas/draft", "POST", schemaBody, hdrs);
|
|
544
|
+
console.log("OK: draft created");
|
|
545
|
+
|
|
546
|
+
const schemaId = schemaBody.schemaId;
|
|
547
|
+
const version = schemaBody.version;
|
|
548
|
+
if (!schemaId || !version) {
|
|
549
|
+
console.log("Done (no schemaId/version in file for follow-up steps).");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (parsed.flags.publish) {
|
|
554
|
+
parsed.flags.review = true;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (parsed.flags.review) {
|
|
558
|
+
console.log(`Submitting review: ${schemaId} v${version} ...`);
|
|
559
|
+
await request(apiUrl, `/schemas/${encodeURIComponent(schemaId)}/${version}/review`, "POST", undefined, hdrs);
|
|
560
|
+
console.log("OK: in review");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (parsed.flags.publish) {
|
|
564
|
+
console.log(`Publishing: ${schemaId} v${version} ...`);
|
|
565
|
+
await request(apiUrl, `/schemas/${encodeURIComponent(schemaId)}/${version}/publish`, "POST", undefined, hdrs);
|
|
566
|
+
console.log("OK: published");
|
|
567
|
+
if (!parsed.flags["no-codegen"]) {
|
|
568
|
+
const outDir = resolve(cwd(), parsed.flags.out || cfg.codegenOut || "src/lib/baysecms");
|
|
569
|
+
console.log(`Running codegen to ${outDir} ...`);
|
|
570
|
+
await runCodegen(apiUrl, hdrs, outDir, undefined);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (cmd === "codegen") {
|
|
577
|
+
const outDir = resolve(cwd(), parsed.flags.out || "src/lib/baysecms");
|
|
578
|
+
const schemaFilter = typeof parsed.flags.schema === "string" ? parsed.flags.schema.trim() : "";
|
|
579
|
+
await runCodegen(apiUrl, hdrs, outDir, schemaFilter);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
console.error("Unknown command. Use: baysecms schema push OR baysecms codegen");
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
main().catch((err) => {
|
|
588
|
+
console.error(err);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "baysecms-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for BayseCMS schema push and code generation workflows",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"baysecms": "./cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cli.mjs",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
}
|
|
17
|
+
}
|