Skip to main content
Glama
download.ts4.54 kB
/** * @fileoverview Tarball download with progress bar */ import fs from 'fs'; import https from 'https'; import chalk from 'chalk'; import cliProgress from 'cli-progress'; import type { TarballInfo } from './types.js'; /** * Format bytes to human-readable string */ export function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /** * Fetch tarball info (URL and size) from npm registry */ export async function fetchTarballInfo( version: string ): Promise<TarballInfo | null> { return new Promise((resolve) => { const options = { hostname: 'registry.npmjs.org', path: `/task-master-ai/${version}`, method: 'GET', headers: { Accept: 'application/json', 'User-Agent': `task-master-ai/${version}` } }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { if (res.statusCode !== 200) { resolve(null); return; } const packageData = JSON.parse(data); const tarballUrl = packageData.dist?.tarball; const unpackedSize = packageData.dist?.unpackedSize; if (!tarballUrl) { resolve(null); return; } resolve({ url: tarballUrl, size: unpackedSize || 0 }); } catch { resolve(null); } }); }); req.on('error', () => { resolve(null); }); req.setTimeout(10000, () => { req.destroy(); resolve(null); }); req.end(); }); } /** * Download tarball with progress bar */ export async function downloadTarballWithProgress( tarballUrl: string, destPath: string, version: string, maxRedirects = 5 ): Promise<boolean> { if (maxRedirects <= 0) { console.error(chalk.red('Too many redirects')); return Promise.resolve(false); } return new Promise((resolve) => { const url = new URL(tarballUrl); const options = { hostname: url.hostname, path: url.pathname, method: 'GET', headers: { 'User-Agent': `task-master-ai/${version}` } }; const req = https.request(options, (res) => { // Handle redirects if (res.statusCode === 301 || res.statusCode === 302) { const redirectUrl = res.headers.location; if (redirectUrl) { downloadTarballWithProgress( redirectUrl, destPath, version, maxRedirects - 1 ) .then(resolve) .catch(() => resolve(false)); return; } resolve(false); return; } if (res.statusCode !== 200) { resolve(false); return; } const totalSize = Number.parseInt( res.headers['content-length'] || '0', 10 ); let downloadedSize = 0; // Create progress bar const progressBar = new cliProgress.SingleBar( { format: `${chalk.blue('Downloading')} ${chalk.cyan('{bar}')} {percentage}% | {downloaded}/{total}`, barCompleteChar: '\u2588', barIncompleteChar: '\u2591', hideCursor: true, clearOnComplete: true }, cliProgress.Presets.shades_classic ); if (totalSize > 0) { progressBar.start(totalSize, 0, { downloaded: formatBytes(0), total: formatBytes(totalSize) }); } else { // If no content-length, show indeterminate progress console.log(chalk.blue(`Downloading task-master-ai@${version}...`)); } const fileStream = fs.createWriteStream(destPath); res.on('data', (chunk: Buffer) => { downloadedSize += chunk.length; if (totalSize > 0) { progressBar.update(downloadedSize, { downloaded: formatBytes(downloadedSize), total: formatBytes(totalSize) }); } }); res.pipe(fileStream); fileStream.on('finish', () => { if (totalSize > 0) { progressBar.stop(); } fileStream.close(() => { console.log( chalk.green('✓') + chalk.dim(` Downloaded ${formatBytes(downloadedSize)}`) ); resolve(true); }); }); fileStream.on('error', (err) => { if (totalSize > 0) { progressBar.stop(); } console.error(chalk.red('Download error:'), err.message); fs.unlink(destPath, () => {}); // Cleanup partial file resolve(false); }); }); req.on('error', (err) => { console.error(chalk.red('Request error:'), err.message); resolve(false); }); req.setTimeout(120000, () => { req.destroy(); console.error(chalk.red('Download timeout')); resolve(false); }); req.end(); }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server