@brightsideoy/kama 1.0.0-alpha.12 → 1.0.0-alpha.16

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 (39) hide show
  1. package/bin/cli.js +293 -155
  2. package/dist/assets/AssetEditor-CEHxvQj0.js +1 -0
  3. package/dist/assets/AssetEditor-D5j7pwcK.css +1 -0
  4. package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
  5. package/dist/assets/CmsHelp-Cnr801lx.js +1 -0
  6. package/dist/assets/CmsPublisher-C3fRoBte.js +3 -0
  7. package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
  8. package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
  9. package/dist/assets/CmsUserCenter-DiuXfjMc.js +1 -0
  10. package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
  11. package/dist/assets/EditorSplitPreview-tpZZF1o5.js +1 -0
  12. package/dist/assets/NodeEditor-Bd89OptF.css +1 -0
  13. package/dist/assets/NodeEditor-DaCIVPGT.js +147 -0
  14. package/dist/assets/TrashWorkspace-CTq0OFA-.js +1 -0
  15. package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
  16. package/dist/assets/_plugin-vue_export-helper-CTn6xl4w.js +5 -0
  17. package/dist/assets/history-XvWNE5zO.js +1 -0
  18. package/dist/assets/index-BbV9FaG-.css +1 -0
  19. package/dist/assets/index-CHczMewv.js +14 -0
  20. package/dist/assets/layers-D0DLWjff.js +1 -0
  21. package/dist/assets/link-Bs_SUS0O.js +1 -0
  22. package/dist/assets/rotate-ccw-CvD7yNQj.js +1 -0
  23. package/dist/assets/save-C8e-QRkP.js +1 -0
  24. package/dist/assets/smartphone-Wy6xQi_i.js +1 -0
  25. package/dist/build-seed.js +37 -28
  26. package/dist/{dist-kIJlDMvm.js → dist-Ca4GRlV3.js} +619 -535
  27. package/dist/index.html +3 -2
  28. package/dist/index.js +619 -535
  29. package/dist/schema-compiler.js +1 -1
  30. package/package.json +1 -1
  31. package/schema.sql +3 -0
  32. package/templates/astro-starter/src/components/blocks/NodeList.astro +59 -63
  33. package/templates/astro-starter/src/middleware.ts +63 -35
  34. package/templates/astro-starter/src/pages/[...slug].astro +5 -12
  35. package/templates/astro-starter/src/pages/api/load-nodes.ts +42 -14
  36. package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
  37. package/templates/kama-worker/setup-local-db.mjs +17 -0
  38. package/dist/assets/index-CGLx9oqF.css +0 -1
  39. package/dist/assets/index-De9O-dAG.js +0 -164
package/bin/cli.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import { execSync } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, cpSync, unlinkSync, appendFileSync } from 'node:fs';
4
4
  import { join, dirname, basename, resolve } from 'node:path';
5
- import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { fileURLToPath } from 'node:url';
6
6
  import crypto from 'node:crypto'; // 🎯 NEW: Cryptographically secure token generator
7
7
 
8
8
  // 🎯 Modern CLI UI Components
9
- import { intro, outro, text, select, confirm, spinner, note, isCancel, cancel } from '@clack/prompts';
9
+ import { intro, outro, text, password, select, confirm, spinner, note, isCancel, cancel } from '@clack/prompts';
10
10
 
11
11
  // --- UI HELPERS (Zero Dependency Colors) ---
