@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
|
@@ -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
|
-
|
|
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
|
|
8
|
-
import
|
|
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 {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
package/src/cli/commands/env.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
}
|