@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,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,9 @@
1
+ /**
2
+ * UI utilities for CLI output.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ export * from "./highlighter.js";
8
+ export * from "./spinners.js";
9
+ export * from "./tables.js";
@@ -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
+ }