@kernel.chat/kbot 2.23.1 → 2.24.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.
@@ -0,0 +1,824 @@
1
+ // K:BOT Deploy Tools — One-command deployment to Vercel, Netlify, Cloudflare, Fly.io, Railway
2
+ //
3
+ // Auto-detects the target platform from project config files and deploys.
4
+ // Also provides status, logs, rollback, and env management.
5
+ //
6
+ // Flow:
7
+ // deploy — auto-detect platform and ship
8
+ // deploy_status — check deployment status
9
+ // deploy_logs — fetch recent deploy logs
10
+ // deploy_rollback — rollback to previous deployment
11
+ // deploy_env — manage environment variables
12
+ import { execSync } from 'node:child_process';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { registerTool } from './index.js';
16
+ // ── Platform CLI mapping ─────────────────────────────────────────────
17
+ const PLATFORM_CLI = {
18
+ vercel: {
19
+ bin: 'vercel',
20
+ installCmd: 'npm i -g vercel',
21
+ checkArgs: ['--version'],
22
+ },
23
+ netlify: {
24
+ bin: 'netlify',
25
+ installCmd: 'npm i -g netlify-cli',
26
+ checkArgs: ['--version'],
27
+ },
28
+ cloudflare: {
29
+ bin: 'wrangler',
30
+ installCmd: 'npm i -g wrangler',
31
+ checkArgs: ['--version'],
32
+ },
33
+ fly: {
34
+ bin: 'flyctl',
35
+ installCmd: 'curl -L https://fly.io/install.sh | sh',
36
+ checkArgs: ['version'],
37
+ },
38
+ railway: {
39
+ bin: 'railway',
40
+ installCmd: 'npm i -g @railway/cli',
41
+ checkArgs: ['--version'],
42
+ },
43
+ };
44
+ // ── Helpers ──────────────────────────────────────────────────────────
45
+ function shell(command, cwd, timeout = 120_000) {
46
+ return execSync(command, {
47
+ encoding: 'utf-8',
48
+ cwd,
49
+ timeout,
50
+ maxBuffer: 10 * 1024 * 1024,
51
+ stdio: ['pipe', 'pipe', 'pipe'],
52
+ }).trim();
53
+ }
54
+ function shellSafe(command, cwd, timeout = 120_000) {
55
+ try {
56
+ const output = shell(command, cwd, timeout);
57
+ return { ok: true, output };
58
+ }
59
+ catch (err) {
60
+ const e = err;
61
+ const output = [e.stdout, e.stderr].filter(Boolean).join('\n').trim();
62
+ return { ok: false, output: output || e.message || 'Command failed' };
63
+ }
64
+ }
65
+ function isCLIInstalled(platform) {
66
+ const cfg = PLATFORM_CLI[platform];
67
+ try {
68
+ execSync(`${cfg.bin} ${cfg.checkArgs.join(' ')}`, {
69
+ encoding: 'utf-8',
70
+ timeout: 10_000,
71
+ stdio: ['pipe', 'pipe', 'pipe'],
72
+ });
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ function resolveCwd(userPath) {
80
+ if (userPath && typeof userPath === 'string' && existsSync(userPath))
81
+ return userPath;
82
+ return process.cwd();
83
+ }
84
+ /** Detect which deploy platform the project targets */
85
+ function detectDeployTarget(cwd) {
86
+ // Vercel
87
+ if (existsSync(join(cwd, 'vercel.json')) || existsSync(join(cwd, '.vercel'))) {
88
+ return { platform: 'vercel', confidence: 'config-file', detail: 'Found vercel.json or .vercel/' };
89
+ }
90
+ // Netlify
91
+ if (existsSync(join(cwd, 'netlify.toml')) || existsSync(join(cwd, '.netlify'))) {
92
+ return { platform: 'netlify', confidence: 'config-file', detail: 'Found netlify.toml or .netlify/' };
93
+ }
94
+ // Cloudflare Workers/Pages
95
+ if (existsSync(join(cwd, 'wrangler.toml')) || existsSync(join(cwd, 'wrangler.jsonc'))) {
96
+ return { platform: 'cloudflare', confidence: 'config-file', detail: 'Found wrangler.toml or wrangler.jsonc' };
97
+ }
98
+ // Fly.io
99
+ if (existsSync(join(cwd, 'fly.toml'))) {
100
+ return { platform: 'fly', confidence: 'config-file', detail: 'Found fly.toml' };
101
+ }
102
+ // Railway
103
+ if (existsSync(join(cwd, 'railway.json')) || existsSync(join(cwd, 'railway.toml'))) {
104
+ return { platform: 'railway', confidence: 'config-file', detail: 'Found railway.json or railway.toml' };
105
+ }
106
+ // Dockerfile → suggest Fly.io (most Docker-friendly PaaS)
107
+ if (existsSync(join(cwd, 'Dockerfile'))) {
108
+ return { platform: 'fly', confidence: 'inferred', detail: 'Found Dockerfile — Fly.io is recommended for container deploys' };
109
+ }
110
+ // package.json with build script → suggest Vercel (best for frontend)
111
+ const pkgPath = join(cwd, 'package.json');
112
+ if (existsSync(pkgPath)) {
113
+ try {
114
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
115
+ if (pkg.scripts?.build) {
116
+ return { platform: 'vercel', confidence: 'inferred', detail: 'Found package.json with build script — Vercel is recommended for JS/TS projects' };
117
+ }
118
+ }
119
+ catch { /* ignore parse errors */ }
120
+ }
121
+ return null;
122
+ }
123
+ /** Detect if a Cloudflare project is Workers or Pages */
124
+ function detectCloudflareType(cwd) {
125
+ const wranglerToml = join(cwd, 'wrangler.toml');
126
+ if (existsSync(wranglerToml)) {
127
+ try {
128
+ const content = readFileSync(wranglerToml, 'utf-8');
129
+ if (content.includes('pages_build_output_dir') || content.includes('[site]')) {
130
+ return 'pages';
131
+ }
132
+ }
133
+ catch { /* fall through */ }
134
+ }
135
+ // Check for common static output dirs
136
+ for (const dir of ['dist', 'build', 'out', 'public', '.next']) {
137
+ if (existsSync(join(cwd, dir))) {
138
+ return 'pages';
139
+ }
140
+ }
141
+ return 'workers';
142
+ }
143
+ /** Extract a URL from deploy command output */
144
+ function extractUrl(output) {
145
+ // Common patterns across platforms
146
+ const patterns = [
147
+ /https?:\/\/[^\s"'<>]+\.vercel\.app[^\s"'<>]*/i,
148
+ /https?:\/\/[^\s"'<>]+\.netlify\.app[^\s"'<>]*/i,
149
+ /https?:\/\/[^\s"'<>]+\.pages\.dev[^\s"'<>]*/i,
150
+ /https?:\/\/[^\s"'<>]+\.workers\.dev[^\s"'<>]*/i,
151
+ /https?:\/\/[^\s"'<>]+\.fly\.dev[^\s"'<>]*/i,
152
+ /https?:\/\/[^\s"'<>]+\.up\.railway\.app[^\s"'<>]*/i,
153
+ // Generic URL match as fallback
154
+ /(?:Production|Preview|Deployed to|Live at|URL|Website):\s*(https?:\/\/[^\s"'<>]+)/i,
155
+ /https?:\/\/[^\s"'<>]+/,
156
+ ];
157
+ for (const pattern of patterns) {
158
+ const match = output.match(pattern);
159
+ if (match)
160
+ return match[1] || match[0];
161
+ }
162
+ return '(URL not detected in output)';
163
+ }
164
+ // ── Platform deploy implementations ──────────────────────────────────
165
+ function deployVercel(cwd, prod) {
166
+ const startTime = Date.now();
167
+ const flags = prod ? '--yes --prod' : '--yes';
168
+ const { ok, output } = shellSafe(`vercel ${flags}`, cwd, 300_000);
169
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
170
+ if (!ok) {
171
+ return { platform: 'vercel', url: '', status: 'failed', duration: `${elapsed}s` };
172
+ }
173
+ return {
174
+ platform: 'vercel',
175
+ url: extractUrl(output),
176
+ status: 'success',
177
+ duration: `${elapsed}s`,
178
+ };
179
+ }
180
+ function deployNetlify(cwd, prod) {
181
+ const startTime = Date.now();
182
+ const flags = prod ? 'deploy --prod --dir=.' : 'deploy --dir=.';
183
+ // Netlify needs a build first if there's a build command
184
+ const pkgPath = join(cwd, 'package.json');
185
+ let deployDir = '.';
186
+ if (existsSync(pkgPath)) {
187
+ try {
188
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
189
+ if (pkg.scripts?.build) {
190
+ shellSafe('npm run build', cwd, 300_000);
191
+ // Detect output directory
192
+ for (const dir of ['dist', 'build', 'out', '.next', 'public']) {
193
+ if (existsSync(join(cwd, dir))) {
194
+ deployDir = dir;
195
+ break;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ catch { /* ignore */ }
201
+ }
202
+ // Also check netlify.toml for publish dir
203
+ const netlifyToml = join(cwd, 'netlify.toml');
204
+ if (existsSync(netlifyToml)) {
205
+ try {
206
+ const content = readFileSync(netlifyToml, 'utf-8');
207
+ const publishMatch = content.match(/publish\s*=\s*"?([^"\s]+)"?/);
208
+ if (publishMatch)
209
+ deployDir = publishMatch[1];
210
+ }
211
+ catch { /* ignore */ }
212
+ }
213
+ const deployFlags = prod ? `deploy --prod --dir=${deployDir}` : `deploy --dir=${deployDir}`;
214
+ const { ok, output } = shellSafe(`netlify ${deployFlags}`, cwd, 300_000);
215
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
216
+ if (!ok) {
217
+ return { platform: 'netlify', url: '', status: 'failed', duration: `${elapsed}s` };
218
+ }
219
+ return {
220
+ platform: 'netlify',
221
+ url: extractUrl(output),
222
+ status: 'success',
223
+ duration: `${elapsed}s`,
224
+ };
225
+ }
226
+ function deployCloudflare(cwd, _prod) {
227
+ const startTime = Date.now();
228
+ const cfType = detectCloudflareType(cwd);
229
+ let result;
230
+ if (cfType === 'pages') {
231
+ // Determine output dir from wrangler.toml or fallback
232
+ let outputDir = 'dist';
233
+ const wranglerToml = join(cwd, 'wrangler.toml');
234
+ if (existsSync(wranglerToml)) {
235
+ try {
236
+ const content = readFileSync(wranglerToml, 'utf-8');
237
+ const dirMatch = content.match(/pages_build_output_dir\s*=\s*"?([^"\s]+)"?/);
238
+ if (dirMatch)
239
+ outputDir = dirMatch[1];
240
+ }
241
+ catch { /* ignore */ }
242
+ }
243
+ // Build first if there's a build script
244
+ const pkgPath = join(cwd, 'package.json');
245
+ if (existsSync(pkgPath)) {
246
+ try {
247
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
248
+ if (pkg.scripts?.build) {
249
+ shellSafe('npm run build', cwd, 300_000);
250
+ }
251
+ }
252
+ catch { /* ignore */ }
253
+ }
254
+ result = shellSafe(`wrangler pages deploy ${outputDir}`, cwd, 300_000);
255
+ }
256
+ else {
257
+ result = shellSafe('wrangler deploy', cwd, 300_000);
258
+ }
259
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
260
+ if (!result.ok) {
261
+ return { platform: 'cloudflare', url: '', status: 'failed', duration: `${elapsed}s` };
262
+ }
263
+ return {
264
+ platform: 'cloudflare',
265
+ url: extractUrl(result.output),
266
+ status: 'success',
267
+ duration: `${elapsed}s`,
268
+ };
269
+ }
270
+ function deployFly(cwd, _prod) {
271
+ const startTime = Date.now();
272
+ const { ok, output } = shellSafe('flyctl deploy', cwd, 600_000);
273
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
274
+ if (!ok) {
275
+ return { platform: 'fly', url: '', status: 'failed', duration: `${elapsed}s` };
276
+ }
277
+ return {
278
+ platform: 'fly',
279
+ url: extractUrl(output),
280
+ status: 'success',
281
+ duration: `${elapsed}s`,
282
+ };
283
+ }
284
+ function deployRailway(cwd, _prod) {
285
+ const startTime = Date.now();
286
+ const { ok, output } = shellSafe('railway up', cwd, 600_000);
287
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
288
+ if (!ok) {
289
+ return { platform: 'railway', url: '', status: 'failed', duration: `${elapsed}s` };
290
+ }
291
+ return {
292
+ platform: 'railway',
293
+ url: extractUrl(output),
294
+ status: 'success',
295
+ duration: `${elapsed}s`,
296
+ };
297
+ }
298
+ const DEPLOYERS = {
299
+ vercel: deployVercel,
300
+ netlify: deployNetlify,
301
+ cloudflare: deployCloudflare,
302
+ fly: deployFly,
303
+ railway: deployRailway,
304
+ };
305
+ // ── Tool registration ────────────────────────────────────────────────
306
+ export function registerDeployTools() {
307
+ // ── deploy ─────────────────────────────────────────────────────────
308
+ registerTool({
309
+ name: 'deploy',
310
+ description: 'Deploy the current project. Auto-detects platform from config files (vercel.json, netlify.toml, wrangler.toml, fly.toml, railway.json). Supports Vercel, Netlify, Cloudflare Workers/Pages, Fly.io, and Railway.',
311
+ parameters: {
312
+ path: { type: 'string', description: 'Project directory (default: cwd)' },
313
+ platform: {
314
+ type: 'string',
315
+ description: 'Override auto-detection: vercel, netlify, cloudflare, fly, railway',
316
+ },
317
+ prod: {
318
+ type: 'boolean',
319
+ description: 'Deploy to production (default: true). Set false for preview/draft deploy.',
320
+ },
321
+ },
322
+ tier: 'pro',
323
+ timeout: 600_000,
324
+ async execute(args) {
325
+ const cwd = resolveCwd(args.path);
326
+ const prod = args.prod !== false;
327
+ const lines = [];
328
+ // Determine platform
329
+ let platform;
330
+ let detectionNote;
331
+ if (args.platform) {
332
+ const requested = String(args.platform).toLowerCase();
333
+ if (!PLATFORM_CLI[requested]) {
334
+ return `Error: Unknown platform "${args.platform}". Supported: vercel, netlify, cloudflare, fly, railway`;
335
+ }
336
+ platform = requested;
337
+ detectionNote = `Platform: ${platform} (user-specified)`;
338
+ }
339
+ else {
340
+ const detected = detectDeployTarget(cwd);
341
+ if (!detected) {
342
+ return [
343
+ 'Could not auto-detect deploy platform. No config files found.',
344
+ '',
345
+ 'Create one of these in your project root:',
346
+ ' - vercel.json → Vercel',
347
+ ' - netlify.toml → Netlify',
348
+ ' - wrangler.toml → Cloudflare Workers/Pages',
349
+ ' - fly.toml → Fly.io',
350
+ ' - railway.json → Railway',
351
+ '',
352
+ 'Or specify --platform explicitly: deploy --platform vercel',
353
+ ].join('\n');
354
+ }
355
+ platform = detected.platform;
356
+ detectionNote = `Platform: ${platform} (${detected.confidence} — ${detected.detail})`;
357
+ }
358
+ lines.push(detectionNote);
359
+ // Check CLI is installed
360
+ if (!isCLIInstalled(platform)) {
361
+ const cfg = PLATFORM_CLI[platform];
362
+ return [
363
+ detectionNote,
364
+ '',
365
+ `Error: ${cfg.bin} CLI is not installed.`,
366
+ '',
367
+ 'Install it with:',
368
+ ` ${cfg.installCmd}`,
369
+ '',
370
+ 'Then run this deploy command again.',
371
+ ].join('\n');
372
+ }
373
+ lines.push(`Mode: ${prod ? 'production' : 'preview'}`);
374
+ lines.push(`Directory: ${cwd}`);
375
+ lines.push('Deploying...');
376
+ lines.push('');
377
+ // Execute deploy
378
+ const deployer = DEPLOYERS[platform];
379
+ const result = deployer(cwd, prod);
380
+ if (result.status === 'failed') {
381
+ lines.push(`Deploy FAILED after ${result.duration}`);
382
+ lines.push('');
383
+ lines.push('Check the error output above. Common fixes:');
384
+ lines.push(' - Ensure you are logged in: run the CLI login command');
385
+ lines.push(' - Verify the project is linked to your account');
386
+ lines.push(' - Check build configuration in your config file');
387
+ return lines.join('\n');
388
+ }
389
+ lines.push(`Deploy SUCCESS in ${result.duration}`);
390
+ lines.push(`URL: ${result.url}`);
391
+ return lines.join('\n');
392
+ },
393
+ });
394
+ // ── deploy_status ──────────────────────────────────────────────────
395
+ registerTool({
396
+ name: 'deploy_status',
397
+ description: 'Check the current deployment status for a project. Shows the latest deployment state, URL, and any errors.',
398
+ parameters: {
399
+ path: { type: 'string', description: 'Project directory (default: cwd)' },
400
+ platform: {
401
+ type: 'string',
402
+ description: 'Override auto-detection: vercel, netlify, cloudflare, fly, railway',
403
+ },
404
+ },
405
+ tier: 'free',
406
+ async execute(args) {
407
+ const cwd = resolveCwd(args.path);
408
+ let platform;
409
+ if (args.platform) {
410
+ const requested = String(args.platform).toLowerCase();
411
+ if (!PLATFORM_CLI[requested]) {
412
+ return `Error: Unknown platform "${args.platform}". Supported: vercel, netlify, cloudflare, fly, railway`;
413
+ }
414
+ platform = requested;
415
+ }
416
+ else {
417
+ const detected = detectDeployTarget(cwd);
418
+ if (!detected)
419
+ return 'Could not auto-detect platform. Specify --platform.';
420
+ platform = detected.platform;
421
+ }
422
+ if (!isCLIInstalled(platform)) {
423
+ const cfg = PLATFORM_CLI[platform];
424
+ return `Error: ${cfg.bin} CLI not installed. Install with: ${cfg.installCmd}`;
425
+ }
426
+ let result;
427
+ switch (platform) {
428
+ case 'vercel':
429
+ result = shellSafe('vercel ls --limit 5', cwd, 30_000);
430
+ if (!result.ok) {
431
+ // Try inspect on the latest
432
+ result = shellSafe('vercel inspect', cwd, 30_000);
433
+ }
434
+ break;
435
+ case 'netlify':
436
+ result = shellSafe('netlify status', cwd, 30_000);
437
+ break;
438
+ case 'cloudflare':
439
+ result = shellSafe('wrangler deployments list', cwd, 30_000);
440
+ break;
441
+ case 'fly':
442
+ result = shellSafe('flyctl status', cwd, 30_000);
443
+ break;
444
+ case 'railway':
445
+ result = shellSafe('railway status', cwd, 30_000);
446
+ break;
447
+ default:
448
+ return `Unsupported platform: ${platform}`;
449
+ }
450
+ if (!result.ok) {
451
+ return `Error fetching status for ${platform}:\n${result.output}`;
452
+ }
453
+ return `**${platform} deployment status**\n\n${result.output}`;
454
+ },
455
+ });
456
+ // ── deploy_logs ────────────────────────────────────────────────────
457
+ registerTool({
458
+ name: 'deploy_logs',
459
+ description: 'Fetch recent deployment logs. Shows build output, runtime logs, or error details from the deploy platform.',
460
+ parameters: {
461
+ path: { type: 'string', description: 'Project directory (default: cwd)' },
462
+ platform: {
463
+ type: 'string',
464
+ description: 'Override auto-detection: vercel, netlify, cloudflare, fly, railway',
465
+ },
466
+ lines: {
467
+ type: 'number',
468
+ description: 'Number of log lines to fetch (default: 100)',
469
+ },
470
+ },
471
+ tier: 'free',
472
+ async execute(args) {
473
+ const cwd = resolveCwd(args.path);
474
+ const lineCount = typeof args.lines === 'number' ? args.lines : 100;
475
+ let platform;
476
+ if (args.platform) {
477
+ const requested = String(args.platform).toLowerCase();
478
+ if (!PLATFORM_CLI[requested]) {
479
+ return `Error: Unknown platform "${args.platform}". Supported: vercel, netlify, cloudflare, fly, railway`;
480
+ }
481
+ platform = requested;
482
+ }
483
+ else {
484
+ const detected = detectDeployTarget(cwd);
485
+ if (!detected)
486
+ return 'Could not auto-detect platform. Specify --platform.';
487
+ platform = detected.platform;
488
+ }
489
+ if (!isCLIInstalled(platform)) {
490
+ const cfg = PLATFORM_CLI[platform];
491
+ return `Error: ${cfg.bin} CLI not installed. Install with: ${cfg.installCmd}`;
492
+ }
493
+ let result;
494
+ switch (platform) {
495
+ case 'vercel':
496
+ // Vercel logs require a deployment URL — try to get the latest
497
+ result = shellSafe('vercel logs --limit 1', cwd, 30_000);
498
+ if (!result.ok) {
499
+ // Fallback: list deployments and grab the first URL
500
+ const listResult = shellSafe('vercel ls --limit 1', cwd, 15_000);
501
+ if (listResult.ok) {
502
+ const url = extractUrl(listResult.output);
503
+ if (url && url.startsWith('http')) {
504
+ result = shellSafe(`vercel logs ${url}`, cwd, 30_000);
505
+ }
506
+ }
507
+ }
508
+ break;
509
+ case 'netlify':
510
+ result = shellSafe('netlify deploy --open=false --json 2>&1 | head -50', cwd, 30_000);
511
+ // Netlify doesn't have a direct "logs" CLI — fetch via status
512
+ if (!result.ok) {
513
+ result = shellSafe('netlify status --verbose', cwd, 30_000);
514
+ }
515
+ break;
516
+ case 'cloudflare':
517
+ result = shellSafe('wrangler tail --format json --once 2>&1 || wrangler deployments list', cwd, 30_000);
518
+ break;
519
+ case 'fly':
520
+ result = shellSafe(`flyctl logs --no-tail -n ${lineCount}`, cwd, 30_000);
521
+ break;
522
+ case 'railway':
523
+ result = shellSafe(`railway logs --lines ${lineCount}`, cwd, 30_000);
524
+ break;
525
+ default:
526
+ return `Unsupported platform: ${platform}`;
527
+ }
528
+ if (!result.ok) {
529
+ return `Error fetching logs for ${platform}:\n${result.output}`;
530
+ }
531
+ return `**${platform} deploy logs** (last ${lineCount} lines)\n\n${result.output}`;
532
+ },
533
+ });
534
+ // ── deploy_rollback ────────────────────────────────────────────────
535
+ registerTool({
536
+ name: 'deploy_rollback',
537
+ description: 'Rollback to the previous deployment. Promotes the last known-good deployment to production.',
538
+ parameters: {
539
+ path: { type: 'string', description: 'Project directory (default: cwd)' },
540
+ platform: {
541
+ type: 'string',
542
+ description: 'Override auto-detection: vercel, netlify, cloudflare, fly, railway',
543
+ },
544
+ deployment_id: {
545
+ type: 'string',
546
+ description: 'Specific deployment ID/URL to rollback to (optional — defaults to previous)',
547
+ },
548
+ },
549
+ tier: 'pro',
550
+ timeout: 300_000,
551
+ async execute(args) {
552
+ const cwd = resolveCwd(args.path);
553
+ const deploymentId = args.deployment_id ? String(args.deployment_id) : '';
554
+ let platform;
555
+ if (args.platform) {
556
+ const requested = String(args.platform).toLowerCase();
557
+ if (!PLATFORM_CLI[requested]) {
558
+ return `Error: Unknown platform "${args.platform}". Supported: vercel, netlify, cloudflare, fly, railway`;
559
+ }
560
+ platform = requested;
561
+ }
562
+ else {
563
+ const detected = detectDeployTarget(cwd);
564
+ if (!detected)
565
+ return 'Could not auto-detect platform. Specify --platform.';
566
+ platform = detected.platform;
567
+ }
568
+ if (!isCLIInstalled(platform)) {
569
+ const cfg = PLATFORM_CLI[platform];
570
+ return `Error: ${cfg.bin} CLI not installed. Install with: ${cfg.installCmd}`;
571
+ }
572
+ let result;
573
+ switch (platform) {
574
+ case 'vercel': {
575
+ if (deploymentId) {
576
+ result = shellSafe(`vercel promote ${deploymentId}`, cwd, 120_000);
577
+ }
578
+ else {
579
+ // List deployments, grab the second one (previous), promote it
580
+ const listResult = shellSafe('vercel ls --limit 5', cwd, 15_000);
581
+ if (!listResult.ok) {
582
+ return `Error listing Vercel deployments:\n${listResult.output}`;
583
+ }
584
+ // Extract URLs from the list — the second production URL is the rollback target
585
+ const urls = listResult.output.match(/https?:\/\/[^\s]+\.vercel\.app/g);
586
+ if (!urls || urls.length < 2) {
587
+ return `Cannot rollback: need at least 2 deployments. Found ${urls?.length || 0}.\n\nDeployments:\n${listResult.output}`;
588
+ }
589
+ result = shellSafe(`vercel promote ${urls[1]}`, cwd, 120_000);
590
+ }
591
+ break;
592
+ }
593
+ case 'netlify': {
594
+ if (deploymentId) {
595
+ result = shellSafe(`netlify deploy --prod --deploy-id ${deploymentId}`, cwd, 120_000);
596
+ }
597
+ else {
598
+ // List deploys and rollback to previous
599
+ const listResult = shellSafe('netlify api listSiteDeploys --data \'{"site_id":"auto","per_page":5}\'', cwd, 15_000);
600
+ if (!listResult.ok) {
601
+ // Fallback: use netlify rollback if available
602
+ result = shellSafe('netlify rollback', cwd, 120_000);
603
+ }
604
+ else {
605
+ // Parse deploy IDs from the output
606
+ try {
607
+ const deploys = JSON.parse(listResult.output);
608
+ if (Array.isArray(deploys) && deploys.length >= 2) {
609
+ const prevId = deploys[1].id;
610
+ result = shellSafe(`netlify api restoreSiteDeploy --data '{"site_id":"auto","deploy_id":"${prevId}"}'`, cwd, 120_000);
611
+ }
612
+ else {
613
+ result = { ok: false, output: 'Need at least 2 deployments to rollback.' };
614
+ }
615
+ }
616
+ catch {
617
+ result = shellSafe('netlify rollback', cwd, 120_000);
618
+ }
619
+ }
620
+ }
621
+ break;
622
+ }
623
+ case 'cloudflare': {
624
+ if (deploymentId) {
625
+ result = shellSafe(`wrangler rollback ${deploymentId}`, cwd, 120_000);
626
+ }
627
+ else {
628
+ result = shellSafe('wrangler rollback', cwd, 120_000);
629
+ }
630
+ break;
631
+ }
632
+ case 'fly': {
633
+ // Fly uses release numbers
634
+ if (deploymentId) {
635
+ result = shellSafe(`flyctl releases rollback ${deploymentId}`, cwd, 120_000);
636
+ }
637
+ else {
638
+ // List releases and rollback to previous
639
+ const listResult = shellSafe('flyctl releases --json', cwd, 15_000);
640
+ if (!listResult.ok) {
641
+ result = { ok: false, output: `Error listing releases:\n${listResult.output}` };
642
+ }
643
+ else {
644
+ try {
645
+ const releases = JSON.parse(listResult.output);
646
+ if (Array.isArray(releases) && releases.length >= 2) {
647
+ const prevVersion = releases[1].Version || releases[1].version;
648
+ result = shellSafe(`flyctl releases rollback ${prevVersion}`, cwd, 120_000);
649
+ }
650
+ else {
651
+ result = { ok: false, output: 'Need at least 2 releases to rollback.' };
652
+ }
653
+ }
654
+ catch {
655
+ // Fallback: rollback without specifying version (Fly picks previous)
656
+ result = shellSafe('flyctl releases rollback', cwd, 120_000);
657
+ }
658
+ }
659
+ }
660
+ break;
661
+ }
662
+ case 'railway': {
663
+ if (deploymentId) {
664
+ result = shellSafe(`railway rollback ${deploymentId}`, cwd, 120_000);
665
+ }
666
+ else {
667
+ result = shellSafe('railway rollback', cwd, 120_000);
668
+ }
669
+ break;
670
+ }
671
+ default:
672
+ return `Unsupported platform: ${platform}`;
673
+ }
674
+ if (!result.ok) {
675
+ return `Rollback FAILED on ${platform}:\n${result.output}`;
676
+ }
677
+ return `**Rollback SUCCESS on ${platform}**\n\n${result.output}`;
678
+ },
679
+ });
680
+ // ── deploy_env ─────────────────────────────────────────────────────
681
+ registerTool({
682
+ name: 'deploy_env',
683
+ description: 'Manage environment variables on the deploy platform. List, set, or remove env vars for production/preview environments.',
684
+ parameters: {
685
+ path: { type: 'string', description: 'Project directory (default: cwd)' },
686
+ platform: {
687
+ type: 'string',
688
+ description: 'Override auto-detection: vercel, netlify, cloudflare, fly, railway',
689
+ },
690
+ action: {
691
+ type: 'string',
692
+ description: 'Action: list, set, remove (default: list)',
693
+ required: true,
694
+ },
695
+ key: { type: 'string', description: 'Environment variable name (required for set/remove)' },
696
+ value: { type: 'string', description: 'Environment variable value (required for set)' },
697
+ environment: {
698
+ type: 'string',
699
+ description: 'Target environment: production, preview, development (default: production)',
700
+ },
701
+ },
702
+ tier: 'pro',
703
+ async execute(args) {
704
+ const cwd = resolveCwd(args.path);
705
+ const action = String(args.action || 'list').toLowerCase();
706
+ const key = args.key ? String(args.key) : '';
707
+ const value = args.value ? String(args.value) : '';
708
+ const environment = String(args.environment || 'production').toLowerCase();
709
+ if ((action === 'set' || action === 'remove') && !key) {
710
+ return `Error: --key is required for ${action} action.`;
711
+ }
712
+ if (action === 'set' && !value) {
713
+ return 'Error: --value is required for set action.';
714
+ }
715
+ let platform;
716
+ if (args.platform) {
717
+ const requested = String(args.platform).toLowerCase();
718
+ if (!PLATFORM_CLI[requested]) {
719
+ return `Error: Unknown platform "${args.platform}". Supported: vercel, netlify, cloudflare, fly, railway`;
720
+ }
721
+ platform = requested;
722
+ }
723
+ else {
724
+ const detected = detectDeployTarget(cwd);
725
+ if (!detected)
726
+ return 'Could not auto-detect platform. Specify --platform.';
727
+ platform = detected.platform;
728
+ }
729
+ if (!isCLIInstalled(platform)) {
730
+ const cfg = PLATFORM_CLI[platform];
731
+ return `Error: ${cfg.bin} CLI not installed. Install with: ${cfg.installCmd}`;
732
+ }
733
+ let result;
734
+ switch (platform) {
735
+ case 'vercel': {
736
+ const envFlag = environment === 'preview' ? 'preview' : environment === 'development' ? 'development' : 'production';
737
+ if (action === 'list') {
738
+ result = shellSafe(`vercel env ls ${envFlag}`, cwd, 15_000);
739
+ }
740
+ else if (action === 'set') {
741
+ // Vercel env add reads value from stdin
742
+ result = shellSafe(`echo "${value}" | vercel env add ${key} ${envFlag}`, cwd, 15_000);
743
+ }
744
+ else if (action === 'remove') {
745
+ result = shellSafe(`vercel env rm ${key} ${envFlag} --yes`, cwd, 15_000);
746
+ }
747
+ else {
748
+ return `Unknown action "${action}". Use: list, set, remove`;
749
+ }
750
+ break;
751
+ }
752
+ case 'netlify': {
753
+ if (action === 'list') {
754
+ result = shellSafe('netlify env:list', cwd, 15_000);
755
+ }
756
+ else if (action === 'set') {
757
+ result = shellSafe(`netlify env:set ${key} "${value}"`, cwd, 15_000);
758
+ }
759
+ else if (action === 'remove') {
760
+ result = shellSafe(`netlify env:unset ${key}`, cwd, 15_000);
761
+ }
762
+ else {
763
+ return `Unknown action "${action}". Use: list, set, remove`;
764
+ }
765
+ break;
766
+ }
767
+ case 'cloudflare': {
768
+ if (action === 'list') {
769
+ result = shellSafe('wrangler secret list', cwd, 15_000);
770
+ }
771
+ else if (action === 'set') {
772
+ // Wrangler secret put reads from stdin
773
+ result = shellSafe(`echo "${value}" | wrangler secret put ${key}`, cwd, 15_000);
774
+ }
775
+ else if (action === 'remove') {
776
+ result = shellSafe(`wrangler secret delete ${key} --force`, cwd, 15_000);
777
+ }
778
+ else {
779
+ return `Unknown action "${action}". Use: list, set, remove`;
780
+ }
781
+ break;
782
+ }
783
+ case 'fly': {
784
+ if (action === 'list') {
785
+ result = shellSafe('flyctl secrets list', cwd, 15_000);
786
+ }
787
+ else if (action === 'set') {
788
+ result = shellSafe(`flyctl secrets set ${key}="${value}"`, cwd, 30_000);
789
+ }
790
+ else if (action === 'remove') {
791
+ result = shellSafe(`flyctl secrets unset ${key}`, cwd, 30_000);
792
+ }
793
+ else {
794
+ return `Unknown action "${action}". Use: list, set, remove`;
795
+ }
796
+ break;
797
+ }
798
+ case 'railway': {
799
+ if (action === 'list') {
800
+ result = shellSafe('railway variables', cwd, 15_000);
801
+ }
802
+ else if (action === 'set') {
803
+ result = shellSafe(`railway variables set ${key}="${value}"`, cwd, 15_000);
804
+ }
805
+ else if (action === 'remove') {
806
+ result = shellSafe(`railway variables delete ${key}`, cwd, 15_000);
807
+ }
808
+ else {
809
+ return `Unknown action "${action}". Use: list, set, remove`;
810
+ }
811
+ break;
812
+ }
813
+ default:
814
+ return `Unsupported platform: ${platform}`;
815
+ }
816
+ if (!result.ok) {
817
+ return `Error managing env vars on ${platform}:\n${result.output}`;
818
+ }
819
+ const actionPast = action === 'list' ? 'listed' : action === 'set' ? 'set' : 'removed';
820
+ return `**${platform} env ${actionPast}** (${environment})\n\n${result.output}`;
821
+ },
822
+ });
823
+ }
824
+ //# sourceMappingURL=deploy.js.map