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,141 @@
|
|
|
1
|
+
"""Migration: Install encoding validation pre-commit hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..registry import MigrationRegistry
|
|
9
|
+
from .base import BaseMigration, MigrationResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@MigrationRegistry.register
|
|
13
|
+
class EncodingHooksMigration(BaseMigration):
|
|
14
|
+
"""Install encoding validation pre-commit hooks.
|
|
15
|
+
|
|
16
|
+
This migration installs git hooks that validate file encoding
|
|
17
|
+
before commits, preventing encoding issues from being committed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
migration_id = "0.5.0_encoding_hooks"
|
|
21
|
+
description = "Install encoding validation pre-commit hooks"
|
|
22
|
+
target_version = "0.5.0"
|
|
23
|
+
|
|
24
|
+
HOOK_FILES = [
|
|
25
|
+
"pre-commit",
|
|
26
|
+
"pre-commit-encoding-check",
|
|
27
|
+
"pre-commit-agent-check",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def detect(self, project_path: Path) -> bool:
|
|
31
|
+
"""Check if encoding hooks are missing."""
|
|
32
|
+
git_dir = project_path / ".git"
|
|
33
|
+
if not git_dir.exists():
|
|
34
|
+
return False # Not a git repo, can't install hooks
|
|
35
|
+
|
|
36
|
+
pre_commit = git_dir / "hooks" / "pre-commit"
|
|
37
|
+
if not pre_commit.exists():
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
content = pre_commit.read_text(encoding="utf-8", errors="ignore")
|
|
42
|
+
except OSError:
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
# Check if it's our hook or a custom one
|
|
46
|
+
return "spec-kitty" not in content.lower() and "encoding" not in content.lower()
|
|
47
|
+
|
|
48
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
49
|
+
"""Check if we can install hooks."""
|
|
50
|
+
git_dir = project_path / ".git"
|
|
51
|
+
if not git_dir.exists():
|
|
52
|
+
return False, "Not a git repository"
|
|
53
|
+
|
|
54
|
+
hooks_dir = git_dir / "hooks"
|
|
55
|
+
if not hooks_dir.exists():
|
|
56
|
+
return True, ""
|
|
57
|
+
|
|
58
|
+
pre_commit = hooks_dir / "pre-commit"
|
|
59
|
+
if pre_commit.exists():
|
|
60
|
+
try:
|
|
61
|
+
content = pre_commit.read_text(encoding="utf-8", errors="ignore")
|
|
62
|
+
except OSError:
|
|
63
|
+
return False, "Cannot read existing pre-commit hook"
|
|
64
|
+
|
|
65
|
+
# Check if it's our hook
|
|
66
|
+
if "spec-kitty" not in content.lower() and "encoding" not in content.lower():
|
|
67
|
+
# It's a custom hook - warn but allow (will append)
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
return True, ""
|
|
71
|
+
|
|
72
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
73
|
+
"""Install or update pre-commit hooks."""
|
|
74
|
+
changes: list[str] = []
|
|
75
|
+
warnings: list[str] = []
|
|
76
|
+
errors: list[str] = []
|
|
77
|
+
|
|
78
|
+
git_dir = project_path / ".git"
|
|
79
|
+
if not git_dir.exists():
|
|
80
|
+
errors.append("Not a git repository")
|
|
81
|
+
return MigrationResult(success=False, errors=errors)
|
|
82
|
+
|
|
83
|
+
hooks_dir = git_dir / "hooks"
|
|
84
|
+
|
|
85
|
+
# Find hook templates - try .kittify/templates first, then package
|
|
86
|
+
template_hooks_dir = project_path / ".kittify" / "templates" / "git-hooks"
|
|
87
|
+
|
|
88
|
+
if not template_hooks_dir.exists():
|
|
89
|
+
# Try to find from package
|
|
90
|
+
try:
|
|
91
|
+
from importlib.resources import files
|
|
92
|
+
|
|
93
|
+
pkg_hooks = files("specify_cli").joinpath("templates", "git-hooks")
|
|
94
|
+
if hasattr(pkg_hooks, "is_dir") and pkg_hooks.is_dir():
|
|
95
|
+
template_hooks_dir = Path(str(pkg_hooks))
|
|
96
|
+
else:
|
|
97
|
+
warnings.append(
|
|
98
|
+
"Hook templates not found in .kittify/templates/ or package"
|
|
99
|
+
)
|
|
100
|
+
return MigrationResult(
|
|
101
|
+
success=True, changes_made=changes, warnings=warnings
|
|
102
|
+
)
|
|
103
|
+
except (ImportError, TypeError):
|
|
104
|
+
warnings.append("Could not locate hook templates")
|
|
105
|
+
return MigrationResult(
|
|
106
|
+
success=True, changes_made=changes, warnings=warnings
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if dry_run:
|
|
110
|
+
changes.append("Would install pre-commit hooks from templates")
|
|
111
|
+
return MigrationResult(success=True, changes_made=changes)
|
|
112
|
+
|
|
113
|
+
# Create hooks directory if needed
|
|
114
|
+
try:
|
|
115
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
116
|
+
except OSError as e:
|
|
117
|
+
errors.append(f"Failed to create hooks directory: {e}")
|
|
118
|
+
return MigrationResult(success=False, errors=errors)
|
|
119
|
+
|
|
120
|
+
# Copy hook files
|
|
121
|
+
for hook_name in self.HOOK_FILES:
|
|
122
|
+
template_hook = template_hooks_dir / hook_name
|
|
123
|
+
dest_hook = hooks_dir / hook_name
|
|
124
|
+
|
|
125
|
+
if template_hook.exists():
|
|
126
|
+
try:
|
|
127
|
+
shutil.copy2(template_hook, dest_hook)
|
|
128
|
+
dest_hook.chmod(0o755)
|
|
129
|
+
changes.append(f"Installed {hook_name} hook")
|
|
130
|
+
except OSError as e:
|
|
131
|
+
errors.append(f"Failed to install {hook_name}: {e}")
|
|
132
|
+
else:
|
|
133
|
+
warnings.append(f"Template for {hook_name} not found")
|
|
134
|
+
|
|
135
|
+
success = len(errors) == 0
|
|
136
|
+
return MigrationResult(
|
|
137
|
+
success=success,
|
|
138
|
+
changes_made=changes,
|
|
139
|
+
errors=errors,
|
|
140
|
+
warnings=warnings,
|
|
141
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Migration: Rename commands/ to command-templates/ directories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..registry import MigrationRegistry
|
|
9
|
+
from .base import BaseMigration, MigrationResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@MigrationRegistry.register
|
|
13
|
+
class CommandsRenameMigration(BaseMigration):
|
|
14
|
+
"""Rename commands/ to command-templates/ in templates and missions.
|
|
15
|
+
|
|
16
|
+
This migration fixes the issue where Claude Code discovers commands
|
|
17
|
+
from .kittify/templates/commands/ and .kittify/missions/*/commands/
|
|
18
|
+
causing duplicate slash commands.
|
|
19
|
+
|
|
20
|
+
The directories are renamed to command-templates/ which Claude Code
|
|
21
|
+
does not automatically discover.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
migration_id = "0.6.5_commands_rename"
|
|
25
|
+
description = "Rename commands/ to command-templates/ directories"
|
|
26
|
+
target_version = "0.6.5"
|
|
27
|
+
|
|
28
|
+
def detect(self, project_path: Path) -> bool:
|
|
29
|
+
"""Check if project uses old commands/ directories."""
|
|
30
|
+
kittify_dir = project_path / ".kittify"
|
|
31
|
+
|
|
32
|
+
# Check templates/commands/
|
|
33
|
+
if (kittify_dir / "templates" / "commands").exists():
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
# Check missions/*/commands/
|
|
37
|
+
missions_dir = kittify_dir / "missions"
|
|
38
|
+
if missions_dir.exists():
|
|
39
|
+
for mission in missions_dir.iterdir():
|
|
40
|
+
if mission.is_dir() and (mission / "commands").exists():
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
# Check worktrees
|
|
44
|
+
worktrees_dir = project_path / ".worktrees"
|
|
45
|
+
if worktrees_dir.exists():
|
|
46
|
+
for worktree in worktrees_dir.iterdir():
|
|
47
|
+
if worktree.is_dir():
|
|
48
|
+
wt_kittify = worktree / ".kittify"
|
|
49
|
+
if (wt_kittify / "templates" / "commands").exists():
|
|
50
|
+
return True
|
|
51
|
+
wt_missions = wt_kittify / "missions"
|
|
52
|
+
if wt_missions.exists():
|
|
53
|
+
for mission in wt_missions.iterdir():
|
|
54
|
+
if mission.is_dir() and (mission / "commands").exists():
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
60
|
+
"""Check for conflicts."""
|
|
61
|
+
kittify_dir = project_path / ".kittify"
|
|
62
|
+
|
|
63
|
+
# Check if both old and new exist in templates
|
|
64
|
+
templates_dir = kittify_dir / "templates"
|
|
65
|
+
if templates_dir.exists():
|
|
66
|
+
old_exists = (templates_dir / "commands").exists()
|
|
67
|
+
new_exists = (templates_dir / "command-templates").exists()
|
|
68
|
+
if old_exists and new_exists:
|
|
69
|
+
return (
|
|
70
|
+
False,
|
|
71
|
+
"Both commands/ and command-templates/ exist in templates - manual merge required",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Check missions
|
|
75
|
+
missions_dir = kittify_dir / "missions"
|
|
76
|
+
if missions_dir.exists():
|
|
77
|
+
for mission in missions_dir.iterdir():
|
|
78
|
+
if mission.is_dir():
|
|
79
|
+
old_exists = (mission / "commands").exists()
|
|
80
|
+
new_exists = (mission / "command-templates").exists()
|
|
81
|
+
if old_exists and new_exists:
|
|
82
|
+
return (
|
|
83
|
+
False,
|
|
84
|
+
f"Both directories exist in mission {mission.name} - manual merge required",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return True, ""
|
|
88
|
+
|
|
89
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
90
|
+
"""Rename commands/ to command-templates/."""
|
|
91
|
+
changes: list[str] = []
|
|
92
|
+
warnings: list[str] = []
|
|
93
|
+
errors: list[str] = []
|
|
94
|
+
|
|
95
|
+
kittify_dir = project_path / ".kittify"
|
|
96
|
+
|
|
97
|
+
def rename_dir(old: Path, new: Path, context: str) -> None:
|
|
98
|
+
if old.exists() and not new.exists():
|
|
99
|
+
if dry_run:
|
|
100
|
+
changes.append(f"Would rename {context}: commands/ -> command-templates/")
|
|
101
|
+
else:
|
|
102
|
+
try:
|
|
103
|
+
shutil.move(str(old), str(new))
|
|
104
|
+
changes.append(f"Renamed {context}: commands/ -> command-templates/")
|
|
105
|
+
except OSError as e:
|
|
106
|
+
errors.append(f"Failed to rename {context}: {e}")
|
|
107
|
+
|
|
108
|
+
# Rename in templates/
|
|
109
|
+
templates_dir = kittify_dir / "templates"
|
|
110
|
+
if templates_dir.exists():
|
|
111
|
+
rename_dir(
|
|
112
|
+
templates_dir / "commands",
|
|
113
|
+
templates_dir / "command-templates",
|
|
114
|
+
".kittify/templates",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Rename in each mission
|
|
118
|
+
missions_dir = kittify_dir / "missions"
|
|
119
|
+
if missions_dir.exists():
|
|
120
|
+
for mission in missions_dir.iterdir():
|
|
121
|
+
if mission.is_dir():
|
|
122
|
+
rename_dir(
|
|
123
|
+
mission / "commands",
|
|
124
|
+
mission / "command-templates",
|
|
125
|
+
f".kittify/missions/{mission.name}",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Handle worktrees - remove old commands/ directories
|
|
129
|
+
# (worktrees should use their own .claude/commands/ not templates)
|
|
130
|
+
worktrees_dir = project_path / ".worktrees"
|
|
131
|
+
if worktrees_dir.exists():
|
|
132
|
+
for worktree in worktrees_dir.iterdir():
|
|
133
|
+
if worktree.is_dir():
|
|
134
|
+
# Remove old templates/commands/
|
|
135
|
+
wt_templates_commands = worktree / ".kittify" / "templates" / "commands"
|
|
136
|
+
if wt_templates_commands.exists():
|
|
137
|
+
if dry_run:
|
|
138
|
+
changes.append(
|
|
139
|
+
f"Would remove old commands/ from worktree {worktree.name}"
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
try:
|
|
143
|
+
shutil.rmtree(wt_templates_commands)
|
|
144
|
+
changes.append(
|
|
145
|
+
f"Removed old commands/ from worktree {worktree.name}"
|
|
146
|
+
)
|
|
147
|
+
except OSError as e:
|
|
148
|
+
warnings.append(
|
|
149
|
+
f"Could not remove old commands/ from worktree {worktree.name}: {e}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Rename missions/*/commands/ in worktree
|
|
153
|
+
wt_missions = worktree / ".kittify" / "missions"
|
|
154
|
+
if wt_missions.exists():
|
|
155
|
+
for mission in wt_missions.iterdir():
|
|
156
|
+
if mission.is_dir():
|
|
157
|
+
rename_dir(
|
|
158
|
+
mission / "commands",
|
|
159
|
+
mission / "command-templates",
|
|
160
|
+
f".worktrees/{worktree.name}/.kittify/missions/{mission.name}",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
success = len(errors) == 0
|
|
164
|
+
return MigrationResult(
|
|
165
|
+
success=success,
|
|
166
|
+
changes_made=changes,
|
|
167
|
+
errors=errors,
|
|
168
|
+
warnings=warnings,
|
|
169
|
+
)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Migration: Ensure all missions are present in the project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from ..registry import MigrationRegistry
|
|
10
|
+
from .base import BaseMigration, MigrationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@MigrationRegistry.register
|
|
14
|
+
class EnsureMissionsMigration(BaseMigration):
|
|
15
|
+
"""Ensure all required missions are present in the project.
|
|
16
|
+
|
|
17
|
+
This migration addresses the bug in v0.6.5-0.6.6 where the software-dev
|
|
18
|
+
mission was missing from PyPI packages due to symlink handling issues
|
|
19
|
+
during build.
|
|
20
|
+
|
|
21
|
+
It copies missing missions from the package to the project.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
migration_id = "0.6.7_ensure_missions"
|
|
25
|
+
description = "Ensure all required missions (software-dev, research) are present"
|
|
26
|
+
target_version = "0.6.7"
|
|
27
|
+
|
|
28
|
+
# Required missions that should always be present
|
|
29
|
+
REQUIRED_MISSIONS = ["software-dev", "research"]
|
|
30
|
+
|
|
31
|
+
def detect(self, project_path: Path) -> bool:
|
|
32
|
+
"""Check if any required missions are missing."""
|
|
33
|
+
missions_dir = project_path / ".kittify" / "missions"
|
|
34
|
+
|
|
35
|
+
if not missions_dir.exists():
|
|
36
|
+
return True # No missions directory at all
|
|
37
|
+
|
|
38
|
+
for mission_name in self.REQUIRED_MISSIONS:
|
|
39
|
+
mission_dir = missions_dir / mission_name
|
|
40
|
+
if not mission_dir.exists():
|
|
41
|
+
return True
|
|
42
|
+
# Check for essential files
|
|
43
|
+
if not (mission_dir / "mission.yaml").exists():
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
49
|
+
"""Check if we can copy missions from the package."""
|
|
50
|
+
# Try to find package missions
|
|
51
|
+
package_missions = self._find_package_missions()
|
|
52
|
+
if package_missions is None:
|
|
53
|
+
# In test environments, package missions may not be available
|
|
54
|
+
# Skip gracefully rather than blocking all upgrades
|
|
55
|
+
return (
|
|
56
|
+
False,
|
|
57
|
+
"Could not locate package missions to copy from. "
|
|
58
|
+
"This is expected in test environments. "
|
|
59
|
+
"Run 'spec-kitty init --force' to repair missions manually.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Check we have all required missions in the package
|
|
63
|
+
missing_in_pkg = []
|
|
64
|
+
for mission_name in self.REQUIRED_MISSIONS:
|
|
65
|
+
pkg_mission = package_missions / mission_name
|
|
66
|
+
if not pkg_mission.exists():
|
|
67
|
+
missing_in_pkg.append(mission_name)
|
|
68
|
+
|
|
69
|
+
if missing_in_pkg:
|
|
70
|
+
return (
|
|
71
|
+
False,
|
|
72
|
+
f"Package is missing missions: {', '.join(missing_in_pkg)}. "
|
|
73
|
+
"Please upgrade spec-kitty-cli to the latest version.",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return True, ""
|
|
77
|
+
|
|
78
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
79
|
+
"""Copy missing missions from the package."""
|
|
80
|
+
changes: list[str] = []
|
|
81
|
+
warnings: list[str] = []
|
|
82
|
+
errors: list[str] = []
|
|
83
|
+
|
|
84
|
+
missions_dir = project_path / ".kittify" / "missions"
|
|
85
|
+
package_missions = self._find_package_missions()
|
|
86
|
+
|
|
87
|
+
if package_missions is None:
|
|
88
|
+
errors.append("Could not locate package missions")
|
|
89
|
+
return MigrationResult(success=False, errors=errors)
|
|
90
|
+
|
|
91
|
+
# Ensure missions directory exists
|
|
92
|
+
if not missions_dir.exists():
|
|
93
|
+
if dry_run:
|
|
94
|
+
changes.append("Would create .kittify/missions/ directory")
|
|
95
|
+
else:
|
|
96
|
+
missions_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
changes.append("Created .kittify/missions/ directory")
|
|
98
|
+
|
|
99
|
+
# Copy missing missions
|
|
100
|
+
for mission_name in self.REQUIRED_MISSIONS:
|
|
101
|
+
dest_mission = missions_dir / mission_name
|
|
102
|
+
src_mission = package_missions / mission_name
|
|
103
|
+
|
|
104
|
+
if dest_mission.exists():
|
|
105
|
+
# Check if it has essential files
|
|
106
|
+
if (dest_mission / "mission.yaml").exists():
|
|
107
|
+
continue
|
|
108
|
+
else:
|
|
109
|
+
# Mission directory exists but is incomplete
|
|
110
|
+
if dry_run:
|
|
111
|
+
changes.append(
|
|
112
|
+
f"Would repair incomplete mission: {mission_name}"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
try:
|
|
116
|
+
# Remove incomplete and copy fresh
|
|
117
|
+
shutil.rmtree(dest_mission)
|
|
118
|
+
shutil.copytree(src_mission, dest_mission)
|
|
119
|
+
changes.append(f"Repaired incomplete mission: {mission_name}")
|
|
120
|
+
except OSError as e:
|
|
121
|
+
errors.append(f"Failed to repair mission {mission_name}: {e}")
|
|
122
|
+
else:
|
|
123
|
+
# Mission doesn't exist, copy it
|
|
124
|
+
if dry_run:
|
|
125
|
+
changes.append(f"Would copy missing mission: {mission_name}")
|
|
126
|
+
else:
|
|
127
|
+
try:
|
|
128
|
+
shutil.copytree(src_mission, dest_mission)
|
|
129
|
+
changes.append(f"Copied missing mission: {mission_name}")
|
|
130
|
+
except OSError as e:
|
|
131
|
+
errors.append(f"Failed to copy mission {mission_name}: {e}")
|
|
132
|
+
|
|
133
|
+
# Also fix worktrees
|
|
134
|
+
worktrees_dir = project_path / ".worktrees"
|
|
135
|
+
if worktrees_dir.exists():
|
|
136
|
+
for worktree in worktrees_dir.iterdir():
|
|
137
|
+
if not worktree.is_dir():
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
wt_missions = worktree / ".kittify" / "missions"
|
|
141
|
+
if not wt_missions.exists():
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
for mission_name in self.REQUIRED_MISSIONS:
|
|
145
|
+
wt_mission = wt_missions / mission_name
|
|
146
|
+
src_mission = package_missions / mission_name
|
|
147
|
+
|
|
148
|
+
if not wt_mission.exists():
|
|
149
|
+
if dry_run:
|
|
150
|
+
changes.append(
|
|
151
|
+
f"Would copy missing mission to worktree {worktree.name}: {mission_name}"
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
try:
|
|
155
|
+
shutil.copytree(src_mission, wt_mission)
|
|
156
|
+
changes.append(
|
|
157
|
+
f"Copied missing mission to worktree {worktree.name}: {mission_name}"
|
|
158
|
+
)
|
|
159
|
+
except OSError as e:
|
|
160
|
+
warnings.append(
|
|
161
|
+
f"Could not copy mission to worktree {worktree.name}: {e}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
success = len(errors) == 0
|
|
165
|
+
return MigrationResult(
|
|
166
|
+
success=success,
|
|
167
|
+
changes_made=changes,
|
|
168
|
+
errors=errors,
|
|
169
|
+
warnings=warnings,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _find_package_missions(self) -> Path | None:
|
|
173
|
+
"""Find the missions directory in the installed package or local repo."""
|
|
174
|
+
# First try from installed package
|
|
175
|
+
try:
|
|
176
|
+
from importlib.resources import files
|
|
177
|
+
|
|
178
|
+
pkg_files = files("specify_cli")
|
|
179
|
+
missions_path = pkg_files.joinpath("missions")
|
|
180
|
+
|
|
181
|
+
# Convert to Path and check if it exists
|
|
182
|
+
missions_str = str(missions_path)
|
|
183
|
+
if Path(missions_str).exists():
|
|
184
|
+
return Path(missions_str)
|
|
185
|
+
|
|
186
|
+
except (ImportError, TypeError, AttributeError):
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
# Try from package __file__ location
|
|
190
|
+
try:
|
|
191
|
+
import specify_cli
|
|
192
|
+
|
|
193
|
+
pkg_dir = Path(specify_cli.__file__).parent
|
|
194
|
+
missions_dir = pkg_dir / "missions"
|
|
195
|
+
if missions_dir.exists():
|
|
196
|
+
return missions_dir
|
|
197
|
+
except (ImportError, AttributeError):
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
# Fallback for development: Check SPEC_KITTY_TEMPLATE_ROOT env var
|
|
201
|
+
import os
|
|
202
|
+
|
|
203
|
+
template_root = os.environ.get("SPEC_KITTY_TEMPLATE_ROOT")
|
|
204
|
+
if template_root:
|
|
205
|
+
missions_dir = Path(template_root) / ".kittify" / "missions"
|
|
206
|
+
if missions_dir.exists():
|
|
207
|
+
return missions_dir
|
|
208
|
+
|
|
209
|
+
# Fallback: Try to find the spec-kitty repo root from current working directory
|
|
210
|
+
# This handles cases where we're running from the repo in development
|
|
211
|
+
try:
|
|
212
|
+
cwd = Path.cwd()
|
|
213
|
+
# Check if we're in the spec-kitty repo
|
|
214
|
+
for parent in [cwd] + list(cwd.parents):
|
|
215
|
+
missions_dir = parent / ".kittify" / "missions"
|
|
216
|
+
pyproject = parent / "pyproject.toml"
|
|
217
|
+
if missions_dir.exists() and pyproject.exists():
|
|
218
|
+
# Verify it's the spec-kitty repo by checking pyproject.toml
|
|
219
|
+
try:
|
|
220
|
+
content = pyproject.read_text(encoding='utf-8-sig')
|
|
221
|
+
if "spec-kitty-cli" in content:
|
|
222
|
+
return missions_dir
|
|
223
|
+
except OSError:
|
|
224
|
+
pass
|
|
225
|
+
except OSError:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
return None
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Migration: Remove duplicate .claude/commands/ from worktrees."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..registry import MigrationRegistry
|
|
9
|
+
from .base import BaseMigration, MigrationResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@MigrationRegistry.register
|
|
13
|
+
class WorktreeCommandsDedupMigration(BaseMigration):
|
|
14
|
+
"""Remove .claude/commands/ from worktrees - they inherit from main repo.
|
|
15
|
+
|
|
16
|
+
Claude Code traverses parent directories looking for .claude/commands/.
|
|
17
|
+
When a worktree is located inside the main repo (at .worktrees/),
|
|
18
|
+
the worktree can find commands by traversing up to the main repo.
|
|
19
|
+
|
|
20
|
+
This migration removes .claude/commands/ from worktrees since they
|
|
21
|
+
don't need their own copy - they inherit from the main repo.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
migration_id = "0.7.2_worktree_commands_dedup"
|
|
25
|
+
description = "Remove duplicate .claude/commands/ from worktrees (inherit from main repo)"
|
|
26
|
+
target_version = "0.7.2"
|
|
27
|
+
|
|
28
|
+
def detect(self, project_path: Path) -> bool:
|
|
29
|
+
"""Check if any worktrees have their own .claude/commands/."""
|
|
30
|
+
worktrees_dir = project_path / ".worktrees"
|
|
31
|
+
|
|
32
|
+
if not worktrees_dir.exists():
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
for worktree in worktrees_dir.iterdir():
|
|
36
|
+
if worktree.is_dir():
|
|
37
|
+
wt_commands = worktree / ".claude" / "commands"
|
|
38
|
+
if wt_commands.exists():
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
44
|
+
"""Check that main repo has commands before removing from worktrees."""
|
|
45
|
+
main_claude_commands = project_path / ".claude" / "commands"
|
|
46
|
+
|
|
47
|
+
if not main_claude_commands.exists():
|
|
48
|
+
return (
|
|
49
|
+
False,
|
|
50
|
+
"Main repo .claude/commands/ must exist before removing from worktrees"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return True, ""
|
|
54
|
+
|
|
55
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
56
|
+
"""Remove .claude/commands/ from all worktrees."""
|
|
57
|
+
changes: list[str] = []
|
|
58
|
+
warnings: list[str] = []
|
|
59
|
+
errors: list[str] = []
|
|
60
|
+
|
|
61
|
+
worktrees_dir = project_path / ".worktrees"
|
|
62
|
+
|
|
63
|
+
if worktrees_dir.exists():
|
|
64
|
+
for worktree in worktrees_dir.iterdir():
|
|
65
|
+
if worktree.is_dir():
|
|
66
|
+
wt_commands = worktree / ".claude" / "commands"
|
|
67
|
+
if wt_commands.exists():
|
|
68
|
+
if dry_run:
|
|
69
|
+
changes.append(
|
|
70
|
+
f"Would remove .claude/commands/ from worktree {worktree.name}"
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
try:
|
|
74
|
+
shutil.rmtree(wt_commands)
|
|
75
|
+
changes.append(
|
|
76
|
+
f"Removed .claude/commands/ from worktree {worktree.name} (inherits from main repo)"
|
|
77
|
+
)
|
|
78
|
+
except OSError as e:
|
|
79
|
+
errors.append(
|
|
80
|
+
f"Failed to remove .claude/commands/ from {worktree.name}: {e}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
success = len(errors) == 0
|
|
84
|
+
return MigrationResult(
|
|
85
|
+
success=success,
|
|
86
|
+
changes_made=changes,
|
|
87
|
+
errors=errors,
|
|
88
|
+
warnings=warnings,
|
|
89
|
+
)
|