@package-broker/cloudflare 0.10.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/dist/index.js ADDED
@@ -0,0 +1,784 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * PACKAGE.broker - Cloudflare CLI
4
+ * Copyright (C) 2025 Łukasz Bajsarowicz
5
+ * Licensed under AGPL-3.0
6
+ */
7
+ import { existsSync, mkdirSync, readdirSync, copyFileSync, writeFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { tmpdir } from 'os';
10
+ import prompts from 'prompts';
11
+ import { randomBytes } from 'crypto';
12
+ import { checkAuth, createD1Database, findD1Database, createKVNamespace, findKVNamespace, createR2Bucket, findR2Bucket, createQueue, findQueue, setSecret, applyMigrations, deployWorker, verifyTokenPermissions, } from './wrangler.js';
13
+ import { renderTemplate, writeWranglerToml } from './template.js';
14
+ import { findMainPackage, findUiPackage } from './paths.js';
15
+ import { parseWranglerToml, findMissingResources, generateWranglerToml, mergeResourcesIntoConfig, wranglerTomlExists, } from './wrangler-config.js';
16
+ // ============================================================================
17
+ // Logging Utilities
18
+ // ============================================================================
19
+ const COLORS = {
20
+ reset: '\x1b[0m',
21
+ bright: '\x1b[1m',
22
+ green: '\x1b[32m',
23
+ blue: '\x1b[34m',
24
+ yellow: '\x1b[33m',
25
+ red: '\x1b[31m',
26
+ };
27
+ const isCI = process.env.CI === 'true';
28
+ /**
29
+ * Log a message to console (interactive mode) or stderr (CI mode with --json)
30
+ */
31
+ function log(message, color = 'reset', options) {
32
+ const output = options?.json ? process.stderr : process.stdout;
33
+ output.write(`${COLORS[color]}${message}${COLORS.reset}\n`);
34
+ }
35
+ /**
36
+ * Log a GitHub Actions annotation (only in CI environment)
37
+ */
38
+ function ghAnnotation(type, message) {
39
+ if (isCI) {
40
+ console.error(`::${type}::${message}`);
41
+ }
42
+ }
43
+ /**
44
+ * Output JSON result to stdout (for --json mode)
45
+ */
46
+ function outputJson(result) {
47
+ console.log(JSON.stringify(result));
48
+ }
49
+ // ============================================================================
50
+ // Argument Parsing
51
+ // ============================================================================
52
+ function parseArgs(argv) {
53
+ const args = argv.slice(2);
54
+ const options = {
55
+ command: 'init',
56
+ ci: false,
57
+ json: false,
58
+ tier: 'free',
59
+ skipUiBuild: false,
60
+ skipMigrations: false,
61
+ };
62
+ // Parse command
63
+ if (args.length > 0 && !args[0].startsWith('-')) {
64
+ const cmd = args[0].toLowerCase();
65
+ if (cmd === 'deploy') {
66
+ options.command = 'deploy';
67
+ }
68
+ else if (cmd === 'init') {
69
+ options.command = 'init';
70
+ }
71
+ else if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
72
+ options.command = 'help';
73
+ }
74
+ }
75
+ // Parse flags
76
+ for (let i = 0; i < args.length; i++) {
77
+ const arg = args[i];
78
+ if (arg === '--ci') {
79
+ options.ci = true;
80
+ }
81
+ else if (arg === '--json') {
82
+ options.json = true;
83
+ }
84
+ else if (arg === '--worker-name' && args[i + 1]) {
85
+ options.workerName = args[++i];
86
+ }
87
+ else if (arg === '--tier' && args[i + 1]) {
88
+ const tier = args[++i].toLowerCase();
89
+ if (tier === 'free' || tier === 'paid') {
90
+ options.tier = tier;
91
+ }
92
+ }
93
+ else if (arg === '--domain' && args[i + 1]) {
94
+ options.domain = args[++i];
95
+ }
96
+ else if (arg === '--skip-ui-build') {
97
+ options.skipUiBuild = true;
98
+ }
99
+ else if (arg === '--skip-migrations') {
100
+ options.skipMigrations = true;
101
+ }
102
+ else if (arg === '-h' || arg === '--help') {
103
+ options.command = 'help';
104
+ }
105
+ }
106
+ // Check environment variable overrides (for CI)
107
+ if (options.ci) {
108
+ if (process.env.WORKER_NAME && !options.workerName) {
109
+ options.workerName = process.env.WORKER_NAME;
110
+ }
111
+ if (process.env.CLOUDFLARE_TIER) {
112
+ const tier = process.env.CLOUDFLARE_TIER.toLowerCase();
113
+ if (tier === 'free' || tier === 'paid') {
114
+ options.tier = tier;
115
+ }
116
+ }
117
+ if (process.env.DOMAIN && !options.domain) {
118
+ options.domain = process.env.DOMAIN;
119
+ }
120
+ if (process.env.SKIP_UI_BUILD === 'true') {
121
+ options.skipUiBuild = true;
122
+ }
123
+ if (process.env.SKIP_MIGRATIONS === 'true') {
124
+ options.skipMigrations = true;
125
+ }
126
+ }
127
+ return options;
128
+ }
129
+ // ============================================================================
130
+ // Help Command
131
+ // ============================================================================
132
+ function showHelp() {
133
+ console.log(`
134
+ PACKAGE.broker Cloudflare CLI
135
+
136
+ Usage: package-broker-cloudflare [command] [options]
137
+
138
+ Commands:
139
+ init Interactive setup (default)
140
+ deploy Deploy to Cloudflare Workers
141
+ help Show this help message
142
+
143
+ Options:
144
+ --ci Non-interactive mode (no prompts)
145
+ --json Output machine-readable JSON
146
+ --worker-name Worker name (default: package-broker)
147
+ --tier Cloudflare tier: free or paid (default: free)
148
+ --domain Custom domain for routes
149
+ --skip-ui-build Skip UI build step
150
+ --skip-migrations Skip database migrations
151
+
152
+ Environment Variables (CI mode):
153
+ CLOUDFLARE_API_TOKEN Cloudflare API token (required)
154
+ CLOUDFLARE_ACCOUNT_ID Cloudflare account ID (required)
155
+ ENCRYPTION_KEY Base64-encoded encryption key (required)
156
+ WORKER_NAME Worker name (overrides --worker-name)
157
+ CLOUDFLARE_TIER Tier: free or paid (overrides --tier)
158
+ DOMAIN Custom domain (overrides --domain)
159
+ SKIP_UI_BUILD Set to 'true' to skip UI build
160
+ SKIP_MIGRATIONS Set to 'true' to skip migrations
161
+
162
+ Examples:
163
+ # Interactive setup
164
+ npx package-broker-cloudflare init
165
+
166
+ # CI deployment
167
+ npx package-broker-cloudflare deploy --ci --json --worker-name my-broker
168
+
169
+ # Deploy with custom domain
170
+ npx package-broker-cloudflare deploy --ci --json --domain packages.example.com
171
+ `);
172
+ }
173
+ // ============================================================================
174
+ // Utility Functions
175
+ // ============================================================================
176
+ function generateEncryptionKey() {
177
+ return randomBytes(32).toString('base64');
178
+ }
179
+ function validateWorkerName(name) {
180
+ return /^[a-zA-Z0-9_-]+$/.test(name);
181
+ }
182
+ async function copyMigrations(targetDir, destDir) {
183
+ const mainPackagePath = findMainPackage(targetDir);
184
+ if (!mainPackagePath) {
185
+ throw new Error('@package-broker/main not found. Please run: npm install @package-broker/main\n' +
186
+ ' Or ensure you are in a directory with @package-broker/main installed.');
187
+ }
188
+ const migrationsDir = destDir || join(targetDir, 'migrations');
189
+ if (!existsSync(migrationsDir)) {
190
+ mkdirSync(migrationsDir, { recursive: true });
191
+ }
192
+ const sourceMigrationsDir = join(mainPackagePath, 'migrations');
193
+ if (!existsSync(sourceMigrationsDir)) {
194
+ throw new Error('Migrations directory not found in @package-broker/main');
195
+ }
196
+ const migrationFiles = readdirSync(sourceMigrationsDir).filter((f) => f.endsWith('.sql'));
197
+ for (const file of migrationFiles) {
198
+ copyFileSync(join(sourceMigrationsDir, file), join(migrationsDir, file));
199
+ }
200
+ return migrationFiles.length;
201
+ }
202
+ // ============================================================================
203
+ // CI Deploy Flow
204
+ // ============================================================================
205
+ async function runCiDeploy(options) {
206
+ const targetDir = process.cwd();
207
+ const jsonOutput = options.json;
208
+ // Validate required environment variables
209
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN;
210
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
211
+ const encryptionKey = process.env.ENCRYPTION_KEY;
212
+ if (!apiToken) {
213
+ const error = 'CLOUDFLARE_API_TOKEN environment variable is required';
214
+ ghAnnotation('error', error);
215
+ if (jsonOutput) {
216
+ outputJson({ error });
217
+ }
218
+ else {
219
+ log(`✗ ${error}`, 'red');
220
+ }
221
+ process.exit(1);
222
+ }
223
+ if (!accountId) {
224
+ const error = 'CLOUDFLARE_ACCOUNT_ID environment variable is required';
225
+ ghAnnotation('error', error);
226
+ if (jsonOutput) {
227
+ outputJson({ error });
228
+ }
229
+ else {
230
+ log(`✗ ${error}`, 'red');
231
+ }
232
+ process.exit(1);
233
+ }
234
+ if (!encryptionKey) {
235
+ const error = 'ENCRYPTION_KEY environment variable is required';
236
+ ghAnnotation('error', error);
237
+ if (jsonOutput) {
238
+ outputJson({ error });
239
+ }
240
+ else {
241
+ log(`✗ ${error}`, 'red');
242
+ }
243
+ process.exit(1);
244
+ }
245
+ // Validate worker name
246
+ const workerName = options.workerName || 'package-broker';
247
+ if (!validateWorkerName(workerName)) {
248
+ const error = `Invalid worker name: ${workerName}. Use only letters, numbers, hyphens, and underscores.`;
249
+ ghAnnotation('error', error);
250
+ if (jsonOutput) {
251
+ outputJson({ error });
252
+ }
253
+ else {
254
+ log(`✗ ${error}`, 'red');
255
+ }
256
+ process.exit(1);
257
+ }
258
+ const paidTier = options.tier === 'paid';
259
+ // Wrangler options for all commands
260
+ const wranglerOpts = {
261
+ apiToken,
262
+ accountId,
263
+ cwd: targetDir,
264
+ };
265
+ try {
266
+ // Check prerequisites
267
+ log('Checking prerequisites...', 'blue', { json: jsonOutput });
268
+ const mainPackagePath = findMainPackage(targetDir);
269
+ if (!mainPackagePath) {
270
+ throw new Error('@package-broker/main not found. Run: npm install @package-broker/main');
271
+ }
272
+ // Check authentication
273
+ log('Verifying Cloudflare authentication...', 'blue', { json: jsonOutput });
274
+ const isAuthenticated = await checkAuth(wranglerOpts);
275
+ if (!isAuthenticated) {
276
+ throw new Error('Cloudflare authentication failed. Check your API token.');
277
+ }
278
+ ghAnnotation('notice', 'Cloudflare authentication successful');
279
+ // Verify token permissions
280
+ log('Verifying token permissions...', 'blue', { json: jsonOutput });
281
+ const permissions = await verifyTokenPermissions({ ...wranglerOpts, paidTier });
282
+ if (!permissions.valid) {
283
+ ghAnnotation('warning', `Token permission issues: ${permissions.errors.join(', ')}`);
284
+ }
285
+ // Check for existing wrangler.toml and parse it
286
+ log('Checking for existing wrangler.toml...', 'blue', { json: jsonOutput });
287
+ const existingConfig = wranglerTomlExists(targetDir) ? parseWranglerToml(targetDir) : null;
288
+ const { needsDatabase, needsKV, needsR2, needsQueue, existingResources } = findMissingResources(existingConfig, workerName, paidTier);
289
+ if (existingConfig) {
290
+ log('Found existing wrangler.toml, extracting resource IDs...', 'blue', { json: jsonOutput });
291
+ ghAnnotation('notice', 'Using existing wrangler.toml configuration');
292
+ }
293
+ // Resource names
294
+ const dbName = existingResources.database_name || `${workerName}-db`;
295
+ const kvTitle = `${workerName}-kv`;
296
+ const r2Bucket = existingResources.r2_bucket_name || `${workerName}-artifacts`;
297
+ const queueName = paidTier ? (existingResources.queue_name || `${workerName}-queue`) : undefined;
298
+ // Create/find missing resources
299
+ const resources = { ...existingResources };
300
+ if (needsDatabase) {
301
+ log(`Creating/finding D1 database: ${dbName}...`, 'blue', { json: jsonOutput });
302
+ const existingDbId = await findD1Database(dbName, wranglerOpts);
303
+ if (existingDbId) {
304
+ log(`Database already exists: ${existingDbId}`, 'green', { json: jsonOutput });
305
+ resources.database_id = existingDbId;
306
+ }
307
+ else {
308
+ const newDbId = await createD1Database(dbName, wranglerOpts);
309
+ log(`Database created: ${newDbId}`, 'green', { json: jsonOutput });
310
+ resources.database_id = newDbId;
311
+ }
312
+ resources.database_name = dbName;
313
+ }
314
+ if (needsKV) {
315
+ log(`Creating/finding KV namespace: ${kvTitle}...`, 'blue', { json: jsonOutput });
316
+ const existingKvId = await findKVNamespace(kvTitle, wranglerOpts);
317
+ if (existingKvId) {
318
+ log(`KV namespace already exists: ${existingKvId}`, 'green', { json: jsonOutput });
319
+ resources.kv_namespace_id = existingKvId;
320
+ }
321
+ else {
322
+ const newKvId = await createKVNamespace(kvTitle, wranglerOpts);
323
+ log(`KV namespace created: ${newKvId}`, 'green', { json: jsonOutput });
324
+ resources.kv_namespace_id = newKvId;
325
+ }
326
+ }
327
+ if (needsR2) {
328
+ log(`Creating/finding R2 bucket: ${r2Bucket}...`, 'blue', { json: jsonOutput });
329
+ const bucketExists = await findR2Bucket(r2Bucket, wranglerOpts);
330
+ if (bucketExists) {
331
+ log('R2 bucket already exists', 'green', { json: jsonOutput });
332
+ }
333
+ else {
334
+ await createR2Bucket(r2Bucket, wranglerOpts);
335
+ log('R2 bucket created', 'green', { json: jsonOutput });
336
+ }
337
+ resources.r2_bucket_name = r2Bucket;
338
+ }
339
+ if (needsQueue && queueName) {
340
+ log(`Creating/finding Queue: ${queueName}...`, 'blue', { json: jsonOutput });
341
+ const queueExists = await findQueue(queueName, wranglerOpts);
342
+ if (queueExists) {
343
+ log('Queue already exists', 'green', { json: jsonOutput });
344
+ }
345
+ else {
346
+ await createQueue(queueName, wranglerOpts);
347
+ log('Queue created', 'green', { json: jsonOutput });
348
+ }
349
+ resources.queue_name = queueName;
350
+ }
351
+ // Create ephemeral workspace
352
+ const ephemeralDir = join(tmpdir(), 'package-broker-cloudflare', `${workerName}-${Date.now()}`);
353
+ mkdirSync(ephemeralDir, { recursive: true });
354
+ log(`Created ephemeral workspace: ${ephemeralDir}`, 'blue', { json: jsonOutput });
355
+ // Generate or merge wrangler.toml
356
+ log('Generating wrangler.toml...', 'blue', { json: jsonOutput });
357
+ let wranglerContent;
358
+ const uiPackagePath = findUiPackage(targetDir);
359
+ const uiAssetsPath = uiPackagePath ? 'node_modules/@package-broker/ui/dist' : undefined;
360
+ if (existingConfig?._raw) {
361
+ // Merge new resource IDs into existing config
362
+ wranglerContent = mergeResourcesIntoConfig(existingConfig._raw, resources, workerName, { paidTier, domain: options.domain });
363
+ }
364
+ else {
365
+ // Generate new config
366
+ wranglerContent = generateWranglerToml(workerName, resources, {
367
+ paidTier,
368
+ domain: options.domain,
369
+ mainPath: 'node_modules/@package-broker/main/dist/index.js',
370
+ uiAssetsPath,
371
+ });
372
+ }
373
+ const ephemeralConfigPath = join(ephemeralDir, 'wrangler.toml');
374
+ writeFileSync(ephemeralConfigPath, wranglerContent, 'utf-8');
375
+ // Copy migrations to ephemeral directory
376
+ log('Copying migrations...', 'blue', { json: jsonOutput });
377
+ const migrationsDir = join(ephemeralDir, 'migrations');
378
+ const migrationCount = await copyMigrations(targetDir, migrationsDir);
379
+ log(`${migrationCount} migration files copied`, 'green', { json: jsonOutput });
380
+ // Check/build UI
381
+ if (!options.skipUiBuild) {
382
+ log('Checking UI assets...', 'blue', { json: jsonOutput });
383
+ const uiDistPath = uiPackagePath ? join(uiPackagePath, 'dist') : null;
384
+ if (!uiDistPath || !existsSync(uiDistPath)) {
385
+ ghAnnotation('warning', 'UI assets not found. UI may not be available.');
386
+ log('UI assets not found. Skipping UI...', 'yellow', { json: jsonOutput });
387
+ }
388
+ else {
389
+ log('UI assets found', 'green', { json: jsonOutput });
390
+ }
391
+ }
392
+ // Set encryption key as secret
393
+ log('Setting encryption key as secret...', 'blue', { json: jsonOutput });
394
+ await setSecret('ENCRYPTION_KEY', encryptionKey, {
395
+ ...wranglerOpts,
396
+ workerName,
397
+ configPath: ephemeralConfigPath,
398
+ });
399
+ log('Encryption key set', 'green', { json: jsonOutput });
400
+ // Apply migrations
401
+ if (!options.skipMigrations) {
402
+ log('Applying database migrations...', 'blue', { json: jsonOutput });
403
+ try {
404
+ await applyMigrations(dbName, migrationsDir, {
405
+ ...wranglerOpts,
406
+ remote: true,
407
+ configPath: ephemeralConfigPath,
408
+ });
409
+ log('Migrations applied', 'green', { json: jsonOutput });
410
+ }
411
+ catch (migrationError) {
412
+ ghAnnotation('warning', `Migration warning: ${migrationError.message}`);
413
+ log(`Migration warning: ${migrationError.message}`, 'yellow', { json: jsonOutput });
414
+ }
415
+ }
416
+ // Deploy worker
417
+ log('Deploying Worker...', 'blue', { json: jsonOutput });
418
+ const workerUrl = await deployWorker({
419
+ ...wranglerOpts,
420
+ workerName,
421
+ configPath: ephemeralConfigPath,
422
+ });
423
+ ghAnnotation('notice', `Deployment complete! Worker URL: ${workerUrl}`);
424
+ // Output result
425
+ const result = {
426
+ worker_url: workerUrl,
427
+ database_id: resources.database_id || '',
428
+ kv_namespace_id: resources.kv_namespace_id || '',
429
+ r2_bucket_name: resources.r2_bucket_name || r2Bucket,
430
+ queue_name: resources.queue_name,
431
+ };
432
+ if (jsonOutput) {
433
+ outputJson(result);
434
+ }
435
+ else {
436
+ log(`\n✅ Deployment complete!`, 'bright');
437
+ log(`🌐 Worker URL: ${workerUrl}`, 'bright');
438
+ if (options.domain) {
439
+ log(`\n📝 Custom Domain Configuration Required:`, 'yellow');
440
+ log(` Create a CNAME record pointing ${options.domain} to your worker`, 'yellow');
441
+ }
442
+ }
443
+ }
444
+ catch (error) {
445
+ const errorMessage = error.message;
446
+ ghAnnotation('error', errorMessage);
447
+ if (jsonOutput) {
448
+ outputJson({ error: errorMessage });
449
+ }
450
+ else {
451
+ log(`\n✗ Deployment failed: ${errorMessage}`, 'red');
452
+ }
453
+ process.exit(1);
454
+ }
455
+ }
456
+ // ============================================================================
457
+ // Interactive Init Flow
458
+ // ============================================================================
459
+ async function runInteractiveInit() {
460
+ const targetDir = process.cwd();
461
+ log('\n🚀 PACKAGE.broker - Cloudflare Workers Setup\n', 'bright');
462
+ // Check prerequisites
463
+ const mainPackagePath = findMainPackage(targetDir);
464
+ if (!mainPackagePath) {
465
+ log('Error: @package-broker/main not found', 'red');
466
+ log(' Please run: npm install @package-broker/main', 'yellow');
467
+ log(' Or ensure you are in a directory with @package-broker/main installed.', 'yellow');
468
+ process.exit(1);
469
+ }
470
+ // Check wrangler.toml
471
+ const wranglerPath = join(targetDir, 'wrangler.toml');
472
+ if (existsSync(wranglerPath)) {
473
+ const response = await prompts({
474
+ type: 'confirm',
475
+ name: 'overwrite',
476
+ message: 'wrangler.toml already exists. Overwrite?',
477
+ initial: false,
478
+ });
479
+ if (!response.overwrite) {
480
+ log('Aborted.', 'yellow');
481
+ process.exit(0);
482
+ }
483
+ }
484
+ // Interactive prompts
485
+ log('\n📋 Configuration\n', 'bright');
486
+ const tierResponse = await prompts({
487
+ type: 'select',
488
+ name: 'tier',
489
+ message: 'Which Cloudflare Workers tier will you use?',
490
+ choices: [
491
+ { title: 'Free tier (100k requests/day, no queues)', value: 'free' },
492
+ { title: 'Paid tier ($5/month, unlimited requests, queues enabled)', value: 'paid' },
493
+ ],
494
+ initial: 0,
495
+ });
496
+ if (!tierResponse.tier) {
497
+ log('Aborted.', 'yellow');
498
+ process.exit(0);
499
+ }
500
+ const paidTier = tierResponse.tier === 'paid';
501
+ const nameResponse = await prompts({
502
+ type: 'text',
503
+ name: 'workerName',
504
+ message: 'Worker name:',
505
+ initial: 'package-broker',
506
+ validate: (value) => {
507
+ if (!value || value.trim().length === 0) {
508
+ return 'Worker name cannot be empty';
509
+ }
510
+ if (!validateWorkerName(value)) {
511
+ return 'Worker name can only contain letters, numbers, hyphens, and underscores';
512
+ }
513
+ return true;
514
+ },
515
+ });
516
+ if (!nameResponse.workerName) {
517
+ log('Aborted.', 'yellow');
518
+ process.exit(0);
519
+ }
520
+ const workerName = nameResponse.workerName.trim();
521
+ // Generate encryption key
522
+ log('\n🔐 Generating encryption key...', 'blue');
523
+ const encryptionKey = generateEncryptionKey();
524
+ log('✓ Encryption key generated', 'green');
525
+ // Check authentication
526
+ log('\n🔑 Checking Cloudflare authentication...', 'blue');
527
+ const isAuthenticated = await checkAuth();
528
+ if (!isAuthenticated) {
529
+ log('⚠️ Not authenticated with Cloudflare', 'yellow');
530
+ log(' Please run: npx wrangler login', 'yellow');
531
+ process.exit(1);
532
+ }
533
+ log('✓ Authenticated', 'green');
534
+ // Create resources
535
+ log('\n📦 Creating Cloudflare resources...\n', 'bright');
536
+ const dbName = `${workerName}-db`;
537
+ const kvTitle = `${workerName}-kv`;
538
+ const r2Bucket = `${workerName}-artifacts`;
539
+ const queueName = paidTier ? `${workerName}-queue` : undefined;
540
+ let dbId;
541
+ let kvId;
542
+ // D1 Database
543
+ log(`Creating D1 database: ${dbName}...`, 'blue');
544
+ try {
545
+ const existingDbId = await findD1Database(dbName);
546
+ if (existingDbId) {
547
+ log(`✓ Database already exists: ${existingDbId}`, 'green');
548
+ dbId = existingDbId;
549
+ }
550
+ else {
551
+ dbId = await createD1Database(dbName);
552
+ log(`✓ Database created: ${dbId}`, 'green');
553
+ }
554
+ }
555
+ catch (error) {
556
+ log(`✗ Failed to create database: ${error.message}`, 'red');
557
+ process.exit(1);
558
+ }
559
+ // KV Namespace
560
+ log(`Creating KV namespace: ${kvTitle}...`, 'blue');
561
+ try {
562
+ const existingKvId = await findKVNamespace(kvTitle);
563
+ if (existingKvId) {
564
+ log(`✓ KV namespace already exists: ${existingKvId}`, 'green');
565
+ kvId = existingKvId;
566
+ }
567
+ else {
568
+ kvId = await createKVNamespace(kvTitle);
569
+ log(`✓ KV namespace created: ${kvId}`, 'green');
570
+ }
571
+ }
572
+ catch (error) {
573
+ log(`✗ Failed to create KV namespace: ${error.message}`, 'red');
574
+ process.exit(1);
575
+ }
576
+ // R2 Bucket
577
+ log(`Creating R2 bucket: ${r2Bucket}...`, 'blue');
578
+ try {
579
+ const bucketExists = await findR2Bucket(r2Bucket);
580
+ if (bucketExists) {
581
+ log(`✓ R2 bucket already exists`, 'green');
582
+ }
583
+ else {
584
+ await createR2Bucket(r2Bucket);
585
+ log(`✓ R2 bucket created`, 'green');
586
+ }
587
+ }
588
+ catch (error) {
589
+ log(`✗ Failed to create R2 bucket: ${error.message}`, 'red');
590
+ process.exit(1);
591
+ }
592
+ // Queue (paid tier only)
593
+ if (paidTier && queueName) {
594
+ log(`Creating Queue: ${queueName}...`, 'blue');
595
+ try {
596
+ const queueExists = await findQueue(queueName);
597
+ if (queueExists) {
598
+ log(`✓ Queue already exists`, 'green');
599
+ }
600
+ else {
601
+ await createQueue(queueName);
602
+ log(`✓ Queue created`, 'green');
603
+ }
604
+ }
605
+ catch (error) {
606
+ log(`✗ Failed to create Queue: ${error.message}`, 'red');
607
+ process.exit(1);
608
+ }
609
+ }
610
+ // Set encryption key as secret
611
+ log('\n🔐 Setting encryption key as Cloudflare secret...', 'blue');
612
+ try {
613
+ await setSecret('ENCRYPTION_KEY', encryptionKey, {
614
+ cwd: targetDir,
615
+ workerName: workerName
616
+ });
617
+ log('✓ Encryption key set as secret', 'green');
618
+ }
619
+ catch (error) {
620
+ log(`✗ Failed to set secret: ${error.message}`, 'red');
621
+ log(' You can set it manually with: wrangler secret put ENCRYPTION_KEY', 'yellow');
622
+ process.exit(1);
623
+ }
624
+ // Generate wrangler.toml
625
+ log('\n📝 Generating wrangler.toml...', 'blue');
626
+ try {
627
+ const templateContent = renderTemplate(targetDir, {
628
+ worker_name: workerName,
629
+ generated_db_id: dbId,
630
+ generated_kv_id: kvId,
631
+ generated_queue_name: queueName,
632
+ paid_tier: paidTier,
633
+ });
634
+ writeWranglerToml(targetDir, templateContent);
635
+ log('✓ wrangler.toml created', 'green');
636
+ }
637
+ catch (error) {
638
+ log(`✗ Failed to generate wrangler.toml: ${error.message}`, 'red');
639
+ process.exit(1);
640
+ }
641
+ // Copy migrations
642
+ log('\n📋 Copying migrations...', 'blue');
643
+ try {
644
+ const migrationCount = await copyMigrations(targetDir);
645
+ log(`✓ ${migrationCount} migration files copied`, 'green');
646
+ }
647
+ catch (error) {
648
+ log(`✗ Failed to copy migrations: ${error.message}`, 'red');
649
+ process.exit(1);
650
+ }
651
+ // Check if UI needs to be built
652
+ log('\n🎨 Checking UI assets...', 'blue');
653
+ const uiPackagePath = findUiPackage(targetDir);
654
+ const uiDistPath = uiPackagePath ? join(uiPackagePath, 'dist') : null;
655
+ if (!uiDistPath || !existsSync(uiDistPath)) {
656
+ log('⚠️ UI assets not found. Checking UI package...', 'yellow');
657
+ try {
658
+ const { execa } = await import('execa');
659
+ // Check if UI package exists
660
+ if (uiPackagePath && existsSync(uiPackagePath)) {
661
+ log(' Building UI...', 'blue');
662
+ await execa('npm', ['run', 'build'], {
663
+ cwd: uiPackagePath,
664
+ stdio: 'pipe',
665
+ });
666
+ log('✓ UI built successfully', 'green');
667
+ }
668
+ else {
669
+ log('⚠️ UI package not found. Installing @package-broker/ui...', 'yellow');
670
+ await execa('npm', ['install', '@package-broker/ui'], {
671
+ cwd: targetDir,
672
+ stdio: 'pipe',
673
+ });
674
+ // Try to build after installation
675
+ const newUiPackagePath = findUiPackage(targetDir);
676
+ if (newUiPackagePath && existsSync(newUiPackagePath)) {
677
+ log(' Building UI...', 'blue');
678
+ await execa('npm', ['run', 'build'], {
679
+ cwd: newUiPackagePath,
680
+ stdio: 'pipe',
681
+ });
682
+ log('✓ UI built successfully', 'green');
683
+ }
684
+ }
685
+ }
686
+ catch {
687
+ log('⚠️ Failed to build UI. UI will not be available.', 'yellow');
688
+ log(' You can build it manually: cd node_modules/@package-broker/ui && npm run build', 'yellow');
689
+ log(' Or install @package-broker/ui which includes pre-built assets.', 'yellow');
690
+ }
691
+ }
692
+ else {
693
+ log('✓ UI assets found', 'green');
694
+ }
695
+ // Deploy confirmation
696
+ log('\n🚀 Deployment\n', 'bright');
697
+ const deployResponse = await prompts({
698
+ type: 'confirm',
699
+ name: 'deploy',
700
+ message: 'Deploy to Cloudflare Workers now?',
701
+ initial: true,
702
+ });
703
+ if (deployResponse.deploy) {
704
+ // Apply migrations
705
+ log('\n📋 Applying database migrations...', 'blue');
706
+ try {
707
+ await applyMigrations(dbName, join(targetDir, 'migrations'), {
708
+ remote: true,
709
+ cwd: targetDir
710
+ });
711
+ log('✓ Migrations applied', 'green');
712
+ }
713
+ catch (error) {
714
+ log(`⚠️ Migration warning: ${error.message}`, 'yellow');
715
+ log(' You can apply migrations manually with:', 'yellow');
716
+ log(` npx wrangler d1 migrations apply ${dbName} --remote`, 'yellow');
717
+ }
718
+ // Deploy
719
+ log('\n🚀 Deploying Worker...', 'blue');
720
+ try {
721
+ const workerUrl = await deployWorker({
722
+ cwd: targetDir,
723
+ workerName: workerName
724
+ });
725
+ log(`✓ Deployed successfully!`, 'green');
726
+ log(`\n🌐 Worker URL: ${workerUrl}`, 'bright');
727
+ log(`\n💡 Note: If the route shows as "Inactive" in the Cloudflare dashboard,`, 'yellow');
728
+ log(` the Worker is still accessible. The status may take a moment to update.`, 'yellow');
729
+ }
730
+ catch (error) {
731
+ log(`✗ Deployment failed: ${error.message}`, 'red');
732
+ process.exit(1);
733
+ }
734
+ }
735
+ // Success message
736
+ log('\n✅ Setup complete!\n', 'bright');
737
+ log('Next steps:', 'blue');
738
+ log('1. Open your Worker URL in a browser', 'blue');
739
+ log('2. Complete the initial setup (email + password)', 'blue');
740
+ log('3. Create an access token in the dashboard', 'blue');
741
+ log('4. Start adding repository sources\n', 'blue');
742
+ if (!deployResponse.deploy) {
743
+ log('To deploy later, run:', 'yellow');
744
+ log(' npx wrangler deploy\n', 'yellow');
745
+ }
746
+ log('Documentation: https://package.broker/docs/', 'bright');
747
+ log('');
748
+ }
749
+ // ============================================================================
750
+ // Main Entry Point
751
+ // ============================================================================
752
+ async function main() {
753
+ const options = parseArgs(process.argv);
754
+ switch (options.command) {
755
+ case 'help':
756
+ showHelp();
757
+ break;
758
+ case 'deploy':
759
+ if (options.ci) {
760
+ await runCiDeploy(options);
761
+ }
762
+ else {
763
+ // Non-CI deploy - run interactive flow
764
+ await runInteractiveInit();
765
+ }
766
+ break;
767
+ case 'init':
768
+ default:
769
+ await runInteractiveInit();
770
+ break;
771
+ }
772
+ }
773
+ main().catch((error) => {
774
+ const isJsonMode = process.argv.includes('--json');
775
+ if (isJsonMode) {
776
+ outputJson({ error: error.message });
777
+ }
778
+ else {
779
+ log(`\n✗ Fatal error: ${error.message}`, 'red');
780
+ }
781
+ ghAnnotation('error', `Fatal error: ${error.message}`);
782
+ process.exit(1);
783
+ });
784
+ //# sourceMappingURL=index.js.map