packages_context.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. #!/usr/bin/env python3
  2. """
  3. Package discovery and context output.
  4. Provides:
  5. get_packages_info - Get structured package info
  6. get_packages_section - Build PACKAGES text section
  7. get_context_packages_text - Full packages text output (--mode packages)
  8. get_context_packages_json - Full packages JSON output (--mode packages --json)
  9. """
  10. from __future__ import annotations
  11. from pathlib import Path
  12. from .config import _is_true_config_value, get_default_package, get_packages, get_spec_scope
  13. from .paths import (
  14. DIR_SPEC,
  15. DIR_WORKFLOW,
  16. get_current_task,
  17. get_repo_root,
  18. )
  19. from .tasks import load_task
  20. # =============================================================================
  21. # Internal Helpers
  22. # =============================================================================
  23. def _scan_spec_layers(spec_dir: Path, package: str | None = None) -> list[str]:
  24. """Scan spec directory for available layers (subdirectories).
  25. For monorepo: scans spec/<package>/
  26. For single-repo: scans spec/
  27. """
  28. target = spec_dir / package if package else spec_dir
  29. if not target.is_dir():
  30. return []
  31. return sorted(
  32. d.name for d in target.iterdir() if d.is_dir() and d.name != "guides"
  33. )
  34. def _get_active_task_package(repo_root: Path) -> str | None:
  35. """Get the package field from the active task's task.json."""
  36. current = get_current_task(repo_root)
  37. if not current:
  38. return None
  39. ct = load_task(repo_root / current)
  40. return ct.package if ct and ct.package else None
  41. def _resolve_scope_set(
  42. packages: dict,
  43. spec_scope,
  44. task_pkg: str | None,
  45. default_pkg: str | None,
  46. ) -> set | None:
  47. """Resolve spec_scope to a set of allowed package names, or None for full scan."""
  48. if not packages:
  49. return None
  50. if spec_scope is None:
  51. return None
  52. if isinstance(spec_scope, str) and spec_scope == "active_task":
  53. if task_pkg and task_pkg in packages:
  54. return {task_pkg}
  55. if default_pkg and default_pkg in packages:
  56. return {default_pkg}
  57. return None
  58. if isinstance(spec_scope, list):
  59. valid = {e for e in spec_scope if e in packages}
  60. if valid:
  61. return valid
  62. # All invalid: fallback
  63. if task_pkg and task_pkg in packages:
  64. return {task_pkg}
  65. if default_pkg and default_pkg in packages:
  66. return {default_pkg}
  67. return None
  68. return None
  69. # =============================================================================
  70. # Public Functions
  71. # =============================================================================
  72. def get_packages_info(repo_root: Path) -> list[dict]:
  73. """Get structured package info for monorepo projects.
  74. Returns list of dicts with keys: name, path, type, default, specLayers,
  75. isSubmodule, isGitRepo.
  76. Returns empty list for single-repo projects.
  77. """
  78. packages = get_packages(repo_root)
  79. if not packages:
  80. return []
  81. default_pkg = get_default_package(repo_root)
  82. spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
  83. result = []
  84. for pkg_name, pkg_config in packages.items():
  85. pkg_path = pkg_config.get("path", pkg_name) if isinstance(pkg_config, dict) else str(pkg_config)
  86. pkg_type = pkg_config.get("type", "local") if isinstance(pkg_config, dict) else "local"
  87. pkg_git = pkg_config.get("git", False) if isinstance(pkg_config, dict) else False
  88. layers = _scan_spec_layers(spec_dir, pkg_name)
  89. result.append({
  90. "name": pkg_name,
  91. "path": pkg_path,
  92. "type": pkg_type,
  93. "default": pkg_name == default_pkg,
  94. "specLayers": layers,
  95. "isSubmodule": pkg_type == "submodule",
  96. "isGitRepo": _is_true_config_value(pkg_git),
  97. })
  98. return result
  99. def get_packages_section(repo_root: Path) -> str:
  100. """Build the PACKAGES section for text output."""
  101. spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
  102. pkg_info = get_packages_info(repo_root)
  103. lines: list[str] = []
  104. lines.append("## PACKAGES")
  105. if not pkg_info:
  106. lines.append("(single-repo mode)")
  107. layers = _scan_spec_layers(spec_dir)
  108. if layers:
  109. lines.append(f"Spec layers: {', '.join(layers)}")
  110. return "\n".join(lines)
  111. default_pkg = get_default_package(repo_root)
  112. for pkg in pkg_info:
  113. layers_str = f" [{', '.join(pkg['specLayers'])}]" if pkg["specLayers"] else ""
  114. submodule_tag = " (submodule)" if pkg["isSubmodule"] else ""
  115. git_repo_tag = " (git repo)" if pkg["isGitRepo"] else ""
  116. default_tag = " *" if pkg["default"] else ""
  117. lines.append(
  118. f"- {pkg['name']:<16} {pkg['path']:<20}{layers_str}{submodule_tag}{git_repo_tag}{default_tag}"
  119. )
  120. if default_pkg:
  121. lines.append(f"Default package: {default_pkg}")
  122. return "\n".join(lines)
  123. def get_context_packages_text(repo_root: Path | None = None) -> str:
  124. """Get packages context as formatted text (for --mode packages)."""
  125. if repo_root is None:
  126. repo_root = get_repo_root()
  127. pkg_info = get_packages_info(repo_root)
  128. lines: list[str] = []
  129. if not pkg_info:
  130. spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
  131. lines.append("Single-repo project (no packages configured)")
  132. lines.append("")
  133. layers = _scan_spec_layers(spec_dir)
  134. if layers:
  135. lines.append(f"Spec layers: {', '.join(layers)}")
  136. return "\n".join(lines)
  137. # Resolve scope for annotations
  138. packages_dict = get_packages(repo_root) or {}
  139. default_pkg = get_default_package(repo_root)
  140. spec_scope = get_spec_scope(repo_root)
  141. task_pkg = _get_active_task_package(repo_root)
  142. scope_set = _resolve_scope_set(packages_dict, spec_scope, task_pkg, default_pkg)
  143. lines.append("## PACKAGES")
  144. lines.append("")
  145. for pkg in pkg_info:
  146. default_tag = " (default)" if pkg["default"] else ""
  147. type_tag = f" [{pkg['type']}]" if pkg["type"] != "local" else ""
  148. git_tag = " [git repo]" if pkg["isGitRepo"] else ""
  149. # Scope annotation
  150. scope_tag = ""
  151. if scope_set is not None and pkg["name"] not in scope_set:
  152. scope_tag = " (out of scope)"
  153. lines.append(f"### {pkg['name']}{default_tag}{type_tag}{git_tag}{scope_tag}")
  154. lines.append(f"Path: {pkg['path']}")
  155. if pkg["specLayers"]:
  156. lines.append(f"Spec layers: {', '.join(pkg['specLayers'])}")
  157. for layer in pkg["specLayers"]:
  158. lines.append(f" - .trellis/spec/{pkg['name']}/{layer}/index.md")
  159. else:
  160. lines.append("Spec: not configured")
  161. lines.append("")
  162. # Also show shared guides
  163. guides_dir = repo_root / DIR_WORKFLOW / DIR_SPEC / "guides"
  164. if guides_dir.is_dir():
  165. lines.append("### Shared Guides (always included)")
  166. lines.append("Path: .trellis/spec/guides/index.md")
  167. lines.append("")
  168. return "\n".join(lines)
  169. def get_context_packages_json(repo_root: Path | None = None) -> dict:
  170. """Get packages context as a dictionary (for --mode packages --json)."""
  171. if repo_root is None:
  172. repo_root = get_repo_root()
  173. pkg_info = get_packages_info(repo_root)
  174. if not pkg_info:
  175. spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
  176. layers = _scan_spec_layers(spec_dir)
  177. return {
  178. "mode": "single-repo",
  179. "specLayers": layers,
  180. }
  181. default_pkg = get_default_package(repo_root)
  182. spec_scope = get_spec_scope(repo_root)
  183. task_pkg = _get_active_task_package(repo_root)
  184. return {
  185. "mode": "monorepo",
  186. "packages": pkg_info,
  187. "defaultPackage": default_pkg,
  188. "specScope": spec_scope,
  189. "activeTaskPackage": task_pkg,
  190. }