preflight-mcp 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -37
- package/README.zh-CN.md +90 -38
- package/dist/bundle/tree.js +224 -0
- package/dist/evidence/dependencyGraph.js +114 -5
- package/dist/mcp/responseMeta.js +100 -0
- package/dist/server.js +579 -43
- package/dist/trace/service.js +269 -11
- package/dist/trace/store.js +109 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,47 +3,81 @@
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[](https://modelcontextprotocol.io/)
|
|
6
|
+
[](https://www.npmjs.com/package/preflight-mcp)
|
|
6
7
|
|
|
7
8
|
> **English** | [中文](./README.zh-CN.md)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
10
|
+
**Give your AI assistant deep knowledge of any codebase — in seconds.**
|
|
11
|
+
|
|
12
|
+
Preflight-MCP creates searchable, indexed knowledge bundles from GitHub repos, so Claude/GPT/Cursor can understand your project structure, find relevant code, and trace dependencies — without copy-pasting or token limits.
|
|
13
|
+
|
|
14
|
+
## Why Preflight?
|
|
15
|
+
|
|
16
|
+
| Problem | Preflight Solution |
|
|
17
|
+
|---------|--------------------|
|
|
18
|
+
| 🤯 AI forgets your codebase context | Persistent, searchable bundles |
|
|
19
|
+
| 📋 Copy-pasting code into chat | One command: `"index this repo"` |
|
|
20
|
+
| 🔍 AI can't find related files | Full-text search + dependency graph |
|
|
21
|
+
| 🧩 Lost in large projects | Auto-generated `START_HERE.md` & `OVERVIEW.md` |
|
|
22
|
+
| 🔗 No idea what tests cover what | Trace links: code↔test↔doc |
|
|
23
|
+
|
|
24
|
+
## Demo
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
You: "Create a bundle for the repository facebook/react"
|
|
28
|
+
|
|
29
|
+
Preflight: ✅ Cloned, indexed 2,847 files, generated overview
|
|
30
|
+
|
|
31
|
+
You: "Search for 'useState' implementation"
|
|
32
|
+
|
|
33
|
+
Preflight: 📍 Found 23 matches:
|
|
34
|
+
→ packages/react/src/ReactHooks.js:24
|
|
35
|
+
→ packages/react-reconciler/src/ReactFiberHooks.js:1042
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
You: "Show me what tests cover useState"
|
|
39
|
+
|
|
40
|
+
Preflight: 🔗 Trace links:
|
|
41
|
+
→ ReactHooks.js tested_by ReactHooksTest.js
|
|
42
|
+
...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Core Features
|
|
46
|
+
|
|
47
|
+
- 🚀 **One-command indexing** — `"index owner/repo"` creates a complete knowledge bundle
|
|
48
|
+
- 🔍 **Full-text search** — SQLite FTS5 search across all code and docs
|
|
49
|
+
- 🗺️ **Dependency graph** — Visualize imports and file relationships
|
|
50
|
+
- 🔗 **Trace links** — Track code↔test↔doc relationships
|
|
51
|
+
- 📖 **Auto-generated guides** — `START_HERE.md`, `AGENTS.md`, `OVERVIEW.md`
|
|
52
|
+
- ☁️ **Cloud sync** — Multi-path mirror backup for redundancy
|
|
53
|
+
- ⚡ **15 MCP tools + 5 prompts** — Complete toolkit for code exploration
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary><b>All Features (click to expand)</b></summary>
|
|
57
|
+
|
|
58
|
+
- **Progress tracking**: Real-time progress for long-running operations
|
|
59
|
+
- **Bundle integrity check**: Prevents operations on incomplete bundles
|
|
60
|
+
- **De-duplication**: Prevent duplicate bundle creation even during timeouts
|
|
61
|
+
- **Resilient GitHub fetching**: Git clone timeout + archive fallback
|
|
62
|
+
- **Offline repair**: Rebuild derived artifacts without re-fetching
|
|
63
|
+
- **Static facts extraction**: `analysis/FACTS.json` (non-LLM)
|
|
64
|
+
- **Resources**: Read bundle files via `preflight://...` URIs
|
|
65
|
+
- **Atomic operations**: Crash-safety with zero orphans
|
|
66
|
+
- **Fast deletion**: 100-300x performance improvement
|
|
67
|
+
- **Auto-cleanup**: Removes orphan bundles on startup
|
|
68
|
+
|
|
69
|
+
</details>
|
|
37
70
|
|
|
38
71
|
## Table of Contents
|
|
39
72
|
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
73
|
+
- [Why Preflight?](#why-preflight)
|
|
74
|
+
- [Demo](#demo)
|
|
75
|
+
- [Core Features](#core-features)
|
|
42
76
|
- [Quick Start](#quick-start)
|
|
43
|
-
- [Tools](#tools-
|
|
77
|
+
- [Tools](#tools-15-total)
|
|
78
|
+
- [Prompts](#prompts-5-total)
|
|
44
79
|
- [Environment Variables](#environment-variables)
|
|
45
80
|
- [Contributing](#contributing)
|
|
46
|
-
- [License](#license)
|
|
47
81
|
|
|
48
82
|
## Requirements
|
|
49
83
|
|
|
@@ -125,7 +159,7 @@ Run end-to-end smoke test:
|
|
|
125
159
|
npm run smoke
|
|
126
160
|
```
|
|
127
161
|
|
|
128
|
-
## Tools (
|
|
162
|
+
## Tools (15 total)
|
|
129
163
|
|
|
130
164
|
### `preflight_list_bundles`
|
|
131
165
|
List bundle IDs in storage.
|
|
@@ -156,10 +190,15 @@ Input (example):
|
|
|
156
190
|
### `preflight_read_file`
|
|
157
191
|
Read file(s) from bundle. Two modes:
|
|
158
192
|
- **Batch mode** (omit `file`): Returns ALL key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, deps/dependency-graph.json, repo READMEs) in one call
|
|
159
|
-
- **Single file mode** (provide `file`): Returns that specific file
|
|
193
|
+
- **Single file mode** (provide `file`): Returns that specific file
|
|
194
|
+
- **Evidence citation**: Use `withLineNumbers: true` to get `N|line` format; use `ranges: ["20-80"]` to read specific lines
|
|
160
195
|
- Triggers: "查看bundle", "bundle概览", "项目信息", "show bundle", "读取依赖图"
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
|
|
197
|
+
### `preflight_repo_tree`
|
|
198
|
+
Get repository structure overview without wasting tokens on search.
|
|
199
|
+
- Returns: ASCII directory tree, file count by extension/directory, entry point candidates
|
|
200
|
+
- Use BEFORE deep analysis to understand project layout
|
|
201
|
+
- Triggers: "show project structure", "what files are in this repo", "项目结构", "文件分布"
|
|
163
202
|
|
|
164
203
|
### `preflight_delete_bundle`
|
|
165
204
|
Delete/remove a bundle permanently.
|
|
@@ -216,9 +255,14 @@ Create or update traceability links (code↔test, code↔doc, file↔requirement
|
|
|
216
255
|
### `preflight_trace_query`
|
|
217
256
|
Query traceability links (code↔test, code↔doc, commit↔ticket).
|
|
218
257
|
- **Proactive use**: LLM automatically queries trace links when analyzing specific files
|
|
219
|
-
-
|
|
258
|
+
- Returns `reason` and `nextSteps` when no edges found (helps LLM decide next action)
|
|
220
259
|
- Fast when `bundleId` is provided; can scan across bundles when omitted.
|
|
221
260
|
|
|
261
|
+
### `preflight_trace_export`
|
|
262
|
+
Export trace links to `trace/trace.json` for direct LLM reading.
|
|
263
|
+
- Note: Auto-exported after each `trace_upsert`, so only needed to manually refresh
|
|
264
|
+
- Triggers: "export trace", "refresh trace.json", "导出trace"
|
|
265
|
+
|
|
222
266
|
### `preflight_cleanup_orphans`
|
|
223
267
|
Remove incomplete or corrupted bundles (bundles without valid manifest.json).
|
|
224
268
|
- Triggers: "clean up broken bundles", "remove orphans", "清理孤儿bundle"
|
package/README.zh-CN.md
CHANGED
|
@@ -3,41 +3,81 @@
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[](https://modelcontextprotocol.io/)
|
|
6
|
+
[](https://www.npmjs.com/package/preflight-mcp)
|
|
6
7
|
|
|
7
8
|
> [English](./README.md) | **中文**
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
10
|
+
**让你的 AI 助手秒懂任何代码仓库。**
|
|
11
|
+
|
|
12
|
+
Preflight-MCP 为 GitHub 仓库创建可搜索的知识库,让 Claude/GPT/Cursor 理解你的项目结构、快速定位代码、追踪依赖关系 —— 无需复制粘贴,不受 token 限制。
|
|
13
|
+
|
|
14
|
+
## 为什么需要 Preflight?
|
|
15
|
+
|
|
16
|
+
| 痛点 | Preflight 解决方案 |
|
|
17
|
+
|------|--------------------|
|
|
18
|
+
| 🤯 AI 记不住你的代码库 | 持久化、可搜索的知识包 |
|
|
19
|
+
| 📋 反复复制粘贴代码 | 一句话:「索引这个仓库」 |
|
|
20
|
+
| 🔍 AI 找不到相关文件 | 全文搜索 + 依赖图 |
|
|
21
|
+
| 🧩 大项目里迷失方向 | 自动生成 `START_HERE.md` 和 `OVERVIEW.md` |
|
|
22
|
+
| 🔗 不知道哪些测试覆盖哪些代码 | 追溯链接:代码↔测试↔文档 |
|
|
23
|
+
|
|
24
|
+
## 效果演示
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
你:「为 facebook/react 创建 bundle」
|
|
28
|
+
|
|
29
|
+
Preflight:✅ 已克隆,索引了 2,847 个文件,生成概览完成
|
|
30
|
+
|
|
31
|
+
你:「搜索 useState 的实现」
|
|
32
|
+
|
|
33
|
+
Preflight:📍 找到 23 处匹配:
|
|
34
|
+
→ packages/react/src/ReactHooks.js:24
|
|
35
|
+
→ packages/react-reconciler/src/ReactFiberHooks.js:1042
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
你:「哪些测试覆盖了 useState」
|
|
39
|
+
|
|
40
|
+
Preflight:🔗 追溯链接:
|
|
41
|
+
→ ReactHooks.js tested_by ReactHooksTest.js
|
|
42
|
+
...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 核心功能
|
|
46
|
+
|
|
47
|
+
- 🚀 **一句话索引** — 「索引 owner/repo」即可创建完整知识包
|
|
48
|
+
- 🔍 **全文搜索** — SQLite FTS5 搜索全部代码和文档
|
|
49
|
+
- 🗺️ **依赖图** — 可视化 import 关系和文件依赖
|
|
50
|
+
- 🔗 **追溯链接** — 追踪代码↔测试↔文档关系
|
|
51
|
+
- 📖 **自动生成指南** — `START_HERE.md`、`AGENTS.md`、`OVERVIEW.md`
|
|
52
|
+
- ☁️ **云端同步** — 多路径镜像备份
|
|
53
|
+
- ⚡ **15 个 MCP 工具 + 5 个 prompts** — 完整的代码探索工具集
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary><b>全部功能(点击展开)</b></summary>
|
|
57
|
+
|
|
58
|
+
- **进度追踪**:长时间操作的实时进度显示
|
|
59
|
+
- **Bundle 完整性检查**:防止对不完整 bundle 进行操作
|
|
60
|
+
- **去重机制**:即使超时也能防止重复创建
|
|
61
|
+
- **可靠的 GitHub 抓取**:git clone 超时 + archive 兜底
|
|
62
|
+
- **离线修复**:无需重新抓取即可重建派生文件
|
|
63
|
+
- **静态事实提取**:`analysis/FACTS.json`(非 LLM)
|
|
64
|
+
- **Resources**:通过 `preflight://...` URI 读取文件
|
|
65
|
+
- **原子操作**:崩溃安全,零孤儿目录
|
|
66
|
+
- **快速删除**:100-300 倍性能提升
|
|
67
|
+
- **自动清理**:启动时自动清理孤儿 bundle
|
|
68
|
+
|
|
69
|
+
</details>
|
|
70
|
+
|
|
71
|
+
## 目录
|
|
72
|
+
|
|
73
|
+
- [为什么需要 Preflight](#为什么需要-preflight)
|
|
74
|
+
- [效果演示](#效果演示)
|
|
75
|
+
- [核心功能](#核心功能)
|
|
76
|
+
- [快速开始](#quick-start)
|
|
77
|
+
- [工具](#tools-15-total)
|
|
78
|
+
- [Prompts](#prompts-5-total)
|
|
79
|
+
- [环境变量](#environment-variables)
|
|
80
|
+
- [贡献指南](#contributing)
|
|
41
81
|
|
|
42
82
|
## Requirements
|
|
43
83
|
|
|
@@ -140,7 +180,7 @@ npm run smoke
|
|
|
140
180
|
- 列表与清理逻辑只接受 UUID v4 作为 bundleId
|
|
141
181
|
- 会自动过滤 `#recycle`、`tmp`、`.deleting` 等非 bundle 目录
|
|
142
182
|
|
|
143
|
-
## Tools (
|
|
183
|
+
## Tools (15 total)
|
|
144
184
|
|
|
145
185
|
### `preflight_list_bundles`
|
|
146
186
|
列出所有 bundle。
|
|
@@ -170,11 +210,16 @@ npm run smoke
|
|
|
170
210
|
|
|
171
211
|
### `preflight_read_file`
|
|
172
212
|
从 bundle 读取文件。两种模式:
|
|
173
|
-
- **批量模式**(省略 `file
|
|
174
|
-
- **单文件模式**(提供 `file
|
|
213
|
+
- **批量模式**(省略 `file`):返回所有关键文件
|
|
214
|
+
- **单文件模式**(提供 `file`):返回指定文件
|
|
215
|
+
- **证据引用**:使用 `withLineNumbers: true` 获取 `N|行` 格式;使用 `ranges: ["20-80"]` 读取指定行
|
|
175
216
|
- 触发词:「查看概览」「项目概览」「bundle详情」「读取依赖图」
|
|
176
|
-
|
|
177
|
-
|
|
217
|
+
|
|
218
|
+
### `preflight_repo_tree`
|
|
219
|
+
获取仓库结构概览,避免浪费 token 搜索。
|
|
220
|
+
- 返回:ASCII 目录树、按扩展名/目录统计文件数、入口点候选
|
|
221
|
+
- 在深入分析前使用,了解项目布局
|
|
222
|
+
- 触发词:「项目结构」「文件分布」「show tree」
|
|
178
223
|
|
|
179
224
|
### `preflight_delete_bundle`
|
|
180
225
|
永久删除/移除一个 bundle。
|
|
@@ -225,7 +270,14 @@ npm run smoke
|
|
|
225
270
|
写入/更新 bundle 级 traceability links(commit↔ticket、symbol↔test、code↔doc 等)。
|
|
226
271
|
|
|
227
272
|
### `preflight_trace_query`
|
|
228
|
-
查询 traceability links
|
|
273
|
+
查询 traceability links。
|
|
274
|
+
- 无匹配边时返回 `reason` 和 `nextSteps`(帮助 LLM 决定下一步)
|
|
275
|
+
- 提供 `bundleId` 时更快;省略时可跨 bundle 扫描
|
|
276
|
+
|
|
277
|
+
### `preflight_trace_export`
|
|
278
|
+
导出 trace links 到 `trace/trace.json`。
|
|
279
|
+
- 注意:每次 `trace_upsert` 后会自动导出,此工具仅用于手动刷新
|
|
280
|
+
- 触发词:「导出trace」「刷新trace.json」
|
|
229
281
|
|
|
230
282
|
### `preflight_cleanup_orphans`
|
|
231
283
|
删除不完整或损坏的 bundle(缺少有效 manifest.json)。
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const ENTRY_POINT_PATTERNS = [
|
|
4
|
+
{ pattern: /^readme\.md$/i, type: 'readme', priority: 100 },
|
|
5
|
+
{ pattern: /^readme$/i, type: 'readme', priority: 95 },
|
|
6
|
+
{ pattern: /^index\.(ts|js|tsx|jsx|py|go|rs)$/i, type: 'index', priority: 90 },
|
|
7
|
+
{ pattern: /^main\.(ts|js|tsx|jsx|py|go|rs)$/i, type: 'main', priority: 85 },
|
|
8
|
+
{ pattern: /^app\.(ts|js|tsx|jsx|py)$/i, type: 'app', priority: 80 },
|
|
9
|
+
{ pattern: /^server\.(ts|js|tsx|jsx|py|go)$/i, type: 'server', priority: 75 },
|
|
10
|
+
{ pattern: /^cli\.(ts|js|py)$/i, type: 'cli', priority: 70 },
|
|
11
|
+
{ pattern: /^__init__\.py$/i, type: 'index', priority: 60 },
|
|
12
|
+
{ pattern: /^mod\.rs$/i, type: 'index', priority: 60 },
|
|
13
|
+
{ pattern: /^lib\.rs$/i, type: 'main', priority: 85 },
|
|
14
|
+
{ pattern: /^package\.json$/i, type: 'config', priority: 50 },
|
|
15
|
+
{ pattern: /^pyproject\.toml$/i, type: 'config', priority: 50 },
|
|
16
|
+
{ pattern: /^cargo\.toml$/i, type: 'config', priority: 50 },
|
|
17
|
+
{ pattern: /^go\.mod$/i, type: 'config', priority: 50 },
|
|
18
|
+
{ pattern: /\.test\.(ts|js|tsx|jsx)$/i, type: 'test', priority: 30 },
|
|
19
|
+
{ pattern: /_test\.(py|go)$/i, type: 'test', priority: 30 },
|
|
20
|
+
{ pattern: /^test_.*\.py$/i, type: 'test', priority: 30 },
|
|
21
|
+
];
|
|
22
|
+
function matchesGlob(filename, patterns) {
|
|
23
|
+
if (patterns.length === 0)
|
|
24
|
+
return true;
|
|
25
|
+
for (const pattern of patterns) {
|
|
26
|
+
// Simple glob matching: * matches any sequence, ** matches any path
|
|
27
|
+
const regexPattern = pattern
|
|
28
|
+
.replace(/\./g, '\\.')
|
|
29
|
+
.replace(/\*\*/g, '.*')
|
|
30
|
+
.replace(/\*/g, '[^/]*');
|
|
31
|
+
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
|
32
|
+
if (regex.test(filename))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function shouldExclude(relativePath, excludePatterns) {
|
|
38
|
+
if (excludePatterns.length === 0)
|
|
39
|
+
return false;
|
|
40
|
+
for (const pattern of excludePatterns) {
|
|
41
|
+
// Simple exclusion matching
|
|
42
|
+
if (relativePath.includes(pattern))
|
|
43
|
+
return true;
|
|
44
|
+
if (pattern.startsWith('*') && relativePath.endsWith(pattern.slice(1)))
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
export async function generateRepoTree(bundleRootDir, bundleId, options = {}) {
|
|
50
|
+
const depth = options.depth ?? 4;
|
|
51
|
+
const includePatterns = options.include ?? [];
|
|
52
|
+
const excludePatterns = options.exclude ?? ['node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build', '*.pyc'];
|
|
53
|
+
const reposDir = path.join(bundleRootDir, 'repos');
|
|
54
|
+
const stats = {
|
|
55
|
+
totalFiles: 0,
|
|
56
|
+
totalDirs: 0,
|
|
57
|
+
byExtension: {},
|
|
58
|
+
byTopDir: {},
|
|
59
|
+
};
|
|
60
|
+
const entryPointCandidates = [];
|
|
61
|
+
// Build tree recursively
|
|
62
|
+
async function buildTree(dir, currentDepth, relativePath) {
|
|
63
|
+
if (currentDepth > depth)
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
const stat = await fs.stat(dir);
|
|
67
|
+
const name = path.basename(dir);
|
|
68
|
+
if (stat.isFile()) {
|
|
69
|
+
// Check include/exclude patterns
|
|
70
|
+
if (includePatterns.length > 0 && !matchesGlob(name, includePatterns)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (shouldExclude(relativePath, excludePatterns)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
stats.totalFiles++;
|
|
77
|
+
// Track extension stats
|
|
78
|
+
const ext = path.extname(name).toLowerCase() || '(no ext)';
|
|
79
|
+
stats.byExtension[ext] = (stats.byExtension[ext] ?? 0) + 1;
|
|
80
|
+
// Track top directory stats
|
|
81
|
+
const topDir = relativePath.split('/')[0] ?? '(root)';
|
|
82
|
+
stats.byTopDir[topDir] = (stats.byTopDir[topDir] ?? 0) + 1;
|
|
83
|
+
// Check for entry point candidates
|
|
84
|
+
for (const ep of ENTRY_POINT_PATTERNS) {
|
|
85
|
+
if (ep.pattern.test(name)) {
|
|
86
|
+
entryPointCandidates.push({
|
|
87
|
+
path: relativePath,
|
|
88
|
+
type: ep.type,
|
|
89
|
+
priority: ep.priority,
|
|
90
|
+
});
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { name, type: 'file', size: stat.size };
|
|
95
|
+
}
|
|
96
|
+
if (stat.isDirectory()) {
|
|
97
|
+
// Check exclude patterns for directories
|
|
98
|
+
if (shouldExclude(name, excludePatterns) || shouldExclude(relativePath, excludePatterns)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
stats.totalDirs++;
|
|
102
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
103
|
+
const children = [];
|
|
104
|
+
// Sort: directories first, then files, alphabetically
|
|
105
|
+
const sortedEntries = entries.sort((a, b) => {
|
|
106
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
107
|
+
return -1;
|
|
108
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
109
|
+
return 1;
|
|
110
|
+
return a.name.localeCompare(b.name);
|
|
111
|
+
});
|
|
112
|
+
for (const entry of sortedEntries) {
|
|
113
|
+
const childPath = path.join(dir, entry.name);
|
|
114
|
+
const childRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
115
|
+
const childNode = await buildTree(childPath, currentDepth + 1, childRelPath);
|
|
116
|
+
if (childNode) {
|
|
117
|
+
children.push(childNode);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { name, type: 'dir', children: children.length > 0 ? children : undefined };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Build tree starting from repos directory
|
|
129
|
+
let rootNode = null;
|
|
130
|
+
try {
|
|
131
|
+
await fs.access(reposDir);
|
|
132
|
+
rootNode = await buildTree(reposDir, 0, '');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// repos directory doesn't exist, try bundle root
|
|
136
|
+
rootNode = await buildTree(bundleRootDir, 0, '');
|
|
137
|
+
}
|
|
138
|
+
// Generate ASCII tree
|
|
139
|
+
function renderTree(node, prefix = '', isLast = true) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
142
|
+
const extension = isLast ? ' ' : '│ ';
|
|
143
|
+
lines.push(`${prefix}${connector}${node.name}${node.type === 'dir' ? '/' : ''}`);
|
|
144
|
+
if (node.children) {
|
|
145
|
+
const childCount = node.children.length;
|
|
146
|
+
node.children.forEach((child, index) => {
|
|
147
|
+
const childLines = renderTree(child, prefix + extension, index === childCount - 1);
|
|
148
|
+
lines.push(...childLines);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
let treeText = '';
|
|
154
|
+
if (rootNode) {
|
|
155
|
+
if (rootNode.children && rootNode.children.length > 0) {
|
|
156
|
+
treeText = `${rootNode.name}/\n`;
|
|
157
|
+
rootNode.children.forEach((child, index) => {
|
|
158
|
+
const childLines = renderTree(child, '', index === rootNode.children.length - 1);
|
|
159
|
+
treeText += childLines.join('\n') + '\n';
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
treeText = `${rootNode.name}/ (empty or filtered out)`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
treeText = '(no files found)';
|
|
168
|
+
}
|
|
169
|
+
// Sort entry point candidates by priority
|
|
170
|
+
entryPointCandidates.sort((a, b) => b.priority - a.priority);
|
|
171
|
+
return {
|
|
172
|
+
bundleId,
|
|
173
|
+
tree: treeText.trim(),
|
|
174
|
+
stats,
|
|
175
|
+
entryPointCandidates: entryPointCandidates.slice(0, 20), // Limit to top 20
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Format tree result as human-readable text
|
|
180
|
+
*/
|
|
181
|
+
export function formatTreeResult(result) {
|
|
182
|
+
const lines = [];
|
|
183
|
+
lines.push(`📂 Repository Structure for bundle: ${result.bundleId}`);
|
|
184
|
+
lines.push('');
|
|
185
|
+
lines.push('## Directory Tree');
|
|
186
|
+
lines.push('```');
|
|
187
|
+
lines.push(result.tree);
|
|
188
|
+
lines.push('```');
|
|
189
|
+
lines.push('');
|
|
190
|
+
lines.push('## Statistics');
|
|
191
|
+
lines.push(`- Total files: ${result.stats.totalFiles}`);
|
|
192
|
+
lines.push(`- Total directories: ${result.stats.totalDirs}`);
|
|
193
|
+
lines.push('');
|
|
194
|
+
// By extension (top 10)
|
|
195
|
+
const extEntries = Object.entries(result.stats.byExtension)
|
|
196
|
+
.sort((a, b) => b[1] - a[1])
|
|
197
|
+
.slice(0, 10);
|
|
198
|
+
if (extEntries.length > 0) {
|
|
199
|
+
lines.push('### Files by Extension');
|
|
200
|
+
for (const [ext, count] of extEntries) {
|
|
201
|
+
lines.push(`- ${ext}: ${count}`);
|
|
202
|
+
}
|
|
203
|
+
lines.push('');
|
|
204
|
+
}
|
|
205
|
+
// By top directory (top 10)
|
|
206
|
+
const dirEntries = Object.entries(result.stats.byTopDir)
|
|
207
|
+
.sort((a, b) => b[1] - a[1])
|
|
208
|
+
.slice(0, 10);
|
|
209
|
+
if (dirEntries.length > 0) {
|
|
210
|
+
lines.push('### Files by Top Directory');
|
|
211
|
+
for (const [dir, count] of dirEntries) {
|
|
212
|
+
lines.push(`- ${dir}: ${count}`);
|
|
213
|
+
}
|
|
214
|
+
lines.push('');
|
|
215
|
+
}
|
|
216
|
+
// Entry point candidates
|
|
217
|
+
if (result.entryPointCandidates.length > 0) {
|
|
218
|
+
lines.push('## Entry Point Candidates');
|
|
219
|
+
for (const ep of result.entryPointCandidates.slice(0, 10)) {
|
|
220
|
+
lines.push(`- \`${ep.path}\` (${ep.type})`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return lines.join('\n');
|
|
224
|
+
}
|