ailock
Version:
AI-Proof File Guard - Protect sensitive files from accidental AI modifications
265 lines • 12.4 kB
JavaScript
import chalk from 'chalk';
import { existsSync } from 'fs';
import { resolve } from 'path';
import { loadConfig, findProtectedFiles } from '../core/config.js';
import { getPlatformAdapter } from '../core/platform.js';
import { info, success, warn, error } from '../utils/output.js';
import { canLockFile, trackFileLocked, trackFileUnlocked, getQuotaStatusSummary } from '../core/directory-tracker.js';
import { getApiService } from './CliApiService.js';
/**
* Service for handling common file operations (lock/unlock)
* Eliminates code duplication between lock and unlock commands
*/
export class FileOperationService {
adapter = getPlatformAdapter();
/**
* Process files for locking or unlocking
* DRY principle - shared logic for both operations
*/
async processFiles(operation, options) {
const result = {
successful: [],
failed: [],
skipped: [],
quotaBlocked: [],
totalFiles: 0
};
// Get files to process
const filesToProcess = await this.getFilesToProcess(options);
result.totalFiles = filesToProcess.length;
if (filesToProcess.length === 0) {
return result;
}
// Display what will be done
if (!options.dryRun) {
this.displayOperationStart(operation, filesToProcess);
}
// Process each file
for (const file of filesToProcess) {
if (options.dryRun) {
info(chalk.gray(` Would ${operation}: ${file}`));
result.successful.push(file);
continue;
}
try {
// Check current state
const isLocked = await this.adapter.isLocked(file);
if (operation === 'lock' && isLocked) {
if (options.verbose) {
info(chalk.gray(` ℹ️ Already locked: ${file}`));
}
result.skipped.push(file);
continue;
}
if (operation === 'unlock' && !isLocked) {
if (options.verbose) {
info(chalk.gray(` ℹ️ Already unlocked: ${file}`));
}
result.skipped.push(file);
continue;
}
// Check quota before locking (only for lock operations)
if (operation === 'lock') {
const quotaCheck = await canLockFile(file);
if (!quotaCheck.canLock) {
result.quotaBlocked.push(file);
if (options.verbose || result.quotaBlocked.length <= 3) {
warn(` 🚫 Quota exceeded for: ${file}`);
}
// Track analytics for conversion trigger
if (result.quotaBlocked.length === 1) {
const apiService = getApiService();
await apiService.trackUsage('lock_attempt_blocked', {
directoryPath: file,
totalLockedCount: quotaCheck.quotaUsage.used
});
}
continue;
}
}
// Perform the operation
if (operation === 'lock') {
await this.adapter.lockFile(file);
await trackFileLocked(file); // Track for quota management
}
else {
await this.adapter.unlockFile(file);
await trackFileUnlocked(file); // Update quota tracking
}
result.successful.push(file);
if (options.verbose) {
const icon = operation === 'lock' ? '🔒' : '🔓';
info(chalk.green(` ${icon} ${file}`));
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
result.failed.push({ file, error: errorMessage });
if (options.verbose) {
console.error(` ❌ Failed to ${operation} ${file}: ${errorMessage}`);
}
}
}
return result;
}
/**
* Get list of files to process based on options
*/
async getFilesToProcess(options) {
// If specific patterns provided, use those
if (options.patterns && options.patterns.length > 0) {
const files = [];
for (const pattern of options.patterns) {
const resolvedPath = resolve(pattern);
if (existsSync(resolvedPath)) {
files.push(resolvedPath);
}
else if (pattern.includes('*')) {
// It's a glob pattern, use config to find files
const config = await loadConfig(undefined, {
includeGitignored: options.includeGitignored
});
const matchedFiles = await findProtectedFiles({
...config,
patterns: [pattern]
});
files.push(...matchedFiles);
}
else {
warn(`⚠️ File not found: ${pattern}`);
}
}
return [...new Set(files)]; // Remove duplicates
}
// Use config file patterns
const config = await loadConfig(undefined, {
includeGitignored: options.includeGitignored
});
if (!config.patterns || config.patterns.length === 0) {
throw new Error('No patterns specified. Use .ailock file or provide file arguments.');
}
return findProtectedFiles(config);
}
/**
* Display operation start message
*/
displayOperationStart(operation, files) {
const action = operation === 'lock' ? 'lock' : 'unlock';
info(chalk.cyan(`Found ${files.length} file(s) to ${action}:`));
for (const file of files.slice(0, 10)) {
const icon = operation === 'lock' ? '🔒' : '🔓';
info(chalk.gray(` ${icon} ${file}`));
}
if (files.length > 10) {
info(chalk.gray(` ... and ${files.length - 10} more`));
}
info(''); // Empty line for readability
}
/**
* Display operation summary
*/
async displaySummary(operation, result, options) {
if (options.dryRun) {
info(chalk.yellow(`\n🔍 Dry run completed. ${result.totalFiles} file(s) would be ${operation}ed.`));
return;
}
// Success summary
if (result.successful.length > 0) {
const action = operation === 'lock' ? 'Locked' : 'Unlocked';
success(`✅ ${action} ${result.successful.length} file(s)`);
}
// Skipped summary
if (result.skipped.length > 0 && options.verbose) {
const state = operation === 'lock' ? 'already locked' : 'already unlocked';
info(chalk.gray(`ℹ️ Skipped ${result.skipped.length} file(s) (${state})`));
}
// Quota blocking summary (most important for conversion)
if (result.quotaBlocked.length > 0) {
error(`\n🚫 Quota exceeded: ${result.quotaBlocked.length} file(s) could not be locked`);
// Show quota status
const quotaStatus = await getQuotaStatusSummary();
warn(` Current quota: ${quotaStatus}`);
// Show conversion message
this.displayConversionMessage(result.quotaBlocked.length);
}
// Failed summary with helpful suggestions
if (result.failed.length > 0) {
error(`\n❌ Failed to ${operation} ${result.failed.length} file(s):`);
for (const failedItem of result.failed) {
error(` • ${failedItem.file}: ${failedItem.error}`);
}
// Provide helpful suggestions based on operation and error types
this.displayErrorSuggestions(operation, result.failed);
}
// Additional hints
if (operation === 'unlock' && result.successful.length > 0) {
warn('\n⚠️ Remember to lock these files again after editing!');
info(chalk.gray('💡 Consider using: ailock edit <filename> (auto-relock after editing)'));
}
}
/**
* Display helpful suggestions based on error types
*/
displayErrorSuggestions(operation, failures) {
const permissionErrors = failures.filter(f => f.error.toLowerCase().includes('permission') ||
f.error.toLowerCase().includes('denied') ||
f.error.toLowerCase().includes('eperm'));
const lockedErrors = failures.filter(f => f.error.toLowerCase().includes('locked') ||
f.error.toLowerCase().includes('busy') ||
f.error.toLowerCase().includes('ebusy'));
const pathErrors = failures.filter(f => f.error.toLowerCase().includes('path') ||
f.error.toLowerCase().includes('directory') ||
f.error.toLowerCase().includes('outside allowed'));
info(chalk.blue('\n💡 Helpful suggestions:'));
if (permissionErrors.length > 0) {
info(chalk.gray(' • Permission errors may require elevated privileges'));
info(chalk.gray(' • Try: sudo ailock ' + (operation === 'lock' ? 'lock' : 'unlock') + ' <file>'));
if (operation === 'unlock') {
info(chalk.gray(' • For editing: ailock edit <file> (handles permissions automatically)'));
}
}
if (lockedErrors.length > 0) {
info(chalk.gray(' • Files may be locked by another process'));
info(chalk.gray(' • Try: ailock emergency-unlock <file> (if files are stuck)'));
info(chalk.gray(' • Or: ailock doctor (diagnose lock issues)'));
}
if (pathErrors.length > 0) {
info(chalk.gray(' • Path validation is blocking access'));
info(chalk.gray(' • Ensure files are in your project directory'));
info(chalk.gray(' • Check file paths for special characters or suspicious patterns'));
}
// General suggestions
if (failures.length > 0) {
info(chalk.gray(' • Run: ailock doctor (comprehensive health check)'));
info(chalk.gray(' • Use: ailock diagnose <file> (detailed file analysis)'));
if (operation === 'unlock' && failures.some(f => f.error.includes('permission'))) {
info(chalk.blue('\n🎯 Quick fix for editing:'));
info(chalk.gray(' ailock edit <file> # Temporarily unlock, open editor, then relock'));
}
}
}
/**
* Display conversion message when quota is exceeded
*/
displayConversionMessage(blockedCount) {
// Main conversion trigger message (as per blueprint)
error(chalk.red('\n🚫 Free quota limit reached.'));
info(chalk.yellow('\n✨ Unlock more capacity instantly!'));
info(chalk.white(' Visit https://ailock.dev to sign up and get 1 FREE auth code (+1 permanent capacity).'));
info(chalk.green('\n💡 Growth hack:'));
info(chalk.white(' Use a friend\'s referral link to sign up and get 2 EXTRA auth codes!'));
info(chalk.gray(' (That\'s 4 total directories instead of 2)'));
info(chalk.cyan('\n🔑 Already have an auth code?'));
info(chalk.white(' Run: ') + chalk.bold.cyan('ailock auth <your_auth_code>'));
// Additional motivation for viral sharing
info(chalk.magenta('\n🎁 Bonus rewards:'));
info(chalk.gray(' • Invite friends = Both get 1 auth code when they activate'));
info(chalk.gray(' • Invite 3 friends = Get 5 BONUS auth codes!'));
info(chalk.gray(' • Every auth code = +1 permanent directory capacity'));
if (blockedCount > 5) {
info(chalk.yellow(`\n📊 You tried to lock ${blockedCount} additional directories.`));
info(chalk.white(' Sign up now to unlock your full productivity potential!'));
}
}
}
//# sourceMappingURL=FileOperationService.js.map