Add project register, unregister, and rename endpoints

This commit is contained in:
Alexander
2026-08-24 12:46:42 +02:00
parent 41d419e5f8
commit 7573e89b5e
2 changed files with 338 additions and 2 deletions
+88 -2
View File
@@ -343,6 +343,29 @@ def auto_register_default():
print(f"[init] Registered default project: {path}")
def register_project(raw_path: str, raw_name: str | None = None) -> tuple:
"""Register a project for raw_path. Returns (payload, status).
Paths without an .md suffix get backlog.md appended; a missing file is
created from the blank template; the name defaults to the parent dir name.
"""
path = Path(raw_path).expanduser().resolve()
if not path.name.endswith(".md"):
path = path / "backlog.md"
name = safe_project_name(raw_name or path.parent.name)
if not name:
return {"ok": False, "error": "Invalid project name"}, 400
registry = CONFIG.load_registry()
if name in registry:
return {"ok": False, "error": f"Project '{name}' already registered"}, 409
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(build_markdown("", BLANK_HISTORY), encoding="utf-8")
registry[name] = {"path": str(path), "name": name}
CONFIG.save_registry(registry)
return {"ok": True, "name": name, "path": str(path)}, 200
# ---------------------------------------------------------------------------
# HTTP Handler
# ---------------------------------------------------------------------------
@@ -358,7 +381,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
self.wfile.write(body)
@@ -418,7 +441,7 @@ class Handler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
@@ -554,6 +577,22 @@ class Handler(BaseHTTPRequestHandler):
parsed = urlparse(self.path)
path = parsed.path
if path == "/api/projects":
body = self._read_json_body_or_400()
if body is None:
return
raw_path = body.get("path")
if not isinstance(raw_path, str) or not raw_path:
self._json_response({"ok": False, "error": "Missing 'path'"}, 400)
return
raw_name = body.get("name")
if raw_name is not None and not isinstance(raw_name, str):
self._json_response({"ok": False, "error": "Invalid project name"}, 400)
return
payload, status = register_project(raw_path, raw_name)
self._json_response(payload, status)
return
if path.startswith("/api/projects/"):
name, action = self._project_route_parts(path)
body = self._read_json_body_or_400()
@@ -562,6 +601,21 @@ class Handler(BaseHTTPRequestHandler):
paths = self._resolve_project_or_404(name)
if paths is None:
return
if action == "rename":
new_name = safe_project_name(body.get("newName", ""))
if not new_name:
self._json_response({"ok": False, "error": "Invalid project name"}, 400)
return
registry = CONFIG.load_registry()
if new_name in registry:
self._json_response(
{"ok": False, "error": f"Project '{new_name}' already registered"}, 409)
return
entry = registry.pop(name)
registry[new_name] = {"path": entry["path"], "name": new_name}
CONFIG.save_registry(registry)
self._json_response({"ok": True, "name": new_name})
return
if action == "backlog":
result = write_master_at(body.get("content", ""), paths)
self._json_response(result)
@@ -671,6 +725,38 @@ class Handler(BaseHTTPRequestHandler):
self._json_response({"error": "Not found"}, 404)
def do_DELETE(self):
parsed = urlparse(self.path)
path = parsed.path
qs = parse_qs(parsed.query)
if path.startswith("/api/projects/"):
name, action = self._project_route_parts(path)
if action:
self._json_response({"error": "Not found"}, 404)
return
registry = CONFIG.load_registry()
if name not in registry:
self._json_response({"error": "Project not found"}, 404)
return
if len(registry) == 1:
self._json_response({"error": "At least one project must remain registered"}, 400)
return
master = Path(registry[name]["path"]).resolve()
del registry[name]
CONFIG.save_registry(registry)
result = {"ok": True, "name": name, "deleted": False}
if qs.get("delete_file", [""])[0] == "true" and master.exists():
try:
master.unlink()
result["deleted"] = True
except OSError as e:
result["warning"] = f"Could not delete file: {e}"
self._json_response(result)
return
self._json_response({"error": "Not found"}, 404)
# ---------------------------------------------------------------------------
# Main