Compare commits

...

5 Commits

Author SHA1 Message Date
Manfred Riem
6fca5d83b2 fix: pin click>=8.1 to prevent Python 3.14/Homebrew env isolation crash (#1648)
* fix: pin click>=8.1 to prevent Python 3.14/Homebrew env isolation failures

Fixes #1631. When uv installs specify-cli on macOS with Homebrew Python
3.14, the virtual environment can fail to fully isolate from the system
site-packages, causing Homebrew's click to be loaded instead of the one
uv installed. If that system click is older than 8.1, it lacks the `ctx`
keyword argument in `ParamType.get_metavar()`, which typer 0.24.0 requires,
resulting in:

  TypeError: ParamType.get_metavar() got an unexpected keyword argument 'ctx'

Adding an explicit `click>=8.1` dependency gives uv a hard constraint so
the correct version is always resolved and installed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update CHANGELOG.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 10:22:16 -06:00
Manfred Riem
465acd9024 fix: include 'src/**' path in release workflow triggers (#1646)
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
2026-02-20 09:46:29 -06:00
dependabot[bot]
04fc3fd1ba chore(deps): bump github/codeql-action from 3 to 4 (#1635)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 16:31:23 -06:00
Manfred Riem
24d76b5d92 Add pytest and Python linting (ruff) to CI (#1637)
* feat: add GitHub Actions workflow for testing and linting Python code

* fix: resolve ruff lint errors in specify_cli

- Remove extraneous f-string prefixes (F541)
- Split multi-statement lines (E701, E702)
- Remove unused variable assignments (F841)
- Remove ruff format check from CI workflow (format-only PR to follow)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: strip ANSI codes in ai-skills help text test

The Rich/Typer CLI injects ANSI escape codes into option names in
--help output, causing plain string matching to fail.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-19 16:21:58 -06:00
Manfred Riem
0f7d04b12b feat: add pull request template for better contribution guidelines (#1634) 2026-02-19 15:49:34 -06:00
8 changed files with 116 additions and 30 deletions

22
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,22 @@
## Description
<!-- What does this PR do? Why is it needed? -->
## Testing
<!-- How did you test your changes? -->
- [ ] Tested locally with `uv run specify --help`
- [ ] Ran existing tests with `uv sync && uv run pytest`
- [ ] Tested with a sample project (if applicable)
## AI Disclosure
<!-- Per our Contributing guidelines, AI assistance must be disclosed. -->
<!-- See: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md#ai-contributions-in-spec-kit -->
- [ ] I **did not** use AI assistance for this contribution
- [ ] I **did** use AI assistance (describe below)
<!-- If you used AI, briefly describe how (e.g., "Code generated by Copilot", "Consulted ChatGPT for approach"): -->

View File

@@ -22,11 +22,11 @@ jobs:
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"

View File

@@ -6,6 +6,7 @@ on:
paths:
- 'memory/**'
- 'scripts/**'
- 'src/**'
- 'templates/**'
- '.github/workflows/**'
workflow_dispatch:

50
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Test & Lint Python
permissions:
contents: read
on:
push:
branches: ["main"]
pull_request:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Run ruff check
run: uvx ruff check src/
pytest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --extra test
- name: Run tests
run: uv run pytest

View File

@@ -8,6 +8,12 @@ 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.1.2] - 2026-02-20
### Fixed
- **Python 3.14 / Homebrew compatibility**: Added explicit `click>=8.1` dependency so the resolver always selects a Click version compatible with Python 3.14 and avoids errors such as `TypeError: ParamType.get_metavar() got an unexpected keyword argument 'ctx'` when an older Click would otherwise be used. Note that broader uv/Homebrew environment isolation or `sys.path` bleed issues (see [#1631](https://github.com/github/spec-kit/issues/1631)) may still require environment-level workarounds.
## [0.1.1] - 2026-02-13
### Added

View File

@@ -1,10 +1,11 @@
[project]
name = "specify-cli"
version = "0.1.1"
version = "0.1.2"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [
"typer",
"click>=8.1",
"rich",
"httpx[socks]",
"platformdirs",

View File

@@ -670,7 +670,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
except ValueError as je:
raise RuntimeError(f"Failed to parse release JSON: {je}\nRaw (truncated 400): {response.text[:400]}")
except Exception as e:
console.print(f"[red]Error fetching release information[/red]")
console.print("[red]Error fetching release information[/red]")
console.print(Panel(str(e), title="Fetch Error", border_style="red"))
raise typer.Exit(1)
@@ -700,7 +700,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
zip_path = download_dir / filename
if verbose:
console.print(f"[cyan]Downloading template...[/cyan]")
console.print("[cyan]Downloading template...[/cyan]")
try:
with client.stream(
@@ -739,7 +739,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk)
except Exception as e:
console.print(f"[red]Error downloading template[/red]")
console.print("[red]Error downloading template[/red]")
detail = str(e)
if zip_path.exists():
zip_path.unlink()
@@ -823,7 +823,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
tracker.add("flatten", "Flatten nested directory")
tracker.complete("flatten")
elif verbose:
console.print(f"[cyan]Found nested directory structure[/cyan]")
console.print("[cyan]Found nested directory structure[/cyan]")
for item in source_dir.iterdir():
dest_path = project_path / item.name
@@ -848,7 +848,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
console.print(f"[yellow]Overwriting file:[/yellow] {item.name}")
shutil.copy2(item, dest_path)
if verbose and not tracker:
console.print(f"[cyan]Template files merged into current directory[/cyan]")
console.print("[cyan]Template files merged into current directory[/cyan]")
else:
zip_ref.extractall(project_path)
@@ -874,7 +874,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
tracker.add("flatten", "Flatten nested directory")
tracker.complete("flatten")
elif verbose:
console.print(f"[cyan]Flattened nested directory structure[/cyan]")
console.print("[cyan]Flattened nested directory structure[/cyan]")
except Exception as e:
if tracker:
@@ -924,13 +924,17 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
continue
except Exception:
continue
st = script.stat(); mode = st.st_mode
st = script.stat()
mode = st.st_mode
if mode & 0o111:
continue
new_mode = mode
if mode & 0o400: new_mode |= 0o100
if mode & 0o040: new_mode |= 0o010
if mode & 0o004: new_mode |= 0o001
if mode & 0o400:
new_mode |= 0o100
if mode & 0o040:
new_mode |= 0o010
if mode & 0o004:
new_mode |= 0o001
if not (new_mode & 0o100):
new_mode |= 0o100
os.chmod(script, new_mode)
@@ -976,7 +980,7 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker |
tracker.add("constitution", "Constitution setup")
tracker.complete("constitution", "copied from template")
else:
console.print(f"[cyan]Initialized constitution from template[/cyan]")
console.print("[cyan]Initialized constitution from template[/cyan]")
except Exception as e:
if tracker:
tracker.add("constitution", "Constitution setup")
@@ -1510,9 +1514,9 @@ def init(
enhancement_lines = [
"Optional commands that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]",
"",
f"○ [cyan]/speckit.clarify[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]/speckit.plan[/] if used)",
f"○ [cyan]/speckit.analyze[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]/speckit.tasks[/], before [cyan]/speckit.implement[/])",
f"○ [cyan]/speckit.checklist[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]/speckit.plan[/])"
"○ [cyan]/speckit.clarify[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]/speckit.plan[/] if used)",
"○ [cyan]/speckit.analyze[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]/speckit.tasks[/], before [cyan]/speckit.implement[/])",
"○ [cyan]/speckit.checklist[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]/speckit.plan[/])"
]
enhancements_panel = Panel("\n".join(enhancement_lines), title="Enhancement Commands", border_style="cyan", padding=(1,2))
console.print()
@@ -1545,10 +1549,10 @@ def check():
# Check VS Code variants (not in agent config)
tracker.add("code", "Visual Studio Code")
code_ok = check_tool("code", tracker=tracker)
check_tool("code", tracker=tracker)
tracker.add("code-insiders", "Visual Studio Code Insiders")
code_insiders_ok = check_tool("code-insiders", tracker=tracker)
check_tool("code-insiders", tracker=tracker)
console.print(tracker.render())
@@ -1814,14 +1818,14 @@ def extension_add(
if zip_path.exists():
zip_path.unlink()
console.print(f"\n[green]✓[/green] Extension installed successfully!")
console.print("\n[green]✓[/green] Extension installed successfully!")
console.print(f"\n[bold]{manifest.name}[/bold] (v{manifest.version})")
console.print(f" {manifest.description}")
console.print(f"\n[bold cyan]Provided commands:[/bold cyan]")
console.print("\n[bold cyan]Provided commands:[/bold cyan]")
for cmd in manifest.commands:
console.print(f"{cmd['name']} - {cmd.get('description', '')}")
console.print(f"\n[yellow]⚠[/yellow] Configuration may be required")
console.print("\n[yellow]⚠[/yellow] Configuration may be required")
console.print(f" Check: .specify/extensions/{manifest.id}/")
except ValidationError as e:
@@ -1871,11 +1875,11 @@ def extension_remove(
# Confirm removal
if not force:
console.print(f"\n[yellow]⚠ This will remove:[/yellow]")
console.print("\n[yellow]⚠ This will remove:[/yellow]")
console.print(f"{cmd_count} commands from AI agent")
console.print(f" • Extension directory: .specify/extensions/{extension}/")
if not keep_config:
console.print(f" • Config files (will be backed up)")
console.print(" • Config files (will be backed up)")
console.print()
confirm = typer.confirm("Continue?")
@@ -1894,7 +1898,7 @@ def extension_remove(
console.print(f"\nConfig files backed up to .specify/extensions/.backup/{extension}/")
console.print(f"\nTo reinstall: specify extension add {extension}")
else:
console.print(f"[red]Error:[/red] Failed to remove extension")
console.print("[red]Error:[/red] Failed to remove extension")
raise typer.Exit(1)
@@ -2169,8 +2173,8 @@ def extension_update(
# TODO: Implement download and reinstall from URL
# For now, just show message
console.print(
f"[yellow]Note:[/yellow] Automatic update not yet implemented. "
f"Please update manually:"
"[yellow]Note:[/yellow] Automatic update not yet implemented. "
"Please update manually:"
)
console.print(f" specify extension remove {ext_id} --keep-config")
console.print(f" specify extension add {ext_id}")
@@ -2270,7 +2274,7 @@ def extension_disable(
hook_executor.save_project_config(config)
console.print(f"[green]✓[/green] Extension '{extension}' disabled")
console.print(f"\nCommands will no longer be available. Hooks will not execute.")
console.print("\nCommands will no longer be available. Hooks will not execute.")
console.print(f"To re-enable: specify extension enable {extension}")

View File

@@ -10,6 +10,7 @@ Tests cover:
- CLI validation: --ai-skills requires --ai
"""
import re
import pytest
import tempfile
import shutil
@@ -626,5 +627,6 @@ class TestCliValidation:
runner = CliRunner()
result = runner.invoke(app, ["init", "--help"])
assert "--ai-skills" in result.output
assert "agent skills" in result.output.lower()
plain = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
assert "--ai-skills" in plain
assert "agent skills" in plain.lower()