diff --git a/README.md b/README.md index 76149512f6..a37c18fd86 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,12 @@ To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed inst uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git ``` +**Note**: To use custom templates from your own fork, install from your fork URL: + +```bash +uv tool install specify-cli --from git+https://github.com/gouzhuang/spec-kit.git +``` + #### Option 2: One-time Usage Run directly without installing: @@ -188,6 +194,7 @@ The `specify` command supports the following options: | `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) | | `--debug` | Flag | Enable detailed debug output for troubleshooting | | `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) | +| `--template-repo` | Option | Custom template repository in `owner/repo` format (e.g., `gouzhuang/spec-kit`) | ### Examples @@ -238,6 +245,9 @@ specify init my-project --ai claude --debug # Use GitHub token for API requests (helpful for corporate environments) specify init my-project --ai claude --github-token ghp_your_token_here +# Use custom template repository (for forks or private deployments) +specify init my-project --ai claude --template-repo gouzhuang/spec-kit + # Check system requirements specify check ``` diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 1dedb31949..931f32db91 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -634,9 +634,32 @@ def deep_merge(base: dict, update: dict) -> dict: return merged -def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Tuple[Path, dict]: - repo_owner = "github" - repo_name = "spec-kit" +def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False, github_token: str = None, template_repo: str = None) -> Tuple[Path, dict]: + """Download template from GitHub releases. + + Args: + ai_assistant: AI assistant type (e.g., 'claude', 'gemini') + download_dir: Directory to download the template to + script_type: Script type ('sh' or 'ps') + verbose: Print verbose output + show_progress: Show download progress bar + client: HTTP client to use + debug: Enable debug output + github_token: GitHub API token + template_repo: Custom repository in 'owner/repo' format (default: 'github/spec-kit') + + Returns: + Tuple of (zip_path, metadata_dict) + """ + if template_repo: + parts = template_repo.split("/") + if len(parts) != 2 or not parts[0] or not parts[1]: + raise RuntimeError(f"Invalid template repository format: '{template_repo}'. Expected format: 'owner/repo' (e.g., 'gouzhuang/spec-kit')") + repo_owner = parts[0] + repo_name = parts[1] + else: + repo_owner = "github" + repo_name = "spec-kit" if client is None: client = httpx.Client(verify=ssl_context) @@ -748,9 +771,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri } return zip_path, metadata -def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Path: +def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False, github_token: str = None, template_repo: str = None) -> Path: """Download the latest release and extract it to create a new project. Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup) + + Args: + template_repo: Custom repository in 'owner/repo' format (default: 'github/spec-kit') """ current_dir = Path.cwd() @@ -765,7 +791,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ show_progress=(tracker is None), client=client, debug=debug, - github_token=github_token + github_token=github_token, + template_repo=template_repo ) if tracker: tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)") @@ -954,6 +981,7 @@ def init( skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"), debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"), 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)"), + template_repo: str = typer.Option(None, "--template-repo", help="Custom template repository in 'owner/repo' format (e.g., 'gouzhuang/spec-kit')"), ): """ Initialize a new Specify project from the latest template. @@ -978,6 +1006,7 @@ def init( specify init --here --ai codebuddy specify init --here specify init --here --force # Skip confirmation when current directory not empty + specify init --template-repo gouzhuang/spec-kit my-project # Use custom fork """ show_banner() @@ -1124,7 +1153,7 @@ def init( 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) + download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token, template_repo=template_repo) ensure_executable_scripts(project_path, tracker=tracker) diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 3c952d683e..2dfdf17e93 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -39,33 +39,14 @@ Given that feature description, do this: - "Create a dashboard for analytics" → "analytics-dashboard" - "Fix payment processing timeout bug" → "fix-payment-timeout" -2. **Check for existing branches before creating new one**: +2. **Create new feature branch**: - a. First, fetch all remote branches to ensure we have the latest information: - - ```bash - git fetch --all --prune - ``` - - b. Find the highest feature number across all sources for the short-name: - - Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-$'` - - Local branches: `git branch | grep -E '^[* ]*[0-9]+-$'` - - Specs directories: Check for directories matching `specs/[0-9]+-` - - c. Determine the next available number: - - Extract all numbers from all three sources - - Find the highest number N - - Use N+1 for the new branch number - - d. Run the script `{SCRIPT}` with the calculated number and short-name: - - Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description - - Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"` - - PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"` + Run the script `{SCRIPT}` with short-name: + - Pass `--short-name "your-short-name"` along with the feature description + - Bash example: `{SCRIPT} --json --short-name "user-auth" "Add user authentication"` + - PowerShell example: `{SCRIPT} -Json -ShortName "user-auth" "Add user authentication"` **IMPORTANT**: - - Check all three sources (remote branches, local branches, specs directories) to find the highest number - - Only match branches/directories with the exact short-name pattern - - If no existing branches/directories found with this short-name, start with number 1 - You must only ever run this script once per feature - The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for - The JSON output will contain BRANCH_NAME and SPEC_FILE paths