organ-ai-zer
Version:
AI-powered file organizer CLI tool
858 lines ⢠39.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConversationalOrganizer = void 0;
const inquirer_1 = __importDefault(require("inquirer"));
const ora_1 = __importDefault(require("ora"));
const chalk_1 = __importDefault(require("chalk"));
const config_service_1 = require("./config-service");
const ai_providers_1 = require("./ai-providers");
const file_scanner_1 = require("./file-scanner");
const pattern_matching_service_1 = require("./pattern-matching-service");
const organization_conversation_1 = require("./organization-conversation");
const file_organizer_1 = require("./file-organizer");
const project_detection_cache_1 = require("./project-detection-cache");
/**
* Conversational File Organizer
* Uses AI conversation context for natural, flexible file organization
*/
class ConversationalOrganizer {
constructor(configService) {
this.aiProvider = null;
this.fileScanner = new file_scanner_1.FileScanner();
this.patternService = new pattern_matching_service_1.PatternMatchingService();
this.configService = configService || config_service_1.ConfigService.getInstance();
}
/**
* Start interactive organization conversation
*/
async organize(directory, dryRun = false, noCache = false) {
console.log(chalk_1.default.blue('š¤ Welcome to AI-powered file organization!\n'));
try {
// Initialize
await this.initialize();
// Scan files
const files = await this.scanFiles(directory);
if (files.length === 0) {
console.log(chalk_1.default.yellow('No files found to organize.'));
return [];
}
// Get user intent conversationally
const intent = await this.getOrganizationIntent(files.length, directory);
// Start AI conversation with higher turn limit since categories use separate conversations
const conversation = new organization_conversation_1.OrganizationConversation(this.aiProvider, files, directory, intent, 'ConversationalOrganizer', {
maxTurns: 15, // Higher limit for main conversation
temperature: 0.4,
maxContextSize: 15000
});
// Optional: Add pattern matching hints
const patternHints = await this.getPatternHints(files, noCache ? undefined : directory);
if (patternHints.length > 0) {
conversation.addPatternHints(patternHints);
}
// Run conversational organization
const suggestions = await this.runConversation(conversation, noCache);
if (suggestions.length === 0) {
console.log(chalk_1.default.yellow('\nNo organization suggestions were finalized.'));
return [];
}
// Execute organization (if not dry run)
if (!dryRun) {
await this.executeOrganization(suggestions, directory);
}
else {
this.showDryRunSummary(suggestions);
}
return suggestions;
}
catch (error) {
if (error instanceof Error && error.message.includes('cancelled')) {
console.log(chalk_1.default.yellow('\nš Organization cancelled. No changes were made.'));
return [];
}
throw error;
}
}
/**
* Initialize AI provider
*/
async initialize() {
const config = await this.configService.loadConfig();
const aiConfig = {
apiKey: config.ai.apiKey,
model: config.ai.model,
maxTokens: Math.max(config.ai.maxTokens || 1000, 2000),
temperature: config.ai.temperature || 0.4,
timeout: Math.max(config.ai.timeout || 90000, 120000)
};
switch (config.ai.provider) {
case 'openai':
this.aiProvider = new ai_providers_1.OpenAIProvider(aiConfig);
break;
case 'anthropic':
this.aiProvider = new ai_providers_1.AnthropicProvider(aiConfig);
break;
default:
throw new Error(`Unsupported AI provider: ${config.ai.provider}`);
}
}
/**
* Scan files in directory
*/
async scanFiles(directory) {
const spinner = (0, ora_1.default)('Scanning files...').start();
try {
const files = await this.fileScanner.scanDirectory(directory, true);
spinner.succeed(`Found ${files.length} files to analyze`);
return files;
}
catch (error) {
spinner.fail('Failed to scan files');
throw error;
}
}
/**
* Get user's organization intent conversationally
*/
async getOrganizationIntent(fileCount, directory) {
console.log(chalk_1.default.cyan(`\nI found ${fileCount} files in ${directory}.\n`));
const { intent } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'intent',
message: 'How would you like to organize these files? (Describe what you have in mind)',
validate: (input) => {
if (input.trim().length < 5) {
return 'Please provide a bit more detail about your organization goals';
}
return true;
}
}
]);
console.log(chalk_1.default.green('\n⨠Got it! Let me analyze your files and understand what we\'re working with...\n'));
return intent.trim();
}
/**
* Get pattern matching hints to help AI
*/
async getPatternHints(files, directory) {
const spinner = (0, ora_1.default)('Analyzing file patterns...').start();
try {
const hints = await this.patternService.getPatternHints(files, directory);
if (hints.length > 0) {
spinner.succeed('Found helpful patterns to guide organization');
return hints;
}
else {
spinner.succeed('Pattern analysis complete');
return [];
}
}
catch (error) {
spinner.warn('Pattern analysis had issues, proceeding with AI-only analysis');
return [];
}
}
/**
* Run the main conversation flow
*/
async runConversation(conversation, noCache = false) {
// Phase 1: Initial AI Analysis
const analysisResult = await this.runAnalysisPhase(conversation, noCache);
// Phase 2: Conversation & Refinement
const finalSuggestions = await this.runConversationPhase(conversation, analysisResult);
return finalSuggestions;
}
/**
* Phase 1: AI Analysis with Project Detection
*/
async runAnalysisPhase(conversation, noCache = false) {
console.log(chalk_1.default.blue('\nš Phase 1: Analyzing directory structure and content...'));
// Step 1: Detect projects first
const projectSpinner = (0, ora_1.default)('š Detecting projects and analyzing file patterns...').start();
const detectedProjects = await this.detectProjects(conversation.getContext().getFiles(), conversation.getContext().getBaseDirectory(), noCache);
if (detectedProjects.length > 0) {
projectSpinner.succeed('Project detection complete!');
this.showDetectedProjects(detectedProjects);
}
else {
projectSpinner.succeed('Pattern analysis complete');
}
// Step 2: AI Content Analysis
const aiSpinner = (0, ora_1.default)('š§ Using AI to analyze file contents...').start();
aiSpinner.text = `š§ Analyzing ${conversation.getContext().getFiles().length} files...`;
try {
const result = await conversation.startAnalysis();
aiSpinner.succeed('š Content Analysis Complete!');
// Show what AI discovered
if (result.discoveredCategories && Object.keys(result.discoveredCategories).length > 0) {
console.log(chalk_1.default.blue('\nš Content Categories Discovered:\n'));
Object.entries(result.discoveredCategories).forEach(([category, files]) => {
const categoryIcon = this.getCategoryIcon(category);
console.log(chalk_1.default.white(` ${categoryIcon} ${category}: ${files.length} files`));
});
console.log();
}
// Add detected projects to the result
if (detectedProjects.length > 0) {
result.detectedProjects = detectedProjects;
}
// Handle clarification if needed
if (result.clarificationNeeded) {
console.log(chalk_1.default.yellow('š¤ I have a few questions to better understand your preferences:\n'));
let questionPrompt = '';
for (const question of result.clarificationNeeded.questions) {
const { answer } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'answer',
message: question,
validate: (input) => input.trim().length > 0 || 'Please provide an answer'
}
]);
questionPrompt += `Q: ${question}\nA: ${answer}\n`;
}
const clarificationSpinner = (0, ora_1.default)('Processing clarifications...').start();
try {
await conversation.continueConversation(questionPrompt);
clarificationSpinner.succeed('Clarifications processed successfully!');
}
catch (error) {
clarificationSpinner.fail('Failed to process clarifications');
throw error;
}
console.log(chalk_1.default.green('\nā
Thanks for the clarification!\n'));
}
return result;
}
catch (error) {
aiSpinner.fail('AI analysis failed');
throw error;
}
}
/**
* Detect coding projects and related file structures with caching
*/
async detectProjects(files, baseDirectory, noCache = false) {
if (files.length === 0)
return [];
const projectCache = project_detection_cache_1.ProjectDetectionCache.getInstance();
// Try to get cached results first (unless cache is disabled)
if (!noCache) {
const cachedProjects = await projectCache.getCachedProjects(baseDirectory, files);
if (cachedProjects) {
return cachedProjects;
}
}
// No cache hit, perform expensive detection
const projects = [];
// Group files by directory
const filesByDirectory = new Map();
files.forEach(file => {
const dir = file.path.split('/').slice(0, -1).join('/');
if (!filesByDirectory.has(dir)) {
filesByDirectory.set(dir, []);
}
filesByDirectory.get(dir).push(file);
});
// Check each directory for project indicators
for (const [directory, dirFiles] of filesByDirectory) {
const indicators = [];
const projectFiles = [];
// Check for common project files
const projectFilePatterns = [
'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
'Cargo.toml', 'Cargo.lock',
'go.mod', 'go.sum',
'requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile',
'pom.xml', 'build.gradle', 'gradlew',
'Makefile', 'CMakeLists.txt',
'.gitignore', 'README.md', 'LICENSE',
'tsconfig.json', 'webpack.config.js', 'vite.config.js',
'Dockerfile', 'docker-compose.yml',
'.env', '.env.example'
];
const codeExtensions = [
'.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte',
'.py', '.pyi', '.ipynb',
'.java', '.kt', '.scala',
'.c', '.cpp', '.h', '.hpp',
'.rs', '.go', '.rb', '.php',
'.cs', '.vb', '.fs',
'.swift', '.m', '.mm',
'.sh', '.bash', '.zsh', '.fish',
'.sql', '.graphql', '.proto'
];
// Check for project configuration files
dirFiles.forEach(file => {
if (projectFilePatterns.includes(file.name.toLowerCase())) {
indicators.push(`Config file: ${file.name}`);
projectFiles.push(file);
}
else if (codeExtensions.includes(file.extension.toLowerCase())) {
projectFiles.push(file);
}
});
// If we found project indicators and code files, consider it a project
if (indicators.length > 0 && projectFiles.length > 2) {
const projectName = directory.split('/').pop() || 'Unknown Project';
const projectType = this.inferProjectType(indicators, projectFiles);
projects.push({
name: projectName,
files: projectFiles,
type: projectType,
rootPath: directory,
indicators
});
}
}
// Cache the results for future use (unless cache is disabled)
if (!noCache) {
await projectCache.cacheProjects(baseDirectory, files, projects);
}
return projects;
}
/**
* Infer project type from indicators and files
*/
inferProjectType(indicators, files) {
const indicatorText = indicators.join(' ').toLowerCase();
const extensions = files.map(f => f.extension.toLowerCase());
if (indicatorText.includes('package.json') || extensions.includes('.js') || extensions.includes('.jsx')) {
if (extensions.includes('.tsx') || extensions.includes('.ts')) {
return 'TypeScript/React project';
}
return 'JavaScript/Node.js project';
}
if (indicatorText.includes('cargo.toml') || extensions.includes('.rs')) {
return 'Rust project';
}
if (indicatorText.includes('go.mod') || extensions.includes('.go')) {
return 'Go project';
}
if (indicatorText.includes('requirements.txt') || indicatorText.includes('setup.py') || extensions.includes('.py')) {
return 'Python project';
}
if (indicatorText.includes('pom.xml') || indicatorText.includes('build.gradle') || extensions.includes('.java')) {
return 'Java project';
}
if (extensions.includes('.c') || extensions.includes('.cpp') || indicatorText.includes('makefile')) {
return 'C/C++ project';
}
return 'Code project';
}
/**
* Show detected projects to user
*/
showDetectedProjects(projects) {
console.log(chalk_1.default.blue('\nšļø Detected Projects:\n'));
projects.forEach(project => {
console.log(chalk_1.default.white(` ⢠${project.name} (${project.files.length} files) - ${project.type}`));
});
console.log(chalk_1.default.green('\nā
Project structures will be preserved during organization\n'));
}
/**
* Phase 2: Targeted Content-Type Conversations (AI-Driven)
*/
async runConversationPhase(conversation, analysisResult) {
console.log(chalk_1.default.blue('\nš¬ Phase 2: Understanding your organization preferences...\n'));
// Get discovered categories from analysis
const discoveredCategories = conversation.getContext().getDiscoveredCategories();
const detectedProjects = analysisResult.detectedProjects || [];
// Handle detected projects first (these have standard handling)
if (detectedProjects.length > 0) {
await this.handleProjectConversations(detectedProjects, conversation);
}
// Conduct AI-driven content-type specific conversations
if (Object.keys(discoveredCategories).length > 0) {
await this.handleAIDrivenContentConversations(discoveredCategories, conversation);
}
// Generate final suggestions after all conversations
return this.generateFinalSuggestionsWithRetry(conversation);
}
/**
* Handle conversations about detected projects (standard handling)
*/
async handleProjectConversations(detectedProjects, conversation) {
console.log(chalk_1.default.blue('šļø I detected coding projects. These will preserve their internal structure:\n'));
detectedProjects.forEach(project => {
console.log(chalk_1.default.green(` ā
${project.name} ā Projects/${project.name}/ (structure preserved)`));
});
console.log();
// Send project info to AI for integration
const projectInfo = detectedProjects.map(p => `${p.name} (${p.type}, ${p.files.length} files)`).join(', ');
await conversation.continueConversation(`I have ${detectedProjects.length} coding projects that will preserve their internal structure: ${projectInfo}. Please organize them appropriately while maintaining their project integrity.`);
}
/**
* Handle AI-driven content-type conversations
*/
async handleAIDrivenContentConversations(discoveredCategories, conversation) {
for (const [category, files] of Object.entries(discoveredCategories)) {
if (files.length === 0)
continue;
const categoryIcon = this.getCategoryIcon(category);
console.log(chalk_1.default.blue(`${categoryIcon} Let's discuss your ${category} files (${files.length} files detected):`));
// Get AI-generated category-specific conversation
const categoryConversation = await this.getCategoryConversationFromAI(category, files, conversation);
if (categoryConversation) {
await this.conductCategoryConversation(categoryConversation, category, conversation);
}
console.log();
}
}
/**
* Get AI-generated conversation questions for a specific category using isolated conversation
*/
async getCategoryConversationFromAI(category, files, mainConversation) {
const spinner = (0, ora_1.default)(`Analyzing ${category} files to understand organization options...`).start();
try {
// Create a separate conversation context for this category to avoid turn limit issues
const categoryConversation = new organization_conversation_1.OrganizationConversation(this.aiProvider, files, mainConversation.getContext().getBaseDirectory(), `Analyze organization options for ${category} files`, `CategoryAnalysis_${category}`, {
maxTurns: 3, // Short limit for category discussions
temperature: 0.4,
maxContextSize: 8000
});
// Use the isolated conversation for category analysis
const result = await categoryConversation.getCategoryConversation(category, files);
// Complete the category conversation since we're done with it
categoryConversation.complete();
spinner.succeed(`Generated organization options for ${category}`);
return result;
}
catch (error) {
spinner.fail(`Failed to analyze ${category} files`);
console.log(chalk_1.default.yellow(`I'll ask you directly about your ${category} preferences.`));
return {
question: `How would you like your ${category} files organized?`,
inputType: "freeform",
reasoning: "Fallback to direct question due to analysis error"
};
}
}
/**
* Conduct the actual conversation with the user for a category
*/
async conductCategoryConversation(categoryConversation, category, conversation) {
let userResponse = '';
if (categoryConversation.inputType === 'choice' && categoryConversation.choices?.length > 0) {
// Present AI-generated choices
const { choice } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'choice',
message: categoryConversation.question,
choices: [
...categoryConversation.choices.map((choice) => ({
name: choice,
value: choice
})),
{ name: 'Let me specify something different', value: 'custom' }
]
}
]);
if (choice === 'custom') {
const { customInput } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'customInput',
message: `Describe how you'd like your ${category} files organized:`,
validate: (input) => input.trim().length > 5 || 'Please provide more detail'
}
]);
userResponse = customInput;
}
else {
userResponse = choice;
}
}
else {
// Use free-form input as determined by AI
const { freeformInput } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'freeformInput',
message: categoryConversation.question || `How would you like your ${category} files organized?`,
validate: (input) => input.trim().length > 5 || 'Please provide more detail about your preferences'
}
]);
userResponse = freeformInput;
}
// Send the user's response back to the AI with context
await conversation.continueConversation(`For ${category} files: ${userResponse}. Please apply this organization preference consistently to all ${category} files.`);
}
/**
* Generate final suggestions with retry logic
*/
async generateFinalSuggestionsWithRetry(conversation) {
let attempts = 0;
const maxAttempts = 4;
while (attempts < maxAttempts) {
attempts++;
// Generate suggestions
const spinner = (0, ora_1.default)('Generating organization suggestions...').start();
try {
const results = await conversation.generateFinalSuggestions();
spinner.succeed(`Generated ${results.suggestions.length} organization suggestions`);
// Show suggestions to user
const feedback = await this.presentSuggestions(results.suggestions);
if (feedback.approved) {
console.log(chalk_1.default.green('\nš Great! Your organization plan is ready.\n'));
return results.suggestions;
}
// Process feedback and continue conversation
await conversation.processFeedback(feedback);
if (feedback.feedback) {
console.log(chalk_1.default.blue('\nš Let me adjust the organization based on your feedback...\n'));
}
}
catch (error) {
spinner.fail(`Attempt ${attempts} failed: ${error}`);
if (attempts < maxAttempts) {
const { shouldRetry } = await inquirer_1.default.prompt([
{
type: 'confirm',
name: 'shouldRetry',
message: 'Would you like me to try a different approach?',
default: true
}
]);
if (!shouldRetry) {
throw new Error('Organization cancelled by user');
}
// Get additional context
const { additionalContext } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'additionalContext',
message: 'Any additional details about how you\'d like the files organized?',
validate: (input) => input.trim().length > 0 || 'Please provide some guidance'
}
]);
await conversation.continueConversation(additionalContext);
}
}
}
throw new Error('Unable to generate satisfactory organization suggestions after multiple attempts');
}
/**
* Present suggestions to user and get feedback with category-grouped preview
*/
async presentSuggestions(suggestions) {
// Group suggestions by category
const categoryGroups = this.groupSuggestionsByCategory(suggestions);
// Show category-grouped preview
await this.showCategoryGroupedPreview(categoryGroups, suggestions.length);
// Get initial user choice
const { response } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'response',
message: 'What would you like to do?',
choices: [
{ name: 'ā
Continue with organization', value: 'approve' },
{ name: 'š View details for a specific category', value: 'view_category' },
{ name: 'š View all changes', value: 'view_all' },
{ name: 'š§ Good direction, but needs adjustments', value: 'adjust' },
{ name: 'ā Not what I want, let me explain differently', value: 'reject' },
{ name: 'šŖ Cancel organization', value: 'cancel' }
]
}
]);
return this.handlePreviewResponse(response, categoryGroups, suggestions);
}
/**
* Group suggestions by category for enhanced preview
*/
groupSuggestionsByCategory(suggestions) {
const categoryGroups = new Map();
suggestions.forEach(suggestion => {
const category = suggestion.category || this.inferCategoryFromPath(suggestion.suggestedPath);
if (!categoryGroups.has(category)) {
categoryGroups.set(category, []);
}
categoryGroups.get(category).push(suggestion);
});
return categoryGroups;
}
/**
* Infer category from suggested path if not explicitly set
*/
inferCategoryFromPath(path) {
const topLevelDir = path.split('/')[0];
return topLevelDir || 'Other';
}
/**
* Show category-grouped preview with samples
*/
async showCategoryGroupedPreview(categoryGroups, totalFiles) {
console.log(chalk_1.default.blue(`\nš Phase 3: Organization Plan Summary (${totalFiles} files total):`));
console.log(chalk_1.default.blue('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n'));
for (const [category, categorySuggestions] of categoryGroups) {
const categoryIcon = this.getCategoryIcon(category);
console.log(chalk_1.default.white(`${categoryIcon} ${category} (${categorySuggestions.length} files):`));
// Show sample files for this category
const sampleSize = Math.min(3, categorySuggestions.length);
const samples = categorySuggestions.slice(0, sampleSize);
samples.forEach(suggestion => {
console.log(chalk_1.default.gray(` ${suggestion.file.name} ā ${suggestion.suggestedPath}`));
});
if (categorySuggestions.length > sampleSize) {
console.log(chalk_1.default.dim(` ... and ${categorySuggestions.length - sampleSize} more files\n`));
}
else {
console.log();
}
}
}
/**
* Get appropriate icon for category
*/
getCategoryIcon(category) {
const categoryLower = category.toLowerCase();
if (categoryLower.includes('movie'))
return 'š';
if (categoryLower.includes('tv') || categoryLower.includes('show'))
return 'šŗ';
if (categoryLower.includes('music') || categoryLower.includes('audio'))
return 'šµ';
if (categoryLower.includes('photo') || categoryLower.includes('image'))
return 'šø';
if (categoryLower.includes('project') || categoryLower.includes('code'))
return 'šļø';
if (categoryLower.includes('document'))
return 'š';
if (categoryLower.includes('video'))
return 'š¬';
return 'š';
}
/**
* Handle user response to preview
*/
async handlePreviewResponse(response, categoryGroups, allSuggestions) {
switch (response) {
case 'approve':
return { approved: true };
case 'cancel':
throw new Error('Organization cancelled by user');
case 'view_category':
return this.handleCategoryDetailView(categoryGroups, allSuggestions);
case 'view_all':
return this.handleViewAllChanges(allSuggestions);
case 'adjust':
const { adjustmentFeedback } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'adjustmentFeedback',
message: 'What adjustments would you like me to make?',
validate: (input) => input.trim().length > 5 || 'Please be specific about what you\'d like changed'
}
]);
return { approved: false, feedback: adjustmentFeedback };
case 'reject':
const { rejectionFeedback } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'rejectionFeedback',
message: 'How would you prefer the files to be organized instead?',
validate: (input) => input.trim().length > 10 || 'Please provide more detail about your preferred approach'
}
]);
return { approved: false, feedback: rejectionFeedback };
default:
return { approved: false };
}
}
/**
* Handle detailed view of a specific category
*/
async handleCategoryDetailView(categoryGroups, allSuggestions) {
const categories = Array.from(categoryGroups.keys());
const { selectedCategory } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'selectedCategory',
message: 'Which category would you like to explore in detail?',
choices: categories.map(cat => ({
name: `${this.getCategoryIcon(cat)} ${cat} (${categoryGroups.get(cat).length} files)`,
value: cat
}))
}
]);
// Show detailed view of selected category
const categorySuggestions = categoryGroups.get(selectedCategory);
console.log(chalk_1.default.blue(`\nš Detailed view: ${selectedCategory}\n`));
categorySuggestions.forEach((suggestion, index) => {
console.log(chalk_1.default.white(` ${index + 1}. ${suggestion.file.name}`));
console.log(chalk_1.default.gray(` ā ${suggestion.suggestedPath}`));
console.log(chalk_1.default.dim(` ${suggestion.reason}\n`));
});
// Ask what to do next
const { nextAction } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'nextAction',
message: 'What would you like to do now?',
choices: [
{ name: 'ā¬
ļø Back to category overview', value: 'back' },
{ name: 'ā
Continue with organization', value: 'approve' },
{ name: 'š§ I want to adjust this category', value: 'adjust_category' },
{ name: 'šŖ Cancel organization', value: 'cancel' }
]
}
]);
if (nextAction === 'back') {
return this.presentSuggestions(allSuggestions);
}
else if (nextAction === 'adjust_category') {
const { categoryFeedback } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'categoryFeedback',
message: `How would you like to change the ${selectedCategory} category organization?`,
validate: (input) => input.trim().length > 5 || 'Please be specific about the changes'
}
]);
return { approved: false, feedback: `For ${selectedCategory} category: ${categoryFeedback}` };
}
else {
return this.handlePreviewResponse(nextAction, categoryGroups, allSuggestions);
}
}
/**
* Handle viewing all changes
*/
async handleViewAllChanges(suggestions) {
console.log(chalk_1.default.blue(`\nš Complete Organization Plan (${suggestions.length} files):\n`));
suggestions.forEach((suggestion, index) => {
console.log(chalk_1.default.white(` ${index + 1}. ${suggestion.file.name}`));
console.log(chalk_1.default.gray(` ā ${suggestion.suggestedPath}`));
console.log(chalk_1.default.dim(` ${suggestion.reason}\n`));
});
// Ask what to do next
const { nextAction } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'nextAction',
message: 'What would you like to do now?',
choices: [
{ name: 'ā¬
ļø Back to category overview', value: 'back' },
{ name: 'ā
Continue with organization', value: 'approve' },
{ name: 'š§ I want to make adjustments', value: 'adjust' },
{ name: 'šŖ Cancel organization', value: 'cancel' }
]
}
]);
if (nextAction === 'back') {
return this.presentSuggestions(suggestions);
}
else {
return this.handlePreviewResponse(nextAction, new Map(), suggestions);
}
}
/**
* Execute the organization
*/
async executeOrganization(suggestions, baseDirectory) {
const { confirmExecute } = await inquirer_1.default.prompt([
{
type: 'confirm',
name: 'confirmExecute',
message: `Ready to organize ${suggestions.length} files. Proceed?`,
default: true
}
]);
if (!confirmExecute) {
throw new Error('Organization cancelled by user');
}
const config = await this.configService.loadConfig();
// Create backup if enabled
if (config.organization.createBackups) {
const spinner = (0, ora_1.default)('Creating backup...').start();
try {
const fileOrganizer = new file_organizer_1.FileOrganizer();
const backupPath = await fileOrganizer.createBackup(baseDirectory);
spinner.succeed(`Backup created: ${backupPath}`);
}
catch (error) {
spinner.warn('Backup creation failed, continuing without backup');
console.log(chalk_1.default.yellow(`Backup error: ${error}`));
}
}
const spinner = (0, ora_1.default)('Organizing files...').start();
try {
const FileOrganizer = (await Promise.resolve().then(() => __importStar(require('./file-organizer')))).FileOrganizer;
const fileOrganizer = new FileOrganizer();
await fileOrganizer.applySuggestions(suggestions, baseDirectory);
spinner.succeed(`Successfully organized ${suggestions.length} files!`);
console.log(chalk_1.default.green('\nš File organization complete!\n'));
}
catch (error) {
spinner.fail('Failed to organize files');
throw error;
}
}
/**
* Show dry run summary
*/
showDryRunSummary(suggestions) {
console.log(chalk_1.default.yellow('\nš DRY RUN SUMMARY\n'));
console.log(chalk_1.default.white(`Would organize ${suggestions.length} files:\n`));
// Group by destination directory
const groups = new Map();
suggestions.forEach(suggestion => {
const dir = suggestion.suggestedPath.split('/').slice(0, -1).join('/');
if (!groups.has(dir)) {
groups.set(dir, []);
}
groups.get(dir).push(suggestion);
});
groups.forEach((groupSuggestions, dir) => {
console.log(chalk_1.default.blue(`š ${dir}/ (${groupSuggestions.length} files)`));
groupSuggestions.slice(0, 3).forEach(suggestion => {
console.log(chalk_1.default.gray(` ${suggestion.file.name}`));
});
if (groupSuggestions.length > 3) {
console.log(chalk_1.default.dim(` ... and ${groupSuggestions.length - 3} more`));
}
console.log();
});
console.log(chalk_1.default.yellow('Run without --dry-run to apply these changes.\n'));
}
}
exports.ConversationalOrganizer = ConversationalOrganizer;
//# sourceMappingURL=conversational-organizer.js.map