@brightsideoy/kama 0.1.0-alpha.98 → 1.0.0-alpha.10

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.
Files changed (119) hide show
  1. package/README.md +169 -0
  2. package/bin/cli.js +726 -261
  3. package/content/definitions/blocks/button.yaml +34 -0
  4. package/content/definitions/blocks/header.yaml +13 -0
  5. package/content/definitions/blocks/header_image.yaml +21 -0
  6. package/content/definitions/blocks/hero.yaml +28 -0
  7. package/content/definitions/blocks/html.yaml +13 -0
  8. package/content/definitions/blocks/image_card.yaml +56 -0
  9. package/content/definitions/blocks/node-list.yaml +61 -0
  10. package/content/definitions/blocks/single_image.yaml +27 -0
  11. package/content/definitions/blocks/text.yaml +15 -0
  12. package/content/definitions/channels/ecommerce.yaml +28 -0
  13. package/content/definitions/channels/website.yaml +41 -0
  14. package/{defaults → content}/definitions/field-sets/access.yaml +3 -5
  15. package/content/definitions/field-sets/channel-base.yaml +57 -0
  16. package/{defaults/definitions/field-sets/page-meta.yaml → content/definitions/field-sets/channel-seo.yaml} +3 -11
  17. package/content/definitions/field-sets/content-block.yaml +41 -0
  18. package/content/definitions/field-sets/page-base.yaml +44 -0
  19. package/content/definitions/field-sets/page-seo.yaml +34 -0
  20. package/{defaults → content}/definitions/field-sets/page-social.yaml +6 -8
  21. package/content/definitions/field-sets/schema-type.yaml +43 -0
  22. package/content/definitions/field-sets/tree-base.yaml +31 -0
  23. package/content/definitions/node-layouts/content.yml +12 -0
  24. package/{defaults → content}/definitions/node-layouts/landing-page.yml +8 -3
  25. package/{defaults → content}/definitions/node-layouts/single-column.yml +10 -5
  26. package/{defaults → content}/definitions/node-layouts/two-column.yml +14 -6
  27. package/content/definitions/nodes/address.yaml +40 -0
  28. package/content/definitions/nodes/article.yaml +65 -0
  29. package/content/definitions/nodes/event.yaml +55 -0
  30. package/content/definitions/nodes/external-link.yaml +29 -0
  31. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  32. package/{defaults → content}/definitions/nodes/page.yaml +9 -20
  33. package/content/definitions/nodes/person.yaml +37 -0
  34. package/content/definitions/nodes/product.yaml +89 -0
  35. package/content/definitions/taxonomies/categories.yaml +15 -0
  36. package/content/definitions/taxonomies/tags.yaml +12 -0
  37. package/content/definitions/trees/navigation.yaml +24 -0
  38. package/definitions/editor/default.ts +1 -0
  39. package/dist/assets/index-Bzbme3Ob.css +1 -0
  40. package/dist/assets/index-DX98CiK-.js +164 -0
  41. package/dist/dist-CErcWOwp.js +27136 -0
  42. package/dist/index.html +4 -2
  43. package/dist/index.js +26159 -8575
  44. package/dist/schema-compiler.js +48 -0
  45. package/documentation/SCHEMAS.md +182 -0
  46. package/package.json +23 -25
  47. package/schema.sql +67 -23
  48. package/templates/astro-starter/astro.config.mjs +27 -0
  49. package/templates/astro-starter/package.json.template +20 -0
  50. package/templates/astro-starter/public/robots.txt +23 -0
  51. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +143 -0
  52. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
  53. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +126 -0
  54. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  55. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
  56. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +91 -0
  57. package/templates/astro-starter/src/components/blocks/NodeList.astro +278 -0
  58. package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
  59. package/templates/astro-starter/src/components/global/Breadcrumb.astro +108 -0
  60. package/templates/astro-starter/src/components/global/Footer.astro +50 -0
  61. package/templates/astro-starter/src/components/global/Header.astro +171 -0
  62. package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
  63. package/templates/astro-starter/src/components/global/NavigationMenu.astro +413 -0
  64. package/templates/astro-starter/src/components/ui/KamaImage.astro +180 -0
  65. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
  66. package/templates/astro-starter/src/layouts/StandardLayout.astro +63 -0
  67. package/templates/astro-starter/src/lib/cms.ts +219 -0
  68. package/templates/astro-starter/src/lib/media/kama-image-service.ts +67 -0
  69. package/templates/astro-starter/src/lib/media/media-proxy.ts +148 -0
  70. package/templates/astro-starter/src/lib/media/media-url.ts +112 -0
  71. package/templates/astro-starter/src/middleware.ts +508 -0
  72. package/templates/astro-starter/src/pages/404.astro +46 -0
  73. package/templates/astro-starter/src/pages/[...slug].astro +318 -0
  74. package/templates/astro-starter/src/pages/api/load-nodes.ts +40 -0
  75. package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
  76. package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
  77. package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
  78. package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
  79. package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
  80. package/templates/astro-starter/src/styles/standard-theme.css +157 -0
  81. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  82. package/templates/astro-starter/tsconfig.json +0 -0
  83. package/templates/kama-worker/index.js +19 -0
  84. package/templates/kama-worker/server.js +104 -0
  85. package/wrangler.json +22 -0
  86. package/defaults/definitions/blocks/button.yaml +0 -15
  87. package/defaults/definitions/blocks/header.yaml +0 -15
  88. package/defaults/definitions/blocks/header_image.yaml +0 -22
  89. package/defaults/definitions/blocks/hero.yaml +0 -27
  90. package/defaults/definitions/blocks/image_card.yaml +0 -57
  91. package/defaults/definitions/blocks/single_image.yaml +0 -28
  92. package/defaults/definitions/blocks/text.yaml +0 -16
  93. package/defaults/definitions/channels/ecommerce.yaml +0 -24
  94. package/defaults/definitions/channels/website.yaml +0 -32
  95. package/defaults/definitions/field-sets/channel-base.yaml +0 -31
  96. package/defaults/definitions/field-sets/content-block.yaml +0 -46
  97. package/defaults/definitions/field-sets/page-base.yaml +0 -49
  98. package/defaults/definitions/field-sets/tree-base.yaml +0 -31
  99. package/defaults/definitions/node-layouts/three-column.yml +0 -41
  100. package/defaults/definitions/nodes/address.yaml +0 -32
  101. package/defaults/definitions/nodes/article.yaml +0 -43
  102. package/defaults/definitions/nodes/event.yaml +0 -49
  103. package/defaults/definitions/nodes/external-link.yaml +0 -26
  104. package/defaults/definitions/nodes/person.yaml +0 -37
  105. package/defaults/definitions/nodes/product.yaml +0 -86
  106. package/defaults/definitions/trees/navigation.yaml +0 -29
  107. package/defaults/definitions/trees/social-links.yaml +0 -9
  108. package/definitions/_page.yaml +0 -52
  109. package/dist/assets/index-TdW5F0pX.css +0 -1
  110. package/dist/assets/index-u9OkBauM.js +0 -1392
  111. package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
  112. package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
  113. package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
  114. package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
  115. package/scripts/bundle-definitions.js +0 -176
  116. /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
  117. /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
  118. /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
  119. /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
