@mks2508/coolify-mks-cli-mcp 0.8.0 → 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 +12 -4
- package/dist/cli/coolify-state.d.ts.map +1 -1
- package/dist/cli/index.js +8886 -7957
- package/dist/coolify/config.d.ts +25 -0
- package/dist/coolify/config.d.ts.map +1 -1
- package/dist/coolify/index.d.ts +118 -10
- package/dist/coolify/index.d.ts.map +1 -1
- package/dist/coolify/types.d.ts +61 -1
- package/dist/coolify/types.d.ts.map +1 -1
- package/dist/index.cjs +2267 -227
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2289 -227
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +56 -8
- package/dist/sdk.d.ts.map +1 -1
- package/dist/server/stdio.js +253 -100
- 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 +2 -1
- package/src/cli/commands/create.ts +279 -37
- package/src/cli/commands/env.ts +348 -54
- package/src/cli/commands/init.ts +69 -15
- package/src/cli/commands/main-menu.ts +1 -1
- package/src/cli/commands/projects.ts +3 -3
- package/src/cli/commands/show.ts +39 -10
- package/src/cli/commands/status.ts +23 -7
- package/src/cli/commands/svc.ts +7 -1
- package/src/cli/commands/update.ts +52 -0
- package/src/cli/commands/volumes.ts +293 -0
- package/src/cli/coolify-state.ts +42 -4
- package/src/cli/index.ts +50 -4
- package/src/cli/ui/banner.ts +3 -3
- package/src/cli/ui/screen.ts +26 -2
- package/src/coolify/config.ts +75 -0
- package/src/coolify/index.ts +325 -106
- package/src/coolify/types.ts +62 -1
- package/src/sdk.ts +87 -39
- 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
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { isErr } from "@mks2508/no-throw";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import ora from "ora";
|
|
10
9
|
import { getCoolifyService } from "../../coolify/index.js";
|
|
11
10
|
import { getCliSdk } from "../actions.js";
|
|
12
11
|
import { resolveUuid } from "../coolify-state.js";
|
|
13
12
|
import { resolveAppNameOrUuid } from "../name-resolver.js";
|
|
13
|
+
import { parseEnvContent } from "../../utils/env-parser.js";
|
|
14
14
|
import {
|
|
15
15
|
createEnvTable,
|
|
16
16
|
createChangeSummary,
|
|
@@ -19,19 +19,47 @@ import {
|
|
|
19
19
|
createDiff,
|
|
20
20
|
} from "../ui/index.js";
|
|
21
21
|
|
|
22
|
+
/** Options accepted by the `env` command. */
|
|
22
23
|
interface IEnvCommandOptions {
|
|
23
|
-
set
|
|
24
|
+
/** One or more `KEY=VALUE` pairs to set (commander accumulator → array). */
|
|
25
|
+
set?: string[];
|
|
26
|
+
/** Single key to delete. */
|
|
24
27
|
delete?: string;
|
|
28
|
+
/** Single key to read (prints value only, no decoration). */
|
|
29
|
+
get?: string;
|
|
30
|
+
/** Mark variables as build-time only. */
|
|
25
31
|
buildtime?: boolean;
|
|
32
|
+
/** Mark variables as runtime only. */
|
|
26
33
|
"runtime-only"?: boolean;
|
|
34
|
+
/** Sync from a file (string path) or stdin (`true` + non-TTY) or default `.env`. */
|
|
27
35
|
sync?: boolean | string;
|
|
36
|
+
/** Preview sync changes without applying. */
|
|
28
37
|
"dry-run"?: boolean;
|
|
38
|
+
/** Delete vars not present in the sync source. */
|
|
29
39
|
prune?: boolean;
|
|
40
|
+
/** Force table view instead of highlighted list. */
|
|
30
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;
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
/**
|
|
34
61
|
* Env vars command handler.
|
|
62
|
+
*
|
|
35
63
|
* If no UUID is provided, reads from .coolify.json in the current directory.
|
|
36
64
|
*
|
|
37
65
|
* @param uuid - Application UUID (optional if .coolify.json exists)
|
|
@@ -60,19 +88,91 @@ export async function envCommand(
|
|
|
60
88
|
return;
|
|
61
89
|
}
|
|
62
90
|
|
|
63
|
-
// Handle --set KEY=VALUE
|
|
64
|
-
if (options.set) {
|
|
65
|
-
const [
|
|
66
|
-
const value = valueParts.join("="); // Handle values with = in them
|
|
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);
|
|
67
94
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 });
|
|
71
115
|
}
|
|
72
116
|
|
|
73
|
-
//
|
|
74
|
-
|
|
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
|
+
}
|
|
75
145
|
|
|
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
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Single --set path: delegate to setEnvironmentVariable (which itself
|
|
174
|
+
// delegates to bulk internally — fix for Bug #1).
|
|
175
|
+
const [{ key, value }] = parsed;
|
|
76
176
|
const result = await coolify.setEnvironmentVariable(
|
|
77
177
|
uuid,
|
|
78
178
|
key,
|
|
@@ -85,7 +185,41 @@ export async function envCommand(
|
|
|
85
185
|
return;
|
|
86
186
|
}
|
|
87
187
|
|
|
88
|
-
|
|
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
|
+
}
|
|
89
223
|
return;
|
|
90
224
|
}
|
|
91
225
|
|
|
@@ -101,56 +235,69 @@ export async function envCommand(
|
|
|
101
235
|
return;
|
|
102
236
|
}
|
|
103
237
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
107
251
|
return;
|
|
108
252
|
}
|
|
109
253
|
|
|
110
|
-
// Handle --sync [file]
|
|
111
|
-
if (options.sync) {
|
|
112
|
-
|
|
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
|
+
}
|
|
113
270
|
|
|
114
271
|
console.log("");
|
|
115
272
|
console.log(chalk.cyan.bold(" 🔄 Env Sync"));
|
|
116
273
|
|
|
117
274
|
try {
|
|
118
275
|
const sdk = getCliSdk();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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();
|
|
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
|
+
}
|
|
154
301
|
|
|
155
302
|
const totalChanges =
|
|
156
303
|
result.added.length + result.updated.length + result.removed.length;
|
|
@@ -186,7 +333,17 @@ export async function envCommand(
|
|
|
186
333
|
const envVars = result.value;
|
|
187
334
|
|
|
188
335
|
if (envVars.length === 0) {
|
|
189
|
-
|
|
336
|
+
if (options.json) {
|
|
337
|
+
console.log("[]");
|
|
338
|
+
} else {
|
|
339
|
+
console.log(chalk.yellow("No environment variables found"));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// --json short-circuits all human formatting. Use this for piping into jq etc.
|
|
345
|
+
if (options.json) {
|
|
346
|
+
console.log(JSON.stringify(envVars));
|
|
190
347
|
return;
|
|
191
348
|
}
|
|
192
349
|
|
|
@@ -224,3 +381,140 @@ export async function envCommand(
|
|
|
224
381
|
}
|
|
225
382
|
}
|
|
226
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);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
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 });
|
|
469
|
+
}
|
|
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
|
+
};
|
|
520
|
+
}
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -8,10 +8,12 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { execSync } from "node:child_process";
|
|
10
10
|
import prompts from "prompts";
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
11
12
|
import chalk from "chalk";
|
|
12
13
|
import ora, { Ora } from "ora";
|
|
13
14
|
import { isOk, isErr } from "@mks2508/no-throw";
|
|
14
15
|
import { getCoolifyService } from "../../coolify/index.js";
|
|
16
|
+
import type { ICoolifyGithubApp } from "../../coolify/types.js";
|
|
15
17
|
import { writeCoolifyState, loadCoolifyState, type ICoolifyDeployState } from "../coolify-state.js";
|
|
16
18
|
import {
|
|
17
19
|
createSpinner,
|
|
@@ -22,6 +24,11 @@ import {
|
|
|
22
24
|
createSummaryCard,
|
|
23
25
|
} from "../ui/index.js";
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* TTY detection — guards interactive @clack/prompts usage.
|
|
29
|
+
*/
|
|
30
|
+
const isTTY = process.stdout.isTTY === true;
|
|
31
|
+
|
|
25
32
|
interface IInitOptions {
|
|
26
33
|
yes?: boolean;
|
|
27
34
|
force?: boolean;
|
|
@@ -42,6 +49,8 @@ interface IDockerConfigs {
|
|
|
42
49
|
hasCompose: boolean;
|
|
43
50
|
dockerfilePath?: string;
|
|
44
51
|
composePath?: string;
|
|
52
|
+
/** Ports detected from Dockerfile EXPOSE directives */
|
|
53
|
+
detectedPorts?: number[];
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
interface IExistingApps {
|
|
@@ -446,11 +455,22 @@ function detectDockerConfigs(cwd: string): IDockerConfigs {
|
|
|
446
455
|
}
|
|
447
456
|
}
|
|
448
457
|
|
|
458
|
+
const resolvedDockerfile =
|
|
459
|
+
dockerfilePath || (hasDockerfile ? join(cwd, "Dockerfile") : undefined);
|
|
460
|
+
|
|
461
|
+
// Parse EXPOSE directives from Dockerfile
|
|
462
|
+
let detectedPorts: number[] = [];
|
|
463
|
+
if (resolvedDockerfile) {
|
|
464
|
+
const { parseDockerfileExpose } = require("../../utils/format.js");
|
|
465
|
+
detectedPorts = parseDockerfileExpose(resolvedDockerfile);
|
|
466
|
+
}
|
|
467
|
+
|
|
449
468
|
return {
|
|
450
469
|
hasDockerfile: hasDockerfile || !!dockerfilePath,
|
|
451
470
|
hasCompose,
|
|
452
471
|
dockerfilePath: dockerfilePath || (hasDockerfile ? "Dockerfile" : undefined),
|
|
453
472
|
composePath: hasCompose ? "docker-compose.yml" : undefined,
|
|
473
|
+
detectedPorts,
|
|
454
474
|
};
|
|
455
475
|
}
|
|
456
476
|
|
|
@@ -791,31 +811,62 @@ async function createNewApp(
|
|
|
791
811
|
}
|
|
792
812
|
|
|
793
813
|
// Create app
|
|
794
|
-
const
|
|
814
|
+
const deploySpinner = createSpinner({
|
|
795
815
|
text: "Creating new deployment...",
|
|
796
816
|
color: "green",
|
|
797
817
|
}).start();
|
|
798
818
|
|
|
799
819
|
try {
|
|
800
820
|
// Get GitHub App (required for private-github-app type)
|
|
801
|
-
const ghAppsResult = await coolify.
|
|
802
|
-
if (isErr(ghAppsResult)
|
|
803
|
-
|
|
804
|
-
console.error(chalk.red(
|
|
805
|
-
console.error(chalk.gray(" Configure one in Coolify Settings → Sources"));
|
|
821
|
+
const ghAppsResult = await coolify.listGithubAppsAll();
|
|
822
|
+
if (isErr(ghAppsResult)) {
|
|
823
|
+
deploySpinner.fail("Failed to list GitHub Apps");
|
|
824
|
+
console.error(chalk.red(` ✗ ${ghAppsResult.error.message}`));
|
|
806
825
|
return;
|
|
807
826
|
}
|
|
808
827
|
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
828
|
+
const privateApps = ghAppsResult.value.filter((a: ICoolifyGithubApp) => !a.is_public);
|
|
829
|
+
|
|
830
|
+
// 0 private apps → loud error
|
|
831
|
+
if (privateApps.length === 0) {
|
|
832
|
+
deploySpinner.fail("No private GitHub Apps found");
|
|
833
|
+
console.error(
|
|
834
|
+
chalk.red(" Configure a private GitHub App in Coolify: Settings → Sources → GitHub App"),
|
|
835
|
+
);
|
|
812
836
|
return;
|
|
813
837
|
}
|
|
814
838
|
|
|
839
|
+
// 1 private app → use silently; 2+ → interactive picker
|
|
840
|
+
let selectedGhAppUuid: string;
|
|
841
|
+
if (privateApps.length === 1) {
|
|
842
|
+
selectedGhAppUuid = privateApps[0].uuid;
|
|
843
|
+
} else {
|
|
844
|
+
if (!isTTY) {
|
|
845
|
+
deploySpinner.fail(
|
|
846
|
+
"Multiple private GitHub Apps found but stdin is not a TTY. " +
|
|
847
|
+
"Pass --github-app-uuid to select explicitly.",
|
|
848
|
+
);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const selected = await p.select({
|
|
852
|
+
message: "Select a GitHub App:",
|
|
853
|
+
options: privateApps.map((app) => ({
|
|
854
|
+
label: app.name,
|
|
855
|
+
value: app.uuid,
|
|
856
|
+
hint: app.organization ?? undefined,
|
|
857
|
+
})),
|
|
858
|
+
});
|
|
859
|
+
if (p.isCancel(selected)) {
|
|
860
|
+
deploySpinner.stop("Cancelled.");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
selectedGhAppUuid = selected as string;
|
|
864
|
+
}
|
|
865
|
+
|
|
815
866
|
// Get environments for the project
|
|
816
867
|
const envsResult = await coolify.getProjectEnvironments(projectUuid);
|
|
817
868
|
if (isErr(envsResult) || envsResult.value.length === 0) {
|
|
818
|
-
|
|
869
|
+
deploySpinner.fail("No environments found");
|
|
819
870
|
console.error(chalk.red(" ✗ No environments found for project"));
|
|
820
871
|
return;
|
|
821
872
|
}
|
|
@@ -828,24 +879,27 @@ async function createNewApp(
|
|
|
828
879
|
environmentUuid,
|
|
829
880
|
serverUuid,
|
|
830
881
|
type: "private-github-app",
|
|
831
|
-
githubAppUuid:
|
|
882
|
+
githubAppUuid: selectedGhAppUuid,
|
|
832
883
|
githubRepoUrl: gitInfo.repoUrl,
|
|
833
884
|
branch: gitInfo.branch,
|
|
834
885
|
buildPack,
|
|
835
|
-
portsExposes:
|
|
886
|
+
portsExposes:
|
|
887
|
+
dockerConfigs.detectedPorts && dockerConfigs.detectedPorts.length > 0
|
|
888
|
+
? dockerConfigs.detectedPorts.join(",")
|
|
889
|
+
: "3000",
|
|
836
890
|
baseDirectory: "/",
|
|
837
891
|
isAutoDeployEnabled: true,
|
|
838
892
|
});
|
|
839
893
|
|
|
840
894
|
if (isErr(newAppResult)) {
|
|
841
|
-
|
|
895
|
+
deploySpinner.fail("Failed to create deployment");
|
|
842
896
|
console.error(chalk.red(` ✗ ${newAppResult.error.message}`));
|
|
843
897
|
return;
|
|
844
898
|
}
|
|
845
899
|
|
|
846
900
|
const newApp = newAppResult.value;
|
|
847
901
|
|
|
848
|
-
|
|
902
|
+
deploySpinner.succeed(`Deployment ${chalk.bold.white(appName)} created`);
|
|
849
903
|
|
|
850
904
|
// Get project and environment names for the state file
|
|
851
905
|
const project = projects.find((p: any) => p.uuid === projectUuid);
|
|
@@ -914,7 +968,7 @@ async function createNewApp(
|
|
|
914
968
|
);
|
|
915
969
|
console.log("");
|
|
916
970
|
} catch (error) {
|
|
917
|
-
|
|
971
|
+
deploySpinner.fail("Failed to create deployment");
|
|
918
972
|
console.error(chalk.red(` ✗ ${error}`));
|
|
919
973
|
console.log("");
|
|
920
974
|
}
|
|
@@ -14,7 +14,7 @@ export async function mainMenu(): Promise<void> {
|
|
|
14
14
|
if (process.stdout.isTTY) {
|
|
15
15
|
console.clear();
|
|
16
16
|
}
|
|
17
|
-
showAutoBanner("0.
|
|
17
|
+
showAutoBanner("0.9.0");
|
|
18
18
|
|
|
19
19
|
// Go directly to status which shows tree + interactive navigation
|
|
20
20
|
await (await import("./status.js")).statusCommand();
|
|
@@ -54,7 +54,7 @@ export async function projectsCommand(
|
|
|
54
54
|
|
|
55
55
|
// Create mode
|
|
56
56
|
if (options.create) {
|
|
57
|
-
const
|
|
57
|
+
const creatingSpinner = createSpinner({
|
|
58
58
|
text: `Creating project "${options.create}"...`,
|
|
59
59
|
color: "green",
|
|
60
60
|
}).start();
|
|
@@ -65,7 +65,7 @@ export async function projectsCommand(
|
|
|
65
65
|
);
|
|
66
66
|
|
|
67
67
|
if (isOk(createResult)) {
|
|
68
|
-
|
|
68
|
+
creatingSpinner.succeed(`Project ${chalk.bold.white(options.create)} created`);
|
|
69
69
|
console.log("");
|
|
70
70
|
console.log(createSummaryCard("Project Details", {
|
|
71
71
|
"Name": { value: options.create, color: chalk.white },
|
|
@@ -74,7 +74,7 @@ export async function projectsCommand(
|
|
|
74
74
|
}));
|
|
75
75
|
console.log("");
|
|
76
76
|
} else {
|
|
77
|
-
|
|
77
|
+
creatingSpinner.fail("Failed to create project");
|
|
78
78
|
console.error(chalk.red(` ✗ ${createResult.error.message}`));
|
|
79
79
|
}
|
|
80
80
|
return;
|