@brightsideoy/kama 1.0.0-alpha.2 → 1.0.0-alpha.21

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 (101) hide show
  1. package/bin/cli.js +604 -364
  2. package/content/definitions/block-kama-forms.yaml +5 -0
  3. package/content/definitions/blocks/button.yaml +15 -5
  4. package/content/definitions/blocks/image-gallery.yaml +34 -0
  5. package/content/definitions/blocks/node-list.yaml +75 -0
  6. package/content/definitions/blocks/youtube.yaml +8 -0
  7. package/content/definitions/field-sets/access.yaml +10 -0
  8. package/content/definitions/field-sets/channel-base.yaml +7 -0
  9. package/content/definitions/field-sets/page-base.yaml +16 -1
  10. package/content/definitions/nodes/article.yaml +17 -1
  11. package/content/definitions/taxonomies/categories.yaml +5 -2
  12. package/content/definitions/taxonomies/media-tag.yaml +15 -0
  13. package/content/definitions/taxonomies/tags.yaml +5 -2
  14. package/content/forms/contact.yaml +44 -0
  15. package/content/forms/subscription.yaml +32 -0
  16. package/definitions/editor/default.ts +15 -1
  17. package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
  18. package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
  19. package/dist/assets/CmsBadge-BNsgxyyB.css +1 -0
  20. package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
  21. package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
  22. package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
  23. package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
  24. package/dist/assets/CmsDataRow-CYNxfVaZ.css +1 -0
  25. package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
  26. package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
  27. package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
  28. package/dist/assets/CmsHelp-BQkr9KyU.js +1 -0
  29. package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
  30. package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
  31. package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
  32. package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
  33. package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
  34. package/dist/assets/CmsPublisher-XSCPrp5L.js +3 -0
  35. package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
  36. package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
  37. package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
  38. package/dist/assets/EditorSplitPreview-Cp2x0O_P.js +1 -0
  39. package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
  40. package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
  41. package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
  42. package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
  43. package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
  44. package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
  45. package/dist/assets/TrashWorkspace-CATe46WF.js +1 -0
  46. package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
  47. package/dist/assets/_plugin-vue_export-helper-CQxWhVtr.js +5 -0
  48. package/dist/assets/circle-check-A-QeFqt3.js +1 -0
  49. package/dist/assets/database-hg5SqKLG.js +1 -0
  50. package/dist/assets/folder-tree-BWYot72H.js +1 -0
  51. package/dist/assets/globe-BraoljaN.js +1 -0
  52. package/dist/assets/history-pqIbKK9u.js +1 -0
  53. package/dist/assets/index-CY0VYSyt.css +1 -0
  54. package/dist/assets/index-DdG9Pozx.js +14 -0
  55. package/dist/assets/layers-B6RgfpPR.js +1 -0
  56. package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
  57. package/dist/assets/monitor-CrpvInqG.js +1 -0
  58. package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
  59. package/dist/assets/plus-CX_GHzXs.js +1 -0
  60. package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
  61. package/dist/assets/save-Ctv8vW9H.js +1 -0
  62. package/dist/assets/schema-helpers-DvficKko.js +1 -0
  63. package/dist/assets/send-F-mJeL54.js +1 -0
  64. package/dist/assets/smartphone-EPce6VdP.js +1 -0
  65. package/dist/assets/trash-2-CAUF1KtI.js +1 -0
  66. package/dist/assets/useLocalStorage-C805uplI.js +1 -0
  67. package/dist/assets/useNodes-CqFlc72m.js +1 -0
  68. package/dist/assets/user-IFlT6mYq.js +1 -0
  69. package/dist/build-seed.js +495 -0
  70. package/dist/{dist-Dy7-HUEG.js → dist-3Xc2TTZw.js} +3189 -1394
  71. package/dist/index.html +34 -2
  72. package/dist/index.js +3116 -1315
  73. package/dist/schema-compiler.js +5 -5
  74. package/documentation/EXTENSIONS.md +204 -0
  75. package/extension-registry.js +5 -0
  76. package/package.json +13 -6
  77. package/schema.sql +44 -1
  78. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +9 -0
  79. package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
  80. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +6 -12
  81. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +5 -6
  82. package/templates/astro-starter/src/components/blocks/NodeList.astro +274 -0
  83. package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
  84. package/templates/astro-starter/src/components/global/Footer.astro +7 -46
  85. package/templates/astro-starter/src/components/global/Header.astro +10 -16
  86. package/templates/astro-starter/src/components/global/NavigationMenu.astro +68 -49
  87. package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
  88. package/templates/astro-starter/src/layouts/StandardLayout.astro +4 -3
  89. package/templates/astro-starter/src/lib/cms.ts +33 -11
  90. package/templates/astro-starter/src/lib/media/kama-image-service.ts +15 -4
  91. package/templates/astro-starter/src/lib/media/media-proxy.ts +22 -3
  92. package/templates/astro-starter/src/lib/media/media-url.ts +17 -3
  93. package/templates/astro-starter/src/middleware.ts +158 -100
  94. package/templates/astro-starter/src/pages/[...slug].astro +77 -3
  95. package/templates/astro-starter/src/pages/api/load-nodes.ts +68 -0
  96. package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
  97. package/templates/kama-worker/index.js +38 -15
  98. package/templates/kama-worker/server.js +13 -5
  99. package/templates/kama-worker/setup-local-db.mjs +17 -0
  100. package/dist/assets/index-F2rfUymo.css +0 -1
  101. package/dist/assets/index-lXxH3VFK.js +0 -164
package/bin/cli.js CHANGED
@@ -2,16 +2,35 @@
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';
6
- import * as readline from 'node:readline/promises';
5
+ import { fileURLToPath } from 'node:url';
6
+ import crypto from 'node:crypto';
7
+
8
+ import { intro, outro, text, password, select, confirm, spinner, note, isCancel, cancel } from '@clack/prompts';
9
+
10
+ const pc = {
11
+ green: (t) => `\x1b[32m${t}\x1b[0m`,
12
+ cyan: (t) => `\x1b[36m${t}\x1b[0m`,
13
+ dim: (t) => `\x1b[2m${t}\x1b[0m`,
14
+ yellow: (t) => `\x1b[33m${t}\x1b[0m`,
15
+ red: (t) => `\x1b[31m${t}\x1b[0m`,
16
+ bgCyan: (t) => `\x1b[46m\x1b[30m ${t} \x1b[0m`,
17
+ bold: (t) => `\x1b[1m${t}\x1b[0m`,
18
+ reset: '\x1b[0m'
19
+ };
20
+
21
+ function handleCancel(value) {
22
+ if (isCancel(value)) {
23
+ cancel('Operation cancelled.');
24
+ process.exit(0);
25
+ }
26
+ return value;
27
+ }
7
28
 
8
- // 🎯 Always use the absolute newest ecosystem
9
29
  const ASTRO_LATEST = "latest";
10
30
  const ASTRO_NODE_LATEST = "latest";
11
31
  const ASTRO_CF_LATEST = "latest";
12
32
  const WRANGLER_LATEST = "latest";
13
33
 
14
- // Dynamically resolve current package version
15
34
  const __filename = fileURLToPath(import.meta.url);
16
35
  const __dirname = dirname(__filename);
17
36
  let currentKamaVersion = 'alpha';
@@ -25,31 +44,284 @@ try {
25
44
  console.warn("⚠️ Could not detect Kama version, defaulting to 'alpha'");
26
45
  }
27
46
 
