phind-cli
Version:
A modern, intuitive, cross-platform command-line tool for finding files and directories recursively, designed with developers in mind.
234 lines • 11.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PhindApp = void 0;
// src/cli.ts
const path_1 = __importDefault(require("path"));
const yargs_1 = __importDefault(require("yargs/yargs"));
const helpers_1 = require("yargs/helpers");
const promises_1 = __importDefault(require("fs/promises"));
const config_1 = require("./config");
const traverser_1 = require("./traverser");
// --- START: Import AI Client ---
const ai_1 = require("./ai"); // Import AI client
// --- END: Import AI Client ---
class PhindApp {
constructor() {
this.config = new config_1.PhindConfig();
}
async parseArguments() {
return await (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
.usage('Usage: $0 [path] [options] [--ai <query>]') // Updated usage
.command('$0 [path]', 'Find files/directories recursively', (yargs) => {
yargs.positional('path', {
describe: 'Directory to search in',
type: 'string',
default: '.',
});
})
// --- Standard Options ---
.option('name', {
alias: 'n',
type: 'string',
array: true,
description: 'Glob pattern(s) for filenames/paths to include (default: *)',
defaultDescription: '"*" (all files/dirs)',
default: ['*'],
})
.option('exclude', {
alias: 'e',
type: 'string',
array: true,
description: `Glob pattern(s) to exclude. Also reads from ${this.config.getGlobalIgnorePath()} unless --skip-global-ignore is used.`,
default: [], // Keep default as empty array for CLI args
defaultDescription: this.config.getDefaultExcludesDescription(),
})
.option('skip-global-ignore', {
type: 'boolean',
description: 'Do not load patterns from the global ignore file.',
default: false,
})
.option('type', {
alias: 't',
type: 'string',
choices: ['f', 'd'],
description: 'Match only files (f) or directories (d)',
})
.option('maxdepth', {
alias: 'd',
type: 'number',
description: 'Maximum directory levels to descend (0 means starting path only)',
default: Infinity, // Default is Infinity
coerce: (val) => {
if (val < 0) {
throw new Error("Argument maxdepth must be a non-negative number.");
}
// Convert Infinity string/concept to a usable large number for yargs/traverser
// Use Number.MAX_SAFE_INTEGER which is large enough for practical purposes.
// Also handle the actual Infinity value if it somehow gets passed directly.
return val === Infinity || String(val).toLowerCase() === 'infinity' ? Number.MAX_SAFE_INTEGER : val;
}
})
.option('ignore-case', {
alias: 'i',
type: 'boolean',
description: 'Perform case-insensitive matching',
default: false,
})
.option('relative', {
alias: 'r',
type: 'boolean',
description: 'Print paths relative to the starting directory (default). Use --relative=false for absolute paths.',
default: true,
defaultDescription: "true (relative paths)",
})
// --- START: Add AI Option ---
.option('ai', {
type: 'string', // Expects the query string
description: 'Use AI (Google Gemini) to find relevant files based on a natural language query. Requires GEMINI_API_KEY env variable.',
// --- FIX: REMOVE ALL CONFLICTS ---
// conflicts: ['type', 'maxdepth', 'ignore-case', 'relative'], // REMOVED THIS LINE ENTIRELY
// --- END FIX ---
coerce: (arg) => {
if (typeof arg === 'string' && arg.trim() === '') {
throw new Error("The --ai option requires a non-empty query string.");
}
return arg;
}
})
// --- END: Add AI Option ---
.help()
.alias('help', 'h')
.strict() // Keep strict mode to catch truly unknown options
.argv;
}
async validateStartPath(startArgPath) {
const startPath = path_1.default.resolve(startArgPath);
try {
const stats = await promises_1.default.stat(startPath);
if (!stats.isDirectory()) {
throw new Error(`Start path "${startArgPath}" (resolved to "${startPath}") is not a directory.`);
}
return startPath;
}
catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`Start path "${startArgPath}" (resolved to "${startPath}") not found.`);
}
else if (err.code === 'EACCES') {
throw new Error(`Permission denied accessing start path "${startArgPath}" (resolved to "${startPath}").`);
}
else {
throw new Error(`Error accessing start path "${startArgPath}" (resolved to "${startPath}"): ${err.message}`);
}
}
}
async run() {
try {
const argv = await this.parseArguments();
// --- START: AI Mode Logic ---
if (argv.ai) {
const aiQuery = argv.ai;
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("AI Mode requires the GEMINI_API_KEY environment variable to be set.");
}
console.log(`AI Mode activated. Query: "${aiQuery}"`);
// --- Get ALL files for AI analysis ---
// We ignore most standard filters for AI mode, but respect global ignore unless skipped.
// We always collect relative paths for consistency in the AI prompt.
if (!argv.skipGlobalIgnore) {
// Ensure global ignores are loaded *before* accessing them
await this.config.loadGlobalIgnores();
}
// Use only hardcoded and global excludes for AI file collection
const aiExcludePatterns = [
// --- Use getter ---
...this.config.getHardcodedDefaultExcludes(),
// --- Use getter ---
...(argv.skipGlobalIgnore ? [] : this.config.getLoadedGlobalIgnorePatterns())
];
const startArgPath = argv.path;
const startPath = await this.validateStartPath(startArgPath);
const basePath = startPath;
const aiTraverseOptions = {
excludePatterns: [...new Set(aiExcludePatterns)], // Use combined excludes (already using Set)
includePatterns: ['*'], // Include everything initially
matchType: null, // Get all types
maxDepth: Number.MAX_SAFE_INTEGER, // No depth limit
ignoreCase: false, // Case doesn't matter for collection
relativePaths: true, // ALWAYS use relative paths for AI input
// --- Use getter ---
defaultExcludes: this.config.getHardcodedDefaultExcludes(),
outputMode: 'collect' // CRITICAL: Collect results instead of printing
};
const aiTraverser = new traverser_1.DirectoryTraverser(aiTraverseOptions, basePath);
console.log("AI Mode: Collecting all file paths...");
await aiTraverser.traverse(startPath);
const allFiles = aiTraverser.getCollectedResults();
console.log(`AI Mode: Collected ${allFiles.length} paths to analyze.`);
if (allFiles.length === 0) {
console.log("AI Mode: No files found matching initial criteria. AI cannot proceed.");
return; // Exit early if no files collected
}
// --- Interact with Gemini ---
const geminiClient = new ai_1.GeminiClient(apiKey);
const relevantFiles = await geminiClient.findRelevantFiles(allFiles, aiQuery);
// --- Print AI Results ---
if (relevantFiles.length > 0) {
console.log("\nAI identified the following relevant files:");
relevantFiles.forEach(file => console.log(file));
}
else {
console.log("\nAI did not identify any relevant files based on your query.");
}
return; // End execution after AI mode
}
// --- END: AI Mode Logic ---
// --- Standard Mode Logic (if --ai is not used) ---
if (!argv.skipGlobalIgnore) {
// Ensure global ignores are loaded before calculating effective excludes
await this.config.loadGlobalIgnores();
}
// Set CLI excludes *after* potentially loading global ones
this.config.setCliExcludes(argv.exclude);
const startArgPath = argv.path;
const startPath = await this.validateStartPath(startArgPath);
const basePath = startPath; // Base path for traversal is the validated start path
// Pass the coerced maxdepth value directly (it's either a number or MAX_SAFE_INTEGER)
const traverseOptions = {
excludePatterns: this.config.getEffectiveExcludePatterns(), // Now gets the combined list
includePatterns: argv.name,
matchType: argv.type ?? null,
maxDepth: argv.maxdepth, // Pass coerced value
ignoreCase: argv.ignoreCase,
relativePaths: argv.relative,
// --- Use getter ---
defaultExcludes: this.config.getHardcodedDefaultExcludes(), // Pass hardcoded defaults for override logic
outputMode: 'print' // Standard mode prints directly
};
const traverser = new traverser_1.DirectoryTraverser(traverseOptions, basePath);
await traverser.traverse(startPath);
}
catch (error) {
console.error("--- Caught Error in PhindApp.run ---");
// console.error(error); // Optionally log the full error object for debugging
console.error("------------------------------------");
console.error(`\nError: ${error.message}`);
process.exit(1);
}
}
}
exports.PhindApp = PhindApp;
// --- Application Entry Point ---
if (require.main === module) {
const app = new PhindApp();
app.run().catch(err => {
console.error("\nAn unexpected critical error occurred:", err);
process.exit(1);
});
}
//# sourceMappingURL=cli.js.map