@mks2508/coolify-mks-cli-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli/index.js +11788 -0
  2. package/dist/coolify/config.d.ts +64 -0
  3. package/dist/coolify/config.d.ts.map +1 -0
  4. package/dist/coolify/index.d.ts +201 -0
  5. package/dist/coolify/index.d.ts.map +1 -0
  6. package/dist/coolify/types.d.ts +282 -0
  7. package/dist/coolify/types.d.ts.map +1 -0
  8. package/dist/index.cjs +29150 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.ts +14 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +29127 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/server/sse.d.ts +11 -0
  15. package/dist/server/sse.d.ts.map +1 -0
  16. package/dist/server/sse.js +32 -0
  17. package/dist/server/stdio.d.ts +13 -0
  18. package/dist/server/stdio.d.ts.map +1 -0
  19. package/dist/server/stdio.js +18326 -0
  20. package/dist/tools/definitions.d.ts +13 -0
  21. package/dist/tools/definitions.d.ts.map +1 -0
  22. package/dist/tools/handlers.d.ts +19 -0
  23. package/dist/tools/handlers.d.ts.map +1 -0
  24. package/dist/utils/format.d.ts +38 -0
  25. package/dist/utils/format.d.ts.map +1 -0
  26. package/package.json +67 -0
  27. package/src/cli/commands/config.ts +83 -0
  28. package/src/cli/commands/deploy.ts +56 -0
  29. package/src/cli/commands/env.ts +60 -0
  30. package/src/cli/commands/list.ts +63 -0
  31. package/src/cli/commands/logs.ts +49 -0
  32. package/src/cli/commands/servers.ts +52 -0
  33. package/src/cli/index.ts +81 -0
  34. package/src/coolify/config.ts +113 -0
  35. package/src/coolify/index.ts +688 -0
  36. package/src/coolify/types.ts +297 -0
  37. package/src/index.ts +864 -0
  38. package/src/server/sse.ts +50 -0
  39. package/src/server/stdio.ts +52 -0
  40. package/src/tools/definitions.ts +435 -0
  41. package/src/tools/handlers.ts +605 -0
  42. package/src/utils/format.ts +104 -0
