ubiquitous-invention/apps/web/server/lib/repo-root.ts

45 lines
1.5 KiB
TypeScript
Raw Permalink Normal View History

feat(markdown-backlog): close the sync loop with DB → frontmatter export Until now the markdown importer was one-way (plans/*.md → DB). Any agent-driven status flip via claim_task / complete_task would be clobbered on the next importer sweep. This change closes the loop: the DB now projects status, priority, agent_prompt, and updated_at back into the file's frontmatter, preserving body bytes, key order, and every other frontmatter key. New: packages/database/src/markdown-backlog/export.ts - `rewriteFrontmatter()` — pure function, covered by 10 Vitest cases (round-trip identity, status flip, priority flip, agent_prompt null/block-scalar/single-line variants, body preservation, trailing-newline preservation, idempotent re-application). - `exportBacklogItemToMarkdown()` — DB-loading wrapper with atomic write (tmp + rename) and tenant fencing. Returns a structured result so callers can surface what happened in their response. Wired into: - `claim_task` MCP tool — exports on the ready → in_progress flip. - `complete_task` MCP tool — exports on any finalStatus transition. - `backlog.updateWorkflowPrompt` tRPC mutation — exports on prompt edits made through the app UI. Robust repo-root resolution (`apps/{mcp-server,web}/src/lib/repo-root.ts`, plus a copy in `import-markdown-backlog.ts`): walk up from the source file looking for `pnpm-workspace.yaml`, falling back to env var or cwd. This fixes a class of bug where `pnpm --filter <pkg>` cd's into the package directory and breaks naive cwd-based path resolution — the importer was deleting all 44 rows during smoke testing before this fix because it found zero files in `packages/database/plans/`. `config/CursorSync.md`: documents the new two-way contract, the DB-wins-on-allow-list conflict policy, and the MARKDOWN_BACKLOG_REPO_ROOT=off escape hatch for production deployments where `plans/` isn't checked out. Smoke verified end-to-end against the homelab DB: claim flips file status to in_progress, complete flips it back to ready, importer round-trips with stable content_hash (true no-op), agent identity preserved throughout. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 11:21:22 -04:00
/**
* Resolve the repo root for the markdown exporter when called from tRPC
* mutations. Sibling of `apps/mcp-server/src/lib/repo-root.ts`; same
* contract (env var > pnpm-workspace.yaml walk > cwd), but lives here
* because the web app's tsconfig and bundle layout differ enough from
* the MCP server's that a single shared helper would need its own
* package. Duplication is cheap; the function is twenty lines.
*
* In production (Coolify) there is no `plans/` directory and no
* `pnpm-workspace.yaml` in the bundled image, so the walk returns null
* and the exporter is skipped cleanly.
*/
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const WORKSPACE_MARKER = "pnpm-workspace.yaml";
const MAX_WALK = 10;
function findByMarker(start: string): string | null {
let dir = start;
for (let i = 0; i < MAX_WALK; i++) {
if (existsSync(join(dir, WORKSPACE_MARKER))) return dir;
const parent = dirname(dir);
if (parent === dir) return null;
dir = parent;
}
return null;
}
export function resolveRepoRoot(): string | null {
const env = process.env.MARKDOWN_BACKLOG_REPO_ROOT?.trim();
if (env === "" || env === "0" || env?.toLowerCase() === "off") return null;
if (env && env.length > 0) return env;
const fileDir = dirname(fileURLToPath(import.meta.url));
const fromFile = findByMarker(fileDir);
if (fromFile) return fromFile;
const fromCwd = findByMarker(process.cwd());
if (fromCwd) return fromCwd;
return null;
}