@brightsideoy/kama 1.0.0-alpha.13 → 1.0.0-alpha.17

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 (40) hide show
  1. package/bin/cli.js +282 -158
  2. package/content/definitions/taxonomies/categories.yaml +5 -2
  3. package/content/definitions/taxonomies/tags.yaml +5 -2
  4. package/dist/assets/AssetEditor-CCNDGlQ_.js +1 -0
  5. package/dist/assets/AssetEditor-D5j7pwcK.css +1 -0
  6. package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
  7. package/dist/assets/CmsHelp-m9EOY_nE.js +1 -0
  8. package/dist/assets/CmsPublisher-Cs_ys68V.js +3 -0
  9. package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
  10. package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
  11. package/dist/assets/CmsUserCenter-D-T6HyWF.js +1 -0
  12. package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
  13. package/dist/assets/EditorSplitPreview-CnWpXZWQ.js +1 -0
  14. package/dist/assets/NodeEditor-B1TvB2eW.css +1 -0
  15. package/dist/assets/NodeEditor-CZGnkwvf.js +147 -0
  16. package/dist/assets/TrashWorkspace-CERX58Ek.js +1 -0
  17. package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
  18. package/dist/assets/_plugin-vue_export-helper-CTn6xl4w.js +5 -0
  19. package/dist/assets/history-XvWNE5zO.js +1 -0
  20. package/dist/assets/index-BpOAL4tE.js +14 -0
  21. package/dist/assets/index-C-EkPNvq.css +1 -0
  22. package/dist/assets/layers-D0DLWjff.js +1 -0
  23. package/dist/assets/link-Bs_SUS0O.js +1 -0
  24. package/dist/assets/rotate-ccw-CvD7yNQj.js +1 -0
  25. package/dist/assets/save-C8e-QRkP.js +1 -0
  26. package/dist/assets/smartphone-Wy6xQi_i.js +1 -0
  27. package/dist/{dist-kIJlDMvm.js → dist-BGv2bzzF.js} +633 -539
  28. package/dist/index.html +3 -2
  29. package/dist/index.js +633 -539
  30. package/dist/schema-compiler.js +1 -1
  31. package/package.json +1 -1
  32. package/schema.sql +3 -0
  33. package/templates/astro-starter/src/components/blocks/NodeList.astro +59 -63
  34. package/templates/astro-starter/src/middleware.ts +63 -35
  35. package/templates/astro-starter/src/pages/[...slug].astro +5 -12
  36. package/templates/astro-starter/src/pages/api/load-nodes.ts +42 -14
  37. package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
  38. package/templates/kama-worker/setup-local-db.mjs +17 -0
  39. package/dist/assets/index-B1ogCRCg.js +0 -164
  40. package/dist/assets/index-CGLx9oqF.css +0 -1
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() {
@@ -156,7 +283,7 @@ function showHelp() {
156
283
  console.log(' npm run setup Initialize database schema (Run Once)');
157
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 });
@@ -410,10 +503,10 @@ async function createFullWorkspace() {
410
503
  "seed": "npm run seed:local",
411
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",
412
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",
413
- "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql > /dev/null 2>&1`,
506
+ "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`,
414
507
  "setup:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`,
415
508
  "dev": "wrangler dev --port 8787 --inspector-port 9239",
416
- "deploy": "npm run setup:remote && wrangler deploy"
509
+ "deploy": "npm run setup:remote && wrangler deploy --secrets-file .secrets"
417
510
  },
418
511
  dependencies: { '@brightsideoy/kama': currentKamaVersion, '@libsql/client': '^0.14.0' },
419
512
  devDependencies: { "wrangler": WRANGLER_LATEST },
