@nodetool-ai/deploy 0.7.0-rc.11
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/admin-client.d.ts +58 -0
- package/dist/admin-client.d.ts.map +1 -0
- package/dist/admin-client.js +275 -0
- package/dist/admin-client.js.map +1 -0
- package/dist/admin-operations.d.ts +197 -0
- package/dist/admin-operations.d.ts.map +1 -0
- package/dist/admin-operations.js +433 -0
- package/dist/admin-operations.js.map +1 -0
- package/dist/admin-routes.d.ts +306 -0
- package/dist/admin-routes.d.ts.map +1 -0
- package/dist/admin-routes.js +383 -0
- package/dist/admin-routes.js.map +1 -0
- package/dist/api-user-manager.d.ts +72 -0
- package/dist/api-user-manager.d.ts.map +1 -0
- package/dist/api-user-manager.js +89 -0
- package/dist/api-user-manager.js.map +1 -0
- package/dist/auth.d.ts +67 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +142 -0
- package/dist/auth.js.map +1 -0
- package/dist/collection-routes.d.ts +37 -0
- package/dist/collection-routes.d.ts.map +1 -0
- package/dist/collection-routes.js +43 -0
- package/dist/collection-routes.js.map +1 -0
- package/dist/compose.d.ts +52 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +153 -0
- package/dist/compose.js.map +1 -0
- package/dist/configure.d.ts +87 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +104 -0
- package/dist/configure.js.map +1 -0
- package/dist/deploy-to-gcp.d.ts +67 -0
- package/dist/deploy-to-gcp.d.ts.map +1 -0
- package/dist/deploy-to-gcp.js +268 -0
- package/dist/deploy-to-gcp.js.map +1 -0
- package/dist/deploy-to-runpod.d.ts +53 -0
- package/dist/deploy-to-runpod.d.ts.map +1 -0
- package/dist/deploy-to-runpod.js +207 -0
- package/dist/deploy-to-runpod.js.map +1 -0
- package/dist/deploy.d.ts +54 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +82 -0
- package/dist/deploy.js.map +1 -0
- package/dist/deployment-config.d.ts +1073 -0
- package/dist/deployment-config.d.ts.map +1 -0
- package/dist/deployment-config.js +536 -0
- package/dist/deployment-config.js.map +1 -0
- package/dist/docker-run.d.ts +85 -0
- package/dist/docker-run.d.ts.map +1 -0
- package/dist/docker-run.js +202 -0
- package/dist/docker-run.js.map +1 -0
- package/dist/docker.d.ts +98 -0
- package/dist/docker.d.ts.map +1 -0
- package/dist/docker.js +396 -0
- package/dist/docker.js.map +1 -0
- package/dist/fly.d.ts +40 -0
- package/dist/fly.d.ts.map +1 -0
- package/dist/fly.js +291 -0
- package/dist/fly.js.map +1 -0
- package/dist/gcp.d.ts +59 -0
- package/dist/gcp.d.ts.map +1 -0
- package/dist/gcp.js +339 -0
- package/dist/gcp.js.map +1 -0
- package/dist/google-cloud-run-api.d.ts +134 -0
- package/dist/google-cloud-run-api.d.ts.map +1 -0
- package/dist/google-cloud-run-api.js +532 -0
- package/dist/google-cloud-run-api.js.map +1 -0
- package/dist/huggingface.d.ts +69 -0
- package/dist/huggingface.d.ts.map +1 -0
- package/dist/huggingface.js +346 -0
- package/dist/huggingface.js.map +1 -0
- package/dist/image-builder.d.ts +35 -0
- package/dist/image-builder.d.ts.map +1 -0
- package/dist/image-builder.js +157 -0
- package/dist/image-builder.js.map +1 -0
- package/dist/image-spec.d.ts +41 -0
- package/dist/image-spec.d.ts.map +1 -0
- package/dist/image-spec.js +49 -0
- package/dist/image-spec.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +121 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +225 -0
- package/dist/manager.js.map +1 -0
- package/dist/progress.d.ts +91 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +262 -0
- package/dist/progress.js.map +1 -0
- package/dist/railway.d.ts +65 -0
- package/dist/railway.d.ts.map +1 -0
- package/dist/railway.js +284 -0
- package/dist/railway.js.map +1 -0
- package/dist/remote-users.d.ts +74 -0
- package/dist/remote-users.d.ts.map +1 -0
- package/dist/remote-users.js +172 -0
- package/dist/remote-users.js.map +1 -0
- package/dist/runpod-api.d.ts +206 -0
- package/dist/runpod-api.d.ts.map +1 -0
- package/dist/runpod-api.js +703 -0
- package/dist/runpod-api.js.map +1 -0
- package/dist/runpod.d.ts +82 -0
- package/dist/runpod.d.ts.map +1 -0
- package/dist/runpod.js +188 -0
- package/dist/runpod.js.map +1 -0
- package/dist/self-hosted.d.ts +157 -0
- package/dist/self-hosted.d.ts.map +1 -0
- package/dist/self-hosted.js +799 -0
- package/dist/self-hosted.js.map +1 -0
- package/dist/ssh.d.ts +166 -0
- package/dist/ssh.d.ts.map +1 -0
- package/dist/ssh.js +430 -0
- package/dist/ssh.js.map +1 -0
- package/dist/state.d.ts +83 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +368 -0
- package/dist/state.js.map +1 -0
- package/dist/storage-routes.d.ts +55 -0
- package/dist/storage-routes.d.ts.map +1 -0
- package/dist/storage-routes.js +160 -0
- package/dist/storage-routes.js.map +1 -0
- package/dist/sync.d.ts +38 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +130 -0
- package/dist/sync.js.map +1 -0
- package/dist/workflow-syncer.d.ts +51 -0
- package/dist/workflow-syncer.d.ts.map +1 -0
- package/dist/workflow-syncer.js +192 -0
- package/dist/workflow-syncer.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-hosted deployment implementation for NodeTool.
|
|
3
|
+
*
|
|
4
|
+
* This module handles deployment to self-hosted servers via SSH or locally, including:
|
|
5
|
+
* - Docker run command generation
|
|
6
|
+
* - Remote/local directory setup
|
|
7
|
+
* - Single container orchestration
|
|
8
|
+
* - Health monitoring
|
|
9
|
+
* - Localhost detection (skips SSH for localhost deployments)
|
|
10
|
+
*/
|
|
11
|
+
import { execSync, execFileSync } from "child_process";
|
|
12
|
+
import * as dns from "dns";
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as os from "os";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import { SSHCommandError, SSHConnection } from "./ssh.js";
|
|
17
|
+
import { DockerRunGenerator } from "./docker-run.js";
|
|
18
|
+
import { DeploymentStatus, dockerDeploymentGetServerUrl, imageConfigFullName } from "./deployment-config.js";
|
|
19
|
+
import { StateManager } from "./state.js";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Utilities
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Quote a string for use in a shell command, but allow ~ at the start.
|
|
25
|
+
* This ensures that the shell can still expand the home directory.
|
|
26
|
+
*/
|
|
27
|
+
export function safeShellQuote(s) {
|
|
28
|
+
if (s.startsWith("~/")) {
|
|
29
|
+
return "~/" + shellQuote(s.slice(2));
|
|
30
|
+
}
|
|
31
|
+
return shellQuote(s);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Simple POSIX shell quoting: wraps in single-quotes, escaping existing
|
|
35
|
+
* single-quotes by ending the quote, inserting an escaped single-quote,
|
|
36
|
+
* then re-opening the quote.
|
|
37
|
+
*/
|
|
38
|
+
function shellQuote(s) {
|
|
39
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Expand a leading ~ to the user's home directory.
|
|
43
|
+
*/
|
|
44
|
+
function expandUser(p) {
|
|
45
|
+
if (p === "~" || p.startsWith("~/")) {
|
|
46
|
+
return path.join(os.homedir(), p.slice(1));
|
|
47
|
+
}
|
|
48
|
+
return p;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if host resolves to the local machine.
|
|
52
|
+
*/
|
|
53
|
+
export function isLocalhost(host) {
|
|
54
|
+
const localhostNames = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
55
|
+
const normalizedHost = host.trim().toLowerCase();
|
|
56
|
+
if (localhostNames.has(normalizedHost)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function resolveIps(target) {
|
|
60
|
+
const ips = new Set();
|
|
61
|
+
try {
|
|
62
|
+
// Synchronous DNS lookup via Node.js dns.resolve
|
|
63
|
+
const addresses = dns.resolve4.length
|
|
64
|
+
? (() => {
|
|
65
|
+
try {
|
|
66
|
+
// Use execFileSync with getent (Linux) as primary method
|
|
67
|
+
const result = execFileSync("getent", ["hosts", target], {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
timeout: 5000
|
|
70
|
+
}).trim();
|
|
71
|
+
const resolved = [];
|
|
72
|
+
for (const line of result.split("\n")) {
|
|
73
|
+
const ip = line.trim().split(/\s+/)[0];
|
|
74
|
+
if (ip)
|
|
75
|
+
resolved.push(ip);
|
|
76
|
+
}
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// getent not available (macOS) — fall back to dscacheutil or just skip
|
|
81
|
+
try {
|
|
82
|
+
const result = execFileSync("dscacheutil", ["-q", "host", "-a", "name", target], { encoding: "utf-8", timeout: 5000 }).trim();
|
|
83
|
+
const resolved = [];
|
|
84
|
+
for (const line of result.split("\n")) {
|
|
85
|
+
const match = line.match(/^ip_address:\s+(.+)/);
|
|
86
|
+
if (match)
|
|
87
|
+
resolved.push(match[1].trim());
|
|
88
|
+
}
|
|
89
|
+
return resolved;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
})()
|
|
96
|
+
: [];
|
|
97
|
+
for (const ip of addresses)
|
|
98
|
+
ips.add(ip);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Unable to resolve
|
|
102
|
+
}
|
|
103
|
+
return ips;
|
|
104
|
+
}
|
|
105
|
+
let hostIps;
|
|
106
|
+
try {
|
|
107
|
+
hostIps = resolveIps(host);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (hostIps.size === 0)
|
|
113
|
+
return false;
|
|
114
|
+
const localIps = new Set();
|
|
115
|
+
for (const target of ["localhost", os.hostname()]) {
|
|
116
|
+
try {
|
|
117
|
+
for (const ip of resolveIps(target))
|
|
118
|
+
localIps.add(ip);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const ip of hostIps) {
|
|
125
|
+
if (localIps.has(ip))
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// LocalExecutor
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* Executes commands locally (mimics SSHConnection interface).
|
|
135
|
+
*/
|
|
136
|
+
export class LocalExecutor {
|
|
137
|
+
/**
|
|
138
|
+
* Open the executor (no-op for local).
|
|
139
|
+
* Provided for symmetry with SSHConnection.
|
|
140
|
+
*/
|
|
141
|
+
open() {
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Close the executor (no-op for local).
|
|
146
|
+
*/
|
|
147
|
+
close() {
|
|
148
|
+
// nothing to do
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Execute a command locally.
|
|
152
|
+
*
|
|
153
|
+
* Matches SSHConnection behavior by running in a shell.
|
|
154
|
+
* This allows shell features like pipes, redirects, &&/|| operators,
|
|
155
|
+
* and environment variable assignments.
|
|
156
|
+
*/
|
|
157
|
+
async execute(command, check = true, timeout) {
|
|
158
|
+
try {
|
|
159
|
+
const result = execSync(command, {
|
|
160
|
+
shell: "/bin/bash",
|
|
161
|
+
encoding: "utf-8",
|
|
162
|
+
timeout: timeout ? timeout * 1000 : undefined,
|
|
163
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
164
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
165
|
+
});
|
|
166
|
+
return [0, result ?? "", ""];
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const e = err;
|
|
170
|
+
const exitCode = e.status ?? -1;
|
|
171
|
+
const stdout = typeof e.stdout === "string" ? e.stdout : "";
|
|
172
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : "";
|
|
173
|
+
// Timeout
|
|
174
|
+
if (e.killed || e.signal === "SIGTERM") {
|
|
175
|
+
throw new SSHCommandError(`Command timed out: ${command}`, -1, stdout, stderr);
|
|
176
|
+
}
|
|
177
|
+
if (check && exitCode !== 0) {
|
|
178
|
+
throw new SSHCommandError(`Command failed: ${command}`, exitCode, stdout, stderr);
|
|
179
|
+
}
|
|
180
|
+
return [exitCode, stdout, stderr];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Create a directory locally.
|
|
185
|
+
*/
|
|
186
|
+
async mkdir(dirPath, _mode = 0o755, parents = true) {
|
|
187
|
+
const expandedPath = expandUser(dirPath);
|
|
188
|
+
if (parents) {
|
|
189
|
+
fs.mkdirSync(expandedPath, { recursive: true, mode: _mode });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
fs.mkdirSync(expandedPath, { mode: _mode });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// BaseSSHDeployer (abstract)
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
/**
|
|
200
|
+
* Base class for SSH-based deployments.
|
|
201
|
+
*
|
|
202
|
+
* Type parameter `T` is the concrete deployment configuration type
|
|
203
|
+
* (e.g. DockerDeployment).
|
|
204
|
+
*/
|
|
205
|
+
export class BaseSSHDeployer {
|
|
206
|
+
deploymentName;
|
|
207
|
+
deployment;
|
|
208
|
+
stateManager;
|
|
209
|
+
isLocalhost;
|
|
210
|
+
constructor(deploymentName, deployment, stateManager) {
|
|
211
|
+
this.deploymentName = deploymentName;
|
|
212
|
+
this.deployment = deployment;
|
|
213
|
+
this.stateManager = stateManager ?? new StateManager();
|
|
214
|
+
this.isLocalhost = isLocalhost(deployment.host);
|
|
215
|
+
}
|
|
216
|
+
// ---- Logging -----------------------------------------------------------
|
|
217
|
+
log(results, message) {
|
|
218
|
+
const steps = results["steps"];
|
|
219
|
+
if (Array.isArray(steps)) {
|
|
220
|
+
steps.push(message);
|
|
221
|
+
}
|
|
222
|
+
console.log(` ${message}`);
|
|
223
|
+
}
|
|
224
|
+
// ---- Executor ----------------------------------------------------------
|
|
225
|
+
getExecutor() {
|
|
226
|
+
if (this.isLocalhost) {
|
|
227
|
+
return new LocalExecutor();
|
|
228
|
+
}
|
|
229
|
+
const sshConfig = this.deployment.ssh;
|
|
230
|
+
if (!sshConfig) {
|
|
231
|
+
throw new Error(`SSH configuration is required for remote host: ${this.deployment.host}`);
|
|
232
|
+
}
|
|
233
|
+
const conn = new SSHConnection({
|
|
234
|
+
host: this.deployment.host,
|
|
235
|
+
user: sshConfig.user,
|
|
236
|
+
keyPath: sshConfig.key_path,
|
|
237
|
+
password: sshConfig.password,
|
|
238
|
+
port: sshConfig.port
|
|
239
|
+
});
|
|
240
|
+
// Wrap SSHConnection to match the async Executor interface.
|
|
241
|
+
return {
|
|
242
|
+
execute(command, check = true, timeout) {
|
|
243
|
+
return conn.execute(command, { check, timeout });
|
|
244
|
+
},
|
|
245
|
+
mkdir(dirPath, mode = 0o755, parents = true) {
|
|
246
|
+
return conn.mkdir(dirPath, mode, parents);
|
|
247
|
+
},
|
|
248
|
+
_sshConnection: conn
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Use an executor within a callback, ensuring proper open/close lifecycle.
|
|
253
|
+
*/
|
|
254
|
+
async withExecutor(fn) {
|
|
255
|
+
const executor = this.getExecutor();
|
|
256
|
+
const sshConn = executor
|
|
257
|
+
._sshConnection;
|
|
258
|
+
if (sshConn) {
|
|
259
|
+
await sshConn.connect();
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// LocalExecutor
|
|
263
|
+
executor.open();
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
return await fn(executor);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
if (sshConn) {
|
|
270
|
+
sshConn.disconnect();
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
executor.close();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// ---- Content upload ----------------------------------------------------
|
|
278
|
+
async uploadContent(ssh, content, remotePath) {
|
|
279
|
+
if (this.isLocalhost) {
|
|
280
|
+
const expanded = remotePath.startsWith("~")
|
|
281
|
+
? expandUser(remotePath)
|
|
282
|
+
: remotePath;
|
|
283
|
+
const dir = path.dirname(expanded);
|
|
284
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
285
|
+
fs.writeFileSync(expanded, content, "utf-8");
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// Remote upload via base64 to avoid shell escaping issues
|
|
289
|
+
const b64Content = Buffer.from(content, "utf-8").toString("base64");
|
|
290
|
+
const dirName = path.posix.dirname(remotePath);
|
|
291
|
+
await ssh.execute(`mkdir -p ${safeShellQuote(dirName)}`, true);
|
|
292
|
+
await ssh.execute(`echo ${b64Content} | base64 -d > ${safeShellQuote(remotePath)}`, true);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// ---- Directory creation ------------------------------------------------
|
|
296
|
+
async createDirectories(ssh, results) {
|
|
297
|
+
this.log(results, "Creating directories...");
|
|
298
|
+
const workspacePath = expandUser(this.deployment.paths.workspace);
|
|
299
|
+
await ssh.mkdir(workspacePath, 0o755, true);
|
|
300
|
+
this.log(results, ` Created: ${workspacePath}`);
|
|
301
|
+
await ssh.mkdir(`${workspacePath}/data`, 0o755, true);
|
|
302
|
+
await ssh.mkdir(`${workspacePath}/assets`, 0o755, true);
|
|
303
|
+
await ssh.mkdir(`${workspacePath}/temp`, 0o755, true);
|
|
304
|
+
// Deployment-specific directories
|
|
305
|
+
await this.createSpecificDirectories(ssh, workspacePath);
|
|
306
|
+
const hfCachePath = expandUser(this.deployment.paths.hf_cache);
|
|
307
|
+
await ssh.mkdir(hfCachePath, 0o755, true);
|
|
308
|
+
this.log(results, ` Created: ${hfCachePath}`);
|
|
309
|
+
}
|
|
310
|
+
// ---- Container runtime helpers -----------------------------------------
|
|
311
|
+
resolveLocalRuntimeCommand() {
|
|
312
|
+
const override = process.env["NODETOOL_CONTAINER_RUNTIME"];
|
|
313
|
+
if (override === "docker" || override === "podman")
|
|
314
|
+
return override;
|
|
315
|
+
try {
|
|
316
|
+
execFileSync("which", ["docker"], { encoding: "utf-8" });
|
|
317
|
+
return "docker";
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// docker not found
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
execFileSync("which", ["podman"], { encoding: "utf-8" });
|
|
324
|
+
return "podman";
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// podman not found
|
|
328
|
+
}
|
|
329
|
+
return "docker";
|
|
330
|
+
}
|
|
331
|
+
runtimeCommandForShell() {
|
|
332
|
+
const override = process.env["NODETOOL_CONTAINER_RUNTIME"];
|
|
333
|
+
if (override === "docker" || override === "podman")
|
|
334
|
+
return override;
|
|
335
|
+
if (this.isLocalhost)
|
|
336
|
+
return this.resolveLocalRuntimeCommand();
|
|
337
|
+
return "$((command -v docker >/dev/null 2>&1 && echo docker) || (command -v podman >/dev/null 2>&1 && echo podman) || echo docker)";
|
|
338
|
+
}
|
|
339
|
+
containerGenerator(runtimeCommand) {
|
|
340
|
+
const cmd = runtimeCommand ?? this.runtimeCommandForShell();
|
|
341
|
+
const d = this.deployment;
|
|
342
|
+
const converted = {
|
|
343
|
+
image: d.image,
|
|
344
|
+
container: d.container,
|
|
345
|
+
paths: {
|
|
346
|
+
workspace: d.paths.workspace,
|
|
347
|
+
hfCache: d.paths.hf_cache
|
|
348
|
+
},
|
|
349
|
+
persistentPaths: d.persistent_paths
|
|
350
|
+
? {
|
|
351
|
+
usersFile: d.persistent_paths.users_file,
|
|
352
|
+
dbPath: d.persistent_paths.db_path,
|
|
353
|
+
chromaPath: d.persistent_paths.chroma_path,
|
|
354
|
+
hfCache: d.persistent_paths.hf_cache,
|
|
355
|
+
assetBucket: d.persistent_paths.asset_bucket,
|
|
356
|
+
logsPath: d.persistent_paths.logs_path
|
|
357
|
+
}
|
|
358
|
+
: undefined,
|
|
359
|
+
serverAuthToken: d.server_auth_token
|
|
360
|
+
};
|
|
361
|
+
return new DockerRunGenerator(converted, cmd);
|
|
362
|
+
}
|
|
363
|
+
containerName() {
|
|
364
|
+
return this.containerGenerator().getContainerName();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Return host port for direct app mode (matches DockerRunGenerator).
|
|
368
|
+
*/
|
|
369
|
+
appHostPort() {
|
|
370
|
+
const container = this.deployment.container;
|
|
371
|
+
if (container && container.port === 7777)
|
|
372
|
+
return 8000;
|
|
373
|
+
return container?.port ?? 8000;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// DockerDeployer
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
/**
|
|
380
|
+
* Deployer for Docker-based self-hosted deployments.
|
|
381
|
+
*/
|
|
382
|
+
export class DockerDeployer extends BaseSSHDeployer {
|
|
383
|
+
async createSpecificDirectories(ssh, workspacePath) {
|
|
384
|
+
await ssh.mkdir(`${workspacePath}/proxy`, 0o755, true);
|
|
385
|
+
await ssh.mkdir(`${workspacePath}/acme`, 0o755, true);
|
|
386
|
+
}
|
|
387
|
+
async plan() {
|
|
388
|
+
const plan = {
|
|
389
|
+
deployment_name: this.deploymentName,
|
|
390
|
+
host: this.deployment.host,
|
|
391
|
+
type: "docker",
|
|
392
|
+
changes: [],
|
|
393
|
+
will_create: [],
|
|
394
|
+
will_update: [],
|
|
395
|
+
will_destroy: []
|
|
396
|
+
};
|
|
397
|
+
const generator = this.containerGenerator();
|
|
398
|
+
const containerName = generator.getContainerName();
|
|
399
|
+
const currentState = await this.stateManager.readState(this.deploymentName);
|
|
400
|
+
const currentHash = currentState
|
|
401
|
+
? currentState["container_run_hash"]
|
|
402
|
+
: undefined;
|
|
403
|
+
const newHash = generator.generateHash();
|
|
404
|
+
if (!currentState || !currentState["last_deployed"]) {
|
|
405
|
+
plan.changes.push("Initial deployment - will create all resources");
|
|
406
|
+
plan.will_create.push(`App container: ${containerName}`);
|
|
407
|
+
}
|
|
408
|
+
else if (currentHash !== newHash) {
|
|
409
|
+
plan.changes.push("Container configuration has changed");
|
|
410
|
+
plan.will_update.push("App container");
|
|
411
|
+
}
|
|
412
|
+
plan.will_create.push(`Directory: ${this.deployment.paths.workspace}`, `Directory: ${this.deployment.paths.hf_cache}`, `Container: ${containerName}`);
|
|
413
|
+
return plan;
|
|
414
|
+
}
|
|
415
|
+
async apply(opts) {
|
|
416
|
+
if (opts?.dryRun) {
|
|
417
|
+
return (await this.plan());
|
|
418
|
+
}
|
|
419
|
+
const results = {
|
|
420
|
+
deployment_name: this.deploymentName,
|
|
421
|
+
status: "success",
|
|
422
|
+
steps: [],
|
|
423
|
+
errors: []
|
|
424
|
+
};
|
|
425
|
+
if (this.isLocalhost) {
|
|
426
|
+
results.steps.push("Deploying to localhost (skipping SSH)");
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
await this.stateManager.updateDeploymentStatus(this.deploymentName, DeploymentStatus.DEPLOYING);
|
|
430
|
+
await this.withExecutor(async (executor) => {
|
|
431
|
+
await this.createDirectories(executor, results);
|
|
432
|
+
await this.ensureImage(executor, results);
|
|
433
|
+
// Stop existing container if present
|
|
434
|
+
await this.stopExistingContainer(executor, results);
|
|
435
|
+
// Start container
|
|
436
|
+
const containerRunHash = await this.startContainer(executor, results);
|
|
437
|
+
// Check health
|
|
438
|
+
await this.checkHealth(executor, results);
|
|
439
|
+
// Update state with success
|
|
440
|
+
const containerName = this.containerName();
|
|
441
|
+
await this.stateManager.writeState(this.deploymentName, {
|
|
442
|
+
status: DeploymentStatus.RUNNING,
|
|
443
|
+
container_run_hash: containerRunHash,
|
|
444
|
+
container_name: containerName,
|
|
445
|
+
container_id: null,
|
|
446
|
+
url: dockerDeploymentGetServerUrl(this.deployment)
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
results.status = "error";
|
|
452
|
+
results.errors.push(String(e));
|
|
453
|
+
await this.stateManager.updateDeploymentStatus(this.deploymentName, DeploymentStatus.ERROR);
|
|
454
|
+
throw e;
|
|
455
|
+
}
|
|
456
|
+
return results;
|
|
457
|
+
}
|
|
458
|
+
// ---- Private helpers ---------------------------------------------------
|
|
459
|
+
async stopExistingContainer(ssh, results) {
|
|
460
|
+
results.steps.push("Checking for existing app container...");
|
|
461
|
+
const containerName = this.containerName();
|
|
462
|
+
if (this.isLocalDockerRuntime()) {
|
|
463
|
+
await this.stopExistingContainerWithShell(ssh, containerName, results);
|
|
464
|
+
await this.stopLocalPortConflictsWithShell(ssh, containerName, this.appHostPort(), results);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const runtime = this.runtimeCommandForShell();
|
|
468
|
+
const checkCommand = `${runtime} ps -a -q -f name=${safeShellQuote(containerName)}`;
|
|
469
|
+
try {
|
|
470
|
+
const [, stdout] = await ssh.execute(checkCommand, false);
|
|
471
|
+
if (stdout.trim()) {
|
|
472
|
+
results.steps.push(` Found existing app container: ${containerName}`);
|
|
473
|
+
await ssh.execute(`${runtime} stop ${safeShellQuote(containerName)}`, false, 60);
|
|
474
|
+
results.steps.push(` Stopped app container: ${containerName}`);
|
|
475
|
+
await ssh.execute(`${runtime} rm ${safeShellQuote(containerName)}`, false, 60);
|
|
476
|
+
results.steps.push(` Removed app container: ${containerName}`);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
results.steps.push(" No existing app container found");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (exc) {
|
|
483
|
+
results.steps.push(` Warning: could not inspect app container: ${exc}`);
|
|
484
|
+
}
|
|
485
|
+
// Clean up legacy NodeTool containers that still hold the same host port
|
|
486
|
+
const publishCheck = `${runtime} ps -a --filter publish=${this.appHostPort()} --format '{{.Names}}'`;
|
|
487
|
+
try {
|
|
488
|
+
const [, stdout] = await ssh.execute(publishCheck, false);
|
|
489
|
+
for (const line of stdout.split("\n")) {
|
|
490
|
+
const conflictName = line.trim();
|
|
491
|
+
if (!conflictName)
|
|
492
|
+
continue;
|
|
493
|
+
if (conflictName === containerName)
|
|
494
|
+
continue;
|
|
495
|
+
if (!conflictName.startsWith("nodetool-"))
|
|
496
|
+
continue;
|
|
497
|
+
results.steps.push(` Found conflicting NodeTool container on port ${this.appHostPort()}: ${conflictName}`);
|
|
498
|
+
await ssh.execute(`${runtime} stop ${safeShellQuote(conflictName)}`, false, 60);
|
|
499
|
+
await ssh.execute(`${runtime} rm ${safeShellQuote(conflictName)}`, false, 60);
|
|
500
|
+
results.steps.push(` Removed conflicting container: ${conflictName}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch (exc) {
|
|
504
|
+
results.steps.push(` Warning: could not check port conflicts: ${exc}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async stopExistingContainerWithShell(ssh, containerName, results) {
|
|
508
|
+
const runtime = this.runtimeCommandForShell();
|
|
509
|
+
try {
|
|
510
|
+
const [, stdout] = await ssh.execute(`${runtime} ps -a -q -f name=${safeShellQuote(containerName)}`, false);
|
|
511
|
+
if (!stdout.trim()) {
|
|
512
|
+
results.steps.push(" No existing app container found");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
results.steps.push(` Found existing app container: ${containerName}`);
|
|
516
|
+
try {
|
|
517
|
+
await ssh.execute(`${runtime} stop ${safeShellQuote(containerName)}`, false, 60);
|
|
518
|
+
results.steps.push(` Stopped app container: ${containerName}`);
|
|
519
|
+
}
|
|
520
|
+
catch (exc) {
|
|
521
|
+
results.steps.push(` Warning: failed stopping app container: ${exc}`);
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
await ssh.execute(`${runtime} rm ${safeShellQuote(containerName)}`, false, 60);
|
|
525
|
+
results.steps.push(` Removed app container: ${containerName}`);
|
|
526
|
+
}
|
|
527
|
+
catch (exc) {
|
|
528
|
+
results.steps.push(` Warning: failed removing app container: ${exc}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (exc) {
|
|
532
|
+
results.steps.push(` Warning: could not inspect app container: ${exc}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async stopLocalPortConflictsWithShell(ssh, containerName, hostPort, results) {
|
|
536
|
+
const runtime = this.runtimeCommandForShell();
|
|
537
|
+
try {
|
|
538
|
+
const publishCheck = `${runtime} ps -a --filter publish=${hostPort} --format '{{.Names}}'`;
|
|
539
|
+
const [, stdout] = await ssh.execute(publishCheck, false);
|
|
540
|
+
for (const line of stdout.split("\n")) {
|
|
541
|
+
const conflictName = line.trim();
|
|
542
|
+
if (!conflictName)
|
|
543
|
+
continue;
|
|
544
|
+
if (conflictName === containerName)
|
|
545
|
+
continue;
|
|
546
|
+
if (!conflictName.startsWith("nodetool-"))
|
|
547
|
+
continue;
|
|
548
|
+
results.steps.push(` Found conflicting NodeTool container on port ${hostPort}: ${conflictName}`);
|
|
549
|
+
try {
|
|
550
|
+
await ssh.execute(`${runtime} stop ${safeShellQuote(conflictName)}`, false, 60);
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// ignore
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
await ssh.execute(`${runtime} rm -f ${safeShellQuote(conflictName)}`, false, 60);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
// ignore
|
|
560
|
+
}
|
|
561
|
+
results.steps.push(` Removed conflicting container: ${conflictName}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch (exc) {
|
|
565
|
+
results.steps.push(` Warning: could not check port conflicts: ${exc}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async startContainer(ssh, results) {
|
|
569
|
+
results.steps.push("Starting app container...");
|
|
570
|
+
const generator = this.containerGenerator();
|
|
571
|
+
const command = generator.generateCommand();
|
|
572
|
+
const containerHash = generator.generateHash();
|
|
573
|
+
results.steps.push(` Command: ${command.slice(0, 120)}...`);
|
|
574
|
+
try {
|
|
575
|
+
const [, stdout] = await ssh.execute(command, true, 300);
|
|
576
|
+
const containerId = stdout.trim() || "<unknown>";
|
|
577
|
+
results.steps.push(` App container started: ${containerId.slice(0, 12)}`);
|
|
578
|
+
}
|
|
579
|
+
catch (exc) {
|
|
580
|
+
if (exc instanceof SSHCommandError) {
|
|
581
|
+
results.errors.push(`Failed to start app container: ${exc.stderr}`);
|
|
582
|
+
}
|
|
583
|
+
throw exc;
|
|
584
|
+
}
|
|
585
|
+
return containerHash;
|
|
586
|
+
}
|
|
587
|
+
async checkHealth(ssh, results) {
|
|
588
|
+
results.steps.push("Checking app health...");
|
|
589
|
+
const containerName = this.containerName();
|
|
590
|
+
const healthUrl = `http://127.0.0.1:${this.appHostPort()}/health`;
|
|
591
|
+
const maxAttempts = 10;
|
|
592
|
+
let lastErrors = [];
|
|
593
|
+
await sleep(2);
|
|
594
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
595
|
+
const attemptErrors = [];
|
|
596
|
+
const containerStatus = await this.getContainerStatus(ssh, containerName);
|
|
597
|
+
if (containerStatus) {
|
|
598
|
+
results.steps.push(` Container status: ${containerStatus}`);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
attemptErrors.push("app container not running");
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
await ssh.execute(`curl -fsS ${safeShellQuote(healthUrl)}`, true, 20);
|
|
605
|
+
if (attemptErrors.length === 0) {
|
|
606
|
+
results.steps.push(` Health endpoint OK: ${healthUrl}`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (exc) {
|
|
611
|
+
const err = exc instanceof SSHCommandError
|
|
612
|
+
? (exc.stderr || String(exc)).trim()
|
|
613
|
+
: String(exc).trim();
|
|
614
|
+
attemptErrors.push(`health check failed: ${err}`);
|
|
615
|
+
}
|
|
616
|
+
lastErrors = attemptErrors;
|
|
617
|
+
if (attempt < maxAttempts) {
|
|
618
|
+
results.steps.push(` Waiting for app startup (attempt ${attempt}/${maxAttempts})...`);
|
|
619
|
+
await sleep(2);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const err of lastErrors) {
|
|
623
|
+
results.steps.push(` Warning: ${err}`);
|
|
624
|
+
}
|
|
625
|
+
results.errors.push(...lastErrors);
|
|
626
|
+
throw new Error(`Deployment health check failed: ${lastErrors.join("; ")}`);
|
|
627
|
+
}
|
|
628
|
+
async getContainerStatus(ssh, containerName) {
|
|
629
|
+
const runtime = this.runtimeCommandForShell();
|
|
630
|
+
const statusCmd = `${runtime} ps -f name=${safeShellQuote(containerName)} --format '{{.Names}} {{.Status}} {{.Ports}}'`;
|
|
631
|
+
try {
|
|
632
|
+
const [, stdout] = await ssh.execute(statusCmd, false);
|
|
633
|
+
return stdout.trim();
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return "";
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
isLocalDockerRuntime() {
|
|
640
|
+
return this.isLocalhost && this.resolveLocalRuntimeCommand() === "docker";
|
|
641
|
+
}
|
|
642
|
+
async destroy() {
|
|
643
|
+
const results = {
|
|
644
|
+
deployment_name: this.deploymentName,
|
|
645
|
+
status: "success",
|
|
646
|
+
steps: [],
|
|
647
|
+
errors: []
|
|
648
|
+
};
|
|
649
|
+
try {
|
|
650
|
+
await this.withExecutor(async (ssh) => {
|
|
651
|
+
const containerName = this.containerName();
|
|
652
|
+
const runtime = this.runtimeCommandForShell();
|
|
653
|
+
// Stop container
|
|
654
|
+
try {
|
|
655
|
+
await ssh.execute(`${runtime} stop ${safeShellQuote(containerName)}`, false, 30);
|
|
656
|
+
results.steps.push(`Container stopped: ${containerName}`);
|
|
657
|
+
}
|
|
658
|
+
catch (e) {
|
|
659
|
+
if (e instanceof SSHCommandError) {
|
|
660
|
+
results.steps.push(`Warning: Failed to stop container: ${e.stderr}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Remove container
|
|
664
|
+
try {
|
|
665
|
+
await ssh.execute(`${runtime} rm ${safeShellQuote(containerName)}`, false, 30);
|
|
666
|
+
results.steps.push(`Container removed: ${containerName}`);
|
|
667
|
+
}
|
|
668
|
+
catch (e) {
|
|
669
|
+
if (e instanceof SSHCommandError) {
|
|
670
|
+
results.errors.push(`Failed to remove container: ${e.stderr}`);
|
|
671
|
+
}
|
|
672
|
+
throw e;
|
|
673
|
+
}
|
|
674
|
+
// Update state
|
|
675
|
+
await this.stateManager.updateDeploymentStatus(this.deploymentName, DeploymentStatus.DESTROYED);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
catch (e) {
|
|
679
|
+
results.status = "error";
|
|
680
|
+
results.errors.push(String(e));
|
|
681
|
+
throw e;
|
|
682
|
+
}
|
|
683
|
+
return results;
|
|
684
|
+
}
|
|
685
|
+
async status() {
|
|
686
|
+
const statusInfo = {
|
|
687
|
+
deployment_name: this.deploymentName,
|
|
688
|
+
host: this.deployment.host,
|
|
689
|
+
container_name: this.containerName(),
|
|
690
|
+
type: "docker"
|
|
691
|
+
};
|
|
692
|
+
const state = await this.stateManager.readState(this.deploymentName);
|
|
693
|
+
if (state) {
|
|
694
|
+
statusInfo.status = state["status"] ?? "unknown";
|
|
695
|
+
statusInfo.last_deployed =
|
|
696
|
+
state["last_deployed"] ?? "unknown";
|
|
697
|
+
statusInfo.url = state["url"] ?? "unknown";
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
await this.withExecutor(async (ssh) => {
|
|
701
|
+
const containerName = this.containerName();
|
|
702
|
+
const runtime = this.runtimeCommandForShell();
|
|
703
|
+
const command = `${runtime} ps -a -f name=${safeShellQuote(containerName)} --format '{{.Status}}'`;
|
|
704
|
+
const [, stdout] = await ssh.execute(command, false);
|
|
705
|
+
statusInfo.live_status = stdout.trim() || "Container not found";
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
catch (e) {
|
|
709
|
+
statusInfo.live_status_error = String(e);
|
|
710
|
+
}
|
|
711
|
+
return statusInfo;
|
|
712
|
+
}
|
|
713
|
+
async logs(opts) {
|
|
714
|
+
const follow = opts?.follow ?? false;
|
|
715
|
+
const tail = opts?.tail ?? 100;
|
|
716
|
+
return this.withExecutor(async (ssh) => {
|
|
717
|
+
const containerName = this.containerName();
|
|
718
|
+
const runtime = this.runtimeCommandForShell();
|
|
719
|
+
let command = `${runtime} logs --tail=${tail}`;
|
|
720
|
+
if (follow) {
|
|
721
|
+
command += " -f";
|
|
722
|
+
}
|
|
723
|
+
command += ` ${safeShellQuote(containerName)}`;
|
|
724
|
+
const [, stdout] = await ssh.execute(command, false, follow ? undefined : 30);
|
|
725
|
+
return stdout;
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
async ensureImage(ssh, results) {
|
|
729
|
+
const image = imageConfigFullName(this.deployment.image);
|
|
730
|
+
this.log(results, `Checking image: ${image}`);
|
|
731
|
+
const runtime = this.runtimeCommandForShell();
|
|
732
|
+
const cmd = `${runtime} images -q ${safeShellQuote(image)}`;
|
|
733
|
+
const [, stdout] = await ssh.execute(cmd, false);
|
|
734
|
+
if (stdout.trim()) {
|
|
735
|
+
this.log(results, " Image already present.");
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (this.isLocalhost) {
|
|
739
|
+
throw new Error(`Image '${image}' not found locally. ` +
|
|
740
|
+
"Pull or build it explicitly before running deploy apply.");
|
|
741
|
+
}
|
|
742
|
+
results.steps.push(" Image missing on host; pushing from local Docker daemon...");
|
|
743
|
+
this.pushImageToRemote(image);
|
|
744
|
+
const [, stdoutAfter] = await ssh.execute(cmd, false);
|
|
745
|
+
if (stdoutAfter.trim()) {
|
|
746
|
+
results.steps.push(" Image transferred successfully.");
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
throw new Error(`Failed to transfer image '${image}' to remote host.`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
pushImageToRemote(image) {
|
|
753
|
+
const sshConfig = this.deployment.ssh;
|
|
754
|
+
if (!sshConfig) {
|
|
755
|
+
throw new Error("SSH configuration required to push image.");
|
|
756
|
+
}
|
|
757
|
+
const localRuntime = this.resolveLocalRuntimeCommand();
|
|
758
|
+
// Check image exists locally
|
|
759
|
+
try {
|
|
760
|
+
execFileSync(localRuntime, ["image", "inspect", image], {
|
|
761
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
throw new Error(`Image '${image}' not found locally. Build or pull it before deploying.`);
|
|
766
|
+
}
|
|
767
|
+
// Pipe docker save through ssh docker load using a single shell command
|
|
768
|
+
try {
|
|
769
|
+
const keyArg = sshConfig.key_path
|
|
770
|
+
? `-i ${shellQuote(expandUser(sshConfig.key_path))}`
|
|
771
|
+
: "";
|
|
772
|
+
const portArg = sshConfig.port && sshConfig.port !== 22
|
|
773
|
+
? `-p ${safeShellQuote(String(sshConfig.port))}`
|
|
774
|
+
: "";
|
|
775
|
+
const sshTarget = safeShellQuote(`${sshConfig.user}@${this.deployment.host}`);
|
|
776
|
+
const runtimeForShell = this.runtimeCommandForShell();
|
|
777
|
+
const pipeCmd = `${shellQuote(localRuntime)} save ${shellQuote(image)} | ssh -o StrictHostKeyChecking=no ${keyArg} ${portArg} ${sshTarget} sh -lc '${runtimeForShell} load'`;
|
|
778
|
+
execSync(pipeCmd, {
|
|
779
|
+
encoding: "utf-8",
|
|
780
|
+
timeout: 600000,
|
|
781
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
const e = err;
|
|
786
|
+
throw new Error(`Failed to push image to remote host: ${e.stderr?.trim() || e.message || "unknown error"}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// ---------------------------------------------------------------------------
|
|
791
|
+
// Helpers
|
|
792
|
+
// ---------------------------------------------------------------------------
|
|
793
|
+
/**
|
|
794
|
+
* Async sleep helper — returns a promise that resolves after the given seconds.
|
|
795
|
+
*/
|
|
796
|
+
async function sleep(seconds) {
|
|
797
|
+
return new Promise((r) => setTimeout(r, seconds * 1000));
|
|
798
|
+
}
|
|
799
|
+
//# sourceMappingURL=self-hosted.js.map
|