@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
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
7
8
|
import { isOk, isErr } from "@mks2508/no-throw";
|
|
8
9
|
import ora from "ora";
|
|
9
10
|
import chalk from "chalk";
|
|
10
11
|
import { getCoolifyService } from "../../coolify/index.js";
|
|
12
|
+
import { validatePorts } from "../../utils/format.js";
|
|
13
|
+
import type { ICoolifyGithubApp } from "../../coolify/types.js";
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Create command options.
|
|
@@ -35,9 +38,178 @@ interface ICreateOptions {
|
|
|
35
38
|
dockerfileLocation?: string;
|
|
36
39
|
baseDirectory?: string;
|
|
37
40
|
githubAppUuid?: string;
|
|
41
|
+
privateKeyUuid?: string;
|
|
38
42
|
domain?: string;
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Identifier format accepted by Coolify.
|
|
47
|
+
*
|
|
48
|
+
* Coolify uses two ID formats:
|
|
49
|
+
* - Standard UUID v4 (8-4-4-4-12 hex with dashes)
|
|
50
|
+
* - Laravel-style 24-char alphanumeric IDs (e.g. `awgcco0k48g4kgw8cckkc808`)
|
|
51
|
+
*
|
|
52
|
+
* Both are accepted; we reject only clearly invalid input to fail fast
|
|
53
|
+
* before hitting the API.
|
|
54
|
+
*/
|
|
55
|
+
const ID_REGEX =
|
|
56
|
+
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[a-z0-9]{20,30})$/i;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Valid --type values.
|
|
60
|
+
*/
|
|
61
|
+
const VALID_APP_TYPES = [
|
|
62
|
+
"public",
|
|
63
|
+
"private-github-app",
|
|
64
|
+
"private-deploy-key",
|
|
65
|
+
"dockerfile",
|
|
66
|
+
"docker-image",
|
|
67
|
+
"docker-compose",
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Valid --build-pack values.
|
|
72
|
+
*/
|
|
73
|
+
const VALID_BUILD_PACKS = ["nixpacks", "static", "dockerfile", "dockercompose"] as const;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* TTY detection — used to guard interactive prompts.
|
|
77
|
+
*/
|
|
78
|
+
const isTTY = process.stdout.isTTY === true;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validates create command options and exits with code 1 on failure.
|
|
82
|
+
* Called before any API calls.
|
|
83
|
+
*
|
|
84
|
+
* @param options - Create options to validate
|
|
85
|
+
*/
|
|
86
|
+
function validateCreateOptions(options: ICreateOptions): void {
|
|
87
|
+
// --type validation
|
|
88
|
+
if (options.type && !VALID_APP_TYPES.includes(options.type)) {
|
|
89
|
+
console.error(chalk.red(`Invalid --type: '${options.type}'`));
|
|
90
|
+
console.error(chalk.gray(` Valid values: ${VALID_APP_TYPES.join(", ")}`));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --build_pack validation
|
|
95
|
+
if (options.buildPack && !VALID_BUILD_PACKS.includes(options.buildPack)) {
|
|
96
|
+
console.error(chalk.red(`Invalid --build-pack: '${options.buildPack}'`));
|
|
97
|
+
console.error(chalk.gray(` Valid values: ${VALID_BUILD_PACKS.join(", ")}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Identifier format validations (UUID v4 OR Coolify 24-char ID)
|
|
102
|
+
const idFields: Array<[string, string]> = [
|
|
103
|
+
["--server", options.server],
|
|
104
|
+
["--project", options.project],
|
|
105
|
+
["--environment", options.environment ?? ""],
|
|
106
|
+
["--github-app-uuid", options.githubAppUuid ?? ""],
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const [flag, value] of idFields) {
|
|
110
|
+
if (value && !ID_REGEX.test(value)) {
|
|
111
|
+
console.error(chalk.red(`Invalid ID format for ${flag}: '${value}'`));
|
|
112
|
+
console.error(
|
|
113
|
+
chalk.gray(
|
|
114
|
+
` Expected: UUID v4 (8-4-4-4-12 hex) or Coolify 24-char ID (e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890 or awgcco0k48g4kgw8cckkc808)`,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Required fields
|
|
122
|
+
if (!options.name) {
|
|
123
|
+
console.error(chalk.red("--name is required"));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
if (!options.server) {
|
|
127
|
+
console.error(chalk.red("--server is required"));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
if (!options.project) {
|
|
131
|
+
console.error(chalk.red("--project is required"));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Prompts the user to select a GitHub App from a list of private apps.
|
|
138
|
+
* Skips prompt entirely if --github-app-uuid is passed explicitly.
|
|
139
|
+
*
|
|
140
|
+
* @param apps - List of GitHub apps (should already be filtered to private)
|
|
141
|
+
* @param explicitUuid - The --github-app-uuid passed by the user (optional)
|
|
142
|
+
* @returns The selected app's uuid, or null if cancelled
|
|
143
|
+
*/
|
|
144
|
+
async function promptGithubAppSelection(
|
|
145
|
+
apps: ICoolifyGithubApp[],
|
|
146
|
+
explicitUuid?: string,
|
|
147
|
+
): Promise<string | null> {
|
|
148
|
+
// If --github-app-uuid was passed, validate it exists in the list
|
|
149
|
+
if (explicitUuid) {
|
|
150
|
+
const found = apps.find((a) => a.uuid === explicitUuid);
|
|
151
|
+
if (!found) {
|
|
152
|
+
console.error(
|
|
153
|
+
chalk.red(`GitHub App '${explicitUuid}' not found among configured private apps`),
|
|
154
|
+
);
|
|
155
|
+
if (apps.length > 0) {
|
|
156
|
+
console.error(
|
|
157
|
+
chalk.gray(
|
|
158
|
+
` Available: ${apps.map((a) => `${a.name} (${a.uuid})`).join(", ")}`,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
return explicitUuid;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 0 private apps → loud error
|
|
168
|
+
if (apps.length === 0) {
|
|
169
|
+
console.error(
|
|
170
|
+
chalk.red("No private GitHub Apps found. Cannot create private-github-app deployment."),
|
|
171
|
+
);
|
|
172
|
+
console.error(
|
|
173
|
+
chalk.gray(" Configure a private GitHub App in Coolify: Settings → Sources → GitHub App"),
|
|
174
|
+
);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 1 private app → silently use it
|
|
179
|
+
if (apps.length === 1) {
|
|
180
|
+
return apps[0].uuid;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 2+ private apps → interactive picker
|
|
184
|
+
if (!isTTY) {
|
|
185
|
+
console.error(
|
|
186
|
+
chalk.red(
|
|
187
|
+
"Multiple private GitHub Apps found but stdin is not a TTY. " +
|
|
188
|
+
"Pass --github-app-uuid <uuid> to select one explicitly.",
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const choices = apps.map((app) => ({
|
|
195
|
+
label: app.name,
|
|
196
|
+
value: app.uuid,
|
|
197
|
+
hint: app.organization ?? undefined,
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
const selected = await p.select({
|
|
201
|
+
message: "Select a GitHub App for this deployment:",
|
|
202
|
+
options: choices,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (p.isCancel(selected)) {
|
|
206
|
+
console.error(chalk.yellow("Cancelled."));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return selected as string;
|
|
211
|
+
}
|
|
212
|
+
|
|
41
213
|
/**
|
|
42
214
|
* Create command handler.
|
|
43
215
|
*
|
|
@@ -47,80 +219,147 @@ export async function createCommand(options: ICreateOptions) {
|
|
|
47
219
|
const spinner = ora("Initializing Coolify connection...").start();
|
|
48
220
|
|
|
49
221
|
try {
|
|
222
|
+
// Fail-fast validation before any API calls
|
|
223
|
+
validateCreateOptions(options);
|
|
224
|
+
|
|
50
225
|
const coolify = getCoolifyService();
|
|
51
226
|
const initResult = await coolify.init();
|
|
52
227
|
|
|
53
228
|
if (isErr(initResult)) {
|
|
54
|
-
spinner.fail(
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
return;
|
|
229
|
+
spinner.fail(chalk.red(`Failed to initialize: ${initResult.error.message}`));
|
|
230
|
+
process.exit(1);
|
|
58
231
|
}
|
|
59
232
|
|
|
60
|
-
//
|
|
233
|
+
// ── Environment resolution ─────────────────────────────────────────────
|
|
61
234
|
let environmentUuid: string | undefined = options.environment;
|
|
235
|
+
let environmentName: string | undefined;
|
|
62
236
|
|
|
63
237
|
if (!environmentUuid) {
|
|
64
238
|
spinner.text = "Fetching project environments...";
|
|
65
|
-
|
|
66
239
|
const envResult = await coolify.getProjectEnvironments(options.project);
|
|
67
240
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
241
|
+
if (isErr(envResult)) {
|
|
242
|
+
spinner.fail(
|
|
243
|
+
chalk.red(`Failed to fetch environments: ${envResult.error.message}`),
|
|
244
|
+
);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (envResult.value.length === 0) {
|
|
249
|
+
spinner.fail(
|
|
250
|
+
chalk.red("No environments found for project. Specify --environment <uuid>"),
|
|
251
|
+
);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (envResult.value.length === 1) {
|
|
70
256
|
environmentUuid = envResult.value[0].uuid;
|
|
71
|
-
|
|
257
|
+
environmentName = envResult.value[0].name;
|
|
72
258
|
spinner.info(
|
|
73
|
-
chalk.cyan(`Using environment: ${
|
|
259
|
+
chalk.cyan(`Using only environment: ${environmentName} (${environmentUuid})`),
|
|
74
260
|
);
|
|
75
261
|
} else {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
262
|
+
// Interactive environment picker
|
|
263
|
+
if (!isTTY) {
|
|
264
|
+
spinner.fail(
|
|
265
|
+
chalk.red(
|
|
266
|
+
"No --environment specified and stdin is not a TTY. " +
|
|
267
|
+
"Pass --environment <uuid> explicitly.",
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const choices = envResult.value.map((env) => ({
|
|
274
|
+
label: env.name,
|
|
275
|
+
value: env.uuid,
|
|
276
|
+
hint: env.description || undefined,
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
const selected = await p.select({
|
|
280
|
+
message: "Select an environment:",
|
|
281
|
+
options: choices,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (p.isCancel(selected)) {
|
|
285
|
+
spinner.stop("Cancelled.");
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
environmentUuid = selected as string;
|
|
290
|
+
environmentName = envResult.value.find((e) => e.uuid === environmentUuid)?.name;
|
|
291
|
+
spinner.info(chalk.cyan(`Selected environment: ${environmentName} (${environmentUuid})`));
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
// Environment was provided — validate it exists and get its name
|
|
295
|
+
spinner.text = "Validating environment...";
|
|
296
|
+
const envResult = await coolify.getProjectEnvironments(options.project);
|
|
297
|
+
if (isOk(envResult)) {
|
|
298
|
+
const env = envResult.value.find((e) => e.uuid === environmentUuid);
|
|
299
|
+
if (env) {
|
|
300
|
+
environmentName = env.name;
|
|
301
|
+
} else {
|
|
302
|
+
spinner.fail(
|
|
303
|
+
chalk.red(`Environment '${environmentUuid}' not found in project.`) +
|
|
304
|
+
chalk.gray(
|
|
305
|
+
`\n Available: ${envResult.value.map((e) => `${e.name} (${e.uuid})`).join(", ")}`,
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
82
310
|
}
|
|
83
311
|
}
|
|
84
312
|
|
|
85
|
-
// Ensure environmentUuid is defined before creating application
|
|
86
313
|
if (!environmentUuid) {
|
|
87
314
|
spinner.fail(chalk.red("Environment UUID is required"));
|
|
88
|
-
|
|
315
|
+
process.exit(1);
|
|
89
316
|
}
|
|
90
317
|
|
|
91
|
-
//
|
|
318
|
+
// ── GitHub App resolution ───────────────────────────────────────────────
|
|
92
319
|
let appType = options.type || "public";
|
|
93
320
|
let githubAppUuid = options.githubAppUuid;
|
|
94
321
|
|
|
95
322
|
if (!githubAppUuid && (appType === "private-github-app" || !options.type)) {
|
|
96
323
|
spinner.text = "Detecting GitHub Apps...";
|
|
97
|
-
const ghAppsResult = await coolify.
|
|
324
|
+
const ghAppsResult = await coolify.listGithubAppsAll();
|
|
98
325
|
|
|
99
326
|
if (isOk(ghAppsResult)) {
|
|
100
|
-
|
|
327
|
+
// Filter to private (non-public) GitHub Apps only.
|
|
328
|
+
const privateApps = ghAppsResult.value.filter((app) => !app.is_public);
|
|
329
|
+
|
|
330
|
+
githubAppUuid = await promptGithubAppSelection(privateApps, options.githubAppUuid);
|
|
331
|
+
|
|
332
|
+
if (githubAppUuid === null) {
|
|
333
|
+
// Should not reach here — promptGithubAppSelection exits on cancel
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
101
336
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
337
|
+
appType = "private-github-app";
|
|
338
|
+
const usedApp = privateApps.find((a) => a.uuid === githubAppUuid);
|
|
339
|
+
if (usedApp) {
|
|
105
340
|
spinner.info(
|
|
106
341
|
chalk.cyan(
|
|
107
|
-
`
|
|
342
|
+
`Using GitHub App: ${usedApp.name}` +
|
|
343
|
+
(usedApp.organization ? ` (${usedApp.organization})` : ""),
|
|
108
344
|
),
|
|
109
345
|
);
|
|
110
|
-
} else if (appType === "private-github-app") {
|
|
111
|
-
spinner.warn(
|
|
112
|
-
chalk.yellow("No private GitHub Apps found. Falling back to public type."),
|
|
113
|
-
);
|
|
114
|
-
appType = "public";
|
|
115
346
|
}
|
|
116
347
|
} else if (appType === "private-github-app") {
|
|
117
|
-
spinner.
|
|
118
|
-
chalk.
|
|
348
|
+
spinner.fail(
|
|
349
|
+
chalk.red(`Could not list GitHub Apps: ${ghAppsResult.error.message}`),
|
|
119
350
|
);
|
|
120
|
-
|
|
351
|
+
process.exit(1);
|
|
121
352
|
}
|
|
122
353
|
}
|
|
123
354
|
|
|
355
|
+
// ── Port validation ─────────────────────────────────────────────────────
|
|
356
|
+
const portsStr = options.ports || "3000";
|
|
357
|
+
const portValidation = validatePorts(portsStr);
|
|
358
|
+
if (!portValidation.valid) {
|
|
359
|
+
spinner.fail(chalk.red(`Invalid ports: ${portValidation.error}`));
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
124
363
|
spinner.text = "Creating application...";
|
|
125
364
|
|
|
126
365
|
const result = await coolify.createApplication(
|
|
@@ -129,18 +368,20 @@ export async function createCommand(options: ICreateOptions) {
|
|
|
129
368
|
description: options.description,
|
|
130
369
|
projectUuid: options.project,
|
|
131
370
|
environmentUuid,
|
|
371
|
+
environmentName,
|
|
132
372
|
serverUuid: options.server,
|
|
133
373
|
type: appType,
|
|
134
374
|
githubAppUuid,
|
|
135
375
|
githubRepoUrl: options.repo,
|
|
136
376
|
branch: options.branch || "main",
|
|
137
377
|
buildPack: options.buildPack || "dockerfile",
|
|
138
|
-
portsExposes:
|
|
378
|
+
portsExposes: portsStr,
|
|
139
379
|
dockerImage: options.dockerImage,
|
|
140
380
|
dockerCompose: options.dockerCompose,
|
|
141
381
|
dockerComposeLocation: options.dockerComposeLocation,
|
|
142
382
|
dockerfileLocation: options.dockerfileLocation,
|
|
143
383
|
baseDirectory: options.baseDirectory,
|
|
384
|
+
privateKeyUuid: options.privateKeyUuid,
|
|
144
385
|
},
|
|
145
386
|
(percent, message) => {
|
|
146
387
|
spinner.text = `${chalk.bold(`[${percent}%]`)} ${message}`;
|
|
@@ -167,10 +408,9 @@ export async function createCommand(options: ICreateOptions) {
|
|
|
167
408
|
});
|
|
168
409
|
|
|
169
410
|
if (isOk(updateResult)) {
|
|
170
|
-
domainSpinner.succeed(
|
|
171
|
-
chalk.green(`Domain set: ${chalk.cyan(domainValue)}`),
|
|
172
|
-
);
|
|
411
|
+
domainSpinner.succeed(chalk.green(`Domain set: ${chalk.cyan(domainValue)}`));
|
|
173
412
|
} else {
|
|
413
|
+
// Non-fatal — domain setting failed but app was created
|
|
174
414
|
domainSpinner.fail(
|
|
175
415
|
chalk.red(`Failed to set domain: ${updateResult.error.message}`),
|
|
176
416
|
);
|
|
@@ -194,6 +434,7 @@ export async function createCommand(options: ICreateOptions) {
|
|
|
194
434
|
);
|
|
195
435
|
} else {
|
|
196
436
|
spinner.fail(chalk.red(`Creation failed: ${result.error.message}`));
|
|
437
|
+
process.exit(1);
|
|
197
438
|
}
|
|
198
439
|
} catch (error) {
|
|
199
440
|
spinner.fail(
|
|
@@ -201,5 +442,6 @@ export async function createCommand(options: ICreateOptions) {
|
|
|
201
442
|
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
202
443
|
),
|
|
203
444
|
);
|
|
445
|
+
process.exit(1);
|
|
204
446
|
}
|
|
205
447
|
}
|