@@ -0,0 +1,605 @@
1
+ /**
2
+ * MCP Tool handlers for Coolify.
3
+ *
4
+ * Handles execution of all 21 Coolify MCP tools.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
10
+ import { isOk, isErr } from '@mks2508/no-throw'
11
+ import type { CoolifyService } from '../coolify/index.js'
12
+
13
+ /**
14
+ * Handler function for tool calls.
15
+ *
16
+ * @param name - Tool name
17
+ * @param args - Tool arguments
18
+ * @param coolify - CoolifyService instance
19
+ * @returns Tool call result
20
+ */
21
+ export async function handleToolCall(
22
+ name: string,
23
+ args: Record<string, unknown>,
24
+ coolify: CoolifyService
25
+ ): Promise<CallToolResult> {
26
+ const initResult = await coolify.init()
27
+ if (isErr(initResult)) {
28
+ return {
29
+ content: [{ type: 'text', text: `Coolify not configured: ${initResult.error.message}` }],
30
+ isError: true
31
+ }
32
+ }
33
+
34
+ switch (name) {
35
+ case 'deploy':
36
+ return handleDeploy(coolify, args as unknown as DeployArgs)
37
+ case 'get_env_vars':
38
+ return handleGetEnvVars(coolify, args as unknown as GetEnvVarsArgs)
39
+ case 'set_env_vars':
40
+ return handleSetEnvVars(coolify, args as unknown as SetEnvVarsArgs)
41
+ case 'get_deployment_status':
42
+ return handleGetDeploymentStatus(coolify, args as unknown as GetDeploymentStatusArgs)
43
+ case 'list_applications':
44
+ return handleListApplications(coolify, args as unknown as ListApplicationsArgs)
45
+ case 'delete_application':
46
+ return handleDeleteApplication(coolify, args as unknown as DeleteApplicationArgs)
47
+ case 'get_application_logs':
48
+ return handleGetApplicationLogs(coolify, args as unknown as GetApplicationLogsArgs)
49
+ case 'start_application':
50
+ return handleStartApplication(coolify, args as unknown as StartApplicationArgs)
51
+ case 'stop_application':
52
+ return handleStopApplication(coolify, args as unknown as StopApplicationArgs)
53
+ case 'restart_application':
54
+ return handleRestartApplication(coolify, args as unknown as RestartApplicationArgs)
55
+ case 'get_deployment_history':
56
+ return handleGetDeploymentHistory(coolify, args as unknown as GetDeploymentHistoryArgs)
57
+ case 'update_application':
58
+ return handleUpdateApplication(coolify, args as unknown as UpdateApplicationArgs)
59
+ case 'list_servers':
60
+ return handleListServers(coolify)
61
+ case 'get_server':
62
+ return handleGetServer(coolify, args as unknown as GetServerArgs)
63
+ case 'list_projects':
64
+ return handleListProjects(coolify)
65
+ case 'list_teams':
66
+ return handleListTeams(coolify)
67
+ case 'get_server_destinations':
68
+ return handleGetServerDestinations(coolify, args as unknown as GetServerDestinationsArgs)
69
+ case 'create_application':
70
+ return handleCreateApplication(coolify, args as unknown as CreateApplicationArgs)
71
+ case 'get_resource_usage':
72
+ return handleGetResourceUsage(coolify, args as unknown as GetResourceUsageArgs)
73
+ case 'health_check':
74
+ return handleHealthCheck(coolify)
75
+ case 'get_application_details':
76
+ return handleGetApplicationDetails(coolify, args as unknown as GetApplicationDetailsArgs)
77
+ default:
78
+ return {
79
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
80
+ isError: true
81
+ }
82
+ }
83
+ }
84
+
85
+ // Tool argument types
86
+ interface DeployArgs {
87
+ uuid: string
88
+ force?: boolean
89
+ tag?: string
90
+ }
91
+
92
+ interface GetEnvVarsArgs {
93
+ uuid: string
94
+ }
95
+
96
+ interface SetEnvVarsArgs {
97
+ uuid: string
98
+ envVars: Record<string, string>
99
+ }
100
+
101
+ interface GetDeploymentStatusArgs {
102
+ uuid: string
103
+ }
104
+
105
+ interface ListApplicationsArgs {
106
+ teamId?: string
107
+ projectId?: string
108
+ }
109
+
110
+ interface DeleteApplicationArgs {
111
+ uuid: string
112
+ }
113
+
114
+ interface GetApplicationLogsArgs {
115
+ uuid: string
116
+ tail?: number
117
+ }
118
+
119
+ interface StartApplicationArgs {
120
+ uuid: string
121
+ }
122
+
123
+ interface StopApplicationArgs {
124
+ uuid: string
125
+ }
126
+
127
+ interface RestartApplicationArgs {
128
+ uuid: string
129
+ }
130
+
131
+ interface GetDeploymentHistoryArgs {
132
+ uuid: string
133
+ }
134
+
135
+ interface UpdateApplicationArgs {
136
+ uuid: string
137
+ name?: string
138
+ description?: string
139
+ buildPack?: 'dockerfile' | 'nixpacks' | 'static'
140
+ gitBranch?: string
141
+ portsExposes?: string
142
+ installCommand?: string
143
+ buildCommand?: string
144
+ startCommand?: string
145
+ }
146
+
147
+ interface GetServerArgs {
148
+ serverUuid: string
149
+ }
150
+
151
+ interface GetServerDestinationsArgs {
152
+ serverUuid: string
153
+ }
154
+
155
+ interface CreateApplicationArgs {
156
+ name: string
157
+ serverUuid: string
158
+ destinationUuid: string
159
+ githubRepoUrl: string
160
+ description?: string
161
+ branch?: string
162
+ buildPack?: 'dockerfile' | 'nixpacks' | 'static'
163
+ }
164
+
165
+ interface GetResourceUsageArgs {
166
+ uuid: string
167
+ }
168
+
169
+ interface GetApplicationDetailsArgs {
170
+ uuid: string
171
+ }
172
+
173
+ // Handler implementations
174
+ async function handleDeploy(coolify: CoolifyService, args: DeployArgs): Promise<CallToolResult> {
175
+ const result = await coolify.deploy({
176
+ uuid: args.uuid,
177
+ force: args.force ?? false,
178
+ tag: args.tag
179
+ })
180
+
181
+ if (isOk(result)) {
182
+ return {
183
+ content: [{
184
+ type: 'text',
185
+ text: JSON.stringify({
186
+ success: true,
187
+ deploymentUuid: result.value.deploymentUuid,
188
+ resourceUuid: result.value.resourceUuid,
189
+ message: 'Deployment started'
190
+ }, null, 2)
191
+ }]
192
+ }
193
+ }
194
+
195
+ return {
196
+ content: [{ type: 'text', text: `Deployment failed: ${result.error.message}` }],
197
+ isError: true
198
+ }
199
+ }
200
+
201
+ async function handleGetEnvVars(coolify: CoolifyService, args: GetEnvVarsArgs): Promise<CallToolResult> {
202
+ const result = await coolify.getEnvironmentVariables(args.uuid)
203
+
204
+ if (isOk(result)) {
205
+ const runtimeVars = result.value.filter(ev => ev.is_runtime)
206
+ const buildtimeVars = result.value.filter(ev => ev.is_buildtime)
207
+ return {
208
+ content: [{
209
+ type: 'text',
210
+ text: JSON.stringify({
211
+ success: true,
212
+ total: result.value.length,
213
+ runtimeCount: runtimeVars.length,
214
+ buildtimeCount: buildtimeVars.length,
215
+ runtime: runtimeVars,
216
+ buildtime: buildtimeVars
217
+ }, null, 2)
218
+ }]
219
+ }
220
+ }
221
+
222
+ return {
223
+ content: [{ type: 'text', text: `Failed to get env vars: ${result.error.message}` }],
224
+ isError: true
225
+ }
226
+ }
227
+
228
+ async function handleSetEnvVars(coolify: CoolifyService, args: SetEnvVarsArgs): Promise<CallToolResult> {
229
+ const result = await coolify.setEnvironmentVariables(args.uuid, args.envVars)
230
+
231
+ if (isOk(result)) {
232
+ return {
233
+ content: [{
234
+ type: 'text',
235
+ text: `Set ${Object.keys(args.envVars).length} environment variable(s). Use deploy tool to apply changes.`
236
+ }]
237
+ }
238
+ }
239
+
240
+ return {
241
+ content: [{ type: 'text', text: `Failed to set env vars: ${result.error.message}` }],
242
+ isError: true
243
+ }
244
+ }
245
+
246
+ async function handleGetDeploymentStatus(coolify: CoolifyService, args: GetDeploymentStatusArgs): Promise<CallToolResult> {
247
+ const result = await coolify.getApplicationStatus(args.uuid)
248
+
249
+ if (isOk(result)) {
250
+ return {
251
+ content: [{ type: 'text', text: JSON.stringify({ status: result.value }, null, 2) }]
252
+ }
253
+ }
254
+
255
+ return {
256
+ content: [{ type: 'text', text: `Failed to get status: ${result.error.message}` }],
257
+ isError: true
258
+ }
259
+ }
260
+
261
+ async function handleListApplications(coolify: CoolifyService, args: ListApplicationsArgs): Promise<CallToolResult> {
262
+ const result = await coolify.listApplications(args.teamId, args.projectId)
263
+
264
+ if (isOk(result)) {
265
+ return {
266
+ content: [{
267
+ type: 'text',
268
+ text: JSON.stringify({ success: true, count: result.value.length, applications: result.value }, null, 2)
269
+ }]
270
+ }
271
+ }
272
+
273
+ return {
274
+ content: [{ type: 'text', text: `Failed to list applications: ${result.error.message}` }],
275
+ isError: true
276
+ }
277
+ }
278
+
279
+ async function handleDeleteApplication(coolify: CoolifyService, args: DeleteApplicationArgs): Promise<CallToolResult> {
280
+ const result = await coolify.deleteApplication(args.uuid)
281
+
282
+ if (isOk(result)) {
283
+ return {
284
+ content: [{
285
+ type: 'text',
286
+ text: JSON.stringify({ success: true, message: `Application ${args.uuid} deleted successfully` }, null, 2)
287
+ }]
288
+ }
289
+ }
290
+
291
+ return {
292
+ content: [{ type: 'text', text: `Failed to delete application: ${result.error.message}` }],
293
+ isError: true
294
+ }
295
+ }
296
+
297
+ async function handleGetApplicationLogs(coolify: CoolifyService, args: GetApplicationLogsArgs): Promise<CallToolResult> {
298
+ const result = await coolify.getApplicationLogs(args.uuid, { tail: args.tail })
299
+
300
+ if (isOk(result)) {
301
+ return {
302
+ content: [{
303
+ type: 'text',
304
+ text: JSON.stringify({
305
+ success: true,
306
+ timestamp: result.value.timestamp,
307
+ logCount: result.value.logs.length,
308
+ logs: result.value.logs
309
+ }, null, 2)
310
+ }]
311
+ }
312
+ }
313
+
314
+ return {
315
+ content: [{ type: 'text', text: `Failed to get logs: ${result.error.message}` }],
316
+ isError: true
317
+ }
318
+ }
319
+
320
+ async function handleStartApplication(coolify: CoolifyService, args: StartApplicationArgs): Promise<CallToolResult> {
321
+ const result = await coolify.startApplication(args.uuid)
322
+
323
+ if (isOk(result)) {
324
+ return {
325
+ content: [{
326
+ type: 'text',
327
+ text: JSON.stringify({ success: true, message: `Application ${args.uuid} started`, application: result.value }, null, 2)
328
+ }]
329
+ }
330
+ }
331
+
332
+ return {
333
+ content: [{ type: 'text', text: `Failed to start application: ${result.error.message}` }],
334
+ isError: true
335
+ }
336
+ }
337
+
338
+ async function handleStopApplication(coolify: CoolifyService, args: StopApplicationArgs): Promise<CallToolResult> {
339
+ const result = await coolify.stopApplication(args.uuid)
340
+
341
+ if (isOk(result)) {
342
+ return {
343
+ content: [{
344
+ type: 'text',
345
+ text: JSON.stringify({ success: true, message: `Application ${args.uuid} stopped`, application: result.value }, null, 2)
346
+ }]
347
+ }
348
+ }
349
+
350
+ return {
351
+ content: [{ type: 'text', text: `Failed to stop application: ${result.error.message}` }],
352
+ isError: true
353
+ }
354
+ }
355
+
356
+ async function handleRestartApplication(coolify: CoolifyService, args: RestartApplicationArgs): Promise<CallToolResult> {
357
+ const result = await coolify.restartApplication(args.uuid)
358
+
359
+ if (isOk(result)) {
360
+ return {
361
+ content: [{
362
+ type: 'text',
363
+ text: JSON.stringify({ success: true, message: `Application ${args.uuid} restarted`, application: result.value }, null, 2)
364
+ }]
365
+ }
366
+ }
367
+
368
+ return {
369
+ content: [{ type: 'text', text: `Failed to restart application: ${result.error.message}` }],
370
+ isError: true
371
+ }
372
+ }
373
+
374
+ async function handleGetDeploymentHistory(coolify: CoolifyService, args: GetDeploymentHistoryArgs): Promise<CallToolResult> {
375
+ const result = await coolify.getApplicationDeploymentHistory(args.uuid)
376
+
377
+ if (isOk(result)) {
378
+ return {
379
+ content: [{
380
+ type: 'text',
381
+ text: JSON.stringify({ success: true, count: result.value.length, deployments: result.value }, null, 2)
382
+ }]
383
+ }
384
+ }
385
+
386
+ return {
387
+ content: [{ type: 'text', text: `Failed to get deployment history: ${result.error.message}` }],
388
+ isError: true
389
+ }
390
+ }
391
+
392
+ async function handleUpdateApplication(coolify: CoolifyService, args: UpdateApplicationArgs): Promise<CallToolResult> {
393
+ const result = await coolify.updateApplication(args.uuid, {
394
+ name: args.name,
395
+ description: args.description,
396
+ buildPack: args.buildPack,
397
+ gitBranch: args.gitBranch,
398
+ portsExposes: args.portsExposes,
399
+ installCommand: args.installCommand,
400
+ buildCommand: args.buildCommand,
401
+ startCommand: args.startCommand
402
+ })
403
+
404
+ if (isOk(result)) {
405
+ return {
406
+ content: [{
407
+ type: 'text',
408
+ text: JSON.stringify({
409
+ success: true,
410
+ message: `Application ${args.uuid} updated. Use deploy tool to apply changes.`,
411
+ application: result.value
412
+ }, null, 2)
413
+ }]
414
+ }
415
+ }
416
+
417
+ return {
418
+ content: [{ type: 'text', text: `Failed to update application: ${result.error.message}` }],
419
+ isError: true
420
+ }
421
+ }
422
+
423
+ async function handleListServers(coolify: CoolifyService): Promise<CallToolResult> {
424
+ const result = await coolify.listServers()
425
+
426
+ if (isOk(result)) {
427
+ return {
428
+ content: [{
429
+ type: 'text',
430
+ text: JSON.stringify({ success: true, count: result.value.length, servers: result.value }, null, 2)
431
+ }]
432
+ }
433
+ }
434
+
435
+ return {
436
+ content: [{ type: 'text', text: `Failed to list servers: ${result.error.message}` }],
437
+ isError: true
438
+ }
439
+ }
440
+
441
+ async function handleGetServer(coolify: CoolifyService, args: GetServerArgs): Promise<CallToolResult> {
442
+ const result = await coolify.getServer(args.serverUuid)
443
+
444
+ if (isOk(result)) {
445
+ return {
446
+ content: [{ type: 'text', text: JSON.stringify({ success: true, server: result.value }, null, 2) }]
447
+ }
448
+ }
449
+
450
+ return {
451
+ content: [{ type: 'text', text: `Failed to get server: ${result.error.message}` }],
452
+ isError: true
453
+ }
454
+ }
455
+
456
+ async function handleListProjects(coolify: CoolifyService): Promise<CallToolResult> {
457
+ const result = await coolify.listProjects()
458
+
459
+ if (isOk(result)) {
460
+ return {
461
+ content: [{
462
+ type: 'text',
463
+ text: JSON.stringify({ success: true, count: result.value.length, projects: result.value }, null, 2)
464
+ }]
465
+ }
466
+ }
467
+
468
+ return {
469
+ content: [{ type: 'text', text: `Failed to list projects: ${result.error.message}` }],
470
+ isError: true
471
+ }
472
+ }
473
+
474
+ async function handleListTeams(coolify: CoolifyService): Promise<CallToolResult> {
475
+ const result = await coolify.listTeams()
476
+
477
+ if (isOk(result)) {
478
+ return {
479
+ content: [{
480
+ type: 'text',
481
+ text: JSON.stringify({ success: true, count: result.value.length, teams: result.value }, null, 2)
482
+ }]
483
+ }
484
+ }
485
+
486
+ return {
487
+ content: [{ type: 'text', text: `Failed to list teams: ${result.error.message}` }],
488
+ isError: true
489
+ }
490
+ }
491
+
492
+ async function handleGetServerDestinations(coolify: CoolifyService, args: GetServerDestinationsArgs): Promise<CallToolResult> {
493
+ const result = await coolify.getServerDestinations(args.serverUuid)
494
+
495
+ if (isOk(result)) {
496
+ return {
497
+ content: [{
498
+ type: 'text',
499
+ text: JSON.stringify({
500
+ success: true,
501
+ serverUuid: args.serverUuid,
502
+ count: result.value.length,
503
+ destinations: result.value
504
+ }, null, 2)
505
+ }]
506
+ }
507
+ }
508
+
509
+ return {
510
+ content: [{ type: 'text', text: `Failed to get destinations: ${result.error.message}` }],
511
+ isError: true
512
+ }
513
+ }
514
+
515
+ async function handleCreateApplication(coolify: CoolifyService, args: CreateApplicationArgs): Promise<CallToolResult> {
516
+ const result = await coolify.createApplication({
517
+ name: args.name,
518
+ description: args.description,
519
+ serverUuid: args.serverUuid,
520
+ destinationUuid: args.destinationUuid,
521
+ githubRepoUrl: args.githubRepoUrl,
522
+ branch: args.branch,
523
+ buildPack: args.buildPack
524
+ })
525
+
526
+ if (isOk(result)) {
527
+ return {
528
+ content: [{
529
+ type: 'text',
530
+ text: JSON.stringify({
531
+ success: true,
532
+ message: `Application "${args.name}" created successfully`,
533
+ uuid: result.value.uuid,
534
+ nextSteps: [
535
+ 'Use set_env_vars to configure environment variables',
536
+ 'Use deploy to start the first deployment'
537
+ ]
538
+ }, null, 2)
539
+ }]
540
+ }
541
+ }
542
+
543
+ return {
544
+ content: [{ type: 'text', text: `Failed to create application: ${result.error.message}` }],
545
+ isError: true
546
+ }
547
+ }
548
+
549
+ async function handleGetResourceUsage(coolify: CoolifyService, args: GetResourceUsageArgs): Promise<CallToolResult> {
550
+ // For now, use getApplicationStatus - Coolify API doesn't have a separate resource usage endpoint
551
+ const result = await coolify.getApplicationStatus(args.uuid)
552
+
553
+ if (isOk(result)) {
554
+ return {
555
+ content: [{ type: 'text', text: JSON.stringify({ success: true, status: result.value }, null, 2) }]
556
+ }
557
+ }
558
+
559
+ return {
560
+ content: [{ type: 'text', text: `Failed to get resource usage: ${result.error.message}` }],
561
+ isError: true
562
+ }
563
+ }
564
+
565
+ async function handleHealthCheck(coolify: CoolifyService): Promise<CallToolResult> {
566
+ // Try to list servers as a health check
567
+ const result = await coolify.listServers()
568
+
569
+ if (isOk(result)) {
570
+ return {
571
+ content: [{
572
+ type: 'text',
573
+ text: JSON.stringify({ success: true, status: 'healthy', message: 'Coolify API is accessible' }, null, 2)
574
+ }]
575
+ }
576
+ }
577
+
578
+ return {
579
+ content: [{ type: 'text', text: `Health check failed: ${result.error.message}` }],
580
+ isError: true
581
+ }
582
+ }
583
+
584
+ async function handleGetApplicationDetails(coolify: CoolifyService, args: GetApplicationDetailsArgs): Promise<CallToolResult> {
585
+ // List all applications and filter by UUID to get full details
586
+ const result = await coolify.listApplications()
587
+
588
+ if (isOk(result)) {
589
+ const app = result.value.find((a) => a.uuid === args.uuid)
590
+ if (app) {
591
+ return {
592
+ content: [{ type: 'text', text: JSON.stringify({ success: true, application: app }, null, 2) }]
593
+ }
594
+ }
595
+ return {
596
+ content: [{ type: 'text', text: `Application ${args.uuid} not found` }],
597
+ isError: true
598
+ }
599
+ }
600
+
601
+ return {
602
+ content: [{ type: 'text', text: `Failed to get application details: ${result.error.message}` }],
603
+ isError: true
604
+ }
605
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Formatting utilities for CLI output.
3
+ *
4
+ * Provides table formatting and color helpers.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import Table from 'cli-table3'
10
+ import chalk from 'chalk'
11
+
12
+ /**
13
+ * Gets the terminal width.
14
+ *
15
+ * @returns Terminal column count
16
+ */
17
+ function getTerminalWidth(): number {
18
+ return process.stdout.columns || 80
19
+ }
20
+
21
+ /**
22
+ * Creates a formatted table with headers.
23
+ *
24
+ * @param headers - Column headers
25
+ * @param colWidths - Optional column widths
26
+ * @returns Configured Table instance
27
+ */
28
+ export function createTable(headers: string[], colWidths?: number[]) {
29
+ const columns = getTerminalWidth()
30
+
31
+ return new Table({
32
+ head: headers.map((h) => chalk.cyan(h)),
33
+ colWidths: colWidths || calculateColumnWidths(columns, headers.length),
34
+ })
35
+ }
36
+
37
+ /**
38
+ * Calculates column widths based on terminal width.
39
+ *
40
+ * @param totalWidth - Total terminal width
41
+ * @param numColumns - Number of columns
42
+ * @returns Array of column widths
43
+ */
44
+ function calculateColumnWidths(totalWidth: number, numColumns: number): number[] {
45
+ const padding = 4
46
+ const availableWidth = totalWidth - padding * numColumns
47
+ const colWidth = Math.floor(availableWidth / numColumns)
48
+ return Array(numColumns).fill(colWidth)
49
+ }
50
+
51
+ /**
52
+ * Formats application status with colored indicator.
53
+ *
54
+ * @param status - Application status string
55
+ * @returns Formatted status string
56
+ */
57
+ export function formatStatus(status: string): string {
58
+ const statusMap: Record<string, string> = {
59
+ running: chalk.green('● Running'),
60
+ 'running:unknown': chalk.green('● Running'),
61
+ stopped: chalk.red('○ Stopped'),
62
+ 'restarting': chalk.yellow('◐ Restarting'),
63
+ error: chalk.red('✗ Error'),
64
+ deploying: chalk.yellow('◐ Deploying'),
65
+ }
66
+
67
+ return statusMap[status] || status
68
+ }
69
+
70
+ /**
71
+ * Formats bytes to human-readable format.
72
+ *
73
+ * @param bytes - Number of bytes
74
+ * @returns Formatted string with unit
75
+ */
76
+ export function formatBytes(bytes: number): string {
77
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
78
+ let i = 0
79
+ while (bytes >= 1024 && i < units.length - 1) {
80
+ bytes /= 1024
81
+ i++
82
+ }
83
+ return `${bytes.toFixed(1)} ${units[i]}`
84
+ }
85
+
86
+ /**
87
+ * Formats timestamp to relative time.
88
+ *
89
+ * @param timestamp - ISO timestamp string
90
+ * @returns Relative time string
91
+ */
92
+ export function formatRelativeTime(timestamp: string): string {
93
+ const date = new Date(timestamp)
94
+ const now = new Date()
95
+ const diffMs = now.getTime() - date.getTime()
96
+ const diffMins = Math.floor(diffMs / 60000)
97
+ const diffHours = Math.floor(diffMs / 3600000)
98
+ const diffDays = Math.floor(diffMs / 86400000)
99
+
100
+ if (diffMins < 1) return 'just now'
101
+ if (diffMins < 60) return `${diffMins}m ago`
102
+ if (diffHours < 24) return `${diffHours}h ago`
103
+ return `${diffDays}d ago`
104
+ }