11import http from 'http' ;
22import { URL } from 'url' ;
3- import { readFileSync , writeFileSync , existsSync , readdirSync , mkdirSync , promises as fsPromises } from 'fs' ;
3+ import { readFileSync , writeFileSync , existsSync , readdirSync , mkdirSync , statSync , promises as fsPromises } from 'fs' ;
44import { join , dirname } from 'path' ;
55import { homedir } from 'os' ;
66import { createHash } from 'crypto' ;
@@ -44,7 +44,8 @@ const MODULE_CSS_FILES = [
4444 '05-context.css' ,
4545 '06-cards.css' ,
4646 '07-managers.css' ,
47- '08-review.css'
47+ '08-review.css' ,
48+ '09-explorer.css'
4849] ;
4950
5051/**
@@ -84,6 +85,7 @@ const MODULE_FILES = [
8485 'components/sidebar.js' ,
8586 'components/carousel.js' ,
8687 'components/notifications.js' ,
88+ 'components/global-notifications.js' ,
8789 'components/mcp-manager.js' ,
8890 'components/hook-manager.js' ,
8991 'components/_exp_helpers.js' ,
@@ -102,6 +104,7 @@ const MODULE_FILES = [
102104 'views/fix-session.js' ,
103105 'views/mcp-manager.js' ,
104106 'views/hook-manager.js' ,
107+ 'views/explorer.js' ,
105108 'main.js'
106109] ;
107110/**
@@ -399,6 +402,41 @@ export async function startServer(options = {}) {
399402 return ;
400403 }
401404
405+ // API: List directory files with .gitignore filtering (Explorer view)
406+ if ( pathname === '/api/files' ) {
407+ const dirPath = url . searchParams . get ( 'path' ) || initialPath ;
408+ const filesData = await listDirectoryFiles ( dirPath ) ;
409+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
410+ res . end ( JSON . stringify ( filesData ) ) ;
411+ return ;
412+ }
413+
414+ // API: Get file content for preview (Explorer view)
415+ if ( pathname === '/api/file-content' ) {
416+ const filePath = url . searchParams . get ( 'path' ) ;
417+ if ( ! filePath ) {
418+ res . writeHead ( 400 , { 'Content-Type' : 'application/json' } ) ;
419+ res . end ( JSON . stringify ( { error : 'File path is required' } ) ) ;
420+ return ;
421+ }
422+ const fileData = await getFileContent ( filePath ) ;
423+ res . writeHead ( fileData . error ? 404 : 200 , { 'Content-Type' : 'application/json' } ) ;
424+ res . end ( JSON . stringify ( fileData ) ) ;
425+ return ;
426+ }
427+
428+ // API: Update CLAUDE.md using CLI tools (Explorer view)
429+ if ( pathname === '/api/update-claude-md' && req . method === 'POST' ) {
430+ handlePostRequest ( req , res , async ( body ) => {
431+ const { path : targetPath , tool = 'gemini' , strategy = 'single-layer' } = body ;
432+ if ( ! targetPath ) {
433+ return { error : 'path is required' , status : 400 } ;
434+ }
435+ return await triggerUpdateClaudeMd ( targetPath , tool , strategy ) ;
436+ } ) ;
437+ return ;
438+ }
439+
402440 // Serve dashboard HTML
403441 if ( pathname === '/' || pathname === '/index.html' ) {
404442 const html = generateServerDashboard ( initialPath ) ;
@@ -1521,3 +1559,305 @@ function deleteHookFromSettings(projectPath, scope, event, hookIndex) {
15211559 return { error : error . message } ;
15221560 }
15231561}
1562+
1563+ // ========================================
1564+ // Explorer View Functions
1565+ // ========================================
1566+
1567+ // Directories to always exclude from file tree
1568+ const EXPLORER_EXCLUDE_DIRS = [
1569+ '.git' , '__pycache__' , 'node_modules' , '.venv' , 'venv' , 'env' ,
1570+ 'dist' , 'build' , '.cache' , '.pytest_cache' , '.mypy_cache' ,
1571+ 'coverage' , '.nyc_output' , 'logs' , 'tmp' , 'temp' , '.next' ,
1572+ '.nuxt' , '.output' , '.turbo' , '.parcel-cache'
1573+ ] ;
1574+
1575+ // File extensions to language mapping for syntax highlighting
1576+ const EXT_TO_LANGUAGE = {
1577+ '.js' : 'javascript' ,
1578+ '.jsx' : 'javascript' ,
1579+ '.ts' : 'typescript' ,
1580+ '.tsx' : 'typescript' ,
1581+ '.py' : 'python' ,
1582+ '.rb' : 'ruby' ,
1583+ '.java' : 'java' ,
1584+ '.go' : 'go' ,
1585+ '.rs' : 'rust' ,
1586+ '.c' : 'c' ,
1587+ '.cpp' : 'cpp' ,
1588+ '.h' : 'c' ,
1589+ '.hpp' : 'cpp' ,
1590+ '.cs' : 'csharp' ,
1591+ '.php' : 'php' ,
1592+ '.swift' : 'swift' ,
1593+ '.kt' : 'kotlin' ,
1594+ '.scala' : 'scala' ,
1595+ '.sh' : 'bash' ,
1596+ '.bash' : 'bash' ,
1597+ '.zsh' : 'bash' ,
1598+ '.ps1' : 'powershell' ,
1599+ '.sql' : 'sql' ,
1600+ '.html' : 'html' ,
1601+ '.htm' : 'html' ,
1602+ '.css' : 'css' ,
1603+ '.scss' : 'scss' ,
1604+ '.sass' : 'sass' ,
1605+ '.less' : 'less' ,
1606+ '.json' : 'json' ,
1607+ '.xml' : 'xml' ,
1608+ '.yaml' : 'yaml' ,
1609+ '.yml' : 'yaml' ,
1610+ '.toml' : 'toml' ,
1611+ '.ini' : 'ini' ,
1612+ '.cfg' : 'ini' ,
1613+ '.conf' : 'nginx' ,
1614+ '.md' : 'markdown' ,
1615+ '.markdown' : 'markdown' ,
1616+ '.txt' : 'plaintext' ,
1617+ '.log' : 'plaintext' ,
1618+ '.env' : 'bash' ,
1619+ '.dockerfile' : 'dockerfile' ,
1620+ '.vue' : 'html' ,
1621+ '.svelte' : 'html'
1622+ } ;
1623+
1624+ /**
1625+ * Parse .gitignore file and return patterns
1626+ * @param {string } gitignorePath - Path to .gitignore file
1627+ * @returns {string[] } Array of gitignore patterns
1628+ */
1629+ function parseGitignore ( gitignorePath ) {
1630+ try {
1631+ if ( ! existsSync ( gitignorePath ) ) return [ ] ;
1632+ const content = readFileSync ( gitignorePath , 'utf8' ) ;
1633+ return content
1634+ . split ( '\n' )
1635+ . map ( line => line . trim ( ) )
1636+ . filter ( line => line && ! line . startsWith ( '#' ) ) ;
1637+ } catch {
1638+ return [ ] ;
1639+ }
1640+ }
1641+
1642+ /**
1643+ * Check if a file/directory should be ignored based on gitignore patterns
1644+ * Simple pattern matching (supports basic glob patterns)
1645+ * @param {string } name - File or directory name
1646+ * @param {string[] } patterns - Gitignore patterns
1647+ * @param {boolean } isDirectory - Whether the entry is a directory
1648+ * @returns {boolean }
1649+ */
1650+ function shouldIgnore ( name , patterns , isDirectory ) {
1651+ // Always exclude certain directories
1652+ if ( isDirectory && EXPLORER_EXCLUDE_DIRS . includes ( name ) ) {
1653+ return true ;
1654+ }
1655+
1656+ // Skip hidden files/directories (starting with .)
1657+ if ( name . startsWith ( '.' ) && name !== '.claude' && name !== '.workflow' ) {
1658+ return true ;
1659+ }
1660+
1661+ for ( const pattern of patterns ) {
1662+ let p = pattern ;
1663+
1664+ // Handle negation patterns (we skip them for simplicity)
1665+ if ( p . startsWith ( '!' ) ) continue ;
1666+
1667+ // Handle directory-only patterns
1668+ if ( p . endsWith ( '/' ) ) {
1669+ if ( ! isDirectory ) continue ;
1670+ p = p . slice ( 0 , - 1 ) ;
1671+ }
1672+
1673+ // Simple pattern matching
1674+ if ( p === name ) return true ;
1675+
1676+ // Handle wildcard patterns
1677+ if ( p . includes ( '*' ) ) {
1678+ const regex = new RegExp ( '^' + p . replace ( / \. / g, '\\.' ) . replace ( / \* / g, '.*' ) + '$' ) ;
1679+ if ( regex . test ( name ) ) return true ;
1680+ }
1681+
1682+ // Handle extension patterns like *.log
1683+ if ( p . startsWith ( '*.' ) ) {
1684+ const ext = p . slice ( 1 ) ;
1685+ if ( name . endsWith ( ext ) ) return true ;
1686+ }
1687+ }
1688+
1689+ return false ;
1690+ }
1691+
1692+ /**
1693+ * List directory files with .gitignore filtering
1694+ * @param {string } dirPath - Directory path to list
1695+ * @returns {Promise<Object> }
1696+ */
1697+ async function listDirectoryFiles ( dirPath ) {
1698+ try {
1699+ // Normalize path
1700+ let normalizedPath = dirPath . replace ( / \\ / g, '/' ) ;
1701+ if ( normalizedPath . match ( / ^ \/ [ a - z A - Z ] \/ / ) ) {
1702+ normalizedPath = normalizedPath . charAt ( 1 ) . toUpperCase ( ) + ':' + normalizedPath . slice ( 2 ) ;
1703+ }
1704+
1705+ if ( ! existsSync ( normalizedPath ) ) {
1706+ return { error : 'Directory not found' , files : [ ] } ;
1707+ }
1708+
1709+ if ( ! statSync ( normalizedPath ) . isDirectory ( ) ) {
1710+ return { error : 'Not a directory' , files : [ ] } ;
1711+ }
1712+
1713+ // Parse .gitignore patterns
1714+ const gitignorePath = join ( normalizedPath , '.gitignore' ) ;
1715+ const gitignorePatterns = parseGitignore ( gitignorePath ) ;
1716+
1717+ // Read directory entries
1718+ const entries = readdirSync ( normalizedPath , { withFileTypes : true } ) ;
1719+
1720+ const files = [ ] ;
1721+ for ( const entry of entries ) {
1722+ const isDirectory = entry . isDirectory ( ) ;
1723+
1724+ // Check if should be ignored
1725+ if ( shouldIgnore ( entry . name , gitignorePatterns , isDirectory ) ) {
1726+ continue ;
1727+ }
1728+
1729+ const entryPath = join ( normalizedPath , entry . name ) ;
1730+ const fileInfo = {
1731+ name : entry . name ,
1732+ type : isDirectory ? 'directory' : 'file' ,
1733+ path : entryPath . replace ( / \\ / g, '/' )
1734+ } ;
1735+
1736+ // Check if directory has CLAUDE.md
1737+ if ( isDirectory ) {
1738+ const claudeMdPath = join ( entryPath , 'CLAUDE.md' ) ;
1739+ fileInfo . hasClaudeMd = existsSync ( claudeMdPath ) ;
1740+ }
1741+
1742+ files . push ( fileInfo ) ;
1743+ }
1744+
1745+ // Sort: directories first, then alphabetically
1746+ files . sort ( ( a , b ) => {
1747+ if ( a . type === 'directory' && b . type !== 'directory' ) return - 1 ;
1748+ if ( a . type !== 'directory' && b . type === 'directory' ) return 1 ;
1749+ return a . name . localeCompare ( b . name ) ;
1750+ } ) ;
1751+
1752+ return {
1753+ path : normalizedPath . replace ( / \\ / g, '/' ) ,
1754+ files,
1755+ gitignorePatterns
1756+ } ;
1757+ } catch ( error ) {
1758+ console . error ( 'Error listing directory:' , error ) ;
1759+ return { error : error . message , files : [ ] } ;
1760+ }
1761+ }
1762+
1763+ /**
1764+ * Get file content for preview
1765+ * @param {string } filePath - Path to file
1766+ * @returns {Promise<Object> }
1767+ */
1768+ async function getFileContent ( filePath ) {
1769+ try {
1770+ // Normalize path
1771+ let normalizedPath = filePath . replace ( / \\ / g, '/' ) ;
1772+ if ( normalizedPath . match ( / ^ \/ [ a - z A - Z ] \/ / ) ) {
1773+ normalizedPath = normalizedPath . charAt ( 1 ) . toUpperCase ( ) + ':' + normalizedPath . slice ( 2 ) ;
1774+ }
1775+
1776+ if ( ! existsSync ( normalizedPath ) ) {
1777+ return { error : 'File not found' } ;
1778+ }
1779+
1780+ const stats = statSync ( normalizedPath ) ;
1781+ if ( stats . isDirectory ( ) ) {
1782+ return { error : 'Cannot read directory' } ;
1783+ }
1784+
1785+ // Check file size (limit to 1MB for preview)
1786+ if ( stats . size > 1024 * 1024 ) {
1787+ return { error : 'File too large for preview (max 1MB)' , size : stats . size } ;
1788+ }
1789+
1790+ // Read file content
1791+ const content = readFileSync ( normalizedPath , 'utf8' ) ;
1792+ const ext = normalizedPath . substring ( normalizedPath . lastIndexOf ( '.' ) ) . toLowerCase ( ) ;
1793+ const language = EXT_TO_LANGUAGE [ ext ] || 'plaintext' ;
1794+ const isMarkdown = ext === '.md' || ext === '.markdown' ;
1795+ const fileName = normalizedPath . split ( '/' ) . pop ( ) ;
1796+
1797+ return {
1798+ content,
1799+ language,
1800+ isMarkdown,
1801+ fileName,
1802+ path : normalizedPath ,
1803+ size : stats . size ,
1804+ lines : content . split ( '\n' ) . length
1805+ } ;
1806+ } catch ( error ) {
1807+ console . error ( 'Error reading file:' , error ) ;
1808+ return { error : error . message } ;
1809+ }
1810+ }
1811+
1812+ /**
1813+ * Trigger update-module-claude tool
1814+ * @param {string } targetPath - Directory path to update
1815+ * @param {string } tool - CLI tool to use (gemini, qwen, codex)
1816+ * @param {string } strategy - Update strategy (single-layer, multi-layer)
1817+ * @returns {Promise<Object> }
1818+ */
1819+ async function triggerUpdateClaudeMd ( targetPath , tool , strategy ) {
1820+ const { execSync } = await import ( 'child_process' ) ;
1821+
1822+ try {
1823+ // Normalize path
1824+ let normalizedPath = targetPath . replace ( / \\ / g, '/' ) ;
1825+ if ( normalizedPath . match ( / ^ \/ [ a - z A - Z ] \/ / ) ) {
1826+ normalizedPath = normalizedPath . charAt ( 1 ) . toUpperCase ( ) + ':' + normalizedPath . slice ( 2 ) ;
1827+ }
1828+
1829+ if ( ! existsSync ( normalizedPath ) ) {
1830+ return { error : 'Directory not found' } ;
1831+ }
1832+
1833+ if ( ! statSync ( normalizedPath ) . isDirectory ( ) ) {
1834+ return { error : 'Not a directory' } ;
1835+ }
1836+
1837+ // Build ccw tool command
1838+ const ccwBin = join ( import . meta. dirname , '../../bin/ccw.js' ) ;
1839+ const command = `node "${ ccwBin } " tool update_module_claude --strategy="${ strategy } " --path="${ normalizedPath } " --tool="${ tool } "` ;
1840+
1841+ console . log ( `[Explorer] Running: ${ command } ` ) ;
1842+
1843+ const output = execSync ( command , {
1844+ encoding : 'utf8' ,
1845+ timeout : 300000 , // 5 minutes
1846+ cwd : normalizedPath
1847+ } ) ;
1848+
1849+ return {
1850+ success : true ,
1851+ message : `CLAUDE.md updated successfully using ${ tool } (${ strategy } )` ,
1852+ output,
1853+ path : normalizedPath
1854+ } ;
1855+ } catch ( error ) {
1856+ console . error ( 'Error updating CLAUDE.md:' , error ) ;
1857+ return {
1858+ success : false ,
1859+ error : error . message ,
1860+ output : error . stdout || error . stderr || ''
1861+ } ;
1862+ }
1863+ }
0 commit comments