212 lines
9.3 KiB
Python
212 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Profile path resolution for AI Workspace scripts.
|
|
|
|
Profiles own their configuration. Reusable scripts should call this module
|
|
instead of hardcoding project paths.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import argparse
|
|
import re
|
|
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}",
|
|
}
|
|
|
|
PROFILE_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
|
KNOWLEDGE_DIRS = [
|
|
"00-start",
|
|
"01-current",
|
|
"02-work-items",
|
|
"03-context/process",
|
|
"03-context/systems",
|
|
"03-context/workstreams",
|
|
"04-people",
|
|
"05-decisions",
|
|
"06-daily",
|
|
"07-maps",
|
|
"08-bases",
|
|
"09-templates",
|
|
"attachments",
|
|
]
|
|
|
|
|
|
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 validate_profile_name(profile: str) -> None:
|
|
if not PROFILE_RE.match(profile):
|
|
raise SystemExit("profile must be lowercase letters, numbers, dashes, or underscores")
|
|
|
|
|
|
def write_file_once(path: Path, text: str) -> None:
|
|
if path.exists():
|
|
return
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
|
|
|
|
def create_profile(profile: str, display_name: str | None = None, root: Path | None = None) -> dict[str, Any]:
|
|
validate_profile_name(profile)
|
|
base = root or ROOT
|
|
display = display_name or profile.replace("-", " ").replace("_", " ").title()
|
|
profile_dir = base / "profiles" / profile
|
|
knowledge = knowledge_dir(profile, root=base)
|
|
inbox = inbox_dir(profile, root=base)
|
|
index = index_dir(profile, root=base)
|
|
|
|
if profile_dir.exists() or knowledge.exists() or inbox.exists():
|
|
raise SystemExit(f"profile already exists or has data directories: {profile}")
|
|
|
|
profile_dir.mkdir(parents=True)
|
|
write_file_once(profile_dir / "profile.md", f"# {display} Profile\n\n## Purpose\n\nDescribe how this project uses AI Workspace.\n\n## Project\n\n- Name: {display}\n- Workspace role: companion AI workspace\n\n## Communication Sources\n\n- Live communication: configure if needed\n- Historical archive: optional\n\n## Work System\n\n- Use the profile project knowledge vault for work items and current state.\n")
|
|
write_file_once(profile_dir / "workspace.json", json.dumps({
|
|
"profile": profile,
|
|
"display_name": display,
|
|
"description": f"AI Workspace profile for {display}.",
|
|
"knowledge_dir": relative_to_root(knowledge, root=base),
|
|
"inbox_dir": relative_to_root(inbox, root=base),
|
|
"index_dir": relative_to_root(index, root=base),
|
|
}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
|
|
write_file_once(profile_dir / "services.json", json.dumps({"profile": profile, "description": f"Local services for {display}.", "services": {}}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
|
|
write_file_once(profile_dir / "context-sources.json", json.dumps({"communication_sources": {}}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
|
|
|
|
for folder in KNOWLEDGE_DIRS:
|
|
(knowledge / folder).mkdir(parents=True, exist_ok=True)
|
|
inbox.mkdir(parents=True, exist_ok=True)
|
|
write_file_once(knowledge / "00-start" / "start-here.md", f"# {display} Start Here\n\nUse this note as the entry point for the {display} project knowledge vault.\n")
|
|
write_file_once(knowledge / "01-current" / "current-work.md", "# Current Work\n\n## Focus\n\n- TBD\n")
|
|
write_file_once(knowledge / "01-current" / "work-items.md", "# Work Items\n\nNo active work items recorded yet.\n")
|
|
write_file_once(knowledge / "03-context" / "project.md", f"# {display} Project Context\n\nDurable project context goes here.\n")
|
|
write_file_once(knowledge / "04-people" / "index.md", "# People\n\nProject people and role mappings go here.\n")
|
|
write_file_once(knowledge / "07-maps" / "index.md", "# Maps\n\nNavigation maps go here.\n")
|
|
write_file_once(knowledge / "09-templates" / "work-item.md", "---\ntype: work-item\ntitle: {{title}}\nupdated: {{date}}\n---\n\n# {{title}}\n")
|
|
write_file_once(knowledge / "09-templates" / "daily.md", "---\ntype: daily\ndate: {{date}}\nupdated: {{date}}\n---\n\n# {{date}}\n")
|
|
write_file_once(inbox / "README.md", f"# {display} Inbox\n\nRaw evidence for this profile. Promote durable facts into the project knowledge vault.\n")
|
|
return {"profile": profile, "profile_dir": relative_to_root(profile_dir, root=base), "knowledge_dir": relative_to_root(knowledge, root=base), "inbox_dir": relative_to_root(inbox, root=base)}
|
|
|
|
|
|
def doctor(profile: str, root: Path | None = None) -> dict[str, Any]:
|
|
base = root or ROOT
|
|
config_path = workspace_config_path(profile, root=base)
|
|
knowledge = knowledge_dir(profile, root=base)
|
|
inbox = inbox_dir(profile, root=base)
|
|
checks = {
|
|
"profile_config_exists": config_path.is_file(),
|
|
"knowledge_dir_exists": knowledge.is_dir(),
|
|
"inbox_dir_exists": inbox.is_dir(),
|
|
"start_here_exists": (knowledge / "00-start" / "start-here.md").is_file(),
|
|
"current_work_exists": (knowledge / "01-current" / "current-work.md").is_file(),
|
|
"work_items_exists": (knowledge / "01-current" / "work-items.md").is_file(),
|
|
"root_project_knowledge_absent": not (base / "project-knowledge").exists(),
|
|
"root_ai_inbox_absent": not (base / "ai" / "inbox").exists(),
|
|
}
|
|
return {"profile": profile, "ok": all(checks.values()), "checks": checks, "paths": {"knowledge_dir": relative_to_root(knowledge, root=base), "inbox_dir": relative_to_root(inbox, root=base), "index_dir": relative_to_root(index_dir(profile, root=base), root=base)}}
|
|
|
|
|
|
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")
|
|
|
|
create_parser = subparsers.add_parser("create", help="Create a new isolated project profile.")
|
|
create_parser.add_argument("profile")
|
|
create_parser.add_argument("--display-name", default="")
|
|
|
|
doctor_parser = subparsers.add_parser("doctor", help="Validate profile workspace layout.")
|
|
doctor_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
|
|
|
|
if args.command == "create":
|
|
print(json.dumps(create_profile(args.profile, args.display_name or None), ensure_ascii=False, indent=2, sort_keys=True))
|
|
return
|
|
|
|
if args.command == "doctor":
|
|
payload = doctor(args.profile)
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
raise SystemExit(0 if payload["ok"] else 1)
|
|
|
|
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()
|