@@ -433,7 +526,7 @@ async function createFullWorkspace() {
433
526
  vars: {
434
527
  CDN_DOMAIN: cmsDomain || 'localhost:8787',
435
528
  PUBLIC_FRONTEND_URL: (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321',
436
- CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID",
529
+ CLOUDFLARE_ZONE_ID: cloudflareZoneId,
437
530
  KAMA_RUNTIME: 'cloudflare'
438
531
  },
439
532
  d1_databases: [{ binding: 'DB', database_name: dbName, database_id: d1DatabaseId }],
@@ -444,7 +537,10 @@ async function createFullWorkspace() {
444
537
  writeFileSync(join(cmsDir, 'wrangler.json'), JSON.stringify(cmsWranglerConfig, null, 2));
445
538
 
446
539
  const publicFrontendUrl = (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321';
447
- 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);
448
544
  } else {
449
545
  const templateServerPath = join(__dirname, '../templates/kama-worker/server.js');
450
546
  if (existsSync(templateServerPath)) cpSync(templateServerPath, join(cmsDir, 'server.js'));
@@ -457,8 +553,8 @@ async function createFullWorkspace() {
457
553
  version: '1.0.0',
458
554
  scripts: {
459
555
  "seed": "npm run seed:local",
460
- "seed:local": "node ../node_modules/@brightsideoy/kama/dist/build-seed.js && cat seed-*.sql | sqlite3 kama.db",
461
- "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",
462
558
  "dev": "tsx server.js",
463
559
  "start": "node server.js"
464
560
  },
@@ -468,6 +564,7 @@ async function createFullWorkspace() {
468
564
  );
469
565
 
470
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'));
471
568
  }
472
569
 
473
570
  s.message('Bundling schemas...');
@@ -478,20 +575,23 @@ async function createFullWorkspace() {
478
575
  await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsFullUrl, workerName, isCloudflare, frontendApiSecret);
479
576
  }
480
577
 
481
- 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.');
482
584
 
483
585
  let nextSteps = '';
484
586
  let stepCount = 1;
485
587
  if (!isCurrentDir) nextSteps += `${stepCount++}. cd ${projName}\n`;
486
- nextSteps += `${stepCount++}. npm install\n`;
487
- nextSteps += `${stepCount++}. npm run setup ${pc.dim('<- Run this once to init the database schema!')}\n`;
488
588
  nextSteps += `${stepCount++}. npm run dev\n`;
489
589
 
490
590
  if (isCloudflare) {
491
591
  nextSteps += `\n${pc.dim('Cloudflare Note:')}\n`;
492
- nextSteps += `${pc.dim('Update CLOUDFLARE_ZONE_ID in wrangler.json')}\n`;
493
- nextSteps += `${pc.dim('Set Cache purge token: npx wrangler secret put CLOUDFLARE_API_TOKEN')}\n`;
494
- 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.')}`;
495
595
  }
496
596
 
497
597
  note(nextSteps, 'Next steps');
@@ -544,22 +644,29 @@ async function createNewSiteOnly() {
544
644
 
545
645
  if (isCloudflare) {
546
646
  siteDomain = handleCancel(await text({ message: 'Website Domain (e.g., brand.com):', defaultValue: '' }));
647
+ siteDomain = normalizeCloudflareDomain(siteDomain, 'Frontend Website Domain');
547
648
  if (detectedCmsDomain) {
548
649
  cmsFullUrl = detectedCmsDomain.startsWith('http') ? detectedCmsDomain : `https://${detectedCmsDomain.replace(/^https?:\/\//, '')}`;
549
650
  } else {
550
651
  const cmsDomainInput = handleCancel(await text({ message: 'CMS API Domain (e.g., admin.brand.com):', defaultValue: '' }));
551
- cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
652
+ cmsFullUrl = `https://${normalizeCloudflareDomain(cmsDomainInput, 'CMS API Domain')}`;
552
653
  }
654
+ ensureCloudflareAuth(rootDir);
553
655
  }
554
656
 
657
+ ignoreSecretFiles(rootDir);
658
+
555
659
  const s = spinner();
556
660
  s.start(`Generating Astro site...`);
557
661
 
558
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' });
559
666
 
560
667
  s.stop(`Site created in sites/${siteFolder}!`);
561
668
 
562
- 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');
563
670
  outro('Site added successfully! 🚀');
564
671
  }
565
672
 
@@ -575,10 +682,11 @@ async function bundleWorkspaceSchemas(targetRootDir, silent = false) {
575
682
  try {
576
683
  let compilerPath = join(__dirname, '../dist/schema-compiler.js');
577
684
  if (!existsSync(compilerPath)) compilerPath = join(__dirname, '../scripts/schema-compiler.ts');
578
- await import(pathToFileURL(compilerPath).href);
685
+ execSync(`${shellQuote(process.execPath)} ${shellQuote(compilerPath)}`, { cwd: rootDir, stdio: 'inherit' });
579
686
  if (!silent) s.stop('Schemas bundled successfully.');
580
687
  } catch (e) {
581
688
  if (!silent) s.stop(`Schema compilation failed: ${e.message}`, true);
689
+ else throw e;
582
690
  }
583
691
  }
584
692
 
@@ -599,21 +707,16 @@ async function promoteLocalToCloudflare() {
599
707
 
600
708
  const s = spinner();
601
709
  s.start('Checking Cloudflare authentication...');
710
+ ensureCloudflareAuth(rootDir);
711
+ s.stop('Cloudflare authentication verified.');
602
712
 
603
- const whoami = runWrangler('whoami');
604
- if (whoami && (whoami.includes('Getting User Settings') || whoami.includes('You are logged in'))) {
605
- s.stop('Logged in to Cloudflare.');
606
- } else {
607
- s.stop('Not logged in to Cloudflare.', true);
608
- console.log(pc.dim('Launching browser for Cloudflare login...'));
609
- const login = runWrangler('login', { stdio: 'inherit' });
610
- if (login === null) {
611
- cancel('Cloudflare login failed.');
612
- process.exit(1);
613
- }
614
- }
615
-
616
- const cmsDomain = handleCancel(await text({ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):', defaultValue: '' }));
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);
617
720
 
618
721
  const sitesFolderDir = join(rootDir, 'sites');
619
722
  let siteDomain = '';
@@ -622,48 +725,24 @@ async function promoteLocalToCloudflare() {
622
725
  const sites = readdirSync(sitesFolderDir);
623
726
  if (sites.length > 0) {
624
727
  siteDir = join(sitesFolderDir, sites[0]);
625
- 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
+ );
626
732
  }
627
733
  }
628
734
 
735
+ ignoreSecretFiles(rootDir);
736
+
629
737
  s.start('Upgrading workspace to Cloudflare Edge...');
630
738
 
631
739
  const projName = basename(rootDir);
632
- let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
633
-
634
- // 🎯 1. ARCHITECTURE FIX: Check if the database already exists
635
- let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
636
- const listOutput = runWrangler('d1 list --json');
637
- let dbExists = false;
638
-
639
- if (listOutput) {
640
- try {
641
- const jsonStart = listOutput.indexOf('[');
642
- const jsonEnd = listOutput.lastIndexOf(']') + 1;
643
- if (jsonStart !== -1 && jsonEnd !== -1) {
644
- const databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
645
- const existingDb = databases.find(db => db.name === dbName);
646
- if (existingDb) {
647
- d1DatabaseId = existingDb.uuid;
648
- dbExists = true;
649
- }
650
- }
651
- } catch (e) { }
652
- }
653
-
654
- // 2. If it doesn't exist, create it safely
655
- if (!dbExists) {
656
- const d1Output = runWrangler(`d1 create ${dbName}`);
657
- if (d1Output) {
658
- 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}/);
659
- if (match) d1DatabaseId = match[0];
660
- }
661
- }
662
-
663
740
  const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
664
741
  const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
665
-
666
- 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);
667
746
 
