@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.
- package/bin/cli.js +604 -364
- package/content/definitions/block-kama-forms.yaml +5 -0
- package/content/definitions/blocks/button.yaml +15 -5
- package/content/definitions/blocks/image-gallery.yaml +34 -0
- package/content/definitions/blocks/node-list.yaml +75 -0
- package/content/definitions/blocks/youtube.yaml +8 -0
- package/content/definitions/field-sets/access.yaml +10 -0
- package/content/definitions/field-sets/channel-base.yaml +7 -0
- package/content/definitions/field-sets/page-base.yaml +16 -1
- package/content/definitions/nodes/article.yaml +17 -1
- package/content/definitions/taxonomies/categories.yaml +5 -2
- package/content/definitions/taxonomies/media-tag.yaml +15 -0
- package/content/definitions/taxonomies/tags.yaml +5 -2
- package/content/forms/contact.yaml +44 -0
- package/content/forms/subscription.yaml +32 -0
- package/definitions/editor/default.ts +15 -1
- package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
- package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
- package/dist/assets/CmsBadge-BNsgxyyB.css +1 -0
- package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
- package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
- package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
- package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
- package/dist/assets/CmsDataRow-CYNxfVaZ.css +1 -0
- package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
- package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
- package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
- package/dist/assets/CmsHelp-BQkr9KyU.js +1 -0
- package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
- package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
- package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
- package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
- package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
- package/dist/assets/CmsPublisher-XSCPrp5L.js +3 -0
- package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
- package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
- package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
- package/dist/assets/EditorSplitPreview-Cp2x0O_P.js +1 -0
- package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
- package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
- package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
- package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
- package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
- package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
- package/dist/assets/TrashWorkspace-CATe46WF.js +1 -0
- package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
- package/dist/assets/_plugin-vue_export-helper-CQxWhVtr.js +5 -0
- package/dist/assets/circle-check-A-QeFqt3.js +1 -0
- package/dist/assets/database-hg5SqKLG.js +1 -0
- package/dist/assets/folder-tree-BWYot72H.js +1 -0
- package/dist/assets/globe-BraoljaN.js +1 -0
- package/dist/assets/history-pqIbKK9u.js +1 -0
- package/dist/assets/index-CY0VYSyt.css +1 -0
- package/dist/assets/index-DdG9Pozx.js +14 -0
- package/dist/assets/layers-B6RgfpPR.js +1 -0
- package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
- package/dist/assets/monitor-CrpvInqG.js +1 -0
- package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
- package/dist/assets/plus-CX_GHzXs.js +1 -0
- package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
- package/dist/assets/save-Ctv8vW9H.js +1 -0
- package/dist/assets/schema-helpers-DvficKko.js +1 -0
- package/dist/assets/send-F-mJeL54.js +1 -0
- package/dist/assets/smartphone-EPce6VdP.js +1 -0
- package/dist/assets/trash-2-CAUF1KtI.js +1 -0
- package/dist/assets/useLocalStorage-C805uplI.js +1 -0
- package/dist/assets/useNodes-CqFlc72m.js +1 -0
- package/dist/assets/user-IFlT6mYq.js +1 -0
- package/dist/build-seed.js +495 -0
- package/dist/{dist-Dy7-HUEG.js → dist-3Xc2TTZw.js} +3189 -1394
- package/dist/index.html +34 -2
- package/dist/index.js +3116 -1315
- package/dist/schema-compiler.js +5 -5
- package/documentation/EXTENSIONS.md +204 -0
- package/extension-registry.js +5 -0
- package/package.json +13 -6
- package/schema.sql +44 -1
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +9 -0
- package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +6 -12
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +5 -6
- package/templates/astro-starter/src/components/blocks/NodeList.astro +274 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
- package/templates/astro-starter/src/components/global/Footer.astro +7 -46
- package/templates/astro-starter/src/components/global/Header.astro +10 -16
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +68 -49
- package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
- package/templates/astro-starter/src/layouts/StandardLayout.astro +4 -3
- package/templates/astro-starter/src/lib/cms.ts +33 -11
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +15 -4
- package/templates/astro-starter/src/lib/media/media-proxy.ts +22 -3
- package/templates/astro-starter/src/lib/media/media-url.ts +17 -3
- package/templates/astro-starter/src/middleware.ts +158 -100
- package/templates/astro-starter/src/pages/[...slug].astro +77 -3
- package/templates/astro-starter/src/pages/api/load-nodes.ts +68 -0
- package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
- package/templates/kama-worker/index.js +38 -15
- package/templates/kama-worker/server.js +13 -5
- package/templates/kama-worker/setup-local-db.mjs +17 -0
- package/dist/assets/index-F2rfUymo.css +0 -1
- 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
|
|
6
|
-
import
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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('
|
|
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('
|
|
44
|
-
console.log(' npm
|
|
45
|
-
console.log(' npm run
|
|
46
|
-
console.log(' npm run
|
|
47
|
-
console.log(' npm run deploy:
|
|
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 (
|
|
332
|
+
if (['help', '--help', '-h'].includes(command)) {
|
|
61
333
|
showHelp();
|
|
62
|
-
rl.close();
|
|
63
334
|
process.exit(0);
|
|
64
335
|
}
|
|
65
336
|
|
|
66
|
-
console.
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
363
|
+
process.exit(0);
|
|
364
|
+
} else if (choice === 'promote') {
|
|
89
365
|
await promoteLocalToCloudflare();
|
|
90
|
-
} else if (choice === '
|
|
366
|
+
} else if (choice === 'bundle') {
|
|
91
367
|
await bundleWorkspaceSchemas();
|
|
92
|
-
} else if (choice === '
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
140
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
187
|
-
|
|
476
|
+
let backendCacheId = '';
|
|
188
477
|
if (isCloudflare) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
492
|
+
...existingRootPkg,
|
|
493
|
+
name: existingRootPkg.name || projName,
|
|
264
494
|
private: true,
|
|
265
|
-
type: 'module',
|
|
266
|
-
workspaces: ['kama-worker', 'sites/*'],
|
|
267
|
-
scripts:
|
|
268
|
-
dependencies: {
|
|
269
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
"
|
|
314
|
-
"
|
|
315
|
-
"seed:
|
|
316
|
-
"
|
|
317
|
-
"
|
|
318
|
-
"
|
|
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:
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
"
|
|
374
|
-
"seed:local": "
|
|
375
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
604
|
+
s.message('Bundling schemas...');
|
|
605
|
+
await bundleWorkspaceSchemas(rootDir, true);
|
|
395
606
|
|
|
396
607
|
if (shouldCreateSite) {
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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(
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
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
|
|
459
|
-
cmsFullUrl = `https://${cmsDomainInput
|
|
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
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
730
|
+
cancel('Error: Could not find kama-worker folder or package.json.');
|
|
731
|
+
process.exit(1);
|
|
496
732
|
}
|
|
497
733
|
|
|
498
|
-
|
|
734
|
+
const frontendApiSecret = getOrGenerateWorkspaceSecret(workerDir);
|
|
499
735
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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 = (
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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.
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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 = "
|
|
580
|
-
workerPkg.scripts.deploy = "npm run
|
|
581
|
-
workerPkg.scripts["
|
|
582
|
-
workerPkg.scripts["
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
706
|
-
|
|
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
|
-
|
|
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
|
|
734
|
-
|
|
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
|
-
|
|
744
|
-
|
|
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'),
|
|
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(
|
|
986
|
+
main().catch(error => {
|
|
987
|
+
console.error(error);
|
|
988
|
+
process.exitCode = 1;
|
|
989
|
+
});
|