mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat(dashboard): unify icons with Lucide Icons library
- Introduce Lucide Icons via CDN for consistent SVG icons - Replace emoji icons with Lucide SVG icons in sidebar navigation - Fix Sessions/Explorer icon confusion (📁/📂 → history/folder-tree) - Update top bar icons (logo, theme toggle, search, refresh) - Update stats section icons with colored Lucide icons - Add icon animations support (animate-spin for loading states) - Update Explorer view with Lucide folder/file icons - Support dark/light theme icon adaptation Icon mapping: - Explorer: folder-tree (was 📂) - Sessions: history (was 📁) - Overview: bar-chart-3 - Active: play-circle - Archived: archive - Lite Plan: file-edit - Lite Fix: wrench - MCP Servers: plug - Hooks: webhook
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, promises as fsPromises } from 'fs';
|
||||
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, statSync, promises as fsPromises } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { createHash } from 'crypto';
|
||||
@@ -44,7 +44,8 @@ const MODULE_CSS_FILES = [
|
||||
'05-context.css',
|
||||
'06-cards.css',
|
||||
'07-managers.css',
|
||||
'08-review.css'
|
||||
'08-review.css',
|
||||
'09-explorer.css'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -84,6 +85,7 @@ const MODULE_FILES = [
|
||||
'components/sidebar.js',
|
||||
'components/carousel.js',
|
||||
'components/notifications.js',
|
||||
'components/global-notifications.js',
|
||||
'components/mcp-manager.js',
|
||||
'components/hook-manager.js',
|
||||
'components/_exp_helpers.js',
|
||||
@@ -102,6 +104,7 @@ const MODULE_FILES = [
|
||||
'views/fix-session.js',
|
||||
'views/mcp-manager.js',
|
||||
'views/hook-manager.js',
|
||||
'views/explorer.js',
|
||||
'main.js'
|
||||
];
|
||||
/**
|
||||
@@ -399,6 +402,41 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: List directory files with .gitignore filtering (Explorer view)
|
||||
if (pathname === '/api/files') {
|
||||
const dirPath = url.searchParams.get('path') || initialPath;
|
||||
const filesData = await listDirectoryFiles(dirPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(filesData));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Get file content for preview (Explorer view)
|
||||
if (pathname === '/api/file-content') {
|
||||
const filePath = url.searchParams.get('path');
|
||||
if (!filePath) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'File path is required' }));
|
||||
return;
|
||||
}
|
||||
const fileData = await getFileContent(filePath);
|
||||
res.writeHead(fileData.error ? 404 : 200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(fileData));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Update CLAUDE.md using CLI tools (Explorer view)
|
||||
if (pathname === '/api/update-claude-md' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { path: targetPath, tool = 'gemini', strategy = 'single-layer' } = body;
|
||||
if (!targetPath) {
|
||||
return { error: 'path is required', status: 400 };
|
||||
}
|
||||
return await triggerUpdateClaudeMd(targetPath, tool, strategy);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Serve dashboard HTML
|
||||
if (pathname === '/' || pathname === '/index.html') {
|
||||
const html = generateServerDashboard(initialPath);
|
||||
@@ -1521,3 +1559,305 @@ function deleteHookFromSettings(projectPath, scope, event, hookIndex) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Explorer View Functions
|
||||
// ========================================
|
||||
|
||||
// Directories to always exclude from file tree
|
||||
const EXPLORER_EXCLUDE_DIRS = [
|
||||
'.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env',
|
||||
'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache',
|
||||
'coverage', '.nyc_output', 'logs', 'tmp', 'temp', '.next',
|
||||
'.nuxt', '.output', '.turbo', '.parcel-cache'
|
||||
];
|
||||
|
||||
// File extensions to language mapping for syntax highlighting
|
||||
const EXT_TO_LANGUAGE = {
|
||||
'.js': 'javascript',
|
||||
'.jsx': 'javascript',
|
||||
'.ts': 'typescript',
|
||||
'.tsx': 'typescript',
|
||||
'.py': 'python',
|
||||
'.rb': 'ruby',
|
||||
'.java': 'java',
|
||||
'.go': 'go',
|
||||
'.rs': 'rust',
|
||||
'.c': 'c',
|
||||
'.cpp': 'cpp',
|
||||
'.h': 'c',
|
||||
'.hpp': 'cpp',
|
||||
'.cs': 'csharp',
|
||||
'.php': 'php',
|
||||
'.swift': 'swift',
|
||||
'.kt': 'kotlin',
|
||||
'.scala': 'scala',
|
||||
'.sh': 'bash',
|
||||
'.bash': 'bash',
|
||||
'.zsh': 'bash',
|
||||
'.ps1': 'powershell',
|
||||
'.sql': 'sql',
|
||||
'.html': 'html',
|
||||
'.htm': 'html',
|
||||
'.css': 'css',
|
||||
'.scss': 'scss',
|
||||
'.sass': 'sass',
|
||||
'.less': 'less',
|
||||
'.json': 'json',
|
||||
'.xml': 'xml',
|
||||
'.yaml': 'yaml',
|
||||
'.yml': 'yaml',
|
||||
'.toml': 'toml',
|
||||
'.ini': 'ini',
|
||||
'.cfg': 'ini',
|
||||
'.conf': 'nginx',
|
||||
'.md': 'markdown',
|
||||
'.markdown': 'markdown',
|
||||
'.txt': 'plaintext',
|
||||
'.log': 'plaintext',
|
||||
'.env': 'bash',
|
||||
'.dockerfile': 'dockerfile',
|
||||
'.vue': 'html',
|
||||
'.svelte': 'html'
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse .gitignore file and return patterns
|
||||
* @param {string} gitignorePath - Path to .gitignore file
|
||||
* @returns {string[]} Array of gitignore patterns
|
||||
*/
|
||||
function parseGitignore(gitignorePath) {
|
||||
try {
|
||||
if (!existsSync(gitignorePath)) return [];
|
||||
const content = readFileSync(gitignorePath, 'utf8');
|
||||
return content
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file/directory should be ignored based on gitignore patterns
|
||||
* Simple pattern matching (supports basic glob patterns)
|
||||
* @param {string} name - File or directory name
|
||||
* @param {string[]} patterns - Gitignore patterns
|
||||
* @param {boolean} isDirectory - Whether the entry is a directory
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldIgnore(name, patterns, isDirectory) {
|
||||
// Always exclude certain directories
|
||||
if (isDirectory && EXPLORER_EXCLUDE_DIRS.includes(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip hidden files/directories (starting with .)
|
||||
if (name.startsWith('.') && name !== '.claude' && name !== '.workflow') {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const pattern of patterns) {
|
||||
let p = pattern;
|
||||
|
||||
// Handle negation patterns (we skip them for simplicity)
|
||||
if (p.startsWith('!')) continue;
|
||||
|
||||
// Handle directory-only patterns
|
||||
if (p.endsWith('/')) {
|
||||
if (!isDirectory) continue;
|
||||
p = p.slice(0, -1);
|
||||
}
|
||||
|
||||
// Simple pattern matching
|
||||
if (p === name) return true;
|
||||
|
||||
// Handle wildcard patterns
|
||||
if (p.includes('*')) {
|
||||
const regex = new RegExp('^' + p.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
||||
if (regex.test(name)) return true;
|
||||
}
|
||||
|
||||
// Handle extension patterns like *.log
|
||||
if (p.startsWith('*.')) {
|
||||
const ext = p.slice(1);
|
||||
if (name.endsWith(ext)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* List directory files with .gitignore filtering
|
||||
* @param {string} dirPath - Directory path to list
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function listDirectoryFiles(dirPath) {
|
||||
try {
|
||||
// Normalize path
|
||||
let normalizedPath = dirPath.replace(/\\/g, '/');
|
||||
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
|
||||
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
|
||||
}
|
||||
|
||||
if (!existsSync(normalizedPath)) {
|
||||
return { error: 'Directory not found', files: [] };
|
||||
}
|
||||
|
||||
if (!statSync(normalizedPath).isDirectory()) {
|
||||
return { error: 'Not a directory', files: [] };
|
||||
}
|
||||
|
||||
// Parse .gitignore patterns
|
||||
const gitignorePath = join(normalizedPath, '.gitignore');
|
||||
const gitignorePatterns = parseGitignore(gitignorePath);
|
||||
|
||||
// Read directory entries
|
||||
const entries = readdirSync(normalizedPath, { withFileTypes: true });
|
||||
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const isDirectory = entry.isDirectory();
|
||||
|
||||
// Check if should be ignored
|
||||
if (shouldIgnore(entry.name, gitignorePatterns, isDirectory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryPath = join(normalizedPath, entry.name);
|
||||
const fileInfo = {
|
||||
name: entry.name,
|
||||
type: isDirectory ? 'directory' : 'file',
|
||||
path: entryPath.replace(/\\/g, '/')
|
||||
};
|
||||
|
||||
// Check if directory has CLAUDE.md
|
||||
if (isDirectory) {
|
||||
const claudeMdPath = join(entryPath, 'CLAUDE.md');
|
||||
fileInfo.hasClaudeMd = existsSync(claudeMdPath);
|
||||
}
|
||||
|
||||
files.push(fileInfo);
|
||||
}
|
||||
|
||||
// Sort: directories first, then alphabetically
|
||||
files.sort((a, b) => {
|
||||
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
||||
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return {
|
||||
path: normalizedPath.replace(/\\/g, '/'),
|
||||
files,
|
||||
gitignorePatterns
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error listing directory:', error);
|
||||
return { error: error.message, files: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content for preview
|
||||
* @param {string} filePath - Path to file
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getFileContent(filePath) {
|
||||
try {
|
||||
// Normalize path
|
||||
let normalizedPath = filePath.replace(/\\/g, '/');
|
||||
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
|
||||
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
|
||||
}
|
||||
|
||||
if (!existsSync(normalizedPath)) {
|
||||
return { error: 'File not found' };
|
||||
}
|
||||
|
||||
const stats = statSync(normalizedPath);
|
||||
if (stats.isDirectory()) {
|
||||
return { error: 'Cannot read directory' };
|
||||
}
|
||||
|
||||
// Check file size (limit to 1MB for preview)
|
||||
if (stats.size > 1024 * 1024) {
|
||||
return { error: 'File too large for preview (max 1MB)', size: stats.size };
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const content = readFileSync(normalizedPath, 'utf8');
|
||||
const ext = normalizedPath.substring(normalizedPath.lastIndexOf('.')).toLowerCase();
|
||||
const language = EXT_TO_LANGUAGE[ext] || 'plaintext';
|
||||
const isMarkdown = ext === '.md' || ext === '.markdown';
|
||||
const fileName = normalizedPath.split('/').pop();
|
||||
|
||||
return {
|
||||
content,
|
||||
language,
|
||||
isMarkdown,
|
||||
fileName,
|
||||
path: normalizedPath,
|
||||
size: stats.size,
|
||||
lines: content.split('\n').length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger update-module-claude tool
|
||||
* @param {string} targetPath - Directory path to update
|
||||
* @param {string} tool - CLI tool to use (gemini, qwen, codex)
|
||||
* @param {string} strategy - Update strategy (single-layer, multi-layer)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function triggerUpdateClaudeMd(targetPath, tool, strategy) {
|
||||
const { execSync } = await import('child_process');
|
||||
|
||||
try {
|
||||
// Normalize path
|
||||
let normalizedPath = targetPath.replace(/\\/g, '/');
|
||||
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
|
||||
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
|
||||
}
|
||||
|
||||
if (!existsSync(normalizedPath)) {
|
||||
return { error: 'Directory not found' };
|
||||
}
|
||||
|
||||
if (!statSync(normalizedPath).isDirectory()) {
|
||||
return { error: 'Not a directory' };
|
||||
}
|
||||
|
||||
// Build ccw tool command
|
||||
const ccwBin = join(import.meta.dirname, '../../bin/ccw.js');
|
||||
const command = `node "${ccwBin}" tool update_module_claude --strategy="${strategy}" --path="${normalizedPath}" --tool="${tool}"`;
|
||||
|
||||
console.log(`[Explorer] Running: ${command}`);
|
||||
|
||||
const output = execSync(command, {
|
||||
encoding: 'utf8',
|
||||
timeout: 300000, // 5 minutes
|
||||
cwd: normalizedPath
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `CLAUDE.md updated successfully using ${tool} (${strategy})`,
|
||||
output,
|
||||
path: normalizedPath
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating CLAUDE.md:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
output: error.stdout || error.stderr || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user