Release ForgeFlow 0.6.0

This commit is contained in:
NuklearRabbit
2026-07-25 05:59:07 +02:00
parent cf1f67a823
commit 9d3933c878
48 changed files with 2208 additions and 466 deletions
+58
View File
@@ -2,6 +2,7 @@
const fs = require('node:fs/promises');
const crypto = require('node:crypto');
const path = require('node:path').posix;
function loadSshClient() {
try { return require('ssh2').Client; }
@@ -120,6 +121,63 @@ class SshService {
});
}
async uploadBuffer(serverId, remotePath, content, { mode = 0o600 } = {}) {
const server = this.store.getServer(serverId);
if (!server?.hostFingerprint) {
const error = new Error('Test and trust the SSH server fingerprint before uploading deployment assets.');
error.code = 'SSH_HOST_NOT_TRUSTED';
throw error;
}
const target = String(remotePath || '').replace(/\\/g, '/');
if (!target.startsWith('/') || target.includes('\0') || target.split('/').includes('..')) throw new Error('Remote upload path must be an absolute safe Unix path.');
const data = Buffer.isBuffer(content) ? content : Buffer.from(content);
return this.withClient(serverId, (client) => new Promise((resolve, reject) => {
client.sftp((sftpError, sftp) => {
if (sftpError) { reject(sftpError); return; }
const directory = path.dirname(target);
const mkdirParts = directory.split('/').filter(Boolean);
let current = '';
const makeNext = (index) => {
if (index >= mkdirParts.length) {
const stream = sftp.createWriteStream(target, { mode });
stream.once('error', reject);
stream.once('close', () => resolve({ remotePath: target, size: data.length }));
stream.end(data);
return;
}
current += `/${mkdirParts[index]}`;
const ensureDirectory = () => {
sftp.stat(current, (statError, attributes) => {
if (!statError) {
if (typeof attributes?.isDirectory === 'function' && !attributes.isDirectory()) {
reject(new Error(`Remote upload parent exists but is not a directory: ${current}`));
return;
}
makeNext(index + 1);
return;
}
if (![2, 'ENOENT'].includes(statError.code)) { reject(statError); return; }
sftp.mkdir(current, { mode: 0o755 }, (mkdirError) => {
if (!mkdirError) { makeNext(index + 1); return; }
sftp.stat(current, (retryError, retryAttributes) => {
if (!retryError && (typeof retryAttributes?.isDirectory !== 'function' || retryAttributes.isDirectory())) makeNext(index + 1);
else reject(mkdirError);
});
});
});
};
ensureDirectory();
};
makeNext(0);
});
}), { trustOnFirstUse: false });
}
async uploadFile(serverId, localPath, remotePath, options = {}) {
const data = await fs.readFile(localPath);
return this.uploadBuffer(serverId, remotePath, data, options);
}
async test(serverId, { trustOnFirstUse = true } = {}) {
return this.withClient(serverId, async (client, server, fingerprint) => {
const result = await this.execClient(client, 'uname -srm && command -v git && (docker compose version || docker-compose version)', { timeout: 30_000 });