@dyrected/core 0.0.1 → 1.0.1

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/dist/index.js CHANGED
@@ -1,374 +1,308 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
1
+ import {
2
+ normalizeConfig
3
+ } from "./chunk-GM4WW6IE.js";
19
4
 
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- MediaService: () => MediaService,
24
- PopulationService: () => PopulationService,
25
- createDyrectedApp: () => createDyrectedApp,
26
- defineCollection: () => defineCollection,
27
- defineConfig: () => defineConfig,
28
- defineGlobal: () => defineGlobal
29
- });
30
- module.exports = __toCommonJS(index_exports);
5
+ // src/utils/setup-prompt.ts
6
+ function generateAIPrompt(activeTab, config) {
7
+ const frameworkLabel = activeTab === "next" ? "Next.js" : activeTab === "nuxt" ? "Nuxt" : activeTab.charAt(0).toUpperCase() + activeTab.slice(1);
8
+ const backendPkg = activeTab === "nuxt" ? "@dyrected/nuxt" : "@dyrected/next";
9
+ const isSelfHosted = config.isSelfHosted ?? (config.baseUrl?.includes("localhost") || !config.apiKey);
10
+ const baseIntro = isSelfHosted ? `You are a Senior Content Architect. Your mission is to integrate Dyrected CMS into a ${config.siteName || "new"} project using ${frameworkLabel}. This is a SELF-HOSTED installation.
11
+ The backend is already configured via ${backendPkg}.
12
+ Your priority is DATA PRESERVATION and creating a CMS that empowers marketing teams.` : `You are a Senior Content Architect. Your mission is to integrate Dyrected CMS into a ${config.siteName || "new"} project using ${frameworkLabel}. Complete the entire setup automatically, including embedding the Admin UI and syncing the schema. Your priority is DATA PRESERVATION and marketing independence.`;
13
+ const credentials = `
14
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
15
+ SITE CREDENTIALS
16
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
17
+ ${config.siteName ? `Site Name : ${config.siteName}
18
+ ` : ""}${isSelfHosted ? "" : `Site ID : ${config.siteId}
19
+ API Key : ${config.apiKey}
20
+ `}API URL : ${config.baseUrl || "https://api.dyrected.cloud"}
21
+ `;
22
+ const importantNotes = `
23
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
24
+ IMPORTANT NOTES \u2014 READ BEFORE ACTING
25
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
26
+ - \`client.collection(slug)\` is the primary API entrypoint. Do NOT use \`client.collections\`.
27
+ - Blocks are stored as \`[{ blockType: '<slug>', ...fields }]\` \u2014 always switch on \`blockType\` when rendering.
28
+ - MARKETING INDEPENDENCE: Always use a dynamic \`pages\` collection with a catch-all route. Marketing should create pages without a developer. (Exceptions: Special pages like Contact or those with complex forms may use static routes).
29
+ - AUTO-SEEDING: Use \`initialData\` in all data fetches to ensure the site is never empty on first load.
30
+ - Globals use \`client.global(slug).get()\` and \`client.global(slug).update(data)\`.
31
+ - Relationship fields are populated to the specified \`depth\` (default: 1). Set \`depth: 0\` for IDs only.
32
+ - SYNC SCHEMA: After creating the content model, you MUST execute \`npx @dyrected/cli sync:schema\` immediately. This is non-negotiable for a working setup.
33
+ - EMBED ADMIN: You MUST create a route (e.g. \`/admin\`) and mount the Admin UI components described below.
34
+ - ALL-IN-ONE: Your goal is to give the user a project that is READY TO SHIP. Do the setup, do the embedding, and do the sync in one go.
35
+ `;
36
+ const strategy = `
37
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
38
+ PHASE 0 \u2014 DATA PRESERVATION & STRATEGY
39
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
40
+ 1. BACKUP: If an existing site is provided, you MUST extract all current content (text, assets, structure) and save it into a \`migration/\` folder as structured .md files BEFORE modifying any code. Never lose data.
41
+ 2. DISCOVERY: If NO existing site is provided, STOP and ask the user:
42
+ - "What are your core content types (e.g. Services, Team, Blog)?"
43
+ - "How do you want your marketing team to manage the page layouts?"
44
+ 3. ARCHITECTURAL CREATIVITY: Design the CMS for longevity. Use \`blocks\` for flexible page builders, \`globals\` for site settings, and \`collections\` for repeated content.
31
45
 
32
- // src/app.ts
33
- var import_hono = require("hono");
34
- var import_logger = require("hono/logger");
35
- var import_cors = require("hono/cors");
36
- var import_request_id = require("hono/request-id");
46
+ STEP 1 \u2014 CONTENT MODEL (dyrected.config.ts)
47
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
48
+ Use \`defineCollection\`, \`defineGlobal\`, and \`defineConfig\` from '@dyrected/core'.
37
49
 
38
- // src/services/population.service.ts
39
- var PopulationService = class {
40
- constructor(db, collections) {
41
- this.db = db;
42
- this.collections = collections;
43
- }
44
- db;
45
- collections;
46
- /**
47
- * Recursively populate relationship fields in a document or array of documents.
48
- */
49
- async populate(args) {
50
- const { data, fields, currentDepth, maxDepth } = args;
51
- if (currentDepth >= maxDepth || !data) {
52
- return data;
53
- }
54
- if (Array.isArray(data)) {
55
- return Promise.all(data.map((item) => this.populate({ ...args, data: item })));
56
- }
57
- const populatedDoc = { ...data };
58
- for (const field of fields) {
59
- const value = populatedDoc[field.name];
60
- if (field.type === "relationship" && field.collection && value) {
61
- const relatedCollection = this.collections.find((c) => c.slug === field.collection);
62
- if (!relatedCollection) continue;
63
- if (Array.isArray(value)) {
64
- populatedDoc[field.name] = await Promise.all(
65
- value.map(async (id) => {
66
- const doc = await this.db.findOne({ collection: field.collection, id });
67
- if (!doc) return id;
68
- return this.populate({
69
- data: doc,
70
- fields: relatedCollection.fields,
71
- currentDepth: currentDepth + 1,
72
- maxDepth
73
- });
74
- })
75
- );
76
- } else if (typeof value === "string") {
77
- const doc = await this.db.findOne({ collection: field.collection, id: value });
78
- if (doc) {
79
- populatedDoc[field.name] = await this.populate({
80
- data: doc,
81
- fields: relatedCollection.fields,
82
- currentDepth: currentDepth + 1,
83
- maxDepth
84
- });
85
- }
86
- }
87
- }
88
- if ((field.type === "array" || field.type === "object") && field.fields && value) {
89
- populatedDoc[field.name] = await this.populate({
90
- data: value,
91
- fields: field.fields,
92
- currentDepth,
93
- // Nested fields don't consume depth, only relationships do
94
- maxDepth
95
- });
96
- }
97
- }
98
- return populatedDoc;
99
- }
100
- /**
101
- * Helper to populate a PaginatedResult
102
- */
103
- async populateResult(result, fields, maxDepth) {
104
- if (maxDepth <= 0) return result;
105
- const populatedDocs = await this.populate({
106
- data: result.docs,
107
- fields,
108
- currentDepth: 0,
109
- maxDepth
110
- });
111
- return {
112
- ...result,
113
- docs: populatedDocs
114
- };
115
- }
116
- };
50
+ SUPPORTED FIELD TYPES:
51
+ Primitive : text | textarea | richText | number | boolean | date | email | url | json
52
+ Choice : select | multiSelect (requires \`options: [{ label, value }]\`)
53
+ Structural : array | object (requires nested \`fields: [...]\`)
54
+ Relation : relationship (requires \`collection: '<slug>'\`)
55
+ Media : image (use a relationship to an upload collection)
56
+ Blocks : blocks (requires \`blocks: [{ slug, labels, fields }]\`)
117
57
 
118
- // src/controllers/collection.controller.ts
119
- var CollectionController = class {
120
- constructor(collection) {
121
- this.collection = collection;
122
- }
123
- collection;
124
- async find(c) {
125
- const config = c.get("config");
126
- const db = config.db;
127
- const limit = Number(c.req.query("limit")) || 10;
128
- const page = Number(c.req.query("page")) || 1;
129
- const depth = Number(c.req.query("depth")) || 0;
130
- let result = await db.find({
131
- collection: this.collection.slug,
132
- limit,
133
- page
134
- });
135
- if (depth > 0) {
136
- const populationService = new PopulationService(db, config.collections);
137
- result = await populationService.populateResult(result, this.collection.fields, depth);
138
- }
139
- return c.json(result);
140
- }
141
- async findOne(c) {
142
- const config = c.get("config");
143
- const db = config.db;
144
- const id = c.req.param("id");
145
- const depth = Number(c.req.query("depth")) || 0;
146
- if (!id) return c.json({ message: "Missing ID" }, 400);
147
- const doc = await db.findOne({ collection: this.collection.slug, id });
148
- if (!doc) return c.json({ message: "Not Found" }, 404);
149
- if (depth > 0 && doc) {
150
- const populationService = new PopulationService(db, config.collections);
151
- const populatedDoc = await populationService.populate({
152
- data: doc,
153
- fields: this.collection.fields,
154
- currentDepth: 0,
155
- maxDepth: depth
156
- });
157
- return c.json(populatedDoc);
158
- }
159
- return c.json(doc);
160
- }
161
- async create(c) {
162
- const db = c.get("config").db;
163
- const body = await c.req.json();
164
- const doc = await db.create({ collection: this.collection.slug, data: body });
165
- return c.json(doc, 201);
166
- }
167
- async update(c) {
168
- const db = c.get("config").db;
169
- const id = c.req.param("id");
170
- if (!id) return c.json({ message: "Missing ID" }, 400);
171
- const body = await c.req.json();
172
- const doc = await db.update({ collection: this.collection.slug, id, data: body });
173
- return c.json(doc);
174
- }
175
- async delete(c) {
176
- const db = c.get("config").db;
177
- const id = c.req.param("id");
178
- if (!id) return c.json({ message: "Missing ID" }, 400);
179
- await db.delete({ collection: this.collection.slug, id });
180
- return c.json({ message: "Deleted" });
181
- }
182
- };
58
+ COLLECTION OPTIONS:
59
+ \`upload: true\` \u2014 turns this collection into a media library (file uploads)
60
+ \`auth: true\` \u2014 adds login/register/me endpoints; password field is auto-added
61
+ \`admin.group\` \u2014 groups this collection under a sidebar heading
62
+ \`admin.useAsTitle\` \u2014 field to use as the display title in the admin list view
63
+ \`admin.hidden\` \u2014 hide from sidebar (useful for internal/system collections)
183
64
 
184
- // src/controllers/global.controller.ts
185
- var GlobalController = class {
186
- constructor(global) {
187
- this.global = global;
188
- }
189
- global;
190
- async get(c) {
191
- const config = c.get("config");
192
- const db = config.db;
193
- const depth = Number(c.req.query("depth")) || 0;
194
- const data = await db.getGlobal({ slug: this.global.slug });
195
- if (depth > 0 && data) {
196
- const populationService = new PopulationService(db, config.collections);
197
- const populatedData = await populationService.populate({
198
- data,
199
- fields: this.global.fields,
200
- currentDepth: 0,
201
- maxDepth: depth
202
- });
203
- return c.json(populatedData);
204
- }
205
- return c.json(data || {});
206
- }
207
- async update(c) {
208
- const db = c.get("config").db;
209
- const body = await c.req.json();
210
- const data = await db.updateGlobal({ slug: this.global.slug, data: body });
211
- return c.json(data);
212
- }
213
- };
65
+ FIELD OPTIONS:
66
+ \`required\` \u2014 validation
67
+ \`unique\` \u2014 database-level uniqueness
68
+ \`defaultValue\` \u2014 fallback value
69
+ \`admin.condition\` \u2014 "expression" \u2014 Jexl string expression to show/hide field (e.g. "status == \\"published\\"")
70
+ \`admin.layout\` \u2014 "radio" | "dropdown" \u2014 Visual layout for select/multiSelect
71
+ \`admin.direction\` \u2014 "vertical" | "horizontal" \u2014 Layout direction for radio groups
72
+ \`admin.readOnly\` \u2014 display-only in the form
73
+ \`admin.hidden\` \u2014 completely hidden from editor UI
74
+ \`access.read\` \u2014 ({ user }) => boolean \u2014 field-level read access
75
+ \`access.update\` \u2014 ({ user }) => boolean \u2014 field-level write access
76
+ \`hooks.beforeChange\` \u2014 [async (value) => newValue] \u2014 transform value before save
77
+ \`hooks.afterRead\` \u2014 [async (value) => newValue] \u2014 transform value after read
214
78
 
215
- // src/controllers/media.controller.ts
216
- var MediaController = class {
217
- async upload(c) {
218
- const config = c.get("config");
219
- const storage = config.storage;
220
- if (!storage) {
221
- return c.json({ message: "Storage not configured" }, 500);
222
- }
223
- const body = await c.req.parseBody();
224
- const file = body["file"];
225
- if (!file) {
226
- return c.json({ message: "No file uploaded" }, 400);
227
- }
228
- const buffer = Buffer.from(await file.arrayBuffer());
229
- const fileData = await storage.upload({
230
- filename: file.name,
231
- buffer,
232
- mimeType: file.type
233
- });
234
- const db = config.db;
235
- const doc = await db.create({
236
- collection: "media",
237
- data: fileData
238
- });
239
- return c.json(doc, 201);
240
- }
241
- async find(c) {
242
- const db = c.get("config").db;
243
- const limit = Number(c.req.query("limit")) || 10;
244
- const page = Number(c.req.query("page")) || 1;
245
- const result = await db.find({
246
- collection: "media",
247
- limit,
248
- page
249
- });
250
- return c.json(result);
251
- }
252
- async delete(c) {
253
- const config = c.get("config");
254
- const storage = config.storage;
255
- const db = config.db;
256
- const id = c.req.param("id");
257
- if (!id) return c.json({ message: "Missing ID" }, 400);
258
- const doc = await db.findOne({ collection: "media", id });
259
- if (!doc) return c.json({ message: "Not Found" }, 404);
260
- if (storage) {
261
- await storage.delete({ filename: doc.filename });
79
+ BLOCKS EXPLAINED:
80
+ A \`blocks\` field stores an ordered array of typed content blocks.
81
+ Each block has a \`blockType\` discriminator and its own set of fields.
82
+ The admin UI renders a drag-and-drop block editor automatically.
83
+ On the frontend, iterate the array and switch on \`block.blockType\`.
84
+
85
+ COMPLETE EXAMPLE:
86
+ \`\`\`typescript
87
+ import { defineCollection, defineGlobal, defineConfig } from '@dyrected/core'
88
+
89
+ // \u2500\u2500 Media \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
90
+ const media = defineCollection({
91
+ slug: 'media',
92
+ labels: { singular: 'Media Item', plural: 'Media' },
93
+ upload: true,
94
+ fields: [
95
+ { name: 'alt', type: 'text', label: 'Alt Text' },
96
+ { name: 'caption', type: 'textarea', label: 'Caption' },
97
+ ],
98
+ })
99
+
100
+ // \u2500\u2500 Authentication collection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
101
+ const customers = defineCollection({
102
+ slug: 'customers',
103
+ labels: { singular: 'Customer', plural: 'Customers' },
104
+ auth: true, // adds /customers/login, /customers/me, etc.
105
+ admin: { group: 'Membership' },
106
+ fields: [
107
+ { name: 'name', type: 'text', required: true },
108
+ { name: 'email', type: 'email', required: true, unique: true },
109
+ // 'password' is auto-added when auth: true
110
+ { name: 'avatar', type: 'relationship', relationTo: 'media' },
111
+ { name: 'role', type: 'select', admin: { layout: 'radio' }, options: [
112
+ { label: 'Member', value: 'member' },
113
+ { label: 'VIP', value: 'vip' },
114
+ ]},
115
+ ],
116
+ })
117
+
118
+ // \u2500\u2500 Pages with blocks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
119
+ const pages = defineCollection({
120
+ slug: 'pages',
121
+ labels: { singular: 'Page', plural: 'Pages' },
122
+ admin: { useAsTitle: 'title', group: 'Content' },
123
+ fields: [
124
+ { name: 'title', type: 'text', required: true },
125
+ { name: 'slug', type: 'text', required: true, unique: true },
126
+ { name: 'seo', type: 'object', fields: [
127
+ { name: 'metaTitle', type: 'text' },
128
+ { name: 'metaDescription', type: 'textarea' },
129
+ { name: 'ogImage', type: 'relationship', relationTo: 'media' },
130
+ ]},
131
+ {
132
+ name: 'layout',
133
+ type: 'blocks',
134
+ label: 'Page Layout',
135
+ blocks: [
136
+ {
137
+ slug: 'hero',
138
+ labels: { singular: 'Hero', plural: 'Heroes' },
139
+ fields: [
140
+ { name: 'heading', type: 'text', required: true },
141
+ { name: 'subheading', type: 'textarea' },
142
+ { name: 'image', type: 'relationship', relationTo: 'media' },
143
+ { name: 'ctaLabel', type: 'text' },
144
+ { name: 'ctaLink', type: 'url' },
145
+ ],
146
+ },
147
+ {
148
+ slug: 'richContent',
149
+ labels: { singular: 'Rich Content', plural: 'Rich Content Blocks' },
150
+ fields: [
151
+ { name: 'content', type: 'richText', required: true },
152
+ ],
153
+ },
154
+ ],
155
+ },
156
+ ],
157
+ })
158
+
159
+ export default defineConfig({
160
+ collections: [media, customers, pages],
161
+ admin: {
162
+ branding: {
163
+ primaryColor: '#4f46e5',
164
+ logo: '/logo.png',
262
165
  }
263
- await db.delete({ collection: "media", id });
264
- return c.json({ message: "Deleted" });
265
166
  }
266
- };
167
+ })
168
+ \`\`\`
267
169
 
268
- // src/router.ts
269
- function registerRoutes(app, config) {
270
- app.get("/api/schemas", (c) => {
271
- return c.json({
272
- collections: config.collections.map((col) => ({
273
- slug: col.slug,
274
- labels: col.labels,
275
- fields: col.fields,
276
- auth: col.auth,
277
- upload: col.upload
278
- })),
279
- globals: config.globals.map((glb) => ({
280
- slug: glb.slug,
281
- label: glb.label,
282
- fields: glb.fields
283
- }))
284
- });
285
- });
286
- if (config.storage) {
287
- const mediaController = new MediaController();
288
- app.get("/api/media", (c) => mediaController.find(c));
289
- app.post("/api/media", (c) => mediaController.upload(c));
290
- app.delete("/api/media/:id", (c) => mediaController.delete(c));
291
- }
292
- for (const collection of config.collections) {
293
- const path = `/api/collections/${collection.slug}`;
294
- const controller = new CollectionController(collection);
295
- app.get(path, (c) => controller.find(c));
296
- app.post(path, (c) => controller.create(c));
297
- app.get(`${path}/:id`, (c) => controller.findOne(c));
298
- app.patch(`${path}/:id`, (c) => controller.update(c));
299
- app.delete(`${path}/:id`, (c) => controller.delete(c));
300
- }
301
- for (const global of config.globals) {
302
- const path = `/api/globals/${global.slug}`;
303
- const controller = new GlobalController(global);
304
- app.get(path, (c) => controller.get(c));
305
- app.patch(path, (c) => controller.update(c));
306
- }
307
- }
170
+ ${isSelfHosted ? "" : `
171
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
172
+ STEP 2 \u2014 CHOOSE YOUR MODE
173
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
174
+ The developer can choose between two modes:
308
175
 
309
- // src/app.ts
310
- function createDyrectedApp(config) {
311
- const app = new import_hono.Hono();
312
- app.use("*", (0, import_request_id.requestId)());
313
- app.use("*", (0, import_logger.logger)());
314
- app.use("*", (0, import_cors.cors)());
315
- app.use("*", async (c, next) => {
316
- c.set("config", config);
317
- if (!c.get("siteId")) {
318
- c.set("siteId", "default");
319
- }
320
- await next();
321
- });
322
- app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
323
- registerRoutes(app, config);
324
- return app;
176
+ 1. CLOUD MODE (Managed)
177
+ - Use the SITE CREDENTIALS above.
178
+ - Point baseUrl to ${config.baseUrl}.
179
+ - Content is stored in Dyrected Cloud.
180
+
181
+ 2. SELF-HOSTED MODE (Core)
182
+ - Do NOT use apiKey/siteId (unless for proxying).
183
+ - Use a database adapter like \`SqliteAdapter\` from '@dyrected/db-sqlite'.
184
+ - Content is stored locally in the developer's project.
185
+ `}
186
+
187
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
188
+ STEP ${isSelfHosted ? "2" : "3"} \u2014 MOUNTING THE ADMIN UI
189
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
190
+ The Admin UI can be mounted on any path (e.g. /cms-admin).
191
+ Pass the \`basename\` prop to the \`<AdminUI />\` component to match your route.
192
+
193
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
194
+ STEP ${isSelfHosted ? "3" : "4"} \u2014 FRONTEND IMPLEMENTATION
195
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
196
+ `;
197
+ const frameworks = {
198
+ next: `Install \`@dyrected/sdk\` (or \`@dyrected/next\` if you want Next.js server helpers).
199
+
200
+ SDK CLIENT SETUP (\`lib/dyrected.ts\`):
201
+ \`\`\`ts
202
+ import { createClient } from '@dyrected/sdk'
203
+
204
+ export const dyrected = createClient({
205
+ baseUrl: '${config.baseUrl || "http://localhost:3000"}',${isSelfHosted ? "" : `
206
+ apiKey: '${config.apiKey}',
207
+ siteId: '${config.siteId}',`}
208
+ })
209
+ \`\`\``,
210
+ nuxt: `Install \`@dyrected/nuxt\` and add it to \`nuxt.config.ts\`:
211
+ \`\`\`ts
212
+ export default defineNuxtConfig({
213
+ modules: ['@dyrected/nuxt'],
214
+ dyrected: {
215
+ baseUrl: '${config.baseUrl || "http://localhost:3000"}',${isSelfHosted ? "" : `
216
+ apiKey: '${config.apiKey}',
217
+ siteId: '${config.siteId}',`}
218
+ },
219
+ })
220
+ \`\`\`
221
+
222
+ MOUNTING THE ADMIN DASHBOARD (\`pages/cms-admin.vue\`):
223
+ \`\`\`vue
224
+ <script setup lang="ts">
225
+ definePageMeta({ layout: false })
226
+ </script>
227
+
228
+ <template>
229
+ <ClientOnly>
230
+ <DyrectedAdmin basename="/cms-admin" />
231
+ </ClientOnly>
232
+ </template>
233
+ \`\`\`
234
+ `,
235
+ react: `Install \`@dyrected/sdk\`:
236
+
237
+ CLIENT SETUP (\`lib/dyrected.ts\`):
238
+ \`\`\`ts
239
+ import { createClient } from '@dyrected/sdk'
240
+
241
+ export const dyrected = createClient({
242
+ baseUrl: '${config.baseUrl || "https://api.dyrected.cloud"}',${isSelfHosted ? "" : `
243
+ apiKey: '${config.apiKey}',
244
+ siteId: '${config.siteId}',`}
245
+ })
246
+ \`\`\`
247
+
248
+ MOUNTING THE ADMIN DASHBOARD (\`pages/admin.tsx\`):
249
+ \`\`\`tsx
250
+ import { AdminUI } from '@dyrected/admin'
251
+ import '@dyrected/admin/styles'
252
+
253
+ export default function AdminPage() {
254
+ return (
255
+ <div style={{ height: '100vh' }}>
256
+ <AdminUI
257
+ apiKey='${config.apiKey}'
258
+ siteId='${config.siteId}'
259
+ baseUrl='${config.baseUrl || "https://api.dyrected.cloud"}'
260
+ />
261
+ </div>
262
+ )
325
263
  }
264
+ \`\`\`
265
+ `,
266
+ vue: `Install \`@dyrected/sdk\`:
326
267
 
327
- // src/services/media.service.ts
328
- var MediaService = class {
329
- /**
330
- * Fetches metadata for a given URL.
331
- * Supports YouTube and Vimeo.
332
- */
333
- static async fetchMetadata(url) {
334
- if (!url) return null;
335
- if (url.includes("youtube.com") || url.includes("youtu.be")) {
336
- const videoId = this.extractYoutubeId(url);
337
- if (videoId) {
338
- return {
339
- provider: "youtube",
340
- provider_id: videoId,
341
- thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
342
- embedUrl: `https://www.youtube.com/embed/${videoId}`,
343
- type: "video"
344
- };
345
- }
346
- }
347
- if (url.includes("vimeo.com")) {
348
- const vimeoId = this.extractVimeoId(url);
349
- if (vimeoId) {
350
- return {
351
- provider: "vimeo",
352
- provider_id: vimeoId,
353
- thumbnail: "",
354
- // Requires oEmbed API for reliable thumbnails
355
- embedUrl: `https://player.vimeo.com/video/${vimeoId}`,
356
- type: "video"
357
- };
358
- }
359
- }
360
- return null;
361
- }
362
- static extractYoutubeId(url) {
363
- const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
364
- const match = url.match(regExp);
365
- return match && match[2].length === 11 ? match[2] : null;
366
- }
367
- static extractVimeoId(url) {
368
- const match = url.match(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/);
369
- return match ? match[1] : null;
370
- }
371
- };
268
+ CLIENT SETUP (\`lib/dyrected.ts\`):
269
+ \`\`\`ts
270
+ import { createClient } from '@dyrected/sdk'
271
+
272
+ export const dyrected = createClient({
273
+ baseUrl: '${config.baseUrl || "https://api.dyrected.cloud"}',${isSelfHosted ? "" : `
274
+ apiKey: '${config.apiKey}',
275
+ siteId: '${config.siteId}',`}
276
+ })
277
+ \`\`\`
278
+
279
+ MOUNTING THE ADMIN DASHBOARD (\`pages/admin.vue\`):
280
+ \`\`\`vue
281
+ <template>
282
+ <div ref="container" style="height: 100vh" />
283
+ </template>
284
+
285
+ <script setup>
286
+ import { ref, onMounted } from 'vue'
287
+ import { renderAdminUI } from '@dyrected/admin'
288
+ import '@dyrected/admin/styles'
289
+
290
+ const container = ref(null)
291
+ onMounted(() => {
292
+ renderAdminUI(container.value, {
293
+ apiKey: '${config.apiKey}',
294
+ siteId: '${config.siteId}',
295
+ baseUrl: '${config.baseUrl || "https://api.dyrected.cloud"}'
296
+ })
297
+ })
298
+ </script>
299
+ \`\`\`
300
+ `
301
+ };
302
+ return baseIntro + credentials + importantNotes + strategy + (frameworks[activeTab] || frameworks.next) + `
303
+
304
+ API Reference: ${config.baseUrl || "http://localhost:3000"}/api/docs`;
305
+ }
372
306
 
373
307
  // src/index.ts
374
308
  function defineCollection(config) {
@@ -380,12 +314,10 @@ function defineGlobal(config) {
380
314
  function defineConfig(config) {
381
315
  return config;
382
316
  }
383
- // Annotate the CommonJS export names for ESM import in node:
384
- 0 && (module.exports = {
385
- MediaService,
386
- PopulationService,
387
- createDyrectedApp,
317
+ export {
388
318
  defineCollection,
389
319
  defineConfig,
390
- defineGlobal
391
- });
320
+ defineGlobal,
321
+ generateAIPrompt,
322
+ normalizeConfig
323
+ };