@gavbarosee/react-kickstart
Version:
A modern CLI tool for creating React applications with various frameworks
293 lines (255 loc) • 8.05 kB
JavaScript
import chalk from "chalk";
import fs from "fs-extra";
import os from "os";
import path from "path";
/**
* Manages project cleanup with safety checks and different cleanup strategies
*/
export class CleanupManager {
constructor() {
this.cleanupHistory = [];
}
/**
* Main cleanup method with comprehensive safety checks
*/
async cleanup(projectPath, options = {}) {
const {
reason = "unknown",
force = false,
verbose = false,
maxAge = 5, // minutes
} = options;
if (!projectPath) {
if (verbose) {
console.log(chalk.yellow("No project path provided for cleanup"));
}
return { success: false, reason: "no_path" };
}
try {
// Safety check 1: Prevent dangerous paths
if (this.isDangerousPath(projectPath)) {
console.error(
chalk.red(`Refusing to clean up potentially dangerous path: ${projectPath}`),
);
return { success: false, reason: "dangerous_path" };
}
// Safety check 2: Check if path exists
if (!fs.existsSync(projectPath)) {
if (verbose) {
console.log(chalk.yellow(`Cleanup target does not exist: ${projectPath}`));
}
return { success: true, reason: "already_missing" };
}
// Safety check 3: Age check (unless forced)
if (!force && !this.isRecentlyCreated(projectPath, maxAge)) {
console.error(
chalk.yellow(
`Skipping cleanup of directory older than ${maxAge} minutes: ${projectPath}`,
),
);
return { success: false, reason: "too_old" };
}
// Safety check 4: Verify it's our project (unless forced)
if (!force && !this.isOurProject(projectPath)) {
console.error(
chalk.yellow(
`Skipping cleanup as directory doesn't appear to be created by react-kickstart: ${projectPath}`,
),
);
return { success: false, reason: "not_our_project" };
}
// Perform cleanup
const cleanupResult = await this.performCleanup(projectPath, options);
// Record cleanup
this.recordCleanup(projectPath, reason, cleanupResult);
if (cleanupResult.success) {
console.log(chalk.yellow(`Cleaned up project directory: ${projectPath}`));
}
return cleanupResult;
} catch (error) {
console.error(chalk.red(`Failed to clean up directory: ${error.message}`));
return { success: false, reason: "cleanup_error", error };
}
}
/**
* Check if a path is dangerous to delete
*/
isDangerousPath(projectPath) {
const normalizedPath = path.resolve(projectPath);
const cwd = process.cwd();
const home = os.homedir();
// Dangerous paths
const dangerousPaths = [
"/",
"/usr",
"/bin",
"/etc",
"/var",
"/opt",
"/boot",
"/sys",
"/proc",
"/dev",
home,
cwd,
path.dirname(cwd),
];
return dangerousPaths.some(
(dangerous) =>
normalizedPath === path.resolve(dangerous) || normalizedPath.length < 10, // Very short paths are suspicious
);
}
/**
* Check if directory was recently created
*/
isRecentlyCreated(projectPath, maxAgeMinutes) {
try {
const stats = fs.statSync(projectPath);
const creationTime = new Date(stats.birthtime).getTime();
const now = new Date().getTime();
const maxAge = maxAgeMinutes * 60 * 1000; // Convert to milliseconds
return now - creationTime <= maxAge;
} catch {
// If we can't check age, err on the side of caution
return false;
}
}
/**
* Check if directory appears to be our project
*/
isOurProject(projectPath) {
try {
// Prefer explicit marker created during this run
const markerPath = path.join(projectPath, ".react-kickstart.tmp");
if (fs.existsSync(markerPath)) {
return true;
}
// Check for package.json with React dependencies
const packageJsonPath = path.join(projectPath, "package.json");
if (fs.existsSync(packageJsonPath)) {
const packageJson = fs.readJsonSync(packageJsonPath);
// Check if scripts contain expected keys
const hasExpectedScripts =
packageJson.scripts &&
(packageJson.scripts.dev ||
packageJson.scripts.build ||
packageJson.scripts.start);
// Check if dependencies contain React
const hasReactDep =
packageJson.dependencies &&
(packageJson.dependencies.react || packageJson.dependencies["react-dom"]);
if (hasExpectedScripts && hasReactDep) {
return true;
}
}
// Check for framework-specific directories
const srcExists = fs.existsSync(path.join(projectPath, "src"));
const publicExists = fs.existsSync(path.join(projectPath, "public"));
const appExists = fs.existsSync(path.join(projectPath, "app")); // Next.js app router
const pagesExists = fs.existsSync(path.join(projectPath, "pages")); // Next.js pages router
// Evidence of our generated structure
return (srcExists && publicExists) || appExists || pagesExists;
} catch {
// If we can't verify, err on the side of caution
return false;
}
}
/**
* Perform the actual cleanup operation
*/
async performCleanup(projectPath, options = {}) {
const { verbose = false, strategy = "remove" } = options;
try {
switch (strategy) {
case "remove":
fs.removeSync(projectPath);
break;
case "empty":
fs.emptyDirSync(projectPath);
break;
case "backup":
await this.backupAndRemove(projectPath);
break;
default:
throw new Error(`Unknown cleanup strategy: ${strategy}`);
}
return { success: true, strategy };
} catch (error) {
return { success: false, error: error.message, strategy };
}
}
/**
* Backup directory before removing (for safety)
*/
async backupAndRemove(projectPath) {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupPath = `${projectPath}.backup.${timestamp}`;
// Move to backup location
fs.moveSync(projectPath, backupPath);
console.log(chalk.cyan(`Project backed up to: ${backupPath}`));
return { backupPath };
}
/**
* Emergency cleanup for critical situations
*/
async emergencyCleanup(projectPath) {
return this.cleanup(projectPath, {
reason: "emergency",
force: true,
strategy: "backup",
verbose: true,
});
}
/**
* Record cleanup operation for auditing
*/
recordCleanup(projectPath, reason, result) {
const record = {
timestamp: new Date().toISOString(),
projectPath,
reason,
success: result.success,
strategy: result.strategy,
};
this.cleanupHistory.push(record);
// Keep only last 20 cleanup records
if (this.cleanupHistory.length > 20) {
this.cleanupHistory.shift();
}
}
/**
* Get cleanup statistics
*/
getCleanupStats() {
return {
totalCleanups: this.cleanupHistory.length,
successRate:
this.cleanupHistory.length > 0
? this.cleanupHistory.filter((r) => r.success).length /
this.cleanupHistory.length
: 0,
cleanupsByReason: this.cleanupHistory.reduce((acc, record) => {
acc[record.reason] = (acc[record.reason] || 0) + 1;
return acc;
}, {}),
recentCleanups: this.cleanupHistory.slice(-5),
};
}
/**
* Setup process cleanup handlers
*/
setupCleanupHandlers(projectPath) {
const cleanup = () => {
this.cleanup(projectPath, {
reason: "process_exit",
verbose: true,
});
};
// Handle various exit scenarios
process.on("exit", cleanup);
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
process.on("SIGUSR1", cleanup);
process.on("SIGUSR2", cleanup);
}
}