@dyrected/cli 0.0.1 → 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/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "@dyrected/cli",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dyrected": "./dist/index.js"
7
7
  },
8
8
  "dependencies": {
9
+ "@types/prompts": "^2.4.9",
9
10
  "chalk": "^5.3.0",
10
11
  "commander": "^12.1.0",
11
12
  "fs-extra": "^11.2.0",
12
- "prettier": "^3.2.5"
13
+ "jiti": "^2.7.0",
14
+ "prettier": "^3.2.5",
15
+ "prompts": "^2.4.2",
16
+ "@dyrected/core": "^1.0.0"
13
17
  },
14
18
  "devDependencies": {
15
19
  "@types/fs-extra": "^11.0.4",
package/src/index.ts CHANGED
@@ -1,38 +1,427 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import prettier from 'prettier';
4
- import chalk from 'chalk';
5
- import fs from 'fs-extra';
6
- import path from 'path';
2
+ import { Command } from "commander";
3
+ import prettier from "prettier";
4
+ import chalk from "chalk";
5
+ import fs from "fs-extra";
6
+ import path from "path";
7
+ import prompts from "prompts";
8
+ import { execSync } from "child_process";
9
+ import { createJiti } from "jiti";
10
+ import { generateAIPrompt } from "@dyrected/core";
7
11
 
8
12
  const program = new Command();
9
13
 
14
+ program.name("dyrected").description("Dyrected CMS CLI tool").version("0.0.1");
15
+
16
+ // ─── init ──────────────────────────────────────────────────────────────────
17
+
10
18
  program
11
- .name('dyrected')
12
- .description('Dyrected CMS CLI tool')
13
- .version('0.0.1');
19
+ .command("init")
20
+ .description("Bootstrap a new Dyrected CMS project")
21
+ .action(async () => {
22
+ console.log(chalk.bold("\n🚀 Welcome to Dyrected CMS\n"));
23
+
24
+ const { framework } = await prompts({
25
+ type: "select",
26
+ name: "framework",
27
+ message: "Which framework are you using?",
28
+ choices: [
29
+ { title: "Next.js (App Router)", value: "next" },
30
+ { title: "Nuxt 3", value: "nuxt" },
31
+ ],
32
+ });
33
+
34
+ if (!framework) {
35
+ console.log(chalk.yellow("\nAborted."));
36
+ process.exit(0);
37
+ }
38
+
39
+ const { quickSetup } = await prompts({
40
+ type: "confirm",
41
+ name: "quickSetup",
42
+ message: "Use Quick Setup (SQLite + Local Storage)?",
43
+ initial: true,
44
+ });
45
+
46
+ const { adminPath } = await prompts({
47
+ type: "text",
48
+ name: "adminPath",
49
+ message: "What path should the admin dashboard use?",
50
+ initial: "cms",
51
+ format: (val) => val.replace(/^\//, "").replace(/\/$/, ""),
52
+ });
53
+
54
+ let db = "sqlite";
55
+ let storage = "local";
56
+
57
+ if (!quickSetup) {
58
+ const dbResponse = await prompts({
59
+ type: "select",
60
+ name: "value",
61
+ message: "Which database adapter?",
62
+ choices: [
63
+ { title: "PostgreSQL (recommended)", value: "postgres" },
64
+ { title: "SQLite (local dev)", value: "sqlite" },
65
+ { title: "MongoDB", value: "mongodb" },
66
+ ],
67
+ });
68
+ db = dbResponse.value;
69
+
70
+ const storageResponse = await prompts({
71
+ type: "select",
72
+ name: "value",
73
+ message: "Which storage adapter?",
74
+ choices: [
75
+ { title: "Local filesystem", value: "local" },
76
+ { title: "AWS S3", value: "s3" },
77
+ { title: "Backblaze B2", value: "b2" },
78
+ { title: "Cloudinary", value: "cloudinary" },
79
+ ],
80
+ });
81
+ storage = storageResponse.value;
82
+ }
83
+
84
+ const cwd = process.cwd();
85
+ const packageManager = detectPackageManager(cwd);
86
+
87
+ // ── 1. Install dependencies ──────────────────────────────────────────
88
+ const frameworkPkg = framework === "next" ? "@dyrected/next" : "@dyrected/nuxt";
89
+ const dbPkg = `@dyrected/db-${db}`;
90
+ const storagePkg = `@dyrected/storage-${storage}`;
91
+ const deps = [frameworkPkg, dbPkg, storagePkg].join(" ");
92
+
93
+ console.log(chalk.blue(`\nInstalling ${deps}...`));
94
+ try {
95
+ execSync(`${packageManager} add ${deps}`, { cwd, stdio: "inherit" });
96
+ } catch {
97
+ console.log(chalk.yellow("\nCould not auto-install. Run the following manually:"));
98
+ console.log(chalk.cyan(` ${packageManager} add ${deps}\n`));
99
+ }
100
+
101
+ // ── 2. Write dyrected.config.ts ──────────────────────────────────────
102
+ const dbImport = `import { ${db}Adapter } from '${dbPkg}'`;
103
+ const storageImport = `import { ${storage}Adapter } from '${storagePkg}'`;
104
+ const dbConfig = buildDbConfig(db);
105
+ const storageConfig = buildStorageConfig(storage);
106
+
107
+ const configContent = `import { defineCollection, defineGlobal, defineConfig } from '@dyrected/core'
108
+ ${dbImport}
109
+ ${storageImport}
110
+
111
+ // ── Collections ──────────────────────────────────────────────────────────
112
+
113
+ const media = defineCollection({
114
+ slug: 'media',
115
+ labels: { singular: 'Media', plural: 'Media' },
116
+ upload: true,
117
+ fields: [
118
+ { name: 'alt', type: 'text' },
119
+ ],
120
+ })
121
+
122
+ const pages = defineCollection({
123
+ slug: 'pages',
124
+ labels: { singular: 'Page', plural: 'Pages' },
125
+ fields: [
126
+ { name: 'title', type: 'text', required: true },
127
+ { name: 'slug', type: 'text', required: true },
128
+ { name: 'content', type: 'richText' },
129
+ { name: 'featuredImage', type: 'relationship', relationTo: 'media' },
130
+ ],
131
+ })
132
+
133
+ const posts = defineCollection({
134
+ slug: 'posts',
135
+ labels: { singular: 'Post', plural: 'Posts' },
136
+ fields: [
137
+ { name: 'title', type: 'text', required: true },
138
+ { name: 'content', type: 'richText' },
139
+ { name: 'featuredImage', type: 'relationship', relationTo: 'media' },
140
+ ],
141
+ })
142
+
143
+ // ── Globals ───────────────────────────────────────────────────────────────
144
+
145
+ const navigation = defineGlobal({
146
+ slug: 'navigation',
147
+ label: 'Navigation',
148
+ fields: [
149
+ {
150
+ name: 'menuItems',
151
+ type: 'array',
152
+ fields: [
153
+ { name: 'label', type: 'text' },
154
+ { name: 'link', type: 'relationship', relationTo: 'pages' },
155
+ ],
156
+ },
157
+ ],
158
+ })
159
+
160
+ const settings = defineGlobal({
161
+ slug: 'settings',
162
+ label: 'Site Settings',
163
+ fields: [
164
+ { name: 'siteName', type: 'text' },
165
+ { name: 'logo', type: 'relationship', relationTo: 'media' },
166
+ { name: 'footerText', type: 'textarea' },
167
+ ],
168
+ })
169
+
170
+ // ── Config ────────────────────────────────────────────────────────────────
171
+
172
+ export default defineConfig({
173
+ collections: [media, pages, posts],
174
+ globals: [navigation, settings],
175
+ db: ${dbConfig},
176
+ storage: ${storageConfig},
177
+ })
178
+ `;
179
+
180
+ const configPath = path.join(cwd, "dyrected.config.ts");
181
+ if (await fs.pathExists(configPath)) {
182
+ const { overwrite } = await prompts({
183
+ type: "confirm",
184
+ name: "overwrite",
185
+ message: "dyrected.config.ts already exists. Overwrite?",
186
+ initial: false,
187
+ });
188
+ if (!overwrite) {
189
+ console.log(chalk.yellow("Skipping config file."));
190
+ } else {
191
+ await fs.outputFile(configPath, configContent);
192
+ console.log(chalk.green("✔ dyrected.config.ts written"));
193
+ }
194
+ } else {
195
+ await fs.outputFile(configPath, configContent);
196
+ console.log(chalk.green("✔ dyrected.config.ts written"));
197
+ }
198
+
199
+ // ── 3. Framework-specific files ──────────────────────────────────────
200
+ if (framework === "next") {
201
+ await writeNextFiles(cwd, adminPath);
202
+ } else {
203
+ await writeNuxtFiles(cwd, adminPath);
204
+ }
205
+
206
+ // ── 4. .env setup ───────────────────────────────────────────────────
207
+ const envContent = buildEnvTemplate(db, storage, framework);
208
+ const envExamplePath = path.join(cwd, ".env.example");
209
+ await fs.outputFile(envExamplePath, envContent);
210
+ console.log(chalk.green("✔ .env.example written"));
211
+
212
+ const envPath = path.join(cwd, ".env");
213
+ if (await fs.pathExists(envPath)) {
214
+ const existingEnv = await fs.readFile(envPath, "utf-8");
215
+ const missingVars = envContent
216
+ .split("\n")
217
+ .filter((line) => {
218
+ if (!line || line.startsWith("#")) return false;
219
+ const key = line.split("=")[0];
220
+ return !existingEnv.includes(`${key}=`);
221
+ })
222
+ .join("\n");
223
+
224
+ if (missingVars) {
225
+ const { appendEnv } = await prompts({
226
+ type: "confirm",
227
+ name: "appendEnv",
228
+ message: ".env already exists. Append missing Dyrected variables?",
229
+ initial: true,
230
+ });
231
+
232
+ if (appendEnv) {
233
+ await fs.appendFile(envPath, `\n# ── Dyrected CMS ──────────────────────────────────────────────────\n${missingVars}`);
234
+ console.log(chalk.green("✔ .env file updated with missing variables"));
235
+ }
236
+ } else {
237
+ console.log(chalk.dim("ℹ .env already contains all required variables."));
238
+ }
239
+ } else {
240
+ const { createEnv } = await prompts({
241
+ type: "confirm",
242
+ name: "createEnv",
243
+ message: ".env file is missing. Create it now?",
244
+ initial: true,
245
+ });
246
+
247
+ if (createEnv) {
248
+ await fs.outputFile(envPath, envContent);
249
+ console.log(chalk.green("✔ .env file created"));
250
+ }
251
+ }
252
+
253
+ // ── Done ─────────────────────────────────────────────────────────────
254
+ console.log(chalk.bold.green("\n✅ Dyrected is ready!\n"));
255
+ console.log(chalk.cyan(` 1. Configure your environment variables in .env`));
256
+ console.log(chalk.cyan(` 2. Open http://localhost:3000/${adminPath} to start managing content.`));
257
+ console.log(chalk.cyan(" 3. Run: npx dyrected generate:types\n"));
258
+
259
+ console.log(chalk.bold.magenta("🤖 AI INTEGRATION PROMPT"));
260
+ console.log(chalk.dim("Copy this prompt and give it to your AI tool (Claude, GPT, etc.) to scaffold your CMS logic:\n"));
261
+
262
+ const promptText = generateAIPrompt(framework as any, {
263
+ baseUrl: "http://localhost:3000",
264
+ isSelfHosted: true,
265
+ });
266
+
267
+ console.log(chalk.white(promptText));
268
+ console.log("\n" + chalk.dim("━".repeat(50)) + "\n");
269
+ });
270
+
271
+ // ── Helpers ────────────────────────────────────────────────────────────────
272
+
273
+ function detectPackageManager(cwd: string): string {
274
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
275
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
276
+ return "npm";
277
+ }
278
+
279
+ function buildDbConfig(db: string): string {
280
+ switch (db) {
281
+ case "postgres":
282
+ return `postgresAdapter({ url: process.env.DATABASE_URL! })`;
283
+ case "sqlite":
284
+ return `sqliteAdapter({ filename: './data.db' })`;
285
+ case "mongodb":
286
+ return `mongodbAdapter({ url: process.env.DATABASE_URL! })`;
287
+ default:
288
+ return `postgresAdapter({ url: process.env.DATABASE_URL! })`;
289
+ }
290
+ }
291
+
292
+ function buildStorageConfig(storage: string): string {
293
+ switch (storage) {
294
+ case "local":
295
+ return `localAdapter({ directory: './public/uploads', serveFrom: '/uploads' })`;
296
+ case "s3":
297
+ return `s3Adapter({ bucket: process.env.S3_BUCKET!, region: process.env.S3_REGION!, accessKeyId: process.env.S3_ACCESS_KEY_ID!, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY! })`;
298
+ case "b2":
299
+ return `b2Adapter({ bucketId: process.env.B2_BUCKET_ID!, keyId: process.env.B2_KEY_ID!, applicationKey: process.env.B2_APPLICATION_KEY! })`;
300
+ case "cloudinary":
301
+ return `cloudinaryAdapter({ cloudName: process.env.CLOUDINARY_CLOUD_NAME!, apiKey: process.env.CLOUDINARY_API_KEY!, apiSecret: process.env.CLOUDINARY_API_SECRET! })`;
302
+ default:
303
+ return `localAdapter({ directory: './public/uploads', serveFrom: '/uploads' })`;
304
+ }
305
+ }
306
+
307
+ async function writeNextFiles(cwd: string, adminPath: string) {
308
+ const apiRoutePath = path.join(cwd, "app/dyrected/[...route]/route.ts");
309
+ if (!(await fs.pathExists(apiRoutePath))) {
310
+ await fs.outputFile(apiRoutePath, `export { GET, POST, PUT, PATCH, DELETE } from '@dyrected/next'\n`);
311
+ console.log(chalk.green("✔ app/dyrected/[...route]/route.ts written"));
312
+ }
313
+
314
+ const adminPagePath = path.join(cwd, `app/${adminPath}/[[...route]]/page.tsx`);
315
+ if (!(await fs.pathExists(adminPagePath))) {
316
+ await fs.outputFile(
317
+ adminPagePath,
318
+ `import { DyrectedAdmin } from '@dyrected/next/admin'
319
+
320
+ export default function AdminPage() {
321
+ return <DyrectedAdmin apiPath="/dyrected" />
322
+ }
323
+ `,
324
+ );
325
+ console.log(chalk.green(`✔ app/${adminPath}/[[...route]]/page.tsx written`));
326
+ }
327
+ }
328
+
329
+ async function writeNuxtFiles(cwd: string, adminPath: string) {
330
+ console.log(chalk.yellow("\n⚠ Add the module to your nuxt.config.ts manually:"));
331
+ console.log(
332
+ chalk.dim(`
333
+ modules: ['@dyrected/nuxt'],
334
+ runtimeConfig: {
335
+ public: { dyrectedApiKey: process.env.NUXT_PUBLIC_DYRECTED_API_KEY }
336
+ }
337
+ `),
338
+ );
339
+
340
+ const adminPagePath = path.join(cwd, `pages/${adminPath}/[[...route]].vue`);
341
+ if (!(await fs.pathExists(adminPagePath))) {
342
+ await fs.outputFile(
343
+ adminPagePath,
344
+ `<template>
345
+ <DyrectedAdmin api-path="/dyrected" />
346
+ </template>
347
+
348
+ <script setup lang="ts">
349
+ import { DyrectedAdmin } from '@dyrected/nuxt/admin'
350
+ </script>
351
+ `,
352
+ );
353
+ console.log(chalk.green(`✔ pages/${adminPath}/[[...route]].vue written`));
354
+ }
355
+ }
356
+
357
+ function buildEnvTemplate(db: string, storage: string, framework: string): string {
358
+ const lines = [
359
+ `# Dyrected CMS — Environment Variables`,
360
+ `DATABASE_URL=${db === "mongodb" ? "mongodb://localhost:27017/dyrected" : "postgres://user:pass@localhost:5432/dyrected"}`,
361
+ `JWT_SECRET=change-me-32-characters-minimum`,
362
+ `ENCRYPTION_KEY=change-me-aes-256-key`,
363
+ ``,
364
+ ];
365
+
366
+ if (storage === "s3") {
367
+ lines.push(`S3_BUCKET=my-bucket`, `S3_REGION=us-east-1`, `S3_ACCESS_KEY_ID=`, `S3_SECRET_ACCESS_KEY=`);
368
+ } else if (storage === "b2") {
369
+ lines.push(`B2_BUCKET_ID=`, `B2_KEY_ID=`, `B2_APPLICATION_KEY=`);
370
+ } else if (storage === "cloudinary") {
371
+ lines.push(`CLOUDINARY_CLOUD_NAME=`, `CLOUDINARY_API_KEY=`, `CLOUDINARY_API_SECRET=`);
372
+ }
373
+
374
+ lines.push(
375
+ ``,
376
+ framework === "next" ? `NEXT_PUBLIC_DYRECTED_API_KEY=local-dev` : `NUXT_PUBLIC_DYRECTED_API_KEY=local-dev`,
377
+ );
378
+ return lines.join("\n") + "\n";
379
+ }
380
+
381
+ // ─── generate:types ─────────────────────────────────────────────────────────
14
382
 
15
383
  program
16
- .command('generate:types')
17
- .description('Generate TypeScript interfaces from your Dyrected schema')
18
- .option('-u, --url <url>', 'Base URL of your Dyrected API', 'http://localhost:3000')
19
- .option('-o, --output <path>', 'Output file path', './dyrected-types.ts')
384
+ .command("generate:types")
385
+ .description("Generate TypeScript interfaces from your Dyrected schema")
386
+ .option("-u, --url <url>", "Base URL of your Dyrected API (Cloud or self-hosted)")
387
+ .option("-c, --config <path>", "Path to your dyrected.config.ts (Self-hosted)", "./dyrected.config.ts")
388
+ .option("-o, --output <path>", "Output file path", "./dyrected-types.ts")
20
389
  .action(async (options) => {
21
390
  try {
22
- console.log(chalk.blue(`Fetching schemas from ${options.url}/api/schemas...`));
23
-
24
- const response = await fetch(`${options.url}/api/schemas`);
25
- if (!response.ok) {
26
- throw new Error(`Failed to fetch schemas: ${response.statusText}`);
391
+ let schema: any;
392
+ if (options.url) {
393
+ console.log(chalk.blue(`Fetching schemas from ${options.url}/api/schemas...`));
394
+ const response = await fetch(`${options.url}/api/schemas`);
395
+ if (!response.ok) {
396
+ throw new Error(`Failed to fetch schemas: ${response.statusText}`);
397
+ }
398
+ schema = await response.json();
399
+ } else {
400
+ const configPath = path.resolve(process.cwd(), options.config);
401
+ if (await fs.pathExists(configPath)) {
402
+ console.log(chalk.blue(`Generating types from local config: ${configPath}...`));
403
+ const jiti = createJiti(configPath);
404
+ const configModule = (await jiti.import(configPath)) as any;
405
+ schema = configModule.default || configModule;
406
+ } else {
407
+ // Fallback to default localhost if no config found
408
+ const url = "http://localhost:3000";
409
+ console.log(chalk.blue(`No local config found. Fetching schemas from ${url}/api/schemas...`));
410
+ const response = await fetch(`${url}/api/schemas`);
411
+ if (!response.ok) {
412
+ throw new Error(`Failed to fetch schemas: ${response.statusText}`);
413
+ }
414
+ schema = await response.json();
415
+ }
416
+ }
417
+
418
+ if (!schema || !schema.collections) {
419
+ throw new Error("Invalid schema: collections missing.");
27
420
  }
28
-
29
- const schema = await response.json();
30
421
  const code = generateTypes(schema);
31
-
32
- const formattedCode = await prettier.format(code, { parser: 'typescript' });
33
-
422
+ const formattedCode = await prettier.format(code, { parser: "typescript" });
34
423
  await fs.outputFile(path.resolve(process.cwd(), options.output), formattedCode);
35
-
424
+
36
425
  console.log(chalk.green(`\nSuccess! Types generated at ${options.output}`));
37
426
  } catch (error: any) {
38
427
  console.error(chalk.red(`\nError: ${error.message}`));
@@ -59,34 +448,58 @@ export interface Media {
59
448
  }
60
449
  \n`;
61
450
 
62
- // Collections
63
451
  for (const col of schema.collections) {
452
+ if (!col || !col.slug) continue;
64
453
  const interfaceName = toPascalCase(col.slug);
454
+
455
+ // Skip if it's the base Media interface and we already have it
456
+ if (interfaceName === "Media") {
457
+ // We could merge fields here if we want, but usually base Media is enough
458
+ // For now, let's just add the custom fields to a separate interface or merge
459
+ code = code.replace("export interface Media {", `export interface MediaBase {`);
460
+ code = code.replace("export interface MediaBase {", `export interface Media {`);
461
+ }
462
+
463
+ const existingInterface = code.includes(`export interface ${interfaceName} {`);
464
+ if (existingInterface && interfaceName !== "Media") continue;
465
+
466
+ if (interfaceName === "Media") {
467
+ // If it's Media, we just want to add the extra fields to the existing interface
468
+ const extraFields = col.fields.filter(
469
+ (f: any) =>
470
+ !["filename", "filesize", "mimeType", "url", "width", "height", "createdAt", "updatedAt"].includes(f.name),
471
+ );
472
+ if (extraFields.length > 0) {
473
+ const insertionPoint = code.indexOf("}", code.indexOf("export interface Media {"));
474
+ const fieldsCode = extraFields
475
+ .map((f: any) => ` ${f.name}${f.required ? "" : "?"}: ${mapFieldType(f)};`)
476
+ .join("\n");
477
+ code = code.slice(0, insertionPoint) + fieldsCode + "\n" + code.slice(insertionPoint);
478
+ }
479
+ continue;
480
+ }
481
+
65
482
  code += `export interface ${interfaceName} {\n`;
66
483
  code += ` id: string;\n`;
67
-
68
484
  for (const field of col.fields) {
69
- code += ` ${field.name}${field.required ? '' : '?'}: ${mapFieldType(field)};\n`;
485
+ if (field.name === "createdAt" || field.name === "updatedAt" || field.name === "id") continue;
486
+ code += ` ${field.name}${field.required ? "" : "?"}: ${mapFieldType(field)};\n`;
70
487
  }
71
-
72
488
  code += ` createdAt: string;\n`;
73
489
  code += ` updatedAt: string;\n`;
74
490
  code += `}\n\n`;
75
491
  }
76
492
 
77
- // Globals
78
493
  for (const glb of schema.globals) {
494
+ if (!glb || !glb.slug) continue;
79
495
  const interfaceName = `${toPascalCase(glb.slug)}Global`;
80
496
  code += `export interface ${interfaceName} {\n`;
81
-
82
497
  for (const field of glb.fields) {
83
- code += ` ${field.name}${field.required ? '' : '?'}: ${mapFieldType(field)};\n`;
498
+ code += ` ${field.name}${field.required ? "" : "?"}: ${mapFieldType(field)};\n`;
84
499
  }
85
-
86
500
  code += `}\n\n`;
87
501
  }
88
502
 
89
- // Registry
90
503
  code += `export interface DyrectedSchema {\n`;
91
504
  code += ` collections: {\n`;
92
505
  for (const col of schema.collections) {
@@ -105,32 +518,120 @@ export interface Media {
105
518
 
106
519
  function mapFieldType(field: any): string {
107
520
  switch (field.type) {
108
- case 'text':
109
- case 'textarea':
110
- case 'richText':
111
- case 'date':
112
- case 'email':
113
- case 'url':
114
- return 'string';
115
- case 'number':
116
- return 'number';
117
- case 'boolean':
118
- return 'boolean';
119
- case 'select':
120
- return field.options ? field.options.map((o: any) => `'${typeof o === 'string' ? o : o.value}'`).join(' | ') : 'string';
121
- case 'relationship':
122
- return field.collection === 'media' ? 'Media | string' : `${toPascalCase(field.collection)} | string`;
123
- case 'array':
124
- return 'any[]'; // Deep nesting handled later
125
- case 'object':
126
- return 'any';
521
+ case "text":
522
+ case "textarea":
523
+ case "richText":
524
+ case "date":
525
+ case "email":
526
+ case "url":
527
+ return "string";
528
+ case "number":
529
+ return "number";
530
+ case "boolean":
531
+ return "boolean";
532
+ case "select":
533
+ return field.options
534
+ ? field.options.map((o: any) => `'${typeof o === "string" ? o : o.value}'`).join(" | ")
535
+ : "string";
536
+ case "relationship": {
537
+ const relationTo = field.relationTo || field.collection;
538
+ return relationTo === "media" ? "Media | string" : `${toPascalCase(relationTo)} | string`;
539
+ }
540
+ case "array":
541
+ if (field.fields) {
542
+ return `Array<{\n${field.fields.map((f: any) => ` ${f.name}${f.required ? "" : "?"}: ${mapFieldType(f)};`).join("\n")}\n }>`;
543
+ }
544
+ return "any[]";
545
+ case "blocks":
546
+ if (field.blocks) {
547
+ return `Array<${field.blocks.map((b: any) => `{\n blockType: '${b.slug}';\n${b.fields.map((f: any) => ` ${f.name}${f.required ? "" : "?"}: ${mapFieldType(f)};`).join("\n")}\n }`).join(" | ")}>`;
548
+ }
549
+ return "any[]";
550
+ case "object":
551
+ if (field.fields) {
552
+ return `{\n${field.fields.map((f: any) => ` ${f.name}${f.required ? "" : "?"}: ${mapFieldType(f)};`).join("\n")}\n }`;
553
+ }
554
+ return "any";
127
555
  default:
128
- return 'any';
556
+ return "any";
129
557
  }
130
558
  }
131
559
 
132
560
  function toPascalCase(str: string) {
133
- return str.replace(/(^\w|-\w)/g, (m) => m.replace(/-/, '').toUpperCase());
561
+ if (!str) return "Unknown";
562
+ return str.replace(/(^\w|-\w)/g, (m) => m.replace(/-/, "").toUpperCase());
134
563
  }
135
564
 
565
+ // ─── sync:schema ─────────────────────────────────────────────────────────────
566
+
567
+ program
568
+ .command("sync:schema")
569
+ .description("Sync your local Dyrected schema with the Cloud dashboard")
570
+ .option("-k, --api-key <key>", "Your Dyrected API Key")
571
+ .option("-s, --site-id <id>", "Your Dyrected Site ID")
572
+ .option("-u, --url <url>", "Cloud API URL", "https://prodeegi-vault.onrender.com")
573
+ .option("-c, --config <path>", "Path to your dyrected.config.ts", "./dyrected.config.ts")
574
+ .option("--skip-on-error", "Do not exit with error if sync fails (useful for local builds)")
575
+ .action(async (options) => {
576
+ try {
577
+ const apiKey = options.apiKey || process.env.DYRECTED_API_KEY;
578
+ const siteId = options.siteId || process.env.DYRECTED_SITE_ID;
579
+ const apiUrl = options.url || process.env.DYRECTED_URL || "https://prodeegi-vault.onrender.com";
580
+ const configPath = path.resolve(process.cwd(), options.config);
581
+
582
+ const jiti = createJiti(configPath);
583
+
584
+ if (!apiKey || !siteId) {
585
+ console.warn(
586
+ chalk.yellow(
587
+ "\n⚠ Skipping schema sync: API Key or Site ID missing. (Required for Cloud sync, but optional for self-hosted builds)\n",
588
+ ),
589
+ );
590
+ return;
591
+ }
592
+
593
+ if (!(await fs.pathExists(configPath))) {
594
+ throw new Error(`Config file not found at ${configPath}`);
595
+ }
596
+
597
+ console.log(chalk.blue(`Loading config from ${configPath}...`));
598
+ const configModule = (await jiti.import(configPath)) as any;
599
+ const config = configModule.default || configModule;
600
+
601
+ if (!config.collections) {
602
+ throw new Error("Invalid config: No collections found.");
603
+ }
604
+
605
+ console.log(chalk.blue(`Syncing schema to ${apiUrl}...`));
606
+
607
+ const response = await fetch(`${apiUrl}/cloud/workspaces/sites/${siteId}/schema/sync`, {
608
+ method: "POST",
609
+ headers: {
610
+ "Content-Type": "application/json",
611
+ Authorization: `Bearer ${apiKey}`,
612
+ "X-API-Key": apiKey,
613
+ },
614
+ body: JSON.stringify({
615
+ collections: config.collections,
616
+ globals: config.globals || [],
617
+ admin: config.admin || {},
618
+ }),
619
+ });
620
+
621
+ if (!response.ok) {
622
+ const error = await response.json().catch(() => ({ message: response.statusText }));
623
+ throw new Error(`Sync failed: ${error.message || response.statusText}`);
624
+ }
625
+
626
+ console.log(chalk.green(`\nSuccess! Schema synced successfully for site ${siteId}`));
627
+ } catch (error: any) {
628
+ if (options.skipOnError) {
629
+ console.warn(chalk.yellow(`\n⚠ Sync failed, but skipping error as requested: ${error.message}`));
630
+ return;
631
+ }
632
+ console.error(chalk.red(`\nError: ${error.message}`));
633
+ process.exit(1);
634
+ }
635
+ });
636
+
136
637
  program.parse();