@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.
Files changed (75) hide show
  1. package/dist/cli/coolify-state.d.ts +101 -5
  2. package/dist/cli/coolify-state.d.ts.map +1 -1
  3. package/dist/cli/index.js +23165 -11543
  4. package/dist/cli/ui/highlighter.d.ts +28 -0
  5. package/dist/cli/ui/highlighter.d.ts.map +1 -0
  6. package/dist/cli/ui/index.d.ts +9 -0
  7. package/dist/cli/ui/index.d.ts.map +1 -0
  8. package/dist/cli/ui/spinners.d.ts +100 -0
  9. package/dist/cli/ui/spinners.d.ts.map +1 -0
  10. package/dist/cli/ui/tables.d.ts +103 -0
  11. package/dist/cli/ui/tables.d.ts.map +1 -0
  12. package/dist/coolify/config.d.ts +25 -0
  13. package/dist/coolify/config.d.ts.map +1 -1
  14. package/dist/coolify/index.d.ts +139 -12
  15. package/dist/coolify/index.d.ts.map +1 -1
  16. package/dist/coolify/types.d.ts +160 -2
  17. package/dist/coolify/types.d.ts.map +1 -1
  18. package/dist/examples/demo-ui.d.ts +8 -0
  19. package/dist/examples/demo-ui.d.ts.map +1 -0
  20. package/dist/index.cjs +2580 -230
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.js +2598 -226
  23. package/dist/index.js.map +1 -1
  24. package/dist/sdk.d.ts +96 -7
  25. package/dist/sdk.d.ts.map +1 -1
  26. package/dist/server/stdio.js +475 -73
  27. package/dist/tools/definitions.d.ts.map +1 -1
  28. package/dist/tools/handlers.d.ts.map +1 -1
  29. package/dist/utils/env-parser.d.ts +24 -0
  30. package/dist/utils/env-parser.d.ts.map +1 -0
  31. package/dist/utils/format.d.ts +32 -0
  32. package/dist/utils/format.d.ts.map +1 -1
  33. package/package.json +17 -4
  34. package/src/cli/actions.ts +9 -2
  35. package/src/cli/commands/create.ts +332 -24
  36. package/src/cli/commands/db.ts +37 -0
  37. package/src/cli/commands/delete.ts +6 -2
  38. package/src/cli/commands/deploy.ts +347 -49
  39. package/src/cli/commands/deployments.ts +6 -2
  40. package/src/cli/commands/diagnose.ts +3 -3
  41. package/src/cli/commands/env.ts +424 -31
  42. package/src/cli/commands/exec.ts +6 -2
  43. package/src/cli/commands/init.ts +991 -0
  44. package/src/cli/commands/logs.ts +224 -24
  45. package/src/cli/commands/main-menu.ts +21 -0
  46. package/src/cli/commands/projects.ts +312 -29
  47. package/src/cli/commands/restart.ts +6 -2
  48. package/src/cli/commands/service-logs.ts +14 -0
  49. package/src/cli/commands/show.ts +45 -12
  50. package/src/cli/commands/start.ts +6 -2
  51. package/src/cli/commands/status.ts +554 -0
  52. package/src/cli/commands/stop.ts +6 -2
  53. package/src/cli/commands/svc.ts +7 -1
  54. package/src/cli/commands/update.ts +79 -2
  55. package/src/cli/commands/volumes.ts +293 -0
  56. package/src/cli/coolify-state.ts +203 -12
  57. package/src/cli/index.ts +138 -11
  58. package/src/cli/name-resolver.ts +228 -0
  59. package/src/cli/ui/banner.ts +276 -0
  60. package/src/cli/ui/highlighter.ts +176 -0
  61. package/src/cli/ui/index.ts +9 -0
  62. package/src/cli/ui/prompts.ts +155 -0
  63. package/src/cli/ui/screen.ts +630 -0
  64. package/src/cli/ui/select.ts +280 -0
  65. package/src/cli/ui/spinners.ts +256 -0
  66. package/src/cli/ui/tables.ts +407 -0
  67. package/src/coolify/config.ts +75 -0
  68. package/src/coolify/index.ts +565 -101
  69. package/src/coolify/types.ts +165 -2
  70. package/src/examples/demo-ui.ts +78 -0
  71. package/src/sdk.ts +211 -1
  72. package/src/tools/definitions.ts +22 -0
  73. package/src/tools/handlers.ts +19 -0
  74. package/src/utils/env-parser.ts +45 -0
  75. package/src/utils/format.ts +178 -0
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/tools/definitions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE/D;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,IAAI,EAmqC9B,CAAC"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/tools/definitions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE/D;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,IAAI,EAyrC9B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/tools/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AA+DzE;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,cAAc,CAAC,CAgfzB"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/tools/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AA+DzE;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,cAAc,CAAC,CAmgBzB"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * .env file parser shared between the SDK (syncEnv) and the CLI (--sync stdin).
3
+ *
4
+ * Kept as a standalone exported function (not a class method) so both the
5
+ * SDK's ApplicationsResource and the CLI's stdin sync handler can use the
6
+ * same implementation without coupling to either.
7
+ *
8
+ * @module
9
+ */
10
+ /**
11
+ * Parses .env-style content into a Map.
12
+ *
13
+ * Handles:
14
+ * - Comments (lines starting with `#`)
15
+ * - Empty lines (skipped)
16
+ * - Quoted values (`"value"` or `'value'`)
17
+ * - Values containing `=` (only the first `=` is the separator)
18
+ * - Lines without `=` (skipped — invalid)
19
+ *
20
+ * @param content - File contents in KEY=VALUE format
21
+ * @returns Map of key → value (empty string for `KEY=` with no value)
22
+ */
23
+ export declare function parseEnvContent(content: string): Map<string, string>;
24
+ //# sourceMappingURL=env-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-parser.d.ts","sourceRoot":"","sources":["../../src/utils/env-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAqBpE"}
@@ -28,6 +28,38 @@ export declare function formatStatus(status: string): string;
28
28
  * @returns Formatted string with unit
