@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
@@ -10,6 +10,7 @@ import { isErr } from "@mks2508/no-throw";
10
10
  import chalk from "chalk";
11
11
  import { getCoolifyService } from "../../coolify/index.js";
12
12
  import { resolveUuid } from "../coolify-state.js";
13
+ import { resolveAppNameOrUuid } from "../name-resolver.js";
13
14
 
14
15
  /**
15
16
  * Executes the delete command.
@@ -22,10 +23,13 @@ export async function deleteCommand(
22
23
  uuid: string | undefined,
23
24
  options: { force?: boolean; yes?: boolean } = {},
24
25
  ): Promise<void> {
25
- const resolvedUuid = resolveUuid(uuid);
26
+ let resolvedUuid = resolveUuid(uuid);
27
+ if (!resolvedUuid && uuid) {
28
+ resolvedUuid = await resolveAppNameOrUuid(uuid);
29
+ }
26
30
  if (!resolvedUuid) {
27
31
  console.error(
28
- chalk.red("Error: No UUID provided and no .coolify.json found"),
32
+ chalk.red("Error: No UUID/name provided and no .coolify.json found"),
29
33
  );
30
34
  return;
31
35
  }
@@ -1,77 +1,375 @@
1
1
  /**
2
- * Deploy command for CLI.
2
+ * Deploy command for CLI — with real-time progress polling and multi-app support.
3
+ * Detects TTY and falls back to static output when not interactive.
3
4
  *
4
5
  * @module
5
6
  */
6
7
 
7
- import { isOk, isErr } from "@mks2508/no-throw";
8
- import ora from "ora";
8
+ import * as p from "@clack/prompts";
9
+ import { isErr } from "@mks2508/no-throw";
10
+ import boxen from "boxen";
9
11
  import chalk from "chalk";
10
12
  import { getCoolifyService } from "../../coolify/index.js";
11
- import { resolveUuid } from "../coolify-state.js";
13
+ import {
14
+ resolveUuid,
15
+ loadMultiAppState,
16
+ type ICoolifyMultiAppState,
17
+ } from "../coolify-state.js";
18
+ import { resolveAppNameOrUuid } from "../name-resolver.js";
19
+
20
+ const POLL_INTERVAL = 3000;
21
+ const isTTY = process.stdout.isTTY === true;
12
22
 
13
23
  /**
14
24
  * Deploy command handler.
15
- * If no UUID is provided, reads from .coolify.json in the current directory.
25
+ * Supports --all for multi-app, --service for specific service,
26
+ * and polls deployment status for real-time progress feedback.
16
27
  *
17
- * @param uuid - Application UUID (optional if .coolify.json exists)
28
+ * @param uuid - Application UUID or name (optional)
18
29
  * @param options - Deploy options
19
30
  */
