@brightsideoy/kama 0.1.0-alpha.99 → 1.0.0-alpha.2

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