28
- const rl = readline.createInterface({
29
- input: process.stdin,
30
- output: process.stdout,
31
- });
47
+ function getOrGenerateWorkspaceSecret(workerDir) {
48
+ const envPath = join(workerDir, '.env');
49
+ const devVarsPath = join(workerDir, '.dev.vars');
50
+
51
+ let secret = null;
52
+
53
+ if (existsSync(envPath)) {
54
+ const match = readFileSync(envPath, 'utf-8').match(/^FRONTEND_API_SECRET=["']?([^"'\n]+)/m);
55
+ if (match) secret = match[1];
56
+ }
57
+
58
+ if (!secret && existsSync(devVarsPath)) {
59
+ const match = readFileSync(devVarsPath, 'utf-8').match(/^FRONTEND_API_SECRET=["']?([^"'\n]+)/m);
60
+ if (match) secret = match[1];
61
+ }
62
+
63
+ if (!secret) {
64
+ secret = crypto.randomBytes(32).toString('hex');
65
+ const secretLine = `\n# KAMA Auto-Generated Server-to-Server Security\nFRONTEND_API_SECRET="${secret}"\n`;
66
+
67
+ if (existsSync(devVarsPath)) {
68
+ appendFileSync(devVarsPath, secretLine);
69
+ } else if (existsSync(envPath)) {
70
+ appendFileSync(envPath, secretLine);
71
+ } else {
72
+ writeFileSync(devVarsPath, secretLine);
73
+ }
74
+ }
75
+
76
+ return secret;
77
+ }
78
+
79
+ function getWranglerCmd() {
80
+ try {
81
+ execSync('wrangler --version', { stdio: 'ignore' });
82
+ return 'wrangler';
83
+ } catch (e) {
84
+ return 'npx --yes --package=wrangler wrangler';
85
+ }
86
+ }
87
+
88
+ function runWrangler(cmd, options = {}) {
89
+ const baseWrangler = getWranglerCmd();
90
+ try {
91
+ const result = execSync(`${baseWrangler} ${cmd}`, {
92
+ encoding: 'utf-8',
93
+ stdio: options.stdio || 'pipe',
94
+ input: options.input,
95
+ cwd: options.cwd || process.cwd(),
96
+ env: {
97
+ ...process.env,
98
+ PATH: process.env.PATH
99
+ }
100
+ });
101
+ return typeof result === 'string' ? result : '';
102
+ } catch (e) {
103
+ if (options.throw) throw new Error(`Wrangler command failed: ${cmd}\n${e.stderr || e.message}`);
104
+ return null;
105
+ }
106
+ }
107
+
108
+ function shellQuote(value) {
109
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
110
+ }
111
+
112
+ function getOrCreateKV(title, cwd = process.cwd()) {
113
+ const listOut = runWrangler('kv namespace list', { cwd });
114
+ if (listOut) {
115
+ try {
116
+ const namespaces = JSON.parse(listOut);
117
+ const found = namespaces.find(n => n.title === title);
118
+ if (found) return found.id;
119
+ } catch (err) { }
120
+ }
121
+
122
+ const createOut = runWrangler(`kv namespace create ${shellQuote(title)}`, { cwd, throw: true });
123
+ const match = createOut.match(/"id":\s*"([a-fA-F0-9]{32})"/);
124
+ if (match) return match[1];
125
+
126
+ throw new Error(`Unable to create or find Cloudflare KV namespace "${title}". Check Wrangler authentication and permissions.`);
127
+ }
128
+
129
+ function getOrCreateD1(name, cwd = process.cwd()) {
130
+ const listOutput = runWrangler('d1 list --json', { cwd, throw: true });
131
+ let databases;
132
+ try {
133
+ databases = JSON.parse(listOutput);
134
+ } catch (error) {
135
+ throw new Error('Wrangler returned invalid JSON while listing D1 databases.');
136
+ }
137
+
138
+ const existing = databases.find(database => database.name === name);
139
+ if (existing?.uuid) return existing.uuid;
140
+
141
+ const createOutput = runWrangler(`d1 create ${shellQuote(name)}`, { cwd, throw: true });
142
+ 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}/);
143
+ if (created) return created[0];
144
+
145
+ throw new Error(`Cloudflare D1 database "${name}" was not created or returned a valid ID.`);
146
+ }
147
+
148
+ function ensureR2Bucket(name, cwd = process.cwd()) {
149
+ if (runWrangler(`r2 bucket info ${shellQuote(name)} --json`, { cwd })) return;
150
+
151
+ const createOutput = runWrangler(`r2 bucket create ${shellQuote(name)}`, { cwd });
152
+ if (createOutput || runWrangler(`r2 bucket info ${shellQuote(name)} --json`, { cwd })) return;
153
+
154
+ throw new Error(`Unable to create or find Cloudflare R2 bucket "${name}". Check Wrangler authentication and permissions.`);
155
+ }
156
+
157
+ function ensureCloudflareAuth(cwd) {
158
+ if (runWrangler('whoami', { cwd })) return;
159
+
160
+ console.log(pc.dim('Launching browser for Cloudflare login...'));
161
+ runWrangler('login', { cwd, stdio: 'inherit', throw: true });
162
+ if (!runWrangler('whoami', { cwd })) {
163
+ throw new Error('Cloudflare authentication could not be verified after login.');
164
+ }
165
+ }
166
+
167
+ function writeWorkerSecrets(workerDir, secret, cloudflareApiToken = '') {
168
+ const values = [`FRONTEND_API_SECRET=${secret}`];
169
+ if (cloudflareApiToken) values.push(`CLOUDFLARE_API_TOKEN=${cloudflareApiToken}`);
170
+ writeFileSync(join(workerDir, '.secrets'), `${values.join('\n')}\n`, { mode: 0o600 });
171
+ }
172
+
173
+ function readEnvValue(filePath, key) {
174
+ if (!existsSync(filePath)) return '';
175
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
176
+ const match = readFileSync(filePath, 'utf8').match(new RegExp(`^${escapedKey}=["']?([^"'\\r\\n]*)`, 'm'));
177
+ return match?.[1]?.trim() || '';
178
+ }
179
+
180
+ async function resolveCloudflareZoneId(domain, token) {
181
+ if (!token) return '';
182
+
183
+ const labels = domain.split('.');
184
+ for (let index = 0; index < labels.length - 1; index++) {
185
+ const zoneName = labels.slice(index).join('.');
186
+ const response = await fetch(`https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent(zoneName)}&per_page=50`, {
187
+ headers: { Authorization: `Bearer ${token}` }
188
+ });
189
+ const data = await response.json();
190
+ if (!response.ok || !data.success) {
191
+ throw new Error(`Could not look up Cloudflare zone for ${domain}. Check the token's Zone Read permission.`);
192
+ }
193
+ const zone = data.result?.find(item => item.name === zoneName && item.status === 'active');
194
+ if (zone?.id) return zone.id;
195
+ }
196
+
197
+ throw new Error(`No active Cloudflare zone was found for ${domain}. Add the domain to Cloudflare or enter its Zone ID manually.`);
198
+ }
199
+
200
+ async function promptForCloudflareApiToken(existingToken = '') {
201
+ const token = handleCancel(await password({
202
+ message: `Optional Cloudflare API token (Zone Read + Zone Cache Purge${existingToken ? '; leave blank to keep existing token' : ''}):`,
203
+ mask: '*'
204
+ }));
205
+ return token.trim() || existingToken;
206
+ }
207
+
208
+ function normalizeCloudflareDomain(value, label) {
209
+ const input = String(value || '').trim();
210
+ if (!input) throw new Error(`${label} is required for Cloudflare deployment.`);
211
+
212
+ try {
213
+ const url = new URL(input.includes('://') ? input : `https://${input}`);
214
+ if (!url.hostname.includes('.') || url.hostname === 'localhost') throw new Error();
215
+ return url.host.toLowerCase();
216
+ } catch {
217
+ throw new Error(`${label} must be a valid Cloudflare domain, for example example.com.`);
218
+ }
219
+ }
220
+
221
+ function ensureGitIgnoreEntries(rootDir, entries) {
222
+ const gitignorePath = join(rootDir, '.gitignore');
223
+ const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
224
+ const lines = new Set(existing.split(/\r?\n/));
225
+ const missing = entries.filter(line => !lines.has(line));
226
+ if (missing.length > 0) appendFileSync(gitignorePath, `${existing && !existing.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`);
227
+ }
228
+
229
+ function ignoreSecretFiles(rootDir) {
230
+ ensureGitIgnoreEntries(rootDir, ['kama-worker/.secrets', 'kama-worker/.dev.vars', 'sites/*/.secrets', 'sites/*/.dev.vars']);
231
+ }
232
+
233
+ function updateEnvFile(filePath, values, removeKeys = []) {
234
+ const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
235
+ const keys = new Set(Object.keys(values));
236
+ const found = new Set();
237
+ const lines = existing.split(/\r?\n/).filter(line => {
238
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/);
239
+ return !match || !removeKeys.includes(match[1]);
240
+ }).map(line => {
241
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/);
242
+ if (!match || !keys.has(match[1])) return line;
243
+ found.add(match[1]);
244
+ return `${match[1]}=${JSON.stringify(values[match[1]])}`;
245
+ });
246
+
247
+ for (const key of keys) {
248
+ if (!found.has(key)) lines.push(`${key}=${JSON.stringify(values[key])}`);
249
+ }
250
+
251
+ writeFileSync(filePath, `${lines.filter((line, index) => line || index < lines.length - 1).join('\n')}\n`);
252
+ }
253
+
254
+ function generateExtensionRegistry(rootDir) {
255
+ const workerDir = join(rootDir, 'kama-worker');
256
+ const extDir = join(rootDir, 'extensions');
257
+ const registryPath = join(workerDir, 'extension-registry.js');
258
+
259
+ let imports = '';
260
+ let arrayElements = [];
261
+
262
+ if (existsSync(extDir)) {
263
+ const folders = readdirSync(extDir, { withFileTypes: true })
264
+ .filter(dirent => dirent.isDirectory())
265
+ .map(dirent => dirent.name);
266
+
267
+ folders.forEach((folder, i) => {
268
+ const tsPath = join(extDir, folder, 'index.ts');
269
+ const jsPath = join(extDir, folder, 'index.js');
270
+ let extFile = '';
271
+
272
+ if (existsSync(tsPath)) extFile = 'index.ts';
273
+ else if (existsSync(jsPath)) extFile = 'index.js';
274
+
275
+ if (extFile) {
276
+ imports += `import ext_${i} from '../extensions/${folder}/${extFile}';\n`;
277
+ arrayElements.push(`ext_${i}`);
278
+ }
279
+
280
+ // Auto-inject extension schemas into the workspace definitions
281
+ const defDir = join(extDir, folder, 'definitions');
282
+ if (existsSync(defDir)) {
283
+ cpSync(defDir, join(rootDir, 'content', 'definitions'), { recursive: true });
284
+ }
285
+
286
+ // 🎯 Enterprise Static Asset Sync for Extensions
287
+ // Syncs extension UI dist/ui static assets into the ASSETS directory served by Cloudflare Wrangler
288
+ const extUiDir = join(extDir, folder, 'ui');
289
+ const extDistDir = join(extDir, folder, 'dist');
290
+ const targetAssetDir = join(rootDir, 'node_modules', '@brightsideoy', 'kama', 'dist', 'ext', folder);
291
+
292
+ if (existsSync(extDistDir)) {
293
+ mkdirSync(targetAssetDir, { recursive: true });
294
+ cpSync(extDistDir, targetAssetDir, { recursive: true });
295
+ } else if (existsSync(extUiDir)) {
296
+ mkdirSync(targetAssetDir, { recursive: true });
297
+ cpSync(extUiDir, targetAssetDir, { recursive: true });
298
+ }
299
+ });
300
+ }
301
+
302
+ const fileContent = `// 🤖 AUTO-GENERATED - DO NOT EDIT\n${imports}\nexport const activeExtensions = [\n${arrayElements.join(',\n ')}\n];\n`;
303
+
304
+ if (existsSync(workerDir)) {
305
+ writeFileSync(registryPath, fileContent, 'utf-8');
306
+ }
307
+ }
32
308
 
