Skip to main content

#!/usr/bin/env python3 """ Register Project for Dashboard UI (J.29.20)

Registers a project in projects.db and adds it to the trajectory dashboard visibility config (dashboard-projects.json).

Task ID: J.29.20 Created: 2026-02-09 ADR: ADR-163

Usage: python3 scripts/register_project_dashboard.py "coditect-core" python3 scripts/register_project_dashboard.py "My Project" --path ~/PROJECTS/my-project python3 scripts/register_project_dashboard.py --list python3 scripts/register_project_dashboard.py "old-project" --remove """

import argparse import json import sqlite3 import sys from datetime import datetime, timezone from pathlib import Path

Add parent to path for imports

sys.path.insert(0, str(Path(file).parent.parent))

try: from scripts.core.paths import get_projects_db_path except ImportError: def get_projects_db_path() -> Path: home = Path.home() candidates = [ home / "PROJECTS" / ".coditect-data" / "context-storage" / "projects.db", home / ".coditect-data" / "context-storage" / "projects.db", ] for c in candidates: if c.exists(): return c return candidates[0]

def get_dashboard_config_path() -> Path: """Resolve path to dashboard-projects.json.""" script_dir = Path(file).resolve().parent return script_dir.parent / "tools" / "trajectory-dashboard" / "public" / "dashboard-projects.json"

def load_config(config_path: Path) -> dict: """Load dashboard visibility config.""" if config_path.exists(): try: with open(config_path) as f: return json.load(f) except (json.JSONDecodeError, OSError): pass return { "visibleProjects": [], "hiddenProjects": [], "showDiscovered": False, "lastUpdated": "", "version": "1.0.0", }

def save_config(config: dict, config_path: Path) -> None: """Save dashboard visibility config.""" config_path.parent.mkdir(parents=True, exist_ok=True) config["lastUpdated"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") with open(config_path, "w") as f: json.dump(config, f, indent=2) f.write("\n")

def check_projects_db(name: str) -> dict | None: """Check if project exists in projects.db.""" db_path = get_projects_db_path() if not db_path.exists(): return None try: conn = sqlite3.connect(str(db_path)) conn.row_factory = sqlite3.Row cursor = conn.execute( "SELECT name, project_type, status, path FROM projects WHERE LOWER(name) = LOWER(?)", (name,), ) row = cursor.fetchone() conn.close() if row: return dict(row) except sqlite3.Error: pass return None

def list_projects(config_path: Path) -> None: """List currently visible projects.""" config = load_config(config_path) visible = config.get("visibleProjects", [])

if not visible:
print("No projects configured for dashboard visibility.")
print(f"Config: {config_path}")
return

print(f"Dashboard Visible Projects ({len(visible)}):")
for i, name in enumerate(visible, 1):
db_info = check_projects_db(name)
source = "unknown"
if db_info:
source = "projects.db"
else:
# Check session-logs
logs_dir = Path.home() / "PROJECTS" / ".coditect-data" / "session-logs" / "projects"
if logs_dir.exists():
for d in logs_dir.iterdir():
if d.is_dir() and d.name.lower() == name.lower():
source = "session-logs"
break
print(f" {i}. {name:<30} [{source}]")

print(f"\nConfig: {config_path}")
updated = config.get("lastUpdated", "never")
print(f"Last Updated: {updated}")

def register(name: str, config_path: Path, remove: bool = False) -> bool: """Register or remove a project from dashboard visibility.""" config = load_config(config_path) visible = config.get("visibleProjects", [])

if remove:
original_len = len(visible)
visible = [v for v in visible if v.lower() != name.lower()]
if len(visible) == original_len:
print(f"'{name}' was not in visibleProjects — nothing to remove.")
return False
config["visibleProjects"] = visible
save_config(config, config_path)
print(f"Removed '{name}' from dashboard visibility.")
print(f"Remaining: {len(visible)} projects")
return True

# Check if already visible
if any(v.lower() == name.lower() for v in visible):
print(f"'{name}' is already in dashboard visibleProjects.")
return True

# Add to visible list
visible.append(name)
config["visibleProjects"] = visible
save_config(config, config_path)
print(f"Added '{name}' to dashboard visibleProjects.")
return True

def main(): parser = argparse.ArgumentParser( description="Register a project for the trajectory dashboard UI" ) parser.add_argument("name", nargs="?", help="Project name to register") parser.add_argument("--path", help="Project root directory path") parser.add_argument("--description", help="Project description") parser.add_argument("--source", help="Source hint (projects.db, session-logs, sessions.db)") parser.add_argument("--remove", action="store_true", help="Remove from dashboard visibility") parser.add_argument("--list", action="store_true", help="List visible projects") parser.add_argument("--config", help="Path to dashboard-projects.json (auto-detected)")

args = parser.parse_args()

config_path = Path(args.config) if args.config else get_dashboard_config_path()

if args.list:
list_projects(config_path)
return

if not args.name:
parser.error("Project name is required (or use --list)")

# Step 1: Check projects.db
db_info = check_projects_db(args.name)
if db_info:
print(f"Database: projects.db — {db_info['name']} (type={db_info['project_type']}, status={db_info['status']})")
else:
print(f"Database: Not in projects.db (will appear as session-derived in adapter)")
if args.path:
print(f" Hint: Use '/cx --register-project {args.path}' to add to projects.db")

# Step 2: Update dashboard-projects.json
success = register(args.name, config_path, remove=args.remove)

if success and not args.remove:
# Show summary
config = load_config(config_path)
visible = config.get("visibleProjects", [])
print(f"\nDashboard Visible Projects ({len(visible)}):")
for i, v in enumerate(visible, 1):
marker = " *" if v.lower() == args.name.lower() else ""
print(f" {i}. {v}{marker}")
print(f"\nConfig: {config_path}")

if name == "main": main()