feat(repos): multi-machine path support via host_paths

Adds a JSONB column `host_paths` to managed_repos mapping
hostname → absolute local path. Fixes the consistency-checker
failure when the same repo lives at different paths on different
machines (e.g. /home/worsch/marki-docx on the workstation vs
/home/tegwick/marki-docx on custodiancore).

Changes:
- Migration g4b5c6d7e8f9: adds host_paths JSONB (default {})
- Model: host_paths Mapped[dict] column
- Schemas: host_paths in RepoRead; new RepoPathRegister schema
- Router: POST /repos/{slug}/paths/ — merges one host entry
- consistency_check.py: resolve_repo_path() prefers host_paths
  [hostname] over local_path; --repo-path CLI override added
- MCP: update_repo_path(slug, path, host?) tool
- Makefile: register-path target; REPO_PATH passthrough on
  check-consistency and fix-consistency targets
- TOOLS.md: documents update_repo_path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 16:30:55 +01:00
parent d66f23026d
commit 82552b8d59
8 changed files with 133 additions and 11 deletions

View File

@@ -33,6 +33,7 @@ from __future__ import annotations
import argparse
import json
import re
import socket
import sys
from dataclasses import dataclass, field
from pathlib import Path
@@ -294,7 +295,22 @@ def _api_post(api_base: str, path: str, body: dict) -> Any:
# Core check engine
# ---------------------------------------------------------------------------
def check_repo(api_base: str, repo_slug: str) -> ConsistencyReport:
def resolve_repo_path(repo: dict, override: str | None = None) -> str:
"""Resolve the local filesystem path for a repo on the current machine.
Priority:
1. Explicit --repo-path CLI override
2. host_paths[current_hostname] — per-machine path registered via POST /repos/{slug}/paths/
3. local_path — legacy single-path field (backward compat)
"""
if override:
return override
hostname = socket.gethostname()
host_paths = repo.get("host_paths") or {}
return host_paths.get(hostname) or repo.get("local_path") or ""
def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = None) -> ConsistencyReport:
"""Run all consistency checks for a registered repo."""
repo = _api_get(api_base, f"/repos/{repo_slug}")
if repo is None:
@@ -307,7 +323,7 @@ def check_repo(api_base: str, repo_slug: str) -> ConsistencyReport:
return report
repo_id: str = repo["id"]
repo_path: str = repo.get("local_path") or ""
repo_path: str = resolve_repo_path(repo, repo_path_override)
report = ConsistencyReport(repo_slug=repo_slug, repo_path=repo_path)
if not repo_path:
@@ -607,9 +623,9 @@ def _check_orphan_db(
# Fix engine
# ---------------------------------------------------------------------------
def fix_repo(api_base: str, repo_slug: str) -> ConsistencyReport:
def fix_repo(api_base: str, repo_slug: str, repo_path_override: str | None = None) -> ConsistencyReport:
"""Run checks then apply all auto-fixable issues. Returns updated report."""
report = check_repo(api_base, repo_slug)
report = check_repo(api_base, repo_slug, repo_path_override)
fixable = [i for i in report.issues if i.fixable]
for issue in fixable:
@@ -866,6 +882,9 @@ def main() -> None:
help="Run checks against all registered repos with local_path")
parser.add_argument("--fix", action="store_true",
help="Apply auto-fixable issues (status drift, repo mismatch, etc.)")
parser.add_argument("--repo-path", metavar="PATH", default=None,
help="Override the local repo path (useful when the DB has a different "
"machine's path). Takes priority over host_paths and local_path.")
parser.add_argument("--api-base", default="http://127.0.0.1:8000",
help="State Hub API base URL")
parser.add_argument("--json", action="store_true", dest="as_json",
@@ -887,7 +906,9 @@ def main() -> None:
repo_slugs = [args.repo]
runner = fix_repo if args.fix else check_repo
reports = [runner(args.api_base, slug) for slug in repo_slugs]
# --repo-path only applies to single-repo runs; silently ignored with --all
path_override = args.repo_path if not args.all else None
reports = [runner(args.api_base, slug, path_override) for slug in repo_slugs]
if args.as_json:
output = (