29
29
  */
30
30
  export declare function formatBytes(bytes: number): string;
31
+ /**
32
+ * Validates a port number string (single or comma-separated).
33
+ * Valid: "3000", "3000,3001", "80,443,8080"
34
+ * Invalid: "abc", "99999", "0", "-1", "3000,", ",3000"
35
+ *
36
+ * @param ports - Port string to validate
37
+ * @returns Object with valid flag, parsed ports, and error message
38
+ */
39
+ export declare function validatePorts(ports: string): {
40
+ valid: boolean;
41
+ ports: number[];
42
+ error?: string;
43
+ };
44
+ /**
45
+ * Parses EXPOSE directives from a Dockerfile.
46
+ *
47
+ * @param dockerfilePath - Path to the Dockerfile
48
+ * @returns Array of exposed port numbers, empty if none found or file unreadable
49
+ */
50
+ export declare function parseDockerfileExpose(dockerfilePath: string): number[];
51
+ /**
52
+ * Validates a .coolify.json state object (single-app format).
53
+ * Checks required fields, types, and port validity.
54
+ *
55
+ * @param state - The parsed JSON object
56
+ * @returns Object with valid flag, warnings, and errors
57
+ */
58
+ export declare function validateCoolifyState(state: Record<string, unknown>): {
59
+ valid: boolean;
60
+ errors: string[];
61
+ warnings: string[];
62
+ };
31
63
  /**
32
64
  * Formats timestamp to relative time.
33
65
  *
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,YAAY,CAAC;AAY/B;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,eAOlE;AAmBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWnD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAY5D"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,YAAY,CAAC;AAY/B;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,eAOlE;AAmBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWnD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQjD;AAID;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG;IAC5C,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAqCA;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,CAqBtE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IACpE,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAmFA;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAY5D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mks2508/coolify-mks-cli-mcp",
3
- "version": "0.6.3",
3
+ "version": "0.9.0",
4
4
  "description": "MCP server and CLI for Coolify deployment management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,7 +8,8 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "bin": {
10
10
  "coolify-mcp": "dist/cli/index.js",
11
- "coolify-mcp-server": "dist/server/stdio.js"
11
+ "coolify-mcp-server": "dist/server/stdio.js",
12
+ "coolify-cli": "dist/cli/index.js"
12
13
  },
13
14
  "exports": {
14
15
  ".": {
@@ -52,16 +53,20 @@
52
53
  "typecheck": "tsgo --noEmit"
53
54
  },
54
55
  "dependencies": {
56
+ "@clack/prompts": "^1.2.0",
55
57
  "@mks2508/better-logger": "^4.0.0",
56
58
  "@mks2508/no-throw": "^0.1.0",
57
59
  "@modelcontextprotocol/sdk": "^1.25.2",
58
60
  "chalk": "^5.4.1",
59
61
  "cli-table3": "^0.6.5",
60
62
  "commander": "^12.1.0",
63
+ "diff": "^8.0.4",
61
64
  "ora": "^8.1.1",
62
- "prompts": "^2.4.2"
65
+ "prompts": "^2.4.2",
66
+ "shiki": "^4.0.2"
63
67
  },
64
68
  "devDependencies": {
69
+ "@types/bun": "^1.3.14",
65
70
  "@types/node": "^22.10.5",
66
71
  "@types/prompts": "^2.4.9",
67
72
  "rolldown": "^1.0.0-beta.58",
@@ -72,5 +77,13 @@
72
77
  },
73
78
  "publishConfig": {
74
79
  "access": "public"
75
- }
80
+ },
81
+ "jsonSchema": [
82
+ {
83
+ "name": ".coolify.json",
84
+ "description": "Coolify deployment configuration",
85
+ "fileMatch": [".coolify.json"],
86
+ "url": "./coolify.json.schema.json"
87
+ }
88
+ ]
76
89
  }
@@ -13,6 +13,7 @@ import Table from "cli-table3";
13
13
  import { Coolify } from "../sdk.js";
14
14
  import { formatStatus } from "../utils/format.js";
15
15
  import { resolveUuid } from "./coolify-state.js";
16
+ import { resolveNameOrUuid } from "./name-resolver.js";
16
17
 
17
18
  /** Singleton SDK instance for CLI (uses env vars / config file). */
