@mks2508/coolify-mks-cli-mcp 0.6.2 → 0.8.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.
- package/dist/cli/coolify-state.d.ts +92 -4
- package/dist/cli/coolify-state.d.ts.map +1 -1
- package/dist/cli/index.js +22228 -11529
- package/dist/cli/ui/highlighter.d.ts +28 -0
- package/dist/cli/ui/highlighter.d.ts.map +1 -0
- package/dist/cli/ui/index.d.ts +9 -0
- package/dist/cli/ui/index.d.ts.map +1 -0
- package/dist/cli/ui/spinners.d.ts +100 -0
- package/dist/cli/ui/spinners.d.ts.map +1 -0
- package/dist/cli/ui/tables.d.ts +103 -0
- package/dist/cli/ui/tables.d.ts.map +1 -0
- package/dist/coolify/index.d.ts +22 -3
- package/dist/coolify/index.d.ts.map +1 -1
- package/dist/coolify/types.d.ts +99 -1
- package/dist/coolify/types.d.ts.map +1 -1
- package/dist/examples/demo-ui.d.ts +8 -0
- package/dist/examples/demo-ui.d.ts.map +1 -0
- package/dist/index.cjs +322 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +322 -12
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +41 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/server/stdio.js +258 -9
- package/package.json +19 -4
- package/src/cli/actions.ts +9 -2
- package/src/cli/commands/create.ts +71 -5
- package/src/cli/commands/db.ts +37 -0
- package/src/cli/commands/delete.ts +6 -2
- package/src/cli/commands/deploy.ts +347 -49
- package/src/cli/commands/deployments.ts +6 -2
- package/src/cli/commands/diagnose.ts +3 -3
- package/src/cli/commands/env.ts +121 -22
- package/src/cli/commands/exec.ts +6 -2
- package/src/cli/commands/init.ts +937 -0
- package/src/cli/commands/logs.ts +224 -24
- package/src/cli/commands/main-menu.ts +21 -0
- package/src/cli/commands/projects.ts +312 -29
- package/src/cli/commands/restart.ts +6 -2
- package/src/cli/commands/service-logs.ts +14 -0
- package/src/cli/commands/show.ts +6 -2
- package/src/cli/commands/start.ts +6 -2
- package/src/cli/commands/status.ts +538 -0
- package/src/cli/commands/stop.ts +6 -2
- package/src/cli/commands/update.ts +27 -2
- package/src/cli/coolify-state.ts +164 -11
- package/src/cli/index.ts +91 -10
- package/src/cli/name-resolver.ts +228 -0
- package/src/cli/ui/banner.ts +276 -0
- package/src/cli/ui/highlighter.ts +176 -0
- package/src/cli/ui/index.ts +9 -0
- package/src/cli/ui/prompts.ts +155 -0
- package/src/cli/ui/screen.ts +606 -0
- package/src/cli/ui/select.ts +280 -0
- package/src/cli/ui/spinners.ts +256 -0
- package/src/cli/ui/tables.ts +407 -0
- package/src/coolify/index.ts +257 -12
- package/src/coolify/types.ts +103 -1
- package/src/examples/demo-ui.ts +78 -0
- package/src/sdk.ts +162 -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
|
+
}
|