@mks2508/coolify-mks-cli-mcp 0.6.3 → 0.8.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 (60) hide show
  1. package/dist/cli/coolify-state.d.ts +92 -4
  2. package/dist/cli/coolify-state.d.ts.map +1 -1
  3. package/dist/cli/index.js +22149 -11456
  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/index.d.ts +22 -3
  13. package/dist/coolify/index.d.ts.map +1 -1
  14. package/dist/coolify/types.d.ts +99 -1
  15. package/dist/coolify/types.d.ts.map +1 -1
  16. package/dist/examples/demo-ui.d.ts +8 -0
  17. package/dist/examples/demo-ui.d.ts.map +1 -0
  18. package/dist/index.cjs +322 -12
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.js +322 -12
  21. package/dist/index.js.map +1 -1
  22. package/dist/sdk.d.ts +41 -0
  23. package/dist/sdk.d.ts.map +1 -1
  24. package/dist/server/stdio.js +258 -9
  25. package/package.json +16 -4
  26. package/src/cli/actions.ts +9 -2
  27. package/src/cli/commands/create.ts +71 -5
  28. package/src/cli/commands/db.ts +37 -0
  29. package/src/cli/commands/delete.ts +6 -2
  30. package/src/cli/commands/deploy.ts +347 -49
  31. package/src/cli/commands/deployments.ts +6 -2
  32. package/src/cli/commands/diagnose.ts +3 -3
  33. package/src/cli/commands/env.ts +121 -22
  34. package/src/cli/commands/exec.ts +6 -2
  35. package/src/cli/commands/init.ts +937 -0
  36. package/src/cli/commands/logs.ts +224 -24
  37. package/src/cli/commands/main-menu.ts +21 -0
  38. package/src/cli/commands/projects.ts +312 -29
  39. package/src/cli/commands/restart.ts +6 -2
  40. package/src/cli/commands/service-logs.ts +14 -0
  41. package/src/cli/commands/show.ts +6 -2
  42. package/src/cli/commands/start.ts +6 -2
  43. package/src/cli/commands/status.ts +538 -0
  44. package/src/cli/commands/stop.ts +6 -2
  45. package/src/cli/commands/update.ts +27 -2
  46. package/src/cli/coolify-state.ts +164 -11
  47. package/src/cli/index.ts +91 -10
  48. package/src/cli/name-resolver.ts +228 -0
  49. package/src/cli/ui/banner.ts +276 -0
  50. package/src/cli/ui/highlighter.ts +176 -0
  51. package/src/cli/ui/index.ts +9 -0
  52. package/src/cli/ui/prompts.ts +155 -0
  53. package/src/cli/ui/screen.ts +606 -0
  54. package/src/cli/ui/select.ts +280 -0
  55. package/src/cli/ui/spinners.ts +256 -0
  56. package/src/cli/ui/tables.ts +407 -0
  57. package/src/coolify/index.ts +257 -12
  58. package/src/coolify/types.ts +103 -1
  59. package/src/examples/demo-ui.ts +78 -0
  60. package/src/sdk.ts +162 -0