18
19
  let _sdk: Coolify | null = null;
@@ -42,7 +43,10 @@ export async function runAction(
42
43
  callFn: (sdk: Coolify, uuid: string) => Promise<unknown>,
43
44
  successMsg: (uuid: string) => string,
44
45
  ): Promise<void> {
45
- const resolved = resolveUuid(uuid);
46
+ let resolved = resolveUuid(uuid);
47
+ if (!resolved && uuid) {
48
+ resolved = await resolveNameOrUuid(uuid);
49
+ }
46
50
  if (!resolved) {
47
51
  console.error(
48
52
  chalk.red("Error: No UUID provided and no .coolify.json found"),
@@ -129,7 +133,10 @@ export async function runGet<T>(
129
133
  callFn: (sdk: Coolify, uuid: string) => Promise<T>,
130
134
  formatFn: (item: T) => void,
131
135
  ): Promise<void> {
132
- const resolved = resolveUuid(uuid);
136
+ let resolved = resolveUuid(uuid);
137
+ if (!resolved && uuid) {
138
+ resolved = await resolveNameOrUuid(uuid);
139
+ }
133
140
  if (!resolved) {
134
141
  console.error(
135
142
  chalk.red("Error: No UUID provided and no .coolify.json found"),
@@ -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.
@@ -34,6 +37,177 @@ interface ICreateOptions {
34
37
  dockerComposeLocation?: string;
35
38
  dockerfileLocation?: string;
36
39
  baseDirectory?: string;
40
+ githubAppUuid?: string;
41
+ privateKeyUuid?: string;
42
+ domain?: string;
43
+ }
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;
37
211
  }
38
212
 
39
213
  /**
@@ -45,45 +219,145 @@ export async function createCommand(options: ICreateOptions) {
45
219
  const spinner = ora("Initializing Coolify connection...").start();
46
220
 
47
221
  try {
222
+ // Fail-fast validation before any API calls
223
+ validateCreateOptions(options);
224
+
48
225
  const coolify = getCoolifyService();
49
226
  const initResult = await coolify.init();
50
227
 
51
228
  if (isErr(initResult)) {
52
- spinner.fail(
53
- chalk.red(`Failed to initialize: ${initResult.error.message}`),
54
- );
55
- return;
229
+ spinner.fail(chalk.red(`Failed to initialize: ${initResult.error.message}`));
230
+ process.exit(1);
56
231
  }
57
232
 
58
- // Auto-fetch environment UUID if not provided
233
+ // ── Environment resolution ─────────────────────────────────────────────
59
234
  let environmentUuid: string | undefined = options.environment;
235
+ let environmentName: string | undefined;
60
236
 
61
237
  if (!environmentUuid) {
62
238
  spinner.text = "Fetching project environments...";
63
-
64
239
  const envResult = await coolify.getProjectEnvironments(options.project);
65
240
 
66
- if (isOk(envResult) && envResult.value.length > 0) {
67
- // 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) {
68
256
  environmentUuid = envResult.value[0].uuid;
69
- const envName = envResult.value[0].name;
257
+ environmentName = envResult.value[0].name;
70
258
  spinner.info(
71
- chalk.cyan(`Using environment: ${envName} (${environmentUuid})`),
259
+ chalk.cyan(`Using only environment: ${environmentName} (${environmentUuid})`),
72
260
  );
73
261
  } else {
74
- spinner.fail(
75
- chalk.red(
76
- "No environments found for project. Please specify --environment <uuid>",
77
- ),
78
- );
79
- 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
+ }
80
310
  }
81
311
  }
82
312
 
83
- // Ensure environmentUuid is defined before creating application
84
313
  if (!environmentUuid) {
85
314
  spinner.fail(chalk.red("Environment UUID is required"));
86
- return;
315
+ process.exit(1);
316
+ }
317
+
318
+ // ── GitHub App resolution ───────────────────────────────────────────────
319
+ let appType = options.type || "public";
320
+ let githubAppUuid = options.githubAppUuid;
321
+
322
+ if (!githubAppUuid && (appType === "private-github-app" || !options.type)) {
323
+ spinner.text = "Detecting GitHub Apps...";
324
+ const ghAppsResult = await coolify.listGithubAppsAll();
325
+
326
+ if (isOk(ghAppsResult)) {
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
+ }
336
+
337
+ appType = "private-github-app";
338
+ const usedApp = privateApps.find((a) => a.uuid === githubAppUuid);
339
+ if (usedApp) {
340
+ spinner.info(
341
+ chalk.cyan(
342
+ `Using GitHub App: ${usedApp.name}` +
343
+ (usedApp.organization ? ` (${usedApp.organization})` : ""),
344
+ ),
345
+ );
346
+ }
347
+ } else if (appType === "private-github-app") {
348
+ spinner.fail(
349
+ chalk.red(`Could not list GitHub Apps: ${ghAppsResult.error.message}`),
350
+ );
351
+ process.exit(1);
352
+ }
353
+ }
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);
87
361
  }
88
362
 
89
363
  spinner.text = "Creating application...";
@@ -94,17 +368,20 @@ export async function createCommand(options: ICreateOptions) {
94
368
  description: options.description,
95
369
  projectUuid: options.project,
96
370
  environmentUuid,
371
+ environmentName,
97
372
  serverUuid: options.server,
98
- type: options.type || "public",
373
+ type: appType,
374
+ githubAppUuid,
99
375
  githubRepoUrl: options.repo,
100
376
  branch: options.branch || "main",
101
377
  buildPack: options.buildPack || "dockerfile",
102
- portsExposes: options.ports || "3000",
378
+ portsExposes: portsStr,
103
379
  dockerImage: options.dockerImage,
104
380
  dockerCompose: options.dockerCompose,
105
381
  dockerComposeLocation: options.dockerComposeLocation,
106
382
  dockerfileLocation: options.dockerfileLocation,
107
383
  baseDirectory: options.baseDirectory,
384
+ privateKeyUuid: options.privateKeyUuid,
108
385
  },
109
386
  (percent, message) => {
110
387
  spinner.text = `${chalk.bold(`[${percent}%]`)} ${message}`;
@@ -112,22 +389,52 @@ export async function createCommand(options: ICreateOptions) {
112
389
  );
113
390
 
114
391
  if (isOk(result)) {
392
+ const createdUuid = result.value.uuid;
115
393
  spinner.succeed(
116
394
  chalk.green(
117
- `Application created! UUID: ${chalk.cyan(result.value.uuid)}`,
395
+ `Application created! UUID: ${chalk.cyan(createdUuid)}`,
118
396
  ),
119
397
  );
398
+
399
+ // Set domain via PATCH if --domain was provided
400
+ if (options.domain && createdUuid) {
401
+ const domainSpinner = ora("Setting domain...").start();
402
+ const domainValue = options.domain.startsWith("http")
403
+ ? options.domain
404
+ : `https://${options.domain}`;
405
+
406
+ const updateResult = await coolify.updateApplication(createdUuid, {
407
+ domains: domainValue,
408
+ });
409
+
410
+ if (isOk(updateResult)) {
411
+ domainSpinner.succeed(chalk.green(`Domain set: ${chalk.cyan(domainValue)}`));
412
+ } else {
413
+ // Non-fatal — domain setting failed but app was created
414
+ domainSpinner.fail(
415
+ chalk.red(`Failed to set domain: ${updateResult.error.message}`),
416
+ );
417
+ }
418
+ }
419
+
120
420
  console.log(` Name: ${chalk.cyan(options.name)}`);
121
- console.log(` Type: ${chalk.cyan(options.type || "public")}`);
421
+ console.log(` Type: ${chalk.cyan(appType)}`);
422
+ if (options.domain) {
423
+ const domainValue = options.domain.startsWith("http")
424
+ ? options.domain
425
+ : `https://${options.domain}`;
426
+ console.log(` Domain: ${chalk.cyan(domainValue)}`);
427
+ }
122
428
  console.log(` Next steps:`);
123
429
  console.log(
124
- ` 1. Set environment variables: ${chalk.yellow("coolify-mcp env " + result.value.uuid)}`,
430
+ ` 1. Set environment variables: ${chalk.yellow("coolify-mcp env " + createdUuid)}`,
125
431
  );
126
432
  console.log(
127
- ` 2. Deploy application: ${chalk.yellow("coolify-mcp deploy " + result.value.uuid)}`,
433
+ ` 2. Deploy application: ${chalk.yellow("coolify-mcp deploy " + createdUuid)}`,
128
434
  );
129
435
  } else {
130
436
  spinner.fail(chalk.red(`Creation failed: ${result.error.message}`));
437
+ process.exit(1);
131
438
  }
132
439
  } catch (error) {
133
440
  spinner.fail(
@@ -135,5 +442,6 @@ export async function createCommand(options: ICreateOptions) {
135
442
  `Error: ${error instanceof Error ? error.message : String(error)}`,
136
443
  ),
137
444
  );
445
+ process.exit(1);
138
446
  }
139
447
  }
@@ -16,6 +16,7 @@ import type {
16
16
  ICoolifyDatabase,
17
17
  ICoolifyDatabaseBackup,
18
18
  } from "../../coolify/types.js";
19
+ import { resolveDbNameOrUuid } from "../name-resolver.js";
19
20
 
20
21
  /** List all databases. */
21
22
  export const dbListCommand = () =>
@@ -110,6 +111,42 @@ export const dbDeleteCommand = (uuid: string) =>
110
111
  (u) => `Database deleted: ${u}`,
111
112
  );
112
113
 
114
+ /**
115
+ * Update a database configuration.
116
+ *
117
+ * @param uuid - Database UUID
118
+ * @param options - Update options (publicPort, isPublic)
119
+ */
120
+ export async function dbUpdateCommand(
121
+ uuidOrName: string,
122
+ options: { publicPort?: string; isPublic?: boolean },
123
+ ): Promise<void> {
124
+ try {
125
+ const uuid = await resolveDbNameOrUuid(uuidOrName) ?? uuidOrName;
126
+ const data: Record<string, unknown> = {};
127
+
128
+ if (options.publicPort !== undefined) data.public_port = options.publicPort;
129
+ if (options.isPublic !== undefined) data.is_public = options.isPublic;
130
+
131
+ if (Object.keys(data).length === 0) {
132
+ console.warn(
133
+ chalk.yellow("No update options provided. Use --help to see available options."),
134
+ );
135
+ return;
136
+ }
137
+
138
+ console.log(chalk.cyan(`Updating database ${chalk.bold(uuid)}...`));
139
+ await getCliSdk().databases.update(uuid, data);
140
+ console.log(chalk.green("Database updated successfully"));
141
+ } catch (error) {
142
+ console.error(
143
+ chalk.red(
144
+ `Error: ${error instanceof Error ? error.message : String(error)}`,
145
+ ),
146
+ );
147
+ }
148
+ }
149
+
113
150
  /** List backups for a database. */
114
151
  export const dbBackupsCommand = (uuid: string) =>
115
152
  runList<ICoolifyDatabaseBackup>(
@@ -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
- const resolvedUuid = resolveUuid(uuid);
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
  }