@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/README.md +95 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +784 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.d.ts +14 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +80 -0
- package/dist/paths.js.map +1 -0
- package/dist/template.d.ts +18 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +114 -0
- package/dist/template.js.map +1 -0
- package/dist/wrangler-config.d.ts +120 -0
- package/dist/wrangler-config.d.ts.map +1 -0
- package/dist/wrangler-config.js +295 -0
- package/dist/wrangler-config.js.map +1 -0
- package/dist/wrangler.d.ts +90 -0
- package/dist/wrangler.d.ts.map +1 -0
- package/dist/wrangler.js +470 -0
- package/dist/wrangler.js.map +1 -0
- package/package.json +42 -0
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
|