@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.
Files changed (52) hide show
  1. package/.github/workflows/publish-core.yml +1 -1
  2. package/dist/deployers/coolify.cjs +61 -50
  3. package/dist/deployers/coolify.cjs.map +1 -1
  4. package/dist/deployers/coolify.d.cts +4 -11
  5. package/dist/deployers/coolify.d.cts.map +1 -1
  6. package/dist/deployers/coolify.d.mts +4 -11
  7. package/dist/deployers/coolify.d.mts.map +1 -1
  8. package/dist/deployers/coolify.mjs +62 -50
  9. package/dist/deployers/coolify.mjs.map +1 -1
  10. package/dist/deployers/dokploy.cjs +106 -29
  11. package/dist/deployers/dokploy.cjs.map +1 -1
  12. package/dist/deployers/dokploy.d.cts +2 -1
  13. package/dist/deployers/dokploy.d.cts.map +1 -1
  14. package/dist/deployers/dokploy.d.mts +2 -1
  15. package/dist/deployers/dokploy.d.mts.map +1 -1
  16. package/dist/deployers/dokploy.mjs +107 -29
  17. package/dist/deployers/dokploy.mjs.map +1 -1
  18. package/dist/deployers/index.cjs.map +1 -1
  19. package/dist/deployers/index.d.cts +2 -2
  20. package/dist/deployers/index.d.cts.map +1 -1
  21. package/dist/deployers/index.d.mts +2 -2
  22. package/dist/deployers/index.d.mts.map +1 -1
  23. package/dist/deployers/index.mjs.map +1 -1
  24. package/dist/deployers/strip-host-ports.cjs +138 -0
  25. package/dist/deployers/strip-host-ports.cjs.map +1 -0
  26. package/dist/deployers/strip-host-ports.d.cts +62 -0
  27. package/dist/deployers/strip-host-ports.d.cts.map +1 -0
  28. package/dist/deployers/strip-host-ports.d.mts +62 -0
  29. package/dist/deployers/strip-host-ports.d.mts.map +1 -0
  30. package/dist/deployers/strip-host-ports.mjs +133 -0
  31. package/dist/deployers/strip-host-ports.mjs.map +1 -0
  32. package/dist/deployers/strip-host-ports.test.cjs +89 -0
  33. package/dist/deployers/strip-host-ports.test.cjs.map +1 -0
  34. package/dist/deployers/strip-host-ports.test.d.cts +1 -0
  35. package/dist/deployers/strip-host-ports.test.d.mts +1 -0
  36. package/dist/deployers/strip-host-ports.test.mjs +90 -0
  37. package/dist/deployers/strip-host-ports.test.mjs.map +1 -0
  38. package/dist/deployers/types.d.cts +173 -2
  39. package/dist/deployers/types.d.cts.map +1 -1
  40. package/dist/deployers/types.d.mts +173 -2
  41. package/dist/deployers/types.d.mts.map +1 -1
  42. package/dist/index.d.cts +2 -2
  43. package/dist/index.d.mts +2 -2
  44. package/package.json +2 -1
  45. package/src/deployers/coolify.ts +198 -103
  46. package/src/deployers/dokploy.ts +209 -55
  47. package/src/deployers/index.ts +1 -0
  48. package/src/deployers/strip-host-ports.test.ts +100 -0
  49. package/src/deployers/strip-host-ports.ts +187 -0
  50. package/src/deployers/types.ts +185 -1
  51. package/src/index.ts +19 -4
  52. package/tsconfig.tsbuildinfo +1 -0
