Merge main into feature/add-qwen-support

Resolved conflicts in release workflow and CLI:
- Integrated external script approach for release package creation
- Added Qwen Code support to release script and CLI tool checking
- Maintained all existing functionality for other AI assistants
This commit is contained in:
Ahmet Çetinkaya
2025-09-12 14:09:43 +03:00
8 changed files with 442 additions and 337 deletions

View File

@@ -60,6 +60,9 @@ AI_CHOICES = {
"qwen": "Qwen Code"
}
# Claude CLI local installation path after migrate-installer
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
# ASCII Art Banner
BANNER = """
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
@@ -336,8 +339,28 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False
return None
def check_tool_for_tracker(tool: str, install_hint: 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, f"not found - {install_hint}")
return False
def check_tool(tool: str, install_hint: str) -> bool:
"""Check if a tool is installed."""
# 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
# and creates an alias at ~/.claude/local/claude instead
# 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():
return True
if shutil.which(tool):
return True
else:
@@ -636,6 +659,67 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
return project_path
def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None:
"""Ensure POSIX .sh scripts in the project .specify/scripts directory have execute bits (no-op on Windows)."""
if os.name == "nt":
return # Windows: skip silently
scripts_dir = project_path / ".specify" / "scripts"
if not scripts_dir.is_dir():
return
failures: list[str] = []
updated = 0
for script in scripts_dir.glob("*.sh"):
try:
# Skip symlinks
if script.is_symlink():
continue
# Must be a regular file
if not script.is_file():
continue
# Quick shebang check
try:
with script.open("rb") as f:
first_two = f.read(2)
if first_two != b"#!":
continue
except Exception:
continue
st = script.stat()
mode = st.st_mode
# If already any execute bit set, skip
if mode & 0o111:
continue
# Only add execute bits that correspond to existing read bits
new_mode = mode
if mode & 0o400: # owner read
new_mode |= 0o100
if mode & 0o040: # group read
new_mode |= 0o010
if mode & 0o004: # other read
new_mode |= 0o001
# Fallback: ensure at least owner execute
if not (new_mode & 0o100):
new_mode |= 0o100
os.chmod(script, new_mode)
updated += 1
except Exception as e:
failures.append(f"{script.name}: {e}")
if tracker:
detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "")
tracker.add("chmod", "Set script permissions")
if failures:
tracker.error("chmod", detail)
else:
tracker.complete("chmod", detail)
else:
if updated:
console.print(f"[cyan]Updated execute permissions on {updated} script(s)[/cyan]")
if failures:
console.print("[yellow]Some scripts could not be updated:[/yellow]")
for f in failures:
console.print(f" - {f}")
@app.command()
def init(
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
@@ -767,6 +851,7 @@ def init(
("extract", "Extract template"),
("zip-list", "Archive contents"),
("extracted-summary", "Extraction summary"),
("chmod", "Ensure scripts executable"),
("cleanup", "Cleanup"),
("git", "Initialize git repository"),
("final", "Finalize")
@@ -784,6 +869,9 @@ def init(
download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client)
# Ensure scripts are executable (POSIX)
ensure_executable_scripts(project_path, tracker=tracker)
# Git step
if not no_git:
tracker.start("git")
@@ -853,38 +941,38 @@ def init(
# Removed farewell line per user request
# Add skip_tls option to check
@app.command()
def check(skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)")):
def check():
"""Check that all required tools are installed."""
show_banner()
console.print("[bold]Checking Specify requirements...[/bold]\n")
console.print("[bold]Checking for installed tools...[/bold]\n")
# Check if we have internet connectivity by trying to reach GitHub API
console.print("[cyan]Checking internet connectivity...[/cyan]")
verify = not skip_tls
local_ssl_context = ssl_context if verify else False
local_client = httpx.Client(verify=local_ssl_context)
try:
response = local_client.get("https://api.github.com", timeout=5, follow_redirects=True)
console.print("[green]✓[/green] Internet connection available")
except httpx.RequestError:
console.print("[red]✗[/red] No internet connection - required for downloading templates")
console.print("[yellow]Please check your internet connection[/yellow]")
console.print("\n[cyan]Optional tools:[/cyan]")
git_ok = check_tool("git", "https://git-scm.com/downloads")
# Create tracker for checking tools
tracker = StepTracker("Check Available Tools")
console.print("\n[cyan]Optional AI tools:[/cyan]")
claude_ok = check_tool("claude", "Install from: https://docs.anthropic.com/en/docs/claude-code/setup")
gemini_ok = check_tool("gemini", "Install from: https://github.com/google-gemini/gemini-cli")
qwen_ok = check_tool("qwen", "Install from: https://github.com/QwenLM/qwen-code")
# Add all tools we want to check
tracker.add("git", "Git version control")
tracker.add("claude", "Claude Code CLI")
tracker.add("gemini", "Gemini CLI")
tracker.add("qwen", "Qwen Code CLI")
console.print("\n[green]✓ Specify CLI is ready to use![/green]")
# Check each tool
git_ok = check_tool_for_tracker("git", "https://git-scm.com/downloads", tracker)
claude_ok = check_tool_for_tracker("claude", "https://docs.anthropic.com/en/docs/claude-code/setup", tracker)
gemini_ok = check_tool_for_tracker("gemini", "https://github.com/google-gemini/gemini-cli", tracker)
qwen_ok = check_tool_for_tracker("qwen", "https://github.com/QwenLM/qwen-code", tracker)
# Render the final tree
console.print(tracker.render())
# Summary
console.print("\n[bold green]Specify CLI is ready to use![/bold green]")
# Recommendations
if not git_ok:
console.print("[yellow]Consider installing git for repository management[/yellow]")
console.print("[dim]Tip: Install git for repository management[/dim]")
if not (claude_ok or gemini_ok or qwen_ok):
console.print("[yellow]Consider installing an AI assistant for the best experience[/yellow]")
console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]")
def main():