import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import https from 'node:https'; import {fileURLToPath} from 'node:url'; import {createWriteStream} from 'node:fs'; import AdmZip from 'adm-zip'; import * as tar from 'tar'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const VERSION = '3.8.1'; const BASE_URL = 'https://download.kiwix.org/release/kiwix-tools'; const PLATFORM_MAP = { linux: {name: 'linux', ext: 'tar.gz'}, darwin: {name: 'macos', ext: 'tar.gz'}, win32: {name: 'win', ext: 'zip'} }; const BIN_DIR = path.join(__dirname, '..', 'bin'); function download(url, dest) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); const agent = new https.Agent({maxFreeSockets: 20, maxTotalSockets: 50}); const req = https.get(url, {agent}, res => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { file.close(); resolve(download(res.headers.location, dest)); return; } if (!res.statusCode || res.statusCode >= 400) { file.close(); reject(new Error(`HTTP ${res.statusCode} - ${res.statusMessage}`)); return; } res.pipe(file); file.on('finish', () => file.close(resolve)); }); req.on('error', err => { fs.unlink(dest, () => {}); reject(err); }); }); } async function extractTarGz(archivePath, destDir) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kiwix-extract-')); const buffer = await streamToBuffer(fs.createReadStream(archivePath)); const tmpTar = path.join(tmpDir, 'kiwix.tar'); fs.writeFileSync(tmpTar, buffer); await tar.extract({ file: tmpTar, cwd: tmpDir, silent: false, filter: filePath => path.basename(filePath).startsWith('kiwix-') }); for (const entry of await fs.promises.readdir(tmpDir)) { const src = path.join(tmpDir, entry); const dest = path.join(destDir, entry); await fs.promises.rename(src, dest); } await fs.promises.rm(tmpDir, {recursive: true, force: true}); } function streamToBuffer(stream) { return new Promise((resolve, reject) => { const chunks = []; stream.on('data', chunk => chunks.push(chunk)); stream.on('end', () => resolve(Buffer.concat(chunks))); stream.on('error', reject); }); } async function extractZip(archivePath, destDir) { const zip = new AdmZip(archivePath); await zip.extractAllToAsync(destDir, true, err => { if (err) throw err; const entries = zip.getEntries(); const rootEntries = entries.filter(e => !e.isDirectory && !e.fileName.includes('/')); if (rootEntries.length > 0) { for (const entry of rootEntries) { const src = path.join(destDir, entry.entryName); const dest = path.join(destDir, path.basename(entry.entryName)); await fs.promises.rename(src, dest); } } }); } async function install() { const platformInfo = PLATFORM_MAP[process.platform]; if (!platformInfo) throw new Error(`Unsupported platform: ${process.platform}`); const archiveName = `kiwix-tools_${platformInfo.name}-x86_64-${VERSION}.${platformInfo.ext}`; const url = `${BASE_URL}/${archiveName}`; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kiwix-install-')); const archivePath = path.join(tmpDir, archiveName); console.log(`Downloading kiwix-tools: ${url}`); await download(url, archivePath); console.log('Extracting...'); await fs.promises.mkdir(BIN_DIR, {recursive: true}); if (platformInfo.ext === 'zip') { await extractZip(archivePath, BIN_DIR); } else { await extractTarGz(archivePath, BIN_DIR); } for (const entry of await fs.promises.readdir(BIN_DIR)) { if (entry.startsWith('kiwix-')) { const fullPath = path.join(BIN_DIR, entry); await fs.promises.chmod(fullPath, 0o755); } } console.log(`Installed to: ${BIN_DIR}`); await fs.promises.rm(tmpDir, {recursive: true, force: true}); } install().catch(err => { console.error('Install failed:', err.message); process.exit(1); });