Files
CodePilot/src/lib/files.ts
T
7418andClaude Opus 4.6 ff1fb38735 feat: CodePilot - desktop GUI for Claude Code
Full-featured desktop application built with Next.js + Electron:
- Streaming chat with Claude Code via Agent SDK
- Session management with SQLite persistence
- Project file tree and file preview
- MCP server and custom skills management
- Permission controls with Code/Plan/Ask modes
- Dark/light theme, token tracking
- macOS DMG packaging with electron-builder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:04:05 +08:00

174 lines
3.9 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import type { FileTreeNode, FilePreview } from '@/types';
const IGNORED_DIRS = new Set([
'node_modules',
'.git',
'dist',
'.next',
'__pycache__',
'.cache',
'.turbo',
'coverage',
'.output',
'build',
]);
const LANGUAGE_MAP: Record<string, string> = {
ts: 'typescript',
tsx: 'typescript',
js: 'javascript',
jsx: 'javascript',
py: 'python',
rb: 'ruby',
rs: 'rust',
go: 'go',
java: 'java',
kt: 'kotlin',
swift: 'swift',
c: 'c',
cpp: 'cpp',
h: 'c',
hpp: 'cpp',
cs: 'csharp',
css: 'css',
scss: 'scss',
less: 'less',
html: 'html',
xml: 'xml',
json: 'json',
yaml: 'yaml',
yml: 'yaml',
toml: 'toml',
md: 'markdown',
mdx: 'markdown',
sql: 'sql',
sh: 'bash',
bash: 'bash',
zsh: 'bash',
fish: 'fish',
ps1: 'powershell',
dockerfile: 'dockerfile',
graphql: 'graphql',
gql: 'graphql',
vue: 'vue',
svelte: 'svelte',
prisma: 'prisma',
env: 'dotenv',
lua: 'lua',
r: 'r',
php: 'php',
dart: 'dart',
zig: 'zig',
};
export function getFileLanguage(ext: string): string {
const normalized = ext.replace(/^\./, '').toLowerCase();
return LANGUAGE_MAP[normalized] || 'plaintext';
}
export function isPathSafe(basePath: string, targetPath: string): boolean {
const resolvedBase = path.resolve(basePath);
const resolvedTarget = path.resolve(targetPath);
return resolvedTarget.startsWith(resolvedBase + path.sep) || resolvedTarget === resolvedBase;
}
export function scanDirectory(dir: string, depth: number = 3): FileTreeNode[] {
const resolvedDir = path.resolve(dir);
if (!fs.existsSync(resolvedDir)) {
return [];
}
return scanDirectoryRecursive(resolvedDir, depth);
}
function scanDirectoryRecursive(dir: string, depth: number): FileTreeNode[] {
if (depth <= 0) return [];
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return [];
}
const nodes: FileTreeNode[] = [];
// Sort: directories first, then files, both alphabetically
const sorted = entries.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (const entry of sorted) {
// Skip hidden files/dirs (except common config files)
if (entry.name.startsWith('.') && !entry.name.startsWith('.env')) {
continue;
}
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name)) continue;
const children = scanDirectoryRecursive(fullPath, depth - 1);
nodes.push({
name: entry.name,
path: fullPath,
type: 'directory',
children,
});
} else if (entry.isFile()) {
const ext = path.extname(entry.name).replace(/^\./, '');
let size: number | undefined;
try {
const stat = fs.statSync(fullPath);
size = stat.size;
} catch {
// Skip files we can't stat
}
nodes.push({
name: entry.name,
path: fullPath,
type: 'file',
size,
extension: ext || undefined,
});
}
}
return nodes;
}
export function readFilePreview(filePath: string, maxLines: number = 200): FilePreview {
const resolvedPath = path.resolve(filePath);
if (!fs.existsSync(resolvedPath)) {
throw new Error(`File not found: ${filePath}`);
}
const stat = fs.statSync(resolvedPath);
if (!stat.isFile()) {
throw new Error(`Not a file: ${filePath}`);
}
// Read the file content, limiting to maxLines
const content = fs.readFileSync(resolvedPath, 'utf-8');
const lines = content.split('\n');
const truncated = lines.slice(0, maxLines).join('\n');
const ext = path.extname(resolvedPath).replace(/^\./, '');
const language = getFileLanguage(ext);
return {
path: resolvedPath,
content: truncated,
language,
line_count: lines.length,
};
}