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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -3,7 +3,30 @@ 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
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
- import * as readline from 'node:readline/promises';
6
+
7
+ // šŸŽÆ Modern CLI UI Components
8
+ import { intro, outro, text, select, confirm, spinner, note, isCancel, cancel } from '@clack/prompts';
9
+
10
+ // --- UI HELPERS (Zero Dependency Colors) ---
11
+ const pc = {
12
+ green: (t) => `\x1b[32m${t}\x1b[0m`,
13
+ cyan: (t) => `\x1b[36m${t}\x1b[0m`,
14
+ dim: (t) => `\x1b[2m${t}\x1b[0m`,
15
+ yellow: (t) => `\x1b[33m${t}\x1b[0m`,
16
+ red: (t) => `\x1b[31m${t}\x1b[0m`,
17
+ bgCyan: (t) => `\x1b[46m\x1b[30m ${t} \x1b[0m`,
18
+ bold: (t) => `\x1b[1m${t}\x1b[0m`,
19
+ reset: '\x1b[0m'
20
+ };
21
+
22
+ // Graceful exit handler for Ctrl+C
23
+ function handleCancel(value) {
24
+ if (isCancel(value)) {
25
+ cancel('Operation cancelled.');
26
+ process.exit(0);
27
+ }
28
+ return value;
29
+ }
7
30
 
8
31
  // šŸŽÆ Always use the absolute newest ecosystem
9
32
  const ASTRO_LATEST = "latest";
@@ -25,28 +48,25 @@ try {
25
48
  console.warn("āš ļø Could not detect Kama version, defaulting to 'alpha'");
26
49
  }
27
50
 
