@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.
- package/dist/cli/coolify-state.d.ts +101 -5
- package/dist/cli/coolify-state.d.ts.map +1 -1
- package/dist/cli/index.js +23165 -11543
- package/dist/cli/ui/highlighter.d.ts +28 -0
- package/dist/cli/ui/highlighter.d.ts.map +1 -0
- package/dist/cli/ui/index.d.ts +9 -0
- package/dist/cli/ui/index.d.ts.map +1 -0
- package/dist/cli/ui/spinners.d.ts +100 -0
- package/dist/cli/ui/spinners.d.ts.map +1 -0
- package/dist/cli/ui/tables.d.ts +103 -0
- package/dist/cli/ui/tables.d.ts.map +1 -0
- package/dist/coolify/config.d.ts +25 -0
- package/dist/coolify/config.d.ts.map +1 -1
- package/dist/coolify/index.d.ts +139 -12
- package/dist/coolify/index.d.ts.map +1 -1
- package/dist/coolify/types.d.ts +160 -2
- package/dist/coolify/types.d.ts.map +1 -1
- package/dist/examples/demo-ui.d.ts +8 -0
- package/dist/examples/demo-ui.d.ts.map +1 -0
- package/dist/index.cjs +2580 -230
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2598 -226
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +96 -7
- package/dist/sdk.d.ts.map +1 -1
- package/dist/server/stdio.js +475 -73
- 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 +17 -4
- package/src/cli/actions.ts +9 -2
- package/src/cli/commands/create.ts +332 -24
- package/src/cli/commands/db.ts +37 -0
- package/src/cli/commands/delete.ts +6 -2
- package/src/cli/commands/deploy.ts +347 -49
- package/src/cli/commands/deployments.ts +6 -2
- package/src/cli/commands/diagnose.ts +3 -3
- package/src/cli/commands/env.ts +424 -31
- package/src/cli/commands/exec.ts +6 -2
- package/src/cli/commands/init.ts +991 -0
- package/src/cli/commands/logs.ts +224 -24
- package/src/cli/commands/main-menu.ts +21 -0
- package/src/cli/commands/projects.ts +312 -29
- package/src/cli/commands/restart.ts +6 -2
- package/src/cli/commands/service-logs.ts +14 -0
- package/src/cli/commands/show.ts +45 -12
- package/src/cli/commands/start.ts +6 -2
- package/src/cli/commands/status.ts +554 -0
- package/src/cli/commands/stop.ts +6 -2
- package/src/cli/commands/svc.ts +7 -1
- package/src/cli/commands/update.ts +79 -2
- package/src/cli/commands/volumes.ts +293 -0
- package/src/cli/coolify-state.ts +203 -12
- package/src/cli/index.ts +138 -11
- package/src/cli/name-resolver.ts +228 -0
- package/src/cli/ui/banner.ts +276 -0
- package/src/cli/ui/highlighter.ts +176 -0
- package/src/cli/ui/index.ts +9 -0
- package/src/cli/ui/prompts.ts +155 -0
- package/src/cli/ui/screen.ts +630 -0
- package/src/cli/ui/select.ts +280 -0
- package/src/cli/ui/spinners.ts +256 -0
- package/src/cli/ui/tables.ts +407 -0
- package/src/coolify/config.ts +75 -0
- package/src/coolify/index.ts +565 -101
- package/src/coolify/types.ts +165 -2
- package/src/examples/demo-ui.ts +78 -0
- package/src/sdk.ts +211 -1
- 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
|
@@ -9,8 +9,10 @@
|
|
|
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";
|
|
15
|
+
import { resolveAppNameOrUuid } from "../name-resolver.js";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Options for the update command.
|
|
@@ -29,7 +31,16 @@ interface IUpdateCommandOptions {
|
|
|
29
31
|
baseDirectory?: string;
|
|
30
32
|
domains?: string;
|
|
31
33
|
autoDeploy?: boolean;
|
|
34
|
+
watchPaths?: string;
|
|
35
|
+
clearWatchPaths?: boolean;
|
|
32
36
|
forceHttps?: boolean;
|
|
37
|
+
healthCheckEnabled?: boolean;
|
|
38
|
+
healthCheckPath?: string;
|
|
39
|
+
healthCheckPort?: string;
|
|
40
|
+
healthCheckInterval?: string;
|
|
41
|
+
healthCheckTimeout?: string;
|
|
42
|
+
healthCheckRetries?: string;
|
|
43
|
+
healthCheckStartPeriod?: string;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
@@ -40,10 +51,13 @@ interface IUpdateCommandOptions {
|
|
|
40
51
|
export async function updateCommand(
|
|
41
52
|
options: IUpdateCommandOptions,
|
|
42
53
|
): Promise<void> {
|
|
43
|
-
|
|
54
|
+
let uuid = resolveUuid(options.uuid);
|
|
55
|
+
if (!uuid && options.uuid) {
|
|
56
|
+
uuid = await resolveAppNameOrUuid(options.uuid);
|
|
57
|
+
}
|
|
44
58
|
if (!uuid) {
|
|
45
59
|
console.error(
|
|
46
|
-
chalk.red("Error: No UUID provided and no .coolify.json found"),
|
|
60
|
+
chalk.red("Error: No UUID/name provided and no .coolify.json found"),
|
|
47
61
|
);
|
|
48
62
|
return;
|
|
49
63
|
}
|
|
@@ -82,7 +96,26 @@ export async function updateCommand(
|
|
|
82
96
|
if (options.domains) updateOptions.domains = options.domains;
|
|
83
97
|
if (options.autoDeploy !== undefined)
|
|
84
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
|
+
}
|
|
85
104
|
if (options.forceHttps) updateOptions.isForceHttpsEnabled = true;
|
|
105
|
+
if (options.healthCheckEnabled !== undefined)
|
|
106
|
+
updateOptions.healthCheckEnabled = options.healthCheckEnabled;
|
|
107
|
+
if (options.healthCheckPath)
|
|
108
|
+
updateOptions.healthCheckPath = options.healthCheckPath;
|
|
109
|
+
if (options.healthCheckPort)
|
|
110
|
+
updateOptions.healthCheckPort = options.healthCheckPort;
|
|
111
|
+
if (options.healthCheckInterval)
|
|
112
|
+
updateOptions.healthCheckInterval = parseInt(options.healthCheckInterval, 10);
|
|
113
|
+
if (options.healthCheckTimeout)
|
|
114
|
+
updateOptions.healthCheckTimeout = parseInt(options.healthCheckTimeout, 10);
|
|
115
|
+
if (options.healthCheckRetries)
|
|
116
|
+
updateOptions.healthCheckRetries = parseInt(options.healthCheckRetries, 10);
|
|
117
|
+
if (options.healthCheckStartPeriod)
|
|
118
|
+
updateOptions.healthCheckStartPeriod = parseInt(options.healthCheckStartPeriod, 10);
|
|
86
119
|
|
|
87
120
|
if (Object.keys(updateOptions).length === 0) {
|
|
88
121
|
console.warn(
|
|
@@ -108,4 +141,48 @@ export async function updateCommand(
|
|
|
108
141
|
if (result.value.description) {
|
|
109
142
|
console.log(chalk.gray(`Description: ${result.value.description}`));
|
|
110
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
|
+
}
|
|
111
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
|
@@ -8,21 +8,33 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
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
|
-
* State stored in .coolify.json (generated by create-bunspace deployer).
|
|
16
|
+
* State stored in .coolify.json (generated by create-bunspace deployer or init command).
|
|
17
|
+
*
|
|
18
|
+
* This file links a local directory to a Coolify deployment, storing all necessary
|
|
19
|
+
* UUIDs and metadata for CLI commands to work without manual UUID entry.
|
|
16
20
|
*/
|
|
17
21
|
export interface ICoolifyDeployState {
|
|
18
22
|
/** Application UUID in Coolify */
|
|
19
23
|
appUuid: string;
|
|
24
|
+
/** Application name (for display purposes) */
|
|
25
|
+
appName?: string;
|
|
20
26
|
/** Server UUID */
|
|
21
27
|
serverUuid: string;
|
|
28
|
+
/** Server name (for display purposes) */
|
|
29
|
+
serverName?: string;
|
|
22
30
|
/** Project UUID */
|
|
23
31
|
projectUuid: string;
|
|
32
|
+
/** Project name (for display purposes) */
|
|
33
|
+
projectName?: string;
|
|
24
34
|
/** Environment UUID */
|
|
25
35
|
environmentUuid: string;
|
|
36
|
+
/** Environment name (for display purposes) */
|
|
37
|
+
environmentName?: string;
|
|
26
38
|
/** Domain if configured */
|
|
27
39
|
domain?: string;
|
|
28
40
|
/** Docker compose or Dockerfile path */
|
|
@@ -31,18 +43,80 @@ export interface ICoolifyDeployState {
|
|
|
31
43
|
baseDirectory?: string;
|
|
32
44
|
/** Git branch */
|
|
33
45
|
branch?: string;
|
|
34
|
-
/**
|
|
46
|
+
/** Git repository URL */
|
|
47
|
+
gitRepository?: string;
|
|
48
|
+
/** Application source type (github-app, deploy-key, docker-image, etc.) */
|
|
49
|
+
sourceType?: string;
|
|
50
|
+
/** Application type (legacy, may be undefined) */
|
|
35
51
|
type?: string;
|
|
36
|
-
/** Build pack */
|
|
52
|
+
/** Build pack (dockerfile, nixpacks, static, dockercompose) */
|
|
37
53
|
buildPack?: string;
|
|
38
|
-
/**
|
|
54
|
+
/** Ports exposed (comma-separated) */
|
|
55
|
+
portsExposes?: string;
|
|
56
|
+
/** Auto-deploy enabled (Coolify default: true) */
|
|
39
57
|
autoDeployEnabled?: boolean;
|
|
58
|
+
/** Watch paths for selective auto-deploy (newline-separated globs, null = all changes) */
|
|
59
|
+
watchPaths?: string | null;
|
|
60
|
+
/** When this state was last updated */
|
|
61
|
+
updatedAt?: string;
|
|
62
|
+
/** Coolify instance URL */
|
|
63
|
+
coolifyUrl?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Multi-app state for monorepos deploying multiple services from one repo.
|
|
68
|
+
*
|
|
69
|
+
* Stored alongside or instead of the single-app state in .coolify.json.
|
|
70
|
+
*/
|
|
71
|
+
export interface ICoolifyMultiAppState {
|
|
72
|
+
/** All apps deployed from this repo */
|
|
73
|
+
apps: Array<{
|
|
74
|
+
/** Application UUID in Coolify */
|
|
75
|
+
uuid: string;
|
|
76
|
+
/** Application name */
|
|
77
|
+
name: string;
|
|
78
|
+
/** Service role (e.g. "backend", "admin", "docs", "proxy") */
|
|
79
|
+
service: string;
|
|
80
|
+
/** Domain if configured (with protocol). Null for internal services. */
|
|
81
|
+
domain?: string | null;
|
|
82
|
+
/** Dockerfile path relative to repo root */
|
|
83
|
+
dockerfile?: string | null;
|
|
84
|
+
/** Exposed port */
|
|
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;
|
|
90
|
+
}>;
|
|
91
|
+
/** Server UUID */
|
|
92
|
+
serverUuid: string;
|
|
93
|
+
/** Server name (for display purposes) */
|
|
94
|
+
serverName?: string;
|
|
95
|
+
/** Project UUID */
|
|
96
|
+
projectUuid: string;
|
|
97
|
+
/** Project name (for display purposes) */
|
|
98
|
+
projectName?: string;
|
|
99
|
+
/** Environment UUID */
|
|
100
|
+
environmentUuid: string;
|
|
101
|
+
/** Environment name (for display purposes) */
|
|
102
|
+
environmentName?: string;
|
|
103
|
+
/** Git branch */
|
|
104
|
+
branch?: string;
|
|
105
|
+
/** Git repository URL */
|
|
106
|
+
gitRepository?: string;
|
|
107
|
+
/** Coolify instance URL */
|
|
108
|
+
coolifyUrl?: string;
|
|
109
|
+
/** When this state was last updated */
|
|
110
|
+
updatedAt?: string;
|
|
40
111
|
}
|
|
41
112
|
|
|
42
113
|
const STATE_FILE = ".coolify.json";
|
|
43
114
|
|
|
44
115
|
/**
|
|
45
|
-
* Loads .coolify.json from the current working directory.
|
|
116
|
+
* Loads .coolify.json from the current working directory (single-app format).
|
|
117
|
+
*
|
|
118
|
+
* Handles both single-app and multi-app formats. For multi-app state with
|
|
119
|
+
* exactly one app, returns a synthesized single-app state for backward compat.
|
|
46
120
|
*
|
|
47
121
|
* @returns The deploy state if found, null otherwise
|
|
48
122
|
*/
|
|
@@ -55,21 +129,127 @@ export function loadCoolifyState(): ICoolifyDeployState | null {
|
|
|
55
129
|
|
|
56
130
|
try {
|
|
57
131
|
const content = readFileSync(statePath, "utf-8");
|
|
58
|
-
const state = JSON.parse(content)
|
|
132
|
+
const state = JSON.parse(content);
|
|
59
133
|
|
|
134
|
+
// Multi-app format — synthesize single-app state if exactly 1 app
|
|
135
|
+
if (Array.isArray(state.apps)) {
|
|
136
|
+
if (state.apps.length === 1) {
|
|
137
|
+
const app = state.apps[0];
|
|
138
|
+
return {
|
|
139
|
+
appUuid: app.uuid,
|
|
140
|
+
appName: app.name,
|
|
141
|
+
serverUuid: state.serverUuid,
|
|
142
|
+
serverName: state.serverName,
|
|
143
|
+
projectUuid: state.projectUuid,
|
|
144
|
+
projectName: state.projectName,
|
|
145
|
+
environmentUuid: state.environmentUuid,
|
|
146
|
+
environmentName: state.environmentName,
|
|
147
|
+
domain: app.domain,
|
|
148
|
+
branch: state.branch,
|
|
149
|
+
gitRepository: state.gitRepository,
|
|
150
|
+
coolifyUrl: state.coolifyUrl,
|
|
151
|
+
updatedAt: state.updatedAt,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Multi-app with multiple apps — cannot auto-select
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Single-app format
|
|
60
159
|
if (!state.appUuid) {
|
|
61
160
|
return null;
|
|
62
161
|
}
|
|
63
162
|
|
|
64
|
-
|
|
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
|
+
|
|
176
|
+
return state as ICoolifyDeployState;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Loads .coolify.json in multi-app format.
|
|
184
|
+
*
|
|
185
|
+
* If the file is in single-app format, returns null (use loadCoolifyState instead).
|
|
186
|
+
*
|
|
187
|
+
* @returns The multi-app state if found, null otherwise
|
|
188
|
+
*/
|
|
189
|
+
export function loadMultiAppState(): ICoolifyMultiAppState | null {
|
|
190
|
+
const statePath = join(process.cwd(), STATE_FILE);
|
|
191
|
+
|
|
192
|
+
if (!existsSync(statePath)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const content = readFileSync(statePath, "utf-8");
|
|
198
|
+
const state = JSON.parse(content);
|
|
199
|
+
|
|
200
|
+
if (!Array.isArray(state.apps)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return state as ICoolifyMultiAppState;
|
|
65
205
|
} catch {
|
|
66
206
|
return null;
|
|
67
207
|
}
|
|
68
208
|
}
|
|
69
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Writes .coolify.json in multi-app format to the current working directory.
|
|
212
|
+
*
|
|
213
|
+
* @param state - The multi-app state to write
|
|
214
|
+
*/
|
|
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
|
+
}
|
|
224
|
+
const statePath = join(process.cwd(), STATE_FILE);
|
|
225
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Writes .coolify.json to the current working directory.
|
|
230
|
+
*
|
|
231
|
+
* @param state - The state to write
|
|
232
|
+
*/
|
|
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
|
+
}
|
|
242
|
+
const statePath = join(process.cwd(), STATE_FILE);
|
|
243
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
244
|
+
}
|
|
245
|
+
|
|
70
246
|
/**
|
|
71
247
|
* Resolves a UUID argument — if not provided, tries to read from .coolify.json.
|
|
72
248
|
*
|
|
249
|
+
* Supports both single-app and multi-app state formats. For multi-app state
|
|
250
|
+
* with exactly one app, auto-selects that app. For multiple apps, returns null
|
|
251
|
+
* (user must specify explicitly).
|
|
252
|
+
*
|
|
73
253
|
* @param uuid - UUID from CLI argument (may be undefined)
|
|
74
254
|
* @param field - Which field to read from .coolify.json (default: appUuid)
|
|
75
255
|
* @returns The resolved UUID or null if not found
|
|
@@ -78,11 +258,22 @@ export function resolveUuid(
|
|
|
78
258
|
uuid: string | undefined,
|
|
79
259
|
field: keyof ICoolifyDeployState = "appUuid",
|
|
80
260
|
): string | null {
|
|
81
|
-
if (
|
|
261
|
+
// Only return directly if it looks like a UUID (lowercase alphanumeric, 16-40 chars)
|
|
262
|
+
// Names like "mks-backend" should fall through to .coolify.json or name resolver
|
|
263
|
+
if (uuid && /^[a-z0-9]{16,40}$/.test(uuid)) return uuid;
|
|
82
264
|
|
|
265
|
+
// Try single-app state first (also handles multi-app with 1 app)
|
|
83
266
|
const state = loadCoolifyState();
|
|
84
|
-
if (
|
|
267
|
+
if (state) {
|
|
268
|
+
const value = state[field];
|
|
269
|
+
return typeof value === "string" ? value : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Try multi-app state — only auto-resolve if exactly 1 app
|
|
273
|
+
const multiState = loadMultiAppState();
|
|
274
|
+
if (multiState && multiState.apps.length === 1 && field === "appUuid") {
|
|
275
|
+
return multiState.apps[0].uuid;
|
|
276
|
+
}
|
|
85
277
|
|
|
86
|
-
|
|
87
|
-
return typeof value === "string" ? value : null;
|
|
278
|
+
return null;
|
|
88
279
|
}
|