Skip to content

Commit

Permalink
Refactor index.ts: Migrate utility functions to src/utils, enhance er…
Browse files Browse the repository at this point in the history
…ror handling, and add text file filtering. Remove utils.ts file. Improve user feedback for directory validation and file selection process.
  • Loading branch information
alwalxed committed Dec 21, 2024
1 parent 47fef13 commit 7c87d08
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 134 deletions.
147 changes: 98 additions & 49 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import path from "path";
import {
generateAIFriendlyOutput,
getAllFiles,
isTextFile,
loadGitignore,
} from "./utils.js";
} from "./src/utils";

const defaultIgnored: string[] = [
"node_modules",
Expand All @@ -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 <file>", "Output file name", "ai_context.json")
.option(
"-e, --exclude <items>",
"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<void> {
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);
});
44 changes: 44 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -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",
]);
142 changes: 142 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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)
}`
);
}
}
Loading

0 comments on commit 7c87d08

Please sign in to comment.