668
747
  const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
669
748
  rootPkg.devDependencies = rootPkg.devDependencies || {};
@@ -679,10 +758,21 @@ async function promoteLocalToCloudflare() {
679
758
  compatibility_flags: ['nodejs_compat'],
680
759
  assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
681
760
  images: { binding: 'IMAGES' },
682
- vars: { CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID", KAMA_RUNTIME: 'cloudflare' }
761
+ vars: { CLOUDFLARE_ZONE_ID: '', KAMA_RUNTIME: 'cloudflare' }
683
762
  };
684
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';
685
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 || '';
686
776
  currentConfig.vars.CDN_DOMAIN = cmsDomain;
687
777
  if (siteDomain) currentConfig.vars.PUBLIC_FRONTEND_URL = `https://${siteDomain}`;
688
778
  currentConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
@@ -692,15 +782,19 @@ async function promoteLocalToCloudflare() {
692
782
 
693
783
  writeFileSync(workerWranglerPath, JSON.stringify(currentConfig, null, 2));
694
784
 
695
- if (!existsSync(join(workerDir, '.dev.vars'))) {
696
- 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`);
697
- }
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);
698
792
 
699
793
  const workerPkgPath = join(workerDir, 'package.json');
700
794
  if (existsSync(workerPkgPath)) {
701
795
  const workerPkg = JSON.parse(readFileSync(workerPkgPath, 'utf-8'));
702
796
  workerPkg.scripts.dev = "wrangler dev --port 8787 --inspector-port 9239";
703
- workerPkg.scripts.deploy = "npm run setup:remote && wrangler deploy";
797
+ workerPkg.scripts.deploy = "npm run setup:remote && wrangler deploy --secrets-file .secrets";
704
798
  workerPkg.scripts["setup:local"] = `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`;
705
799
  workerPkg.scripts["setup:remote"] = `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`;
706
800
 
@@ -718,8 +812,8 @@ async function promoteLocalToCloudflare() {
718
812
 
719
813
  if (siteDir && siteDomain) {
720
814
  const prefix = basename(siteDir).replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
721
- const sessionId = getOrCreateKV(`SESSION_${prefix}`);
722
- const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`);
815
+ const sessionId = getOrCreateKV(`SESSION_${prefix}`, rootDir);
816
+ const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`, rootDir);
723
817
 
724
818
  const siteWranglerPath = join(siteDir, 'wrangler.json');
725
819
  const siteConfig = {
@@ -747,14 +841,35 @@ async function promoteLocalToCloudflare() {
747
841
  sitePkg.dependencies['@astrojs/cloudflare'] = ASTRO_CF_LATEST;
748
842
  sitePkg.devDependencies = sitePkg.devDependencies || {};
749
843
  sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
844
+ sitePkg.scripts = sitePkg.scripts || {};
845
+ sitePkg.scripts.deploy = 'astro build && wrangler deploy --secrets-file .secrets';
750
846
  writeFileSync(sitePkgPath, JSON.stringify(sitePkg, null, 2));
751
847
  }
752
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 });
753
855
  convertAstroConfigToCloudflare(siteDir);
754
856
  }
755
857
 
756
- s.stop('Workspace successfully upgraded to Cloudflare Edge!');
757
- 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.');
758
873
  }
759
874
 
760
875
  /**
@@ -792,6 +907,7 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
792
907
  if (sitePkg.dependencies['wrangler']) delete sitePkg.dependencies['wrangler'];
793
908
  sitePkg.devDependencies = sitePkg.devDependencies || {};
794
909
  sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
910
+ sitePkg.scripts.deploy = 'astro build && wrangler deploy --secrets-file .secrets';
795
911
  convertAstroConfigToCloudflare(siteDir);
796
912
  }
797
913
 
@@ -825,15 +941,23 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
825
941
 
826
942
  if (siteDomain !== 'localhost:4321') siteConfig.routes = [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }];
827
943
  writeFileSync(join(siteDir, 'wrangler.json'), JSON.stringify(siteConfig, null, 2));
944
+ writeWorkerSecrets(siteDir, frontendApiSecret);
828
945
  }
829
946
 
830
- 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`;
831
949
 
832
- if (frontendApiSecret) {
950
+ if (frontendApiSecret && !isCloudflare) {
833
951
  envContent += `\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`;
834
952
  }
835
953
 
836
954
  writeFileSync(join(siteDir, '.env'), envContent);
955
+ if (isCloudflare && frontendApiSecret) {
956
+ writeFileSync(join(siteDir, '.dev.vars'), `FRONTEND_API_SECRET="${frontendApiSecret}"\n`, { mode: 0o600 });
957
+ }
837
958
  }
838
959
 
839
- main().catch(console.error);
960
+ main().catch(error => {
961
+ console.error(error);
962
+ process.exitCode = 1;
963
+ });