tidyai-ts
Version:
AI-powered cross-platform file organizer using OpenRouter API
313 lines (312 loc) • 13.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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.organizeFolder = organizeFolder;
exports.deleteUnnecessaryFiles = deleteUnnecessaryFiles;
exports.undoOrganize = undoOrganize;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const openrouter_1 = require("./openrouter");
const cli_utils_1 = require("./cli-utils");
const readline = __importStar(require("readline"));
const HISTORY_FILE = '.tidyai/history.json';
const DELETE_HISTORY_FILE = '.tidyai/delete-history.json';
function promptUser(message) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question(`${message} (y/N): `, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
});
});
}
async function organizeFolder(folderPath) {
(0, cli_utils_1.displaySectionHeader)('Organization Process');
// Check if already organized
const historyPath = path.join(folderPath, HISTORY_FILE);
try {
await fs.access(historyPath);
(0, cli_utils_1.displayInfo)('This folder appears to be already organized. Use --undo to revert changes.');
// Create a log entry
await (0, cli_utils_1.createLogEntry)(folderPath, 'Folder already organized, no action taken');
return;
}
catch (error) {
// History file doesn't exist, which is what we expect
}
// Create history directory
const historyDir = path.dirname(historyPath);
await fs.mkdir(historyDir, { recursive: true });
// Read files in the directory
(0, cli_utils_1.displayInfo)('Scanning folder contents...');
const files = await fs.readdir(folderPath);
// Filter out directories and history file
const fileNames = [];
for (const file of files) {
const filePath = path.join(folderPath, file);
const stat = await fs.stat(filePath);
if (stat.isFile() && filePath !== historyPath) {
fileNames.push(file);
}
}
if (fileNames.length === 0) {
(0, cli_utils_1.displayInfo)('No files to organize in this directory.');
// Create a log entry
await (0, cli_utils_1.createLogEntry)(folderPath, 'No files to organize in directory');
return;
}
(0, cli_utils_1.displayInfo)(`Found ${fileNames.length} files to organize`);
// Get folder suggestions from AI
(0, cli_utils_1.displayInfo)('Getting folder suggestions from AI...');
const client = new openrouter_1.OpenRouterClient();
const suggestions = await client.getSuggestions(fileNames);
// Create folders and move files
(0, cli_utils_1.displaySectionHeader)('Moving Files');
const moves = [];
// Progress tracking
let processedFiles = 0;
const totalFiles = fileNames.length;
for (const fileName of fileNames) {
const suggestedFolder = suggestions[fileName] || 'Other';
const targetDir = path.join(folderPath, suggestedFolder);
// Create target directory if it doesn't exist
await fs.mkdir(targetDir, { recursive: true });
// Move file
const originalPath = path.join(folderPath, fileName);
const newPath = path.join(targetDir, fileName);
await fs.rename(originalPath, newPath);
moves.push({
fileName,
originalPath,
newPath
});
// Update progress
processedFiles++;
const percentage = processedFiles / totalFiles;
const progressBar = (0, cli_utils_1.displayProgressBar)(percentage);
(0, cli_utils_1.updateProgress)(`Moving files: [${progressBar}] ${Math.round(percentage * 100)}% (${processedFiles}/${totalFiles}) ${fileName}`);
}
// Clear the progress line and show completion
(0, cli_utils_1.clearProgressLine)();
(0, cli_utils_1.displaySuccess)(`Moved ${moves.length} files into folders`);
// Save history
(0, cli_utils_1.displayInfo)('Saving organization history...');
const history = {
timestamp: new Date().toISOString(),
moves
};
await fs.writeFile(historyPath, JSON.stringify(history, null, 2));
// Create a log entry
await (0, cli_utils_1.createLogEntry)(folderPath, `Organized ${moves.length} files into folders`, '.tidyai/logs');
(0, cli_utils_1.displaySuccess)('Organization complete!');
}
async function deleteUnnecessaryFiles(folderPath) {
(0, cli_utils_1.displaySectionHeader)('Delete Unnecessary Files');
// First organize the folder
await organizeFolder(folderPath);
// Find potentially unnecessary files
(0, cli_utils_1.displayInfo)('Scanning for unnecessary files...');
const unnecessaryFiles = [];
// Walk through all subdirectories to find files
async function walkDir(dir) {
const files = await fs.readdir(dir);
for (const file of files) {
// Skip .tidyai directory
if (file === '.tidyai')
continue;
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
await walkDir(filePath);
}
else {
// Check if file is potentially unnecessary
const fileName = path.basename(file).toLowerCase();
if (fileName === 'thumbs.db' ||
fileName === '.ds_store' ||
fileName.endsWith('.tmp') ||
fileName.endsWith('.log') ||
fileName === 'desktop.ini') {
unnecessaryFiles.push(filePath);
}
}
}
}
await walkDir(folderPath);
if (unnecessaryFiles.length === 0) {
(0, cli_utils_1.displayInfo)('No unnecessary files found.');
await (0, cli_utils_1.createLogEntry)(folderPath, 'No unnecessary files found during deletion process', '.tidyai/logs');
return;
}
(0, cli_utils_1.displayWarning)(`Found ${unnecessaryFiles.length} potentially unnecessary files:`);
for (const file of unnecessaryFiles) {
console.log(` - ${file}`);
}
// Prompt user for confirmation
const confirmDelete = await promptUser('Are you sure you want to delete these files? This action cannot be undone without a backup.');
if (!confirmDelete) {
(0, cli_utils_1.displayInfo)('File deletion cancelled by user.');
await (0, cli_utils_1.createLogEntry)(folderPath, 'File deletion cancelled by user', '.tidyai/logs');
return;
}
// Save delete history for potential undo
const deleteHistoryPath = path.join(folderPath, DELETE_HISTORY_FILE);
const deleteHistory = {
timestamp: new Date().toISOString(),
deletedFiles: []
};
// Create delete history directory
const deleteHistoryDir = path.dirname(deleteHistoryPath);
await fs.mkdir(deleteHistoryDir, { recursive: true });
// Delete files and save their paths for undo
(0, cli_utils_1.displaySectionHeader)('Deleting Files');
let deletedCount = 0;
const totalFiles = unnecessaryFiles.length;
for (const filePath of unnecessaryFiles) {
try {
// Save file content for potential recovery
deleteHistory.deletedFiles.push(filePath);
// Delete file
await fs.rm(filePath);
deletedCount++;
// Update progress
const percentage = deletedCount / totalFiles;
const progressBar = (0, cli_utils_1.displayProgressBar)(percentage);
(0, cli_utils_1.updateProgress)(`Deleting files: [${progressBar}] ${Math.round(percentage * 100)}% (${deletedCount}/${totalFiles}) ${path.basename(filePath)}`);
}
catch (error) {
(0, cli_utils_1.displayWarning)(`Failed to delete ${filePath}: ${error.message}`);
}
}
// Clear the progress line and show completion
(0, cli_utils_1.clearProgressLine)();
// Save delete history
await fs.writeFile(deleteHistoryPath, JSON.stringify(deleteHistory, null, 2));
(0, cli_utils_1.displaySuccess)(`Deleted ${deletedCount} unnecessary files.`);
await (0, cli_utils_1.createLogEntry)(folderPath, `Deleted ${deletedCount} unnecessary files`, '.tidyai/logs');
}
async function undoOrganize(folderPath) {
(0, cli_utils_1.displaySectionHeader)('Undo Process');
const historyPath = path.join(folderPath, HISTORY_FILE);
const deleteHistoryPath = path.join(folderPath, DELETE_HISTORY_FILE);
// Check if we have delete history to undo
let hasDeleteHistory = false;
try {
await fs.access(deleteHistoryPath);
hasDeleteHistory = true;
}
catch (error) {
// No delete history, that's fine
}
// First undo file deletions if they exist
if (hasDeleteHistory) {
try {
const deleteHistoryData = await fs.readFile(deleteHistoryPath, 'utf-8');
const deleteHistory = JSON.parse(deleteHistoryData);
(0, cli_utils_1.displayWarning)('Undoing file deletions is not possible as we do not store file contents.');
(0, cli_utils_1.displayInfo)('However, we can remove the delete history record.');
// Remove delete history file
await fs.rm(deleteHistoryPath);
await (0, cli_utils_1.createLogEntry)(folderPath, 'Removed delete history during undo process', '.tidyai/logs');
}
catch (error) {
(0, cli_utils_1.displayWarning)(`Error processing delete history: ${error.message}`);
}
}
// Then undo file organization
try {
const historyData = await fs.readFile(historyPath, 'utf-8');
const history = JSON.parse(historyData);
(0, cli_utils_1.displayInfo)(`Found history for ${history.moves.length} files`);
// Move files back to their original locations
(0, cli_utils_1.displaySectionHeader)('Restoring Files');
// Progress tracking
let processedFiles = 0;
const totalFiles = history.moves.length;
for (const move of history.moves) {
// Create directory for original path if needed
const originalDir = path.dirname(move.originalPath);
await fs.mkdir(originalDir, { recursive: true });
// Move file back
await fs.rename(move.newPath, move.originalPath);
// Update progress
processedFiles++;
const percentage = processedFiles / totalFiles;
const progressBar = (0, cli_utils_1.displayProgressBar)(percentage);
(0, cli_utils_1.updateProgress)(`Restoring files: [${progressBar}] ${Math.round(percentage * 100)}% (${processedFiles}/${totalFiles}) ${move.fileName}`);
}
// Clear the progress line and show completion
(0, cli_utils_1.clearProgressLine)();
(0, cli_utils_1.displaySuccess)(`Restored ${history.moves.length} files to their original locations`);
// Remove empty folders
(0, cli_utils_1.displayInfo)('Cleaning up empty folders...');
const folders = [...new Set(history.moves.map(move => path.dirname(move.newPath)))];
let removedFolders = 0;
for (const folder of folders) {
try {
const files = await fs.readdir(folder);
if (files.length === 0) {
await fs.rmdir(folder);
removedFolders++;
}
}
catch (error) {
// Ignore errors when trying to remove folders
}
}
if (removedFolders > 0) {
(0, cli_utils_1.displaySuccess)(`Removed ${removedFolders} empty folders`);
}
// Remove history file
await fs.rm(historyPath);
// Create a log entry
await (0, cli_utils_1.createLogEntry)(folderPath, `Undid organization of ${history.moves.length} files`, '.tidyai/logs');
(0, cli_utils_1.displaySuccess)('Undo complete!');
}
catch (error) {
if (error.code === 'ENOENT') {
(0, cli_utils_1.displayInfo)('No organization history found for this folder.');
// Create a log entry
await (0, cli_utils_1.createLogEntry)(folderPath, 'No organization history found for folder', '.tidyai/logs');
}
else {
(0, cli_utils_1.displayError)(`Error during undo: ${error.message}`);
throw error;
}
}
}