@@ -1,49 +1,61 @@
1
1
  /**
2
- * Coolify PaaS deployer — deploys Docker Compose stacks via the Coolify v4 REST API.
2
+ * Coolify PaaS deployer
3
3
  *
4
- * API docs: https://coolify.io/docs/api-reference/api/
5
- * Auth: Authorization: Bearer <token>
6
- * Base path: /api/v1
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
- /** Shape returned by Coolify's project endpoints. */
19
+ /* ----------------------------- */
20
+ /* Coolify API Types */
21
+ /* ----------------------------- */
22
+
12
23
  interface CoolifyProject {
13
24
  uuid: string;
14
25
  name: string;
15
- environments?: { uuid: string; name: string }[];
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
- /** Build a full Coolify API URL (base + /api/v1 + path). */
38
- function apiUrl(target: DeployTarget, path: string): string {
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
- // use raw text
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
- return JSON.parse(text) as T;
88
+
89
+ return JSON.parse(text);
76
90
  }
77
91
 
78
- /**
79
- * Parse .env content into key-value pairs for Coolify's bulk env API.
80
- */
81
- function parseEnvContent(envContent: string): {
82
- key: string;
83
- value: string;
84
- is_preview: boolean;
85
- is_build_time: boolean;
86
- is_literal: boolean;
87
- }[] {
88
- const result: {
89
- key: string;
90
- value: string;
91
- is_preview: boolean;
92
- is_build_time: boolean;
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 eqIdx = trimmed.indexOf("=");
101
- if (eqIdx <= 0) continue;
113
+ const idx = trimmed.indexOf("=");
102
114
 
103
- const key = trimmed.slice(0, eqIdx);
104
- const value = trimmed.slice(eqIdx + 1);
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
- * Deploys Docker Compose stacks to a Coolify v4 instance.
120
- *
121
- * Deploy flow (5 steps):
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): Promise<{ ok: boolean; error?: string }> {
140
+ async testConnection(target: DeployTarget) {
133
141
  try {
134
- await coolifyFetch<unknown>(target, "/version");
142
+ await coolifyFetch(target, "/version");
143
+
135
144
  return { ok: true };
136
145
  } catch (err) {
137
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
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 = { step: "Create project", status: "pending" };
144
- const step3: DeployStep = { step: "Create compose service", status: "pending" };
145
- const step4: DeployStep = { step: "Set environment variables", status: "pending" };
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
- // Step 1: Discover default server
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
- if (!servers || servers.length === 0) {
156
- throw new Error("No servers found in Coolify instance");
184
+
185
+ if (!servers.length) {
186
+ throw new Error("No Coolify servers available");
157
187
  }
158
- const server = servers[0] as CoolifyServer;
188
+
189
+ const server = servers[0]!;
190
+
159
191
  step1.status = "done";
160
- step1.detail = `Server: ${server.name} (${server.ip})`;
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
- const project = await coolifyFetch<CoolifyProject>(input.target, "/projects", {
165
- method: "POST",
166
- body: {
167
- name: input.projectName,
168
- description: input.description ?? `OpenClaw stack: ${input.projectName}`,
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 = `Project: ${project.uuid}`;
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
- // Step 3: Create compose service with docker_compose_raw
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
- const service = await coolifyFetch<CoolifyService>(input.target, "/services", {
189
- method: "POST",
190
- body: {
191
- project_uuid: project.uuid,
192
- server_uuid: server.uuid,
193
- environment_name: envName,
194
- environment_uuid: envUuid,
195
- docker_compose_raw: input.composeYaml,
196
- name: input.projectName,
197
- description: input.description ?? "Deployed via OpenClaw web builder",
198
- instant_deploy: false,
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 = `Service: ${service.uuid}`;
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
- if (envVars.length > 0) {
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} variables set`;
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
- const deployments = await coolifyFetch<{ deployments: CoolifyDeployment[] }>(
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?.deployments?.[0]?.deployment_uuid ?? "Deployment triggered";
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 failedStep = steps.find((s) => s.status === "running");
231
- if (failedStep) {
232
- failedStep.status = "error";
233
- failedStep.detail = err instanceof Error ? err.message : String(err);
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
- return result;
332
+ return result;
333
+ }
239
334
  }
240
335
  }