import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; // Manifest directory location const MANIFEST_DIR = join(homedir(), '.claude-manifests'); export interface ManifestFileEntry { path: string; type: 'File'; timestamp: string; } export interface ManifestDirectoryEntry { path: string; type: 'Directory'; timestamp: string; } export interface Manifest { manifest_id: string; version: string; installation_mode: string; installation_path: string; installation_date: string; installer_version: string; files: ManifestFileEntry[]; directories: ManifestDirectoryEntry[]; } export interface ManifestWithMetadata extends Manifest { manifest_file: string; application_version: string; files_count: number; directories_count: number; } /** * Ensure manifest directory exists */ function ensureManifestDir(): void { if (!existsSync(MANIFEST_DIR)) { mkdirSync(MANIFEST_DIR, { recursive: true }); } } /** * Create a new installation manifest * @param mode - Installation mode (Global/Path) * @param installPath - Installation path * @returns New manifest object */ export function createManifest(mode: string, installPath: string): Manifest { ensureManifestDir(); const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').split('.')[0]; const modePrefix = mode === 'Global' ? 'manifest-global' : 'manifest-path'; const manifestId = `${modePrefix}-${timestamp}`; return { manifest_id: manifestId, version: '1.0', installation_mode: mode, installation_path: installPath, installation_date: new Date().toISOString(), installer_version: '1.0.0', files: [], directories: [] }; } /** * Add file entry to manifest * @param manifest - Manifest object * @param filePath - File path */ export function addFileEntry(manifest: Manifest, filePath: string): void { manifest.files.push({ path: filePath, type: 'File', timestamp: new Date().toISOString() }); } /** * Add directory entry to manifest * @param manifest - Manifest object * @param dirPath - Directory path */ export function addDirectoryEntry(manifest: Manifest, dirPath: string): void { manifest.directories.push({ path: dirPath, type: 'Directory', timestamp: new Date().toISOString() }); } /** * Save manifest to disk * @param manifest - Manifest object * @returns Path to saved manifest */ export function saveManifest(manifest: Manifest): string { ensureManifestDir(); // Remove old manifests for same path and mode removeOldManifests(manifest.installation_path, manifest.installation_mode); const manifestPath = join(MANIFEST_DIR, `${manifest.manifest_id}.json`); writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8'); return manifestPath; } /** * Remove old manifests for the same installation path and mode * @param installPath - Installation path * @param mode - Installation mode */ function removeOldManifests(installPath: string, mode: string): void { if (!existsSync(MANIFEST_DIR)) return; const normalizedPath = installPath.toLowerCase().replace(/[\\/]+$/, ''); try { const files = readdirSync(MANIFEST_DIR).filter(f => f.endsWith('.json')); for (const file of files) { try { const filePath = join(MANIFEST_DIR, file); const content = JSON.parse(readFileSync(filePath, 'utf8')) as Partial; const manifestPath = (content.installation_path || '').toLowerCase().replace(/[\\/]+$/, ''); const manifestMode = content.installation_mode || 'Global'; if (manifestPath === normalizedPath && manifestMode === mode) { unlinkSync(filePath); } } catch { // Skip invalid manifest files } } } catch { // Ignore errors } } /** * Get all installation manifests * @returns Array of manifest objects */ export function getAllManifests(): ManifestWithMetadata[] { if (!existsSync(MANIFEST_DIR)) return []; const manifests: ManifestWithMetadata[] = []; try { const files = readdirSync(MANIFEST_DIR).filter(f => f.endsWith('.json')); for (const file of files) { try { const filePath = join(MANIFEST_DIR, file); const content = JSON.parse(readFileSync(filePath, 'utf8')) as Manifest; // Try to read version.json for application version let appVersion = 'unknown'; try { const versionPath = join(content.installation_path, '.claude', 'version.json'); if (existsSync(versionPath)) { const versionInfo = JSON.parse(readFileSync(versionPath, 'utf8')) as { version?: string }; appVersion = versionInfo.version || 'unknown'; } } catch { // Ignore } manifests.push({ ...content, manifest_file: filePath, application_version: appVersion, files_count: content.files?.length || 0, directories_count: content.directories?.length || 0 }); } catch { // Skip invalid manifest files } } // Sort by installation date (newest first) manifests.sort((a, b) => new Date(b.installation_date).getTime() - new Date(a.installation_date).getTime()); } catch { // Ignore errors } return manifests; } /** * Find manifest for a specific path and mode * @param installPath - Installation path * @param mode - Installation mode * @returns Manifest or null */ export function findManifest(installPath: string, mode: string): ManifestWithMetadata | null { const manifests = getAllManifests(); const normalizedPath = installPath.toLowerCase().replace(/[\\/]+$/, ''); return manifests.find(m => { const manifestPath = (m.installation_path || '').toLowerCase().replace(/[\\/]+$/, ''); return manifestPath === normalizedPath && m.installation_mode === mode; }) || null; } /** * Delete a manifest file * @param manifestFile - Path to manifest file */ export function deleteManifest(manifestFile: string): void { if (existsSync(manifestFile)) { unlinkSync(manifestFile); } } /** * Get manifest directory path * @returns Manifest directory path */ export function getManifestDir(): string { return MANIFEST_DIR; } /** * Get file reference counts across all manifests * Returns a map of file path -> array of manifest IDs that reference it * @param excludeManifestId - Optional manifest ID to exclude from counting * @returns Map of file paths to referencing manifest IDs */ export function getFileReferenceCounts(excludeManifestId?: string): Map { const fileRefs = new Map(); const manifests = getAllManifests(); for (const manifest of manifests) { // Skip the excluded manifest (usually the one being replaced) if (excludeManifestId && manifest.manifest_id === excludeManifestId) { continue; } for (const fileEntry of manifest.files || []) { const normalizedPath = fileEntry.path.toLowerCase().replace(/\\/g, '/'); const refs = fileRefs.get(normalizedPath) || []; refs.push(manifest.manifest_id); fileRefs.set(normalizedPath, refs); } } return fileRefs; } /** * Check if a file is referenced by other installations * @param filePath - File path to check * @param excludeManifestId - Manifest ID to exclude from checking * @returns True if file is referenced by other installations */ export function isFileReferencedByOthers(filePath: string, excludeManifestId: string): boolean { const fileRefs = getFileReferenceCounts(excludeManifestId); const normalizedPath = filePath.toLowerCase().replace(/\\/g, '/'); const refs = fileRefs.get(normalizedPath) || []; return refs.length > 0; }