126 lines
4.5 KiB
Python
126 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Dry-run or apply ECHO vault schema migrations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
|
|
import echo # noqa: E402
|
|
|
|
|
|
CURRENT_SCHEMA = 2
|
|
|
|
|
|
def get_text(path: str) -> str | None:
|
|
status, body = echo.request("GET", echo.vault_url(path))
|
|
if status == 404:
|
|
return None
|
|
echo.check(status, body, f"get {path}")
|
|
return body.decode(errors="replace")
|
|
|
|
|
|
def list_files(path: str) -> list[str]:
|
|
if not path.endswith("/"):
|
|
path += "/"
|
|
status, body = echo.request("GET", echo.vault_url(path))
|
|
if status == 404:
|
|
return []
|
|
echo.check(status, body, f"ls {path}")
|
|
try:
|
|
payload = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
return []
|
|
return [entry for entry in payload.get("files", []) if not entry.endswith("/")]
|
|
|
|
|
|
def put_text(path: str, text: str) -> None:
|
|
status, body = echo.request("PUT", echo.vault_url(path), data=text.encode(), headers={"Content-Type": "text/markdown"})
|
|
echo.check(status, body, f"put {path}")
|
|
|
|
|
|
def delete(path: str) -> None:
|
|
status, body = echo.request("DELETE", echo.vault_url(path))
|
|
if status != 404:
|
|
echo.check(status, body, f"delete {path}")
|
|
|
|
|
|
def move(src: str, dst: str) -> None:
|
|
text = get_text(src)
|
|
if text is None:
|
|
return
|
|
put_text(dst, text)
|
|
delete(src)
|
|
|
|
|
|
def do_or_show(apply: bool, desc: str, func=None) -> None:
|
|
if apply:
|
|
print(f"migrate: APPLY {desc}")
|
|
if func:
|
|
func()
|
|
else:
|
|
print(f"migrate: PLAN {desc}")
|
|
|
|
|
|
def marker_schema() -> int:
|
|
marker = get_text("_agent/echo-vault.md")
|
|
if marker is None:
|
|
print("migrate: marker missing - vault not bootstrapped. Run bootstrap.py, not migrate.py.")
|
|
raise SystemExit(3)
|
|
for line in marker.splitlines():
|
|
if line.startswith("schema_version:"):
|
|
try:
|
|
return int(line.split(":", 1)[1].strip())
|
|
except ValueError:
|
|
return 0
|
|
return 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Migrate ECHO vault schema")
|
|
parser.add_argument("--apply", action="store_true")
|
|
args = parser.parse_args(argv)
|
|
|
|
start = marker_schema()
|
|
print(f"migrate: vault schema_version={start}, plugin schema={CURRENT_SCHEMA} {'(APPLY)' if args.apply else '(dry-run)'}")
|
|
if start >= CURRENT_SCHEMA:
|
|
print("migrate: up to date - nothing to do.")
|
|
return 0
|
|
|
|
if start < 1:
|
|
print("migrate: [0->1] retire in-vault control docs")
|
|
for filename in ["CLAUDE.md", "BOOTSTRAP.md", "STRUCTURE.md", "index.md"]:
|
|
if get_text(filename) is not None:
|
|
do_or_show(args.apply, f"delete vault/{filename} after external backup", lambda f=filename: delete(f))
|
|
print("migrate: [0->1] reminder: scrub dangling control-doc links from Related sections.")
|
|
|
|
if start < 2:
|
|
print("migrate: [1->2] fold reviews/ into journal/ and _agent/health/")
|
|
for filename in list_files("reviews/weekly"):
|
|
if filename.endswith(".md"):
|
|
dst = f"journal/weekly/{filename.replace('-review.md', '.md')}"
|
|
do_or_show(args.apply, f"move reviews/weekly/{filename} -> {dst}", lambda f=filename, d=dst: move(f"reviews/weekly/{f}", d))
|
|
for filename in list_files("reviews/monthly"):
|
|
if filename.endswith(".md"):
|
|
dst = f"_agent/health/{filename}" if filename.endswith("vault-health.md") else f"journal/monthly/{filename}"
|
|
do_or_show(args.apply, f"move reviews/monthly/{filename} -> {dst}", lambda f=filename, d=dst: move(f"reviews/monthly/{f}", d))
|
|
for period in ["quarterly", "annual"]:
|
|
for filename in list_files(f"reviews/{period}"):
|
|
if filename.endswith(".md"):
|
|
dst = f"journal/{period}/{filename}"
|
|
do_or_show(args.apply, f"move reviews/{period}/{filename} -> {dst}", lambda p=period, f=filename, d=dst: move(f"reviews/{p}/{f}", d))
|
|
print("migrate: [1->2] reminder: update inbound reviews wikilinks manually.")
|
|
|
|
do_or_show(args.apply, f"set _agent/echo-vault.md schema_version -> {CURRENT_SCHEMA}", lambda: echo.cmd_fm("_agent/echo-vault.md", "schema_version", str(CURRENT_SCHEMA)))
|
|
print("migrate: migration complete." if args.apply else "migrate: dry-run only. Re-run with --apply to perform changes.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|