Skip to content

Instantly share code, notes, and snippets.

@subtleGradient
Created October 23, 2024 20:31
Show Gist options
  • Save subtleGradient/9bac5ea40810375e1ea137f90bea1249 to your computer and use it in GitHub Desktop.
Save subtleGradient/9bac5ea40810375e1ea137f90bea1249 to your computer and use it in GitHub Desktop.
combine a bunch of files into a single markdown file to paste into an AI or whatever
#!/usr/bin/env -S bun
import { readdir } from "fs/promises"
// Function to read and format file content
async function readFileContent(filePath: string): Promise<string> {
const file = Bun.file(filePath)
const content = await file.text()
const extension = filePath.split(".").pop()
if (extension === "md" || extension === "mdx") return content.replace(/^(#+) /gm, "#$1 ") // increase heading levels by 1
const codeBlockSyntax = getSyntaxForExtension(extension)
return content
? `\`\`\`${codeBlockSyntax} filename="${filePath}"\n${content.trim()}\n\`\`\``
: "(Contents available upon request)"
}
// Function to get code block syntax based on file extension
function getSyntaxForExtension(extension?: string): string {
switch (extension) {
case "ts":
case "tsx":
return "typescript"
case "js":
case "jsx":
return "javascript"
case "json":
return "json"
default:
return extension || ""
}
}
// Function to determine if a file is binary based on its MIME type
async function isBinaryFile(filePath: string): Promise<boolean> {
const file = Bun.file(filePath)
const mimeType = file.type
return !(
(mimeType?.startsWith("text/") || mimeType === "application/json" || mimeType === "image/svg+xml") // Example of non-binary type you might still want to include
)
}
// Recursively walk the target folder and collect file paths
async function walkFolder(folderPath: string): Promise<string[]> {
const files: string[] = []
const entries = await readdir(folderPath, { withFileTypes: true })
for (const entry of entries) {
const entryPath = `${folderPath}/${entry.name}`
if (entry.isDirectory()) {
files.push(...(await walkFolder(entryPath)))
} else {
// Check if the file is binary before adding
const binary = await isBinaryFile(entryPath)
if (binary) continue
if (entryPath.includes("/.turbo/")) continue
files.push(entryPath)
}
}
return files
}
const filenameToSortKey = (filename: string) =>
filename.toLowerCase().includes("/readme")
? `0${filename}` //
: filename.toLowerCase().endsWith(".md")
? `1${filename}`
: filename
const fileSorter = (a: string, b: string) => filenameToSortKey(b).localeCompare(filenameToSortKey(a))
// Main function
async function main() {
// Get target folder and output file from command line arguments
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [$0, $1, ...sourceFiles] = process.argv
if (sourceFiles.length === 0) {
const commandName = __filename.replace(__dirname + "/", "")
console.warn(`No source files provided`)
console.warn(`\t${__filename}`)
console.warn(`\t${commandName} $(rg -l 'search term') > ../output.md`)
console.warn(
`\t${commandName} $(rg -l 'prisma.*relay|relay.*prisma' | grep -vE 'json|yaml|svg|yml|snap') > ../prisma+relay.md`,
)
process.exit(1)
}
// Collect all file paths
const filePaths = await Promise.all(sourceFiles.map(file => walkFolder(file).catch(() => file))).then(files =>
files.flat().sort(fileSorter).reverse(),
)
const header = `
# Combined Files
This markdown file was generated by combining an entire folder structure into a single document.
## Files Included:
${filePaths.map(pathname => `* ${pathname.replace(process.env.HOME!, "~")}`).join("\n")}
---
`
// Read and format each file content
const fileContents = await Promise.all(
filePaths.map(async filePath => {
const fileName = filePath.replace(`${sourceFiles}/`, "")
const content = await readFileContent(filePath)
return `\n<details><summary>${fileName}</summary>\n\n${content.trim()}\n</details>\n`
}),
)
// Combine all content into a single markdown string
const markdownContent = header + fileContents.join("\n\n") + "\n"
console.log(markdownContent)
// Write the markdown content to the output file
// await Bun.write(outputFile, markdownContent)
// console.log(`Combined files into ${outputFile}`)
}
// Run the main function
// @ts-ignore - ignore top-level await error
await main()
@subtleGradient
Copy link
Author

usage:

combine_files_to_markdown.ts $(rg -l '@RelayResolver') | pbcopy

uses ripgrep to find files that mention @RelayResolver and then copy the combined markdown to your clipboard on macOS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment