@dommaker/harness 0.16.1 → 0.16.3
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/CHANGELOG.md +26 -0
- package/README.md +1 -0
- package/bin/harness.js +2 -1
- package/dist/cli/commands/sync-docs.d.ts +2 -0
- package/dist/cli/commands/sync-docs.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs.js +322 -2
- package/dist/cli/commands/sync-docs.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/CONTEXT.md +3 -3
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +423 -0
- package/src/cli/commands/__tests__/sync-docs.test.ts +27 -0
- package/src/cli/commands/sync-docs.ts +357 -2
|
@@ -25,6 +25,8 @@ export interface SyncDocsOptions {
|
|
|
25
25
|
changelog?: boolean;
|
|
26
26
|
/** 输出 JSON 格式(供 LLM 消费) */
|
|
27
27
|
json?: boolean;
|
|
28
|
+
/** 同步 AGENTS.md(agent 导读),在默认章节之外追加 */
|
|
29
|
+
agents?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface ModuleInfo {
|
|
@@ -140,10 +142,26 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
// 4c. AGENTS.md(--agents 启用):缺失或内容漂移
|
|
146
|
+
let agentsMdExpected: string | null = null;
|
|
147
|
+
let agentsMdExists = false;
|
|
148
|
+
let agentsMdStale = false;
|
|
149
|
+
if (options.agents) {
|
|
150
|
+
agentsMdExpected = await buildAgentsMd(projectPath, srcDirs);
|
|
151
|
+
try {
|
|
152
|
+
const existing = await fs.readFile(path.join(projectPath, 'AGENTS.md'), 'utf-8');
|
|
153
|
+
agentsMdExists = true;
|
|
154
|
+
agentsMdStale = existing !== agentsMdExpected;
|
|
155
|
+
} catch {
|
|
156
|
+
agentsMdStale = true; // 缺失视为漂移
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const hasAgentsIssues = options.agents === true && agentsMdStale;
|
|
160
|
+
|
|
143
161
|
const hasTableIssues = result.added.length > 0 || result.removed.length > 0;
|
|
144
162
|
const hasCapIssues = capCountMismatches.length > 0;
|
|
145
163
|
const hasContextIssues = result.contextMissing.length > 0 || result.contextStale.length > 0;
|
|
146
|
-
const hasIssues = hasTableIssues || hasCapIssues || hasContextIssues;
|
|
164
|
+
const hasIssues = hasTableIssues || hasCapIssues || hasContextIssues || hasAgentsIssues;
|
|
147
165
|
|
|
148
166
|
// 5. JSON 输出模式:结构化输出供 LLM 消费
|
|
149
167
|
if (isJson) {
|
|
@@ -203,6 +221,17 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
203
221
|
);
|
|
204
222
|
}
|
|
205
223
|
|
|
224
|
+
if (options.agents) {
|
|
225
|
+
jsonOutput.agentsMd = { file: 'AGENTS.md', exists: agentsMdExists, stale: agentsMdStale };
|
|
226
|
+
if (agentsMdStale) {
|
|
227
|
+
(jsonOutput.resolution as Array<Record<string, unknown>>).push({
|
|
228
|
+
action: 'sync-agents-md',
|
|
229
|
+
command: 'harness sync-docs --agents',
|
|
230
|
+
details: '生成/更新 AGENTS.md(agent 导读)',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
206
235
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
207
236
|
return !hasIssues;
|
|
208
237
|
}
|
|
@@ -233,6 +262,15 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
233
262
|
result.contextStale.forEach(d => console.log(chalk.gray(` - ${d}/CONTEXT.md`)));
|
|
234
263
|
}
|
|
235
264
|
|
|
265
|
+
if (hasAgentsIssues) {
|
|
266
|
+
console.log(
|
|
267
|
+
chalk.yellow(agentsMdExists
|
|
268
|
+
? `\n🤖 AGENTS.md 与当前项目状态不一致:`
|
|
269
|
+
: `\n🤖 缺少 AGENTS.md(agent 导读):`)
|
|
270
|
+
);
|
|
271
|
+
console.log(chalk.gray(` - AGENTS.md`));
|
|
272
|
+
}
|
|
273
|
+
|
|
236
274
|
if (!hasIssues) {
|
|
237
275
|
console.log(chalk.green('✅ 所有文档都是最新的'));
|
|
238
276
|
return true;
|
|
@@ -261,6 +299,11 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
261
299
|
console.log(chalk.green(`✅ 已创建 ${dir}/CONTEXT.md`));
|
|
262
300
|
}
|
|
263
301
|
|
|
302
|
+
if (hasAgentsIssues && agentsMdExpected !== null) {
|
|
303
|
+
await fs.writeFile(path.join(projectPath, 'AGENTS.md'), agentsMdExpected, 'utf-8');
|
|
304
|
+
console.log(chalk.green(agentsMdExists ? `✅ 已更新 AGENTS.md` : `✅ 已生成 AGENTS.md`));
|
|
305
|
+
}
|
|
306
|
+
|
|
264
307
|
return !hasIssues;
|
|
265
308
|
}
|
|
266
309
|
|
|
@@ -514,7 +557,8 @@ async function updateCapabilitiesFile(
|
|
|
514
557
|
// 移除已删除文件的行
|
|
515
558
|
for (const removed of result.removed) {
|
|
516
559
|
const escapedFile = removed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
517
|
-
|
|
560
|
+
// 第二列存完整路径,basename 只在末尾出现,用 [^|]* 匹配路径前缀
|
|
561
|
+
const rowRegex = new RegExp(`^\\|[^|]*\\|[^|]*\\b${escapedFile}\\s*\\|.*$`, 'gm');
|
|
518
562
|
content = content.replace(rowRegex, '');
|
|
519
563
|
}
|
|
520
564
|
|
|
@@ -741,3 +785,314 @@ function updateCapabilityCounts(content: string, projectPath: string): string {
|
|
|
741
785
|
return content;
|
|
742
786
|
}
|
|
743
787
|
|
|
788
|
+
|
|
789
|
+
// ========================================
|
|
790
|
+
// AGENTS.md 生成(--agents)
|
|
791
|
+
// ========================================
|
|
792
|
+
|
|
793
|
+
/** 生成 AGENTS.md 目录结构表时跳过的目录(依赖/构建产物/报告等) */
|
|
794
|
+
const AGENTS_MD_SKIP_DIRS = new Set([
|
|
795
|
+
'node_modules', 'dist', 'build', 'out', 'coverage',
|
|
796
|
+
'playwright-report', 'test-results', '.git', '.next', '.turbo',
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
/** 知名顶层目录的一句话说明 */
|
|
800
|
+
const AGENTS_MD_DIR_ROLES: Record<string, string> = {
|
|
801
|
+
docs: '项目文档',
|
|
802
|
+
scripts: '工具脚本',
|
|
803
|
+
tests: '测试',
|
|
804
|
+
test: '测试',
|
|
805
|
+
bin: '可执行入口/脚本',
|
|
806
|
+
templates: '项目模板',
|
|
807
|
+
'.github': 'CI/CD 配置',
|
|
808
|
+
'.harness': 'harness 配置与运行时状态',
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
/** 常用命令候选(按展示顺序),仅列出 package.json 中实际存在的脚本 */
|
|
812
|
+
const AGENTS_MD_COMMANDS: Array<[string, string]> = [
|
|
813
|
+
['dev', '启动开发环境'],
|
|
814
|
+
['build', '构建'],
|
|
815
|
+
['test', '运行测试'],
|
|
816
|
+
['test:e2e', '端到端测试'],
|
|
817
|
+
['test:unit', '单元测试'],
|
|
818
|
+
['typecheck', '类型检查'],
|
|
819
|
+
['lint', '代码检查'],
|
|
820
|
+
['start', '启动生产服务'],
|
|
821
|
+
];
|
|
822
|
+
|
|
823
|
+
interface PackageJsonLite {
|
|
824
|
+
name?: string;
|
|
825
|
+
description?: string;
|
|
826
|
+
scripts?: Record<string, string>;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
interface GovernanceInfo {
|
|
830
|
+
hasConfig: boolean;
|
|
831
|
+
preset?: string;
|
|
832
|
+
hasClaudeGovernance: boolean;
|
|
833
|
+
ironLaws?: number;
|
|
834
|
+
guidelines?: number;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* 生成 AGENTS.md 内容(agent 导读:项目简介/目录结构/常用命令/约束治理/知识入口)
|
|
839
|
+
*
|
|
840
|
+
* 纯静态提取(零 LLM),确定性输出:同一代码库状态生成同一内容(无时间戳),
|
|
841
|
+
* 因此重跑幂等,--check 可直接用文本对比检测漂移。
|
|
842
|
+
*/
|
|
843
|
+
async function buildAgentsMd(projectPath: string, srcDirs: string[]): Promise<string> {
|
|
844
|
+
const pkg = await readPackageJsonLite(projectPath);
|
|
845
|
+
const [dirs, governance, knowledgeCount, contextDocCount] = await Promise.all([
|
|
846
|
+
getTopLevelDirEntries(projectPath, srcDirs),
|
|
847
|
+
getGovernanceInfo(projectPath),
|
|
848
|
+
countKnowledgeEntries(projectPath),
|
|
849
|
+
countContextDocs(projectPath, srcDirs),
|
|
850
|
+
]);
|
|
851
|
+
|
|
852
|
+
const name = pkg?.name || path.basename(projectPath);
|
|
853
|
+
const description = pkg?.description || await getConfigDescription(projectPath);
|
|
854
|
+
|
|
855
|
+
const lines: string[] = [
|
|
856
|
+
'# AGENTS.md',
|
|
857
|
+
'',
|
|
858
|
+
'> 本文件由 `harness sync-docs --agents` 自动生成,请勿手改。内容漂移时重新运行该命令更新。',
|
|
859
|
+
'',
|
|
860
|
+
'## 项目简介',
|
|
861
|
+
'',
|
|
862
|
+
description ? `**${name}** — ${description}` : `**${name}**`,
|
|
863
|
+
'',
|
|
864
|
+
'## 目录结构',
|
|
865
|
+
'',
|
|
866
|
+
];
|
|
867
|
+
|
|
868
|
+
if (dirs.length > 0) {
|
|
869
|
+
lines.push('| 目录 | 说明 |', '|------|------|');
|
|
870
|
+
for (const d of dirs) {
|
|
871
|
+
lines.push(`| \`${d.dir}/\` | ${d.role} |`);
|
|
872
|
+
}
|
|
873
|
+
} else {
|
|
874
|
+
lines.push('(未检测到顶层目录)');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
lines.push('', '## 常用命令', '');
|
|
878
|
+
const pm = detectPackageManager(projectPath);
|
|
879
|
+
const scripts = pkg?.scripts || {};
|
|
880
|
+
const commands = AGENTS_MD_COMMANDS.filter(([n]) => scripts[n]);
|
|
881
|
+
if (commands.length > 0) {
|
|
882
|
+
lines.push('```bash');
|
|
883
|
+
for (const [n, comment] of commands) {
|
|
884
|
+
lines.push(`${formatScriptCommand(pm, n)} # ${comment}`);
|
|
885
|
+
}
|
|
886
|
+
lines.push('```');
|
|
887
|
+
} else {
|
|
888
|
+
lines.push('(package.json 中未检测到常用脚本)');
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
lines.push('', '## 约束与治理', '');
|
|
892
|
+
if (governance.hasConfig) {
|
|
893
|
+
lines.push(`- 治理配置:\`.harness/config.yml\`${governance.preset ? `(preset: ${governance.preset})` : ''}`);
|
|
894
|
+
}
|
|
895
|
+
if (governance.hasClaudeGovernance) {
|
|
896
|
+
const counts = [
|
|
897
|
+
governance.ironLaws !== undefined ? `Iron Laws ${governance.ironLaws} 条` : null,
|
|
898
|
+
governance.guidelines !== undefined ? `Guidelines ${governance.guidelines} 条` : null,
|
|
899
|
+
].filter(Boolean).join('、');
|
|
900
|
+
lines.push(`- 约束清单:\`CLAUDE.md\` Governance Rules 块${counts ? `(${counts})` : ''}`);
|
|
901
|
+
}
|
|
902
|
+
if (!governance.hasConfig && !governance.hasClaudeGovernance) {
|
|
903
|
+
lines.push('- 未检测到 harness 治理配置,可运行 `harness init` 初始化');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
lines.push('', '## 知识入口', '');
|
|
907
|
+
if (knowledgeCount !== null) {
|
|
908
|
+
lines.push(`- \`.harness/knowledge/\`:项目知识库(${knowledgeCount} 条),用 \`harness knowledge\` 查询`);
|
|
909
|
+
}
|
|
910
|
+
lines.push(
|
|
911
|
+
contextDocCount > 0
|
|
912
|
+
? `- 各源码目录的 \`CONTEXT.md\` 是权威模块文档(现有 ${contextDocCount} 个),改动代码时同步更新`
|
|
913
|
+
: '- 各源码目录的 `CONTEXT.md` 是权威模块文档,缺失目录可由 `harness sync-docs` 生成模板'
|
|
914
|
+
);
|
|
915
|
+
lines.push('');
|
|
916
|
+
|
|
917
|
+
return lines.join('\n');
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** 读取 package.json(不存在或无法解析时返回 null) */
|
|
921
|
+
async function readPackageJsonLite(dir: string): Promise<PackageJsonLite | null> {
|
|
922
|
+
try {
|
|
923
|
+
return JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf-8')) as PackageJsonLite;
|
|
924
|
+
} catch {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** 从 .harness/config.yml 读取项目描述(package.json 无 description 时的兜底) */
|
|
930
|
+
async function getConfigDescription(projectPath: string): Promise<string> {
|
|
931
|
+
try {
|
|
932
|
+
const content = await fs.readFile(path.join(projectPath, '.harness', 'config.yml'), 'utf-8');
|
|
933
|
+
const config = yaml.load(content) as Record<string, unknown> | undefined;
|
|
934
|
+
return typeof config?.description === 'string' ? config.description : '';
|
|
935
|
+
} catch {
|
|
936
|
+
return '';
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** 检测包管理器(决定命令前缀):pnpm workspace/lockfile → yarn lockfile → 默认 npm */
|
|
941
|
+
function detectPackageManager(projectPath: string): 'pnpm' | 'yarn' | 'npm' {
|
|
942
|
+
if (existsSync(path.join(projectPath, 'pnpm-workspace.yaml')) || existsSync(path.join(projectPath, 'pnpm-lock.yaml'))) {
|
|
943
|
+
return 'pnpm';
|
|
944
|
+
}
|
|
945
|
+
if (existsSync(path.join(projectPath, 'yarn.lock'))) return 'yarn';
|
|
946
|
+
return 'npm';
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** 格式化脚本调用方式(npm 的 start/test 可省略 run) */
|
|
950
|
+
function formatScriptCommand(pm: 'pnpm' | 'yarn' | 'npm', name: string): string {
|
|
951
|
+
if (pm === 'pnpm') return `pnpm ${name}`;
|
|
952
|
+
if (pm === 'yarn') return `yarn ${name}`;
|
|
953
|
+
return name === 'start' || name === 'test' ? `npm ${name}` : `npm run ${name}`;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** 收集顶层目录及其一句话说明(排序保证确定性) */
|
|
957
|
+
async function getTopLevelDirEntries(
|
|
958
|
+
projectPath: string,
|
|
959
|
+
srcDirs: string[]
|
|
960
|
+
): Promise<Array<{ dir: string; role: string }>> {
|
|
961
|
+
let dirents: import('fs').Dirent[];
|
|
962
|
+
try {
|
|
963
|
+
dirents = await fs.readdir(projectPath, { withFileTypes: true });
|
|
964
|
+
} catch {
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const entries: Array<{ dir: string; role: string }> = [];
|
|
969
|
+
for (const d of dirents) {
|
|
970
|
+
if (!d.isDirectory()) continue;
|
|
971
|
+
if (AGENTS_MD_SKIP_DIRS.has(d.name)) continue;
|
|
972
|
+
// 隐藏目录只保留 .harness / .github
|
|
973
|
+
if (d.name.startsWith('.') && d.name !== '.harness' && d.name !== '.github') continue;
|
|
974
|
+
entries.push({ dir: d.name, role: await describeTopLevelDir(projectPath, d.name, srcDirs) });
|
|
975
|
+
}
|
|
976
|
+
entries.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
|
|
977
|
+
return entries;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** 顶层目录的一句话说明:知名目录 → monorepo 成员 → 源码根 → 子包 description → 占位 */
|
|
981
|
+
async function describeTopLevelDir(projectPath: string, dir: string, srcDirs: string[]): Promise<string> {
|
|
982
|
+
const known = AGENTS_MD_DIR_ROLES[dir];
|
|
983
|
+
if (known) return known;
|
|
984
|
+
|
|
985
|
+
if (dir === 'apps' || dir === 'packages') {
|
|
986
|
+
const members = await listWorkspaceMembers(path.join(projectPath, dir));
|
|
987
|
+
if (members.length > 0) {
|
|
988
|
+
return `monorepo ${dir === 'apps' ? '应用' : '共享包'}:${members.join('、')}`;
|
|
989
|
+
}
|
|
990
|
+
return 'monorepo 工作区';
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (srcDirs.includes(dir)) return '源码目录';
|
|
994
|
+
|
|
995
|
+
const subPkg = await readPackageJsonLite(path.join(projectPath, dir));
|
|
996
|
+
if (subPkg?.description) return subPkg.description;
|
|
997
|
+
|
|
998
|
+
return '—';
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** 列出 monorepo 工作区成员(含 package.json 的子目录名,排序) */
|
|
1002
|
+
async function listWorkspaceMembers(dir: string): Promise<string[]> {
|
|
1003
|
+
try {
|
|
1004
|
+
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
1005
|
+
const members: string[] = [];
|
|
1006
|
+
for (const d of dirents) {
|
|
1007
|
+
if (!d.isDirectory() || d.name.startsWith('.')) continue;
|
|
1008
|
+
if (existsSync(path.join(dir, d.name, 'package.json'))) {
|
|
1009
|
+
members.push(d.name);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return members.sort();
|
|
1013
|
+
} catch {
|
|
1014
|
+
return [];
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** 收集治理信息:.harness/config.yml 与 CLAUDE.md 治理块(含铁律/指南计数) */
|
|
1019
|
+
async function getGovernanceInfo(projectPath: string): Promise<GovernanceInfo> {
|
|
1020
|
+
const info: GovernanceInfo = { hasConfig: false, hasClaudeGovernance: false };
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
const content = await fs.readFile(path.join(projectPath, '.harness', 'config.yml'), 'utf-8');
|
|
1024
|
+
const config = yaml.load(content) as Record<string, unknown> | undefined;
|
|
1025
|
+
info.hasConfig = true;
|
|
1026
|
+
if (config && typeof config.preset === 'string') {
|
|
1027
|
+
info.preset = config.preset;
|
|
1028
|
+
}
|
|
1029
|
+
} catch {
|
|
1030
|
+
// 配置不存在
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
try {
|
|
1034
|
+
const claude = await fs.readFile(path.join(projectPath, 'CLAUDE.md'), 'utf-8');
|
|
1035
|
+
if (/^##\s+Governance Rules/m.test(claude) || claude.includes('HARNESS_CONSTRAINTS_START')) {
|
|
1036
|
+
info.hasClaudeGovernance = true;
|
|
1037
|
+
info.ironLaws = countConstraintBullets(claude, 'Iron Laws');
|
|
1038
|
+
info.guidelines = countConstraintBullets(claude, 'Guidelines');
|
|
1039
|
+
}
|
|
1040
|
+
} catch {
|
|
1041
|
+
// CLAUDE.md 不存在
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return info;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** 统计 CLAUDE.md 指定约束分节下的条目数(`- **key**:` 行),分节不存在时返回 undefined */
|
|
1048
|
+
function countConstraintBullets(content: string, section: string): number | undefined {
|
|
1049
|
+
const heading = new RegExp(`^###\\s+${section}[^\\n]*$`, 'm').exec(content);
|
|
1050
|
+
if (!heading) return undefined;
|
|
1051
|
+
const rest = content.slice(heading.index + heading[0].length);
|
|
1052
|
+
const end = rest.search(/^###\s|^\s*<!--\s*HARNESS_CONSTRAINTS_END/m);
|
|
1053
|
+
const block = end === -1 ? rest : rest.slice(0, end);
|
|
1054
|
+
const items = block.match(/^-\s+\*\*[A-Za-z0-9_]+\*\*/gm);
|
|
1055
|
+
return items ? items.length : 0;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** 统计 .harness/knowledge 下的知识条目数(.md 文件),目录不存在时返回 null */
|
|
1059
|
+
async function countKnowledgeEntries(projectPath: string): Promise<number | null> {
|
|
1060
|
+
try {
|
|
1061
|
+
const files = await fs.readdir(path.join(projectPath, '.harness', 'knowledge'));
|
|
1062
|
+
return files.filter(f => f.endsWith('.md')).length;
|
|
1063
|
+
} catch {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** 统计源码根内的 CONTEXT.md 数量(含源码根本身,跨根去重) */
|
|
1069
|
+
async function countContextDocs(projectPath: string, srcDirs: string[]): Promise<number> {
|
|
1070
|
+
const found = new Set<string>();
|
|
1071
|
+
|
|
1072
|
+
async function scan(dir: string): Promise<void> {
|
|
1073
|
+
let entries: import('fs').Dirent[];
|
|
1074
|
+
try {
|
|
1075
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1076
|
+
} catch {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
for (const entry of entries) {
|
|
1080
|
+
if (!entry.isDirectory()) continue;
|
|
1081
|
+
if (entry.name === 'node_modules' || entry.name === '__tests__' || entry.name === 'dist') continue;
|
|
1082
|
+
const entryPath = path.join(dir, entry.name);
|
|
1083
|
+
if (existsSync(path.join(entryPath, 'CONTEXT.md'))) {
|
|
1084
|
+
found.add(entryPath);
|
|
1085
|
+
}
|
|
1086
|
+
await scan(entryPath);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
for (const root of srcDirs) {
|
|
1091
|
+
const rootPath = path.join(projectPath, root);
|
|
1092
|
+
if (existsSync(path.join(rootPath, 'CONTEXT.md'))) {
|
|
1093
|
+
found.add(rootPath);
|
|
1094
|
+
}
|
|
1095
|
+
await scan(rootPath);
|
|
1096
|
+
}
|
|
1097
|
+
return found.size;
|
|
1098
|
+
}
|