worktree.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. from __future__ import annotations
  2. import json
  3. import subprocess
  4. from contextlib import suppress
  5. from pathlib import Path
  6. from setup_helpers.base import _git
  7. CALLER_CONSENT_PLAN = """\
  8. # Custom Greeting Implementation Plan
  9. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
  10. > or superpowers:executing-plans to implement this plan task-by-task.
  11. **Goal:** Add a small greeting customization feature to the Node fixture.
  12. ---
  13. ### Task 1: Custom greeting
  14. **Files:**
  15. - Modify: `src/index.js`
  16. - Modify: `src/utils.js`
  17. - Create: `tests/greeting.test.js`
  18. **Acceptance Criteria:**
  19. - The app can greet a provided name instead of always greeting `world`.
  20. - The default behavior remains `Hello, world!`.
  21. - A test covers both the default and custom-name paths.
  22. - [ ] **Step 1: Add tests for default and custom greetings.**
  23. - [ ] **Step 2: Update the greeting implementation.**
  24. - [ ] **Step 3: Run the relevant tests.**
  25. """
  26. def add_worktree(repo_dir: Path, branch: str, worktree_path: str) -> None:
  27. subprocess.run(
  28. ["git", "worktree", "add", "-b", branch, worktree_path],
  29. cwd=repo_dir,
  30. check=True,
  31. capture_output=True,
  32. )
  33. def detach_head(worktree_path: str) -> None:
  34. result = subprocess.run(
  35. ["git", "rev-parse", "HEAD"],
  36. cwd=worktree_path,
  37. capture_output=True,
  38. text=True,
  39. check=True,
  40. )
  41. commit = result.stdout.strip()
  42. result = subprocess.run(
  43. ["git", "branch", "--show-current"],
  44. cwd=worktree_path,
  45. capture_output=True,
  46. text=True,
  47. check=True,
  48. )
  49. branch = result.stdout.strip()
  50. subprocess.run(
  51. ["git", "checkout", "--detach", commit],
  52. cwd=worktree_path,
  53. check=True,
  54. capture_output=True,
  55. )
  56. if branch:
  57. subprocess.run(
  58. ["git", "branch", "-D", branch],
  59. cwd=worktree_path,
  60. capture_output=True,
  61. )
  62. def add_existing_worktree(workdir: Path) -> None:
  63. """Create an existing worktree (for 'already inside' scenarios)."""
  64. wt_path = workdir.parent / f"{workdir.name}-existing-worktree"
  65. add_worktree(workdir, "existing-feature", str(wt_path))
  66. def detach_worktree_head(workdir: Path) -> None:
  67. """Detach HEAD in the existing worktree."""
  68. wt_path = workdir.parent / f"{workdir.name}-existing-worktree"
  69. detach_head(str(wt_path))
  70. def symlink_superpowers(workdir: Path, superpowers_root: str) -> None:
  71. skills_dir = Path(workdir) / ".agents" / "skills"
  72. skills_dir.mkdir(parents=True, exist_ok=True)
  73. target = Path(superpowers_root) / "skills"
  74. link = skills_dir / "superpowers"
  75. link.symlink_to(target)
  76. def link_gemini_extension(workdir: Path, superpowers_root: str) -> None:
  77. """Link superpowers as a Gemini CLI extension and inject project context.
  78. Extensions are global, but GEMINI.md context loading is project-scoped.
  79. Temp workdirs need a GEMINI.md with absolute paths so Gemini loads
  80. the using-superpowers instructions that tell it to invoke skills.
  81. """
  82. extension_name = "superpowers"
  83. manifest = Path(superpowers_root) / "gemini-extension.json"
  84. if manifest.exists():
  85. with suppress(json.JSONDecodeError):
  86. extension_name = json.loads(manifest.read_text()).get("name", extension_name)
  87. # Gemini extensions are global; replace any prior link so this run tests
  88. # the requested SUPERPOWERS_ROOT checkout rather than a stale install.
  89. subprocess.run(
  90. ["gemini", "extensions", "uninstall", extension_name],
  91. capture_output=True,
  92. )
  93. subprocess.run(
  94. ["gemini", "extensions", "link", superpowers_root],
  95. capture_output=True,
  96. input="y\n",
  97. text=True,
  98. check=True,
  99. )
  100. # Create GEMINI.md with absolute @imports so context loads in the temp workdir
  101. skills_root = Path(superpowers_root) / "skills"
  102. gemini_md = workdir / "GEMINI.md"
  103. gemini_md.write_text(
  104. f"@{skills_root}/using-superpowers/SKILL.md\n"
  105. f"@{skills_root}/using-superpowers/references/gemini-tools.md\n"
  106. )
  107. def create_caller_consent_plan(workdir: Path) -> None:
  108. """Add a committed implementation plan that should trigger caller-layer gating."""
  109. plan_path = workdir / "docs" / "superpowers" / "plans" / "custom-greeting.md"
  110. plan_path.parent.mkdir(parents=True, exist_ok=True)
  111. plan_path.write_text(CALLER_CONSENT_PLAN)
  112. _git(["git", "add", str(plan_path.relative_to(workdir))], cwd=workdir)
  113. _git(["git", "commit", "-m", "add caller consent gate plan"], cwd=workdir)