@better-openclaw/core 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.github/workflows/publish-core.yml +1 -1
  2. package/dist/deployers/coolify.cjs +61 -50
  3. package/dist/deployers/coolify.cjs.map +1 -1
  4. package/dist/deployers/coolify.d.cts +4 -11
  5. package/dist/deployers/coolify.d.cts.map +1 -1
  6. package/dist/deployers/coolify.d.mts +4 -11
  7. package/dist/deployers/coolify.d.mts.map +1 -1
  8. package/dist/deployers/coolify.mjs +62 -50
  9. package/dist/deployers/coolify.mjs.map +1 -1
  10. package/dist/deployers/dokploy.cjs +106 -29
  11. package/dist/deployers/dokploy.cjs.map +1 -1
  12. package/dist/deployers/dokploy.d.cts +2 -1
  13. package/dist/deployers/dokploy.d.cts.map +1 -1
  14. package/dist/deployers/dokploy.d.mts +2 -1
  15. package/dist/deployers/dokploy.d.mts.map +1 -1
  16. package/dist/deployers/dokploy.mjs +107 -29
  17. package/dist/deployers/dokploy.mjs.map +1 -1
  18. package/dist/deployers/index.cjs.map +1 -1
  19. package/dist/deployers/index.d.cts +2 -2
  20. package/dist/deployers/index.d.cts.map +1 -1
  21. package/dist/deployers/index.d.mts +2 -2
  22. package/dist/deployers/index.d.mts.map +1 -1
  23. package/dist/deployers/index.mjs.map +1 -1
  24. package/dist/deployers/strip-host-ports.cjs +138 -0
  25. package/dist/deployers/strip-host-ports.cjs.map +1 -0
  26. package/dist/deployers/strip-host-ports.d.cts +62 -0
  27. package/dist/deployers/strip-host-ports.d.cts.map +1 -0
  28. package/dist/deployers/strip-host-ports.d.mts +62 -0
  29. package/dist/deployers/strip-host-ports.d.mts.map +1 -0
  30. package/dist/deployers/strip-host-ports.mjs +133 -0
  31. package/dist/deployers/strip-host-ports.mjs.map +1 -0
  32. package/dist/deployers/strip-host-ports.test.cjs +89 -0
  33. package/dist/deployers/strip-host-ports.test.cjs.map +1 -0
  34. package/dist/deployers/strip-host-ports.test.d.cts +1 -0
  35. package/dist/deployers/strip-host-ports.test.d.mts +1 -0
  36. package/dist/deployers/strip-host-ports.test.mjs +90 -0
  37. package/dist/deployers/strip-host-ports.test.mjs.map +1 -0
  38. package/dist/deployers/types.d.cts +173 -2
  39. package/dist/deployers/types.d.cts.map +1 -1
  40. package/dist/deployers/types.d.mts +173 -2
  41. package/dist/deployers/types.d.mts.map +1 -1
  42. package/dist/index.d.cts +2 -2
  43. package/dist/index.d.mts +2 -2
  44. package/package.json +2 -1
  45. package/src/deployers/coolify.ts +198 -103
  46. package/src/deployers/dokploy.ts +209 -55
  47. package/src/deployers/index.ts +1 -0
  48. package/src/deployers/strip-host-ports.test.ts +100 -0
  49. package/src/deployers/strip-host-ports.ts +187 -0
  50. package/src/deployers/types.ts +185 -1
  51. package/src/index.ts +19 -4
  52. package/tsconfig.tsbuildinfo +1 -0
@@ -1,6 +1,14 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_deployers_strip_host_ports = require('./strip-host-ports.cjs');
2
3
 
3
4
  //#region src/deployers/dokploy.ts
5
+ /**
6
+ * Dokploy PaaS deployer — deploys Docker Compose stacks via the Dokploy REST API.
7
+ *
8
+ * API docs: https://docs.dokploy.com/docs/api
9
+ * Auth: x-api-key header
10
+ * Endpoints use dot-notation (e.g. /api/project.create, /api/compose.deploy)
11
+ */
4
12
  /** Build a full Dokploy API URL from a dot-notation endpoint (e.g. "project.create"). */
