@brightsideoy/kama 0.1.0-alpha.1 → 0.1.0-alpha.101

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/bin/cli.js ADDED
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import * as readline from 'node:readline/promises';
6
+
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ });
11
+
12
+ async function main() {
13
+ console.log('\nšŸ”„ --- KAMA SITE GENERATOR --- šŸ”„\n');
14
+
15
+ console.log('Select mode:');
16
+ console.log(' 1) Kickstart new Kama Monorepo (CMS + Initial Site)');
17
+ console.log(' 2) Add a new Astro Site to current workspace (sites/)');
18
+
19
+ const choice = await rl.question('\nChoice [1/2]: ');
20
+
21
+ if (choice.trim() === '2') {
22
+ await createNewSiteOnly();
23
+ } else {
24
+ await createFullWorkspace();
25
+ }
26
+
27
+ rl.close();
28
+ }
29
+
30
+ /**
31
+ * MODE 1: Kickstart full Monorepo Workspace
32
+ */
33
+ async function createFullWorkspace() {
34
+ const projName = (await rl.question('Project folder name [kama-workspace]: ')) || 'kama-workspace';
35
+ const cmsDomain = (await rl.question('CMS Admin Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
36
+ const siteDomain = (await rl.question('First Website Domain (e.g., kama.t3cloud.eu): ')) || 'kama.t3cloud.eu';
37
+
38
+ const rootDir = join(process.cwd(), projName);
39
+
40
+ // šŸŽÆ Derive dynamic worker, database, and bucket names from project folder name
41
+ const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
42
+ let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
43
+ const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
44
+
45
+ console.log(`\nšŸ“ Creating workspace folder at ./${projName}...`);
46
+ mkdirSync(rootDir, { recursive: true });
47
+
48
+ // šŸŽÆ Scaffold schemas directory for project-level custom YAML definitions
49
+ mkdirSync(join(rootDir, 'schemas', 'nodes'), { recursive: true });
50
+ mkdirSync(join(rootDir, 'schemas', 'blocks'), { recursive: true });
51
+
52
+ // --- AUTO D1 CREATION MAGIC ---
53
+ let d1DatabaseId = 'REPLACE_WITH_YOUR_D1_ID';
54
+ let isAuthenticated = false;
55
+
56
+ console.log(`\nšŸ”‘ Checking Cloudflare authentication...`);
57
+ try {
58
+ execSync('npx wrangler whoami', { stdio: 'pipe' });
59
+ isAuthenticated = true;
60
+ console.log(`āœ… Logged in to Cloudflare.`);
61
+ } catch (error) {
62
+ console.log(`āš ļø You are not logged in to Cloudflare.`);
63
+ console.log(`🌐 Launching browser for Cloudflare login...`);
64
+ try {
65
+ execSync('npx wrangler login', { stdio: 'inherit' });
66
+ isAuthenticated = true;
67
+ console.log(`āœ… Successfully logged in!`);
68
+ } catch (loginError) {
69
+ console.log(`āŒ Login failed or was cancelled.`);
70
+ }
71
+ }
72
+
73
+ if (isAuthenticated) {
74
+ try {
75
+ console.log(`\nšŸ—„ļø Checking existing Cloudflare D1 databases...`);
76
+ const listOutput = execSync(`npx wrangler d1 list --json`, { encoding: 'utf-8', stdio: 'pipe' });
77
+
78
+ const jsonStart = listOutput.indexOf('[');
79
+ const jsonEnd = listOutput.lastIndexOf(']') + 1;
80
+ let databases = [];
81
+
82
+ if (jsonStart !== -1 && jsonEnd !== -1) {
83
+ databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
84
+ }
85
+
86
+ const existingDb = databases.find(db => db.name === dbName);
87
+
88
+ if (existingDb) {
89
+ console.log(`āš ļø Database '${dbName}' already exists on your account.`);
90
+ const useExisting = await rl.question(`Do you want to link this existing database? (y/n) [y]: `);
91
+
92
+ if (useExisting.toLowerCase() !== 'n') {
93
+ d1DatabaseId = existingDb.uuid;
94
+ console.log(`āœ… Linked existing database ID: ${d1DatabaseId}`);
95
+ } else {
96
+ dbName = (await rl.question(`Enter a new database name: `)).trim();
97
+ console.log(`\nšŸ—„ļø Creating new database '${dbName}'...`);
98
+ const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
99
+ const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
100
+ if (match) {
101
+ d1DatabaseId = match[0];
102
+ console.log(`āœ… Success! Database ID bound: ${d1DatabaseId}`);
103
+ }
104
+ }
105
+ } else {
106
+ console.log(`\nšŸ—„ļø Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
107
+ const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
108
+ const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
109
+ if (match) {
110
+ d1DatabaseId = match[0];
111
+ console.log(`āœ… Success! Database ID bound: ${d1DatabaseId}`);
112
+ }
113
+ }
114
+ } catch (error) {
115
+ console.log(`āš ļø Database operation failed. We'll leave a placeholder.`);
116
+ }
117
+ } else {
118
+ console.log(`āš ļø Skipping database creation. We'll leave a placeholder in wrangler.json.`);
119
+ }
120
+ // ------------------------------
121
+
122
+ // šŸŽÆ 1. Root package.json
123
+ writeFileSync(
124
+ join(rootDir, 'package.json'),
125
+ JSON.stringify({
126
+ name: projName,
127
+ private: true,
128
+ type: 'module',
129
+ workspaces: ['cms-worker', 'sites/*'],
130
+ scripts: {
131
+ 'bundle:schemas': 'node node_modules/@brightsideoy/kama/scripts/bundle-definitions.js',
132
+ 'dev:cms': 'npm run bundle:schemas && npm run dev -w cms-worker',
133
+ 'deploy:cms': 'npm run bundle:schemas && npm run deploy -w cms-worker',
134
+ },
135
+ dependencies: {
136
+ '@brightsideoy/kama': 'alpha',
137
+ }
138
+ }, null, 2)
139
+ );
140
+
141
+ // šŸŽÆ 2. Setup CMS Worker with Dynamic Names
142
+ console.log(`\nā˜ļø Configuring CMS Worker '${workerName}' (${cmsDomain})...`);
143
+ const cmsDir = join(rootDir, 'cms-worker');
144
+ mkdirSync(cmsDir, { recursive: true });
145
+
146
+ writeFileSync(
147
+ join(cmsDir, 'index.js'),
148
+ `export { default } from '@brightsideoy/kama';\n`
149
+ );
150
+
151
+ writeFileSync(
152
+ join(cmsDir, 'package.json'),
153
+ JSON.stringify({
154
+ name: workerName,
155
+ type: 'module',
156
+ version: '1.0.0',
157
+ scripts: {
158
+ "bundle:schemas": "node ../node_modules/@brightsideoy/kama/scripts/bundle-definitions.js",
159
+ "schema:local": `npm run bundle:schemas && wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`,
160
+ "schema:remote": `npm run bundle:schemas && wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`,
161
+ "dev": "npm run schema:local && wrangler dev",
162
+ "deploy": "npm run schema:remote && wrangler deploy"
163
+ },
164
+ dependencies: {
165
+ '@brightsideoy/kama': 'alpha',
166
+ },
167
+ devDependencies: {
168
+ wrangler: '^4.127.1',
169
+ tsx: '^4.19.2',
170
+ },
171
+ }, null, 2)
172
+ );
173
+
174
+ // šŸŽÆ Fixed CMS wrangler.json (Added PUBLIC_FRONTEND_URL & CLOUDFLARE_ZONE_ID to public vars)
175
+ writeFileSync(
176
+ join(cmsDir, 'wrangler.json'),
177
+ JSON.stringify({
178
+ $schema: '../node_modules/wrangler/config-schema.json',
179
+ name: workerName,
180
+ main: 'index.js',
181
+ compatibility_date: '2026-08-01',
182
+ compatibility_flags: ['nodejs_compat'],
183
+ assets: {
184
+ directory: '../node_modules/@brightsideoy/kama/dist',
185
+ binding: 'ASSETS'
186
+ },
187
+ images: {
188
+ binding: 'IMAGES'
189
+ },
190
+ routes: [{ pattern: cmsDomain, custom_domain: true }],
191
+ vars: {
192
+ CDN_DOMAIN: cmsDomain,
193
+ PUBLIC_FRONTEND_URL: `https://${siteDomain}`,
194
+ CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID"
195
+ },
196
+ d1_databases: [{
197
+ binding: 'DB',
198
+ database_name: dbName,
199
+ database_id: d1DatabaseId,
200
+ }],
201
+ r2_buckets: [{
202
+ binding: 'MEDIA_BUCKET',
203
+ bucket_name: bucketName,
204
+ }],
205
+ }, null, 2)
206
+ );
207
+
208
+ // šŸŽÆ Fixed .dev.vars (Only contains API token for local dev, not public Zone ID)
209
+ writeFileSync(
210
+ join(cmsDir, '.dev.vars'),
211
+ `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\n`
212
+ );
213
+
214
+ // šŸŽÆ 3. Setup First Astro Site tied to target workerName
215
+ const siteFolderName = siteDomain.split('.')[0];
216
+ await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsDomain, workerName);
217
+
218
+ console.log(`\n✨ Workspace ready! Next steps:`);
219
+ console.log(` 1. cd ${projName}`);
220
+ console.log(` 2. npm install`);
221
+ if (d1DatabaseId === 'REPLACE_WITH_YOUR_D1_ID') {
222
+ console.log(` 3. Run 'npx wrangler login' and create a D1 database.`);
223
+ }
224
+ console.log(` 4. Update CLOUDFLARE_ZONE_ID in cms-worker/wrangler.json with your actual Zone ID`);
225
+ console.log(` 5. Securely store your purge token: cd cms-worker && npx wrangler secret put CLOUDFLARE_API_TOKEN`);
226
+ console.log(` 6. Run 'npm run dev:cms' from the workspace root to start the backend.`);
227
+ }
228
+
229
+ /**
230
+ * MODE 2: Add Site to existing workspace
231
+ */
232
+ async function createNewSiteOnly() {
233
+ const rootDir = process.cwd();
234
+
235
+ if (!existsSync(join(rootDir, 'package.json'))) {
236
+ console.error('āŒ Error: Must run this command inside the root of your workspace.');
237
+ return;
238
+ }
239
+
240
+ let targetWorkerName = 'kama-worker';
241
+ const cmsWranglerPath = join(rootDir, 'cms-worker', 'wrangler.json');
242
+ if (existsSync(cmsWranglerPath)) {
243
+ try {
244
+ const cmsWrangler = JSON.parse(readFileSync(cmsWranglerPath, 'utf-8'));
245
+ if (cmsWrangler.name) targetWorkerName = cmsWrangler.name;
246
+ } catch (e) { }
247
+ }
248
+
249
+ const siteFolder = await rl.question('Site folder name (e.g., my-new-site): ');
250
+ const siteDomain = await rl.question('Website Domain (e.g., brand.com): ');
251
+ const cmsDomain = (await rl.question('CMS API Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
252
+
253
+ await generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain.replace(/^https?:\/\//, ''), targetWorkerName);
254
+
255
+ console.log(`\n✨ Site created in sites/${siteFolder}! Run 'npm install' at workspace root.`);
256
+ }
257
+
258
+ /**
259
+ * Helper: Scaffolds the Astro site with middleware, wrangler.json, & slug page
260
+ */
261
+ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain, workerName = 'kama-worker') {
262
+ console.log(`\nšŸš€ Scaffolding Astro site (${siteFolder}) mapped to ${siteDomain} (Service: ${workerName})...`);
263
+
264
+ const siteDir = join(rootDir, 'sites', siteFolder);
265
+ mkdirSync(join(siteDir, 'src/pages'), { recursive: true });
266
+ mkdirSync(join(siteDir, 'src/layouts'), { recursive: true });
267
+ mkdirSync(join(siteDir, 'src/components/blocks'), { recursive: true });
268
+ mkdirSync(join(siteDir, 'src/lib'), { recursive: true });
269
+
270
+ writeFileSync(
271
+ join(siteDir, 'package.json'),
272
+ JSON.stringify({
273
+ name: siteFolder,
274
+ type: 'module',
275
+ version: '0.0.1',
276
+ engines: { node: '>=22.12.0' },
277
+ scripts: {
278
+ dev: 'astro dev',
279
+ build: 'astro build',
280
+ preview: 'astro preview',
281
+ deploy: 'astro build && wrangler deploy',
282
+ },
283
+ dependencies: {
284
+ '@astrojs/cloudflare': '^14.2.5',
285
+ '@brightsideoy/kama': 'alpha',
286
+ astro: '^7.2.2',
287
+ wrangler: '^4.127.1',
288
+ },
289
+ }, null, 2)
290
+ );
291
+
292
+ // šŸŽÆ Fixed Astro wrangler.json: Added `run_worker_first: true` to prevent media routing 404s on Edge
293
+ writeFileSync(
294
+ join(siteDir, 'wrangler.json'),
295
+ JSON.stringify({
296
+ $schema: 'node_modules/wrangler/config-schema.json',
297
+ name: siteFolder,
298
+ compatibility_date: '2026-08-01',
299
+ compatibility_flags: ['nodejs_compat'],
300
+ assets: {
301
+ directory: './dist/client',
302
+ binding: 'ASSETS',
303
+ html_handling: 'drop-trailing-slash',
304
+ not_found_handling: '404-page',
305
+ run_worker_first: true
306
+ },
307
+ services: [{ binding: 'CMS_WORKER', service: workerName }],
308
+ routes: [{ pattern: siteDomain, custom_domain: true }],
309
+ vars: {
310
+ PUBLIC_CMS_URL: `https://${cmsDomain}`,
311
+ PUBLIC_API_URL: `https://${cmsDomain}`,
312
+ },
313
+ }, null, 2)
314
+ );
315
+
316
+ const astroConfig = `import { defineConfig } from 'astro/config';
317
+ import cloudflare from '@astrojs/cloudflare';
318
+
319
+ export default defineConfig({
320
+ output: 'server',
321
+ trailingSlash: 'ignore',
322
+ image: {
323
+ remotePatterns: [
324
+ { protocol: 'http', hostname: 'localhost', port: '3000' },
325
+ { protocol: 'https', hostname: '${cmsDomain}' }
326
+ ]
327
+ },
328
+ build: {
329
+ inlineStylesheets: 'always',
330
+ },
331
+ adapter: cloudflare({
332
+ imageService: 'cloudflare',
333
+ })
334
+ });`;
335
+ writeFileSync(join(siteDir, 'astro.config.mjs'), astroConfig);
336
+
337
+ writeFileSync(
338
+ join(siteDir, '.env'),
339
+ `PUBLIC_CMS_URL="https://${cmsDomain}"\nPUBLIC_API_URL="https://${cmsDomain}"\n`
340
+ );
341
+ }
342
+
343
+ main().catch(console.error);
@@ -0,0 +1,52 @@
1
+ id: _page
2
+ label: Page Old
3
+ inTree: true
4
+ inVault: true
5
+ hasCanvas: true
6
+
7
+ layout:
8
+ columns: "10fr 4fr"
9
+ areas:
10
+ - ["header", "header"]
11
+ - ["main", "sidebar"]
12
+ - ["footer", "footer"]
13
+
14
+ regions:
15
+ header:
16
+ label: Header
17
+ inherit: parent
18
+ main:
19
+ label: Content
20
+ sidebar:
21
+ label: Sidebar
22
+ footer:
23
+ label: Footer
24
+ inherit: parent
25
+
26
+ allowedBlocks:
27
+ - header
28
+ - hero
29
+ - text
30
+ - image_card
31
+
32
+ # šŸ‘‰ TABS DEFINITION
33
+ tabs:
34
+ settings:
35
+ label: "Page"
36
+ fields:
37
+ # Imports core fields: title, slug, isHidden
38
+ - fieldSet: page-base
39
+
40
+ # Overrides & custom properties
41
+ - menuTitle:
42
+ label: Menu Title
43
+ type: text
44
+ dbType: text
45
+ public: true
46
+ placeholder: "Short title for navigation menus"
47
+
48
+ # SEO / OpenGraph metadata
49
+ - fieldSet: meta
50
+
51
+ # Role permissions & access control
52
+ - fieldSet: access
@@ -0,0 +1,11 @@
1
+ import StarterKit from "@tiptap/starter-kit";
2
+ import type { RteProfile } from "../../utils/rteProfiles";
3
+
4
+ export const profile: RteProfile = {
5
+ toolbar: [
6
+ "bold",
7
+ "italic",
8
+ "superscript",
9
+ "subscript",
10
+ ],
11
+ };
@@ -0,0 +1,21 @@
1
+ import StarterKit from "@tiptap/starter-kit";
2
+ import type { RteProfile } from "../../utils/rteProfiles";
3
+ import Link from "../../src/utils/CmsLink";
4
+
5
+ export const profile: RteProfile = {
6
+ extensions: [StarterKit, Link.configure({ openOnClick: false })],
7
+ toolbar: [
8
+ "heading-1",
9
+ "heading-2",
10
+ "heading-3",
11
+ "heading-4",
12
+ "bold",
13
+ "italic",
14
+ "bullet-list",
15
+ "ordered-list",
16
+ "blockquote",
17
+ "link",
18
+ "superscript",
19
+ "subscript",
20
+ ],
21
+ };
@@ -0,0 +1,36 @@
1
+ // definitions/editor/header.ts
2
+ import StarterKit from "@tiptap/starter-kit";
3
+ import Document from "@tiptap/extension-document";
4
+ import Subscript from "@tiptap/extension-subscript";
5
+ import Superscript from "@tiptap/extension-superscript";
6
+ import type { RteProfile } from "../../src/utils/rteProfiles";
7
+ import Link from "../../src/utils/CmsLink";
8
+
9
+ // Custom document node forcing default top-level block to be a heading (H2)
10
+ const CustomDocument = Document.extend({
11
+ content: "heading block*",
12
+ });
13
+
14
+ export const profile: RteProfile = {
15
+ extensions: [
16
+ CustomDocument,
17
+ StarterKit.configure({
18
+ document: false, // Replaces TipTap's default Document with CustomDocument
19
+ heading: { levels: [1, 2, 3, 4] },
20
+ }),
21
+ Subscript.configure(),
22
+ Superscript.configure(),
23
+ Link.configure({ openOnClick: false }),
24
+ ],
25
+ toolbar: [
26
+ "heading-1",
27
+ "heading-2",
28
+ "heading-3",
29
+ "heading-4",
30
+ "bold",
31
+ "italic",
32
+ "superscript",
33
+ "subscript",
34
+ "link",
35
+ ],
36
+ };
@@ -0,0 +1,15 @@
1
+ import StarterKit from "@tiptap/starter-kit";
2
+ import type { RteProfile } from "../../utils/rteProfiles";
3
+ import Link from "../../src/utils/CmsLink";
4
+
5
+ export const profile: RteProfile = {
6
+ extensions: [StarterKit, Link.configure({ openOnClick: false })],
7
+ toolbar: [
8
+ "bold",
9
+ "italic",
10
+ "bullet-list",
11
+ "ordered-list",
12
+ "superscript",
13
+ "subscript",
14
+ ],
15
+ };
@@ -0,0 +1,12 @@
1
+ // definitions/validators/iban.ts
2
+
3
+ export const iban = (value: any, ruleValue: any, field: any) => {
4
+ if (!value || !ruleValue) return "";
5
+
6
+ const isValid = /^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/.test(String(value).toUpperCase());
7
+ if (!isValid) {
8
+ return field.rules?.customMessage || "Invalid IBAN format.";
9
+ }
10
+
11
+ return "";
12
+ };
@@ -0,0 +1,7 @@
1
+ // definitions/validators/test.ts
2
+ export const isMagicWord = (value: any, ruleValue: any, field: any) => {
3
+ if (value && value.toLowerCase() !== "magic") {
4
+ return field.rules?.customMessage || "You didn't say the magic word!";
5
+ }
6
+ return "";
7
+ };