12
12
  const pc = {
@@ -106,31 +106,29 @@ function getWranglerCmd() {
106
106
  function runWrangler(cmd, options = {}) {
107
107
  const baseWrangler = getWranglerCmd();
108
108
  try {
109
- return execSync(`${baseWrangler} ${cmd}`, {
109
+ const result = execSync(`${baseWrangler} ${cmd}`, {
110
110
  encoding: 'utf-8',
111
- stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
111
+ stdio: options.stdio || 'pipe',
112
+ input: options.input,
113
+ cwd: options.cwd || process.cwd(),
112
114
  env: {
113
115
  ...process.env,
114
116
  PATH: process.env.PATH
115
117
  }
116
118
  });
119
+ return typeof result === 'string' ? result : '';
117
120
  } catch (e) {
118
- if (options.throw) throw e;
121
+ if (options.throw) throw new Error(`Wrangler command failed: ${cmd}\n${e.stderr || e.message}`);
119
122
  return null;
120
123
  }
121
124
  }
122
125
 
123
- /**
124
- - 🎯 BULLETPROOF KV PROVISIONING ENGINE
125
- */
126
- function getOrCreateKV(title) {
127
- const out = runWrangler(`kv namespace create "${title}"`);
128
- if (out) {
129
- const match = out.match(/"id":\s*"([a-fA-F0-9]{32})"/);
130
- if (match) return match[1];
131
- }
126
+ function shellQuote(value) {
127
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
128
+ }
132
129
 
133
- const listOut = runWrangler(`kv namespace list`);
130
+ function getOrCreateKV(title, cwd = process.cwd()) {
131
+ const listOut = runWrangler('kv namespace list', { cwd });
134
132
  if (listOut) {
135
133
  try {
136
134
  const namespaces = JSON.parse(listOut);
@@ -139,7 +137,136 @@ function getOrCreateKV(title) {
139
137
  } catch (err) { }
140
138
  }
141
139
 
142
- return `REPLACE_WITH_${title}_ID`;
140
+ const createOut = runWrangler(`kv namespace create ${shellQuote(title)}`, { cwd, throw: true });
141
+ const match = createOut.match(/"id":\s*"([a-fA-F0-9]{32})"/);
142
+ if (match) return match[1];
143
+
144
+ throw new Error(`Unable to create or find Cloudflare KV namespace "${title}". Check Wrangler authentication and permissions.`);
145
+ }
146
+
147
+ function getOrCreateD1(name, cwd = process.cwd()) {
148
+ const listOutput = runWrangler('d1 list --json', { cwd, throw: true });
149
+ let databases;
150
+ try {
151
+ databases = JSON.parse(listOutput);
152
+ } catch (error) {
153
+ throw new Error('Wrangler returned invalid JSON while listing D1 databases.');
154
+ }
155
+
156
+ const existing = databases.find(database => database.name === name);
157
+ if (existing?.uuid) return existing.uuid;
158
+
159
+ const createOutput = runWrangler(`d1 create ${shellQuote(name)}`, { cwd, throw: true });
160
+ const created = createOutput.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}/);
161
+ if (created) return created[0];
162
+
163
+ throw new Error(`Cloudflare D1 database "${name}" was not created or returned a valid ID.`);
164
+ }
165
+
166
+ function ensureR2Bucket(name, cwd = process.cwd()) {
167
+ if (runWrangler(`r2 bucket info ${shellQuote(name)} --json`, { cwd })) return;
168
+
169
+ const createOutput = runWrangler(`r2 bucket create ${shellQuote(name)}`, { cwd });
170
+ if (createOutput || runWrangler(`r2 bucket info ${shellQuote(name)} --json`, { cwd })) return;
171
+
172
+ throw new Error(`Unable to create or find Cloudflare R2 bucket "${name}". Check Wrangler authentication and permissions.`);
173
+ }
174
+
175
+ function ensureCloudflareAuth(cwd) {
176
+ if (runWrangler('whoami', { cwd })) return;
177
+
178
+ console.log(pc.dim('Launching browser for Cloudflare login...'));
179
+ runWrangler('login', { cwd, stdio: 'inherit', throw: true });
180
+ if (!runWrangler('whoami', { cwd })) {
181
+ throw new Error('Cloudflare authentication could not be verified after login.');
182
+ }
183
+ }
184
+
185
+ function writeWorkerSecrets(workerDir, secret, cloudflareApiToken = '') {
186
+ const values = [`FRONTEND_API_SECRET=${secret}`];
187
+ if (cloudflareApiToken) values.push(`CLOUDFLARE_API_TOKEN=${cloudflareApiToken}`);
188
+ writeFileSync(join(workerDir, '.secrets'), `${values.join('\n')}\n`, { mode: 0o600 });
189
+ }
190
+
191
+ function readEnvValue(filePath, key) {
192
+ if (!existsSync(filePath)) return '';
193
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
194
+ const match = readFileSync(filePath, 'utf8').match(new RegExp(`^${escapedKey}=["']?([^"'\\r\\n]*)`, 'm'));
195
+ return match?.[1]?.trim() || '';
196
+ }
197
+
198
+ async function resolveCloudflareZoneId(domain, token) {
199
+ if (!token) return '';
200
+
201
+ const labels = domain.split('.');
202
+ for (let index = 0; index < labels.length - 1; index++) {
203
+ const zoneName = labels.slice(index).join('.');
204
+ const response = await fetch(`https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent(zoneName)}&per_page=50`, {
205
+ headers: { Authorization: `Bearer ${token}` }
206
+ });
207
+ const data = await response.json();
208
+ if (!response.ok || !data.success) {
209
+ throw new Error(`Could not look up Cloudflare zone for ${domain}. Check the token's Zone Read permission.`);
210
+ }
211
+ const zone = data.result?.find(item => item.name === zoneName && item.status === 'active');
212
+ if (zone?.id) return zone.id;
213
+ }
214
+
215
+ throw new Error(`No active Cloudflare zone was found for ${domain}. Add the domain to Cloudflare or enter its Zone ID manually.`);
216
+ }
217
+
218
+ async function promptForCloudflareApiToken(existingToken = '') {
219
+ const token = handleCancel(await password({
220
+ message: `Optional Cloudflare API token (Zone Read + Zone Cache Purge${existingToken ? '; leave blank to keep existing token' : ''}):`,
221
+ mask: '*'
222
+ }));
223
+ return token.trim() || existingToken;
224
+ }
225
+
226
+ function normalizeCloudflareDomain(value, label) {
227
+ const input = String(value || '').trim();
228
+ if (!input) throw new Error(`${label} is required for Cloudflare deployment.`);
229
+
230
+ try {
231
+ const url = new URL(input.includes('://') ? input : `https://${input}`);
232
+ if (!url.hostname.includes('.') || url.hostname === 'localhost') throw new Error();
233
+ return url.host.toLowerCase();
234
+ } catch {
235
+ throw new Error(`${label} must be a valid Cloudflare domain, for example example.com.`);
236
+ }
237
+ }
238
+
239
+ function ensureGitIgnoreEntries(rootDir, entries) {
240
+ const gitignorePath = join(rootDir, '.gitignore');
241
+ const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
242
+ const lines = new Set(existing.split(/\r?\n/));
243
+ const missing = entries.filter(line => !lines.has(line));
244
+ if (missing.length > 0) appendFileSync(gitignorePath, `${existing && !existing.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`);
245
+ }
246
+
247
+ function ignoreSecretFiles(rootDir) {
248
+ ensureGitIgnoreEntries(rootDir, ['kama-worker/.secrets', 'kama-worker/.dev.vars', 'sites/*/.secrets', 'sites/*/.dev.vars']);
249
+ }
250
+
251
+ function updateEnvFile(filePath, values, removeKeys = []) {
252
+ const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
253
+ const keys = new Set(Object.keys(values));
254
+ const found = new Set();
255
+ const lines = existing.split(/\r?\n/).filter(line => {
256
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/);
257
+ return !match || !removeKeys.includes(match[1]);
258
+ }).map(line => {
259
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/);
260
+ if (!match || !keys.has(match[1])) return line;
261
+ found.add(match[1]);
262
+ return `${match[1]}=${JSON.stringify(values[match[1]])}`;
263
+ });
264
+
265
+ for (const key of keys) {
266
+ if (!found.has(key)) lines.push(`${key}=${JSON.stringify(values[key])}`);
267
+ }
268
+
269
+ writeFileSync(filePath, `${lines.filter((line, index) => line || index < lines.length - 1).join('\n')}\n`);
143
270
  }
144
271
 
145
272
  function showHelp() {
@@ -154,9 +281,9 @@ function showHelp() {
154
281
  console.log(pc.cyan('Workspace Scripts (Run from project root):'));
155
282
  console.log(' npm update Update to latest KAMA version');
156
283
  console.log(' npm run setup Initialize database schema (Run Once)');
157
- console.log(' npm run seed -w kama-worker Seed the database with mock test data'); // 🎯 NEW LINE
284
+ console.log(' npm run seed -w kama-worker Seed the database with mock test data');
158
285
  console.log(' npm run dev Boot up the CMS backend and Astro frontend concurrently');
159
- console.log(' npm run deploy:cms Deploy the backend worker and database to Cloudflare');
286
+ console.log(' npm run deploy:kama Deploy the backend worker and database to Cloudflare');
160
287
  console.log(' npm run deploy:site Deploy the Astro frontend site to Cloudflare\n');
161
288
  }
162
289
 
@@ -278,8 +405,14 @@ async function createFullWorkspace() {
278
405
  defaultValue: ''
279
406
  }));
280
407
  }