33
309
  function showHelp() {
34
- console.log('\n🔥 --- KAMA ENGINE CLI --- 🔥\n');
310
+ console.log(`\n${pc.bgCyan(' KAMA • ENGINE CLI ')}\n`);
35
311
  console.log('Usage: npx kama <command> [options]\n');
36
- console.log('Commands:');
312
+ console.log(pc.cyan('Commands:'));
37
313
  console.log(' init Kickstart a new Monorepo (CMS Worker + Optional Astro Site)');
38
314
  console.log(' add site Add a new Astro Frontend Site to the current workspace (sites/)');
39
315
  console.log(' bundle Compile and bundle YAML schemas into bundled-definitions.js for Edge deployment');
40
316
  console.log(' promote Promote the current Local workspace to Cloudflare Edge');
41
317
  console.log(' help, -h Show this help message\n');
42
-
43
- console.log('Workspace Scripts (Run from project root):');
44
- console.log(' npm update Update to latest KAMA version');
45
- console.log(' npm run dev Boot up the CMS backend and Astro frontend concurrently');
46
- console.log(' npm run deploy:cms Deploy the backend worker and database to Cloudflare');
47
- console.log(' npm run deploy:site Deploy the Astro frontend site to Cloudflare\n');
48
-
49
- console.log('Examples:');
50
- console.log(' npx kama init');
51
- console.log(' npx kama add site');
52
- console.log(' npx kama bundle\n');
318
+ console.log(pc.cyan('Workspace Scripts (Run from project root):'));
319
+ console.log(' npm update Update to latest KAMA version');
320
+ console.log(' npm run setup Initialize database schema (Run Once)');
321
+ console.log(' npm run seed -w kama-worker Seed the database with mock test data');
322
+ console.log(' npm run dev Boot up the CMS backend and Astro frontend concurrently');
323
+ console.log(' npm run deploy:kama Deploy the backend worker and database to Cloudflare');
324
+ console.log(' npm run deploy:site Deploy the Astro frontend site to Cloudflare\n');
53
325
  }
54
326
 