5
13
  function apiUrl(target, endpoint) {
6
14
  return `${target.instanceUrl.replace(/\/+$/, "")}/api/${endpoint}`;
@@ -32,6 +40,17 @@ async function dokployFetch(target, endpoint, options = {}) {
32
40
  return JSON.parse(text);
33
41
  }
34
42
  /**
43
+ * Simple hash for compose diff detection
44
+ */
45
+ function hashString(str) {
46
+ let hash = 0;
47
+ for (let i = 0; i < str.length; i++) {
48
+ hash = (hash << 5) - hash + str.charCodeAt(i);
49
+ hash |= 0;
50
+ }
51
+ return hash;
52
+ }
53
+ /**
35
54
  * Deploys Docker Compose stacks to a Dokploy instance.
36
55
  *
37
56
  * Deploy flow (4 steps):
@@ -54,89 +73,147 @@ var DokployDeployer = class {
54
73
  };
55
74
  }
56
75
  }
76
+ async listServers(target) {
77
+ try {
78
+ return (await dokployFetch(target, "server.all")).map((s) => ({
79
+ id: s.serverId,
80
+ name: s.name,
81
+ ip: s.ipAddress
82
+ }));
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
57
87
  async deploy(input) {
58
88
  const step1 = {
59
- step: "Create project",
89
+ step: "Find or create project",
60
90
  status: "pending"
61
91
  };
62
92
  const step2 = {
63
- step: "Create compose stack",
93
+ step: "Find default environment",
64
94
  status: "pending"
65
95
  };
66
96
  const step3 = {
67
- step: "Set environment variables",
97
+ step: "Find or create compose stack",
68
98
  status: "pending"
69
99
  };
70
100
  const step4 = {
71
- step: "Trigger deployment",
101
+ step: "Update stack configuration",
102
+ status: "pending"
103
+ };
104
+ const step5 = {
105
+ step: "Deploy stack",
72
106
  status: "pending"
73
107
  };
74
108
  const steps = [
75
109
  step1,
76
110
  step2,
77
111
  step3,
78
- step4
112
+ step4,
113
+ step5
79
114
  ];
80
115
  const result = {
81
116
  success: false,
82
117
  steps
83
118
  };
119
+ const composeYaml = require_deployers_strip_host_ports.sanitizeComposeForPaas(input.composeYaml);
84
120
  try {
121
+ /**
122
+ * STEP 1
123
+ * Find or create project
124
+ */
85
125
  step1.status = "running";
86
- const project = await dokployFetch(input.target, "project.create", {
126
+ let project = (await dokployFetch(input.target, "project.all")).find((p) => p.name === input.projectName);
127
+ if (!project) project = (await dokployFetch(input.target, "project.create", {
87
128
  method: "POST",
88
129
  body: {
89
130
  name: input.projectName,
90
131
  description: input.description ?? `OpenClaw stack: ${input.projectName}`
91
132
  }
92
- });
133
+ })).project;
93
134
  result.projectId = project.projectId;
94
135
  step1.status = "done";
95
136
  step1.detail = `Project ID: ${project.projectId}`;
96
- const envId = (await dokployFetch(input.target, `project.one?projectId=${project.projectId}`)).environments?.[0]?.environmentId;
97
- if (!envId) throw new Error("No default environment found in project");
137
+ /**
138
+ * STEP 2
139
+ * Find default environment
140
+ */
98
141
  step2.status = "running";
99
- const compose = await dokployFetch(input.target, "compose.create", {
142
+ const env = (await dokployFetch(input.target, `project.one?projectId=${project.projectId}`)).environments?.find((e) => e.isDefault);
143
+ if (!env) throw new Error("No default environment");
144
+ step2.status = "done";
145
+ step2.detail = env.environmentId;
146
+ /**
147
+ * STEP 3
148
+ * Find or create compose stack
149
+ */
150
+ step3.status = "running";
151
+ let stack = null;
152
+ stack = await dokployFetch(input.target, "compose.create", {
100
153
  method: "POST",
101
154
  body: {
102
155
  name: input.projectName,
103
- environmentId: envId,
104
- composeFile: input.composeYaml
156
+ description: input.description ?? `Stack ${input.projectName}`,
157
+ environmentId: env.environmentId,
158
+ composeType: "docker-compose",
159
+ composeFile: composeYaml,
160
+ ...input.serverId ? { serverId: input.serverId } : {}
105
161
  }
106
162
  });
107
- result.composeId = compose.composeId;
108
- step2.status = "done";
109
- step2.detail = `Compose ID: ${compose.composeId}`;
110
- step3.status = "running";
111
- await dokployFetch(input.target, "compose.update", {
163
+ if (stack?.composeId) await dokployFetch(input.target, "compose.update", {
112
164
  method: "POST",
113
165
  body: {
114
- composeId: compose.composeId,
115
- env: input.envContent
166
+ composeId: stack.composeId,
167
+ sourceType: "raw"
116
168
  }
117
169
  });
170
+ result.composeId = stack?.composeId;
118
171
  step3.status = "done";
172
+ step3.detail = stack?.composeId;
173
+ /**
174
+ * STEP 4
175
+ * Update stack if compose changed
176
+ */
119
177
  step4.status = "running";
178
+ const existingStack = await dokployFetch(input.target, `compose.one?composeId=${stack?.composeId}`);
179
+ if (hashString(composeYaml) !== hashString(existingStack.compose ?? "")) {
180
+ await dokployFetch(input.target, "compose.update", {
181
+ method: "POST",
182
+ body: {
183
+ composeId: stack?.composeId,
184
+ composeFile: composeYaml,
185
+ env: input.envContent ?? ""
186
+ }
187
+ });
188
+ step4.detail = "Stack updated";
189
+ } else step4.detail = "No compose changes";
190
+ step4.status = "done";
191
+ /**
192
+ * STEP 5
193
+ * Deploy
194
+ */
195
+ step5.status = "running";
120
196
  await dokployFetch(input.target, "compose.deploy", {
121
197
  method: "POST",
122
198
  body: {
123
- composeId: compose.composeId,
124
- title: `Initial deploy: ${input.projectName}`,
125
- description: input.description ?? "Deployed via OpenClaw web builder"
199
+ composeId: stack?.composeId,
200
+ title: `Deploy ${input.projectName}`,
201
+ description: "CI deployment"
126
202
  }
127
203
  });
128
- step4.status = "done";
204
+ step5.status = "done";
129
205
  result.success = true;
130
- result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/dashboard/project/${project.projectId}`;
206
+ result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/dashboard/project/${project.projectId}/environment/${env.environmentId}/services/compose/${stack?.composeId}?tab=deployments`;
207
+ return result;
131
208
  } catch (err) {
132
- const failedStep = steps.find((s) => s.status === "running");
133
- if (failedStep) {
134
- failedStep.status = "error";
135
- failedStep.detail = err instanceof Error ? err.message : String(err);
209
+ const running = steps.find((s) => s.status === "running");
210
+ if (running) {
211
+ running.status = "error";
212
+ running.detail = err instanceof Error ? err.message : String(err);
136
213
  }
137
214
  result.error = err instanceof Error ? err.message : String(err);
215
+ return result;
138
216
  }
139
- return result;
140
217
  }
141
218
  };
142
219
 
@@ -1 +1 @@
1
- {"version":3,"file":"dokploy.cjs","names":[],"sources":["../../src/deployers/dokploy.ts"],"sourcesContent":["/**\n * Dokploy PaaS deployer — deploys Docker Compose stacks via the Dokploy REST API.\n *\n * API docs: https://docs.dokploy.com/docs/api\n * Auth: x-api-key header\n * Endpoints use dot-notation (e.g. /api/project.create, /api/compose.deploy)\n */\n\nimport type { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from \"./types.js\";\n\n/** Shape returned by Dokploy's project endpoints. */\ninterface DokployProject {\n\tprojectId: string;\n\tname: string;\n\tdescription: string;\n\tenvironments?: { environmentId: string; name: string }[];\n}\n\n/** Shape returned by Dokploy's compose endpoints. */\ninterface DokployCompose {\n\tcomposeId: string;\n\tname: string;\n\tstatus?: string;\n}\n\n/** Build a full Dokploy API URL from a dot-notation endpoint (e.g. \"project.create\"). */\nfunction apiUrl(target: DeployTarget, endpoint: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/${endpoint}`;\n}\n\n/**\n * Typed fetch wrapper for the Dokploy API.\n * Handles JSON serialisation, x-api-key auth, and error extraction.\n */\nasync function dokployFetch<T>(\n\ttarget: DeployTarget,\n\tendpoint: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, endpoint), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"x-api-key\": target.apiKey,\n\t\t},\n\t\tbody: options.body ? JSON.stringify(options.body) : undefined,\n\t});\n\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => \"\");\n\t\tlet detail = text;\n\t\ttry {\n\t\t\tconst json = JSON.parse(text);\n\t\t\tdetail = json.message || json.error || text;\n\t\t} catch {\n\t\t\t// use raw text\n\t\t}\n\t\tthrow new Error(`Dokploy API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\tif (!text) return undefined as T;\n\treturn JSON.parse(text) as T;\n}\n\n/**\n * Deploys Docker Compose stacks to a Dokploy instance.\n *\n * Deploy flow (4 steps):\n * 1. Create a Dokploy project\n * 2. Create a compose stack inside the project's default environment\n * 3. Push .env variables to the compose stack\n * 4. Trigger the deployment\n */\nexport class DokployDeployer implements PaasDeployer {\n\treadonly name = \"Dokploy\";\n\treadonly id = \"dokploy\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait dokployFetch<DokployProject[]>(target, \"project.all\");\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn { ok: false, error: err instanceof Error ? err.message : String(err) };\n\t\t}\n\t}\n\n\tasync deploy(input: DeployInput): Promise<DeployResult> {\n\t\tconst step1: DeployStep = { step: \"Create project\", status: \"pending\" };\n\t\tconst step2: DeployStep = { step: \"Create compose stack\", status: \"pending\" };\n\t\tconst step3: DeployStep = { step: \"Set environment variables\", status: \"pending\" };\n\t\tconst step4: DeployStep = { step: \"Trigger deployment\", status: \"pending\" };\n\t\tconst steps: DeployStep[] = [step1, step2, step3, step4];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\ttry {\n\t\t\t// Step 1: Create project\n\t\t\tstep1.status = \"running\";\n\t\t\tconst project = await dokployFetch<DokployProject>(input.target, \"project.create\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.projectId = project.projectId;\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `Project ID: ${project.projectId}`;\n\n\t\t\t// Get the default environment ID\n\t\t\tconst projectDetail = await dokployFetch<DokployProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`project.one?projectId=${project.projectId}`,\n\t\t\t);\n\t\t\tconst envId = projectDetail.environments?.[0]?.environmentId;\n\t\t\tif (!envId) {\n\t\t\t\tthrow new Error(\"No default environment found in project\");\n\t\t\t}\n\n\t\t\t// Step 2: Create compose stack\n\t\t\tstep2.status = \"running\";\n\t\t\tconst compose = await dokployFetch<DokployCompose>(input.target, \"compose.create\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tenvironmentId: envId,\n\t\t\t\t\tcomposeFile: input.composeYaml,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.composeId = compose.composeId;\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = `Compose ID: ${compose.composeId}`;\n\n\t\t\t// Step 3: Set environment variables\n\t\t\tstep3.status = \"running\";\n\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: compose.composeId,\n\t\t\t\t\tenv: input.envContent,\n\t\t\t\t},\n\t\t\t});\n\t\t\tstep3.status = \"done\";\n\n\t\t\t// Step 4: Trigger deployment\n\t\t\tstep4.status = \"running\";\n\t\t\tawait dokployFetch(input.target, \"compose.deploy\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: compose.composeId,\n\t\t\t\t\ttitle: `Initial deploy: ${input.projectName}`,\n\t\t\t\t\tdescription: input.description ?? \"Deployed via OpenClaw web builder\",\n\t\t\t\t},\n\t\t\t});\n\t\t\tstep4.status = \"done\";\n\n\t\t\tresult.success = true;\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\t\t\tresult.dashboardUrl = `${base}/dashboard/project/${project.projectId}`;\n\t\t} catch (err) {\n\t\t\tconst failedStep = steps.find((s) => s.status === \"running\");\n\t\t\tif (failedStep) {\n\t\t\t\tfailedStep.status = \"error\";\n\t\t\t\tfailedStep.detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"dokploy.cjs","names":["sanitizeComposeForPaas"],"sources":["../../src/deployers/dokploy.ts"],"sourcesContent":["/**\n * Dokploy PaaS deployer — deploys Docker Compose stacks via the Dokploy REST API.\n *\n * API docs: https://docs.dokploy.com/docs/api\n * Auth: x-api-key header\n * Endpoints use dot-notation (e.g. /api/project.create, /api/compose.deploy)\n */\n\nimport { sanitizeComposeForPaas } from \"./strip-host-ports.js\";\nimport type {\n\tDeployInput,\n\tDeployResult,\n\tDeployStep,\n\tDeployTarget,\n\tDokployEnvironment,\n\tPaasDeployer,\n\tPaasServer,\n} from \"./types.js\";\n\ninterface DokployProject {\n\tprojectId: string;\n\tname: string;\n\tdescription: string;\n\tenvironments?: DokployEnvironment[];\n}\n\ninterface DokployCompose {\n\tcomposeId: string;\n\tname: string;\n\tstatus?: string;\n\tcompose?: string;\n}\n\ninterface ProjectCreateResult {\n\tproject: DokployProject;\n\tprojectId: string;\n\tname: string;\n\tdescription: string;\n\tenvironments?: { environmentId: string; name: string }[];\n}\n\n/** Build a full Dokploy API URL from a dot-notation endpoint (e.g. \"project.create\"). */\nfunction apiUrl(target: DeployTarget, endpoint: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/${endpoint}`;\n}\n/**\n * Typed fetch wrapper for the Dokploy API.\n * Handles JSON serialisation, x-api-key auth, and error extraction.\n */\nasync function dokployFetch<T>(\n\ttarget: DeployTarget,\n\tendpoint: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, endpoint), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"x-api-key\": target.apiKey,\n\t\t},\n\t\tbody: options.body ? JSON.stringify(options.body) : undefined,\n\t});\n\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => \"\");\n\t\tlet detail = text;\n\n\t\ttry {\n\t\t\tconst json = JSON.parse(text);\n\t\t\tdetail = json.message || json.error || text;\n\t\t} catch {}\n\n\t\tthrow new Error(`Dokploy API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\tif (!text) return undefined as T;\n\n\treturn JSON.parse(text) as T;\n}\n\n/**\n * Simple hash for compose diff detection\n */\nfunction hashString(str: string) {\n\tlet hash = 0;\n\n\tfor (let i = 0; i < str.length; i++) {\n\t\thash = (hash << 5) - hash + str.charCodeAt(i);\n\t\thash |= 0;\n\t}\n\n\treturn hash;\n}\n\n/**\n * Deploys Docker Compose stacks to a Dokploy instance.\n *\n * Deploy flow (4 steps):\n * 1. Create a Dokploy project\n * 2. Create a compose stack inside the project's default environment\n * 3. Push .env variables to the compose stack\n * 4. Trigger the deployment\n */\n\nexport class DokployDeployer implements PaasDeployer {\n\treadonly name = \"Dokploy\";\n\treadonly id = \"dokploy\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait dokployFetch<DokployProject[]>(target, \"project.all\");\n\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t};\n\t\t}\n\t}\n\n\tasync listServers(target: DeployTarget): Promise<PaasServer[]> {\n\t\ttry {\n\t\t\tconst servers = await dokployFetch<{ serverId: string; name: string; ipAddress: string }[]>(\n\t\t\t\ttarget,\n\t\t\t\t\"server.all\",\n\t\t\t);\n\t\t\treturn servers.map((s) => ({\n\t\t\t\tid: s.serverId,\n\t\t\t\tname: s.name,\n\t\t\t\tip: s.ipAddress,\n\t\t\t}));\n\t\t} catch {\n\t\t\t// Return empty list if server API is not available\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tasync deploy(input: DeployInput): Promise<DeployResult> {\n\t\tconst step1: DeployStep = {\n\t\t\tstep: \"Find or create project\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step2: DeployStep = {\n\t\t\tstep: \"Find default environment\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step3: DeployStep = {\n\t\t\tstep: \"Find or create compose stack\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step4: DeployStep = {\n\t\t\tstep: \"Update stack configuration\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step5: DeployStep = { step: \"Deploy stack\", status: \"pending\" };\n\t\tconst steps: DeployStep[] = [step1, step2, step3, step4, step5];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\t// Strip host port bindings — Dokploy routes via Traefik,\n\t\t// so host ports are unnecessary and cause \"port already allocated\" errors.\n\t\tconst composeYaml = sanitizeComposeForPaas(input.composeYaml);\n\n\t\ttry {\n\t\t\t/**\n\t\t\t * STEP 1\n\t\t\t * Find or create project\n\t\t\t */\n\n\t\t\tstep1.status = \"running\";\n\n\t\t\tconst projects = await dokployFetch<DokployProject[]>(input.target, \"project.all\");\n\n\t\t\tlet project = projects.find((p) => p.name === input.projectName);\n\n\t\t\tif (!project) {\n\t\t\t\tconst created = await dokployFetch<ProjectCreateResult>(input.target, \"project.create\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tname: input.projectName,\n\t\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tproject = created.project;\n\t\t\t}\n\n\t\t\tresult.projectId = project.projectId;\n\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `Project ID: ${project.projectId}`;\n\n\t\t\t/**\n\t\t\t * STEP 2\n\t\t\t * Find default environment\n\t\t\t */\n\n\t\t\tstep2.status = \"running\";\n\n\t\t\tconst projectDetail = await dokployFetch<DokployProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`project.one?projectId=${project.projectId}`,\n\t\t\t);\n\n\t\t\tconst env = projectDetail.environments?.find((e) => e.isDefault);\n\n\t\t\tif (!env) throw new Error(\"No default environment\");\n\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = env.environmentId;\n\n\t\t\t/**\n\t\t\t * STEP 3\n\t\t\t * Find or create compose stack\n\t\t\t */\n\n\t\t\tstep3.status = \"running\";\n\n\t\t\tlet stack: DokployCompose | null = null;\n\n\t\t\tstack = await dokployFetch<DokployCompose>(input.target, \"compose.create\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? `Stack ${input.projectName}`,\n\t\t\t\t\tenvironmentId: env.environmentId,\n\t\t\t\t\tcomposeType: \"docker-compose\",\n\t\t\t\t\tcomposeFile: composeYaml,\n\t\t\t\t\t...(input.serverId ? { serverId: input.serverId } : {}),\n\t\t\t\t},\n\t\t\t});\n\n\t\t\t// Dokploy's compose.create schema does NOT accept sourceType;\n\t\t\t// it defaults to \"github\". We must update it to \"raw\" so the\n\t\t\t// deploy step writes the compose file from the stored YAML\n\t\t\t// instead of attempting to clone from a Git provider.\n\t\t\tif (stack?.composeId) {\n\t\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tcomposeId: stack.composeId,\n\t\t\t\t\t\tsourceType: \"raw\",\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tresult.composeId = stack?.composeId;\n\t\t\tstep3.status = \"done\";\n\t\t\tstep3.detail = stack?.composeId;\n\n\t\t\t/**\n\t\t\t * STEP 4\n\t\t\t * Update stack if compose changed\n\t\t\t */\n\n\t\t\tstep4.status = \"running\";\n\n\t\t\tconst existingStack = await dokployFetch<DokployCompose>(\n\t\t\t\tinput.target,\n\t\t\t\t`compose.one?composeId=${stack?.composeId}`,\n\t\t\t);\n\n\t\t\tconst newHash = hashString(composeYaml);\n\t\t\tconst oldHash = hashString(existingStack.compose ?? \"\");\n\n\t\t\tif (newHash !== oldHash) {\n\t\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tcomposeId: stack?.composeId,\n\t\t\t\t\t\tcomposeFile: composeYaml,\n\t\t\t\t\t\tenv: input.envContent ?? \"\",\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tstep4.detail = \"Stack updated\";\n\t\t\t} else {\n\t\t\t\tstep4.detail = \"No compose changes\";\n\t\t\t}\n\n\t\t\tstep4.status = \"done\";\n\n\t\t\t/**\n\t\t\t * STEP 5\n\t\t\t * Deploy\n\t\t\t */\n\n\t\t\tstep5.status = \"running\";\n\n\t\t\tawait dokployFetch(input.target, \"compose.deploy\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: stack?.composeId,\n\n\t\t\t\t\ttitle: `Deploy ${input.projectName}`,\n\n\t\t\t\t\tdescription: \"CI deployment\",\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tstep5.status = \"done\";\n\n\t\t\tresult.success = true;\n\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\n\t\t\tresult.dashboardUrl = `${base}/dashboard/project/${project.projectId}/environment/${env.environmentId}/services/compose/${stack?.composeId}?tab=deployments`;\n\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tconst running = steps.find((s) => s.status === \"running\");\n\n\t\t\tif (running) {\n\t\t\t\trunning.status = \"error\";\n\n\t\t\t\trunning.detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\n\t\t\treturn result;\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AA0CA,SAAS,OAAO,QAAsB,UAA0B;AAE/D,QAAO,GADM,OAAO,YAAY,QAAQ,QAAQ,GAAG,CACpC,OAAO;;;;;;AAMvB,eAAe,aACd,QACA,UACA,UAA+C,EAAE,EACpC;CACb,MAAM,MAAM,MAAM,MAAM,OAAO,QAAQ,SAAS,EAAE;EACjD,QAAQ,QAAQ,UAAU;EAC1B,SAAS;GACR,gBAAgB;GAChB,aAAa,OAAO;GACpB;EACD,MAAM,QAAQ,OAAO,KAAK,UAAU,QAAQ,KAAK,GAAG;EACpD,CAAC;AAEF,KAAI,CAAC,IAAI,IAAI;EACZ,MAAM,OAAO,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG;EAC7C,IAAI,SAAS;AAEb,MAAI;GACH,MAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,YAAS,KAAK,WAAW,KAAK,SAAS;UAChC;AAER,QAAM,IAAI,MAAM,eAAe,IAAI,OAAO,IAAI,SAAS;;CAGxD,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,KAAI,CAAC,KAAM,QAAO;AAElB,QAAO,KAAK,MAAM,KAAK;;;;;AAMxB,SAAS,WAAW,KAAa;CAChC,IAAI,OAAO;AAEX,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACpC,UAAQ,QAAQ,KAAK,OAAO,IAAI,WAAW,EAAE;AAC7C,UAAQ;;AAGT,QAAO;;;;;;;;;;;AAaR,IAAa,kBAAb,MAAqD;CACpD,AAAS,OAAO;CAChB,AAAS,KAAK;CAEd,MAAM,eAAe,QAAgE;AACpF,MAAI;AACH,SAAM,aAA+B,QAAQ,cAAc;AAE3D,UAAO,EAAE,IAAI,MAAM;WACX,KAAK;AACb,UAAO;IACN,IAAI;IACJ,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IACvD;;;CAIH,MAAM,YAAY,QAA6C;AAC9D,MAAI;AAKH,WAJgB,MAAM,aACrB,QACA,aACA,EACc,KAAK,OAAO;IAC1B,IAAI,EAAE;IACN,MAAM,EAAE;IACR,IAAI,EAAE;IACN,EAAE;UACI;AAEP,UAAO,EAAE;;;CAIX,MAAM,OAAO,OAA2C;EACvD,MAAM,QAAoB;GACzB,MAAM;GACN,QAAQ;GACR;EACD,MAAM,QAAoB;GACzB,MAAM;GACN,QAAQ;GACR;EACD,MAAM,QAAoB;GACzB,MAAM;GACN,QAAQ;GACR;EACD,MAAM,QAAoB;GACzB,MAAM;GACN,QAAQ;GACR;EACD,MAAM,QAAoB;GAAE,MAAM;GAAgB,QAAQ;GAAW;EACrE,MAAM,QAAsB;GAAC;GAAO;GAAO;GAAO;GAAO;GAAM;EAE/D,MAAM,SAAuB;GAAE,SAAS;GAAO;GAAO;EAItD,MAAM,cAAcA,0DAAuB,MAAM,YAAY;AAE7D,MAAI;;;;;AAMH,SAAM,SAAS;GAIf,IAAI,WAFa,MAAM,aAA+B,MAAM,QAAQ,cAAc,EAE3D,MAAM,MAAM,EAAE,SAAS,MAAM,YAAY;AAEhE,OAAI,CAAC,QASJ,YARgB,MAAM,aAAkC,MAAM,QAAQ,kBAAkB;IACvF,QAAQ;IACR,MAAM;KACL,MAAM,MAAM;KACZ,aAAa,MAAM,eAAe,mBAAmB,MAAM;KAC3D;IACD,CAAC,EAEgB;AAGnB,UAAO,YAAY,QAAQ;AAE3B,SAAM,SAAS;AACf,SAAM,SAAS,eAAe,QAAQ;;;;;AAOtC,SAAM,SAAS;GAOf,MAAM,OALgB,MAAM,aAC3B,MAAM,QACN,yBAAyB,QAAQ,YACjC,EAEyB,cAAc,MAAM,MAAM,EAAE,UAAU;AAEhE,OAAI,CAAC,IAAK,OAAM,IAAI,MAAM,yBAAyB;AAEnD,SAAM,SAAS;AACf,SAAM,SAAS,IAAI;;;;;AAOnB,SAAM,SAAS;GAEf,IAAI,QAA+B;AAEnC,WAAQ,MAAM,aAA6B,MAAM,QAAQ,kBAAkB;IAC1E,QAAQ;IACR,MAAM;KACL,MAAM,MAAM;KACZ,aAAa,MAAM,eAAe,SAAS,MAAM;KACjD,eAAe,IAAI;KACnB,aAAa;KACb,aAAa;KACb,GAAI,MAAM,WAAW,EAAE,UAAU,MAAM,UAAU,GAAG,EAAE;KACtD;IACD,CAAC;AAMF,OAAI,OAAO,UACV,OAAM,aAAa,MAAM,QAAQ,kBAAkB;IAClD,QAAQ;IACR,MAAM;KACL,WAAW,MAAM;KACjB,YAAY;KACZ;IACD,CAAC;AAGH,UAAO,YAAY,OAAO;AAC1B,SAAM,SAAS;AACf,SAAM,SAAS,OAAO;;;;;AAOtB,SAAM,SAAS;GAEf,MAAM,gBAAgB,MAAM,aAC3B,MAAM,QACN,yBAAyB,OAAO,YAChC;AAKD,OAHgB,WAAW,YAAY,KACvB,WAAW,cAAc,WAAW,GAAG,EAE9B;AACxB,UAAM,aAAa,MAAM,QAAQ,kBAAkB;KAClD,QAAQ;KACR,MAAM;MACL,WAAW,OAAO;MAClB,aAAa;MACb,KAAK,MAAM,cAAc;MACzB;KACD,CAAC;AAEF,UAAM,SAAS;SAEf,OAAM,SAAS;AAGhB,SAAM,SAAS;;;;;AAOf,SAAM,SAAS;AAEf,SAAM,aAAa,MAAM,QAAQ,kBAAkB;IAClD,QAAQ;IACR,MAAM;KACL,WAAW,OAAO;KAElB,OAAO,UAAU,MAAM;KAEvB,aAAa;KACb;IACD,CAAC;AAEF,SAAM,SAAS;AAEf,UAAO,UAAU;AAIjB,UAAO,eAAe,GAFT,MAAM,OAAO,YAAY,QAAQ,QAAQ,GAAG,CAE3B,qBAAqB,QAAQ,UAAU,eAAe,IAAI,cAAc,oBAAoB,OAAO,UAAU;AAE3I,UAAO;WACC,KAAK;GACb,MAAM,UAAU,MAAM,MAAM,MAAM,EAAE,WAAW,UAAU;AAEzD,OAAI,SAAS;AACZ,YAAQ,SAAS;AAEjB,YAAQ,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;;AAGlE,UAAO,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAE/D,UAAO"}
@@ -1,4 +1,4 @@
1
- import { DeployInput, DeployResult, DeployTarget, PaasDeployer } from "./types.cjs";
1
+ import { DeployInput, DeployResult, DeployTarget, PaasDeployer, PaasServer } from "./types.cjs";
2
2
 
3
3
  //#region src/deployers/dokploy.d.ts
4
4
  /**
@@ -17,6 +17,7 @@ declare class DokployDeployer implements PaasDeployer {
17
17
  ok: boolean;
18
18
  error?: string;
19
19
  }>;
20
+ listServers(target: DeployTarget): Promise<PaasServer[]>;
20
21
  deploy(input: DeployInput): Promise<DeployResult>;
21
22
  }
22
23
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"dokploy.d.cts","names":[],"sources":["../../src/deployers/dokploy.ts"],"mappings":";;;;;;;;;;;;cA2Ea,eAAA,YAA2B,YAAA;EAAA,SAC9B,IAAA;EAAA,SACA,EAAA;EAEH,cAAA,CAAe,MAAA,EAAQ,YAAA,GAAe,OAAA;IAAU,EAAA;IAAa,KAAA;EAAA;EAS7D,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,OAAA,CAAQ,YAAA;AAAA"}
1
+ {"version":3,"file":"dokploy.d.cts","names":[],"sources":["../../src/deployers/dokploy.ts"],"mappings":";;;;;;;;;;;;cA0Ga,eAAA,YAA2B,YAAA;EAAA,SAC9B,IAAA;EAAA,SACA,EAAA;EAEH,cAAA,CAAe,MAAA,EAAQ,YAAA,GAAe,OAAA;IAAU,EAAA;IAAa,KAAA;EAAA;EAa7D,WAAA,CAAY,MAAA,EAAQ,YAAA,GAAe,OAAA,CAAQ,UAAA;EAiB3C,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,OAAA,CAAQ,YAAA;AAAA"}
@@ -1,4 +1,4 @@
1
- import { DeployInput, DeployResult, DeployTarget, PaasDeployer } from "./types.mjs";
1
+ import { DeployInput, DeployResult, DeployTarget, PaasDeployer, PaasServer } from "./types.mjs";
2
2
 
3
3
  //#region src/deployers/dokploy.d.ts
4
4
  /**
@@ -17,6 +17,7 @@ declare class DokployDeployer implements PaasDeployer {
17
17
  ok: boolean;
18
18
  error?: string;
19
19
  }>;
20
+ listServers(target: DeployTarget): Promise<PaasServer[]>;
20
21
  deploy(input: DeployInput): Promise<DeployResult>;
21
22
  }
22
23
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"dokploy.d.mts","names":[],"sources":["../../src/deployers/dokploy.ts"],"mappings":";;;;;;;;;;;;cA2Ea,eAAA,YAA2B,YAAA;EAAA,SAC9B,IAAA;EAAA,SACA,EAAA;EAEH,cAAA,CAAe,MAAA,EAAQ,YAAA,GAAe,OAAA;IAAU,EAAA;IAAa,KAAA;EAAA;EAS7D,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,OAAA,CAAQ,YAAA;AAAA"}
1
+ {"version":3,"file":"dokploy.d.mts","names":[],"sources":["../../src/deployers/dokploy.ts"],"mappings":";;;;;;;;;;;;cA0Ga,eAAA,YAA2B,YAAA;EAAA,SAC9B,IAAA;EAAA,SACA,EAAA;EAEH,cAAA,CAAe,MAAA,EAAQ,YAAA,GAAe,OAAA;IAAU,EAAA;IAAa,KAAA;EAAA;EAa7D,WAAA,CAAY,MAAA,EAAQ,YAAA,GAAe,OAAA,CAAQ,UAAA;EAiB3C,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,OAAA,CAAQ,YAAA;AAAA"}
@@ -1,4 +1,13 @@
1
+ import { sanitizeComposeForPaas } from "./strip-host-ports.mjs";
2
+
1
3
  //#region src/deployers/dokploy.ts
4
+ /**
5
+ * Dokploy PaaS deployer — deploys Docker Compose stacks via the Dokploy REST API.
6
+ *
7
+ * API docs: https://docs.dokploy.com/docs/api
8
+ * Auth: x-api-key header
9
+ * Endpoints use dot-notation (e.g. /api/project.create, /api/compose.deploy)
10
+ */
2
11
  /** Build a full Dokploy API URL from a dot-notation endpoint (e.g. "project.create"). */
3
12
  function apiUrl(target, endpoint) {
4
13
  return `${target.instanceUrl.replace(/\/+$/, "")}/api/${endpoint}`;
@@ -30,6 +39,17 @@ async function dokployFetch(target, endpoint, options = {}) {
30
39
  return JSON.parse(text);
31
40
  }
32
41
  /**
42
+ * Simple hash for compose diff detection
43
+ */
44
+ function hashString(str) {
45
+ let hash = 0;
46
+ for (let i = 0; i < str.length; i++) {
47
+ hash = (hash << 5) - hash + str.charCodeAt(i);
48
+ hash |= 0;
49
+ }
50
+ return hash;
51
+ }
52
+ /**
33
53
  * Deploys Docker Compose stacks to a Dokploy instance.
34
54
  *
35
55
  * Deploy flow (4 steps):
@@ -52,89 +72,147 @@ var DokployDeployer = class {
52
72
  };
53
73
  }
54
74
  }
75
+ async listServers(target) {
76
+ try {
77
+ return (await dokployFetch(target, "server.all")).map((s) => ({
78
+ id: s.serverId,
79
+ name: s.name,
80
+ ip: s.ipAddress
81
+ }));
82
+ } catch {
83
+ return [];
84
+ }
85
+ }
55
86
  async deploy(input) {
56
87
  const step1 = {
57
- step: "Create project",
88
+ step: "Find or create project",
58
89
  status: "pending"
59
90
  };
60
91
  const step2 = {
61
- step: "Create compose stack",
92
+ step: "Find default environment",
62
93
  status: "pending"
63
94
  };
64
95
  const step3 = {
65
- step: "Set environment variables",
96
+ step: "Find or create compose stack",
66
97
  status: "pending"
67
98
  };
68
99
  const step4 = {
69
- step: "Trigger deployment",
100
+ step: "Update stack configuration",
101
+ status: "pending"
102
+ };
103
+ const step5 = {
104
+ step: "Deploy stack",
70
105
  status: "pending"
71
106
  };
72
107
  const steps = [
73
108
  step1,
74
109
  step2,
75
110
  step3,
76
- step4
111
+ step4,
112
+ step5
77
113
  ];
78
114
  const result = {
79
115
  success: false,
80
116
  steps
81
117
  };
118
+ const composeYaml = sanitizeComposeForPaas(input.composeYaml);
82
119
  try {
120
+ /**
121
+ * STEP 1
122
+ * Find or create project
123
+ */
83
124
  step1.status = "running";
84
- const project = await dokployFetch(input.target, "project.create", {
125
+ let project = (await dokployFetch(input.target, "project.all")).find((p) => p.name === input.projectName);
126
+ if (!project) project = (await dokployFetch(input.target, "project.create", {
85
127
  method: "POST",
86
128
  body: {
87
129
  name: input.projectName,
88
130
  description: input.description ?? `OpenClaw stack: ${input.projectName}`
89
131
  }
90
- });
132
+ })).project;
91
133
  result.projectId = project.projectId;
92
134
  step1.status = "done";
93
135
  step1.detail = `Project ID: ${project.projectId}`;
94
- const envId = (await dokployFetch(input.target, `project.one?projectId=${project.projectId}`)).environments?.[0]?.environmentId;
95
- if (!envId) throw new Error("No default environment found in project");
136
+ /**
137
+ * STEP 2
138
+ * Find default environment
139
+ */
96
140
  step2.status = "running";
97
- const compose = await dokployFetch(input.target, "compose.create", {
141
+ const env = (await dokployFetch(input.target, `project.one?projectId=${project.projectId}`)).environments?.find((e) => e.isDefault);
142
+ if (!env) throw new Error("No default environment");
143
+ step2.status = "done";
144
+ step2.detail = env.environmentId;
145
+ /**
146
+ * STEP 3
147
+ * Find or create compose stack
148
+ */
149
+ step3.status = "running";
150
+ let stack = null;
151
+ stack = await dokployFetch(input.target, "compose.create", {
98
152
  method: "POST",
99
153
  body: {
100
154
  name: input.projectName,
101
- environmentId: envId,
102
- composeFile: input.composeYaml
155
+ description: input.description ?? `Stack ${input.projectName}`,
156
+ environmentId: env.environmentId,
157
+ composeType: "docker-compose",
158
+ composeFile: composeYaml,
159
+ ...input.serverId ? { serverId: input.serverId } : {}
103
160
  }
104
161
  });
105
- result.composeId = compose.composeId;
106
- step2.status = "done";
107
- step2.detail = `Compose ID: ${compose.composeId}`;
108
- step3.status = "running";
109
- await dokployFetch(input.target, "compose.update", {
162
+ if (stack?.composeId) await dokployFetch(input.target, "compose.update", {
110
163
  method: "POST",
111
164
  body: {
112
- composeId: compose.composeId,
113
- env: input.envContent
165
+ composeId: stack.composeId,
166
+ sourceType: "raw"
114
167
  }
115
168
  });
169
+ result.composeId = stack?.composeId;
116
170
  step3.status = "done";
171
+ step3.detail = stack?.composeId;
172
+ /**
173
+ * STEP 4
174
+ * Update stack if compose changed
175
+ */
117
176
  step4.status = "running";
177
+ const existingStack = await dokployFetch(input.target, `compose.one?composeId=${stack?.composeId}`);
178
+ if (hashString(composeYaml) !== hashString(existingStack.compose ?? "")) {
179
+ await dokployFetch(input.target, "compose.update", {
180
+ method: "POST",
181
+ body: {
182
+ composeId: stack?.composeId,
183
+ composeFile: composeYaml,
184
+ env: input.envContent ?? ""
185
+ }
186
+ });
187
+ step4.detail = "Stack updated";
188
+ } else step4.detail = "No compose changes";
189
+ step4.status = "done";
190
+ /**
191
+ * STEP 5
192
+ * Deploy
193
+ */
194
+ step5.status = "running";
118
195
  await dokployFetch(input.target, "compose.deploy", {
119
196
  method: "POST",
120
197
  body: {
121
- composeId: compose.composeId,
122
- title: `Initial deploy: ${input.projectName}`,
123
- description: input.description ?? "Deployed via OpenClaw web builder"
198
+ composeId: stack?.composeId,
199
+ title: `Deploy ${input.projectName}`,
200
+ description: "CI deployment"
124
201
  }
125
202
  });
126
- step4.status = "done";
203
+ step5.status = "done";
127
204
  result.success = true;
128
- result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/dashboard/project/${project.projectId}`;
205
+ result.dashboardUrl = `${input.target.instanceUrl.replace(/\/+$/, "")}/dashboard/project/${project.projectId}/environment/${env.environmentId}/services/compose/${stack?.composeId}?tab=deployments`;
206
+ return result;
129
207
  } catch (err) {
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);
208
+ const running = steps.find((s) => s.status === "running");
209
+ if (running) {
210
+ running.status = "error";
211
+ running.detail = err instanceof Error ? err.message : String(err);
134
212
  }
135
213
  result.error = err instanceof Error ? err.message : String(err);
214
+ return result;
136
215
  }
137
- return result;
138
216
  }
139
217
  };
140
218
 
@@ -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 step1: DeployStep = { step: \"Create project\", status: \"pending\" };\n\t\tconst step2: DeployStep = { step: \"Create compose stack\", status: \"pending\" };\n\t\tconst step3: DeployStep = { step: \"Set environment variables\", status: \"pending\" };\n\t\tconst step4: DeployStep = { step: \"Trigger deployment\", status: \"pending\" };\n\t\tconst steps: DeployStep[] = [step1, step2, step3, step4];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\ttry {\n\t\t\t// Step 1: Create project\n\t\t\tstep1.status = \"running\";\n\t\t\tconst project = await dokployFetch<DokployProject>(input.target, \"project.create\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.projectId = project.projectId;\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `Project ID: ${project.projectId}`;\n\n\t\t\t// Get the default environment ID\n\t\t\tconst projectDetail = await dokployFetch<DokployProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`project.one?projectId=${project.projectId}`,\n\t\t\t);\n\t\t\tconst envId = projectDetail.environments?.[0]?.environmentId;\n\t\t\tif (!envId) {\n\t\t\t\tthrow new Error(\"No default environment found in project\");\n\t\t\t}\n\n\t\t\t// Step 2: Create compose stack\n\t\t\tstep2.status = \"running\";\n\t\t\tconst compose = await dokployFetch<DokployCompose>(input.target, \"compose.create\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tenvironmentId: envId,\n\t\t\t\t\tcomposeFile: input.composeYaml,\n\t\t\t\t},\n\t\t\t});\n\t\t\tresult.composeId = compose.composeId;\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = `Compose ID: ${compose.composeId}`;\n\n\t\t\t// Step 3: Set environment variables\n\t\t\tstep3.status = \"running\";\n\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: compose.composeId,\n\t\t\t\t\tenv: input.envContent,\n\t\t\t\t},\n\t\t\t});\n\t\t\tstep3.status = \"done\";\n\n\t\t\t// Step 4: Trigger deployment\n\t\t\tstep4.status = \"running\";\n\t\t\tawait dokployFetch(input.target, \"compose.deploy\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: compose.composeId,\n\t\t\t\t\ttitle: `Initial deploy: ${input.projectName}`,\n\t\t\t\t\tdescription: input.description ?? \"Deployed via OpenClaw web builder\",\n\t\t\t\t},\n\t\t\t});\n\t\t\tstep4.status = \"done\";\n\n\t\t\tresult.success = true;\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\t\t\tresult.dashboardUrl = `${base}/dashboard/project/${project.projectId}`;\n\t\t} catch (err) {\n\t\t\tconst failedStep = steps.find((s) => s.status === \"running\");\n\t\t\tif (failedStep) {\n\t\t\t\tfailedStep.status = \"error\";\n\t\t\t\tfailedStep.detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"dokploy.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 { sanitizeComposeForPaas } from \"./strip-host-ports.js\";\nimport type {\n\tDeployInput,\n\tDeployResult,\n\tDeployStep,\n\tDeployTarget,\n\tDokployEnvironment,\n\tPaasDeployer,\n\tPaasServer,\n} from \"./types.js\";\n\ninterface DokployProject {\n\tprojectId: string;\n\tname: string;\n\tdescription: string;\n\tenvironments?: DokployEnvironment[];\n}\n\ninterface DokployCompose {\n\tcomposeId: string;\n\tname: string;\n\tstatus?: string;\n\tcompose?: string;\n}\n\ninterface ProjectCreateResult {\n\tproject: DokployProject;\n\tprojectId: string;\n\tname: string;\n\tdescription: string;\n\tenvironments?: { environmentId: string; name: string }[];\n}\n\n/** Build a full Dokploy API URL from a dot-notation endpoint (e.g. \"project.create\"). */\nfunction apiUrl(target: DeployTarget, endpoint: string): string {\n\tconst base = target.instanceUrl.replace(/\\/+$/, \"\");\n\treturn `${base}/api/${endpoint}`;\n}\n/**\n * Typed fetch wrapper for the Dokploy API.\n * Handles JSON serialisation, x-api-key auth, and error extraction.\n */\nasync function dokployFetch<T>(\n\ttarget: DeployTarget,\n\tendpoint: string,\n\toptions: { method?: string; body?: unknown } = {},\n): Promise<T> {\n\tconst res = await fetch(apiUrl(target, endpoint), {\n\t\tmethod: options.method ?? \"GET\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"x-api-key\": target.apiKey,\n\t\t},\n\t\tbody: options.body ? JSON.stringify(options.body) : undefined,\n\t});\n\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => \"\");\n\t\tlet detail = text;\n\n\t\ttry {\n\t\t\tconst json = JSON.parse(text);\n\t\t\tdetail = json.message || json.error || text;\n\t\t} catch {}\n\n\t\tthrow new Error(`Dokploy API ${res.status}: ${detail}`);\n\t}\n\n\tconst text = await res.text();\n\tif (!text) return undefined as T;\n\n\treturn JSON.parse(text) as T;\n}\n\n/**\n * Simple hash for compose diff detection\n */\nfunction hashString(str: string) {\n\tlet hash = 0;\n\n\tfor (let i = 0; i < str.length; i++) {\n\t\thash = (hash << 5) - hash + str.charCodeAt(i);\n\t\thash |= 0;\n\t}\n\n\treturn hash;\n}\n\n/**\n * Deploys Docker Compose stacks to a Dokploy instance.\n *\n * Deploy flow (4 steps):\n * 1. Create a Dokploy project\n * 2. Create a compose stack inside the project's default environment\n * 3. Push .env variables to the compose stack\n * 4. Trigger the deployment\n */\n\nexport class DokployDeployer implements PaasDeployer {\n\treadonly name = \"Dokploy\";\n\treadonly id = \"dokploy\";\n\n\tasync testConnection(target: DeployTarget): Promise<{ ok: boolean; error?: string }> {\n\t\ttry {\n\t\t\tawait dokployFetch<DokployProject[]>(target, \"project.all\");\n\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t};\n\t\t}\n\t}\n\n\tasync listServers(target: DeployTarget): Promise<PaasServer[]> {\n\t\ttry {\n\t\t\tconst servers = await dokployFetch<{ serverId: string; name: string; ipAddress: string }[]>(\n\t\t\t\ttarget,\n\t\t\t\t\"server.all\",\n\t\t\t);\n\t\t\treturn servers.map((s) => ({\n\t\t\t\tid: s.serverId,\n\t\t\t\tname: s.name,\n\t\t\t\tip: s.ipAddress,\n\t\t\t}));\n\t\t} catch {\n\t\t\t// Return empty list if server API is not available\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tasync deploy(input: DeployInput): Promise<DeployResult> {\n\t\tconst step1: DeployStep = {\n\t\t\tstep: \"Find or create project\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step2: DeployStep = {\n\t\t\tstep: \"Find default environment\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step3: DeployStep = {\n\t\t\tstep: \"Find or create compose stack\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step4: DeployStep = {\n\t\t\tstep: \"Update stack configuration\",\n\t\t\tstatus: \"pending\",\n\t\t};\n\t\tconst step5: DeployStep = { step: \"Deploy stack\", status: \"pending\" };\n\t\tconst steps: DeployStep[] = [step1, step2, step3, step4, step5];\n\n\t\tconst result: DeployResult = { success: false, steps };\n\n\t\t// Strip host port bindings — Dokploy routes via Traefik,\n\t\t// so host ports are unnecessary and cause \"port already allocated\" errors.\n\t\tconst composeYaml = sanitizeComposeForPaas(input.composeYaml);\n\n\t\ttry {\n\t\t\t/**\n\t\t\t * STEP 1\n\t\t\t * Find or create project\n\t\t\t */\n\n\t\t\tstep1.status = \"running\";\n\n\t\t\tconst projects = await dokployFetch<DokployProject[]>(input.target, \"project.all\");\n\n\t\t\tlet project = projects.find((p) => p.name === input.projectName);\n\n\t\t\tif (!project) {\n\t\t\t\tconst created = await dokployFetch<ProjectCreateResult>(input.target, \"project.create\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tname: input.projectName,\n\t\t\t\t\t\tdescription: input.description ?? `OpenClaw stack: ${input.projectName}`,\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tproject = created.project;\n\t\t\t}\n\n\t\t\tresult.projectId = project.projectId;\n\n\t\t\tstep1.status = \"done\";\n\t\t\tstep1.detail = `Project ID: ${project.projectId}`;\n\n\t\t\t/**\n\t\t\t * STEP 2\n\t\t\t * Find default environment\n\t\t\t */\n\n\t\t\tstep2.status = \"running\";\n\n\t\t\tconst projectDetail = await dokployFetch<DokployProject>(\n\t\t\t\tinput.target,\n\t\t\t\t`project.one?projectId=${project.projectId}`,\n\t\t\t);\n\n\t\t\tconst env = projectDetail.environments?.find((e) => e.isDefault);\n\n\t\t\tif (!env) throw new Error(\"No default environment\");\n\n\t\t\tstep2.status = \"done\";\n\t\t\tstep2.detail = env.environmentId;\n\n\t\t\t/**\n\t\t\t * STEP 3\n\t\t\t * Find or create compose stack\n\t\t\t */\n\n\t\t\tstep3.status = \"running\";\n\n\t\t\tlet stack: DokployCompose | null = null;\n\n\t\t\tstack = await dokployFetch<DokployCompose>(input.target, \"compose.create\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tname: input.projectName,\n\t\t\t\t\tdescription: input.description ?? `Stack ${input.projectName}`,\n\t\t\t\t\tenvironmentId: env.environmentId,\n\t\t\t\t\tcomposeType: \"docker-compose\",\n\t\t\t\t\tcomposeFile: composeYaml,\n\t\t\t\t\t...(input.serverId ? { serverId: input.serverId } : {}),\n\t\t\t\t},\n\t\t\t});\n\n\t\t\t// Dokploy's compose.create schema does NOT accept sourceType;\n\t\t\t// it defaults to \"github\". We must update it to \"raw\" so the\n\t\t\t// deploy step writes the compose file from the stored YAML\n\t\t\t// instead of attempting to clone from a Git provider.\n\t\t\tif (stack?.composeId) {\n\t\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tcomposeId: stack.composeId,\n\t\t\t\t\t\tsourceType: \"raw\",\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tresult.composeId = stack?.composeId;\n\t\t\tstep3.status = \"done\";\n\t\t\tstep3.detail = stack?.composeId;\n\n\t\t\t/**\n\t\t\t * STEP 4\n\t\t\t * Update stack if compose changed\n\t\t\t */\n\n\t\t\tstep4.status = \"running\";\n\n\t\t\tconst existingStack = await dokployFetch<DokployCompose>(\n\t\t\t\tinput.target,\n\t\t\t\t`compose.one?composeId=${stack?.composeId}`,\n\t\t\t);\n\n\t\t\tconst newHash = hashString(composeYaml);\n\t\t\tconst oldHash = hashString(existingStack.compose ?? \"\");\n\n\t\t\tif (newHash !== oldHash) {\n\t\t\t\tawait dokployFetch(input.target, \"compose.update\", {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tcomposeId: stack?.composeId,\n\t\t\t\t\t\tcomposeFile: composeYaml,\n\t\t\t\t\t\tenv: input.envContent ?? \"\",\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tstep4.detail = \"Stack updated\";\n\t\t\t} else {\n\t\t\t\tstep4.detail = \"No compose changes\";\n\t\t\t}\n\n\t\t\tstep4.status = \"done\";\n\n\t\t\t/**\n\t\t\t * STEP 5\n\t\t\t * Deploy\n\t\t\t */\n\n\t\t\tstep5.status = \"running\";\n\n\t\t\tawait dokployFetch(input.target, \"compose.deploy\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tcomposeId: stack?.composeId,\n\n\t\t\t\t\ttitle: `Deploy ${input.projectName}`,\n\n\t\t\t\t\tdescription: \"CI deployment\",\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tstep5.status = \"done\";\n\n\t\t\tresult.success = true;\n\n\t\t\tconst base = input.target.instanceUrl.replace(/\\/+$/, \"\");\n\n\t\t\tresult.dashboardUrl = `${base}/dashboard/project/${project.projectId}/environment/${env.environmentId}/services/compose/${stack?.composeId}?tab=deployments`;\n\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tconst running = steps.find((s) => s.status === \"running\");\n\n\t\t\tif (running) {\n\t\t\t\trunning.status = \"error\";\n\n\t\t\t\trunning.detail = err instanceof Error ? err.message : String(err);\n\t\t\t}\n\n\t\t\tresult.error = err instanceof Error ? err.message : String(err);\n\n\t\t\treturn result;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["DokployDeployer","CoolifyDeployer"],"sources":["../../src/deployers/index.ts"],"sourcesContent":["/**\n * PaaS deployer registry — barrel export + lookup helpers.\n *\n * To add a new provider, implement `PaasDeployer` and register it in\n * `deployerRegistry` below.\n */\n\nexport { CoolifyDeployer } from \"./coolify.js\";\nexport { DokployDeployer } from \"./dokploy.js\";\nexport type {\n\tDeployInput,\n\tDeployResult,\n\tDeployStep,\n\tDeployTarget,\n\tPaasDeployer,\n} from \"./types.js\";\n\nimport { CoolifyDeployer } from \"./coolify.js\";\nimport { DokployDeployer } from \"./dokploy.js\";\nimport type { PaasDeployer } from \"./types.js\";\n\n/** Registry of all available PaaS deployers. */\nexport const deployerRegistry: Record<string, PaasDeployer> = {\n\tdokploy: new DokployDeployer(),\n\tcoolify: new CoolifyDeployer(),\n};\n\n/** Get a deployer by ID, or undefined if not found. */\nexport function getDeployer(id: string): PaasDeployer | undefined {\n\treturn deployerRegistry[id];\n}\n\n/** List all available deployer IDs. */\nexport function getAvailableDeployers(): string[] {\n\treturn Object.keys(deployerRegistry);\n}\n"],"mappings":";;;;;;AAsBA,MAAa,mBAAiD;CAC7D,SAAS,IAAIA,2CAAiB;CAC9B,SAAS,IAAIC,2CAAiB;CAC9B;;AAGD,SAAgB,YAAY,IAAsC;AACjE,QAAO,iBAAiB;;;AAIzB,SAAgB,wBAAkC;AACjD,QAAO,OAAO,KAAK,iBAAiB"}
1
+ {"version":3,"file":"index.cjs","names":["DokployDeployer","CoolifyDeployer"],"sources":["../../src/deployers/index.ts"],"sourcesContent":["/**\n * PaaS deployer registry — barrel export + lookup helpers.\n *\n * To add a new provider, implement `PaasDeployer` and register it in\n * `deployerRegistry` below.\n */\n\nexport { CoolifyDeployer } from \"./coolify.js\";\nexport { DokployDeployer } from \"./dokploy.js\";\nexport type {\n\tDeployInput,\n\tDeployResult,\n\tDeployStep,\n\tDeployTarget,\n\tPaasDeployer,\n\tPaasServer,\n} from \"./types.js\";\n\nimport { CoolifyDeployer } from \"./coolify.js\";\nimport { DokployDeployer } from \"./dokploy.js\";\nimport type { PaasDeployer } from \"./types.js\";\n\n/** Registry of all available PaaS deployers. */\nexport const deployerRegistry: Record<string, PaasDeployer> = {\n\tdokploy: new DokployDeployer(),\n\tcoolify: new CoolifyDeployer(),\n};\n\n/** Get a deployer by ID, or undefined if not found. */\nexport function getDeployer(id: string): PaasDeployer | undefined {\n\treturn deployerRegistry[id];\n}\n\n/** List all available deployer IDs. */\nexport function getAvailableDeployers(): string[] {\n\treturn Object.keys(deployerRegistry);\n}\n"],"mappings":";;;;;;AAuBA,MAAa,mBAAiD;CAC7D,SAAS,IAAIA,2CAAiB;CAC9B,SAAS,IAAIC,2CAAiB;CAC9B;;AAGD,SAAgB,YAAY,IAAsC;AACjE,QAAO,iBAAiB;;;AAIzB,SAAgB,wBAAkC;AACjD,QAAO,OAAO,KAAK,iBAAiB"}
@@ -1,4 +1,4 @@
1
- import { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from "./types.cjs";
1
+ import { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer, PaasServer } from "./types.cjs";
2
2
  import { CoolifyDeployer } from "./coolify.cjs";
3
3
  import { DokployDeployer } from "./dokploy.cjs";
4
4
 
@@ -10,5 +10,5 @@ declare function getDeployer(id: string): PaasDeployer | undefined;
10
10
  /** List all available deployer IDs. */
11
11
  declare function getAvailableDeployers(): string[];
12
12
  //#endregion
13
- export { CoolifyDeployer, type DeployInput, type DeployResult, type DeployStep, type DeployTarget, DokployDeployer, type PaasDeployer, deployerRegistry, getAvailableDeployers, getDeployer };
13
+ export { CoolifyDeployer, type DeployInput, type DeployResult, type DeployStep, type DeployTarget, DokployDeployer, type PaasDeployer, type PaasServer, deployerRegistry, getAvailableDeployers, getDeployer };
14
14
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/deployers/index.ts"],"mappings":";;;;;AA4BA;AAAA,cANa,gBAAA,EAAkB,MAAA,SAAe,YAAA;;iBAM9B,WAAA,CAAY,EAAA,WAAa,YAAA;;iBAKzB,qBAAA,CAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/deployers/index.ts"],"mappings":";;;;;AA6BA;AAAA,cANa,gBAAA,EAAkB,MAAA,SAAe,YAAA;;iBAM9B,WAAA,CAAY,EAAA,WAAa,YAAA;;iBAKzB,qBAAA,CAAA"}
@@ -1,4 +1,4 @@
1
- import { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer } from "./types.mjs";
1
+ import { DeployInput, DeployResult, DeployStep, DeployTarget, PaasDeployer, PaasServer } from "./types.mjs";
2
2
  import { CoolifyDeployer } from "./coolify.mjs";
3
3
  import { DokployDeployer } from "./dokploy.mjs";
4
4
 
@@ -10,5 +10,5 @@ declare function getDeployer(id: string): PaasDeployer | undefined;
10
10
  /** List all available deployer IDs. */
11
11
  declare function getAvailableDeployers(): string[];
12
12
  //#endregion
13
- export { CoolifyDeployer, type DeployInput, type DeployResult, type DeployStep, type DeployTarget, DokployDeployer, type PaasDeployer, deployerRegistry, getAvailableDeployers, getDeployer };
13
+ export { CoolifyDeployer, type DeployInput, type DeployResult, type DeployStep, type DeployTarget, DokployDeployer, type PaasDeployer, type PaasServer, deployerRegistry, getAvailableDeployers, getDeployer };
14
14
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/deployers/index.ts"],"mappings":";;;;;AA4BA;AAAA,cANa,gBAAA,EAAkB,MAAA,SAAe,YAAA;;iBAM9B,WAAA,CAAY,EAAA,WAAa,YAAA;;iBAKzB,qBAAA,CAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/deployers/index.ts"],"mappings":";;;;;AA6BA;AAAA,cANa,gBAAA,EAAkB,MAAA,SAAe,YAAA;;iBAM9B,WAAA,CAAY,EAAA,WAAa,YAAA;;iBAKzB,qBAAA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/deployers/index.ts"],"sourcesContent":["/**\n * PaaS deployer registry — barrel export + lookup helpers.\n *\n * To add a new provider, implement `PaasDeployer` and register it in\n * `deployerRegistry` below.\n */\n\nexport { CoolifyDeployer } from \"./coolify.js\";\nexport { DokployDeployer } from \"./dokploy.js\";\nexport type {\n\tDeployInput,\n\tDeployResult,\n\tDeployStep,\n\tDeployTarget,\n\tPaasDeployer,\n} from \"./types.js\";\n\nimport { CoolifyDeployer } from \"./coolify.js\";\nimport { DokployDeployer } from \"./dokploy.js\";\nimport type { PaasDeployer } from \"./types.js\";\n\n/** Registry of all available PaaS deployers. */\nexport const deployerRegistry: Record<string, PaasDeployer> = {\n\tdokploy: new DokployDeployer(),\n\tcoolify: new CoolifyDeployer(),\n};\n\n/** Get a deployer by ID, or undefined if not found. */\nexport function getDeployer(id: string): PaasDeployer | undefined {\n\treturn deployerRegistry[id];\n}\n\n/** List all available deployer IDs. */\nexport function getAvailableDeployers(): string[] {\n\treturn Object.keys(deployerRegistry);\n}\n"],"mappings":";;;;;AAsBA,MAAa,mBAAiD;CAC7D,SAAS,IAAI,iBAAiB;CAC9B,SAAS,IAAI,iBAAiB;CAC9B;;AAGD,SAAgB,YAAY,IAAsC;AACjE,QAAO,iBAAiB;;;AAIzB,SAAgB,wBAAkC;AACjD,QAAO,OAAO,KAAK,iBAAiB"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/deployers/index.ts"],"sourcesContent":["/**\n * PaaS deployer registry — barrel export + lookup helpers.\n *\n * To add a new provider, implement `PaasDeployer` and register it in\n * `deployerRegistry` below.\n */\n\nexport { CoolifyDeployer } from \"./coolify.js\";\nexport { DokployDeployer } from \"./dokploy.js\";\nexport type {\n\tDeployInput,\n\tDeployResult,\n\tDeployStep,\n\tDeployTarget,\n\tPaasDeployer,\n\tPaasServer,\n} from \"./types.js\";\n\nimport { CoolifyDeployer } from \"./coolify.js\";\nimport { DokployDeployer } from \"./dokploy.js\";\nimport type { PaasDeployer } from \"./types.js\";\n\n/** Registry of all available PaaS deployers. */\nexport const deployerRegistry: Record<string, PaasDeployer> = {\n\tdokploy: new DokployDeployer(),\n\tcoolify: new CoolifyDeployer(),\n};\n\n/** Get a deployer by ID, or undefined if not found. */\nexport function getDeployer(id: string): PaasDeployer | undefined {\n\treturn deployerRegistry[id];\n}\n\n/** List all available deployer IDs. */\nexport function getAvailableDeployers(): string[] {\n\treturn Object.keys(deployerRegistry);\n}\n"],"mappings":";;;;;AAuBA,MAAa,mBAAiD;CAC7D,SAAS,IAAI,iBAAiB;CAC9B,SAAS,IAAI,iBAAiB;CAC9B;;AAGD,SAAgB,YAAY,IAAsC;AACjE,QAAO,iBAAiB;;;AAIzB,SAAgB,wBAAkC;AACjD,QAAO,OAAO,KAAK,iBAAiB"}