+206
-93
@@ -8,2 +8,3 @@ #!/usr/bin/env node | ||
| const node_url = require('node:url'); | ||
| const readline = require('readline'); | ||
| const ignore = require('ignore'); | ||
@@ -17,2 +18,3 @@ | ||
| const process__default = /*#__PURE__*/_interopDefaultCompat(process); | ||
| const readline__default = /*#__PURE__*/_interopDefaultCompat(readline); | ||
| const ignore__default = /*#__PURE__*/_interopDefaultCompat(ignore); | ||
@@ -277,6 +279,11 @@ | ||
| extensions: null, | ||
| verbose: false | ||
| verbose: false, | ||
| includeFiles: null, | ||
| excludeFiles: null, | ||
| includeDirs: null, | ||
| excludeDirs: null | ||
| }; | ||
| for (let i = 2; i < argv.length; i++) { | ||
| const arg = argv[i]; | ||
| const args = argv[0]?.endsWith("node") ? argv.slice(2) : argv; | ||
| for (let i = 0; i < args.length; i++) { | ||
| const arg = args[i]; | ||
| if (arg === "-h" || arg === "--help") { | ||
@@ -287,7 +294,7 @@ printHelp(); | ||
| result.verbose = true; | ||
| } else if ((arg === "-o" || arg === "--output") && argv[i + 1]) { | ||
| result.output = argv[i + 1]; | ||
| } else if ((arg === "-o" || arg === "--output") && args[i + 1]) { | ||
| result.output = args[i + 1]; | ||
| i++; | ||
| } else if ((arg === "--max-tokens" || arg === "-tok") && argv[i + 1]) { | ||
| const tokens = parseInt(argv[i + 1]); | ||
| } else if ((arg === "--max-tokens" || arg === "-tok") && args[i + 1]) { | ||
| const tokens = parseInt(args[i + 1]); | ||
| if (!isNaN(tokens)) { | ||
@@ -297,5 +304,25 @@ result.maxTokens = tokens; | ||
| i++; | ||
| } else if ((arg === "-e" || arg === "--extension") && argv[i + 1]) { | ||
| result.extensions = argv[i + 1].split(",").map((ext) => ext.startsWith(".") ? ext : `.${ext}`); | ||
| } else if ((arg === "-e" || arg === "--extension") && args[i + 1]) { | ||
| result.extensions = args[i + 1].split(",").map((ext) => ext.startsWith(".") ? ext : `.${ext}`); | ||
| i++; | ||
| } else if ((arg === "-if" || arg === "--include-files") && args[i + 1]) { | ||
| result.includeFiles = args[i + 1].split(","); | ||
| i++; | ||
| } else if ((arg === "-ef" || arg === "--exclude-files") && args[i + 1]) { | ||
| result.excludeFiles = args[i + 1].split(","); | ||
| i++; | ||
| } else if ((arg === "-id" || arg === "--include-dir") && args[i + 1]) { | ||
| result.includeDirs = args[i + 1].split(","); | ||
| i++; | ||
| } else if ((arg === "-ed" || arg === "--exclude-dir") && args[i + 1]) { | ||
| result.excludeDirs = args[i + 1].split(","); | ||
| i++; | ||
| } else if (arg.startsWith("--include-files=")) { | ||
| result.includeFiles = arg.split("=")[1].split(","); | ||
| } else if (arg.startsWith("--exclude-files=")) { | ||
| result.excludeFiles = arg.split("=")[1].split(","); | ||
| } else if (arg.startsWith("--include-dir=")) { | ||
| result.includeDirs = arg.split("=")[1].split(","); | ||
| } else if (arg.startsWith("--exclude-dir=")) { | ||
| result.excludeDirs = arg.split("=")[1].split(","); | ||
| } | ||
@@ -305,45 +332,119 @@ } | ||
| } | ||
| const { output, maxTokens, extensions, verbose } = parseArgs(process__default.argv); | ||
| const ig = ignore__default().add( | ||
| DEFAULT_IGNORE_PATTERNS.split("\n").filter( | ||
| (line) => line && !line.startsWith("#") | ||
| ) | ||
| ); | ||
| try { | ||
| const gitignoreContent = fs__default.readFileSync( | ||
| path__default.join(process__default.cwd(), ".gitignore"), | ||
| "utf8" | ||
| async function main() { | ||
| const { | ||
| output: outputFile, | ||
| // rename to avoid conflict | ||
| maxTokens, | ||
| extensions, | ||
| verbose, | ||
| includeFiles, | ||
| excludeFiles, | ||
| includeDirs, | ||
| excludeDirs | ||
| } = parseArgs(process__default.argv); | ||
| const ig = ignore__default().add( | ||
| DEFAULT_IGNORE_PATTERNS.split("\n").filter( | ||
| (line) => line && !line.startsWith("#") | ||
| ) | ||
| ); | ||
| ig.add(gitignoreContent); | ||
| } catch { | ||
| try { | ||
| const gitignoreContent = fs__default.readFileSync( | ||
| path__default.join(process__default.cwd(), ".gitignore"), | ||
| "utf8" | ||
| ); | ||
| ig.add(gitignoreContent); | ||
| } catch { | ||
| } | ||
| try { | ||
| const codefetchignoreContent = fs__default.readFileSync( | ||
| path__default.join(process__default.cwd(), ".codefetchignore"), | ||
| "utf8" | ||
| ); | ||
| ig.add(codefetchignoreContent); | ||
| } catch { | ||
| } | ||
| const extensionSet = extensions ? new Set(extensions) : null; | ||
| const allFiles = await collectFiles(process__default.cwd(), { | ||
| ig, | ||
| extensionSet, | ||
| excludeFiles, | ||
| includeFiles, | ||
| excludeDirs, | ||
| includeDirs | ||
| }); | ||
| if (outputFile) { | ||
| const codefetchDir = path__default.join(process__default.cwd(), "codefetch"); | ||
| if (!fs__default.existsSync(codefetchDir)) { | ||
| fs__default.mkdirSync(codefetchDir, { recursive: true }); | ||
| console.log("Created codefetch directory."); | ||
| } | ||
| const codefetchignorePath = path__default.join(process__default.cwd(), ".codefetchignore"); | ||
| if (!fs__default.existsSync(codefetchignorePath)) { | ||
| const ignoreContent = "# Codefetch specific ignores\ncodefetch/\n"; | ||
| fs__default.writeFileSync(codefetchignorePath, ignoreContent, "utf8"); | ||
| console.log( | ||
| "Created .codefetchignore file. Add 'codefetch/' to your .gitignore to avoid committing fetched code." | ||
| ); | ||
| } | ||
| } | ||
| const outputPath = outputFile ? path__default.join(process__default.cwd(), "codefetch", outputFile) : null; | ||
| const totalTokens = await generateMarkdown(allFiles, { | ||
| outputPath, | ||
| maxTokens, | ||
| verbose | ||
| }); | ||
| if (outputFile) { | ||
| console.log("\nSummary:"); | ||
| console.log("\u2713 Code was successfully fetched"); | ||
| console.log(`\u2713 Output written to: ${outputPath}`); | ||
| console.log(`\u2713 Approximate token count: ${totalTokens}`); | ||
| } | ||
| } | ||
| try { | ||
| const codefetchignoreContent = fs__default.readFileSync( | ||
| path__default.join(process__default.cwd(), ".codefetchignore"), | ||
| "utf8" | ||
| async function collectFiles(dir, options) { | ||
| const results = []; | ||
| const list = await fs__default.promises.readdir(dir); | ||
| const excludePatterns = options.excludeFiles?.map( | ||
| (pattern) => new RegExp(pattern.replace(/\*/g, ".*")) | ||
| ); | ||
| ig.add(codefetchignoreContent); | ||
| } catch { | ||
| } | ||
| function collectFiles(dir) { | ||
| const results = []; | ||
| const list = fs__default.readdirSync(dir); | ||
| const includePatterns = options.includeFiles?.map( | ||
| (pattern) => new RegExp(pattern.replace(/\*/g, ".*")) | ||
| ); | ||
| for (const filename of list) { | ||
| const filePath = path__default.join(dir, filename); | ||
| const relPath = path__default.relative(process__default.cwd(), filePath); | ||
| if (ig.ignores(relPath)) { | ||
| if (options.ig.ignores(relPath)) { | ||
| continue; | ||
| } | ||
| const stat = fs__default.statSync(filePath); | ||
| const stat = await fs__default.promises.stat(filePath); | ||
| if (stat.isDirectory()) { | ||
| verbose && console.log(`Processing directory: ${relPath}`); | ||
| results.push(...collectFiles(filePath)); | ||
| const dirName = path__default.basename(filePath); | ||
| if (options.excludeDirs && options.excludeDirs.includes(dirName)) { | ||
| continue; | ||
| } | ||
| if (options.includeDirs && !options.includeDirs.includes(dirName)) { | ||
| continue; | ||
| } | ||
| results.push( | ||
| ...await collectFiles(filePath, { | ||
| ig: options.ig, | ||
| extensionSet: options.extensionSet, | ||
| excludeFiles: options.excludeFiles, | ||
| includeFiles: options.includeFiles, | ||
| excludeDirs: options.excludeDirs, | ||
| includeDirs: options.includeDirs | ||
| }) | ||
| ); | ||
| } else { | ||
| if (extensions) { | ||
| if (excludePatterns && excludePatterns.some((pattern) => pattern.test(filename))) { | ||
| continue; | ||
| } | ||
| if (includePatterns && !includePatterns.some((pattern) => pattern.test(filename))) { | ||
| continue; | ||
| } | ||
| if (options.extensionSet) { | ||
| const ext = path__default.extname(filename); | ||
| if (!extensions.includes(ext)) { | ||
| if (!options.extensionSet.has(ext)) { | ||
| continue; | ||
| } | ||
| } | ||
| verbose && console.log(`Processing file: ${relPath}`); | ||
| results.push(filePath); | ||
@@ -354,59 +455,52 @@ } | ||
| } | ||
| const allFiles = collectFiles(process__default.cwd()); | ||
| function estimateTokens(text) { | ||
| return text.split(/[\s\p{P}]+/u).length; | ||
| } | ||
| function generateMarkdown(files) { | ||
| const lines = []; | ||
| async function generateMarkdown(files, options) { | ||
| if (options.outputPath) { | ||
| const outputDir = path__default.dirname(options.outputPath); | ||
| if (!fs__default.existsSync(outputDir)) { | ||
| fs__default.mkdirSync(outputDir, { recursive: true }); | ||
| } | ||
| } | ||
| const output = options.outputPath ? fs__default.createWriteStream(options.outputPath) : process__default.stdout; | ||
| let totalTokens = 0; | ||
| verbose && console.log("\nGenerating markdown output..."); | ||
| for (const file of files) { | ||
| const relativePath = path__default.relative(process__default.cwd(), file); | ||
| const content = fs__default.readFileSync(file, "utf8"); | ||
| const fileTokens = estimateTokens(content); | ||
| if (maxTokens && totalTokens + fileTokens > maxTokens) { | ||
| verbose && console.log(`Skipping ${relativePath} (would exceed token limit)`); | ||
| continue; | ||
| const fileStream = fs__default.createReadStream(file, { encoding: "utf8" }); | ||
| const rl = readline__default.createInterface({ | ||
| input: fileStream, | ||
| crlfDelay: Infinity | ||
| }); | ||
| output.write(`/${relativePath}: | ||
| `); | ||
| output.write("-".repeat(80) + "\n"); | ||
| let lineNumber = 1; | ||
| for await (const line of rl) { | ||
| const lineTokens = estimateTokens(line); | ||
| if (options.maxTokens && totalTokens + lineTokens > options.maxTokens) { | ||
| break; | ||
| } | ||
| const formattedLine = `${lineNumber} | ${line} | ||
| `; | ||
| output.write(formattedLine); | ||
| totalTokens += lineTokens; | ||
| lineNumber++; | ||
| } | ||
| verbose && console.log(`Adding to output: ${relativePath} (${fileTokens} tokens)`); | ||
| totalTokens += fileTokens; | ||
| lines.push(`/${relativePath}:`); | ||
| lines.push( | ||
| "--------------------------------------------------------------------------------" | ||
| ); | ||
| const fileLines = content.split("\n"); | ||
| fileLines.forEach((line, i) => { | ||
| lines.push(`${i + 1} | ${line}`); | ||
| output.write("-".repeat(80) + "\n\n"); | ||
| if (options.maxTokens && totalTokens >= options.maxTokens) { | ||
| break; | ||
| } | ||
| } | ||
| if (options.outputPath) { | ||
| await new Promise((resolve, reject) => { | ||
| output.end((err) => { | ||
| if (err) | ||
| reject(err); | ||
| else | ||
| resolve(); | ||
| }); | ||
| }); | ||
| lines.push(""); | ||
| lines.push( | ||
| "--------------------------------------------------------------------------------" | ||
| ); | ||
| } | ||
| return lines.join("\n"); | ||
| return totalTokens; | ||
| } | ||
| const final = generateMarkdown(allFiles); | ||
| if (output) { | ||
| const codefetchDir = path__default.join(process__default.cwd(), "codefetch"); | ||
| if (!fs__default.existsSync(codefetchDir)) { | ||
| fs__default.mkdirSync(codefetchDir, { recursive: true }); | ||
| console.log("Created codefetch directory."); | ||
| } | ||
| const codefetchignorePath = path__default.join(process__default.cwd(), ".codefetchignore"); | ||
| if (!fs__default.existsSync(codefetchignorePath)) { | ||
| const ignoreContent = "# Codefetch specific ignores\ncodefetch/\n"; | ||
| fs__default.writeFileSync(codefetchignorePath, ignoreContent, "utf8"); | ||
| console.log( | ||
| "Created .codefetchignore file. Add 'codefetch/' to your .gitignore to avoid committing fetched code." | ||
| ); | ||
| } | ||
| const outputPath = path__default.join(codefetchDir, output); | ||
| fs__default.writeFileSync(outputPath, final, "utf8"); | ||
| const totalTokens = estimateTokens(final); | ||
| console.log("\nSummary:"); | ||
| console.log("\u2713 Code was successfully fetched"); | ||
| console.log(`\u2713 Output written to: ${outputPath}`); | ||
| console.log(`\u2713 Approximate token count: ${totalTokens}`); | ||
| } else { | ||
| console.log(final); | ||
| function estimateTokens(text) { | ||
| return text.split(/[\s\p{P}]+/u).filter(Boolean).length; | ||
| } | ||
@@ -418,8 +512,27 @@ function printHelp() { | ||
| Options: | ||
| -o, --output <file> Specify output filename | ||
| -tok, --max-tokens <n> Limit output tokens (useful for AI models) | ||
| -e, --extension <ext,...> Filter by file extensions (e.g., .ts,.js) | ||
| -v, --verbose Show detailed processing information | ||
| -h, --help Display this help message | ||
| -o, --output <file> Specify output filename | ||
| -tok, --max-tokens <n> Limit output tokens (useful for AI models) | ||
| -e, --extension <ext,...> Filter by file extensions (e.g., .ts,.js) | ||
| -if, --include-files <p,..> Include specific files (supports patterns) | ||
| -ef, --exclude-files <p,..> Exclude specific files (supports patterns) | ||
| -id, --include-dir <d,...> Include specific directories | ||
| -ed, --exclude-dir <d,...> Exclude specific directories | ||
| -v, --verbose Show detailed processing information | ||
| -h, --help Display this help message | ||
| Examples: | ||
| codefetch --exclude-dir=node_modules,public | ||
| codefetch --include-files=*.ts,*.js | ||
| codefetch -ef test.ts,temp.js -id src | ||
| `); | ||
| } | ||
| if (require.main === module) { | ||
| main().catch((error) => { | ||
| console.error("Error:", error); | ||
| process__default.exit(1); | ||
| }); | ||
| } | ||
| exports.collectFiles = collectFiles; | ||
| exports.generateMarkdown = generateMarkdown; | ||
| exports.parseArgs = parseArgs; |
+38
-1
@@ -0,2 +1,39 @@ | ||
| import ignore from 'ignore'; | ||
| export { } | ||
| interface ParsedArgs { | ||
| output: string | null; | ||
| maxTokens: number | null; | ||
| extensions: string[] | null; | ||
| verbose: boolean; | ||
| includeFiles: string[] | null; | ||
| excludeFiles: string[] | null; | ||
| includeDirs: string[] | null; | ||
| excludeDirs: string[] | null; | ||
| } | ||
| /** | ||
| * Simple function to parse CLI args: | ||
| * | ||
| * -o, --output <file> : specify output filename | ||
| * --max-tokens, -tok <number> : limit output tokens | ||
| * -e, --extension <ext,...> : filter by file extensions (.ts,.js etc) | ||
| * -if, --include-files <pattern,...> : include specific files | ||
| * -ef, --exclude-files <pattern,...> : exclude specific files | ||
| * -id, --include-dir <pattern,...> : include specific directories | ||
| * -ed, --exclude-dir <pattern,...> : exclude specific directories | ||
| */ | ||
| declare function parseArgs(argv: string[]): ParsedArgs; | ||
| declare function collectFiles(dir: string, options: { | ||
| ig: ReturnType<typeof ignore>; | ||
| extensionSet: Set<string> | null; | ||
| excludeFiles: string[] | null; | ||
| includeFiles: string[] | null; | ||
| excludeDirs: string[] | null; | ||
| includeDirs: string[] | null; | ||
| }): Promise<string[]>; | ||
| declare function generateMarkdown(files: string[], options: { | ||
| outputPath: string | null; | ||
| maxTokens: number | null; | ||
| verbose: boolean; | ||
| }): Promise<number>; | ||
| export { type ParsedArgs, collectFiles, generateMarkdown, parseArgs }; |
+38
-1
@@ -0,2 +1,39 @@ | ||
| import ignore from 'ignore'; | ||
| export { } | ||
| interface ParsedArgs { | ||
| output: string | null; | ||
| maxTokens: number | null; | ||
| extensions: string[] | null; | ||
| verbose: boolean; | ||
| includeFiles: string[] | null; | ||
| excludeFiles: string[] | null; | ||
| includeDirs: string[] | null; | ||
| excludeDirs: string[] | null; | ||
| } | ||
| /** | ||
| * Simple function to parse CLI args: | ||
| * | ||
| * -o, --output <file> : specify output filename | ||
| * --max-tokens, -tok <number> : limit output tokens | ||
| * -e, --extension <ext,...> : filter by file extensions (.ts,.js etc) | ||
| * -if, --include-files <pattern,...> : include specific files | ||
| * -ef, --exclude-files <pattern,...> : exclude specific files | ||
| * -id, --include-dir <pattern,...> : include specific directories | ||
| * -ed, --exclude-dir <pattern,...> : exclude specific directories | ||
| */ | ||
| declare function parseArgs(argv: string[]): ParsedArgs; | ||
| declare function collectFiles(dir: string, options: { | ||
| ig: ReturnType<typeof ignore>; | ||
| extensionSet: Set<string> | null; | ||
| excludeFiles: string[] | null; | ||
| includeFiles: string[] | null; | ||
| excludeDirs: string[] | null; | ||
| includeDirs: string[] | null; | ||
| }): Promise<string[]>; | ||
| declare function generateMarkdown(files: string[], options: { | ||
| outputPath: string | null; | ||
| maxTokens: number | null; | ||
| verbose: boolean; | ||
| }): Promise<number>; | ||
| export { type ParsedArgs, collectFiles, generateMarkdown, parseArgs }; |
+38
-1
@@ -0,2 +1,39 @@ | ||
| import ignore from 'ignore'; | ||
| export { } | ||
| interface ParsedArgs { | ||
| output: string | null; | ||
| maxTokens: number | null; | ||
| extensions: string[] | null; | ||
| verbose: boolean; | ||
| includeFiles: string[] | null; | ||
| excludeFiles: string[] | null; | ||
| includeDirs: string[] | null; | ||
| excludeDirs: string[] | null; | ||
| } | ||
| /** | ||
| * Simple function to parse CLI args: | ||
| * | ||
| * -o, --output <file> : specify output filename | ||
| * --max-tokens, -tok <number> : limit output tokens | ||
| * -e, --extension <ext,...> : filter by file extensions (.ts,.js etc) | ||
| * -if, --include-files <pattern,...> : include specific files | ||
| * -ef, --exclude-files <pattern,...> : exclude specific files | ||
| * -id, --include-dir <pattern,...> : include specific directories | ||
| * -ed, --exclude-dir <pattern,...> : exclude specific directories | ||
| */ | ||
| declare function parseArgs(argv: string[]): ParsedArgs; | ||
| declare function collectFiles(dir: string, options: { | ||
| ig: ReturnType<typeof ignore>; | ||
| extensionSet: Set<string> | null; | ||
| excludeFiles: string[] | null; | ||
| includeFiles: string[] | null; | ||
| excludeDirs: string[] | null; | ||
| includeDirs: string[] | null; | ||
| }): Promise<string[]>; | ||
| declare function generateMarkdown(files: string[], options: { | ||
| outputPath: string | null; | ||
| maxTokens: number | null; | ||
| verbose: boolean; | ||
| }): Promise<number>; | ||
| export { type ParsedArgs, collectFiles, generateMarkdown, parseArgs }; |
+203
-93
@@ -6,2 +6,3 @@ #!/usr/bin/env node | ||
| import { fileURLToPath } from 'node:url'; | ||
| import readline from 'readline'; | ||
| import ignore from 'ignore'; | ||
@@ -266,6 +267,11 @@ | ||
| extensions: null, | ||
| verbose: false | ||
| verbose: false, | ||
| includeFiles: null, | ||
| excludeFiles: null, | ||
| includeDirs: null, | ||
| excludeDirs: null | ||
| }; | ||
| for (let i = 2; i < argv.length; i++) { | ||
| const arg = argv[i]; | ||
| const args = argv[0]?.endsWith("node") ? argv.slice(2) : argv; | ||
| for (let i = 0; i < args.length; i++) { | ||
| const arg = args[i]; | ||
| if (arg === "-h" || arg === "--help") { | ||
@@ -276,7 +282,7 @@ printHelp(); | ||
| result.verbose = true; | ||
| } else if ((arg === "-o" || arg === "--output") && argv[i + 1]) { | ||
| result.output = argv[i + 1]; | ||
| } else if ((arg === "-o" || arg === "--output") && args[i + 1]) { | ||
| result.output = args[i + 1]; | ||
| i++; | ||
| } else if ((arg === "--max-tokens" || arg === "-tok") && argv[i + 1]) { | ||
| const tokens = parseInt(argv[i + 1]); | ||
| } else if ((arg === "--max-tokens" || arg === "-tok") && args[i + 1]) { | ||
| const tokens = parseInt(args[i + 1]); | ||
| if (!isNaN(tokens)) { | ||
@@ -286,5 +292,25 @@ result.maxTokens = tokens; | ||
| i++; | ||
| } else if ((arg === "-e" || arg === "--extension") && argv[i + 1]) { | ||
| result.extensions = argv[i + 1].split(",").map((ext) => ext.startsWith(".") ? ext : `.${ext}`); | ||
| } else if ((arg === "-e" || arg === "--extension") && args[i + 1]) { | ||
| result.extensions = args[i + 1].split(",").map((ext) => ext.startsWith(".") ? ext : `.${ext}`); | ||
| i++; | ||
| } else if ((arg === "-if" || arg === "--include-files") && args[i + 1]) { | ||
| result.includeFiles = args[i + 1].split(","); | ||
| i++; | ||
| } else if ((arg === "-ef" || arg === "--exclude-files") && args[i + 1]) { | ||
| result.excludeFiles = args[i + 1].split(","); | ||
| i++; | ||
| } else if ((arg === "-id" || arg === "--include-dir") && args[i + 1]) { | ||
| result.includeDirs = args[i + 1].split(","); | ||
| i++; | ||
| } else if ((arg === "-ed" || arg === "--exclude-dir") && args[i + 1]) { | ||
| result.excludeDirs = args[i + 1].split(","); | ||
| i++; | ||
| } else if (arg.startsWith("--include-files=")) { | ||
| result.includeFiles = arg.split("=")[1].split(","); | ||
| } else if (arg.startsWith("--exclude-files=")) { | ||
| result.excludeFiles = arg.split("=")[1].split(","); | ||
| } else if (arg.startsWith("--include-dir=")) { | ||
| result.includeDirs = arg.split("=")[1].split(","); | ||
| } else if (arg.startsWith("--exclude-dir=")) { | ||
| result.excludeDirs = arg.split("=")[1].split(","); | ||
| } | ||
@@ -294,45 +320,119 @@ } | ||
| } | ||
| const { output, maxTokens, extensions, verbose } = parseArgs(process.argv); | ||
| const ig = ignore().add( | ||
| DEFAULT_IGNORE_PATTERNS.split("\n").filter( | ||
| (line) => line && !line.startsWith("#") | ||
| ) | ||
| ); | ||
| try { | ||
| const gitignoreContent = fs.readFileSync( | ||
| path.join(process.cwd(), ".gitignore"), | ||
| "utf8" | ||
| async function main() { | ||
| const { | ||
| output: outputFile, | ||
| // rename to avoid conflict | ||
| maxTokens, | ||
| extensions, | ||
| verbose, | ||
| includeFiles, | ||
| excludeFiles, | ||
| includeDirs, | ||
| excludeDirs | ||
| } = parseArgs(process.argv); | ||
| const ig = ignore().add( | ||
| DEFAULT_IGNORE_PATTERNS.split("\n").filter( | ||
| (line) => line && !line.startsWith("#") | ||
| ) | ||
| ); | ||
| ig.add(gitignoreContent); | ||
| } catch { | ||
| try { | ||
| const gitignoreContent = fs.readFileSync( | ||
| path.join(process.cwd(), ".gitignore"), | ||
| "utf8" | ||
| ); | ||
| ig.add(gitignoreContent); | ||
| } catch { | ||
| } | ||
| try { | ||
| const codefetchignoreContent = fs.readFileSync( | ||
| path.join(process.cwd(), ".codefetchignore"), | ||
| "utf8" | ||
| ); | ||
| ig.add(codefetchignoreContent); | ||
| } catch { | ||
| } | ||
| const extensionSet = extensions ? new Set(extensions) : null; | ||
| const allFiles = await collectFiles(process.cwd(), { | ||
| ig, | ||
| extensionSet, | ||
| excludeFiles, | ||
| includeFiles, | ||
| excludeDirs, | ||
| includeDirs | ||
| }); | ||
| if (outputFile) { | ||
| const codefetchDir = path.join(process.cwd(), "codefetch"); | ||
| if (!fs.existsSync(codefetchDir)) { | ||
| fs.mkdirSync(codefetchDir, { recursive: true }); | ||
| console.log("Created codefetch directory."); | ||
| } | ||
| const codefetchignorePath = path.join(process.cwd(), ".codefetchignore"); | ||
| if (!fs.existsSync(codefetchignorePath)) { | ||
| const ignoreContent = "# Codefetch specific ignores\ncodefetch/\n"; | ||
| fs.writeFileSync(codefetchignorePath, ignoreContent, "utf8"); | ||
| console.log( | ||
| "Created .codefetchignore file. Add 'codefetch/' to your .gitignore to avoid committing fetched code." | ||
| ); | ||
| } | ||
| } | ||
| const outputPath = outputFile ? path.join(process.cwd(), "codefetch", outputFile) : null; | ||
| const totalTokens = await generateMarkdown(allFiles, { | ||
| outputPath, | ||
| maxTokens, | ||
| verbose | ||
| }); | ||
| if (outputFile) { | ||
| console.log("\nSummary:"); | ||
| console.log("\u2713 Code was successfully fetched"); | ||
| console.log(`\u2713 Output written to: ${outputPath}`); | ||
| console.log(`\u2713 Approximate token count: ${totalTokens}`); | ||
| } | ||
| } | ||
| try { | ||
| const codefetchignoreContent = fs.readFileSync( | ||
| path.join(process.cwd(), ".codefetchignore"), | ||
| "utf8" | ||
| async function collectFiles(dir, options) { | ||
| const results = []; | ||
| const list = await fs.promises.readdir(dir); | ||
| const excludePatterns = options.excludeFiles?.map( | ||
| (pattern) => new RegExp(pattern.replace(/\*/g, ".*")) | ||
| ); | ||
| ig.add(codefetchignoreContent); | ||
| } catch { | ||
| } | ||
| function collectFiles(dir) { | ||
| const results = []; | ||
| const list = fs.readdirSync(dir); | ||
| const includePatterns = options.includeFiles?.map( | ||
| (pattern) => new RegExp(pattern.replace(/\*/g, ".*")) | ||
| ); | ||
| for (const filename of list) { | ||
| const filePath = path.join(dir, filename); | ||
| const relPath = path.relative(process.cwd(), filePath); | ||
| if (ig.ignores(relPath)) { | ||
| if (options.ig.ignores(relPath)) { | ||
| continue; | ||
| } | ||
| const stat = fs.statSync(filePath); | ||
| const stat = await fs.promises.stat(filePath); | ||
| if (stat.isDirectory()) { | ||
| verbose && console.log(`Processing directory: ${relPath}`); | ||
| results.push(...collectFiles(filePath)); | ||
| const dirName = path.basename(filePath); | ||
| if (options.excludeDirs && options.excludeDirs.includes(dirName)) { | ||
| continue; | ||
| } | ||
| if (options.includeDirs && !options.includeDirs.includes(dirName)) { | ||
| continue; | ||
| } | ||
| results.push( | ||
| ...await collectFiles(filePath, { | ||
| ig: options.ig, | ||
| extensionSet: options.extensionSet, | ||
| excludeFiles: options.excludeFiles, | ||
| includeFiles: options.includeFiles, | ||
| excludeDirs: options.excludeDirs, | ||
| includeDirs: options.includeDirs | ||
| }) | ||
| ); | ||
| } else { | ||
| if (extensions) { | ||
| if (excludePatterns && excludePatterns.some((pattern) => pattern.test(filename))) { | ||
| continue; | ||
| } | ||
| if (includePatterns && !includePatterns.some((pattern) => pattern.test(filename))) { | ||
| continue; | ||
| } | ||
| if (options.extensionSet) { | ||
| const ext = path.extname(filename); | ||
| if (!extensions.includes(ext)) { | ||
| if (!options.extensionSet.has(ext)) { | ||
| continue; | ||
| } | ||
| } | ||
| verbose && console.log(`Processing file: ${relPath}`); | ||
| results.push(filePath); | ||
@@ -343,59 +443,52 @@ } | ||
| } | ||
| const allFiles = collectFiles(process.cwd()); | ||
| function estimateTokens(text) { | ||
| return text.split(/[\s\p{P}]+/u).length; | ||
| } | ||
| function generateMarkdown(files) { | ||
| const lines = []; | ||
| async function generateMarkdown(files, options) { | ||
| if (options.outputPath) { | ||
| const outputDir = path.dirname(options.outputPath); | ||
| if (!fs.existsSync(outputDir)) { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
| } | ||
| } | ||
| const output = options.outputPath ? fs.createWriteStream(options.outputPath) : process.stdout; | ||
| let totalTokens = 0; | ||
| verbose && console.log("\nGenerating markdown output..."); | ||
| for (const file of files) { | ||
| const relativePath = path.relative(process.cwd(), file); | ||
| const content = fs.readFileSync(file, "utf8"); | ||
| const fileTokens = estimateTokens(content); | ||
| if (maxTokens && totalTokens + fileTokens > maxTokens) { | ||
| verbose && console.log(`Skipping ${relativePath} (would exceed token limit)`); | ||
| continue; | ||
| const fileStream = fs.createReadStream(file, { encoding: "utf8" }); | ||
| const rl = readline.createInterface({ | ||
| input: fileStream, | ||
| crlfDelay: Infinity | ||
| }); | ||
| output.write(`/${relativePath}: | ||
| `); | ||
| output.write("-".repeat(80) + "\n"); | ||
| let lineNumber = 1; | ||
| for await (const line of rl) { | ||
| const lineTokens = estimateTokens(line); | ||
| if (options.maxTokens && totalTokens + lineTokens > options.maxTokens) { | ||
| break; | ||
| } | ||
| const formattedLine = `${lineNumber} | ${line} | ||
| `; | ||
| output.write(formattedLine); | ||
| totalTokens += lineTokens; | ||
| lineNumber++; | ||
| } | ||
| verbose && console.log(`Adding to output: ${relativePath} (${fileTokens} tokens)`); | ||
| totalTokens += fileTokens; | ||
| lines.push(`/${relativePath}:`); | ||
| lines.push( | ||
| "--------------------------------------------------------------------------------" | ||
| ); | ||
| const fileLines = content.split("\n"); | ||
| fileLines.forEach((line, i) => { | ||
| lines.push(`${i + 1} | ${line}`); | ||
| output.write("-".repeat(80) + "\n\n"); | ||
| if (options.maxTokens && totalTokens >= options.maxTokens) { | ||
| break; | ||
| } | ||
| } | ||
| if (options.outputPath) { | ||
| await new Promise((resolve, reject) => { | ||
| output.end((err) => { | ||
| if (err) | ||
| reject(err); | ||
| else | ||
| resolve(); | ||
| }); | ||
| }); | ||
| lines.push(""); | ||
| lines.push( | ||
| "--------------------------------------------------------------------------------" | ||
| ); | ||
| } | ||
| return lines.join("\n"); | ||
| return totalTokens; | ||
| } | ||
| const final = generateMarkdown(allFiles); | ||
| if (output) { | ||
| const codefetchDir = path.join(process.cwd(), "codefetch"); | ||
| if (!fs.existsSync(codefetchDir)) { | ||
| fs.mkdirSync(codefetchDir, { recursive: true }); | ||
| console.log("Created codefetch directory."); | ||
| } | ||
| const codefetchignorePath = path.join(process.cwd(), ".codefetchignore"); | ||
| if (!fs.existsSync(codefetchignorePath)) { | ||
| const ignoreContent = "# Codefetch specific ignores\ncodefetch/\n"; | ||
| fs.writeFileSync(codefetchignorePath, ignoreContent, "utf8"); | ||
| console.log( | ||
| "Created .codefetchignore file. Add 'codefetch/' to your .gitignore to avoid committing fetched code." | ||
| ); | ||
| } | ||
| const outputPath = path.join(codefetchDir, output); | ||
| fs.writeFileSync(outputPath, final, "utf8"); | ||
| const totalTokens = estimateTokens(final); | ||
| console.log("\nSummary:"); | ||
| console.log("\u2713 Code was successfully fetched"); | ||
| console.log(`\u2713 Output written to: ${outputPath}`); | ||
| console.log(`\u2713 Approximate token count: ${totalTokens}`); | ||
| } else { | ||
| console.log(final); | ||
| function estimateTokens(text) { | ||
| return text.split(/[\s\p{P}]+/u).filter(Boolean).length; | ||
| } | ||
@@ -407,8 +500,25 @@ function printHelp() { | ||
| Options: | ||
| -o, --output <file> Specify output filename | ||
| -tok, --max-tokens <n> Limit output tokens (useful for AI models) | ||
| -e, --extension <ext,...> Filter by file extensions (e.g., .ts,.js) | ||
| -v, --verbose Show detailed processing information | ||
| -h, --help Display this help message | ||
| -o, --output <file> Specify output filename | ||
| -tok, --max-tokens <n> Limit output tokens (useful for AI models) | ||
| -e, --extension <ext,...> Filter by file extensions (e.g., .ts,.js) | ||
| -if, --include-files <p,..> Include specific files (supports patterns) | ||
| -ef, --exclude-files <p,..> Exclude specific files (supports patterns) | ||
| -id, --include-dir <d,...> Include specific directories | ||
| -ed, --exclude-dir <d,...> Exclude specific directories | ||
| -v, --verbose Show detailed processing information | ||
| -h, --help Display this help message | ||
| Examples: | ||
| codefetch --exclude-dir=node_modules,public | ||
| codefetch --include-files=*.ts,*.js | ||
| codefetch -ef test.ts,temp.js -id src | ||
| `); | ||
| } | ||
| if (require.main === module) { | ||
| main().catch((error) => { | ||
| console.error("Error:", error); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| export { collectFiles, generateMarkdown, parseArgs }; |
+8
-3
| { | ||
| "name": "codefetch", | ||
| "version": "1.1.5", | ||
| "version": "1.1.6", | ||
| "description": "Fetches all files in the current directory and outputs them in a Markdown file.", | ||
@@ -21,3 +21,6 @@ "type": "module", | ||
| "dev": "unbuild --stub", | ||
| "prepack": "unbuild" | ||
| "prepack": "unbuild", | ||
| "test": "vitest run --coverage", | ||
| "test:watch": "vitest", | ||
| "test:types": "tsc --noEmit --skipLibCheck" | ||
| }, | ||
@@ -33,3 +36,5 @@ "files": [ | ||
| "typescript": "^5.0.0", | ||
| "@types/node": "^20.0.0" | ||
| "@types/node": "^20.0.0", | ||
| "vitest": "^1.1.3", | ||
| "@vitest/coverage-v8": "^1.1.3" | ||
| }, | ||
@@ -36,0 +41,0 @@ "main": "./dist/index.cjs", |
+18
-0
@@ -28,2 +28,14 @@ # codefetch | ||
| Include or exclude specific files and directories: | ||
| ```bash | ||
| # Exclude node_modules and public directories | ||
| npx codefetch --exclude-dir=node_modules,public -o output.md | ||
| # Include only TypeScript files | ||
| npx codefetch --include-files=*.ts -o typescript-only.md | ||
| # Include src directory, exclude test files | ||
| npx codefetch --include-dir=src --exclude-files=*.test.ts -o src-no-tests.md | ||
| ``` | ||
| If no output file is specified (`-o` or `--output`), it will print to stdout. | ||
@@ -38,4 +50,10 @@ | ||
| | `--extension <ext,...>` | `-e` | Filter by file extensions (e.g., .ts,.js) | | ||
| | `--include-files <pattern,...>` | `-if` | Include specific files (supports patterns like *.ts) | | ||
| | `--exclude-files <pattern,...>` | `-ef` | Exclude specific files (supports patterns like *.test.ts) | | ||
| | `--include-dir <dir,...>` | `-id` | Include specific directories | | ||
| | `--exclude-dir <dir,...>` | `-ed` | Exclude specific directories | | ||
| | `--verbose` | `-v` | Show detailed processing information | | ||
| All options that accept multiple values use comma-separated lists. File patterns support wildcards (e.g., `*.ts`, `src/*.js`). | ||
| ## Installation | ||
@@ -42,0 +60,0 @@ |
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
32991
59.39%1034
32.73%1
-50%128
16.36%5
66.67%