/** * Sync utilities — pure functions for git diff parsing, filtering, and slug management. * * SYNC DATA FLOW: * git diff --name-status -M LAST..HEAD * │ * buildSyncManifest() → parse A/M/D/R lines * │ * isSyncable() → filter to .md pages only * │ * pathToSlug() → convert file paths to page slugs */ export interface SyncManifest { added: string[]; modified: string[]; deleted: string[]; renamed: Array<{ from: string; to: string }>; } export interface RawManifestEntry { action: 'A' | 'M' | 'D' | 'R'; path: string; oldPath?: string; } /** * Parse the output of `git diff --name-status -M LAST..HEAD` into structured entries. * * Input format (tab-separated): * A path/to/new-file.md * M path/to/modified-file.md * D path/to/deleted-file.md * R100 old/path.md new/path.md */ export function buildSyncManifest(gitDiffOutput: string): SyncManifest { const manifest: SyncManifest = { added: [], modified: [], deleted: [], renamed: [], }; const lines = gitDiffOutput.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; const parts = trimmed.split('\t'); if (parts.length < 2) continue; const action = parts[0]; const path = parts[parts.length === 3 ? 2 : 1]; // For renames, new path is 3rd column if (action === 'A') { manifest.added.push(path); } else if (action === 'M') { manifest.modified.push(path); } else if (action === 'D') { manifest.deleted.push(parts[1]); } else if (action.startsWith('R')) { // Rename: R100\told-path\tnew-path const oldPath = parts[1]; const newPath = parts[2]; if (oldPath && newPath) { manifest.renamed.push({ from: oldPath, to: newPath }); } } } return manifest; } /** * Filter a file path to determine if it should be synced to GBrain. */ export function isSyncable(path: string): boolean { // Must be .md if (!path.endsWith('.md')) return false; // Skip hidden directories if (path.split('/').some(p => p.startsWith('.'))) return false; // Skip .raw/ sidecar directories if (path.includes('.raw/')) return false; // Skip meta files that aren't pages const skipFiles = ['schema.md', 'index.md', 'log.md', 'README.md']; const basename = path.split('/').pop() || ''; if (skipFiles.includes(basename)) return false; // Skip ops/ directory if (path.startsWith('ops/')) return false; return true; } /** * Convert a repo-relative file path to a GBrain page slug. * * Examples: * people/pedro-franceschi.md → people/pedro-franceschi * daily/2026-04-05.md → daily/2026-04-05 * notes.md → notes */ export function pathToSlug(filePath: string, repoPrefix?: string): string { // Strip .md extension let slug = filePath.replace(/\.md$/, ''); // Normalize separators slug = slug.replace(/\\/g, '/'); // Strip leading slash slug = slug.replace(/^\//, ''); // Add repo prefix for multi-repo setups if (repoPrefix) slug = `${repoPrefix}/${slug}`; return slug; }