init helder

This commit is contained in:
2026-06-15 22:33:46 +02:00
parent 3d77fdfeff
commit 3f5078841d
38 changed files with 6349 additions and 2083 deletions

117
src/main/fs-service.ts Normal file
View File

@@ -0,0 +1,117 @@
import { readdir, readFile, stat, writeFile } from 'node:fs/promises'
import { join, relative, sep } from 'node:path'
export interface FileNode {
name: string
type: 'dir' | 'file'
path: string
open?: boolean
children?: FileNode[]
}
/** Directories never walked — noise or huge, and not part of "the project". */
const IGNORE_DIRS = new Set([
'node_modules', '.git', 'out', 'dist', 'build', '.next', '.nuxt',
'.cache', 'coverage', 'vendor', '.idea', '.vscode', '.helder', '.turbo',
])
const MAX_FILE_BYTES = 300_000
const MAX_INDEXED_FILES = 6000
function ignored(name: string): boolean {
return IGNORE_DIRS.has(name) || name === '.DS_Store'
}
/** Recursive project tree, dirs first then files, alphabetical. */
export async function readTree(root: string): Promise<FileNode> {
const name = root.split(sep).filter(Boolean).pop() || root
return { name, type: 'dir', path: '', open: true, children: await readDir(root, root, 0) }
}
async function readDir(abs: string, root: string, depth: number): Promise<FileNode[]> {
let entries: import('node:fs').Dirent[]
try {
entries = await readdir(abs, { withFileTypes: true })
} catch {
return []
}
const dirs: FileNode[] = []
const files: FileNode[] = []
for (const e of entries) {
if (ignored(e.name)) continue
const childAbs = join(abs, e.name)
const rel = relative(root, childAbs).split(sep).join('/')
if (e.isDirectory()) {
dirs.push({
name: e.name, type: 'dir', path: rel, open: depth < 1,
children: depth < 12 ? await readDir(childAbs, root, depth + 1) : [],
})
} else if (e.isFile()) {
files.push({ name: e.name, type: 'file', path: rel })
}
}
dirs.sort((a, b) => a.name.localeCompare(b.name))
files.sort((a, b) => a.name.localeCompare(b.name))
return [...dirs, ...files]
}
function looksBinary(buf: Buffer): boolean {
const n = Math.min(buf.length, 8000)
for (let i = 0; i < n; i++) if (buf[i] === 0) return true
return false
}
/** Read a single text file (relative path) → string. */
export async function readProjectFile(root: string, rel: string): Promise<string> {
const buf = await readFile(join(root, rel))
if (looksBinary(buf)) return ''
return buf.toString('utf8')
}
/** Write a text file (relative path). Used by the editable buffer's save. */
export async function writeProjectFile(root: string, rel: string, content: string): Promise<void> {
await writeFile(join(root, rel), content, 'utf8')
}
/**
* Build an in-memory content index of all (small, text) files — powers content
* search and plain-file viewing without touching disk per keystroke. Capped to
* keep large repos sane. PHASE: swap content search to ripgrep when scaling up.
*/
export async function readAll(root: string): Promise<Record<string, string>> {
const out: Record<string, string> = {}
let count = 0
async function walk(abs: string): Promise<void> {
if (count >= MAX_INDEXED_FILES) return
let entries: import('node:fs').Dirent[]
try {
entries = await readdir(abs, { withFileTypes: true })
} catch {
return
}
for (const e of entries) {
if (count >= MAX_INDEXED_FILES) return
if (ignored(e.name)) continue
const childAbs = join(abs, e.name)
if (e.isDirectory()) {
await walk(childAbs)
} else if (e.isFile()) {
try {
const s = await stat(childAbs)
if (s.size > MAX_FILE_BYTES) continue
const buf = await readFile(childAbs)
if (looksBinary(buf)) continue
const rel = relative(root, childAbs).split(sep).join('/')
out[rel] = buf.toString('utf8')
count++
} catch {
/* skip unreadable */
}
}
}
}
await walk(root)
return out
}