package/bin/cli.js CHANGED
@@ -1,343 +1,808 @@
1
1
  #!/usr/bin/env node
2
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';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, cpSync, unlinkSync, appendFileSync } from 'node:fs';
4
+ import { join, dirname, basename, resolve } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import crypto from 'node:crypto'; // 🎯 NEW: Cryptographically secure token generator
7
+
8
+ // 🎯 Modern CLI UI Components
9
+ import { intro, outro, text, select, confirm, spinner, note, isCancel, cancel } from '@clack/prompts';
10
+
11
+ // --- UI HELPERS (Zero Dependency Colors) ---
12
+ const pc = {
13
+ green: (t) => `\x1b[32m${t}\x1b[0m`,
14
+ cyan: (t) => `\x1b[36m${t}\x1b[0m`,
15
+ dim: (t) => `\x1b[2m${t}\x1b[0m`,
16
+ yellow: (t) => `\x1b[33m${t}\x1b[0m`,
17
+ red: (t) => `\x1b[31m${t}\x1b[0m`,
18
+ bgCyan: (t) => `\x1b[46m\x1b[30m ${t} \x1b[0m`,
19
+ bold: (t) => `\x1b[1m${t}\x1b[0m`,
20
+ reset: '\x1b[0m'
21
+ };
22
+
23
+ // Graceful exit handler for Ctrl+C
24
+ function handleCancel(value) {
25
+ if (isCancel(value)) {
26
+ cancel('Operation cancelled.');
27
+ process.exit(0);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ // 🎯 Always use the absolute newest ecosystem
33
+ const ASTRO_LATEST = "latest";
34
+ const ASTRO_NODE_LATEST = "latest";
35
+ const ASTRO_CF_LATEST = "latest";
36
+ const WRANGLER_LATEST = "latest";
37
+
38
+ // Dynamically resolve current package version
39
+ const __filename = fileURLToPath(import.meta.url);
40
+ const __dirname = dirname(__filename);
41
+ let currentKamaVersion = 'alpha';
42
+ try {
43
+ const pkgPath = join(__dirname, '../package.json');
44
+ if (existsSync(pkgPath)) {
45
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
46
+ if (pkg.version) currentKamaVersion = `^${pkg.version}`;
47
+ }
48
+ } catch (e) {
49
+ console.warn("⚠️ Could not detect Kama version, defaulting to 'alpha'");
50
+ }
51
+
52
+ /**
53
+ * 🎯 SECURE SECRET MANAGER
54
+ * Gracefully retrieves an existing workspace secret, or generates and injects one if missing.
55
+ */
56
+ function getOrGenerateWorkspaceSecret(workerDir) {
57
+ const envPath = join(workerDir, '.env');
58
+ const devVarsPath = join(workerDir, '.dev.vars');
59
+
60
+ let secret = null;
61
+
62
+ // Check local Node .env
63
+ if (existsSync(envPath)) {
64
+ const match = readFileSync(envPath, 'utf-8').match(/^FRONTEND_API_SECRET=["']?([^"'\n]+)/m);
65
+ if (match) secret = match[1];
66
+ }
67
+
68
+ // Check Cloudflare .dev.vars
69
+ if (!secret && existsSync(devVarsPath)) {
70
+ const match = readFileSync(devVarsPath, 'utf-8').match(/^FRONTEND_API_SECRET=["']?([^"'\n]+)/m);
71
+ if (match) secret = match[1];
72
+ }
73
+
74
+ // Generate and inject if missing
75
+ if (!secret) {
76
+ secret = crypto.randomBytes(32).toString('hex');
77
+ const secretLine = `\n# KAMA Auto-Generated Server-to-Server Security\nFRONTEND_API_SECRET="${secret}"\n`;
78
+
79
+ if (existsSync(devVarsPath)) {
80
+ appendFileSync(devVarsPath, secretLine);
81
+ } else if (existsSync(envPath)) {
82
+ appendFileSync(envPath, secretLine);
83
+ } else {
84
+ writeFileSync(devVarsPath, secretLine);
85
+ }
86
+ }
87
+
88
+ return secret;
89
+ }
90
+
91
+ /**
92
+ * 🎯 BULLETPROOF WRANGLER EXECUTION RESOLVER
93
+ */
94
+ function getWranglerCmd() {
95
+ try {
96
+ execSync('wrangler --version', { stdio: 'ignore' });
97
+ return 'wrangler';
98
+ } catch (e) {
99
+ return 'npx --yes --package=wrangler wrangler';
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 🎯 ROBUST WRANGLER RUNNER
105
+ */
106
+ function runWrangler(cmd, options = {}) {
107
+ const baseWrangler = getWranglerCmd();
108
+ try {
109
+ return execSync(`${baseWrangler} ${cmd}`, {
110
+ encoding: 'utf-8',
111
+ stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
112
+ env: {
113
+ ...process.env,
114
+ PATH: process.env.PATH
115
+ }
116
+ });
117
+ } catch (e) {
118
+ if (options.throw) throw e;
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * 🎯 BULLETPROOF KV PROVISIONING ENGINE
125
+ */
126
+ function getOrCreateKV(title) {
127
+ const out = runWrangler(`kv namespace create "${title}"`);
128
+ if (out) {
129
+ const match = out.match(/"id":\s*"([a-fA-F0-9]{32})"/);
130
+ if (match) return match[1];
131
+ }
132
+
133
+ const listOut = runWrangler(`kv namespace list`);
134
+ if (listOut) {
135
+ try {
136
+ const namespaces = JSON.parse(listOut);
137
+ const found = namespaces.find(n => n.title === title);
138
+ if (found) return found.id;
139
+ } catch (err) { }
140
+ }
141
+
142
+ return `REPLACE_WITH_${title}_ID`;
143
+ }
6
144
 
7
- const rl = readline.createInterface({
8
- input: process.stdin,
9
- output: process.stdout,
10
- });
145
+ function showHelp() {
146
+ console.log(`\n${pc.bgCyan(' KAMA • ENGINE CLI ')}\n`);
147
+ console.log('Usage: npx kama <command> [options]\n');
148
+ console.log(pc.cyan('Commands:'));
149
+ console.log(' init Kickstart a new Monorepo (CMS Worker + Optional Astro Site)');
150
+ console.log(' add site Add a new Astro Frontend Site to the current workspace (sites/)');
151
+ console.log(' bundle Compile and bundle YAML schemas into bundled-definitions.js for Edge deployment');
152
+ console.log(' promote Promote the current Local workspace to Cloudflare Edge');
153
+ console.log(' help, -h Show this help message\n');
154
+ console.log(pc.cyan('Workspace Scripts (Run from project root):'));
155
+ console.log(' npm update Update to latest KAMA version');
156
+ console.log(' npm run setup Initialize database schema (Run Once)');
157
+ console.log(' npm run dev Boot up the CMS backend and Astro frontend concurrently');
158
+ console.log(' npm run deploy:cms Deploy the backend worker and database to Cloudflare');
159
+ console.log(' npm run deploy:site Deploy the Astro frontend site to Cloudflare\n');
160
+ }
11
161
 
12
162
  async function main() {
13
- console.log('\n🔥 --- KAMA SITE GENERATOR --- 🔥\n');
163
+ const args = process.argv.slice(2);
164
+ const command = args[0]?.toLowerCase();
165
+ const subCommand = args[1]?.toLowerCase();
14
166
 
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/)');
167
+ if (['help', '--help', '-h'].includes(command)) {
168
+ showHelp();
169
+ process.exit(0);
170
+ }
18
171
 
19
- const choice = await rl.question('\nChoice [1/2]: ');
172
+ console.clear();
173
+ intro(`${pc.bgCyan(' KAMA • ENGINE ')} ${pc.dim('v' + currentKamaVersion.replace('^', ''))}`);
20
174
 
21
- if (choice.trim() === '2') {
175
+ if (command === 'init') {
176
+ await createFullWorkspace();
177
+ } else if (command === 'add' && subCommand === 'site') {
22
178
  await createNewSiteOnly();
179
+ } else if (command === 'promote') {
180
+ await promoteLocalToCloudflare();
181
+ } else if (command === 'bundle') {
182
+ await bundleWorkspaceSchemas();
23
183
  } else {
24
- await createFullWorkspace();
184
+ const choice = handleCancel(await select({
185
+ message: 'What would you like to do?',
186
+ options: [
187
+ { value: 'init', label: 'Init', hint: 'Kickstart new Workspace (CMS + Optional Site)' },
188
+ { value: 'add_site', label: 'Add Site', hint: 'Add Astro Site to current workspace (sites/)' },
189
+ { value: 'bundle', label: 'Bundle', hint: 'Compile YAML schemas for Edge deployment' },
190
+ { value: 'promote', label: 'Promote', hint: 'Promote current Local workspace to Cloudflare Edge' },
191
+ { value: 'help', label: 'Help', hint: 'Show all commands and usage' },
192
+ { value: 'exit', label: 'Exit' }
193
+ ]
194
+ }));
195
+
196
+ if (choice === 'help') {
197
+ showHelp();
198
+ process.exit(0);
199
+ } else if (choice === 'promote') {
200
+ await promoteLocalToCloudflare();
201
+ } else if (choice === 'bundle') {
202
+ await bundleWorkspaceSchemas();
203
+ } else if (choice === 'add_site') {
204
+ await createNewSiteOnly();
205
+ } else if (choice === 'init') {
206
+ await createFullWorkspace();
207
+ } else {
208
+ outro('Goodbye!');
209
+ process.exit(0);
210
+ }
25
211
  }
212
+ }
26
213
 
27
- rl.close();
214
+ function convertAstroConfigToCloudflare(siteDir) {
215
+ const configPath = join(siteDir, 'astro.config.mjs');
216
+ if (existsSync(configPath)) {
217
+ let content = readFileSync(configPath, 'utf-8');
218
+ content = content
219
+ .replace("import node from '@astrojs/node';", "import cloudflare from '@astrojs/cloudflare';")
220
+ .replace('adapter: node({ mode: "standalone" })', 'adapter: cloudflare({ imageService: process.env.NODE_ENV === "production" ? "cloudflare" : "passthrough" })');
221
+ writeFileSync(configPath, content, 'utf-8');
222
+ }
28
223
  }
29
224
 
30
225
  /**
31
- * MODE 1: Kickstart full Monorepo Workspace
226
+ * COMMAND: npx kama init
32
227
  */
33
228
  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';
229
+ const cwdName = basename(process.cwd());
230
+ const hasExistingPkg = existsSync(join(process.cwd(), 'package.json'));
231
+ const defaultDirPrompt = hasExistingPkg ? '.' : cwdName;
232
+
233
+ const inputName = handleCancel(await text({
234
+ message: 'Project folder name (use "." for current dir):',
235
+ placeholder: defaultDirPrompt,
236
+ defaultValue: defaultDirPrompt
237
+ }));
238
+
239
+ const chosenFolder = inputName.trim() || defaultDirPrompt;
240
+ const isCurrentDir = chosenFolder === '.';
241
+ const projName = isCurrentDir ? cwdName : chosenFolder;
242
+ const rootDir = isCurrentDir ? process.cwd() : join(process.cwd(), projName);
243
+
244
+ const isCloudflare = handleCancel(await select({
245
+ message: 'Choose your Deployment Architecture:',
246
+ options: [
247
+ { value: true, label: 'Cloudflare Edge', hint: 'Workers, D1, R2, CF Image Resizing' },
248
+ { value: false, label: 'VPS / Local Server', hint: 'Node.js, SQLite, Sharp cropping' }
249
+ ]
250
+ }));
251
+
252
+ const shouldCreateSite = handleCancel(await confirm({
253
+ message: 'Do you want to scaffold an initial Astro frontend site?',
254
+ initialValue: true
255
+ }));
256
+
257
+ let siteFolderName = 'kama-starter';
258
+ let siteDomain = 'localhost:4321';
259
+ let cmsDomain = 'localhost:8787';
260
+
261
+ if (shouldCreateSite) {
262
+ siteFolderName = handleCancel(await text({
263
+ message: 'Astro package/folder name (e.g., my-website.eu):',
264
+ placeholder: 'kama-starter',
265
+ defaultValue: 'kama-starter'
266
+ }));
267
+ }
268
+
269
+ if (isCloudflare) {
270
+ cmsDomain = handleCancel(await text({
271
+ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):',
272
+ defaultValue: ''
273
+ }));
274
+ if (shouldCreateSite) {
275
+ siteDomain = handleCancel(await text({
276
+ message: 'First Website Domain (e.g., yourdomain.com):',
277
+ defaultValue: ''
278
+ }));
279
+ }
280
+ }
281
+
282
+ const cmsProtocol = isCloudflare ? 'https://' : 'http://';
283
+ const cmsFullUrl = cmsDomain ? `${cmsProtocol}${cmsDomain.replace(/^https?:\/\//, '')}` : 'http://localhost:8787';
37
284
 
38
- const rootDir = join(process.cwd(), projName);
285
+ // 🎯 INITIALIZE UNIQUE SECURE API SECRET
286
+ const frontendApiSecret = crypto.randomBytes(32).toString('hex');
39
287
 
40
- // 🎯 Derive dynamic worker, database, and bucket names from project folder name
41
288
  const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
42
289
  let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
43
290
  const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
44
291
 
45
- console.log(`\n📁 Creating workspace folder at ./${projName}...`);
46
- mkdirSync(rootDir, { recursive: true });
292
+ const s = spinner();
293
+ s.start('Provisioning workspace...');
294
+
295
+ if (!isCurrentDir) mkdirSync(rootDir, { recursive: true });
296
+
297
+ writeFileSync(
298
+ join(rootDir, '.gitignore'),
299
+ `node_modules/\n.env\n.DS_Store\nkama-worker/.dev.vars\nkama-worker/.env\nkama-worker/.wrangler/\nkama-worker/public/\nkama-worker/*.db\nsites/*/.env\nsites/*/.astro/\nsites/*/dist/\nkama-worker/bundled-definitions.js\n`
300
+ );
47
301
 
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 });
302
+ const sourceDefsDir = join(__dirname, '../content/definitions');
303
+ const targetDefsDir = join(rootDir, 'content', 'definitions');
304
+ if (existsSync(sourceDefsDir)) cpSync(sourceDefsDir, targetDefsDir, { recursive: true });
51
305
 
52
- // --- AUTO D1 CREATION MAGIC ---
53
- let d1DatabaseId = 'REPLACE_WITH_YOUR_D1_ID';
306
+ let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
307
+ let backendCacheId = 'REPLACE_WITH_CACHE_KV_ID';
54
308
  let isAuthenticated = false;
55
309
 
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' });
310
+ if (isCloudflare) {
311
+ s.message('Checking Cloudflare authentication...');
312
+ const whoami = runWrangler('whoami');
313
+ if (whoami && (whoami.includes('Getting User Settings') || whoami.includes('You are logged in'))) {
66
314
  isAuthenticated = true;
67
- console.log(`✅ Successfully logged in!`);
68
- } catch (loginError) {
69
- console.log(`❌ Login failed or was cancelled.`);
315
+ } else {
316
+ s.stop('Not authenticated with Cloudflare.', true);
317
+ console.log(pc.dim('Launching browser for Cloudflare login...'));
318
+ const login = runWrangler('login', { stdio: 'inherit' });
319
+ if (login !== null) isAuthenticated = true;
320
+ s.start('Resuming provisioning...');
70
321
  }
71
- }
72
322
 
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}`);
323
+ if (isAuthenticated) {
324
+ s.message('Provisioning backend KV Cache...');
325
+ backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`);
326
+
327
+ s.message('Checking existing Cloudflare D1 databases...');
328
+ const listOutput = runWrangler('d1 list --json');
329
+ if (listOutput) {
330
+ try {
331
+ const jsonStart = listOutput.indexOf('[');
332
+ const jsonEnd = listOutput.lastIndexOf(']') + 1;
333
+ let databases = [];
334
+
335
+ if (jsonStart !== -1 && jsonEnd !== -1) databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
336
+ const existingDb = databases.find(db => db.name === dbName);
337
+
338
+ if (existingDb) {
339
+ s.stop(`Database '${dbName}' already exists on your account.`);
340
+ const useExisting = handleCancel(await confirm({ message: 'Do you want to link this existing database?', initialValue: true }));
341
+
342
+ if (useExisting) {
343
+ d1DatabaseId = existingDb.uuid;
344
+ s.start(`Linked existing database ID: ${d1DatabaseId}`);
345
+ } else {
346
+ dbName = handleCancel(await text({ message: 'Enter a new database name:' }));
347
+ s.start(`Creating new database '${dbName}'...`);
348
+ const d1Output = runWrangler(`d1 create ${dbName}`);
349
+ const match = d1Output ? 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}/) : null;
350
+ if (match) d1DatabaseId = match[0];
351
+ }
352
+ } else {
353
+ s.message(`Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
354
+ const d1Output = runWrangler(`d1 create ${dbName}`);
355
+ const match = d1Output ? 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}/) : null;
356
+ if (match) d1DatabaseId = match[0];
103
357
  }
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
- }
358
+ } catch (error) { }
113
359
  }
114
- } catch (error) {
115
- console.log(`⚠️ Database operation failed. We'll leave a placeholder.`);
116
360
  }
361
+ }
362
+
363
+ s.message('Writing configuration files...');
364
+
365
+ const rootPkg = {
366
+ name: projName,
367
+ private: true,
368
+ type: 'module',
369
+ workspaces: ['kama-worker', 'sites/*'],
370
+ scripts: { 'setup': 'npm run setup:local -w kama-worker' },
371
+ dependencies: { '@brightsideoy/kama': currentKamaVersion },
372
+ devDependencies: {}
373
+ };
374
+
375
+ if (isCloudflare) rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
376
+
377
+ if (shouldCreateSite) {
378
+ rootPkg.devDependencies['concurrently'] = '^9.0.0';
379
+ rootPkg.scripts['dev:site'] = `npm run dev -w sites/${siteFolderName}`;
380
+ rootPkg.scripts['predev'] = 'npx kama bundle';
381
+ rootPkg.scripts['dev'] = 'npx concurrently "npm run dev -w kama-worker" "npm run dev -w sites/' + siteFolderName + '"';
382
+ rootPkg.scripts['deploy:site'] = `npm run deploy -w sites/${siteFolderName}`;
383
+ rootPkg.scripts['deploy:cms'] = `npm run deploy -w kama-worker`;
117
384
  } else {
118
- console.log(`⚠️ Skipping database creation. We'll leave a placeholder in wrangler.json.`);
385
+ rootPkg.scripts['predev'] = 'npx kama bundle';
386
+ rootPkg.scripts['dev'] = 'npm run dev -w kama-worker';
387
+ rootPkg.scripts['deploy:cms'] = `npm run deploy -w kama-worker`;
119
388
  }
120
- // ------------------------------
121
389
 
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
- );
390
+ writeFileSync(join(rootDir, 'package.json'), JSON.stringify(rootPkg, null, 2));
140
391
 
141
- // 🎯 2. Setup CMS Worker with Dynamic Names
142
- console.log(`\n☁️ Configuring CMS Worker '${workerName}' (${cmsDomain})...`);
143
- const cmsDir = join(rootDir, 'cms-worker');
392
+ const cmsDir = join(rootDir, 'kama-worker');
144
393
  mkdirSync(cmsDir, { recursive: true });
145
394
 
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({
395
+ const emptyDefs = `{ nodes: {}, trees: {}, blocks: {}, nodeLayouts: {}, blockLayouts: {}, channels: {} }`;
396
+ writeFileSync(join(cmsDir, 'bundled-definitions.js'), `const defs = ${emptyDefs};\nexport const BUNDLED_DEFINITIONS = defs;\nexport default defs;\n`);
397
+
398
+ if (isCloudflare) {
399
+ const templateIndexPath = join(__dirname, '../templates/kama-worker/index.js');
400
+ if (existsSync(templateIndexPath)) cpSync(templateIndexPath, join(cmsDir, 'index.js'));
401
+
402
+ writeFileSync(
403
+ join(cmsDir, 'package.json'),
404
+ JSON.stringify({
405
+ name: workerName,
406
+ type: 'module',
407
+ version: '1.0.0',
408
+ scripts: {
409
+ "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql > /dev/null 2>&1`,
410
+ "setup:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`,
411
+ "dev": "wrangler dev --port 8787 --inspector-port 9239",
412
+ "deploy": "npm run setup:remote && wrangler deploy"
413
+ },
414
+ dependencies: { '@brightsideoy/kama': currentKamaVersion, '@libsql/client': '^0.14.0' },
415
+ devDependencies: { "wrangler": WRANGLER_LATEST },
416
+ }, null, 2)
417
+ );
418
+
419
+ const cmsWranglerConfig = {
178
420
  $schema: '../node_modules/wrangler/config-schema.json',
179
421
  name: workerName,
180
422
  main: 'index.js',
181
- compatibility_date: '2026-08-01',
423
+ compatibility_date: '2024-09-23',
182
424
  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 }],
425
+ dev: { host: 'localhost', port: 8787 },
426
+ assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
427
+ images: { binding: 'IMAGES' },
428
+ kv_namespaces: [{ binding: 'CACHE_KV', id: backendCacheId }],
191
429
  vars: {
192
- CDN_DOMAIN: cmsDomain,
193
- PUBLIC_FRONTEND_URL: `https://${siteDomain}`,
194
- CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID"
430
+ CDN_DOMAIN: cmsDomain || 'localhost:8787',
431
+ PUBLIC_FRONTEND_URL: (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321',
432
+ CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID",
433
+ KAMA_RUNTIME: 'cloudflare'
195
434
  },
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
- );
435
+ d1_databases: [{ binding: 'DB', database_name: dbName, database_id: d1DatabaseId }],
436
+ r2_buckets: [{ binding: 'MEDIA_BUCKET', bucket_name: bucketName }],
437
+ routes: cmsDomain ? [{ pattern: cmsDomain, custom_domain: true }] : [{ pattern: "", custom_domain: true }]
438
+ };
207
439
 
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
- );
440
+ writeFileSync(join(cmsDir, 'wrangler.json'), JSON.stringify(cmsWranglerConfig, null, 2));
441
+
442
+ // 🎯 Inject API Secret directly into .dev.vars
443
+ const publicFrontendUrl = (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321';
444
+ writeFileSync(join(cmsDir, '.dev.vars'), `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\n\n# Local Development Overrides\nCDN_DOMAIN=\nPUBLIC_FRONTEND_URL=${publicFrontendUrl}\n\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`);
445
+ } else {
446
+ const templateServerPath = join(__dirname, '../templates/kama-worker/server.js');
447
+ if (existsSync(templateServerPath)) cpSync(templateServerPath, join(cmsDir, 'server.js'));
448
+
449
+ writeFileSync(
450
+ join(cmsDir, 'package.json'),
451
+ JSON.stringify({
452
+ name: workerName,
453
+ type: 'module',
454
+ version: '1.0.0',
455
+ scripts: {
456
+ "setup:local": "sqlite3 kama.db < ../node_modules/@brightsideoy/kama/schema.sql",
457
+ "dev": "tsx server.js",
458
+ "start": "node server.js"
459
+ },
460
+ dependencies: { '@brightsideoy/kama': currentKamaVersion, '@hono/node-server': '^1.13.0', '@libsql/client': '^0.14.0', 'sharp': '^0.33.5', 'dotenv': '^16.4.5' },
461
+ devDependencies: { "tsx": "^4.19.0" },
462
+ }, null, 2)
463
+ );
464
+
465
+ // 🎯 Inject API Secret directly into .env
466
+ writeFileSync(join(cmsDir, '.env'), `PORT=8787\nKAMA_RUNTIME=node\nDB_PATH=./kama.db\nLOCAL_MEDIA_PATH=./public/media\nPUBLIC_CMS_URL=http://127.0.0.1:8787\nPUBLIC_API_URL=http://127.0.0.1:8787\nPUBLIC_FRONTEND_URL=http://127.0.0.1:4321\n\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`);
467
+ }
468
+
469
+ s.message('Bundling schemas...');
470
+ await bundleWorkspaceSchemas(rootDir, true);
471
+
472
+ if (shouldCreateSite) {
473
+ s.message(`Scaffolding Astro site (${siteFolderName})...`);
474
+ // 🎯 Pass secret to the frontend generator
475
+ await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsFullUrl, workerName, isCloudflare, frontendApiSecret);
476
+ }
477
+
478
+ s.stop('Workspace provisioned successfully.');
213
479
 
214
- // 🎯 3. Setup First Astro Site tied to target workerName
215
- const siteFolderName = siteDomain.split('.')[0];
216
- await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsDomain, workerName);
480
+ let nextSteps = '';
481
+ let stepCount = 1;
482
+ if (!isCurrentDir) nextSteps += `${stepCount++}. cd ${projName}\n`;
483
+ nextSteps += `${stepCount++}. npm install\n`;
484
+ nextSteps += `${stepCount++}. npm run setup ${pc.dim('<- Run this once to init the database schema!')}\n`;
485
+ nextSteps += `${stepCount++}. npm run dev\n`;
217
486
 
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.`);
487
+ if (isCloudflare) {
488
+ nextSteps += `\n${pc.dim('Cloudflare Note:')}\n`;
489
+ nextSteps += `${pc.dim('Update CLOUDFLARE_ZONE_ID in wrangler.json')}\n`;
490
+ nextSteps += `${pc.dim('Set Cache purge token: npx wrangler secret put CLOUDFLARE_API_TOKEN')}\n`;
491
+ nextSteps += `${pc.dim('Set Frontend API secret: npx wrangler secret put FRONTEND_API_SECRET')}`;
223
492
  }
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.`);
493
+
494
+ note(nextSteps, 'Next steps');
495
+ outro(`KAMA • ENGINE is ready! 🚀`);
227
496
  }