20
31
  export async function deployCommand(
21
32
  uuid: string | undefined,
22
- options: { force?: boolean; tag?: string },
33
+ options: { force?: boolean; tag?: string; all?: boolean; service?: string },
23
34
  ) {
24
- const resolvedUuid = resolveUuid(uuid);
25
- if (!resolvedUuid) {
26
- console.error(
27
- chalk.red(
28
- "Error: No UUID provided and no .coolify.json found in current directory",
29
- ),
30
- );
35
+ const coolify = getCoolifyService();
36
+ const initResult = await coolify.init();
37
+
38
+ if (isErr(initResult)) {
39
+ console.error(chalk.red(`Error: ${initResult.error.message}`));
31
40
  return;
32
41
  }
33
- uuid = resolvedUuid;
34
- const spinner = ora("Initializing Coolify connection...").start();
35
42
 
36
- try {
37
- const coolify = getCoolifyService();
38
- const initResult = await coolify.init();
43
+ // Multi-app deploy: --all or --service
44
+ const multiState = loadMultiAppState();
45
+
46
+ if (options.all && multiState?.apps?.length) {
47
+ await deployMultiApp(coolify, multiState, options);
48
+ return;
49
+ }
39
50
 
40
- if (isErr(initResult)) {
41
- spinner.fail(
42
- chalk.red(`Failed to initialize: ${initResult.error.message}`),
51
+ if (options.service && multiState?.apps?.length) {
52
+ const app = multiState.apps.find(
53
+ (a) => a.service === options.service || a.name === options.service,
54
+ );
55
+ if (!app) {
56
+ console.error(
57
+ chalk.red(`Service "${options.service}" not found in .coolify.json`),
58
+ );
59
+ console.log(
60
+ chalk.gray(
61
+ `Available: ${multiState.apps.map((a) => a.service || a.name).join(", ")}`,
62
+ ),
43
63
  );
44
64
  return;
45
65
  }
66
+ await deploySingleApp(coolify, app.uuid, app.name, options);
67
+ return;
68
+ }
46
69
 
47
- spinner.text = "Triggering deployment...";
48
-
49
- const result = await coolify.deploy(
50
- {
51
- uuid,
52
- force: options.force,
53
- tag: options.tag,
54
- },
55
- (percent, message) => {
56
- spinner.text = `${chalk.bold(`[${percent}%]`)} ${message}`;
57
- },
58
- );
70
+ // Interactive selection if no UUID
71
+ let displayName = uuid || "";
72
+ if (!uuid) {
73
+ uuid = await resolveOrPromptApp(multiState);
74
+ if (!uuid) return;
75
+ const app = multiState?.apps?.find((a) => a.uuid === uuid);
76
+ displayName = app?.name || uuid.slice(0, 8);
77
+ } else {
78
+ displayName = uuid;
79
+ let resolvedUuid = resolveUuid(uuid);
80
+ if (!resolvedUuid) {
81
+ resolvedUuid = await resolveAppNameOrUuid(uuid);
82
+ }
83
+ if (!resolvedUuid) {
84
+ console.error(chalk.red("Error: Could not resolve app UUID/name"));
85
+ return;
86
+ }
87
+ uuid = resolvedUuid;
88
+ }
59
89
 
60
- if (isOk(result)) {
61
- spinner.succeed(
62
- chalk.green(
63
- `Deployment triggered! UUID: ${chalk.cyan(result.value.deploymentUuid)}`,
64
- ),
65
- );
66
- console.log(` Resource UUID: ${chalk.cyan(result.value.resourceUuid)}`);
67
- } else {
68
- spinner.fail(chalk.red(`Deployment failed: ${result.error.message}`));
90
+ if (uuid === "_all" && multiState) {
91
+ await deployMultiApp(coolify, multiState, options);
92
+ return;
93
+ }
94
+
95
+ await deploySingleApp(coolify, uuid, displayName, options);
96
+ }
97
+
98
+ /**
99
+ * Deploy a single app with real-time progress polling.
100
+ */
101
+ async function deploySingleApp(
102
+ coolify: ReturnType<typeof getCoolifyService>,
103
+ uuid: string,
104
+ displayName: string,
105
+ options: { force?: boolean; tag?: string },
106
+ ): Promise<{ success: boolean; deploymentUuid?: string }> {
107
+ log(`Triggering deployment for ${displayName}...`);
108
+
109
+ const result = await coolify.deploy({
110
+ uuid,
111
+ force: options.force,
112
+ tag: options.tag,
113
+ });
114
+
115
+ if (isErr(result)) {
116
+ log(chalk.red(`Deploy failed: ${result.error.message}`));
117
+ return { success: false };
118
+ }
119
+
120
+ const deploymentUuid = result.value.deploymentUuid;
121
+ log(chalk.green(`✓ Deployment #${deploymentUuid.slice(0, 8)} triggered for ${displayName}`));
122
+
123
+ await pollDeploymentProgress(coolify, deploymentUuid, displayName);
124
+
125
+ return { success: true, deploymentUuid };
126
+ }
127
+
128
+ /**
129
+ * Poll deployment status until finished or failed.
130
+ * Uses spinner in TTY mode, static lines in non-TTY mode.
131
+ */
132
+ async function pollDeploymentProgress(
133
+ coolify: ReturnType<typeof getCoolifyService>,
134
+ deploymentUuid: string,
135
+ displayName: string,
136
+ ): Promise<void> {
137
+ let spinner: ReturnType<typeof p.spinner> | null = null;
138
+ if (isTTY) {
139
+ spinner = p.spinner();
140
+ spinner.start(`${chalk.cyan(displayName)} — Building...`);
141
+ }
142
+
143
+ let lastLogLength = 0;
144
+ let lastStatus = "";
145
+
146
+ while (true) {
147
+ const result = await coolify.getDeploymentLogs(deploymentUuid);
148
+
149
+ if (isErr(result)) {
150
+ if (spinner) spinner.stop(chalk.yellow("Could not fetch deployment status"));
151
+ else log(chalk.yellow("Could not fetch deployment status"));
152
+ break;
69
153
  }
70
- } catch (error) {
71
- spinner.fail(
72
- chalk.red(
73
- `Error: ${error instanceof Error ? error.message : String(error)}`,
74
- ),
75
- );
154
+
155
+ const { status, logs } = result.value;
156
+
157
+ const logLines = logs ? logs.split("\n") : [];
158
+ const newLines = logLines.slice(lastLogLength);
159
+ lastLogLength = logLines.length;
160
+
161
+ const progressHint = extractProgressHint(newLines);
162
+
163
+ if (status !== lastStatus || progressHint) {
164
+ const statusIcon = getStatusIcon(status);
165
+ const progressText = progressHint ? ` — ${progressHint}` : "";
166
+ const msg = `${statusIcon} ${displayName} ${status}${progressText}`;
167
+
168
+ if (spinner) {
169
+ spinner.message(msg);
170
+ } else if (status !== lastStatus) {
171
+ // Non-TTY: only log on status change to avoid spam
172
+ log(msg);
173
+ }
174
+ lastStatus = status;
175
+ }
176
+
177
+ // Terminal states
178
+ if (status === "finished") {
179
+ const msg = `${chalk.green("✓")} ${chalk.cyan(displayName)} — deployed successfully`;
180
+ if (spinner) spinner.stop(msg);
181
+ else log(msg);
182
+ break;
183
+ }
184
+
185
+ if (status === "failed" || status === "cancelled") {
186
+ const msg = `${chalk.red("✗")} ${chalk.cyan(displayName)} — ${status}`;
187
+ if (spinner) spinner.stop(msg);
188
+ else log(msg);
189
+
190
+ const errorLines = logLines.slice(-5).filter((l) => l.trim().length > 0);
191
+ if (errorLines.length > 0) {
192
+ if (isTTY) {
193
+ console.log(
194
+ boxen(errorLines.join("\n"), {
195
+ title: chalk.red("Error"),
196
+ borderStyle: "round",
197
+ borderColor: "red",
198
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
199
+ }),
200
+ );
201
+ } else {
202
+ log(chalk.red("--- Error context ---"));
203
+ errorLines.forEach((l) => log(` ${l}`));
204
+ }
205
+ }
206
+ break;
207
+ }
208
+
209
+ await sleep(POLL_INTERVAL);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Deploy multiple apps in parallel with individual progress tracking.
215
+ */
216
+ async function deployMultiApp(
217
+ coolify: ReturnType<typeof getCoolifyService>,
218
+ state: ICoolifyMultiAppState,
219
+ options: { force?: boolean; tag?: string },
220
+ ): Promise<void> {
221
+ const apps = state.apps;
222
+ log(`Deploying ${apps.length} apps in parallel...`);
223
+
224
+ const deployPromises = apps.map(async (app) => {
225
+ log(` ${chalk.cyan(app.name)} — triggering...`);
226
+
227
+ const result = await coolify.deploy({
228
+ uuid: app.uuid,
229
+ force: options.force,
230
+ tag: options.tag,
231
+ });
232
+
233
+ if (isErr(result)) {
234
+ log(` ${chalk.red("✗")} ${app.name} — ${result.error.message}`);
235
+ return { app, success: false, deploymentUuid: undefined };
236
+ }
237
+
238
+ const deploymentUuid = result.value.deploymentUuid;
239
+ log(` ${chalk.cyan(app.name)} — building (#${deploymentUuid.slice(0, 8)})`);
240
+
241
+ let finalStatus = "unknown";
242
+ while (true) {
243
+ const statusResult = await coolify.getDeploymentLogs(deploymentUuid);
244
+
245
+ if (isErr(statusResult)) {
246
+ log(` ${chalk.yellow("⚠")} ${app.name} — could not fetch status`);
247
+ break;
248
+ }
249
+
250
+ const { status, logs } = statusResult.value;
251
+
252
+ if (status !== finalStatus) {
253
+ const hint = extractProgressHint(logs ? logs.split("\n").slice(-10) : []);
254
+ log(` ${getStatusIcon(status)} ${app.name} — ${status}${hint ? ` (${hint})` : ""}`);
255
+ finalStatus = status;
256
+ }
257
+
258
+ if (status === "finished") {
259
+ log(` ${chalk.green("✓")} ${app.name} — deployed`);
260
+ break;
261
+ }
262
+
263
+ if (status === "failed" || status === "cancelled") {
264
+ log(` ${chalk.red("✗")} ${app.name} — ${status}`);
265
+ break;
266
+ }
267
+
268
+ await sleep(POLL_INTERVAL);
269
+ }
270
+
271
+ return { app, success: finalStatus === "finished", deploymentUuid };
272
+ });
273
+
274
+ const results = await Promise.allSettled(deployPromises);
275
+
276
+ const succeeded = results.filter(
277
+ (r) => r.status === "fulfilled" && r.value?.success,
278
+ ).length;
279
+ const failed = results.length - succeeded;
280
+
281
+ log("");
282
+ if (failed === 0) {
283
+ log(chalk.green(`All ${succeeded} apps deployed successfully`));
284
+ } else {
285
+ log(`${chalk.green(`${succeeded} succeeded`)}, ${chalk.red(`${failed} failed`)}`);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Extract a human-readable progress hint from recent build log lines.
291
+ */
292
+ function extractProgressHint(lines: string[]): string | null {
293
+ for (let i = lines.length - 1; i >= 0; i--) {
294
+ const line = lines[i].trim();
295
+ if (!line) continue;
296
+
297
+ const stepMatch = line.match(/Step (\d+)\/(\d+)/i);
298
+ if (stepMatch) return `Step ${stepMatch[1]}/${stepMatch[2]}`;
299
+
300
+ const buildKitMatch = line.match(/#(\d+) \[.*?\] (.*)/);
301
+ if (buildKitMatch) return buildKitMatch[2].slice(0, 40);
302
+
303
+ if (/cloning|clone/i.test(line)) return "Git clone";
304
+ if (/installing|npm install|bun install|yarn install/i.test(line))
305
+ return "Installing dependencies";
306
+ if (/health.?check|healthy/i.test(line)) return "Health check";
307
+ if (/starting|container.*start/i.test(line)) return "Starting container";
308
+ }
309
+ return null;
310
+ }
311
+
312
+ /**
313
+ * Get a status icon for deployment state.
314
+ */
315
+ function getStatusIcon(status: string): string {
316
+ switch (status) {
317
+ case "finished":
318
+ return chalk.green("✓");
319
+ case "failed":
320
+ case "cancelled":
321
+ return chalk.red("✗");
322
+ case "in_progress":
323
+ case "queued":
324
+ return chalk.cyan("●");
325
+ default:
326
+ return chalk.yellow("○");
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Resolve app UUID or prompt user to pick from multi-app state.
332
+ */
333
+ async function resolveOrPromptApp(
334
+ multiState: ICoolifyMultiAppState | null,
335
+ ): Promise<string | null> {
336
+ const resolved = resolveUuid(undefined);
337
+ if (resolved) return resolved;
338
+
339
+ if (isTTY && multiState && multiState.apps.length > 0) {
340
+ const response = await p.select({
341
+ message: "Deploy which app?",
342
+ options: [
343
+ {
344
+ label: `All (${multiState.apps.length} apps in parallel)`,
345
+ value: "_all",
346
+ hint: "parallel deploy",
347
+ },
348
+ ...multiState.apps.map((app) => ({
349
+ label: app.name,
350
+ value: app.uuid,
351
+ hint: app.domain || app.service,
352
+ })),
353
+ ],
354
+ });
355
+
356
+ if (p.isCancel(response)) return null;
357
+ return response as string;
76
358
  }
359
+
360
+ console.error(
361
+ chalk.red("Error: No UUID/name provided and no .coolify.json found"),
362
+ );
363
+ return null;
364
+ }
365
+
366
+ /**
367
+ * Log a message to stdout (static, no spinner frames).
368
+ */
369
+ function log(msg: string): void {
370
+ console.log(msg);
371
+ }
372
+
373
+ function sleep(ms: number): Promise<void> {
374
+ return new Promise((resolve) => setTimeout(resolve, ms));
77
375
  }
@@ -12,6 +12,7 @@ import Table from "cli-table3";
12
12
  import { getCoolifyService } from "../../coolify/index.js";
13
13
  import { formatStatus } from "../../utils/format.js";
14
14
  import { resolveUuid } from "../coolify-state.js";
15
+ import { resolveAppNameOrUuid } from "../name-resolver.js";
15
16
 
16
17
  /**
17
18
  * Deployments command handler.
@@ -24,10 +25,13 @@ export async function deploymentsCommand(
24
25
  uuid: string | undefined,
25
26
  options: { full?: boolean; limit?: number } = {},
26
27
  ) {
27
- const resolvedUuid = resolveUuid(uuid);
28
+ let resolvedUuid = resolveUuid(uuid);
29
+ if (!resolvedUuid && uuid) {
30
+ resolvedUuid = await resolveAppNameOrUuid(uuid);
31
+ }
28
32
  if (!resolvedUuid) {
29
33
  console.error(
30
- chalk.red("Error: No UUID provided and no .coolify.json found"),
34
+ chalk.red("Error: No UUID/name provided and no .coolify.json found"),
31
35
  );
32
36
  return;
33
37
  }
@@ -38,9 +38,9 @@ export async function diagnoseAppCommand(query?: string): Promise<void> {
38
38
  const status = d.status?.includes("failed")
39
39
  ? chalk.red(d.status)
40
40
  : chalk.green(d.status);
41
- console.log(
42
- ` ${chalk.gray(d.uuid.slice(0, 12))} ${status} ${chalk.gray(new Date(d.created_at).toLocaleString())}`,
43
- );
41
+ const id = (d.deployment_uuid || d.uuid || "").slice(0, 12) || "-";
42
+ const date = d.created_at ? new Date(d.created_at).toLocaleString() : "";
43
+ console.log(` ${chalk.gray(id)} ${status} ${chalk.gray(date)}`);
44
44
  }
45
45
  }
46
46
 
@@ -6,13 +6,28 @@
6
6
 
7
7
  import { isErr } from "@mks2508/no-throw";
8
8
  import chalk from "chalk";
9
+ import ora from "ora";
9
10
  import { getCoolifyService } from "../../coolify/index.js";
11
+ import { getCliSdk } from "../actions.js";
10
12
  import { resolveUuid } from "../coolify-state.js";
13
+ import { resolveAppNameOrUuid } from "../name-resolver.js";
14
+ import {
15
+ createEnvTable,
16
+ createChangeSummary,
17
+ createSpinner,
18
+ highlightEnvLine,
19
+ createDiff,
20
+ } from "../ui/index.js";
11
21
 
12
22
  interface IEnvCommandOptions {
13
23
  set?: string;
14
24
  delete?: string;
15
25
  buildtime?: boolean;
26
+ "runtime-only"?: boolean;
27
+ sync?: boolean | string;
28
+ "dry-run"?: boolean;
29
+ prune?: boolean;
30
+ table?: boolean;
16
31
  }
17
32
 
18
33
  /**
@@ -26,10 +41,13 @@ export async function envCommand(
26
41
  uuid: string | undefined,
27
42
  options: IEnvCommandOptions = {},
28
43
  ) {
29
- const resolvedUuid = resolveUuid(uuid);
44
+ let resolvedUuid = resolveUuid(uuid);
45
+ if (!resolvedUuid && uuid) {
46
+ resolvedUuid = await resolveAppNameOrUuid(uuid);
47
+ }
30
48
  if (!resolvedUuid) {
31
49
  console.error(
32
- chalk.red("Error: No UUID provided and no .coolify.json found"),
50
+ chalk.red("Error: No UUID/name provided and no .coolify.json found"),
33
51
  );
34
52
  return;
35
53
  }
@@ -52,11 +70,14 @@ export async function envCommand(
52
70
  return;
53
71
  }
54
72
 
73
+ // --runtime-only explicitly sets is_buildtime=false, --buildtime sets it to true
74
+ const isBuildTime = options["runtime-only"] ? false : (options.buildtime ?? false);
75
+
55
76
  const result = await coolify.setEnvironmentVariable(
56
77
  uuid,
57
78
  key,
58
79
  value,
59
- options.buildtime ?? false,
80
+ isBuildTime,
60
81
  );
61
82
 
62
83
  if (isErr(result)) {
@@ -86,6 +107,74 @@ export async function envCommand(
86
107
  return;
87
108
  }
88
109
 
110
+ // Handle --sync [file]
111
+ if (options.sync) {
112
+ const envFile = options.sync === true ? ".env" : options.sync;
113
+
114
+ console.log("");
115
+ console.log(chalk.cyan.bold(" 🔄 Env Sync"));
116
+
117
+ try {
118
+ const sdk = getCliSdk();
119
+ const spinner = createSpinner({
120
+ text: "Reading environment variables...",
121
+ color: "cyan",
122
+ }).start();
123
+
124
+ const result = await sdk.applications.syncEnv(uuid, {
125
+ filePath: envFile,
126
+ dryRun: options["dry-run"] ?? false,
127
+ prune: options.prune ?? false,
128
+ onProgress: (update) => {
129
+ if (update.type === "add") {
130
+ spinner.succeed(
131
+ ` ${chalk.green("+")} ${highlightEnvLine(`${update.key}=${update.value ?? ""}`)}`,
132
+ );
133
+ spinner.text = "Syncing...";
134
+ spinner.start();
135
+ } else if (update.type === "update") {
136
+ const diff = createDiff(
137
+ result.updated.find((u) => u.key === update.key)?.oldValue ?? "",
138
+ update.value ?? "",
139
+ );
140
+ spinner.succeed(
141
+ ` ${chalk.yellow("~")} ${chalk.bold(update.key)} updated`,
142
+ );
143
+ spinner.text = "Syncing...";
144
+ spinner.start();
145
+ } else if (update.type === "remove") {
146
+ spinner.succeed(` ${chalk.red("-")} ${chalk.bold(update.key)} removed`);
147
+ spinner.text = "Syncing...";
148
+ spinner.start();
149
+ }
150
+ },
151
+ });
152
+
153
+ spinner.stop();
154
+
155
+ const totalChanges =
156
+ result.added.length + result.updated.length + result.removed.length;
157
+
158
+ if (totalChanges === 0) {
159
+ console.log("");
160
+ console.log(chalk.green(" ✓ All variables are already in sync"));
161
+ console.log("");
162
+ } else {
163
+ console.log(createChangeSummary(result));
164
+
165
+ if (options["dry-run"]) {
166
+ console.log(chalk.yellow(" ⚠ Dry run mode - no changes applied"));
167
+ } else {
168
+ console.log(chalk.green(` ✓ Synced ${totalChanges} variable(s)`));
169
+ }
170
+ console.log("");
171
+ }
172
+ } catch (error) {
173
+ console.error(chalk.red(` ✗ Error: ${error}`));
174
+ }
175
+ return;
176
+ }
177
+
89
178
  // Default: list env vars
90
179
  const result = await coolify.getEnvironmentVariables(uuid);
91
180
 
@@ -101,27 +190,37 @@ export async function envCommand(
101
190
  return;
102
191
  }
103
192
 
104
- console.log(chalk.cyan(`Environment variables (${envVars.length}):\n`));
105
-
106
- // Separar runtime de buildtime
107
- const runtimeVars = envVars.filter((ev) => ev.is_runtime);
108
- const buildtimeVars = envVars.filter((ev) => ev.is_buildtime);
109
-
110
- if (runtimeVars.length > 0) {
111
- console.log(chalk.yellow.bold("Runtime:"));
112
- for (const ev of runtimeVars) {
113
- const required = ev.is_required ? chalk.red(" *") : "";
114
- console.log(
115
- ` ${chalk.green(ev.key)}${required} = ${chalk.gray(ev.value)}`,
116
- );
193
+ // Use table view if --table flag, otherwise use highlighted list view
194
+ if (options.table) {
195
+ console.log(
196
+ createEnvTable(envVars, {
197
+ compact: true,
198
+ showType: true,
199
+ }),
200
+ );
201
+ } else {
202
+ console.log(chalk.cyan(`Environment variables (${envVars.length}):\n`));
203
+
204
+ // Separar runtime de buildtime
205
+ const runtimeVars = envVars.filter((ev) => ev.is_runtime);
206
+ const buildtimeVars = envVars.filter((ev) => ev.is_buildtime);
207
+
208
+ if (runtimeVars.length > 0) {
209
+ console.log(chalk.yellow.bold("Runtime:"));
210
+ for (const ev of runtimeVars) {
211
+ const required = ev.is_required ? chalk.red(" *") : "";
212
+ const line = `${ev.key}=${ev.value}`;
213
+ console.log(` ${highlightEnvLine(line)}${required}`);
214
+ }
215
+ console.log();
117
216
  }
118
- console.log();
119
- }
120
217
 
121
- if (buildtimeVars.length > 0) {
122
- console.log(chalk.blue.bold("Buildtime:"));
123
- for (const ev of buildtimeVars) {
124
- console.log(` ${chalk.green(ev.key)} = ${chalk.gray(ev.value)}`);
218
+ if (buildtimeVars.length > 0) {
219
+ console.log(chalk.blue.bold("Buildtime:"));
220
+ for (const ev of buildtimeVars) {
221
+ const line = `${ev.key}=${ev.value}`;
222
+ console.log(` ${highlightEnvLine(line)}`);
223
+ }
125
224
  }
126
225
  }
127
226
  }