@brightsideoy/kama 1.0.0-alpha.4 → 1.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, cpSync, unlinkSync, appendFileSync } from 'node:fs';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, cpSync, unlinkSync } from 'node:fs';
4
4
  import { join, dirname, basename, resolve } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
 
@@ -48,28 +48,78 @@ try {
48
48
  console.warn("⚠️ Could not detect Kama version, defaulting to 'alpha'");
49
49
  }
50
50
 
51
+ /**
52
+ * 🎯 BULLETPROOF WRANGLER EXECUTION RESOLVER
53
+ * Forces npx to load wrangler via explicit --package definition to prevent
54
+ * subshell binary resolution crashes inside automated npx contexts.
55
+ */
56
+ function getWranglerCmd() {
57
+ try {
58
+ execSync('wrangler --version', { stdio: 'ignore' });
59
+ return 'wrangler';
60
+ } catch (e) {
61
+ return 'npx --yes --package=wrangler wrangler';
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 🎯 ROBUST WRANGLER RUNNER
67
+ * Inherits full process environment and executes safely across isolated subshells.
68
+ */
69
+ function runWrangler(cmd, options = {}) {
70
+ const baseWrangler = getWranglerCmd();
71
+ try {
72
+ return execSync(`${baseWrangler} ${cmd}`, {
73
+ encoding: 'utf-8',
74
+ stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
75
+ env: {
76
+ ...process.env,
77
+ PATH: process.env.PATH
78
+ }
79
+ });
80
+ } catch (e) {
81
+ if (options.throw) throw e;
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 🎯 BULLETPROOF KV PROVISIONING ENGINE
88
+ */
89
+ function getOrCreateKV(title) {
90
+ const out = runWrangler(`kv namespace create "${title}"`);
91
+ if (out) {
92
+ const match = out.match(/"id":\s*"([a-fA-F0-9]{32})"/);
93
+ if (match) return match[1];
94
+ }
95
+
96
+ const listOut = runWrangler(`kv namespace list`);
97
+ if (listOut) {
98
+ try {
99
+ const namespaces = JSON.parse(listOut);
100
+ const found = namespaces.find(n => n.title === title);
101
+ if (found) return found.id;
102
+ } catch (err) { }
103
+ }
104
+
105
+ return `REPLACE_WITH_${title}_ID`;
106
+ }
107
+
51
108
  function showHelp() {
52
109
  console.log(`\n${pc.bgCyan(' KAMA • ENGINE CLI ')}\n`);
53
110
  console.log('Usage: npx kama <command> [options]\n');
54
-
55
111
  console.log(pc.cyan('Commands:'));
56
112
  console.log(' init Kickstart a new Monorepo (CMS Worker + Optional Astro Site)');
57
113
  console.log(' add site Add a new Astro Frontend Site to the current workspace (sites/)');
58
114
  console.log(' bundle Compile and bundle YAML schemas into bundled-definitions.js for Edge deployment');
59
115
  console.log(' promote Promote the current Local workspace to Cloudflare Edge');
60
116
  console.log(' help, -h Show this help message\n');
61
-
62
117
  console.log(pc.cyan('Workspace Scripts (Run from project root):'));
63
118
  console.log(' npm update Update to latest KAMA version');
64
- console.log(' npm run setup Initialize database schema and seed data (Run Once)');
119
+ console.log(' npm run setup Initialize database schema (Run Once)');
65
120
  console.log(' npm run dev Boot up the CMS backend and Astro frontend concurrently');
66
121
  console.log(' npm run deploy:cms Deploy the backend worker and database to Cloudflare');
67
122
  console.log(' npm run deploy:site Deploy the Astro frontend site to Cloudflare\n');
68
-
69
- console.log(pc.cyan('Examples:'));
70
- console.log(' npx kama init');
71
- console.log(' npx kama add site');
72
- console.log(' npx kama bundle\n');
73
123
  }
74
124
 
75
125
  async function main() {
@@ -124,9 +174,6 @@ async function main() {
124
174
  }
125
175
  }
126
176
 
127
- /**
128
- * Helper to update astro.config.mjs for Cloudflare
129
- */
130
177
  function convertAstroConfigToCloudflare(siteDir) {
131
178
  const configPath = join(siteDir, 'astro.config.mjs');
132
179
  if (existsSync(configPath)) {
@@ -140,7 +187,6 @@ function convertAstroConfigToCloudflare(siteDir) {
140
187
 
141
188
  /**
142
189
  * COMMAND: npx kama init
143
- * Kickstart full Monorepo Workspace
144
190
  */
145
191
  async function createFullWorkspace() {
146
192
  const cwdName = basename(process.cwd());
@@ -206,9 +252,7 @@ async function createFullWorkspace() {
206
252
  const s = spinner();
207
253
  s.start('Provisioning workspace...');
208
254
 
209
- if (!isCurrentDir) {
210
- mkdirSync(rootDir, { recursive: true });
211
- }
255
+ if (!isCurrentDir) mkdirSync(rootDir, { recursive: true });
212
256
 
213
257
  writeFileSync(
214
258
  join(rootDir, '.gitignore'),
@@ -217,100 +261,78 @@ async function createFullWorkspace() {
217
261
 
218
262
  const sourceDefsDir = join(__dirname, '../content/definitions');
219
263
  const targetDefsDir = join(rootDir, 'content', 'definitions');
220
- if (existsSync(sourceDefsDir)) {
221
- cpSync(sourceDefsDir, targetDefsDir, { recursive: true });
222
- }
264
+ if (existsSync(sourceDefsDir)) cpSync(sourceDefsDir, targetDefsDir, { recursive: true });
223
265
 
224
266
  let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
267
+ let backendCacheId = 'REPLACE_WITH_CACHE_KV_ID';
225
268
  let isAuthenticated = false;
226
269
 
227
270
  if (isCloudflare) {
228
271
  s.message('Checking Cloudflare authentication...');
229
- try {
230
- execSync('npx wrangler whoami', { stdio: 'ignore' });
272
+ const whoami = runWrangler('whoami');
273
+ if (whoami && (whoami.includes('Getting User Settings') || whoami.includes('You are logged in'))) {
231
274
  isAuthenticated = true;
232
- } catch (error) {
275
+ } else {
233
276
  s.stop('Not authenticated with Cloudflare.', true);
234
277
  console.log(pc.dim('Launching browser for Cloudflare login...'));
235
- try {
236
- execSync('npx wrangler login', { stdio: 'inherit' });
237
- isAuthenticated = true;
238
- } catch (loginError) {
239
- // Ignore
240
- }
278
+ const login = runWrangler('login', { stdio: 'inherit' });
279
+ if (login !== null) isAuthenticated = true;
241
280
  s.start('Resuming provisioning...');
242
281
  }
243
282
 
244
283
  if (isAuthenticated) {
245
- try {
246
- s.message('Checking existing Cloudflare D1 databases...');
247
- const listOutput = execSync(`npx wrangler d1 list --json`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
248
- const jsonStart = listOutput.indexOf('[');
249
- const jsonEnd = listOutput.lastIndexOf(']') + 1;
250
- let databases = [];
251
-
252
- if (jsonStart !== -1 && jsonEnd !== -1) {
253
- databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
254
- }
255
-
256
- const existingDb = databases.find(db => db.name === dbName);
257
-
258
- if (existingDb) {
259
- s.stop(`Database '${dbName}' already exists on your account.`);
260
- const useExisting = handleCancel(await confirm({
261
- message: 'Do you want to link this existing database?',
262
- initialValue: true
263
- }));
264
-
265
- if (useExisting) {
266
- d1DatabaseId = existingDb.uuid;
267
- s.start(`Linked existing database ID: ${d1DatabaseId}`);
268
- } else {
269
- dbName = handleCancel(await text({
270
- message: 'Enter a new database name:'
271
- }));
272
- s.start(`Creating new database '${dbName}'...`);
273
- const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
274
- 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}/);
275
- if (match) {
276
- d1DatabaseId = match[0];
284
+ s.message('Provisioning backend KV Cache...');
285
+ backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`);
286
+
287
+ s.message('Checking existing Cloudflare D1 databases...');
288
+ const listOutput = runWrangler('d1 list --json');
289
+ if (listOutput) {
290
+ try {
291
+ const jsonStart = listOutput.indexOf('[');
292
+ const jsonEnd = listOutput.lastIndexOf(']') + 1;
293
+ let databases = [];
294
+
295
+ if (jsonStart !== -1 && jsonEnd !== -1) databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
296
+ const existingDb = databases.find(db => db.name === dbName);
297
+
298
+ if (existingDb) {
299
+ s.stop(`Database '${dbName}' already exists on your account.`);
300
+ const useExisting = handleCancel(await confirm({ message: 'Do you want to link this existing database?', initialValue: true }));
301
+
302
+ if (useExisting) {
303
+ d1DatabaseId = existingDb.uuid;
304
+ s.start(`Linked existing database ID: ${d1DatabaseId}`);
305
+ } else {
306
+ dbName = handleCancel(await text({ message: 'Enter a new database name:' }));
307
+ s.start(`Creating new database '${dbName}'...`);
308
+ const d1Output = runWrangler(`d1 create ${dbName}`);
309
+ const match = d1Output ? d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/) : null;
310
+ if (match) d1DatabaseId = match[0];
277
311
  }
312
+ } else {
313
+ s.message(`Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
314
+ const d1Output = runWrangler(`d1 create ${dbName}`);
315
+ const match = d1Output ? d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/) : null;
316
+ if (match) d1DatabaseId = match[0];
278
317
  }
279
- } else {
280
- s.message(`Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
281
- const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
282
- 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}/);
283
- if (match) {
284
- d1DatabaseId = match[0];
285
- }
286
- }
287
- } catch (error) {
288
- // Placeholder
318
+ } catch (error) { }
289
319
  }
290
320
  }
291
321
  }
292
322
 
293
323
  s.message('Writing configuration files...');
294
324
 
295
- const rootScripts = {
296
- 'setup': 'npm run setup:local -w kama-worker',
297
- };
298
-
299
325
  const rootPkg = {
300
326
  name: projName,
301
327
  private: true,
302
328
  type: 'module',
303
329
  workspaces: ['kama-worker', 'sites/*'],
304
- scripts: rootScripts,
305
- dependencies: {
306
- '@brightsideoy/kama': currentKamaVersion,
307
- },
330
+ scripts: { 'setup': 'npm run setup:local -w kama-worker' },
331
+ dependencies: { '@brightsideoy/kama': currentKamaVersion },
308
332
  devDependencies: {}
309
333
  };
310
334
 
311
- if (isCloudflare) {
312
- rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
313
- }
335
+ if (isCloudflare) rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
314
336
 
315
337
  if (shouldCreateSite) {
316
338
  rootPkg.devDependencies['concurrently'] = '^9.0.0';
@@ -330,16 +352,12 @@ async function createFullWorkspace() {
330
352
  const cmsDir = join(rootDir, 'kama-worker');
331
353
  mkdirSync(cmsDir, { recursive: true });
332
354
 
333
- writeFileSync(join(cmsDir, 'seed.sql'), '-- Kama Auto-Generated Seed File\nSELECT 1;\n');
334
-
335
355
  const emptyDefs = `{ nodes: {}, trees: {}, blocks: {}, nodeLayouts: {}, blockLayouts: {}, channels: {} }`;
336
356
  writeFileSync(join(cmsDir, 'bundled-definitions.js'), `const defs = ${emptyDefs};\nexport const BUNDLED_DEFINITIONS = defs;\nexport default defs;\n`);
337
357
 
338
358
  if (isCloudflare) {
339
359
  const templateIndexPath = join(__dirname, '../templates/kama-worker/index.js');
340
- if (existsSync(templateIndexPath)) {
341
- cpSync(templateIndexPath, join(cmsDir, 'index.js'));
342
- }
360
+ if (existsSync(templateIndexPath)) cpSync(templateIndexPath, join(cmsDir, 'index.js'));
343
361
 
344
362
  writeFileSync(
345
363
  join(cmsDir, 'package.json'),
@@ -348,18 +366,13 @@ async function createFullWorkspace() {
348
366
  type: 'module',
349
367
  version: '1.0.0',
350
368
  scripts: {
351
- "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql > /dev/null 2>&1 && wrangler d1 execute ${dbName} --local --file=seed.sql > /dev/null 2>&1`,
352
- "setup:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql && wrangler d1 execute ${dbName} --remote --file=seed.sql`,
369
+ "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql > /dev/null 2>&1`,
370
+ "setup:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`,
353
371
  "dev": "wrangler dev --port 8787 --inspector-port 9239",
354
372
  "deploy": "npm run setup:remote && wrangler deploy"
355
373
  },
356
- dependencies: {
357
- '@brightsideoy/kama': currentKamaVersion,
358
- '@libsql/client': '^0.14.0'
359
- },
360
- devDependencies: {
361
- "wrangler": WRANGLER_LATEST
362
- },
374
+ dependencies: { '@brightsideoy/kama': currentKamaVersion, '@libsql/client': '^0.14.0' },
375
+ devDependencies: { "wrangler": WRANGLER_LATEST },
363
376
  }, null, 2)
364
377
  );
365
378
 
@@ -370,9 +383,9 @@ async function createFullWorkspace() {
370
383
  compatibility_date: '2024-09-23',
371
384
  compatibility_flags: ['nodejs_compat'],
372
385
  dev: { host: 'localhost', port: 8787 },
373
- cache: { enabled: true },
374
386
  assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
375
387
  images: { binding: 'IMAGES' },
388
+ kv_namespaces: [{ binding: 'CACHE_KV', id: backendCacheId }],
376
389
  vars: {
377
390
  CDN_DOMAIN: cmsDomain || 'localhost:8787',
378
391
  PUBLIC_FRONTEND_URL: (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321',
@@ -385,13 +398,10 @@ async function createFullWorkspace() {
385
398
  };
386
399
 
387
400
  writeFileSync(join(cmsDir, 'wrangler.json'), JSON.stringify(cmsWranglerConfig, null, 2));
388
-
389
401
  writeFileSync(join(cmsDir, '.dev.vars'), `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\n\n# Local Development Overrides\nCDN_DOMAIN=\nPUBLIC_FRONTEND_URL=http://localhost:4321\n`);
390
402
  } else {
391
403
  const templateServerPath = join(__dirname, '../templates/kama-worker/server.js');
392
- if (existsSync(templateServerPath)) {
393
- cpSync(templateServerPath, join(cmsDir, 'server.js'));
394
- }
404
+ if (existsSync(templateServerPath)) cpSync(templateServerPath, join(cmsDir, 'server.js'));
395
405
 
396
406
  writeFileSync(
397
407
  join(cmsDir, 'package.json'),
@@ -400,20 +410,12 @@ async function createFullWorkspace() {
400
410
  type: 'module',
401
411
  version: '1.0.0',
402
412
  scripts: {
403
- "setup:local": "sqlite3 kama.db < ../node_modules/@brightsideoy/kama/schema.sql && sqlite3 kama.db < seed.sql",
413
+ "setup:local": "sqlite3 kama.db < ../node_modules/@brightsideoy/kama/schema.sql",
404
414
  "dev": "tsx server.js",
405
415
  "start": "node server.js"
406
416
  },
407
- dependencies: {
408
- '@brightsideoy/kama': currentKamaVersion,
409
- '@hono/node-server': '^1.13.0',
410
- '@libsql/client': '^0.14.0',
411
- 'sharp': '^0.33.5',
412
- 'dotenv': '^16.4.5'
413
- },
414
- devDependencies: {
415
- "tsx": "^4.19.0"
416
- },
417
+ dependencies: { '@brightsideoy/kama': currentKamaVersion, '@hono/node-server': '^1.13.0', '@libsql/client': '^0.14.0', 'sharp': '^0.33.5', 'dotenv': '^16.4.5' },
418
+ devDependencies: { "tsx": "^4.19.0" },
417
419
  }, null, 2)
418
420
  );
419
421
 
@@ -434,7 +436,7 @@ async function createFullWorkspace() {
434
436
  let stepCount = 1;
435
437
  if (!isCurrentDir) nextSteps += `${stepCount++}. cd ${projName}\n`;
436
438
  nextSteps += `${stepCount++}. npm install\n`;
437
- nextSteps += `${stepCount++}. npm run setup ${pc.dim('<- Run this once to init the database!')}\n`;
439
+ nextSteps += `${stepCount++}. npm run setup ${pc.dim('<- Run this once to init the database schema!')}\n`;
438
440
  nextSteps += `${stepCount++}. npm run dev\n`;
439
441
 
440
442
  if (isCloudflare) {
@@ -489,17 +491,11 @@ async function createNewSiteOnly() {
489
491
  let cmsFullUrl = 'http://localhost:8787';
490
492
 
491
493
  if (isCloudflare) {
492
- siteDomain = handleCancel(await text({
493
- message: 'Website Domain (e.g., brand.com):',
494
- defaultValue: ''
495
- }));
494
+ siteDomain = handleCancel(await text({ message: 'Website Domain (e.g., brand.com):', defaultValue: '' }));
496
495
  if (detectedCmsDomain) {
497
496
  cmsFullUrl = detectedCmsDomain.startsWith('http') ? detectedCmsDomain : `https://${detectedCmsDomain.replace(/^https?:\/\//, '')}`;
498
497
  } else {
499
- const cmsDomainInput = handleCancel(await text({
500
- message: 'CMS API Domain (e.g., admin.brand.com):',
501
- defaultValue: ''
502
- }));
498
+ const cmsDomainInput = handleCancel(await text({ message: 'CMS API Domain (e.g., admin.brand.com):', defaultValue: '' }));
503
499
  cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
504
500
  }
505
501
  }
@@ -509,7 +505,7 @@ async function createNewSiteOnly() {
509
505
  await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare);
510
506
  s.stop(`Site created in sites/${siteFolder}!`);
511
507
 
512
- note(`1. npm install\n2. npm run setup ${pc.dim('<- Re-run to register the new site seed data')}`, 'Next steps');
508
+ note(`1. npm install\n2. npm run dev`, 'Next steps');
513
509
  outro('Site added successfully! 🚀');
514
510
  }
515
511
 
@@ -541,27 +537,27 @@ async function promoteLocalToCloudflare() {
541
537
  const rootPkgPath = join(rootDir, 'package.json');
542
538
 
543
539
  if (!existsSync(workerDir) || !existsSync(rootPkgPath)) {
544
- cancel('Error: Could not find kama-worker folder or package.json. Run this command inside your workspace root.');
540
+ cancel('Error: Could not find kama-worker folder or package.json.');
545
541
  process.exit(1);
546
542
  }
547
543
 
548
544
  const s = spinner();
549
545
  s.start('Checking Cloudflare authentication...');
550
546
 
551
- try {
552
- execSync('npx wrangler whoami', { stdio: 'ignore' });
547
+ const whoami = runWrangler('whoami');
548
+ if (whoami && (whoami.includes('Getting User Settings') || whoami.includes('You are logged in'))) {
553
549
  s.stop('Logged in to Cloudflare.');
554
- } catch (e) {
550
+ } else {
555
551
  s.stop('Not logged in to Cloudflare.', true);
556
552
  console.log(pc.dim('Launching browser for Cloudflare login...'));
557
- try { execSync('npx wrangler login', { stdio: 'ignore' }); }
558
- catch (loginError) { cancel('Cloudflare login failed.'); process.exit(1); }
553
+ const login = runWrangler('login', { stdio: 'inherit' });
554
+ if (login === null) {
555
+ cancel('Cloudflare login failed.');
556
+ process.exit(1);
557
+ }
559
558
  }
560
559
 
561
- const cmsDomain = handleCancel(await text({
562
- message: 'CMS Admin Domain (e.g., admin.yourdomain.com):',
563
- defaultValue: ''
564
- }));
560
+ const cmsDomain = handleCancel(await text({ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):', defaultValue: '' }));
565
561
 
566
562
  const sitesFolderDir = join(rootDir, 'sites');
567
563
  let siteDomain = '';
@@ -570,10 +566,7 @@ async function promoteLocalToCloudflare() {
570
566
  const sites = readdirSync(sitesFolderDir);
571
567
  if (sites.length > 0) {
572
568
  siteDir = join(sitesFolderDir, sites[0]);
573
- siteDomain = handleCancel(await text({
574
- message: `Frontend Website Domain for ${sites[0]} (e.g., yourdomain.com):`,
575
- defaultValue: ''
576
- }));
569
+ siteDomain = handleCancel(await text({ message: `Frontend Website Domain for ${sites[0]}:`, defaultValue: '' }));
577
570
  }
578
571
  }
579
572
 
@@ -583,41 +576,41 @@ async function promoteLocalToCloudflare() {
583
576
  let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
584
577
  let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
585
578
 
586
- try {
587
- const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
579
+ const d1Output = runWrangler(`d1 create ${dbName}`);
580
+ if (d1Output) {
588
581
  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}/);
589
582
  if (match) d1DatabaseId = match[0];
590
- } catch (err) { }
583
+ }
584
+
585
+ const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
586
+ const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
587
+
588
+ const backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`);
591
589
 
592
590
  const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
593
591
  rootPkg.devDependencies = rootPkg.devDependencies || {};
594
592
  rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
595
593
  writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2));
596
594
 
597
- const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
598
- const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
599
595
  const workerWranglerPath = join(workerDir, 'wrangler.json');
600
-
601
- let currentConfig = existsSync(workerWranglerPath)
602
- ? JSON.parse(readFileSync(workerWranglerPath, 'utf-8'))
603
- : {
604
- $schema: '../node_modules/wrangler/config-schema.json',
605
- name: workerName,
606
- main: 'index.js',
607
- compatibility_date: '2024-09-23',
608
- compatibility_flags: ['nodejs_compat'],
609
- assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
610
- images: { binding: 'IMAGES' },
611
- vars: { CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID", KAMA_RUNTIME: 'cloudflare' }
612
- };
596
+ let currentConfig = existsSync(workerWranglerPath) ? JSON.parse(readFileSync(workerWranglerPath, 'utf-8')) : {
597
+ $schema: '../node_modules/wrangler/config-schema.json',
598
+ name: workerName,
599
+ main: 'index.js',
600
+ compatibility_date: '2024-09-23',
601
+ compatibility_flags: ['nodejs_compat'],
602
+ assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
603
+ images: { binding: 'IMAGES' },
604
+ vars: { CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID", KAMA_RUNTIME: 'cloudflare' }
605
+ };
613
606
 
614
607
  currentConfig.dev = currentConfig.dev || { host: 'localhost', port: 8787 };
615
- currentConfig.cache = { enabled: true };
616
608
  currentConfig.vars.CDN_DOMAIN = cmsDomain;
617
609
  if (siteDomain) currentConfig.vars.PUBLIC_FRONTEND_URL = `https://${siteDomain}`;
618
610
  currentConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
619
611
  currentConfig.d1_databases = [{ binding: 'DB', database_name: dbName, database_id: d1DatabaseId }];
620
612
  currentConfig.r2_buckets = [{ binding: 'MEDIA_BUCKET', bucket_name: bucketName }];
613
+ currentConfig.kv_namespaces = [{ binding: 'CACHE_KV', id: backendCacheId }];
621
614
 
622
615
  writeFileSync(workerWranglerPath, JSON.stringify(currentConfig, null, 2));
623
616
 
@@ -625,37 +618,22 @@ async function promoteLocalToCloudflare() {
625
618
  writeFileSync(join(workerDir, '.dev.vars'), `CLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\nCDN_DOMAIN=\nPUBLIC_FRONTEND_URL=http://localhost:4321\n`);
626
619
  }
627
620
 
628
- const workerIndexPath = join(workerDir, 'index.js');
629
- if (!existsSync(workerIndexPath)) {
630
- const templateIndexPath = join(__dirname, '../templates/kama-worker/index.js');
631
- if (existsSync(templateIndexPath)) {
632
- cpSync(templateIndexPath, workerIndexPath);
633
- }
634
- }
635
-
636
621
  const workerPkgPath = join(workerDir, 'package.json');
637
622
  if (existsSync(workerPkgPath)) {
638
623
  const workerPkg = JSON.parse(readFileSync(workerPkgPath, 'utf-8'));
639
624
  workerPkg.scripts.dev = "wrangler dev --port 8787 --inspector-port 9239";
640
625
  workerPkg.scripts.deploy = "npm run setup:remote && wrangler deploy";
641
- workerPkg.scripts["setup:local"] = `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql && wrangler d1 execute ${dbName} --local --file=seed.sql`;
642
- workerPkg.scripts["setup:remote"] = `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql && wrangler d1 execute ${dbName} --remote --file=seed.sql`;
626
+ workerPkg.scripts["setup:local"] = `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`;
627
+ workerPkg.scripts["setup:remote"] = `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`;
643
628
  workerPkg.devDependencies = workerPkg.devDependencies || {};
644
629
  workerPkg.devDependencies.wrangler = WRANGLER_LATEST;
645
630
  writeFileSync(workerPkgPath, JSON.stringify(workerPkg, null, 2));
646
631
  }
647
632
 
648
633
  if (siteDir && siteDomain) {
649
- let sessionId = "REPLACE_WITH_YOUR_SESSION_KV_ID";
650
- let htmlCacheId = "REPLACE_WITH_YOUR_HTML_CACHE_KV_ID";
651
634
  const prefix = basename(siteDir).replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
652
-
653
- try {
654
- const sessionOut = execSync(`npx wrangler kv namespace create "SESSION_${prefix}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
655
- if (sessionOut.match(/"id":\s*"([a-f0-9]{32})"/)) sessionId = sessionOut.match(/"id":\s*"([a-f0-9]{32})"/)[1];
656
- const cacheOut = execSync(`npx wrangler kv namespace create "HTML_CACHE_${prefix}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
657
- if (cacheOut.match(/"id":\s*"([a-f0-9]{32})"/)) htmlCacheId = cacheOut.match(/"id":\s*"([a-f0-9]{32})"/)[1];
658
- } catch (err) { }
635
+ const sessionId = getOrCreateKV(`SESSION_${prefix}`);
636
+ const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`);
659
637
 
660
638
  const siteWranglerPath = join(siteDir, 'wrangler.json');
661
639
  const siteConfig = {
@@ -678,25 +656,14 @@ async function promoteLocalToCloudflare() {
678
656
  const sitePkg = JSON.parse(readFileSync(sitePkgPath, 'utf-8'));
679
657
  sitePkg.dependencies = sitePkg.dependencies || {};
680
658
  sitePkg.dependencies['astro'] = ASTRO_LATEST;
681
-
682
659
  if (sitePkg.dependencies['@astrojs/node']) delete sitePkg.dependencies['@astrojs/node'];
683
660
  sitePkg.dependencies['@astrojs/cloudflare'] = ASTRO_CF_LATEST;
684
-
685
661
  sitePkg.devDependencies = sitePkg.devDependencies || {};
686
662
  sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
687
-
688
663
  writeFileSync(sitePkgPath, JSON.stringify(sitePkg, null, 2));
689
664
  }
690
665
 
691
666
  convertAstroConfigToCloudflare(siteDir);
692
-
693
- const tsConfigPath = join(siteDir, 'tsconfig.json');
694
- if (existsSync(tsConfigPath)) {
695
- const tsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf-8'));
696
- tsConfig.compilerOptions = tsConfig.compilerOptions || {};
697
- tsConfig.compilerOptions.types = ["@cloudflare/workers-types"];
698
- writeFileSync(tsConfigPath, JSON.stringify(tsConfig, null, 2));
699
- }
700
667
  }
701
668
 
702
669
  s.stop('Workspace successfully upgraded to Cloudflare Edge!');
@@ -710,16 +677,11 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
710
677
  const siteDir = join(rootDir, 'sites', siteFolder);
711
678
  const templateDir = join(__dirname, '../templates/astro-starter');
712
679
 
713
- if (!existsSync(templateDir)) {
714
- return;
715
- }
680
+ if (!existsSync(templateDir)) return;
716
681
 
717
682
  cpSync(templateDir, siteDir, { recursive: true });
718
683
 
719
- const tsConfig = {
720
- extends: "astro/tsconfigs/strict",
721
- compilerOptions: { baseUrl: ".", paths: { "@/*": ["src/*"] } }
722
- };
684
+ const tsConfig = { extends: "astro/tsconfigs/strict", compilerOptions: { baseUrl: ".", paths: { "@/*": ["src/*"] } } };
723
685
  if (isCloudflare) tsConfig.compilerOptions.types = ["@cloudflare/workers-types"];
724
686
  writeFileSync(join(siteDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
725
687
 
@@ -728,7 +690,6 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
728
690
  const rawContent = readFileSync(templatePkgPath, 'utf-8')
729
691
  .replace(/\{\{SITE_NAME\}\}/g, siteFolder)
730
692
  .replace(/\{\{KAMA_VERSION\}\}/g, currentKamaVersion);
731
-
732
693
  const sitePkg = JSON.parse(rawContent);
733
694
 
734
695
  sitePkg.dependencies = sitePkg.dependencies || {};
@@ -738,14 +699,12 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
738
699
  if (sitePkg.dependencies['@astrojs/cloudflare']) delete sitePkg.dependencies['@astrojs/cloudflare'];
739
700
  if (sitePkg.dependencies['wrangler']) delete sitePkg.dependencies['wrangler'];
740
701
  if (sitePkg.devDependencies && sitePkg.devDependencies['wrangler']) delete sitePkg.devDependencies['wrangler'];
741
-
742
702
  sitePkg.dependencies['@astrojs/node'] = ASTRO_NODE_LATEST;
743
703
  } else {
744
704
  sitePkg.dependencies['@astrojs/cloudflare'] = ASTRO_CF_LATEST;
745
705
  if (sitePkg.dependencies['wrangler']) delete sitePkg.dependencies['wrangler'];
746
706
  sitePkg.devDependencies = sitePkg.devDependencies || {};
747
707
  sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
748
-
749
708
  convertAstroConfigToCloudflare(siteDir);
750
709
  }
751
710
 
@@ -754,23 +713,14 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
754
713
  }
755
714
 
756
715
  const siteLabel = siteFolder.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
757
- const safeSiteLabel = siteLabel.replace(/'/g, "''");
758
716
  const channelKey = siteFolder.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
759
- const homeSlug = `welcome-to-${channelKey}`;
760
717
  const treeKey = 'main-tree';
761
718
  const safeWorkerName = `${siteFolder.toLowerCase().replace(/[^a-z0-9-]/g, '-')}-site`;
762
719
 
763
720
  if (isCloudflare) {
764
- let sessionId = "REPLACE_WITH_YOUR_SESSION_KV_ID";
765
- let htmlCacheId = "REPLACE_WITH_YOUR_HTML_CACHE_KV_ID";
766
-
767
- try {
768
- const prefix = siteFolder.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
769
- const sessionOut = execSync(`npx wrangler kv namespace create "SESSION_${prefix}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
770
- if (sessionOut.match(/"id":\s*"([a-f0-9]{32})"/)) sessionId = sessionOut.match(/"id":\s*"([a-f0-9]{32})"/)[1];
771
- const cacheOut = execSync(`npx wrangler kv namespace create "HTML_CACHE_${prefix}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
772
- if (cacheOut.match(/"id":\s*"([a-f0-9]{32})"/)) htmlCacheId = cacheOut.match(/"id":\s*"([a-f0-9]{32})"/)[1];
773
- } catch (err) { }
721
+ const prefix = siteFolder.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
722
+ const sessionId = getOrCreateKV(`SESSION_${prefix}`);
723
+ const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`);
774
724
 
775
725
  const siteConfig = {
776
726
  $schema: "node_modules/wrangler/config-schema.json",
@@ -789,78 +739,6 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
789
739
  writeFileSync(join(siteDir, 'wrangler.json'), JSON.stringify(siteConfig, null, 2));
790
740
  }
791
741
 
792
- const siteUrl = (siteDomain === 'localhost:4321' || !isCloudflare) ? `http://${siteDomain}` : `https://${siteDomain}`;
793
-
794
- const seedSql = `
795
- -- Seed data for site: ${safeSiteLabel}
796
-
797
- -- 1. Create content node
798
- INSERT INTO content_nodes (contentType, title, slug, language, status, hasUnpublishedChanges, disabledRegionsJson, dataJson, draftDataJson, publishedDataJson, publishedAt)
799
- SELECT 'page', 'Welcome to ${safeSiteLabel}', '${homeSlug}', '', 'published', 0, '{}',
800
- '{"titleSuffix":"– ${safeSiteLabel}","layoutId":"single-column","subpageLayoutId":"two-column","logoText":"${safeSiteLabel}","schemaType":"WebPage"}',
801
- '{"titleSuffix":"– ${safeSiteLabel}","layoutId":"single-column","subpageLayoutId":"two-column","logoText":"${safeSiteLabel}","schemaType":"WebPage"}',
802
- '{"titleSuffix":"– ${safeSiteLabel}","layoutId":"single-column","subpageLayoutId":"two-column","logoText":"${safeSiteLabel}","schemaType":"WebPage"}',
803
- CURRENT_TIMESTAMP
804
- WHERE NOT EXISTS (SELECT 1 FROM content_nodes WHERE slug = '${homeSlug}' AND title = 'Welcome to ${safeSiteLabel}');
805
-
806
- -- 2. Create channel
807
- INSERT OR IGNORE INTO channels (key, label, channelType, baseUrl, defaultLanguage, isHidden, dataJson)
808
- VALUES ('${channelKey}', '${safeSiteLabel}', 'website', '${siteUrl}', '', 0, '{}');
809
-
810
- -- 3. Link channelRoot in channel JSON
811
- UPDATE channels
812
- SET dataJson = json_patch(
813
- COALESCE(NULLIF(dataJson, ''), '{}'),
814
- json_object(
815
- 'channelRoot', json_object(
816
- 'href', '#node:' || (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' AND title = 'Welcome to ${safeSiteLabel}' ORDER BY id DESC LIMIT 1),
817
- 'kind', 'node',
818
- 'target', '_self',
819
- 'label', 'Welcome to ${safeSiteLabel}',
820
- 'nodeId', (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' AND title = 'Welcome to ${safeSiteLabel}' ORDER BY id DESC LIMIT 1)
821
- )
822
- )
823
- )
824
- WHERE key = '${channelKey}';
825
-
826
- -- 4. Create Trees & Relations
827
- INSERT OR IGNORE INTO trees (key, label, treeType, channelId, isHidden, dataJson)
828
- VALUES ('${treeKey}', 'Main Tree', 'navigation', (SELECT id FROM channels WHERE key = '${channelKey}'), 0, '{}');
829
-
830
- INSERT OR IGNORE INTO channel_trees (channelId, treeId, key)
831
- VALUES ((SELECT id FROM channels WHERE key = '${channelKey}'), (SELECT id FROM trees WHERE key = '${treeKey}'), 'main');
832
-
833
- INSERT OR IGNORE INTO node_channels (nodeId, channelId)
834
- VALUES ((SELECT id FROM content_nodes WHERE slug = '${homeSlug}' ORDER BY id DESC LIMIT 1), (SELECT id FROM channels WHERE key = '${channelKey}'));
835
-
836
- INSERT OR IGNORE INTO tree_nodes (treeId, contentId, parentId, sortOrder)
837
- VALUES ((SELECT id FROM trees WHERE key = '${treeKey}'), (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' ORDER BY id DESC LIMIT 1), NULL, 0);
838
-
839
- -- 5. Node Routing (TWO records: Canonical '/' and Redirect '/slug' -> '/')
840
- INSERT OR IGNORE INTO node_routes (path, channelId, nodeId, treeId, isCanonical, redirectUrl)
841
- VALUES (
842
- '/',
843
- (SELECT id FROM channels WHERE key = '${channelKey}'),
844
- (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' ORDER BY id DESC LIMIT 1),
845
- (SELECT id FROM trees WHERE key = '${treeKey}'),
846
- 1,
847
- NULL
848
- );
849
-
850
- INSERT OR IGNORE INTO node_routes (path, channelId, nodeId, treeId, isCanonical, redirectUrl)
851
- VALUES (
852
- '/${homeSlug}',
853
- (SELECT id FROM channels WHERE key = '${channelKey}'),
854
- (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' ORDER BY id DESC LIMIT 1),
855
- NULL,
856
- 0,
857
- '/'
858
- );
859
- `;
860
-
861
- const workerDir = join(rootDir, 'kama-worker');
862
- if (existsSync(workerDir)) appendFileSync(join(workerDir, 'seed.sql'), seedSql);
863
-
864
742
  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_SITE_NAME="${siteLabel.replace(/"/g, '\\"')}"\n`);
865
743
  }
866
744