@mks2508/coolify-mks-cli-mcp 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/coolify-state.d.ts +12 -4
- package/dist/cli/coolify-state.d.ts.map +1 -1
- package/dist/cli/index.js +8886 -7957
- package/dist/coolify/config.d.ts +25 -0
- package/dist/coolify/config.d.ts.map +1 -1
- package/dist/coolify/index.d.ts +118 -10
- package/dist/coolify/index.d.ts.map +1 -1
- package/dist/coolify/types.d.ts +61 -1
- package/dist/coolify/types.d.ts.map +1 -1
- package/dist/index.cjs +2267 -227
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2289 -227
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +56 -8
- package/dist/sdk.d.ts.map +1 -1
- package/dist/server/stdio.js +253 -100
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/utils/env-parser.d.ts +24 -0
- package/dist/utils/env-parser.d.ts.map +1 -0
- package/dist/utils/format.d.ts +32 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/cli/commands/create.ts +279 -37
- package/src/cli/commands/env.ts +348 -54
- package/src/cli/commands/init.ts +69 -15
- package/src/cli/commands/main-menu.ts +1 -1
- package/src/cli/commands/projects.ts +3 -3
- package/src/cli/commands/show.ts +39 -10
- package/src/cli/commands/status.ts +23 -7
- package/src/cli/commands/svc.ts +7 -1
- package/src/cli/commands/update.ts +52 -0
- package/src/cli/commands/volumes.ts +293 -0
- package/src/cli/coolify-state.ts +42 -4
- package/src/cli/index.ts +50 -4
- package/src/cli/ui/banner.ts +3 -3
- package/src/cli/ui/screen.ts +26 -2
- package/src/coolify/config.ts +75 -0
- package/src/coolify/index.ts +325 -106
- package/src/coolify/types.ts +62 -1
- package/src/sdk.ts +87 -39
- package/src/tools/definitions.ts +22 -0
- package/src/tools/handlers.ts +19 -0
- package/src/utils/env-parser.ts +45 -0
- package/src/utils/format.ts +178 -0
package/src/cli/commands/show.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { isErr } from "@mks2508/no-throw";
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { getCoolifyService } from "../../coolify/index.js";
|
|
12
|
+
import { getCachedAppSettings } from "../../coolify/config.js";
|
|
12
13
|
import { resolveUuid } from "../coolify-state.js";
|
|
13
14
|
import { resolveAppNameOrUuid } from "../name-resolver.js";
|
|
14
15
|
|
|
@@ -38,22 +39,14 @@ export async function showCommand(uuid?: string) {
|
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
// For now, use listApplications and filter
|
|
43
|
-
const result = await coolify.listApplications();
|
|
42
|
+
const result = await coolify.getApplication(uuid);
|
|
44
43
|
|
|
45
44
|
if (isErr(result)) {
|
|
46
45
|
console.error(chalk.red(`Error: ${result.error.message}`));
|
|
47
46
|
return;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
const
|
|
51
|
-
const app = apps.find((a) => a.uuid === uuid || a.uuid.startsWith(uuid));
|
|
52
|
-
|
|
53
|
-
if (!app) {
|
|
54
|
-
console.error(chalk.red(`Application not found: ${uuid}`));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
49
|
+
const app = result.value;
|
|
57
50
|
|
|
58
51
|
console.log(chalk.cyan("Application Details:"));
|
|
59
52
|
console.log("");
|
|
@@ -83,6 +76,42 @@ export async function showCommand(uuid?: string) {
|
|
|
83
76
|
chalk.gray("Base Dir: ") + chalk.white(app.base_directory || "N/A"),
|
|
84
77
|
);
|
|
85
78
|
console.log("");
|
|
79
|
+
console.log(chalk.cyan("Deploy Settings:"));
|
|
80
|
+
// Coolify API GET does not return settings.is_auto_deploy_enabled (stored in
|
|
81
|
+
// application_settings table, not eager-loaded by the API). We check:
|
|
82
|
+
// 1. API response (future-proof for when Coolify fixes this)
|
|
83
|
+
// 2. Local cache (set when user runs `update --auto-deploy/--no-auto-deploy`)
|
|
84
|
+
// 3. Default: ON (Coolify default)
|
|
85
|
+
const cached = await getCachedAppSettings(uuid);
|
|
86
|
+
const autoDeployFromApi = app.settings?.is_auto_deploy_enabled;
|
|
87
|
+
const autoDeployFromCache = cached?.isAutoDeployEnabled;
|
|
88
|
+
const autoDeployEnabled = autoDeployFromApi ?? autoDeployFromCache;
|
|
89
|
+
|
|
90
|
+
if (autoDeployEnabled !== undefined) {
|
|
91
|
+
const source = autoDeployFromApi ? "" : chalk.dim(" (cached)");
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.gray("Auto-deploy:") +
|
|
94
|
+
(autoDeployEnabled ? chalk.green(" ON") : chalk.red(" OFF")) +
|
|
95
|
+
source,
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(
|
|
99
|
+
chalk.gray("Auto-deploy:") +
|
|
100
|
+
chalk.dim(" unknown (default: ON, set via --auto-deploy/--no-auto-deploy)"),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (app.watch_paths) {
|
|
104
|
+
console.log(chalk.gray("Watch paths:"));
|
|
105
|
+
for (const p of app.watch_paths.split("\n").filter(Boolean)) {
|
|
106
|
+
console.log(chalk.gray(` ${p}`));
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.gray("Watch paths:") +
|
|
111
|
+
chalk.yellow(" none (deploys on all changes)"),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
console.log("");
|
|
86
115
|
console.log(chalk.cyan("Destination:"));
|
|
87
116
|
if (app.destination) {
|
|
88
117
|
console.log(chalk.gray(" UUID: ") + chalk.white(app.destination.uuid));
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "../ui/screen.js";
|
|
27
27
|
import { showStatusDashboard, formatStatus } from "../ui/tables.js";
|
|
28
28
|
import { loadCoolifyState, loadMultiAppState } from "../coolify-state.js";
|
|
29
|
-
import { loadConfig } from "../../coolify/config.js";
|
|
29
|
+
import { loadConfig, getCachedAppSettings } from "../../coolify/config.js";
|
|
30
30
|
import {
|
|
31
31
|
inlineSelect,
|
|
32
32
|
fullSelect,
|
|
@@ -232,8 +232,20 @@ async function navigateResource(
|
|
|
232
232
|
let logPreview: string[] | undefined;
|
|
233
233
|
if (kind === "app") {
|
|
234
234
|
const coolify = getCoolifyService();
|
|
235
|
-
const [dr, lp] = await Promise.all([
|
|
236
|
-
|
|
235
|
+
const [dr, lp, cached] = await Promise.all([
|
|
236
|
+
coolify.getApplication(uuid),
|
|
237
|
+
fetchLogPreview(uuid, 6),
|
|
238
|
+
getCachedAppSettings(uuid),
|
|
239
|
+
]);
|
|
240
|
+
if (!isErr(dr)) {
|
|
241
|
+
appDetail = dr.value;
|
|
242
|
+
// Augment with cached settings (API GET doesn't return settings)
|
|
243
|
+
if (cached && !appDetail.settings) {
|
|
244
|
+
appDetail.settings = {
|
|
245
|
+
is_auto_deploy_enabled: cached.isAutoDeployEnabled,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
237
249
|
if (lp.length > 0 && !lp[0].includes("unavailable")) logPreview = lp;
|
|
238
250
|
}
|
|
239
251
|
|
|
@@ -348,10 +360,14 @@ async function logsSubMenu(uuid: string, appName: string, pos?: { row: number; c
|
|
|
348
360
|
const { spawnSync } = await import("child_process");
|
|
349
361
|
const bunPath = process.argv[0] || "bun";
|
|
350
362
|
const scriptPath = process.argv[1] || "coolify-cli";
|
|
351
|
-
const
|
|
352
|
-
const
|
|
363
|
+
const cmd = `'${bunPath}' '${scriptPath}' logs ${uuid} -f`;
|
|
364
|
+
const childEnv = {
|
|
365
|
+
...process.env,
|
|
366
|
+
COOLIFY_URL: process.env.COOLIFY_URL ?? "",
|
|
367
|
+
COOLIFY_TOKEN: process.env.COOLIFY_TOKEN ?? "",
|
|
368
|
+
};
|
|
353
369
|
spawnSync("tmux", ["kill-session", "-t", sessionName], { stdio: "ignore" });
|
|
354
|
-
spawnSync("tmux", ["new-session", "-d", "-s", sessionName, cmd]);
|
|
370
|
+
spawnSync("tmux", ["new-session", "-d", "-s", sessionName, cmd], { env: childEnv });
|
|
355
371
|
const check = spawnSync("tmux", ["has-session", "-t", sessionName]);
|
|
356
372
|
if (check.status === 0) {
|
|
357
373
|
console.log(chalk.green(`\n ✓ Session "${sessionName}" created`));
|
|
@@ -392,7 +408,7 @@ async function envSubMenu(uuid: string, appName: string, pos?: { row: number; co
|
|
|
392
408
|
case "list": await envCommand(uuid, {}); await waitForKey(); break;
|
|
393
409
|
case "set": {
|
|
394
410
|
const input = await textInput("KEY=VALUE:");
|
|
395
|
-
if (input) { await envCommand(uuid, { set: input }); await waitForKey(); }
|
|
411
|
+
if (input) { await envCommand(uuid, { set: [input] }); await waitForKey(); }
|
|
396
412
|
break;
|
|
397
413
|
}
|
|
398
414
|
case "delete": {
|
package/src/cli/commands/svc.ts
CHANGED
|
@@ -83,7 +83,13 @@ export const svcEnvCommand = (uuid: string) =>
|
|
|
83
83
|
{ header: "Runtime", value: (e) => (e.is_runtime ? "Yes" : "No") },
|
|
84
84
|
]);
|
|
85
85
|
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Set env var for a service.
|
|
88
|
+
*
|
|
89
|
+
* Note: `services.setEnv` delegates to the bulk endpoint internally, so
|
|
90
|
+
* calling this for a key that already exists as an override UPDATES the
|
|
91
|
+
* value instead of failing with 409. Repeatable across deploys.
|
|
92
|
+
*/
|
|
87
93
|
export async function svcSetEnvCommand(
|
|
88
94
|
uuid: string,
|
|
89
95
|
keyValue: string,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { isErr } from "@mks2508/no-throw";
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { getCoolifyService } from "../../coolify/index.js";
|
|
12
|
+
import { cacheAppSettings } from "../../coolify/config.js";
|
|
12
13
|
import type { ICoolifyUpdateOptions } from "../../coolify/types.js";
|
|
13
14
|
import { resolveUuid } from "../coolify-state.js";
|
|
14
15
|
import { resolveAppNameOrUuid } from "../name-resolver.js";
|
|
@@ -30,6 +31,8 @@ interface IUpdateCommandOptions {
|
|
|
30
31
|
baseDirectory?: string;
|
|
31
32
|
domains?: string;
|
|
32
33
|
autoDeploy?: boolean;
|
|
34
|
+
watchPaths?: string;
|
|
35
|
+
clearWatchPaths?: boolean;
|
|
33
36
|
forceHttps?: boolean;
|
|
34
37
|
healthCheckEnabled?: boolean;
|
|
35
38
|
healthCheckPath?: string;
|
|
@@ -93,6 +96,11 @@ export async function updateCommand(
|
|
|
93
96
|
if (options.domains) updateOptions.domains = options.domains;
|
|
94
97
|
if (options.autoDeploy !== undefined)
|
|
95
98
|
updateOptions.isAutoDeployEnabled = options.autoDeploy;
|
|
99
|
+
if (options.clearWatchPaths) {
|
|
100
|
+
updateOptions.watchPaths = null;
|
|
101
|
+
} else if (options.watchPaths) {
|
|
102
|
+
updateOptions.watchPaths = options.watchPaths.replace(/\\n/g, "\n");
|
|
103
|
+
}
|
|
96
104
|
if (options.forceHttps) updateOptions.isForceHttpsEnabled = true;
|
|
97
105
|
if (options.healthCheckEnabled !== undefined)
|
|
98
106
|
updateOptions.healthCheckEnabled = options.healthCheckEnabled;
|
|
@@ -133,4 +141,48 @@ export async function updateCommand(
|
|
|
133
141
|
if (result.value.description) {
|
|
134
142
|
console.log(chalk.gray(`Description: ${result.value.description}`));
|
|
135
143
|
}
|
|
144
|
+
|
|
145
|
+
// Cache and display auto-deploy and watch_paths settings
|
|
146
|
+
if (
|
|
147
|
+
options.autoDeploy !== undefined ||
|
|
148
|
+
options.watchPaths ||
|
|
149
|
+
options.clearWatchPaths
|
|
150
|
+
) {
|
|
151
|
+
// Cache settings locally (API GET doesn't return is_auto_deploy_enabled)
|
|
152
|
+
const settingsToCache: Record<string, unknown> = {};
|
|
153
|
+
if (options.autoDeploy !== undefined) {
|
|
154
|
+
settingsToCache.isAutoDeployEnabled = options.autoDeploy;
|
|
155
|
+
}
|
|
156
|
+
if (options.clearWatchPaths) {
|
|
157
|
+
settingsToCache.watchPaths = null;
|
|
158
|
+
} else if (options.watchPaths) {
|
|
159
|
+
settingsToCache.watchPaths = updateOptions.watchPaths;
|
|
160
|
+
}
|
|
161
|
+
await cacheAppSettings(options.uuid, settingsToCache);
|
|
162
|
+
|
|
163
|
+
// Display what was set
|
|
164
|
+
if (options.autoDeploy !== undefined) {
|
|
165
|
+
console.log(
|
|
166
|
+
chalk.gray(`Auto-deploy: `) +
|
|
167
|
+
(options.autoDeploy ? chalk.green("ON") : chalk.red("OFF")),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Verify watch_paths from API (this field IS returned by GET)
|
|
172
|
+
const verifyResult = await service.getApplication(options.uuid);
|
|
173
|
+
if (!isErr(verifyResult)) {
|
|
174
|
+
const app = verifyResult.value;
|
|
175
|
+
if (app.watch_paths) {
|
|
176
|
+
console.log(chalk.gray(`Watch paths:`));
|
|
177
|
+
for (const p of app.watch_paths.split("\n").filter(Boolean)) {
|
|
178
|
+
console.log(chalk.gray(` ${p}`));
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
console.log(
|
|
182
|
+
chalk.gray(`Watch paths: `) +
|
|
183
|
+
chalk.yellow("none (deploys on all changes)"),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
136
188
|
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Volumes subcommands for CLI — add/list/remove bind mounts on docker-compose apps.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getCliSdk, chalk, Table } from "../actions.js";
|
|
8
|
+
|
|
9
|
+
interface IComposeService {
|
|
10
|
+
image?: string;
|
|
11
|
+
volumes?: string[];
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IComposeFile {
|
|
16
|
+
services?: Record<string, IComposeService>;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a volume string like "/host/path:/container/path" or "named_vol:/container/path".
|
|
22
|
+
*/
|
|
23
|
+
function parseVolumeEntry(v: string): { source: string; target: string } {
|
|
24
|
+
const colonIdx = v.indexOf(":");
|
|
25
|
+
if (colonIdx === -1) return { source: "", target: v };
|
|
26
|
+
return { source: v.slice(0, colonIdx), target: v.slice(colonIdx + 1) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* List volumes for an application.
|
|
31
|
+
*/
|
|
32
|
+
export async function volumesListCommand(
|
|
33
|
+
uuid: string,
|
|
34
|
+
options: { service?: string },
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
const sdk = getCliSdk();
|
|
38
|
+
const app = await sdk.applications.get(uuid);
|
|
39
|
+
|
|
40
|
+
if (!app.docker_compose_raw) {
|
|
41
|
+
console.log(chalk.yellow("Application has no docker_compose_raw (not a docker-compose app)."));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let compose: IComposeFile;
|
|
46
|
+
try {
|
|
47
|
+
compose = Bun.YAML.parse(app.docker_compose_raw) as IComposeFile;
|
|
48
|
+
} catch {
|
|
49
|
+
console.error(chalk.red("Failed to parse docker_compose_raw YAML."));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const services = compose.services ?? {};
|
|
54
|
+
const serviceNames = Object.keys(services);
|
|
55
|
+
|
|
56
|
+
if (serviceNames.length === 0) {
|
|
57
|
+
console.log(chalk.yellow("No services found in docker-compose."));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Filter by service if specified
|
|
62
|
+
const displayServices = options.service
|
|
63
|
+
? serviceNames.filter((s) => s === options.service)
|
|
64
|
+
: serviceNames;
|
|
65
|
+
|
|
66
|
+
if (options.service && displayServices.length === 0) {
|
|
67
|
+
console.error(
|
|
68
|
+
chalk.red(`Service "${options.service}" not found. Available: ${serviceNames.join(", ")}`),
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const svcName of displayServices) {
|
|
74
|
+
const svc = services[svcName];
|
|
75
|
+
const volumes = svc?.volumes ?? [];
|
|
76
|
+
|
|
77
|
+
console.log(chalk.cyan(`\nService: ${chalk.bold(svcName)}`));
|
|
78
|
+
|
|
79
|
+
if (volumes.length === 0) {
|
|
80
|
+
console.log(chalk.gray(" No volumes configured."));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const table = new Table({
|
|
85
|
+
head: [chalk.cyan("Source"), chalk.cyan("Target"), chalk.cyan("Type")],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
for (const vol of volumes) {
|
|
89
|
+
const { source, target } = parseVolumeEntry(vol);
|
|
90
|
+
const volType = source.startsWith("/") ? "bind" : "named";
|
|
91
|
+
table.push([source || "(no source)", target, volType]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(table.toString());
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(
|
|
98
|
+
chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Add a volume (bind mount) to an application.
|
|
105
|
+
*/
|
|
106
|
+
export async function volumesAddCommand(
|
|
107
|
+
uuid: string,
|
|
108
|
+
options: {
|
|
109
|
+
source: string;
|
|
110
|
+
target: string;
|
|
111
|
+
service?: string;
|
|
112
|
+
noRestart?: boolean;
|
|
113
|
+
},
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
try {
|
|
116
|
+
const sdk = getCliSdk();
|
|
117
|
+
const app = await sdk.applications.get(uuid);
|
|
118
|
+
|
|
119
|
+
if (!app.docker_compose_raw) {
|
|
120
|
+
console.log(
|
|
121
|
+
chalk.yellow("Application has no docker_compose_raw. Only docker-compose buildPack apps support volume management via CLI."),
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let compose: IComposeFile;
|
|
127
|
+
try {
|
|
128
|
+
compose = Bun.YAML.parse(app.docker_compose_raw) as IComposeFile;
|
|
129
|
+
} catch {
|
|
130
|
+
console.error(chalk.red("Failed to parse docker_compose_raw YAML."));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const services = compose.services ?? {};
|
|
135
|
+
const serviceNames = Object.keys(services);
|
|
136
|
+
|
|
137
|
+
// Determine target service
|
|
138
|
+
let targetService = options.service;
|
|
139
|
+
if (!targetService) {
|
|
140
|
+
if (serviceNames.length === 1) {
|
|
141
|
+
targetService = serviceNames[0];
|
|
142
|
+
} else {
|
|
143
|
+
console.error(
|
|
144
|
+
chalk.red(
|
|
145
|
+
`Multiple services found (${serviceNames.join(", ")}). Use --service to specify which one.`,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!services[targetService]) {
|
|
153
|
+
console.error(
|
|
154
|
+
chalk.red(`Service "${targetService}" not found. Available: ${serviceNames.join(", ")}`),
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const svc = services[targetService];
|
|
160
|
+
const volumes = svc.volumes ?? [];
|
|
161
|
+
|
|
162
|
+
// Check if volume already exists
|
|
163
|
+
const existing = volumes.find((v) => {
|
|
164
|
+
const { target: t } = parseVolumeEntry(v);
|
|
165
|
+
return t === options.target;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (existing) {
|
|
169
|
+
console.log(chalk.yellow(`Volume with target "${options.target}" already exists — skipping.`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Add the volume
|
|
174
|
+
const volStr = `${options.source}:${options.target}`;
|
|
175
|
+
svc.volumes = [...volumes, volStr];
|
|
176
|
+
|
|
177
|
+
// Serialize back to YAML
|
|
178
|
+
const updatedYaml = Bun.YAML.stringify(compose);
|
|
179
|
+
|
|
180
|
+
// PATCH the application
|
|
181
|
+
console.log(chalk.cyan(`Adding volume: ${chalk.bold(volStr)} to service "${targetService}"...`));
|
|
182
|
+
await sdk.applications.update(uuid, { dockerComposeRaw: updatedYaml });
|
|
183
|
+
console.log(chalk.green("Volume added successfully."));
|
|
184
|
+
|
|
185
|
+
// Deploy if not --no-restart
|
|
186
|
+
if (!options.noRestart) {
|
|
187
|
+
console.log(chalk.cyan("Redeploying to apply changes..."));
|
|
188
|
+
await sdk.applications.deploy(uuid);
|
|
189
|
+
console.log(chalk.green("Redeploy triggered."));
|
|
190
|
+
} else {
|
|
191
|
+
console.log(chalk.gray("Skipped redeploy (--no-restart). Changes apply on next deploy."));
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(
|
|
195
|
+
chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Remove a volume from an application.
|
|
202
|
+
*/
|
|
203
|
+
export async function volumesRemoveCommand(
|
|
204
|
+
uuid: string,
|
|
205
|
+
options: {
|
|
206
|
+
target: string;
|
|
207
|
+
service?: string;
|
|
208
|
+
noRestart?: boolean;
|
|
209
|
+
},
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
try {
|
|
212
|
+
const sdk = getCliSdk();
|
|
213
|
+
const app = await sdk.applications.get(uuid);
|
|
214
|
+
|
|
215
|
+
if (!app.docker_compose_raw) {
|
|
216
|
+
console.log(
|
|
217
|
+
chalk.yellow("Application has no docker_compose_raw. Only docker-compose buildPack apps support volume management via CLI."),
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let compose: IComposeFile;
|
|
223
|
+
try {
|
|
224
|
+
compose = Bun.YAML.parse(app.docker_compose_raw) as IComposeFile;
|
|
225
|
+
} catch {
|
|
226
|
+
console.error(chalk.red("Failed to parse docker_compose_raw YAML."));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const services = compose.services ?? {};
|
|
231
|
+
const serviceNames = Object.keys(services);
|
|
232
|
+
|
|
233
|
+
// Determine target service
|
|
234
|
+
let targetService = options.service;
|
|
235
|
+
if (!targetService) {
|
|
236
|
+
if (serviceNames.length === 1) {
|
|
237
|
+
targetService = serviceNames[0];
|
|
238
|
+
} else {
|
|
239
|
+
console.error(
|
|
240
|
+
chalk.red(
|
|
241
|
+
`Multiple services found (${serviceNames.join(", ")}). Use --service to specify which one.`,
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!services[targetService]) {
|
|
249
|
+
console.error(
|
|
250
|
+
chalk.red(`Service "${targetService}" not found. Available: ${serviceNames.join(", ")}`),
|
|
251
|
+
);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const svc = services[targetService];
|
|
256
|
+
const volumes = svc.volumes ?? [];
|
|
257
|
+
|
|
258
|
+
// Find volume by target path
|
|
259
|
+
const idx = volumes.findIndex((v) => {
|
|
260
|
+
const { target: t } = parseVolumeEntry(v);
|
|
261
|
+
return t === options.target;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (idx === -1) {
|
|
265
|
+
console.error(chalk.red(`No volume found with target "${options.target}".`));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const removed = volumes[idx];
|
|
270
|
+
svc.volumes = volumes.filter((_, i) => i !== idx);
|
|
271
|
+
|
|
272
|
+
// Serialize back to YAML
|
|
273
|
+
const updatedYaml = Bun.YAML.stringify(compose);
|
|
274
|
+
|
|
275
|
+
// PATCH the application
|
|
276
|
+
console.log(chalk.cyan(`Removing volume: ${chalk.bold(removed)} from service "${targetService}"...`));
|
|
277
|
+
await sdk.applications.update(uuid, { dockerComposeRaw: updatedYaml });
|
|
278
|
+
console.log(chalk.green("Volume removed successfully."));
|
|
279
|
+
|
|
280
|
+
// Deploy if not --no-restart
|
|
281
|
+
if (!options.noRestart) {
|
|
282
|
+
console.log(chalk.cyan("Redeploying to apply changes..."));
|
|
283
|
+
await sdk.applications.deploy(uuid);
|
|
284
|
+
console.log(chalk.green("Redeploy triggered."));
|
|
285
|
+
} else {
|
|
286
|
+
console.log(chalk.gray("Skipped redeploy (--no-restart). Changes apply on next deploy."));
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(
|
|
290
|
+
chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
package/src/cli/coolify-state.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
+
import { validateCoolifyState } from "../utils/format.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* State stored in .coolify.json (generated by create-bunspace deployer or init command).
|
|
@@ -50,8 +51,12 @@ export interface ICoolifyDeployState {
|
|
|
50
51
|
type?: string;
|
|
51
52
|
/** Build pack (dockerfile, nixpacks, static, dockercompose) */
|
|
52
53
|
buildPack?: string;
|
|
53
|
-
/**
|
|
54
|
+
/** Ports exposed (comma-separated) */
|
|
55
|
+
portsExposes?: string;
|
|
56
|
+
/** Auto-deploy enabled (Coolify default: true) */
|
|
54
57
|
autoDeployEnabled?: boolean;
|
|
58
|
+
/** Watch paths for selective auto-deploy (newline-separated globs, null = all changes) */
|
|
59
|
+
watchPaths?: string | null;
|
|
55
60
|
/** When this state was last updated */
|
|
56
61
|
updatedAt?: string;
|
|
57
62
|
/** Coolify instance URL */
|
|
@@ -72,12 +77,16 @@ export interface ICoolifyMultiAppState {
|
|
|
72
77
|
name: string;
|
|
73
78
|
/** Service role (e.g. "backend", "admin", "docs", "proxy") */
|
|
74
79
|
service: string;
|
|
75
|
-
/** Domain if configured */
|
|
76
|
-
domain?: string;
|
|
80
|
+
/** Domain if configured (with protocol). Null for internal services. */
|
|
81
|
+
domain?: string | null;
|
|
77
82
|
/** Dockerfile path relative to repo root */
|
|
78
|
-
dockerfile?: string;
|
|
83
|
+
dockerfile?: string | null;
|
|
79
84
|
/** Exposed port */
|
|
80
85
|
port?: number;
|
|
86
|
+
/** Auto-deploy enabled (Coolify default: true) */
|
|
87
|
+
autoDeployEnabled?: boolean;
|
|
88
|
+
/** Watch paths for selective auto-deploy (newline-separated globs, null = all changes) */
|
|
89
|
+
watchPaths?: string | null;
|
|
81
90
|
}>;
|
|
82
91
|
/** Server UUID */
|
|
83
92
|
serverUuid: string;
|
|
@@ -151,6 +160,19 @@ export function loadCoolifyState(): ICoolifyDeployState | null {
|
|
|
151
160
|
return null;
|
|
152
161
|
}
|
|
153
162
|
|
|
163
|
+
// Validate state against schema rules
|
|
164
|
+
const validation = validateCoolifyState(state);
|
|
165
|
+
if (!validation.valid) {
|
|
166
|
+
console.error(
|
|
167
|
+
`Warning: .coolify.json has errors: ${validation.errors.join("; ")}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (validation.warnings.length > 0) {
|
|
171
|
+
for (const w of validation.warnings) {
|
|
172
|
+
console.error(`Warning: .coolify.json — ${w}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
154
176
|
return state as ICoolifyDeployState;
|
|
155
177
|
} catch {
|
|
156
178
|
return null;
|
|
@@ -191,6 +213,14 @@ export function loadMultiAppState(): ICoolifyMultiAppState | null {
|
|
|
191
213
|
* @param state - The multi-app state to write
|
|
192
214
|
*/
|
|
193
215
|
export function writeMultiAppState(state: ICoolifyMultiAppState): void {
|
|
216
|
+
const validation = validateCoolifyState(
|
|
217
|
+
state as unknown as Record<string, unknown>,
|
|
218
|
+
);
|
|
219
|
+
if (!validation.valid) {
|
|
220
|
+
console.error(
|
|
221
|
+
`Warning: writing .coolify.json with errors: ${validation.errors.join("; ")}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
194
224
|
const statePath = join(process.cwd(), STATE_FILE);
|
|
195
225
|
writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
196
226
|
}
|
|
@@ -201,6 +231,14 @@ export function writeMultiAppState(state: ICoolifyMultiAppState): void {
|
|
|
201
231
|
* @param state - The state to write
|
|
202
232
|
*/
|
|
203
233
|
export function writeCoolifyState(state: ICoolifyDeployState): void {
|
|
234
|
+
const validation = validateCoolifyState(
|
|
235
|
+
state as unknown as Record<string, unknown>,
|
|
236
|
+
);
|
|
237
|
+
if (!validation.valid) {
|
|
238
|
+
console.error(
|
|
239
|
+
`Warning: writing .coolify.json with errors: ${validation.errors.join("; ")}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
204
242
|
const statePath = join(process.cwd(), STATE_FILE);
|
|
205
243
|
writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
206
244
|
}
|