@mks2508/coolify-mks-cli-mcp 0.6.3 → 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.
Files changed (60) hide show
  1. package/dist/cli/coolify-state.d.ts +92 -4
  2. package/dist/cli/coolify-state.d.ts.map +1 -1
  3. package/dist/cli/index.js +22149 -11456
  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/index.d.ts +22 -3
  13. package/dist/coolify/index.d.ts.map +1 -1
  14. package/dist/coolify/types.d.ts +99 -1
  15. package/dist/coolify/types.d.ts.map +1 -1
  16. package/dist/examples/demo-ui.d.ts +8 -0
  17. package/dist/examples/demo-ui.d.ts.map +1 -0
  18. package/dist/index.cjs +322 -12
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.js +322 -12
  21. package/dist/index.js.map +1 -1
  22. package/dist/sdk.d.ts +41 -0
  23. package/dist/sdk.d.ts.map +1 -1
  24. package/dist/server/stdio.js +258 -9
  25. package/package.json +16 -4
  26. package/src/cli/actions.ts +9 -2
  27. package/src/cli/commands/create.ts +71 -5
  28. package/src/cli/commands/db.ts +37 -0
  29. package/src/cli/commands/delete.ts +6 -2
  30. package/src/cli/commands/deploy.ts +347 -49
  31. package/src/cli/commands/deployments.ts +6 -2
  32. package/src/cli/commands/diagnose.ts +3 -3
  33. package/src/cli/commands/env.ts +121 -22
  34. package/src/cli/commands/exec.ts +6 -2
  35. package/src/cli/commands/init.ts +937 -0
  36. package/src/cli/commands/logs.ts +224 -24
  37. package/src/cli/commands/main-menu.ts +21 -0
  38. package/src/cli/commands/projects.ts +312 -29
  39. package/src/cli/commands/restart.ts +6 -2
  40. package/src/cli/commands/service-logs.ts +14 -0
  41. package/src/cli/commands/show.ts +6 -2
  42. package/src/cli/commands/start.ts +6 -2
  43. package/src/cli/commands/status.ts +538 -0
  44. package/src/cli/commands/stop.ts +6 -2
  45. package/src/cli/commands/update.ts +27 -2
  46. package/src/cli/coolify-state.ts +164 -11
  47. package/src/cli/index.ts +91 -10
  48. package/src/cli/name-resolver.ts +228 -0
  49. package/src/cli/ui/banner.ts +276 -0
  50. package/src/cli/ui/highlighter.ts +176 -0
  51. package/src/cli/ui/index.ts +9 -0
  52. package/src/cli/ui/prompts.ts +155 -0
  53. package/src/cli/ui/screen.ts +606 -0
  54. package/src/cli/ui/select.ts +280 -0
  55. package/src/cli/ui/spinners.ts +256 -0
  56. package/src/cli/ui/tables.ts +407 -0
  57. package/src/coolify/index.ts +257 -12
  58. package/src/coolify/types.ts +103 -1
  59. package/src/examples/demo-ui.ts +78 -0
  60. package/src/sdk.ts +162 -0
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Custom ANSI selectors with live preview support.
3
+ * The tree IS the selector — arrow keys navigate, right panel updates live.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import chalk from "chalk";
9
+
10
+ export interface ISelectOption {
11
+ label: string;
12
+ value: string;
13
+ hint?: string;
14
+ }
15
+
16
+ // ─── Theme ───────────────────────────────────────────────────────────────────
17
+
18
+ const T = {
19
+ accent: "#a875ff", // brighter purple for bullets
20
+ selectedBg: "#2d1f5e", // visible bg even on dark terminals
21
+ selectedFg: "#ffffff", // white text on selected
22
+ normal: "#cccccc", // normal unselected labels (high contrast)
23
+ dim: "#888888", // dimmed unselected labels
24
+ hint: "#999999", // hints next to labels
25
+ sep: "#444444", // separator lines
26
+ };
27
+
28
+ // ─── Dual-panel select (tree left + preview right) ───────────────────────────
29
+
30
+ /**
31
+ * Interactive selector rendered at a position, with a callback on each
32
+ * highlight change so the caller can update a right-side preview panel.
33
+ *
34
+ * @param options - Selectable items
35
+ * @param startRow - Terminal row (1-based) where the selector starts
36
+ * @param startCol - Terminal column (1-based) where the selector starts
37
+ * @param onHighlight - Called on each arrow key move with the highlighted value.
38
+ * Use this to re-render a detail panel to the right.
39
+ * @returns Selected value on Enter, null on Esc/Ctrl+C
40
+ */
41
+ export function inlineSelect(
42
+ options: ISelectOption[],
43
+ startRow: number,
44
+ startCol: number,
45
+ onHighlight?: (value: string, index: number) => void,
46
+ ): Promise<string | null> {
47
+ let selected = 0;
48
+ // Skip separators
49
+ while (selected < options.length && (!options[selected].value || options[selected].value === "_sep")) selected++;
50
+
51
+ function render(clearExtra = false): void {
52
+ for (let i = 0; i < options.length; i++) {
53
+ process.stdout.write(`\x1b[${startRow + i};${startCol}H\x1b[K`);
54
+ process.stdout.write(formatItem(options[i], i === selected));
55
+ }
56
+ // Clear leftover lines from previous menus (up to 15 extra lines)
57
+ if (clearExtra) {
58
+ for (let i = 0; i < 15; i++) {
59
+ process.stdout.write(`\x1b[${startRow + options.length + i};${startCol}H\x1b[K`);
60
+ }
61
+ }
62
+ }
63
+
64
+ return new Promise((resolve) => {
65
+ if (!process.stdin.isTTY) { resolve(null); return; }
66
+
67
+ process.stdout.write("\x1b[?25l"); // hide cursor
68
+ const wasRaw = process.stdin.isRaw;
69
+ process.stdin.setRawMode(true);
70
+ process.stdin.resume();
71
+ process.stdin.setEncoding("utf8");
72
+
73
+ render(true); // First render clears leftover lines from previous menus
74
+ onHighlight?.(options[selected].value, selected);
75
+
76
+ const onData = (key: string): void => {
77
+ if (key === "\x1b[A" || key === "k") { // Up
78
+ do { selected = (selected - 1 + options.length) % options.length; }
79
+ while (!options[selected].value || options[selected].value === "_sep");
80
+ render();
81
+ onHighlight?.(options[selected].value, selected);
82
+ return;
83
+ }
84
+ if (key === "\x1b[B" || key === "j") { // Down
85
+ do { selected = (selected + 1) % options.length; }
86
+ while (!options[selected].value || options[selected].value === "_sep");
87
+ render();
88
+ onHighlight?.(options[selected].value, selected);
89
+ return;
90
+ }
91
+ if (key.charCodeAt(0) === 13) { cleanup(); resolve(options[selected].value); return; } // Enter
92
+ if ((key.charCodeAt(0) === 27 && key.length === 1) || key === "q") { cleanup(); resolve(null); return; } // Esc
93
+ if (key.charCodeAt(0) === 3) { cleanup(); resolve(null); return; } // Ctrl+C
94
+ };
95
+
96
+ function cleanup(): void {
97
+ process.stdin.removeListener("data", onData);
98
+ process.stdin.setRawMode(wasRaw ?? false);
99
+ process.stdin.pause();
100
+ process.stdout.write("\x1b[?25h");
101
+ process.stdout.write(`\x1b[${startRow + options.length + 1};1H`);
102
+ }
103
+
104
+ process.stdin.on("data", onData);
105
+ });
106
+ }
107
+
108
+ // ─── Full-width select (below current output) ───────────────────────────────
109
+
110
+ /**
111
+ * Full-width selector below current output. For sub-menus (actions, logs, env).
112
+ */
113
+ export async function fullSelect(
114
+ title: string,
115
+ options: ISelectOption[],
116
+ onHighlight?: (value: string, index: number, startRow: number) => void,
117
+ ): Promise<string | null> {
118
+ let selected = 0;
119
+ while (selected < options.length && (!options[selected].value || options[selected].value === "_sep")) selected++;
120
+
121
+ // Print title + reserve space for options
122
+ if (title) console.log(`\n ${chalk.bold(title)}`);
123
+
124
+ // Use CSI 6n to get actual cursor row BEFORE printing option lines
125
+ const cursorRowBefore = await getCursorRowAsync();
126
+ const optionsStartRow = cursorRowBefore;
127
+
128
+ // Reserve vertical space
129
+ for (let i = 0; i < options.length; i++) process.stdout.write("\n");
130
+
131
+ function render(): void {
132
+ // Use absolute row positioning to avoid drift
133
+ for (let i = 0; i < options.length; i++) {
134
+ process.stdout.write(`\x1b[${optionsStartRow + i};4H\x1b[K`);
135
+ process.stdout.write(formatItem(options[i], i === selected));
136
+ }
137
+ // Park cursor after the list
138
+ process.stdout.write(`\x1b[${optionsStartRow + options.length};1H`);
139
+ }
140
+
141
+ return new Promise((resolve) => {
142
+ if (!process.stdin.isTTY) { resolve(null); return; }
143
+
144
+ process.stdout.write("\x1b[?25l");
145
+ const wasRaw = process.stdin.isRaw;
146
+ process.stdin.setRawMode(true);
147
+ process.stdin.resume();
148
+ process.stdin.setEncoding("utf8");
149
+
150
+ render();
151
+ onHighlight?.(options[selected].value, selected, optionsStartRow);
152
+
153
+ const onData = (key: string): void => {
154
+ if (key === "\x1b[A" || key === "k") {
155
+ do { selected = (selected - 1 + options.length) % options.length; }
156
+ while (!options[selected].value || options[selected].value === "_sep");
157
+ render(); onHighlight?.(options[selected].value, selected, optionsStartRow); return;
158
+ }
159
+ if (key === "\x1b[B" || key === "j") {
160
+ do { selected = (selected + 1) % options.length; }
161
+ while (!options[selected].value || options[selected].value === "_sep");
162
+ render(); onHighlight?.(options[selected].value, selected, optionsStartRow); return;
163
+ }
164
+ if (key.charCodeAt(0) === 13) { cleanup(); resolve(options[selected].value); return; }
165
+ if ((key.charCodeAt(0) === 27 && key.length === 1) || key === "q") { cleanup(); resolve(null); return; }
166
+ if (key.charCodeAt(0) === 3) { cleanup(); resolve(null); return; }
167
+ };
168
+
169
+ function cleanup(): void {
170
+ process.stdin.removeListener("data", onData);
171
+ process.stdin.setRawMode(wasRaw ?? false);
172
+ process.stdin.pause();
173
+ process.stdout.write("\x1b[?25h");
174
+ }
175
+
176
+ process.stdin.on("data", onData);
177
+ });
178
+ }
179
+
180
+ // ─── Text input ──────────────────────────────────────────────────────────────
181
+
182
+ export function textInput(message: string): Promise<string | null> {
183
+ return new Promise((resolve) => {
184
+ if (!process.stdin.isTTY) { resolve(null); return; }
185
+ process.stdout.write(`\n ${chalk.hex(T.accent)("?")} ${chalk.bold(message)} `);
186
+ process.stdin.setRawMode(false);
187
+ process.stdin.resume();
188
+ process.stdin.setEncoding("utf8");
189
+ const onData = (data: string): void => {
190
+ process.stdin.removeListener("data", onData);
191
+ process.stdin.pause();
192
+ resolve(data.trim() || null);
193
+ };
194
+ process.stdin.on("data", onData);
195
+ });
196
+ }
197
+
198
+ export function confirm(message: string): Promise<boolean | null> {
199
+ return new Promise((resolve) => {
200
+ if (!process.stdin.isTTY) { resolve(null); return; }
201
+ process.stdout.write(`\n ${chalk.hex(T.accent)("?")} ${chalk.bold(message)} ${chalk.gray("(y/n)")} `);
202
+ const wasRaw = process.stdin.isRaw;
203
+ process.stdin.setRawMode(true);
204
+ process.stdin.resume();
205
+ process.stdin.setEncoding("utf8");
206
+ const onData = (key: string): void => {
207
+ process.stdin.removeListener("data", onData);
208
+ process.stdin.setRawMode(wasRaw ?? false);
209
+ process.stdin.pause();
210
+ process.stdout.write("\n");
211
+ if (key === "y" || key === "Y") resolve(true);
212
+ else if (key.charCodeAt(0) === 3 || key.charCodeAt(0) === 27) resolve(null);
213
+ else resolve(false);
214
+ };
215
+ process.stdin.on("data", onData);
216
+ });
217
+ }
218
+
219
+ // ─── Formatting ──────────────────────────────────────────────────────────────
220
+
221
+ function formatItem(opt: ISelectOption, isSelected: boolean): string {
222
+ if (!opt.value || opt.value === "_sep") {
223
+ return chalk.hex(T.sep)(" ───────────────────");
224
+ }
225
+
226
+ if (isSelected) {
227
+ const bullet = chalk.hex(T.accent)("▸ ");
228
+ const label = chalk.bgHex(T.selectedBg).hex(T.selectedFg).bold(` ${opt.label} `);
229
+ // Don't wrap hint in a color — it may contain pre-colored text (●, ✗)
230
+ const hint = opt.hint ? ` ${opt.hint}` : "";
231
+ return `${bullet}${label}${hint}`;
232
+ }
233
+
234
+ const bullet = chalk.hex(T.dim)(" ");
235
+ const label = chalk.hex(T.normal)(opt.label);
236
+ // Hints on unselected items: pass through (may have colors)
237
+ const hint = opt.hint ? ` ${opt.hint}` : "";
238
+ return `${bullet}${label}${hint}`;
239
+ }
240
+
241
+ // ─── Cursor position query ───────────────────────────────────────────────────
242
+
243
+ /**
244
+ * Get the current cursor row using CSI 6n (Device Status Report).
245
+ * Falls back to a reasonable guess if the terminal doesn't respond.
246
+ */
247
+ function getCursorRowAsync(): Promise<number> {
248
+ return new Promise((resolve) => {
249
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
250
+ resolve(10);
251
+ return;
252
+ }
253
+
254
+ const timeout = setTimeout(() => {
255
+ process.stdin.removeListener("data", onData);
256
+ process.stdin.setRawMode(wasRaw ?? false);
257
+ process.stdin.pause();
258
+ resolve(10); // fallback
259
+ }, 150);
260
+
261
+ const wasRaw = process.stdin.isRaw;
262
+ process.stdin.setRawMode(true);
263
+ process.stdin.resume();
264
+ process.stdin.setEncoding("utf8");
265
+
266
+ const onData = (data: string): void => {
267
+ const match = data.match(/\x1b\[(\d+);(\d+)R/);
268
+ if (match) {
269
+ clearTimeout(timeout);
270
+ process.stdin.removeListener("data", onData);
271
+ process.stdin.setRawMode(wasRaw ?? false);
272
+ process.stdin.pause();
273
+ resolve(parseInt(match[1], 10));
274
+ }
275
+ };
276
+
277
+ process.stdin.on("data", onData);
278
+ process.stdout.write("\x1b[6n"); // Query cursor position
279
+ });
280
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Enhanced spinner utilities for CLI operations.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import chalk from "chalk";
8
+ import ora, { Ora } from "ora";
9
+
10
+ export type SpinnerStatus = "loading" | "success" | "error" | "warning" | "info";
11
+
12
+ export interface ISpinnerOptions {
13
+ /** Initial text to display */
14
+ text: string;
15
+ /** Spinner color */
16
+ color?: "cyan" | "yellow" | "green" | "red" | "blue" | "magenta";
17
+ /** Hide the cursor */
18
+ hideCursor?: boolean;
19
+ /** Use a specific spinner style */
20
+ spinner?: "dots" | "line" | "arrow" | "bouncingBar" | "simpleDots";
21
+ }
22
+
23
+ const spinnerStyles: Record<
24
+ Required<ISpinnerOptions>["spinner"],
25
+ any
26
+ > = {
27
+ dots: "dots",
28
+ line: "line",
29
+ arrow: "arrow2",
30
+ bouncingBar: "bouncingBar",
31
+ simpleDots: "simpleDots",
32
+ };
33
+
34
+ /**
35
+ * Create an enhanced spinner with better defaults.
36
+ */
37
+ export function createSpinner(options: ISpinnerOptions): Ora {
38
+ const { text, color = "cyan", hideCursor = true, spinner = "dots" } = options;
39
+
40
+ return ora({
41
+ text,
42
+ color,
43
+ hideCursor,
44
+ spinner: spinnerStyles[spinner],
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Multi-spinner manager for parallel operations.
50
+ */
51
+ export class MultiSpinner {
52
+ private spinners: Map<string, Ora> = new Map();
53
+ private interval: ReturnType<typeof setInterval> | null = null;
54
+
55
+ constructor(private options: { interval?: number } = {}) {}
56
+
57
+ /**
58
+ * Add a new spinner for a named operation.
59
+ */
60
+ add(name: string, text: string, status: SpinnerStatus = "loading"): void {
61
+ const spinner = ora({
62
+ text,
63
+ prefixText: this.getPrefixText(name),
64
+ }).start();
65
+
66
+ this.spinners.set(name, spinner);
67
+ }
68
+
69
+ /**
70
+ * Update a spinner's text.
71
+ */
72
+ update(name: string, text: string): void {
73
+ const spinner = this.spinners.get(name);
74
+ if (spinner) {
75
+ spinner.text = text;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Mark a spinner as succeeded.
81
+ */
82
+ succeed(name: string, text?: string): void {
83
+ const spinner = this.spinners.get(name);
84
+ if (spinner) {
85
+ spinner.succeed(text);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Mark a spinner as failed.
91
+ */
92
+ fail(name: string, text?: string): void {
93
+ const spinner = this.spinners.get(name);
94
+ if (spinner) {
95
+ spinner.fail(text);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Mark a spinner with a warning.
101
+ */
102
+ warn(name: string, text?: string): void {
103
+ const spinner = this.spinners.get(name);
104
+ if (spinner) {
105
+ spinner.warn(text);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Stop all spinners.
111
+ */
112
+ stopAll(persist = false): void {
113
+ for (const spinner of this.spinners.values()) {
114
+ if (persist) {
115
+ spinner.stop();
116
+ } else {
117
+ spinner.stopAndPersist({ symbol: " ", text: "" });
118
+ }
119
+ }
120
+ this.spinners.clear();
121
+ }
122
+
123
+ /**
124
+ * Get the prefix text for a spinner.
125
+ */
126
+ private getPrefixText(name: string): string {
127
+ return chalk.gray(`[${name}]`);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Create a progress bar for long-running operations.
133
+ */
134
+ export class ProgressBar {
135
+ private current = 0;
136
+ private total: number;
137
+ private width: number;
138
+ private label: string;
139
+ private startTime: number;
140
+
141
+ constructor(total: number, options: { width?: number; label?: string } = {}) {
142
+ this.total = total;
143
+ this.width = options.width ?? 40;
144
+ this.label = options.label ?? "Progress";
145
+ this.startTime = Date.now();
146
+ }
147
+
148
+ /**
149
+ * Update the progress.
150
+ */
151
+ update(current: number): void {
152
+ this.current = Math.min(current, this.total);
153
+ this.render();
154
+ }
155
+
156
+ /**
157
+ * Increment the progress by 1.
158
+ */
159
+ increment(): void {
160
+ this.current = Math.min(this.current + 1, this.total);
161
+ this.render();
162
+ }
163
+
164
+ /**
165
+ * Render the progress bar.
166
+ */
167
+ private render(): void {
168
+ const percentage = this.current / this.total;
169
+ const filled = Math.round(this.width * percentage);
170
+ const empty = this.width - filled;
171
+
172
+ const filledBar = chalk.green("█".repeat(filled));
173
+ const emptyBar = chalk.gray("░".repeat(empty));
174
+
175
+ const elapsed = Date.now() - this.startTime;
176
+ const elapsedSec = (elapsed / 1000).toFixed(1);
177
+
178
+ const eta =
179
+ this.current > 0
180
+ ? ((elapsed / this.current) * (this.total - this.current) / 1000).toFixed(
181
+ 1,
182
+ )
183
+ : "—";
184
+
185
+ process.stdout.write(
186
+ `\r${this.label}: [${filledBar}${emptyBar}] ${Math.round(percentage * 100)}% (${this.current}/${this.total}) ETA: ${eta}s `,
187
+ );
188
+
189
+ if (this.current >= this.total) {
190
+ process.stdout.write("\n");
191
+ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Create a stylized status indicator.
197
+ */
198
+ export function createStatusIndicator(
199
+ status: SpinnerStatus,
200
+ text: string,
201
+ ): string {
202
+ const icons = {
203
+ loading: chalk.cyan("●"),
204
+ success: chalk.green("✓"),
205
+ error: chalk.red("✗"),
206
+ warning: chalk.yellow("⚠"),
207
+ info: chalk.blue("ℹ"),
208
+ };
209
+
210
+ const colors = {
211
+ loading: chalk.cyan,
212
+ success: chalk.green,
213
+ error: chalk.red,
214
+ warning: chalk.yellow,
215
+ info: chalk.blue,
216
+ };
217
+
218
+ return `${icons[status]} ${colors[status](text)}`;
219
+ }
220
+
221
+ /**
222
+ * Create a step-by-step progress display.
223
+ */
224
+ export class StepProgress {
225
+ private steps: Array<{ name: string; status: SpinnerStatus }> = [];
226
+
227
+ addStep(name: string): void {
228
+ this.steps.push({ name, status: "loading" });
229
+ this.render();
230
+ }
231
+
232
+ completeStep(name: string, success = true): void {
233
+ const step = this.steps.find((s) => s.name === name);
234
+ if (step) {
235
+ step.status = success ? "success" : "error";
236
+ this.render();
237
+ }
238
+ }
239
+
240
+ private render(): void {
241
+ const lines = ["", chalk.bold("Progress:"), ""];
242
+
243
+ for (const step of this.steps) {
244
+ const status = createStatusIndicator(step.status, step.name);
245
+ lines.push(` ${status}`);
246
+ }
247
+
248
+ // Clear previous output and render
249
+ process.stdout.write("\x1b[2K"); // Clear line
250
+ for (let i = 0; i < this.steps.length + 3; i++) {
251
+ process.stdout.write("\x1b[1A"); // Move up
252
+ }
253
+
254
+ process.stdout.write(lines.join("\n") + "\n");
255
+ }
256
+ }