diff --git a/src/container.ts b/src/container.ts index 447b17b..4b2fa92 100644 --- a/src/container.ts +++ b/src/container.ts @@ -593,8 +593,11 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\ // Exclude macOS resource fork files and .DS_Store when creating git archive // Also strip extended attributes to prevent macOS xattr issues in Docker const tarFlags = getTarFlags(); + // On macOS, also exclude extended attributes that cause Docker issues + const additionalFlags = (process.platform as string) === "darwin" ? "--no-xattrs --no-fflags" : ""; + const combinedFlags = `${tarFlags} ${additionalFlags}`.trim(); execSync( - `tar -cf "${gitTarFile}" --exclude="._*" --exclude=".DS_Store" ${tarFlags} .git`, + `tar -cf "${gitTarFile}" --exclude="._*" --exclude=".DS_Store" ${combinedFlags} .git`, { cwd: workDir, stdio: "pipe", @@ -785,8 +788,11 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\ const tarFile = `/tmp/claude-dir-${Date.now()}.tar`; const tarFlags = getTarFlags(); + // On macOS, also exclude extended attributes that cause Docker issues + const additionalFlags = (process.platform as string) === "darwin" ? "--no-xattrs --no-fflags" : ""; + const combinedFlags = `${tarFlags} ${additionalFlags}`.trim(); execSync( - `tar -cf "${tarFile}" ${tarFlags} -C "${os.homedir()}" .claude`, + `tar -cf "${tarFile}" ${combinedFlags} -C "${os.homedir()}" .claude`, { stdio: "pipe", }, diff --git a/src/git/shadow-repository.ts b/src/git/shadow-repository.ts index d472131..c2f144b 100644 --- a/src/git/shadow-repository.ts +++ b/src/git/shadow-repository.ts @@ -1,8 +1,8 @@ -import * as path from "path"; -import * as fs from "fs-extra"; -import { exec } from "child_process"; -import { promisify } from "util"; -import chalk from "chalk"; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import chalk from 'chalk'; const execAsync = promisify(exec); @@ -19,19 +19,19 @@ export class ShadowRepository { constructor( private options: ShadowRepoOptions, - private basePath: string = "/tmp/claude-shadows", + private basePath: string = '/tmp/claude-shadows' ) { this.shadowPath = path.join(this.basePath, this.options.sessionId); this.rsyncExcludeFile = path.join( this.basePath, - `${this.options.sessionId}-excludes.txt`, + `${this.options.sessionId}-excludes.txt` ); } async initialize(): Promise { if (this.initialized) return; - console.log(chalk.blue("๐Ÿ”จ Creating shadow repository...")); + console.log(chalk.blue('๐Ÿ”จ Creating shadow repository...')); // Ensure base directory exists await fs.ensureDir(this.basePath); @@ -51,12 +51,12 @@ export class ShadowRepository { try { // First, determine the current branch in the original repo const { stdout: currentBranch } = await execAsync( - "git branch --show-current", + 'git branch --show-current', { - cwd: this.options.originalRepo, - }, + cwd: this.options.originalRepo + } ); - const sourceBranch = currentBranch.trim() || "main"; + const sourceBranch = currentBranch.trim() || 'main'; // Try different clone approaches for robustness let cloneSuccess = false; @@ -68,7 +68,7 @@ export class ShadowRepository { cloneSuccess = true; } catch (cloneError) { console.log( - chalk.yellow(" Standard clone failed, trying alternative..."), + chalk.yellow(' Standard clone failed, trying alternative...') ); // Approach 2: Try without depth limit @@ -78,106 +78,106 @@ export class ShadowRepository { cloneSuccess = true; } catch (cloneError2) { console.log( - chalk.yellow(" Alternative clone failed, trying copy approach..."), + chalk.yellow(' Alternative clone failed, trying copy approach...') ); // Approach 3: Copy working tree and init new repo await fs.ensureDir(this.shadowPath); await execAsync( - `cp -r "${this.options.originalRepo}/." "${this.shadowPath}/"`, + `cp -r "${this.options.originalRepo}/." "${this.shadowPath}/"` ); // Remove and reinit git repo - await fs.remove(path.join(this.shadowPath, ".git")); - await execAsync("git init", { cwd: this.shadowPath }); - await execAsync("git add .", { cwd: this.shadowPath }); + await fs.remove(path.join(this.shadowPath, '.git')); + await execAsync('git init', { cwd: this.shadowPath }); + await execAsync('git add .', { cwd: this.shadowPath }); await execAsync( `git commit -m "Initial commit from ${sourceBranch}"`, - { cwd: this.shadowPath }, + { cwd: this.shadowPath } ); cloneSuccess = true; } } if (!cloneSuccess) { - throw new Error("All clone approaches failed"); + throw new Error('All clone approaches failed'); } // Create the Claude branch locally if it's different from source if (this.options.claudeBranch !== sourceBranch) { await execAsync(`git checkout -b ${this.options.claudeBranch}`, { - cwd: this.shadowPath, + cwd: this.shadowPath }); } // Configure remote to point to the actual GitHub remote, not local repo try { const { stdout: remoteUrl } = await execAsync( - "git remote get-url origin", + 'git remote get-url origin', { - cwd: this.options.originalRepo, - }, + cwd: this.options.originalRepo + } ); const actualRemote = remoteUrl.trim(); if ( actualRemote && - !actualRemote.startsWith("/") && - !actualRemote.startsWith("file://") + !actualRemote.startsWith('/') && + !actualRemote.startsWith('file://') ) { // Set the remote to the actual GitHub/remote URL await execAsync(`git remote set-url origin "${actualRemote}"`, { - cwd: this.shadowPath, + cwd: this.shadowPath }); console.log(chalk.blue(` โœ“ Configured remote: ${actualRemote}`)); } } catch (remoteError) { console.log( - chalk.gray(" (Could not configure remote URL, using local)"), + chalk.gray(' (Could not configure remote URL, using local)') ); } // Create an initial commit if the repo is empty (no HEAD) try { - await execAsync("git rev-parse HEAD", { cwd: this.shadowPath }); + await execAsync('git rev-parse HEAD', { cwd: this.shadowPath }); } catch (noHeadError) { // No HEAD exists, create initial commit - console.log(chalk.blue(" Creating initial commit...")); + console.log(chalk.blue(' Creating initial commit...')); try { - await execAsync("git add .", { cwd: this.shadowPath }); + await execAsync('git add .', { cwd: this.shadowPath }); await execAsync('git commit -m "Initial commit" --allow-empty', { - cwd: this.shadowPath, + cwd: this.shadowPath }); - console.log(chalk.green(" โœ“ Initial commit created")); + console.log(chalk.green(' โœ“ Initial commit created')); } catch (commitError) { // If commit fails, create empty commit await execAsync( 'git commit --allow-empty -m "Initial empty commit"', - { cwd: this.shadowPath }, + { cwd: this.shadowPath } ); - console.log(chalk.green(" โœ“ Empty initial commit created")); + console.log(chalk.green(' โœ“ Empty initial commit created')); } } - console.log(chalk.green("โœ“ Shadow repository created")); + console.log(chalk.green('โœ“ Shadow repository created')); this.initialized = true; // Stage all files after initial setup to track them try { - await execAsync("git add .", { cwd: this.shadowPath }); - console.log(chalk.gray(" Staged all files for tracking")); + await execAsync('git add .', { cwd: this.shadowPath }); + console.log(chalk.gray(' Staged all files for tracking')); // Create initial commit to ensure deletions can be tracked await execAsync( 'git commit -m "Initial snapshot of working directory" --allow-empty', - { cwd: this.shadowPath }, + { cwd: this.shadowPath } ); - console.log(chalk.gray(" Created initial commit for change tracking")); + console.log(chalk.gray(' Created initial commit for change tracking')); } catch (stageError: any) { - console.log(chalk.gray(" Could not stage files:", stageError.message)); + console.log(chalk.gray(' Could not stage files:', stageError.message)); } } catch (error) { - console.error(chalk.red("Failed to create shadow repository:"), error); + console.error(chalk.red('Failed to create shadow repository:'), error); throw error; } } @@ -186,72 +186,72 @@ export class ShadowRepository { try { // Start with built-in excludes that should never be synced const excludes: string[] = [ - ".git", - ".git/**", - "node_modules", - "node_modules/**", - ".next", - ".next/**", - "__pycache__", - "__pycache__/**", - ".venv", - ".venv/**", - "*.pyc", - "*.pyo", - ".DS_Store", - "Thumbs.db", + '.git', + '.git/**', + 'node_modules', + 'node_modules/**', + '.next', + '.next/**', + '__pycache__', + '__pycache__/**', + '.venv', + '.venv/**', + '*.pyc', + '*.pyo', + '.DS_Store', + 'Thumbs.db' ]; // Get list of git-tracked files to ensure they're always included let trackedFiles: string[] = []; try { - const { stdout } = await execAsync("git ls-files", { - cwd: this.options.originalRepo, + const { stdout } = await execAsync('git ls-files', { + cwd: this.options.originalRepo }); trackedFiles = stdout .trim() - .split("\n") + .split('\n') .filter((f) => f.trim()); console.log( - chalk.gray(` Found ${trackedFiles.length} git-tracked files`), + chalk.gray(` Found ${trackedFiles.length} git-tracked files`) ); } catch (error) { console.log( - chalk.yellow(" Warning: Could not get git-tracked files:", error), + chalk.yellow(' Warning: Could not get git-tracked files:', error) ); } // Check for .gitignore in original repo - const gitignorePath = path.join(this.options.originalRepo, ".gitignore"); + const gitignorePath = path.join(this.options.originalRepo, '.gitignore'); if (await fs.pathExists(gitignorePath)) { - const gitignoreContent = await fs.readFile(gitignorePath, "utf-8"); - const lines = gitignoreContent.split("\n"); + const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); + const lines = gitignoreContent.split('\n'); for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comments - if (!trimmed || trimmed.startsWith("#")) continue; + if (!trimmed || trimmed.startsWith('#')) continue; // Convert gitignore patterns to rsync patterns - let pattern = trimmed; + const pattern = trimmed; // Handle negation (gitignore: !pattern, rsync: + pattern) - if (pattern.startsWith("!")) { + if (pattern.startsWith('!')) { // Rsync uses + for inclusion, but we'll skip these for simplicity continue; } // If pattern ends with /, it's a directory - if (pattern.endsWith("/")) { + if (pattern.endsWith('/')) { excludes.push(pattern); - excludes.push(pattern + "**"); + excludes.push(pattern + '**'); } else { // Add the pattern as-is excludes.push(pattern); // If it doesn't contain /, it matches anywhere, so add **/ prefix - if (!pattern.includes("/")) { - excludes.push("**/" + pattern); + if (!pattern.includes('/')) { + excludes.push('**/' + pattern); } } } @@ -263,9 +263,9 @@ export class ShadowRepository { for (const file of trackedFiles) { includes.push(`+ ${file}`); // Also include parent directories - const parts = file.split("/"); + const parts = file.split('/'); for (let i = 1; i < parts.length; i++) { - const dir = parts.slice(0, i).join("/"); + const dir = parts.slice(0, i).join('/'); includes.push(`+ ${dir}/`); } } @@ -276,32 +276,32 @@ export class ShadowRepository { // Write the rsync rules file: includes first, then excludes // Rsync processes rules in order, so includes must come before excludes const allRules = [...uniqueIncludes, ...excludes.map((e) => `- ${e}`)]; - await fs.writeFile(this.rsyncExcludeFile, allRules.join("\n")); + await fs.writeFile(this.rsyncExcludeFile, allRules.join('\n')); console.log( chalk.gray( - ` Created rsync rules file with ${uniqueIncludes.length} includes and ${excludes.length} excludes`, - ), + ` Created rsync rules file with ${uniqueIncludes.length} includes and ${excludes.length} excludes` + ) ); } catch (error) { console.log( - chalk.yellow(" Warning: Could not prepare rsync rules:", error), + chalk.yellow(' Warning: Could not prepare rsync rules:', error) ); // Create a basic exclude file with just the essentials const basicExcludes = [ - "- .git", - "- node_modules", - "- .next", - "- __pycache__", - "- .venv", + '- .git', + '- node_modules', + '- .next', + '- __pycache__', + '- .venv' ]; - await fs.writeFile(this.rsyncExcludeFile, basicExcludes.join("\n")); + await fs.writeFile(this.rsyncExcludeFile, basicExcludes.join('\n')); } } async resetToContainerBranch(containerId: string): Promise { console.log( - chalk.blue("๐Ÿ”„ Resetting shadow repo to match container branch..."), + chalk.blue('๐Ÿ”„ Resetting shadow repo to match container branch...') ); try { @@ -312,34 +312,34 @@ export class ShadowRepository { // Get the current branch from the container const { stdout: containerBranch } = await execAsync( - `docker exec ${containerId} git -C /workspace rev-parse --abbrev-ref HEAD`, + `docker exec ${containerId} git -C /workspace rev-parse --abbrev-ref HEAD` ); const targetBranch = containerBranch.trim(); console.log(chalk.blue(` Container is on branch: ${targetBranch}`)); // Get the current branch in shadow repo (if it has one) - let currentShadowBranch = ""; + let currentShadowBranch = ''; try { const { stdout: shadowBranch } = await execAsync( - "git rev-parse --abbrev-ref HEAD", - { cwd: this.shadowPath }, + 'git rev-parse --abbrev-ref HEAD', + { cwd: this.shadowPath } ); currentShadowBranch = shadowBranch.trim(); console.log(chalk.blue(` Shadow repo is on: ${currentShadowBranch}`)); } catch (error) { - console.log(chalk.blue(` Shadow repo has no HEAD yet`)); + console.log(chalk.blue(' Shadow repo has no HEAD yet')); } if (targetBranch !== currentShadowBranch) { console.log( - chalk.blue(` Resetting shadow repo to match container...`), + chalk.blue(' Resetting shadow repo to match container...') ); // Fetch all branches from the original repo try { - await execAsync("git fetch origin", { cwd: this.shadowPath }); + await execAsync('git fetch origin', { cwd: this.shadowPath }); } catch (error) { - console.warn(chalk.yellow("Warning: Failed to fetch from origin")); + console.warn(chalk.yellow('Warning: Failed to fetch from origin')); } // Check if the target branch exists remotely and create/checkout accordingly @@ -347,70 +347,70 @@ export class ShadowRepository { // Try to checkout the branch if it exists remotely and reset to match it await execAsync( `git checkout -B ${targetBranch} origin/${targetBranch}`, - { cwd: this.shadowPath }, + { cwd: this.shadowPath } ); console.log( chalk.green( - `โœ“ Shadow repo reset to remote branch: ${targetBranch}`, - ), + `โœ“ Shadow repo reset to remote branch: ${targetBranch}` + ) ); } catch (error) { try { // If that fails, try to checkout locally existing branch await execAsync(`git checkout ${targetBranch}`, { - cwd: this.shadowPath, + cwd: this.shadowPath }); console.log( chalk.green( - `โœ“ Shadow repo switched to local branch: ${targetBranch}`, - ), + `โœ“ Shadow repo switched to local branch: ${targetBranch}` + ) ); } catch (localError) { // If that fails too, create a new branch await execAsync(`git checkout -b ${targetBranch}`, { - cwd: this.shadowPath, + cwd: this.shadowPath }); console.log( - chalk.green(`โœ“ Shadow repo created new branch: ${targetBranch}`), + chalk.green(`โœ“ Shadow repo created new branch: ${targetBranch}`) ); } } // Mark that we need to resync after branch reset console.log( - chalk.blue(`โœ“ Branch reset complete - files will be synced next`), + chalk.blue('โœ“ Branch reset complete - files will be synced next') ); } else { console.log( chalk.gray( - ` Shadow repo already on correct branch: ${targetBranch}`, - ), + ` Shadow repo already on correct branch: ${targetBranch}` + ) ); } } catch (error) { console.warn( - chalk.yellow("โš  Failed to reset shadow repo branch:"), - error, + chalk.yellow('โš  Failed to reset shadow repo branch:'), + error ); } } async syncFromContainer( containerId: string, - containerPath: string = "/workspace", + containerPath: string = '/workspace' ): Promise { if (!this.initialized) { await this.initialize(); } - console.log(chalk.blue("๐Ÿ”„ Syncing files from container...")); + console.log(chalk.blue('๐Ÿ”„ Syncing files from container...')); // Prepare rsync rules await this.prepareRsyncRules(); // First, ensure files in container are owned by claude user try { - console.log(chalk.blue(" Fixing file ownership in container...")); + console.log(chalk.blue(' Fixing file ownership in container...')); // Try multiple approaches to fix ownership let ownershipFixed = false; @@ -418,21 +418,21 @@ export class ShadowRepository { // Approach 1: Run as root try { await execAsync( - `docker exec --user root ${containerId} chown -R claude:claude ${containerPath}`, + `docker exec --user root ${containerId} chown -R claude:claude ${containerPath}` ); ownershipFixed = true; } catch (rootError) { // Approach 2: Try without --user root try { await execAsync( - `docker exec ${containerId} chown -R claude:claude ${containerPath}`, + `docker exec ${containerId} chown -R claude:claude ${containerPath}` ); ownershipFixed = true; } catch (normalError) { // Approach 3: Use sudo if available try { await execAsync( - `docker exec ${containerId} sudo chown -R claude:claude ${containerPath}`, + `docker exec ${containerId} sudo chown -R claude:claude ${containerPath}` ); ownershipFixed = true; } catch (sudoError) { @@ -445,32 +445,32 @@ export class ShadowRepository { if (ownershipFixed) { try { const { stdout: verification } = await execAsync( - `docker exec ${containerId} ls -la ${containerPath}/README.md 2>/dev/null || echo "no readme"`, + `docker exec ${containerId} ls -la ${containerPath}/README.md 2>/dev/null || echo "no readme"` ); - if (verification.includes("claude claude")) { - console.log(chalk.green(" โœ“ Container file ownership fixed")); + if (verification.includes('claude claude')) { + console.log(chalk.green(' โœ“ Container file ownership fixed')); } else { console.log( chalk.yellow( - " โš  Ownership fix verification failed, but continuing...", - ), + ' โš  Ownership fix verification failed, but continuing...' + ) ); } } catch (verifyError) { console.log( - chalk.gray(" (Could not verify ownership fix, continuing...)"), + chalk.gray(' (Could not verify ownership fix, continuing...)') ); } } else { console.log( chalk.gray( - " (Could not fix container file ownership, continuing...)", - ), + ' (Could not fix container file ownership, continuing...)' + ) ); } } catch (error) { console.log( - chalk.gray(" (Ownership fix failed, continuing with sync...)"), + chalk.gray(' (Ownership fix failed, continuing with sync...)') ); } @@ -485,12 +485,12 @@ export class ShadowRepository { // Stage all changes including deletions try { - await execAsync("git add -A", { cwd: this.shadowPath }); + await execAsync('git add -A', { cwd: this.shadowPath }); } catch (stageError) { - console.log(chalk.gray(" Could not stage changes:", stageError)); + console.log(chalk.gray(' Could not stage changes:', stageError)); } - console.log(chalk.green("โœ“ Files synced successfully")); + console.log(chalk.green('โœ“ Files synced successfully')); } private async checkRsyncInContainer(containerId: string): Promise { @@ -500,14 +500,14 @@ export class ShadowRepository { } catch { // Try to install rsync if not available try { - console.log(chalk.yellow(" Installing rsync in container...")); + console.log(chalk.yellow(' Installing rsync in container...')); // Try different package managers const installCommands = [ - "apk add --no-cache rsync", // Alpine - "apt-get update && apt-get install -y rsync", // Ubuntu/Debian - "yum install -y rsync", // CentOS/RHEL - "dnf install -y rsync", // Fedora + 'apk add --no-cache rsync', // Alpine + 'apt-get update && apt-get install -y rsync', // Ubuntu/Debian + 'yum install -y rsync', // CentOS/RHEL + 'dnf install -y rsync' // Fedora ]; for (const cmd of installCommands) { @@ -516,7 +516,7 @@ export class ShadowRepository { const execCommands = [ `docker exec --user root ${containerId} sh -c "${cmd}"`, `docker exec ${containerId} sh -c "sudo ${cmd}"`, - `docker exec ${containerId} sh -c "${cmd}"`, + `docker exec ${containerId} sh -c "${cmd}"` ]; for (const execCmd of execCommands) { @@ -524,7 +524,7 @@ export class ShadowRepository { await execAsync(execCmd); // Test if rsync is now available await execAsync(`docker exec ${containerId} which rsync`); - console.log(chalk.green(" โœ“ rsync installed successfully")); + console.log(chalk.green(' โœ“ rsync installed successfully')); return true; } catch (execError) { continue; @@ -537,11 +537,11 @@ export class ShadowRepository { } console.log( - chalk.gray(" (Could not install rsync with any package manager)"), + chalk.gray(' (Could not install rsync with any package manager)') ); return false; } catch (installError) { - console.log(chalk.gray(" (Could not install rsync, using docker cp)")); + console.log(chalk.gray(' (Could not install rsync, using docker cp)')); return false; } } @@ -549,22 +549,22 @@ export class ShadowRepository { private async syncWithRsync( containerId: string, - containerPath: string, + containerPath: string ): Promise { // Create a temporary directory in container for rsync - const tempDir = "/tmp/sync-staging"; + const tempDir = '/tmp/sync-staging'; await execAsync(`docker exec ${containerId} mkdir -p ${tempDir}`); // Copy exclude file to container - const containerExcludeFile = "/tmp/rsync-excludes.txt"; + const containerExcludeFile = '/tmp/rsync-excludes.txt'; await execAsync( - `docker cp ${this.rsyncExcludeFile} ${containerId}:${containerExcludeFile}`, + `docker cp ${this.rsyncExcludeFile} ${containerId}:${containerExcludeFile}` ); // Rsync directly from container to shadow repo with proper deletion handling // First, clear the shadow repo (except .git) to ensure deletions are reflected await execAsync( - `find ${this.shadowPath} -mindepth 1 -not -path '${this.shadowPath}/.git*' -delete`, + `find ${this.shadowPath} -mindepth 1 -not -path '${this.shadowPath}/.git*' -delete` ); // Rsync within container to staging area using exclude file @@ -576,14 +576,14 @@ export class ShadowRepository { // Copy from container staging to shadow repo await execAsync( - `docker cp ${containerId}:${tempDir}/. ${this.shadowPath}/`, + `docker cp ${containerId}:${tempDir}/. ${this.shadowPath}/` ); // Clean up staging directory and exclude file try { await execAsync(`docker exec ${containerId} rm -rf ${tempDir}`); await execAsync( - `docker exec --user root ${containerId} rm -f ${containerExcludeFile}`, + `docker exec --user root ${containerId} rm -f ${containerExcludeFile}` ); } catch (cleanupError) { // Ignore cleanup errors @@ -592,14 +592,14 @@ export class ShadowRepository { private async syncWithDockerCp( containerId: string, - containerPath: string, + containerPath: string ): Promise { console.log( - chalk.yellow("โš ๏ธ Using docker cp (rsync not available in container)"), + chalk.yellow('โš ๏ธ Using docker cp (rsync not available in container)') ); // Create a temp directory for staging the copy - const tempCopyPath = path.join(this.basePath, "temp-copy"); + const tempCopyPath = path.join(this.basePath, 'temp-copy'); try { // Remove temp directory if it exists @@ -612,7 +612,7 @@ export class ShadowRepository { // Copy files to temp directory first (to avoid corrupting shadow repo) await execAsync( - `docker cp ${containerId}:${containerPath}/. ${tempCopyPath}/`, + `docker cp ${containerId}:${containerPath}/. ${tempCopyPath}/` ); // Now selectively copy files to shadow repo, using exclude file @@ -620,27 +620,27 @@ export class ShadowRepository { // Use rsync on host to copy files using exclude file try { await execAsync( - `rsync -av --exclude-from=${this.rsyncExcludeFile} ${tempCopyPath}/ ${this.shadowPath}/`, + `rsync -av --exclude-from=${this.rsyncExcludeFile} ${tempCopyPath}/ ${this.shadowPath}/` ); } catch (rsyncError) { // Fallback to cp if rsync not available on host - console.log(chalk.gray(" (rsync not available on host, using cp)")); + console.log(chalk.gray(' (rsync not available on host, using cp)')); // Manual copy excluding directories - read exclude patterns const excludeContent = await fs.readFile( this.rsyncExcludeFile, - "utf-8", + 'utf-8' ); const excludePatterns = excludeContent - .split("\n") + .split('\n') .filter((p) => p.trim()); const { stdout: fileList } = await execAsync( - `find ${tempCopyPath} -type f`, + `find ${tempCopyPath} -type f` ); const files = fileList .trim() - .split("\n") + .split('\n') .filter((f) => f.trim()); for (const file of files) { @@ -652,13 +652,13 @@ export class ShadowRepository { if (!pattern) continue; // Simple pattern matching (not full glob) - if (pattern.includes("**")) { - const basePattern = pattern.replace("**/", "").replace("/**", ""); + if (pattern.includes('**')) { + const basePattern = pattern.replace('**/', '').replace('/**', ''); if (relativePath.includes(basePattern)) { shouldExclude = true; break; } - } else if (pattern.endsWith("*")) { + } else if (pattern.endsWith('*')) { const prefix = pattern.slice(0, -1); if ( relativePath.startsWith(prefix) || @@ -670,7 +670,7 @@ export class ShadowRepository { } else { if ( relativePath === pattern || - relativePath.startsWith(pattern + "/") || + relativePath.startsWith(pattern + '/') || path.basename(relativePath) === pattern ) { shouldExclude = true; @@ -694,13 +694,13 @@ export class ShadowRepository { // Fix ownership of copied files try { const currentUser = - process.env.USER || process.env.USERNAME || "claude"; + process.env.USER || process.env.USERNAME || 'claude'; await execAsync( - `chown -R ${currentUser}:${currentUser} ${this.shadowPath}`, + `chown -R ${currentUser}:${currentUser} ${this.shadowPath}` ); } catch (error) { console.log( - chalk.gray(" (Could not fix file ownership, continuing...)"), + chalk.gray(' (Could not fix file ownership, continuing...)') ); } } finally { @@ -712,23 +712,23 @@ export class ShadowRepository { } async getChanges(): Promise<{ hasChanges: boolean; summary: string }> { - const { stdout: status } = await execAsync("git status --porcelain", { - cwd: this.shadowPath, + const { stdout: status } = await execAsync('git status --porcelain', { + cwd: this.shadowPath }); if (!status.trim()) { - return { hasChanges: false, summary: "No changes detected" }; + return { hasChanges: false, summary: 'No changes detected' }; } - const lines = status.trim().split("\n"); + const lines = status.trim().split('\n'); const modified = lines.filter( - (l) => l.startsWith(" M") || l.startsWith("M ") || l.startsWith("MM"), + (l) => l.startsWith(' M') || l.startsWith('M ') || l.startsWith('MM') ).length; const added = lines.filter( - (l) => l.startsWith("??") || l.startsWith("A ") || l.startsWith("AM"), + (l) => l.startsWith('??') || l.startsWith('A ') || l.startsWith('AM') ).length; const deleted = lines.filter( - (l) => l.startsWith(" D") || l.startsWith("D "), + (l) => l.startsWith(' D') || l.startsWith('D ') ).length; const summary = `Modified: ${modified}, Added: ${added}, Deleted: ${deleted}`; @@ -737,7 +737,7 @@ export class ShadowRepository { } async showDiff(): Promise { - const { stdout } = await execAsync("git diff", { cwd: this.shadowPath }); + const { stdout } = await execAsync('git diff', { cwd: this.shadowPath }); console.log(stdout); } @@ -746,21 +746,21 @@ export class ShadowRepository { try { // Try to force remove with rm -rf first await execAsync(`rm -rf "${this.shadowPath}"`); - console.log(chalk.gray("๐Ÿงน Shadow repository cleaned up")); + console.log(chalk.gray('๐Ÿงน Shadow repository cleaned up')); } catch (error) { // Fallback to fs.remove with retry logic let retries = 3; while (retries > 0) { try { await fs.remove(this.shadowPath); - console.log(chalk.gray("๐Ÿงน Shadow repository cleaned up")); + console.log(chalk.gray('๐Ÿงน Shadow repository cleaned up')); break; } catch (err) { retries--; if (retries === 0) { console.error( - chalk.yellow("โš  Failed to cleanup shadow repository:"), - err, + chalk.yellow('โš  Failed to cleanup shadow repository:'), + err ); } else { // Wait a bit before retry