Skip to main content
Glama

Heroku MCP server

Official
by heroku
tarball.ts3.88 kB
import zlib from 'node:zlib'; import { readFile, stat, readdir } from 'node:fs/promises'; import path from 'node:path'; import { execSync } from 'node:child_process'; import * as tar from 'tar-stream'; export type GeneratedContent = { relativePath: string; contents: Uint8Array }; /** * Creates a compressed tarball (tar.gz) from the contents of a workspace folder. * The function respects git ignore rules and excludes node_modules directory. * * @param root - The workspace folder to create a tarball from or undefined when only generated content should be included * @param additionalContents - Additional contents to include in the tarball * @returns A Promise that resolves to a Uint8Array containing the compressed tarball data * * @example * const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; * if (workspaceFolder) { * const tarballData = await packSources(workspaceFolder); * } * * - Files in node_modules are automatically excluded * - Files matched by .gitignore rules are excluded * - File paths in the archive are relative to the workspace root * - The function uses streaming to handle large directories efficiently * * @throws {Error} If unable to read workspace files or create the tarball */ export async function packSources( root: string | undefined, additionalContents: GeneratedContent[] = [] ): Promise<Uint8Array> { const files = root ? await getSourceFilePaths(root) : []; const pack = tar.pack(); const gzip = zlib.createGzip(); const chunks: Buffer[] = []; gzip.on('data', (chunk) => chunks.push(chunk as Buffer)); pack.pipe(gzip); for (const file of files) { try { const content = await readFile(file); const relativePath = path.relative(root!, file); pack.entry({ name: relativePath }, Buffer.from(content)); } catch { // Unreadable file or directory } } for (const additionalContent of additionalContents) { const { contents, relativePath } = additionalContent; pack.entry({ name: relativePath }, Buffer.from(contents)); } pack.finalize(); await new Promise((resolve) => gzip.on('end', resolve)); return Buffer.concat(chunks); } /** * Gets all file paths from the root directory of the workspace * * @param root The root directory of the workspace * @returns A promise that resolves to an array of file paths */ export async function getSourceFilePaths(root: string): Promise<string[]> { let result: string; try { // We expect most users to have a git repo in their workspace result = execSync('git ls-files -c -o --exclude-standard', { cwd: root }).toString(); } catch { // If no git repo is found, we'll just return the root directory result = './'; } const paths = result.split('\n').filter((file) => Boolean(file)); const allFiles: string[] = []; for (const p of paths) { const fullPath = path.join(root, p); try { const stats = await stat(fullPath); if (stats.isDirectory()) { // Recursively get all files in directory const files = await walkDirectory(fullPath); allFiles.push(...files); } else { allFiles.push(fullPath); } } catch { // Ignore } } return allFiles; } /** * Walks a directory and returns all file paths * * @param dir The directory to walk * @returns A promise that resolves to an array of file paths */ async function walkDirectory(dir: string): Promise<string[]> { const files: string[] = []; try { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { const subFiles = await walkDirectory(fullPath); files.push(...subFiles); } else { files.push(fullPath); } } } catch { // Ignore } return files; }

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/heroku/heroku-mcp-server'

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