55
327
  async function main() {
@@ -57,13 +329,13 @@ async function main() {
57
329
  const command = args[0]?.toLowerCase();
58
330
  const subCommand = args[1]?.toLowerCase();
59
331
 
60
- if (command === 'help' || command === '--help' || command === '-h') {
332
+ if (['help', '--help', '-h'].includes(command)) {
61
333
  showHelp();
62
- rl.close();
63
334
  process.exit(0);
64
335
  }
65
336
 
66
- console.log('\n🔥 --- KAMA ENGINE CLI --- 🔥\n');
337
+ console.clear();
338
+ intro(`${pc.bgCyan(' KAMA • ENGINE ')} ${pc.dim('v' + currentKamaVersion.replace('^', ''))}`);
67
339
 
68
340
  if (command === 'init') {
69
341
  await createFullWorkspace();
@@ -74,34 +346,36 @@ async function main() {
74
346
  } else if (command === 'bundle') {
75
347
  await bundleWorkspaceSchemas();
76
348
  } else {
77
- console.log('Available Commands:');
78
- console.log(' 1) npx kama init - Kickstart new Monorepo (CMS + Optional Site)');
79
- console.log(' 2) npx kama add site - Add new Astro Site to current workspace (sites/)');
80
- console.log(' 3) npx kama bundle - Compile and bundle YAML schemas for Edge deployment');
81
- console.log(' 4) npx kama promote - Promote current Local workspace to Cloudflare Edge');
82
- console.log(' 5) Help - Show all commands and usage');
83
-
84
- const choice = (await rl.question('\nSelect choice [1/2/3/4/5]: ')).trim();
85
-
86
- if (choice === '5') {
349
+ const choice = handleCancel(await select({
350
+ message: 'What would you like to do?',
351
+ options: [
352
+ { value: 'init', label: 'Init', hint: 'Kickstart new Workspace (CMS + Optional Site)' },
353
+ { value: 'add_site', label: 'Add Site', hint: 'Add Astro Site to current workspace (sites/)' },
354
+ { value: 'bundle', label: 'Bundle', hint: 'Compile YAML schemas for Edge deployment' },
355
+ { value: 'promote', label: 'Promote', hint: 'Promote current Local workspace to Cloudflare Edge' },
356
+ { value: 'help', label: 'Help', hint: 'Show all commands and usage' },
357
+ { value: 'exit', label: 'Exit' }
358
+ ]
359
+ }));
360
+
361
+ if (choice === 'help') {
87
362
  showHelp();
88
- } else if (choice === '4') {
363
+ process.exit(0);
364
+ } else if (choice === 'promote') {
89
365
  await promoteLocalToCloudflare();
90
- } else if (choice === '3') {
366
+ } else if (choice === 'bundle') {
91
367
  await bundleWorkspaceSchemas();
92
- } else if (choice === '2') {
368
+ } else if (choice === 'add_site') {
93
369
  await createNewSiteOnly();
94
- } else {
370
+ } else if (choice === 'init') {
95
371
  await createFullWorkspace();
372
+ } else {
373
+ outro('Goodbye!');
374
+ process.exit(0);
96
375
  }
97
376
  }
98
-
99
- rl.close();
100
377
  }
101
378
 
102
- /**
103
- * Helper to update astro.config.mjs for Cloudflare
104
- */
105
379
  function convertAstroConfigToCloudflare(siteDir) {
106
380
  const configPath = join(siteDir, 'astro.config.mjs');
107
381
  if (existsSync(configPath)) {
@@ -113,195 +387,145 @@ function convertAstroConfigToCloudflare(siteDir) {
113
387
  }
114
388
  }
115
389
 
116
- /**
117
- * COMMAND: npx kama init
118
- * Kickstart full Monorepo Workspace
119
- */
120
390
  async function createFullWorkspace() {
121
391
  const cwdName = basename(process.cwd());
122
392
  const hasExistingPkg = existsSync(join(process.cwd(), 'package.json'));
123
-
124
393
  const defaultDirPrompt = hasExistingPkg ? '.' : cwdName;
125
- const inputName = await rl.question(`Project folder name (use '.' for current dir) [${defaultDirPrompt}]: `);
394
+
395
+ const inputName = handleCancel(await text({
396
+ message: 'Project folder name (use "." for current dir):',
397
+ placeholder: defaultDirPrompt,
398
+ defaultValue: defaultDirPrompt
399
+ }));
126
400
 
127
401
  const chosenFolder = inputName.trim() || defaultDirPrompt;
128
402
  const isCurrentDir = chosenFolder === '.';
129
-
130
403
  const projName = isCurrentDir ? cwdName : chosenFolder;
131
404
  const rootDir = isCurrentDir ? process.cwd() : join(process.cwd(), projName);
132
405
 
133
- console.log('\n🌍 Choose your Deployment Architecture:');
134
- console.log(' 1) VPS / Local Server (Node.js, SQLite, Sharp cropping)');
135
- console.log(' 2) Cloudflare Edge (Workers, D1, R2, CF Image Resizing)');
136
- const envChoice = await rl.question('\nChoice [1/2]: ');
137
- const isCloudflare = envChoice.trim() === '2';
406
+ const isCloudflare = handleCancel(await select({
407
+ message: 'Choose your Deployment Architecture:',
408
+ options: [
409
+ { value: true, label: 'Cloudflare Edge', hint: 'Workers, D1, R2, CF Image Resizing' },
410
+ { value: false, label: 'VPS / Local Server', hint: 'Node.js, SQLite, Sharp cropping' }
411
+ ]
412
+ }));
138
413
 
139
- const createSiteInput = await rl.question('\n🚀 Do you want to scaffold an initial Astro frontend site? (y/n) [y]: ');
140
- const shouldCreateSite = createSiteInput.trim().toLowerCase() !== 'n';
414
+ const shouldCreateSite = handleCancel(await confirm({
415
+ message: 'Do you want to scaffold an initial Astro frontend site?',
416
+ initialValue: true
417
+ }));
141
418
 
142
419
  let siteFolderName = 'kama-starter';
143
420
  let siteDomain = 'localhost:4321';
144
421
  let cmsDomain = 'localhost:8787';
145
422
 
146
423
  if (shouldCreateSite) {
147
- siteFolderName = (await rl.question('Astro package/folder name (e.g., my-website) [kama-starter]: ')).trim() || 'kama-starter';
424
+ siteFolderName = handleCancel(await text({
425
+ message: 'Astro package/folder name (e.g., my-website.eu):',
426
+ placeholder: 'kama-starter',
427
+ defaultValue: 'kama-starter'
428
+ }));
148
429
  }
149
430
 
150
431
  if (isCloudflare) {
151
- cmsDomain = (await rl.question('\nCMS Admin Domain (e.g., admin.yourdomain.com): ')).trim();
432
+ cmsDomain = handleCancel(await text({
433
+ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):',
434
+ defaultValue: ''
435
+ }));
152
436
  if (shouldCreateSite) {
153
- siteDomain = (await rl.question('First Website Domain (e.g., yourdomain.com): ')).trim();
437
+ siteDomain = handleCancel(await text({
438
+ message: 'First Website Domain (e.g., yourdomain.com):',
439
+ defaultValue: ''
440
+ }));
154
441
  }
442
+
443
+ cmsDomain = normalizeCloudflareDomain(cmsDomain, 'CMS Admin Domain');
444
+ if (shouldCreateSite) siteDomain = normalizeCloudflareDomain(siteDomain, 'Frontend Website Domain');
155
445
  }
156
446
 
447
+ const cloudflareApiToken = isCloudflare ? await promptForCloudflareApiToken() : '';
448
+ const cloudflareZoneId = isCloudflare ? await resolveCloudflareZoneId(cmsDomain, cloudflareApiToken) : '';
449
+
157
450
  const cmsProtocol = isCloudflare ? 'https://' : 'http://';
158
451
  const cmsFullUrl = cmsDomain ? `${cmsProtocol}${cmsDomain.replace(/^https?:\/\//, '')}` : 'http://localhost:8787';
159
452
 
453
+ const frontendApiSecret = crypto.randomBytes(32).toString('hex');
454
+
160
455
  const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
161
456
  let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
162
457
  const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
163
458
 
164
- if (!isCurrentDir) {
165
- console.log(`\n📁 Creating workspace folder at ./${projName}...`);
166
- mkdirSync(rootDir, { recursive: true });
167
- } else {
168
- console.log(`\n📁 Initializing workspace in current directory (${rootDir})...`);
169
- }
459
+ const s = spinner();
460
+ s.start('Provisioning workspace...');
170
461
 
171
- writeFileSync(
172
- join(rootDir, '.gitignore'),
173
- `node_modules/\n.env\n.DS_Store\nkama-worker/.dev.vars\nkama-worker/.env\nkama-worker/.wrangler/\nkama-worker/public/\nkama-worker/*.db\nsites/*/.env\nsites/*/.astro/\nsites/*/dist/\nkama-worker/bundled-definitions.js\n`
174
- );
462
+ if (!isCurrentDir) mkdirSync(rootDir, { recursive: true });
463
+
464
+ ensureGitIgnoreEntries(rootDir, [
465
+ 'node_modules/', '.env', '.DS_Store', 'kama-worker/.dev.vars', 'kama-worker/.secrets',
466
+ 'kama-worker/.env', 'kama-worker/.wrangler/', 'kama-worker/public/', 'kama-worker/*.db',
467
+ 'sites/*/.env', 'sites/*/.secrets', 'sites/*/.astro/', 'sites/*/dist/',
468
+ 'kama-worker/bundled-definitions.js', 'kama-worker/extension-registry.js'
469
+ ]);
175
470
 
176
- console.log(`\n📄 Copying default content definitions...`);
177
471
  const sourceDefsDir = join(__dirname, '../content/definitions');
178
472
  const targetDefsDir = join(rootDir, 'content', 'definitions');
179
- if (existsSync(sourceDefsDir)) {
180
- cpSync(sourceDefsDir, targetDefsDir, { recursive: true });
181
- } else {
182
- console.warn(`⚠️ Could not find default content definitions at ${sourceDefsDir}`);
183
- }
473
+ if (existsSync(sourceDefsDir)) cpSync(sourceDefsDir, targetDefsDir, { recursive: true });
184
474
 
185
475
  let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
186
- let isAuthenticated = false;
187
-
476
+ let backendCacheId = '';
188
477
  if (isCloudflare) {
189
- console.log(`\n🔑 Checking Cloudflare authentication...`);
190
- try {
191
- // Pass stdio: 'ignore' so child process doesn't drain parent stdin stream!
192
- execSync('npx wrangler whoami', { stdio: 'ignore' });
193
- isAuthenticated = true;
194
- console.log(`✅ Logged in to Cloudflare.`);
195
- } catch (error) {
196
- console.log(`⚠️ You are not logged in to Cloudflare.`);
197
- console.log(`🌐 Launching browser for Cloudflare login...`);
198
- try {
199
- execSync('npx wrangler login', { stdio: 'ignore' });
200
- isAuthenticated = true;
201
- console.log(`✅ Successfully logged in!`);
202
- } catch (loginError) {
203
- console.log(`❌ Login failed or was cancelled.`);
204
- }
205
- }
206
-
207
- if (isAuthenticated) {
208
- try {
209
- console.log(`\n🗄️ Checking existing Cloudflare D1 databases...`);
210
- const listOutput = execSync(`npx wrangler d1 list --json`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
211
- const jsonStart = listOutput.indexOf('[');
212
- const jsonEnd = listOutput.lastIndexOf(']') + 1;
213
- let databases = [];
214
-
215
- if (jsonStart !== -1 && jsonEnd !== -1) {
216
- databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
217
- }
218
-
219
- const existingDb = databases.find(db => db.name === dbName);
220
-
221
- if (existingDb) {
222
- console.log(`⚠️ Database '${dbName}' already exists on your account.`);
223
- const useExisting = await rl.question(`Do you want to link this existing database? (y/n) [y]: `);
224
-
225
- if (useExisting.toLowerCase() !== 'n') {
226
- d1DatabaseId = existingDb.uuid;
227
- console.log(`✅ Linked existing database ID: ${d1DatabaseId}`);
228
- } else {
229
- dbName = (await rl.question(`Enter a new database name: `)).trim();
230
- console.log(`\n🗄️ Creating new database '${dbName}'...`);
231
- const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
232
- const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
233
- if (match) {
234
- d1DatabaseId = match[0];
235
- console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
236
- }
237
- }
238
- } else {
239
- console.log(`\n🗄️ Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
240
- const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
241
- const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
242
- if (match) {
243
- d1DatabaseId = match[0];
244
- console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
245
- }
246
- }
247
- } catch (error) {
248
- console.log(`⚠️ Database operation failed. We'll leave a placeholder.`);
249
- }
250
- } else {
251
- console.log(`⚠️ Skipping database creation. We'll leave a placeholder in wrangler.json.`);
252
- }
253
- } else {
254
- console.log(`\n💻 Local VPS mode selected. Skipping Cloudflare authentication and domain setup...`);
478
+ s.message('Authenticating with Cloudflare...');
479
+ ensureCloudflareAuth(process.cwd());
480
+ s.message('Provisioning D1, KV, and R2 resources...');
481
+ backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`, rootDir);
482
+ d1DatabaseId = getOrCreateD1(dbName, rootDir);
483
+ ensureR2Bucket(bucketName, rootDir);
255
484
  }
256
485
 
257
- const rootScripts = {
258
- 'dev:cms': 'npm run dev -w kama-worker',
259
- 'deploy:cms': 'npm run deploy -w kama-worker',
260
- };
486
+ s.message('Writing configuration files...');
261
487
 
488
+ const rootPkgPath = join(rootDir, 'package.json');
489
+ const existingRootPkg = existsSync(rootPkgPath) ? JSON.parse(readFileSync(rootPkgPath, 'utf-8')) : {};
490
+ const existingWorkspaces = Array.isArray(existingRootPkg.workspaces) ? existingRootPkg.workspaces : [];
262
491
  const rootPkg = {
263
- name: projName,
492
+ ...existingRootPkg,
493
+ name: existingRootPkg.name || projName,
264
494
  private: true,
265
- type: 'module',
266
- workspaces: ['kama-worker', 'sites/*'],
267
- scripts: rootScripts,
268
- dependencies: {
269
- '@brightsideoy/kama': currentKamaVersion,
270
- },
271
- devDependencies: {}
495
+ type: existingRootPkg.type || 'module',
496
+ workspaces: [...new Set([...existingWorkspaces, 'kama-worker', 'sites/*'])],
497
+ scripts: { ...existingRootPkg.scripts, 'setup': 'npx kama bundle && npm run setup:local -w kama-worker' },
498
+ dependencies: { ...existingRootPkg.dependencies, '@brightsideoy/kama': currentKamaVersion },
499
+ devDependencies: { ...existingRootPkg.devDependencies }
272
500
  };
273
501
 
274
- if (isCloudflare) {
275
- rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
276
- }
502
+ if (isCloudflare) rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
277
503
 
278
504
  if (shouldCreateSite) {
279
505
  rootPkg.devDependencies['concurrently'] = '^9.0.0';
280
506
  rootPkg.scripts['dev:site'] = `npm run dev -w sites/${siteFolderName}`;
281
507
  rootPkg.scripts['predev'] = 'npx kama bundle';
282
- rootPkg.scripts['dev'] = 'npx concurrently "npm run dev:cms" "npm run dev:site"';
508
+ rootPkg.scripts['dev'] = 'npx concurrently "npm run dev -w kama-worker" "npm run dev -w sites/' + siteFolderName + '"';
283
509
  rootPkg.scripts['deploy:site'] = `npm run deploy -w sites/${siteFolderName}`;
510
+ rootPkg.scripts['deploy:kama'] = `npm run deploy -w kama-worker`;
284
511
  } else {
285
512
  rootPkg.scripts['predev'] = 'npx kama bundle';
286
- rootPkg.scripts['dev'] = 'npm run dev:cms';
513
+ rootPkg.scripts['dev'] = 'npm run dev -w kama-worker';
514
+ rootPkg.scripts['deploy:kama'] = `npm run deploy -w kama-worker`;
287
515
  }
288
516
 
289
- writeFileSync(join(rootDir, 'package.json'), JSON.stringify(rootPkg, null, 2));
517
+ writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2));
290
518
 
291
- console.log(`\n☁️ Configuring CMS Worker '${workerName}' (${cmsDomain || 'localhost:8787'})...`);
292
519
  const cmsDir = join(rootDir, 'kama-worker');
293
520
  mkdirSync(cmsDir, { recursive: true });
294
521
 
295
- writeFileSync(join(cmsDir, 'seed.sql'), '-- Kama Auto-Generated Seed File\nSELECT 1;\n');
296
-
297
522
  const emptyDefs = `{ nodes: {}, trees: {}, blocks: {}, nodeLayouts: {}, blockLayouts: {}, channels: {} }`;
298
523
  writeFileSync(join(cmsDir, 'bundled-definitions.js'), `const defs = ${emptyDefs};\nexport const BUNDLED_DEFINITIONS = defs;\nexport default defs;\n`);
524
+ writeFileSync(join(cmsDir, 'extension-registry.js'), `export const activeExtensions = [];\n`);
299
525
 
300
526
  if (isCloudflare) {
301
527
  const templateIndexPath = join(__dirname, '../templates/kama-worker/index.js');
302
- if (existsSync(templateIndexPath)) {
303
- cpSync(templateIndexPath, join(cmsDir, 'index.js'));
304
- }
528
+ if (existsSync(templateIndexPath)) cpSync(templateIndexPath, join(cmsDir, 'index.js'));
305
529
 
306
530
  writeFileSync(
307
531
  join(cmsDir, 'package.json'),
@@ -310,20 +534,16 @@ async function createFullWorkspace() {
310
534
  type: 'module',
311
535
  version: '1.0.0',
312
536
  scripts: {
313
- "schema:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql > /dev/null 2>&1`,
314
- "schema:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`,
315
- "seed:local": `wrangler d1 execute ${dbName} --local --file=seed.sql > /dev/null 2>&1`,
316
- "seed:remote": `wrangler d1 execute ${dbName} --remote --file=seed.sql`,
317
- "dev": "npm run schema:local && npm run seed:local && wrangler dev --port 8787 --inspector-port 9239",
318
- "deploy": "npm run schema:remote && npm run seed:remote && wrangler deploy"
319
- },
320
- dependencies: {
321
- '@brightsideoy/kama': currentKamaVersion,
322
- '@libsql/client': '^0.14.0'
323
- },
324
- devDependencies: {
325
- "wrangler": WRANGLER_LATEST
537
+ "seed": "npm run seed:local",
538
+ "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",
539
+ "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",
540
+ "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`,
541
+ "setup:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`,
542
+ "dev": "wrangler dev --port 8787 --inspector-port 9239",
543
+ "deploy": "npm run setup:remote && wrangler deploy --secrets-file .secrets"
326
544
  },
545
+ dependencies: { '@brightsideoy/kama': currentKamaVersion, 'hono': '^4.13.0', '@libsql/client': '^0.14.0' },
546
+ devDependencies: { "wrangler": WRANGLER_LATEST },
327
547
  }, null, 2)
328
548
  );
329
549
 
@@ -334,13 +554,13 @@ async function createFullWorkspace() {
334
554
  compatibility_date: '2024-09-23',
335
555
  compatibility_flags: ['nodejs_compat'],
336
556
  dev: { host: 'localhost', port: 8787 },
337
- cache: { enabled: true },
338
557
  assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
339
558
  images: { binding: 'IMAGES' },
559
+ kv_namespaces: [{ binding: 'CACHE_KV', id: backendCacheId }],
340
560
  vars: {
341
561
  CDN_DOMAIN: cmsDomain || 'localhost:8787',
342
562
  PUBLIC_FRONTEND_URL: (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321',
343
- CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID",
563
+ CLOUDFLARE_ZONE_ID: cloudflareZoneId,
344
564
  KAMA_RUNTIME: 'cloudflare'
345
565
  },
346
566
  d1_databases: [{ binding: 'DB', database_name: dbName, database_id: d1DatabaseId }],
@@ -350,18 +570,14 @@ async function createFullWorkspace() {
350
570
 
351
571
  writeFileSync(join(cmsDir, 'wrangler.json'), JSON.stringify(cmsWranglerConfig, null, 2));
352
572
 
353
- writeFileSync(join(cmsDir, '.dev.vars'), `# Cloudflare Edge Cache Purging Secrets
354
- CLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN
355
-
356
- # Local Development Overrides
357
- CDN_DOMAIN=
358
- PUBLIC_FRONTEND_URL=http://localhost:4321
359
- `);
573
+ const publicFrontendUrl = (shouldCreateSite) ? (siteDomain && siteDomain !== 'localhost:4321' ? `https://${siteDomain}` : 'http://localhost:4321') : 'http://localhost:4321';
574
+ const devVars = [`# Local Development Overrides`, `CDN_DOMAIN=`, `PUBLIC_FRONTEND_URL=${publicFrontendUrl}`, ``, `# Server-to-Server API Security`, `FRONTEND_API_SECRET="${frontendApiSecret}"`];
575
+ if (cloudflareApiToken) devVars.push(`CLOUDFLARE_API_TOKEN="${cloudflareApiToken}"`);
576
+ writeFileSync(join(cmsDir, '.dev.vars'), `${devVars.join('\n')}\n`, { mode: 0o600 });
577
+ writeWorkerSecrets(cmsDir, frontendApiSecret, cloudflareApiToken);
360
578
  } else {
361
579
  const templateServerPath = join(__dirname, '../templates/kama-worker/server.js');
362
- if (existsSync(templateServerPath)) {
363
- cpSync(templateServerPath, join(cmsDir, 'server.js'));
364
- }
580
+ if (existsSync(templateServerPath)) cpSync(templateServerPath, join(cmsDir, 'server.js'));
365
581
 
366
582
  writeFileSync(
367
583
  join(cmsDir, 'package.json'),
@@ -370,67 +586,65 @@ PUBLIC_FRONTEND_URL=http://localhost:4321
370
586
  type: 'module',
371
587
  version: '1.0.0',
372
588
  scripts: {
373
- "schema:local": "sqlite3 kama.db < ../node_modules/@brightsideoy/kama/schema.sql",
374
- "seed:local": "sqlite3 kama.db < seed.sql",
375
- "dev": "npm run schema:local && npm run seed:local && tsx server.js",
589
+ "seed": "npm run seed:local",
590
+ "seed:local": "node ../node_modules/@brightsideoy/kama/dist/build-seed.js && node setup-local-db.mjs seed",
591
+ "setup:local": "node setup-local-db.mjs schema",
592
+ "dev": "tsx server.js",
376
593
  "start": "node server.js"
377
594
  },
378
- dependencies: {
379
- '@brightsideoy/kama': currentKamaVersion,
380
- '@hono/node-server': '^1.13.0',
381
- '@libsql/client': '^0.14.0',
382
- 'sharp': '^0.33.5',
383
- 'dotenv': '^16.4.5'
384
- },
385
- devDependencies: {
386
- "tsx": "^4.19.0"
387
- },
595
+ dependencies: { '@brightsideoy/kama': currentKamaVersion, 'hono': '^4.13.0', '@hono/node-server': '^1.13.0', '@libsql/client': '^0.14.0', 'sharp': '^0.33.5', 'dotenv': '^16.4.5' },
596
+ devDependencies: { "tsx": "^4.19.0" },
388
597
  }, null, 2)
389
598
  );
390
599
 
391
- writeFileSync(join(cmsDir, '.env'), `PORT=8787\nKAMA_RUNTIME=node\nDB_PATH=./kama.db\nLOCAL_MEDIA_PATH=./public/media\nPUBLIC_CMS_URL=http://127.0.0.1:8787\nPUBLIC_API_URL=http://127.0.0.1:8787\nPUBLIC_FRONTEND_URL=http://127.0.0.1:4321\n`);
600
+ 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`);
601
+ cpSync(join(__dirname, '../templates/kama-worker/setup-local-db.mjs'), join(cmsDir, 'setup-local-db.mjs'));
392
602
  }
393
603
 
394
- await bundleWorkspaceSchemas(rootDir);
604
+ s.message('Bundling schemas...');
605
+ await bundleWorkspaceSchemas(rootDir, true);
395
606
 
396
607
  if (shouldCreateSite) {
397
- await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsFullUrl, workerName, isCloudflare);
398
- } else {
399
- console.log(`\nℹ️ Skipping Astro site creation. You can add a site later by running 'npx kama add site' inside this workspace.`);
608
+ s.message(`Scaffolding Astro site (${siteFolderName})...`);
609
+ await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsFullUrl, workerName, isCloudflare, frontendApiSecret);
400
610
  }
401
611
 
402
- console.log(`\n✨ Workspace ready! Next steps:`);
403
- if (!isCurrentDir) console.log(` 1. cd ${projName}`);
404
- console.log(` 2. npm install`);
612
+ s.message('Installing workspace dependencies...');
613
+ execSync('npm install', { cwd: rootDir, stdio: 'inherit' });
614
+ s.message('Bundling schemas and initializing the local database...');
615
+ execSync('npm run setup', { cwd: rootDir, stdio: 'inherit' });
616
+
617
+ s.stop('Workspace installed and initialized successfully.');
618
+
619
+ let nextSteps = '';
620
+ let stepCount = 1;
621
+ if (!isCurrentDir) nextSteps += `${stepCount++}. cd ${projName}\n`;
622
+ nextSteps += `${stepCount++}. npm run dev\n`;
405
623
 
406
624
  if (isCloudflare) {
407
- if (d1DatabaseId === '00000000-0000-0000-0000-000000000000') {
408
- console.log(` 3. Run 'npx wrangler login' and create a D1 database manually.`);
409
- }
410
- console.log(` 4. Update CLOUDFLARE_ZONE_ID in kama-worker/wrangler.json`);
411
- console.log(` 5. Securely store your purge token: cd kama-worker && npx wrangler secret put CLOUDFLARE_API_TOKEN`);
412
- console.log(` 6. Run 'npm run dev' from the workspace root to start everything.`);
413
- } else {
414
- console.log(` 3. Run 'npm run dev' from the workspace root to start the local backend and frontend.`);
415
- console.log(`\n💡 Note: You can deploy to Cloudflare later by running 'npx kama promote'.`);
625
+ nextSteps += `\n${pc.dim('Cloudflare Note:')}\n`;
626
+ nextSteps += cloudflareApiToken
627
+ ? `${pc.dim('Zone ID was discovered automatically; the purge token is stored as an ignored Worker secret.')}`
628
+ : `${pc.dim('Zone-level cache purging is disabled. Run kama promote with a token to enable it.')}`;
416
629
  }
630
+
631
+ note(nextSteps, 'Next steps');
632
+ outro(`KAMA • ENGINE is ready! 🚀`);
417
633
  }
418
634
 
419
- /**
420
- * COMMAND: npx kama add site
421
- */
422
635
  async function createNewSiteOnly() {
423
636
  const rootDir = process.cwd();
424
637
 
425
638
  if (!existsSync(join(rootDir, 'package.json'))) {
426
- console.error('❌ Error: Must run this command inside the root of your workspace.');
427
- return;
639
+ cancel('Error: Must run this command inside the root of your workspace.');
640
+ process.exit(1);
428
641
  }
429
642
 
430
643
  let targetWorkerName = 'kama-worker';
431
644
  let detectedCmsDomain = '';
645
+ const workerDir = join(rootDir, 'kama-worker');
432
646
 
433
- const cmsWranglerPath = join(rootDir, 'kama-worker', 'wrangler.json');
647
+ const cmsWranglerPath = join(workerDir, 'wrangler.json');
434
648
  if (existsSync(cmsWranglerPath)) {
435
649
  try {
436
650
  const cmsWrangler = JSON.parse(readFileSync(cmsWranglerPath, 'utf-8'));
@@ -440,73 +654,97 @@ async function createNewSiteOnly() {
440
654
  } catch (e) { }
441
655
  }
442
656
 
443
- console.log('\n🌍 Deployment Mode:');
444
- console.log(' 1) VPS / Local Server (Node.js)');
445
- console.log(' 2) Cloudflare Edge');
446
- const envChoice = await rl.question('\nChoice [1/2]: ');
447
- const isCloudflare = envChoice.trim() === '2';
657
+ const frontendApiSecret = existsSync(workerDir) ? getOrGenerateWorkspaceSecret(workerDir) : crypto.randomBytes(32).toString('hex');
658
+
659
+ const isCloudflare = handleCancel(await select({
660
+ message: 'Deployment Architecture for this site:',
661
+ options: [
662
+ { value: true, label: 'Cloudflare Edge' },
663
+ { value: false, label: 'VPS / Local Server' }
664
+ ]
665
+ }));
666
+
667
+ const siteFolder = handleCancel(await text({
668
+ message: 'Astro package/folder name (e.g., my-new-site):',
669
+ placeholder: 'kama-starter',
670
+ defaultValue: 'kama-starter'
671
+ }));
448
672
 
449
- const siteFolder = (await rl.question('Astro package/folder name (e.g., my-new-site) [kama-starter]: ')).trim() || 'kama-starter';
450
673
  let siteDomain = 'localhost:4321';
451
674
  let cmsFullUrl = 'http://localhost:8787';
452
675
 
453
676
  if (isCloudflare) {
454
- siteDomain = (await rl.question('Website Domain (e.g., brand.com): ')).trim();
677
+ siteDomain = handleCancel(await text({ message: 'Website Domain (e.g., brand.com):', defaultValue: '' }));
678
+ siteDomain = normalizeCloudflareDomain(siteDomain, 'Frontend Website Domain');
455
679
  if (detectedCmsDomain) {
456
680
  cmsFullUrl = detectedCmsDomain.startsWith('http') ? detectedCmsDomain : `https://${detectedCmsDomain.replace(/^https?:\/\//, '')}`;
457
681
  } else {
458
- const cmsDomainInput = (await rl.question('CMS API Domain (e.g., admin.brand.com): ')).trim();
459
- cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
682
+ const cmsDomainInput = handleCancel(await text({ message: 'CMS API Domain (e.g., admin.brand.com):', defaultValue: '' }));
683
+ cmsFullUrl = `https://${normalizeCloudflareDomain(cmsDomainInput, 'CMS API Domain')}`;
460
684
  }
685
+ ensureCloudflareAuth(rootDir);
461
686
  }
462
687
 
463
- await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare);
688
+ ignoreSecretFiles(rootDir);
689
+
690
+ const s = spinner();
691
+ s.start(`Generating Astro site...`);
692
+
693
+ await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare, frontendApiSecret);
694
+ ignoreSecretFiles(rootDir);
695
+ s.message('Installing workspace dependencies...');
696
+ execSync('npm install', { cwd: rootDir, stdio: 'inherit' });
464
697
 
465
- console.log(`\n✨ Site created in sites/${siteFolder}!`);
466
- console.log(`📦 Run 'npm install' at workspace root.`);
698
+ s.stop(`Site created in sites/${siteFolder}!`);
699
+
700
+ note('Run `npm run dev` from the workspace root to start the new site.', 'Next steps');
701
+ outro('Site added successfully! 🚀');
467
702
  }
468
703
 
469
- /**
470
- * COMMAND: npx kama bundle
471
- */
472
- async function bundleWorkspaceSchemas(targetRootDir) {
704
+ async function bundleWorkspaceSchemas(targetRootDir, silent = false) {
473
705
  let rootDir = targetRootDir || process.cwd();
474
706
  if (basename(rootDir) === 'kama-worker') rootDir = resolve(rootDir, '..');
475
707
 
708
+ const s = spinner();
709
+ if (!silent) s.start('Bundling schemas & extensions...');
476
710
  try {
711
+ generateExtensionRegistry(rootDir);
712
+
477
713
  let compilerPath = join(__dirname, '../dist/schema-compiler.js');
478
714
  if (!existsSync(compilerPath)) compilerPath = join(__dirname, '../scripts/schema-compiler.ts');
479
- await import(pathToFileURL(compilerPath).href);
715
+ execSync(`${shellQuote(process.execPath)} ${shellQuote(compilerPath)}`, { cwd: rootDir, stdio: 'inherit' });
716
+
717
+ if (!silent) s.stop('Schemas and Extensions bundled successfully.');
480
718
  } catch (e) {
481
- console.error(`❌ Schema compilation failed:`, e.message || e);
719
+ if (!silent) s.stop(`Compilation failed: ${e.message}`, true);
720
+ else throw e;
482
721
  }
483
722
  }
484
723
 
485
- /**
486
- * COMMAND: npx kama promote
487
- */
488
724
  async function promoteLocalToCloudflare() {
489
725
  const rootDir = process.cwd();
490
726
  const workerDir = join(rootDir, 'kama-worker');
491
727
  const rootPkgPath = join(rootDir, 'package.json');
492
728
 
493
729
  if (!existsSync(workerDir) || !existsSync(rootPkgPath)) {
494
- console.error('❌ Error: Could not find kama-worker folder or package.json. Run this command inside your workspace root.');
495
- return;
730
+ cancel('Error: Could not find kama-worker folder or package.json.');
731
+ process.exit(1);
496
732
  }
497
733
 
498
- console.log('\n🌐 --- UPGRADING WORKSPACE TO CLOUDFLARE EDGE --- 🌐\n');
734
+ const frontendApiSecret = getOrGenerateWorkspaceSecret(workerDir);
499
735
 
500
- try {
501
- execSync('npx wrangler whoami', { stdio: 'ignore' });
502
- console.log('✅ Logged in to Cloudflare.');
503
- } catch (e) {
504
- console.log('🌐 Launching browser for Cloudflare login...');
505
- try { execSync('npx wrangler login', { stdio: 'ignore' }); }
506
- catch (loginError) { console.log('❌ Cloudflare login failed.'); return; }
507
- }
736
+ const s = spinner();
737
+ s.start('Checking Cloudflare authentication...');
738
+ ensureCloudflareAuth(rootDir);
739
+ s.stop('Cloudflare authentication verified.');
508
740
 
509
- const cmsDomain = (await rl.question('\nCMS Admin Domain (e.g., admin.yourdomain.com): ')).trim();
741
+ const cmsDomain = normalizeCloudflareDomain(
742
+ handleCancel(await text({ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):', defaultValue: '' })),
743
+ 'CMS Admin Domain'
744
+ );
745
+ const existingCloudflareToken = readEnvValue(join(workerDir, '.dev.vars'), 'CLOUDFLARE_API_TOKEN') || readEnvValue(join(workerDir, '.secrets'), 'CLOUDFLARE_API_TOKEN');
746
+ const cloudflareApiToken = await promptForCloudflareApiToken(existingCloudflareToken);
747
+ const cloudflareZoneId = await resolveCloudflareZoneId(cmsDomain, cloudflareApiToken);
510
748
 
511
749
  const sitesFolderDir = join(rootDir, 'sites');
512
750
  let siteDomain = '';
@@ -515,87 +753,97 @@ async function promoteLocalToCloudflare() {
515
753
  const sites = readdirSync(sitesFolderDir);
516
754
  if (sites.length > 0) {
517
755
  siteDir = join(sitesFolderDir, sites[0]);
518
- siteDomain = (await rl.question('Frontend Website Domain (e.g., yourdomain.com): ')).trim();
756
+ siteDomain = normalizeCloudflareDomain(
757
+ handleCancel(await text({ message: `Frontend Website Domain for ${sites[0]}:`, defaultValue: '' })),
758
+ 'Frontend Website Domain'
759
+ );
519
760
  }
520
761
  }
521
762
 
522
- const projName = basename(rootDir);
523
- let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
524
- let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
763
+ ignoreSecretFiles(rootDir);
525
764
 
526
- try {
527
- const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
528
- const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
529
- if (match) d1DatabaseId = match[0];
530
- } catch (err) { }
765
+ s.start('Upgrading workspace to Cloudflare Edge...');
766
+
767
+ const projName = basename(rootDir);
768
+ const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
769
+ const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
770
+ const dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
771
+ const d1DatabaseId = getOrCreateD1(dbName, rootDir);
772
+ const backendCacheId = getOrCreateKV(`CACHE_KV_${workerName.toUpperCase().replace(/-/g, '_')}`, rootDir);
773
+ ensureR2Bucket(bucketName, rootDir);
531
774
 
532
775
  const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
533
776
  rootPkg.devDependencies = rootPkg.devDependencies || {};
534
777
  rootPkg.devDependencies['wrangler'] = WRANGLER_LATEST;
535
778
  writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2));
536
779
 
537
- const workerName = `${projName}-worker`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
538
- const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
539
780
  const workerWranglerPath = join(workerDir, 'wrangler.json');
781
+ let currentConfig = existsSync(workerWranglerPath) ? JSON.parse(readFileSync(workerWranglerPath, 'utf-8')) : {
782
+ $schema: '../node_modules/wrangler/config-schema.json',
783
+ name: workerName,
784
+ main: 'index.js',
785
+ compatibility_date: '2024-09-23',
786
+ compatibility_flags: ['nodejs_compat'],
787
+ assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
788
+ images: { binding: 'IMAGES' },
789
+ vars: { CLOUDFLARE_ZONE_ID: '', KAMA_RUNTIME: 'cloudflare' }
790
+ };
540
791
 
541
- let currentConfig = existsSync(workerWranglerPath)
542
- ? JSON.parse(readFileSync(workerWranglerPath, 'utf-8'))
543
- : {
544
- $schema: '../node_modules/wrangler/config-schema.json',
545
- name: workerName,
546
- main: 'index.js',
547
- compatibility_date: '2024-09-23',
548
- compatibility_flags: ['nodejs_compat'],
549
- assets: { directory: '../node_modules/@brightsideoy/kama/dist', binding: 'ASSETS' },
550
- images: { binding: 'IMAGES' },
551
- vars: { CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID", KAMA_RUNTIME: 'cloudflare' }
552
- };
792
+ const cloudflareEntryPath = join(workerDir, 'index.js');
793
+ if (!existsSync(cloudflareEntryPath)) {
794
+ cpSync(join(__dirname, '../templates/kama-worker/index.js'), cloudflareEntryPath);
795
+ }
553
796
 
797
+ currentConfig.main = 'index.js';
554
798
  currentConfig.dev = currentConfig.dev || { host: 'localhost', port: 8787 };
555
- currentConfig.cache = { enabled: true };
799
+ currentConfig.vars = currentConfig.vars || {};
800
+ if (String(currentConfig.vars.CLOUDFLARE_ZONE_ID || '').startsWith('REPLACE_WITH_')) {
801
+ currentConfig.vars.CLOUDFLARE_ZONE_ID = '';
802
+ }
803
+ currentConfig.vars.CLOUDFLARE_ZONE_ID = cloudflareZoneId || currentConfig.vars.CLOUDFLARE_ZONE_ID || '';
556
804
  currentConfig.vars.CDN_DOMAIN = cmsDomain;
557
805
  if (siteDomain) currentConfig.vars.PUBLIC_FRONTEND_URL = `https://${siteDomain}`;
558
806
  currentConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
559
807
  currentConfig.d1_databases = [{ binding: 'DB', database_name: dbName, database_id: d1DatabaseId }];
560
808
  currentConfig.r2_buckets = [{ binding: 'MEDIA_BUCKET', bucket_name: bucketName }];
809
+ currentConfig.kv_namespaces = [{ binding: 'CACHE_KV', id: backendCacheId }];
561
810
 
562
811
  writeFileSync(workerWranglerPath, JSON.stringify(currentConfig, null, 2));
563
812
 
564
- if (!existsSync(join(workerDir, '.dev.vars'))) {
565
- writeFileSync(join(workerDir, '.dev.vars'), `CLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\nCDN_DOMAIN=\nPUBLIC_FRONTEND_URL=http://localhost:4321\n`);
566
- }
567
-
568
- const workerIndexPath = join(workerDir, 'index.js');
569
- if (!existsSync(workerIndexPath)) {
570
- const templateIndexPath = join(__dirname, '../templates/kama-worker/index.js');
571
- if (existsSync(templateIndexPath)) {
572
- cpSync(templateIndexPath, workerIndexPath);
573
- }
574
- }
813
+ updateEnvFile(join(workerDir, '.dev.vars'), {
814
+ CDN_DOMAIN: '',
815
+ PUBLIC_FRONTEND_URL: 'http://localhost:4321',
816
+ FRONTEND_API_SECRET: frontendApiSecret,
817
+ ...(cloudflareApiToken ? { CLOUDFLARE_API_TOKEN: cloudflareApiToken } : {})
818
+ });
819
+ writeWorkerSecrets(workerDir, frontendApiSecret, cloudflareApiToken);
575
820
 
576
821
  const workerPkgPath = join(workerDir, 'package.json');
577
822
  if (existsSync(workerPkgPath)) {
578
823
  const workerPkg = JSON.parse(readFileSync(workerPkgPath, 'utf-8'));
579
- workerPkg.scripts.dev = "npm run schema:local && npm run seed:local && wrangler dev --port 8787 --inspector-port 9239";
580
- workerPkg.scripts.deploy = "npm run schema:remote && npm run seed:remote && wrangler deploy";
581
- workerPkg.scripts["schema:remote"] = `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`;
582
- workerPkg.scripts["seed:remote"] = `wrangler d1 execute ${dbName} --remote --file=seed.sql`;
824
+ workerPkg.scripts.dev = "wrangler dev --port 8787 --inspector-port 9239";
825
+ workerPkg.scripts.deploy = "npm run setup:remote && wrangler deploy --secrets-file .secrets";
826
+ workerPkg.scripts["setup:local"] = `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql`;
827
+ workerPkg.scripts["setup:remote"] = `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql`;
828
+
829
+ workerPkg.scripts["seed"] = "npm run seed:local";
830
+ 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`;
831
+ 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`;
832
+
833
+ workerPkg.dependencies = workerPkg.dependencies || {};
834
+ workerPkg.dependencies.hono = '^4.13.0';
835
+
583
836
  workerPkg.devDependencies = workerPkg.devDependencies || {};
584
837
  workerPkg.devDependencies.wrangler = WRANGLER_LATEST;
838
+ if (workerPkg.devDependencies.tsx) delete workerPkg.devDependencies.tsx;
839
+
585
840
  writeFileSync(workerPkgPath, JSON.stringify(workerPkg, null, 2));
586
841
  }
587
842
 
588
843
  if (siteDir && siteDomain) {
589
- let sessionId = "REPLACE_WITH_YOUR_SESSION_KV_ID";
590
- let htmlCacheId = "REPLACE_WITH_YOUR_HTML_CACHE_KV_ID";
591
844
  const prefix = basename(siteDir).replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
592
-
593
- try {
594
- const sessionOut = execSync(`npx wrangler kv namespace create "SESSION_${prefix}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
595
- if (sessionOut.match(/"id":\s*"([a-f0-9]{32})"/)) sessionId = sessionOut.match(/"id":\s*"([a-f0-9]{32})"/)[1];
596
- const cacheOut = execSync(`npx wrangler kv namespace create "HTML_CACHE_${prefix}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
597
- if (cacheOut.match(/"id":\s*"([a-f0-9]{32})"/)) htmlCacheId = cacheOut.match(/"id":\s*"([a-f0-9]{32})"/)[1];
598
- } catch (err) { }
845
+ const sessionId = getOrCreateKV(`SESSION_${prefix}`, rootDir);
846
+ const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`, rootDir);
599
847
 
600
848
  const siteWranglerPath = join(siteDir, 'wrangler.json');
601
849
  const siteConfig = {
@@ -608,7 +856,7 @@ async function promoteLocalToCloudflare() {
608
856
  assets: { directory: "./dist/client", binding: "ASSETS", run_worker_first: true },
609
857
  kv_namespaces: [{ binding: "SESSION", id: sessionId }, { binding: "HTML_CACHE", id: htmlCacheId }],
610
858
  services: [{ binding: "CMS_WORKER", service: currentConfig.name }],
611
- routes: [{ pattern: siteDomain, zone_name: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }],
859
+ routes: [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }],
612
860
  vars: { PUBLIC_CMS_URL: `https://${cmsDomain}`, PUBLIC_API_URL: `https://${cmsDomain}` }
613
861
  };
614
862
  writeFileSync(siteWranglerPath, JSON.stringify(siteConfig, null, 2));
@@ -618,50 +866,50 @@ async function promoteLocalToCloudflare() {
618
866
  const sitePkg = JSON.parse(readFileSync(sitePkgPath, 'utf-8'));
619
867
  sitePkg.dependencies = sitePkg.dependencies || {};
620
868
  sitePkg.dependencies['astro'] = ASTRO_LATEST;
621
-
622
869
  if (sitePkg.dependencies['@astrojs/node']) delete sitePkg.dependencies['@astrojs/node'];
623
870
  sitePkg.dependencies['@astrojs/cloudflare'] = ASTRO_CF_LATEST;
624
-
625
871
  sitePkg.devDependencies = sitePkg.devDependencies || {};
626
872
  sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
627
-
873
+ sitePkg.scripts = sitePkg.scripts || {};
874
+ sitePkg.scripts.deploy = 'astro build && wrangler deploy --secrets-file .secrets';
628
875
  writeFileSync(sitePkgPath, JSON.stringify(sitePkg, null, 2));
629
876
  }
630
877
 
878
+ writeWorkerSecrets(siteDir, frontendApiSecret);
879
+ updateEnvFile(join(siteDir, '.env'), {
880
+ PUBLIC_CMS_URL: `https://${cmsDomain}`,
881
+ PUBLIC_API_URL: `https://${cmsDomain}`
882
+ }, ['FRONTEND_API_SECRET']);
883
+ updateEnvFile(join(siteDir, '.dev.vars'), { FRONTEND_API_SECRET: frontendApiSecret });
631
884
  convertAstroConfigToCloudflare(siteDir);
885
+ }
632
886
 
633
- const tsConfigPath = join(siteDir, 'tsconfig.json');
634
- if (existsSync(tsConfigPath)) {
635
- const tsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf-8'));
636
- tsConfig.compilerOptions = tsConfig.compilerOptions || {};
637
- tsConfig.compilerOptions.types = ["@cloudflare/workers-types"];
638
- writeFileSync(tsConfigPath, JSON.stringify(tsConfig, null, 2));
639
- }
887
+ s.message('Installing Cloudflare dependencies...');
888
+ execSync('npm install', { cwd: rootDir, stdio: 'inherit' });
889
+ s.message('Bundling workspace schemas...');
890
+ await bundleWorkspaceSchemas(rootDir, true);
891
+ s.message('Deploying CMS and initializing the remote database...');
892
+ execSync('npm run deploy -w kama-worker', { cwd: rootDir, stdio: 'inherit' });
893
+
894
+ if (siteDir && siteDomain) {
895
+ const siteFolder = basename(siteDir);
896
+ s.message(`Building and deploying ${siteFolder}...`);
897
+ execSync(`npm run deploy -w sites/${shellQuote(siteFolder)}`, { cwd: rootDir, stdio: 'inherit' });
640
898
  }
641
899
 
642
- console.log(`\n✅ Workspace successfully upgraded to Cloudflare Edge!`);
900
+ s.stop('Workspace provisioned and deployed to Cloudflare.');
901
+ outro('Promotion complete.');
643
902
  }
644
903
 
645
- /**
646
- * Helper: Copies the Astro starter template and hydrates runtime tokens
647
- */
648
- async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, workerName = 'kama-worker', isCloudflare = false) {
649
- console.log(`\n🚀 Scaffolding Astro site (${siteFolder}) mapped to ${siteDomain} (Service: ${workerName})...`);
650
-
904
+ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, workerName = 'kama-worker', isCloudflare = false, frontendApiSecret = '') {
651
905
  const siteDir = join(rootDir, 'sites', siteFolder);
652
906
  const templateDir = join(__dirname, '../templates/astro-starter');
653
907
 
654
- if (!existsSync(templateDir)) {
655
- console.error(`❌ Error: Astro template not found at ${templateDir}`);
656
- return;
657
- }
908
+ if (!existsSync(templateDir)) return;
658
909
 
659
910
  cpSync(templateDir, siteDir, { recursive: true });
660
911
 
661
- const tsConfig = {
662
- extends: "astro/tsconfigs/strict",
663
- compilerOptions: { baseUrl: ".", paths: { "@/*": ["src/*"] } }
664
- };
912
+ const tsConfig = { extends: "astro/tsconfigs/strict", compilerOptions: { baseUrl: ".", paths: { "@/*": ["src/*"] } } };
665
913
  if (isCloudflare) tsConfig.compilerOptions.types = ["@cloudflare/workers-types"];
666
914
  writeFileSync(join(siteDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
667
915
 
@@ -670,7 +918,6 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
670
918
  const rawContent = readFileSync(templatePkgPath, 'utf-8')
671
919
  .replace(/\{\{SITE_NAME\}\}/g, siteFolder)
672
920
  .replace(/\{\{KAMA_VERSION\}\}/g, currentKamaVersion);
673
-
674
921
  const sitePkg = JSON.parse(rawContent);
675
922
 
676
923
  sitePkg.dependencies = sitePkg.dependencies || {};
@@ -680,14 +927,13 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
680
927
  if (sitePkg.dependencies['@astrojs/cloudflare']) delete sitePkg.dependencies['@astrojs/cloudflare'];
681
928
  if (sitePkg.dependencies['wrangler']) delete sitePkg.dependencies['wrangler'];
682
929
  if (sitePkg.devDependencies && sitePkg.devDependencies['wrangler']) delete sitePkg.devDependencies['wrangler'];
683
-
684
930
  sitePkg.dependencies['@astrojs/node'] = ASTRO_NODE_LATEST;
685
931
  } else {
686
932
  sitePkg.dependencies['@astrojs/cloudflare'] = ASTRO_CF_LATEST;
687
933
  if (sitePkg.dependencies['wrangler']) delete sitePkg.dependencies['wrangler'];
688
934
  sitePkg.devDependencies = sitePkg.devDependencies || {};
689
935
  sitePkg.devDependencies['wrangler'] = WRANGLER_LATEST;
690
-
936
+ sitePkg.scripts.deploy = 'astro build && wrangler deploy --secrets-file .secrets';
691
937
  convertAstroConfigToCloudflare(siteDir);
692
938
  }
693
939
 
@@ -696,22 +942,14 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
696
942
  }
697
943
 
698
944
  const siteLabel = siteFolder.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
699
- const safeSiteLabel = siteLabel.replace(/'/g, "''");
700
945
  const channelKey = siteFolder.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
701
946
  const treeKey = 'main-tree';
702
947
  const safeWorkerName = `${siteFolder.toLowerCase().replace(/[^a-z0-9-]/g, '-')}-site`;
703
948
 
704
949
  if (isCloudflare) {
705
- let sessionId = "REPLACE_WITH_YOUR_SESSION_KV_ID";
706
- let htmlCacheId = "REPLACE_WITH_YOUR_HTML_CACHE_KV_ID";
707
-
708
- try {
709
- const prefix = siteFolder.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
710
- const sessionOut = execSync(`npx wrangler kv namespace create "SESSION_${prefix}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
711
- if (sessionOut.match(/"id":\s*"([a-f0-9]{32})"/)) sessionId = sessionOut.match(/"id":\s*"([a-f0-9]{32})"/)[1];
712
- const cacheOut = execSync(`npx wrangler kv namespace create "HTML_CACHE_${prefix}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
713
- if (cacheOut.match(/"id":\s*"([a-f0-9]{32})"/)) htmlCacheId = cacheOut.match(/"id":\s*"([a-f0-9]{32})"/)[1];
714
- } catch (err) { }
950
+ const prefix = siteFolder.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
951
+ const sessionId = getOrCreateKV(`SESSION_${prefix}`);
952
+ const htmlCacheId = getOrCreateKV(`HTML_CACHE_${prefix}`);
715
953
 
716
954
  const siteConfig = {
717
955
  $schema: "node_modules/wrangler/config-schema.json",
@@ -723,27 +961,29 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
723
961
  assets: { directory: "./dist/client", binding: "ASSETS", run_worker_first: true },
724
962
  kv_namespaces: [{ binding: "SESSION", id: sessionId }, { binding: "HTML_CACHE", id: htmlCacheId }],
725
963
  services: [{ binding: "CMS_WORKER", service: workerName }],
726
- vars: { PUBLIC_CMS_URL: cmsFullUrl, PUBLIC_API_URL: cmsFullUrl, PUBLIC_CHANNEL_ID: channelKey, PUBLIC_TREE_ID: treeKey, PUBLIC_HOME_SLUG: "index", PUBLIC_SITE_NAME: siteLabel }
964
+ routes: [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }],
965
+ vars: { PUBLIC_CMS_URL: cmsFullUrl, PUBLIC_API_URL: cmsFullUrl, PUBLIC_CHANNEL_ID: channelKey, PUBLIC_TREE_ID: treeKey, PUBLIC_SITE_NAME: siteLabel }
727
966
  };
728
967
 
729
968
  if (siteDomain !== 'localhost:4321') siteConfig.routes = [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }];
730
969
  writeFileSync(join(siteDir, 'wrangler.json'), JSON.stringify(siteConfig, null, 2));
970
+ writeWorkerSecrets(siteDir, frontendApiSecret);
731
971
  }
732
972
 
733
- const seedSql = `
734
- -- Seed data for site: ${safeSiteLabel}
735
- INSERT OR IGNORE INTO channels (key, label, channelType, defaultLanguage, isHidden, dataJson) VALUES ('${channelKey}', '${safeSiteLabel}', 'website', '', 0, '{}');
736
- INSERT OR IGNORE INTO trees (key, label, treeType, channelId, isHidden, dataJson) VALUES ('${treeKey}', 'Main Tree', 'navigation', (SELECT id FROM channels WHERE key = '${channelKey}'), 0, '{}');
737
- INSERT OR IGNORE INTO channel_trees (channelId, treeId, key) VALUES ((SELECT id FROM channels WHERE key = '${channelKey}'), (SELECT id FROM trees WHERE key = '${treeKey}'), 'main');
738
- INSERT INTO content_nodes (contentType, title, slug, language, status, dataJson) SELECT 'page', 'Welcome to ${safeSiteLabel}', 'index', '', 'published', '{"titleSuffix":"– ${safeSiteLabel}","layoutId":"single-column","subpageLayoutId":"two-column","logoText":"${safeSiteLabel}"}' WHERE NOT EXISTS (SELECT 1 FROM content_nodes WHERE slug = 'index' AND title = 'Welcome to ${safeSiteLabel}');
739
- INSERT OR IGNORE INTO node_channels (nodeId, channelId) VALUES ((SELECT id FROM content_nodes WHERE slug = 'index' AND title = 'Welcome to ${safeSiteLabel}' ORDER BY id DESC LIMIT 1), (SELECT id FROM channels WHERE key = '${channelKey}'));
740
- INSERT OR IGNORE INTO tree_nodes (treeId, contentId, parentId, sortOrder) VALUES ((SELECT id FROM trees WHERE key = '${treeKey}'), (SELECT id FROM content_nodes WHERE slug = 'index' AND title = 'Welcome to ${safeSiteLabel}' ORDER BY id DESC LIMIT 1), NULL, 0);
741
- `;
973
+ const localCmsUrl = isCloudflare ? cmsFullUrl : 'http://127.0.0.1:8787';
974
+ 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`;
742
975
 
743
- const workerDir = join(rootDir, 'kama-worker');
744
- if (existsSync(workerDir)) appendFileSync(join(workerDir, 'seed.sql'), seedSql);
976
+ if (frontendApiSecret && !isCloudflare) {
977
+ envContent += `\n# Server-to-Server API Security\nFRONTEND_API_SECRET="${frontendApiSecret}"\n`;
978
+ }
745
979
 
746
- writeFileSync(join(siteDir, '.env'), `# Local Development Overrides\nPUBLIC_CMS_URL="http://127.0.0.1:8787"\nPUBLIC_API_URL="http://127.0.0.1:8787"\nPUBLIC_CHANNEL_ID="${channelKey}"\nPUBLIC_TREE_ID="${treeKey}"\nPUBLIC_HOME_SLUG="index"\nPUBLIC_SITE_NAME="${siteLabel.replace(/"/g, '\\"')}"\n`);
980
+ writeFileSync(join(siteDir, '.env'), envContent);
981
+ if (isCloudflare && frontendApiSecret) {
982
+ writeFileSync(join(siteDir, '.dev.vars'), `FRONTEND_API_SECRET="${frontendApiSecret}"\n`, { mode: 0o600 });
983
+ }
747
984
  }
748
985
 
749
- main().catch(console.error);
986
+ main().catch(error => {
987
+ console.error(error);
988
+ process.exitCode = 1;
989
+ });