@better-openclaw/core 1.0.18 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/package.json +1 -1
- package/src/deployers/coolify.ts +26 -27
- package/src/deployers/dokploy.ts +19 -20
- package/src/port-scanner.ts +1 -1
|
@@ -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":";;AAqCA,SAAS,OAAO,QAAsB,MAAsB;AAE3D,QAAO,GADM,OAAO,YAAY,QAAQ,QAAQ,GAAG,CACpC,SAAS;;;;;;AAOzB,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;AACb,MAAI;GACH,MAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,YAAS,KAAK,WAAW,KAAK,SAAS;UAChC;AAGR,QAAM,IAAI,MAAM,eAAe,IAAI,OAAO,IAAI,SAAS;;CAGxD,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,KAAI,CAAC,KAAM,QAAO;AAClB,QAAO,KAAK,MAAM,KAAK;;;;;AAMxB,SAAS,gBAAgB,YAMrB;CACH,MAAM,SAMA,EAAE;AAER,MAAK,MAAM,QAAQ,WAAW,MAAM,KAAK,EAAE;EAC1C,MAAM,UAAU,KAAK,MAAM;AAC3B,MAAI,CAAC,WAAW,QAAQ,WAAW,IAAI,CAAE;EAEzC,MAAM,QAAQ,QAAQ,QAAQ,IAAI;AAClC,MAAI,SAAS,EAAG;EAEhB,MAAM,MAAM,QAAQ,MAAM,GAAG,MAAM;EACnC,MAAM,QAAQ,QAAQ,MAAM,QAAQ,EAAE;AAEtC,SAAO,KAAK;GACX;GACA;GACA,YAAY;GACZ,eAAe;GACf,YAAY;GACZ,CAAC;;AAGH,QAAO;;;;;;;;;;;;AAaR,IAAa,kBAAb,MAAqD;CACpD,AAAS,OAAO;CAChB,AAAS,KAAK;CAEd,MAAM,eAAe,QAAgE;AACpF,MAAI;AACH,SAAM,aAAsB,QAAQ,WAAW;AAC/C,UAAO,EAAE,IAAI,MAAM;WACX,KAAK;AACb,UAAO;IAAE,IAAI;IAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IAAE;;;CAI/E,MAAM,OAAO,OAA2C;EACvD,MAAM,QAAsB;GAC3B;IAAE,MAAM;IAAmB,QAAQ;IAAW;GAC9C;IAAE,MAAM;IAAkB,QAAQ;IAAW;GAC7C;IAAE,MAAM;IAA0B,QAAQ;IAAW;GACrD;IAAE,MAAM;IAA6B,QAAQ;IAAW;GACxD;IAAE,MAAM;IAAsB,QAAQ;IAAW;GACjD;EAED,MAAM,SAAuB;GAAE,SAAS;GAAO;GAAO;AAEtD,MAAI;AAEH,SAAM,GAAG,SAAS;GAClB,MAAM,UAAU,MAAM,aAA8B,MAAM,QAAQ,WAAW;AAC7E,OAAI,CAAC,WAAW,QAAQ,WAAW,EAClC,OAAM,IAAI,MAAM,uCAAuC;GAExD,MAAM,SAAS,QAAQ;AACvB,SAAM,GAAG,SAAS;AAClB,SAAM,GAAG,SAAS,WAAW,OAAO,KAAK,IAAI,OAAO,GAAG;AAGvD,SAAM,GAAG,SAAS;GAClB,MAAM,UAAU,MAAM,aAA6B,MAAM,QAAQ,aAAa;IAC7E,QAAQ;IACR,MAAM;KACL,MAAM,MAAM;KACZ,aAAa,MAAM,eAAe,mBAAmB,MAAM;KAC3D;IACD,CAAC;AACF,UAAO,YAAY,QAAQ;AAC3B,SAAM,GAAG,SAAS;AAClB,SAAM,GAAG,SAAS,YAAY,QAAQ;GAGtC,MAAM,gBAAgB,MAAM,aAC3B,MAAM,QACN,aAAa,QAAQ,OACrB;GACD,MAAM,UAAU,cAAc,eAAe,IAAI;GACjD,MAAM,UAAU,cAAc,eAAe,IAAI,QAAQ;AACzD,OAAI,CAAC,QACJ,OAAM,IAAI,MAAM,0CAA0C;AAI3D,SAAM,GAAG,SAAS;GAClB,MAAM,UAAU,MAAM,aAA6B,MAAM,QAAQ,aAAa;IAC7E,QAAQ;IACR,MAAM;KACL,cAAc,QAAQ;KACtB,aAAa,OAAO;KACpB,kBAAkB;KAClB,kBAAkB;KAClB,oBAAoB,MAAM;KAC1B,MAAM,MAAM;KACZ,aAAa,MAAM,eAAe;KAClC,gBAAgB;KAChB;IACD,CAAC;AACF,UAAO,YAAY,QAAQ;AAC3B,SAAM,GAAG,SAAS;AAClB,SAAM,GAAG,SAAS,YAAY,QAAQ;AAGtC,SAAM,GAAG,SAAS;GAClB,MAAM,UAAU,gBAAgB,MAAM,WAAW;AACjD,OAAI,QAAQ,SAAS,EACpB,OAAM,aAAa,MAAM,QAAQ,aAAa,QAAQ,KAAK,QAAQ;IAClE,QAAQ;IACR,MAAM;IACN,CAAC;AAEH,SAAM,GAAG,SAAS;AAClB,SAAM,GAAG,SAAS,GAAG,QAAQ,OAAO;AAGpC,SAAM,GAAG,SAAS;GAClB,MAAM,cAAc,MAAM,aACzB,MAAM,QACN,gBAAgB,QAAQ,KAAK,aAC7B;AACD,SAAM,GAAG,SAAS;AAClB,SAAM,GAAG,SAAS,aAAa,cAAc,IAAI,mBAAmB;AAEpE,UAAO,UAAU;AAEjB,UAAO,eAAe,GADT,MAAM,OAAO,YAAY,QAAQ,QAAQ,GAAG,CAC3B,WAAW,QAAQ;WACzC,KAAK;GACb,MAAM,YAAY,MAAM,WAAW,MAAM,EAAE,WAAW,UAAU;AAChE,OAAI,aAAa,GAAG;AACnB,UAAM,WAAW,SAAS;AAC1B,UAAM,WAAW,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;;AAE3E,UAAO,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;;AAGhE,SAAO"}
|
|
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
|
}
|
|
@@ -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
|
|
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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"}
|
|
@@ -53,30 +53,34 @@ var DokployDeployer = class {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
async deploy(input) {
|
|
56
|
+
const step1 = {
|
|
57
|
+
step: "Create project",
|
|
58
|
+
status: "pending"
|
|
59
|
+
};
|
|
60
|
+
const step2 = {
|
|
61
|
+
step: "Create compose stack",
|
|
62
|
+
status: "pending"
|
|
63
|
+
};
|
|
64
|
+
const step3 = {
|
|
65
|
+
step: "Set environment variables",
|
|
66
|
+
status: "pending"
|
|
67
|
+
};
|
|
68
|
+
const step4 = {
|
|
69
|
+
step: "Trigger deployment",
|
|
70
|
+
status: "pending"
|
|
71
|
+
};
|
|
56
72
|
const steps = [
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{
|
|
62
|
-
step: "Create compose stack",
|
|
63
|
-
status: "pending"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
step: "Set environment variables",
|
|
67
|
-
status: "pending"
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
step: "Trigger deployment",
|
|
71
|
-
status: "pending"
|
|
72
|
-
}
|
|
73
|
+
step1,
|
|
74
|
+
step2,
|
|
75
|
+
step3,
|
|
76
|
+
step4
|
|
73
77
|
];
|
|
74
78
|
const result = {
|
|
75
79
|
success: false,
|
|
76
80
|
steps
|
|
77
81
|
};
|
|
78
82
|
try {
|
|
79
|
-
|
|
83
|
+
step1.status = "running";
|
|
80
84
|
const project = await dokployFetch(input.target, "project.create", {
|
|
81
85
|
method: "POST",
|
|
82
86
|
body: {
|
|
@@ -85,11 +89,11 @@ var DokployDeployer = class {
|
|
|
85
89
|
}
|
|
86
90
|
});
|
|
87
91
|
result.projectId = project.projectId;
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
step1.status = "done";
|
|
93
|
+
step1.detail = `Project ID: ${project.projectId}`;
|
|
90
94
|
const envId = (await dokployFetch(input.target, `project.one?projectId=${project.projectId}`)).environments?.[0]?.environmentId;
|
|
91
95
|
if (!envId) throw new Error("No default environment found in project");
|
|
92
|
-
|
|
96
|
+
step2.status = "running";
|
|
93
97
|
const compose = await dokployFetch(input.target, "compose.create", {
|
|
94
98
|
method: "POST",
|
|
95
99
|
body: {
|
|
@@ -99,9 +103,9 @@ var DokployDeployer = class {
|
|
|
99
103
|
}
|
|
100
104
|
});
|
|
101
105
|
result.composeId = compose.composeId;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
step2.status = "done";
|
|
107
|
+
step2.detail = `Compose ID: ${compose.composeId}`;
|
|
108
|
+
step3.status = "running";
|
|
105
109
|
await dokployFetch(input.target, "compose.update", {
|
|
106
110
|
method: "POST",
|
|
107
111
|
body: {
|
|
@@ -109,8 +113,8 @@ var DokployDeployer = class {
|
|
|
109
113
|
env: input.envContent
|
|
110
114
|
}
|
|
111
115
|
});
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
step3.status = "done";
|
|
117
|
+
step4.status = "running";
|
|
114
118
|
await dokployFetch(input.target, "compose.deploy", {
|
|
115
119
|
method: "POST",
|
|
116
120
|
body: {
|
|
@@ -119,14 +123,14 @@ var DokployDeployer = class {
|
|
|
119
123
|
description: input.description ?? "Deployed via OpenClaw web builder"
|
|
120
124
|
}
|
|
121
125
|
});
|
|
122
|
-
|
|
126
|
+
step4.status = "done";
|
|
123
127
|
result.success = true;
|
|
124
128
|
result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/dashboard/project/${project.projectId}`;
|
|
125
129
|
} catch (err) {
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
const failedStep = steps.find((s) => s.status === "running");
|
|
131
|
+
if (failedStep) {
|
|
132
|
+
failedStep.status = "error";
|
|
133
|
+
failedStep.detail = err instanceof Error ? err.message : String(err);
|
|
130
134
|
}
|
|
131
135
|
result.error = err instanceof Error ? err.message : String(err);
|
|
132
136
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dokploy.mjs","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
|
|
1
|
+
{"version":3,"file":"dokploy.mjs","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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"}
|
package/dist/port-scanner.cjs
CHANGED
|
@@ -40,7 +40,7 @@ async function isPortAvailable(port) {
|
|
|
40
40
|
})) return false;
|
|
41
41
|
return await new Promise((resolve) => {
|
|
42
42
|
const server = net.createServer();
|
|
43
|
-
server.once("error", (
|
|
43
|
+
server.once("error", (_err) => {
|
|
44
44
|
resolve(false);
|
|
45
45
|
});
|
|
46
46
|
server.listen(port, "0.0.0.0", () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port-scanner.cjs","names":[],"sources":["../src/port-scanner.ts"],"sourcesContent":["import type { ServiceDefinition } from \"./types.js\";\n\n/**\n * Scans system for ports in use and suggests alternatives for conflicts\n */\nexport interface PortConflict {\n\tport: number;\n\tserviceId: string;\n\tdescription: string;\n\tsuggestedPort: number;\n}\n\n/**\n * Check if a port is available using a two-phase approach:\n * 1. TCP connect — detects if something is already listening\n * 2. Bind check — detects OS-reserved/excluded port ranges (e.g. Hyper-V on Windows)\n *\n * The connect-only approach misses Windows excluded port ranges because\n * ECONNREFUSED (nothing listening) looks the same as \"available\" even though\n * Docker's bind() will fail on those reserved ranges.\n */\nasync function isPortAvailable(port: number): Promise<boolean> {\n\ttry {\n\t\tconst net = await import(\"node:net\");\n\n\t\t// Phase 1: TCP connect check — is something actively listening?\n\t\tconst isListening = await new Promise<boolean>((resolve) => {\n\t\t\tconst socket = new net.Socket();\n\t\t\tlet resolved = false;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tsocket.removeAllListeners();\n\t\t\t\t\tsocket.destroy();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tsocket.once(\"connect\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(true);\n\t\t\t});\n\n\t\t\tsocket.once(\"error\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tsocket.setTimeout(500);\n\t\t\tsocket.once(\"timeout\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tsocket.connect(port, \"127.0.0.1\");\n\t\t});\n\n\t\tif (isListening) return false; // Port is in use\n\n\t\t// Phase 2: Bind check — catches OS-reserved/excluded port ranges\n\t\t// On Windows, Hyper-V/WSL reserve dynamic port ranges that TCP connect\n\t\t// cannot detect. Attempting to bind reveals these reservations.\n\t\tconst canBind = await new Promise<boolean>((resolve) => {\n\t\t\tconst server = net.createServer();\n\n\t\t\tserver.once(\"error\", (
|
|
1
|
+
{"version":3,"file":"port-scanner.cjs","names":[],"sources":["../src/port-scanner.ts"],"sourcesContent":["import type { ServiceDefinition } from \"./types.js\";\n\n/**\n * Scans system for ports in use and suggests alternatives for conflicts\n */\nexport interface PortConflict {\n\tport: number;\n\tserviceId: string;\n\tdescription: string;\n\tsuggestedPort: number;\n}\n\n/**\n * Check if a port is available using a two-phase approach:\n * 1. TCP connect — detects if something is already listening\n * 2. Bind check — detects OS-reserved/excluded port ranges (e.g. Hyper-V on Windows)\n *\n * The connect-only approach misses Windows excluded port ranges because\n * ECONNREFUSED (nothing listening) looks the same as \"available\" even though\n * Docker's bind() will fail on those reserved ranges.\n */\nasync function isPortAvailable(port: number): Promise<boolean> {\n\ttry {\n\t\tconst net = await import(\"node:net\");\n\n\t\t// Phase 1: TCP connect check — is something actively listening?\n\t\tconst isListening = await new Promise<boolean>((resolve) => {\n\t\t\tconst socket = new net.Socket();\n\t\t\tlet resolved = false;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tsocket.removeAllListeners();\n\t\t\t\t\tsocket.destroy();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tsocket.once(\"connect\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(true);\n\t\t\t});\n\n\t\t\tsocket.once(\"error\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tsocket.setTimeout(500);\n\t\t\tsocket.once(\"timeout\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tsocket.connect(port, \"127.0.0.1\");\n\t\t});\n\n\t\tif (isListening) return false; // Port is in use\n\n\t\t// Phase 2: Bind check — catches OS-reserved/excluded port ranges\n\t\t// On Windows, Hyper-V/WSL reserve dynamic port ranges that TCP connect\n\t\t// cannot detect. Attempting to bind reveals these reservations.\n\t\tconst canBind = await new Promise<boolean>((resolve) => {\n\t\t\tconst server = net.createServer();\n\n\t\t\tserver.once(\"error\", (_err: NodeJS.ErrnoException) => {\n\t\t\t\t// EACCES / EADDRINUSE / EPERM = port is reserved or in use\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tserver.listen(port, \"0.0.0.0\", () => {\n\t\t\t\t// Successfully bound — port is truly available\n\t\t\t\tserver.close(() => resolve(true));\n\t\t\t});\n\t\t});\n\n\t\treturn canBind;\n\t} catch (error) {\n\t\tconsole.warn(`Port scanner error for port ${port}:`, error);\n\t\treturn true; // Failsafe: assume available\n\t}\n}\n\n/**\n * Find next available port starting from a base port.\n * Also skips ports already claimed by other services in this generation.\n */\nasync function findNextAvailablePort(\n\tstartPort: number,\n\tclaimedPorts: Set<number>,\n\tmaxAttempts = 100,\n): Promise<number> {\n\tfor (let i = 0; i < maxAttempts; i++) {\n\t\tconst port = startPort + i;\n\t\tif (port > 65535) break;\n\t\tif (claimedPorts.has(port)) continue;\n\t\tif (await isPortAvailable(port)) {\n\t\t\treturn port;\n\t\t}\n\t}\n\t// Fallback: return a high random port\n\treturn 50000 + Math.floor(Math.random() * 10000);\n}\n\n/**\n * Scan for port conflicts and suggest alternatives.\n *\n * Detects two types of conflicts:\n * 1. Host conflicts — port is already in use or reserved by the OS\n * 2. Inter-service conflicts — multiple selected services claim the same host port\n */\nexport async function scanPortConflicts(\n\tservices: ServiceDefinition[],\n): Promise<Map<string, Map<number, number>>> {\n\tconst portReassignments = new Map<string, Map<number, number>>();\n\n\t// Track which host ports are already claimed by services in this stack.\n\t// First service to claim a port wins; subsequent services get reassigned.\n\tconst claimedPorts = new Map<number, string>(); // port → serviceId that claimed it\n\n\tfor (const service of services) {\n\t\tif (!service.ports || service.ports.length === 0) continue;\n\n\t\tconst serviceReassignments = new Map<number, number>();\n\n\t\tfor (const portDef of service.ports) {\n\t\t\tif (!portDef.exposed) continue;\n\n\t\t\tconst port = portDef.host;\n\n\t\t\t// Check inter-service conflict first\n\t\t\tconst claimedBy = claimedPorts.get(port);\n\t\t\tif (claimedBy && claimedBy !== service.id) {\n\t\t\t\t// Another service already claimed this port — must reassign\n\t\t\t\tconst newPort = await findNextAvailablePort(port + 1000, new Set(claimedPorts.keys()));\n\t\t\t\tserviceReassignments.set(port, newPort);\n\t\t\t\tclaimedPorts.set(newPort, service.id);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Check host availability (TCP connect + bind check)\n\t\t\tconst isAvailable = await isPortAvailable(port);\n\n\t\t\tif (!isAvailable) {\n\t\t\t\tconst newPort = await findNextAvailablePort(port + 1000, new Set(claimedPorts.keys()));\n\t\t\t\tserviceReassignments.set(port, newPort);\n\t\t\t\tclaimedPorts.set(newPort, service.id);\n\t\t\t} else {\n\t\t\t\t// Port is available and not claimed — claim it\n\t\t\t\tclaimedPorts.set(port, service.id);\n\t\t\t}\n\t\t}\n\n\t\tif (serviceReassignments.size > 0) {\n\t\t\tportReassignments.set(service.id, serviceReassignments);\n\t\t}\n\t}\n\n\treturn portReassignments;\n}\n\n/**\n * Get conflicts in a user-friendly format\n */\nexport function formatPortConflicts(\n\tservices: ServiceDefinition[],\n\treassignments: Map<string, Map<number, number>>,\n): PortConflict[] {\n\tconst conflicts: PortConflict[] = [];\n\n\tfor (const service of services) {\n\t\tconst serviceReassignments = reassignments.get(service.id);\n\t\tif (!serviceReassignments) continue;\n\n\t\tfor (const portDef of service.ports || []) {\n\t\t\tconst newPort = serviceReassignments.get(portDef.host);\n\t\t\tif (newPort) {\n\t\t\t\tconflicts.push({\n\t\t\t\t\tport: portDef.host,\n\t\t\t\t\tserviceId: service.id,\n\t\t\t\t\tdescription: portDef.description || `${service.name} port`,\n\t\t\t\t\tsuggestedPort: newPort,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn conflicts;\n}\n"],"mappings":"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"}
|
package/dist/port-scanner.mjs
CHANGED
|
@@ -38,7 +38,7 @@ async function isPortAvailable(port) {
|
|
|
38
38
|
})) return false;
|
|
39
39
|
return await new Promise((resolve) => {
|
|
40
40
|
const server = net.createServer();
|
|
41
|
-
server.once("error", (
|
|
41
|
+
server.once("error", (_err) => {
|
|
42
42
|
resolve(false);
|
|
43
43
|
});
|
|
44
44
|
server.listen(port, "0.0.0.0", () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port-scanner.mjs","names":[],"sources":["../src/port-scanner.ts"],"sourcesContent":["import type { ServiceDefinition } from \"./types.js\";\n\n/**\n * Scans system for ports in use and suggests alternatives for conflicts\n */\nexport interface PortConflict {\n\tport: number;\n\tserviceId: string;\n\tdescription: string;\n\tsuggestedPort: number;\n}\n\n/**\n * Check if a port is available using a two-phase approach:\n * 1. TCP connect — detects if something is already listening\n * 2. Bind check — detects OS-reserved/excluded port ranges (e.g. Hyper-V on Windows)\n *\n * The connect-only approach misses Windows excluded port ranges because\n * ECONNREFUSED (nothing listening) looks the same as \"available\" even though\n * Docker's bind() will fail on those reserved ranges.\n */\nasync function isPortAvailable(port: number): Promise<boolean> {\n\ttry {\n\t\tconst net = await import(\"node:net\");\n\n\t\t// Phase 1: TCP connect check — is something actively listening?\n\t\tconst isListening = await new Promise<boolean>((resolve) => {\n\t\t\tconst socket = new net.Socket();\n\t\t\tlet resolved = false;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tsocket.removeAllListeners();\n\t\t\t\t\tsocket.destroy();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tsocket.once(\"connect\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(true);\n\t\t\t});\n\n\t\t\tsocket.once(\"error\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tsocket.setTimeout(500);\n\t\t\tsocket.once(\"timeout\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tsocket.connect(port, \"127.0.0.1\");\n\t\t});\n\n\t\tif (isListening) return false; // Port is in use\n\n\t\t// Phase 2: Bind check — catches OS-reserved/excluded port ranges\n\t\t// On Windows, Hyper-V/WSL reserve dynamic port ranges that TCP connect\n\t\t// cannot detect. Attempting to bind reveals these reservations.\n\t\tconst canBind = await new Promise<boolean>((resolve) => {\n\t\t\tconst server = net.createServer();\n\n\t\t\tserver.once(\"error\", (
|
|
1
|
+
{"version":3,"file":"port-scanner.mjs","names":[],"sources":["../src/port-scanner.ts"],"sourcesContent":["import type { ServiceDefinition } from \"./types.js\";\n\n/**\n * Scans system for ports in use and suggests alternatives for conflicts\n */\nexport interface PortConflict {\n\tport: number;\n\tserviceId: string;\n\tdescription: string;\n\tsuggestedPort: number;\n}\n\n/**\n * Check if a port is available using a two-phase approach:\n * 1. TCP connect — detects if something is already listening\n * 2. Bind check — detects OS-reserved/excluded port ranges (e.g. Hyper-V on Windows)\n *\n * The connect-only approach misses Windows excluded port ranges because\n * ECONNREFUSED (nothing listening) looks the same as \"available\" even though\n * Docker's bind() will fail on those reserved ranges.\n */\nasync function isPortAvailable(port: number): Promise<boolean> {\n\ttry {\n\t\tconst net = await import(\"node:net\");\n\n\t\t// Phase 1: TCP connect check — is something actively listening?\n\t\tconst isListening = await new Promise<boolean>((resolve) => {\n\t\t\tconst socket = new net.Socket();\n\t\t\tlet resolved = false;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tsocket.removeAllListeners();\n\t\t\t\t\tsocket.destroy();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tsocket.once(\"connect\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(true);\n\t\t\t});\n\n\t\t\tsocket.once(\"error\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tsocket.setTimeout(500);\n\t\t\tsocket.once(\"timeout\", () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tsocket.connect(port, \"127.0.0.1\");\n\t\t});\n\n\t\tif (isListening) return false; // Port is in use\n\n\t\t// Phase 2: Bind check — catches OS-reserved/excluded port ranges\n\t\t// On Windows, Hyper-V/WSL reserve dynamic port ranges that TCP connect\n\t\t// cannot detect. Attempting to bind reveals these reservations.\n\t\tconst canBind = await new Promise<boolean>((resolve) => {\n\t\t\tconst server = net.createServer();\n\n\t\t\tserver.once(\"error\", (_err: NodeJS.ErrnoException) => {\n\t\t\t\t// EACCES / EADDRINUSE / EPERM = port is reserved or in use\n\t\t\t\tresolve(false);\n\t\t\t});\n\n\t\t\tserver.listen(port, \"0.0.0.0\", () => {\n\t\t\t\t// Successfully bound — port is truly available\n\t\t\t\tserver.close(() => resolve(true));\n\t\t\t});\n\t\t});\n\n\t\treturn canBind;\n\t} catch (error) {\n\t\tconsole.warn(`Port scanner error for port ${port}:`, error);\n\t\treturn true; // Failsafe: assume available\n\t}\n}\n\n/**\n * Find next available port starting from a base port.\n * Also skips ports already claimed by other services in this generation.\n */\nasync function findNextAvailablePort(\n\tstartPort: number,\n\tclaimedPorts: Set<number>,\n\tmaxAttempts = 100,\n): Promise<number> {\n\tfor (let i = 0; i < maxAttempts; i++) {\n\t\tconst port = startPort + i;\n\t\tif (port > 65535) break;\n\t\tif (claimedPorts.has(port)) continue;\n\t\tif (await isPortAvailable(port)) {\n\t\t\treturn port;\n\t\t}\n\t}\n\t// Fallback: return a high random port\n\treturn 50000 + Math.floor(Math.random() * 10000);\n}\n\n/**\n * Scan for port conflicts and suggest alternatives.\n *\n * Detects two types of conflicts:\n * 1. Host conflicts — port is already in use or reserved by the OS\n * 2. Inter-service conflicts — multiple selected services claim the same host port\n */\nexport async function scanPortConflicts(\n\tservices: ServiceDefinition[],\n): Promise<Map<string, Map<number, number>>> {\n\tconst portReassignments = new Map<string, Map<number, number>>();\n\n\t// Track which host ports are already claimed by services in this stack.\n\t// First service to claim a port wins; subsequent services get reassigned.\n\tconst claimedPorts = new Map<number, string>(); // port → serviceId that claimed it\n\n\tfor (const service of services) {\n\t\tif (!service.ports || service.ports.length === 0) continue;\n\n\t\tconst serviceReassignments = new Map<number, number>();\n\n\t\tfor (const portDef of service.ports) {\n\t\t\tif (!portDef.exposed) continue;\n\n\t\t\tconst port = portDef.host;\n\n\t\t\t// Check inter-service conflict first\n\t\t\tconst claimedBy = claimedPorts.get(port);\n\t\t\tif (claimedBy && claimedBy !== service.id) {\n\t\t\t\t// Another service already claimed this port — must reassign\n\t\t\t\tconst newPort = await findNextAvailablePort(port + 1000, new Set(claimedPorts.keys()));\n\t\t\t\tserviceReassignments.set(port, newPort);\n\t\t\t\tclaimedPorts.set(newPort, service.id);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Check host availability (TCP connect + bind check)\n\t\t\tconst isAvailable = await isPortAvailable(port);\n\n\t\t\tif (!isAvailable) {\n\t\t\t\tconst newPort = await findNextAvailablePort(port + 1000, new Set(claimedPorts.keys()));\n\t\t\t\tserviceReassignments.set(port, newPort);\n\t\t\t\tclaimedPorts.set(newPort, service.id);\n\t\t\t} else {\n\t\t\t\t// Port is available and not claimed — claim it\n\t\t\t\tclaimedPorts.set(port, service.id);\n\t\t\t}\n\t\t}\n\n\t\tif (serviceReassignments.size > 0) {\n\t\t\tportReassignments.set(service.id, serviceReassignments);\n\t\t}\n\t}\n\n\treturn portReassignments;\n}\n\n/**\n * Get conflicts in a user-friendly format\n */\nexport function formatPortConflicts(\n\tservices: ServiceDefinition[],\n\treassignments: Map<string, Map<number, number>>,\n): PortConflict[] {\n\tconst conflicts: PortConflict[] = [];\n\n\tfor (const service of services) {\n\t\tconst serviceReassignments = reassignments.get(service.id);\n\t\tif (!serviceReassignments) continue;\n\n\t\tfor (const portDef of service.ports || []) {\n\t\t\tconst newPort = serviceReassignments.get(portDef.host);\n\t\t\tif (newPort) {\n\t\t\t\tconflicts.push({\n\t\t\t\t\tport: portDef.host,\n\t\t\t\t\tserviceId: service.id,\n\t\t\t\t\tdescription: portDef.description || `${service.name} port`,\n\t\t\t\t\tsuggestedPort: newPort,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn conflicts;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-openclaw/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Core logic for better-openclaw: schemas, service registry, resolver, composer, validators and generators",
|
|
6
6
|
"author": "bidew.io <bachir@bidew.io>",
|
package/src/deployers/coolify.ts
CHANGED
|
@@ -139,29 +139,28 @@ export class CoolifyDeployer implements PaasDeployer {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
async deploy(input: DeployInput): Promise<DeployResult> {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
];
|
|
142
|
+
const step1: DeployStep = { step: "Discover server", status: "pending" };
|
|
143
|
+
const step2: DeployStep = { step: "Create project", status: "pending" };
|
|
144
|
+
const step3: DeployStep = { step: "Create compose service", status: "pending" };
|
|
145
|
+
const step4: DeployStep = { step: "Set environment variables", status: "pending" };
|
|
146
|
+
const step5: DeployStep = { step: "Trigger deployment", status: "pending" };
|
|
147
|
+
const steps: DeployStep[] = [step1, step2, step3, step4, step5];
|
|
149
148
|
|
|
150
149
|
const result: DeployResult = { success: false, steps };
|
|
151
150
|
|
|
152
151
|
try {
|
|
153
152
|
// Step 1: Discover default server
|
|
154
|
-
|
|
153
|
+
step1.status = "running";
|
|
155
154
|
const servers = await coolifyFetch<CoolifyServer[]>(input.target, "/servers");
|
|
156
155
|
if (!servers || servers.length === 0) {
|
|
157
156
|
throw new Error("No servers found in Coolify instance");
|
|
158
157
|
}
|
|
159
|
-
const server = servers[0];
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
const server = servers[0] as CoolifyServer;
|
|
159
|
+
step1.status = "done";
|
|
160
|
+
step1.detail = `Server: ${server.name} (${server.ip})`;
|
|
162
161
|
|
|
163
162
|
// Step 2: Create project
|
|
164
|
-
|
|
163
|
+
step2.status = "running";
|
|
165
164
|
const project = await coolifyFetch<CoolifyProject>(input.target, "/projects", {
|
|
166
165
|
method: "POST",
|
|
167
166
|
body: {
|
|
@@ -170,8 +169,8 @@ export class CoolifyDeployer implements PaasDeployer {
|
|
|
170
169
|
},
|
|
171
170
|
});
|
|
172
171
|
result.projectId = project.uuid;
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
step2.status = "done";
|
|
173
|
+
step2.detail = `Project: ${project.uuid}`;
|
|
175
174
|
|
|
176
175
|
// Get the default environment
|
|
177
176
|
const projectDetail = await coolifyFetch<CoolifyProject>(
|
|
@@ -185,7 +184,7 @@ export class CoolifyDeployer implements PaasDeployer {
|
|
|
185
184
|
}
|
|
186
185
|
|
|
187
186
|
// Step 3: Create compose service with docker_compose_raw
|
|
188
|
-
|
|
187
|
+
step3.status = "running";
|
|
189
188
|
const service = await coolifyFetch<CoolifyService>(input.target, "/services", {
|
|
190
189
|
method: "POST",
|
|
191
190
|
body: {
|
|
@@ -200,11 +199,11 @@ export class CoolifyDeployer implements PaasDeployer {
|
|
|
200
199
|
},
|
|
201
200
|
});
|
|
202
201
|
result.composeId = service.uuid;
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
step3.status = "done";
|
|
203
|
+
step3.detail = `Service: ${service.uuid}`;
|
|
205
204
|
|
|
206
205
|
// Step 4: Set environment variables
|
|
207
|
-
|
|
206
|
+
step4.status = "running";
|
|
208
207
|
const envVars = parseEnvContent(input.envContent);
|
|
209
208
|
if (envVars.length > 0) {
|
|
210
209
|
await coolifyFetch(input.target, `/services/${service.uuid}/envs`, {
|
|
@@ -212,26 +211,26 @@ export class CoolifyDeployer implements PaasDeployer {
|
|
|
212
211
|
body: envVars,
|
|
213
212
|
});
|
|
214
213
|
}
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
step4.status = "done";
|
|
215
|
+
step4.detail = `${envVars.length} variables set`;
|
|
217
216
|
|
|
218
217
|
// Step 5: Trigger deployment
|
|
219
|
-
|
|
218
|
+
step5.status = "running";
|
|
220
219
|
const deployments = await coolifyFetch<{ deployments: CoolifyDeployment[] }>(
|
|
221
220
|
input.target,
|
|
222
221
|
`/deploy?uuid=${service.uuid}&force=true`,
|
|
223
222
|
);
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
step5.status = "done";
|
|
224
|
+
step5.detail = deployments?.deployments?.[0]?.deployment_uuid ?? "Deployment triggered";
|
|
226
225
|
|
|
227
226
|
result.success = true;
|
|
228
227
|
const base = input.target.instanceUrl.replace(/\/+$/, "");
|
|
229
228
|
result.dashboardUrl = `${base}/project/${project.uuid}`;
|
|
230
229
|
} catch (err) {
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
const failedStep = steps.find((s) => s.status === "running");
|
|
231
|
+
if (failedStep) {
|
|
232
|
+
failedStep.status = "error";
|
|
233
|
+
failedStep.detail = err instanceof Error ? err.message : String(err);
|
|
235
234
|
}
|
|
236
235
|
result.error = err instanceof Error ? err.message : String(err);
|
|
237
236
|
}
|
package/src/deployers/dokploy.ts
CHANGED
|
@@ -87,18 +87,17 @@ export class DokployDeployer implements PaasDeployer {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async deploy(input: DeployInput): Promise<DeployResult> {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
];
|
|
90
|
+
const step1: DeployStep = { step: "Create project", status: "pending" };
|
|
91
|
+
const step2: DeployStep = { step: "Create compose stack", status: "pending" };
|
|
92
|
+
const step3: DeployStep = { step: "Set environment variables", status: "pending" };
|
|
93
|
+
const step4: DeployStep = { step: "Trigger deployment", status: "pending" };
|
|
94
|
+
const steps: DeployStep[] = [step1, step2, step3, step4];
|
|
96
95
|
|
|
97
96
|
const result: DeployResult = { success: false, steps };
|
|
98
97
|
|
|
99
98
|
try {
|
|
100
99
|
// Step 1: Create project
|
|
101
|
-
|
|
100
|
+
step1.status = "running";
|
|
102
101
|
const project = await dokployFetch<DokployProject>(input.target, "project.create", {
|
|
103
102
|
method: "POST",
|
|
104
103
|
body: {
|
|
@@ -107,8 +106,8 @@ export class DokployDeployer implements PaasDeployer {
|
|
|
107
106
|
},
|
|
108
107
|
});
|
|
109
108
|
result.projectId = project.projectId;
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
step1.status = "done";
|
|
110
|
+
step1.detail = `Project ID: ${project.projectId}`;
|
|
112
111
|
|
|
113
112
|
// Get the default environment ID
|
|
114
113
|
const projectDetail = await dokployFetch<DokployProject>(
|
|
@@ -121,7 +120,7 @@ export class DokployDeployer implements PaasDeployer {
|
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
// Step 2: Create compose stack
|
|
124
|
-
|
|
123
|
+
step2.status = "running";
|
|
125
124
|
const compose = await dokployFetch<DokployCompose>(input.target, "compose.create", {
|
|
126
125
|
method: "POST",
|
|
127
126
|
body: {
|
|
@@ -131,11 +130,11 @@ export class DokployDeployer implements PaasDeployer {
|
|
|
131
130
|
},
|
|
132
131
|
});
|
|
133
132
|
result.composeId = compose.composeId;
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
step2.status = "done";
|
|
134
|
+
step2.detail = `Compose ID: ${compose.composeId}`;
|
|
136
135
|
|
|
137
136
|
// Step 3: Set environment variables
|
|
138
|
-
|
|
137
|
+
step3.status = "running";
|
|
139
138
|
await dokployFetch(input.target, "compose.update", {
|
|
140
139
|
method: "POST",
|
|
141
140
|
body: {
|
|
@@ -143,10 +142,10 @@ export class DokployDeployer implements PaasDeployer {
|
|
|
143
142
|
env: input.envContent,
|
|
144
143
|
},
|
|
145
144
|
});
|
|
146
|
-
|
|
145
|
+
step3.status = "done";
|
|
147
146
|
|
|
148
147
|
// Step 4: Trigger deployment
|
|
149
|
-
|
|
148
|
+
step4.status = "running";
|
|
150
149
|
await dokployFetch(input.target, "compose.deploy", {
|
|
151
150
|
method: "POST",
|
|
152
151
|
body: {
|
|
@@ -155,16 +154,16 @@ export class DokployDeployer implements PaasDeployer {
|
|
|
155
154
|
description: input.description ?? "Deployed via OpenClaw web builder",
|
|
156
155
|
},
|
|
157
156
|
});
|
|
158
|
-
|
|
157
|
+
step4.status = "done";
|
|
159
158
|
|
|
160
159
|
result.success = true;
|
|
161
160
|
const base = input.target.instanceUrl.replace(/\/+$/, "");
|
|
162
161
|
result.dashboardUrl = `${base}/dashboard/project/${project.projectId}`;
|
|
163
162
|
} catch (err) {
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
const failedStep = steps.find((s) => s.status === "running");
|
|
164
|
+
if (failedStep) {
|
|
165
|
+
failedStep.status = "error";
|
|
166
|
+
failedStep.detail = err instanceof Error ? err.message : String(err);
|
|
168
167
|
}
|
|
169
168
|
result.error = err instanceof Error ? err.message : String(err);
|
|
170
169
|
}
|
package/src/port-scanner.ts
CHANGED
|
@@ -63,7 +63,7 @@ async function isPortAvailable(port: number): Promise<boolean> {
|
|
|
63
63
|
const canBind = await new Promise<boolean>((resolve) => {
|
|
64
64
|
const server = net.createServer();
|
|
65
65
|
|
|
66
|
-
server.once("error", (
|
|
66
|
+
server.once("error", (_err: NodeJS.ErrnoException) => {
|
|
67
67
|
// EACCES / EADDRINUSE / EPERM = port is reserved or in use
|
|
68
68
|
resolve(false);
|
|
69
69
|
});
|