diff --git a/.github/workflows/scripts/create-github-release.sh b/.github/workflows/scripts/create-github-release.sh index 26b08880..3c12dff7 100644 --- a/.github/workflows/scripts/create-github-release.sh +++ b/.github/workflows/scripts/create-github-release.sh @@ -38,6 +38,8 @@ gh release create "$VERSION" \ .genreleases/spec-kit-template-auggie-ps-"$VERSION".zip \ .genreleases/spec-kit-template-roo-sh-"$VERSION".zip \ .genreleases/spec-kit-template-roo-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-codebuddy-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-codebuddy-ps-"$VERSION".zip \ .genreleases/spec-kit-template-q-sh-"$VERSION".zip \ .genreleases/spec-kit-template-q-ps-"$VERSION".zip \ --title "Spec Kit Templates - $VERSION_NO_V" \ diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 19e49d39..155b8322 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -91,6 +91,7 @@ generate_commands() { case $ext in toml) + body=$(printf '%s\n' "$body" | sed 's/\\/\\\\/g') { echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/speckit.$name.$ext" ;; md) echo "$body" > "$output_dir/speckit.$name.$ext" ;; @@ -176,6 +177,10 @@ build_variant() { roo) mkdir -p "$base_dir/.roo/commands" generate_commands roo md "\$ARGUMENTS" "$base_dir/.roo/commands" "$script" ;; + codebuddy) + mkdir -p "$base_dir/.codebuddy/commands" + generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;; + q) mkdir -p "$base_dir/.amazonq/prompts" generate_commands q md "\$ARGUMENTS" "$base_dir/.amazonq/prompts" "$script" ;; @@ -185,10 +190,9 @@ build_variant() { } # Determine agent list -ALL_AGENTS=(claude gemini copilot cursor qwen opencode windsurf codex kilocode auggie roo q) +ALL_AGENTS=(claude gemini copilot cursor qwen opencode windsurf codex kilocode auggie roo codebuddy q) ALL_SCRIPTS=(sh ps) - norm_list() { # convert comma+space separated -> space separated unique while preserving order of first occurrence tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i)}}}END{printf("\n")}' diff --git a/AGENTS.md b/AGENTS.md index 6cae6703..cd88dd34 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,9 +38,9 @@ Specify supports multiple AI agents by generating agent-specific command files a | **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI | | **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI | | **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows | +| **CodeBuddy** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy | | **Amazon Q Developer CLI** | `.amazonq/prompts/` | Markdown | `q` | Amazon Q Developer CLI | - ### Step-by-Step Integration Guide Follow these steps to add a new agent (using Windsurf as an example): @@ -58,7 +58,8 @@ AI_CHOICES = { "qwen": "Qwen Code", "opencode": "opencode", "windsurf": "Windsurf", - "q": "Amazon Q Developer CLI" # Add new agent here + "codebuddy": "CodeBuddy" + "q": "Amazon Q Developer CLI" } ``` @@ -72,11 +73,12 @@ agent_folder_map = { "qwen": ".qwen/", "opencode": ".opencode/", "codex": ".codex/", - "windsurf": ".windsurf/", + "windsurf": ".windsurf/", "kilocode": ".kilocode/", "auggie": ".auggie/", "copilot": ".github/", - "q": ".amazonq/" # Add new agent folder here + "q": ".amazonq/", + "codebuddy": ".codebuddy/" } ``` @@ -201,6 +203,7 @@ Require a command-line tool to be installed: - **Cursor**: `cursor-agent` CLI - **Qwen Code**: `qwen` CLI - **opencode**: `opencode` CLI +- **CodeBuddy**: `codebuddy` CLI ### IDE-Based Agents Work within integrated development environments: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4706b826..219a4cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ All notable changes to the Specify CLI and templates are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.19] - 2025-10-10 + +### Added + +- Support for CodeBuddy (thank you to [@lispking](https://github.com/lispking) for the contribution). +- You can now see Git-sourced errors in the Specify CLI. + +### Changed + +- Fixed the path to the constitution in `plan.md` (thank you to [@lyzno1](https://github.com/lyzno1) for spotting). +- Fixed backslash escapes in generated TOML files for Gemini (thank you to [@hsin19](https://github.com/hsin19) for the contribution). +- Implementation command now ensures that the correct ignore files are added (thank you to [@sigent-amazon](https://github.com/sigent-amazon) for the contribution). + ## [0.0.18] - 2025-10-06 ### Added diff --git a/README.md b/README.md index 4f9da1c5..9f51efa5 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c | [Windsurf](https://windsurf.com/) | ✅ | | | [Kilo Code](https://github.com/Kilo-Org/kilocode) | ✅ | | | [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | | +| [CodeBuddy](https://www.codebuddy.ai/) | ✅ | | | [Roo Code](https://roocode.com/) | ✅ | | | [Codex CLI](https://github.com/openai/codex) | ✅ | | | [Amazon Q Developer CLI](https://aws.amazon.com/developer/learning/q-developer-cli/) | ⚠️ | Amazon Q Developer CLI [does not support](https://github.com/aws/amazon-q-developer-cli/issues/3064) custom arguments for slash commands. | @@ -157,7 +158,7 @@ The `specify` command supports the following options: | Argument/Option | Type | Description | |------------------------|----------|------------------------------------------------------------------------------| | `` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) | -| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, or `q` | +| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, or `q` | | `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) | | `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code | | `--no-git` | Flag | Skip git repository initialization | @@ -177,7 +178,7 @@ specify init my-project specify init my-project --ai claude # Initialize with Cursor support -specify init my-project --ai cursor +specify init my-project --ai cursor-agent # Initialize with Windsurf support specify init my-project --ai windsurf diff --git a/pyproject.toml b/pyproject.toml index 86f9abba..f1a79030 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "specify-cli" -version = "0.0.18" +version = "0.0.19" description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)." requires-python = ">=3.11" dependencies = [ diff --git a/scripts/bash/update-agent-context.sh b/scripts/bash/update-agent-context.sh index 036d6b21..1ad88293 100644 --- a/scripts/bash/update-agent-context.sh +++ b/scripts/bash/update-agent-context.sh @@ -69,6 +69,7 @@ WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" +CODEBUDDY_FILE="$REPO_ROOT/.codebuddy/rules/specify-rules.md" Q_FILE="$REPO_ROOT/AGENTS.md" # Template file @@ -581,6 +582,9 @@ update_specific_agent() { roo) update_agent_file "$ROO_FILE" "Roo Code" ;; + codebuddy) + update_agent_file "$CODEBUDDY_FILE" "CodeBuddy" + ;; q) update_agent_file "$Q_FILE" "Amazon Q Developer CLI" ;; @@ -646,6 +650,11 @@ update_all_existing_agents() { found_agent=true fi + if [[ -f "$CODEBUDDY_FILE" ]]; then + update_agent_file "$CODEBUDDY_FILE" "CodeBuddy" + found_agent=true + fi + if [[ -f "$Q_FILE" ]]; then update_agent_file "$Q_FILE" "Amazon Q Developer CLI" found_agent=true @@ -674,7 +683,8 @@ print_summary() { fi echo - log_info "Usage: $0 [claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|q]" + + log_info "Usage: $0 [claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|codebuddy|q]" } #============================================================================== diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 index 1743eee6..baf586e4 100644 --- a/scripts/powershell/update-agent-context.ps1 +++ b/scripts/powershell/update-agent-context.ps1 @@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1 #> param( [Parameter(Position=0)] - [ValidateSet('claude','gemini','copilot','cursor','qwen','opencode','codex','windsurf','kilocode','auggie','roo','q')] + [ValidateSet('claude','gemini','copilot','cursor','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','q')] [string]$AgentType ) @@ -54,6 +54,7 @@ $WINDSURF_FILE = Join-Path $REPO_ROOT '.windsurf/rules/specify-rules.md' $KILOCODE_FILE = Join-Path $REPO_ROOT '.kilocode/rules/specify-rules.md' $AUGGIE_FILE = Join-Path $REPO_ROOT '.augment/rules/specify-rules.md' $ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md' +$CODEBUDDY_FILE = Join-Path $REPO_ROOT '.codebuddy/rules/specify-rules.md' $Q_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md' @@ -377,8 +378,9 @@ function Update-SpecificAgent { 'kilocode' { Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code' } 'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' } 'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' } + 'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy' } 'q' { Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI' } - default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo|q'; return $false } + default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|q'; return $false } } } @@ -395,6 +397,7 @@ function Update-AllExistingAgents { if (Test-Path $KILOCODE_FILE) { if (-not (Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }; $found = $true } if (Test-Path $AUGGIE_FILE) { if (-not (Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }; $found = $true } if (Test-Path $ROO_FILE) { if (-not (Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }; $found = $true } + if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy')) { $ok = $false }; $found = $true } if (Test-Path $Q_FILE) { if (-not (Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI')) { $ok = $false }; $found = $true } if (-not $found) { Write-Info 'No existing agent files found, creating default Claude file...' @@ -410,7 +413,7 @@ function Print-Summary { if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" } if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" } Write-Host '' - Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo|q]' + Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|q]' } function Main { diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index a0652539..ed51f066 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -64,19 +64,86 @@ def _github_auth_headers(cli_token: str | None = None) -> dict: token = _github_token(cli_token) return {"Authorization": f"Bearer {token}"} if token else {} -AI_CHOICES = { - "copilot": "GitHub Copilot", - "claude": "Claude Code", - "gemini": "Gemini CLI", - "cursor": "Cursor", - "qwen": "Qwen Code", - "opencode": "opencode", - "codex": "Codex CLI", - "windsurf": "Windsurf", - "kilocode": "Kilo Code", - "auggie": "Auggie CLI", - "roo": "Roo Code", - "q": "Amazon Q Developer CLI", +# Agent configuration with name, folder, install URL, and CLI tool requirement +AGENT_CONFIG = { + "copilot": { + "name": "GitHub Copilot", + "folder": ".github/", + "install_url": None, # IDE-based, no CLI check needed + "requires_cli": False, + }, + "claude": { + "name": "Claude Code", + "folder": ".claude/", + "install_url": "https://docs.anthropic.com/en/docs/claude-code/setup", + "requires_cli": True, + }, + "gemini": { + "name": "Gemini CLI", + "folder": ".gemini/", + "install_url": "https://github.com/google-gemini/gemini-cli", + "requires_cli": True, + }, + "cursor-agent": { + "name": "Cursor", + "folder": ".cursor/", + "install_url": None, # IDE-based + "requires_cli": False, + }, + "qwen": { + "name": "Qwen Code", + "folder": ".qwen/", + "install_url": "https://github.com/QwenLM/qwen-code", + "requires_cli": True, + }, + "opencode": { + "name": "opencode", + "folder": ".opencode/", + "install_url": "https://opencode.ai", + "requires_cli": True, + }, + "codex": { + "name": "Codex CLI", + "folder": ".codex/", + "install_url": "https://github.com/openai/codex", + "requires_cli": True, + }, + "windsurf": { + "name": "Windsurf", + "folder": ".windsurf/", + "install_url": None, # IDE-based + "requires_cli": False, + }, + "kilocode": { + "name": "Kilo Code", + "folder": ".kilocode/", + "install_url": None, # IDE-based + "requires_cli": False, + }, + "auggie": { + "name": "Auggie CLI", + "folder": ".augment/", + "install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli", + "requires_cli": True, + }, + "codebuddy": { + "name": "CodeBuddy", + "folder": ".codebuddy/", + "install_url": "https://www.codebuddy.ai", + "requires_cli": True, + }, + "roo": { + "name": "Roo Code", + "folder": ".roo/", + "install_url": None, # IDE-based + "requires_cli": False, + }, + "q": { + "name": "Amazon Q Developer CLI", + "folder": ".amazonq/", + "install_url": "https://aws.amazon.com/developer/learning/q-developer-cli/", + "requires_cli": True, + }, } SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} @@ -131,7 +198,7 @@ class StepTracker: s["detail"] = detail self._maybe_refresh() return - # If not present, add it + self.steps.append({"key": key, "label": key, "status": status, "detail": detail}) self._maybe_refresh() @@ -148,7 +215,6 @@ class StepTracker: label = step["label"] detail_text = step["detail"].strip() if step["detail"] else "" - # Circles (unchanged styling) status = step["status"] if status == "done": symbol = "[green]●[/green]" @@ -272,7 +338,6 @@ def select_with_arrows(options: dict, prompt_text: str = "Select an option", def console.print("\n[red]Selection failed.[/red]") raise typer.Exit(1) - # Suppress explicit selection print; tracker / later logic will report consolidated status return selected_key console = Console() @@ -296,7 +361,6 @@ app = typer.Typer( def show_banner(): """Display the ASCII art banner.""" - # Create gradient effect with different colors banner_lines = BANNER.strip().split('\n') colors = ["bright_blue", "blue", "cyan", "bright_cyan", "white", "bright_white"] @@ -312,8 +376,6 @@ def show_banner(): @app.callback() def callback(ctx: typer.Context): """Show banner when no subcommand is provided.""" - # Show banner only when no subcommand and no help flag - # (help is handled by BannerGroup) if ctx.invoked_subcommand is None and "--help" not in sys.argv and "-h" not in sys.argv: show_banner() console.print(Align.center("[dim]Run 'specify --help' for usage information[/dim]")) @@ -337,18 +399,16 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False raise return None -def check_tool_for_tracker(tool: str, tracker: StepTracker) -> bool: - """Check if a tool is installed and update tracker.""" - if shutil.which(tool): - tracker.complete(tool, "available") - return True - else: - tracker.error(tool, "not found") - return False - -def check_tool(tool: str, install_hint: str) -> bool: - """Check if a tool is installed.""" +def check_tool(tool: str, tracker: StepTracker = None) -> bool: + """Check if a tool is installed. Optionally update tracker. + Args: + tool: Name of the tool to check + tracker: Optional StepTracker to update with results + + Returns: + True if tool is found, False otherwise + """ # Special handling for Claude CLI after `claude migrate-installer` # See: https://github.com/github/spec-kit/issues/123 # The migrate-installer command REMOVES the original executable from PATH @@ -356,12 +416,19 @@ def check_tool(tool: str, install_hint: str) -> bool: # This path should be prioritized over other claude executables in PATH if tool == "claude": if CLAUDE_LOCAL_PATH.exists() and CLAUDE_LOCAL_PATH.is_file(): + if tracker: + tracker.complete(tool, "available") return True - if shutil.which(tool): - return True - else: - return False + found = shutil.which(tool) is not None + + if tracker: + if found: + tracker.complete(tool, "available") + else: + tracker.error(tool, "not found") + + return found def is_git_repo(path: Path = None) -> bool: """Check if the specified path is inside a git repository.""" @@ -383,26 +450,38 @@ def is_git_repo(path: Path = None) -> bool: except (subprocess.CalledProcessError, FileNotFoundError): return False -def init_git_repo(project_path: Path, quiet: bool = False) -> bool: +def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Optional[str]]: """Initialize a git repository in the specified path. - quiet: if True suppress console output (tracker handles status) + + Args: + project_path: Path to initialize git repository in + quiet: if True suppress console output (tracker handles status) + + Returns: + Tuple of (success: bool, error_message: Optional[str]) """ try: original_cwd = Path.cwd() os.chdir(project_path) if not quiet: console.print("[cyan]Initializing git repository...[/cyan]") - subprocess.run(["git", "init"], check=True, capture_output=True) - subprocess.run(["git", "add", "."], check=True, capture_output=True) - subprocess.run(["git", "commit", "-m", "Initial commit from Specify template"], check=True, capture_output=True) + subprocess.run(["git", "init"], check=True, capture_output=True, text=True) + subprocess.run(["git", "add", "."], check=True, capture_output=True, text=True) + subprocess.run(["git", "commit", "-m", "Initial commit from Specify template"], check=True, capture_output=True, text=True) if not quiet: console.print("[green]✓[/green] Git repository initialized") - return True + return True, None except subprocess.CalledProcessError as e: + error_msg = f"Command: {' '.join(e.cmd)}\nExit code: {e.returncode}" + if e.stderr: + error_msg += f"\nError: {e.stderr.strip()}" + elif e.stdout: + error_msg += f"\nOutput: {e.stdout.strip()}" + if not quiet: console.print(f"[red]Error initializing git repository:[/red] {e}") - return False + return False, error_msg finally: os.chdir(original_cwd) @@ -438,7 +517,6 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri console.print(Panel(str(e), title="Fetch Error", border_style="red")) raise typer.Exit(1) - # Find the template asset for the specified AI assistant assets = release_data.get("assets", []) pattern = f"spec-kit-template-{ai_assistant}-{script_type}" matching_assets = [ @@ -523,7 +601,6 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ """ current_dir = Path.cwd() - # Step: fetch + download combined if tracker: tracker.start("fetch", "contacting GitHub API") try: @@ -556,12 +633,10 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ console.print("Extracting template...") try: - # Create project directory only if not using current directory if not is_current_dir: project_path.mkdir(parents=True) with zipfile.ZipFile(zip_path, 'r') as zip_ref: - # List all files in the ZIP for debugging zip_contents = zip_ref.namelist() if tracker: tracker.start("zip-list") @@ -569,13 +644,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ elif verbose: console.print(f"[cyan]ZIP contains {len(zip_contents)} items[/cyan]") - # For current directory, extract to a temp location first if is_current_dir: with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) zip_ref.extractall(temp_path) - # Check what was extracted extracted_items = list(temp_path.iterdir()) if tracker: tracker.start("extracted-summary") @@ -583,7 +656,6 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ elif verbose: console.print(f"[cyan]Extracted {len(extracted_items)} items to temp location[/cyan]") - # Handle GitHub-style ZIP with a single root directory source_dir = temp_path if len(extracted_items) == 1 and extracted_items[0].is_dir(): source_dir = extracted_items[0] @@ -593,14 +665,12 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ elif verbose: console.print(f"[cyan]Found nested directory structure[/cyan]") - # Copy contents to current directory for item in source_dir.iterdir(): dest_path = project_path / item.name if item.is_dir(): if dest_path.exists(): if verbose and not tracker: console.print(f"[yellow]Merging directory:[/yellow] {item.name}") - # Recursively copy directory contents for sub_item in item.rglob('*'): if sub_item.is_file(): rel_path = sub_item.relative_to(item) @@ -616,10 +686,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ if verbose and not tracker: console.print(f"[cyan]Template files merged into current directory[/cyan]") else: - # Extract directly to project directory (original behavior) zip_ref.extractall(project_path) - # Check what was extracted extracted_items = list(project_path.iterdir()) if tracker: tracker.start("extracted-summary") @@ -629,16 +697,14 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ for item in extracted_items: console.print(f" - {item.name} ({'dir' if item.is_dir() else 'file'})") - # Handle GitHub-style ZIP with a single root directory if len(extracted_items) == 1 and extracted_items[0].is_dir(): - # Move contents up one level nested_dir = extracted_items[0] temp_move_dir = project_path.parent / f"{project_path.name}_temp" - # Move the nested directory contents to temp location + shutil.move(str(nested_dir), str(temp_move_dir)) - # Remove the now-empty project directory + project_path.rmdir() - # Rename temp directory to project directory + shutil.move(str(temp_move_dir), str(project_path)) if tracker: tracker.add("flatten", "Flatten nested directory") @@ -654,7 +720,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ console.print(f"[red]Error extracting template:[/red] {e}") if debug: console.print(Panel(str(e), title="Extraction Error", border_style="red")) - # Clean up project directory if created and not current directory + if not is_current_dir and project_path.exists(): shutil.rmtree(project_path) raise typer.Exit(1) @@ -664,7 +730,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ finally: if tracker: tracker.add("cleanup", "Remove temporary archive") - # Clean up downloaded ZIP file + if zip_path.exists(): zip_path.unlink() if tracker: @@ -722,7 +788,7 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = @app.command() def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"), - ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor, qwen, opencode, codex, windsurf, kilocode, auggie or q"), + ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, or q"), script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"), ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"), no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), @@ -737,7 +803,7 @@ def init( This command will: 1. Check that required tools are installed (git is optional) - 2. Let you choose your AI assistant (Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, or Amazon Q Developer CLI) + 2. Let you choose your AI assistant 3. Download the appropriate template from GitHub 4. Extract the template to a new project directory or current directory 5. Initialize a fresh git repository (if not --no-git and no existing repo) @@ -752,13 +818,13 @@ def init( specify init . # Initialize in current directory (interactive AI selection) specify init --here --ai claude # Alternative syntax for current directory specify init --here --ai codex + specify init --here --ai codebuddy specify init --here specify init --here --force # Skip confirmation when current directory not empty """ show_banner() - # Handle '.' as shorthand for current directory (equivalent to --here) if project_name == ".": here = True project_name = None # Clear project_name to use existing validation logic @@ -809,14 +875,11 @@ def init( f"{'Working Path':<15} [dim]{current_dir}[/dim]", ] - # Add target path only if different from working dir if not here: setup_lines.append(f"{'Target Path':<15} [dim]{project_path}[/dim]") console.print(Panel("\n".join(setup_lines), border_style="cyan", padding=(1, 2))) - # Check git only if we might need it (not --no-git) - # Only set to True if the user wants it and the tool is available should_init_git = False if not no_git: should_init_git = check_tool("git", "https://git-scm.com/downloads") @@ -824,76 +887,45 @@ def init( console.print("[yellow]Git not found - will skip repository initialization[/yellow]") if ai_assistant: - if ai_assistant not in AI_CHOICES: - console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AI_CHOICES.keys())}") + if ai_assistant not in AGENT_CONFIG: + console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AGENT_CONFIG.keys())}") raise typer.Exit(1) selected_ai = ai_assistant else: - # Use arrow-key selection interface + # Create options dict for selection (agent_key: display_name) + ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()} selected_ai = select_with_arrows( - AI_CHOICES, + ai_choices, "Choose your AI assistant:", "copilot" ) - # Check agent tools unless ignored if not ignore_agent_tools: - agent_tool_missing = False - install_url = "" - if selected_ai == "claude": - if not check_tool("claude", "https://docs.anthropic.com/en/docs/claude-code/setup"): - install_url = "https://docs.anthropic.com/en/docs/claude-code/setup" - agent_tool_missing = True - elif selected_ai == "gemini": - if not check_tool("gemini", "https://github.com/google-gemini/gemini-cli"): - install_url = "https://github.com/google-gemini/gemini-cli" - agent_tool_missing = True - elif selected_ai == "qwen": - if not check_tool("qwen", "https://github.com/QwenLM/qwen-code"): - install_url = "https://github.com/QwenLM/qwen-code" - agent_tool_missing = True - elif selected_ai == "opencode": - if not check_tool("opencode", "https://opencode.ai"): - install_url = "https://opencode.ai" - agent_tool_missing = True - elif selected_ai == "codex": - if not check_tool("codex", "https://github.com/openai/codex"): - install_url = "https://github.com/openai/codex" - agent_tool_missing = True - elif selected_ai == "auggie": - if not check_tool("auggie", "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli"): - install_url = "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli" - agent_tool_missing = True - elif selected_ai == "q": - if not check_tool("q", "https://github.com/aws/amazon-q-developer-cli"): - install_url = "https://aws.amazon.com/developer/learning/q-developer-cli/" - agent_tool_missing = True - # GitHub Copilot and Cursor checks are not needed as they're typically available in supported IDEs + agent_config = AGENT_CONFIG.get(selected_ai) + if agent_config and agent_config["requires_cli"]: + install_url = agent_config["install_url"] + if not check_tool(selected_ai, install_url): + error_panel = Panel( + f"[cyan]{selected_ai}[/cyan] not found\n" + f"Install from: [cyan]{install_url}[/cyan]\n" + f"{agent_config['name']} is required to continue with this project type.\n\n" + "Tip: Use [cyan]--ignore-agent-tools[/cyan] to skip this check", + title="[red]Agent Detection Error[/red]", + border_style="red", + padding=(1, 2) + ) + console.print() + console.print(error_panel) + raise typer.Exit(1) - if agent_tool_missing: - error_panel = Panel( - f"[cyan]{selected_ai}[/cyan] not found\n" - f"Install with: [cyan]{install_url}[/cyan]\n" - f"{AI_CHOICES[selected_ai]} is required to continue with this project type.\n\n" - "Tip: Use [cyan]--ignore-agent-tools[/cyan] to skip this check", - title="[red]Agent Detection Error[/red]", - border_style="red", - padding=(1, 2) - ) - console.print() - console.print(error_panel) - raise typer.Exit(1) - - # Determine script type (explicit, interactive, or OS default) if script_type: if script_type not in SCRIPT_TYPE_CHOICES: console.print(f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}") raise typer.Exit(1) selected_script = script_type else: - # Auto-detect default default_script = "ps" if os.name == "nt" else "sh" - # Provide interactive selection similar to AI if stdin is a TTY + if sys.stdin.isatty(): selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script) else: @@ -902,12 +934,10 @@ def init( console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}") console.print(f"[cyan]Selected script type:[/cyan] {selected_script}") - # Download and set up project - # New tree-based progress (no emojis); include earlier substeps tracker = StepTracker("Initialize Specify Project") - # Flag to allow suppressing legacy headings + sys._specify_tracker_active = True - # Pre steps recorded as completed before live rendering + tracker.add("precheck", "Check required tools") tracker.complete("precheck", "ok") tracker.add("ai-select", "Select AI assistant") @@ -927,30 +957,31 @@ def init( ]: tracker.add(key, label) - # Use transient so live tree is replaced by the final static render (avoids duplicate output) + # Track git error message outside Live context so it persists + git_error_message = None + with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live: tracker.attach_refresh(lambda: live.update(tracker.render())) try: - # Create a httpx client with verify based on skip_tls verify = not skip_tls local_ssl_context = ssl_context if verify else False local_client = httpx.Client(verify=local_ssl_context) download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token) - # Ensure scripts are executable (POSIX) ensure_executable_scripts(project_path, tracker=tracker) - # Git step if not no_git: tracker.start("git") if is_git_repo(project_path): tracker.complete("git", "existing repo detected") elif should_init_git: - if init_git_repo(project_path, quiet=True): + success, error_msg = init_git_repo(project_path, quiet=True) + if success: tracker.complete("git", "initialized") else: tracker.error("git", "init failed") + git_error_message = error_msg else: tracker.skip("git", "git not available") else: @@ -973,31 +1004,32 @@ def init( shutil.rmtree(project_path) raise typer.Exit(1) finally: - # Force final render pass - # Final static tree (ensures finished state visible after Live context ends) console.print(tracker.render()) console.print("\n[bold green]Project ready.[/bold green]") + + # Show git error details if initialization failed + if git_error_message: + console.print() + git_error_panel = Panel( + f"[yellow]Warning:[/yellow] Git repository initialization failed\n\n" + f"{git_error_message}\n\n" + f"[dim]You can initialize git manually later with:[/dim]\n" + f"[cyan]cd {project_path if not here else '.'}[/cyan]\n" + f"[cyan]git init[/cyan]\n" + f"[cyan]git add .[/cyan]\n" + f"[cyan]git commit -m \"Initial commit\"[/cyan]", + title="[red]Git Initialization Failed[/red]", + border_style="red", + padding=(1, 2) + ) + console.print(git_error_panel) # Agent folder security notice - agent_folder_map = { - "claude": ".claude/", - "gemini": ".gemini/", - "cursor": ".cursor/", - "qwen": ".qwen/", - "opencode": ".opencode/", - "codex": ".codex/", - "windsurf": ".windsurf/", - "kilocode": ".kilocode/", - "auggie": ".augment/", - "copilot": ".github/", - "roo": ".roo/", - "q": ".amazonq/" - } - - if selected_ai in agent_folder_map: - agent_folder = agent_folder_map[selected_ai] + agent_config = AGENT_CONFIG.get(selected_ai) + if agent_config: + agent_folder = agent_config["folder"] security_notice = Panel( f"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.\n" f"Consider adding [cyan]{agent_folder}[/cyan] (or parts of it) to [cyan].gitignore[/cyan] to prevent accidental credential leakage.", @@ -1008,7 +1040,6 @@ def init( console.print() console.print(security_notice) - # Boxed "Next steps" section steps_lines = [] if not here: steps_lines.append(f"1. Go to the project folder: [cyan]cd {project_name}[/cyan]") @@ -1061,32 +1092,21 @@ def check(): tracker = StepTracker("Check Available Tools") tracker.add("git", "Git version control") - tracker.add("claude", "Claude Code CLI") - tracker.add("gemini", "Gemini CLI") - tracker.add("qwen", "Qwen Code CLI") + git_ok = check_tool("git", tracker=tracker) + + agent_results = {} + for agent_key, agent_config in AGENT_CONFIG.items(): + agent_name = agent_config["name"] + + tracker.add(agent_key, agent_name) + agent_results[agent_key] = check_tool(agent_key, tracker=tracker) + + # Check VS Code variants (not in agent config) tracker.add("code", "Visual Studio Code") + code_ok = check_tool("code", tracker=tracker) + tracker.add("code-insiders", "Visual Studio Code Insiders") - tracker.add("cursor-agent", "Cursor IDE agent") - tracker.add("windsurf", "Windsurf IDE") - tracker.add("kilocode", "Kilo Code IDE") - tracker.add("opencode", "opencode") - tracker.add("codex", "Codex CLI") - tracker.add("auggie", "Auggie CLI") - tracker.add("q", "Amazon Q Developer CLI") - - git_ok = check_tool_for_tracker("git", tracker) - claude_ok = check_tool_for_tracker("claude", tracker) - gemini_ok = check_tool_for_tracker("gemini", tracker) - qwen_ok = check_tool_for_tracker("qwen", tracker) - code_ok = check_tool_for_tracker("code", tracker) - code_insiders_ok = check_tool_for_tracker("code-insiders", tracker) - cursor_ok = check_tool_for_tracker("cursor-agent", tracker) - windsurf_ok = check_tool_for_tracker("windsurf", tracker) - kilocode_ok = check_tool_for_tracker("kilocode", tracker) - opencode_ok = check_tool_for_tracker("opencode", tracker) - codex_ok = check_tool_for_tracker("codex", tracker) - auggie_ok = check_tool_for_tracker("auggie", tracker) - q_ok = check_tool_for_tracker("q", tracker) + code_insiders_ok = check_tool("code-insiders", tracker=tracker) console.print(tracker.render()) @@ -1094,7 +1114,8 @@ def check(): if not git_ok: console.print("[dim]Tip: Install git for repository management[/dim]") - if not (claude_ok or gemini_ok or cursor_ok or qwen_ok or windsurf_ok or kilocode_ok or opencode_ok or codex_ok or auggie_ok or q_ok): + + if not any(agent_results.values()): console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]") def main(): diff --git a/templates/commands/implement.md b/templates/commands/implement.md index da8021e4..60555975 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -54,27 +54,60 @@ You **MUST** consider the user input before proceeding (if not empty). - **IF EXISTS**: Read research.md for technical decisions and constraints - **IF EXISTS**: Read quickstart.md for integration scenarios -4. Parse tasks.md structure and extract: +4. **Project Setup Verification**: + - **REQUIRED**: Create/verify ignore files based on actual project setup: + + **Detection & Creation Logic**: + - Check if the following command succeeds to determine if the repository is a git repo (create/verify .gitignore if so): + + ```sh + git rev-parse --git-dir 2>/dev/null + ``` + - Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore + - Check if .eslintrc* or eslint.config.* exists → create/verify .eslintignore + - Check if .prettierrc* exists → create/verify .prettierignore + - Check if .npmrc or package.json exists → create/verify .npmignore (if publishing) + - Check if terraform files (*.tf) exist → create/verify .terraformignore + - Check if .helmignore needed (helm charts present) → create/verify .helmignore + + **If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only + **If ignore file missing**: Create with full pattern set for detected technology + + **Common Patterns by Technology** (from plan.md tech stack): + - **Node.js/JavaScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*` + - **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/` + - **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/` + - **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/` + - **Go**: `*.exe`, `*.test`, `vendor/`, `*.out` + - **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/` + + **Tool-Specific Patterns**: + - **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/` + - **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js` + - **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` + - **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl` + +5. Parse tasks.md structure and extract: - **Task phases**: Setup, Tests, Core, Integration, Polish - **Task dependencies**: Sequential vs parallel execution rules - **Task details**: ID, description, file paths, parallel markers [P] - **Execution flow**: Order and dependency requirements -5. Execute implementation following the task plan: +6. Execute implementation following the task plan: - **Phase-by-phase execution**: Complete each phase before moving to the next - **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together - **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks - **File-based coordination**: Tasks affecting the same files must run sequentially - **Validation checkpoints**: Verify each phase completion before proceeding -6. Implementation execution rules: +7. Implementation execution rules: - **Setup first**: Initialize project structure, dependencies, configuration - **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios - **Core development**: Implement models, services, CLI commands, endpoints - **Integration work**: Database connections, middleware, logging, external services - **Polish and validation**: Unit tests, performance optimization, documentation -7. Progress tracking and error handling: +8. Progress tracking and error handling: - Report progress after each completed task - Halt execution if any non-parallel task fails - For parallel tasks [P], continue with successful tasks, report failed ones @@ -82,7 +115,7 @@ You **MUST** consider the user input before proceeding (if not empty). - Suggest next steps if implementation cannot proceed - **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file. -8. Completion validation: +9. Completion validation: - Verify all required tasks are completed - Check that implemented features match the original specification - Validate that tests pass and coverage meets requirements diff --git a/templates/commands/plan.md b/templates/commands/plan.md index f65ad38f..42bb6023 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -20,7 +20,7 @@ You **MUST** consider the user input before proceeding (if not empty). 1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). -2. **Load context**: Read FEATURE_SPEC and `.specify/memory/constitution.md`. Load IMPL_PLAN template (already copied). +2. **Load context**: Read FEATURE_SPEC and `/memory/constitution.md`. Load IMPL_PLAN template (already copied). 3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to: - Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")