[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,670 @@
const fs = require("node:fs");
const path = require("node:path");
const YMODEM = Object.freeze({
SOH: 0x01,
STX: 0x02,
EOT: 0x04,
ACK: 0x06,
NAK: 0x15,
CAN: 0x18,
CRC16: 0x43,
BACKSPACE: 0x08,
PACKET_SIZE_128: 128,
PACKET_SIZE_1024: 1024,
});
const DEFAULT_TIMEOUT_MS = 60_000;
const DEFAULT_RETRY_LIMIT = 10;
const YMODEM_CANCEL_SEQUENCE = Buffer.from([
YMODEM.CAN,
YMODEM.CAN,
YMODEM.CAN,
YMODEM.CAN,
YMODEM.CAN,
YMODEM.BACKSPACE,
YMODEM.BACKSPACE,
YMODEM.BACKSPACE,
YMODEM.BACKSPACE,
YMODEM.BACKSPACE,
]);
class YmodemTransferError extends Error {
constructor(message, code = "YMODEM_TRANSFER_ERROR") {
super(message);
this.name = "YmodemTransferError";
this.code = code;
}
}
function crc16Xmodem(buffer, start = 0, length = buffer.length - start) {
let crc = 0;
for (let i = start; i < start + length; i += 1) {
let code = ((crc >>> 8) ^ buffer[i]) & 0xff;
code ^= code >>> 4;
crc = ((crc << 8) ^ (code << 12) ^ (code << 5) ^ code) & 0xffff;
}
return crc;
}
function createPacket(header, blockNumber, payload) {
const size = payload.length;
const packet = Buffer.alloc(3 + size + 2, 0x00);
packet[0] = header;
packet[1] = blockNumber & 0xff;
packet[2] = 0xff - packet[1];
payload.copy(packet, 3);
packet.writeUInt16BE(crc16Xmodem(packet, 3, size), packet.length - 2);
return packet;
}
function createYmodemFileInfoPacket({ filename, size, mtime = 0, mode = 0o100644 }) {
const baseName = path.basename(filename || "file");
const metadata = `${baseName}\0${size} ${Math.trunc(mtime).toString(8)} ${mode.toString(8)}`;
const metadataBytes = Buffer.from(metadata, "utf8");
const packetSize = metadataBytes.length >= YMODEM.PACKET_SIZE_128
? YMODEM.PACKET_SIZE_1024
: YMODEM.PACKET_SIZE_128;
const payload = Buffer.alloc(packetSize, 0x00);
metadataBytes.copy(payload, 0, 0, Math.min(metadataBytes.length, packetSize));
return createPacket(
packetSize === YMODEM.PACKET_SIZE_1024 ? YMODEM.STX : YMODEM.SOH,
0,
payload,
);
}
function createYmodemDataPackets(buffer) {
if (!Buffer.isBuffer(buffer)) {
buffer = Buffer.from(buffer);
}
const packets = [];
let blockNumber = 1;
for (let offset = 0; offset < buffer.length; offset += YMODEM.PACKET_SIZE_1024) {
const payload = Buffer.alloc(YMODEM.PACKET_SIZE_1024, 0x1a);
buffer.copy(payload, 0, offset, Math.min(offset + YMODEM.PACKET_SIZE_1024, buffer.length));
packets.push(createPacket(YMODEM.STX, blockNumber, payload));
blockNumber = (blockNumber + 1) & 0xff;
}
return packets;
}
function createYmodemEndSessionPacket() {
return createPacket(YMODEM.SOH, 0, Buffer.alloc(YMODEM.PACKET_SIZE_128, 0x00));
}
function sanitizeYmodemFilename(filename) {
const normalized = String(filename || "").replace(/\\/g, "/");
const baseName = path.basename(normalized).replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").trim();
if (!baseName || baseName === "." || baseName === "..") {
return "ymodem-received.bin";
}
return baseName;
}
function parseYmodemFileInfoPayload(payload) {
const separatorIndex = payload.indexOf(0x00);
const rawName = (separatorIndex >= 0 ? payload.subarray(0, separatorIndex) : payload).toString("utf8");
if (!rawName) return null;
const metadata = separatorIndex >= 0
? payload.subarray(separatorIndex + 1).toString("ascii").replace(/\0.*$/u, "").trim()
: "";
const [sizeText] = metadata.split(/\s+/u);
if (!/^\d+$/u.test(sizeText || "")) {
throw new YmodemTransferError("YMODEM file header has an invalid size", "YMODEM_INVALID_SIZE");
}
const parsedSize = Number.parseInt(sizeText, 10);
return {
fileName: sanitizeYmodemFilename(rawName),
totalBytes: Number.isFinite(parsedSize) && parsedSize >= 0 ? parsedSize : 0,
};
}
async function resolveUniqueDestinationPath(destinationDir, fileName) {
const parsed = path.parse(fileName);
for (let attempt = 0; attempt < 10_000; attempt += 1) {
const candidateName = attempt === 0
? fileName
: `${parsed.name} (${attempt})${parsed.ext}`;
const candidatePath = path.join(destinationDir, candidateName);
try {
await fs.promises.access(candidatePath);
} catch (error) {
if (error?.code === "ENOENT") return candidatePath;
throw error;
}
}
throw new YmodemTransferError("Could not choose a destination file name", "YMODEM_DESTINATION_EXISTS");
}
function writeAndDrain(serialPort, buffer) {
return new Promise((resolve, reject) => {
let settled = false;
const done = (err) => {
if (settled) return;
if (err) {
settled = true;
reject(err);
return;
}
if (typeof serialPort.drain !== "function") {
settled = true;
resolve();
return;
}
serialPort.drain((drainErr) => {
if (settled) return;
settled = true;
drainErr ? reject(drainErr) : resolve();
});
};
try {
serialPort.write(buffer, done);
} catch (error) {
reject(error);
}
});
}
function createSerialByteReader(serialPort, abortSignal) {
const queue = [];
const waiters = [];
const rejectAll = (error) => {
while (waiters.length) {
const waiter = waiters.shift();
clearTimeout(waiter.timer);
waiter.reject(error);
}
};
const onData = (chunk) => {
for (const byte of Buffer.from(chunk)) {
if (waiters.length) {
const waiter = waiters.shift();
clearTimeout(waiter.timer);
waiter.resolve(byte);
} else {
queue.push(byte);
}
}
};
const onAbort = () => {
rejectAll(new YmodemTransferError("YMODEM transfer cancelled", "YMODEM_CANCELLED"));
};
const onError = (error) => {
rejectAll(error instanceof Error ? error : new Error(String(error)));
};
const onClose = () => {
rejectAll(new YmodemTransferError("Serial port closed during YMODEM transfer", "YMODEM_SERIAL_CLOSED"));
};
serialPort.on("data", onData);
serialPort.on?.("error", onError);
serialPort.on?.("close", onClose);
abortSignal?.addEventListener?.("abort", onAbort, { once: true });
return {
unreadByte(byte) {
queue.unshift(byte);
},
readByte(timeoutMs) {
if (abortSignal?.aborted) {
return Promise.reject(new YmodemTransferError("YMODEM transfer cancelled", "YMODEM_CANCELLED"));
}
if (queue.length) {
return Promise.resolve(queue.shift());
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
const index = waiters.findIndex((waiter) => waiter.timer === timer);
if (index >= 0) waiters.splice(index, 1);
reject(new YmodemTransferError("Timed out waiting for YMODEM receiver", "YMODEM_TIMEOUT"));
}, timeoutMs);
waiters.push({ resolve, reject, timer });
});
},
cleanup() {
serialPort.off("data", onData);
serialPort.off?.("error", onError);
serialPort.off?.("close", onClose);
abortSignal?.removeEventListener?.("abort", onAbort);
rejectAll(new YmodemTransferError("YMODEM reader closed", "YMODEM_CLOSED"));
},
};
}
function sendYmodemCancel(serialPort) {
if (!serialPort || typeof serialPort.write !== "function") return Promise.resolve();
return writeAndDrain(serialPort, YMODEM_CANCEL_SEQUENCE).catch(() => {});
}
async function readExpected(reader, expected, timeoutMs, label) {
for (;;) {
const byte = await reader.readByte(timeoutMs);
if (byte === YMODEM.CAN) {
const next = await reader.readByte(1_000).catch(() => null);
if (next === YMODEM.CAN) {
throw new YmodemTransferError("YMODEM transfer cancelled by receiver", "YMODEM_REMOTE_CANCELLED");
}
if (next !== null) {
reader.unreadByte?.(next);
}
continue;
}
if (expected.includes(byte)) return byte;
if (byte === YMODEM.CRC16 && expected.includes(YMODEM.CRC16)) return byte;
if (byte === YMODEM.NAK && expected.includes(YMODEM.NAK)) return byte;
if (byte === YMODEM.ACK && expected.includes(YMODEM.ACK)) return byte;
if (byte === YMODEM.CRC16 || byte === YMODEM.ACK || byte === YMODEM.NAK) continue;
throw new YmodemTransferError(`Unexpected YMODEM byte while waiting for ${label}: 0x${byte.toString(16)}`);
}
}
async function sendPacketWithRetry({ serialPort, reader, packet, timeoutMs, retryLimit, onProgress }) {
for (let attempt = 0; attempt < retryLimit; attempt += 1) {
await writeAndDrain(serialPort, packet);
onProgress?.();
const response = await readExpected(reader, [YMODEM.ACK, YMODEM.NAK], timeoutMs, "packet ACK");
if (response === YMODEM.ACK) return;
}
throw new YmodemTransferError("YMODEM receiver rejected the packet too many times", "YMODEM_RETRY_LIMIT");
}
async function readYmodemFrame(reader, timeoutMs) {
const header = await reader.readByte(timeoutMs);
if (header === YMODEM.CAN) {
const next = await reader.readByte(1_000).catch(() => null);
if (next === YMODEM.CAN) {
throw new YmodemTransferError("YMODEM transfer cancelled by sender", "YMODEM_REMOTE_CANCELLED");
}
if (next !== null) {
reader.unreadByte?.(next);
}
return readYmodemFrame(reader, timeoutMs);
}
if (header === YMODEM.EOT) {
return { type: "eot" };
}
if (header !== YMODEM.SOH && header !== YMODEM.STX) {
throw new YmodemTransferError(`Unexpected YMODEM packet header: 0x${header.toString(16)}`);
}
const payloadSize = header === YMODEM.SOH ? YMODEM.PACKET_SIZE_128 : YMODEM.PACKET_SIZE_1024;
const blockNumber = await reader.readByte(timeoutMs);
const blockComplement = await reader.readByte(timeoutMs);
const payload = Buffer.alloc(payloadSize);
for (let i = 0; i < payloadSize; i += 1) {
payload[i] = await reader.readByte(timeoutMs);
}
const sentCrc = ((await reader.readByte(timeoutMs)) << 8) | await reader.readByte(timeoutMs);
const expectedCrc = crc16Xmodem(payload);
const valid = blockComplement === (0xff - blockNumber) && sentCrc === expectedCrc;
return {
type: "packet",
blockNumber,
payload,
valid,
};
}
async function readYmodemPacketWithRetry({
serialPort,
reader,
expectedBlockNumber,
requestByte,
timeoutMs,
retryLimit,
label,
}) {
for (let attempt = 0; attempt < retryLimit; attempt += 1) {
if (requestByte !== undefined) {
await writeAndDrain(serialPort, Buffer.from([requestByte]));
}
let frame;
try {
frame = await readYmodemFrame(reader, timeoutMs);
} catch (error) {
if (error?.code === "YMODEM_TIMEOUT" && attempt < retryLimit - 1) {
continue;
}
throw error;
}
if (frame.type === "eot") {
return frame;
}
if (frame.valid && frame.blockNumber === expectedBlockNumber) {
return frame;
}
if (frame.valid && frame.blockNumber === ((expectedBlockNumber - 1) & 0xff)) {
await writeAndDrain(serialPort, Buffer.from([YMODEM.ACK]));
continue;
}
await writeAndDrain(serialPort, Buffer.from([YMODEM.NAK]));
}
throw new YmodemTransferError(`YMODEM sender did not provide a valid ${label}`, "YMODEM_RETRY_LIMIT");
}
async function receiveYmodemFileData({
serialPort,
reader,
fileHandle,
totalBytes,
timeoutMs,
retryLimit,
onProgress,
}) {
let expectedBlockNumber = 1;
let writtenBytes = 0;
let rejectedPackets = 0;
for (;;) {
let frame;
try {
frame = await readYmodemFrame(reader, timeoutMs);
} catch (error) {
if (error?.code === "YMODEM_TIMEOUT" && rejectedPackets < retryLimit) {
rejectedPackets += 1;
await writeAndDrain(serialPort, Buffer.from([YMODEM.NAK]));
continue;
}
throw error;
}
if (frame.type === "eot") {
await writeAndDrain(serialPort, Buffer.from([YMODEM.NAK]));
const secondEot = await readYmodemFrame(reader, timeoutMs);
if (secondEot.type !== "eot") {
throw new YmodemTransferError("YMODEM sender did not confirm end of file", "YMODEM_EOT_EXPECTED");
}
await writeAndDrain(serialPort, Buffer.from([YMODEM.ACK]));
if (writtenBytes !== totalBytes) {
throw new YmodemTransferError(
`YMODEM received incomplete file (${writtenBytes}/${totalBytes} bytes)`,
"YMODEM_INCOMPLETE_FILE",
);
}
return writtenBytes;
}
if (!frame.valid) {
rejectedPackets += 1;
if (rejectedPackets > retryLimit) {
throw new YmodemTransferError("YMODEM sender sent too many invalid packets", "YMODEM_RETRY_LIMIT");
}
await writeAndDrain(serialPort, Buffer.from([YMODEM.NAK]));
continue;
}
if (frame.blockNumber === ((expectedBlockNumber - 1) & 0xff)) {
await writeAndDrain(serialPort, Buffer.from([YMODEM.ACK]));
continue;
}
if (frame.blockNumber !== expectedBlockNumber) {
rejectedPackets += 1;
if (rejectedPackets > retryLimit) {
throw new YmodemTransferError("YMODEM sender sent packets out of order", "YMODEM_RETRY_LIMIT");
}
await writeAndDrain(serialPort, Buffer.from([YMODEM.NAK]));
continue;
}
const remainingBytes = Math.max(0, totalBytes - writtenBytes);
const bytesToWrite = Math.min(frame.payload.length, remainingBytes);
if (bytesToWrite > 0) {
await fileHandle.write(frame.payload, 0, bytesToWrite);
writtenBytes += bytesToWrite;
}
rejectedPackets = 0;
expectedBlockNumber = (expectedBlockNumber + 1) & 0xff;
await writeAndDrain(serialPort, Buffer.from([YMODEM.ACK]));
onProgress?.({ transferredBytes: writtenBytes, totalBytes, stage: "data" });
}
}
async function receiveYmodemFiles(serialPort, {
destinationDir,
timeoutMs = DEFAULT_TIMEOUT_MS,
retryLimit = DEFAULT_RETRY_LIMIT,
abortSignal,
onProgress,
} = {}) {
if (!serialPort) {
throw new YmodemTransferError("Serial session is not available", "YMODEM_NO_SERIAL");
}
if (!destinationDir || typeof destinationDir !== "string") {
throw new YmodemTransferError("No destination directory selected", "YMODEM_NO_DESTINATION");
}
const resolvedDestinationDir = path.resolve(destinationDir);
let destinationStat;
try {
destinationStat = await fs.promises.stat(resolvedDestinationDir);
} catch (error) {
throw new YmodemTransferError("Selected destination is not a directory", "YMODEM_DESTINATION_NOT_DIRECTORY");
}
if (!destinationStat.isDirectory()) {
throw new YmodemTransferError("Selected destination is not a directory", "YMODEM_DESTINATION_NOT_DIRECTORY");
}
const reader = createSerialByteReader(serialPort, abortSignal);
const files = [];
try {
for (;;) {
const headerFrame = await readYmodemPacketWithRetry({
serialPort,
reader,
expectedBlockNumber: 0,
requestByte: YMODEM.CRC16,
timeoutMs,
retryLimit,
label: "file header",
});
if (headerFrame.type === "eot") {
await writeAndDrain(serialPort, Buffer.from([YMODEM.ACK]));
continue;
}
const fileInfo = parseYmodemFileInfoPayload(headerFrame.payload);
await writeAndDrain(serialPort, Buffer.from([YMODEM.ACK]));
if (!fileInfo) {
break;
}
await writeAndDrain(serialPort, Buffer.from([YMODEM.CRC16]));
onProgress?.({ transferredBytes: 0, totalBytes: fileInfo.totalBytes, stage: "header" });
const filePath = await resolveUniqueDestinationPath(resolvedDestinationDir, fileInfo.fileName);
let fileHandle;
let closeNeeded = false;
let createdFile = false;
try {
fileHandle = await fs.promises.open(filePath, "wx");
closeNeeded = true;
createdFile = true;
const writtenBytes = await receiveYmodemFileData({
serialPort,
reader,
fileHandle,
totalBytes: fileInfo.totalBytes,
timeoutMs,
retryLimit,
onProgress,
});
await fileHandle.close();
closeNeeded = false;
files.push({
fileName: path.basename(filePath),
filePath,
totalBytes: fileInfo.totalBytes,
writtenBytes,
});
} catch (error) {
if (closeNeeded) {
await fileHandle.close().catch(() => {});
}
if (createdFile) {
await fs.promises.rm(filePath, { force: true }).catch(() => {});
}
throw error;
}
}
const totalBytes = files.reduce((sum, file) => sum + file.totalBytes, 0);
const writtenBytes = files.reduce((sum, file) => sum + file.writtenBytes, 0);
return {
files,
fileCount: files.length,
totalBytes,
writtenBytes,
fileName: files[0]?.fileName,
filePath: files[0]?.filePath,
};
} finally {
reader.cleanup();
}
}
async function sendYmodemBuffer(serialPort, {
filename,
buffer,
mtime = 0,
timeoutMs = DEFAULT_TIMEOUT_MS,
retryLimit = DEFAULT_RETRY_LIMIT,
abortSignal,
onProgress,
} = {}) {
if (!serialPort) {
throw new YmodemTransferError("Serial session is not available", "YMODEM_NO_SERIAL");
}
const fileBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer || []);
const dataPackets = createYmodemDataPackets(fileBuffer);
const packets = [
createYmodemFileInfoPacket({ filename, size: fileBuffer.length, mtime }),
...dataPackets,
];
const reader = createSerialByteReader(serialPort, abortSignal);
let packetsSent = 0;
try {
await readExpected(reader, [YMODEM.CRC16], timeoutMs, "initial receiver request");
await sendPacketWithRetry({
serialPort,
reader,
packet: packets[0],
timeoutMs,
retryLimit,
onProgress: () => {
packetsSent += 1;
onProgress?.({ transferredBytes: 0, totalBytes: fileBuffer.length, stage: "header" });
},
});
for (let attempt = 0; attempt < retryLimit; attempt += 1) {
const response = await readExpected(reader, [YMODEM.CRC16, YMODEM.NAK], timeoutMs, "data receiver request");
if (response === YMODEM.CRC16) break;
await sendPacketWithRetry({
serialPort,
reader,
packet: packets[0],
timeoutMs,
retryLimit,
onProgress: () => {
packetsSent += 1;
onProgress?.({ transferredBytes: 0, totalBytes: fileBuffer.length, stage: "header" });
},
});
if (attempt === retryLimit - 1) {
throw new YmodemTransferError("YMODEM receiver did not request data after header", "YMODEM_RETRY_LIMIT");
}
}
for (let i = 1; i < packets.length; i += 1) {
await sendPacketWithRetry({
serialPort,
reader,
packet: packets[i],
timeoutMs,
retryLimit,
onProgress: () => {
packetsSent += 1;
onProgress?.({
transferredBytes: Math.min(i * YMODEM.PACKET_SIZE_1024, fileBuffer.length),
totalBytes: fileBuffer.length,
stage: "data",
});
},
});
}
await writeAndDrain(serialPort, Buffer.from([YMODEM.EOT]));
const eotResponse = await readExpected(reader, [YMODEM.NAK, YMODEM.ACK], timeoutMs, "first EOT response");
if (eotResponse === YMODEM.NAK) {
await writeAndDrain(serialPort, Buffer.from([YMODEM.EOT]));
await readExpected(reader, [YMODEM.ACK], timeoutMs, "second EOT ACK");
}
await readExpected(reader, [YMODEM.CRC16], timeoutMs, "end session request");
await sendPacketWithRetry({
serialPort,
reader,
packet: createYmodemEndSessionPacket(),
timeoutMs,
retryLimit,
});
onProgress?.({
transferredBytes: fileBuffer.length,
totalBytes: fileBuffer.length,
stage: "complete",
});
return {
fileName: path.basename(filename || "file"),
totalBytes: fileBuffer.length,
writtenBytes: fileBuffer.length,
packetsSent,
};
} finally {
reader.cleanup();
}
}
async function sendYmodemFile(serialPort, filePath, options = {}) {
const stat = await fs.promises.stat(filePath);
if (!stat.isFile()) {
throw new YmodemTransferError("Selected path is not a file", "YMODEM_NOT_FILE");
}
const buffer = await fs.promises.readFile(filePath);
return sendYmodemBuffer(serialPort, {
...options,
filename: filePath,
buffer,
mtime: Math.floor(stat.mtimeMs / 1000),
});
}
module.exports = {
YMODEM,
YmodemTransferError,
crc16Xmodem,
createYmodemFileInfoPacket,
createYmodemDataPackets,
createYmodemEndSessionPacket,
receiveYmodemFiles,
sendYmodemCancel,
sendYmodemBuffer,
sendYmodemFile,
};