mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
fix: Refactor installation process for improved cleanup and backup handling
This commit is contained in:
@@ -5,9 +5,9 @@ import { fileURLToPath } from 'url';
|
|||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { showHeader, createSpinner, info, warning, error, summaryBox, divider } from '../utils/ui.js';
|
import { showHeader, createSpinner, info, warning, error, summaryBox, divider } from '../utils/ui.js';
|
||||||
import { createManifest, addFileEntry, addDirectoryEntry, saveManifest, findManifest, getAllManifests, getFileReferenceCounts } from '../core/manifest.js';
|
import { createManifest, addFileEntry, addDirectoryEntry, saveManifest, findManifest, getAllManifests } from '../core/manifest.js';
|
||||||
import { validatePath } from '../utils/path-resolver.js';
|
import { validatePath } from '../utils/path-resolver.js';
|
||||||
import type { Spinner } from 'ora';
|
import type { Ora } from 'ora';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -125,43 +125,47 @@ export async function installCommand(options: InstallOptions): Promise<void> {
|
|||||||
|
|
||||||
// Check for existing installation at target path
|
// Check for existing installation at target path
|
||||||
const existingManifest = findManifest(installPath, mode);
|
const existingManifest = findManifest(installPath, mode);
|
||||||
let cleanupStats = { removed: 0, skipped: 0 };
|
let cleanStats = { removed: 0, preserved: 0 };
|
||||||
|
|
||||||
|
// Check if any target directories exist (regardless of manifest)
|
||||||
|
const existingDirs = SOURCE_DIRS.filter(dir => existsSync(join(installPath, dir)));
|
||||||
|
const hasExistingFiles = existingDirs.length > 0;
|
||||||
|
|
||||||
|
if (hasExistingFiles) {
|
||||||
|
if (existingManifest) {
|
||||||
|
warning('Existing installation found at this location');
|
||||||
|
} else {
|
||||||
|
warning('Existing configuration directories found (no manifest)');
|
||||||
|
}
|
||||||
|
info(` Found: ${existingDirs.join(', ')}`);
|
||||||
|
|
||||||
if (existingManifest) {
|
|
||||||
warning('Existing installation found at this location');
|
|
||||||
const { backup } = await inquirer.prompt([{
|
const { backup } = await inquirer.prompt([{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'backup',
|
name: 'backup',
|
||||||
message: 'Create backup before reinstalling?',
|
message: 'Create backup before clean install?',
|
||||||
default: true
|
default: true
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
if (backup) {
|
if (backup) {
|
||||||
await createBackup(installPath, existingManifest);
|
await createBackup(installPath, existingManifest || { files: [], directories: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up old files that won't be replaced
|
// Clean install: remove all old files before copying new ones
|
||||||
console.log('');
|
console.log('');
|
||||||
const cleanupSpinner = createSpinner('Analyzing files to clean up...').start();
|
const cleanSpinner = createSpinner('Performing clean install...').start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get list of files that will be installed
|
cleanSpinner.text = 'Removing old files...';
|
||||||
const newFiles = getNewInstallationFiles(sourceDir, installPath, mode);
|
cleanStats = await cleanTargetDirectories(installPath, mode, cleanSpinner);
|
||||||
// Also add version.json which will be created
|
|
||||||
const versionPath = join(installPath, '.claude', 'version.json');
|
|
||||||
newFiles.add(versionPath.toLowerCase().replace(/\\/g, '/'));
|
|
||||||
|
|
||||||
cleanupSpinner.text = 'Cleaning up obsolete files...';
|
if (cleanStats.removed > 0 || cleanStats.preserved > 0) {
|
||||||
cleanupStats = await cleanupOldFiles(existingManifest, newFiles, cleanupSpinner);
|
cleanSpinner.succeed(`Clean install: ${cleanStats.removed} files removed, ${cleanStats.preserved} user files preserved`);
|
||||||
|
|
||||||
if (cleanupStats.removed > 0 || cleanupStats.skipped > 0) {
|
|
||||||
cleanupSpinner.succeed(`Cleanup: ${cleanupStats.removed} files removed, ${cleanupStats.skipped} shared files preserved`);
|
|
||||||
} else {
|
} else {
|
||||||
cleanupSpinner.succeed('No obsolete files to clean up');
|
cleanSpinner.succeed('Clean install: directories prepared');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = err as Error;
|
const errMsg = err as Error;
|
||||||
cleanupSpinner.warn(`Cleanup warning: ${errMsg.message}`);
|
cleanSpinner.warn(`Cleanup warning: ${errMsg.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,11 +247,11 @@ export async function installCommand(options: InstallOptions): Promise<void> {
|
|||||||
chalk.gray(`Directories created: ${totalDirs}`)
|
chalk.gray(`Directories created: ${totalDirs}`)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add cleanup stats if any files were processed
|
// Add clean install stats if any files were processed
|
||||||
if (cleanupStats.removed > 0 || cleanupStats.skipped > 0) {
|
if (cleanStats.removed > 0 || cleanStats.preserved > 0) {
|
||||||
summaryLines.push(chalk.gray(`Obsolete files removed: ${cleanupStats.removed}`));
|
summaryLines.push(chalk.gray(`Old files removed: ${cleanStats.removed}`));
|
||||||
if (cleanupStats.skipped > 0) {
|
if (cleanStats.preserved > 0) {
|
||||||
summaryLines.push(chalk.gray(`Shared files preserved: ${cleanupStats.skipped}`));
|
summaryLines.push(chalk.gray(`User files preserved: ${cleanStats.preserved}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,136 +327,105 @@ async function selectPath(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of files that will be installed from source directories
|
* Clean target directories before installation
|
||||||
* @param sourceDir - Source directory
|
* Removes all files except user-specific settings files
|
||||||
* @param installPath - Installation path
|
* @param installPath - Installation path
|
||||||
* @param mode - Installation mode
|
* @param mode - Installation mode
|
||||||
* @returns Set of normalized file paths that will be installed
|
* @param spinner - Spinner for progress display
|
||||||
|
* @returns Count of removed files and preserved files
|
||||||
*/
|
*/
|
||||||
function getNewInstallationFiles(sourceDir: string, installPath: string, mode: string): Set<string> {
|
async function cleanTargetDirectories(
|
||||||
const newFiles = new Set<string>();
|
installPath: string,
|
||||||
|
mode: string,
|
||||||
|
spinner: Ora
|
||||||
|
): Promise<{ removed: number; preserved: number }> {
|
||||||
|
let removed = 0;
|
||||||
|
let preserved = 0;
|
||||||
const globalPath = homedir();
|
const globalPath = homedir();
|
||||||
|
|
||||||
// For Path mode, also include global subdirectories
|
// For Path mode, also clean global subdirectories
|
||||||
if (mode === 'Path') {
|
if (mode === 'Path') {
|
||||||
for (const subdir of GLOBAL_SUBDIRS) {
|
for (const subdir of GLOBAL_SUBDIRS) {
|
||||||
const srcPath = join(sourceDir, '.claude', subdir);
|
const targetPath = join(globalPath, '.claude', subdir);
|
||||||
if (existsSync(srcPath)) {
|
if (existsSync(targetPath)) {
|
||||||
const destPath = join(globalPath, '.claude', subdir);
|
spinner.text = `Cleaning global ${subdir}...`;
|
||||||
collectFilesRecursive(srcPath, destPath, newFiles);
|
const stats = cleanDirectoryRecursive(targetPath, EXCLUDED_FILES);
|
||||||
|
removed += stats.removed;
|
||||||
|
preserved += stats.preserved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect files from all source directories
|
// Clean all target directories
|
||||||
const availableDirs = SOURCE_DIRS.filter(dir => existsSync(join(sourceDir, dir)));
|
for (const dir of SOURCE_DIRS) {
|
||||||
for (const dir of availableDirs) {
|
const targetPath = join(installPath, dir);
|
||||||
const srcPath = join(sourceDir, dir);
|
if (existsSync(targetPath)) {
|
||||||
const destPath = join(installPath, dir);
|
spinner.text = `Cleaning ${dir}...`;
|
||||||
const excludeDirs = (mode === 'Path' && dir === '.claude') ? GLOBAL_SUBDIRS : [];
|
// For Path mode on .claude, exclude global subdirs (they're handled separately)
|
||||||
collectFilesRecursive(srcPath, destPath, newFiles, excludeDirs);
|
const excludeDirs = (mode === 'Path' && dir === '.claude') ? GLOBAL_SUBDIRS : [];
|
||||||
}
|
const stats = cleanDirectoryRecursive(targetPath, EXCLUDED_FILES, excludeDirs);
|
||||||
|
removed += stats.removed;
|
||||||
return newFiles;
|
preserved += stats.preserved;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively collect file paths from source to destination mapping
|
|
||||||
* @param srcDir - Source directory
|
|
||||||
* @param destDir - Destination directory
|
|
||||||
* @param files - Set to add file paths to
|
|
||||||
* @param excludeDirs - Directories to exclude
|
|
||||||
* @param excludeFiles - Files to exclude
|
|
||||||
*/
|
|
||||||
function collectFilesRecursive(
|
|
||||||
srcDir: string,
|
|
||||||
destDir: string,
|
|
||||||
files: Set<string>,
|
|
||||||
excludeDirs: string[] = [],
|
|
||||||
excludeFiles: string[] = EXCLUDED_FILES
|
|
||||||
): void {
|
|
||||||
if (!existsSync(srcDir)) return;
|
|
||||||
|
|
||||||
const entries = readdirSync(srcDir);
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (excludeDirs.includes(entry)) continue;
|
|
||||||
if (excludeFiles.includes(entry)) continue;
|
|
||||||
|
|
||||||
const srcPath = join(srcDir, entry);
|
|
||||||
const destPath = join(destDir, entry);
|
|
||||||
const stat = statSync(srcPath);
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
collectFilesRecursive(srcPath, destPath, files, [], excludeFiles);
|
|
||||||
} else {
|
|
||||||
files.add(destPath.toLowerCase().replace(/\\/g, '/'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { removed, preserved };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up old installation files that won't be replaced by new installation
|
* Recursively clean a directory, removing all files except excluded ones
|
||||||
* @param existingManifest - Existing manifest with old file list
|
* @param dirPath - Directory to clean
|
||||||
* @param newFiles - Set of file paths that will be installed
|
* @param excludeFiles - Files to preserve
|
||||||
* @param spinner - Spinner for progress display
|
* @param excludeDirs - Directories to skip
|
||||||
* @returns Count of removed files and skipped files
|
* @returns Count of removed and preserved files
|
||||||
*/
|
*/
|
||||||
async function cleanupOldFiles(
|
function cleanDirectoryRecursive(
|
||||||
existingManifest: any,
|
dirPath: string,
|
||||||
newFiles: Set<string>,
|
excludeFiles: string[] = [],
|
||||||
spinner: any
|
excludeDirs: string[] = []
|
||||||
): Promise<{ removed: number; skipped: number }> {
|
): { removed: number; preserved: number } {
|
||||||
let removed = 0;
|
let removed = 0;
|
||||||
let skipped = 0;
|
let preserved = 0;
|
||||||
|
|
||||||
const oldFiles = existingManifest.files || [];
|
if (!existsSync(dirPath)) {
|
||||||
const manifestId = existingManifest.manifest_id;
|
return { removed, preserved };
|
||||||
|
|
||||||
// Get file reference counts from other installations
|
|
||||||
const fileRefs = getFileReferenceCounts(manifestId);
|
|
||||||
|
|
||||||
// Process files in reverse order (deepest first)
|
|
||||||
const sortedFiles = [...oldFiles].sort((a: any, b: any) => b.path.length - a.path.length);
|
|
||||||
|
|
||||||
for (const fileEntry of sortedFiles) {
|
|
||||||
const filePath = fileEntry.path;
|
|
||||||
const normalizedPath = filePath.toLowerCase().replace(/\\/g, '/');
|
|
||||||
|
|
||||||
// Skip if file will be replaced by new installation
|
|
||||||
if (newFiles.has(normalizedPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if file is referenced by other installations
|
|
||||||
const refs = fileRefs.get(normalizedPath) || [];
|
|
||||||
if (refs.length > 0) {
|
|
||||||
skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to remove the file
|
|
||||||
try {
|
|
||||||
if (existsSync(filePath)) {
|
|
||||||
spinner.text = `Cleaning: ${basename(filePath)}`;
|
|
||||||
unlinkSync(filePath);
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors during cleanup
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up empty directories from old installation
|
const entries = readdirSync(dirPath);
|
||||||
const oldDirs = existingManifest.directories || [];
|
|
||||||
const sortedDirs = [...oldDirs].sort((a: any, b: any) => b.path.length - a.path.length);
|
for (const entry of entries) {
|
||||||
|
const entryPath = join(dirPath, entry);
|
||||||
|
|
||||||
|
// Skip excluded directories
|
||||||
|
if (excludeDirs.includes(entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (const dirEntry of sortedDirs) {
|
|
||||||
const dirPath = dirEntry.path;
|
|
||||||
try {
|
try {
|
||||||
if (existsSync(dirPath)) {
|
const stat = statSync(entryPath);
|
||||||
const contents = readdirSync(dirPath);
|
|
||||||
if (contents.length === 0) {
|
if (stat.isDirectory()) {
|
||||||
rmdirSync(dirPath);
|
// Recursively clean subdirectory
|
||||||
|
const stats = cleanDirectoryRecursive(entryPath, excludeFiles, []);
|
||||||
|
removed += stats.removed;
|
||||||
|
preserved += stats.preserved;
|
||||||
|
|
||||||
|
// Remove empty directory
|
||||||
|
try {
|
||||||
|
const contents = readdirSync(entryPath);
|
||||||
|
if (contents.length === 0) {
|
||||||
|
rmdirSync(entryPath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check if file should be preserved
|
||||||
|
if (excludeFiles.includes(entry)) {
|
||||||
|
preserved++;
|
||||||
|
} else {
|
||||||
|
unlinkSync(entryPath);
|
||||||
|
removed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -460,15 +433,15 @@ async function cleanupOldFiles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { removed, skipped };
|
return { removed, preserved };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create backup of existing installation
|
* Create backup of existing installation
|
||||||
* @param {string} installPath - Installation path
|
* @param installPath - Installation path
|
||||||
* @param {Object} manifest - Existing manifest
|
* @param _manifest - Existing manifest (unused, kept for compatibility)
|
||||||
*/
|
*/
|
||||||
async function createBackup(installPath: string, manifest: any): Promise<void> {
|
async function createBackup(installPath: string, _manifest: any): Promise<void> {
|
||||||
const spinner = createSpinner('Creating backup...').start();
|
const spinner = createSpinner('Creating backup...').start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -477,13 +450,24 @@ async function createBackup(installPath: string, manifest: any): Promise<void> {
|
|||||||
|
|
||||||
mkdirSync(backupDir, { recursive: true });
|
mkdirSync(backupDir, { recursive: true });
|
||||||
|
|
||||||
// Copy existing .claude directory
|
// Backup all existing source directories
|
||||||
const claudeDir = join(installPath, '.claude');
|
let backedUp = 0;
|
||||||
if (existsSync(claudeDir)) {
|
for (const dir of SOURCE_DIRS) {
|
||||||
await copyDirectory(claudeDir, join(backupDir, '.claude'));
|
const srcDir = join(installPath, dir);
|
||||||
|
if (existsSync(srcDir)) {
|
||||||
|
spinner.text = `Backing up ${dir}...`;
|
||||||
|
await copyDirectory(srcDir, join(backupDir, dir));
|
||||||
|
backedUp++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
spinner.succeed(`Backup created: ${backupDir}`);
|
if (backedUp > 0) {
|
||||||
|
spinner.succeed(`Backup created: ${backupDir}`);
|
||||||
|
} else {
|
||||||
|
spinner.info('No directories to backup');
|
||||||
|
// Remove empty backup dir
|
||||||
|
try { rmdirSync(backupDir); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = err as Error;
|
const errMsg = err as Error;
|
||||||
spinner.warn(`Backup failed: ${errMsg.message}`);
|
spinner.warn(`Backup failed: ${errMsg.message}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user