somewhat guest login

This commit is contained in:
zefie
2026-05-02 10:15:24 -04:00
parent 4f20c08ed0
commit 92958e4c64
18 changed files with 155 additions and 281 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
const process = require('process');
const fs = require('fs');
const path = require('path');
const classPath = path.resolve(__dirname + path.sep + "includes" + path.sep + "classes" + path.sep) + path.sep;
const { WTVShared, clientShowAlert } = require(classPath + "/WTVShared.js");
const wtvshared = new WTVShared(); // creates minisrv_config
const minisrv_config = wtvshared.getMiniSrvConfig(); // snatches minisrv_config
// primitive recursive diskmap generator, usage:
// node diskmap_gen.js path_in_servicevault diskmap_name wtvdest [service_name]
// service_name defaults to wtv-disk
// will create a primitive diskmap you can then edit it as you need
// example: node diskmap_gen.js content/Demo/ Demo.json DealerDemo file://Disk/Demo/
if (process.argv.length < 6) {
console.error("Usage:", process.argv[0], process.argv[1], "path_in_service_vault", "diskmap_name", "wtv_file_dest", "groupname", "[service_name]");
console.error("Example:", process.argv[0], process.argv[1], "content/Demo/ Demo.json DealerDemo file://Disk/Demo/");
process.exit(1);
}
const service_vault_subdir = process.argv[2];
const out_file = process.argv[3];
const group_name = process.argv[4];
let client_dest = process.argv[5];
let service_name;
if (process.argv.length >= 7) service_name = process.argv[6];
else service_name = "wtv-disk";
// find which service_vault the files are in
// nothing fancy, won't support generating a list across multiple vaults
// so be sure to choose ONE vault to keep all the files in before scanning
// Can be any vault, and after the scan you could technically move files across vaults
// so long as they are on the same service still
let service_vault = null;
let service_vault_dir = null;
if (minisrv_config.config.ServiceVaults) {
Object.keys(minisrv_config.config.ServiceVaults).forEach(function (k) {
if (service_vault_dir) return;
const test = wtvshared.makeSafePath(wtvshared.returnAbsolutePath(minisrv_config.config.ServiceVaults[k]), service_name + path.sep + service_vault_subdir);
console.log(" * Looking for", test);
if (fs.existsSync(test)) {
console.log(" * Found", test);
service_vault = wtvshared.makeSafePath(wtvshared.returnAbsolutePath(minisrv_config.config.ServiceVaults[k]), service_name);
service_vault_dir = test;
}
})
}
if (!service_vault) {
console.error("Could not find", service_vault_subdir, "in any configured Service Vaults!");
process.exit(1);
}
const recursiveDirList = function (dirPath, arrayOfFiles = null) {
const files = fs.readdirSync(dirPath)
arrayOfFiles = arrayOfFiles || []
files.forEach(function (file) {
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
arrayOfFiles = recursiveDirList(dirPath + "/" + file, arrayOfFiles)
} else {
arrayOfFiles.push(path.join(dirPath, "/", file))
}
})
return arrayOfFiles
}
const fileList = recursiveDirList(service_vault_dir);
if (fileList.length > 0) {
const diskmap = {};
diskmap[group_name] = {};
if (client_dest.slice(client_dest.length - 1, 1) !== '/') client_dest += '/';
diskmap[group_name].base = client_dest;
diskmap[group_name].location = service_vault_subdir;
diskmap[group_name].files = [];
fileList.forEach(function (v) {
diskmap[group_name].files.push({ "file": v.replace(service_vault_dir, client_dest).replace(new RegExp('\\' + path.sep, 'g'), '/') });
});
// diskmap[group_name].files = diskmap_files;
fs.writeFileSync(out_file,JSON.stringify(diskmap, null, "\t"));
} else {
throw ("No files found in", service_vault);
}

View File