228
497
 
229
498
  /**
230
- * MODE 2: Add Site to existing workspace
499
+ * COMMAND: npx kama add site
231
500
  */
232
501
  async function createNewSiteOnly() {
233
502
  const rootDir = process.cwd();
234
503
 
235
504
  if (!existsSync(join(rootDir, 'package.json'))) {
236
- console.error('❌ Error: Must run this command inside the root of your workspace.');
237
- return;
505
+ cancel('Error: Must run this command inside the root of your workspace.');
506
+ process.exit(1);
238
507
  }
239
508
 
240
509
  let targetWorkerName = 'kama-worker';
241
- const cmsWranglerPath = join(rootDir, 'cms-worker', 'wrangler.json');
510
+ let detectedCmsDomain = '';
511
+ const workerDir = join(rootDir, 'kama-worker');
512
+
513
+ const cmsWranglerPath = join(workerDir, 'wrangler.json');
242
514
  if (existsSync(cmsWranglerPath)) {
243
515
  try {
244
516
  const cmsWrangler = JSON.parse(readFileSync(cmsWranglerPath, 'utf-8'));
245
517
  if (cmsWrangler.name) targetWorkerName = cmsWrangler.name;
518
+ if (cmsWrangler.vars?.CDN_DOMAIN) detectedCmsDomain = cmsWrangler.vars.CDN_DOMAIN;
519
+ else if (cmsWrangler.routes?.[0]?.pattern) detectedCmsDomain = cmsWrangler.routes[0].pattern;
246
520
  } catch (e) { }
247
521
  }
248
522
 
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';
523
+ // 🎯 Ensure secret exists in worker before provisioning site
524
+ const frontendApiSecret = existsSync(workerDir) ? getOrGenerateWorkspaceSecret(workerDir) : crypto.randomBytes(32).toString('hex');
525
+
526
+ const isCloudflare = handleCancel(await select({
527
+ message: 'Deployment Architecture for this site:',
528
+ options: [
529
+ { value: true, label: 'Cloudflare Edge' },
530
+ { value: false, label: 'VPS / Local Server' }
531
+ ]
532
+ }));
533
+
534
+ const siteFolder = handleCancel(await text({
535
+ message: 'Astro package/folder name (e.g., my-new-site):',
536
+ placeholder: 'kama-starter',
537
+ defaultValue: 'kama-starter'
538
+ }));
539
+
540
+ let siteDomain = 'localhost:4321';
541
+ let cmsFullUrl = 'http://localhost:8787';
542
+
543
+ if (isCloudflare) {
544
+ siteDomain = handleCancel(await text({ message: 'Website Domain (e.g., brand.com):', defaultValue: '' }));
545
+ if (detectedCmsDomain) {
546
+ cmsFullUrl = detectedCmsDomain.startsWith('http') ? detectedCmsDomain : `https://${detectedCmsDomain.replace(/^https?:\/\//, '')}`;
547
+ } else {
548
+ const cmsDomainInput = handleCancel(await text({ message: 'CMS API Domain (e.g., admin.brand.com):', defaultValue: '' }));
549
+ cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
550
+ }
551
+ }
552
+
553
+ const s = spinner();
554
+ s.start(`Generating Astro site...`);
252
555
 
253
- await generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain.replace(/^https?:\/\//, ''), targetWorkerName);
556
+ // 🎯 Pass secret to the frontend generator
557
+ await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare, frontendApiSecret);
254
558
 
255
- console.log(`\n✨ Site created in sites/${siteFolder}! Run 'npm install' at workspace root.`);
559
+ s.stop(`Site created in sites/${siteFolder}!`);
560
+
561
+ note(`1. npm install\n2. npm run dev`, 'Next steps');
562
+ outro('Site added successfully! 🚀');
563
+ }
564
+
565
+ /**
566
+ * COMMAND: npx kama bundle
567
+ */
568
+ async function bundleWorkspaceSchemas(targetRootDir, silent = false) {
569
+ let rootDir = targetRootDir || process.cwd();
570
+ if (basename(rootDir) === 'kama-worker') rootDir = resolve(rootDir, '..');
571
+
572
+ const s = spinner();
573
+ if (!silent) s.start('Bundling schemas...');
574
+ try {
575
+ let compilerPath = join(__dirname, '../dist/schema-compiler.js');
576
+ if (!existsSync(compilerPath)) compilerPath = join(__dirname, '../scripts/schema-compiler.ts');
577
+ await import(pathToFileURL(compilerPath).href);
578
+ if (!silent) s.stop('Schemas bundled successfully.');
579
+ } catch (e) {
580
+ if (!silent) s.stop(`Schema compilation failed: ${e.message}`, true);
581
+ }
256
582
  }
