@better-openclaw/core 1.0.20 → 1.0.21
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/.github/workflows/publish-core.yml +1 -1
- package/dist/deployers/coolify.cjs +61 -50
- package/dist/deployers/coolify.cjs.map +1 -1
- package/dist/deployers/coolify.d.cts +4 -11
- package/dist/deployers/coolify.d.cts.map +1 -1
- package/dist/deployers/coolify.d.mts +4 -11
- package/dist/deployers/coolify.d.mts.map +1 -1
- package/dist/deployers/coolify.mjs +62 -50
- package/dist/deployers/coolify.mjs.map +1 -1
- package/dist/deployers/dokploy.cjs +106 -29
- package/dist/deployers/dokploy.cjs.map +1 -1
- package/dist/deployers/dokploy.d.cts +2 -1
- package/dist/deployers/dokploy.d.cts.map +1 -1
- package/dist/deployers/dokploy.d.mts +2 -1
- package/dist/deployers/dokploy.d.mts.map +1 -1
- package/dist/deployers/dokploy.mjs +107 -29
- package/dist/deployers/dokploy.mjs.map +1 -1
- package/dist/deployers/index.cjs.map +1 -1
- package/dist/deployers/index.d.cts +2 -2
- package/dist/deployers/index.d.cts.map +1 -1
- package/dist/deployers/index.d.mts +2 -2
- package/dist/deployers/index.d.mts.map +1 -1
- package/dist/deployers/index.mjs.map +1 -1
- package/dist/deployers/strip-host-ports.cjs +138 -0
- package/dist/deployers/strip-host-ports.cjs.map +1 -0
- package/dist/deployers/strip-host-ports.d.cts +62 -0
- package/dist/deployers/strip-host-ports.d.cts.map +1 -0
- package/dist/deployers/strip-host-ports.d.mts +62 -0
- package/dist/deployers/strip-host-ports.d.mts.map +1 -0
- package/dist/deployers/strip-host-ports.mjs +133 -0
- package/dist/deployers/strip-host-ports.mjs.map +1 -0
- package/dist/deployers/strip-host-ports.test.cjs +89 -0
- package/dist/deployers/strip-host-ports.test.cjs.map +1 -0
- package/dist/deployers/strip-host-ports.test.d.cts +1 -0
- package/dist/deployers/strip-host-ports.test.d.mts +1 -0
- package/dist/deployers/strip-host-ports.test.mjs +90 -0
- package/dist/deployers/strip-host-ports.test.mjs.map +1 -0
- package/dist/deployers/types.d.cts +173 -2
- package/dist/deployers/types.d.cts.map +1 -1
- package/dist/deployers/types.d.mts +173 -2
- package/dist/deployers/types.d.mts.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/package.json +2 -1
- package/src/deployers/coolify.ts +198 -103
- package/src/deployers/dokploy.ts +209 -55
- package/src/deployers/index.ts +1 -0
- package/src/deployers/strip-host-ports.test.ts +100 -0
- package/src/deployers/strip-host-ports.ts +187 -0
- package/src/deployers/types.ts +185 -1
- package/src/index.ts +19 -4
- package/tsconfig.tsbuildinfo +1 -0
package/src/deployers/coolify.ts
CHANGED
|
@@ -1,49 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Coolify PaaS deployer
|
|
2
|
+
* Coolify PaaS deployer
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Deploys Docker Compose stacks via Coolify v4 API
|
|
5
|
+
*
|
|
6
|
+
* Docs:
|
|
7
|
+
* https://coolify.io/docs/api-reference/api
|
|
8
|
+
*
|
|
9
|
+
* Auth:
|
|
10
|
+
* Authorization: Bearer <token>
|
|
11
|
+
*
|
|
12
|
+
* Base path:
|
|
13
|
+
* /api/v1
|
|
7
14
|
*/
|
|
8
15
|
|
|
16
|
+
import { sanitizeComposeForPaas } from "./strip-host-ports.js";
|
|
9
17
|
import type { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from "./types.js";
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
/* ----------------------------- */
|
|
20
|
+
/* Coolify API Types */
|
|
21
|
+
/* ----------------------------- */
|
|
22
|
+
|
|
12
23
|
interface CoolifyProject {
|
|
13
24
|
uuid: string;
|
|
14
25
|
name: string;
|
|
15
|
-
environments?: {
|
|
26
|
+
environments?: {
|
|
27
|
+
uuid: string;
|
|
28
|
+
name: string;
|
|
29
|
+
}[];
|
|
16
30
|
}
|
|
17
31
|
|
|
18
|
-
/** Shape returned by Coolify's server listing. */
|
|
19
32
|
interface CoolifyServer {
|
|
20
33
|
uuid: string;
|
|
21
34
|
name: string;
|
|
22
35
|
ip: string;
|
|
23
36
|
}
|
|
24
37
|
|
|
25
|
-
/** Shape returned when creating a Coolify service (compose stack). */
|
|
26
38
|
interface CoolifyService {
|
|
27
39
|
uuid: string;
|
|
40
|
+
name: string;
|
|
41
|
+
docker_compose_raw?: string;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
/** Shape returned by Coolify's deploy trigger endpoint. */
|
|
31
44
|
interface CoolifyDeployment {
|
|
32
45
|
message: string;
|
|
33
46
|
resource_uuid: string;
|
|
34
47
|
deployment_uuid: string;
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
/* ----------------------------- */
|
|
51
|
+
/* Utilities */
|
|
52
|
+
/* ----------------------------- */
|
|
53
|
+
|
|
54
|
+
function apiUrl(target: DeployTarget, path: string) {
|
|
39
55
|
const base = target.instanceUrl.replace(/\/+$/, "");
|
|
40
56
|
return `${base}/api/v1${path}`;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
|
-
/**
|
|
44
|
-
* Typed fetch wrapper for the Coolify v4 API.
|
|
45
|
-
* Handles JSON serialisation, Bearer auth, and error extraction.
|
|
46
|
-
*/
|
|
47
59
|
async function coolifyFetch<T>(
|
|
48
60
|
target: DeployTarget,
|
|
49
61
|
path: string,
|
|
@@ -61,47 +73,49 @@ async function coolifyFetch<T>(
|
|
|
61
73
|
if (!res.ok) {
|
|
62
74
|
const text = await res.text().catch(() => "");
|
|
63
75
|
let detail = text;
|
|
76
|
+
|
|
64
77
|
try {
|
|
65
78
|
const json = JSON.parse(text);
|
|
66
79
|
detail = json.message || json.error || text;
|
|
67
|
-
} catch {
|
|
68
|
-
|
|
69
|
-
}
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
70
82
|
throw new Error(`Coolify API ${res.status}: ${detail}`);
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
const text = await res.text();
|
|
86
|
+
|
|
74
87
|
if (!text) return undefined as T;
|
|
75
|
-
|
|
88
|
+
|
|
89
|
+
return JSON.parse(text);
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
is_literal: boolean;
|
|
94
|
-
}[] = [];
|
|
92
|
+
function hashString(str: string) {
|
|
93
|
+
let hash = 0;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < str.length; i++) {
|
|
96
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
97
|
+
hash |= 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return hash;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseEnvContent(envContent?: string) {
|
|
104
|
+
if (!envContent) return [];
|
|
105
|
+
|
|
106
|
+
const result = [];
|
|
95
107
|
|
|
96
108
|
for (const line of envContent.split("\n")) {
|
|
97
109
|
const trimmed = line.trim();
|
|
110
|
+
|
|
98
111
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
99
112
|
|
|
100
|
-
const
|
|
101
|
-
if (eqIdx <= 0) continue;
|
|
113
|
+
const idx = trimmed.indexOf("=");
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
if (idx <= 0) continue;
|
|
116
|
+
|
|
117
|
+
const key = trimmed.slice(0, idx);
|
|
118
|
+
const value = trimmed.slice(idx + 1);
|
|
105
119
|
|
|
106
120
|
result.push({
|
|
107
121
|
key,
|
|
@@ -115,126 +129,207 @@ function parseEnvContent(envContent: string): {
|
|
|
115
129
|
return result;
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* 1. Discover the first available server
|
|
123
|
-
* 2. Create a Coolify project
|
|
124
|
-
* 3. Create a compose service with the raw docker-compose YAML
|
|
125
|
-
* 4. Push .env variables via the bulk env API
|
|
126
|
-
* 5. Trigger the deployment
|
|
127
|
-
*/
|
|
132
|
+
/* ----------------------------- */
|
|
133
|
+
/* Coolify Deployer */
|
|
134
|
+
/* ----------------------------- */
|
|
135
|
+
|
|
128
136
|
export class CoolifyDeployer implements PaasDeployer {
|
|
129
137
|
readonly name = "Coolify";
|
|
130
138
|
readonly id = "coolify";
|
|
131
139
|
|
|
132
|
-
async testConnection(target: DeployTarget)
|
|
140
|
+
async testConnection(target: DeployTarget) {
|
|
133
141
|
try {
|
|
134
|
-
await coolifyFetch
|
|
142
|
+
await coolifyFetch(target, "/version");
|
|
143
|
+
|
|
135
144
|
return { ok: true };
|
|
136
145
|
} catch (err) {
|
|
137
|
-
return {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: err instanceof Error ? err.message : String(err),
|
|
149
|
+
};
|
|
138
150
|
}
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
async deploy(input: DeployInput): Promise<DeployResult> {
|
|
142
154
|
const step1: DeployStep = { step: "Discover server", status: "pending" };
|
|
143
|
-
const step2: DeployStep = {
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
const step2: DeployStep = {
|
|
156
|
+
step: "Find or create project",
|
|
157
|
+
status: "pending",
|
|
158
|
+
};
|
|
159
|
+
const step3: DeployStep = {
|
|
160
|
+
step: "Find or create service",
|
|
161
|
+
status: "pending",
|
|
162
|
+
};
|
|
163
|
+
const step4: DeployStep = {
|
|
164
|
+
step: "Update environment variables",
|
|
165
|
+
status: "pending",
|
|
166
|
+
};
|
|
146
167
|
const step5: DeployStep = { step: "Trigger deployment", status: "pending" };
|
|
147
168
|
const steps: DeployStep[] = [step1, step2, step3, step4, step5];
|
|
148
169
|
|
|
149
170
|
const result: DeployResult = { success: false, steps };
|
|
150
171
|
|
|
172
|
+
// Strip host port bindings — Coolify routes via Traefik,
|
|
173
|
+
// so host ports are unnecessary and cause "port already allocated" errors.
|
|
174
|
+
const composeYaml = sanitizeComposeForPaas(input.composeYaml);
|
|
175
|
+
|
|
151
176
|
try {
|
|
152
|
-
|
|
177
|
+
/* ----------------------------- */
|
|
178
|
+
/* STEP 1: Discover server */
|
|
179
|
+
/* ----------------------------- */
|
|
180
|
+
|
|
153
181
|
step1.status = "running";
|
|
182
|
+
|
|
154
183
|
const servers = await coolifyFetch<CoolifyServer[]>(input.target, "/servers");
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
|
|
185
|
+
if (!servers.length) {
|
|
186
|
+
throw new Error("No Coolify servers available");
|
|
157
187
|
}
|
|
158
|
-
|
|
188
|
+
|
|
189
|
+
const server = servers[0]!;
|
|
190
|
+
|
|
159
191
|
step1.status = "done";
|
|
160
|
-
step1.detail =
|
|
192
|
+
step1.detail = `${server.name} (${server.ip})`;
|
|
193
|
+
|
|
194
|
+
/* ----------------------------- */
|
|
195
|
+
/* STEP 2: Find or create project */
|
|
196
|
+
/* ----------------------------- */
|
|
161
197
|
|
|
162
|
-
// Step 2: Create project
|
|
163
198
|
step2.status = "running";
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
|
|
200
|
+
const projects = await coolifyFetch<CoolifyProject[]>(input.target, "/projects");
|
|
201
|
+
|
|
202
|
+
let project = projects.find((p) => p.name === input.projectName);
|
|
203
|
+
|
|
204
|
+
if (!project) {
|
|
205
|
+
project = await coolifyFetch<CoolifyProject>(input.target, "/projects", {
|
|
206
|
+
method: "POST",
|
|
207
|
+
body: {
|
|
208
|
+
name: input.projectName,
|
|
209
|
+
description: input.description ?? `Stack ${input.projectName}`,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
171
214
|
result.projectId = project.uuid;
|
|
215
|
+
|
|
172
216
|
step2.status = "done";
|
|
173
|
-
step2.detail =
|
|
217
|
+
step2.detail = project.uuid;
|
|
218
|
+
|
|
219
|
+
/* ----------------------------- */
|
|
220
|
+
/* Find environment */
|
|
221
|
+
/* ----------------------------- */
|
|
174
222
|
|
|
175
|
-
// Get the default environment
|
|
176
223
|
const projectDetail = await coolifyFetch<CoolifyProject>(
|
|
177
224
|
input.target,
|
|
178
225
|
`/projects/${project.uuid}`,
|
|
179
226
|
);
|
|
180
|
-
const envUuid = projectDetail.environments?.[0]?.uuid;
|
|
181
|
-
const envName = projectDetail.environments?.[0]?.name ?? "production";
|
|
182
|
-
if (!envUuid) {
|
|
183
|
-
throw new Error("No default environment found in project");
|
|
184
|
-
}
|
|
185
227
|
|
|
186
|
-
|
|
228
|
+
const env =
|
|
229
|
+
projectDetail.environments?.find((e) => e.name === "production") ??
|
|
230
|
+
projectDetail.environments?.[0];
|
|
231
|
+
|
|
232
|
+
if (!env) throw new Error("No environment found");
|
|
233
|
+
|
|
234
|
+
/* ----------------------------- */
|
|
235
|
+
/* STEP 3: Find or create service */
|
|
236
|
+
/* ----------------------------- */
|
|
237
|
+
|
|
187
238
|
step3.status = "running";
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
239
|
+
|
|
240
|
+
const services = await coolifyFetch<CoolifyService[]>(
|
|
241
|
+
input.target,
|
|
242
|
+
`/projects/${project.uuid}/services`,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
let service = services.find((s) => s.name === input.projectName);
|
|
246
|
+
|
|
247
|
+
if (!service) {
|
|
248
|
+
service = await coolifyFetch<CoolifyService>(input.target, "/services", {
|
|
249
|
+
method: "POST",
|
|
250
|
+
body: {
|
|
251
|
+
project_uuid: project.uuid,
|
|
252
|
+
|
|
253
|
+
server_uuid: server.uuid,
|
|
254
|
+
|
|
255
|
+
environment_uuid: env.uuid,
|
|
256
|
+
|
|
257
|
+
environment_name: env.name,
|
|
258
|
+
|
|
259
|
+
docker_compose_raw: composeYaml,
|
|
260
|
+
|
|
261
|
+
name: input.projectName,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
const newHash = hashString(composeYaml);
|
|
266
|
+
const oldHash = hashString(service.docker_compose_raw ?? "");
|
|
267
|
+
|
|
268
|
+
if (newHash !== oldHash) {
|
|
269
|
+
await coolifyFetch(input.target, `/services/${service.uuid}`, {
|
|
270
|
+
method: "PATCH",
|
|
271
|
+
body: {
|
|
272
|
+
docker_compose_raw: composeYaml,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
201
278
|
result.composeId = service.uuid;
|
|
279
|
+
|
|
202
280
|
step3.status = "done";
|
|
203
|
-
step3.detail =
|
|
281
|
+
step3.detail = service.uuid;
|
|
282
|
+
|
|
283
|
+
/* ----------------------------- */
|
|
284
|
+
/* STEP 4: Environment variables */
|
|
285
|
+
/* ----------------------------- */
|
|
204
286
|
|
|
205
|
-
// Step 4: Set environment variables
|
|
206
287
|
step4.status = "running";
|
|
288
|
+
|
|
207
289
|
const envVars = parseEnvContent(input.envContent);
|
|
208
|
-
|
|
290
|
+
|
|
291
|
+
if (envVars.length) {
|
|
209
292
|
await coolifyFetch(input.target, `/services/${service.uuid}/envs`, {
|
|
210
293
|
method: "PATCH",
|
|
211
294
|
body: envVars,
|
|
212
295
|
});
|
|
213
296
|
}
|
|
297
|
+
|
|
214
298
|
step4.status = "done";
|
|
215
|
-
step4.detail = `${envVars.length}
|
|
299
|
+
step4.detail = `${envVars.length} vars`;
|
|
300
|
+
|
|
301
|
+
/* ----------------------------- */
|
|
302
|
+
/* STEP 5: Deploy */
|
|
303
|
+
/* ----------------------------- */
|
|
216
304
|
|
|
217
|
-
// Step 5: Trigger deployment
|
|
218
305
|
step5.status = "running";
|
|
219
|
-
|
|
306
|
+
|
|
307
|
+
const deploy = await coolifyFetch<{ deployments: CoolifyDeployment[] }>(
|
|
220
308
|
input.target,
|
|
221
309
|
`/deploy?uuid=${service.uuid}&force=true`,
|
|
222
310
|
);
|
|
311
|
+
|
|
223
312
|
step5.status = "done";
|
|
224
|
-
step5.detail = deployments?.
|
|
313
|
+
step5.detail = deploy.deployments?.[0]?.deployment_uuid ?? "Deployment started";
|
|
225
314
|
|
|
226
315
|
result.success = true;
|
|
316
|
+
|
|
227
317
|
const base = input.target.instanceUrl.replace(/\/+$/, "");
|
|
318
|
+
|
|
228
319
|
result.dashboardUrl = `${base}/project/${project.uuid}`;
|
|
320
|
+
|
|
321
|
+
return result;
|
|
229
322
|
} catch (err) {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
323
|
+
const running = steps.find((s) => s.status === "running");
|
|
324
|
+
|
|
325
|
+
if (running) {
|
|
326
|
+
running.status = "error";
|
|
327
|
+
running.detail = err instanceof Error ? err.message : String(err);
|
|
234
328
|
}
|
|
329
|
+
|
|
235
330
|
result.error = err instanceof Error ? err.message : String(err);
|
|
236
|
-
}
|
|
237
331
|
|
|
238
|
-
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
239
334
|
}
|
|
240
335
|
}
|