Files
claude-mem/src/services/worker/BranchManager.ts
T
Alex Newman ded9671a82 Refactor worker port handling and improve logging
- Replaced hardcoded migration port with dynamic port retrieval using `getWorkerPort()` in worker-cli.ts.
- Updated context generator to clarify error handling comments.
- Introduced timeout constants in ProcessManager for better maintainability.
- Configured SQLite settings using constants for mmap size and cache size in DatabaseManager.
- Added timeout constants for Git and NPM commands in BranchManager.
- Enhanced error logging in FormattingService and SearchManager to provide more context on failures.
- Removed deprecated silentDebug function and replaced its usage with logger.debug.
- Updated tests to use dynamic worker port retrieval instead of hardcoded values.
2025-12-11 14:49:47 -05:00

255 lines
6.4 KiB
TypeScript

/**
* BranchManager: Git branch detection and switching for beta feature toggle
*
* Enables users to switch between stable (main) and beta branches via the UI.
* The installed plugin at ~/.claude/plugins/marketplaces/thedotmack/ is a git repo.
*/
import { execSync } from 'child_process';
import { existsSync, unlinkSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { logger } from '../../utils/logger.js';
const INSTALLED_PLUGIN_PATH = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Timeout constants
const GIT_COMMAND_TIMEOUT_MS = 30_000;
const NPM_INSTALL_TIMEOUT_MS = 120_000;
const DEFAULT_SHELL_TIMEOUT_MS = 60_000;
export interface BranchInfo {
branch: string | null;
isBeta: boolean;
isGitRepo: boolean;
isDirty: boolean;
canSwitch: boolean;
error?: string;
}
export interface SwitchResult {
success: boolean;
branch?: string;
message?: string;
error?: string;
}
/**
* Execute git command in installed plugin directory
*/
function execGit(command: string): string {
return execSync(`git ${command}`, {
cwd: INSTALLED_PLUGIN_PATH,
encoding: 'utf-8',
timeout: GIT_COMMAND_TIMEOUT_MS,
windowsHide: true
}).trim();
}
/**
* Execute shell command in installed plugin directory
*/
function execShell(command: string, timeoutMs: number = DEFAULT_SHELL_TIMEOUT_MS): string {
return execSync(command, {
cwd: INSTALLED_PLUGIN_PATH,
encoding: 'utf-8',
timeout: timeoutMs,
windowsHide: true
}).trim();
}
/**
* Get current branch information
*/
export function getBranchInfo(): BranchInfo {
// Check if git repo exists
const gitDir = join(INSTALLED_PLUGIN_PATH, '.git');
if (!existsSync(gitDir)) {
return {
branch: null,
isBeta: false,
isGitRepo: false,
isDirty: false,
canSwitch: false,
error: 'Installed plugin is not a git repository'
};
}
try {
// Get current branch
const branch = execGit('rev-parse --abbrev-ref HEAD');
// Check if dirty (has uncommitted changes)
const status = execGit('status --porcelain');
const isDirty = status.length > 0;
// Determine if on beta branch
const isBeta = branch.startsWith('beta');
return {
branch,
isBeta,
isGitRepo: true,
isDirty,
canSwitch: true // We can always switch (will discard local changes)
};
} catch (error) {
logger.error('BRANCH', 'Failed to get branch info', {}, error as Error);
return {
branch: null,
isBeta: false,
isGitRepo: true,
isDirty: false,
canSwitch: false,
error: (error as Error).message
};
}
}
/**
* Switch to a different branch
*
* Steps:
* 1. Discard local changes (from rsync syncs)
* 2. Fetch latest from origin
* 3. Checkout target branch
* 4. Pull latest
* 5. Clear install marker and run npm install
* 6. Restart worker (handled by caller after response)
*/
export async function switchBranch(targetBranch: string): Promise<SwitchResult> {
const info = getBranchInfo();
if (!info.isGitRepo) {
return {
success: false,
error: 'Installed plugin is not a git repository. Please reinstall.'
};
}
if (info.branch === targetBranch) {
return {
success: true,
branch: targetBranch,
message: `Already on branch ${targetBranch}`
};
}
try {
logger.info('BRANCH', 'Starting branch switch', {
from: info.branch,
to: targetBranch
});
// 1. Discard local changes (safe - user data is at ~/.claude-mem/)
logger.debug('BRANCH', 'Discarding local changes');
execGit('checkout -- .');
execGit('clean -fd'); // Remove untracked files too
// 2. Fetch latest
logger.debug('BRANCH', 'Fetching from origin');
execGit('fetch origin');
// 3. Checkout target branch
logger.debug('BRANCH', 'Checking out branch', { branch: targetBranch });
try {
execGit(`checkout ${targetBranch}`);
} catch {
// Branch might not exist locally, try tracking remote
execGit(`checkout -b ${targetBranch} origin/${targetBranch}`);
}
// 4. Pull latest
logger.debug('BRANCH', 'Pulling latest');
execGit(`pull origin ${targetBranch}`);
// 5. Clear install marker and run npm install
const installMarker = join(INSTALLED_PLUGIN_PATH, '.install-version');
if (existsSync(installMarker)) {
unlinkSync(installMarker);
}
logger.debug('BRANCH', 'Running npm install');
execShell('npm install', NPM_INSTALL_TIMEOUT_MS);
logger.success('BRANCH', 'Branch switch complete', {
branch: targetBranch
});
return {
success: true,
branch: targetBranch,
message: `Switched to ${targetBranch}. Worker will restart automatically.`
};
} catch (error) {
logger.error('BRANCH', 'Branch switch failed', { targetBranch }, error as Error);
// Try to recover by checking out original branch
try {
if (info.branch) {
execGit(`checkout ${info.branch}`);
}
} catch {
// Recovery failed, user needs manual intervention
}
return {
success: false,
error: `Branch switch failed: ${(error as Error).message}`
};
}
}
/**
* Pull latest updates for current branch
*/
export async function pullUpdates(): Promise<SwitchResult> {
const info = getBranchInfo();
if (!info.isGitRepo || !info.branch) {
return {
success: false,
error: 'Cannot pull updates: not a git repository'
};
}
try {
logger.info('BRANCH', 'Pulling updates', { branch: info.branch });
// Discard local changes first
execGit('checkout -- .');
// Fetch and pull
execGit('fetch origin');
execGit(`pull origin ${info.branch}`);
// Clear install marker and reinstall
const installMarker = join(INSTALLED_PLUGIN_PATH, '.install-version');
if (existsSync(installMarker)) {
unlinkSync(installMarker);
}
execShell('npm install', NPM_INSTALL_TIMEOUT_MS);
logger.success('BRANCH', 'Updates pulled', { branch: info.branch });
return {
success: true,
branch: info.branch,
message: `Updated ${info.branch}. Worker will restart automatically.`
};
} catch (error) {
logger.error('BRANCH', 'Pull failed', {}, error as Error);
return {
success: false,
error: `Pull failed: ${(error as Error).message}`
};
}
}
/**
* Get installed plugin path (for external use)
*/
export function getInstalledPluginPath(): string {
return INSTALLED_PLUGIN_PATH;
}