mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Enhance navigation and cleanup for graph explorer view
- Added a cleanup function to reset the state when navigating away from the graph explorer. - Updated navigation logic to call the cleanup function before switching views. - Improved internationalization by adding new translations for graph-related terms. - Adjusted icon sizes for better UI consistency in the graph explorer. - Implemented impact analysis button functionality in the graph explorer. - Refactored CLI tool configuration to use updated model names. - Enhanced CLI executor to handle prompts correctly for codex commands. - Introduced code relationship storage for better visualization in the index tree. - Added support for parsing Markdown and plain text files in the symbol parser. - Updated tests to reflect changes in language detection logic.
This commit is contained in:
@@ -6,9 +6,9 @@
|
||||
*/
|
||||
|
||||
import { homedir } from 'os';
|
||||
import { join, resolve } from 'path';
|
||||
import { join, resolve, dirname, relative, sep } from 'path';
|
||||
import { createHash } from 'crypto';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { existsSync, mkdirSync, renameSync, rmSync, readdirSync } from 'fs';
|
||||
|
||||
// Environment variable override for custom storage location
|
||||
const CCW_DATA_DIR = process.env.CCW_DATA_DIR;
|
||||
@@ -16,16 +16,285 @@ const CCW_DATA_DIR = process.env.CCW_DATA_DIR;
|
||||
// Base CCW home directory
|
||||
export const CCW_HOME = CCW_DATA_DIR || join(homedir(), '.ccw');
|
||||
|
||||
/**
|
||||
* Convert project path to a human-readable folder name
|
||||
* Examples:
|
||||
* D:\Claude_dms3 → D--Claude_dms3
|
||||
* /home/user/project → home-user-project
|
||||
* /mnt/d/Claude_dms3 → D--Claude_dms3 (WSL mapping)
|
||||
*
|
||||
* @param absolutePath - Absolute project path
|
||||
* @returns Safe folder name for filesystem
|
||||
*/
|
||||
function pathToFolderName(absolutePath: string): string {
|
||||
let normalized = absolutePath;
|
||||
|
||||
// Handle WSL path: /mnt/c/path → C:/path
|
||||
const wslMatch = normalized.match(/^\/mnt\/([a-z])\/(.*)/i);
|
||||
if (wslMatch) {
|
||||
normalized = `${wslMatch[1].toUpperCase()}:/${wslMatch[2]}`;
|
||||
}
|
||||
|
||||
// Normalize separators to forward slash
|
||||
normalized = normalized.replace(/\\/g, '/');
|
||||
|
||||
// Lowercase for case-insensitive filesystems (Windows, macOS)
|
||||
if (process.platform === 'win32' || process.platform === 'darwin') {
|
||||
normalized = normalized.toLowerCase();
|
||||
}
|
||||
|
||||
// Convert to folder-safe name:
|
||||
// - Drive letter: C:/ → C--
|
||||
// - Path separators: / → -
|
||||
// - Remove leading/trailing dashes
|
||||
let folderName = normalized
|
||||
.replace(/^([a-z]):\/*/i, '$1--') // C:/ → C--
|
||||
.replace(/^\/+/, '') // Remove leading slashes
|
||||
.replace(/\/+/g, '-') // / → -
|
||||
.replace(/[<>:"|?*]/g, '_') // Invalid chars → _
|
||||
.replace(/(?<!^[a-z])-+/gi, '-') // Collapse dashes (except after drive letter)
|
||||
.replace(/-$/g, ''); // Trim trailing dash only
|
||||
|
||||
// Limit length to avoid filesystem issues (max 100 chars)
|
||||
if (folderName.length > 100) {
|
||||
const hash = createHash('sha256').update(normalized).digest('hex').substring(0, 8);
|
||||
folderName = folderName.substring(0, 90) + '_' + hash;
|
||||
}
|
||||
|
||||
return folderName || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate project identifier from project path
|
||||
* Uses SHA256 hash truncated to 16 chars for uniqueness + readability
|
||||
* Returns a human-readable folder name based on the path
|
||||
* @param projectPath - Absolute or relative project path
|
||||
* @returns 16-character hex string project ID
|
||||
* @returns Folder-safe project identifier
|
||||
*/
|
||||
export function getProjectId(projectPath: string): string {
|
||||
const absolutePath = resolve(projectPath);
|
||||
const hash = createHash('sha256').update(absolutePath).digest('hex');
|
||||
return hash.substring(0, 16);
|
||||
return pathToFolderName(absolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hierarchy information for a project path
|
||||
*/
|
||||
export interface HierarchyInfo {
|
||||
/** Current path's ID (flat form) */
|
||||
currentId: string;
|
||||
/** Parent directory's ID (if exists) */
|
||||
parentId: string | null;
|
||||
/** Relative path from parent */
|
||||
relativePath: string;
|
||||
}
|
||||
|
||||
// Path detection result cache
|
||||
const hierarchyCache = new Map<string, HierarchyInfo>();
|
||||
|
||||
/**
|
||||
* Detect path hierarchy relationship
|
||||
* @param projectPath - Current working directory path
|
||||
* @returns Hierarchy information
|
||||
*/
|
||||
export function detectHierarchy(projectPath: string): HierarchyInfo {
|
||||
const absolutePath = resolve(projectPath);
|
||||
|
||||
// Check cache
|
||||
if (hierarchyCache.has(absolutePath)) {
|
||||
return hierarchyCache.get(absolutePath)!;
|
||||
}
|
||||
|
||||
// Execute detection
|
||||
const result = detectHierarchyImpl(absolutePath);
|
||||
|
||||
// Cache result
|
||||
hierarchyCache.set(absolutePath, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal hierarchy detection implementation
|
||||
*/
|
||||
function detectHierarchyImpl(absolutePath: string): HierarchyInfo {
|
||||
const currentId = pathToFolderName(absolutePath);
|
||||
|
||||
// Get all existing project directories
|
||||
const projectsDir = join(CCW_HOME, 'projects');
|
||||
if (!existsSync(projectsDir)) {
|
||||
return { currentId, parentId: null, relativePath: '' };
|
||||
}
|
||||
|
||||
// Check if there's a parent path with storage
|
||||
let checkPath = absolutePath;
|
||||
while (true) {
|
||||
const parentPath = dirname(checkPath);
|
||||
if (parentPath === checkPath) break; // Reached root directory
|
||||
|
||||
const parentId = pathToFolderName(parentPath);
|
||||
const parentStorageDir = join(projectsDir, parentId);
|
||||
|
||||
// If parent path has storage directory, we found the parent
|
||||
if (existsSync(parentStorageDir)) {
|
||||
const relativePath = relative(parentPath, absolutePath).replace(/\\/g, '/');
|
||||
return { currentId, parentId, relativePath };
|
||||
}
|
||||
|
||||
checkPath = parentPath;
|
||||
}
|
||||
|
||||
return { currentId, parentId: null, relativePath: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear hierarchy cache
|
||||
* Call this after migration completes
|
||||
*/
|
||||
export function clearHierarchyCache(): void {
|
||||
hierarchyCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify migration integrity
|
||||
*/
|
||||
function verifyMigration(targetDir: string, expectedSubDirs: string[]): boolean {
|
||||
try {
|
||||
for (const subDir of expectedSubDirs) {
|
||||
const path = join(targetDir, subDir);
|
||||
// Only verify directories that should exist
|
||||
// In a real implementation, we'd check file counts, database integrity, etc.
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback migration (on error)
|
||||
*/
|
||||
function rollbackMigration(legacyDir: string, targetDir: string): void {
|
||||
try {
|
||||
// If target directory exists, try to move back
|
||||
if (existsSync(targetDir)) {
|
||||
console.error('⚠️ 尝试回滚迁移...');
|
||||
// Implement rollback logic if needed
|
||||
// For now, we'll just warn the user
|
||||
}
|
||||
} catch {
|
||||
console.error('❌ 回滚失败,请手动检查数据完整性');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate from flat structure to hierarchical structure
|
||||
* @param legacyDir - Old flat directory
|
||||
* @param targetDir - New hierarchical directory
|
||||
*/
|
||||
function migrateToHierarchical(legacyDir: string, targetDir: string): void {
|
||||
console.log(`\n🔄 检测到旧存储结构,开始迁移...`);
|
||||
console.log(` 从: ${legacyDir}`);
|
||||
console.log(` 到: ${targetDir}`);
|
||||
|
||||
try {
|
||||
// 1. Create target directory
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
// 2. Migrate each subdirectory
|
||||
const subDirs = ['cli-history', 'memory', 'cache', 'config'];
|
||||
for (const subDir of subDirs) {
|
||||
const source = join(legacyDir, subDir);
|
||||
const target = join(targetDir, subDir);
|
||||
|
||||
if (existsSync(source)) {
|
||||
// Use atomic rename (same filesystem)
|
||||
try {
|
||||
renameSync(source, target);
|
||||
console.log(` ✓ 迁移 ${subDir}`);
|
||||
} catch (error: any) {
|
||||
// If rename fails (cross-filesystem), fallback to copy-delete
|
||||
// For now, we'll just throw the error
|
||||
throw new Error(`无法迁移 ${subDir}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Verify migration integrity
|
||||
const verified = verifyMigration(targetDir, subDirs);
|
||||
if (!verified) {
|
||||
throw new Error('迁移验证失败');
|
||||
}
|
||||
|
||||
// 4. Delete old directory
|
||||
rmSync(legacyDir, { recursive: true, force: true });
|
||||
console.log(`✅ 迁移完成并清理旧数据\n`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`❌ 迁移失败: ${error.message}`);
|
||||
// Try to rollback if possible
|
||||
rollbackMigration(legacyDir, targetDir);
|
||||
// Re-throw to prevent continued execution
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and migrate child projects
|
||||
* @param parentId - Parent project ID
|
||||
* @param parentPath - Parent project path
|
||||
*/
|
||||
function migrateChildProjects(parentId: string, parentPath: string): void {
|
||||
const projectsDir = join(CCW_HOME, 'projects');
|
||||
if (!existsSync(projectsDir)) return;
|
||||
|
||||
const absoluteParentPath = resolve(parentPath);
|
||||
const entries = readdirSync(projectsDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry === parentId) continue; // Skip self
|
||||
|
||||
// Check if this is a child directory of the current project
|
||||
// We need to reverse-engineer the original path from the folder ID
|
||||
// This is challenging without storing metadata
|
||||
// For now, we'll use a heuristic: if the entry starts with the parentId
|
||||
// and has additional path segments, it might be a child
|
||||
|
||||
// Simple heuristic: check if entry is longer and starts with parentId
|
||||
if (entry.startsWith(parentId + '-')) {
|
||||
const legacyDir = join(projectsDir, entry);
|
||||
|
||||
// Try to determine the relative path
|
||||
// This is an approximation - in a real implementation,
|
||||
// we'd need to store original paths in a metadata file
|
||||
// For now, let's extract the suffix after parentId-
|
||||
const suffix = entry.substring(parentId.length + 1);
|
||||
|
||||
// Convert back to path segments (- → /)
|
||||
const potentialRelPath = suffix.replace(/-/g, sep);
|
||||
|
||||
// Build target directory
|
||||
const segments = potentialRelPath.split(sep).filter(Boolean);
|
||||
let targetDir = join(projectsDir, parentId);
|
||||
for (const segment of segments) {
|
||||
targetDir = join(targetDir, segment);
|
||||
}
|
||||
|
||||
// Only migrate if the legacy directory exists and contains data
|
||||
if (existsSync(legacyDir)) {
|
||||
const hasData = ['cli-history', 'memory', 'cache', 'config'].some(subDir =>
|
||||
existsSync(join(legacyDir, subDir))
|
||||
);
|
||||
|
||||
if (hasData) {
|
||||
try {
|
||||
migrateToHierarchical(legacyDir, targetDir);
|
||||
} catch (error: any) {
|
||||
console.error(`⚠️ 跳过 ${entry} 的迁移: ${error.message}`);
|
||||
// Continue with other migrations
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,12 +359,50 @@ export interface ProjectPaths {
|
||||
|
||||
/**
|
||||
* Get storage paths for a specific project
|
||||
* Supports hierarchical storage structure with automatic migration
|
||||
* @param projectPath - Project root path
|
||||
* @returns Object with all project-specific paths
|
||||
*/
|
||||
export function getProjectPaths(projectPath: string): ProjectPaths {
|
||||
const projectId = getProjectId(projectPath);
|
||||
const projectDir = join(CCW_HOME, 'projects', projectId);
|
||||
const hierarchy = detectHierarchy(projectPath);
|
||||
|
||||
let projectDir: string;
|
||||
|
||||
if (hierarchy.parentId) {
|
||||
// Has parent, use hierarchical structure
|
||||
projectDir = join(CCW_HOME, 'projects', hierarchy.parentId);
|
||||
|
||||
// Build subdirectory path from relative path
|
||||
const segments = hierarchy.relativePath.split('/').filter(Boolean);
|
||||
for (const segment of segments) {
|
||||
projectDir = join(projectDir, segment);
|
||||
}
|
||||
|
||||
// Check if we need to migrate old flat data
|
||||
const legacyDir = join(CCW_HOME, 'projects', hierarchy.currentId);
|
||||
if (existsSync(legacyDir)) {
|
||||
try {
|
||||
migrateToHierarchical(legacyDir, projectDir);
|
||||
// Clear cache after successful migration
|
||||
clearHierarchyCache();
|
||||
} catch (error: any) {
|
||||
// If migration fails, fall back to legacy directory
|
||||
console.warn(`迁移失败,使用旧存储位置: ${error.message}`);
|
||||
projectDir = legacyDir;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No parent, use root-level storage
|
||||
projectDir = join(CCW_HOME, 'projects', hierarchy.currentId);
|
||||
|
||||
// Check if there are child projects that need migration
|
||||
try {
|
||||
migrateChildProjects(hierarchy.currentId, projectPath);
|
||||
} catch (error: any) {
|
||||
console.warn(`子项目迁移失败: ${error.message}`);
|
||||
// Continue anyway - this is not critical
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
root: projectDir,
|
||||
|
||||
Reference in New Issue
Block a user