@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.
Files changed (45) hide show
  1. package/dist/cli/coolify-state.d.ts +12 -4
  2. package/dist/cli/coolify-state.d.ts.map +1 -1
  3. package/dist/cli/index.js +8886 -7957
  4. package/dist/coolify/config.d.ts +25 -0
  5. package/dist/coolify/config.d.ts.map +1 -1
  6. package/dist/coolify/index.d.ts +118 -10
  7. package/dist/coolify/index.d.ts.map +1 -1
  8. package/dist/coolify/types.d.ts +61 -1
  9. package/dist/coolify/types.d.ts.map +1 -1
  10. package/dist/index.cjs +2267 -227
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +2289 -227
  13. package/dist/index.js.map +1 -1
  14. package/dist/sdk.d.ts +56 -8
  15. package/dist/sdk.d.ts.map +1 -1
  16. package/dist/server/stdio.js +253 -100
  17. package/dist/tools/definitions.d.ts.map +1 -1
  18. package/dist/tools/handlers.d.ts.map +1 -1
  19. package/dist/utils/env-parser.d.ts +24 -0
  20. package/dist/utils/env-parser.d.ts.map +1 -0
  21. package/dist/utils/format.d.ts +32 -0
  22. package/dist/utils/format.d.ts.map +1 -1
  23. package/package.json +2 -1
  24. package/src/cli/commands/create.ts +279 -37
  25. package/src/cli/commands/env.ts +348 -54
  26. package/src/cli/commands/init.ts +69 -15
  27. package/src/cli/commands/main-menu.ts +1 -1
  28. package/src/cli/commands/projects.ts +3 -3
  29. package/src/cli/commands/show.ts +39 -10
  30. package/src/cli/commands/status.ts +23 -7
  31. package/src/cli/commands/svc.ts +7 -1
  32. package/src/cli/commands/update.ts +52 -0
  33. package/src/cli/commands/volumes.ts +293 -0
  34. package/src/cli/coolify-state.ts +42 -4
  35. package/src/cli/index.ts +50 -4
  36. package/src/cli/ui/banner.ts +3 -3
  37. package/src/cli/ui/screen.ts +26 -2
  38. package/src/coolify/config.ts +75 -0
  39. package/src/coolify/index.ts +325 -106
  40. package/src/coolify/types.ts +62 -1
  41. package/src/sdk.ts +87 -39
  42. package/src/tools/definitions.ts +22 -0
  43. package/src/tools/handlers.ts +19 -0
  44. package/src/utils/env-parser.ts +45 -0
  45. 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
- chalk.red(`Failed to initialize: ${initResult.error.message}`),
56
- );
57
- return;
229
+ spinner.fail(chalk.red(`Failed to initialize: ${initResult.error.message}`));
230
+ process.exit(1);
58
231
  }
59
232
 
60
- // Auto-fetch environment UUID if not provided
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 (isOk(envResult) && envResult.value.length > 0) {
69
- // Use the first environment (usually "production")
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
- const envName = envResult.value[0].name;
257
+ environmentName = envResult.value[0].name;
72
258
  spinner.info(
73
- chalk.cyan(`Using environment: ${envName} (${environmentUuid})`),
259
+ chalk.cyan(`Using only environment: ${environmentName} (${environmentUuid})`),
74
260
  );
75
261
  } else {
76
- spinner.fail(
77
- chalk.red(
78
- "No environments found for project. Please specify --environment <uuid>",
79
- ),
80
- );
81
- return;
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
- return;
315
+ process.exit(1);
89
316
  }
90
317
 
91
- // Auto-detect GitHub App when type is private-github-app or not specified
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.listGithubApps();
324
+ const ghAppsResult = await coolify.listGithubAppsAll();
98
325
 
99
326
  if (isOk(ghAppsResult)) {
100
- const nonPublicApps = ghAppsResult.value.filter((app) => !app.is_public && app.id !== 0);
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
- if (nonPublicApps.length > 0) {
103
- githubAppUuid = nonPublicApps[0].uuid;
104
- appType = "private-github-app";
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
- `Auto-detected GitHub App: ${nonPublicApps[0].name} (${githubAppUuid})`,
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.warn(
118
- chalk.yellow(`Could not list GitHub Apps: ${ghAppsResult.error.message}. Falling back to public type.`),
348
+ spinner.fail(
349
+ chalk.red(`Could not list GitHub Apps: ${ghAppsResult.error.message}`),
119
350
  );
120
- appType = "public";
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: options.ports || "3000",
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
  }