@brightsideoy/kama 0.1.0-alpha.14 → 0.1.0-alpha.140

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 (85) hide show
  1. package/README.md +195 -0
  2. package/bin/cli.js +624 -180
  3. package/content/definitions/block-layouts/three-column.yaml +20 -0
  4. package/content/definitions/block-layouts/two-column.yaml +29 -0
  5. package/content/definitions/blocks/button.yaml +24 -0
  6. package/content/definitions/blocks/header.yaml +13 -0
  7. package/content/definitions/blocks/header_image.yaml +21 -0
  8. package/content/definitions/blocks/hero.yaml +28 -0
  9. package/content/definitions/blocks/html.yaml +12 -0
  10. package/content/definitions/blocks/image_card.yaml +56 -0
  11. package/content/definitions/blocks/single_image.yaml +27 -0
  12. package/content/definitions/blocks/text.yaml +15 -0
  13. package/content/definitions/channels/ecommerce.yaml +28 -0
  14. package/content/definitions/channels/website.yaml +41 -0
  15. package/content/definitions/field-sets/access.yaml +27 -0
  16. package/content/definitions/field-sets/channel-base.yaml +50 -0
  17. package/content/definitions/field-sets/channel-seo.yaml +21 -0
  18. package/content/definitions/field-sets/comments.yaml +11 -0
  19. package/content/definitions/field-sets/content-block.yaml +41 -0
  20. package/content/definitions/field-sets/page-base.yaml +44 -0
  21. package/content/definitions/field-sets/page-seo.yaml +34 -0
  22. package/content/definitions/field-sets/page-social.yaml +78 -0
  23. package/content/definitions/field-sets/schema-type.yaml +43 -0
  24. package/content/definitions/field-sets/tree-base.yaml +31 -0
  25. package/content/definitions/field-sets/visibility.yaml +8 -0
  26. package/content/definitions/node-layouts/content.yml +12 -0
  27. package/content/definitions/node-layouts/landing-page.yml +25 -0
  28. package/content/definitions/node-layouts/single-column.yml +41 -0
  29. package/content/definitions/node-layouts/two-column.yml +46 -0
  30. package/content/definitions/nodes/address.yaml +40 -0
  31. package/content/definitions/nodes/article.yaml +37 -0
  32. package/content/definitions/nodes/event.yaml +55 -0
  33. package/content/definitions/nodes/external-link.yaml +29 -0
  34. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  35. package/content/definitions/nodes/page.yaml +70 -0
  36. package/content/definitions/nodes/person.yaml +41 -0
  37. package/content/definitions/nodes/product.yaml +89 -0
  38. package/content/definitions/trees/navigation.yaml +24 -0
  39. package/definitions/editor/button.ts +11 -0
  40. package/definitions/editor/default.ts +21 -0
  41. package/definitions/editor/header.ts +36 -0
  42. package/definitions/editor/minimal.ts +15 -0
  43. package/definitions/validators/iban.ts +12 -0
  44. package/definitions/validators/magic-word.ts +7 -0
  45. package/dist/assets/index-Bv7fPRX7.css +1 -0
  46. package/dist/assets/index-D5P3jaVv.js +162 -0
  47. package/dist/index.html +13 -0
  48. package/dist/index.js +12327 -2110
  49. package/documentation/SCHEMAS.md +182 -0
  50. package/package.json +12 -14
  51. package/schema.sql +204 -0
  52. package/templates/astro-starter/astro.config.mjs +40 -0
  53. package/templates/astro-starter/middleware/api-purge.ts +130 -0
  54. package/templates/astro-starter/middleware/index.ts +10 -0
  55. package/templates/astro-starter/middleware/kv-cache.ts +76 -0
  56. package/templates/astro-starter/middleware/response-headers.ts +43 -0
  57. package/templates/astro-starter/package.json.template +20 -0
  58. package/templates/astro-starter/public/css/standard-theme.css +157 -0
  59. package/templates/astro-starter/public/robots.txt +23 -0
  60. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +133 -0
  61. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
  62. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
  63. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  64. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +24 -0
  65. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
  66. package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
  67. package/templates/astro-starter/src/components/global/Breadcrumb.astro +65 -0
  68. package/templates/astro-starter/src/components/global/Footer.astro +48 -0
  69. package/templates/astro-starter/src/components/global/Header.astro +101 -0
  70. package/templates/astro-starter/src/components/global/NavigationMenu.astro +466 -0
  71. package/templates/astro-starter/src/components/seo/SeoHead.astro +150 -0
  72. package/templates/astro-starter/src/components/ui/KamaImage.astro +149 -0
  73. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +45 -0
  74. package/templates/astro-starter/src/layouts/StandardLayout.astro +52 -0
  75. package/templates/astro-starter/src/lib/cms.ts +149 -0
  76. package/templates/astro-starter/src/lib/media/kama-image-service.ts +41 -0
  77. package/templates/astro-starter/src/lib/media/media-proxy.ts +127 -0
  78. package/templates/astro-starter/src/lib/media/media-url.ts +34 -0
  79. package/templates/astro-starter/src/pages/404.astro +46 -0
  80. package/templates/astro-starter/src/pages/[...slug].astro +224 -0
  81. package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
  82. package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
  83. package/templates/astro-starter/src/pages/media/[...path].ts +7 -0
  84. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  85. package/templates/astro-starter/tsconfig.json +0 -0
