#!/usr/bin/env python3 """Profile path resolution for AI Workspace scripts. Profiles own their configuration. Reusable scripts should call this module instead of hardcoding root-level project paths. """ from __future__ import annotations import json import argparse from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[2] DEFAULT_WORKSPACE = { "knowledge_dir": "workspaces/{profile}/project-knowledge", "inbox_dir": "workspaces/{profile}/inbox", "index_dir": ".aiw/indexes/{profile}", } def workspace_config_path(profile: str, root: Path | None = None) -> Path: base = root or ROOT return base / "profiles" / profile / "workspace.json" def load_workspace_config(profile: str, root: Path | None = None) -> dict[str, Any]: base = root or ROOT config = dict(DEFAULT_WORKSPACE) config["profile"] = profile path = workspace_config_path(profile, root=base) if path.is_file(): try: loaded = json.loads(path.read_text(encoding="utf-8")) if isinstance(loaded, dict): config.update(loaded) except json.JSONDecodeError: pass return config def resolve_path(raw: str | None, *, profile: str, root: Path | None = None, fallback: str) -> Path: base = root or ROOT value = (raw or fallback).format(profile=profile) path = Path(value).expanduser() return path if path.is_absolute() else base / path def knowledge_dir(profile: str, root: Path | None = None) -> Path: config = load_workspace_config(profile, root=root) return resolve_path(config.get("knowledge_dir"), profile=profile, root=root, fallback="workspaces/{profile}/project-knowledge") def inbox_dir(profile: str, root: Path | None = None) -> Path: config = load_workspace_config(profile, root=root) return resolve_path(config.get("inbox_dir"), profile=profile, root=root, fallback="workspaces/{profile}/inbox") def index_dir(profile: str, root: Path | None = None) -> Path: config = load_workspace_config(profile, root=root) return resolve_path(config.get("index_dir"), profile=profile, root=root, fallback=".aiw/indexes/{profile}") def relative_to_root(path: Path, root: Path | None = None) -> str: base = root or ROOT try: return str(path.relative_to(base)) except ValueError: return str(path) def main() -> None: parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) path_parser = subparsers.add_parser("path", help="Print a resolved profile path.") path_parser.add_argument("kind", choices=["knowledge", "inbox", "index"]) path_parser.add_argument("--profile", default="fidelity") config_parser = subparsers.add_parser("config", help="Print resolved workspace configuration as JSON.") config_parser.add_argument("--profile", default="fidelity") args = parser.parse_args() if args.command == "path": if args.kind == "knowledge": print(knowledge_dir(args.profile)) elif args.kind == "inbox": print(inbox_dir(args.profile)) else: print(index_dir(args.profile)) return config = load_workspace_config(args.profile) config["resolved"] = { "knowledge_dir": str(knowledge_dir(args.profile)), "inbox_dir": str(inbox_dir(args.profile)), "index_dir": str(index_dir(args.profile)), } print(json.dumps(config, ensure_ascii=False, indent=2, sort_keys=True)) if __name__ == "__main__": main()