@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
@@ -1,43 +1,68 @@
1
1
  /**
2
- * Logs command for CLI.
2
+ * Logs command application logs with follow mode (polling).
3
+ * Ctrl+C stops the follow loop without killing the process.
3
4
  *
4
5
  * @module
5
6
  */
6
7
 
8
+ import * as p from "@clack/prompts";
7
9
  import { isErr } from "@mks2508/no-throw";
8
10
  import chalk from "chalk";
9
11
  import { getCoolifyService } from "../../coolify/index.js";
10
- import { resolveUuid } from "../coolify-state.js";
12
+ import { resolveUuid, loadMultiAppState } from "../coolify-state.js";
13
+ import { resolveAppNameOrUuid } from "../name-resolver.js";
14
+
15
+ const POLL_INTERVAL = 2000;
11
16
 
12
17
  /**
13
- * Logs command handler.
14
- * If no UUID is provided, reads from .coolify.json in the current directory.
15
- *
16
- * @param uuid - Application UUID (optional if .coolify.json exists)
17
- * @param options - Logs options
18
+ * Logs command handler (CLI entry point).
18
19
  */
19
20
  export async function logsCommand(
20
21
  uuid: string | undefined,
21
- options: { lines?: number; follow?: boolean },
22
+ options: {
23
+ lines?: number;
24
+ follow?: boolean;
25
+ errors?: boolean;
26
+ since?: string;
27
+ },
22
28
  ) {
23
- const resolvedUuid = resolveUuid(uuid);
24
- if (!resolvedUuid) {
25
- console.error(
26
- chalk.red("Error: No UUID provided and no .coolify.json found"),
27
- );
28
- return;
29
+ if (!uuid) {
30
+ uuid = await resolveOrPromptApp();
31
+ if (!uuid) return;
32
+ } else {
33
+ let resolvedUuid = resolveUuid(uuid);
34
+ if (!resolvedUuid) resolvedUuid = await resolveAppNameOrUuid(uuid);
35
+ if (!resolvedUuid) {
36
+ console.error(chalk.red("Error: Could not resolve app UUID/name"));
37
+ return;
38
+ }
39
+ uuid = resolvedUuid;
29
40
  }
30
- uuid = resolvedUuid;
41
+
31
42
  const coolify = getCoolifyService();
32
43
  const initResult = await coolify.init();
33
-
34
44
  if (isErr(initResult)) {
35
45
  console.error(chalk.red(`Error: ${initResult.error.message}`));
36
46
  return;
37
47
  }
38
48
 
49
+ if (options.follow) {
50
+ await followLogs(coolify, uuid, options);
51
+ } else {
52
+ await showLogs(coolify, uuid, options);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Show logs once (non-follow).
58
+ */
59
+ async function showLogs(
60
+ coolify: ReturnType<typeof getCoolifyService>,
61
+ uuid: string,
62
+ options: { lines?: number; errors?: boolean; since?: string },
63
+ ): Promise<void> {
39
64
  const result = await coolify.getApplicationLogs(uuid, {
40
- tail: options.lines || 50,
65
+ tail: options.lines || 100,
41
66
  });
42
67
 
43
68
  if (isErr(result)) {
@@ -45,20 +70,195 @@ export async function logsCommand(
45
70
  return;
46
71
  }
47
72
 
48
- const logs = result.value;
73
+ let logs = result.value.logs;
74
+ logs = filterLogs(logs, options);
49
75
 
50
- if (logs.logs.length === 0) {
76
+ if (logs.length === 0) {
51
77
  console.log(chalk.yellow("No logs available"));
52
78
  return;
53
79
  }
54
80
 
55
- console.log(chalk.gray(`Application logs (${logs.logs.length} lines):\n`));
81
+ console.log(chalk.gray(`Logs (${logs.length} lines):\n`));
82
+ for (const line of logs) {
83
+ console.log(colorizeLogLine(line));
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Follow logs with polling.
89
+ * Ctrl+C stops the follow and returns control to caller (does NOT exit process).
90
+ */
91
+ async function followLogs(
92
+ coolify: ReturnType<typeof getCoolifyService>,
93
+ uuid: string,
94
+ options: { lines?: number; errors?: boolean; since?: string },
95
+ ): Promise<void> {
96
+ console.log(
97
+ chalk.gray(
98
+ `Following logs for ${chalk.cyan(uuid.slice(0, 12))} — ${chalk.bold("Ctrl+C")} to stop\n`,
99
+ ),
100
+ );
101
+
102
+ let seenLines = new Set<string>();
103
+ let tail = options.lines || 20; // Start small — only recent lines
104
+ let isFirstFetch = true;
105
+ let running = true;
106
+
107
+ // Ctrl+C handler — stops the loop, does NOT exit process
108
+ const abort = () => {
109
+ running = false;
110
+ };
111
+ process.on("SIGINT", abort);
112
+
113
+ try {
114
+ while (running) {
115
+ const result = await coolify.getApplicationLogs(uuid, { tail });
116
+
117
+ if (isErr(result)) {
118
+ if (!running) break; // Aborted during fetch
119
+ console.error(chalk.red(`Error: ${result.error.message}`));
120
+ await sleep(POLL_INTERVAL);
121
+ continue;
122
+ }
56
123
 
57
- for (const line of logs.logs) {
58
- console.log(line);
124
+ let logs = result.value.logs;
125
+ logs = filterLogs(logs, options);
126
+
127
+ for (const line of logs) {
128
+ const lineHash = line.trim();
129
+ if (!seenLines.has(lineHash) && lineHash.length > 0) {
130
+ seenLines.add(lineHash);
131
+ console.log(colorizeLogLine(line));
132
+ }
133
+ }
134
+
135
+ if (seenLines.size > 5000) {
136
+ const entries = [...seenLines];
137
+ seenLines = new Set(entries.slice(-2000));
138
+ }
139
+
140
+ // Keep tail small — we rely on dedup to only show new lines
141
+ tail = 30;
142
+
143
+ // Interruptible sleep
144
+ await new Promise<void>((resolve) => {
145
+ const timer = setTimeout(resolve, POLL_INTERVAL);
146
+ const checkAbort = () => {
147
+ if (!running) {
148
+ clearTimeout(timer);
149
+ resolve();
150
+ }
151
+ };
152
+ // Check abort every 100ms during sleep
153
+ const interval = setInterval(checkAbort, 100);
154
+ setTimeout(() => clearInterval(interval), POLL_INTERVAL + 50);
155
+ });
156
+ }
157
+ } finally {
158
+ process.removeListener("SIGINT", abort);
59
159
  }
60
160
 
61
- if (options.follow) {
62
- console.log(chalk.yellow("\nFollow mode not yet implemented"));
161
+ console.log(chalk.gray("\nStopped following logs."));
162
+ }
163
+
164
+ /**
165
+ * Fetch latest N log lines for a preview panel (no follow, just snapshot).
166
+ * Returns colorized string lines.
167
+ *
168
+ * @param uuid - Application UUID (already resolved)
169
+ * @param lines - Number of lines to fetch
170
+ * @returns Array of colorized log lines
171
+ */
172
+ export async function fetchLogPreview(
173
+ uuid: string,
174
+ lines: number = 8,
175
+ ): Promise<string[]> {
176
+ const coolify = getCoolifyService();
177
+ const result = await coolify.getApplicationLogs(uuid, { tail: lines });
178
+ if (isErr(result)) return [chalk.gray("(logs unavailable)")];
179
+
180
+ const logs = result.value.logs;
181
+ if (logs.length === 0) return [chalk.gray("(no logs)")];
182
+
183
+ return logs.slice(-lines).map(colorizeLogLine);
184
+ }
185
+
186
+ // ─── Filters & helpers ───────────────────────────────────────────────────────
187
+
188
+ function filterLogs(
189
+ logs: string[],
190
+ options: { errors?: boolean; since?: string },
191
+ ): string[] {
192
+ let filtered = logs;
193
+
194
+ if (options.errors) {
195
+ filtered = filtered.filter(
196
+ (line) =>
197
+ /\b(error|err|fatal|panic|exception|fail)\b/i.test(line) ||
198
+ /stderr/i.test(line),
199
+ );
63
200
  }
201
+
202
+ if (options.since) {
203
+ const sinceMs = parseDuration(options.since);
204
+ if (sinceMs > 0) {
205
+ const cutoff = Date.now() - sinceMs;
206
+ filtered = filtered.filter((line) => {
207
+ const ts = extractTimestamp(line);
208
+ return ts === null || ts >= cutoff;
209
+ });
210
+ }
211
+ }
212
+
213
+ return filtered;
214
+ }
215
+
216
+ export function colorizeLogLine(line: string): string {
217
+ if (/\b(error|fatal|panic)\b/i.test(line)) return chalk.red(line);
218
+ if (/\b(warn|warning)\b/i.test(line)) return chalk.yellow(line);
219
+ if (/\b(info)\b/i.test(line)) return chalk.white(line);
220
+ if (/\b(debug|trace)\b/i.test(line)) return chalk.gray(line);
221
+ return line;
222
+ }
223
+
224
+ function parseDuration(str: string): number {
225
+ const match = str.match(/^(\d+)(s|m|h|d)$/);
226
+ if (!match) return 0;
227
+ const value = parseInt(match[1], 10);
228
+ const multipliers: Record<string, number> = {
229
+ s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000,
230
+ };
231
+ return value * (multipliers[match[2]] || 0);
232
+ }
233
+
234
+ function extractTimestamp(line: string): number | null {
235
+ const isoMatch = line.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
236
+ if (isoMatch) return new Date(isoMatch[0]).getTime();
237
+ return null;
238
+ }
239
+
240
+ async function resolveOrPromptApp(): Promise<string | null> {
241
+ const resolved = resolveUuid(undefined);
242
+ if (resolved) return resolved;
243
+
244
+ const multiState = loadMultiAppState();
245
+ if (multiState && multiState.apps.length > 0) {
246
+ const response = await p.select({
247
+ message: "Select an application:",
248
+ options: multiState.apps.map((app) => ({
249
+ label: app.name,
250
+ value: app.uuid,
251
+ hint: app.domain || app.service,
252
+ })),
253
+ });
254
+ if (p.isCancel(response)) return null;
255
+ return response as string;
256
+ }
257
+
258
+ console.error(chalk.red("Error: No UUID/name and no .coolify.json found"));
259
+ return null;
260
+ }
261
+
262
+ function sleep(ms: number): Promise<void> {
263
+ return new Promise((resolve) => setTimeout(resolve, ms));
64
264
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Main interactive menu when CLI is called without arguments.
3
+ * Shows banner + infrastructure tree + interactive navigation.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import { showAutoBanner } from "../ui/banner.js";
9
+
10
+ /**
11
+ * Main interactive menu entry point.
12
+ */
13
+ export async function mainMenu(): Promise<void> {
14
+ if (process.stdout.isTTY) {
15
+ console.clear();
16
+ }
17
+ showAutoBanner("0.8.0");
18
+
19
+ // Go directly to status which shows tree + interactive navigation
20
+ await (await import("./status.js")).statusCommand();
21
+ }