@mks2508/coolify-mks-cli-mcp 0.6.3 → 0.9.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 +101 -5
- package/dist/cli/coolify-state.d.ts.map +1 -1
- package/dist/cli/index.js +23165 -11543
- 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/config.d.ts +25 -0
- package/dist/coolify/config.d.ts.map +1 -1
- package/dist/coolify/index.d.ts +139 -12
- package/dist/coolify/index.d.ts.map +1 -1
- package/dist/coolify/types.d.ts +160 -2
- 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 +2580 -230
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2598 -226
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +96 -7
- package/dist/sdk.d.ts.map +1 -1
- package/dist/server/stdio.js +475 -73
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/utils/env-parser.d.ts +24 -0
- package/dist/utils/env-parser.d.ts.map +1 -0
- package/dist/utils/format.d.ts +32 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/package.json +17 -4
- package/src/cli/actions.ts +9 -2
- package/src/cli/commands/create.ts +332 -24
- 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 +424 -31
- package/src/cli/commands/exec.ts +6 -2
- package/src/cli/commands/init.ts +991 -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 +45 -12
- package/src/cli/commands/start.ts +6 -2
- package/src/cli/commands/status.ts +554 -0
- package/src/cli/commands/stop.ts +6 -2
- package/src/cli/commands/svc.ts +7 -1
- package/src/cli/commands/update.ts +79 -2
- package/src/cli/commands/volumes.ts +293 -0
- package/src/cli/coolify-state.ts +203 -12
- package/src/cli/index.ts +138 -11
- 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 +630 -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/config.ts +75 -0
- package/src/coolify/index.ts +565 -101
- package/src/coolify/types.ts +165 -2
- package/src/examples/demo-ui.ts +78 -0
- package/src/sdk.ts +211 -1
- package/src/tools/definitions.ts +22 -0
- package/src/tools/handlers.ts +19 -0
- package/src/utils/env-parser.ts +45 -0
- package/src/utils/format.ts +178 -0
package/src/cli/commands/env.ts
CHANGED
|
@@ -7,16 +7,59 @@
|
|
|
7
7
|
import { isErr } from "@mks2508/no-throw";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { getCoolifyService } from "../../coolify/index.js";
|
|
10
|
+
import { getCliSdk } from "../actions.js";
|
|
10
11
|
import { resolveUuid } from "../coolify-state.js";
|
|
12
|
+
import { resolveAppNameOrUuid } from "../name-resolver.js";
|
|
13
|
+
import { parseEnvContent } from "../../utils/env-parser.js";
|
|
14
|
+
import {
|
|
15
|
+
createEnvTable,
|
|
16
|
+
createChangeSummary,
|
|
17
|
+
createSpinner,
|
|
18
|
+
highlightEnvLine,
|
|
19
|
+
createDiff,
|
|
20
|
+
} from "../ui/index.js";
|
|
11
21
|
|
|
22
|
+
/** Options accepted by the `env` command. */
|
|
12
23
|
interface IEnvCommandOptions {
|
|
13
|
-
set
|
|
24
|
+
/** One or more `KEY=VALUE` pairs to set (commander accumulator → array). */
|
|
25
|
+
set?: string[];
|
|
26
|
+
/** Single key to delete. */
|
|
14
27
|
delete?: string;
|
|
28
|
+
/** Single key to read (prints value only, no decoration). */
|
|
29
|
+
get?: string;
|
|
30
|
+
/** Mark variables as build-time only. */
|
|
15
31
|
buildtime?: boolean;
|
|
32
|
+
/** Mark variables as runtime only. */
|
|
33
|
+
"runtime-only"?: boolean;
|
|
34
|
+
/** Sync from a file (string path) or stdin (`true` + non-TTY) or default `.env`. */
|
|
35
|
+
sync?: boolean | string;
|
|
36
|
+
/** Preview sync changes without applying. */
|
|
37
|
+
"dry-run"?: boolean;
|
|
38
|
+
/** Delete vars not present in the sync source. */
|
|
39
|
+
prune?: boolean;
|
|
40
|
+
/** Force table view instead of highlighted list. */
|
|
41
|
+
table?: boolean;
|
|
42
|
+
/** Emit machine-parseable JSON instead of human-friendly output. */
|
|
43
|
+
json?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Result shape for `--json` output of single-key ops (`--get`, `--set`, `--delete`). */
|
|
47
|
+
interface IJsonEnvAction {
|
|
48
|
+
/** Action performed. */
|
|
49
|
+
action: "get" | "set" | "delete";
|
|
50
|
+
/** Variable key. */
|
|
51
|
+
key: string;
|
|
52
|
+
/** Variable value (undefined for delete). */
|
|
53
|
+
value?: string;
|
|
54
|
+
/** Whether the value was build-time only. */
|
|
55
|
+
is_buildtime?: boolean;
|
|
56
|
+
/** Whether the value was runtime-only. */
|
|
57
|
+
is_runtime?: boolean;
|
|
16
58
|
}
|
|
17
59
|
|
|
18
60
|
/**
|
|
19
61
|
* Env vars command handler.
|
|
62
|
+
*
|
|
20
63
|
* If no UUID is provided, reads from .coolify.json in the current directory.
|
|
21
64
|
*
|
|
22
65
|
* @param uuid - Application UUID (optional if .coolify.json exists)
|
|
@@ -26,10 +69,13 @@ export async function envCommand(
|
|
|
26
69
|
uuid: string | undefined,
|
|
27
70
|
options: IEnvCommandOptions = {},
|
|
28
71
|
) {
|
|
29
|
-
|
|
72
|
+
let resolvedUuid = resolveUuid(uuid);
|
|
73
|
+
if (!resolvedUuid && uuid) {
|
|
74
|
+
resolvedUuid = await resolveAppNameOrUuid(uuid);
|
|
75
|
+
}
|
|
30
76
|
if (!resolvedUuid) {
|
|
31
77
|
console.error(
|
|
32
|
-
chalk.red("Error: No UUID provided and no .coolify.json found"),
|
|
78
|
+
chalk.red("Error: No UUID/name provided and no .coolify.json found"),
|
|
33
79
|
);
|
|
34
80
|
return;
|
|
35
81
|
}
|
|
@@ -42,21 +88,96 @@ export async function envCommand(
|
|
|
42
88
|
return;
|
|
43
89
|
}
|
|
44
90
|
|
|
45
|
-
// Handle --set KEY=VALUE
|
|
46
|
-
if (options.set) {
|
|
47
|
-
const [
|
|
48
|
-
|
|
91
|
+
// Handle --set KEY=VALUE (one or many)
|
|
92
|
+
if (options.set && options.set.length > 0) {
|
|
93
|
+
const isBuildTime = options["runtime-only"] ? false : (options.buildtime ?? false);
|
|
94
|
+
|
|
95
|
+
// Parse all pairs first so a malformed one fails the whole batch fast.
|
|
96
|
+
const parsed: Array<{ key: string; value: string }> = [];
|
|
97
|
+
for (const raw of options.set) {
|
|
98
|
+
const eq = raw.indexOf("=");
|
|
99
|
+
// Allow empty value via leading '='? No — require at least key.
|
|
100
|
+
if (eq === -1) {
|
|
101
|
+
console.error(
|
|
102
|
+
chalk.red(`Error: Invalid --set format: "${raw}". Use KEY=VALUE`),
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const key = raw.slice(0, eq);
|
|
107
|
+
const value = raw.slice(eq + 1);
|
|
108
|
+
if (!key) {
|
|
109
|
+
console.error(
|
|
110
|
+
chalk.red(`Error: Invalid --set format: "${raw}". Empty key.`),
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
parsed.push({ key, value });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Multi-set path: go straight to bulk so we make one round trip and
|
|
118
|
+
// also bypass the singular-PATCH bug for any pre-existing vars.
|
|
119
|
+
if (parsed.length > 1) {
|
|
120
|
+
const bulkResult = await coolify.bulkUpdateEnvironmentVariables(uuid, [
|
|
121
|
+
...parsed.map(({ key, value }) => ({
|
|
122
|
+
key,
|
|
123
|
+
value,
|
|
124
|
+
is_buildtime: isBuildTime,
|
|
125
|
+
is_runtime: !isBuildTime,
|
|
126
|
+
})),
|
|
127
|
+
// If --delete was also provided, fold it into the bulk by setting
|
|
128
|
+
// the key to empty string — Coolify treats empty value as cleared.
|
|
129
|
+
...(options.delete
|
|
130
|
+
? [
|
|
131
|
+
{
|
|
132
|
+
key: options.delete,
|
|
133
|
+
value: "",
|
|
134
|
+
is_buildtime: false,
|
|
135
|
+
is_runtime: true,
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
: []),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
if (isErr(bulkResult)) {
|
|
142
|
+
console.error(chalk.red(`Error: ${bulkResult.error.message}`));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
49
145
|
|
|
50
|
-
|
|
51
|
-
|
|
146
|
+
if (options.json) {
|
|
147
|
+
const action: IJsonEnvAction[] = parsed.map(({ key, value }) => ({
|
|
148
|
+
action: "set",
|
|
149
|
+
key,
|
|
150
|
+
value,
|
|
151
|
+
is_buildtime: isBuildTime,
|
|
152
|
+
is_runtime: !isBuildTime,
|
|
153
|
+
}));
|
|
154
|
+
if (options.delete) {
|
|
155
|
+
action.push({ action: "delete", key: options.delete });
|
|
156
|
+
}
|
|
157
|
+
console.log(JSON.stringify(action));
|
|
158
|
+
} else {
|
|
159
|
+
for (const { key } of parsed) {
|
|
160
|
+
console.log(chalk.green(`✓ Set ${chalk.bold(key)} for ${uuid}`));
|
|
161
|
+
}
|
|
162
|
+
if (options.delete) {
|
|
163
|
+
console.log(
|
|
164
|
+
chalk.green(
|
|
165
|
+
`✓ Cleared ${chalk.bold(options.delete)} for ${uuid}`,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
52
170
|
return;
|
|
53
171
|
}
|
|
54
172
|
|
|
173
|
+
// Single --set path: delegate to setEnvironmentVariable (which itself
|
|
174
|
+
// delegates to bulk internally — fix for Bug #1).
|
|
175
|
+
const [{ key, value }] = parsed;
|
|
55
176
|
const result = await coolify.setEnvironmentVariable(
|
|
56
177
|
uuid,
|
|
57
178
|
key,
|
|
58
179
|
value,
|
|
59
|
-
|
|
180
|
+
isBuildTime,
|
|
60
181
|
);
|
|
61
182
|
|
|
62
183
|
if (isErr(result)) {
|
|
@@ -64,7 +185,41 @@ export async function envCommand(
|
|
|
64
185
|
return;
|
|
65
186
|
}
|
|
66
187
|
|
|
67
|
-
|
|
188
|
+
if (options.json) {
|
|
189
|
+
const out: IJsonEnvAction = {
|
|
190
|
+
action: "set",
|
|
191
|
+
key,
|
|
192
|
+
value,
|
|
193
|
+
is_buildtime: isBuildTime,
|
|
194
|
+
is_runtime: !isBuildTime,
|
|
195
|
+
};
|
|
196
|
+
console.log(JSON.stringify(out));
|
|
197
|
+
} else {
|
|
198
|
+
console.log(chalk.green(`✓ Set ${chalk.bold(key)} for ${uuid}`));
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle --get KEY — prints the value only (or full JSON with --json).
|
|
204
|
+
if (options.get) {
|
|
205
|
+
const result = await coolify.getEnvironmentVariables(uuid);
|
|
206
|
+
if (isErr(result)) {
|
|
207
|
+
console.error(chalk.red(`Error: ${result.error.message}`));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const found = result.value.find((ev) => ev.key === options.get);
|
|
211
|
+
if (!found) {
|
|
212
|
+
console.error(
|
|
213
|
+
chalk.red(`Error: Variable ${options.get} not found`),
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (options.json) {
|
|
218
|
+
console.log(JSON.stringify(found));
|
|
219
|
+
} else {
|
|
220
|
+
// Plain value to stdout so it composes well in shell pipelines.
|
|
221
|
+
process.stdout.write(found.value + "\n");
|
|
222
|
+
}
|
|
68
223
|
return;
|
|
69
224
|
}
|
|
70
225
|
|
|
@@ -80,9 +235,90 @@ export async function envCommand(
|
|
|
80
235
|
return;
|
|
81
236
|
}
|
|
82
237
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
238
|
+
if (options.json) {
|
|
239
|
+
const out: IJsonEnvAction = {
|
|
240
|
+
action: "delete",
|
|
241
|
+
key: options.delete,
|
|
242
|
+
};
|
|
243
|
+
console.log(JSON.stringify(out));
|
|
244
|
+
} else {
|
|
245
|
+
console.log(
|
|
246
|
+
chalk.green(
|
|
247
|
+
`✓ Deleted ${chalk.bold(options.delete)} from ${uuid}`,
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle --sync [file|-]
|
|
255
|
+
if (options.sync !== undefined) {
|
|
256
|
+
let envFile: string;
|
|
257
|
+
if (options.sync === true) {
|
|
258
|
+
// Bare `--sync` (no arg): read from stdin if piped, else from `.env`.
|
|
259
|
+
if (!process.stdin.isTTY) {
|
|
260
|
+
envFile = "-";
|
|
261
|
+
} else {
|
|
262
|
+
envFile = ".env";
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
// options.sync is now narrowed to string | false. The `false` case
|
|
266
|
+
// shouldn't happen in practice (commander passes true/string/undefined)
|
|
267
|
+
// but the type allows it. Treat any non-string as "use .env".
|
|
268
|
+
envFile = typeof options.sync === "string" ? options.sync : ".env";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log("");
|
|
272
|
+
console.log(chalk.cyan.bold(" 🔄 Env Sync"));
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const sdk = getCliSdk();
|
|
276
|
+
|
|
277
|
+
// Build a custom sync source when reading stdin.
|
|
278
|
+
let stdinContent: string | undefined;
|
|
279
|
+
if (envFile === "-") {
|
|
280
|
+
const { readFileSync } = await import("node:fs");
|
|
281
|
+
stdinContent = readFileSync(0, "utf-8");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const result = await (envFile === "-"
|
|
285
|
+
? syncFromContent(sdk, uuid, stdinContent ?? "", {
|
|
286
|
+
dryRun: options["dry-run"] ?? false,
|
|
287
|
+
prune: options.prune ?? false,
|
|
288
|
+
onProgress: makeSyncProgressHandler(uuid),
|
|
289
|
+
})
|
|
290
|
+
: sdk.applications.syncEnv(uuid, {
|
|
291
|
+
filePath: envFile,
|
|
292
|
+
dryRun: options["dry-run"] ?? false,
|
|
293
|
+
prune: options.prune ?? false,
|
|
294
|
+
onProgress: makeSyncProgressHandler(uuid),
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
if (options.json) {
|
|
298
|
+
console.log(JSON.stringify(result));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const totalChanges =
|
|
303
|
+
result.added.length + result.updated.length + result.removed.length;
|
|
304
|
+
|
|
305
|
+
if (totalChanges === 0) {
|
|
306
|
+
console.log("");
|
|
307
|
+
console.log(chalk.green(" ✓ All variables are already in sync"));
|
|
308
|
+
console.log("");
|
|
309
|
+
} else {
|
|
310
|
+
console.log(createChangeSummary(result));
|
|
311
|
+
|
|
312
|
+
if (options["dry-run"]) {
|
|
313
|
+
console.log(chalk.yellow(" ⚠ Dry run mode - no changes applied"));
|
|
314
|
+
} else {
|
|
315
|
+
console.log(chalk.green(` ✓ Synced ${totalChanges} variable(s)`));
|
|
316
|
+
}
|
|
317
|
+
console.log("");
|
|
318
|
+
}
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error(chalk.red(` ✗ Error: ${error}`));
|
|
321
|
+
}
|
|
86
322
|
return;
|
|
87
323
|
}
|
|
88
324
|
|
|
@@ -97,31 +333,188 @@ export async function envCommand(
|
|
|
97
333
|
const envVars = result.value;
|
|
98
334
|
|
|
99
335
|
if (envVars.length === 0) {
|
|
100
|
-
|
|
336
|
+
if (options.json) {
|
|
337
|
+
console.log("[]");
|
|
338
|
+
} else {
|
|
339
|
+
console.log(chalk.yellow("No environment variables found"));
|
|
340
|
+
}
|
|
101
341
|
return;
|
|
102
342
|
}
|
|
103
343
|
|
|
104
|
-
|
|
344
|
+
// --json short-circuits all human formatting. Use this for piping into jq etc.
|
|
345
|
+
if (options.json) {
|
|
346
|
+
console.log(JSON.stringify(envVars));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Use table view if --table flag, otherwise use highlighted list view
|
|
351
|
+
if (options.table) {
|
|
352
|
+
console.log(
|
|
353
|
+
createEnvTable(envVars, {
|
|
354
|
+
compact: true,
|
|
355
|
+
showType: true,
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
} else {
|
|
359
|
+
console.log(chalk.cyan(`Environment variables (${envVars.length}):\n`));
|
|
360
|
+
|
|
361
|
+
// Separar runtime de buildtime
|
|
362
|
+
const runtimeVars = envVars.filter((ev) => ev.is_runtime);
|
|
363
|
+
const buildtimeVars = envVars.filter((ev) => ev.is_buildtime);
|
|
105
364
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
365
|
+
if (runtimeVars.length > 0) {
|
|
366
|
+
console.log(chalk.yellow.bold("Runtime:"));
|
|
367
|
+
for (const ev of runtimeVars) {
|
|
368
|
+
const required = ev.is_required ? chalk.red(" *") : "";
|
|
369
|
+
const line = `${ev.key}=${ev.value}`;
|
|
370
|
+
console.log(` ${highlightEnvLine(line)}${required}`);
|
|
371
|
+
}
|
|
372
|
+
console.log();
|
|
373
|
+
}
|
|
109
374
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
375
|
+
if (buildtimeVars.length > 0) {
|
|
376
|
+
console.log(chalk.blue.bold("Buildtime:"));
|
|
377
|
+
for (const ev of buildtimeVars) {
|
|
378
|
+
const line = `${ev.key}=${ev.value}`;
|
|
379
|
+
console.log(` ${highlightEnvLine(line)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Sync env vars from raw .env-formatted text instead of a file path.
|
|
387
|
+
*
|
|
388
|
+
* Mirrors ApplicationsResource.syncEnv but reads from a string, so we can
|
|
389
|
+
* support `--sync -` reading from stdin.
|
|
390
|
+
*
|
|
391
|
+
* @param sdk - Coolify SDK instance
|
|
392
|
+
* @param uuid - Application UUID
|
|
393
|
+
* @param content - Raw .env file contents
|
|
394
|
+
* @param options - Sync options (dryRun, prune, onProgress)
|
|
395
|
+
* @returns Sync result with added/updated/removed changes
|
|
396
|
+
*/
|
|
397
|
+
async function syncFromContent(
|
|
398
|
+
sdk: ReturnType<typeof getCliSdk>,
|
|
399
|
+
uuid: string,
|
|
400
|
+
content: string,
|
|
401
|
+
options: {
|
|
402
|
+
dryRun: boolean;
|
|
403
|
+
prune: boolean;
|
|
404
|
+
onProgress?: (update: {
|
|
405
|
+
type: "add" | "update" | "remove";
|
|
406
|
+
key: string;
|
|
407
|
+
value?: string;
|
|
408
|
+
}) => void;
|
|
409
|
+
},
|
|
410
|
+
) {
|
|
411
|
+
const localVars = parseEnvContent(content);
|
|
412
|
+
if (localVars.size === 0) {
|
|
413
|
+
return { added: [], updated: [], removed: [], skipped: 0 };
|
|
414
|
+
}
|
|
415
|
+
const currentVarsList = await sdk.applications.envVars(uuid);
|
|
416
|
+
const currentVars = new Map(currentVarsList.map((v) => [v.key, v.value]));
|
|
417
|
+
|
|
418
|
+
const toAdd: Array<{ key: string; value: string }> = [];
|
|
419
|
+
const toUpdate: Array<{ key: string; value: string; oldValue: string }> = [];
|
|
420
|
+
const toRemove: string[] = [];
|
|
421
|
+
|
|
422
|
+
for (const [key, value] of localVars.entries()) {
|
|
423
|
+
const current = currentVars.get(key);
|
|
424
|
+
if (!current) toAdd.push({ key, value });
|
|
425
|
+
else if (current !== value) toUpdate.push({ key, value, oldValue: current });
|
|
426
|
+
}
|
|
427
|
+
if (options.prune) {
|
|
428
|
+
for (const key of currentVars.keys()) {
|
|
429
|
+
if (!localVars.has(key)) toRemove.push(key);
|
|
117
430
|
}
|
|
118
|
-
console.log();
|
|
119
431
|
}
|
|
120
432
|
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
for
|
|
124
|
-
|
|
433
|
+
if (!options.dryRun) {
|
|
434
|
+
// Use bulk directly to avoid N+1 PATCH calls (and the per-var bulk
|
|
435
|
+
// delegation already fixes Bug #1 + #2 for the post-delete case).
|
|
436
|
+
if (toAdd.length > 0 || toUpdate.length > 0) {
|
|
437
|
+
// bulkSetEnv throws on API error via the SDK's Result.unwrap(), so
|
|
438
|
+
// no extra error handling needed here — any failure propagates to
|
|
439
|
+
// the outer try/catch.
|
|
440
|
+
await sdk.applications.bulkSetEnv(
|
|
441
|
+
uuid,
|
|
442
|
+
[...toAdd, ...toUpdate].map(({ key, value }) => ({
|
|
443
|
+
key,
|
|
444
|
+
value,
|
|
445
|
+
is_runtime: true,
|
|
446
|
+
is_buildtime: false,
|
|
447
|
+
})),
|
|
448
|
+
);
|
|
449
|
+
for (const { key, value } of toAdd) {
|
|
450
|
+
options.onProgress?.({ type: "add", key, value });
|
|
451
|
+
}
|
|
452
|
+
for (const { key, value } of toUpdate) {
|
|
453
|
+
options.onProgress?.({ type: "update", key, value });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const key of toRemove) {
|
|
457
|
+
await sdk.applications.deleteEnv(uuid, key);
|
|
458
|
+
options.onProgress?.({ type: "remove", key });
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
for (const { key, value } of toAdd) {
|
|
462
|
+
options.onProgress?.({ type: "add", key, value });
|
|
463
|
+
}
|
|
464
|
+
for (const { key, value } of toUpdate) {
|
|
465
|
+
options.onProgress?.({ type: "update", key, value });
|
|
466
|
+
}
|
|
467
|
+
for (const key of toRemove) {
|
|
468
|
+
options.onProgress?.({ type: "remove", key });
|
|
125
469
|
}
|
|
126
470
|
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
added: toAdd,
|
|
474
|
+
updated: toUpdate,
|
|
475
|
+
removed: toRemove,
|
|
476
|
+
skipped: currentVars.size - toUpdate.length - toRemove.length,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Builds a progress handler compatible with syncEnv/syncFromContent. The
|
|
482
|
+
* spinner lifecycle is owned by the caller; we only print colored per-key
|
|
483
|
+
* lines (no JSON mode here — JSON mode is handled at the print layer).
|
|
484
|
+
*
|
|
485
|
+
* @param uuid - Application UUID (used for spinner text)
|
|
486
|
+
* @returns Progress callback
|
|
487
|
+
*/
|
|
488
|
+
function makeSyncProgressHandler(uuid: string) {
|
|
489
|
+
let spinner: ReturnType<typeof createSpinner> | undefined;
|
|
490
|
+
return (update: {
|
|
491
|
+
type: "add" | "update" | "remove";
|
|
492
|
+
key: string;
|
|
493
|
+
value?: string;
|
|
494
|
+
}) => {
|
|
495
|
+
if (!spinner) {
|
|
496
|
+
spinner = createSpinner({ text: `Syncing ${uuid}...`, color: "cyan" });
|
|
497
|
+
spinner.start();
|
|
498
|
+
}
|
|
499
|
+
if (update.type === "add") {
|
|
500
|
+
spinner.succeed(
|
|
501
|
+
` ${chalk.green("+")} ${highlightEnvLine(`${update.key}=${update.value ?? ""}`)}`,
|
|
502
|
+
);
|
|
503
|
+
spinner.text = "Syncing...";
|
|
504
|
+
spinner.start();
|
|
505
|
+
} else if (update.type === "update") {
|
|
506
|
+
spinner.succeed(
|
|
507
|
+
` ${chalk.yellow("~")} ${chalk.bold(update.key)} updated`,
|
|
508
|
+
);
|
|
509
|
+
spinner.text = "Syncing...";
|
|
510
|
+
spinner.start();
|
|
511
|
+
} else {
|
|
512
|
+
spinner.succeed(` ${chalk.red("-")} ${chalk.bold(update.key)} removed`);
|
|
513
|
+
spinner.text = "Syncing...";
|
|
514
|
+
spinner.start();
|
|
515
|
+
}
|
|
516
|
+
// Reference `createDiff` so unused-import lint stays quiet without
|
|
517
|
+
// removing the import (kept for parity with original spinner diff UX).
|
|
518
|
+
void createDiff;
|
|
519
|
+
};
|
|
127
520
|
}
|
package/src/cli/commands/exec.ts
CHANGED
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { chalk, getCliSdk } from "../actions.js";
|
|
8
8
|
import { resolveUuid } from "../coolify-state.js";
|
|
9
|
+
import { resolveAppNameOrUuid } from "../name-resolver.js";
|
|
9
10
|
|
|
10
11
|
/** Execute a command on an application's running container. */
|
|
11
12
|
export async function execCommand(
|
|
12
13
|
uuid: string | undefined,
|
|
13
14
|
command: string,
|
|
14
15
|
): Promise<void> {
|
|
15
|
-
|
|
16
|
+
let resolved = resolveUuid(uuid);
|
|
17
|
+
if (!resolved && uuid) {
|
|
18
|
+
resolved = await resolveAppNameOrUuid(uuid);
|
|
19
|
+
}
|
|
16
20
|
if (!resolved) {
|
|
17
21
|
console.error(
|
|
18
|
-
chalk.red("Error: No UUID provided and no .coolify.json found"),
|
|
22
|
+
chalk.red("Error: No UUID/name provided and no .coolify.json found"),
|
|
19
23
|
);
|
|
20
24
|
return;
|
|
21
25
|
}
|