257
583
 
258
584
  /**
259
- * Helper: Scaffolds the Astro site with middleware, wrangler.json, & slug page
585
+ * COMMAND: npx kama promote
260
586
  */
261
- async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain, workerName = 'kama-worker') {
262
- console.log(`\n🚀 Scaffolding Astro site (${siteFolder}) mapped to ${siteDomain} (Service: ${workerName})...`);
587
+ async function promoteLocalToCloudflare() {
588
+ const rootDir = process.cwd();
589
+ const workerDir = join(rootDir, 'kama-worker');
590
+ const rootPkgPath = join(rootDir, 'package.json');
591
+
592
+ if (!existsSync(workerDir) || !existsSync(rootPkgPath)) {
593
+ cancel('Error: Could not find kama-worker folder or package.json.');
594
+ process.exit(1);
595
+ }
596
+
597
+ // 🎯 Safely persist the secret during promotion
598
+ const frontendApiSecret = getOrGenerateWorkspaceSecret(workerDir);
599
+
600
+ const s = spinner();
601
+ s.start('Checking Cloudflare authentication...');
602
+
603
+ const whoami = runWrangler('whoami');
604
+ if (whoami && (whoami.includes('Getting User Settings') || whoami.includes('You are logged in'))) {
605
+ s.stop('Logged in to Cloudflare.');
606
+ } else {
607
+ s.stop('Not logged in to Cloudflare.', true);
608
+ console.log(pc.dim('Launching browser for Cloudflare login...'));
609
+ const login = runWrangler('login', { stdio: 'inherit' });
610
+ if (login === null) {
611
+ cancel('Cloudflare login failed.');
612
+ process.exit(1);
613
+ }
614
+ }
615
+
616
+ const cmsDomain = handleCancel(await text({ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):', defaultValue: '' }));
617
+
618
+ const sitesFolderDir = join(rootDir, 'sites');
619
+ let siteDomain = '';
620
+ let siteDir = null;
621
+ if (existsSync(sitesFolderDir)) {
622
+ const sites = readdirSync(sitesFolderDir);
623
+ if (sites.length > 0) {
624
+ siteDir = join(sitesFolderDir, sites[0]);
625
+ siteDomain = handleCancel(await text({ message: `Frontend Website Domain for ${sites[0]}:`, defaultValue: '' }));
626
+ }
627
+ }
628
+
629
+ s.start('Upgrading workspace to Cloudflare Edge...');
630
+
631
+ const projName = basename(rootDir);
632
+ let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
633
+ let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
634
+
635
+ const d1Output = runWrangler(`d1 create ${dbName}`);
636
+ if (d1Output) {
637
+ 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}/);
638
+ if (match) d1DatabaseId = match[0];
639
+ }
640
+
641
+ const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
642
+ const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
643
+
644
+ const backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`);
645
+
646
+ const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
647
+ rootPkg.devDependencies = rootPkg.devDependencies || {};
648
+ rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
649
+ writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2));
650
+
651
+ const workerWranglerPath = join(workerDir, 'wrangler.json');
652
+ let currentConfig = existsSync(workerWranglerPath) ? JSON.parse(readFileSync(workerWranglerPath, 'utf-8')) : {
653
+ $schema: '../node_modules/wrangler/config-schema.json',
654
+ name: workerName,
655
+ main: 'index.js',
656
+ compatibility_date: '2024-09-23',
657
+ compatibility_flags: ['nodejs_compat'],
658
+ assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
659
+ images: { binding: 'IMAGES' },
660
+ vars: { CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID", KAMA_RUNTIME: 'cloudflare' }
661
+ };
662
+
663
+ currentConfig.dev = currentConfig.dev || { host: 'localhost', port: 8787 };
664
+ currentConfig.vars.CDN_DOMAIN = cmsDomain;
665
+ if (siteDomain) currentConfig.vars.PUBLIC_FRONTEND_URL = `https://${siteDomain}`;
666
+ currentConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
667
+ currentConfig.d1_databases = [{ binding: 'DB', database_name: dbName, database_id: d1DatabaseId }];
668
+ currentConfig.r2_buckets = [{ binding: 'MEDIA_BUCKET', bucket_name: bucketName }];
669
+ currentConfig.kv_namespaces = [{ binding: 'CACHE_KV', id: backendCacheId }];
670
+
671
+ writeFileSync(workerWranglerPath, JSON.stringify(currentConfig, null, 2));
672
+
673
+ if (!existsSync(join(workerDir, '.dev.vars'))) {
674
+ writeFileSync(join(workerDir, '.dev.vars'), `CLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\nCDN_DOMAIN=\nPUBLIC_FRONTEND_URL=http://localhost:4321\n\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`);
675
+ }
676
+
677
+ const workerPkgPath = join(workerDir, 'package.json');
678
+ if (existsSync(workerPkgPath)) {
679
+ const workerPkg = JSON.parse(readFileSync(workerPkgPath, 'utf-8'));
680
+ workerPkg.scripts.dev = "wrangler dev --port 8787 --inspector-port 9239";
681
+ workerPkg.scripts.deploy = "npm run setup:remote && wrangler deploy";
682
+ workerPkg.scripts["setup:local"] = `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`;
683
+ workerPkg.scripts["setup:remote"] = `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`;
684
+ workerPkg.devDependencies = workerPkg.devDependencies || {};
685
+ workerPkg.devDependencies.wrangler = WRANGLER_LATEST;
686
+ writeFileSync(workerPkgPath, JSON.stringify(workerPkg, null, 2));
687
+ }
688
+
689
+ if (siteDir && siteDomain) {
690
+ const prefix = basename(siteDir).replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
691
+ const sessionId = getOrCreateKV(`SESSION_${prefix}`);
692
+ const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`);
693
+
694
+ const siteWranglerPath = join(siteDir, 'wrangler.json');
695
+ const siteConfig = {
696
+ $schema: "node_modules/wrangler/config-schema.json",
697
+ name: `${basename(siteDir).toLowerCase().replace(/[^a-z0-9-]/g, '-')}-site`,
698
+ compatibility_date: "2024-09-23",
699
+ compatibility_flags: ["nodejs_compat"],
700
+ dev: { host: 'localhost', port: 4321 },
701
+ cache: { enabled: true },
702
+ assets: { directory: "./dist/client", binding: "ASSETS", run_worker_first: true },
703
+ kv_namespaces: [{ binding: "SESSION", id: sessionId }, { binding: "HTML_CACHE", id: htmlCacheId }],
704
+ services: [{ binding: "CMS_WORKER", service: currentConfig.name }],
705
+ routes: [{ pattern: siteDomain, zone_name: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }],
706
+ vars: { PUBLIC_CMS_URL: `https://${cmsDomain}`, PUBLIC_API_URL: `https://${cmsDomain}` }
707
+ };
708
+ writeFileSync(siteWranglerPath, JSON.stringify(siteConfig, null, 2));
709
+
710
+ const sitePkgPath = join(siteDir, 'package.json');
711
+ if (existsSync(sitePkgPath)) {
712
+ const sitePkg = JSON.parse(readFileSync(sitePkgPath, 'utf-8'));
713
+ sitePkg.dependencies = sitePkg.dependencies || {};
714
+ sitePkg.dependencies['astro'] = ASTRO_LATEST;
715
+ if (sitePkg.dependencies['@astrojs/node']) delete sitePkg.dependencies['@astrojs/node'];
716
+ sitePkg.dependencies['@astrojs/cloudflare'] = ASTRO_CF_LATEST;
717
+ sitePkg.devDependencies = sitePkg.devDependencies || {};
718
+ sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
719
+ writeFileSync(sitePkgPath, JSON.stringify(sitePkg, null, 2));
720
+ }
721
+
722
+ convertAstroConfigToCloudflare(siteDir);
723
+ }
724
+
725
+ s.stop('Workspace successfully upgraded to Cloudflare Edge!');
726
+ outro('Promotion complete. 🚀\nRemember to run `npx wrangler secret put FRONTEND_API_SECRET` in both your worker and site directories before production deployment!');
727
+ }
263
728
 