@@ -0,0 +1,937 @@
1
+ /**
2
+ * Init command - Link existing Coolify app or create new one.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { execSync } from "node:child_process";
10
+ import prompts from "prompts";
11
+ import chalk from "chalk";
12
+ import ora, { Ora } from "ora";
13
+ import { isOk, isErr } from "@mks2508/no-throw";
14
+ import { getCoolifyService } from "../../coolify/index.js";
15
+ import { writeCoolifyState, loadCoolifyState, type ICoolifyDeployState } from "../coolify-state.js";
16
+ import {
17
+ createSpinner,
18
+ createStatusIndicator,
19
+ } from "../ui/spinners.js";
20
+ import {
21
+ createEnvTable,
22
+ createSummaryCard,
23
+ } from "../ui/index.js";
24
+
25
+ interface IInitOptions {
26
+ yes?: boolean;
27
+ force?: boolean;
28
+ name?: string;
29
+ link?: boolean;
30
+ }
31
+
32
+ interface IGitInfo {
33
+ repoUrl: string;
34
+ branch: string;
35
+ name: string;
36
+ owner: string;
37
+ repo: string;
38
+ }
39
+
40
+ interface IDockerConfigs {
41
+ hasDockerfile: boolean;
42
+ hasCompose: boolean;
43
+ dockerfilePath?: string;
44
+ composePath?: string;
45
+ }
46
+
47
+ interface IExistingApps {
48
+ exact: Array<{
49
+ uuid: string;
50
+ name: string;
51
+ project: string;
52
+ projectName: string;
53
+ status: string;
54
+ branch: string;
55
+ fqdn?: string;
56
+ }>;
57
+ partial: Array<{
58
+ uuid: string;
59
+ name: string;
60
+ project: string;
61
+ projectName: string;
62
+ status: string;
63
+ branch: string;
64
+ fqdn?: string;
65
+ }>;
66
+ }
67
+
68
+ /**
69
+ * Init command handler with enhanced UX/UI.
70
+ */
71
+ export async function initCommand(options: IInitOptions): Promise<void> {
72
+ console.log("");
73
+ console.log(chalk.bold.cyan("┌─────────────────────────────────────────────┐"));
74
+ console.log(chalk.bold.cyan("│") + chalk.bold.white(" 🐳 Coolify Init") + chalk.bold.cyan(" │"));
75
+ console.log(chalk.bold.cyan("│") + chalk.gray(" Link existing app or create new deployment") + " │");
76
+ console.log(chalk.bold.cyan("└─────────────────────────────────────────────┘"));
77
+ console.log("");
78
+
79
+ // 1. Check for existing .coolify.json
80
+ const existingState = loadCoolifyState();
81
+ if (existingState && !options.force) {
82
+ console.log(
83
+ chalk.yellow(" ⚠ Existing configuration found:"),
84
+ chalk.gray(`.coolify.json`),
85
+ );
86
+ console.log("");
87
+ console.log(chalk.gray(" App: ") + chalk.cyan(existingState.appUuid.slice(0, 8)) + chalk.gray("..."));
88
+ console.log("");
89
+
90
+ if (!options.yes) {
91
+ const confirm = await prompts({
92
+ type: "confirm",
93
+ name: "value",
94
+ message: chalk.yellow("Overwrite existing configuration?"),
95
+ initial: false,
96
+ });
97
+
98
+ if (!confirm.value) {
99
+ console.log("");
100
+ console.log(chalk.gray(" ✓ Keeping existing configuration"));
101
+ console.log("");
102
+ return;
103
+ }
104
+ }
105
+ }
106
+
107
+ // 2. Detect Git info with enhanced display
108
+ const gitSpinner = createSpinner({
109
+ text: "Detecting Git repository...",
110
+ color: "cyan",
111
+ }).start();
112
+
113
+ const gitInfo = detectGitInfo(process.cwd());
114
+ if (!gitInfo) {
115
+ gitSpinner.fail("Not a Git repository");
116
+ console.error(chalk.red(" ✗ Not in a Git repository"));
117
+ console.error(chalk.gray(" Run: ") + chalk.cyan("git init"));
118
+ console.error("");
119
+ return;
120
+ }
121
+
122
+ gitSpinner.succeed("Git repository detected");
123
+
124
+ // Display Git info beautifully
125
+ console.log("");
126
+ console.log(chalk.gray(" ") + chalk.bold("Repository:"));
127
+ console.log(
128
+ " " +
129
+ chalk.cyan("📦") +
130
+ chalk.gray(" URL: ") +
131
+ chalk.white(gitInfo.repoUrl),
132
+ );
133
+ console.log(
134
+ " " +
135
+ chalk.cyan("🌿") +
136
+ chalk.gray(" Branch: ") +
137
+ chalk.green(gitInfo.branch),
138
+ );
139
+ console.log(
140
+ " " +
141
+ chalk.cyan("👤") +
142
+ chalk.gray(" Owner: ") +
143
+ chalk.white(gitInfo.owner),
144
+ );
145
+ console.log("");
146
+
147
+ // 3. Detect Docker configs
148
+ const dockerSpinner = createSpinner({
149
+ text: "Scanning for Docker configurations...",
150
+ color: "blue",
151
+ }).start();
152
+
153
+ const dockerConfigs = detectDockerConfigs(process.cwd());
154
+ dockerSpinner.stop();
155
+
156
+ // Display Docker status
157
+ const dockerfileStatus = dockerConfigs.hasDockerfile
158
+ ? chalk.green("✓ Found")
159
+ : chalk.red("✗ Missing");
160
+ const composeStatus = dockerConfigs.hasCompose
161
+ ? chalk.green("✓ Found")
162
+ : chalk.red("✗ Missing");
163
+
164
+ console.log(chalk.gray(" ") + chalk.bold("Docker Configuration:"));
165
+ console.log(" " + dockerfileStatus + chalk.gray(" Dockerfile"));
166
+ console.log(" " + composeStatus + chalk.gray(" docker-compose.yml"));
167
+ console.log("");
168
+
169
+ // 4. If no Docker configs and not in link mode, offer scaffolder
170
+ if (!dockerConfigs.hasDockerfile && !dockerConfigs.hasCompose && !options.link) {
171
+ console.log(chalk.yellow(" ⚠ No Docker configuration found"));
172
+ console.log("");
173
+
174
+ if (!options.yes) {
175
+ const scaffolder = await prompts({
176
+ type: "confirm",
177
+ name: "value",
178
+ message: chalk.cyan("Generate Docker configs with create-bunspace?"),
179
+ initial: true,
180
+ });
181
+
182
+ if (scaffolder.value) {
183
+ console.log("");
184
+ console.log(chalk.gray(" Run this command:"));
185
+ console.log(
186
+ " " +
187
+ chalk.cyan("create-bunspace coolify init") +
188
+ chalk.gray(" # Generate Dockerfile + docker-compose.yml"),
189
+ );
190
+ console.log("");
191
+ return;
192
+ }
193
+ }
194
+ }
195
+
196
+ // 5. Initialize Coolify connection
197
+ const coolifySpinner = createSpinner({
198
+ text: "Connecting to Coolify instance...",
199
+ color: "magenta",
200
+ }).start();
201
+
202
+ const coolify = getCoolifyService();
203
+
204
+ const initResult = await coolify.init();
205
+ if (isErr(initResult)) {
206
+ coolifySpinner.fail("Connection failed");
207
+ console.error(chalk.red(" ✗ Failed to connect to Coolify"));
208
+ console.error(chalk.gray(` ${initResult.error.message}`));
209
+ console.error("");
210
+ return;
211
+ }
212
+
213
+ coolifySpinner.succeed("Connected to Coolify");
214
+
215
+ // 6. Search for existing apps with visual feedback
216
+ const searchSpinner = createSpinner({
217
+ text: "Searching for existing deployments...",
218
+ color: "yellow",
219
+ }).start();
220
+
221
+ const existingApps = await findExistingApps(gitInfo.repoUrl, gitInfo.branch, coolify);
222
+
223
+ if (existingApps.exact.length > 0) {
224
+ searchSpinner.succeed(
225
+ `Found ${chalk.bold.green(String(existingApps.exact.length))} existing deployment(s)`,
226
+ );
227
+
228
+ console.log("");
229
+ console.log(chalk.gray(" ") + chalk.bold("Existing Deployments:"));
230
+ console.log("");
231
+
232
+ for (let i = 0; i < existingApps.exact.length; i++) {
233
+ const app = existingApps.exact[i];
234
+ const statusIcon =
235
+ app.status.includes("running") || app.status.includes("healthy")
236
+ ? chalk.green("●")
237
+ : chalk.yellow("○");
238
+ const prefix = i === 0 ? "┌─ " : i === existingApps.exact.length - 1 ? "└─ " : "├─ ";
239
+ const last = i === existingApps.exact.length - 1;
240
+
241
+ console.log(chalk.gray(prefix) + statusIcon + " " + chalk.bold.white(app.name));
242
+ console.log(
243
+ chalk.gray(i === existingApps.exact.length - 1 ? "│ " : "│ ") +
244
+ chalk.gray(" Project: ") +
245
+ chalk.cyan(app.projectName),
246
+ );
247
+ console.log(
248
+ chalk.gray(i === existingApps.exact.length - 1 ? "│ " : "│ ") +
249
+ chalk.gray(" Status: ") +
250
+ chalk.white(app.status),
251
+ );
252
+ console.log(
253
+ chalk.gray(i === existingApps.exact.length - 1 ? "│ " : "│ ") +
254
+ chalk.gray(" Branch: ") +
255
+ chalk.yellow(app.branch),
256
+ );
257
+ console.log(
258
+ chalk.gray(i === existingApps.exact.length - 1 ? "│ " : "│ ") +
259
+ chalk.gray(" UUID: ") +
260
+ chalk.gray(app.uuid.slice(0, 8)) + chalk.gray("..."),
261
+ );
262
+
263
+ if (!last) {
264
+ console.log(chalk.gray("│"));
265
+ }
266
+ }
267
+
268
+ console.log("");
269
+
270
+ if (!options.yes) {
271
+ const linkChoice = await prompts({
272
+ type: "select",
273
+ name: "value",
274
+ message: chalk.cyan("Select deployment to link:"),
275
+ choices: [
276
+ ...existingApps.exact.map((app, idx) => ({
277
+ title: `${app.name} ${chalk.gray(`(${app.projectName})`)}`,
278
+ value: app.uuid,
279
+ description: app.status,
280
+ })),
281
+ {
282
+ title: chalk.red("➜ Create new deployment instead"),
283
+ value: "create",
284
+ },
285
+ ],
286
+ });
287
+
288
+ console.log("");
289
+ if (linkChoice.value === "create") {
290
+ await createNewApp(gitInfo, dockerConfigs, options, coolify);
291
+ } else {
292
+ await linkToApp(linkChoice.value as string, coolify, gitInfo, dockerConfigs);
293
+ }
294
+ } else {
295
+ // Auto-link to first app
296
+ console.log(
297
+ chalk.gray(" → Auto-linking to: ") +
298
+ chalk.bold.white(existingApps.exact[0].name),
299
+ );
300
+ console.log("");
301
+ await linkToApp(existingApps.exact[0].uuid, coolify, gitInfo, dockerConfigs);
302
+ }
303
+ } else if (existingApps.partial.length > 0) {
304
+ searchSpinner.warn(
305
+ `Found ${chalk.bold.yellow(String(existingApps.partial.length))} app(s) from same repo (different branches)`,
306
+ );
307
+
308
+ console.log("");
309
+ console.log(chalk.gray(" ") + chalk.bold("Existing Deployments (different branches):"));
310
+ console.log("");
311
+
312
+ for (const app of existingApps.partial) {
313
+ const statusIcon = app.status.includes("running")
314
+ ? chalk.green("●")
315
+ : chalk.yellow("○");
316
+ console.log(
317
+ " " + statusIcon + " " + chalk.bold.white(app.name) + chalk.gray(` (${chalk.yellow(app.branch)})`),
318
+ );
319
+ console.log(
320
+ " " +
321
+ chalk.gray("Project: ") +
322
+ chalk.cyan(app.projectName) +
323
+ chalk.gray(" | UUID: ") +
324
+ chalk.gray(app.uuid.slice(0, 8)) +
325
+ chalk.gray("..."),
326
+ );
327
+ }
328
+
329
+ console.log("");
330
+
331
+ if (options.link) {
332
+ console.log(chalk.red(" ✗ Cannot link: no matching deployment found"));
333
+ console.log("");
334
+ return;
335
+ }
336
+
337
+ if (!options.yes) {
338
+ const choice = await prompts({
339
+ type: "select",
340
+ name: "value",
341
+ message: chalk.cyan("What would you like to do?"),
342
+ choices: [
343
+ { title: chalk.green("➜ Create new deployment"), value: "create" },
344
+ {
345
+ title: chalk.yellow("🔗 Link to existing deployment (different branch)"),
346
+ value: "link",
347
+ },
348
+ { title: chalk.red("✗ Cancel"), value: "cancel" },
349
+ ],
350
+ });
351
+
352
+ console.log("");
353
+ if (choice.value === "create") {
354
+ await createNewApp(gitInfo, dockerConfigs, options, coolify);
355
+ } else if (choice.value === "link") {
356
+ const linkChoice = await prompts({
357
+ type: "select",
358
+ name: "value",
359
+ message: chalk.cyan("Select deployment to link:"),
360
+ choices: existingApps.partial.map((app) => ({
361
+ title: `${app.name} (${app.branch})`,
362
+ value: app.uuid,
363
+ })),
364
+ });
365
+ console.log("");
366
+ await linkToApp(linkChoice.value as string, coolify, gitInfo, dockerConfigs);
367
+ } else {
368
+ console.log("");
369
+ console.log(chalk.gray(" ✓ Cancelled"));
370
+ console.log("");
371
+ return;
372
+ }
373
+ } else {
374
+ await createNewApp(gitInfo, dockerConfigs, options, coolify);
375
+ }
376
+ } else {
377
+ searchSpinner.stop();
378
+ console.log("");
379
+ console.log(chalk.yellow(" ⚠ No existing deployments found for this repository"));
380
+ console.log("");
381
+
382
+ if (options.link) {
383
+ console.log(chalk.red(" ✗ Cannot link: no existing deployments found"));
384
+ console.log("");
385
+ return;
386
+ }
387
+
388
+ await createNewApp(gitInfo, dockerConfigs, options, coolify);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Detect Git repository information.
394
+ */
395
+ function detectGitInfo(cwd: string): IGitInfo | null {
396
+ try {
397
+ const repoUrl = execSync("git remote get-url origin", { cwd, encoding: "utf-8" }).trim();
398
+ const branch = execSync("git branch --show-current", { cwd, encoding: "utf-8" }).trim();
399
+ const name = cwd.split("/").pop() || "app";
400
+
401
+ // Normalize Git URL
402
+ let normalizedUrl = repoUrl;
403
+
404
+ // Convert SSH to HTTPS: git@github.com:user/repo.git → https://github.com/user/repo
405
+ if (normalizedUrl.startsWith("git@")) {
406
+ normalizedUrl = normalizedUrl.replace(/^git@([^:]+):/, "https://$1/");
407
+ }
408
+
409
+ // Remove .git suffix
410
+ normalizedUrl = normalizedUrl.replace(/\.git$/, "");
411
+
412
+ // Extract owner and repo
413
+ const urlParts = normalizedUrl.split("/");
414
+ const owner = urlParts[urlParts.length - 2] || "";
415
+ const repo = urlParts[urlParts.length - 1] || "";
416
+
417
+ return { repoUrl: normalizedUrl, branch, name, owner, repo };
418
+ } catch {
419
+ return null;
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Detect Docker configuration files.
425
+ */
426
+ function detectDockerConfigs(cwd: string): IDockerConfigs {
427
+ const hasDockerfile = existsSync(join(cwd, "Dockerfile"));
428
+ const hasCompose = existsSync(join(cwd, "docker-compose.yml"));
429
+
430
+ // Check common subdirectories
431
+ const appsDir = join(cwd, "apps");
432
+ let dockerfilePath: string | undefined;
433
+ let composePath: string | undefined;
434
+
435
+ if (!hasDockerfile && !hasCompose && existsSync(appsDir)) {
436
+ // Look for Dockerfile in subdirectories
437
+ const entries = require("node:fs").readdirSync(appsDir, { withFileTypes: true });
438
+ for (const entry of entries) {
439
+ if (entry.isDirectory()) {
440
+ const df = join(appsDir, entry.name, "Dockerfile");
441
+ if (existsSync(df)) {
442
+ dockerfilePath = df;
443
+ break;
444
+ }
445
+ }
446
+ }
447
+ }
448
+
449
+ return {
450
+ hasDockerfile: hasDockerfile || !!dockerfilePath,
451
+ hasCompose,
452
+ dockerfilePath: dockerfilePath || (hasDockerfile ? "Dockerfile" : undefined),
453
+ composePath: hasCompose ? "docker-compose.yml" : undefined,
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Search for existing apps in Coolify with enhanced matching.
459
+ */
460
+ async function findExistingApps(
461
+ repoUrl: string,
462
+ branch: string,
463
+ coolify: any,
464
+ ): Promise<IExistingApps> {
465
+ const listResult = await coolify.listApplications();
466
+ if (isErr(listResult)) {
467
+ return { exact: [], partial: [] };
468
+ }
469
+
470
+ const allApps = listResult.value;
471
+
472
+ // Get all projects to map names
473
+ const projectsResult = await coolify.listProjects();
474
+ const projectsMap = new Map(
475
+ isOk(projectsResult)
476
+ ? projectsResult.value.map((p: any) => [p.uuid, p.name] as [string, string])
477
+ : [],
478
+ );
479
+
480
+ // Extract owner/repo from URL
481
+ const urlParts = repoUrl.split("/");
482
+ const ownerRepo = `${urlParts[urlParts.length - 2]}/${urlParts[urlParts.length - 1]}`;
483
+
484
+ const exact: IExistingApps["exact"] = [];
485
+ const partial: IExistingApps["partial"] = [];
486
+
487
+ for (const app of allApps) {
488
+ const projectName = projectsMap.get(app.project_uuid || "") || "Unknown";
489
+
490
+ // Check by git_repository (owner/repo format)
491
+ if (app.git_repository === ownerRepo) {
492
+ if (app.git_branch === branch) {
493
+ exact.push({
494
+ uuid: app.uuid,
495
+ name: app.name,
496
+ project: app.project_uuid || "",
497
+ projectName,
498
+ status: app.status,
499
+ branch: app.git_branch || "unknown",
500
+ fqdn: app.fqdn,
501
+ });
502
+ } else {
503
+ partial.push({
504
+ uuid: app.uuid,
505
+ name: app.name,
506
+ project: app.project_uuid || "",
507
+ projectName,
508
+ status: app.status,
509
+ branch: app.git_branch || "unknown",
510
+ fqdn: app.fqdn,
511
+ });
512
+ }
513
+ continue;
514
+ }
515
+
516
+ // Check by full URL
517
+ if (app.git_full_url && app.git_full_url.includes(ownerRepo)) {
518
+ if (app.git_branch === branch) {
519
+ exact.push({
520
+ uuid: app.uuid,
521
+ name: app.name,
522
+ project: app.project_uuid || "",
523
+ projectName,
524
+ status: app.status,
525
+ branch: app.git_branch || "unknown",
526
+ fqdn: app.fqdn,
527
+ });
528
+ } else {
529
+ partial.push({
530
+ uuid: app.uuid,
531
+ name: app.name,
532
+ project: app.project_uuid || "",
533
+ projectName,
534
+ status: app.status,
535
+ branch: app.git_branch || "unknown",
536
+ fqdn: app.fqdn,
537
+ });
538
+ }
539
+ }
540
+ }
541
+
542
+ return { exact, partial };
543
+ }
544
+
545
+ /**
546
+ * Link to an existing app and create .coolify.json with enhanced output.
547
+ */
548
+ async function linkToApp(
549
+ appUuid: string,
550
+ coolify: any,
551
+ gitInfo: IGitInfo,
552
+ dockerConfigs: IDockerConfigs,
553
+ ): Promise<void> {
554
+ const linkSpinner = createSpinner({
555
+ text: "Fetching deployment details...",
556
+ color: "blue",
557
+ }).start();
558
+
559
+ try {
560
+ // Use getApplication to get full application details
561
+ const appResult = await coolify.getApplication(appUuid);
562
+ if (isErr(appResult)) {
563
+ throw new Error(appResult.error.message);
564
+ }
565
+
566
+ const app = appResult.value;
567
+ linkSpinner.succeed("Deployment details retrieved");
568
+
569
+ // Get project and environment info by searching through projects
570
+ const projectSpinner = createSpinner({
571
+ text: "Resolving project and environment...",
572
+ color: "cyan",
573
+ }).start();
574
+
575
+ let projectUuid = "";
576
+ let environmentUuid = "";
577
+ let projectName = "Unknown";
578
+ let environmentName = "Unknown";
579
+
580
+ // Get all projects
581
+ const projectsResult = await coolify.listProjects();
582
+ if (isOk(projectsResult)) {
583
+ // Search through each project's environments to find matching environment_id
584
+ for (const project of projectsResult.value) {
585
+ const envsResult = await coolify.getProjectEnvironments(project.uuid);
586
+ if (isOk(envsResult)) {
587
+ const matchingEnv = envsResult.value.find(
588
+ (e: any) => e.id === app.environment_id,
589
+ );
590
+ if (matchingEnv) {
591
+ projectUuid = project.uuid;
592
+ projectName = project.name;
593
+ environmentUuid = matchingEnv.uuid;
594
+ environmentName = matchingEnv.name;
595
+ break;
596
+ }
597
+ }
598
+ }
599
+ }
600
+
601
+ projectSpinner.succeed("Project and environment resolved");
602
+
603
+ // Build complete state with additional metadata
604
+ const state: ICoolifyDeployState = {
605
+ appUuid: app.uuid,
606
+ appName: app.name,
607
+ serverUuid: app.destination?.server?.uuid || "",
608
+ serverName: app.destination?.server?.name,
609
+ projectUuid,
610
+ projectName,
611
+ environmentUuid,
612
+ environmentName,
613
+ domain: app.fqdn,
614
+ dockerComposePath: dockerConfigs.composePath || app.dockerfile_location,
615
+ baseDirectory: app.base_directory || "/",
616
+ branch: gitInfo.branch,
617
+ gitRepository: app.git_full_url || gitInfo.repoUrl,
618
+ sourceType: app.source_type,
619
+ type: app.type,
620
+ buildPack: app.build_pack,
621
+ autoDeployEnabled: app.is_auto_deploy_enabled ?? false,
622
+ updatedAt: new Date().toISOString(),
623
+ coolifyUrl: process.env.COOLIFY_URL,
624
+ };
625
+
626
+ writeCoolifyState(state);
627
+
628
+ console.log("");
629
+ console.log(chalk.green(" ✓ Successfully linked to deployment!"));
630
+ console.log("");
631
+
632
+ // Display summary card
633
+ const appType = getAppTypeFromSourceType(app.source_type);
634
+ console.log(createSummaryCard("Deployment Details", {
635
+ "Name": { value: app.name, color: chalk.white },
636
+ "Project": { value: projectName, color: chalk.cyan },
637
+ "Environment": { value: environmentName, color: chalk.magenta },
638
+ "Branch": { value: gitInfo.branch, color: chalk.yellow },
639
+ "Type": { value: appType, color: chalk.gray },
640
+ "Status": { value: app.status, color: app.status.includes("running") ? chalk.green : chalk.yellow },
641
+ }));
642
+
643
+ console.log(chalk.gray(" ") + chalk.bold("Next steps:"));
644
+ console.log("");
645
+ console.log(
646
+ " " +
647
+ chalk.cyan("coolify-cli show") +
648
+ chalk.gray(" - Show deployment details"),
649
+ );
650
+ console.log(
651
+ " " +
652
+ chalk.cyan("coolify-cli deploy") +
653
+ chalk.gray(" - Trigger deployment"),
654
+ );
655
+ console.log(
656
+ " " +
657
+ chalk.cyan("coolify-cli env sync") +
658
+ chalk.gray(" - Sync environment variables"),
659
+ );
660
+ console.log("");
661
+ } catch (error) {
662
+ linkSpinner.fail("Failed to link");
663
+ console.error(chalk.red(` ✗ ${error}`));
664
+ console.log("");
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Create a new app in Coolify with enhanced UX.
670
+ */
671
+ async function createNewApp(
672
+ gitInfo: IGitInfo,
673
+ dockerConfigs: IDockerConfigs,
674
+ options: IInitOptions,
675
+ coolify: any,
676
+ ): Promise<void> {
677
+ // List servers
678
+ const serversSpinner = createSpinner({
679
+ text: "Fetching available servers...",
680
+ color: "blue",
681
+ }).start();
682
+
683
+ const serversResult = await coolify.listServers();
684
+ serversSpinner.stop();
685
+
686
+ if (isErr(serversResult)) {
687
+ console.error(chalk.red(" ✗ Failed to list servers"));
688
+ return;
689
+ }
690
+
691
+ const servers = serversResult.value;
692
+
693
+ // List projects
694
+ const projectsSpinner = createSpinner({
695
+ text: "Fetching available projects...",
696
+ color: "cyan",
697
+ }).start();
698
+
699
+ const projectsResult = await coolify.listProjects();
700
+ projectsSpinner.stop();
701
+
702
+ if (isErr(projectsResult)) {
703
+ console.error(chalk.red(" ✗ Failed to list projects"));
704
+ return;
705
+ }
706
+
707
+ const projects = projectsResult.value;
708
+
709
+ let serverUuid: string;
710
+ let projectUuid: string;
711
+ let appName: string;
712
+
713
+ if (options.yes) {
714
+ // Auto-mode: use first available
715
+ serverUuid = servers[0].uuid;
716
+ projectUuid = projects[0].uuid;
717
+ appName = options.name || gitInfo.name;
718
+ } else {
719
+ // Interactive mode
720
+ console.log("");
721
+ console.log(chalk.gray(" ") + chalk.bold("Configuration:"));
722
+ console.log("");
723
+
724
+ const serverChoice = await prompts({
725
+ type: "select",
726
+ name: "value",
727
+ message: chalk.cyan("Select server:"),
728
+ choices: servers.map((s: { name: string; uuid: string; ip?: string }) => ({
729
+ title: `${s.name} ${s.ip ? chalk.gray(`(${s.ip})`) : ""}`,
730
+ value: s.uuid,
731
+ })),
732
+ });
733
+ serverUuid = serverChoice.value;
734
+
735
+ const projectChoices = [
736
+ ...projects.map((p: { name: string; uuid: string }) => ({
737
+ title: p.name,
738
+ value: p.uuid,
739
+ })),
740
+ { title: chalk.green("+ Create new project"), value: "__new__" },
741
+ ];
742
+
743
+ const projectChoice = await prompts({
744
+ type: "select",
745
+ name: "value",
746
+ message: chalk.cyan("Select project:"),
747
+ choices: projectChoices,
748
+ });
749
+
750
+ if (projectChoice.value === "__new__") {
751
+ const projectName = await prompts({
752
+ type: "text",
753
+ name: "value",
754
+ message: chalk.cyan("Project name:"),
755
+ initial: gitInfo.name,
756
+ });
757
+
758
+ const newProjectSpinner = createSpinner({
759
+ text: "Creating new project...",
760
+ color: "green",
761
+ }).start();
762
+
763
+ const newProjectResult = await coolify.createProject(projectName.value);
764
+ if (isErr(newProjectResult)) {
765
+ newProjectSpinner.fail("Failed to create project");
766
+ console.error(chalk.red(` ✗ ${newProjectResult.error.message}`));
767
+ return;
768
+ }
769
+
770
+ projectUuid = newProjectResult.value.uuid;
771
+ newProjectSpinner.succeed(
772
+ `Project ${chalk.bold.white(projectName.value)} created`,
773
+ );
774
+ } else {
775
+ projectUuid = projectChoice.value;
776
+ }
777
+
778
+ const nameChoice = await prompts({
779
+ type: "text",
780
+ name: "value",
781
+ message: chalk.cyan("Deployment name:"),
782
+ initial: gitInfo.name,
783
+ });
784
+ appName = nameChoice.value;
785
+ }
786
+
787
+ // Determine build pack
788
+ let buildPack: "dockerfile" | "dockercompose" | "nixpacks" = "dockerfile";
789
+ if (dockerConfigs.hasCompose) {
790
+ buildPack = "dockercompose";
791
+ }
792
+
793
+ // Create app
794
+ const createSpinner = createSpinner({
795
+ text: "Creating new deployment...",
796
+ color: "green",
797
+ }).start();
798
+
799
+ try {
800
+ // Get GitHub App (required for private-github-app type)
801
+ const ghAppsResult = await coolify.listGithubApps();
802
+ if (isErr(ghAppsResult) || ghAppsResult.value.length === 0) {
803
+ createSpinner.fail("No GitHub App configured");
804
+ console.error(chalk.red(" ✗ No private GitHub App found"));
805
+ console.error(chalk.gray(" Configure one in Coolify Settings → Sources"));
806
+ return;
807
+ }
808
+
809
+ const privateGhApp = ghAppsResult.value.find((a: any) => !a.is_public);
810
+ if (!privateGhApp) {
811
+ createSpinner.fail("No private GitHub App found");
812
+ return;
813
+ }
814
+
815
+ // Get environments for the project
816
+ const envsResult = await coolify.getProjectEnvironments(projectUuid);
817
+ if (isErr(envsResult) || envsResult.value.length === 0) {
818
+ createSpinner.fail("No environments found");
819
+ console.error(chalk.red(" ✗ No environments found for project"));
820
+ return;
821
+ }
822
+
823
+ const environmentUuid = envsResult.value[0].uuid;
824
+
825
+ const newAppResult = await coolify.createApplication({
826
+ name: appName,
827
+ projectUuid,
828
+ environmentUuid,
829
+ serverUuid,
830
+ type: "private-github-app",
831
+ githubAppUuid: privateGhApp.uuid,
832
+ githubRepoUrl: gitInfo.repoUrl,
833
+ branch: gitInfo.branch,
834
+ buildPack,
835
+ portsExposes: "3000",
836
+ baseDirectory: "/",
837
+ isAutoDeployEnabled: true,
838
+ });
839
+
840
+ if (isErr(newAppResult)) {
841
+ createSpinner.fail("Failed to create deployment");
842
+ console.error(chalk.red(` ✗ ${newAppResult.error.message}`));
843
+ return;
844
+ }
845
+
846
+ const newApp = newAppResult.value;
847
+
848
+ createSpinner.succeed(`Deployment ${chalk.bold.white(appName)} created`);
849
+
850
+ // Get project and environment names for the state file
851
+ const project = projects.find((p: any) => p.uuid === projectUuid);
852
+ const envsForResult = await coolify.getProjectEnvironments(projectUuid);
853
+ const environment = isOk(envsForResult) && envsForResult.value.length > 0
854
+ ? envsForResult.value.find((e: any) => e.uuid === environmentUuid) || envsForResult.value[0]
855
+ : undefined;
856
+
857
+ // Get server name
858
+ const serversResult = await coolify.listServers();
859
+ const server = isOk(serversResult)
860
+ ? serversResult.value.find((s: any) => s.uuid === serverUuid)
861
+ : undefined;
862
+
863
+ // Write .coolify.json with complete metadata
864
+ const state: ICoolifyDeployState = {
865
+ appUuid: newApp.uuid!,
866
+ appName,
867
+ serverUuid,
868
+ serverName: server?.name,
869
+ projectUuid,
870
+ projectName: project?.name,
871
+ environmentUuid,
872
+ environmentName: environment?.name,
873
+ dockerComposePath: dockerConfigs.composePath,
874
+ baseDirectory: "/",
875
+ branch: gitInfo.branch,
876
+ gitRepository: gitInfo.repoUrl,
877
+ sourceType: "App\\Models\\GithubApp",
878
+ type: "private-github-app",
879
+ buildPack,
880
+ autoDeployEnabled: true,
881
+ updatedAt: new Date().toISOString(),
882
+ coolifyUrl: process.env.COOLIFY_URL,
883
+ };
884
+
885
+ writeCoolifyState(state);
886
+
887
+ console.log("");
888
+ console.log(chalk.green(" ✓ Deployment configured successfully!"));
889
+ console.log("");
890
+
891
+ console.log(createSummaryCard("Deployment Details", {
892
+ "Name": { value: appName, color: chalk.white },
893
+ "UUID": { value: newApp.uuid!.slice(0, 8) + "...", color: chalk.gray },
894
+ "Type": { value: "private-github-app", color: chalk.gray },
895
+ "Build Pack": { value: buildPack, color: chalk.cyan },
896
+ }));
897
+
898
+ console.log(chalk.gray(" ") + chalk.bold("Next steps:"));
899
+ console.log("");
900
+ console.log(
901
+ " " +
902
+ chalk.cyan("coolify-cli show") +
903
+ chalk.gray(" - Show deployment details"),
904
+ );
905
+ console.log(
906
+ " " +
907
+ chalk.cyan("coolify-cli deploy") +
908
+ chalk.gray(" - Trigger first deployment"),
909
+ );
910
+ console.log(
911
+ " " +
912
+ chalk.cyan("coolify-cli env sync") +
913
+ chalk.gray(" - Sync environment variables"),
914
+ );
915
+ console.log("");
916
+ } catch (error) {
917
+ createSpinner.fail("Failed to create deployment");
918
+ console.error(chalk.red(` ✗ ${error}`));
919
+ console.log("");
920
+ }
921
+ }
922
+
923
+ /**
924
+ * Map source_type to readable app type.
925
+ */
926
+ function getAppTypeFromSourceType(sourceType: string | undefined): string {
927
+ if (!sourceType) return "unknown";
928
+
929
+ if (sourceType.includes("GithubApp")) return "github-app";
930
+ if (sourceType.includes("DeployKey")) return "deploy-key";
931
+ if (sourceType.includes("Dockerfile")) return "dockerfile";
932
+ if (sourceType.includes("DockerCompose")) return "docker-compose";
933
+ if (sourceType.includes("DockerImage")) return "docker-image";
934
+ if (sourceType.includes("Public")) return "public";
935
+
936
+ return "unknown";
937
+ }