* Sync rules: for each tool's rules/ directory, find rule files that differ * from the team repo and check whether the diff is from a team update.
( teamConfig: TeamaiConfig, localConfig: LocalConfig, repoPath: string, baseDir: string, lastPullRev: string, )
| 58 | * from the team repo and check whether the diff is from a team update. |
| 59 | */ |
| 60 | async function syncRulesToLocal( |
| 61 | teamConfig: TeamaiConfig, |
| 62 | localConfig: LocalConfig, |
| 63 | repoPath: string, |
| 64 | baseDir: string, |
| 65 | lastPullRev: string, |
| 66 | ): Promise<void> { |
| 67 | const teamRulesDir = path.join(repoPath, 'rules'); |
| 68 | if (!await pathExists(teamRulesDir)) return; |
| 69 | |
| 70 | for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) { |
| 71 | if (!toolPath.rules) continue; |
| 72 | if (!await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) continue; |
| 73 | |
| 74 | const rulesDir = path.join(baseDir, toolPath.rules); |
| 75 | if (!await pathExists(rulesDir)) continue; |
| 76 | |
| 77 | // Cursor-compatible copies are `.mdc` with derived frontmatter; every other |
| 78 | // tool's are verbatim `.md`. Matching only `.md` would skip `.mdc` tools, |
| 79 | // leaving a stale copy that push then reports as "modified" and sends |
| 80 | // upstream — silently reverting the teammate's update. |
| 81 | const ext = ruleFileExtensionForTool(tool); |
| 82 | const isMdcTool = usesCursorMdcRules(tool); |
| 83 | |
| 84 | const files = await listFilesRecursive(rulesDir); |
| 85 | for (const file of files) { |
| 86 | if (!file.endsWith(ext)) continue; |
| 87 | const name = file.slice(0, -ext.length); |
| 88 | if (EXCLUDED_RULE_NAMES.has(name)) continue; |
| 89 | |
| 90 | const localFilePath = path.join(rulesDir, file); |
| 91 | // The team repo always stores the tool-neutral `.md`. |
| 92 | const teamRelPath = `rules/${name}.md`; |
| 93 | const teamFilePath = path.join(teamRulesDir, `${name}.md`); |
| 94 | |
| 95 | // Only process files that exist in both places but differ |
| 96 | if (!await pathExists(teamFilePath)) continue; |
| 97 | if (isMdcTool) { |
| 98 | const localRaw = await readFileSafe(localFilePath); |
| 99 | const teamRaw = await readFileSafe(teamFilePath); |
| 100 | if (localRaw === null || teamRaw === null) continue; |
| 101 | if (cursorMdcBodyEqualsTeamMd(localRaw, teamRaw)) continue; |
| 102 | |
| 103 | const oldContent = await getFileContentAtRev(repoPath, lastPullRev, teamRelPath); |
| 104 | if (oldContent === null) continue; // Didn't exist at lastPullRev — ambiguous, skip |
| 105 | |
| 106 | // Compare bodies: the local `.mdc` never matched the team `.md` byte for byte. |
| 107 | if (cursorMdcBodyEqualsTeamMd(localRaw, oldContent.toString('utf-8'))) { |
| 108 | await writeFile(localFilePath, teamRuleToCursorMdc(teamRaw)); |
| 109 | log.debug(`Pre-push sync: updated ${tool} rule ${name} to match team repo`); |
| 110 | } |
| 111 | continue; |
| 112 | } |
| 113 | |
| 114 | if (await fileContentEqual(localFilePath, teamFilePath)) continue; |
| 115 | |
| 116 | // They differ — check if local matches the old team repo version |
| 117 | const oldContent = await getFileContentAtRev(repoPath, lastPullRev, teamRelPath); |
no test coverage detected