capuzzella 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +103 -0
  2. package/bin/capuzzella +411 -0
  3. package/package.json +32 -0
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Capuzzella CLI
2
+
3
+ Command-line interface for the [Capuzzella](https://capuzzella.com) AI-Powered Website Builder.
4
+
5
+ Manage pages, publishing, scheduling, API keys, AI providers, email, and custom domains — all from your terminal.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 18+
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g capuzzella
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ Set your API key and server URL:
20
+
21
+ ```bash
22
+ export CAPUZZELLA_API_KEY=cap_...
23
+ export CAPUZZELLA_URL=https://your-instance.capuzzella.com
24
+ ```
25
+
26
+ `CAPUZZELLA_URL` defaults to `http://localhost:3000` if not set.
27
+
28
+ ## Usage
29
+
30
+ ```
31
+ capuzzella <command> [options]
32
+ ```
33
+
34
+ ### Identity
35
+
36
+ ```bash
37
+ capuzzella whoami
38
+ ```
39
+
40
+ ### Pages
41
+
42
+ ```bash
43
+ capuzzella pages list
44
+ capuzzella pages get index.html
45
+ capuzzella pages save blog/post.html --file ./post.html
46
+ capuzzella pages delete blog/old.html
47
+ ```
48
+
49
+ ### Publishing
50
+
51
+ ```bash
52
+ capuzzella publish all
53
+ capuzzella publish index.html
54
+ capuzzella unpublish index.html
55
+ capuzzella status index.html
56
+ ```
57
+
58
+ ### Scheduling
59
+
60
+ ```bash
61
+ capuzzella schedule list
62
+ capuzzella schedule list all
63
+ capuzzella schedule add index.html --at 2026-04-01T09:00:00Z
64
+ capuzzella schedule get 1
65
+ capuzzella schedule cancel 1
66
+ ```
67
+
68
+ ### API Keys
69
+
70
+ ```bash
71
+ capuzzella keys list
72
+ capuzzella keys create --name "CI Deploy" --role editor
73
+ capuzzella keys revoke 3
74
+ capuzzella keys delete 3
75
+ capuzzella keys audit 3
76
+ ```
77
+
78
+ ### AI Provider
79
+
80
+ ```bash
81
+ capuzzella ai-provider get
82
+ capuzzella ai-provider set --provider anthropic --model claude-sonnet-4-20250514 --api-key sk-...
83
+ ```
84
+
85
+ ### Transactional Email
86
+
87
+ ```bash
88
+ capuzzella email get
89
+ capuzzella email set --domain example.com --contact-email hello@example.com --api-key re_...
90
+ capuzzella email remove
91
+ ```
92
+
93
+ ### Custom Domains
94
+
95
+ ```bash
96
+ capuzzella domains list
97
+ capuzzella domains add example.com
98
+ capuzzella domains delete <domain-id>
99
+ ```
100
+
101
+ ## License
102
+
103
+ See [LICENSE.md](LICENSE.md).
package/bin/capuzzella ADDED
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+
5
+ const API_KEY = process.env.CAPUZZELLA_API_KEY;
6
+ const BASE_URL = (process.env.CAPUZZELLA_URL || 'http://localhost:3000').replace(/\/$/, '');
7
+
8
+ if (!API_KEY) {
9
+ console.error('Error: CAPUZZELLA_API_KEY environment variable is not set.');
10
+ console.error('Set it with: export CAPUZZELLA_API_KEY=cap_...');
11
+ process.exit(1);
12
+ }
13
+
14
+ const headers = {
15
+ 'Authorization': `Bearer ${API_KEY}`,
16
+ 'Content-Type': 'application/json',
17
+ };
18
+
19
+ async function request(method, path, body) {
20
+ const url = `${BASE_URL}${path}`;
21
+ const opts = { method, headers };
22
+ if (body !== undefined) opts.body = JSON.stringify(body);
23
+
24
+ let res;
25
+ try {
26
+ res = await fetch(url, opts);
27
+ } catch (err) {
28
+ console.error(`Connection failed: ${err.message}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ let data;
33
+ const contentType = res.headers.get('content-type') || '';
34
+ if (contentType.includes('application/json')) {
35
+ data = await res.json();
36
+ } else {
37
+ data = await res.text();
38
+ }
39
+
40
+ if (!res.ok) {
41
+ const msg = typeof data === 'object' ? data.error || JSON.stringify(data) : data;
42
+ console.error(`Error ${res.status}: ${msg}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ return data;
47
+ }
48
+
49
+ function printJson(data) {
50
+ console.log(JSON.stringify(data, null, 2));
51
+ }
52
+
53
+ // ---- Commands ----
54
+
55
+ const commands = {
56
+ async 'whoami'() {
57
+ const data = await request('GET', '/api/me');
58
+ console.log(`Role: ${data.role}`);
59
+ if (data.keyName) console.log(`Key: ${data.keyName} (id ${data.keyId})`);
60
+ if (data.email) console.log(`User: ${data.email}`);
61
+ },
62
+
63
+ async 'pages list'() {
64
+ const data = await request('GET', '/api/pages');
65
+ printJson(data);
66
+ },
67
+
68
+ async 'pages get'(args) {
69
+ const pagePath = args[0];
70
+ if (!pagePath) { console.error('Usage: capuzzella pages get <path>'); process.exit(1); }
71
+ const data = await request('GET', `/api/pages/${pagePath}`);
72
+ if (typeof data === 'object' && data.html) {
73
+ console.log(data.html);
74
+ } else {
75
+ printJson(data);
76
+ }
77
+ },
78
+
79
+ async 'pages save'(args) {
80
+ const pagePath = args[0];
81
+ if (!pagePath) { console.error('Usage: capuzzella pages save <path> --file <file>'); process.exit(1); }
82
+ const fileIdx = args.indexOf('--file');
83
+ if (fileIdx === -1 || !args[fileIdx + 1]) { console.error('Missing --file <file>'); process.exit(1); }
84
+ const filePath = args[fileIdx + 1];
85
+ if (!existsSync(filePath)) { console.error(`File not found: ${filePath}`); process.exit(1); }
86
+ const html = readFileSync(filePath, 'utf8');
87
+ const data = await request('PUT', `/api/pages/${pagePath}`, { html });
88
+ printJson(data);
89
+ },
90
+
91
+ async 'pages delete'(args) {
92
+ const pagePath = args[0];
93
+ if (!pagePath) { console.error('Usage: capuzzella pages delete <path>'); process.exit(1); }
94
+ const data = await request('DELETE', `/api/pages/${pagePath}`);
95
+ printJson(data);
96
+ },
97
+
98
+ async 'publish all'() {
99
+ const data = await request('POST', '/publish/');
100
+ printJson(data);
101
+ },
102
+
103
+ async 'publish'(args) {
104
+ const pagePath = args[0];
105
+ if (!pagePath) { console.error('Usage: capuzzella publish <path>'); process.exit(1); }
106
+ const data = await request('POST', `/publish/${pagePath}`);
107
+ printJson(data);
108
+ },
109
+
110
+ async 'unpublish'(args) {
111
+ const pagePath = args[0];
112
+ if (!pagePath) { console.error('Usage: capuzzella unpublish <path>'); process.exit(1); }
113
+ const data = await request('DELETE', `/publish/${pagePath}`);
114
+ printJson(data);
115
+ },
116
+
117
+ async 'status'(args) {
118
+ const pagePath = args[0];
119
+ if (!pagePath) { console.error('Usage: capuzzella status <path>'); process.exit(1); }
120
+ const data = await request('GET', `/publish/status/${pagePath}`);
121
+ printJson(data);
122
+ },
123
+
124
+ async 'schedule list'(args) {
125
+ const showAll = args[0] === 'all';
126
+ const url = showAll ? '/publish/schedule' : '/publish/schedule?status=pending';
127
+ const data = await request('GET', url);
128
+ if (data.schedules && data.schedules.length === 0) {
129
+ console.log(showAll ? 'No scheduled publishes.' : 'No pending scheduled publishes.');
130
+ return;
131
+ }
132
+ for (const s of data.schedules) {
133
+ const dt = new Date(s.publish_at * 1000).toISOString();
134
+ console.log(` #${s.id} ${s.page_path} → ${dt} [${s.status}]`);
135
+ }
136
+ },
137
+
138
+ async 'schedule add'(args) {
139
+ const pagePath = args[0];
140
+ const atIdx = args.indexOf('--at');
141
+ const publishAt = atIdx !== -1 ? args.slice(atIdx + 1).join(' ') : null;
142
+ if (!pagePath || !publishAt) {
143
+ console.error('Usage: capuzzella schedule add <path> --at <ISO datetime>');
144
+ console.error('Example: capuzzella schedule add index.html --at 2026-03-15T09:00:00Z');
145
+ process.exit(1);
146
+ }
147
+ const data = await request('POST', '/publish/schedule', { pagePath, publishAt });
148
+ if (data.success) {
149
+ const s = data.schedule;
150
+ const dt = new Date(s.publish_at * 1000).toISOString();
151
+ console.log(`Scheduled #${s.id}: ${s.page_path} will publish at ${dt}`);
152
+ } else {
153
+ printJson(data);
154
+ }
155
+ },
156
+
157
+ async 'schedule cancel'(args) {
158
+ const id = args[0];
159
+ if (!id) { console.error('Usage: capuzzella schedule cancel <id>'); process.exit(1); }
160
+ const data = await request('DELETE', `/publish/schedule/${id}`);
161
+ if (data.success) {
162
+ console.log(`Schedule #${id} cancelled.`);
163
+ } else {
164
+ printJson(data);
165
+ }
166
+ },
167
+
168
+ async 'schedule get'(args) {
169
+ const id = args[0];
170
+ if (!id) { console.error('Usage: capuzzella schedule get <id>'); process.exit(1); }
171
+ const data = await request('GET', `/publish/schedule/${id}`);
172
+ printJson(data);
173
+ },
174
+
175
+ async 'keys list'() {
176
+ const data = await request('GET', '/settings/api-keys');
177
+ printJson(data);
178
+ },
179
+
180
+ async 'keys create'(args) {
181
+ const nameIdx = args.indexOf('--name');
182
+ const roleIdx = args.indexOf('--role');
183
+ const limitIdx = args.indexOf('--rate-limit');
184
+ const name = nameIdx !== -1 ? args[nameIdx + 1] : null;
185
+ const role = roleIdx !== -1 ? args[roleIdx + 1] : null;
186
+ const rateLimit = limitIdx !== -1 ? args[limitIdx + 1] : '60';
187
+ if (!name || !role) { console.error('Usage: capuzzella keys create --name <name> --role <role> [--rate-limit <n>]'); process.exit(1); }
188
+ const data = await request('POST', '/settings/api-keys', { name, role, rateLimit });
189
+ if (data.key) {
190
+ console.log('API key created. Copy it now — it will not be shown again:');
191
+ console.log(` ${data.key}`);
192
+ } else {
193
+ printJson(data);
194
+ }
195
+ },
196
+
197
+ async 'keys revoke'(args) {
198
+ const id = args[0];
199
+ if (!id) { console.error('Usage: capuzzella keys revoke <id>'); process.exit(1); }
200
+ const data = await request('POST', `/settings/api-keys/${id}/revoke`);
201
+ printJson(data);
202
+ },
203
+
204
+ async 'keys delete'(args) {
205
+ const id = args[0];
206
+ if (!id) { console.error('Usage: capuzzella keys delete <id>'); process.exit(1); }
207
+ const data = await request('POST', `/settings/api-keys/${id}/delete`);
208
+ printJson(data);
209
+ },
210
+
211
+ async 'keys audit'(args) {
212
+ const id = args[0];
213
+ if (!id) { console.error('Usage: capuzzella keys audit <id>'); process.exit(1); }
214
+ const data = await request('GET', `/settings/api-keys/${id}/audit`);
215
+ if (!data.entries || data.entries.length === 0) {
216
+ console.log('No audit entries.');
217
+ return;
218
+ }
219
+ for (const e of data.entries) {
220
+ const ts = new Date(e.timestamp).toISOString();
221
+ console.log(` ${ts} ${e.method.padEnd(6)} ${e.path}`);
222
+ }
223
+ },
224
+
225
+ async 'ai-provider get'() {
226
+ const data = await request('GET', '/settings/ai-provider');
227
+ console.log(`Active provider: ${data.activeProvider}`);
228
+ for (const p of data.providers) {
229
+ const status = [];
230
+ if (p.active) status.push('active');
231
+ if (p.configured) status.push('configured');
232
+ const flags = status.length ? ` [${status.join(', ')}]` : '';
233
+ console.log(` ${p.id} (${p.name})${flags}`);
234
+ console.log(` Model: ${p.selectedModel}`);
235
+ console.log(` Available: ${p.models.join(', ')}`);
236
+ }
237
+ },
238
+
239
+ async 'ai-provider set'(args) {
240
+ const providerIdx = args.indexOf('--provider');
241
+ const modelIdx = args.indexOf('--model');
242
+ const keyIdx = args.indexOf('--api-key');
243
+ const provider = providerIdx !== -1 ? args[providerIdx + 1] : null;
244
+ const model = modelIdx !== -1 ? args[modelIdx + 1] : null;
245
+ const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : null;
246
+ if (!provider) {
247
+ console.error('Usage: capuzzella ai-provider set --provider <id> [--model <model>] [--api-key <key>]');
248
+ console.error('Providers: openai, anthropic');
249
+ process.exit(1);
250
+ }
251
+ const body = { provider };
252
+ if (model) body.model = model;
253
+ if (apiKey) body.apiKey = apiKey;
254
+ const data = await request('POST', '/settings/ai-provider', body);
255
+ if (data.success) {
256
+ console.log(`AI provider set to ${data.activeProvider}, model: ${data.model}`);
257
+ } else {
258
+ printJson(data);
259
+ }
260
+ },
261
+
262
+ async 'email get'() {
263
+ const data = await request('GET', '/settings/resend');
264
+ if (!data.configured) {
265
+ console.log('Transactional email is not configured.');
266
+ } else {
267
+ console.log('Transactional email configuration:');
268
+ console.log(` Domain: ${data.domain}`);
269
+ console.log(` Contact Email: ${data.contactEmail}`);
270
+ }
271
+ },
272
+
273
+ async 'email set'(args) {
274
+ const keyIdx = args.indexOf('--api-key');
275
+ const domainIdx = args.indexOf('--domain');
276
+ const contactIdx = args.indexOf('--contact-email');
277
+ const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : null;
278
+ const domain = domainIdx !== -1 ? args[domainIdx + 1] : null;
279
+ const contactEmail = contactIdx !== -1 ? args[contactIdx + 1] : null;
280
+ if (!domain || !contactEmail) {
281
+ console.error('Usage: capuzzella email set --domain <domain> --contact-email <email> [--api-key <key>]');
282
+ process.exit(1);
283
+ }
284
+ const body = { domain, contactEmail };
285
+ if (apiKey) body.apiKey = apiKey;
286
+ const data = await request('POST', '/settings/resend', body);
287
+ if (data.success) {
288
+ console.log(`Email configured: domain=${data.domain}, contact=${data.contactEmail}`);
289
+ } else {
290
+ printJson(data);
291
+ }
292
+ },
293
+
294
+ async 'email remove'() {
295
+ const data = await request('POST', '/settings/resend', { action: 'remove' });
296
+ if (data.success) {
297
+ console.log('Transactional email configuration removed.');
298
+ } else {
299
+ printJson(data);
300
+ }
301
+ },
302
+
303
+ async 'domains list'() {
304
+ const data = await request('GET', '/settings/custom-domains');
305
+ if (!data.domains || data.domains.length === 0) {
306
+ console.log('No custom domains configured.');
307
+ return;
308
+ }
309
+ for (const d of data.domains) {
310
+ const status = d.verificationStatus === 'verified' ? 'verified' : 'pending';
311
+ console.log(` ${d.name} [${status}] id: ${d.id}`);
312
+ }
313
+ },
314
+
315
+ async 'domains add'(args) {
316
+ const domainName = args[0];
317
+ if (!domainName) {
318
+ console.error('Usage: capuzzella domains add <domain>');
319
+ console.error('Example: capuzzella domains add example.com');
320
+ process.exit(1);
321
+ }
322
+ const data = await request('POST', '/settings/custom-domains', { domainName });
323
+ if (data.success) {
324
+ console.log(`Custom domain added: ${data.domain.name || domainName}`);
325
+ console.log('Configure a CNAME or A record at your DNS provider to complete setup.');
326
+ } else {
327
+ printJson(data);
328
+ }
329
+ },
330
+
331
+ async 'domains delete'(args) {
332
+ const domainId = args[0];
333
+ if (!domainId) {
334
+ console.error('Usage: capuzzella domains delete <domain-id>');
335
+ console.error('Use "capuzzella domains list" to find domain IDs.');
336
+ process.exit(1);
337
+ }
338
+ const data = await request('DELETE', `/settings/custom-domains/${domainId}`);
339
+ if (data.success) {
340
+ console.log('Custom domain removed.');
341
+ } else {
342
+ printJson(data);
343
+ }
344
+ },
345
+ };
346
+
347
+ // ---- Argument Parsing ----
348
+
349
+ const args = process.argv.slice(2);
350
+ const USAGE = `Capuzzella CLI
351
+
352
+ Usage: capuzzella <command> [options]
353
+
354
+ Commands:
355
+ whoami Show current key role and identity
356
+
357
+ pages list List all pages
358
+ pages get <path> Get page HTML
359
+ pages save <path> --file <file> Save page from local file
360
+ pages delete <path> Delete a page
361
+
362
+ publish all Publish all drafts
363
+ publish <path> Publish a single page
364
+ unpublish <path> Unpublish a page
365
+ status <path> Check publish status
366
+
367
+ schedule list List pending scheduled publishes
368
+ schedule list all List all scheduled publishes
369
+ schedule add <path> --at <datetime> Schedule a page for future publish
370
+ schedule get <id> Get details of a scheduled publish
371
+ schedule cancel <id> Cancel a pending scheduled publish
372
+
373
+ keys list List API keys
374
+ keys create --name <n> --role <r> Create a new API key
375
+ keys revoke <id> Revoke an API key
376
+ keys delete <id> Delete an API key
377
+ keys audit <id> Show audit log for an API key
378
+
379
+ ai-provider get Show AI provider configuration
380
+ ai-provider set --provider <id> Set active AI provider
381
+ [--model <model>] [--api-key <key>]
382
+
383
+ email get Show transactional email config
384
+ email set --domain <d> Configure transactional emails
385
+ --contact-email <e> [--api-key <k>]
386
+ email remove Remove transactional email config
387
+
388
+ domains list List custom domains
389
+ domains add <domain> Add a custom domain
390
+ domains delete <domain-id> Remove a custom domain
391
+
392
+ Environment:
393
+ CAPUZZELLA_API_KEY API key (required)
394
+ CAPUZZELLA_URL Server URL (default: http://localhost:3000)`;
395
+
396
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
397
+ console.log(USAGE);
398
+ process.exit(0);
399
+ }
400
+
401
+ // Match two-word commands first, then single-word
402
+ const twoWord = `${args[0]} ${args[1]}`;
403
+ if (commands[twoWord]) {
404
+ await commands[twoWord](args.slice(2));
405
+ } else if (commands[args[0]]) {
406
+ await commands[args[0]](args.slice(1));
407
+ } else {
408
+ console.error(`Unknown command: ${args.join(' ')}`);
409
+ console.log(USAGE);
410
+ process.exit(1);
411
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "capuzzella",
3
+ "version": "1.0.0",
4
+ "description": "CLI for the Capuzzella AI-Powered Website Builder",
5
+ "type": "module",
6
+ "bin": {
7
+ "capuzzella": "bin/capuzzella"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "keywords": [
13
+ "capuzzella",
14
+ "cli",
15
+ "website-builder",
16
+ "ai",
17
+ "cms"
18
+ ],
19
+ "author": "Maurice Wipf",
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/mauricewipf/capuzzella-cli.git"
27
+ },
28
+ "homepage": "https://github.com/mauricewipf/capuzzella-cli#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/mauricewipf/capuzzella-cli/issues"
31
+ }
32
+ }