From 7c87d0828a939734784f17055458f08729d9587a Mon Sep 17 00:00:00 2001 From: Awa Date: Sat, 21 Dec 2024 15:06:48 +0300 Subject: [PATCH] Refactor index.ts: Migrate utility functions to src/utils, enhance error handling, and add text file filtering. Remove utils.ts file. Improve user feedback for directory validation and file selection process. --- index.ts | 147 +++++++++++++++++++++++++++++++---------------- src/constants.ts | 44 ++++++++++++++ src/utils.ts | 142 +++++++++++++++++++++++++++++++++++++++++++++ utils.ts | 85 --------------------------- 4 files changed, 284 insertions(+), 134 deletions(-) create mode 100644 src/constants.ts create mode 100644 src/utils.ts delete mode 100644 utils.ts diff --git a/index.ts b/index.ts index afd5e0e..d0eba1e 100644 --- a/index.ts +++ b/index.ts @@ -7,8 +7,9 @@ import path from "path"; import { generateAIFriendlyOutput, getAllFiles, + isTextFile, loadGitignore, -} from "./utils.js"; +} from "./src/utils"; const defaultIgnored: string[] = [ "node_modules", @@ -17,85 +18,133 @@ const defaultIgnored: string[] = [ "build", ".vscode", ".idea", + ".Trash", ]; +process.on("SIGINT", () => { + console.log("\nOperation cancelled by user"); + process.exit(0); +}); + program .version("1.0.0") .argument("[directory]", "Directory to process", "./") .option("-o, --output ", "Output file name", "ai_context.json") .option( "-e, --exclude ", - "Comma-separated list of files/folders to exclude", + "Comma-separated list of files/folders to exclude" ) .parse(process.argv); const options = program.opts(); -const inputDirectory: string = program.args[0] || "./"; +const inputDirectory: string = path.resolve(program.args[0] || "./"); const outputFile: string = options.output; const excludeFiles: string[] = options.exclude ? options.exclude.split(",") : []; async function run(): Promise { - if (!fs.existsSync(inputDirectory)) { - console.error(`Error: The directory "${inputDirectory}" does not exist.`); - process.exit(1); - } + try { + // Validate input directory + try { + const stats = fs.statSync(inputDirectory); + if (!stats.isDirectory()) { + console.error(`Error: "${inputDirectory}" is not a directory.`); + process.exit(1); + } + } catch (error) { + console.error( + `Error: Cannot access directory "${inputDirectory}": ${ + error instanceof Error ? error.message : String(error) + }` + ); + process.exit(1); + } - const ignoredFiles: string[] = [ - ...defaultIgnored, - ...loadGitignore(inputDirectory), - ...excludeFiles, - ]; + const ignoredFiles: string[] = [ + ...defaultIgnored, + ...loadGitignore(inputDirectory), + ...excludeFiles, + ]; - const allFiles: string[] = getAllFiles(inputDirectory, ignoredFiles); + let allFiles: string[]; + try { + allFiles = getAllFiles(inputDirectory, ignoredFiles); + } catch (error) { + console.error( + `Error scanning directory: ${ + error instanceof Error ? error.message : String(error) + }` + ); + process.exit(1); + } - if (allFiles.length === 0) { - console.log( - "No files found in the specified directory after applying exclusions.", - ); - process.exit(0); - } + if (allFiles.length === 0) { + console.log( + "No valid text files found in the specified directory after applying exclusions." + ); + process.exit(0); + } - const choices = allFiles.map((file) => ({ - name: path.relative(inputDirectory, file), - value: file, - checked: false, - })); + // Filter out non-text files and create choices + const choices = allFiles + .filter((file) => isTextFile(file)) + .map((file) => ({ + name: path.relative(inputDirectory, file), + value: file, + checked: false, + })); - const answers = await (inquirer as any).prompt([ - { - type: "checkbox", - name: "selectedFiles", - message: "Select files to include in the AI context:", - choices, - pageSize: 20, - validate: (answer: string[]) => { - if (answer.length === 0) { - return "You must select at least one file or press Ctrl+C to cancel."; - } - return true; - }, - }, - ]); + try { + // @ts-ignore + const answers = await inquirer.prompt([ + { + type: "checkbox", + name: "selectedFiles", + message: "Select files to include in the AI context:", + choices, + pageSize: 20, + validate: (answer: string[]) => { + if (answer.length === 0) { + return "You must select at least one file."; + } + return true; + }, + }, + ]); - const { selectedFiles } = answers; + const { selectedFiles } = answers; - if (selectedFiles.length === 0) { - console.log("No files selected. Exiting without generating output."); - process.exit(0); - } + if (selectedFiles.length === 0) { + console.log("No files selected. Exiting without generating output."); + process.exit(0); + } - try { - generateAIFriendlyOutput(inputDirectory, outputFile, selectedFiles); - console.log(`AI-friendly context written to ${outputFile}`); + generateAIFriendlyOutput(inputDirectory, outputFile, selectedFiles); + console.log(`AI-friendly context written to ${outputFile}`); + } catch (error) { + if ( + error instanceof Error && + error.message?.includes("User force closed") + ) { + console.log("\nOperation cancelled by user"); + process.exit(0); + } + throw error; + } } catch (error) { - console.error("Error generating output:", error); + console.error( + "Error:", + error instanceof Error ? error.message : String(error) + ); process.exit(1); } } run().catch((error) => { - console.error("An unexpected error occurred:", error); + console.error( + "An unexpected error occurred:", + error instanceof Error ? error.message : String(error) + ); process.exit(1); }); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..25908c6 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,44 @@ +export const textFileExtensions = new Set([ + // languages + "ts", + "js", + "jsx", + "tsx", + "py", + "rb", + "php", + "java", + "cpp", + "c", + "h", + "cs", + "go", + "rs", + // web + "html", + "css", + "scss", + "sass", + "less", + // configs + "json", + "yml", + "yaml", + "toml", + "ini", + "env", + // docs + "md", + "txt", + "rst", + "asciidoc", + // scripts + "sh", + "bash", + "zsh", + // common + "xml", + "svg", + "csv", + "log", +]); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..1658a17 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,142 @@ +import fs from "fs"; +import path from "path"; +import { textFileExtensions } from "./constants"; + +export function isTextFile(filePath: string): boolean { + const ext = path.extname(filePath).toLowerCase().slice(1); + return textFileExtensions.has(ext); +} + +export function loadGitignore(directory: string): string[] { + const gitignorePath = path.join(directory, ".gitignore"); + try { + if (!fs.existsSync(gitignorePath)) return []; + + const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8"); + return gitignoreContent + .split("\n") + .filter((line) => line && !line.startsWith("#")) + .map((line) => line.trim()); + } catch (error) { + console.warn( + `Warning: Unable to read .gitignore file: ${ + error instanceof Error ? error.message : String(error) + }` + ); + return []; + } +} + +export function getAllFiles( + dirPath: string, + ignoredFiles: string[], + arrayOfFiles: string[] = [] +): string[] { + try { + const files = fs.readdirSync(dirPath, { withFileTypes: true }); + + for (const file of files) { + const fullPath = path.join(dirPath, file.name); + + // Skip if path matches any ignored pattern + if (ignoredFiles.some((ignored) => fullPath.includes(ignored))) continue; + + try { + if (file.isDirectory()) { + getAllFiles(fullPath, ignoredFiles, arrayOfFiles); + } else if (file.isFile() && isTextFile(fullPath)) { + arrayOfFiles.push(fullPath); + } + } catch (error) { + if (error instanceof Error && "code" in error) { + if (error.code === "EACCES" || error.code === "EPERM") { + console.warn(`Warning: Permission denied accessing ${fullPath}`); + continue; + } + } + throw error; + } + } + + return arrayOfFiles; + } catch (error) { + if (error instanceof Error && "code" in error) { + if (error.code === "EACCES" || error.code === "EPERM") { + console.warn( + `Warning: Permission denied accessing directory ${dirPath}` + ); + return arrayOfFiles; + } + } + throw error; + } +} + +interface FileContent { + content: string; + extension: string; +} + +interface AIContext { + project_structure: string[]; + file_contents: { + [key: string]: FileContent; + }; +} + +export function generateAIFriendlyOutput( + directory: string, + outputFile: string, + selectedFiles: string[] +): void { + if (selectedFiles.length === 0) { + throw new Error("No files selected for output generation."); + } + + const context: AIContext = { + project_structure: [], + file_contents: {}, + }; + + let successfulReads = 0; + + selectedFiles.forEach((file) => { + const relativePath = path.relative(directory, file); + + try { + if (!isTextFile(file)) { + console.warn(`Warning: Skipping non-text file ${file}`); + return; + } + + const fileContent = fs.readFileSync(file, "utf-8"); + context.project_structure.push(relativePath); + + context.file_contents[relativePath] = { + content: fileContent, + extension: path.extname(file).slice(1), + }; + successfulReads++; + } catch (error) { + console.warn( + `Warning: Unable to read file ${file}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }); + + if (successfulReads === 0) { + throw new Error("No valid files could be read for output generation."); + } + + try { + fs.writeFileSync(outputFile, JSON.stringify(context, null, 2), "utf-8"); + } catch (error: unknown) { + throw new Error( + `Failed to write output file: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } +} diff --git a/utils.ts b/utils.ts deleted file mode 100644 index b8ad165..0000000 --- a/utils.ts +++ /dev/null @@ -1,85 +0,0 @@ -import fs from "fs"; -import path from "path"; - -export function loadGitignore(directory: string): string[] { - const gitignorePath = path.join(directory, ".gitignore"); - if (!fs.existsSync(gitignorePath)) return []; - - const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8"); - return gitignoreContent - .split("\n") - .filter((line) => line && !line.startsWith("#")) - .map((line) => line.trim()); -} - -export function getAllFiles( - dirPath: string, - ignoredFiles: string[], - arrayOfFiles: string[] = [], -): string[] { - const files = fs.readdirSync(dirPath); - - files.forEach((file) => { - const fullPath = path.join(dirPath, file); - - if (ignoredFiles.some((ignored) => fullPath.includes(ignored))) return; - - if (fs.statSync(fullPath).isDirectory()) { - arrayOfFiles = getAllFiles(fullPath, ignoredFiles, arrayOfFiles); - } else { - arrayOfFiles.push(fullPath); - } - }); - - return arrayOfFiles; -} - -interface FileContent { - content: string; - extension: string; -} - -interface AIContext { - project_structure: string[]; - file_contents: { - [key: string]: FileContent; - }; -} - -export function generateAIFriendlyOutput( - directory: string, - outputFile: string, - selectedFiles: string[], -): void { - if (selectedFiles.length === 0) { - throw new Error("No files selected for output generation."); - } - - const context: AIContext = { - project_structure: [], - file_contents: {}, - }; - - selectedFiles.forEach((file) => { - const relativePath = path.relative(directory, file); - - try { - const fileContent = fs.readFileSync(file, "utf-8"); - - context.project_structure.push(relativePath); - - context.file_contents[relativePath] = { - content: fileContent, - extension: path.extname(file).slice(1), // Remove the leading dot - }; - } catch (error) { - console.warn(`Warning: Unable to read file ${file}. It will be skipped.`); - } - }); - - if (Object.keys(context.file_contents).length === 0) { - throw new Error("No valid files could be read for output generation."); - } - - fs.writeFileSync(outputFile, JSON.stringify(context, null, 2), "utf-8"); -}