@better-openclaw/core 1.0.19 → 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/dependabot.yml +32 -0
- package/.github/workflows/ci.yml +8 -8
- package/.github/workflows/publish-core.yml +4 -4
- package/SECURITY.md +62 -0
- 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/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 +4 -4
- package/src/__snapshots__/composer.snapshot.test.ts.snap +248 -38
- 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/src/services/definitions/usesend.ts +4 -4
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
import { sanitizeComposeForPaas } from "./strip-host-ports.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/deployers/coolify.ts
|
|
2
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Coolify PaaS deployer
|
|
6
|
+
*
|
|
7
|
+
* Deploys Docker Compose stacks via Coolify v4 API
|
|
8
|
+
*
|
|
9
|
+
* Docs:
|
|
10
|
+
* https://coolify.io/docs/api-reference/api
|
|
11
|
+
*
|
|
12
|
+
* Auth:
|
|
13
|
+
* Authorization: Bearer <token>
|
|
14
|
+
*
|
|
15
|
+
* Base path:
|
|
16
|
+
* /api/v1
|
|
17
|
+
*/
|
|
3
18
|
function apiUrl(target, path) {
|
|
4
19
|
return `${target.instanceUrl.replace(/\/+$/, "")}/api/v1${path}`;
|
|
5
20
|
}
|
|
6
|
-
/**
|
|
7
|
-
* Typed fetch wrapper for the Coolify v4 API.
|
|
8
|
-
* Handles JSON serialisation, Bearer auth, and error extraction.
|
|
9
|
-
*/
|
|
10
21
|
async function coolifyFetch(target, path, options = {}) {
|
|
11
22
|
const res = await fetch(apiUrl(target, path), {
|
|
12
23
|
method: options.method ?? "GET",
|
|
@@ -29,18 +40,24 @@ async function coolifyFetch(target, path, options = {}) {
|
|
|
29
40
|
if (!text) return void 0;
|
|
30
41
|
return JSON.parse(text);
|
|
31
42
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
function hashString(str) {
|
|
44
|
+
let hash = 0;
|
|
45
|
+
for (let i = 0; i < str.length; i++) {
|
|
46
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
47
|
+
hash |= 0;
|
|
48
|
+
}
|
|
49
|
+
return hash;
|
|
50
|
+
}
|
|
35
51
|
function parseEnvContent(envContent) {
|
|
52
|
+
if (!envContent) return [];
|
|
36
53
|
const result = [];
|
|
37
54
|
for (const line of envContent.split("\n")) {
|
|
38
55
|
const trimmed = line.trim();
|
|
39
56
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
const key = trimmed.slice(0,
|
|
43
|
-
const value = trimmed.slice(
|
|
57
|
+
const idx = trimmed.indexOf("=");
|
|
58
|
+
if (idx <= 0) continue;
|
|
59
|
+
const key = trimmed.slice(0, idx);
|
|
60
|
+
const value = trimmed.slice(idx + 1);
|
|
44
61
|
result.push({
|
|
45
62
|
key,
|
|
46
63
|
value,
|
|
@@ -51,16 +68,6 @@ function parseEnvContent(envContent) {
|
|
|
51
68
|
}
|
|
52
69
|
return result;
|
|
53
70
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Deploys Docker Compose stacks to a Coolify v4 instance.
|
|
56
|
-
*
|
|
57
|
-
* Deploy flow (5 steps):
|
|
58
|
-
* 1. Discover the first available server
|
|
59
|
-
* 2. Create a Coolify project
|
|
60
|
-
* 3. Create a compose service with the raw docker-compose YAML
|
|
61
|
-
* 4. Push .env variables via the bulk env API
|
|
62
|
-
* 5. Trigger the deployment
|
|
63
|
-
*/
|
|
64
71
|
var CoolifyDeployer = class {
|
|
65
72
|
name = "Coolify";
|
|
66
73
|
id = "coolify";
|
|
@@ -81,15 +88,15 @@ var CoolifyDeployer = class {
|
|
|
81
88
|
status: "pending"
|
|
82
89
|
};
|
|
83
90
|
const step2 = {
|
|
84
|
-
step: "
|
|
91
|
+
step: "Find or create project",
|
|
85
92
|
status: "pending"
|
|
86
93
|
};
|
|
87
94
|
const step3 = {
|
|
88
|
-
step: "
|
|
95
|
+
step: "Find or create service",
|
|
89
96
|
status: "pending"
|
|
90
97
|
};
|
|
91
98
|
const step4 = {
|
|
92
|
-
step: "
|
|
99
|
+
step: "Update environment variables",
|
|
93
100
|
status: "pending"
|
|
94
101
|
};
|
|
95
102
|
const step5 = {
|
|
@@ -107,68 +114,73 @@ var CoolifyDeployer = class {
|
|
|
107
114
|
success: false,
|
|
108
115
|
steps
|
|
109
116
|
};
|
|
117
|
+
const composeYaml = sanitizeComposeForPaas(input.composeYaml);
|
|
110
118
|
try {
|
|
111
119
|
step1.status = "running";
|
|
112
120
|
const servers = await coolifyFetch(input.target, "/servers");
|
|
113
|
-
if (!servers
|
|
121
|
+
if (!servers.length) throw new Error("No Coolify servers available");
|
|
114
122
|
const server = servers[0];
|
|
115
123
|
step1.status = "done";
|
|
116
|
-
step1.detail =
|
|
124
|
+
step1.detail = `${server.name} (${server.ip})`;
|
|
117
125
|
step2.status = "running";
|
|
118
|
-
|
|
126
|
+
let project = (await coolifyFetch(input.target, "/projects")).find((p) => p.name === input.projectName);
|
|
127
|
+
if (!project) project = await coolifyFetch(input.target, "/projects", {
|
|
119
128
|
method: "POST",
|
|
120
129
|
body: {
|
|
121
130
|
name: input.projectName,
|
|
122
|
-
description: input.description ?? `
|
|
131
|
+
description: input.description ?? `Stack ${input.projectName}`
|
|
123
132
|
}
|
|
124
133
|
});
|
|
125
134
|
result.projectId = project.uuid;
|
|
126
135
|
step2.status = "done";
|
|
127
|
-
step2.detail =
|
|
136
|
+
step2.detail = project.uuid;
|
|
128
137
|
const projectDetail = await coolifyFetch(input.target, `/projects/${project.uuid}`);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
if (!envUuid) throw new Error("No default environment found in project");
|
|
138
|
+
const env = projectDetail.environments?.find((e) => e.name === "production") ?? projectDetail.environments?.[0];
|
|
139
|
+
if (!env) throw new Error("No environment found");
|
|
132
140
|
step3.status = "running";
|
|
133
|
-
|
|
141
|
+
let service = (await coolifyFetch(input.target, `/projects/${project.uuid}/services`)).find((s) => s.name === input.projectName);
|
|
142
|
+
if (!service) service = await coolifyFetch(input.target, "/services", {
|
|
134
143
|
method: "POST",
|
|
135
144
|
body: {
|
|
136
145
|
project_uuid: project.uuid,
|
|
137
146
|
server_uuid: server.uuid,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
docker_compose_raw:
|
|
141
|
-
name: input.projectName
|
|
142
|
-
description: input.description ?? "Deployed via OpenClaw web builder",
|
|
143
|
-
instant_deploy: false
|
|
147
|
+
environment_uuid: env.uuid,
|
|
148
|
+
environment_name: env.name,
|
|
149
|
+
docker_compose_raw: composeYaml,
|
|
150
|
+
name: input.projectName
|
|
144
151
|
}
|
|
145
152
|
});
|
|
153
|
+
else if (hashString(composeYaml) !== hashString(service.docker_compose_raw ?? "")) await coolifyFetch(input.target, `/services/${service.uuid}`, {
|
|
154
|
+
method: "PATCH",
|
|
155
|
+
body: { docker_compose_raw: composeYaml }
|
|
156
|
+
});
|
|
146
157
|
result.composeId = service.uuid;
|
|
147
158
|
step3.status = "done";
|
|
148
|
-
step3.detail =
|
|
159
|
+
step3.detail = service.uuid;
|
|
149
160
|
step4.status = "running";
|
|
150
161
|
const envVars = parseEnvContent(input.envContent);
|
|
151
|
-
if (envVars.length
|
|
162
|
+
if (envVars.length) await coolifyFetch(input.target, `/services/${service.uuid}/envs`, {
|
|
152
163
|
method: "PATCH",
|
|
153
164
|
body: envVars
|
|
154
165
|
});
|
|
155
166
|
step4.status = "done";
|
|
156
|
-
step4.detail = `${envVars.length}
|
|
167
|
+
step4.detail = `${envVars.length} vars`;
|
|
157
168
|
step5.status = "running";
|
|
158
|
-
const
|
|
169
|
+
const deploy = await coolifyFetch(input.target, `/deploy?uuid=${service.uuid}&force=true`);
|
|
159
170
|
step5.status = "done";
|
|
160
|
-
step5.detail = deployments?.
|
|
171
|
+
step5.detail = deploy.deployments?.[0]?.deployment_uuid ?? "Deployment started";
|
|
161
172
|
result.success = true;
|
|
162
173
|
result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/project/${project.uuid}`;
|
|
174
|
+
return result;
|
|
163
175
|
} catch (err) {
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
const running = steps.find((s) => s.status === "running");
|
|
177
|
+
if (running) {
|
|
178
|
+
running.status = "error";
|
|
179
|
+
running.detail = err instanceof Error ? err.message : String(err);
|
|
168
180
|
}
|
|
169
181
|
result.error = err instanceof Error ? err.message : String(err);
|
|
182
|
+
return result;
|
|
170
183
|
}
|
|
171
|
-
return result;
|
|
172
184
|
}
|
|
173
185
|
};
|
|
174
186
|
|
|
@@ -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 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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"}
|
|
1
|
+
{"version":3,"file":"coolify.mjs","names":[],"sources":["../../src/deployers/coolify.ts"],"sourcesContent":["/**\n * Coolify PaaS deployer\n *\n * Deploys Docker Compose stacks via Coolify v4 API\n *\n * Docs:\n * https://coolify.io/docs/api-reference/api\n *\n * Auth:\n * Authorization: Bearer <token>\n *\n * Base path:\n * /api/v1\n */\n\nimport { sanitizeComposeForPaas } from \"./strip-host-ports.js\";\nimport type { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from \"./types.js\";\n\n/* ----------------------------- */\n/* Coolify API Types */\n/* ----------------------------- */\n\ninterface CoolifyProject {\n\tuuid: string;\n\tname: string;\n\tenvironments?: {\n\t\tuuid: string;\n\t\tname: string;\n\t}[];\n}\n\ninterface CoolifyServer {\n\tuuid: string;\n\tname: string;\n\tip: string;\n}\n\ninterface CoolifyService {\n\tuuid: string;\n\tname: string;\n\tdocker_compose_raw?: string;\n}\n\ninterface CoolifyDeployment {\n\tmessage: string;\n\tresource_uuid: string;\n\tdeployment_uuid: string;\n}\n\n/* ----------------------------- */\n/* Utilities */\n/* ----------------------------- */\n\nfunction apiUrl(target: DeployTarget, path: string) {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/v1${path}`;\n}\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\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\n\t\tthrow new Error(`Coolify API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\n\tif (!text) return undefined as T;\n\n\treturn JSON.parse(text);\n}\n\nfunction hashString(str: string) {\n\tlet hash = 0;\n\n\tfor (let i = 0; i < str.length; i++) {\n\t\thash = (hash << 5) - hash + str.charCodeAt(i);\n\t\thash |= 0;\n\t}\n\n\treturn hash;\n}\n\nfunction parseEnvContent(envContent?: string) {\n\tif (!envContent) return [];\n\n\tconst result = [];\n\n\tfor (const line of envContent.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n\t\tconst idx = trimmed.indexOf(\"=\");\n\n\t\tif (idx <= 0) continue;\n\n\t\tconst key = trimmed.slice(0, idx);\n\t\tconst value = trimmed.slice(idx + 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/* Coolify Deployer */\n/* ----------------------------- */\n\nexport class CoolifyDeployer implements PaasDeployer {\n\treadonly name = \"Coolify\";\n\treadonly id = \"coolify\";\n\n\tasync testConnection(target: DeployTarget) {\n\t\ttry {\n\t\t\tawait coolifyFetch(target, \"/version\");\n\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t};\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 = {\n\t\t\tstep: \"Find or create project\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step3: DeployStep = {\n\t\t\tstep: \"Find or create service\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step4: DeployStep = {\n\t\t\tstep: \"Update environment variables\",\n\t\t\tstatus: \"pending\",\n\t\t};\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\t// Strip host port bindings — Coolify routes via Traefik,\n\t\t// so host ports are unnecessary and cause \"port already allocated\" errors.\n\t\tconst composeYaml = sanitizeComposeForPaas(input.composeYaml);\n\n\t\ttry {\n\t\t\t/* ----------------------------- */\n\t\t\t/* STEP 1: Discover server */\n\t\t\t/* ----------------------------- */\n\n\t\t\tstep1.status = \"running\";\n\n\t\t\tconst servers = await coolifyFetch<CoolifyServer[]>(input.target, \"/servers\");\n\n\t\t\tif (!servers.length) {\n\t\t\t\tthrow new Error(\"No Coolify servers available\");\n\t\t\t}\n\n\t\t\tconst server = servers[0]!;\n\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `${server.name} (${server.ip})`;\n\n\t\t\t/* ----------------------------- */\n\t\t\t/* STEP 2: Find or create project */\n\t\t\t/* ----------------------------- */\n\n\t\t\tstep2.status = \"running\";\n\n\t\t\tconst projects = await coolifyFetch<CoolifyProject[]>(input.target, \"/projects\");\n\n\t\t\tlet project = projects.find((p) => p.name === input.projectName);\n\n\t\t\tif (!project) {\n\t\t\t\tproject = await coolifyFetch<CoolifyProject>(input.target, \"/projects\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tname: input.projectName,\n\t\t\t\t\t\tdescription: input.description ?? `Stack ${input.projectName}`,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tresult.projectId = project.uuid;\n\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = project.uuid;\n\n\t\t\t/* ----------------------------- */\n\t\t\t/* Find environment */\n\t\t\t/* ----------------------------- */\n\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\n\t\t\tconst env =\n\t\t\t\tprojectDetail.environments?.find((e) => e.name === \"production\") ??\n\t\t\t\tprojectDetail.environments?.[0];\n\n\t\t\tif (!env) throw new Error(\"No environment found\");\n\n\t\t\t/* ----------------------------- */\n\t\t\t/* STEP 3: Find or create service */\n\t\t\t/* ----------------------------- */\n\n\t\t\tstep3.status = \"running\";\n\n\t\t\tconst services = await coolifyFetch<CoolifyService[]>(\n\t\t\t\tinput.target,\n\t\t\t\t`/projects/${project.uuid}/services`,\n\t\t\t);\n\n\t\t\tlet service = services.find((s) => s.name === input.projectName);\n\n\t\t\tif (!service) {\n\t\t\t\tservice = await coolifyFetch<CoolifyService>(input.target, \"/services\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tproject_uuid: project.uuid,\n\n\t\t\t\t\t\tserver_uuid: server.uuid,\n\n\t\t\t\t\t\tenvironment_uuid: env.uuid,\n\n\t\t\t\t\t\tenvironment_name: env.name,\n\n\t\t\t\t\t\tdocker_compose_raw: composeYaml,\n\n\t\t\t\t\t\tname: input.projectName,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst newHash = hashString(composeYaml);\n\t\t\t\tconst oldHash = hashString(service.docker_compose_raw ?? \"\");\n\n\t\t\t\tif (newHash !== oldHash) {\n\t\t\t\t\tawait coolifyFetch(input.target, `/services/${service.uuid}`, {\n\t\t\t\t\t\tmethod: \"PATCH\",\n\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\tdocker_compose_raw: composeYaml,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tresult.composeId = service.uuid;\n\n\t\t\tstep3.status = \"done\";\n\t\t\tstep3.detail = service.uuid;\n\n\t\t\t/* ----------------------------- */\n\t\t\t/* STEP 4: Environment variables */\n\t\t\t/* ----------------------------- */\n\n\t\t\tstep4.status = \"running\";\n\n\t\t\tconst envVars = parseEnvContent(input.envContent);\n\n\t\t\tif (envVars.length) {\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\n\t\t\tstep4.status = \"done\";\n\t\t\tstep4.detail = `${envVars.length} vars`;\n\n\t\t\t/* ----------------------------- */\n\t\t\t/* STEP 5: Deploy */\n\t\t\t/* ----------------------------- */\n\n\t\t\tstep5.status = \"running\";\n\n\t\t\tconst deploy = 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\n\t\t\tstep5.status = \"done\";\n\t\t\tstep5.detail = deploy.deployments?.[0]?.deployment_uuid ?? \"Deployment started\";\n\n\t\t\tresult.success = true;\n\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\n\t\t\tresult.dashboardUrl = `${base}/project/${project.uuid}`;\n\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tconst running = steps.find((s) => s.status === \"running\");\n\n\t\t\tif (running) {\n\t\t\t\trunning.status = \"error\";\n\t\t\t\trunning.detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\n\t\t\treturn result;\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqDA,SAAS,OAAO,QAAsB,MAAc;AAEnD,QAAO,GADM,OAAO,YAAY,QAAQ,QAAQ,GAAG,CACpC,SAAS;;AAGzB,eAAe,aACd,QACA,MACA,UAA+C,EAAE,EACpC;CACb,MAAM,MAAM,MAAM,MAAM,OAAO,QAAQ,KAAK,EAAE;EAC7C,QAAQ,QAAQ,UAAU;EAC1B,SAAS;GACR,gBAAgB;GAChB,eAAe,UAAU,OAAO;GAChC;EACD,MAAM,QAAQ,OAAO,KAAK,UAAU,QAAQ,KAAK,GAAG;EACpD,CAAC;AAEF,KAAI,CAAC,IAAI,IAAI;EACZ,MAAM,OAAO,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG;EAC7C,IAAI,SAAS;AAEb,MAAI;GACH,MAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,YAAS,KAAK,WAAW,KAAK,SAAS;UAChC;AAER,QAAM,IAAI,MAAM,eAAe,IAAI,OAAO,IAAI,SAAS;;CAGxD,MAAM,OAAO,MAAM,IAAI,MAAM;AAE7B,KAAI,CAAC,KAAM,QAAO;AAElB,QAAO,KAAK,MAAM,KAAK;;AAGxB,SAAS,WAAW,KAAa;CAChC,IAAI,OAAO;AAEX,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACpC,UAAQ,QAAQ,KAAK,OAAO,IAAI,WAAW,EAAE;AAC7C,UAAQ;;AAGT,QAAO;;AAGR,SAAS,gBAAgB,YAAqB;AAC7C,KAAI,CAAC,WAAY,QAAO,EAAE;CAE1B,MAAM,SAAS,EAAE;AAEjB,MAAK,MAAM,QAAQ,WAAW,MAAM,KAAK,EAAE;EAC1C,MAAM,UAAU,KAAK,MAAM;AAE3B,MAAI,CAAC,WAAW,QAAQ,WAAW,IAAI,CAAE;EAEzC,MAAM,MAAM,QAAQ,QAAQ,IAAI;AAEhC,MAAI,OAAO,EAAG;EAEd,MAAM,MAAM,QAAQ,MAAM,GAAG,IAAI;EACjC,MAAM,QAAQ,QAAQ,MAAM,MAAM,EAAE;AAEpC,SAAO,KAAK;GACX;GACA;GACA,YAAY;GACZ,eAAe;GACf,YAAY;GACZ,CAAC;;AAGH,QAAO;;AAOR,IAAa,kBAAb,MAAqD;CACpD,AAAS,OAAO;CAChB,AAAS,KAAK;CAEd,MAAM,eAAe,QAAsB;AAC1C,MAAI;AACH,SAAM,aAAa,QAAQ,WAAW;AAEtC,UAAO,EAAE,IAAI,MAAM;WACX,KAAK;AACb,UAAO;IACN,IAAI;IACJ,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IACvD;;;CAIH,MAAM,OAAO,OAA2C;EACvD,MAAM,QAAoB;GAAE,MAAM;GAAmB,QAAQ;GAAW;EACxE,MAAM,QAAoB;GACzB,MAAM;GACN,QAAQ;GACR;EACD,MAAM,QAAoB;GACzB,MAAM;GACN,QAAQ;GACR;EACD,MAAM,QAAoB;GACzB,MAAM;GACN,QAAQ;GACR;EACD,MAAM,QAAoB;GAAE,MAAM;GAAsB,QAAQ;GAAW;EAC3E,MAAM,QAAsB;GAAC;GAAO;GAAO;GAAO;GAAO;GAAM;EAE/D,MAAM,SAAuB;GAAE,SAAS;GAAO;GAAO;EAItD,MAAM,cAAc,uBAAuB,MAAM,YAAY;AAE7D,MAAI;AAKH,SAAM,SAAS;GAEf,MAAM,UAAU,MAAM,aAA8B,MAAM,QAAQ,WAAW;AAE7E,OAAI,CAAC,QAAQ,OACZ,OAAM,IAAI,MAAM,+BAA+B;GAGhD,MAAM,SAAS,QAAQ;AAEvB,SAAM,SAAS;AACf,SAAM,SAAS,GAAG,OAAO,KAAK,IAAI,OAAO,GAAG;AAM5C,SAAM,SAAS;GAIf,IAAI,WAFa,MAAM,aAA+B,MAAM,QAAQ,YAAY,EAEzD,MAAM,MAAM,EAAE,SAAS,MAAM,YAAY;AAEhE,OAAI,CAAC,QACJ,WAAU,MAAM,aAA6B,MAAM,QAAQ,aAAa;IACvE,QAAQ;IACR,MAAM;KACL,MAAM,MAAM;KACZ,aAAa,MAAM,eAAe,SAAS,MAAM;KACjD;IACD,CAAC;AAGH,UAAO,YAAY,QAAQ;AAE3B,SAAM,SAAS;AACf,SAAM,SAAS,QAAQ;GAMvB,MAAM,gBAAgB,MAAM,aAC3B,MAAM,QACN,aAAa,QAAQ,OACrB;GAED,MAAM,MACL,cAAc,cAAc,MAAM,MAAM,EAAE,SAAS,aAAa,IAChE,cAAc,eAAe;AAE9B,OAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uBAAuB;AAMjD,SAAM,SAAS;GAOf,IAAI,WALa,MAAM,aACtB,MAAM,QACN,aAAa,QAAQ,KAAK,WAC1B,EAEsB,MAAM,MAAM,EAAE,SAAS,MAAM,YAAY;AAEhE,OAAI,CAAC,QACJ,WAAU,MAAM,aAA6B,MAAM,QAAQ,aAAa;IACvE,QAAQ;IACR,MAAM;KACL,cAAc,QAAQ;KAEtB,aAAa,OAAO;KAEpB,kBAAkB,IAAI;KAEtB,kBAAkB,IAAI;KAEtB,oBAAoB;KAEpB,MAAM,MAAM;KACZ;IACD,CAAC;YAEc,WAAW,YAAY,KACvB,WAAW,QAAQ,sBAAsB,GAAG,CAG3D,OAAM,aAAa,MAAM,QAAQ,aAAa,QAAQ,QAAQ;IAC7D,QAAQ;IACR,MAAM,EACL,oBAAoB,aACpB;IACD,CAAC;AAIJ,UAAO,YAAY,QAAQ;AAE3B,SAAM,SAAS;AACf,SAAM,SAAS,QAAQ;AAMvB,SAAM,SAAS;GAEf,MAAM,UAAU,gBAAgB,MAAM,WAAW;AAEjD,OAAI,QAAQ,OACX,OAAM,aAAa,MAAM,QAAQ,aAAa,QAAQ,KAAK,QAAQ;IAClE,QAAQ;IACR,MAAM;IACN,CAAC;AAGH,SAAM,SAAS;AACf,SAAM,SAAS,GAAG,QAAQ,OAAO;AAMjC,SAAM,SAAS;GAEf,MAAM,SAAS,MAAM,aACpB,MAAM,QACN,gBAAgB,QAAQ,KAAK,aAC7B;AAED,SAAM,SAAS;AACf,SAAM,SAAS,OAAO,cAAc,IAAI,mBAAmB;AAE3D,UAAO,UAAU;AAIjB,UAAO,eAAe,GAFT,MAAM,OAAO,YAAY,QAAQ,QAAQ,GAAG,CAE3B,WAAW,QAAQ;AAEjD,UAAO;WACC,KAAK;GACb,MAAM,UAAU,MAAM,MAAM,MAAM,EAAE,WAAW,UAAU;AAEzD,OAAI,SAAS;AACZ,YAAQ,SAAS;AACjB,YAAQ,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;;AAGlE,UAAO,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAE/D,UAAO"}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_deployers_strip_host_ports = require('./strip-host-ports.cjs');
|
|
2
3
|
|
|
3
4
|
//#region src/deployers/dokploy.ts
|
|
5
|
+
/**
|
|
6
|
+
* Dokploy PaaS deployer — deploys Docker Compose stacks via the Dokploy REST API.
|
|
7
|
+
*
|
|
8
|
+
* API docs: https://docs.dokploy.com/docs/api
|
|
9
|
+
* Auth: x-api-key header
|
|
10
|
+
* Endpoints use dot-notation (e.g. /api/project.create, /api/compose.deploy)
|
|
11
|
+
*/
|
|
4
12
|
/** Build a full Dokploy API URL from a dot-notation endpoint (e.g. "project.create"). */
|
|
5
13
|
function apiUrl(target, endpoint) {
|
|
6
14
|
return `${target.instanceUrl.replace(/\/+$/, "")}/api/${endpoint}`;
|
|
@@ -32,6 +40,17 @@ async function dokployFetch(target, endpoint, options = {}) {
|
|
|
32
40
|
return JSON.parse(text);
|
|
33
41
|
}
|
|
34
42
|
/**
|
|
43
|
+
* Simple hash for compose diff detection
|
|
44
|
+
*/
|
|
45
|
+
function hashString(str) {
|
|
46
|
+
let hash = 0;
|
|
47
|
+
for (let i = 0; i < str.length; i++) {
|
|
48
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
49
|
+
hash |= 0;
|
|
50
|
+
}
|
|
51
|
+
return hash;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
35
54
|
* Deploys Docker Compose stacks to a Dokploy instance.
|
|
36
55
|
*
|
|
37
56
|
* Deploy flow (4 steps):
|
|
@@ -54,89 +73,147 @@ var DokployDeployer = class {
|
|
|
54
73
|
};
|
|
55
74
|
}
|
|
56
75
|
}
|
|
76
|
+
async listServers(target) {
|
|
77
|
+
try {
|
|
78
|
+
return (await dokployFetch(target, "server.all")).map((s) => ({
|
|
79
|
+
id: s.serverId,
|
|
80
|
+
name: s.name,
|
|
81
|
+
ip: s.ipAddress
|
|
82
|
+
}));
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
57
87
|
async deploy(input) {
|
|
58
88
|
const step1 = {
|
|
59
|
-
step: "
|
|
89
|
+
step: "Find or create project",
|
|
60
90
|
status: "pending"
|
|
61
91
|
};
|
|
62
92
|
const step2 = {
|
|
63
|
-
step: "
|
|
93
|
+
step: "Find default environment",
|
|
64
94
|
status: "pending"
|
|
65
95
|
};
|
|
66
96
|
const step3 = {
|
|
67
|
-
step: "
|
|
97
|
+
step: "Find or create compose stack",
|
|
68
98
|
status: "pending"
|
|
69
99
|
};
|
|
70
100
|
const step4 = {
|
|
71
|
-
step: "
|
|
101
|
+
step: "Update stack configuration",
|
|
102
|
+
status: "pending"
|
|
103
|
+
};
|
|
104
|
+
const step5 = {
|
|
105
|
+
step: "Deploy stack",
|
|
72
106
|
status: "pending"
|
|
73
107
|
};
|
|
74
108
|
const steps = [
|
|
75
109
|
step1,
|
|
76
110
|
step2,
|
|
77
111
|
step3,
|
|
78
|
-
step4
|
|
112
|
+
step4,
|
|
113
|
+
step5
|
|
79
114
|
];
|
|
80
115
|
const result = {
|
|
81
116
|
success: false,
|
|
82
117
|
steps
|
|
83
118
|
};
|
|
119
|
+
const composeYaml = require_deployers_strip_host_ports.sanitizeComposeForPaas(input.composeYaml);
|
|
84
120
|
try {
|
|
121
|
+
/**
|
|
122
|
+
* STEP 1
|
|
123
|
+
* Find or create project
|
|
124
|
+
*/
|
|
85
125
|
step1.status = "running";
|
|
86
|
-
|
|
126
|
+
let project = (await dokployFetch(input.target, "project.all")).find((p) => p.name === input.projectName);
|
|
127
|
+
if (!project) project = (await dokployFetch(input.target, "project.create", {
|
|
87
128
|
method: "POST",
|
|
88
129
|
body: {
|
|
89
130
|
name: input.projectName,
|
|
90
131
|
description: input.description ?? `OpenClaw stack: ${input.projectName}`
|
|
91
132
|
}
|
|
92
|
-
});
|
|
133
|
+
})).project;
|
|
93
134
|
result.projectId = project.projectId;
|
|
94
135
|
step1.status = "done";
|
|
95
136
|
step1.detail = `Project ID: ${project.projectId}`;
|
|
96
|
-
|
|
97
|
-
|
|
137
|
+
/**
|
|
138
|
+
* STEP 2
|
|
139
|
+
* Find default environment
|
|
140
|
+
*/
|
|
98
141
|
step2.status = "running";
|
|
99
|
-
const
|
|
142
|
+
const env = (await dokployFetch(input.target, `project.one?projectId=${project.projectId}`)).environments?.find((e) => e.isDefault);
|
|
143
|
+
if (!env) throw new Error("No default environment");
|
|
144
|
+
step2.status = "done";
|
|
145
|
+
step2.detail = env.environmentId;
|
|
146
|
+
/**
|
|
147
|
+
* STEP 3
|
|
148
|
+
* Find or create compose stack
|
|
149
|
+
*/
|
|
150
|
+
step3.status = "running";
|
|
151
|
+
let stack = null;
|
|
152
|
+
stack = await dokployFetch(input.target, "compose.create", {
|
|
100
153
|
method: "POST",
|
|
101
154
|
body: {
|
|
102
155
|
name: input.projectName,
|
|
103
|
-
|
|
104
|
-
|
|
156
|
+
description: input.description ?? `Stack ${input.projectName}`,
|
|
157
|
+
environmentId: env.environmentId,
|
|
158
|
+
composeType: "docker-compose",
|
|
159
|
+
composeFile: composeYaml,
|
|
160
|
+
...input.serverId ? { serverId: input.serverId } : {}
|
|
105
161
|
}
|
|
106
162
|
});
|
|
107
|
-
|
|
108
|
-
step2.status = "done";
|
|
109
|
-
step2.detail = `Compose ID: ${compose.composeId}`;
|
|
110
|
-
step3.status = "running";
|
|
111
|
-
await dokployFetch(input.target, "compose.update", {
|
|
163
|
+
if (stack?.composeId) await dokployFetch(input.target, "compose.update", {
|
|
112
164
|
method: "POST",
|
|
113
165
|
body: {
|
|
114
|
-
composeId:
|
|
115
|
-
|
|
166
|
+
composeId: stack.composeId,
|
|
167
|
+
sourceType: "raw"
|
|
116
168
|
}
|
|
117
169
|
});
|
|
170
|
+
result.composeId = stack?.composeId;
|
|
118
171
|
step3.status = "done";
|
|
172
|
+
step3.detail = stack?.composeId;
|
|
173
|
+
/**
|
|
174
|
+
* STEP 4
|
|
175
|
+
* Update stack if compose changed
|
|
176
|
+
*/
|
|
119
177
|
step4.status = "running";
|
|
178
|
+
const existingStack = await dokployFetch(input.target, `compose.one?composeId=${stack?.composeId}`);
|
|
179
|
+
if (hashString(composeYaml) !== hashString(existingStack.compose ?? "")) {
|
|
180
|
+
await dokployFetch(input.target, "compose.update", {
|
|
181
|
+
method: "POST",
|
|
182
|
+
body: {
|
|
183
|
+
composeId: stack?.composeId,
|
|
184
|
+
composeFile: composeYaml,
|
|
185
|
+
env: input.envContent ?? ""
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
step4.detail = "Stack updated";
|
|
189
|
+
} else step4.detail = "No compose changes";
|
|
190
|
+
step4.status = "done";
|
|
191
|
+
/**
|
|
192
|
+
* STEP 5
|
|
193
|
+
* Deploy
|
|
194
|
+
*/
|
|
195
|
+
step5.status = "running";
|
|
120
196
|
await dokployFetch(input.target, "compose.deploy", {
|
|
121
197
|
method: "POST",
|
|
122
198
|
body: {
|
|
123
|
-
composeId:
|
|
124
|
-
title: `
|
|
125
|
-
description:
|
|
199
|
+
composeId: stack?.composeId,
|
|
200
|
+
title: `Deploy ${input.projectName}`,
|
|
201
|
+
description: "CI deployment"
|
|
126
202
|
}
|
|
127
203
|
});
|
|
128
|
-
|
|
204
|
+
step5.status = "done";
|
|
129
205
|
result.success = true;
|
|
130
|
-
result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/dashboard/project/${project.projectId}`;
|
|
206
|
+
result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/dashboard/project/${project.projectId}/environment/${env.environmentId}/services/compose/${stack?.composeId}?tab=deployments`;
|
|
207
|
+
return result;
|
|
131
208
|
} catch (err) {
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
209
|
+
const running = steps.find((s) => s.status === "running");
|
|
210
|
+
if (running) {
|
|
211
|
+
running.status = "error";
|
|
212
|
+
running.detail = err instanceof Error ? err.message : String(err);
|
|
136
213
|
}
|
|
137
214
|
result.error = err instanceof Error ? err.message : String(err);
|
|
215
|
+
return result;
|
|
138
216
|
}
|
|
139
|
-
return result;
|
|
140
217
|
}
|
|
141
218
|
};
|
|
142
219
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dokploy.cjs","names":[],"sources":["../../src/deployers/dokploy.ts"],"sourcesContent":["/**\n * Dokploy PaaS deployer — deploys Docker Compose stacks via the Dokploy REST API.\n *\n * API docs: https://docs.dokploy.com/docs/api\n * Auth: x-api-key header\n * Endpoints use dot-notation (e.g. /api/project.create, /api/compose.deploy)\n */\n\nimport type { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from \"./types.js\";\n\n/** Shape returned by Dokploy's project endpoints. */\ninterface DokployProject {\n\tprojectId: string;\n\tname: string;\n\tdescription: string;\n\tenvironments?: { environmentId: string; name: string }[];\n}\n\n/** Shape returned by Dokploy's compose endpoints. */\ninterface DokployCompose {\n\tcomposeId: string;\n\tname: string;\n\tstatus?: string;\n}\n\n/** Build a full Dokploy API URL from a dot-notation endpoint (e.g. \"project.create\"). */\nfunction apiUrl(target: DeployTarget, endpoint: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/${endpoint}`;\n}\n\n/**\n * Typed fetch wrapper for the Dokploy API.\n * Handles JSON serialisation, x-api-key auth, and error extraction.\n */\nasync function dokployFetch<T>(\n\ttarget: DeployTarget,\n\tendpoint: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, endpoint), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"x-api-key\": 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(`Dokploy 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 * Deploys Docker Compose stacks to a Dokploy instance.\n *\n * Deploy flow (4 steps):\n * 1. Create a Dokploy project\n * 2. Create a compose stack inside the project's default environment\n * 3. Push .env variables to the compose stack\n * 4. Trigger the deployment\n */\nexport class DokployDeployer implements PaasDeployer {\n\treadonly name = \"Dokploy\";\n\treadonly id = \"dokploy\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait dokployFetch<DokployProject[]>(target, \"project.all\");\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: \"Create project\", status: \"pending\" };\n\t\tconst step2: DeployStep = { step: \"Create compose stack\", status: \"pending\" };\n\t\tconst step3: DeployStep = { step: \"Set environment variables\", status: \"pending\" };\n\t\tconst step4: DeployStep = { step: \"Trigger deployment\", status: \"pending\" };\n\t\tconst steps: DeployStep[] = [step1, step2, step3, step4];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\ttry {\n\t\t\t// Step 1: Create project\n\t\t\tstep1.status = \"running\";\n\t\t\tconst project = await dokployFetch<DokployProject>(input.target, \"project.create\", {\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.projectId;\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `Project ID: ${project.projectId}`;\n\n\t\t\t// Get the default environment ID\n\t\t\tconst projectDetail = await dokployFetch<DokployProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`project.one?projectId=${project.projectId}`,\n\t\t\t);\n\t\t\tconst envId = projectDetail.environments?.[0]?.environmentId;\n\t\t\tif (!envId) {\n\t\t\t\tthrow new Error(\"No default environment found in project\");\n\t\t\t}\n\n\t\t\t// Step 2: Create compose stack\n\t\t\tstep2.status = \"running\";\n\t\t\tconst compose = await dokployFetch<DokployCompose>(input.target, \"compose.create\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tenvironmentId: envId,\n\t\t\t\t\tcomposeFile: input.composeYaml,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.composeId = compose.composeId;\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = `Compose ID: ${compose.composeId}`;\n\n\t\t\t// Step 3: Set environment variables\n\t\t\tstep3.status = \"running\";\n\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: compose.composeId,\n\t\t\t\t\tenv: input.envContent,\n\t\t\t\t},\n\t\t\t});\n\t\t\tstep3.status = \"done\";\n\n\t\t\t// Step 4: Trigger deployment\n\t\t\tstep4.status = \"running\";\n\t\t\tawait dokployFetch(input.target, \"compose.deploy\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: compose.composeId,\n\t\t\t\t\ttitle: `Initial deploy: ${input.projectName}`,\n\t\t\t\t\tdescription: input.description ?? \"Deployed via OpenClaw web builder\",\n\t\t\t\t},\n\t\t\t});\n\t\t\tstep4.status = \"done\";\n\n\t\t\tresult.success = true;\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\t\t\tresult.dashboardUrl = `${base}/dashboard/project/${project.projectId}`;\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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"}
|
|
1
|
+
{"version":3,"file":"dokploy.cjs","names":["sanitizeComposeForPaas"],"sources":["../../src/deployers/dokploy.ts"],"sourcesContent":["/**\n * Dokploy PaaS deployer — deploys Docker Compose stacks via the Dokploy REST API.\n *\n * API docs: https://docs.dokploy.com/docs/api\n * Auth: x-api-key header\n * Endpoints use dot-notation (e.g. /api/project.create, /api/compose.deploy)\n */\n\nimport { sanitizeComposeForPaas } from \"./strip-host-ports.js\";\nimport type {\n\tDeployInput,\n\tDeployResult,\n\tDeployStep,\n\tDeployTarget,\n\tDokployEnvironment,\n\tPaasDeployer,\n\tPaasServer,\n} from \"./types.js\";\n\ninterface DokployProject {\n\tprojectId: string;\n\tname: string;\n\tdescription: string;\n\tenvironments?: DokployEnvironment[];\n}\n\ninterface DokployCompose {\n\tcomposeId: string;\n\tname: string;\n\tstatus?: string;\n\tcompose?: string;\n}\n\ninterface ProjectCreateResult {\n\tproject: DokployProject;\n\tprojectId: string;\n\tname: string;\n\tdescription: string;\n\tenvironments?: { environmentId: string; name: string }[];\n}\n\n/** Build a full Dokploy API URL from a dot-notation endpoint (e.g. \"project.create\"). */\nfunction apiUrl(target: DeployTarget, endpoint: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/${endpoint}`;\n}\n/**\n * Typed fetch wrapper for the Dokploy API.\n * Handles JSON serialisation, x-api-key auth, and error extraction.\n */\nasync function dokployFetch<T>(\n\ttarget: DeployTarget,\n\tendpoint: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, endpoint), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"x-api-key\": 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\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\n\t\tthrow new Error(`Dokploy API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\tif (!text) return undefined as T;\n\n\treturn JSON.parse(text) as T;\n}\n\n/**\n * Simple hash for compose diff detection\n */\nfunction hashString(str: string) {\n\tlet hash = 0;\n\n\tfor (let i = 0; i < str.length; i++) {\n\t\thash = (hash << 5) - hash + str.charCodeAt(i);\n\t\thash |= 0;\n\t}\n\n\treturn hash;\n}\n\n/**\n * Deploys Docker Compose stacks to a Dokploy instance.\n *\n * Deploy flow (4 steps):\n * 1. Create a Dokploy project\n * 2. Create a compose stack inside the project's default environment\n * 3. Push .env variables to the compose stack\n * 4. Trigger the deployment\n */\n\nexport class DokployDeployer implements PaasDeployer {\n\treadonly name = \"Dokploy\";\n\treadonly id = \"dokploy\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait dokployFetch<DokployProject[]>(target, \"project.all\");\n\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t};\n\t\t}\n\t}\n\n\tasync listServers(target: DeployTarget): Promise<PaasServer[]> {\n\t\ttry {\n\t\t\tconst servers = await dokployFetch<{ serverId: string; name: string; ipAddress: string }[]>(\n\t\t\t\ttarget,\n\t\t\t\t\"server.all\",\n\t\t\t);\n\t\t\treturn servers.map((s) => ({\n\t\t\t\tid: s.serverId,\n\t\t\t\tname: s.name,\n\t\t\t\tip: s.ipAddress,\n\t\t\t}));\n\t\t} catch {\n\t\t\t// Return empty list if server API is not available\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tasync deploy(input: DeployInput): Promise<DeployResult> {\n\t\tconst step1: DeployStep = {\n\t\t\tstep: \"Find or create project\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step2: DeployStep = {\n\t\t\tstep: \"Find default environment\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step3: DeployStep = {\n\t\t\tstep: \"Find or create compose stack\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step4: DeployStep = {\n\t\t\tstep: \"Update stack configuration\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step5: DeployStep = { step: \"Deploy stack\", status: \"pending\" };\n\t\tconst steps: DeployStep[] = [step1, step2, step3, step4, step5];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\t// Strip host port bindings — Dokploy routes via Traefik,\n\t\t// so host ports are unnecessary and cause \"port already allocated\" errors.\n\t\tconst composeYaml = sanitizeComposeForPaas(input.composeYaml);\n\n\t\ttry {\n\t\t\t/**\n\t\t\t * STEP 1\n\t\t\t * Find or create project\n\t\t\t */\n\n\t\t\tstep1.status = \"running\";\n\n\t\t\tconst projects = await dokployFetch<DokployProject[]>(input.target, \"project.all\");\n\n\t\t\tlet project = projects.find((p) => p.name === input.projectName);\n\n\t\t\tif (!project) {\n\t\t\t\tconst created = await dokployFetch<ProjectCreateResult>(input.target, \"project.create\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tname: input.projectName,\n\t\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tproject = created.project;\n\t\t\t}\n\n\t\t\tresult.projectId = project.projectId;\n\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `Project ID: ${project.projectId}`;\n\n\t\t\t/**\n\t\t\t * STEP 2\n\t\t\t * Find default environment\n\t\t\t */\n\n\t\t\tstep2.status = \"running\";\n\n\t\t\tconst projectDetail = await dokployFetch<DokployProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`project.one?projectId=${project.projectId}`,\n\t\t\t);\n\n\t\t\tconst env = projectDetail.environments?.find((e) => e.isDefault);\n\n\t\t\tif (!env) throw new Error(\"No default environment\");\n\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = env.environmentId;\n\n\t\t\t/**\n\t\t\t * STEP 3\n\t\t\t * Find or create compose stack\n\t\t\t */\n\n\t\t\tstep3.status = \"running\";\n\n\t\t\tlet stack: DokployCompose | null = null;\n\n\t\t\tstack = await dokployFetch<DokployCompose>(input.target, \"compose.create\", {\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 ?? `Stack ${input.projectName}`,\n\t\t\t\t\tenvironmentId: env.environmentId,\n\t\t\t\t\tcomposeType: \"docker-compose\",\n\t\t\t\t\tcomposeFile: composeYaml,\n\t\t\t\t\t...(input.serverId ? { serverId: input.serverId } : {}),\n\t\t\t\t},\n\t\t\t});\n\n\t\t\t// Dokploy's compose.create schema does NOT accept sourceType;\n\t\t\t// it defaults to \"github\". We must update it to \"raw\" so the\n\t\t\t// deploy step writes the compose file from the stored YAML\n\t\t\t// instead of attempting to clone from a Git provider.\n\t\t\tif (stack?.composeId) {\n\t\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tcomposeId: stack.composeId,\n\t\t\t\t\t\tsourceType: \"raw\",\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tresult.composeId = stack?.composeId;\n\t\t\tstep3.status = \"done\";\n\t\t\tstep3.detail = stack?.composeId;\n\n\t\t\t/**\n\t\t\t * STEP 4\n\t\t\t * Update stack if compose changed\n\t\t\t */\n\n\t\t\tstep4.status = \"running\";\n\n\t\t\tconst existingStack = await dokployFetch<DokployCompose>(\n\t\t\t\tinput.target,\n\t\t\t\t`compose.one?composeId=${stack?.composeId}`,\n\t\t\t);\n\n\t\t\tconst newHash = hashString(composeYaml);\n\t\t\tconst oldHash = hashString(existingStack.compose ?? \"\");\n\n\t\t\tif (newHash !== oldHash) {\n\t\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tcomposeId: stack?.composeId,\n\t\t\t\t\t\tcomposeFile: composeYaml,\n\t\t\t\t\t\tenv: input.envContent ?? \"\",\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tstep4.detail = \"Stack updated\";\n\t\t\t} else {\n\t\t\t\tstep4.detail = \"No compose changes\";\n\t\t\t}\n\n\t\t\tstep4.status = \"done\";\n\n\t\t\t/**\n\t\t\t * STEP 5\n\t\t\t * Deploy\n\t\t\t */\n\n\t\t\tstep5.status = \"running\";\n\n\t\t\tawait dokployFetch(input.target, \"compose.deploy\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: stack?.composeId,\n\n\t\t\t\t\ttitle: `Deploy ${input.projectName}`,\n\n\t\t\t\t\tdescription: \"CI deployment\",\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tstep5.status = \"done\";\n\n\t\t\tresult.success = true;\n\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\n\t\t\tresult.dashboardUrl = `${base}/dashboard/project/${project.projectId}/environment/${env.environmentId}/services/compose/${stack?.composeId}?tab=deployments`;\n\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tconst running = steps.find((s) => s.status === \"running\");\n\n\t\t\tif (running) {\n\t\t\t\trunning.status = \"error\";\n\n\t\t\t\trunning.detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\n\t\t\treturn result;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeployInput, DeployResult, DeployTarget, PaasDeployer } from "./types.cjs";
|
|
1
|
+
import { DeployInput, DeployResult, DeployTarget, PaasDeployer, PaasServer } from "./types.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/deployers/dokploy.d.ts
|
|
4
4
|
/**
|
|
@@ -17,6 +17,7 @@ declare class DokployDeployer implements PaasDeployer {
|
|
|
17
17
|
ok: boolean;
|
|
18
18
|
error?: string;
|
|
19
19
|
}>;
|
|
20
|
+
listServers(target: DeployTarget): Promise<PaasServer[]>;
|
|
20
21
|
deploy(input: DeployInput): Promise<DeployResult>;
|
|
21
22
|
}
|
|
22
23
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dokploy.d.cts","names":[],"sources":["../../src/deployers/dokploy.ts"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"dokploy.d.cts","names":[],"sources":["../../src/deployers/dokploy.ts"],"mappings":";;;;;;;;;;;;cA0Ga,eAAA,YAA2B,YAAA;EAAA,SAC9B,IAAA;EAAA,SACA,EAAA;EAEH,cAAA,CAAe,MAAA,EAAQ,YAAA,GAAe,OAAA;IAAU,EAAA;IAAa,KAAA;EAAA;EAa7D,WAAA,CAAY,MAAA,EAAQ,YAAA,GAAe,OAAA,CAAQ,UAAA;EAiB3C,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,OAAA,CAAQ,YAAA;AAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeployInput, DeployResult, DeployTarget, PaasDeployer } from "./types.mjs";
|
|
1
|
+
import { DeployInput, DeployResult, DeployTarget, PaasDeployer, PaasServer } from "./types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/deployers/dokploy.d.ts
|
|
4
4
|
/**
|
|
@@ -17,6 +17,7 @@ declare class DokployDeployer implements PaasDeployer {
|
|
|
17
17
|
ok: boolean;
|
|
18
18
|
error?: string;
|
|
19
19
|
}>;
|
|
20
|
+
listServers(target: DeployTarget): Promise<PaasServer[]>;
|
|
20
21
|
deploy(input: DeployInput): Promise<DeployResult>;
|
|
21
22
|
}
|
|
22
23
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dokploy.d.mts","names":[],"sources":["../../src/deployers/dokploy.ts"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"dokploy.d.mts","names":[],"sources":["../../src/deployers/dokploy.ts"],"mappings":";;;;;;;;;;;;cA0Ga,eAAA,YAA2B,YAAA;EAAA,SAC9B,IAAA;EAAA,SACA,EAAA;EAEH,cAAA,CAAe,MAAA,EAAQ,YAAA,GAAe,OAAA;IAAU,EAAA;IAAa,KAAA;EAAA;EAa7D,WAAA,CAAY,MAAA,EAAQ,YAAA,GAAe,OAAA,CAAQ,UAAA;EAiB3C,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,OAAA,CAAQ,YAAA;AAAA"}
|