@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.
- package/dist/cli/coolify-state.d.ts +92 -4
- package/dist/cli/coolify-state.d.ts.map +1 -1
- package/dist/cli/index.js +22149 -11456
- 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 +16 -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,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI banner — renders the Coolify "C" logo from SVG data as terminal art.
|
|
3
|
+
* Uses half-block Unicode characters (▀▄█) with chalk hex colors for
|
|
4
|
+
* a faithful multi-layer shadow reproduction of the original SVG.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
// ─── SVG source data ─────────────────────────────────────────────────────────
|
|
12
|
+
// Original SVG: 500×500 viewport, 3 layers of a "C" shape.
|
|
13
|
+
// Each layer is 3 rectangles (top bar, left bar, bottom bar).
|
|
14
|
+
|
|
15
|
+
const SVG_SIZE = 500;
|
|
16
|
+
|
|
17
|
+
/** The 3 rectangles forming the "C" shape (in SVG coords). */
|
|
18
|
+
const C_RECTS = [
|
|
19
|
+
{ x: 162, y: 97, w: 224, h: 56 }, // top bar
|
|
20
|
+
{ x: 106, y: 153, w: 56, h: 168 }, // left bar
|
|
21
|
+
{ x: 162, y: 321, w: 224, h: 56 }, // bottom bar
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** Layers from back (shadow) to front (solid). Higher contrast for dark terms. */
|
|
25
|
+
const LAYERS = [
|
|
26
|
+
{ dx: 35, dy: 35, color: "#3d2570" }, // shadow — visible on dark bg
|
|
27
|
+
{ dx: 17, dy: 17, color: "#6b40c0" }, // mid shadow
|
|
28
|
+
{ dx: 0, dy: 0, color: "#a875ff" }, // foreground — brighter purple
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// ─── Grid renderer ───────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const GRID_W = 46;
|
|
34
|
+
const GRID_H = 46;
|
|
35
|
+
const CELL = SVG_SIZE / GRID_W;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Paint a rectangle onto the grid (higher value = front layer wins).
|
|
39
|
+
*/
|
|
40
|
+
function paintRect(
|
|
41
|
+
grid: number[][],
|
|
42
|
+
sx: number,
|
|
43
|
+
sy: number,
|
|
44
|
+
sw: number,
|
|
45
|
+
sh: number,
|
|
46
|
+
layerIdx: number,
|
|
47
|
+
): void {
|
|
48
|
+
const x1 = Math.round(sx / CELL);
|
|
49
|
+
const y1 = Math.round(sy / CELL);
|
|
50
|
+
const x2 = Math.round((sx + sw) / CELL);
|
|
51
|
+
const y2 = Math.round((sy + sh) / CELL);
|
|
52
|
+
|
|
53
|
+
for (let y = y1; y < y2 && y < GRID_H; y++) {
|
|
54
|
+
for (let x = x1; x < x2 && x < GRID_W; x++) {
|
|
55
|
+
if (y >= 0 && x >= 0) {
|
|
56
|
+
grid[y][x] = Math.max(grid[y][x], layerIdx + 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the pixel grid from SVG layers.
|
|
64
|
+
* Returns a 2D array where 0=empty, 1=shadow, 2=mid, 3=solid.
|
|
65
|
+
*/
|
|
66
|
+
function buildGrid(): number[][] {
|
|
67
|
+
const grid: number[][] = Array.from({ length: GRID_H }, () =>
|
|
68
|
+
Array(GRID_W).fill(0),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (let li = 0; li < LAYERS.length; li++) {
|
|
72
|
+
const { dx, dy } = LAYERS[li];
|
|
73
|
+
for (const rect of C_RECTS) {
|
|
74
|
+
paintRect(grid, rect.x + dx, rect.y + dy, rect.w, rect.h, li);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return grid;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Trim empty rows/cols from the grid, returns a compact subgrid.
|
|
83
|
+
*/
|
|
84
|
+
function trimGrid(grid: number[][]): number[][] {
|
|
85
|
+
let minR = grid.length,
|
|
86
|
+
maxR = 0,
|
|
87
|
+
minC = grid[0].length,
|
|
88
|
+
maxC = 0;
|
|
89
|
+
|
|
90
|
+
for (let r = 0; r < grid.length; r++) {
|
|
91
|
+
for (let c = 0; c < grid[0].length; c++) {
|
|
92
|
+
if (grid[r][c] > 0) {
|
|
93
|
+
minR = Math.min(minR, r);
|
|
94
|
+
maxR = Math.max(maxR, r);
|
|
95
|
+
minC = Math.min(minC, c);
|
|
96
|
+
maxC = Math.max(maxC, c);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (minR > maxR) return [[]];
|
|
102
|
+
|
|
103
|
+
return grid.slice(minR, maxR + 1).map((row) => row.slice(minC, maxC + 1));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Render grid to terminal lines using half-block characters.
|
|
108
|
+
* Each terminal line encodes 2 grid rows using ▀▄█ characters.
|
|
109
|
+
*/
|
|
110
|
+
function renderHalfBlocks(grid: number[][]): string[] {
|
|
111
|
+
const colors = ["", ...LAYERS.map((l) => l.color)];
|
|
112
|
+
const lines: string[] = [];
|
|
113
|
+
|
|
114
|
+
// Process 2 rows at a time
|
|
115
|
+
for (let r = 0; r < grid.length; r += 2) {
|
|
116
|
+
let line = "";
|
|
117
|
+
const topRow = grid[r];
|
|
118
|
+
const botRow = r + 1 < grid.length ? grid[r + 1] : topRow.map(() => 0);
|
|
119
|
+
|
|
120
|
+
for (let c = 0; c < topRow.length; c++) {
|
|
121
|
+
const top = topRow[c];
|
|
122
|
+
const bot = botRow[c];
|
|
123
|
+
|
|
124
|
+
if (top === 0 && bot === 0) {
|
|
125
|
+
line += " ";
|
|
126
|
+
} else if (top === bot) {
|
|
127
|
+
// Both same color → full block
|
|
128
|
+
line += chalk.hex(colors[top])("█");
|
|
129
|
+
} else if (top > 0 && bot === 0) {
|
|
130
|
+
// Only top → upper half block
|
|
131
|
+
line += chalk.hex(colors[top])("▀");
|
|
132
|
+
} else if (top === 0 && bot > 0) {
|
|
133
|
+
// Only bottom → lower half block
|
|
134
|
+
line += chalk.hex(colors[bot])("▄");
|
|
135
|
+
} else {
|
|
136
|
+
// Different colors → ▀ with fg=top, bg=bot
|
|
137
|
+
line += chalk.hex(colors[top]).bgHex(colors[bot])("▀");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push(line);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Banner assembly ─────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render the Coolify "C" logo as terminal art (full size ~15 lines).
|
|
151
|
+
* Returns an array of styled terminal lines.
|
|
152
|
+
*/
|
|
153
|
+
function renderLogo(): string[] {
|
|
154
|
+
const grid = buildGrid();
|
|
155
|
+
const trimmed = trimGrid(grid);
|
|
156
|
+
return renderHalfBlocks(trimmed);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get logo lines for external use (e.g. header integration).
|
|
161
|
+
*/
|
|
162
|
+
export function getLogoLines(): string[] {
|
|
163
|
+
return renderLogo();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Render a smaller version of the logo (~7 lines) for persistent headers.
|
|
168
|
+
* Uses a coarser grid (24×24 instead of 46×46).
|
|
169
|
+
*/
|
|
170
|
+
export function getMiniLogoLines(): string[] {
|
|
171
|
+
const miniGridW = 24;
|
|
172
|
+
const miniGridH = 24;
|
|
173
|
+
const miniCell = SVG_SIZE / miniGridW;
|
|
174
|
+
|
|
175
|
+
const grid: number[][] = Array.from({ length: miniGridH }, () =>
|
|
176
|
+
Array(miniGridW).fill(0),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
for (let li = 0; li < LAYERS.length; li++) {
|
|
180
|
+
const { dx, dy } = LAYERS[li];
|
|
181
|
+
for (const rect of C_RECTS) {
|
|
182
|
+
const x1 = Math.round((rect.x + dx) / miniCell);
|
|
183
|
+
const y1 = Math.round((rect.y + dy) / miniCell);
|
|
184
|
+
const x2 = Math.round((rect.x + dx + rect.w) / miniCell);
|
|
185
|
+
const y2 = Math.round((rect.y + dy + rect.h) / miniCell);
|
|
186
|
+
for (let y = y1; y < y2 && y < miniGridH; y++) {
|
|
187
|
+
for (let x = x1; x < x2 && x < miniGridW; x++) {
|
|
188
|
+
if (y >= 0 && x >= 0) grid[y][x] = Math.max(grid[y][x], li + 1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const trimmed = trimGrid(grid);
|
|
195
|
+
return renderHalfBlocks(trimmed);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Show the full banner with logo + title + info.
|
|
200
|
+
* Layout: logo on left, text on right.
|
|
201
|
+
*
|
|
202
|
+
* @param version - CLI version string
|
|
203
|
+
* @param subtitle - Optional subtitle line
|
|
204
|
+
*/
|
|
205
|
+
export function showBanner(
|
|
206
|
+
version: string = "0.8.0",
|
|
207
|
+
subtitle?: string,
|
|
208
|
+
): void {
|
|
209
|
+
const logoLines = renderLogo();
|
|
210
|
+
const logoWidth = logoLines.reduce(
|
|
211
|
+
(max, line) => Math.max(max, stripAnsi(line).length),
|
|
212
|
+
0,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Text block (appears next to logo, vertically centered)
|
|
216
|
+
const textLines = [
|
|
217
|
+
"",
|
|
218
|
+
chalk.bold.hex("#8c52ff")(" Coolify CLI"),
|
|
219
|
+
chalk.gray(` v${version}`),
|
|
220
|
+
"",
|
|
221
|
+
chalk.gray(subtitle || " Manage your deployments"),
|
|
222
|
+
"",
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
// Vertically center text relative to logo
|
|
226
|
+
const textStart = Math.max(0, Math.floor((logoLines.length - textLines.length) / 2));
|
|
227
|
+
|
|
228
|
+
const output: string[] = [];
|
|
229
|
+
const maxLines = Math.max(logoLines.length, textStart + textLines.length);
|
|
230
|
+
|
|
231
|
+
for (let i = 0; i < maxLines; i++) {
|
|
232
|
+
const logoPart = i < logoLines.length ? logoLines[i] : "";
|
|
233
|
+
const logoPadded = logoPart + " ".repeat(Math.max(0, logoWidth - stripAnsi(logoPart).length));
|
|
234
|
+
const textIdx = i - textStart;
|
|
235
|
+
const textPart = textIdx >= 0 && textIdx < textLines.length ? textLines[textIdx] : "";
|
|
236
|
+
output.push(` ${logoPadded}${textPart}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log(output.join("\n"));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Show a compact single-line banner (for non-interactive / piped output).
|
|
244
|
+
*
|
|
245
|
+
* @param version - CLI version string
|
|
246
|
+
*/
|
|
247
|
+
export function showCompactBanner(version: string = "0.8.0"): void {
|
|
248
|
+
console.log(
|
|
249
|
+
`${chalk.hex("#8c52ff")("█")} ${chalk.bold("Coolify CLI")} ${chalk.gray(`v${version}`)}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Show the appropriate banner based on TTY status.
|
|
255
|
+
*
|
|
256
|
+
* @param version - CLI version string
|
|
257
|
+
* @param subtitle - Optional subtitle for full banner
|
|
258
|
+
*/
|
|
259
|
+
export function showAutoBanner(
|
|
260
|
+
version: string = "0.8.0",
|
|
261
|
+
subtitle?: string,
|
|
262
|
+
): void {
|
|
263
|
+
if (process.stdout.isTTY) {
|
|
264
|
+
showBanner(version, subtitle);
|
|
265
|
+
} else {
|
|
266
|
+
showCompactBanner(version);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Strip ANSI escape codes from a string (for measuring visible width).
|
|
272
|
+
*/
|
|
273
|
+
function stripAnsi(str: string): string {
|
|
274
|
+
// eslint-disable-next-line no-control-regex
|
|
275
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
276
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax highlighting utilities for CLI output.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import type { Highlighter, BundledLanguage } from "shiki";
|
|
9
|
+
|
|
10
|
+
let highlighterInstance: Highlighter | null = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get or initialize the Shiki highlighter.
|
|
14
|
+
*/
|
|
15
|
+
async function getHighlighter(): Promise<Highlighter> {
|
|
16
|
+
if (!highlighterInstance) {
|
|
17
|
+
const { createHighlighter } = await import("shiki");
|
|
18
|
+
highlighterInstance = await createHighlighter({
|
|
19
|
+
themes: ["github-dark"],
|
|
20
|
+
langs: ["bash", "javascript", "typescript", "json", "env", "yaml"],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return highlighterInstance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Highlight a code snippet with Shiki and convert to terminal colors.
|
|
28
|
+
*/
|
|
29
|
+
export async function highlightCode(
|
|
30
|
+
code: string,
|
|
31
|
+
lang: BundledLanguage = "bash",
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
try {
|
|
34
|
+
const highlighter = await getHighlighter();
|
|
35
|
+
const html = highlighter.codeToHtml(code, {
|
|
36
|
+
lang,
|
|
37
|
+
theme: "github-dark",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Convert HTML colors to chalk ANSI codes
|
|
41
|
+
return htmlToChalk(html);
|
|
42
|
+
} catch {
|
|
43
|
+
// Fallback to plain text if highlighting fails
|
|
44
|
+
return code;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert Shiki HTML output to chalk-colored terminal output.
|
|
50
|
+
*/
|
|
51
|
+
function htmlToChalk(html: string): string {
|
|
52
|
+
// Map Shiki's github-dark theme colors to chalk
|
|
53
|
+
const colorMap: Record<string, (text: string | string[], ...args: unknown[]) => string> = {
|
|
54
|
+
"#79c0ff": chalk.blue, // variable, function
|
|
55
|
+
"#d2a8ff": chalk.magentaBright, // type, class
|
|
56
|
+
"#ffa657": chalk.yellow, // string
|
|
57
|
+
"#a5d6ff": chalk.cyan, // property
|
|
58
|
+
"#ffbfb7": chalk.redBright, // error
|
|
59
|
+
"#f0883e": chalk.hex("#f0883e"), // number
|
|
60
|
+
"#7ee787": chalk.green, // success
|
|
61
|
+
"#e5edf9": chalk.white, // text
|
|
62
|
+
"#8b949e": chalk.gray, // comment
|
|
63
|
+
"#f778ba": chalk.magentaBright, // special
|
|
64
|
+
"#ff7b72": chalk.red, // keyword
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let result = html;
|
|
68
|
+
|
|
69
|
+
// Remove HTML tags but keep content
|
|
70
|
+
result = result.replace(/<code[^>]*>/g, "");
|
|
71
|
+
result = result.replace(/<\/code>/g, "");
|
|
72
|
+
|
|
73
|
+
// Convert <span style="color:#..."> to chalk
|
|
74
|
+
const spanRegex = /<span style="color:([^"]+)">([^<]*)<\/span>/g;
|
|
75
|
+
result = result.replace(spanRegex, (_, color, content) => {
|
|
76
|
+
const chalkFn = colorMap[color];
|
|
77
|
+
return chalkFn ? chalkFn(content) : content;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Remove any remaining HTML tags
|
|
81
|
+
result = result.replace(/<[^>]+>/g, "");
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Highlight an .env file with syntax highlighting.
|
|
88
|
+
* Shows KEY in blue, = in gray, VALUE in yellow.
|
|
89
|
+
*/
|
|
90
|
+
export function highlightEnvLine(line: string): string {
|
|
91
|
+
const eqIndex = line.indexOf("=");
|
|
92
|
+
if (eqIndex === -1) {
|
|
93
|
+
// Comment or invalid line
|
|
94
|
+
return line.startsWith("#")
|
|
95
|
+
? chalk.gray(line)
|
|
96
|
+
: chalk.red(line);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const key = line.slice(0, eqIndex).trim();
|
|
100
|
+
const value = line.slice(eqIndex + 1).trim();
|
|
101
|
+
|
|
102
|
+
// Color the KEY
|
|
103
|
+
const coloredKey = chalk.cyan.bold(key);
|
|
104
|
+
|
|
105
|
+
// Color the value (handle quotes)
|
|
106
|
+
let coloredValue = value;
|
|
107
|
+
if (
|
|
108
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
109
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
110
|
+
) {
|
|
111
|
+
const quote = value[0];
|
|
112
|
+
const innerValue = value.slice(1, -1);
|
|
113
|
+
coloredValue = `${chalk.gray(quote)}${chalk.yellow(innerValue)}${chalk.gray(
|
|
114
|
+
quote,
|
|
115
|
+
)}`;
|
|
116
|
+
} else {
|
|
117
|
+
coloredValue = chalk.yellow(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `${coloredKey}${chalk.gray("=")}${coloredValue}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Highlight multiple .env lines.
|
|
125
|
+
*/
|
|
126
|
+
export function highlightEnvBlock(content: string): string {
|
|
127
|
+
return content
|
|
128
|
+
.split("\n")
|
|
129
|
+
.map((line) => highlightEnvLine(line))
|
|
130
|
+
.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create a visual diff between two values.
|
|
135
|
+
*/
|
|
136
|
+
export function createDiff(
|
|
137
|
+
oldValue: string,
|
|
138
|
+
newValue: string,
|
|
139
|
+
): {
|
|
140
|
+
removed: string;
|
|
141
|
+
added: string;
|
|
142
|
+
lineDiff: string;
|
|
143
|
+
} {
|
|
144
|
+
// Simple character-level diff visualization
|
|
145
|
+
const removed = chalk.red(`- ${oldValue}`);
|
|
146
|
+
const added = chalk.green(`+ ${newValue}`);
|
|
147
|
+
|
|
148
|
+
// Create a line-by-line comparison
|
|
149
|
+
const oldLines = oldValue.split("\n");
|
|
150
|
+
const newLines = newValue.split("\n");
|
|
151
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
152
|
+
|
|
153
|
+
const lineDiff: string[] = [];
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < maxLines; i++) {
|
|
156
|
+
const oldLine = oldLines[i] ?? "";
|
|
157
|
+
const newLine = newLines[i] ?? "";
|
|
158
|
+
|
|
159
|
+
if (oldLine === newLine) {
|
|
160
|
+
lineDiff.push(chalk.gray(` ${oldLine}`));
|
|
161
|
+
} else {
|
|
162
|
+
if (oldLine) {
|
|
163
|
+
lineDiff.push(chalk.red(`- ${oldLine}`));
|
|
164
|
+
}
|
|
165
|
+
if (newLine) {
|
|
166
|
+
lineDiff.push(chalk.green(`+ ${newLine}`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
removed,
|
|
173
|
+
added,
|
|
174
|
+
lineDiff: lineDiff.join("\n"),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized prompt utilities using @clack/prompts.
|
|
3
|
+
* Provides consistent UX across all interactive commands.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
import type { ICoolifyApplication } from "../../coolify/types.js";
|
|
10
|
+
import type { ICoolifyAppState } from "../coolify-state.js";
|
|
11
|
+
import type { ICoolifyProject } from "../../coolify/types.js";
|
|
12
|
+
import type { ICoolifyEnvironment } from "../../coolify/types.js";
|
|
13
|
+
|
|
14
|
+
export { isCancel };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Prompt for selecting an application from list.
|
|
18
|
+
*/
|
|
19
|
+
export async function promptAppSelection(
|
|
20
|
+
apps: ICoolifyApplication[],
|
|
21
|
+
options: { message?: string } = {},
|
|
22
|
+
): Promise<string | null> {
|
|
23
|
+
const response = await p.select({
|
|
24
|
+
message: options.message || "Select an application:",
|
|
25
|
+
options: apps.map((app) => ({
|
|
26
|
+
label: app.name,
|
|
27
|
+
value: app.uuid,
|
|
28
|
+
hint: app.status,
|
|
29
|
+
})),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (p.isCancel(response)) return null;
|
|
33
|
+
return response as string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Prompt for selecting multiple applications.
|
|
38
|
+
*/
|
|
39
|
+
export async function promptMultiAppSelection(
|
|
40
|
+
apps: ICoolifyAppState[],
|
|
41
|
+
options: { message?: string } = {},
|
|
42
|
+
): Promise<string[] | null> {
|
|
43
|
+
const response = await p.multiselect({
|
|
44
|
+
message: options.message || "Select applications:",
|
|
45
|
+
options: [
|
|
46
|
+
{ label: "All (parallel)", value: "_all" },
|
|
47
|
+
...apps.map((app) => ({
|
|
48
|
+
label: app.name,
|
|
49
|
+
value: app.uuid,
|
|
50
|
+
hint: app.domain,
|
|
51
|
+
})),
|
|
52
|
+
],
|
|
53
|
+
required: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (p.isCancel(response)) return null;
|
|
57
|
+
return response as string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Prompt for selecting a project.
|
|
62
|
+
*/
|
|
63
|
+
export async function promptProjectSelection(
|
|
64
|
+
projects: ICoolifyProject[],
|
|
65
|
+
): Promise<string | null> {
|
|
66
|
+
const response = await p.select({
|
|
67
|
+
message: "Select a project:",
|
|
68
|
+
options: projects.map((proj) => ({
|
|
69
|
+
label: proj.name,
|
|
70
|
+
value: proj.uuid,
|
|
71
|
+
hint: `${proj.environments?.length || 0} environments`,
|
|
72
|
+
})),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (p.isCancel(response)) return null;
|
|
76
|
+
return response as string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prompt for selecting an environment.
|
|
81
|
+
*/
|
|
82
|
+
export async function promptEnvironmentSelection(
|
|
83
|
+
environments: ICoolifyEnvironment[],
|
|
84
|
+
): Promise<string | null> {
|
|
85
|
+
const response = await p.select({
|
|
86
|
+
message: "Select an environment:",
|
|
87
|
+
options: environments.map((env) => ({
|
|
88
|
+
label: env.name,
|
|
89
|
+
value: env.uuid,
|
|
90
|
+
hint: `${env.applications?.length || 0} apps`,
|
|
91
|
+
})),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (p.isCancel(response)) return null;
|
|
95
|
+
return response as string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Prompt for text input (app name, etc).
|
|
100
|
+
*/
|
|
101
|
+
export async function promptText(
|
|
102
|
+
message: string,
|
|
103
|
+
options: { placeholder?: string; defaultValue?: string } = {},
|
|
104
|
+
): Promise<string | null> {
|
|
105
|
+
const response = await p.text({
|
|
106
|
+
message,
|
|
107
|
+
placeholder: options.placeholder,
|
|
108
|
+
initialValue: options.defaultValue,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (p.isCancel(response)) return null;
|
|
112
|
+
return response as string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Prompt for confirmation.
|
|
117
|
+
*/
|
|
118
|
+
export async function promptConfirm(
|
|
119
|
+
message: string,
|
|
120
|
+
): Promise<boolean | null> {
|
|
121
|
+
const response = await p.confirm({
|
|
122
|
+
message,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (p.isCancel(response)) return null;
|
|
126
|
+
return response as boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Show spinner for async operations.
|
|
131
|
+
*/
|
|
132
|
+
export function showSpinner() {
|
|
133
|
+
return p.spinner();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Show intro message.
|
|
138
|
+
*/
|
|
139
|
+
export function showIntro(title: string) {
|
|
140
|
+
p.intro(title);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Show outro message.
|
|
145
|
+
*/
|
|
146
|
+
export function showOutro(message: string) {
|
|
147
|
+
p.outro(message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Show note/group.
|
|
152
|
+
*/
|
|
153
|
+
export function showNote(message: string) {
|
|
154
|
+
p.note(message);
|
|
155
|
+
}
|