UNPKG

repomix

Version:

A tool to pack repository contents to single file for AI consumption

189 lines (187 loc) 8.57 kB
import * as fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import pc from 'picocolors'; import { execGitShallowClone } from '../../core/git/gitCommand.js'; import { downloadGitHubArchive, isArchiveDownloadSupported } from '../../core/git/gitHubArchive.js'; import { getRemoteRefs } from '../../core/git/gitRemoteHandle.js'; import { isGitHubRepository, parseGitHubRepoInfo, parseRemoteValue } from '../../core/git/gitRemoteParse.js'; import { isGitInstalled } from '../../core/git/gitRepositoryHandle.js'; import { generateDefaultSkillNameFromUrl, generateProjectNameFromUrl } from '../../core/skill/skillUtils.js'; import { RepomixError } from '../../shared/errorHandle.js'; import { logger } from '../../shared/logger.js'; import { Spinner } from '../cliSpinner.js'; import { promptSkillLocation, resolveAndPrepareSkillDir } from '../prompts/skillPrompts.js'; import { runDefaultAction } from './defaultAction.js'; export const runRemoteAction = async (repoUrl, cliOptions, deps = { isGitInstalled, execGitShallowClone, getRemoteRefs, runDefaultAction, downloadGitHubArchive, isGitHubRepository, parseGitHubRepoInfo, isArchiveDownloadSupported, }) => { if (cliOptions.config && !path.isAbsolute(cliOptions.config)) { throw new RepomixError(`In remote mode, --config must be an absolute path to avoid loading config from the cloned repository.\n` + ` Provided: ${cliOptions.config}\n` + ` Example: repomix --remote <url> --config /home/user/repomix.config.json`); } let tempDirPath = await createTempDirectory(); let result; let downloadMethod = 'git'; try { const githubRepoInfo = deps.parseGitHubRepoInfo(repoUrl); const shouldTryArchive = githubRepoInfo && deps.isArchiveDownloadSupported(githubRepoInfo); if (shouldTryArchive) { const spinner = new Spinner('Downloading repository archive...', cliOptions); try { spinner.start(); const repoInfoWithBranch = { ...githubRepoInfo, ref: cliOptions.remoteBranch ?? githubRepoInfo.ref, }; await deps.downloadGitHubArchive(repoInfoWithBranch, tempDirPath, { timeout: 60000, retries: 2, }, (progress) => { if (progress.percentage !== null) { spinner.update(`Downloading repository archive... (${progress.percentage}%)`); } else { const downloadedMB = (progress.downloaded / 1024 / 1024).toFixed(1); spinner.update(`Downloading repository archive... (${downloadedMB} MB)`); } }); downloadMethod = 'archive'; spinner.succeed('Repository archive downloaded successfully!'); logger.log(''); } catch (archiveError) { spinner.fail('Archive download failed, trying git clone...'); logger.trace('Archive download error:', archiveError.message); await cleanupTempDirectory(tempDirPath); tempDirPath = await createTempDirectory(); await performGitClone(repoUrl, tempDirPath, cliOptions, deps); downloadMethod = 'git'; } } else { await performGitClone(repoUrl, tempDirPath, cliOptions, deps); downloadMethod = 'git'; } let skillName; let skillDir; let skillProjectName; if (cliOptions.skillGenerate !== undefined) { skillName = typeof cliOptions.skillGenerate === 'string' ? cliOptions.skillGenerate : generateDefaultSkillNameFromUrl(repoUrl); skillProjectName = generateProjectNameFromUrl(repoUrl); if (cliOptions.skillOutput) { if (!cliOptions.skillOutput.trim()) { throw new RepomixError('--skill-output path cannot be empty'); } skillDir = await resolveAndPrepareSkillDir(cliOptions.skillOutput, process.cwd(), cliOptions.force ?? false); } else { const promptResult = await promptSkillLocation(skillName, process.cwd()); skillDir = promptResult.skillDir; } } const skillSourceUrl = cliOptions.skillGenerate !== undefined ? repoUrl : undefined; const trustRemoteConfig = cliOptions.remoteTrustConfig || process.env.REPOMIX_REMOTE_TRUST_CONFIG === 'true'; const optionsWithSkill = { ...cliOptions, skillName, skillDir, skillProjectName, skillSourceUrl, skipLocalConfig: !trustRemoteConfig, }; result = await deps.runDefaultAction([tempDirPath], tempDirPath, optionsWithSkill); if (!cliOptions.stdout && result.config.skillGenerate === undefined) { await copyOutputToCurrentDirectory(tempDirPath, process.cwd(), result.config.output.filePath); } logger.trace(`Repository obtained via ${downloadMethod} method`); } finally { await cleanupTempDirectory(tempDirPath); } return result; }; const performGitClone = async (repoUrl, tempDirPath, cliOptions, deps) => { if (!(await deps.isGitInstalled())) { throw new RepomixError('Git is not installed or not in the system PATH.'); } let refs = []; try { refs = await deps.getRemoteRefs(parseRemoteValue(repoUrl).repoUrl); logger.trace(`Retrieved ${refs.length} refs from remote repository`); } catch (error) { logger.trace('Failed to get remote refs, proceeding without them:', error.message); } const parsedFields = parseRemoteValue(repoUrl, refs); const spinner = new Spinner('Cloning repository...', cliOptions); try { spinner.start(); await cloneRepository(parsedFields.repoUrl, tempDirPath, cliOptions.remoteBranch || parsedFields.remoteBranch, { execGitShallowClone: deps.execGitShallowClone, }); spinner.succeed('Repository cloned successfully!'); logger.log(''); } catch (error) { spinner.fail('Error during repository cloning. cleanup...'); throw error; } }; export const createTempDirectory = async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'repomix-')); logger.trace(`Created temporary directory. (path: ${pc.dim(tempDir)})`); return tempDir; }; export const cloneRepository = async (url, directory, remoteBranch, deps = { execGitShallowClone, }) => { logger.log(`Clone repository: ${url} to temporary directory. ${pc.dim(`path: ${directory}`)}`); logger.log(''); try { await deps.execGitShallowClone(url, directory, remoteBranch); } catch (error) { throw new RepomixError(`Failed to clone repository: ${error.message}`); } }; export const cleanupTempDirectory = async (directory) => { logger.trace(`Cleaning up temporary directory: ${directory}`); await fs.rm(directory, { recursive: true, force: true }); }; export const copyOutputToCurrentDirectory = async (sourceDir, targetDir, outputFileName) => { const sourcePath = path.resolve(sourceDir, outputFileName); const targetPath = path.resolve(targetDir, outputFileName); if (sourcePath === targetPath) { logger.trace(`Source and target are the same (${sourcePath}), skipping copy`); return; } try { logger.trace(`Copying output file from: ${sourcePath} to: ${targetPath}`); await fs.mkdir(path.dirname(targetPath), { recursive: true }); await fs.copyFile(sourcePath, targetPath); } catch (error) { const nodeError = error; if (nodeError.code === 'EPERM' || nodeError.code === 'EACCES') { throw new RepomixError(`Failed to copy output file to ${targetPath}: Permission denied. The current directory may be protected or require elevated permissions. Please try one of the following: • Run from a different directory (e.g., your home directory or Documents folder) • Use the --output flag to specify a writable location: --output ~/repomix-output.xml • Use --stdout to print output directly to the console`); } throw new RepomixError(`Failed to copy output file: ${error.message}`); } };