aico-pack
Version:
A tool to pack repository contents to single file for AI consumption
249 lines • 11.3 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import fs from 'node:fs/promises';
import path from 'node:path';
import { globby } from 'globby';
import { minimatch } from 'minimatch';
import { defaultIgnoreList } from '../../config/defaultIgnore.js';
import { RepomixError } from '../../shared/errorHandle.js';
import { logger } from '../../shared/logger.js';
import { sortPaths } from './filePathSort.js';
import { PermissionError, checkDirectoryPermissions } from './permissionCheck.js';
const findEmptyDirectories = (rootDir, directories, ignorePatterns) => __awaiter(void 0, void 0, void 0, function* () {
const emptyDirs = [];
for (const dir of directories) {
const fullPath = path.join(rootDir, dir);
try {
const entries = yield fs.readdir(fullPath);
const hasVisibleContents = entries.some((entry) => !entry.startsWith('.'));
if (!hasVisibleContents) {
// This checks if the directory itself matches any ignore patterns
const shouldIgnore = ignorePatterns.some((pattern) => minimatch(dir, pattern) || minimatch(`${dir}/`, pattern));
if (!shouldIgnore) {
emptyDirs.push(dir);
}
}
}
catch (error) {
logger.debug(`Error checking directory ${dir}:`, error);
}
}
return emptyDirs;
});
// Check if a path is a git worktree reference file
const isGitWorktreeRef = (gitPath) => __awaiter(void 0, void 0, void 0, function* () {
try {
const stats = yield fs.stat(gitPath);
if (!stats.isFile()) {
return false;
}
const content = yield fs.readFile(gitPath, 'utf8');
return content.startsWith('gitdir:');
}
catch (_a) {
return false;
}
});
/**
* Escapes special characters in glob patterns to handle paths with parentheses.
* Example: "src/(categories)" -> "src/\\(categories\\)"
*/
export const escapeGlobPattern = (pattern) => {
// First escape backslashes
const escapedBackslashes = pattern.replace(/\\/g, '\\\\');
// Then escape special characters () and [], but NOT {}
return escapedBackslashes.replace(/[()[\]]/g, '\\$&');
};
/**
* Normalizes glob patterns by removing trailing slashes and ensuring consistent directory pattern handling.
* Makes "**\/folder", "**\/folder/", and "**\/folder/**\/*" behave identically.
*
* @param pattern The glob pattern to normalize
* @returns The normalized pattern
*/
export const normalizeGlobPattern = (pattern) => {
// Remove trailing slash but preserve patterns that end with "**/"
if (pattern.endsWith('/') && !pattern.endsWith('**/')) {
return pattern.slice(0, -1);
}
// Convert **/folder to **/folder/** for consistent ignore pattern behavior
if (pattern.startsWith('**/') && !pattern.includes('/**')) {
return `${pattern}/**`;
}
return pattern;
};
// Get all file paths considering the config
export const searchFiles = (rootDir, config) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
// Check if the path exists and get its type
let pathStats;
try {
pathStats = yield fs.stat(rootDir);
}
catch (error) {
if (error instanceof Error && 'code' in error) {
const errorCode = error.code;
if (errorCode === 'ENOENT') {
throw new RepomixError(`Target path does not exist: ${rootDir}`);
}
if (errorCode === 'EPERM' || errorCode === 'EACCES') {
throw new PermissionError(`Permission denied while accessing path. Please check folder access permissions for your terminal app. path: ${rootDir}`, rootDir, errorCode);
}
// Handle other specific error codes with more context
throw new RepomixError(`Failed to access path: ${rootDir}. Error code: ${errorCode}. ${error.message}`);
}
// Preserve original error stack trace for debugging
const repomixError = new RepomixError(`Failed to access path: ${rootDir}. Reason: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
repomixError.cause = error;
throw repomixError;
}
// Check if the path is a directory
if (!pathStats.isDirectory()) {
throw new RepomixError(`Target path is not a directory: ${rootDir}. Please specify a directory path, not a file path.`);
}
// Now check directory permissions
const permissionCheck = yield checkDirectoryPermissions(rootDir);
if (((_a = permissionCheck.details) === null || _a === void 0 ? void 0 : _a.read) !== true) {
if (permissionCheck.error instanceof PermissionError) {
throw permissionCheck.error;
}
throw new RepomixError(`Target directory is not readable or does not exist. Please check folder access permissions for your terminal app.\npath: ${rootDir}`);
}
const includePatterns = config.include.length > 0 ? config.include.map((pattern) => escapeGlobPattern(pattern)) : ['**/*'];
try {
const [ignorePatterns, ignoreFilePatterns] = yield Promise.all([
getIgnorePatterns(rootDir, config),
getIgnoreFilePatterns(config),
]);
// Normalize ignore patterns to handle trailing slashes consistently
const normalizedIgnorePatterns = ignorePatterns.map(normalizeGlobPattern);
logger.trace('Include patterns:', includePatterns);
logger.trace('Ignore patterns:', normalizedIgnorePatterns);
logger.trace('Ignore file patterns:', ignoreFilePatterns);
// Check if .git is a worktree reference
const gitPath = path.join(rootDir, '.git');
const isWorktree = yield isGitWorktreeRef(gitPath);
// Modify ignore patterns for git worktree
const adjustedIgnorePatterns = [...normalizedIgnorePatterns];
if (isWorktree) {
// Remove '.git/**' pattern and add '.git' to ignore the reference file
const gitIndex = adjustedIgnorePatterns.indexOf('.git/**');
if (gitIndex !== -1) {
adjustedIgnorePatterns.splice(gitIndex, 1);
adjustedIgnorePatterns.push('.git');
}
}
const filePaths = yield globby(includePatterns, {
cwd: rootDir,
ignore: [...adjustedIgnorePatterns],
ignoreFiles: [...ignoreFilePatterns],
onlyFiles: true,
absolute: false,
dot: true,
followSymbolicLinks: false,
}).catch((error) => {
// Handle EPERM errors specifically
if (error.code === 'EPERM' || error.code === 'EACCES') {
throw new PermissionError(`Permission denied while scanning directory. Please check folder access permissions for your terminal app. path: ${rootDir}`, rootDir);
}
throw error;
});
let emptyDirPaths = [];
if (config.output.includeEmptyDirectories) {
const directories = yield globby(includePatterns, {
cwd: rootDir,
ignore: [...adjustedIgnorePatterns],
ignoreFiles: [...ignoreFilePatterns],
onlyDirectories: true,
absolute: false,
dot: true,
followSymbolicLinks: false,
});
emptyDirPaths = yield findEmptyDirectories(rootDir, directories, adjustedIgnorePatterns);
}
logger.trace(`Filtered ${filePaths.length} files`);
return {
filePaths: sortPaths(filePaths),
emptyDirPaths: sortPaths(emptyDirPaths),
};
}
catch (error) {
// Re-throw PermissionError as is
if (error instanceof PermissionError) {
throw error;
}
if (error instanceof Error) {
logger.error('Error filtering files:', error.message);
throw new Error(`Failed to filter files in directory ${rootDir}. Reason: ${error.message}`);
}
logger.error('An unexpected error occurred:', error);
throw new Error('An unexpected error occurred while filtering files.');
}
});
export const parseIgnoreContent = (content) => {
if (!content)
return [];
return content.split('\n').reduce((acc, line) => {
const trimmedLine = line.trim();
if (trimmedLine && !trimmedLine.startsWith('#')) {
acc.push(trimmedLine);
}
return acc;
}, []);
};
export const getIgnoreFilePatterns = (config) => __awaiter(void 0, void 0, void 0, function* () {
const ignoreFilePatterns = [];
if (config.ignore.useGitignore) {
ignoreFilePatterns.push('**/.gitignore');
}
ignoreFilePatterns.push('**/.repomixignore');
return ignoreFilePatterns;
});
export const getIgnorePatterns = (rootDir, config) => __awaiter(void 0, void 0, void 0, function* () {
const ignorePatterns = new Set();
// Add default ignore patterns
if (config.ignore.useDefaultPatterns) {
logger.trace('Adding default ignore patterns');
for (const pattern of defaultIgnoreList) {
ignorePatterns.add(pattern);
}
}
// Add repomix output file
if (config.output.filePath) {
const absoluteOutputPath = path.resolve(config.cwd, config.output.filePath);
const relativeToTargetPath = path.relative(rootDir, absoluteOutputPath);
logger.trace('Adding output file to ignore patterns:', relativeToTargetPath);
ignorePatterns.add(relativeToTargetPath);
}
// Add custom ignore patterns
if (config.ignore.customPatterns) {
logger.trace('Adding custom ignore patterns:', config.ignore.customPatterns);
for (const pattern of config.ignore.customPatterns) {
ignorePatterns.add(pattern);
}
}
// Add patterns from .git/info/exclude if useGitignore is enabled
if (config.ignore.useGitignore) {
const excludeFilePath = path.join(rootDir, '.git', 'info', 'exclude');
try {
const excludeFileContent = yield fs.readFile(excludeFilePath, 'utf8');
const excludePatterns = parseIgnoreContent(excludeFileContent);
for (const pattern of excludePatterns) {
ignorePatterns.add(pattern);
}
}
catch (error) {
// File might not exist or might not be accessible, which is fine
logger.trace('Could not read .git/info/exclude file:', error instanceof Error ? error.message : String(error));
}
}
return Array.from(ignorePatterns);
});
//# sourceMappingURL=fileSearch.js.map