408
+
409
+ cmsDomain = normalizeCloudflareDomain(cmsDomain, 'CMS Admin Domain');
410
+ if (shouldCreateSite) siteDomain = normalizeCloudflareDomain(siteDomain, 'Frontend Website Domain');
281
411
  }
282
412
 
413
+ const cloudflareApiToken = isCloudflare ? await promptForCloudflareApiToken() : '';
414
+ const cloudflareZoneId = isCloudflare ? await resolveCloudflareZoneId(cmsDomain, cloudflareApiToken) : '';
415
+
283
416
  const cmsProtocol = isCloudflare ? 'https://' : 'http://';
284
417
  const cmsFullUrl = cmsDomain ? `${cmsProtocol}${cmsDomain.replace(/^https?:\/\//, '')}` : 'http://localhost:8787';
285
418
 
@@ -295,82 +428,42 @@ async function createFullWorkspace() {
295
428
 
296
429
  if (!isCurrentDir) mkdirSync(rootDir, { recursive: true });
297
430
 
298
- writeFileSync(
299
- join(rootDir, '.gitignore'),
300
- `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`
301
- );
431
+ ensureGitIgnoreEntries(rootDir, [
432
+ 'node_modules/', '.env', '.DS_Store', 'kama-worker/.dev.vars', 'kama-worker/.secrets',
433
+ 'kama-worker/.env', 'kama-worker/.wrangler/', 'kama-worker/public/', 'kama-worker/*.db',
434
+ 'sites/*/.env', 'sites/*/.secrets', 'sites/*/.astro/', 'sites/*/dist/',
435
+ 'kama-worker/bundled-definitions.js'
436
+ ]);
302
437
 
303
438
  const sourceDefsDir = join(__dirname, '../content/definitions');
304
439
  const targetDefsDir = join(rootDir, 'content', 'definitions');
305
440
  if (existsSync(sourceDefsDir)) cpSync(sourceDefsDir, targetDefsDir, { recursive: true });
306
441
 
307
442
  let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
308
- let backendCacheId = 'REPLACE_WITH_CACHE_KV_ID';
309
- let isAuthenticated = false;
310
-
443
+ let backendCacheId = '';
311
444
  if (isCloudflare) {
312
- s.message('Checking Cloudflare authentication...');
313
- const whoami = runWrangler('whoami');
314
- if (whoami && (whoami.includes('Getting User Settings') || whoami.includes('You are logged in'))) {
315
- isAuthenticated = true;
316
- } else {
317
- s.stop('Not authenticated with Cloudflare.', true);
318
- console.log(pc.dim('Launching browser for Cloudflare login...'));
319
- const login = runWrangler('login', { stdio: 'inherit' });
320
- if (login !== null) isAuthenticated = true;
321
- s.start('Resuming provisioning...');
322
- }
323
-
324
- if (isAuthenticated) {
325
- s.message('Provisioning backend KV Cache...');
326
- backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`);
327
-
328
- s.message('Checking existing Cloudflare D1 databases...');
329
- const listOutput = runWrangler('d1 list --json');
330
- if (listOutput) {
331
- try {
332
- const jsonStart = listOutput.indexOf('[');
333
- const jsonEnd = listOutput.lastIndexOf(']') + 1;
334
- let databases = [];
335
-
336
- if (jsonStart !== -1 && jsonEnd !== -1) databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
337
- const existingDb = databases.find(db => db.name === dbName);
338
-
339
- if (existingDb) {
340
- s.stop(`Database '${dbName}' already exists on your account.`);
341
- const useExisting = handleCancel(await confirm({ message: 'Do you want to link this existing database?', initialValue: true }));
342
-
343
- if (useExisting) {
344
- d1DatabaseId = existingDb.uuid;
345
- s.start(`Linked existing database ID: ${d1DatabaseId}`);
346
- } else {
347
- dbName = handleCancel(await text({ message: 'Enter a new database name:' }));
348
- s.start(`Creating new database '${dbName}'...`);
349
- const d1Output = runWrangler(`d1 create ${dbName}`);
350
- 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;
351
- if (match) d1DatabaseId = match[0];
352
- }
353
- } else {
354
- s.message(`Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
355
- const d1Output = runWrangler(`d1 create ${dbName}`);
356
- 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;
357
- if (match) d1DatabaseId = match[0];
358
- }
359
- } catch (error) { }
360
- }
361
- }
445
+ s.message('Authenticating with Cloudflare...');
446
+ ensureCloudflareAuth(process.cwd());
447
+ s.message('Provisioning D1, KV, and R2 resources...');
448
+ backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`, rootDir);
449
+ d1DatabaseId = getOrCreateD1(dbName, rootDir);
450
+ ensureR2Bucket(bucketName, rootDir);
362
451
  }
363
452
 
364
453
  s.message('Writing configuration files...');
365
454
 
455
+ const rootPkgPath = join(rootDir, 'package.json');
456
+ const existingRootPkg = existsSync(rootPkgPath) ? JSON.parse(readFileSync(rootPkgPath, 'utf-8')) : {};
457
+ const existingWorkspaces = Array.isArray(existingRootPkg.workspaces) ? existingRootPkg.workspaces : [];
366
458
  const rootPkg = {
367
- name: projName,
459
+ ...existingRootPkg,
460
+ name: existingRootPkg.name || projName,
368
461
  private: true,
369
- type: 'module',
370
- workspaces: ['kama-worker', 'sites/*'],
371
- scripts: { 'setup': 'npm run setup:local -w kama-worker' },
372
- dependencies: { '@brightsideoy/kama': currentKamaVersion },
373
- devDependencies: {}
462
+ type: existingRootPkg.type || 'module',
463
+ workspaces: [...new Set([...existingWorkspaces, 'kama-worker', 'sites/*'])],
464
+ scripts: { ...existingRootPkg.scripts, 'setup': 'npx kama bundle && npm run setup:local -w kama-worker' },
465
+ dependencies: { ...existingRootPkg.dependencies, '@brightsideoy/kama': currentKamaVersion },
466
+ devDependencies: { ...existingRootPkg.devDependencies }
374
467
  };
375
468
 
376
469
  if (isCloudflare) rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
@@ -381,14 +474,14 @@ async function createFullWorkspace() {
381
474
  rootPkg.scripts['predev'] = 'npx kama bundle';
382
475
  rootPkg.scripts['dev'] = 'npx concurrently "npm run dev -w kama-worker" "npm run dev -w sites/' + siteFolderName + '"';
383
476
  rootPkg.scripts['deploy:site'] = `npm run deploy -w sites/${siteFolderName}`;
384
- rootPkg.scripts['deploy:cms'] = `npm run deploy -w kama-worker`;
477
+ rootPkg.scripts['deploy:kama'] = `npm run deploy -w kama-worker`;
385
478
  } else {
386
479
  rootPkg.scripts['predev'] = 'npx kama bundle';
387
480
  rootPkg.scripts['dev'] = 'npm run dev -w kama-worker';
388
- rootPkg.scripts['deploy:cms'] = `npm run deploy -w kama-worker`;
481
+ rootPkg.scripts['deploy:kama'] = `npm run deploy -w kama-worker`;
389
482
  }
390
483
 
391
- writeFileSync(join(rootDir, 'package.json'), JSON.stringify(rootPkg, null, 2));
484
+ writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2));
392
485
 
393
486
  const cmsDir = join(rootDir, 'kama-worker');
394
487
  mkdirSync(cmsDir, { recursive: true });
@@ -407,17 +500,16 @@ async function createFullWorkspace() {
407
500
  type: 'module',
408
501
  version: '1.0.0',
409
502
  scripts: {
410
- // 🎯 Uses native node with the compiled dist/build-seed.js
411
503
  "seed": "npm run seed:local",
412
- "seed:local": "node ../node_modules/@brightsideoy/kama/dist/build-seed.js && wrangler d1 execute " + dbName + " --local --file=seed.sql",
413
- "seed:remote": "node ../node_modules/@brightsideoy/kama/dist/build-seed.js && wrangler d1 execute " + dbName + " --remote --file=seed.sql",
414
- "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql > /dev/null 2>&1`,
504
+ "seed:local": "node ../node_modules/@brightsideoy/kama/dist/build-seed.js && for f in seed-*.sql; do wrangler d1 execute " + dbName + " --local --file=\"$f\"; done",
505
+ "seed:remote": "node ../node_modules/@brightsideoy/kama/dist/build-seed.js && for f in seed-*.sql; do wrangler d1 execute " + dbName + " --remote --file=\"$f\"; done",
506
+ "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`,
415
507
  "setup:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`,
