Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ specify init my-project --ai shai
# Initialize with IBM Bob support
specify init my-project --ai bob

# Add new agents to existing project (automatic - just run init again!)
# Your constitution, specs, and plans are automatically preserved
specify init . --ai copilot # Adds copilot, preserves your work
specify init . --ai gemini # Adds gemini, preserves your work

# Force reinitialize (overwrites everything including your work)
specify init . --ai copilot --force # Use with caution!

# Initialize with PowerShell scripts (Windows/cross-platform)
specify init my-project --ai copilot --script ps

Expand Down
61 changes: 58 additions & 3 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,9 +748,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, preserve_specify: bool = False) -> 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:
preserve_specify: If True, skip .specify/ directory to preserve existing work
"""
current_dir = Path.cwd()

Expand Down Expand Up @@ -820,6 +823,13 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_

for item in source_dir.iterdir():
dest_path = project_path / item.name

# Skip .specify/ directory if preserve_specify is True
if item.name == ".specify" and preserve_specify:
if verbose and not tracker:
console.print(f"[yellow]Skipping .specify/ (preserving existing)[/yellow]")
continue

if item.is_dir():
if dest_path.exists():
if verbose and not tracker:
Expand Down Expand Up @@ -950,7 +960,7 @@ def init(
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"),
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
force: bool = typer.Option(False, "--force", help="Force merge/overwrite when using --here (skip confirmation)"),
force: bool = typer.Option(False, "--force", help="Force reinitialize project (overwrites .specify/ directory with constitution, specs, and plans)"),
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)"),
Expand Down Expand Up @@ -1091,6 +1101,51 @@ def init(
console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}")
console.print(f"[cyan]Selected script type:[/cyan] {selected_script}")

# Smart detection: Check if .specify/ exists with content
specify_dir = project_path / ".specify"
has_existing_project = False
preserve_specify = False

if specify_dir.exists() and specify_dir.is_dir():
# Check if .specify/ has actual content (not just empty directory)
specify_contents = list(specify_dir.rglob('*'))
if specify_contents:
has_existing_project = True

if force:
# User explicitly wants to reinitialize (overwrite)
console.print()
console.print(Panel(
"[yellow]Warning: --force flag detected[/yellow]\n\n"
"Your existing .specify/ directory will be OVERWRITTEN, including:\n"
" • Constitution and project principles\n"
" • Specifications and requirements\n"
" • Implementation plans\n"
" • Task lists\n\n"
"All your work in .specify/ will be lost!",
title="[red]Reinitializing Project[/red]",
border_style="red",
padding=(1, 2)
))
preserve_specify = False
else:
# Smart default: preserve existing work
console.print()
console.print(Panel(
"[green]Existing project detected[/green]\n\n"
"Your .specify/ directory will be preserved, including:\n"
" • Constitution and project principles\n"
" • Specifications and requirements\n"
" • Implementation plans\n"
" • Task lists\n\n"
"Only new agent-specific directories will be added.\n\n"
"[dim]Use --force to reinitialize and overwrite everything[/dim]",
title="[cyan]Adding Agent to Existing Project[/cyan]",
border_style="cyan",
padding=(1, 2)
))
preserve_specify = True

tracker = StepTracker("Initialize Specify Project")

sys._specify_tracker_active = True
Expand Down Expand Up @@ -1124,7 +1179,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, preserve_specify=preserve_specify)

ensure_executable_scripts(project_path, tracker=tracker)

Expand Down