package/bin/cli.js CHANGED
@@ -1,137 +1,414 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { join } from 'node:path';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, cpSync, unlinkSync, statSync, appendFileSync } from 'node:fs';
4
+ import { join, dirname, basename, relative, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import * as readline from 'node:readline/promises';
6
7
 
8
+ // Dynamically resolve current package version
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ let currentKamaVersion = 'alpha';
12
+ try {
13
+ const pkgPath = join(__dirname, '../package.json');
14
+ if (existsSync(pkgPath)) {
15
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
16
+ if (pkg.version) currentKamaVersion = `^${pkg.version}`;
17
+ }
18
+ } catch (e) {
19
+ console.warn("⚠️ Could not detect Kama version, defaulting to 'alpha'");
20
+ }
21
+
7
22
  const rl = readline.createInterface({
8
23
  input: process.stdin,
9
24
  output: process.stdout,
10
25
  });
11
26
 
27
+ function showHelp() {
28
+ console.log('\n🔥 --- KAMA ENGINE CLI --- 🔥\n');
29
+ console.log('Usage: npx kama <command> [options]\n');
30
+ console.log('Commands:');
31
+ console.log(' init Kickstart a new Monorepo (CMS Worker + Optional Astro Site)');
32
+ console.log(' add site Add a new Astro Frontend Site to the current workspace (sites/)');
33
+ console.log(' bundle Bundle YAML schemas into a static module for Edge deployment');
34
+ console.log(' promote Promote the current Local workspace to Cloudflare Edge');
35
+ console.log(' help, -h Show this help message\n');
36
+
37
+ console.log('Workspace Scripts (Run from project root):');
38
+ console.log(' npm update Update to latest KAMA version');
39
+ console.log(' npm run dev Boot up the CMS backend and Astro frontend concurrently');
40
+ console.log(' npm run deploy:cms Deploy the backend worker and database to Cloudflare');
41
+ console.log(' npm run deploy:site Deploy the Astro frontend site to Cloudflare\n');
42
+
43
+ console.log('Examples:');
44
+ console.log(' npx kama init');
45
+ console.log(' npx kama add site');
46
+ console.log(' npx kama bundle\n');
47
+ }
48
+
12
49
  async function main() {
13
- console.log('\n🔥 --- KAMA SITE GENERATOR --- 🔥\n');
50
+ const args = process.argv.slice(2);
51
+ const command = args[0]?.toLowerCase();
52
+ const subCommand = args[1]?.toLowerCase();
14
53
 
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/)');
54
+ if (command === 'help' || command === '--help' || command === '-h') {
55
+ showHelp();
56
+ rl.close();
57
+ process.exit(0);
58
+ }
18
59
 
19
- const choice = await rl.question('\nChoice [1/2]: ');
60
+ console.log('\n🔥 --- KAMA ENGINE CLI --- 🔥\n');
20
61
 
21
- if (choice.trim() === '2') {
62
+ if (command === 'init') {
63
+ await createFullWorkspace();
64
+ } else if (command === 'add' && subCommand === 'site') {
22
65
  await createNewSiteOnly();
66
+ } else if (command === 'promote') {
67
+ await promoteLocalToCloudflare();
68
+ } else if (command === 'bundle') {
69
+ await bundleWorkspaceSchemas();
23
70
  } else {
24
- await createFullWorkspace();
71
+ console.log('Available Commands:');
72
+ console.log(' 1) npx kama init - Kickstart new Monorepo (CMS + Optional Site)');
73
+ console.log(' 2) npx kama add site - Add new Astro Site to current workspace (sites/)');
74
+ console.log(' 3) npx kama bundle - Bundle YAML schemas for Edge deployment');
75
+ console.log(' 4) npx kama promote - Promote current Local workspace to Cloudflare Edge');
76
+ console.log(' 5) Help - Show all commands and usage');
77
+
78
+ const choice = await rl.question('\nSelect choice [1/2/3/4/5]: ');
79
+
80
+ if (choice.trim() === '5') {
81
+ showHelp();
82
+ } else if (choice.trim() === '4') {
83
+ await promoteLocalToCloudflare();
84
+ } else if (choice.trim() === '3') {
85
+ await bundleWorkspaceSchemas();
86
+ } else if (choice.trim() === '2') {
87
+ await createNewSiteOnly();
88
+ } else {
89
+ await createFullWorkspace();
90
+ }
25
91
  }
26
92
 
27
93
  rl.close();
28
94
  }
29
95
 
30
96
  /**
31
- * MODE 1: Kickstart full Monorepo Workspace
97
+ * COMMAND: npx kama init
98
+ * Kickstart full Monorepo Workspace
32
99
  */
33
100
  async function createFullWorkspace() {
34
- const projName = (await rl.question('Project folder name [kama-network]: ')) || 'kama-network';
35
- const domain = (await rl.question('Base domain [t3cloud.eu]: ')) || 't3cloud.eu';
36
- const sitePrefix = (await rl.question('First site prefix/subdomain [kamafe]: ')) || 'kamafe';
101
+ const cwdName = basename(process.cwd());
102
+ const hasExistingPkg = existsSync(join(process.cwd(), 'package.json'));
103
+
104
+ const defaultDirPrompt = hasExistingPkg ? '.' : cwdName;
105
+ const inputName = await rl.question(`Project folder name (use '.' for current dir) [${defaultDirPrompt}]: `);
106
+
107
+ const chosenFolder = inputName.trim() || defaultDirPrompt;
108
+ const isCurrentDir = chosenFolder === '.';
109
+
110
+ const projName = isCurrentDir ? cwdName : chosenFolder;
111
+ const rootDir = isCurrentDir ? process.cwd() : join(process.cwd(), projName);
37
112
 
38
- const rootDir = join(process.cwd(), projName);
39
- const cmsDomain = `kama.${domain}`;
40
- const siteDomain = `${sitePrefix}.${domain}`;
113
+ console.log('\n🌍 Where do you plan to deploy KAMA?');
114
+ console.log(' 1) Local Development Only (SQLite / Testing)');
115
+ console.log(' 2) Cloudflare Edge (Workers / D1 / R2)');
116
+ const envChoice = await rl.question('\nChoice [1/2]: ');
117
+ const isCloudflare = envChoice.trim() === '2';
41
118
 
42
- console.log(`\n📁 Creating workspace folder at ./${projName}...`);
43
- mkdirSync(rootDir, { recursive: true });
119
+ const createSiteInput = await rl.question('\n🚀 Do you want to scaffold an initial Astro frontend site? (y/n) [y]: ');
120
+ const shouldCreateSite = createSiteInput.trim().toLowerCase() !== 'n';
121
+
122
+ let siteFolderName = 'kama-starter';
123
+ let siteDomain = 'localhost:4321';
124
+ let cmsDomain = 'localhost:8787';
125
+
126
+ if (shouldCreateSite) {
127
+ // 🎯 FIX: Default to 'kama-starter'
128
+ siteFolderName = (await rl.question('Astro package/folder name (e.g., my-website) [kama-starter]: ')).trim() || 'kama-starter';
129
+ }
130
+
131
+ if (isCloudflare) {
132
+ cmsDomain = (await rl.question('\nCMS Admin Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
133
+ if (shouldCreateSite) {
134
+ siteDomain = (await rl.question('First Website Domain (e.g., kama.t3cloud.eu): ')) || 'kama.t3cloud.eu';
135
+ }
136
+ }
137
+
138
+ const cmsProtocol = isCloudflare ? 'https://' : 'http://';
139
+ const cmsFullUrl = `${cmsProtocol}${cmsDomain.replace(/^https?:\/\//, '')}`;
140
+
141
+ const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
142
+ let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
143
+ const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
144
+
145
+ if (!isCurrentDir) {
146
+ console.log(`\n📁 Creating workspace folder at ./${projName}...`);
147
+ mkdirSync(rootDir, { recursive: true });
148
+ } else {
149
+ console.log(`\n📁 Initializing workspace in current directory (${rootDir})...`);
150
+ }
151
+
152
+ writeFileSync(
153
+ join(rootDir, '.gitignore'),
154
+ `node_modules/
155
+ .env
156
+ .DS_Store
157
+ kama-worker/.dev.vars
158
+ kama-worker/.wrangler/
159
+ sites/*/.env
160
+ sites/*/.astro/
161
+ sites/*/dist/
162
+ kama-worker/workspace-schemas.js
163
+ `
164
+ );
165
+
166
+ console.log(`\n📄 Copying default content definitions...`);
167
+ const sourceDefsDir = join(__dirname, '../content/definitions');
168
+ const targetDefsDir = join(rootDir, 'content', 'definitions');
169
+ if (existsSync(sourceDefsDir)) {
170
+ cpSync(sourceDefsDir, targetDefsDir, { recursive: true });
171
+ } else {
172
+ console.warn(`⚠️ Could not find default content definitions at ${sourceDefsDir}`);
173
+ }
174
+
175
+ let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
176
+ let isAuthenticated = false;
177
+
178
+ if (isCloudflare) {
179
+ console.log(`\n🔑 Checking Cloudflare authentication...`);
180
+ try {
181
+ execSync('npx wrangler whoami', { stdio: 'pipe' });
182
+ isAuthenticated = true;
183
+ console.log(`✅ Logged in to Cloudflare.`);
184
+ } catch (error) {
185
+ console.log(`⚠️ You are not logged in to Cloudflare.`);
186
+ console.log(`🌐 Launching browser for Cloudflare login...`);
187
+ try {
188
+ execSync('npx wrangler login', { stdio: 'inherit' });
189
+ isAuthenticated = true;
190
+ console.log(`✅ Successfully logged in!`);
191
+ } catch (loginError) {
192
+ console.log(`❌ Login failed or was cancelled.`);
193
+ }
194
+ }
195
+
196
+ if (isAuthenticated) {
197
+ try {
198
+ console.log(`\n🗄️ Checking existing Cloudflare D1 databases...`);
199
+ const listOutput = execSync(`npx wrangler d1 list --json`, { encoding: 'utf-8', stdio: 'pipe' });
200
+ const jsonStart = listOutput.indexOf('[');
201
+ const jsonEnd = listOutput.lastIndexOf(']') + 1;
202
+ let databases = [];
203
+
204
+ if (jsonStart !== -1 && jsonEnd !== -1) {
205
+ databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
206
+ }
207
+
208
+ const existingDb = databases.find(db => db.name === dbName);
209
+
210
+ if (existingDb) {
211
+ console.log(`⚠️ Database '${dbName}' already exists on your account.`);
212
+ const useExisting = await rl.question(`Do you want to link this existing database? (y/n) [y]: `);
213
+
214
+ if (useExisting.toLowerCase() !== 'n') {
215
+ d1DatabaseId = existingDb.uuid;
216
+ console.log(`✅ Linked existing database ID: ${d1DatabaseId}`);
217
+ } else {
218
+ dbName = (await rl.question(`Enter a new database name: `)).trim();
219
+ console.log(`\n🗄️ Creating new database '${dbName}'...`);
220
+ const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
221
+ 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}/);
222
+ if (match) {
223
+ d1DatabaseId = match[0];
224
+ console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
225
+ }
226
+ }
227
+ } else {
228
+ console.log(`\n🗄️ Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
229
+ const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
230
+ 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}/);
231
+ if (match) {
232
+ d1DatabaseId = match[0];
233
+ console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
234
+ }
235
+ }
236
+ } catch (error) {
237
+ console.log(`⚠️ Database operation failed. We'll leave a placeholder.`);
238
+ }
239
+ } else {
240
+ console.log(`⚠️ Skipping database creation. We'll leave a placeholder in wrangler.json.`);
241
+ }
242
+ } else {
243
+ console.log(`\n💻 Local mode selected. Skipping Cloudflare authentication and domain setup...`);
244
+ }
245
+
246
+ const rootScripts = {
247
+ 'dev:cms': 'npm run dev -w kama-worker',
248
+ 'deploy:cms': 'npm run deploy -w kama-worker',
249
+ };
250
+
251
+ const rootPkg = {
252
+ name: projName,
253
+ private: true,
254
+ type: 'module',
255
+ workspaces: ['kama-worker', 'sites/*'],
256
+ scripts: rootScripts,
257
+ dependencies: {
258
+ '@brightsideoy/kama': currentKamaVersion,
259
+ },
260
+ devDependencies: {
261
+ 'wrangler': '^4.130.0'
262
+ }
263
+ };
264
+
265
+ const bundleScript = 'npx kama bundle || node ./node_modules/@brightsideoy/kama/bin/cli.js bundle';
266
+
267
+ if (shouldCreateSite) {
268
+ rootPkg.devDependencies['concurrently'] = '^9.0.0';
269
+ rootPkg.scripts['dev:site'] = `npm run dev -w sites/${siteFolderName}`;
270
+
271
+ rootPkg.scripts['predev'] = bundleScript;
272
+ rootPkg.scripts['dev'] = 'npx concurrently "npm run dev:cms" "npm run dev:site"';
273
+ rootPkg.scripts['deploy:site'] = `npm run deploy -w sites/${siteFolderName}`;
274
+ } else {
275
+ rootPkg.scripts['predev'] = bundleScript;
276
+ rootPkg.scripts['dev'] = 'npm run dev:cms';
277
+ }
44
278
 
45
- // 1. Root package.json
46
279
  writeFileSync(
47
280
  join(rootDir, 'package.json'),
48
- JSON.stringify(
49
- {
50
- name: projName,
51
- private: true,
52
- type: 'module',
53
- workspaces: ['cms-worker', 'sites/*'],
54
- scripts: {
55
- 'dev:cms': 'npm run dev -w cms-worker',
56
- 'deploy:cms': 'npm run deploy -w cms-worker',
57
- },
58
- },
59
- null,
60
- 2
61
- )
281
+ JSON.stringify(rootPkg, null, 2)
62
282
  );
63
283
 
64
- // 2. Setup CMS Worker
65
- console.log(`\n☁️ Configuring CMS Worker (${cmsDomain})...`);
66
- const cmsDir = join(rootDir, 'cms-worker');
284
+ console.log(`\n☁️ Configuring CMS Worker '${workerName}' (${cmsDomain})...`);
285
+ const cmsDir = join(rootDir, 'kama-worker');
67
286
  mkdirSync(cmsDir, { recursive: true });
68
287
 
288
+ writeFileSync(join(cmsDir, 'seed.sql'), '-- Kama Auto-Generated Seed File\nSELECT 1;\n');
289
+
290
+ const cmsWorkerIndexContent = `import kamaWorker from '@brightsideoy/kama';
291
+ import customSchemas from './workspace-schemas.js';
292
+
293
+ export default {
294
+ async fetch(request, env, ctx) {
295
+ // 🎯 FIX: Mutate the Cloudflare env directly instead of spreading it!
296
+ // Spreading {...env} breaks Cloudflare D1/R2 proxy bindings.
297
+ env.BUNDLED_DEFINITIONS = customSchemas;
298
+
299
+ // Execute the Hono app fetch handler
300
+ if (kamaWorker && typeof kamaWorker.fetch === 'function') {
301
+ return kamaWorker.fetch(request, env, ctx);
302
+ }
303
+
304
+ // Fallback just in case the export structure changes
305
+ if (kamaWorker && kamaWorker.app && typeof kamaWorker.app.fetch === 'function') {
306
+ return kamaWorker.app.fetch(request, env, ctx);
307
+ }
308
+
309
+ return new Response('Kama Engine Worker Ready', { status: 200 });
310
+ }
311
+ };
312
+ `;
313
+
314
+ writeFileSync(join(cmsDir, 'index.js'), cmsWorkerIndexContent);
315
+
69
316
  writeFileSync(
70
317
  join(cmsDir, 'package.json'),
71
- JSON.stringify(
72
- {
73
- name: 'kama-worker',
74
- type: 'module',
75
- version: '1.0.0',
76
- scripts: {
77
- dev: 'wrangler dev',
78
- deploy: 'wrangler deploy',
79
- },
80
- dependencies: {
81
- '@brightsideoy/kama': 'latest',
82
- },
83
- devDependencies: {
84
- wrangler: '^4.127.1',
85
- tsx: '^4.19.2',
86
- },
318
+ JSON.stringify({
319
+ name: workerName,
320
+ type: 'module',
321
+ version: '1.0.0',
322
+ scripts: {
323
+ "schema:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql > /dev/null 2>&1`,
324
+ "schema:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`,
325
+ "seed:local": `wrangler d1 execute ${dbName} --local --file=seed.sql > /dev/null 2>&1`,
326
+ "seed:remote": `wrangler d1 execute ${dbName} --remote --file=seed.sql`,
327
+ "dev": "npm run schema:local && npm run seed:local && wrangler dev",
328
+ "deploy": "npm run schema:remote && npm run seed:remote && wrangler deploy"
87
329
  },
88
- null,
89
- 2
90
- )
330
+ dependencies: {
331
+ '@brightsideoy/kama': currentKamaVersion,
332
+ },
333
+ devDependencies: {
334
+ "wrangler": "^4.130.0",
335
+ "tsx": "^4.19.2",
336
+ },
337
+ }, null, 2)
91
338
  );
92
339
 
340
+ const cmsWranglerConfig = {
341
+ $schema: '../node_modules/wrangler/config-schema.json',
342
+ name: workerName,
343
+ main: 'index.js',
344
+ compatibility_date: '2026-08-01',
345
+ compatibility_flags: ['nodejs_compat'],
346
+ assets: {
347
+ directory: '../node_modules/@brightsideoy/kama/dist',
348
+ binding: 'ASSETS'
349
+ },
350
+ images: {
351
+ binding: 'IMAGES'
352
+ },
353
+ vars: {
354
+ CDN_DOMAIN: cmsDomain,
355
+ PUBLIC_FRONTEND_URL: (isCloudflare && shouldCreateSite) ? `https://${siteDomain}` : 'http://localhost:4321',
356
+ CLOUDFLARE_ZONE_ID: isCloudflare ? "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID" : "local-zone-not-needed"
357
+ },
358
+ d1_databases: [{
359
+ binding: 'DB',
360
+ database_name: dbName,
361
+ database_id: d1DatabaseId,
362
+ }],
363
+ r2_buckets: [{
364
+ binding: 'MEDIA_BUCKET',
365
+ bucket_name: bucketName,
366
+ }],
367
+ };
368
+
369
+ if (isCloudflare) {
370
+ cmsWranglerConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
371
+ }
372
+
373
+ writeFileSync(join(cmsDir, 'wrangler.json'), JSON.stringify(cmsWranglerConfig, null, 2));
374
+
93
375
  writeFileSync(
94
- join(cmsDir, 'wrangler.json'),
95
- JSON.stringify(
96
- {
97
- $schema: 'node_modules/wrangler/config-schema.json',
98
- name: 'kama-worker',
99
- main: 'node_modules/@brightsideoy/kama/dist/index.js',
100
- compatibility_date: '2026-08-01',
101
- compatibility_flags: ['nodejs_compat'],
102
- assets: { directory: './dist', binding: 'ASSETS' },
103
- routes: [{ pattern: cmsDomain, custom_domain: true }],
104
- vars: { CDN_DOMAIN: cmsDomain },
105
- d1_databases: [
106
- {
107
- binding: 'DB',
108
- database_name: 'kama_cms_db',
109
- database_id: 'REPLACE_WITH_YOUR_D1_ID',
110
- },
111
- ],
112
- r2_buckets: [
113
- {
114
- binding: 'MEDIA_BUCKET',
115
- bucket_name: 'kama-media-prod',
116
- },
117
- ],
118
- },
119
- null,
120
- 2
121
- )
376
+ join(cmsDir, '.dev.vars'),
377
+ `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=${isCloudflare ? 'cfut_YOUR_CUSTOM_PURGE_TOKEN' : 'local_token'}\n`
122
378
  );
123
379
 
124
- // 3. Setup First Astro Site
125
- await generateAstroSite(rootDir, sitePrefix, siteDomain, cmsDomain);
380
+ await bundleWorkspaceSchemas(rootDir);
381
+
382
+ if (shouldCreateSite) {
383
+ await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsFullUrl, workerName, isCloudflare);
384
+ } else {
385
+ console.log(`\nℹ️ Skipping Astro site creation. You can add a site later by running 'npx kama add site' inside this workspace.`);
386
+ }
126
387
 
127
388
  console.log(`\n✨ Workspace ready! Next steps:`);
128
- console.log(` 1. cd ${projName}`);
129
- console.log(` 2. npm install`);
130
- console.log(` 3. Update D1 ID in cms-worker/wrangler.json`);
389
+ if (!isCurrentDir) {
390
+ console.log(` 1. cd ${projName}`);
391
+ console.log(` 2. npm install`);
392
+ } else {
393
+ console.log(` 1. npm install`);
394
+ }
395
+
396
+ if (isCloudflare) {
397
+ if (d1DatabaseId === '00000000-0000-0000-0000-000000000000' || d1DatabaseId === 'REPLACE_WITH_YOUR_D1_ID') {
398
+ console.log(` 3. Run 'npx wrangler login' and create a D1 database manually.`);
399
+ }
400
+ console.log(` 4. Update CLOUDFLARE_ZONE_ID in kama-worker/wrangler.json with your actual Zone ID`);
401
+ console.log(` 5. Securely store your purge token: cd kama-worker && npx wrangler secret put CLOUDFLARE_API_TOKEN`);
402
+ console.log(` 6. Run 'npm run dev' from the workspace root to start everything.`);
403
+ } else {
404
+ console.log(` 3. Run 'npm run dev' from the workspace root to start the local backend and frontend.`);
405
+ console.log(`\n💡 Note: You can deploy to Cloudflare later by running 'npx kama promote'.`);
406
+ }
131
407
  }
132
408
 
133
409
  /**
134
- * MODE 2: Add Site to existing workspace
410
+ * COMMAND: npx kama add site
411
+ * Add Astro Site to existing workspace
135
412
  */
136
413
  async function createNewSiteOnly() {
137
414
  const rootDir = process.cwd();
@@ -141,108 +418,275 @@ async function createNewSiteOnly() {
141
418
  return;
142
419
  }
143
420
 
144
- const siteName = await rl.question('Site folder name (e.g., site-two): ');
145
- const customDomain = await rl.question('Custom domain (e.g., brand.com or brand.t3cloud.eu): ');
146
- const cmsDomain = (await rl.question('CMS API URL [https://kama.t3cloud.eu]: ')) || 'https://kama.t3cloud.eu';
421
+ let targetWorkerName = 'kama-worker';
422
+ const cmsWranglerPath = join(rootDir, 'kama-worker', 'wrangler.json');
423
+ if (existsSync(cmsWranglerPath)) {
424
+ try {
425
+ const cmsWrangler = JSON.parse(readFileSync(cmsWranglerPath, 'utf-8'));
426
+ if (cmsWrangler.name) targetWorkerName = cmsWrangler.name;
427
+ } catch (e) { }
428
+ }
429
+
430
+ console.log('\n🌍 Deployment Mode:');
431
+ console.log(' 1) Local Development');
432
+ console.log(' 2) Cloudflare Edge');
433
+ const envChoice = await rl.question('\nChoice [1/2]: ');
434
+ const isCloudflare = envChoice.trim() === '2';
435
+
436
+ // 🎯 FIX: Default to 'kama-starter'
437
+ const siteFolder = (await rl.question('Astro package/folder name (e.g., my-new-site) [kama-starter]: ')).trim() || 'kama-starter';
438
+
439
+ let siteDomain = 'localhost:4321';
440
+ let cmsFullUrl = 'http://localhost:8787';
441
+
442
+ if (isCloudflare) {
443
+ siteDomain = await rl.question('Website Domain (e.g., brand.com): ');
444
+ const cmsDomainInput = (await rl.question('CMS API Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
445
+ cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
446
+ }
447
+
448
+ await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare);
449
+
450
+ console.log(`\n✨ Site created in sites/${siteFolder}!`);
451
+ console.log(`📦 Run 'npm install' at workspace root.`);
452
+ console.log(`🌱 Run 'npm run dev' to boot up and seed the new site data.`);
453
+ }
454
+
455
+ /**
456
+ * COMMAND: npx kama bundle
457
+ * Reads YAML from workspace and builds a static module for the worker
458
+ */
459
+ async function bundleWorkspaceSchemas(targetRootDir) {
460
+ const rootDir = targetRootDir || process.cwd();
461
+ const defsDir = join(rootDir, 'content', 'definitions');
462
+ const workerDir = join(rootDir, 'kama-worker');
463
+
464
+ if (!existsSync(defsDir)) {
465
+ console.warn(`\n⚠️ Skipping schema bundling: 'content/definitions' folder not found at ${defsDir}`);
466
+ return;
467
+ }
468
+
469
+ console.log(`\n📦 Bundling workspace YAML schemas for Edge deployment...`);
470
+
471
+ function crawlDirectory(dir, baseDir, result = {}) {
472
+ if (!existsSync(dir)) return result;
473
+ const items = readdirSync(dir);
474
+ for (const item of items) {
475
+ const fullPath = join(dir, item);
476
+ if (existsSync(fullPath) && statSync(fullPath).isDirectory()) {
477
+ crawlDirectory(fullPath, baseDir, result);
478
+ } else if (fullPath.endsWith('.yaml') || fullPath.endsWith('.yml')) {
479
+ const relativePath = './' + relative(baseDir, fullPath).replace(/\\/g, '/');
480
+ result[relativePath] = readFileSync(fullPath, 'utf-8');
481
+ }
482
+ }
483
+ return result;
484
+ }
147
485
 
148
- await generateAstroSite(rootDir, siteName, customDomain, cmsDomain.replace(/^https?:\/\//, ''));
486
+ const rawGlobs = {
487
+ nodes: crawlDirectory(join(defsDir, 'nodes'), defsDir),
488
+ trees: crawlDirectory(join(defsDir, 'trees'), defsDir),
489
+ blocks: crawlDirectory(join(defsDir, 'blocks'), defsDir),
490
+ nodeLayouts: crawlDirectory(join(defsDir, 'node-layouts'), defsDir),
491
+ blockLayouts: crawlDirectory(join(defsDir, 'block-layouts'), defsDir),
492
+ fieldSets: crawlDirectory(join(defsDir, 'field-sets'), defsDir),
493
+ channels: crawlDirectory(join(defsDir, 'channels'), defsDir),
494
+ };
495
+
496
+ const fileContent = `// AUTO-GENERATED BUNDLE - DO NOT EDIT
497
+ // This file is generated by 'npx kama bundle' prior to local dev or deployment.
498
+
499
+ export default ${JSON.stringify(rawGlobs, null, 2)};
500
+ `;
501
+
502
+ if (!existsSync(workerDir)) {
503
+ mkdirSync(workerDir, { recursive: true });
504
+ }
149
505
 
150
- console.log(`\n✨ Site created in sites/${siteName}! Run 'npm install' at workspace root.`);
506
+ const outPath = join(workerDir, 'workspace-schemas.js');
507
+ writeFileSync(outPath, fileContent, 'utf-8');
508
+
509
+ console.log(`✅ Schema bundle generated successfully at ${outPath}`);
151
510
  }
152
511
 
153
512
  /**
154
- * Helper: Scaffolds the Astro site with middleware, wrangler.json, & slug page
513
+ * COMMAND: npx kama promote
514
+ * Promote current Local workspace to Cloudflare Edge
155
515
  */
156
- async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain) {
157
- console.log(`\n🚀 Scaffolding Astro site (${siteFolder}) mapped to ${siteDomain}...`);
516
+ async function promoteLocalToCloudflare() {
517
+ const rootDir = process.cwd();
518
+ const workerWranglerPath = join(rootDir, 'kama-worker', 'wrangler.json');
519
+
520
+ if (!existsSync(workerWranglerPath)) {
521
+ console.error('❌ Error: Could not find kama-worker/wrangler.json. Run this command inside your workspace root.');
522
+ return;
523
+ }
524
+
525
+ console.log('\n🌐 --- PROMOTING WORKSPACE TO CLOUDFLARE EDGE --- 🌐\n');
526
+
527
+ try {
528
+ execSync('npx wrangler whoami', { stdio: 'pipe' });
529
+ console.log('✅ Logged in to Cloudflare.');
530
+ } catch (e) {
531
+ console.log('🌐 Launching browser for Cloudflare login...');
532
+ try {
533
+ execSync('npx wrangler login', { stdio: 'inherit' });
534
+ } catch (loginError) {
535
+ console.log('❌ Cloudflare login failed or was cancelled.');
536
+ return;
537
+ }
538
+ }
539
+
540
+ const cmsDomain = (await rl.question('\nCMS Admin Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
541
+
542
+ const sitesFolderDir = join(rootDir, 'sites');
543
+ let siteDomain = '';
544
+ if (existsSync(sitesFolderDir)) {
545
+ siteDomain = await rl.question('Frontend Website Domain (e.g., kama.t3cloud.eu): ');
546
+ }
547
+
548
+ const currentConfig = JSON.parse(readFileSync(workerWranglerPath, 'utf-8'));
549
+ const dbName = currentConfig.d1_databases?.[0]?.database_name || 'kama_db';
550
+
551
+ console.log(`\n🗄️ Provisioning remote Cloudflare D1 Database '${dbName}'...`);
552
+ let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
553
+
554
+ try {
555
+ const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
556
+ 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}/);
557
+ if (match) d1DatabaseId = match[0];
558
+ } catch (err) {
559
+ console.log('⚠️ Could not auto-create D1 (it may already exist). Please verify on your Cloudflare dashboard.');
560
+ }
561
+
562
+ currentConfig.vars.CDN_DOMAIN = cmsDomain;
563
+ if (siteDomain) {
564
+ currentConfig.vars.PUBLIC_FRONTEND_URL = `https://${siteDomain}`;
565
+ }
566
+ currentConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
567
+ if (currentConfig.d1_databases?.[0]) {
568
+ currentConfig.d1_databases[0].database_id = d1DatabaseId;
569
+ }
570
+
571
+ writeFileSync(workerWranglerPath, JSON.stringify(currentConfig, null, 2));
572
+
573
+ if (siteDomain && existsSync(sitesFolderDir)) {
574
+ const sites = readdirSync(sitesFolderDir);
575
+ if (sites.length > 0) {
576
+ const siteWranglerPath = join(sitesFolderDir, sites[0], 'wrangler.json');
577
+ if (existsSync(siteWranglerPath)) {
578
+ const siteConfig = JSON.parse(readFileSync(siteWranglerPath, 'utf-8'));
579
+ siteConfig.routes = [{ pattern: siteDomain, custom_domain: true }];
580
+ siteConfig.vars.PUBLIC_CMS_URL = `https://${cmsDomain}`;
581
+ siteConfig.vars.PUBLIC_API_URL = `https://${cmsDomain}`;
582
+ writeFileSync(siteWranglerPath, JSON.stringify(siteConfig, null, 2));
583
+ }
584
+ }
585
+ }
586
+
587
+ console.log(`\n✅ Workspace successfully configured for Cloudflare Edge!`);
588
+ console.log(`\nNext steps:`);
589
+ console.log(` 1. Run 'npm run deploy:cms' from the root to publish your backend and database.`);
590
+ }
591
+
592
+ /**
593
+ * Helper: Copies the Astro starter template and hydrates runtime tokens
594
+ */
595
+ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, workerName = 'kama-worker', isCloudflare = false) {
596
+ console.log(`\n🚀 Scaffolding Astro site (${siteFolder}) mapped to ${siteDomain} (Service: ${workerName})...`);
158
597
 
159
598
  const siteDir = join(rootDir, 'sites', siteFolder);
160
- mkdirSync(join(siteDir, 'src/pages'), { recursive: true });
161
- mkdirSync(join(siteDir, 'src/layouts'), { recursive: true });
162
- mkdirSync(join(siteDir, 'src/components/blocks'), { recursive: true });
163
- mkdirSync(join(siteDir, 'src/lib'), { recursive: true });
599
+ const templateDir = join(__dirname, '../templates/astro-starter');
164
600
 
165
- // 1. package.json
166
- writeFileSync(
167
- join(siteDir, 'package.json'),
168
- JSON.stringify(
169
- {
170
- name: siteFolder,
171
- type: 'module',
172
- version: '0.0.1',
173
- engines: { node: '>=22.12.0' },
174
- scripts: {
175
- dev: 'astro dev',
176
- build: 'astro build',
177
- preview: 'astro preview',
178
- deploy: 'astro build && wrangler deploy',
179
- },
180
- dependencies: {
181
- '@astrojs/cloudflare': '^14.2.5',
182
- '@brightsideoy/kama': 'latest',
183
- astro: '^7.2.2',
184
- wrangler: '^4.127.1',
185
- },
186
- },
187
- null,
188
- 2
189
- )
190
- );
601
+ if (!existsSync(templateDir)) {
602
+ console.error(`❌ Error: Astro template not found at ${templateDir}`);
603
+ console.error(`Please ensure your Kama package installation is complete and templates are published.`);
604
+ return;
605
+ }
191
606
 
192
- // 2. wrangler.json
193
- writeFileSync(
194
- join(siteDir, 'wrangler.json'),
195
- JSON.stringify(
196
- {
197
- $schema: 'node_modules/wrangler/config-schema.json',
198
- name: siteFolder,
199
- compatibility_date: '2026-08-01',
200
- compatibility_flags: ['nodejs_compat'],
201
- assets: {
202
- directory: './dist',
203
- binding: 'ASSETS',
204
- html_handling: 'drop-trailing-slash',
205
- not_found_handling: '404-page',
206
- },
207
- services: [{ binding: 'CMS_WORKER', service: 'kama-worker' }],
208
- routes: [{ pattern: siteDomain, custom_domain: true }],
209
- vars: {
210
- PUBLIC_CMS_URL: `https://${cmsDomain}`,
211
- PUBLIC_API_URL: `https://${cmsDomain}`,
212
- },
213
- },
214
- null,
215
- 2
216
- )
217
- );
607
+ cpSync(templateDir, siteDir, { recursive: true });
218
608
 
219
- // 3. astro.config.mjs
220
- const astroConfig = `import { defineConfig } from 'astro/config';
221
- import cloudflare from '@astrojs/cloudflare';
222
-
223
- export default defineConfig({
224
- output: 'static',
225
- trailingSlash: 'ignore',
226
- image: {
227
- remotePatterns: [
228
- { protocol: 'http', hostname: 'localhost', port: '3000' },
229
- { protocol: 'https', hostname: '${cmsDomain}' }
230
- ]
231
- },
232
- build: {
233
- inlineStylesheets: 'always',
234
- },
235
- adapter: cloudflare({
236
- imageService: 'cloudflare',
237
- })
238
- });`;
239
- writeFileSync(join(siteDir, 'astro.config.mjs'), astroConfig);
240
-
241
- // 4. .env file
242
- writeFileSync(
243
- join(siteDir, '.env'),
244
- `PUBLIC_CMS_URL="https://${cmsDomain}"\nPUBLIC_API_URL="https://${cmsDomain}"\n`
245
- );
609
+ const templatePkgPath = join(siteDir, 'package.json.template');
610
+ if (existsSync(templatePkgPath)) {
611
+ const pkgContent = readFileSync(templatePkgPath, 'utf-8')
612
+ .replace(/\{\{SITE_NAME\}\}/g, siteFolder)
613
+ .replace(/\{\{KAMA_VERSION\}\}/g, currentKamaVersion);
614
+
615
+ writeFileSync(join(siteDir, 'package.json'), pkgContent);
616
+ unlinkSync(templatePkgPath);
617
+ }
618
+
619
+ const wranglerPath = join(siteDir, 'wrangler.json');
620
+ if (existsSync(wranglerPath)) {
621
+ let wranglerStr = readFileSync(wranglerPath, 'utf-8')
622
+ .replace(/\{\{SITE_NAME\}\}/g, siteFolder)
623
+ .replace(/\{\{WORKER_NAME\}\}/g, workerName)
624
+ .replace(/\{\{CMS_URL\}\}/g, cmsFullUrl);
625
+
626
+ if (isCloudflare) {
627
+ const configObj = JSON.parse(wranglerStr);
628
+ configObj.routes = [{ pattern: siteDomain, custom_domain: true }];
629
+ wranglerStr = JSON.stringify(configObj, null, 2);
630
+ }
631
+
632
+ writeFileSync(wranglerPath, wranglerStr);
633
+ }
634
+
635
+ // 🎯 FIX: Smart capitalization for dynamic site labels
636
+ const siteLabel = siteFolder.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
637
+ const safeSiteLabel = siteLabel.replace(/'/g, "''");
638
+
639
+ const channelKey = siteFolder.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
640
+ const treeKey = `${channelKey}-nav`;
641
+
642
+ // 🎯 FIX: Inject `safeSiteLabel` into the logoText, titleSuffix, and titles!
643
+ const seedSql = `
644
+ -- Seed data for site: ${safeSiteLabel}
645
+ INSERT OR IGNORE INTO channels (key, label, channelType, defaultLanguage, isHidden, dataJson)
646
+ VALUES ('${channelKey}', '${safeSiteLabel}', 'website', 'en', 0, '{}');
647
+
648
+ INSERT OR IGNORE INTO trees (key, label, treeType, channelId, isHidden, dataJson)
649
+ VALUES ('${treeKey}', 'Main Navigation', 'main-navigation', (SELECT id FROM channels WHERE key = '${channelKey}'), 0, '{}');
650
+
651
+ INSERT OR IGNORE INTO channel_trees (channelId, treeId, key)
652
+ VALUES ((SELECT id FROM channels WHERE key = '${channelKey}'), (SELECT id FROM trees WHERE key = '${treeKey}'), 'main');
653
+
654
+ INSERT INTO content_nodes (contentType, title, slug, language, status, dataJson)
655
+ SELECT 'page', 'Welcome to ${safeSiteLabel}', 'home', 'en', 'published', '{"titleSuffix":"– ${safeSiteLabel}","layoutId":"single-column","subpageLayoutId":"two-column","logoText":"${safeSiteLabel}"}'
656
+ WHERE NOT EXISTS (SELECT 1 FROM content_nodes WHERE slug = 'home' AND title = 'Welcome to ${safeSiteLabel}');
657
+
658
+ INSERT OR IGNORE INTO node_channels (nodeId, channelId)
659
+ VALUES (
660
+ (SELECT id FROM content_nodes WHERE slug = 'home' AND title = 'Welcome to ${safeSiteLabel}' ORDER BY id DESC LIMIT 1),
661
+ (SELECT id FROM channels WHERE key = '${channelKey}')
662
+ );
663
+
664
+ INSERT OR IGNORE INTO tree_nodes (treeId, contentId, parentId, sortOrder)
665
+ VALUES (
666
+ (SELECT id FROM trees WHERE key = '${treeKey}'),
667
+ (SELECT id FROM content_nodes WHERE slug = 'home' AND title = 'Welcome to ${safeSiteLabel}' ORDER BY id DESC LIMIT 1),
668
+ NULL,
669
+ 0
670
+ );
671
+ `;
672
+
673
+ const workerDir = join(rootDir, 'kama-worker');
674
+ const seedPath = join(workerDir, 'seed.sql');
675
+ if (existsSync(workerDir)) {
676
+ appendFileSync(seedPath, seedSql);
677
+ }
678
+
679
+ // 🎯 FIX: Expose the title-cased name to Astro ENV vars as well
680
+ const envContent = [
681
+ `PUBLIC_CMS_URL="${cmsFullUrl}"`,
682
+ `PUBLIC_API_URL="${cmsFullUrl}"`,
683
+ `PUBLIC_CHANNEL_ID="${channelKey}"`,
684
+ `PUBLIC_TREE_ID="${treeKey}"`,
685
+ `PUBLIC_HOME_SLUG="home"`,
686
+ `PUBLIC_SITE_NAME="${siteLabel.replace(/"/g, '\\"')}"`
687
+ ].join('\n') + '\n';
688
+
689
+ writeFileSync(join(siteDir, '.env'), envContent);
246
690
  }
247
691
 
248
692
  main().catch(console.error);