729
+ /**
730
+ * Helper: Copies the Astro starter template and hydrates runtime tokens
731
+ */
732
+ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, workerName = 'kama-worker', isCloudflare = false, frontendApiSecret = '') {
264
733
  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 });
734
+ const templateDir = join(__dirname, '../templates/astro-starter');
735
+
736
+ if (!existsSync(templateDir)) return;
737
+
738
+ cpSync(templateDir, siteDir, { recursive: true });
739
+
740
+ const tsConfig = { extends: "astro/tsconfigs/strict", compilerOptions: { baseUrl: ".", paths: { "@/*": ["src/*"] } } };
741
+ if (isCloudflare) tsConfig.compilerOptions.types = ["@cloudflare/workers-types"];
742
+ writeFileSync(join(siteDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
743
+
744
+ const templatePkgPath = join(siteDir, 'package.json.template');
745
+ if (existsSync(templatePkgPath)) {
746
+ const rawContent = readFileSync(templatePkgPath, 'utf-8')
747
+ .replace(/\{\{SITE_NAME\}\}/g, siteFolder)
748
+ .replace(/\{\{KAMA_VERSION\}\}/g, currentKamaVersion);
749
+ const sitePkg = JSON.parse(rawContent);
750
+
751
+ sitePkg.dependencies = sitePkg.dependencies || {};
752
+ sitePkg.dependencies['astro'] = ASTRO_LATEST;
753
+
754
+ if (!isCloudflare) {
755
+ if (sitePkg.dependencies['@astrojs/cloudflare']) delete sitePkg.dependencies['@astrojs/cloudflare'];
756
+ if (sitePkg.dependencies['wrangler']) delete sitePkg.dependencies['wrangler'];
757
+ if (sitePkg.devDependencies && sitePkg.devDependencies['wrangler']) delete sitePkg.devDependencies['wrangler'];
758
+ sitePkg.dependencies['@astrojs/node'] = ASTRO_NODE_LATEST;
759
+ } else {
760
+ sitePkg.dependencies['@astrojs/cloudflare'] = ASTRO_CF_LATEST;
761
+ if (sitePkg.dependencies['wrangler']) delete sitePkg.dependencies['wrangler'];
762
+ sitePkg.devDependencies = sitePkg.devDependencies || {};
763
+ sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
764
+ convertAstroConfigToCloudflare(siteDir);
765
+ }
269
766
 
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
- );
767
+ writeFileSync(join(siteDir, 'package.json'), JSON.stringify(sitePkg, null, 2));
768
+ unlinkSync(templatePkgPath);
769
+ }
291
770
 
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
- );
771
+ const siteLabel = siteFolder.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
772
+ const channelKey = siteFolder.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
773
+ const treeKey = 'main-tree';
774
+ const safeWorkerName = `${siteFolder.toLowerCase().replace(/[^a-z0-9-]/g, '-')}-site`;
775
+
776
+ if (isCloudflare) {
777
+ const prefix = siteFolder.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
778
+ const sessionId = getOrCreateKV(`SESSION_${prefix}`);
779
+ const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`);
780
+
781
+ const siteConfig = {
782
+ $schema: "node_modules/wrangler/config-schema.json",
783
+ name: safeWorkerName,
784
+ compatibility_date: "2024-09-23",
785
+ compatibility_flags: ["nodejs_compat"],
786
+ dev: { host: "localhost", port: 4321 },
787
+ cache: { enabled: true },
788
+ assets: { directory: "./dist/client", binding: "ASSETS", run_worker_first: true },
789
+ kv_namespaces: [{ binding: "SESSION", id: sessionId }, { binding: "HTML_CACHE", id: htmlCacheId }],
790
+ services: [{ binding: "CMS_WORKER", service: workerName }],
791
+ vars: { PUBLIC_CMS_URL: cmsFullUrl, PUBLIC_API_URL: cmsFullUrl, PUBLIC_CHANNEL_ID: channelKey, PUBLIC_TREE_ID: treeKey, PUBLIC_SITE_NAME: siteLabel }
792
+ };
793
+
794
+ if (siteDomain !== 'localhost:4321') siteConfig.routes = [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }];
795
+ writeFileSync(join(siteDir, 'wrangler.json'), JSON.stringify(siteConfig, null, 2));
796
+ }
315
797
 
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);
798
+ // 🎯 Dynamically append the secret to the Astro environment setup
799
+ let envContent = `# Local Development Overrides\nPUBLIC_CMS_URL="http://127.0.0.1:8787"\nPUBLIC_API_URL="http://127.0.0.1:8787"\nPUBLIC_CHANNEL_ID="${channelKey}"\nPUBLIC_TREE_ID="${treeKey}"\nPUBLIC_SITE_NAME="${siteLabel.replace(/"/g, '\\"')}"\n`;
336
800
 
337
- writeFileSync(
338
- join(siteDir, '.env'),
339
- `PUBLIC_CMS_URL="https://${cmsDomain}"\nPUBLIC_API_URL="https://${cmsDomain}"\n`
340
- );
801
+ if (frontendApiSecret) {
802
+ envContent += `\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`;
803
+ }
804
+
805
+ writeFileSync(join(siteDir, '.env'), envContent);
341
806
  }
342
807
 
343
808
  main().catch(console.error);