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,827 @@
|
|
|
1
|
+
"""Init command implementation for Spec Kitty CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.live import Live
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from ruamel.yaml import YAML
|
|
17
|
+
|
|
18
|
+
from specify_cli.cli import StepTracker, select_with_arrows, multi_select_with_arrows
|
|
19
|
+
from specify_cli.core import (
|
|
20
|
+
AGENT_TOOL_REQUIREMENTS,
|
|
21
|
+
AI_CHOICES,
|
|
22
|
+
DEFAULT_MISSION_KEY,
|
|
23
|
+
DEFAULT_TEMPLATE_REPO,
|
|
24
|
+
MISSION_CHOICES,
|
|
25
|
+
SCRIPT_TYPE_CHOICES,
|
|
26
|
+
check_tool,
|
|
27
|
+
init_git_repo,
|
|
28
|
+
is_git_repo,
|
|
29
|
+
)
|
|
30
|
+
from specify_cli.core.vcs import (
|
|
31
|
+
is_git_available,
|
|
32
|
+
VCSBackend,
|
|
33
|
+
)
|
|
34
|
+
from specify_cli.dashboard import ensure_dashboard_running
|
|
35
|
+
from specify_cli.gitignore_manager import GitignoreManager, ProtectionResult
|
|
36
|
+
from specify_cli.orchestrator.agent_config import (
|
|
37
|
+
AgentConfig,
|
|
38
|
+
AgentSelectionConfig,
|
|
39
|
+
SelectionStrategy,
|
|
40
|
+
save_agent_config,
|
|
41
|
+
)
|
|
42
|
+
from .init_help import INIT_COMMAND_DOC
|
|
43
|
+
from specify_cli.template import (
|
|
44
|
+
GitHubClientError,
|
|
45
|
+
SSL_CONTEXT,
|
|
46
|
+
build_http_client,
|
|
47
|
+
copy_specify_base_from_local,
|
|
48
|
+
copy_specify_base_from_package,
|
|
49
|
+
download_and_extract_template,
|
|
50
|
+
generate_agent_assets,
|
|
51
|
+
get_local_repo_root,
|
|
52
|
+
parse_repo_slug,
|
|
53
|
+
prepare_command_templates,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Module-level variables to hold injected dependencies
|
|
57
|
+
_console: Console | None = None
|
|
58
|
+
_show_banner: Callable[[], None] | None = None
|
|
59
|
+
_activate_mission: Callable[[Path, str, str, Console], str] | None = None
|
|
60
|
+
_ensure_executable_scripts: Callable[[Path, StepTracker | None], None] | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# VCS Detection and Configuration
|
|
65
|
+
# =============================================================================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class VCSNotFoundError(Exception):
|
|
69
|
+
"""Raised when no VCS tools are available."""
|
|
70
|
+
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _detect_default_vcs() -> VCSBackend:
|
|
75
|
+
"""Detect the default VCS based on tool availability.
|
|
76
|
+
|
|
77
|
+
Returns VCSBackend.GIT if git is available.
|
|
78
|
+
Raises VCSNotFoundError if git is not available.
|
|
79
|
+
|
|
80
|
+
Note: jj support removed due to sparse checkout incompatibility.
|
|
81
|
+
"""
|
|
82
|
+
if is_git_available():
|
|
83
|
+
return VCSBackend.GIT
|
|
84
|
+
else:
|
|
85
|
+
raise VCSNotFoundError("git is not available. Please install git.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _display_vcs_info(detected_vcs: VCSBackend, console: Console) -> None:
|
|
89
|
+
"""Display informational message about VCS selection.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
detected_vcs: The detected/selected VCS backend (always GIT)
|
|
93
|
+
console: Rich console for output
|
|
94
|
+
"""
|
|
95
|
+
console.print("[green]✓ git detected[/green] - will be used for version control")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _save_vcs_config(config_path: Path, detected_vcs: VCSBackend) -> None:
|
|
99
|
+
"""Save VCS preference to config.yaml.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
config_path: Path to .kittify directory
|
|
103
|
+
detected_vcs: The detected/selected VCS backend (always GIT)
|
|
104
|
+
"""
|
|
105
|
+
config_file = config_path / "config.yaml"
|
|
106
|
+
|
|
107
|
+
yaml = YAML()
|
|
108
|
+
yaml.preserve_quotes = True
|
|
109
|
+
|
|
110
|
+
# Load existing config or create new
|
|
111
|
+
if config_file.exists():
|
|
112
|
+
with open(config_file, "r") as f:
|
|
113
|
+
config = yaml.load(f) or {}
|
|
114
|
+
else:
|
|
115
|
+
config = {}
|
|
116
|
+
config_path.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
# Add/update vcs section (git only)
|
|
119
|
+
config["vcs"] = {
|
|
120
|
+
"type": "git",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Write back
|
|
124
|
+
with open(config_file, "w") as f:
|
|
125
|
+
yaml.dump(config, f)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _install_git_hooks(project_path: Path, templates_root: Path | None = None, tracker: StepTracker | None = None) -> None:
|
|
129
|
+
"""Install git hooks from templates to .git/hooks directory.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
project_path: Path to the project root
|
|
133
|
+
templates_root: Path to the templates directory (if available)
|
|
134
|
+
tracker: Optional progress tracker
|
|
135
|
+
"""
|
|
136
|
+
git_hooks_dir = project_path / ".git" / "hooks"
|
|
137
|
+
# Use templates_root if available, otherwise fall back to user project (for backwards compat)
|
|
138
|
+
if templates_root:
|
|
139
|
+
template_hooks_dir = templates_root / "git-hooks"
|
|
140
|
+
else:
|
|
141
|
+
template_hooks_dir = project_path / ".kittify" / "templates" / "git-hooks"
|
|
142
|
+
|
|
143
|
+
if not git_hooks_dir.exists():
|
|
144
|
+
if tracker:
|
|
145
|
+
tracker.skip("git-hooks", ".git/hooks directory not found")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if not template_hooks_dir.exists():
|
|
149
|
+
if tracker:
|
|
150
|
+
tracker.skip("git-hooks", "no hook templates found")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
installed_count = 0
|
|
154
|
+
for hook_template in template_hooks_dir.iterdir():
|
|
155
|
+
if hook_template.is_file() and not hook_template.name.startswith('.'):
|
|
156
|
+
hook_dest = git_hooks_dir / hook_template.name
|
|
157
|
+
shutil.copy2(hook_template, hook_dest)
|
|
158
|
+
# Make executable on POSIX systems
|
|
159
|
+
if os.name != "nt":
|
|
160
|
+
hook_dest.chmod(0o755)
|
|
161
|
+
installed_count += 1
|
|
162
|
+
|
|
163
|
+
if tracker:
|
|
164
|
+
if installed_count > 0:
|
|
165
|
+
tracker.complete("git-hooks", f"{installed_count} hook(s) installed")
|
|
166
|
+
else:
|
|
167
|
+
tracker.skip("git-hooks", "no hooks to install")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def init(
|
|
171
|
+
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
|
|
172
|
+
ai_assistant: str = typer.Option(None, "--ai", help="Comma-separated AI assistants (claude,codex,gemini,...)", rich_help_panel="Selection"),
|
|
173
|
+
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps", rich_help_panel="Selection"),
|
|
174
|
+
mission_key: str = typer.Option(None, "--mission", hidden=True, help="[DEPRECATED] Mission selection moved to /spec-kitty.specify"),
|
|
175
|
+
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
|
|
176
|
+
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
|
|
177
|
+
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
|
|
178
|
+
force: bool = typer.Option(False, "--force", help="Force merge/overwrite when using --here (skip confirmation)"),
|
|
179
|
+
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
|
|
180
|
+
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
|
|
181
|
+
github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
|
|
182
|
+
template_root: str = typer.Option(None, "--template-root", help="Override default template location (useful for development mode)"),
|
|
183
|
+
):
|
|
184
|
+
"""Initialize a new Spec Kitty project."""
|
|
185
|
+
# Use the injected dependencies
|
|
186
|
+
assert _console is not None
|
|
187
|
+
assert _show_banner is not None
|
|
188
|
+
assert _activate_mission is not None
|
|
189
|
+
assert _ensure_executable_scripts is not None
|
|
190
|
+
|
|
191
|
+
_show_banner()
|
|
192
|
+
|
|
193
|
+
# Handle '.' as shorthand for current directory (equivalent to --here)
|
|
194
|
+
if project_name == ".":
|
|
195
|
+
here = True
|
|
196
|
+
project_name = None # Clear project_name to use existing validation logic
|
|
197
|
+
|
|
198
|
+
if here and project_name:
|
|
199
|
+
_console.print("[red]Error:[/red] Cannot specify both project name and --here flag")
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
|
|
202
|
+
if not here and not project_name:
|
|
203
|
+
_console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag")
|
|
204
|
+
raise typer.Exit(1)
|
|
205
|
+
|
|
206
|
+
if here:
|
|
207
|
+
try:
|
|
208
|
+
project_path = Path.cwd()
|
|
209
|
+
project_name = project_path.name
|
|
210
|
+
except (OSError, FileNotFoundError) as e:
|
|
211
|
+
_console.print("[red]Error:[/red] Cannot access current directory")
|
|
212
|
+
_console.print(f"[dim]{e}[/dim]")
|
|
213
|
+
_console.print("[yellow]Hint:[/yellow] Your current directory may have been deleted or is no longer accessible")
|
|
214
|
+
raise typer.Exit(1)
|
|
215
|
+
|
|
216
|
+
existing_items = list(project_path.iterdir())
|
|
217
|
+
if existing_items:
|
|
218
|
+
_console.print(f"[yellow]Warning:[/yellow] Current directory is not empty ({len(existing_items)} items)")
|
|
219
|
+
_console.print("[yellow]Template files will be merged with existing content and may overwrite existing files[/yellow]")
|
|
220
|
+
if force:
|
|
221
|
+
_console.print("[cyan]--force supplied: skipping confirmation and proceeding with merge[/cyan]")
|
|
222
|
+
else:
|
|
223
|
+
response = typer.confirm("Do you want to continue?")
|
|
224
|
+
if not response:
|
|
225
|
+
_console.print("[yellow]Operation cancelled[/yellow]")
|
|
226
|
+
raise typer.Exit(0)
|
|
227
|
+
else:
|
|
228
|
+
project_path = Path(project_name).resolve()
|
|
229
|
+
if project_path.exists():
|
|
230
|
+
error_panel = Panel(
|
|
231
|
+
f"Directory '[cyan]{project_name}[/cyan]' already exists\n"
|
|
232
|
+
"Please choose a different project name or remove the existing directory.",
|
|
233
|
+
title="[red]Directory Conflict[/red]",
|
|
234
|
+
border_style="red",
|
|
235
|
+
padding=(1, 2)
|
|
236
|
+
)
|
|
237
|
+
_console.print()
|
|
238
|
+
_console.print(error_panel)
|
|
239
|
+
raise typer.Exit(1)
|
|
240
|
+
|
|
241
|
+
current_dir = Path.cwd()
|
|
242
|
+
|
|
243
|
+
setup_lines = [
|
|
244
|
+
"[cyan]Specify Project Setup[/cyan]",
|
|
245
|
+
"",
|
|
246
|
+
f"{'Project':<15} [green]{project_path.name}[/green]",
|
|
247
|
+
f"{'Working Path':<15} [dim]{current_dir}[/dim]",
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
# Add target path only if different from working dir
|
|
251
|
+
if not here:
|
|
252
|
+
setup_lines.append(f"{'Target Path':<15} [dim]{project_path}[/dim]")
|
|
253
|
+
|
|
254
|
+
_console.print(Panel("\n".join(setup_lines), border_style="cyan", padding=(1, 2)))
|
|
255
|
+
|
|
256
|
+
# Check git only if we might need it (not --no-git)
|
|
257
|
+
# Only set to True if the user wants it and the tool is available
|
|
258
|
+
should_init_git = False
|
|
259
|
+
if not no_git:
|
|
260
|
+
should_init_git = check_tool("git", "https://git-scm.com/downloads")
|
|
261
|
+
if not should_init_git:
|
|
262
|
+
_console.print("[yellow]Git not found - will skip repository initialization[/yellow]")
|
|
263
|
+
|
|
264
|
+
# Detect VCS (git only, jj support removed)
|
|
265
|
+
selected_vcs: VCSBackend | None = None
|
|
266
|
+
try:
|
|
267
|
+
selected_vcs = _detect_default_vcs()
|
|
268
|
+
_console.print()
|
|
269
|
+
_display_vcs_info(selected_vcs, _console)
|
|
270
|
+
_console.print()
|
|
271
|
+
except VCSNotFoundError:
|
|
272
|
+
# git not available - not an error, just informational
|
|
273
|
+
selected_vcs = None
|
|
274
|
+
_console.print("[yellow]ℹ git not detected[/yellow] - install git for version control")
|
|
275
|
+
|
|
276
|
+
if ai_assistant:
|
|
277
|
+
raw_agents = [part.strip().lower() for part in ai_assistant.replace(";", ",").split(",") if part.strip()]
|
|
278
|
+
if not raw_agents:
|
|
279
|
+
_console.print("[red]Error:[/red] --ai flag did not contain any valid agent identifiers")
|
|
280
|
+
raise typer.Exit(1)
|
|
281
|
+
selected_agents: list[str] = []
|
|
282
|
+
seen_agents: set[str] = set()
|
|
283
|
+
invalid_agents: list[str] = []
|
|
284
|
+
for key in raw_agents:
|
|
285
|
+
if key not in AI_CHOICES:
|
|
286
|
+
invalid_agents.append(key)
|
|
287
|
+
continue
|
|
288
|
+
if key not in seen_agents:
|
|
289
|
+
selected_agents.append(key)
|
|
290
|
+
seen_agents.add(key)
|
|
291
|
+
if invalid_agents:
|
|
292
|
+
_console.print(
|
|
293
|
+
f"[red]Error:[/red] Invalid AI assistant(s): {', '.join(invalid_agents)}. "
|
|
294
|
+
f"Choose from: {', '.join(AI_CHOICES.keys())}"
|
|
295
|
+
)
|
|
296
|
+
raise typer.Exit(1)
|
|
297
|
+
else:
|
|
298
|
+
selected_agents = multi_select_with_arrows(
|
|
299
|
+
AI_CHOICES,
|
|
300
|
+
"Choose your AI assistant(s):",
|
|
301
|
+
default_keys=["copilot"],
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if not selected_agents:
|
|
305
|
+
_console.print("[red]Error:[/red] No AI assistants selected")
|
|
306
|
+
raise typer.Exit(1)
|
|
307
|
+
|
|
308
|
+
# Check agent tools unless ignored
|
|
309
|
+
if not ignore_agent_tools:
|
|
310
|
+
missing_agents: list[tuple[str, str, str]] = [] # (agent key, display, url)
|
|
311
|
+
for agent_key in selected_agents:
|
|
312
|
+
requirement = AGENT_TOOL_REQUIREMENTS.get(agent_key)
|
|
313
|
+
if not requirement:
|
|
314
|
+
continue
|
|
315
|
+
tool_name, url = requirement
|
|
316
|
+
if not check_tool(tool_name, url, agent_name=agent_key):
|
|
317
|
+
missing_agents.append((agent_key, AI_CHOICES[agent_key], url))
|
|
318
|
+
|
|
319
|
+
if missing_agents:
|
|
320
|
+
lines = []
|
|
321
|
+
for agent_key, display_name, url in missing_agents:
|
|
322
|
+
lines.append(f"[cyan]{display_name}[/cyan] ({agent_key}) → install: [cyan]{url}[/cyan]")
|
|
323
|
+
lines.append("")
|
|
324
|
+
lines.append("These tools are optional. You can install them later to enable additional features.")
|
|
325
|
+
warning_panel = Panel(
|
|
326
|
+
"\n".join(lines),
|
|
327
|
+
title="[yellow]Optional Agent Tool(s) Not Found[/yellow]",
|
|
328
|
+
border_style="yellow",
|
|
329
|
+
padding=(1, 2),
|
|
330
|
+
)
|
|
331
|
+
_console.print()
|
|
332
|
+
_console.print(warning_panel)
|
|
333
|
+
# Continue with init instead of blocking
|
|
334
|
+
|
|
335
|
+
# ==========================================================================
|
|
336
|
+
# Agent Selection Strategy (for orchestrator)
|
|
337
|
+
# ==========================================================================
|
|
338
|
+
_console.print()
|
|
339
|
+
_console.print("[cyan]Agent Selection Strategy[/cyan]")
|
|
340
|
+
_console.print("[dim]This determines how agents are chosen for implementation and review tasks.[/dim]")
|
|
341
|
+
_console.print()
|
|
342
|
+
|
|
343
|
+
strategy_choices = {
|
|
344
|
+
"preferred": "Preferred Agents - You choose which agent implements and which reviews",
|
|
345
|
+
"random": "Random - Randomly select from available agents each time",
|
|
346
|
+
}
|
|
347
|
+
selected_strategy = select_with_arrows(
|
|
348
|
+
strategy_choices,
|
|
349
|
+
"How should agents be selected for tasks?",
|
|
350
|
+
default_key="preferred",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
preferred_implementer: str | None = None
|
|
354
|
+
preferred_reviewer: str | None = None
|
|
355
|
+
|
|
356
|
+
if selected_strategy == "preferred":
|
|
357
|
+
# Ask for preferred implementer
|
|
358
|
+
agent_display_map = {key: AI_CHOICES[key] for key in selected_agents}
|
|
359
|
+
|
|
360
|
+
_console.print()
|
|
361
|
+
preferred_implementer = select_with_arrows(
|
|
362
|
+
agent_display_map,
|
|
363
|
+
"Which agent should be the preferred IMPLEMENTER?",
|
|
364
|
+
default_key=selected_agents[0],
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Ask for preferred reviewer (prefer different from implementer)
|
|
368
|
+
_console.print()
|
|
369
|
+
if len(selected_agents) > 1:
|
|
370
|
+
# Default to a different agent for review
|
|
371
|
+
default_reviewer = next((a for a in selected_agents if a != preferred_implementer), selected_agents[0])
|
|
372
|
+
preferred_reviewer = select_with_arrows(
|
|
373
|
+
agent_display_map,
|
|
374
|
+
"Which agent should be the preferred REVIEWER?",
|
|
375
|
+
default_key=default_reviewer,
|
|
376
|
+
)
|
|
377
|
+
if preferred_reviewer == preferred_implementer and len(selected_agents) > 1:
|
|
378
|
+
_console.print("[yellow]Note:[/yellow] Same agent for implementation and review (cross-review disabled)")
|
|
379
|
+
else:
|
|
380
|
+
# Only one agent - same for both
|
|
381
|
+
preferred_reviewer = preferred_implementer
|
|
382
|
+
_console.print(f"[dim]Single agent mode: {AI_CHOICES[preferred_implementer]} will do both implementation and review[/dim]")
|
|
383
|
+
|
|
384
|
+
# Build agent config to save later
|
|
385
|
+
agent_config = AgentConfig(
|
|
386
|
+
available=selected_agents,
|
|
387
|
+
selection=AgentSelectionConfig(
|
|
388
|
+
strategy=SelectionStrategy(selected_strategy),
|
|
389
|
+
preferred_implementer=preferred_implementer,
|
|
390
|
+
preferred_reviewer=preferred_reviewer,
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Determine script type (explicit or auto-detect)
|
|
395
|
+
if script_type:
|
|
396
|
+
if script_type not in SCRIPT_TYPE_CHOICES:
|
|
397
|
+
_console.print(f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}")
|
|
398
|
+
raise typer.Exit(1)
|
|
399
|
+
selected_script = script_type
|
|
400
|
+
else:
|
|
401
|
+
# Auto-detect based on platform
|
|
402
|
+
selected_script = "ps" if os.name == "nt" else "sh"
|
|
403
|
+
_console.print(f"[dim]Auto-detected script type:[/dim] [cyan]{SCRIPT_TYPE_CHOICES[selected_script]}[/cyan]")
|
|
404
|
+
|
|
405
|
+
# Mission selection deprecated - missions are now per-feature
|
|
406
|
+
if mission_key:
|
|
407
|
+
_console.print("[yellow]Warning:[/yellow] The --mission flag is deprecated. Missions are now selected per-feature during /spec-kitty.specify")
|
|
408
|
+
_console.print("[dim]Ignoring --mission flag and continuing with initialization...[/dim]")
|
|
409
|
+
_console.print()
|
|
410
|
+
|
|
411
|
+
# No longer select a project-level mission - just use software-dev for initial setup
|
|
412
|
+
selected_mission = DEFAULT_MISSION_KEY
|
|
413
|
+
mission_display = MISSION_CHOICES.get(selected_mission, "Software Dev Kitty")
|
|
414
|
+
|
|
415
|
+
template_mode = "package"
|
|
416
|
+
local_repo = get_local_repo_root(override_path=template_root)
|
|
417
|
+
if local_repo is not None:
|
|
418
|
+
template_mode = "local"
|
|
419
|
+
if debug:
|
|
420
|
+
_console.print(f"[cyan]Using local templates from[/cyan] {local_repo}")
|
|
421
|
+
|
|
422
|
+
repo_owner = repo_name = None
|
|
423
|
+
remote_slug_env = os.environ.get("SPECIFY_TEMPLATE_REPO")
|
|
424
|
+
if remote_slug_env:
|
|
425
|
+
try:
|
|
426
|
+
repo_owner, repo_name = parse_repo_slug(remote_slug_env)
|
|
427
|
+
except ValueError as exc:
|
|
428
|
+
_console.print(f"[red]Error:[/red] {exc}")
|
|
429
|
+
raise typer.Exit(1)
|
|
430
|
+
template_mode = "remote"
|
|
431
|
+
if debug:
|
|
432
|
+
_console.print(f"[cyan]Using remote templates from[/cyan] {repo_owner}/{repo_name}")
|
|
433
|
+
elif template_mode == "package" and debug:
|
|
434
|
+
_console.print("[cyan]Using templates bundled with specify_cli package[/cyan]")
|
|
435
|
+
|
|
436
|
+
ai_display = ", ".join(AI_CHOICES[key] for key in selected_agents)
|
|
437
|
+
_console.print(f"[cyan]Selected AI assistant(s):[/cyan] {ai_display}")
|
|
438
|
+
_console.print(f"[cyan]Selected script type:[/cyan] {selected_script}")
|
|
439
|
+
_console.print(f"[cyan]Selected mission:[/cyan] {mission_display}")
|
|
440
|
+
|
|
441
|
+
# Download and set up project
|
|
442
|
+
# New tree-based progress (no emojis); include earlier substeps
|
|
443
|
+
tracker = StepTracker("Initialize Specify Project")
|
|
444
|
+
# Flag to allow suppressing legacy headings
|
|
445
|
+
sys._specify_tracker_active = True
|
|
446
|
+
# Pre steps recorded as completed before live rendering
|
|
447
|
+
tracker.add("precheck", "Check required tools")
|
|
448
|
+
tracker.complete("precheck", "ok")
|
|
449
|
+
tracker.add("ai-select", "Select AI assistant(s)")
|
|
450
|
+
tracker.complete("ai-select", ai_display)
|
|
451
|
+
tracker.add("script-select", "Select script type")
|
|
452
|
+
tracker.complete("script-select", selected_script)
|
|
453
|
+
tracker.add("mission-select", "Select mission")
|
|
454
|
+
tracker.complete("mission-select", mission_display)
|
|
455
|
+
tracker.add("mission-activate", "Activate mission")
|
|
456
|
+
for agent_key in selected_agents:
|
|
457
|
+
label = AI_CHOICES[agent_key]
|
|
458
|
+
tracker.add(f"{agent_key}-fetch", f"{label}: fetch latest release")
|
|
459
|
+
tracker.add(f"{agent_key}-download", f"{label}: download template")
|
|
460
|
+
tracker.add(f"{agent_key}-extract", f"{label}: extract template")
|
|
461
|
+
tracker.add(f"{agent_key}-zip-list", f"{label}: archive contents")
|
|
462
|
+
tracker.add(f"{agent_key}-extracted-summary", f"{label}: extraction summary")
|
|
463
|
+
tracker.add(f"{agent_key}-cleanup", f"{label}: cleanup")
|
|
464
|
+
for key, label in [
|
|
465
|
+
("chmod", "Ensure scripts executable"),
|
|
466
|
+
("git", "Initialize git repository"),
|
|
467
|
+
("final", "Finalize"),
|
|
468
|
+
]:
|
|
469
|
+
tracker.add(key, label)
|
|
470
|
+
|
|
471
|
+
if template_mode in ("local", "package") and not here and not project_path.exists():
|
|
472
|
+
project_path.mkdir(parents=True)
|
|
473
|
+
|
|
474
|
+
command_templates_dir: Path | None = None
|
|
475
|
+
render_templates_dir: Path | None = None
|
|
476
|
+
templates_root: Path | None = None # Track template source for later use
|
|
477
|
+
base_prepared = False
|
|
478
|
+
if template_mode == "remote" and (repo_owner is None or repo_name is None):
|
|
479
|
+
repo_owner, repo_name = parse_repo_slug(DEFAULT_TEMPLATE_REPO)
|
|
480
|
+
|
|
481
|
+
with Live(tracker.render(), console=_console, refresh_per_second=8, transient=True) as live:
|
|
482
|
+
tracker.attach_refresh(lambda: live.update(tracker.render()))
|
|
483
|
+
try:
|
|
484
|
+
# Create a httpx client with verify based on skip_tls
|
|
485
|
+
local_client = build_http_client(skip_tls=skip_tls)
|
|
486
|
+
|
|
487
|
+
for index, agent_key in enumerate(selected_agents):
|
|
488
|
+
if template_mode in ("local", "package"):
|
|
489
|
+
source_detail = "local checkout" if template_mode == "local" else "packaged data"
|
|
490
|
+
tracker.start(f"{agent_key}-fetch")
|
|
491
|
+
tracker.complete(f"{agent_key}-fetch", source_detail)
|
|
492
|
+
tracker.start(f"{agent_key}-download")
|
|
493
|
+
tracker.complete(f"{agent_key}-download", "local files")
|
|
494
|
+
tracker.start(f"{agent_key}-extract")
|
|
495
|
+
try:
|
|
496
|
+
if not base_prepared:
|
|
497
|
+
if template_mode == "local":
|
|
498
|
+
command_templates_dir = copy_specify_base_from_local(local_repo, project_path, selected_script)
|
|
499
|
+
else:
|
|
500
|
+
command_templates_dir = copy_specify_base_from_package(project_path, selected_script)
|
|
501
|
+
base_prepared = True
|
|
502
|
+
# Track templates root for later use (AGENTS.md, .claudeignore, git-hooks)
|
|
503
|
+
if command_templates_dir:
|
|
504
|
+
templates_root = command_templates_dir.parent
|
|
505
|
+
if command_templates_dir is None:
|
|
506
|
+
raise RuntimeError("Command templates directory was not prepared")
|
|
507
|
+
if render_templates_dir is None:
|
|
508
|
+
mission_templates_dir = (
|
|
509
|
+
project_path
|
|
510
|
+
/ ".kittify"
|
|
511
|
+
/ "missions"
|
|
512
|
+
/ selected_mission
|
|
513
|
+
/ "command-templates"
|
|
514
|
+
)
|
|
515
|
+
render_templates_dir = prepare_command_templates(
|
|
516
|
+
command_templates_dir,
|
|
517
|
+
mission_templates_dir,
|
|
518
|
+
)
|
|
519
|
+
generate_agent_assets(render_templates_dir, project_path, agent_key, selected_script)
|
|
520
|
+
except Exception as exc:
|
|
521
|
+
tracker.error(f"{agent_key}-extract", str(exc))
|
|
522
|
+
raise
|
|
523
|
+
else:
|
|
524
|
+
tracker.complete(f"{agent_key}-extract", "commands generated")
|
|
525
|
+
tracker.start(f"{agent_key}-zip-list")
|
|
526
|
+
tracker.complete(f"{agent_key}-zip-list", "templates ready")
|
|
527
|
+
tracker.start(f"{agent_key}-extracted-summary")
|
|
528
|
+
tracker.complete(f"{agent_key}-extracted-summary", "commands ready")
|
|
529
|
+
tracker.start(f"{agent_key}-cleanup")
|
|
530
|
+
tracker.complete(f"{agent_key}-cleanup", "done")
|
|
531
|
+
else:
|
|
532
|
+
is_current_dir_flag = here if index == 0 else True
|
|
533
|
+
allow_existing_flag = index > 0
|
|
534
|
+
download_and_extract_template(
|
|
535
|
+
project_path,
|
|
536
|
+
agent_key,
|
|
537
|
+
selected_script,
|
|
538
|
+
is_current_dir_flag,
|
|
539
|
+
verbose=False,
|
|
540
|
+
tracker=tracker,
|
|
541
|
+
tracker_prefix=agent_key,
|
|
542
|
+
allow_existing=allow_existing_flag,
|
|
543
|
+
client=local_client,
|
|
544
|
+
debug=debug,
|
|
545
|
+
github_token=github_token,
|
|
546
|
+
repo_owner=repo_owner,
|
|
547
|
+
repo_name=repo_name,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
tracker.start("mission-activate")
|
|
551
|
+
try:
|
|
552
|
+
mission_status = _activate_mission(project_path, selected_mission, mission_display, _console)
|
|
553
|
+
except Exception as exc:
|
|
554
|
+
tracker.error("mission-activate", str(exc))
|
|
555
|
+
raise
|
|
556
|
+
else:
|
|
557
|
+
tracker.complete("mission-activate", mission_status)
|
|
558
|
+
|
|
559
|
+
# Ensure scripts are executable (POSIX)
|
|
560
|
+
_ensure_executable_scripts(project_path, tracker=tracker)
|
|
561
|
+
|
|
562
|
+
# Git step - must happen BEFORE hook installation
|
|
563
|
+
if not no_git:
|
|
564
|
+
tracker.start("git")
|
|
565
|
+
if is_git_repo(project_path):
|
|
566
|
+
tracker.complete("git", "existing repo detected")
|
|
567
|
+
elif should_init_git:
|
|
568
|
+
if init_git_repo(project_path, quiet=True):
|
|
569
|
+
tracker.complete("git", "initialized")
|
|
570
|
+
else:
|
|
571
|
+
tracker.error("git", "init failed")
|
|
572
|
+
else:
|
|
573
|
+
tracker.skip("git", "git not available")
|
|
574
|
+
else:
|
|
575
|
+
tracker.skip("git", "--no-git flag")
|
|
576
|
+
|
|
577
|
+
# Install git hooks AFTER git is initialized
|
|
578
|
+
if not no_git and is_git_repo(project_path):
|
|
579
|
+
tracker.add("git-hooks", "Install git hooks")
|
|
580
|
+
tracker.start("git-hooks")
|
|
581
|
+
_install_git_hooks(project_path, templates_root=templates_root, tracker=tracker)
|
|
582
|
+
|
|
583
|
+
tracker.complete("final", "project ready")
|
|
584
|
+
except Exception as e:
|
|
585
|
+
tracker.error("final", str(e))
|
|
586
|
+
_console.print(Panel(f"Initialization failed: {e}", title="Failure", border_style="red"))
|
|
587
|
+
if debug:
|
|
588
|
+
_env_pairs = [
|
|
589
|
+
("Python", sys.version.split()[0]),
|
|
590
|
+
("Platform", sys.platform),
|
|
591
|
+
("CWD", str(Path.cwd())),
|
|
592
|
+
]
|
|
593
|
+
_label_width = max(len(k) for k, _ in _env_pairs)
|
|
594
|
+
env_lines = [f"{k.ljust(_label_width)} → [bright_black]{v}[/bright_black]" for k, v in _env_pairs]
|
|
595
|
+
_console.print(Panel("\n".join(env_lines), title="Debug Environment", border_style="magenta"))
|
|
596
|
+
if not here and project_path.exists():
|
|
597
|
+
shutil.rmtree(project_path)
|
|
598
|
+
raise typer.Exit(1)
|
|
599
|
+
finally:
|
|
600
|
+
# Force final render
|
|
601
|
+
pass
|
|
602
|
+
|
|
603
|
+
# Final static tree (ensures finished state visible after Live context ends)
|
|
604
|
+
_console.print(tracker.render())
|
|
605
|
+
_console.print("\n[bold green]Project ready.[/bold green]")
|
|
606
|
+
|
|
607
|
+
# Agent folder security notice
|
|
608
|
+
agent_folder_map = {
|
|
609
|
+
"claude": ".claude/",
|
|
610
|
+
"gemini": ".gemini/",
|
|
611
|
+
"cursor": ".cursor/",
|
|
612
|
+
"qwen": ".qwen/",
|
|
613
|
+
"opencode": ".opencode/",
|
|
614
|
+
"codex": ".codex/",
|
|
615
|
+
"windsurf": ".windsurf/",
|
|
616
|
+
"kilocode": ".kilocode/",
|
|
617
|
+
"auggie": ".augment/",
|
|
618
|
+
"copilot": ".github/",
|
|
619
|
+
"roo": ".roo/",
|
|
620
|
+
"q": ".amazonq/"
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
notice_entries = []
|
|
624
|
+
for agent_key in selected_agents:
|
|
625
|
+
folder = agent_folder_map.get(agent_key)
|
|
626
|
+
if folder:
|
|
627
|
+
notice_entries.append((AI_CHOICES[agent_key], folder))
|
|
628
|
+
|
|
629
|
+
if notice_entries:
|
|
630
|
+
body_lines = [
|
|
631
|
+
"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.",
|
|
632
|
+
"Consider adding the following folders (or subsets) to [cyan].gitignore[/cyan]:",
|
|
633
|
+
"",
|
|
634
|
+
]
|
|
635
|
+
body_lines.extend(f"- {display}: [cyan]{folder}[/cyan]" for display, folder in notice_entries)
|
|
636
|
+
security_notice = Panel(
|
|
637
|
+
"\n".join(body_lines),
|
|
638
|
+
title="[yellow]Agent Folder Security[/yellow]",
|
|
639
|
+
border_style="yellow",
|
|
640
|
+
padding=(1, 2),
|
|
641
|
+
)
|
|
642
|
+
_console.print()
|
|
643
|
+
_console.print(security_notice)
|
|
644
|
+
|
|
645
|
+
# Boxed "Next steps" section
|
|
646
|
+
steps_lines = []
|
|
647
|
+
step_num = 1
|
|
648
|
+
if not here:
|
|
649
|
+
steps_lines.append(f"{step_num}. Go to the project folder: [cyan]cd {project_name}[/cyan]")
|
|
650
|
+
else:
|
|
651
|
+
steps_lines.append(f"{step_num}. You're already in the project directory!")
|
|
652
|
+
step_num += 1
|
|
653
|
+
|
|
654
|
+
steps_lines.append(f"{step_num}. Available missions: [cyan]software-dev[/cyan], [cyan]research[/cyan] (selected per-feature during [cyan]/spec-kitty.specify[/cyan])")
|
|
655
|
+
step_num += 1
|
|
656
|
+
|
|
657
|
+
steps_lines.append(f"{step_num}. Start using slash commands with your AI agent (in workflow order):")
|
|
658
|
+
step_num += 1
|
|
659
|
+
|
|
660
|
+
steps_lines.append(" - [cyan]/spec-kitty.dashboard[/] - Open the real-time kanban dashboard")
|
|
661
|
+
steps_lines.append(" - [cyan]/spec-kitty.constitution[/] - Establish project principles")
|
|
662
|
+
steps_lines.append(" - [cyan]/spec-kitty.specify[/] - Create baseline specification")
|
|
663
|
+
steps_lines.append(" - [cyan]/spec-kitty.plan[/] - Create implementation plan")
|
|
664
|
+
steps_lines.append(" - [cyan]/spec-kitty.research[/] - Run mission-specific Phase 0 research scaffolding")
|
|
665
|
+
steps_lines.append(" - [cyan]/spec-kitty.tasks[/] - Generate tasks and kanban-ready prompt files")
|
|
666
|
+
steps_lines.append(" - [cyan]/spec-kitty.implement[/] - Execute implementation from /tasks/doing/")
|
|
667
|
+
steps_lines.append(" - [cyan]/spec-kitty.review[/] - Review prompts and move them to /tasks/done/")
|
|
668
|
+
steps_lines.append(" - [cyan]/spec-kitty.accept[/] - Run acceptance checks and verify feature complete")
|
|
669
|
+
steps_lines.append(" - [cyan]/spec-kitty.merge[/] - Merge feature into main and cleanup worktree")
|
|
670
|
+
|
|
671
|
+
steps_panel = Panel("\n".join(steps_lines), title="Next Steps", border_style="cyan", padding=(1,2))
|
|
672
|
+
_console.print()
|
|
673
|
+
_console.print(steps_panel)
|
|
674
|
+
|
|
675
|
+
enhancement_lines = [
|
|
676
|
+
"Optional commands that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]",
|
|
677
|
+
"",
|
|
678
|
+
f"○ [cyan]/spec-kitty.clarify[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]/spec-kitty.plan[/] if used)",
|
|
679
|
+
f"○ [cyan]/spec-kitty.analyze[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]/spec-kitty.tasks[/], before [cyan]/spec-kitty.implement[/])",
|
|
680
|
+
f"○ [cyan]/spec-kitty.checklist[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]/spec-kitty.plan[/])"
|
|
681
|
+
]
|
|
682
|
+
enhancements_panel = Panel("\n".join(enhancement_lines), title="Enhancement Commands", border_style="cyan", padding=(1,2))
|
|
683
|
+
_console.print()
|
|
684
|
+
_console.print(enhancements_panel)
|
|
685
|
+
|
|
686
|
+
# Start or reconnect to the dashboard server as a detached background process
|
|
687
|
+
_console.print()
|
|
688
|
+
try:
|
|
689
|
+
dashboard_url, port, started = ensure_dashboard_running(project_path)
|
|
690
|
+
|
|
691
|
+
title = "[bold green]Spec Kitty Dashboard Started[/bold green]" if started else "[bold green]Spec Kitty Dashboard Ready[/bold green]"
|
|
692
|
+
status_line = (
|
|
693
|
+
"[dim]The dashboard is running in the background and will continue even after\n"
|
|
694
|
+
"this command exits. It will automatically update as you work.[/dim]"
|
|
695
|
+
if started
|
|
696
|
+
else "[dim]An existing dashboard instance is running and ready.[/dim]"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
dashboard_panel = Panel(
|
|
700
|
+
f"[bold cyan]Dashboard URL:[/bold cyan] {dashboard_url}\n"
|
|
701
|
+
f"[bold cyan]Port:[/bold cyan] {port}\n\n"
|
|
702
|
+
f"{status_line}\n\n"
|
|
703
|
+
f"[yellow]Tip:[/yellow] Run [cyan]/spec-kitty.dashboard[/cyan] or [cyan]spec-kitty dashboard[/cyan] to open it in your browser",
|
|
704
|
+
title=title,
|
|
705
|
+
border_style="green",
|
|
706
|
+
padding=(1, 2)
|
|
707
|
+
)
|
|
708
|
+
_console.print(dashboard_panel)
|
|
709
|
+
_console.print()
|
|
710
|
+
except Exception as e:
|
|
711
|
+
_console.print(f"[yellow]Warning: Could not start dashboard: {e}[/yellow]")
|
|
712
|
+
_console.print("[dim]Continuing without dashboard...[/dim]")
|
|
713
|
+
|
|
714
|
+
# Protect ALL agent directories in .gitignore
|
|
715
|
+
manager = GitignoreManager(project_path)
|
|
716
|
+
result = manager.protect_all_agents() # Note: ALL agents, not just selected
|
|
717
|
+
|
|
718
|
+
# Display results to user
|
|
719
|
+
if result.modified:
|
|
720
|
+
_console.print("[cyan]Updated .gitignore to exclude AI agent directories:[/cyan]")
|
|
721
|
+
for entry in result.entries_added:
|
|
722
|
+
_console.print(f" • {entry}")
|
|
723
|
+
if result.entries_skipped:
|
|
724
|
+
_console.print(f" ({len(result.entries_skipped)} already protected)")
|
|
725
|
+
elif result.entries_skipped:
|
|
726
|
+
_console.print(f"[dim]All {len(result.entries_skipped)} agent directories already in .gitignore[/dim]")
|
|
727
|
+
|
|
728
|
+
# Show warnings (especially for .github/)
|
|
729
|
+
for warning in result.warnings:
|
|
730
|
+
_console.print(f"[yellow]⚠️ {warning}[/yellow]")
|
|
731
|
+
|
|
732
|
+
# Show errors if any
|
|
733
|
+
for error in result.errors:
|
|
734
|
+
_console.print(f"[red]❌ {error}[/red]")
|
|
735
|
+
|
|
736
|
+
# Copy AGENTS.md from template source (not user project)
|
|
737
|
+
if templates_root:
|
|
738
|
+
agents_target = project_path / ".kittify" / "AGENTS.md"
|
|
739
|
+
agents_template = templates_root / "AGENTS.md"
|
|
740
|
+
if not agents_target.exists() and agents_template.exists():
|
|
741
|
+
shutil.copy2(agents_template, agents_target)
|
|
742
|
+
|
|
743
|
+
# Generate .claudeignore from template source
|
|
744
|
+
claudeignore_template = templates_root / "claudeignore-template"
|
|
745
|
+
claudeignore_dest = project_path / ".claudeignore"
|
|
746
|
+
if claudeignore_template.exists() and not claudeignore_dest.exists():
|
|
747
|
+
shutil.copy2(claudeignore_template, claudeignore_dest)
|
|
748
|
+
_console.print("[dim]Created .claudeignore to optimize AI assistant scanning[/dim]")
|
|
749
|
+
|
|
750
|
+
# Create project metadata for upgrade tracking
|
|
751
|
+
try:
|
|
752
|
+
from datetime import datetime
|
|
753
|
+
import platform as plat
|
|
754
|
+
import sys as system
|
|
755
|
+
from specify_cli import __version__
|
|
756
|
+
from specify_cli.upgrade.metadata import ProjectMetadata
|
|
757
|
+
|
|
758
|
+
metadata = ProjectMetadata(
|
|
759
|
+
version=__version__,
|
|
760
|
+
initialized_at=datetime.now(),
|
|
761
|
+
python_version=plat.python_version(),
|
|
762
|
+
platform=system.platform,
|
|
763
|
+
platform_version=plat.platform(),
|
|
764
|
+
)
|
|
765
|
+
metadata.save(project_path / ".kittify")
|
|
766
|
+
except Exception as e:
|
|
767
|
+
# Don't fail init if metadata creation fails
|
|
768
|
+
_console.print(f"[dim]Note: Could not create project metadata: {e}[/dim]")
|
|
769
|
+
|
|
770
|
+
# Save VCS preference to config.yaml
|
|
771
|
+
if selected_vcs:
|
|
772
|
+
try:
|
|
773
|
+
_save_vcs_config(project_path / ".kittify", selected_vcs)
|
|
774
|
+
except Exception as e:
|
|
775
|
+
# Don't fail init if VCS config creation fails
|
|
776
|
+
_console.print(f"[dim]Note: Could not save VCS config: {e}[/dim]")
|
|
777
|
+
|
|
778
|
+
# Save agent configuration to config.yaml
|
|
779
|
+
try:
|
|
780
|
+
save_agent_config(project_path, agent_config)
|
|
781
|
+
_console.print(f"[dim]Saved agent configuration ({selected_strategy} strategy)[/dim]")
|
|
782
|
+
except Exception as e:
|
|
783
|
+
# Don't fail init if agent config creation fails
|
|
784
|
+
_console.print(f"[dim]Note: Could not save agent config: {e}[/dim]")
|
|
785
|
+
|
|
786
|
+
# Clean up templates directory - it's only needed during init
|
|
787
|
+
# User projects should only have the generated agent commands, not the source templates
|
|
788
|
+
templates_dir = project_path / ".kittify" / "templates"
|
|
789
|
+
if templates_dir.exists():
|
|
790
|
+
try:
|
|
791
|
+
shutil.rmtree(templates_dir)
|
|
792
|
+
except PermissionError:
|
|
793
|
+
_console.print("[dim]Note: Could not remove .kittify/templates/ (permission denied)[/dim]")
|
|
794
|
+
except Exception as e:
|
|
795
|
+
# Log but don't fail init if cleanup fails
|
|
796
|
+
_console.print(f"[dim]Note: Could not remove .kittify/templates/: {e}[/dim]")
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def register_init_command(
|
|
800
|
+
app: typer.Typer,
|
|
801
|
+
*,
|
|
802
|
+
console: Console,
|
|
803
|
+
show_banner: Callable[[], None],
|
|
804
|
+
activate_mission: Callable[[Path, str, str, Console], str],
|
|
805
|
+
ensure_executable_scripts: Callable[[Path, StepTracker | None], None],
|
|
806
|
+
) -> None:
|
|
807
|
+
"""Register the init command with injected dependencies."""
|
|
808
|
+
global _console, _show_banner, _activate_mission, _ensure_executable_scripts
|
|
809
|
+
|
|
810
|
+
# Store the dependencies
|
|
811
|
+
_console = console
|
|
812
|
+
_show_banner = show_banner
|
|
813
|
+
_activate_mission = activate_mission
|
|
814
|
+
_ensure_executable_scripts = ensure_executable_scripts
|
|
815
|
+
|
|
816
|
+
# Set the docstring
|
|
817
|
+
init.__doc__ = INIT_COMMAND_DOC
|
|
818
|
+
|
|
819
|
+
# Ensure app is in multi-command mode by checking if there are existing commands
|
|
820
|
+
# If not, add a hidden dummy command to force subcommand mode
|
|
821
|
+
if not hasattr(app, 'registered_commands') or not getattr(app, 'registered_commands'):
|
|
822
|
+
@app.command("__force_multi_command_mode__", hidden=True)
|
|
823
|
+
def _dummy():
|
|
824
|
+
pass
|
|
825
|
+
|
|
826
|
+
# Register the command with explicit name to ensure it's always a subcommand
|
|
827
|
+
app.command("init")(init)
|