export interface ProjectEntry { path: string; name: string; addedAt: string; source: 'scan' | 'manual'; } export interface ProjectView extends ProjectEntry { mtime: number | null; missing: boolean; running: boolean; sessionId: string | null; } export interface ScanResult { started: boolean; reason?: 'scanning'; added?: number; scanned?: number; } export interface ProjectsStoreOpts { /** 测试注入目录;缺省 ~/.godot-mcp/web-gui/ */ dir?: string; /** 会话快照注入(接线层取 listRunSessionsDetailed;缺省空数组) */ getSessions?: () => Array<{ projectPath: string; status: string; }>; /** 时钟注入(测试可控 addedAt);缺省 Date */ now?: () => Date; /** 扫描根注入(测试可控);缺省 allowlist realpath 归一化链(坏根 catch-跳过) */ roots?: () => string[]; /** 单次扫描目录条目上限(测试注入模拟 5000);缺省 MAX_TREE_ENTRIES */ maxTreeEntries?: number; } export declare class ProjectsStore { private readonly dirPath; private readonly listPath; private readonly getSessions; private readonly nowFn; private readonly optsRoots; private readonly maxTreeEntries; constructor(opts?: ProjectsStoreOpts); /** 读取清单;文件缺失/JSON 损坏/结构异常 → 空清单(v2/M3 损坏容错)。 */ private readRaw; private writeRaw; /** 清单快照(§3.3):mtime 降序,Missing 沉底(null 当 0)。 */ listProjects(): Promise; /** 手动添加(§3.1):enqueue 内——resolve 归一 + project.godot 存在校验 + 去重 * (normalizeProjectKey)+ 200 上限逐出(manual 豁免)+ 写回。 * 白名单校验在端点层(Task 3,isPathInAllowedRoots 403),store 层不重复。 */ addProject(path: string, source?: 'manual'): Promise<{ ok: boolean; reason?: 'not_a_project' | 'duplicate'; }>; /** 仅清单移除(不删文件;§4 remove 端点语义)。 */ removeProject(path: string): Promise<{ ok: boolean; reason?: 'not_found'; }>; /** 扫描(§3.2):规则1 互斥 → UNRESTRICTED 拒绝 → roots(空落 cwd)→ BFS → * 规则2 合并(enqueue + re-read)。 */ scanProjects(onProgress?: (found: number, scanned: number) => void): Promise; private runScan; /** BFS 扫描:限深 4、跳过清单目录、条目上限、不跟随符号链接/junction * (readdir withFileTypes 判 isDirectory() && !isSymbolicLink();Windows junction * 判别有版本差异,防环退化为限深+条目上限兜底——spec v2.1)。发现项目即 * 收集且不下钻(Godot 项目内无嵌套项目)。 */ private bfsScan; }