spec-kitty-cli 0.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- specify_cli/workspace_context.py +224 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VCS Detection Module
|
|
3
|
+
====================
|
|
4
|
+
|
|
5
|
+
This module provides tool detection and the get_vcs() factory function.
|
|
6
|
+
It detects which VCS tools (git, jj) are available and returns the
|
|
7
|
+
appropriate implementation.
|
|
8
|
+
|
|
9
|
+
See kitty-specs/015-first-class-jujutsu-vcs-integration/contracts/vcs-protocol.py
|
|
10
|
+
for the factory function contract.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
from functools import lru_cache
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from .exceptions import (
|
|
24
|
+
VCSBackendMismatchError,
|
|
25
|
+
VCSNotFoundError,
|
|
26
|
+
)
|
|
27
|
+
from .types import VCSBackend
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .protocol import VCSProtocol
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Tool Detection Functions
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lru_cache(maxsize=1)
|
|
39
|
+
def is_jj_available() -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Check if jj is installed and working.
|
|
42
|
+
|
|
43
|
+
DISABLED: jj colocated mode is incompatible with sparse checkouts.
|
|
44
|
+
This function now always returns False to prevent jj detection.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
False (jj detection disabled)
|
|
48
|
+
"""
|
|
49
|
+
# DISABLED: jj is not compatible with sparse checkouts
|
|
50
|
+
# Keeping function signature for VCS abstraction layer compatibility
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
# Original implementation (commented out for reference):
|
|
54
|
+
# if shutil.which("jj") is None:
|
|
55
|
+
# return False
|
|
56
|
+
# try:
|
|
57
|
+
# result = subprocess.run(
|
|
58
|
+
# ["jj", "--version"],
|
|
59
|
+
# capture_output=True,
|
|
60
|
+
# timeout=5,
|
|
61
|
+
# )
|
|
62
|
+
# return result.returncode == 0
|
|
63
|
+
# except (subprocess.TimeoutExpired, OSError):
|
|
64
|
+
# return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@lru_cache(maxsize=1)
|
|
68
|
+
def is_git_available() -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if git is installed and working.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if git is installed and responds to --version, False otherwise.
|
|
74
|
+
"""
|
|
75
|
+
if shutil.which("git") is None:
|
|
76
|
+
return False
|
|
77
|
+
try:
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
["git", "--version"],
|
|
80
|
+
capture_output=True,
|
|
81
|
+
timeout=5,
|
|
82
|
+
)
|
|
83
|
+
return result.returncode == 0
|
|
84
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@lru_cache(maxsize=1)
|
|
89
|
+
def get_jj_version() -> str | None:
|
|
90
|
+
"""
|
|
91
|
+
Get installed jj version, or None if not installed.
|
|
92
|
+
|
|
93
|
+
DISABLED: jj detection is disabled (incompatible with sparse checkouts).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
None (jj detection disabled)
|
|
97
|
+
"""
|
|
98
|
+
# DISABLED: jj is not compatible with sparse checkouts
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@lru_cache(maxsize=1)
|
|
103
|
+
def get_git_version() -> str | None:
|
|
104
|
+
"""
|
|
105
|
+
Get installed git version, or None if not installed.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Version string (e.g., "2.43.0") or None if git is not available.
|
|
109
|
+
"""
|
|
110
|
+
if not is_git_available():
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
["git", "--version"],
|
|
115
|
+
capture_output=True,
|
|
116
|
+
timeout=5,
|
|
117
|
+
text=True,
|
|
118
|
+
)
|
|
119
|
+
if result.returncode != 0:
|
|
120
|
+
return None
|
|
121
|
+
# git version format: "git version 2.43.0" or "git version 2.43.0.windows.1"
|
|
122
|
+
output = result.stdout.strip()
|
|
123
|
+
match = re.search(r"git version\s+(\d+\.\d+\.\d+)", output)
|
|
124
|
+
if match:
|
|
125
|
+
return match.group(1)
|
|
126
|
+
# Fallback: return everything after "git version "
|
|
127
|
+
if "git version " in output:
|
|
128
|
+
return output.split("git version ")[1].strip()
|
|
129
|
+
return "unknown"
|
|
130
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def detect_available_backends() -> list[VCSBackend]:
|
|
135
|
+
"""
|
|
136
|
+
Detect which VCS tools are installed and available.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of available backends, in preference order (jj first if available).
|
|
140
|
+
"""
|
|
141
|
+
backends = []
|
|
142
|
+
if is_jj_available():
|
|
143
|
+
backends.append(VCSBackend.JUJUTSU)
|
|
144
|
+
if is_git_available():
|
|
145
|
+
backends.append(VCSBackend.GIT)
|
|
146
|
+
return backends
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# =============================================================================
|
|
150
|
+
# Factory Function
|
|
151
|
+
# =============================================================================
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _get_locked_vcs_from_feature(path: Path) -> VCSBackend | None:
|
|
155
|
+
"""
|
|
156
|
+
Read VCS from feature meta.json if path is inside that feature.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
path: A path that might be within a feature directory.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The locked VCSBackend if path is inside a feature with locked VCS, None otherwise.
|
|
163
|
+
|
|
164
|
+
Note:
|
|
165
|
+
Only returns a locked VCS if `path` is actually inside the feature directory
|
|
166
|
+
(either in kitty-specs/###-feature/ or in a worktree for that feature).
|
|
167
|
+
Does NOT return VCS for unrelated features.
|
|
168
|
+
"""
|
|
169
|
+
current = path.resolve()
|
|
170
|
+
|
|
171
|
+
# Strategy 1: Check if path is directly inside kitty-specs/###-feature/
|
|
172
|
+
# e.g., /repo/kitty-specs/015-feature/tasks/WP01.md
|
|
173
|
+
for parent in [current, *current.parents]:
|
|
174
|
+
if parent.parent and parent.parent.name == "kitty-specs":
|
|
175
|
+
# parent is a feature directory like kitty-specs/015-feature/
|
|
176
|
+
meta_path = parent / "meta.json"
|
|
177
|
+
if meta_path.is_file():
|
|
178
|
+
try:
|
|
179
|
+
meta = json.loads(meta_path.read_text())
|
|
180
|
+
if "vcs" in meta:
|
|
181
|
+
return VCSBackend(meta["vcs"])
|
|
182
|
+
except (json.JSONDecodeError, ValueError, OSError):
|
|
183
|
+
pass
|
|
184
|
+
# Path is in a feature dir but no valid meta.json
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
# Strategy 2: Check if we're in a worktree for a feature
|
|
188
|
+
# e.g., .worktrees/015-feature-name-WP01/src/file.py
|
|
189
|
+
if ".worktrees" in str(current):
|
|
190
|
+
# Find the worktree root (direct child of .worktrees/)
|
|
191
|
+
worktree_root = None
|
|
192
|
+
for parent in [current, *current.parents]:
|
|
193
|
+
if parent.parent and parent.parent.name == ".worktrees":
|
|
194
|
+
worktree_root = parent
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
if worktree_root:
|
|
198
|
+
# Extract feature number from worktree name
|
|
199
|
+
# Pattern: ###-feature-name-WP##
|
|
200
|
+
worktree_name = worktree_root.name
|
|
201
|
+
match = re.match(r"(\d{3})-", worktree_name)
|
|
202
|
+
if match:
|
|
203
|
+
feature_num = match.group(1)
|
|
204
|
+
# Find main repo (parent of .worktrees)
|
|
205
|
+
main_repo = worktree_root.parent.parent
|
|
206
|
+
kitty_specs = main_repo / "kitty-specs"
|
|
207
|
+
if kitty_specs.is_dir():
|
|
208
|
+
# Find the specific feature directory matching feature_num
|
|
209
|
+
for feature_dir in kitty_specs.iterdir():
|
|
210
|
+
if feature_dir.is_dir() and feature_dir.name.startswith(
|
|
211
|
+
f"{feature_num}-"
|
|
212
|
+
):
|
|
213
|
+
meta_path = feature_dir / "meta.json"
|
|
214
|
+
if meta_path.is_file():
|
|
215
|
+
try:
|
|
216
|
+
meta = json.loads(meta_path.read_text())
|
|
217
|
+
if "vcs" in meta:
|
|
218
|
+
return VCSBackend(meta["vcs"])
|
|
219
|
+
except (json.JSONDecodeError, ValueError, OSError):
|
|
220
|
+
pass
|
|
221
|
+
# Found feature dir but no valid meta.json
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
# Path is not inside any feature
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _instantiate_backend(backend: VCSBackend) -> "VCSProtocol":
|
|
229
|
+
"""
|
|
230
|
+
Instantiate the appropriate VCS implementation.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
backend: The backend to instantiate.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
A VCSProtocol implementation.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
VCSNotFoundError: If the requested backend is not available.
|
|
240
|
+
"""
|
|
241
|
+
if backend == VCSBackend.JUJUTSU:
|
|
242
|
+
if not is_jj_available():
|
|
243
|
+
raise VCSNotFoundError(
|
|
244
|
+
"jj is not available. Install jj from https://github.com/martinvonz/jj"
|
|
245
|
+
)
|
|
246
|
+
# Lazy import to avoid circular imports
|
|
247
|
+
from .jujutsu import JujutsuVCS
|
|
248
|
+
|
|
249
|
+
return JujutsuVCS()
|
|
250
|
+
elif backend == VCSBackend.GIT:
|
|
251
|
+
if not is_git_available():
|
|
252
|
+
raise VCSNotFoundError("git is not available. Please install git.")
|
|
253
|
+
# Lazy import to avoid circular imports
|
|
254
|
+
from .git import GitVCS
|
|
255
|
+
|
|
256
|
+
return GitVCS()
|
|
257
|
+
else:
|
|
258
|
+
raise VCSNotFoundError(f"Unknown VCS backend: {backend}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_vcs(
|
|
262
|
+
path: Path,
|
|
263
|
+
backend: VCSBackend | None = None,
|
|
264
|
+
prefer_jj: bool = True,
|
|
265
|
+
) -> "VCSProtocol":
|
|
266
|
+
"""
|
|
267
|
+
Factory function to get appropriate VCS implementation.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
path: Path within a repository or feature directory.
|
|
271
|
+
backend: Explicit backend choice (None = auto-detect).
|
|
272
|
+
prefer_jj: If auto-detecting, prefer jj over git when both available.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
VCSProtocol implementation (GitVCS or JujutsuVCS).
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
VCSNotFoundError: Neither jj nor git available.
|
|
279
|
+
VCSBackendMismatchError: Requested backend doesn't match feature's locked VCS.
|
|
280
|
+
|
|
281
|
+
Detection order:
|
|
282
|
+
1. If backend specified, use that
|
|
283
|
+
2. If path is in a feature, read meta.json for locked VCS
|
|
284
|
+
3. If jj available and prefer_jj=True, use jj
|
|
285
|
+
4. If git available, use git
|
|
286
|
+
5. Raise VCSNotFoundError
|
|
287
|
+
"""
|
|
288
|
+
# 1. If explicit backend specified, use that
|
|
289
|
+
if backend is not None:
|
|
290
|
+
# Check if there's a locked VCS that conflicts
|
|
291
|
+
locked = _get_locked_vcs_from_feature(path)
|
|
292
|
+
if locked is not None and locked != backend:
|
|
293
|
+
raise VCSBackendMismatchError(
|
|
294
|
+
f"Requested backend '{backend.value}' doesn't match feature's "
|
|
295
|
+
f"locked VCS '{locked.value}'. "
|
|
296
|
+
f"Features must use the same VCS throughout their lifecycle."
|
|
297
|
+
)
|
|
298
|
+
return _instantiate_backend(backend)
|
|
299
|
+
|
|
300
|
+
# 2. Check for locked VCS in feature meta.json
|
|
301
|
+
locked = _get_locked_vcs_from_feature(path)
|
|
302
|
+
if locked is not None:
|
|
303
|
+
return _instantiate_backend(locked)
|
|
304
|
+
|
|
305
|
+
# 3. Auto-detect based on availability
|
|
306
|
+
# DISABLED: jj detection disabled (incompatible with sparse checkouts)
|
|
307
|
+
# if prefer_jj and is_jj_available():
|
|
308
|
+
# # Lazy import to avoid circular imports
|
|
309
|
+
# from .jujutsu import JujutsuVCS
|
|
310
|
+
#
|
|
311
|
+
# return JujutsuVCS()
|
|
312
|
+
|
|
313
|
+
if is_git_available():
|
|
314
|
+
# Lazy import to avoid circular imports
|
|
315
|
+
from .git import GitVCS
|
|
316
|
+
|
|
317
|
+
return GitVCS()
|
|
318
|
+
|
|
319
|
+
# 4. git not available
|
|
320
|
+
raise VCSNotFoundError(
|
|
321
|
+
"git is not available. "
|
|
322
|
+
"Please install git: https://git-scm.com/downloads"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# =============================================================================
|
|
327
|
+
# Cache Management (for testing)
|
|
328
|
+
# =============================================================================
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _clear_detection_cache() -> None:
|
|
332
|
+
"""
|
|
333
|
+
Clear the detection cache. For testing purposes only.
|
|
334
|
+
|
|
335
|
+
This clears the cached results of is_jj_available, is_git_available,
|
|
336
|
+
get_jj_version, and get_git_version.
|
|
337
|
+
"""
|
|
338
|
+
is_jj_available.cache_clear()
|
|
339
|
+
is_git_available.cache_clear()
|
|
340
|
+
get_jj_version.cache_clear()
|
|
341
|
+
get_git_version.cache_clear()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VCS Exceptions Module
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
This module defines the exception hierarchy for VCS operations.
|
|
6
|
+
All VCS-related exceptions inherit from VCSError.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VCSError(Exception):
|
|
13
|
+
"""
|
|
14
|
+
Base exception for VCS operations.
|
|
15
|
+
|
|
16
|
+
All VCS-related exceptions inherit from this class.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VCSNotFoundError(VCSError):
|
|
23
|
+
"""
|
|
24
|
+
Neither jj nor git is available.
|
|
25
|
+
|
|
26
|
+
Raised when attempting VCS operations but no supported
|
|
27
|
+
VCS tool is installed or accessible.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VCSCapabilityError(VCSError):
|
|
34
|
+
"""
|
|
35
|
+
Operation not supported by this backend.
|
|
36
|
+
|
|
37
|
+
Raised when attempting an operation that the current
|
|
38
|
+
VCS backend does not support (e.g., jj undo on git).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class VCSBackendMismatchError(VCSError):
|
|
45
|
+
"""
|
|
46
|
+
Requested backend doesn't match feature's locked VCS.
|
|
47
|
+
|
|
48
|
+
Raised when explicitly requesting a backend that differs
|
|
49
|
+
from the VCS locked in the feature's meta.json.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class VCSLockError(VCSError):
|
|
56
|
+
"""
|
|
57
|
+
Attempted to change VCS for a feature after it was locked.
|
|
58
|
+
|
|
59
|
+
Once a feature has its VCS set (on first workspace creation),
|
|
60
|
+
it cannot be changed. This exception is raised on such attempts.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class VCSConflictError(VCSError):
|
|
67
|
+
"""
|
|
68
|
+
Operation blocked due to unresolved conflicts.
|
|
69
|
+
|
|
70
|
+
Raised when an operation cannot proceed because the
|
|
71
|
+
workspace has unresolved conflicts that must be addressed first.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class VCSSyncError(VCSError):
|
|
78
|
+
"""
|
|
79
|
+
Sync operation failed.
|
|
80
|
+
|
|
81
|
+
Raised when workspace synchronization fails due to
|
|
82
|
+
network issues, permissions, or other errors.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
pass
|