@mks2508/coolify-mks-cli-mcp 0.6.3 → 0.9.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 (75) hide show
  1. package/dist/cli/coolify-state.d.ts +101 -5
  2. package/dist/cli/coolify-state.d.ts.map +1 -1
  3. package/dist/cli/index.js +23165 -11543
  4. package/dist/cli/ui/highlighter.d.ts +28 -0
  5. package/dist/cli/ui/highlighter.d.ts.map +1 -0
  6. package/dist/cli/ui/index.d.ts +9 -0
  7. package/dist/cli/ui/index.d.ts.map +1 -0
  8. package/dist/cli/ui/spinners.d.ts +100 -0
  9. package/dist/cli/ui/spinners.d.ts.map +1 -0
  10. package/dist/cli/ui/tables.d.ts +103 -0
  11. package/dist/cli/ui/tables.d.ts.map +1 -0
  12. package/dist/coolify/config.d.ts +25 -0
  13. package/dist/coolify/config.d.ts.map +1 -1
  14. package/dist/coolify/index.d.ts +139 -12
  15. package/dist/coolify/index.d.ts.map +1 -1
  16. package/dist/coolify/types.d.ts +160 -2
  17. package/dist/coolify/types.d.ts.map +1 -1
  18. package/dist/examples/demo-ui.d.ts +8 -0
  19. package/dist/examples/demo-ui.d.ts.map +1 -0
  20. package/dist/index.cjs +2580 -230
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.js +2598 -226
  23. package/dist/index.js.map +1 -1
  24. package/dist/sdk.d.ts +96 -7
  25. package/dist/sdk.d.ts.map +1 -1
  26. package/dist/server/stdio.js +475 -73
  27. package/dist/tools/definitions.d.ts.map +1 -1
  28. package/dist/tools/handlers.d.ts.map +1 -1
  29. package/dist/utils/env-parser.d.ts +24 -0
  30. package/dist/utils/env-parser.d.ts.map +1 -0
  31. package/dist/utils/format.d.ts +32 -0
  32. package/dist/utils/format.d.ts.map +1 -1
  33. package/package.json +17 -4
  34. package/src/cli/actions.ts +9 -2
  35. package/src/cli/commands/create.ts +332 -24
  36. package/src/cli/commands/db.ts +37 -0
  37. package/src/cli/commands/delete.ts +6 -2
  38. package/src/cli/commands/deploy.ts +347 -49
  39. package/src/cli/commands/deployments.ts +6 -2
  40. package/src/cli/commands/diagnose.ts +3 -3
  41. package/src/cli/commands/env.ts +424 -31
  42. package/src/cli/commands/exec.ts +6 -2
  43. package/src/cli/commands/init.ts +991 -0
  44. package/src/cli/commands/logs.ts +224 -24
  45. package/src/cli/commands/main-menu.ts +21 -0
  46. package/src/cli/commands/projects.ts +312 -29
  47. package/src/cli/commands/restart.ts +6 -2
  48. package/src/cli/commands/service-logs.ts +14 -0
  49. package/src/cli/commands/show.ts +45 -12
  50. package/src/cli/commands/start.ts +6 -2
  51. package/src/cli/commands/status.ts +554 -0
  52. package/src/cli/commands/stop.ts +6 -2
  53. package/src/cli/commands/svc.ts +7 -1
  54. package/src/cli/commands/update.ts +79 -2
  55. package/src/cli/commands/volumes.ts +293 -0
  56. package/src/cli/coolify-state.ts +203 -12
  57. package/src/cli/index.ts +138 -11
  58. package/src/cli/name-resolver.ts +228 -0
  59. package/src/cli/ui/banner.ts +276 -0
  60. package/src/cli/ui/highlighter.ts +176 -0
  61. package/src/cli/ui/index.ts +9 -0
  62. package/src/cli/ui/prompts.ts +155 -0
  63. package/src/cli/ui/screen.ts +630 -0
  64. package/src/cli/ui/select.ts +280 -0
  65. package/src/cli/ui/spinners.ts +256 -0
  66. package/src/cli/ui/tables.ts +407 -0
  67. package/src/coolify/config.ts +75 -0
  68. package/src/coolify/index.ts +565 -101
  69. package/src/coolify/types.ts +165 -2
  70. package/src/examples/demo-ui.ts +78 -0
  71. package/src/sdk.ts +211 -1
  72. package/src/tools/definitions.ts +22 -0
  73. package/src/tools/handlers.ts +19 -0
  74. package/src/utils/env-parser.ts +45 -0
  75. package/src/utils/format.ts +178 -0
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Table formatting utilities for CLI output.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import boxen from "boxen";
8
+ import chalk from "chalk";
9
+ import Table from "cli-table3";
10
+
11
+ export interface ITableColumn {
12
+ /** Header text */
13
+ header: string;
14
+ /** Width in characters (optional, auto-calculated if not provided) */
15
+ width?: number;
16
+ /** Alignment: 'left' | 'center' | 'right' */
17
+ align?: "left" | "center" | "right";
18
+ /** Color function to apply to all values in this column */
19
+ color?: (text: string | string[], ...args: unknown[]) => string;
20
+ }
21
+
22
+ export interface ITableOptions {
23
+ /** Table title */
24
+ title?: string;
25
+ /** Column definitions */
26
+ columns: ITableColumn[];
27
+ /** Row data */
28
+ rows: Array<Record<string, unknown>>;
29
+ /** Show borders */
30
+ borders?: boolean;
31
+ /** Compact mode (less padding) */
32
+ compact?: boolean;
33
+ }
34
+
35
+ /**
36
+ * Create and render a formatted table.
37
+ */
38
+ export function createTable(options: ITableOptions): string {
39
+ const { title, columns, rows, borders = true, compact = false } = options;
40
+
41
+ const tableConfig = {
42
+ chars: borders
43
+ ? {
44
+ top: "─",
45
+ "top-mid": "┬",
46
+ "top-left": "╭",
47
+ "top-right": "╮",
48
+ bottom: "─",
49
+ "bottom-mid": "┴",
50
+ "bottom-left": "╰",
51
+ "bottom-right": "╯",
52
+ left: "│",
53
+ "left-mid": "├",
54
+ mid: "─",
55
+ "mid-mid": "┼",
56
+ right: "│",
57
+ "right-mid": "┤",
58
+ middle: "│",
59
+ }
60
+ : {
61
+ top: "",
62
+ "top-mid": "",
63
+ "top-left": "",
64
+ "top-right": "",
65
+ bottom: "",
66
+ "bottom-mid": "",
67
+ "bottom-left": "",
68
+ "bottom-right": "",
69
+ left: "",
70
+ "left-mid": "",
71
+ mid: "",
72
+ "mid-mid": "",
73
+ right: "",
74
+ "right-mid": "",
75
+ middle: " ",
76
+ },
77
+ style: {
78
+ head: [],
79
+ border: compact ? [] : ["gray"],
80
+ compact,
81
+ },
82
+ };
83
+
84
+ const table = new Table({
85
+ ...tableConfig,
86
+ head: columns.map((col) => chalk.bold(col.header)),
87
+ colWidths: columns.map((col) => col.width).filter((w): w is number => w !== undefined),
88
+ colAligns: columns.map((col) => col.align ?? "left"),
89
+ });
90
+
91
+ // Add rows
92
+ for (const row of rows) {
93
+ const coloredRow: string[] = [];
94
+ for (const col of columns) {
95
+ let value = String(row[col.header] ?? "");
96
+ if (col.color) {
97
+ value = col.color([value])[0];
98
+ }
99
+ coloredRow.push(value);
100
+ }
101
+ table.push(coloredRow);
102
+ }
103
+
104
+ let output = table.toString();
105
+
106
+ if (title) {
107
+ const titleLine = chalk.bold.cyan(`\n${title}`);
108
+ const separator = "─".repeat(
109
+ Math.max(title.length - 1, output.split("\n")[0]?.length ?? 0),
110
+ );
111
+ output = `${titleLine}\n${chalk.gray(separator)}\n${output}`;
112
+ }
113
+
114
+ return output;
115
+ }
116
+
117
+ /**
118
+ * Create a table for environment variables.
119
+ */
120
+ export function createEnvTable(
121
+ envVars: Array<{
122
+ key: string;
123
+ value: string;
124
+ is_runtime?: boolean;
125
+ is_buildtime?: boolean;
126
+ is_required?: boolean;
127
+ }>,
128
+ options?: { compact?: boolean; showType?: boolean },
129
+ ): string {
130
+ const columns: ITableColumn[] = [
131
+ { header: "Key", align: "left" },
132
+ { header: "Value", align: "left" },
133
+ ];
134
+
135
+ if (options?.showType) {
136
+ columns.push({ header: "Type", align: "center" });
137
+ }
138
+
139
+ const rows = envVars.map((env) => {
140
+ const row: Record<string, string> = {
141
+ Key: env.key,
142
+ Value: truncateValue(env.value, 50),
143
+ };
144
+
145
+ if (options?.showType) {
146
+ const types = [];
147
+ if (env.is_runtime) types.push("Runtime");
148
+ if (env.is_buildtime) types.push("Build");
149
+ if (env.is_required) types.push(chalk.red("*"));
150
+ row.Type = types.join(" ") || chalk.gray("—");
151
+ }
152
+
153
+ return row;
154
+ });
155
+
156
+ return createTable({
157
+ title: chalk.bold("Environment Variables"),
158
+ columns,
159
+ rows,
160
+ compact: options?.compact ?? true,
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Truncate a value to a maximum length with ellipsis.
166
+ */
167
+ function truncateValue(value: string, maxLength: number): string {
168
+ if (value.length <= maxLength) {
169
+ return chalk.gray(value);
170
+ }
171
+ return chalk.gray(`${value.slice(0, maxLength - 3)}...`);
172
+ }
173
+
174
+ /**
175
+ * Create a summary card with key-value pairs using boxen.
176
+ */
177
+ export function createSummaryCard(
178
+ title: string,
179
+ data: Record<
180
+ string,
181
+ { value: string; label?: string; color?: (text: string) => string }
182
+ >,
183
+ ): string {
184
+ const lines: string[] = [];
185
+
186
+ for (const [key, item] of Object.entries(data)) {
187
+ const label = item.label ?? key;
188
+ const color = item.color ?? chalk.white;
189
+ lines.push(`${chalk.gray(label)}: ${color(item.value)}`);
190
+ }
191
+
192
+ return boxen(lines.join("\n"), {
193
+ title: chalk.bold.cyan(title),
194
+ titleAlignment: "left",
195
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
196
+ borderStyle: "round",
197
+ borderColor: "gray",
198
+ width: Math.min(60, process.stdout.columns || 80),
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Create a change summary for env sync operations using boxen.
204
+ */
205
+ export function createChangeSummary(changes: {
206
+ added: Array<{ key: string; value: string }>;
207
+ updated: Array<{ key: string; value: string; oldValue: string }>;
208
+ removed: string[];
209
+ }): string {
210
+ const lines: string[] = [];
211
+
212
+ if (changes.added.length > 0) {
213
+ lines.push(`${chalk.green("+")} Add ${changes.added.length} new`);
214
+ for (const { key, value } of changes.added.slice(0, 5)) {
215
+ lines.push(` ${chalk.green(key)} = ${chalk.gray(truncateValue(value, 40))}`);
216
+ }
217
+ if (changes.added.length > 5) {
218
+ lines.push(chalk.gray(` ... and ${changes.added.length - 5} more`));
219
+ }
220
+ lines.push("");
221
+ }
222
+
223
+ if (changes.updated.length > 0) {
224
+ lines.push(`${chalk.yellow("~")} Update ${changes.updated.length}`);
225
+ for (const { key, oldValue } of changes.updated.slice(0, 5)) {
226
+ lines.push(` ${chalk.yellow(key)}: ${chalk.gray(stripe(oldValue))} -> ${chalk.green("new")}`);
227
+ }
228
+ if (changes.updated.length > 5) {
229
+ lines.push(chalk.gray(` ... and ${changes.updated.length - 5} more`));
230
+ }
231
+ lines.push("");
232
+ }
233
+
234
+ if (changes.removed.length > 0) {
235
+ lines.push(`${chalk.red("-")} Remove ${changes.removed.length}`);
236
+ for (const key of changes.removed.slice(0, 5)) {
237
+ lines.push(` ${chalk.red(key)}`);
238
+ }
239
+ if (changes.removed.length > 5) {
240
+ lines.push(chalk.gray(` ... and ${changes.removed.length - 5} more`));
241
+ }
242
+ }
243
+
244
+ return boxen(lines.join("\n"), {
245
+ title: chalk.bold.cyan("Changes to apply"),
246
+ titleAlignment: "left",
247
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
248
+ borderStyle: "round",
249
+ borderColor: "yellow",
250
+ width: Math.min(60, process.stdout.columns || 80),
251
+ });
252
+ }
253
+
254
+ function stripe(text: string): string {
255
+ return text.length > 20 ? `${text.slice(0, 17)}...` : text;
256
+ }
257
+
258
+ /**
259
+ * Truncate a name, handling repo-style names (user/repo:branch-uuid).
260
+ */
261
+ function truncateName(name: string, maxLen: number): string {
262
+ // Strip repo URL prefix patterns like "m-k-s2508/repo:branch-uuid"
263
+ if (name.includes("/") && name.includes(":")) {
264
+ const parts = name.split("/");
265
+ const last = parts[parts.length - 1];
266
+ const repoName = last.split(":")[0];
267
+ name = repoName;
268
+ }
269
+ if (name.length > maxLen) {
270
+ return name.slice(0, maxLen - 1) + "\u2026";
271
+ }
272
+ return name;
273
+ }
274
+
275
+ /**
276
+ * Format status with colors.
277
+ */
278
+ export function formatStatus(status: string): string {
279
+ if (status.includes("healthy")) return chalk.green(status);
280
+ if (status === "running") return chalk.yellow(status);
281
+ if (status === "exited") return chalk.red(status);
282
+ if (status === "deploying") return chalk.blue(status);
283
+ return status;
284
+ }
285
+
286
+ /**
287
+ * Show status dashboard from infrastructure tree.
288
+ */
289
+ export function showStatusDashboard(
290
+ data: {
291
+ server: { name: string; ip?: string };
292
+ projects: Array<{
293
+ name: string;
294
+ uuid: string;
295
+ environments: Array<{
296
+ name: string;
297
+ resources: Array<{ name: string; kind: string; status: string; fqdn?: string | null }>;
298
+ }>;
299
+ }>;
300
+ counts: {
301
+ apps: number;
302
+ databases: number;
303
+ services: number;
304
+ healthy: number;
305
+ running: number;
306
+ stopped: number;
307
+ unhealthy: number;
308
+ };
309
+ },
310
+ highlightProjectUuid?: string,
311
+ ): void {
312
+ const termWidth = Math.min(76, (process.stdout.columns || 80) - 4);
313
+ const serverLabel = data.server.ip
314
+ ? `${data.server.name} (${data.server.ip})`
315
+ : data.server.name;
316
+
317
+ const c = data.counts;
318
+
319
+ // Summary line
320
+ const lines: string[] = [
321
+ `${chalk.cyan("Apps")} ${c.apps} ${chalk.cyan("DBs")} ${c.databases} ${chalk.cyan("Svcs")} ${c.services} ${chalk.green("●")} ${c.healthy} healthy ${chalk.yellow("○")} ${c.running} running ${chalk.red("✗")} ${c.stopped} stopped`,
322
+ "",
323
+ ];
324
+
325
+ // Project tree
326
+ for (let pi = 0; pi < data.projects.length; pi++) {
327
+ const project = data.projects[pi];
328
+ const isLastProject = pi === data.projects.length - 1;
329
+ const projectPrefix = isLastProject ? "└─" : "├─";
330
+
331
+ const isCurrent = project.uuid === highlightProjectUuid;
332
+ const projectLabel = isCurrent
333
+ ? `${chalk.bold.cyan(project.name)} ${chalk.cyan("←")}`
334
+ : chalk.bold(project.name);
335
+ lines.push(`${chalk.gray(projectPrefix)} ${projectLabel}`);
336
+
337
+ for (let ei = 0; ei < project.environments.length; ei++) {
338
+ const env = project.environments[ei];
339
+ const isLastEnv = ei === project.environments.length - 1;
340
+ const envBranch = isLastProject ? " " : "│ ";
341
+ const envPrefix = isLastEnv ? "└─" : "├─";
342
+
343
+ lines.push(`${chalk.gray(envBranch + envPrefix)} ${chalk.gray(env.name)}`);
344
+
345
+ for (let ri = 0; ri < env.resources.length; ri++) {
346
+ const res = env.resources[ri];
347
+ const isLastRes = ri === env.resources.length - 1;
348
+ const resBranch = envBranch + (isLastEnv ? " " : "│ ");
349
+ const resPrefix = isLastRes ? "└─" : "├─";
350
+
351
+ const kindIcon = res.kind === "database" ? chalk.blue("[db]")
352
+ : res.kind === "service" ? chalk.magenta("[svc]")
353
+ : "";
354
+ const statusIcon = formatStatusIcon(res.status);
355
+ const name = truncateName(res.name, 20);
356
+ // Calculate available space for domain after prefix + icon + name
357
+ const prefixLen = (resBranch + resPrefix).length + 3 + (kindIcon ? 6 : 0) + Math.min(name.length, 20);
358
+ const maxDomainLen = Math.max(0, termWidth - prefixLen - 6);
359
+ const domain = res.fqdn && maxDomainLen > 10
360
+ ? chalk.gray(` ${truncateName(stripProtocol(pickFirstDomain(res.fqdn)), maxDomainLen)}`)
361
+ : "";
362
+
363
+ lines.push(
364
+ `${chalk.gray(resBranch + resPrefix)} ${statusIcon} ${kindIcon}${kindIcon ? " " : ""}${name}${domain}`,
365
+ );
366
+ }
367
+ }
368
+ }
369
+
370
+ console.log(
371
+ boxen(lines.join("\n"), {
372
+ title: `${chalk.bold("Coolify")} ${chalk.gray("—")} ${chalk.cyan(serverLabel)}`,
373
+ titleAlignment: "left",
374
+ padding: { left: 1, right: 1, top: 1, bottom: 1 },
375
+ borderStyle: "round",
376
+ borderColor: "cyan",
377
+ width: termWidth,
378
+ }),
379
+ );
380
+ }
381
+
382
+ /**
383
+ * Format a status into a colored icon.
384
+ */
385
+ function formatStatusIcon(status: string): string {
386
+ if (status.includes("healthy") && !status.includes("unhealthy"))
387
+ return chalk.green("●");
388
+ if (status.includes("unhealthy")) return chalk.red("●");
389
+ if (status.startsWith("running")) return chalk.yellow("○");
390
+ if (status.includes("exited")) return chalk.red("✗");
391
+ return chalk.gray("○");
392
+ }
393
+
394
+ /**
395
+ * Strip protocol from domain for compact display.
396
+ */
397
+ function stripProtocol(url: string): string {
398
+ return url.replace(/^https?:\/\//, "");
399
+ }
400
+
401
+ /**
402
+ * Pick the first domain from a comma-separated FQDN string.
403
+ */
404
+ function pickFirstDomain(fqdn: string): string {
405
+ const first = fqdn.split(",")[0].trim();
406
+ return first;
407
+ }
@@ -112,4 +112,79 @@ export async function getCoolifyToken(): Promise<string | undefined> {
112
112
  return process.env.COOLIFY_TOKEN;
113
113
  }
114
114
 
115
+ // ─── App Settings Cache ──────────────────────────────────────────────────────
116
+ // Coolify API GET does not return application_settings (is_auto_deploy_enabled,
117
+ // is_force_https_enabled, etc). We cache these locally when the user sets them
118
+ // via our CLI so we can display them later in `show`.
119
+
120
+ const SETTINGS_CACHE_FILE = join(CONFIG_DIR, "app-settings-cache.json");
121
+
122
+ /**
123
+ * Cached application settings that the Coolify API doesn't expose in GET responses.
124
+ */
125
+ export interface ICachedAppSettings {
126
+ /** Auto-deploy on git push */
127
+ isAutoDeployEnabled?: boolean;
128
+ /** Watch paths for selective deploy */
129
+ watchPaths?: string | null;
130
+ /** When this cache entry was last updated */
131
+ cachedAt?: string;
132
+ }
133
+
134
+ /**
135
+ * Reads the full settings cache from disk.
136
+ *
137
+ * @returns Map of appUuid → cached settings
138
+ */
139
+ async function readSettingsCache(): Promise<Record<string, ICachedAppSettings>> {
140
+ try {
141
+ if (existsSync(SETTINGS_CACHE_FILE)) {
142
+ const content = await readFile(SETTINGS_CACHE_FILE, "utf-8");
143
+ return JSON.parse(content);
144
+ }
145
+ } catch {
146
+ // Corrupted file — start fresh
147
+ }
148
+ return {};
149
+ }
150
+
151
+ /**
152
+ * Caches application settings locally after a successful PATCH.
153
+ *
154
+ * @param appUuid - Application UUID
155
+ * @param settings - Settings to cache (merged with existing)
156
+ */
157
+ export async function cacheAppSettings(
158
+ appUuid: string,
159
+ settings: Partial<ICachedAppSettings>,
160
+ ): Promise<void> {
161
+ try {
162
+ const cache = await readSettingsCache();
163
+ cache[appUuid] = {
164
+ ...cache[appUuid],
165
+ ...settings,
166
+ cachedAt: new Date().toISOString(),
167
+ };
168
+ if (!existsSync(CONFIG_DIR)) {
169
+ await mkdir(CONFIG_DIR, { recursive: true });
170
+ }
171
+ await writeFile(SETTINGS_CACHE_FILE, JSON.stringify(cache, null, 2));
172
+ } catch {
173
+ // Non-critical — silently ignore cache write failures
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Reads cached settings for an application.
179
+ *
180
+ * @param appUuid - Application UUID
181
+ * @returns Cached settings or null if not cached
182
+ */
183
+ export async function getCachedAppSettings(
184
+ appUuid: string,
185
+ ): Promise<ICachedAppSettings | null> {
186
+ const cache = await readSettingsCache();
187
+ return cache[appUuid] || null;
188
+ }
189
+
115
190
  export { CONFIG_DIR, CONFIG_FILE };