416
508
  "dev": "wrangler dev --port 8787 --inspector-port 9239",
417
- "deploy": "npm run setup:remote && wrangler deploy"
509
+ "deploy": "npm run setup:remote && wrangler deploy --secrets-file .secrets"
418
510
  },
419
511
  dependencies: { '@brightsideoy/kama': currentKamaVersion, '@libsql/client': '^0.14.0' },
420
- devDependencies: { "wrangler": WRANGLER_LATEST }, // 🎯 Removed TSX requirement
512
+ devDependencies: { "wrangler": WRANGLER_LATEST },
421
513
  }, null, 2)
422
514
  );
423
515
 
@@ -434,7 +526,7 @@ async function createFullWorkspace() {
434
526
  vars: {
435
527
  CDN_DOMAIN: cmsDomain || 'localhost:8787',
436
528
  PUBLIC_FRONTEND_URL: (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321',
437
- CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID",
529
+ CLOUDFLARE_ZONE_ID: cloudflareZoneId,
438
530
  KAMA_RUNTIME: 'cloudflare'
439
531
  },
440
532
  d1_databases: [{ binding: 'DB', database_name: dbName, database_id: d1DatabaseId }],
@@ -444,9 +536,11 @@ async function createFullWorkspace() {
444
536
 
445
537
  writeFileSync(join(cmsDir, 'wrangler.json'), JSON.stringify(cmsWranglerConfig, null, 2));
446
538
 
447
- // 🎯 Inject API Secret directly into .dev.vars
448
539
  const publicFrontendUrl = (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321';
449
- writeFileSync(join(cmsDir, '.dev.vars'), `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\n\n# Local Development Overrides\nCDN_DOMAIN=\nPUBLIC_FRONTEND_URL=${publicFrontendUrl}\n\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`);
540
+ const devVars = [`# Local Development Overrides`, `CDN_DOMAIN=`, `PUBLIC_FRONTEND_URL=${publicFrontendUrl}`, ``, `# Server-to-Server API Security`, `FRONTEND_API_SECRET="${frontendApiSecret}"`];
541
+ if (cloudflareApiToken) devVars.push(`CLOUDFLARE_API_TOKEN="${cloudflareApiToken}"`);
542
+ writeFileSync(join(cmsDir, '.dev.vars'), `${devVars.join('\n')}\n`, { mode: 0o600 });
543
+ writeWorkerSecrets(cmsDir, frontendApiSecret, cloudflareApiToken);
450
544
  } else {
451
545
  const templateServerPath = join(__dirname, '../templates/kama-worker/server.js');
452
546
  if (existsSync(templateServerPath)) cpSync(templateServerPath, join(cmsDir, 'server.js'));
@@ -458,20 +552,19 @@ async function createFullWorkspace() {
458
552
  type: 'module',
459
553
  version: '1.0.0',
460
554
  scripts: {
461
- // 🎯 Uses native node with the compiled dist/build-seed.js
462
555
  "seed": "npm run seed:local",
463
- "seed:local": "node ../node_modules/@brightsideoy/kama/dist/build-seed.js && sqlite3 kama.db < seed.sql",
464
- "setup:local": "sqlite3 kama.db < ../node_modules/@brightsideoy/kama/schema.sql",
556
+ "seed:local": "node ../node_modules/@brightsideoy/kama/dist/build-seed.js && node setup-local-db.mjs seed",
557
+ "setup:local": "node setup-local-db.mjs schema",
465
558
  "dev": "tsx server.js",
466
559
  "start": "node server.js"
467
560
  },
468
561
  dependencies: { '@brightsideoy/kama': currentKamaVersion, '@hono/node-server': '^1.13.0', '@libsql/client': '^0.14.0', 'sharp': '^0.33.5', 'dotenv': '^16.4.5' },
469
- devDependencies: { "tsx": "^4.19.0" }, // 🎯 Kept because "dev" uses it
562
+ devDependencies: { "tsx": "^4.19.0" },
470
563
  }, null, 2)
471
564
  );
472
565
 
473
- // 🎯 Inject API Secret directly into .env
474
566
  writeFileSync(join(cmsDir, '.env'), `PORT=8787\nKAMA_RUNTIME=node\nDB_PATH=./kama.db\nLOCAL_MEDIA_PATH=./public/media\nPUBLIC_CMS_URL=http://127.0.0.1:8787\nPUBLIC_API_URL=http://127.0.0.1:8787\nPUBLIC_FRONTEND_URL=http://127.0.0.1:4321\n\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`);
567
+ cpSync(join(__dirname, '../templates/kama-worker/setup-local-db.mjs'), join(cmsDir, 'setup-local-db.mjs'));
475
568
  }
476
569
 
477
570
  s.message('Bundling schemas...');
@@ -479,24 +572,26 @@ async function createFullWorkspace() {
479
572
 
480
573
  if (shouldCreateSite) {
481
574
  s.message(`Scaffolding Astro site (${siteFolderName})...`);
482
- // 🎯 Pass secret to the frontend generator
483
575
  await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsFullUrl, workerName, isCloudflare, frontendApiSecret);
484
576
  }
485
577
 
486
- s.stop('Workspace provisioned successfully.');
578
+ s.message('Installing workspace dependencies...');
579
+ execSync('npm install', { cwd: rootDir, stdio: 'inherit' });
580
+ s.message('Bundling schemas and initializing the local database...');
581
+ execSync('npm run setup', { cwd: rootDir, stdio: 'inherit' });
582
+
583
+ s.stop('Workspace installed and initialized successfully.');
487
584
 
488
585
  let nextSteps = '';
489
586
  let stepCount = 1;
490
587
  if (!isCurrentDir) nextSteps += `${stepCount++}. cd ${projName}\n`;
491
- nextSteps += `${stepCount++}. npm install\n`;
492
- nextSteps += `${stepCount++}. npm run setup ${pc.dim('<- Run this once to init the database schema!')}\n`;
493
588
  nextSteps += `${stepCount++}. npm run dev\n`;
494
589
 
495
590
  if (isCloudflare) {
496
591
  nextSteps += `\n${pc.dim('Cloudflare Note:')}\n`;
497
- nextSteps += `${pc.dim('Update CLOUDFLARE_ZONE_ID in wrangler.json')}\n`;
498
- nextSteps += `${pc.dim('Set Cache purge token: npx wrangler secret put CLOUDFLARE_API_TOKEN')}\n`;
499
- nextSteps += `${pc.dim('Set Frontend API secret: npx wrangler secret put FRONTEND_API_SECRET')}`;
592
+ nextSteps += cloudflareApiToken
593
+ ? `${pc.dim('Zone ID was discovered automatically; the purge token is stored as an ignored Worker secret.')}`
594
+ : `${pc.dim('Zone-level cache purging is disabled. Run kama promote with a token to enable it.')}`;
500
595
  }
501
596
 
502
597
  note(nextSteps, 'Next steps');
@@ -528,7 +623,6 @@ async function createNewSiteOnly() {
528
623
  } catch (e) { }
529
624
  }