28
- const rl = readline.createInterface({
29
- input: process.stdin,
30
- output: process.stdout,
31
- });
32
-
33
51
  function showHelp() {
34
- console.log('\nšŸ”„ --- KAMA ENGINE CLI --- šŸ”„\n');
52
+ console.log(`\n${pc.bgCyan(' KAMA • ENGINE CLI ')}\n`);
35
53
  console.log('Usage: npx kama <command> [options]\n');
36
- console.log('Commands:');
54
+
55
+ console.log(pc.cyan('Commands:'));
37
56
  console.log(' init Kickstart a new Monorepo (CMS Worker + Optional Astro Site)');
38
57
  console.log(' add site Add a new Astro Frontend Site to the current workspace (sites/)');
39
58
  console.log(' bundle Compile and bundle YAML schemas into bundled-definitions.js for Edge deployment');
40
59
  console.log(' promote Promote the current Local workspace to Cloudflare Edge');
41
60
  console.log(' help, -h Show this help message\n');
42
61
 
43
- console.log('Workspace Scripts (Run from project root):');
62
+ console.log(pc.cyan('Workspace Scripts (Run from project root):'));
44
63
  console.log(' npm update Update to latest KAMA version');
64
+ console.log(' npm run setup Initialize database schema and seed data (Run Once)');
45
65
  console.log(' npm run dev Boot up the CMS backend and Astro frontend concurrently');
46
66
  console.log(' npm run deploy:cms Deploy the backend worker and database to Cloudflare');
47
67
  console.log(' npm run deploy:site Deploy the Astro frontend site to Cloudflare\n');
48
68
 
49
- console.log('Examples:');
69
+ console.log(pc.cyan('Examples:'));
50
70
  console.log(' npx kama init');
51
71
  console.log(' npx kama add site');
52
72
  console.log(' npx kama bundle\n');
@@ -57,13 +77,13 @@ async function main() {
57
77
  const command = args[0]?.toLowerCase();
58
78
  const subCommand = args[1]?.toLowerCase();
59
79
 
60
- if (command === 'help' || command === '--help' || command === '-h') {
80
+ if (['help', '--help', '-h'].includes(command)) {
61
81
  showHelp();
62
- rl.close();
63
82
  process.exit(0);
64
83
  }
65
84
 
66
- console.log('\nšŸ”„ --- KAMA ENGINE CLI --- šŸ”„\n');
85
+ console.clear();
86
+ intro(`${pc.bgCyan(' KAMA • ENGINE ')} ${pc.dim('v' + currentKamaVersion.replace('^', ''))}`);
67
87
 
68
88
  if (command === 'init') {
69
89
  await createFullWorkspace();
@@ -74,29 +94,34 @@ async function main() {
74
94
  } else if (command === 'bundle') {
75
95
  await bundleWorkspaceSchemas();
76
96
  } 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') {
97
+ const choice = handleCancel(await select({
98
+ message: 'What would you like to do?',
99
+ options: [
100
+ { value: 'init', label: 'Init', hint: 'Kickstart new Workspace (CMS + Optional Site)' },
101
+ { value: 'add_site', label: 'Add Site', hint: 'Add Astro Site to current workspace (sites/)' },
102
+ { value: 'bundle', label: 'Bundle', hint: 'Compile YAML schemas for Edge deployment' },
103
+ { value: 'promote', label: 'Promote', hint: 'Promote current Local workspace to Cloudflare Edge' },
104
+ { value: 'help', label: 'Help', hint: 'Show all commands and usage' },
105
+ { value: 'exit', label: 'Exit' }
106
+ ]
107
+ }));
108
+
109
+ if (choice === 'help') {
87
110
  showHelp();
88
- } else if (choice === '4') {
111
+ process.exit(0);
112
+ } else if (choice === 'promote') {
89
113
  await promoteLocalToCloudflare();
90
- } else if (choice === '3') {
114
+ } else if (choice === 'bundle') {
91
115
  await bundleWorkspaceSchemas();
92
- } else if (choice === '2') {
116
+ } else if (choice === 'add_site') {
93
117
  await createNewSiteOnly();
94
- } else {
118
+ } else if (choice === 'init') {
95
119
  await createFullWorkspace();
120
+ } else {
121
+ outro('Goodbye!');
122
+ process.exit(0);
96
123
  }
97
124
  }
98
-
99
- rl.close();
100
125
  }
101
126
 
102
127
  /**
@@ -120,37 +145,54 @@ function convertAstroConfigToCloudflare(siteDir) {
120
145
  async function createFullWorkspace() {
121
146
  const cwdName = basename(process.cwd());
122
147
  const hasExistingPkg = existsSync(join(process.cwd(), 'package.json'));
123
-
124
148
  const defaultDirPrompt = hasExistingPkg ? '.' : cwdName;
125
- const inputName = await rl.question(`Project folder name (use '.' for current dir) [${defaultDirPrompt}]: `);
149
+
150
+ const inputName = handleCancel(await text({
151
+ message: 'Project folder name (use "." for current dir):',
152
+ placeholder: defaultDirPrompt,
153
+ defaultValue: defaultDirPrompt
154
+ }));
126
155
 
127
156
  const chosenFolder = inputName.trim() || defaultDirPrompt;
128
157
  const isCurrentDir = chosenFolder === '.';
129
-
130
158
  const projName = isCurrentDir ? cwdName : chosenFolder;
131
159
  const rootDir = isCurrentDir ? process.cwd() : join(process.cwd(), projName);
132
160
 
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';
161
+ const isCloudflare = handleCancel(await select({
162
+ message: 'Choose your Deployment Architecture:',
163
+ options: [
164
+ { value: true, label: 'Cloudflare Edge', hint: 'Workers, D1, R2, CF Image Resizing' },
165
+ { value: false, label: 'VPS / Local Server', hint: 'Node.js, SQLite, Sharp cropping' }
166
+ ]
167
+ }));
138
168
 
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';
169
+ const shouldCreateSite = handleCancel(await confirm({
170
+ message: 'Do you want to scaffold an initial Astro frontend site?',
171
+ initialValue: true
172
+ }));
141
173
 
142
174
  let siteFolderName = 'kama-starter';
143
175
  let siteDomain = 'localhost:4321';
144
176
  let cmsDomain = 'localhost:8787';
145
177
 
146
178
  if (shouldCreateSite) {
147
- siteFolderName = (await rl.question('Astro package/folder name (e.g., my-website) [kama-starter]: ')).trim() || 'kama-starter';
179
+ siteFolderName = handleCancel(await text({
180
+ message: 'Astro package/folder name (e.g., my-website.eu):',
181
+ placeholder: 'kama-starter',
182
+ defaultValue: 'kama-starter'
183
+ }));
148
184
  }
149
185
 
150
186
  if (isCloudflare) {
151
- cmsDomain = (await rl.question('\nCMS Admin Domain (e.g., admin.yourdomain.com): ')).trim();
187
+ cmsDomain = handleCancel(await text({
188
+ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):',
189
+ defaultValue: ''
190
+ }));
152
191
  if (shouldCreateSite) {
153
- siteDomain = (await rl.question('First Website Domain (e.g., yourdomain.com): ')).trim();
192
+ siteDomain = handleCancel(await text({
193
+ message: 'First Website Domain (e.g., yourdomain.com):',
194
+ defaultValue: ''
195
+ }));
154
196
  }
155
197
  }
156
198
 
@@ -161,11 +203,11 @@ async function createFullWorkspace() {
161
203
  let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
162
204
  const bucketName = `${projName}-media-prod`.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
163
205
 
206
+ const s = spinner();
207
+ s.start('Provisioning workspace...');
208
+
164
209
  if (!isCurrentDir) {
165
- console.log(`\nšŸ“ Creating workspace folder at ./${projName}...`);
166
210
  mkdirSync(rootDir, { recursive: true });
167
- } else {
168
- console.log(`\nšŸ“ Initializing workspace in current directory (${rootDir})...`);
169
211
  }
170
212
 
171
213
  writeFileSync(
@@ -173,40 +215,35 @@ async function createFullWorkspace() {
173
215
  `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
216
  );
175
217
 
176
- console.log(`\nšŸ“„ Copying default content definitions...`);
177
218
  const sourceDefsDir = join(__dirname, '../content/definitions');
178
219
  const targetDefsDir = join(rootDir, 'content', 'definitions');
179
220
  if (existsSync(sourceDefsDir)) {
180
221
  cpSync(sourceDefsDir, targetDefsDir, { recursive: true });
181
- } else {
182
- console.warn(`āš ļø Could not find default content definitions at ${sourceDefsDir}`);
183
222
  }
184
223
 
185
224
  let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
186
225
  let isAuthenticated = false;
187
226
 
188
227
  if (isCloudflare) {
189
- console.log(`\nšŸ”‘ Checking Cloudflare authentication...`);
228
+ s.message('Checking Cloudflare authentication...');
190
229
  try {
191
- // Pass stdio: 'ignore' so child process doesn't drain parent stdin stream!
192
230
  execSync('npx wrangler whoami', { stdio: 'ignore' });
193
231
  isAuthenticated = true;
194
- console.log(`āœ… Logged in to Cloudflare.`);
195
232
  } catch (error) {
196
- console.log(`āš ļø You are not logged in to Cloudflare.`);
197
- console.log(`🌐 Launching browser for Cloudflare login...`);
233
+ s.stop('Not authenticated with Cloudflare.', true);
234
+ console.log(pc.dim('Launching browser for Cloudflare login...'));
198
235
  try {
199
- execSync('npx wrangler login', { stdio: 'ignore' });
236
+ execSync('npx wrangler login', { stdio: 'inherit' });
200
237
  isAuthenticated = true;
201
- console.log(`āœ… Successfully logged in!`);
202
238
  } catch (loginError) {
203
- console.log(`āŒ Login failed or was cancelled.`);
239
+ // Ignore
204
240
  }
241
+ s.start('Resuming provisioning...');
205
242
  }
206
243
 
207
244
  if (isAuthenticated) {
208
245
  try {
209
- console.log(`\nšŸ—„ļø Checking existing Cloudflare D1 databases...`);
246
+ s.message('Checking existing Cloudflare D1 databases...');
210
247
  const listOutput = execSync(`npx wrangler d1 list --json`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
211
248
  const jsonStart = listOutput.indexOf('[');
212
249
  const jsonEnd = listOutput.lastIndexOf(']') + 1;
@@ -219,44 +256,44 @@ async function createFullWorkspace() {
219
256
  const existingDb = databases.find(db => db.name === dbName);
220
257
 
221
258
  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]: `);
259
+ s.stop(`Database '${dbName}' already exists on your account.`);
260
+ const useExisting = handleCancel(await confirm({
261
+ message: 'Do you want to link this existing database?',
262
+ initialValue: true
263
+ }));
224
264
 
225
- if (useExisting.toLowerCase() !== 'n') {
265
+ if (useExisting) {
226
266
  d1DatabaseId = existingDb.uuid;
227
- console.log(`āœ… Linked existing database ID: ${d1DatabaseId}`);
267
+ s.start(`Linked existing database ID: ${d1DatabaseId}`);
228
268
  } else {
229
- dbName = (await rl.question(`Enter a new database name: `)).trim();
230
- console.log(`\nšŸ—„ļø Creating new database '${dbName}'...`);
269
+ dbName = handleCancel(await text({
270
+ message: 'Enter a new database name:'
271
+ }));
272
+ s.start(`Creating new database '${dbName}'...`);
231
273
  const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
232
274
  const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
233
275
  if (match) {
234
276
  d1DatabaseId = match[0];
235
- console.log(`āœ… Success! Database ID bound: ${d1DatabaseId}`);
236
277
  }
237
278
  }
238
279
  } else {
239
- console.log(`\nšŸ—„ļø Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
280
+ s.message(`Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
240
281
  const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
241
282
  const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
242
283
  if (match) {
243
284
  d1DatabaseId = match[0];
244
- console.log(`āœ… Success! Database ID bound: ${d1DatabaseId}`);
245
285
  }
246
286
  }
247
287
  } catch (error) {
248
- console.log(`āš ļø Database operation failed. We'll leave a placeholder.`);
288
+ // Placeholder
249
289
  }
250
- } else {
251
- console.log(`āš ļø Skipping database creation. We'll leave a placeholder in wrangler.json.`);
252
290
  }
253
- } else {
254
- console.log(`\nšŸ’» Local VPS mode selected. Skipping Cloudflare authentication and domain setup...`);
255
291
  }
256
292
 
293
+ s.message('Writing configuration files...');
294
+
257
295
  const rootScripts = {
258
- 'dev:cms': 'npm run dev -w kama-worker',
259
- 'deploy:cms': 'npm run deploy -w kama-worker',
296
+ 'setup': 'npm run setup:local -w kama-worker',
260
297
  };
261
298
 
262
299
  const rootPkg = {
@@ -279,16 +316,17 @@ async function createFullWorkspace() {
279
316
  rootPkg.devDependencies['concurrently'] = '^9.0.0';
280
317
  rootPkg.scripts['dev:site'] = `npm run dev -w sites/${siteFolderName}`;
281
318
  rootPkg.scripts['predev'] = 'npx kama bundle';
282
- rootPkg.scripts['dev'] = 'npx concurrently "npm run dev:cms" "npm run dev:site"';
319
+ rootPkg.scripts['dev'] = 'npx concurrently "npm run dev -w kama-worker" "npm run dev -w sites/' + siteFolderName + '"';
283
320
  rootPkg.scripts['deploy:site'] = `npm run deploy -w sites/${siteFolderName}`;
321
+ rootPkg.scripts['deploy:cms'] = `npm run deploy -w kama-worker`;
284
322
  } else {
285
323
  rootPkg.scripts['predev'] = 'npx kama bundle';
286
- rootPkg.scripts['dev'] = 'npm run dev:cms';
324
+ rootPkg.scripts['dev'] = 'npm run dev -w kama-worker';
325
+ rootPkg.scripts['deploy:cms'] = `npm run deploy -w kama-worker`;
287
326
  }
288
327
 
289
328
  writeFileSync(join(rootDir, 'package.json'), JSON.stringify(rootPkg, null, 2));
290
329
 
291
- console.log(`\nā˜ļø Configuring CMS Worker '${workerName}' (${cmsDomain || 'localhost:8787'})...`);
292
330
  const cmsDir = join(rootDir, 'kama-worker');
293
331
  mkdirSync(cmsDir, { recursive: true });
294
332
 
@@ -310,12 +348,10 @@ async function createFullWorkspace() {
310
348
  type: 'module',
311
349
  version: '1.0.0',
312
350
  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"
351
+ "setup:local": `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql > /dev/null 2>&1 && wrangler d1 execute ${dbName} --local --file=seed.sql > /dev/null 2>&1`,
352
+ "setup:remote": `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql && wrangler d1 execute ${dbName} --remote --file=seed.sql`,
353
+ "dev": "wrangler dev --port 8787 --inspector-port 9239",
354
+ "deploy": "npm run setup:remote && wrangler deploy"
319
355
  },
320
356
  dependencies: {
321
357
  '@brightsideoy/kama': currentKamaVersion,
@@ -350,13 +386,7 @@ async function createFullWorkspace() {
350
386
 
351
387
  writeFileSync(join(cmsDir, 'wrangler.json'), JSON.stringify(cmsWranglerConfig, null, 2));
352
388
 
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
- `);
389
+ writeFileSync(join(cmsDir, '.dev.vars'), `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\n\n# Local Development Overrides\nCDN_DOMAIN=\nPUBLIC_FRONTEND_URL=http://localhost:4321\n`);
360
390
  } else {
361
391
  const templateServerPath = join(__dirname, '../templates/kama-worker/server.js');
362
392
  if (existsSync(templateServerPath)) {
@@ -370,9 +400,8 @@ PUBLIC_FRONTEND_URL=http://localhost:4321
370
400
  type: 'module',
371
401
  version: '1.0.0',
372
402
  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",
403
+ "setup:local": "sqlite3 kama.db < ../node_modules/@brightsideoy/kama/schema.sql && sqlite3 kama.db < seed.sql",
404
+ "dev": "tsx server.js",
376
405
  "start": "node server.js"
377
406
  },
378
407
  dependencies: {
@@ -391,29 +420,31 @@ PUBLIC_FRONTEND_URL=http://localhost:4321
391
420
  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`);
392
421
  }
393
422
 
394
- await bundleWorkspaceSchemas(rootDir);
423
+ s.message('Bundling schemas...');
424
+ await bundleWorkspaceSchemas(rootDir, true);
395
425
 
396
426
  if (shouldCreateSite) {
427
+ s.message(`Scaffolding Astro site (${siteFolderName})...`);
397
428
  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.`);
400
429
  }
401
430
 
402
- console.log(`\n✨ Workspace ready! Next steps:`);
403
- if (!isCurrentDir) console.log(` 1. cd ${projName}`);
404
- console.log(` 2. npm install`);
431
+ s.stop('Workspace provisioned successfully.');
432
+
433
+ let nextSteps = '';
434
+ let stepCount = 1;
435
+ if (!isCurrentDir) nextSteps += `${stepCount++}. cd ${projName}\n`;
436
+ nextSteps += `${stepCount++}. npm install\n`;
437
+ nextSteps += `${stepCount++}. npm run setup ${pc.dim('<- Run this once to init the database!')}\n`;
438
+ nextSteps += `${stepCount++}. npm run dev\n`;
405
439
 
406
440
  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'.`);
441
+ nextSteps += `\n${pc.dim('Cloudflare Note:')}\n`;
442
+ nextSteps += `${pc.dim('Update CLOUDFLARE_ZONE_ID in wrangler.json')}\n`;
443
+ nextSteps += `${pc.dim('Set API token: npx wrangler secret put CLOUDFLARE_API_TOKEN')}`;
416
444
  }
445
+
446
+ note(nextSteps, 'Next steps');
447
+ outro(`KAMA • ENGINE is ready! šŸš€`);
417
448
  }
418
449
 
419
450
  /**
@@ -423,8 +454,8 @@ async function createNewSiteOnly() {
423
454
  const rootDir = process.cwd();
424
455
 
425
456
  if (!existsSync(join(rootDir, 'package.json'))) {
426
- console.error('āŒ Error: Must run this command inside the root of your workspace.');
427
- return;
457
+ cancel('Error: Must run this command inside the root of your workspace.');
458
+ process.exit(1);
428
459
  }
429
460
 
430
461
  let targetWorkerName = 'kama-worker';
@@ -440,45 +471,64 @@ async function createNewSiteOnly() {
440
471
  } catch (e) { }
441
472
  }
442
473
 
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';
474
+ const isCloudflare = handleCancel(await select({
475
+ message: 'Deployment Architecture for this site:',
476
+ options: [
477
+ { value: true, label: 'Cloudflare Edge' },
478
+ { value: false, label: 'VPS / Local Server' }
479
+ ]
480
+ }));
481
+
482
+ const siteFolder = handleCancel(await text({
483
+ message: 'Astro package/folder name (e.g., my-new-site):',
484
+ placeholder: 'kama-starter',
485
+ defaultValue: 'kama-starter'
486
+ }));
448
487
 
449
- const siteFolder = (await rl.question('Astro package/folder name (e.g., my-new-site) [kama-starter]: ')).trim() || 'kama-starter';
450
488
  let siteDomain = 'localhost:4321';
451
489
  let cmsFullUrl = 'http://localhost:8787';
452
490
 
453
491
  if (isCloudflare) {
454
- siteDomain = (await rl.question('Website Domain (e.g., brand.com): ')).trim();
492
+ siteDomain = handleCancel(await text({
493
+ message: 'Website Domain (e.g., brand.com):',
494
+ defaultValue: ''
495
+ }));
455
496
  if (detectedCmsDomain) {
456
497
  cmsFullUrl = detectedCmsDomain.startsWith('http') ? detectedCmsDomain : `https://${detectedCmsDomain.replace(/^https?:\/\//, '')}`;
457
498
  } else {
458
- const cmsDomainInput = (await rl.question('CMS API Domain (e.g., admin.brand.com): ')).trim();
499
+ const cmsDomainInput = handleCancel(await text({
500
+ message: 'CMS API Domain (e.g., admin.brand.com):',
501
+ defaultValue: ''
502
+ }));
459
503
  cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
460
504
  }
461
505
  }
462
506
 
507
+ const s = spinner();
508
+ s.start(`Generating Astro site...`);
463
509
  await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare);
510
+ s.stop(`Site created in sites/${siteFolder}!`);
464
511
 
465
- console.log(`\n✨ Site created in sites/${siteFolder}!`);
466
- console.log(`šŸ“¦ Run 'npm install' at workspace root.`);
512
+ note(`1. npm install\n2. npm run setup ${pc.dim('<- Re-run to register the new site seed data')}`, 'Next steps');
513
+ outro('Site added successfully! šŸš€');
467
514
  }
468
515
 
469
516
  /**
470
517
  * COMMAND: npx kama bundle
471
518
  */
472
- async function bundleWorkspaceSchemas(targetRootDir) {
519
+ async function bundleWorkspaceSchemas(targetRootDir, silent = false) {
473
520
  let rootDir = targetRootDir || process.cwd();
474
521
  if (basename(rootDir) === 'kama-worker') rootDir = resolve(rootDir, '..');
475
522
 
523
+ const s = spinner();
524
+ if (!silent) s.start('Bundling schemas...');
476
525
  try {
477
526
  let compilerPath = join(__dirname, '../dist/schema-compiler.js');
478
527
  if (!existsSync(compilerPath)) compilerPath = join(__dirname, '../scripts/schema-compiler.ts');
479
528
  await import(pathToFileURL(compilerPath).href);
529
+ if (!silent) s.stop('Schemas bundled successfully.');
480
530
  } catch (e) {
481
- console.error(`āŒ Schema compilation failed:`, e.message || e);
531
+ if (!silent) s.stop(`Schema compilation failed: ${e.message}`, true);
482
532
  }
483
533
  }
484
534
 
@@ -491,22 +541,27 @@ async function promoteLocalToCloudflare() {
491
541
  const rootPkgPath = join(rootDir, 'package.json');
492
542
 
493
543
  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;
544
+ cancel('Error: Could not find kama-worker folder or package.json. Run this command inside your workspace root.');
545
+ process.exit(1);
496
546
  }
497
547
 
498
- console.log('\n🌐 --- UPGRADING WORKSPACE TO CLOUDFLARE EDGE --- 🌐\n');
548
+ const s = spinner();
549
+ s.start('Checking Cloudflare authentication...');
499
550
 
500
551
  try {
501
552
  execSync('npx wrangler whoami', { stdio: 'ignore' });
502
- console.log('āœ… Logged in to Cloudflare.');
553
+ s.stop('Logged in to Cloudflare.');
503
554
  } catch (e) {
504
- console.log('🌐 Launching browser for Cloudflare login...');
555
+ s.stop('Not logged in to Cloudflare.', true);
556
+ console.log(pc.dim('Launching browser for Cloudflare login...'));
505
557
  try { execSync('npx wrangler login', { stdio: 'ignore' }); }
506
- catch (loginError) { console.log('āŒ Cloudflare login failed.'); return; }
558
+ catch (loginError) { cancel('Cloudflare login failed.'); process.exit(1); }
507
559
  }
508
560
 
509
- const cmsDomain = (await rl.question('\nCMS Admin Domain (e.g., admin.yourdomain.com): ')).trim();
561
+ const cmsDomain = handleCancel(await text({
562
+ message: 'CMS Admin Domain (e.g., admin.yourdomain.com):',
563
+ defaultValue: ''
564
+ }));
510
565
 
511
566
  const sitesFolderDir = join(rootDir, 'sites');
512
567
  let siteDomain = '';
@@ -515,10 +570,15 @@ async function promoteLocalToCloudflare() {
515
570
  const sites = readdirSync(sitesFolderDir);
516
571
  if (sites.length > 0) {
517
572
  siteDir = join(sitesFolderDir, sites[0]);
518
- siteDomain = (await rl.question('Frontend Website Domain (e.g., yourdomain.com): ')).trim();
573
+ siteDomain = handleCancel(await text({
574
+ message: `Frontend Website Domain for ${sites[0]} (e.g., yourdomain.com):`,
575
+ defaultValue: ''
576
+ }));
519
577
  }
520
578
  }
521
579
 
580
+ s.start('Upgrading workspace to Cloudflare Edge...');
581
+
522
582
  const projName = basename(rootDir);
523
583
  let dbName = `${projName}-db`.replace(/[^a-zA-Z0-9_-]/g, '_');
524
584
  let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
@@ -576,10 +636,10 @@ async function promoteLocalToCloudflare() {
576
636
  const workerPkgPath = join(workerDir, 'package.json');
577
637
  if (existsSync(workerPkgPath)) {
578
638
  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`;
639
+ workerPkg.scripts.dev = "wrangler dev --port 8787 --inspector-port 9239";
640
+ workerPkg.scripts.deploy = "npm run setup:remote && wrangler deploy";
641
+ workerPkg.scripts["setup:local"] = `wrangler d1 execute ${dbName} --local --file=../node_modules/@brightsideoy/kama/schema.sql && wrangler d1 execute ${dbName} --local --file=seed.sql`;
642
+ workerPkg.scripts["setup:remote"] = `wrangler d1 execute ${dbName} --remote --file=../node_modules/@brightsideoy/kama/schema.sql && wrangler d1 execute ${dbName} --remote --file=seed.sql`;
583
643
  workerPkg.devDependencies = workerPkg.devDependencies || {};
584
644
  workerPkg.devDependencies.wrangler = WRANGLER_LATEST;
585
645
  writeFileSync(workerPkgPath, JSON.stringify(workerPkg, null, 2));
@@ -639,20 +699,18 @@ async function promoteLocalToCloudflare() {
639
699
  }
640
700
  }
641
701
 
642
- console.log(`\nāœ… Workspace successfully upgraded to Cloudflare Edge!`);
702
+ s.stop('Workspace successfully upgraded to Cloudflare Edge!');
703
+ outro('Promotion complete. šŸš€');
643
704
  }
644
705
 
645
706
  /**
646
707
  * Helper: Copies the Astro starter template and hydrates runtime tokens
647
708
  */
648
709
  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
-
651
710
  const siteDir = join(rootDir, 'sites', siteFolder);
652
711
  const templateDir = join(__dirname, '../templates/astro-starter');
653
712
 
654
713
  if (!existsSync(templateDir)) {
655
- console.error(`āŒ Error: Astro template not found at ${templateDir}`);
656
714
  return;
657
715
  }
658
716
 
@@ -698,6 +756,7 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
698
756
  const siteLabel = siteFolder.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
699
757
  const safeSiteLabel = siteLabel.replace(/'/g, "''");
700
758
  const channelKey = siteFolder.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
759
+ const homeSlug = `welcome-to-${channelKey}`;
701
760
  const treeKey = 'main-tree';
702
761
  const safeWorkerName = `${siteFolder.toLowerCase().replace(/[^a-z0-9-]/g, '-')}-site`;
703
762
 
@@ -723,27 +782,86 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, wo
723
782
  assets: { directory: "./dist/client", binding: "ASSETS", run_worker_first: true },
724
783
  kv_namespaces: [{ binding: "SESSION", id: sessionId }, { binding: "HTML_CACHE", id: htmlCacheId }],
725
784
  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 }
785
+ vars: { PUBLIC_CMS_URL: cmsFullUrl, PUBLIC_API_URL: cmsFullUrl, PUBLIC_CHANNEL_ID: channelKey, PUBLIC_TREE_ID: treeKey, PUBLIC_SITE_NAME: siteLabel }
727
786
  };
728
787
 
729
788
  if (siteDomain !== 'localhost:4321') siteConfig.routes = [{ pattern: siteDomain, custom_domain: true, enabled: true, previews_enabled: false }];
730
789
  writeFileSync(join(siteDir, 'wrangler.json'), JSON.stringify(siteConfig, null, 2));
731
790
  }
732
791
 
792
+ const siteUrl = (siteDomain === 'localhost:4321' || !isCloudflare) ? `http://${siteDomain}` : `https://${siteDomain}`;
793
+
733
794
  const seedSql = `
734
795
  -- 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);
796
+
797
+ -- 1. Create content node
798
+ INSERT INTO content_nodes (contentType, title, slug, language, status, hasUnpublishedChanges, disabledRegionsJson, dataJson, draftDataJson, publishedDataJson, publishedAt)
799
+ SELECT 'page', 'Welcome to ${safeSiteLabel}', '${homeSlug}', '', 'published', 0, '{}',
800
+ '{"titleSuffix":"– ${safeSiteLabel}","layoutId":"single-column","subpageLayoutId":"two-column","logoText":"${safeSiteLabel}","schemaType":"WebPage"}',
801
+ '{"titleSuffix":"– ${safeSiteLabel}","layoutId":"single-column","subpageLayoutId":"two-column","logoText":"${safeSiteLabel}","schemaType":"WebPage"}',
802
+ '{"titleSuffix":"– ${safeSiteLabel}","layoutId":"single-column","subpageLayoutId":"two-column","logoText":"${safeSiteLabel}","schemaType":"WebPage"}',
803
+ CURRENT_TIMESTAMP
804
+ WHERE NOT EXISTS (SELECT 1 FROM content_nodes WHERE slug = '${homeSlug}' AND title = 'Welcome to ${safeSiteLabel}');
805
+
806
+ -- 2. Create channel
807
+ INSERT OR IGNORE INTO channels (key, label, channelType, baseUrl, defaultLanguage, isHidden, dataJson)
808
+ VALUES ('${channelKey}', '${safeSiteLabel}', 'website', '${siteUrl}', '', 0, '{}');
809
+
810
+ -- 3. Link channelRoot in channel JSON
811
+ UPDATE channels
812
+ SET dataJson = json_patch(
813
+ COALESCE(NULLIF(dataJson, ''), '{}'),
814
+ json_object(
815
+ 'channelRoot', json_object(
816
+ 'href', '#node:' || (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' AND title = 'Welcome to ${safeSiteLabel}' ORDER BY id DESC LIMIT 1),
817
+ 'kind', 'node',
818
+ 'target', '_self',
819
+ 'label', 'Welcome to ${safeSiteLabel}',
820
+ 'nodeId', (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' AND title = 'Welcome to ${safeSiteLabel}' ORDER BY id DESC LIMIT 1)
821
+ )
822
+ )
823
+ )
824
+ WHERE key = '${channelKey}';
825
+
826
+ -- 4. Create Trees & Relations
827
+ INSERT OR IGNORE INTO trees (key, label, treeType, channelId, isHidden, dataJson)
828
+ VALUES ('${treeKey}', 'Main Tree', 'navigation', (SELECT id FROM channels WHERE key = '${channelKey}'), 0, '{}');
829
+
830
+ INSERT OR IGNORE INTO channel_trees (channelId, treeId, key)
831
+ VALUES ((SELECT id FROM channels WHERE key = '${channelKey}'), (SELECT id FROM trees WHERE key = '${treeKey}'), 'main');
832
+
833
+ INSERT OR IGNORE INTO node_channels (nodeId, channelId)
834
+ VALUES ((SELECT id FROM content_nodes WHERE slug = '${homeSlug}' ORDER BY id DESC LIMIT 1), (SELECT id FROM channels WHERE key = '${channelKey}'));
835
+
836
+ INSERT OR IGNORE INTO tree_nodes (treeId, contentId, parentId, sortOrder)
837
+ VALUES ((SELECT id FROM trees WHERE key = '${treeKey}'), (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' ORDER BY id DESC LIMIT 1), NULL, 0);
838
+
839
+ -- 5. Node Routing (TWO records: Canonical '/' and Redirect '/slug' -> '/')
840
+ INSERT OR IGNORE INTO node_routes (path, channelId, nodeId, treeId, isCanonical, redirectUrl)
841
+ VALUES (
842
+ '/',
843
+ (SELECT id FROM channels WHERE key = '${channelKey}'),
844
+ (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' ORDER BY id DESC LIMIT 1),
845
+ (SELECT id FROM trees WHERE key = '${treeKey}'),
846
+ 1,
847
+ NULL
848
+ );
849
+
850
+ INSERT OR IGNORE INTO node_routes (path, channelId, nodeId, treeId, isCanonical, redirectUrl)
851
+ VALUES (
852
+ '/${homeSlug}',
853
+ (SELECT id FROM channels WHERE key = '${channelKey}'),
854
+ (SELECT id FROM content_nodes WHERE slug = '${homeSlug}' ORDER BY id DESC LIMIT 1),
855
+ NULL,
856
+ 0,
857
+ '/'
858
+ );
741
859
  `;
742
860
 
743
861
  const workerDir = join(rootDir, 'kama-worker');
744
862
  if (existsSync(workerDir)) appendFileSync(join(workerDir, 'seed.sql'), seedSql);
745
863
 
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`);
864
+ writeFileSync(join(siteDir, '.env'), `# Local Development Overrides\nPUBLIC_CMS_URL="http://127.0.0.1:8787"\nPUBLIC_API_URL="http://127.0.0.1:8787"\nPUBLIC_CHANNEL_ID="${channelKey}"\nPUBLIC_TREE_ID="${treeKey}"\nPUBLIC_SITE_NAME="${siteLabel.replace(/"/g, '\\"')}"\n`);
747
865
  }
748
866
 
749
867
  main().catch(console.error);