@better-openclaw/core 1.0.18 → 1.0.20
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/dependabot.yml +32 -0
- package/.github/workflows/ci.yml +8 -8
- package/.github/workflows/publish-core.yml +3 -3
- package/SECURITY.md +62 -0
- package/dist/deployers/coolify.cjs +44 -39
- package/dist/deployers/coolify.cjs.map +1 -1
- package/dist/deployers/coolify.mjs +44 -39
- package/dist/deployers/coolify.mjs.map +1 -1
- package/dist/deployers/dokploy.cjs +34 -30
- package/dist/deployers/dokploy.cjs.map +1 -1
- package/dist/deployers/dokploy.mjs +34 -30
- package/dist/deployers/dokploy.mjs.map +1 -1
- package/dist/port-scanner.cjs +1 -1
- package/dist/port-scanner.cjs.map +1 -1
- package/dist/port-scanner.mjs +1 -1
- package/dist/port-scanner.mjs.map +1 -1
- package/dist/services/definitions/usesend.cjs +4 -4
- package/dist/services/definitions/usesend.cjs.map +1 -1
- package/dist/services/definitions/usesend.mjs +4 -4
- package/dist/services/definitions/usesend.mjs.map +1 -1
- package/package.json +3 -4
- package/src/__snapshots__/composer.snapshot.test.ts.snap +248 -38
- package/src/deployers/coolify.ts +26 -27
- package/src/deployers/dokploy.ts +19 -20
- package/src/port-scanner.ts +1 -1
- package/src/services/definitions/usesend.ts +4 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: npm
|
|
9
|
+
directory: /
|
|
10
|
+
schedule:
|
|
11
|
+
interval: weekly
|
|
12
|
+
day: monday
|
|
13
|
+
open-pull-requests-limit: 10
|
|
14
|
+
groups:
|
|
15
|
+
dev-dependencies:
|
|
16
|
+
dependency-type: development
|
|
17
|
+
update-types:
|
|
18
|
+
- minor
|
|
19
|
+
- patch
|
|
20
|
+
production-dependencies:
|
|
21
|
+
dependency-type: production
|
|
22
|
+
update-types:
|
|
23
|
+
- patch
|
|
24
|
+
labels:
|
|
25
|
+
- dependencies
|
|
26
|
+
|
|
27
|
+
- package-ecosystem: github-actions
|
|
28
|
+
directory: /
|
|
29
|
+
schedule:
|
|
30
|
+
interval: weekly
|
|
31
|
+
labels:
|
|
32
|
+
- ci
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -10,9 +10,9 @@ jobs:
|
|
|
10
10
|
lint:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
14
|
- uses: pnpm/action-setup@v4
|
|
15
|
-
- uses: actions/setup-node@
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
16
|
with:
|
|
17
17
|
node-version: 22
|
|
18
18
|
cache: pnpm
|
|
@@ -22,9 +22,9 @@ jobs:
|
|
|
22
22
|
typecheck:
|
|
23
23
|
runs-on: ubuntu-latest
|
|
24
24
|
steps:
|
|
25
|
-
- uses: actions/checkout@
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
26
|
- uses: pnpm/action-setup@v4
|
|
27
|
-
- uses: actions/setup-node@
|
|
27
|
+
- uses: actions/setup-node@v6
|
|
28
28
|
with:
|
|
29
29
|
node-version: 22
|
|
30
30
|
cache: pnpm
|
|
@@ -34,9 +34,9 @@ jobs:
|
|
|
34
34
|
test:
|
|
35
35
|
runs-on: ubuntu-latest
|
|
36
36
|
steps:
|
|
37
|
-
- uses: actions/checkout@
|
|
37
|
+
- uses: actions/checkout@v6
|
|
38
38
|
- uses: pnpm/action-setup@v4
|
|
39
|
-
- uses: actions/setup-node@
|
|
39
|
+
- uses: actions/setup-node@v6
|
|
40
40
|
with:
|
|
41
41
|
node-version: 22
|
|
42
42
|
cache: pnpm
|
|
@@ -47,9 +47,9 @@ jobs:
|
|
|
47
47
|
runs-on: ubuntu-latest
|
|
48
48
|
needs: [lint, typecheck, test]
|
|
49
49
|
steps:
|
|
50
|
-
- uses: actions/checkout@
|
|
50
|
+
- uses: actions/checkout@v6
|
|
51
51
|
- uses: pnpm/action-setup@v4
|
|
52
|
-
- uses: actions/setup-node@
|
|
52
|
+
- uses: actions/setup-node@v6
|
|
53
53
|
with:
|
|
54
54
|
node-version: 22
|
|
55
55
|
cache: pnpm
|
|
@@ -11,13 +11,13 @@ jobs:
|
|
|
11
11
|
contents: read
|
|
12
12
|
id-token: write
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
15
|
-
- uses: actions/setup-node@
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
16
|
with:
|
|
17
17
|
node-version: "20.x"
|
|
18
18
|
registry-url: "https://registry.npmjs.org"
|
|
19
19
|
|
|
20
|
-
- uses: pnpm/action-setup@
|
|
20
|
+
- uses: pnpm/action-setup@v4
|
|
21
21
|
with:
|
|
22
22
|
version: 9
|
|
23
23
|
run_install: false
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
| ------- | ------------------ |
|
|
7
|
+
| 5.1.x | :white_check_mark: |
|
|
8
|
+
| 5.0.x | :x: |
|
|
9
|
+
| 4.0.x | :white_check_mark: |
|
|
10
|
+
| < 4.0 | :x: |
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Reporting a Vulnerability
|
|
14
|
+
|
|
15
|
+
If you discover a security vulnerability in better-openclaw, please report it responsibly.
|
|
16
|
+
|
|
17
|
+
**Do NOT open a public GitHub issue for security vulnerabilities.**
|
|
18
|
+
|
|
19
|
+
### How to Report
|
|
20
|
+
|
|
21
|
+
1. **Email**: Send details to [bachir@bidew.io](mailto:bachir@bidew.io)
|
|
22
|
+
2. **GitHub Security Advisories**: Use the [private vulnerability reporting](https://github.com/bidewio/better-openclaw/security/advisories/new) feature
|
|
23
|
+
|
|
24
|
+
### What to Include
|
|
25
|
+
|
|
26
|
+
- Description of the vulnerability
|
|
27
|
+
- Steps to reproduce
|
|
28
|
+
- Impact assessment
|
|
29
|
+
- Suggested fix (if any)
|
|
30
|
+
|
|
31
|
+
### Response Timeline
|
|
32
|
+
|
|
33
|
+
- **Acknowledgement**: Within 48 hours
|
|
34
|
+
- **Initial Assessment**: Within 1 week
|
|
35
|
+
- **Fix & Disclosure**: Coordinated with reporter, typically within 30 days
|
|
36
|
+
|
|
37
|
+
### Scope
|
|
38
|
+
|
|
39
|
+
The following are in scope:
|
|
40
|
+
|
|
41
|
+
- `@better-openclaw/core` — service definitions, resolver, composer, generators
|
|
42
|
+
- `@better-openclaw/mcp` — MCP server
|
|
43
|
+
- `create-better-openclaw` — CLI tool
|
|
44
|
+
- `@better-openclaw/api` — REST API
|
|
45
|
+
- Generated Docker Compose files and scripts
|
|
46
|
+
- Secret generation logic
|
|
47
|
+
|
|
48
|
+
### Out of Scope
|
|
49
|
+
|
|
50
|
+
- Third-party Docker images referenced by service definitions
|
|
51
|
+
- Vulnerabilities in upstream dependencies (report to the upstream project)
|
|
52
|
+
- The hosted website (better-openclaw.dev) infrastructure
|
|
53
|
+
|
|
54
|
+
## Security Best Practices
|
|
55
|
+
|
|
56
|
+
When using better-openclaw:
|
|
57
|
+
|
|
58
|
+
- Always review generated `.env` files before deploying
|
|
59
|
+
- Never commit `.env` files to version control
|
|
60
|
+
- Use `--generate-secrets` (default) to create unique passwords per stack
|
|
61
|
+
- Keep Docker images updated (`docker compose pull`)
|
|
62
|
+
- Use a reverse proxy (Caddy/Traefik) with TLS in production
|
|
@@ -78,40 +78,45 @@ var CoolifyDeployer = class {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
async deploy(input) {
|
|
81
|
+
const step1 = {
|
|
82
|
+
step: "Discover server",
|
|
83
|
+
status: "pending"
|
|
84
|
+
};
|
|
85
|
+
const step2 = {
|
|
86
|
+
step: "Create project",
|
|
87
|
+
status: "pending"
|
|
88
|
+
};
|
|
89
|
+
const step3 = {
|
|
90
|
+
step: "Create compose service",
|
|
91
|
+
status: "pending"
|
|
92
|
+
};
|
|
93
|
+
const step4 = {
|
|
94
|
+
step: "Set environment variables",
|
|
95
|
+
status: "pending"
|
|
96
|
+
};
|
|
97
|
+
const step5 = {
|
|
98
|
+
step: "Trigger deployment",
|
|
99
|
+
status: "pending"
|
|
100
|
+
};
|
|
81
101
|
const steps = [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
step: "Create project",
|
|
88
|
-
status: "pending"
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
step: "Create compose service",
|
|
92
|
-
status: "pending"
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
step: "Set environment variables",
|
|
96
|
-
status: "pending"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
step: "Trigger deployment",
|
|
100
|
-
status: "pending"
|
|
101
|
-
}
|
|
102
|
+
step1,
|
|
103
|
+
step2,
|
|
104
|
+
step3,
|
|
105
|
+
step4,
|
|
106
|
+
step5
|
|
102
107
|
];
|
|
103
108
|
const result = {
|
|
104
109
|
success: false,
|
|
105
110
|
steps
|
|
106
111
|
};
|
|
107
112
|
try {
|
|
108
|
-
|
|
113
|
+
step1.status = "running";
|
|
109
114
|
const servers = await coolifyFetch(input.target, "/servers");
|
|
110
115
|
if (!servers || servers.length === 0) throw new Error("No servers found in Coolify instance");
|
|
111
116
|
const server = servers[0];
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
step1.status = "done";
|
|
118
|
+
step1.detail = `Server: ${server.name} (${server.ip})`;
|
|
119
|
+
step2.status = "running";
|
|
115
120
|
const project = await coolifyFetch(input.target, "/projects", {
|
|
116
121
|
method: "POST",
|
|
117
122
|
body: {
|
|
@@ -120,13 +125,13 @@ var CoolifyDeployer = class {
|
|
|
120
125
|
}
|
|
121
126
|
});
|
|
122
127
|
result.projectId = project.uuid;
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
step2.status = "done";
|
|
129
|
+
step2.detail = `Project: ${project.uuid}`;
|
|
125
130
|
const projectDetail = await coolifyFetch(input.target, `/projects/${project.uuid}`);
|
|
126
131
|
const envUuid = projectDetail.environments?.[0]?.uuid;
|
|
127
132
|
const envName = projectDetail.environments?.[0]?.name ?? "production";
|
|
128
133
|
if (!envUuid) throw new Error("No default environment found in project");
|
|
129
|
-
|
|
134
|
+
step3.status = "running";
|
|
130
135
|
const service = await coolifyFetch(input.target, "/services", {
|
|
131
136
|
method: "POST",
|
|
132
137
|
body: {
|
|
@@ -141,27 +146,27 @@ var CoolifyDeployer = class {
|
|
|
141
146
|
}
|
|
142
147
|
});
|
|
143
148
|
result.composeId = service.uuid;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
step3.status = "done";
|
|
150
|
+
step3.detail = `Service: ${service.uuid}`;
|
|
151
|
+
step4.status = "running";
|
|
147
152
|
const envVars = parseEnvContent(input.envContent);
|
|
148
153
|
if (envVars.length > 0) await coolifyFetch(input.target, `/services/${service.uuid}/envs`, {
|
|
149
154
|
method: "PATCH",
|
|
150
155
|
body: envVars
|
|
151
156
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
step4.status = "done";
|
|
158
|
+
step4.detail = `${envVars.length} variables set`;
|
|
159
|
+
step5.status = "running";
|
|
155
160
|
const deployments = await coolifyFetch(input.target, `/deploy?uuid=${service.uuid}&force=true`);
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
step5.status = "done";
|
|
162
|
+
step5.detail = deployments?.deployments?.[0]?.deployment_uuid ?? "Deployment triggered";
|
|
158
163
|
result.success = true;
|
|
159
164
|
result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/project/${project.uuid}`;
|
|
160
165
|
} catch (err) {
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
const failedStep = steps.find((s) => s.status === "running");
|
|
167
|
+
if (failedStep) {
|
|
168
|
+
failedStep.status = "error";
|
|
169
|
+
failedStep.detail = err instanceof Error ? err.message : String(err);
|
|
165
170
|
}
|
|
166
171
|
result.error = err instanceof Error ? err.message : String(err);
|
|
167
172
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coolify.cjs","names":[],"sources":["../../src/deployers/coolify.ts"],"sourcesContent":["/**\n * Coolify PaaS deployer — deploys Docker Compose stacks via the Coolify v4 REST API.\n *\n * API docs: https://coolify.io/docs/api-reference/api/\n * Auth: Authorization: Bearer <token>\n * Base path: /api/v1\n */\n\nimport type { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from \"./types.js\";\n\n/** Shape returned by Coolify's project endpoints. */\ninterface CoolifyProject {\n\tuuid: string;\n\tname: string;\n\tenvironments?: { uuid: string; name: string }[];\n}\n\n/** Shape returned by Coolify's server listing. */\ninterface CoolifyServer {\n\tuuid: string;\n\tname: string;\n\tip: string;\n}\n\n/** Shape returned when creating a Coolify service (compose stack). */\ninterface CoolifyService {\n\tuuid: string;\n}\n\n/** Shape returned by Coolify's deploy trigger endpoint. */\ninterface CoolifyDeployment {\n\tmessage: string;\n\tresource_uuid: string;\n\tdeployment_uuid: string;\n}\n\n/** Build a full Coolify API URL (base + /api/v1 + path). */\nfunction apiUrl(target: DeployTarget, path: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/v1${path}`;\n}\n\n/**\n * Typed fetch wrapper for the Coolify v4 API.\n * Handles JSON serialisation, Bearer auth, and error extraction.\n */\nasync function coolifyFetch<T>(\n\ttarget: DeployTarget,\n\tpath: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, path), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tAuthorization: `Bearer ${target.apiKey}`,\n\t\t},\n\t\tbody: options.body ? JSON.stringify(options.body) : undefined,\n\t});\n\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => \"\");\n\t\tlet detail = text;\n\t\ttry {\n\t\t\tconst json = JSON.parse(text);\n\t\t\tdetail = json.message || json.error || text;\n\t\t} catch {\n\t\t\t// use raw text\n\t\t}\n\t\tthrow new Error(`Coolify API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\tif (!text) return undefined as T;\n\treturn JSON.parse(text) as T;\n}\n\n/**\n * Parse .env content into key-value pairs for Coolify's bulk env API.\n */\nfunction parseEnvContent(envContent: string): {\n\tkey: string;\n\tvalue: string;\n\tis_preview: boolean;\n\tis_build_time: boolean;\n\tis_literal: boolean;\n}[] {\n\tconst result: {\n\t\tkey: string;\n\t\tvalue: string;\n\t\tis_preview: boolean;\n\t\tis_build_time: boolean;\n\t\tis_literal: boolean;\n\t}[] = [];\n\n\tfor (const line of envContent.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx <= 0) continue;\n\n\t\tconst key = trimmed.slice(0, eqIdx);\n\t\tconst value = trimmed.slice(eqIdx + 1);\n\n\t\tresult.push({\n\t\t\tkey,\n\t\t\tvalue,\n\t\t\tis_preview: false,\n\t\t\tis_build_time: false,\n\t\t\tis_literal: true,\n\t\t});\n\t}\n\n\treturn result;\n}\n\n/**\n * Deploys Docker Compose stacks to a Coolify v4 instance.\n *\n * Deploy flow (5 steps):\n * 1. Discover the first available server\n * 2. Create a Coolify project\n * 3. Create a compose service with the raw docker-compose YAML\n * 4. Push .env variables via the bulk env API\n * 5. Trigger the deployment\n */\nexport class CoolifyDeployer implements PaasDeployer {\n\treadonly name = \"Coolify\";\n\treadonly id = \"coolify\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait coolifyFetch<unknown>(target, \"/version\");\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn { ok: false, error: err instanceof Error ? err.message : String(err) };\n\t\t}\n\t}\n\n\tasync deploy(input: DeployInput): Promise<DeployResult> {\n\t\tconst steps: DeployStep[] = [\n\t\t\t{ step: \"Discover server\", status: \"pending\" },\n\t\t\t{ step: \"Create project\", status: \"pending\" },\n\t\t\t{ step: \"Create compose service\", status: \"pending\" },\n\t\t\t{ step: \"Set environment variables\", status: \"pending\" },\n\t\t\t{ step: \"Trigger deployment\", status: \"pending\" },\n\t\t];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\ttry {\n\t\t\t// Step 1: Discover default server\n\t\t\tsteps[0].status = \"running\";\n\t\t\tconst servers = await coolifyFetch<CoolifyServer[]>(input.target, \"/servers\");\n\t\t\tif (!servers || servers.length === 0) {\n\t\t\t\tthrow new Error(\"No servers found in Coolify instance\");\n\t\t\t}\n\t\t\tconst server = servers[0];\n\t\t\tsteps[0].status = \"done\";\n\t\t\tsteps[0].detail = `Server: ${server.name} (${server.ip})`;\n\n\t\t\t// Step 2: Create project\n\t\t\tsteps[1].status = \"running\";\n\t\t\tconst project = await coolifyFetch<CoolifyProject>(input.target, \"/projects\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.projectId = project.uuid;\n\t\t\tsteps[1].status = \"done\";\n\t\t\tsteps[1].detail = `Project: ${project.uuid}`;\n\n\t\t\t// Get the default environment\n\t\t\tconst projectDetail = await coolifyFetch<CoolifyProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`/projects/${project.uuid}`,\n\t\t\t);\n\t\t\tconst envUuid = projectDetail.environments?.[0]?.uuid;\n\t\t\tconst envName = projectDetail.environments?.[0]?.name ?? \"production\";\n\t\t\tif (!envUuid) {\n\t\t\t\tthrow new Error(\"No default environment found in project\");\n\t\t\t}\n\n\t\t\t// Step 3: Create compose service with docker_compose_raw\n\t\t\tsteps[2].status = \"running\";\n\t\t\tconst service = await coolifyFetch<CoolifyService>(input.target, \"/services\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tproject_uuid: project.uuid,\n\t\t\t\t\tserver_uuid: server.uuid,\n\t\t\t\t\tenvironment_name: envName,\n\t\t\t\t\tenvironment_uuid: envUuid,\n\t\t\t\t\tdocker_compose_raw: input.composeYaml,\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? \"Deployed via OpenClaw web builder\",\n\t\t\t\t\tinstant_deploy: false,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.composeId = service.uuid;\n\t\t\tsteps[2].status = \"done\";\n\t\t\tsteps[2].detail = `Service: ${service.uuid}`;\n\n\t\t\t// Step 4: Set environment variables\n\t\t\tsteps[3].status = \"running\";\n\t\t\tconst envVars = parseEnvContent(input.envContent);\n\t\t\tif (envVars.length > 0) {\n\t\t\t\tawait coolifyFetch(input.target, `/services/${service.uuid}/envs`, {\n\t\t\t\t\tmethod: \"PATCH\",\n\t\t\t\t\tbody: envVars,\n\t\t\t\t});\n\t\t\t}\n\t\t\tsteps[3].status = \"done\";\n\t\t\tsteps[3].detail = `${envVars.length} variables set`;\n\n\t\t\t// Step 5: Trigger deployment\n\t\t\tsteps[4].status = \"running\";\n\t\t\tconst deployments = await coolifyFetch<{ deployments: CoolifyDeployment[] }>(\n\t\t\t\tinput.target,\n\t\t\t\t`/deploy?uuid=${service.uuid}&force=true`,\n\t\t\t);\n\t\t\tsteps[4].status = \"done\";\n\t\t\tsteps[4].detail = deployments?.deployments?.[0]?.deployment_uuid ?? \"Deployment triggered\";\n\n\t\t\tresult.success = true;\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\t\t\tresult.dashboardUrl = `${base}/project/${project.uuid}`;\n\t\t} catch (err) {\n\t\t\tconst failedIdx = steps.findIndex((s) => s.status === \"running\");\n\t\t\tif (failedIdx >= 0) {\n\t\t\t\tsteps[failedIdx].status = \"error\";\n\t\t\t\tsteps[failedIdx].detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"coolify.cjs","names":[],"sources":["../../src/deployers/coolify.ts"],"sourcesContent":["/**\n * Coolify PaaS deployer — deploys Docker Compose stacks via the Coolify v4 REST API.\n *\n * API docs: https://coolify.io/docs/api-reference/api/\n * Auth: Authorization: Bearer <token>\n * Base path: /api/v1\n */\n\nimport type { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from \"./types.js\";\n\n/** Shape returned by Coolify's project endpoints. */\ninterface CoolifyProject {\n\tuuid: string;\n\tname: string;\n\tenvironments?: { uuid: string; name: string }[];\n}\n\n/** Shape returned by Coolify's server listing. */\ninterface CoolifyServer {\n\tuuid: string;\n\tname: string;\n\tip: string;\n}\n\n/** Shape returned when creating a Coolify service (compose stack). */\ninterface CoolifyService {\n\tuuid: string;\n}\n\n/** Shape returned by Coolify's deploy trigger endpoint. */\ninterface CoolifyDeployment {\n\tmessage: string;\n\tresource_uuid: string;\n\tdeployment_uuid: string;\n}\n\n/** Build a full Coolify API URL (base + /api/v1 + path). */\nfunction apiUrl(target: DeployTarget, path: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/v1${path}`;\n}\n\n/**\n * Typed fetch wrapper for the Coolify v4 API.\n * Handles JSON serialisation, Bearer auth, and error extraction.\n */\nasync function coolifyFetch<T>(\n\ttarget: DeployTarget,\n\tpath: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, path), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tAuthorization: `Bearer ${target.apiKey}`,\n\t\t},\n\t\tbody: options.body ? JSON.stringify(options.body) : undefined,\n\t});\n\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => \"\");\n\t\tlet detail = text;\n\t\ttry {\n\t\t\tconst json = JSON.parse(text);\n\t\t\tdetail = json.message || json.error || text;\n\t\t} catch {\n\t\t\t// use raw text\n\t\t}\n\t\tthrow new Error(`Coolify API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\tif (!text) return undefined as T;\n\treturn JSON.parse(text) as T;\n}\n\n/**\n * Parse .env content into key-value pairs for Coolify's bulk env API.\n */\nfunction parseEnvContent(envContent: string): {\n\tkey: string;\n\tvalue: string;\n\tis_preview: boolean;\n\tis_build_time: boolean;\n\tis_literal: boolean;\n}[] {\n\tconst result: {\n\t\tkey: string;\n\t\tvalue: string;\n\t\tis_preview: boolean;\n\t\tis_build_time: boolean;\n\t\tis_literal: boolean;\n\t}[] = [];\n\n\tfor (const line of envContent.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx <= 0) continue;\n\n\t\tconst key = trimmed.slice(0, eqIdx);\n\t\tconst value = trimmed.slice(eqIdx + 1);\n\n\t\tresult.push({\n\t\t\tkey,\n\t\t\tvalue,\n\t\t\tis_preview: false,\n\t\t\tis_build_time: false,\n\t\t\tis_literal: true,\n\t\t});\n\t}\n\n\treturn result;\n}\n\n/**\n * Deploys Docker Compose stacks to a Coolify v4 instance.\n *\n * Deploy flow (5 steps):\n * 1. Discover the first available server\n * 2. Create a Coolify project\n * 3. Create a compose service with the raw docker-compose YAML\n * 4. Push .env variables via the bulk env API\n * 5. Trigger the deployment\n */\nexport class CoolifyDeployer implements PaasDeployer {\n\treadonly name = \"Coolify\";\n\treadonly id = \"coolify\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait coolifyFetch<unknown>(target, \"/version\");\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn { ok: false, error: err instanceof Error ? err.message : String(err) };\n\t\t}\n\t}\n\n\tasync deploy(input: DeployInput): Promise<DeployResult> {\n\t\tconst step1: DeployStep = { step: \"Discover server\", status: \"pending\" };\n\t\tconst step2: DeployStep = { step: \"Create project\", status: \"pending\" };\n\t\tconst step3: DeployStep = { step: \"Create compose service\", status: \"pending\" };\n\t\tconst step4: DeployStep = { step: \"Set environment variables\", status: \"pending\" };\n\t\tconst step5: DeployStep = { step: \"Trigger deployment\", status: \"pending\" };\n\t\tconst steps: DeployStep[] = [step1, step2, step3, step4, step5];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\ttry {\n\t\t\t// Step 1: Discover default server\n\t\t\tstep1.status = \"running\";\n\t\t\tconst servers = await coolifyFetch<CoolifyServer[]>(input.target, \"/servers\");\n\t\t\tif (!servers || servers.length === 0) {\n\t\t\t\tthrow new Error(\"No servers found in Coolify instance\");\n\t\t\t}\n\t\t\tconst server = servers[0] as CoolifyServer;\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `Server: ${server.name} (${server.ip})`;\n\n\t\t\t// Step 2: Create project\n\t\t\tstep2.status = \"running\";\n\t\t\tconst project = await coolifyFetch<CoolifyProject>(input.target, \"/projects\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.projectId = project.uuid;\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = `Project: ${project.uuid}`;\n\n\t\t\t// Get the default environment\n\t\t\tconst projectDetail = await coolifyFetch<CoolifyProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`/projects/${project.uuid}`,\n\t\t\t);\n\t\t\tconst envUuid = projectDetail.environments?.[0]?.uuid;\n\t\t\tconst envName = projectDetail.environments?.[0]?.name ?? \"production\";\n\t\t\tif (!envUuid) {\n\t\t\t\tthrow new Error(\"No default environment found in project\");\n\t\t\t}\n\n\t\t\t// Step 3: Create compose service with docker_compose_raw\n\t\t\tstep3.status = \"running\";\n\t\t\tconst service = await coolifyFetch<CoolifyService>(input.target, \"/services\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tproject_uuid: project.uuid,\n\t\t\t\t\tserver_uuid: server.uuid,\n\t\t\t\t\tenvironment_name: envName,\n\t\t\t\t\tenvironment_uuid: envUuid,\n\t\t\t\t\tdocker_compose_raw: input.composeYaml,\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? \"Deployed via OpenClaw web builder\",\n\t\t\t\t\tinstant_deploy: false,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.composeId = service.uuid;\n\t\t\tstep3.status = \"done\";\n\t\t\tstep3.detail = `Service: ${service.uuid}`;\n\n\t\t\t// Step 4: Set environment variables\n\t\t\tstep4.status = \"running\";\n\t\t\tconst envVars = parseEnvContent(input.envContent);\n\t\t\tif (envVars.length > 0) {\n\t\t\t\tawait coolifyFetch(input.target, `/services/${service.uuid}/envs`, {\n\t\t\t\t\tmethod: \"PATCH\",\n\t\t\t\t\tbody: envVars,\n\t\t\t\t});\n\t\t\t}\n\t\t\tstep4.status = \"done\";\n\t\t\tstep4.detail = `${envVars.length} variables set`;\n\n\t\t\t// Step 5: Trigger deployment\n\t\t\tstep5.status = \"running\";\n\t\t\tconst deployments = await coolifyFetch<{ deployments: CoolifyDeployment[] }>(\n\t\t\t\tinput.target,\n\t\t\t\t`/deploy?uuid=${service.uuid}&force=true`,\n\t\t\t);\n\t\t\tstep5.status = \"done\";\n\t\t\tstep5.detail = deployments?.deployments?.[0]?.deployment_uuid ?? \"Deployment triggered\";\n\n\t\t\tresult.success = true;\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\t\t\tresult.dashboardUrl = `${base}/project/${project.uuid}`;\n\t\t} catch (err) {\n\t\t\tconst failedStep = steps.find((s) => s.status === \"running\");\n\t\t\tif (failedStep) {\n\t\t\t\tfailedStep.status = \"error\";\n\t\t\t\tfailedStep.detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -76,40 +76,45 @@ var CoolifyDeployer = class {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
async deploy(input) {
|
|
79
|
+
const step1 = {
|
|
80
|
+
step: "Discover server",
|
|
81
|
+
status: "pending"
|
|
82
|
+
};
|
|
83
|
+
const step2 = {
|
|
84
|
+
step: "Create project",
|
|
85
|
+
status: "pending"
|
|
86
|
+
};
|
|
87
|
+
const step3 = {
|
|
88
|
+
step: "Create compose service",
|
|
89
|
+
status: "pending"
|
|
90
|
+
};
|
|
91
|
+
const step4 = {
|
|
92
|
+
step: "Set environment variables",
|
|
93
|
+
status: "pending"
|
|
94
|
+
};
|
|
95
|
+
const step5 = {
|
|
96
|
+
step: "Trigger deployment",
|
|
97
|
+
status: "pending"
|
|
98
|
+
};
|
|
79
99
|
const steps = [
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
step: "Create project",
|
|
86
|
-
status: "pending"
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
step: "Create compose service",
|
|
90
|
-
status: "pending"
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
step: "Set environment variables",
|
|
94
|
-
status: "pending"
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
step: "Trigger deployment",
|
|
98
|
-
status: "pending"
|
|
99
|
-
}
|
|
100
|
+
step1,
|
|
101
|
+
step2,
|
|
102
|
+
step3,
|
|
103
|
+
step4,
|
|
104
|
+
step5
|
|
100
105
|
];
|
|
101
106
|
const result = {
|
|
102
107
|
success: false,
|
|
103
108
|
steps
|
|
104
109
|
};
|
|
105
110
|
try {
|
|
106
|
-
|
|
111
|
+
step1.status = "running";
|
|
107
112
|
const servers = await coolifyFetch(input.target, "/servers");
|
|
108
113
|
if (!servers || servers.length === 0) throw new Error("No servers found in Coolify instance");
|
|
109
114
|
const server = servers[0];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
step1.status = "done";
|
|
116
|
+
step1.detail = `Server: ${server.name} (${server.ip})`;
|
|
117
|
+
step2.status = "running";
|
|
113
118
|
const project = await coolifyFetch(input.target, "/projects", {
|
|
114
119
|
method: "POST",
|
|
115
120
|
body: {
|
|
@@ -118,13 +123,13 @@ var CoolifyDeployer = class {
|
|
|
118
123
|
}
|
|
119
124
|
});
|
|
120
125
|
result.projectId = project.uuid;
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
step2.status = "done";
|
|
127
|
+
step2.detail = `Project: ${project.uuid}`;
|
|
123
128
|
const projectDetail = await coolifyFetch(input.target, `/projects/${project.uuid}`);
|
|
124
129
|
const envUuid = projectDetail.environments?.[0]?.uuid;
|
|
125
130
|
const envName = projectDetail.environments?.[0]?.name ?? "production";
|
|
126
131
|
if (!envUuid) throw new Error("No default environment found in project");
|
|
127
|
-
|
|
132
|
+
step3.status = "running";
|
|
128
133
|
const service = await coolifyFetch(input.target, "/services", {
|
|
129
134
|
method: "POST",
|
|
130
135
|
body: {
|
|
@@ -139,27 +144,27 @@ var CoolifyDeployer = class {
|
|
|
139
144
|
}
|
|
140
145
|
});
|
|
141
146
|
result.composeId = service.uuid;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
step3.status = "done";
|
|
148
|
+
step3.detail = `Service: ${service.uuid}`;
|
|
149
|
+
step4.status = "running";
|
|
145
150
|
const envVars = parseEnvContent(input.envContent);
|
|
146
151
|
if (envVars.length > 0) await coolifyFetch(input.target, `/services/${service.uuid}/envs`, {
|
|
147
152
|
method: "PATCH",
|
|
148
153
|
body: envVars
|
|
149
154
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
step4.status = "done";
|
|
156
|
+
step4.detail = `${envVars.length} variables set`;
|
|
157
|
+
step5.status = "running";
|
|
153
158
|
const deployments = await coolifyFetch(input.target, `/deploy?uuid=${service.uuid}&force=true`);
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
step5.status = "done";
|
|
160
|
+
step5.detail = deployments?.deployments?.[0]?.deployment_uuid ?? "Deployment triggered";
|
|
156
161
|
result.success = true;
|
|
157
162
|
result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/project/${project.uuid}`;
|
|
158
163
|
} catch (err) {
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
const failedStep = steps.find((s) => s.status === "running");
|
|
165
|
+
if (failedStep) {
|
|
166
|
+
failedStep.status = "error";
|
|
167
|
+
failedStep.detail = err instanceof Error ? err.message : String(err);
|
|
163
168
|
}
|
|
164
169
|
result.error = err instanceof Error ? err.message : String(err);
|
|
165
170
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coolify.mjs","names":[],"sources":["../../src/deployers/coolify.ts"],"sourcesContent":["/**\n * Coolify PaaS deployer — deploys Docker Compose stacks via the Coolify v4 REST API.\n *\n * API docs: https://coolify.io/docs/api-reference/api/\n * Auth: Authorization: Bearer <token>\n * Base path: /api/v1\n */\n\nimport type { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from \"./types.js\";\n\n/** Shape returned by Coolify's project endpoints. */\ninterface CoolifyProject {\n\tuuid: string;\n\tname: string;\n\tenvironments?: { uuid: string; name: string }[];\n}\n\n/** Shape returned by Coolify's server listing. */\ninterface CoolifyServer {\n\tuuid: string;\n\tname: string;\n\tip: string;\n}\n\n/** Shape returned when creating a Coolify service (compose stack). */\ninterface CoolifyService {\n\tuuid: string;\n}\n\n/** Shape returned by Coolify's deploy trigger endpoint. */\ninterface CoolifyDeployment {\n\tmessage: string;\n\tresource_uuid: string;\n\tdeployment_uuid: string;\n}\n\n/** Build a full Coolify API URL (base + /api/v1 + path). */\nfunction apiUrl(target: DeployTarget, path: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/v1${path}`;\n}\n\n/**\n * Typed fetch wrapper for the Coolify v4 API.\n * Handles JSON serialisation, Bearer auth, and error extraction.\n */\nasync function coolifyFetch<T>(\n\ttarget: DeployTarget,\n\tpath: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, path), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tAuthorization: `Bearer ${target.apiKey}`,\n\t\t},\n\t\tbody: options.body ? JSON.stringify(options.body) : undefined,\n\t});\n\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => \"\");\n\t\tlet detail = text;\n\t\ttry {\n\t\t\tconst json = JSON.parse(text);\n\t\t\tdetail = json.message || json.error || text;\n\t\t} catch {\n\t\t\t// use raw text\n\t\t}\n\t\tthrow new Error(`Coolify API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\tif (!text) return undefined as T;\n\treturn JSON.parse(text) as T;\n}\n\n/**\n * Parse .env content into key-value pairs for Coolify's bulk env API.\n */\nfunction parseEnvContent(envContent: string): {\n\tkey: string;\n\tvalue: string;\n\tis_preview: boolean;\n\tis_build_time: boolean;\n\tis_literal: boolean;\n}[] {\n\tconst result: {\n\t\tkey: string;\n\t\tvalue: string;\n\t\tis_preview: boolean;\n\t\tis_build_time: boolean;\n\t\tis_literal: boolean;\n\t}[] = [];\n\n\tfor (const line of envContent.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx <= 0) continue;\n\n\t\tconst key = trimmed.slice(0, eqIdx);\n\t\tconst value = trimmed.slice(eqIdx + 1);\n\n\t\tresult.push({\n\t\t\tkey,\n\t\t\tvalue,\n\t\t\tis_preview: false,\n\t\t\tis_build_time: false,\n\t\t\tis_literal: true,\n\t\t});\n\t}\n\n\treturn result;\n}\n\n/**\n * Deploys Docker Compose stacks to a Coolify v4 instance.\n *\n * Deploy flow (5 steps):\n * 1. Discover the first available server\n * 2. Create a Coolify project\n * 3. Create a compose service with the raw docker-compose YAML\n * 4. Push .env variables via the bulk env API\n * 5. Trigger the deployment\n */\nexport class CoolifyDeployer implements PaasDeployer {\n\treadonly name = \"Coolify\";\n\treadonly id = \"coolify\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait coolifyFetch<unknown>(target, \"/version\");\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn { ok: false, error: err instanceof Error ? err.message : String(err) };\n\t\t}\n\t}\n\n\tasync deploy(input: DeployInput): Promise<DeployResult> {\n\t\tconst steps: DeployStep[] = [\n\t\t\t{ step: \"Discover server\", status: \"pending\" },\n\t\t\t{ step: \"Create project\", status: \"pending\" },\n\t\t\t{ step: \"Create compose service\", status: \"pending\" },\n\t\t\t{ step: \"Set environment variables\", status: \"pending\" },\n\t\t\t{ step: \"Trigger deployment\", status: \"pending\" },\n\t\t];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\ttry {\n\t\t\t// Step 1: Discover default server\n\t\t\tsteps[0].status = \"running\";\n\t\t\tconst servers = await coolifyFetch<CoolifyServer[]>(input.target, \"/servers\");\n\t\t\tif (!servers || servers.length === 0) {\n\t\t\t\tthrow new Error(\"No servers found in Coolify instance\");\n\t\t\t}\n\t\t\tconst server = servers[0];\n\t\t\tsteps[0].status = \"done\";\n\t\t\tsteps[0].detail = `Server: ${server.name} (${server.ip})`;\n\n\t\t\t// Step 2: Create project\n\t\t\tsteps[1].status = \"running\";\n\t\t\tconst project = await coolifyFetch<CoolifyProject>(input.target, \"/projects\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.projectId = project.uuid;\n\t\t\tsteps[1].status = \"done\";\n\t\t\tsteps[1].detail = `Project: ${project.uuid}`;\n\n\t\t\t// Get the default environment\n\t\t\tconst projectDetail = await coolifyFetch<CoolifyProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`/projects/${project.uuid}`,\n\t\t\t);\n\t\t\tconst envUuid = projectDetail.environments?.[0]?.uuid;\n\t\t\tconst envName = projectDetail.environments?.[0]?.name ?? \"production\";\n\t\t\tif (!envUuid) {\n\t\t\t\tthrow new Error(\"No default environment found in project\");\n\t\t\t}\n\n\t\t\t// Step 3: Create compose service with docker_compose_raw\n\t\t\tsteps[2].status = \"running\";\n\t\t\tconst service = await coolifyFetch<CoolifyService>(input.target, \"/services\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tproject_uuid: project.uuid,\n\t\t\t\t\tserver_uuid: server.uuid,\n\t\t\t\t\tenvironment_name: envName,\n\t\t\t\t\tenvironment_uuid: envUuid,\n\t\t\t\t\tdocker_compose_raw: input.composeYaml,\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? \"Deployed via OpenClaw web builder\",\n\t\t\t\t\tinstant_deploy: false,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.composeId = service.uuid;\n\t\t\tsteps[2].status = \"done\";\n\t\t\tsteps[2].detail = `Service: ${service.uuid}`;\n\n\t\t\t// Step 4: Set environment variables\n\t\t\tsteps[3].status = \"running\";\n\t\t\tconst envVars = parseEnvContent(input.envContent);\n\t\t\tif (envVars.length > 0) {\n\t\t\t\tawait coolifyFetch(input.target, `/services/${service.uuid}/envs`, {\n\t\t\t\t\tmethod: \"PATCH\",\n\t\t\t\t\tbody: envVars,\n\t\t\t\t});\n\t\t\t}\n\t\t\tsteps[3].status = \"done\";\n\t\t\tsteps[3].detail = `${envVars.length} variables set`;\n\n\t\t\t// Step 5: Trigger deployment\n\t\t\tsteps[4].status = \"running\";\n\t\t\tconst deployments = await coolifyFetch<{ deployments: CoolifyDeployment[] }>(\n\t\t\t\tinput.target,\n\t\t\t\t`/deploy?uuid=${service.uuid}&force=true`,\n\t\t\t);\n\t\t\tsteps[4].status = \"done\";\n\t\t\tsteps[4].detail = deployments?.deployments?.[0]?.deployment_uuid ?? \"Deployment triggered\";\n\n\t\t\tresult.success = true;\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\t\t\tresult.dashboardUrl = `${base}/project/${project.uuid}`;\n\t\t} catch (err) {\n\t\t\tconst failedIdx = steps.findIndex((s) => s.status === \"running\");\n\t\t\tif (failedIdx >= 0) {\n\t\t\t\tsteps[failedIdx].status = \"error\";\n\t\t\t\tsteps[failedIdx].detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"coolify.mjs","names":[],"sources":["../../src/deployers/coolify.ts"],"sourcesContent":["/**\n * Coolify PaaS deployer — deploys Docker Compose stacks via the Coolify v4 REST API.\n *\n * API docs: https://coolify.io/docs/api-reference/api/\n * Auth: Authorization: Bearer <token>\n * Base path: /api/v1\n */\n\nimport type { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from \"./types.js\";\n\n/** Shape returned by Coolify's project endpoints. */\ninterface CoolifyProject {\n\tuuid: string;\n\tname: string;\n\tenvironments?: { uuid: string; name: string }[];\n}\n\n/** Shape returned by Coolify's server listing. */\ninterface CoolifyServer {\n\tuuid: string;\n\tname: string;\n\tip: string;\n}\n\n/** Shape returned when creating a Coolify service (compose stack). */\ninterface CoolifyService {\n\tuuid: string;\n}\n\n/** Shape returned by Coolify's deploy trigger endpoint. */\ninterface CoolifyDeployment {\n\tmessage: string;\n\tresource_uuid: string;\n\tdeployment_uuid: string;\n}\n\n/** Build a full Coolify API URL (base + /api/v1 + path). */\nfunction apiUrl(target: DeployTarget, path: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/v1${path}`;\n}\n\n/**\n * Typed fetch wrapper for the Coolify v4 API.\n * Handles JSON serialisation, Bearer auth, and error extraction.\n */\nasync function coolifyFetch<T>(\n\ttarget: DeployTarget,\n\tpath: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, path), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tAuthorization: `Bearer ${target.apiKey}`,\n\t\t},\n\t\tbody: options.body ? JSON.stringify(options.body) : undefined,\n\t});\n\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => \"\");\n\t\tlet detail = text;\n\t\ttry {\n\t\t\tconst json = JSON.parse(text);\n\t\t\tdetail = json.message || json.error || text;\n\t\t} catch {\n\t\t\t// use raw text\n\t\t}\n\t\tthrow new Error(`Coolify API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\tif (!text) return undefined as T;\n\treturn JSON.parse(text) as T;\n}\n\n/**\n * Parse .env content into key-value pairs for Coolify's bulk env API.\n */\nfunction parseEnvContent(envContent: string): {\n\tkey: string;\n\tvalue: string;\n\tis_preview: boolean;\n\tis_build_time: boolean;\n\tis_literal: boolean;\n}[] {\n\tconst result: {\n\t\tkey: string;\n\t\tvalue: string;\n\t\tis_preview: boolean;\n\t\tis_build_time: boolean;\n\t\tis_literal: boolean;\n\t}[] = [];\n\n\tfor (const line of envContent.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx <= 0) continue;\n\n\t\tconst key = trimmed.slice(0, eqIdx);\n\t\tconst value = trimmed.slice(eqIdx + 1);\n\n\t\tresult.push({\n\t\t\tkey,\n\t\t\tvalue,\n\t\t\tis_preview: false,\n\t\t\tis_build_time: false,\n\t\t\tis_literal: true,\n\t\t});\n\t}\n\n\treturn result;\n}\n\n/**\n * Deploys Docker Compose stacks to a Coolify v4 instance.\n *\n * Deploy flow (5 steps):\n * 1. Discover the first available server\n * 2. Create a Coolify project\n * 3. Create a compose service with the raw docker-compose YAML\n * 4. Push .env variables via the bulk env API\n * 5. Trigger the deployment\n */\nexport class CoolifyDeployer implements PaasDeployer {\n\treadonly name = \"Coolify\";\n\treadonly id = \"coolify\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait coolifyFetch<unknown>(target, \"/version\");\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn { ok: false, error: err instanceof Error ? err.message : String(err) };\n\t\t}\n\t}\n\n\tasync deploy(input: DeployInput): Promise<DeployResult> {\n\t\tconst step1: DeployStep = { step: \"Discover server\", status: \"pending\" };\n\t\tconst step2: DeployStep = { step: \"Create project\", status: \"pending\" };\n\t\tconst step3: DeployStep = { step: \"Create compose service\", status: \"pending\" };\n\t\tconst step4: DeployStep = { step: \"Set environment variables\", status: \"pending\" };\n\t\tconst step5: DeployStep = { step: \"Trigger deployment\", status: \"pending\" };\n\t\tconst steps: DeployStep[] = [step1, step2, step3, step4, step5];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\ttry {\n\t\t\t// Step 1: Discover default server\n\t\t\tstep1.status = \"running\";\n\t\t\tconst servers = await coolifyFetch<CoolifyServer[]>(input.target, \"/servers\");\n\t\t\tif (!servers || servers.length === 0) {\n\t\t\t\tthrow new Error(\"No servers found in Coolify instance\");\n\t\t\t}\n\t\t\tconst server = servers[0] as CoolifyServer;\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `Server: ${server.name} (${server.ip})`;\n\n\t\t\t// Step 2: Create project\n\t\t\tstep2.status = \"running\";\n\t\t\tconst project = await coolifyFetch<CoolifyProject>(input.target, \"/projects\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.projectId = project.uuid;\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = `Project: ${project.uuid}`;\n\n\t\t\t// Get the default environment\n\t\t\tconst projectDetail = await coolifyFetch<CoolifyProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`/projects/${project.uuid}`,\n\t\t\t);\n\t\t\tconst envUuid = projectDetail.environments?.[0]?.uuid;\n\t\t\tconst envName = projectDetail.environments?.[0]?.name ?? \"production\";\n\t\t\tif (!envUuid) {\n\t\t\t\tthrow new Error(\"No default environment found in project\");\n\t\t\t}\n\n\t\t\t// Step 3: Create compose service with docker_compose_raw\n\t\t\tstep3.status = \"running\";\n\t\t\tconst service = await coolifyFetch<CoolifyService>(input.target, \"/services\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tproject_uuid: project.uuid,\n\t\t\t\t\tserver_uuid: server.uuid,\n\t\t\t\t\tenvironment_name: envName,\n\t\t\t\t\tenvironment_uuid: envUuid,\n\t\t\t\t\tdocker_compose_raw: input.composeYaml,\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? \"Deployed via OpenClaw web builder\",\n\t\t\t\t\tinstant_deploy: false,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.composeId = service.uuid;\n\t\t\tstep3.status = \"done\";\n\t\t\tstep3.detail = `Service: ${service.uuid}`;\n\n\t\t\t// Step 4: Set environment variables\n\t\t\tstep4.status = \"running\";\n\t\t\tconst envVars = parseEnvContent(input.envContent);\n\t\t\tif (envVars.length > 0) {\n\t\t\t\tawait coolifyFetch(input.target, `/services/${service.uuid}/envs`, {\n\t\t\t\t\tmethod: \"PATCH\",\n\t\t\t\t\tbody: envVars,\n\t\t\t\t});\n\t\t\t}\n\t\t\tstep4.status = \"done\";\n\t\t\tstep4.detail = `${envVars.length} variables set`;\n\n\t\t\t// Step 5: Trigger deployment\n\t\t\tstep5.status = \"running\";\n\t\t\tconst deployments = await coolifyFetch<{ deployments: CoolifyDeployment[] }>(\n\t\t\t\tinput.target,\n\t\t\t\t`/deploy?uuid=${service.uuid}&force=true`,\n\t\t\t);\n\t\t\tstep5.status = \"done\";\n\t\t\tstep5.detail = deployments?.deployments?.[0]?.deployment_uuid ?? \"Deployment triggered\";\n\n\t\t\tresult.success = true;\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\t\t\tresult.dashboardUrl = `${base}/project/${project.uuid}`;\n\t\t} catch (err) {\n\t\t\tconst failedStep = steps.find((s) => s.status === \"running\");\n\t\t\tif (failedStep) {\n\t\t\t\tfailedStep.status = \"error\";\n\t\t\t\tfailedStep.detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -55,30 +55,34 @@ var DokployDeployer = class {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
async deploy(input) {
|
|
58
|
+
const step1 = {
|
|
59
|
+
step: "Create project",
|
|
60
|
+
status: "pending"
|
|
61
|
+
};
|
|
62
|
+
const step2 = {
|
|
63
|
+
step: "Create compose stack",
|
|
64
|
+
status: "pending"
|
|
65
|
+
};
|
|
66
|
+
const step3 = {
|
|
67
|
+
step: "Set environment variables",
|
|
68
|
+
status: "pending"
|
|
69
|
+
};
|
|
70
|
+
const step4 = {
|
|
71
|
+
step: "Trigger deployment",
|
|
72
|
+
status: "pending"
|
|
73
|
+
};
|
|
58
74
|
const steps = [
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
step: "Create compose stack",
|
|
65
|
-
status: "pending"
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
step: "Set environment variables",
|
|
69
|
-
status: "pending"
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
step: "Trigger deployment",
|
|
73
|
-
status: "pending"
|
|
74
|
-
}
|
|
75
|
+
step1,
|
|
76
|
+
step2,
|
|
77
|
+
step3,
|
|
78
|
+
step4
|
|
75
79
|
];
|
|
76
80
|
const result = {
|
|
77
81
|
success: false,
|
|
78
82
|
steps
|
|
79
83
|
};
|
|
80
84
|
try {
|
|
81
|
-
|
|
85
|
+
step1.status = "running";
|
|
82
86
|
const project = await dokployFetch(input.target, "project.create", {
|
|
83
87
|
method: "POST",
|
|
84
88
|
body: {
|
|
@@ -87,11 +91,11 @@ var DokployDeployer = class {
|
|
|
87
91
|
}
|
|
88
92
|
});
|
|
89
93
|
result.projectId = project.projectId;
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
step1.status = "done";
|
|
95
|
+
step1.detail = `Project ID: ${project.projectId}`;
|
|
92
96
|
const envId = (await dokployFetch(input.target, `project.one?projectId=${project.projectId}`)).environments?.[0]?.environmentId;
|
|
93
97
|
if (!envId) throw new Error("No default environment found in project");
|
|
94
|
-
|
|
98
|
+
step2.status = "running";
|
|
95
99
|
const compose = await dokployFetch(input.target, "compose.create", {
|
|
96
100
|
method: "POST",
|
|
97
101
|
body: {
|
|
@@ -101,9 +105,9 @@ var DokployDeployer = class {
|
|
|
101
105
|
}
|
|
102
106
|
});
|
|
103
107
|
result.composeId = compose.composeId;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
step2.status = "done";
|
|
109
|
+
step2.detail = `Compose ID: ${compose.composeId}`;
|
|
110
|
+
step3.status = "running";
|
|
107
111
|
await dokployFetch(input.target, "compose.update", {
|
|
108
112
|
method: "POST",
|
|
109
113
|
body: {
|
|
@@ -111,8 +115,8 @@ var DokployDeployer = class {
|
|
|
111
115
|
env: input.envContent
|
|
112
116
|
}
|
|
113
117
|
});
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
step3.status = "done";
|
|
119
|
+
step4.status = "running";
|
|
116
120
|
await dokployFetch(input.target, "compose.deploy", {
|
|
117
121
|
method: "POST",
|
|
118
122
|
body: {
|
|
@@ -121,14 +125,14 @@ var DokployDeployer = class {
|
|
|
121
125
|
description: input.description ?? "Deployed via OpenClaw web builder"
|
|
122
126
|
}
|
|
123
127
|
});
|
|
124
|
-
|
|
128
|
+
step4.status = "done";
|
|
125
129
|
result.success = true;
|
|
126
130
|
result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/dashboard/project/${project.projectId}`;
|
|
127
131
|
} catch (err) {
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
const failedStep = steps.find((s) => s.status === "running");
|
|
133
|
+
if (failedStep) {
|
|
134
|
+
failedStep.status = "error";
|
|
135
|
+
failedStep.detail = err instanceof Error ? err.message : String(err);
|
|
132
136
|
}
|
|
133
137
|
result.error = err instanceof Error ? err.message : String(err);
|
|
134
138
|
}
|