@@ -0,0 +1,172 @@
'use strict';
const fs = require('fs');
const path = require('path');
const forge = require('node-forge');
const workspaceRoot = __dirname;
const httpsDir = path.join(workspaceRoot, '..', 'includes', 'ServiceDeps', 'https');
const msnDir = path.join(workspaceRoot, '..', 'includes', 'ServiceDeps', 'msntv2');
const domainsFile = path.join(msnDir, 'msn_domains.txt');
const defaultCaCertPath = path.join(msnDir, 'msntv2.crt');
const defaultCaKeyPath = path.join(msnDir, 'msntv2.key');
const defaultOutCertPath = path.join(msnDir, 'msn_domains.crt');
const defaultOutKeyPath = path.join(msnDir, 'msn_domains.key');
function parseArgs(argv) {
const out = {};
for (let i = 2; i < argv.length; i++) {
const part = argv[i];
if (!part.startsWith('--')) continue;
const key = part.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith('--')) {
out[key] = true;
continue;
}
out[key] = next;
i += 1;
}
return out;
}
function extractDomainsFromRedirectMap(text) {
const found = [];
const seen = new Set();
const re = /"([A-Za-z0-9.-]+\.)"\s*:\s*self\.redirect_ip/g;
let match;
while ((match = re.exec(text))) {
const clean = match[1].replace(/\.$/, '').toLowerCase();
if (!seen.has(clean)) {
seen.add(clean);
found.push(clean);
}
}
return found;
}
function loadDomains(args) {
if (args['from-map-file']) {
const mapText = fs.readFileSync(path.resolve(workspaceRoot, args['from-map-file']), 'utf8');
const domains = extractDomainsFromRedirectMap(mapText);
if (!domains.length) {
throw new Error('No domains were extracted from --from-map-file.');
}
return domains;
}
if (!fs.existsSync(domainsFile)) {
throw new Error('Domain file not found: ' + domainsFile);
}
const domains = fs.readFileSync(domainsFile, 'utf8')
.split(/\r?\n/)
.map((s) => s.trim().toLowerCase())
.filter((s) => s && !s.startsWith('#'));
return Array.from(new Set(domains));
}
function loadPemOrThrow(filePath, label) {
if (!fs.existsSync(filePath)) {
throw new Error(label + ' file missing: ' + filePath);
}
return fs.readFileSync(filePath, 'utf8');
}
function ensureDirFor(filePath) {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function signAlgorithm(sigName) {
const lower = String(sigName || 'sha1').toLowerCase();
if (lower === 'sha256') return forge.md.sha256.create();
if (lower === 'sha384') return forge.md.sha384.create();
if (lower === 'sha512') return forge.md.sha512.create();
return forge.md.sha1.create();
}
function generateCert({ domains, caCertPem, caKeyPem, outCertPath, outKeyPath, years, sig }) {
const caCert = forge.pki.certificateFromPem(caCertPem);
const caKey = forge.pki.privateKeyFromPem(caKeyPem);
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = forge.util.bytesToHex(forge.random.getBytesSync(16));
const now = new Date();
cert.validity.notBefore = new Date(now.getTime() - 24 * 60 * 60 * 1000);
cert.validity.notAfter = new Date(now.getTime() + years * 365 * 24 * 60 * 60 * 1000);
const cn = domains[0] || 'headwaiter.trusted.msntv.msn.com';
cert.setSubject([
{ name: 'commonName', value: cn },
{ name: 'organizationName', value: 'Zefie Networks' },
{ name: 'countryName', value: 'US' }
]);
cert.setIssuer(caCert.subject.attributes);
cert.setExtensions([
{ name: 'basicConstraints', cA: false },
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
{ name: 'extKeyUsage', serverAuth: true },
{
name: 'subjectAltName',
altNames: domains.map((d) => ({ type: 2, value: d }))
}
]);
cert.sign(caKey, signAlgorithm(sig));
ensureDirFor(outCertPath);
ensureDirFor(outKeyPath);
fs.writeFileSync(outKeyPath, forge.pki.privateKeyToPem(keys.privateKey), 'utf8');
fs.writeFileSync(outCertPath, forge.pki.certificateToPem(cert), 'utf8');
return { cn, count: domains.length };
}
function main() {
const args = parseArgs(process.argv);
const caCertPath = path.resolve(workspaceRoot, args['ca-cert'] || defaultCaCertPath);
const caKeyPath = path.resolve(workspaceRoot, args['ca-key'] || defaultCaKeyPath);
const outCertPath = path.resolve(workspaceRoot, args['out-cert'] || defaultOutCertPath);
const outKeyPath = path.resolve(workspaceRoot, args['out-key'] || defaultOutKeyPath);
const years = Number(args.years || 15);
const sig = String(args.sig || 'sha1');
const domains = loadDomains(args);
const caCertPem = loadPemOrThrow(caCertPath, 'CA cert');
const caKeyPem = loadPemOrThrow(caKeyPath, 'CA key');
const result = generateCert({
domains,
caCertPem,
caKeyPem,
outCertPath,
outKeyPath,
years,
sig
});
console.log('[msn-san-cert] generated cert:', outCertPath);
console.log('[msn-san-cert] generated key :', outKeyPath);
console.log('[msn-san-cert] domains :', result.count);
console.log('[msn-san-cert] common name :', result.cn);
console.log('[msn-san-cert] signature alg :', sig);
}
if (require.main === module) {
try {
main();
} catch (err) {
console.error('[msn-san-cert] error:', err.message);
process.exit(1);
}
}

View File

@@ -0,0 +1,361 @@
// Rockwell to USRobotics Modem Proxy for MAME Bitbanger
const net = require('net');
const { SerialPort } = require('serialport');
const { ReadlineParser } = require('@serialport/parser-readline');
// Configuration
const TCP_IP = '127.0.0.1';
const TCP_PORT = 57388;
const SERIAL_PORT = 'COM3';
const SERIAL_BAUDRATE = 115200;
let NEXT_RECV_IS_LAST_ASCII = false;
let DATA_MODE = false;
const THINGS_TO_STRIP = ["S95=36", "&Q5", "S51=31", "S220=0", "&Q5", "&K3", "&D2"];
const THINGS_TO_REPLACE = [
["M0", "M1"], // M1 = Speaker on
["S11=110", "S11=50"], // S11 = Dial speed
["S11=200", "S11=50"], // S11 = Dial speed
["18004653537", "5736666"],
["18006138199", "5736666"]
];
// Global variables
let serialPort = null;
let server = null;
let currentClient = null;
// Initialize serial port
function initSerial() {
return new Promise((resolve, reject) => {
try {
serialPort = new SerialPort({
path: SERIAL_PORT,
baudRate: SERIAL_BAUDRATE,
autoOpen: false,
// Disable buffering for immediate data flow
highWaterMark: 1,
// Set minimal timeouts
dataBits: 8,
stopBits: 1,
parity: 'none',
rtscts: false,
xon: false,
xoff: false,
xany: false
});
serialPort.open((err) => {
if (err) {
console.error('Error opening serial port:', err.message);
reject(err);
return;
}
console.log(`Serial port ${SERIAL_PORT} opened at ${SERIAL_BAUDRATE} baud`);
// Disable any internal buffering
serialPort.set({
brk: false,
cts: false,
dtr: true,
rts: true
});
// Add a small delay to ensure port is ready
setTimeout(() => {
resolve(serialPort);
}, 100);
});
} catch (error) {
console.error('Error initializing serial port:', error);
reject(error);
}
});
}
// Reset modem to command mode and hang up
function resetModemToCommandMode() {
try {
// Send escape sequence to exit data mode
serialPort.write(Buffer.from('+++', 'ascii'));
setTimeout(() => {
// Send hang up command
serialPort.write(Buffer.from('ATH\r', 'ascii'));
console.log("Sent modem reset commands: +++ and ATH");
}, 500);
} catch (error) {
console.error('Error resetting modem:', error);
}
}
// Handle data from socket to serial
function handleSocketToSerial(socket) {
let buffer = Buffer.alloc(0);
socket.on('data', (data) => {
if (!DATA_MODE) {
buffer = Buffer.concat([buffer, data]);
// Process commands only after receiving a complete command ending in '\r'
// Use Buffer.indexOf to find CR byte (0x0D) for binary safety
const crIndex = buffer.indexOf(0x0D); // '\r' = 0x0D
if (crIndex === -1) {
return; // Wait for complete command
}
// Extract command as buffer first, then convert to string only for processing
const commandBuffer = buffer.slice(0, crIndex);
let command = commandBuffer.toString('ascii').trim();
buffer = buffer.slice(crIndex + 1);
// Apply string stripping and replacement
THINGS_TO_STRIP.forEach(s => {
command = command.replace(s, "");
});
THINGS_TO_REPLACE.forEach(([find, replace]) => {
command = command.replace(find, replace);
});
const commandBytes = Buffer.from(command + '\r', 'ascii');
console.log("WEBTV COMMAND:", commandBytes.toString('ascii').trim());
if (command + '\r' === 'ATD\r') {
NEXT_RECV_IS_LAST_ASCII = true;
console.log("ATD detected - next serial response will trigger data mode");
}
try {
serialPort.write(commandBytes, (err) => {
if (err) {
console.error('Error writing command to serial port:', err);
} else {
// Force immediate transmission of commands too
serialPort.drain();
}
});
} catch (error) {
console.error('Error writing to serial port:', error);
}
} else {
// In data mode, pass through binary data unchanged with immediate write
try {
serialPort.write(data, (err) => {
if (err) {
console.error('Error writing to serial port:', err);
} else {
// Force immediate transmission
serialPort.drain((drainErr) => {
if (drainErr) {
console.error('Error draining serial port:', drainErr);
}
});
}
});
} catch (error) {
console.error('Error writing to serial port:', error);
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
if (DATA_MODE) {
console.log("Exiting data mode due to socket error");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
});
socket.on('close', () => {
console.log("Socket closed by remote");
if (DATA_MODE) {
console.log("Exiting data mode due to socket disconnect");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
currentClient = null;
});
}
// Handle data from serial to socket
function handleSerialToSocket(socket) {
const dataHandler = (data) => {
// Check if we're switching to data mode
if (!DATA_MODE && NEXT_RECV_IS_LAST_ASCII) {
DATA_MODE = true;
NEXT_RECV_IS_LAST_ASCII = false;
// Provide connection result and enable data mode
data = Buffer.from('79\r\n67\r\n19\r\n', 'ascii');
console.log("MODEM CONNECT RESULT:", data.toString('ascii'));
console.log("Data mode enabled");
} else if (!DATA_MODE) {
console.log("MODEM COMMAND RESPONSE:", data.toString('ascii'));
}
// Check for disabling data mode if unsupported or exit flags are received
// Use Buffer.equals for binary-safe comparison
const escapeSeq = Buffer.from("+++\r", 'ascii');
const exitCode = Buffer.from("3\r", 'ascii');
if (data.equals(escapeSeq) || data.equals(exitCode)) {
console.log("Data mode disabled by serial response");
DATA_MODE = false;
}
try {
if (socket && !socket.destroyed) {
socket.write(data);
} else {
console.log("[SERIAL->SOCKET] Cannot send - socket destroyed or null");
}
} catch (error) {
console.error('Send error:', error);
if (DATA_MODE) {
console.log("Exiting data mode due to send error");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
}
};
const errorHandler = (err) => {
console.error('Serial port error:', err);
if (DATA_MODE) {
console.log("Exiting data mode due to serial error");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
};
console.log("[SERIAL] Setting up data and error handlers");
serialPort.on('data', dataHandler);
serialPort.on('error', errorHandler);
// Return cleanup function
return () => {
console.log("[SERIAL] Cleaning up event handlers");
serialPort.removeListener('data', dataHandler);
serialPort.removeListener('error', errorHandler);
};
}
// Handle new client connection
function handleClient(socket) {
// Reset state for new connection
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
console.log("Reset modem state for new connection");
// Disable TCP buffering for immediate data flow
socket.setNoDelay(true);
socket.setTimeout(0);
currentClient = socket;
handleSocketToSerial(socket);
const cleanupSerial = handleSerialToSocket(socket);
socket.on('close', () => {
// Ensure data mode is reset when client disconnects
if (DATA_MODE) {
console.log("Client disconnected, exiting data mode");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
// Clean up serial event listeners
cleanupSerial();
currentClient = null;
});
socket.on('error', (err) => {
console.error('Client socket error:', err);
cleanupSerial();
currentClient = null;
});
}
// Clean shutdown procedure
function cleanup() {
console.log("Cleaning up...");
// Reset modem if we're in data mode
if (DATA_MODE) {
console.log("Resetting modem before shutdown");
DATA_MODE = false;
NEXT_RECV_IS_LAST_ASCII = false;
resetModemToCommandMode();
}
if (currentClient) {
currentClient.destroy();
}
if (server) {
server.close();
}
if (serialPort && serialPort.isOpen) {
serialPort.close();
}
}
// Signal handlers
process.on('SIGINT', () => {
console.log('\nReceived interrupt signal, shutting down...');
cleanup();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\nReceived terminate signal, shutting down...');
cleanup();
process.exit(0);
});
// Main execution
async function main() {
try {
// Initialize serial port and wait for it to be ready
await initSerial();
// Start TCP server
server = net.createServer((socket) => {
console.log(`Connection from ${socket.remoteAddress}:${socket.remotePort}`);
handleClient(socket);
});
server.listen(TCP_PORT, TCP_IP, () => {
console.log(`Listening on ${TCP_IP}:${TCP_PORT}...`);
});
server.on('error', (err) => {
console.error('Server error:', err);
cleanup();
process.exit(1);
});
} catch (error) {
console.error('Failed to initialize:', error);
process.exit(1);
}
}
// Start the proxy
if (require.main === module) {
main();
}
module.exports = {
main,
cleanup
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
#!/usr/bin/env node
/**
* wtV_img_converter.js - WebTV PNG/GIF conversion CLI
*
* Usage:
* node wtV_img_converter.js <command> [options] <input> [output]
*
* Commands:
* convert Convert a PNG to the best WebTV format (auto: JPEG or Artemis GIF)
* encode Convert a PNG with alpha to an Artemis ALP/ALF GIF
* decode Convert a WebTV Artemis ALP/ALF GIF back to a PNG
* detect Report whether a GIF contains an Artemis ALP/ALF block
*
* Options:
* --type <ALP|ALF> Artemis variant to use for encoding (default: ALP)
* --colors <n> Palette size for full-color quantization (default: 256)
* --quality <n> JPEG quality when output is JPEG (default: 85)
* --output, -o <file> Output file path (alternative to positional argument)
* --help, -h Show this help
*/
'use strict';
const fs = require('fs');
const path = require('path');
const WTVImage = require('../includes/classes/WTVImage');
// ---------------------------------------------------------------------------
// Argument parser
// ---------------------------------------------------------------------------
function parseArgs(argv) {
const args = { options: {}, positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--help' || a === '-h') {
args.options.help = true;
} else if (a === '--type') {
args.options.type = argv[++i];
} else if (a === '--colors') {
args.options.colors = parseInt(argv[++i], 10);
} else if (a === '--quality') {
args.options.quality = parseInt(argv[++i], 10);
} else if (a === '--max-width') {
args.options.maxWidth = parseInt(argv[++i], 10);
} else if (a === '--max-height') {
args.options.maxHeight = parseInt(argv[++i], 10);
} else if (a === '--output' || a === '-o') {
args.options.output = argv[++i];
} else if (a.startsWith('--')) {
console.error(`Unknown option: ${a}`);
process.exit(1);
} else {
args.positional.push(a);
}
}
return args;
}
function printHelp() {
console.log(`
WebTV Image Converter
=======================
Usage: node wtv_img_converter.js <command> [options] <input> [output]
Commands:
convert Convert an image to the best WebTV format
- No alpha → JPEG
- Palette PNG → Artemis GIF (palette 1:1, no requantization)
- Full-color RGBA → Artemis GIF (quantized)
encode Convert a PNG with alpha to an Artemis ALP or ALF GIF
(throws if the PNG has no alpha channel)
decode Convert a WebTV Artemis ALP/ALF GIF back to a standard RGBA PNG
detect Report whether a file is an Artemis ALP/ALF GIF (no output file needed)
Options:
--type <ALP|ALF> Artemis variant for encoding/convert (default: ALF)
--colors <n> Palette size for full-color quantization (default: 256)
--quality <n> JPEG quality when output is JPEG (default: 85)
--max-width <n> Maximum width to scale input before encoding
--max-height <n> Maximum height to scale input before encoding
--output, -o <file> Output file path
--help, -h Show this help
Examples:
node wtV_img_converter.js convert logo.png
node wtV_img_converter.js convert logo.png logo_wtv.gif --type ALF --colors 128
node wtV_img_converter.js encode icon.png icon.gif --type ALP
node wtV_img_converter.js decode artemis.gif result.png
node wtV_img_converter.js detect artemis.gif
`.trim());
}
// ---------------------------------------------------------------------------
// Output path helpers
// ---------------------------------------------------------------------------
function resolveOutput(inputFile, suggestedExt, override) {
if (override) return override;
const base = path.join(
path.dirname(inputFile),
path.basename(inputFile, path.extname(inputFile))
);
return base + suggestedExt;
}
// ---------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------
async function cmdConvert(inputFile, outputFile, opts) {
const ImageBuf = fs.readFileSync(inputFile);
const { data, mime } = await WTVImage.ImageToWebTV(ImageBuf, {
type: opts.type || 'ALP',
colors: opts.colors || 256,
jpegQuality: opts.quality || 85,
maxWidth: opts.maxWidth,
maxHeight: opts.maxHeight
}, {
"quality": 80,
"compressionLevel": 9,
"palette": true,
"effort": 10
});
const ext = mime === 'image/gif' ? '.gif' : '.jpg';
const dest = resolveOutput(inputFile, ext, outputFile);
fs.writeFileSync(dest, data);
console.log(`[convert] ${inputFile}${dest} (${mime}, ${data.length} bytes)`);
}
async function cmdEncode(inputFile, outputFile, opts) {
const ImageBuf = fs.readFileSync(inputFile);
const gifBuf = await WTVImage.ImageToGIF(ImageBuf, {
type: opts.type || 'ALF',
colors: opts.colors || 256
});
const dest = resolveOutput(inputFile, '.gif', outputFile);
fs.writeFileSync(dest, gifBuf);
const type = WTVImage.detect(gifBuf);
console.log(`[encode] ${inputFile}${dest} (Artemis ${type}, ${gifBuf.length} bytes)`);
}
async function cmdDecode(inputFile, outputFile, opts) {
const gifBuf = fs.readFileSync(inputFile);
const type = WTVImage.detect(gifBuf);
if (!type) {
console.error(`[decode] ${inputFile} does not contain an Artemis ALP/ALF block.`);
process.exit(1);
}
const ImageBuf = await WTVImage.gifToPNG(gifBuf);
const dest = resolveOutput(inputFile, '.png', outputFile);
fs.writeFileSync(dest, ImageBuf);
console.log(`[decode] ${inputFile} (Artemis ${type}) → ${dest} (${ImageBuf.length} bytes)`);
}
function cmdDetect(inputFile) {
const buf = fs.readFileSync(inputFile);
const type = WTVImage.detect(buf);
if (type) {
console.log(`[detect] ${inputFile} → Artemis ${type}`);
} else {
console.log(`[detect] ${inputFile} → Not an Artemis ALP/ALF GIF`);
}
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
(async () => {
const raw = process.argv.slice(2);
const args = parseArgs(raw);
if (args.options.help || args.positional.length === 0) {
printHelp();
process.exit(0);
}
const command = args.positional[0];
const inputFile = args.positional[1];
const outputFile = args.options.output || args.positional[2] || null;
if (!inputFile) {
console.error('Error: no input file specified.');
printHelp();
process.exit(1);
}
if (!fs.existsSync(inputFile)) {
console.error(`Error: input file not found: ${inputFile}`);
process.exit(1);
}
try {
switch (command) {
case 'convert': await cmdConvert(inputFile, outputFile, args.options); break;
case 'encode': await cmdEncode(inputFile, outputFile, args.options); break;
case 'decode': await cmdDecode(inputFile, outputFile, args.options); break;
case 'detect': cmdDetect(inputFile); break;
default:
console.error(`Unknown command: ${command}`);
printHelp();
process.exit(1);
}
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
})();