530
625
 
531
- // 🎯 Ensure secret exists in worker before provisioning site
532
626
  const frontendApiSecret = existsSync(workerDir) ? getOrGenerateWorkspaceSecret(workerDir) : crypto.randomBytes(32).toString('hex');
533
627
 
534
628
  const isCloudflare = handleCancel(await select({
@@ -550,23 +644,29 @@ async function createNewSiteOnly() {
550
644
 
551
645
  if (isCloudflare) {
552
646
  siteDomain = handleCancel(await text({ message: 'Website Domain (e.g., brand.com):', defaultValue: '' }));
647
+ siteDomain = normalizeCloudflareDomain(siteDomain, 'Frontend Website Domain');
553
648
  if (detectedCmsDomain) {
554
649
  cmsFullUrl = detectedCmsDomain.startsWith('http') ? detectedCmsDomain : `https://${detectedCmsDomain.replace(/^https?:\/\//, '')}`;
555
650
  } else {
556
651
  const cmsDomainInput = handleCancel(await text({ message: 'CMS API Domain (e.g., admin.brand.com):', defaultValue: '' }));
557
- cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
652
+ cmsFullUrl = `https://${normalizeCloudflareDomain(cmsDomainInput, 'CMS API Domain')}`;
558
653
  }
654
+ ensureCloudflareAuth(rootDir);
559
655
  }
560
656
 
657
+ ignoreSecretFiles(rootDir);
658
+
561
659
  const s = spinner();
562
660
  s.start(`Generating Astro site...`);
563
661
 
564
- // 🎯 Pass secret to the frontend generator
565
662
  await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare, frontendApiSecret);
663
+ ignoreSecretFiles(rootDir);
664
+ s.message('Installing workspace dependencies...');
665
+ execSync('npm install', { cwd: rootDir, stdio: 'inherit' });
566
666
 
567
667
  s.stop(`Site created in sites/${siteFolder}!`);
568
668
 
569
- note(`1. npm install\n2. npm run dev`, 'Next steps');
669
+ note('Run `npm run dev` from the workspace root to start the new site.', 'Next steps');
570
670
  outro('Site added successfully! 🚀');
571
671
  }
572
672
 
@@ -582,10 +682,11 @@ async function bundleWorkspaceSchemas(targetRootDir, silent = false) {
582
682
  try {
583
683
  let compilerPath = join(__dirname, '../dist/schema-compiler.js');
584
684
  if (!existsSync(compilerPath)) compilerPath = join(__dirname, '../scripts/schema-compiler.ts');
585
- await import(pathToFileURL(compilerPath).href);
685
+ execSync(`${shellQuote(process.execPath)} ${shellQuote(compilerPath)}`, { cwd: rootDir, stdio: 'inherit' });
586
686
  if (!silent) s.stop('Schemas bundled successfully.');
587
687
  } catch (e) {
588
688
  if (!silent) s.stop(`Schema compilation failed: ${e.message}`, true);
689
+ else throw e;
589
690
  }
590
691
  }
591
692
 
@@ -602,26 +703,20 @@ async function promoteLocalToCloudflare() {
602
703
  process.exit(1);
603
704
  }
604
705
 
605
- // 🎯 Safely persist the secret during promotion
606
706
  const frontendApiSecret = getOrGenerateWorkspaceSecret(workerDir);
607
707
 
608
708
  const s = spinner();
609
709
  s.start('Checking Cloudflare authentication...');
710
+ ensureCloudflareAuth(rootDir);
711
+ s.stop('Cloudflare authentication verified.');
610
712
 
611
- const whoami = runWrangler('whoami');
612
- if (whoami && (whoami.includes('Getting User Settings') || whoami.includes('You are logged in'))) {
613
- s.stop('Logged in to Cloudflare.');
614
- } else {
615
- s.stop('Not logged in to Cloudflare.', true);
616
- console.log(pc.dim('Launching browser for Cloudflare login...'));
617
- const login = runWrangler('login', { stdio: 'inherit' });
618
- if (login === null) {
619
- cancel('Cloudflare login failed.');
620
- process.exit(1);
621
- }
622
- }
623
-
624
- const cmsDomain = handleCancel(await text({ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):', defaultValue: '' }));
713
+ const cmsDomain = normalizeCloudflareDomain(
714
+ handleCancel(await text({ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):', defaultValue: '' })),
715
+ 'CMS Admin Domain'
716
+ );
717
+ const existingCloudflareToken = readEnvValue(join(workerDir, '.dev.vars'), 'CLOUDFLARE_API_TOKEN') || readEnvValue(join(workerDir, '.secrets'), 'CLOUDFLARE_API_TOKEN');
718
+ const cloudflareApiToken = await promptForCloudflareApiToken(existingCloudflareToken);
719
+ const cloudflareZoneId = await resolveCloudflareZoneId(cmsDomain, cloudflareApiToken);
625
720
 
626
721
  const sitesFolderDir = join(rootDir, 'sites');
627
722
  let siteDomain = '';
@@ -630,26 +725,24 @@ async function promoteLocalToCloudflare() {
630
725
  const sites = readdirSync(sitesFolderDir);
631
726
  if (sites.length > 0) {
632
727
  siteDir = join(sitesFolderDir, sites[0]);
633
- siteDomain = handleCancel(await text({ message: `Frontend Website Domain for ${sites[0]}:`, defaultValue: '' }));
728
+ siteDomain = normalizeCloudflareDomain(
729
+ handleCancel(await text({ message: `Frontend Website Domain for ${sites[0]}:`, defaultValue: '' })),
730
+ 'Frontend Website Domain'
731
+ );
634
732
  }
635
733
  }
636
734
 
735
+ ignoreSecretFiles(rootDir);
736
+
637
737
  s.start('Upgrading workspace to Cloudflare Edge...');
638
738
 
639
739
  const projName = basename(rootDir);
640
- let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
641
- let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
642
-
643
- const d1Output = runWrangler(`d1 create ${dbName}`);
644
- if (d1Output) {
645
- 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}/);
646
- if (match) d1DatabaseId = match[0];
647
- }
648
-
649
740
  const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
650
741
  const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
651
-
652
- const backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`);
742
+ const dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
743
+ const d1DatabaseId = getOrCreateD1(dbName, rootDir);
744
+ const backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`, rootDir);
745
+ ensureR2Bucket(bucketName, rootDir);
653
746
 
654
747
  const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
655
748
  rootPkg.devDependencies = rootPkg.devDependencies || {};
@@ -665,10 +758,21 @@ async function promoteLocalToCloudflare() {
665
758
  compatibility_flags: ['nodejs_compat'],
666
759
  assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
667
760
  images: { binding: 'IMAGES' },
668
- vars: { CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID", KAMA_RUNTIME: 'cloudflare' }
761
+ vars: { CLOUDFLARE_ZONE_ID: '', KAMA_RUNTIME: 'cloudflare' }
669
762
  };
670
763
 
764
+ const cloudflareEntryPath = join(workerDir, 'index.js');
765
+ if (!existsSync(cloudflareEntryPath)) {
766
+ cpSync(join(__dirname, '../templates/kama-worker/index.js'), cloudflareEntryPath);
767
+ }
768
+
769
+ currentConfig.main = 'index.js';
671
770
  currentConfig.dev = currentConfig.dev || { host: 'localhost', port: 8787 };
771
+ currentConfig.vars = currentConfig.vars || {};
772
+ if (String(currentConfig.vars.CLOUDFLARE_ZONE_ID || '').startsWith('REPLACE_WITH_')) {
773
+ currentConfig.vars.CLOUDFLARE_ZONE_ID = '';
774
+ }
775
+ currentConfig.vars.CLOUDFLARE_ZONE_ID = cloudflareZoneId || currentConfig.vars.CLOUDFLARE_ZONE_ID || '';
672
776
  currentConfig.vars.CDN_DOMAIN = cmsDomain;
673
777
  if (siteDomain) currentConfig.vars.PUBLIC_FRONTEND_URL = `https://${siteDomain}`;
674
778
  currentConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
@@ -678,26 +782,29 @@ async function promoteLocalToCloudflare() {
678
782
 
679
783
  writeFileSync(workerWranglerPath, JSON.stringify(currentConfig, null, 2));
680
784
 
681
- if (!existsSync(join(workerDir, '.dev.vars'))) {
682
- writeFileSync(join(workerDir, '.dev.vars'), `CLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\nCDN_DOMAIN=\nPUBLIC_FRONTEND_URL=http://localhost:4321\n\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`);
683
- }
785
+ updateEnvFile(join(workerDir, '.dev.vars'), {
786
+ CDN_DOMAIN: '',
787
+ PUBLIC_FRONTEND_URL: 'http://localhost:4321',
788
+ FRONTEND_API_SECRET: frontendApiSecret,
789
+ ...(cloudflareApiToken ? { CLOUDFLARE_API_TOKEN: cloudflareApiToken } : {})
790
+ });
791
+ writeWorkerSecrets(workerDir, frontendApiSecret, cloudflareApiToken);
684
792
 
685
793
  const workerPkgPath = join(workerDir, 'package.json');
686
794
  if (existsSync(workerPkgPath)) {
687
795
  const workerPkg = JSON.parse(readFileSync(workerPkgPath, 'utf-8'));
688
796
  workerPkg.scripts.dev = "wrangler dev --port 8787 --inspector-port 9239";
689
- workerPkg.scripts.deploy = "npm run setup:remote && wrangler deploy";
797
+ workerPkg.scripts.deploy = "npm run setup:remote && wrangler deploy --secrets-file .secrets";
690
798
  workerPkg.scripts["setup:local"] = `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`;
691
799
  workerPkg.scripts["setup:remote"] = `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`;
692
800
 
693
- // 🎯 Uses native node with the compiled dist/build-seed.js
694
801
  workerPkg.scripts["seed"] = "npm run seed:local";
695
- workerPkg.scripts["seed:local"] = `node ../node_modules/@brightsideoy/kama/dist/build-seed.js && wrangler d1 execute ${dbName} --local --file=seed.sql`;
696
- workerPkg.scripts["seed:remote"] = `node ../node_modules/@brightsideoy/kama/dist/build-seed.js && wrangler d1 execute ${dbName} --remote --file=seed.sql`;
802
+ // 🎯 3. ARCHITECTURE FIX: Chunked seed loops for promoted worker
803
+ workerPkg.scripts["seed:local"] = `node ../node_modules/@brightsideoy/kama/dist/build-seed.js && for f in seed-*.sql; do wrangler d1 execute ${dbName} --local --file="$f"; done`;
804
+ workerPkg.scripts["seed:remote"] = `node ../node_modules/@brightsideoy/kama/dist/build-seed.js && for f in seed-*.sql; do wrangler d1 execute ${dbName} --remote --file="$f"; done`;
697
805
 
698
806
  workerPkg.devDependencies = workerPkg.devDependencies || {};
699
807
  workerPkg.devDependencies.wrangler = WRANGLER_LATEST;
700
- // 🎯 explicit deletion of TSX addition on promotion
701
808
  if (workerPkg.devDependencies.tsx) delete workerPkg.devDependencies.tsx;
702
809
 
703
810
  writeFileSync(workerPkgPath, JSON.stringify(workerPkg, null, 2));
@@ -705,8 +812,8 @@ async function promoteLocalToCloudflare() {
705
812
 
706
813
  if (siteDir && siteDomain) {
707
814
  const prefix = basename(siteDir).replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
708
- const sessionId = getOrCreateKV(`SESSION_${prefix}`);
709
- const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`);
815
+ const sessionId = getOrCreateKV(`SESSION_${prefix}`, rootDir);
816
+ const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`, rootDir);
710
817
 
711
818
  const siteWranglerPath = join(siteDir, 'wrangler.json');
712
819
  const siteConfig = {
@@ -719,7 +826,8 @@ async function promoteLocalToCloudflare() {
719
826
  assets: { directory: "./dist/client", binding: "ASSETS", run_worker_first: true },
720
827
  kv_namespaces: [{ binding: "SESSION", id: sessionId }, { binding: "HTML_CACHE", id: htmlCacheId }],
721
828
  services: [{ binding: "CMS_WORKER", service: currentConfig.name }],
722
- routes: [{ pattern: siteDomain, zone_name: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }],
829
+ // 🎯 2. ARCHITECTURE FIX: Removed strict zone_name requirement
830
+ routes: [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }],
723
831
  vars: { PUBLIC_CMS_URL: `https://${cmsDomain}`, PUBLIC_API_URL: `https://${cmsDomain}` }
724
832
  };
725
833
  writeFileSync(siteWranglerPath, JSON.stringify(siteConfig, null, 2));
@@ -733,14 +841,35 @@ async function promoteLocalToCloudflare() {
733
841
  sitePkg.dependencies['@astrojs/cloudflare'] = ASTRO_CF_LATEST;
734
842
  sitePkg.devDependencies = sitePkg.devDependencies || {};
735
843
  sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
844
+ sitePkg.scripts = sitePkg.scripts || {};
845
+ sitePkg.scripts.deploy = 'astro build && wrangler deploy --secrets-file .secrets';
736
846
  writeFileSync(sitePkgPath, JSON.stringify(sitePkg, null, 2));
737
847
  }
738
848
 
849
+ writeWorkerSecrets(siteDir, frontendApiSecret);
850
+ updateEnvFile(join(siteDir, '.env'), {
851
+ PUBLIC_CMS_URL: `https://${cmsDomain}`,
852
+ PUBLIC_API_URL: `https://${cmsDomain}`
853
+ }, ['FRONTEND_API_SECRET']);
854
+ updateEnvFile(join(siteDir, '.dev.vars'), { FRONTEND_API_SECRET: frontendApiSecret });
739
855
  convertAstroConfigToCloudflare(siteDir);
740
856
  }
741
857
 
742
- s.stop('Workspace successfully upgraded to Cloudflare Edge!');
743
- outro('Promotion complete. 🚀\nRemember to run `npx wrangler secret put FRONTEND_API_SECRET` in both your worker and site directories before production deployment!');
858
+ s.message('Installing Cloudflare dependencies...');
859
+ execSync('npm install', { cwd: rootDir, stdio: 'inherit' });
860
+ s.message('Bundling workspace schemas...');
861
+ await bundleWorkspaceSchemas(rootDir, true);
862
+ s.message('Deploying CMS and initializing the remote database...');
863
+ execSync('npm run deploy -w kama-worker', { cwd: rootDir, stdio: 'inherit' });
864
+
865
+ if (siteDir && siteDomain) {
866
+ const siteFolder = basename(siteDir);
867
+ s.message(`Building and deploying ${siteFolder}...`);
868
+ execSync(`npm run deploy -w sites/${shellQuote(siteFolder)}`, { cwd: rootDir, stdio: 'inherit' });
869
+ }
870
+
871
+ s.stop('Workspace provisioned and deployed to Cloudflare.');
872
+ outro('Promotion complete.');
744
873
  }
745
874
 
746
875
  /**
@@ -778,6 +907,7 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
778
907
  if (sitePkg.dependencies['wrangler']) delete sitePkg.dependencies['wrangler'];
779
908
  sitePkg.devDependencies = sitePkg.devDependencies || {};
780
909
  sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
910
+ sitePkg.scripts.deploy = 'astro build && wrangler deploy --secrets-file .secrets';
781
911
  convertAstroConfigToCloudflare(siteDir);
782
912
  }
783
913
 
@@ -805,21 +935,29 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
805
935
  assets: { directory: "./dist/client", binding: "ASSETS", run_worker_first: true },
806
936
  kv_namespaces: [{ binding: "SESSION", id: sessionId }, { binding: "HTML_CACHE", id: htmlCacheId }],
807
937
  services: [{ binding: "CMS_WORKER", service: workerName }],
938
+ routes: [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }],
808
939
  vars: { PUBLIC_CMS_URL: cmsFullUrl, PUBLIC_API_URL: cmsFullUrl, PUBLIC_CHANNEL_ID: channelKey, PUBLIC_TREE_ID: treeKey, PUBLIC_SITE_NAME: siteLabel }
809
940
  };
810
941
 
811
942
  if (siteDomain !== 'localhost:4321') siteConfig.routes = [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }];
812
943
  writeFileSync(join(siteDir, 'wrangler.json'), JSON.stringify(siteConfig, null, 2));
944
+ writeWorkerSecrets(siteDir, frontendApiSecret);
813
945
  }
814
946
 
815
- // 🎯 Dynamically append the secret to the Astro environment setup
816
- let envContent = `# Local Development Overrides\nPUBLIC_CMS_URL="http://127.0.0.1:8787"\nPUBLIC_API_URL="http://127.0.0.1:8787"\nPUBLIC_CHANNEL_ID="${channelKey}"\nPUBLIC_TREE_ID="${treeKey}"\nPUBLIC_SITE_NAME="${siteLabel.replace(/"/g, '\\"')}"\n`;
947
+ const localCmsUrl = isCloudflare ? cmsFullUrl : 'http://127.0.0.1:8787';
948
+ let envContent = `# Local Development Overrides\nPUBLIC_CMS_URL="${localCmsUrl}"\nPUBLIC_API_URL="${localCmsUrl}"\nPUBLIC_CHANNEL_ID="${channelKey}"\nPUBLIC_TREE_ID="${treeKey}"\nPUBLIC_SITE_NAME="${siteLabel.replace(/"/g, '\\"')}"\n`;
817
949
 
818
- if (frontendApiSecret) {
950
+ if (frontendApiSecret && !isCloudflare) {
819
951
  envContent += `\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`;
820
952
  }
821
953
 
822
954
  writeFileSync(join(siteDir, '.env'), envContent);
955
+ if (isCloudflare && frontendApiSecret) {
956
+ writeFileSync(join(siteDir, '.dev.vars'), `FRONTEND_API_SECRET="${frontendApiSecret}"\n`, { mode: 0o600 });
957
+ }
823
958
  }
824
959
 
825
- main().catch(console.error);
960
+ main().catch(error => {
961
+ console.error(error);
962
+ process.exitCode = 1;
963
+ });