diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb29563b8f..9633a1a1b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,6 +94,8 @@ jobs: - spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip - spec-kit-template-cursor-sh-${{ steps.get_tag.outputs.new_version }}.zip - spec-kit-template-cursor-ps-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-kilocode-sh-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-kilocode-ps-${{ steps.get_tag.outputs.new_version }}.zip EOF echo "Generated release notes:" @@ -104,7 +106,7 @@ jobs: # Remove 'v' prefix from version for release title VERSION_NO_V=${{ steps.get_tag.outputs.new_version }} VERSION_NO_V=${VERSION_NO_V#v} - + gh release create ${{ steps.get_tag.outputs.new_version }} \ spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip \ spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip \ @@ -114,6 +116,8 @@ jobs: spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip \ spec-kit-template-cursor-sh-${{ steps.get_tag.outputs.new_version }}.zip \ spec-kit-template-cursor-ps-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-kilocode-sh-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-kilocode-ps-${{ steps.get_tag.outputs.new_version }}.zip \ --title "Spec Kit Templates - $VERSION_NO_V" \ --notes-file release_notes.md env: diff --git a/README.md b/README.md index a919545c4d..6292dd0e9c 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ The `specify` command supports the following options: | Argument/Option | Type | Description | |------------------------|----------|------------------------------------------------------------------------------| | `` | Argument | Name for your new project directory (optional if using `--here`) | -| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, or `cursor` | +| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor`, or `kilocode` | | `--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 | @@ -108,6 +108,9 @@ specify init my-project --ai claude # Initialize with Cursor support specify init my-project --ai cursor +# Initialize with Kilo Code support +specify init my-project --ai kilocode + # Initialize with PowerShell scripts (Windows/cross-platform) specify init my-project --ai copilot --script ps @@ -170,7 +173,7 @@ Our research and experimentation focus on: ## 🔧 Prerequisites - **Linux/macOS** (or WSL2 on Windows) -- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Cursor](https://cursor.sh/) +- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Cursor](https://cursor.sh/), or [Kilo Code](https://kilocode.ai/) - [uv](https://docs.astral.sh/uv/) for package management - [Python 3.11+](https://www.python.org/downloads/) - [Git](https://git-scm.com/downloads) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 88c166998b..cda3c15ef0 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -57,7 +57,8 @@ "copilot": "GitHub Copilot", "claude": "Claude Code", "gemini": "Gemini CLI", - "cursor": "Cursor" + "cursor": "Cursor", + "kilocode": "Kilo Code" } # Add script type choices SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} @@ -375,7 +376,7 @@ def is_git_repo(path: Path = None) -> bool: """Check if the specified path is inside a git repository.""" if path is None: path = Path.cwd() - + if not path.is_dir(): return False @@ -392,6 +393,21 @@ def is_git_repo(path: Path = None) -> bool: return False +def get_repo_root() -> str: + """Get the absolute path to the repository root.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + # Fallback to current directory if not in a git repo + return str(Path.cwd()) + + def init_git_repo(project_path: Path, quiet: bool = False) -> bool: """Initialize a git repository in the specified path. quiet: if True suppress console output (tracker handles status) @@ -719,10 +735,87 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = console.print(f" - {f}") +def generate_kilocode_modes_from_templates(project_path: Path, script_type: str) -> None: + """Generate .kilocodemodes file by converting existing command templates to Kilo Code format.""" + modes_file = project_path / ".kilocodemodes" + + # Get absolute paths for scripts + repo_root = get_repo_root() + script_base = f"{repo_root}/.specify/scripts" + bash_script = f"{script_base}/bash" if script_type == "sh" else f"{script_base}/powershell" + + # Command configurations with role definitions + command_configs = { + "specify": { + "name": "Specify", + "description": "Create feature specifications", + "role_definition": "You are Kilo Code, a feature specification expert specializing in converting natural language feature descriptions into structured, comprehensive specifications following spec-driven development principles.", + "when_to_use": "Use this mode when you need to convert a natural language feature description into a structured specification document with user stories, acceptance criteria, and technical requirements.", + }, + "plan": { + "name": "Plan", + "description": "Implementation planning workflow", + "role_definition": "You are Kilo Code, an implementation planning expert who creates detailed technical plans from feature specifications, including architecture decisions, technology choices, and implementation phases.", + "when_to_use": "Use this mode when you need to generate implementation plans and design artifacts from feature specifications, including data models, API contracts, and research documentation.", + }, + "tasks": { + "name": "Tasks", + "description": "Generate actionable task lists", + "role_definition": "You are Kilo Code, a task generation expert who breaks down implementation plans into dependency-ordered, actionable tasks with clear acceptance criteria.", + "when_to_use": "Use this mode when you need to create dependency-ordered task lists from design artifacts, breaking complex implementations into manageable, verifiable steps.", + } + } + + custom_modes = [] + + for command_slug, config in command_configs.items(): + # Read existing command template + template_file = project_path / ".specify" / "templates" / "commands" / f"{command_slug}.md" + + if template_file.exists(): + with open(template_file, 'r') as f: + template_content = f.read() + + # Extract custom instructions from template (skip YAML frontmatter) + lines = template_content.split('\n') + custom_instructions = "" + in_frontmatter = False + + for line in lines: + if line.strip() == '---': + in_frontmatter = not in_frontmatter + continue + if not in_frontmatter: + custom_instructions += line + '\n' + + # Replace template variables with actual paths + custom_instructions = custom_instructions.replace('{SCRIPT}', f'{bash_script}/{command_slug}.sh') + custom_instructions = custom_instructions.replace('{ARGS}', '$ARGUMENTS') + custom_instructions = custom_instructions.replace('repo root', f'{repo_root}') + + mode_config = { + "slug": command_slug, + "name": config["name"], + "description": config["description"], + "roleDefinition": config["role_definition"], + "whenToUse": config["when_to_use"], + "groups": ["read", "edit", "command"], + "customInstructions": custom_instructions.strip() + } + + custom_modes.append(mode_config) + + modes_config = {"customModes": custom_modes} + + # Write JSON file (uses built-in json module) + with open(modes_file, 'w') as f: + json.dump(modes_config, f, indent=2) + + @app.command() def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"), - ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, or cursor"), + ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor, or kilocode"), 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"), @@ -888,6 +981,22 @@ def init( # Ensure scripts are executable (POSIX) ensure_executable_scripts(project_path, tracker=tracker) + # Generate AI-specific integration files + if selected_ai == "kilocode": + generate_kilocode_modes_from_templates(project_path, selected_script) + elif selected_ai == "cursor": + # Existing cursor logic unchanged + cursor_commands_dir = project_path / ".cursor" / "commands" + cursor_commands_dir.mkdir(parents=True, exist_ok=True) + + template_commands_dir = project_path / ".specify" / "templates" / "commands" + for cmd_file in ["specify.md", "plan.md", "tasks.md"]: + src = template_commands_dir / cmd_file + dst = cursor_commands_dir / cmd_file + if src.exists(): + shutil.copy2(src, dst) + # Other AI assistants unchanged... + # Git step if not no_git: tracker.start("git") @@ -950,6 +1059,8 @@ def init( steps_lines.append(" - See GEMINI.md for all available commands") elif selected_ai == "copilot": steps_lines.append(f"{step_num}. Open in Visual Studio Code and use [bold cyan]/specify[/], [bold cyan]/plan[/], [bold cyan]/tasks[/] commands with GitHub Copilot") + elif selected_ai == "kilocode": + steps_lines.append(f"{step_num}. Open in your IDE and use [bold cyan]/specify[/], [bold cyan]/plan[/], [bold cyan]/tasks[/] commands with Kilo Code") # Removed script variant step (scripts are transparent to users) step_num += 1