From de4139d7ce214e97cb0a28e3bb26010af3ede846 Mon Sep 17 00:00:00 2001 From: Andrew Gower Date: Mon, 29 Sep 2025 19:58:26 +0100 Subject: [PATCH 1/4] feat: Add support for Factory CLI (droid) in Specify CLI - Updated `__init__.py` to include Factory CLI in AI_CHOICES and agent folder mapping. - Modified release scripts to package Factory CLI templates. - Enhanced agent context update scripts to manage Factory CLI files. - Updated documentation to reflect the addition of Factory CLI support. --- .../scripts/create-github-release.sh | 4 +- .../scripts/create-release-packages.sh | 27 +-- scripts/bash/update-agent-context.sh | 163 +++++++++--------- scripts/powershell/update-agent-context.ps1 | 55 +++--- src/specify_cli/__init__.py | 151 ++++++++-------- templates/plan-template.md | 24 +-- 6 files changed, 225 insertions(+), 199 deletions(-) diff --git a/.github/workflows/scripts/create-github-release.sh b/.github/workflows/scripts/create-github-release.sh index 0257520f5..c86c34cbb 100644 --- a/.github/workflows/scripts/create-github-release.sh +++ b/.github/workflows/scripts/create-github-release.sh @@ -38,5 +38,7 @@ gh release create "$VERSION" \ .genreleases/spec-kit-template-auggie-ps-"$VERSION".zip \ .genreleases/spec-kit-template-roo-sh-"$VERSION".zip \ .genreleases/spec-kit-template-roo-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-droid-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-droid-ps-"$VERSION".zip \ --title "Spec Kit Templates - $VERSION_NO_V" \ - --notes-file release_notes.md \ No newline at end of file + --notes-file release_notes.md diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 1a12e5582..849f81a6d 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -44,22 +44,22 @@ generate_commands() { [[ -f "$template" ]] || continue local name description script_command body name=$(basename "$template" .md) - + # Normalize line endings file_content=$(tr -d '\r' < "$template") - + # Extract description and script command from YAML frontmatter description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}') script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}') - + if [[ -z $script_command ]]; then echo "Warning: no script command found for $script_variant in $template" >&2 script_command="(Missing script command for $script_variant)" fi - + # Replace {SCRIPT} placeholder with the script command body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g") - + # Remove the scripts: section from frontmatter while preserving YAML structure body=$(printf '%s\n' "$body" | awk ' /^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next } @@ -68,10 +68,10 @@ generate_commands() { in_frontmatter && skip_scripts && /^[[:space:]]/ { next } { print } ') - + # Apply other substitutions body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths) - + case $ext in toml) { echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/$name.$ext" ;; @@ -88,13 +88,13 @@ build_variant() { local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}" echo "Building $agent ($script) package..." mkdir -p "$base_dir" - + # Copy base structure but filter scripts by variant SPEC_DIR="$base_dir/.specify" mkdir -p "$SPEC_DIR" - + [[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; } - + # Only copy the relevant script variant directory if [[ -d scripts ]]; then mkdir -p "$SPEC_DIR/scripts" @@ -111,7 +111,7 @@ build_variant() { ;; esac fi - + [[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; } # Inject variant into plan-template.md within .specify/templates if present local plan_tpl="$base_dir/.specify/templates/plan-template.md" @@ -172,13 +172,16 @@ build_variant() { roo) mkdir -p "$base_dir/.roo/commands" generate_commands roo md "\$ARGUMENTS" "$base_dir/.roo/commands" "$script" ;; + droid) + mkdir -p "$base_dir/.factory/commands" + generate_commands droid md "\$ARGUMENTS" "$base_dir/.factory/commands" "$script" ;; esac ( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . ) echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" } # Determine agent list -ALL_AGENTS=(claude gemini copilot cursor qwen opencode windsurf codex kilocode auggie roo) +ALL_AGENTS=(claude gemini copilot cursor qwen opencode windsurf codex kilocode auggie roo droid) ALL_SCRIPTS=(sh ps) diff --git a/scripts/bash/update-agent-context.sh b/scripts/bash/update-agent-context.sh index d3cc422ed..383783915 100644 --- a/scripts/bash/update-agent-context.sh +++ b/scripts/bash/update-agent-context.sh @@ -2,7 +2,7 @@ # Update agent context files with information from plan.md # -# This script maintains AI agent context files by parsing feature specifications +# This script maintains AI agent context files by parsing feature specifications # and updating agent-specific configuration files with project information. # # MAIN FUNCTIONS: @@ -30,12 +30,12 @@ # # 5. Multi-Agent Support # - Handles agent-specific file paths and naming conventions -# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf +# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Factory CLI # - Can update single agents or all existing agent files # - Creates default Claude file if no agent files exist # # Usage: ./update-agent-context.sh [agent_type] -# Agent types: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf +# Agent types: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|droid # Leave empty to update all existing agent files set -e @@ -58,7 +58,7 @@ eval $(get_feature_paths) NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code AGENT_TYPE="${1:-}" -# Agent-specific file paths +# Agent-specific file paths CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" GEMINI_FILE="$REPO_ROOT/GEMINI.md" COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md" @@ -69,6 +69,7 @@ WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" +DROID_FILE="$REPO_ROOT/AGENTS.md" # Template file TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" @@ -125,7 +126,7 @@ validate_environment() { fi exit 1 fi - + # Check if plan.md exists if [[ ! -f "$NEW_PLAN" ]]; then log_error "No plan.md found at $NEW_PLAN" @@ -135,7 +136,7 @@ validate_environment() { fi exit 1 fi - + # Check if template exists (needed for new files) if [[ ! -f "$TEMPLATE_FILE" ]]; then log_warning "Template file not found at $TEMPLATE_FILE" @@ -150,7 +151,7 @@ validate_environment() { extract_plan_field() { local field_pattern="$1" local plan_file="$2" - + grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ head -1 | \ sed "s|^\*\*${field_pattern}\*\*: ||" | \ @@ -161,39 +162,39 @@ extract_plan_field() { parse_plan_data() { local plan_file="$1" - + if [[ ! -f "$plan_file" ]]; then log_error "Plan file not found: $plan_file" return 1 fi - + if [[ ! -r "$plan_file" ]]; then log_error "Plan file is not readable: $plan_file" return 1 fi - + log_info "Parsing plan data from $plan_file" - + NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file") NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file") NEW_DB=$(extract_plan_field "Storage" "$plan_file") NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file") - + # Log what we found if [[ -n "$NEW_LANG" ]]; then log_info "Found language: $NEW_LANG" else log_warning "No language information found in plan" fi - + if [[ -n "$NEW_FRAMEWORK" ]]; then log_info "Found framework: $NEW_FRAMEWORK" fi - + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then log_info "Found database: $NEW_DB" fi - + if [[ -n "$NEW_PROJECT_TYPE" ]]; then log_info "Found project type: $NEW_PROJECT_TYPE" fi @@ -203,11 +204,11 @@ format_technology_stack() { local lang="$1" local framework="$2" local parts=() - + # Add non-empty parts [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang") [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework") - + # Join with proper formatting if [[ ${#parts[@]} -eq 0 ]]; then echo "" @@ -229,7 +230,7 @@ format_technology_stack() { get_project_structure() { local project_type="$1" - + if [[ "$project_type" == *"web"* ]]; then echo "backend/\\nfrontend/\\ntests/" else @@ -239,7 +240,7 @@ get_project_structure() { get_commands_for_language() { local lang="$1" - + case "$lang" in *"Python"*) echo "cd src && pytest && ruff check ." @@ -266,40 +267,40 @@ create_new_agent_file() { local temp_file="$2" local project_name="$3" local current_date="$4" - + if [[ ! -f "$TEMPLATE_FILE" ]]; then log_error "Template not found at $TEMPLATE_FILE" return 1 fi - + if [[ ! -r "$TEMPLATE_FILE" ]]; then log_error "Template file is not readable: $TEMPLATE_FILE" return 1 fi - + log_info "Creating new agent context file from template..." - + if ! cp "$TEMPLATE_FILE" "$temp_file"; then log_error "Failed to copy template file" return 1 fi - + # Replace template placeholders local project_structure project_structure=$(get_project_structure "$NEW_PROJECT_TYPE") - + local commands commands=$(get_commands_for_language "$NEW_LANG") - + local language_conventions language_conventions=$(get_language_conventions "$NEW_LANG") - + # Perform substitutions with error checking using safer approach # Escape special characters for sed by using a different delimiter or escaping local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g') local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g') local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g') - + # Build technology stack and recent change strings conditionally local tech_stack if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then @@ -332,7 +333,7 @@ create_new_agent_file() { "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|" "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" ) - + for substitution in "${substitutions[@]}"; do if ! sed -i.bak -e "$substitution" "$temp_file"; then log_error "Failed to perform substitution: $substitution" @@ -340,14 +341,14 @@ create_new_agent_file() { return 1 fi done - + # Convert \n sequences to actual newlines newline=$(printf '\n') sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" - + # Clean up backup files rm -f "$temp_file.bak" "$temp_file.bak2" - + return 0 } @@ -357,44 +358,44 @@ create_new_agent_file() { update_existing_agent_file() { local target_file="$1" local current_date="$2" - + log_info "Updating existing agent context file..." - + # Use a single temporary file for atomic update local temp_file temp_file=$(mktemp) || { log_error "Failed to create temporary file" return 1 } - + # Process the file in one pass local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") local new_tech_entries=() local new_change_entry="" - + # Prepare new technology entries if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") fi - + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)") fi - + # Prepare new change entry if [[ -n "$tech_stack" ]]; then new_change_entry="- $CURRENT_BRANCH: Added $tech_stack" elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB" fi - + # Process file line by line local in_tech_section=false local in_changes_section=false local tech_entries_added=false local changes_entries_added=false local existing_changes_count=0 - + while IFS= read -r line || [[ -n "$line" ]]; do # Handle Active Technologies section if [[ "$line" == "## Active Technologies" ]]; then @@ -419,7 +420,7 @@ update_existing_agent_file() { echo "$line" >> "$temp_file" continue fi - + # Handle Recent Changes section if [[ "$line" == "## Recent Changes" ]]; then echo "$line" >> "$temp_file" @@ -442,7 +443,7 @@ update_existing_agent_file() { fi continue fi - + # Update timestamp if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" @@ -450,19 +451,19 @@ update_existing_agent_file() { echo "$line" >> "$temp_file" fi done < "$target_file" - + # Post-loop check: if we're still in the Active Technologies section and haven't added new entries if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" fi - + # Move temp file to target atomically if ! mv "$temp_file" "$target_file"; then log_error "Failed to update target file" rm -f "$temp_file" return 1 fi - + return 0 } #============================================================================== @@ -472,19 +473,19 @@ update_existing_agent_file() { update_agent_file() { local target_file="$1" local agent_name="$2" - + if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then log_error "update_agent_file requires target_file and agent_name parameters" return 1 fi - + log_info "Updating $agent_name context file: $target_file" - + local project_name project_name=$(basename "$REPO_ROOT") local current_date current_date=$(date +%Y-%m-%d) - + # Create directory if it doesn't exist local target_dir target_dir=$(dirname "$target_file") @@ -494,7 +495,7 @@ update_agent_file() { return 1 fi fi - + if [[ ! -f "$target_file" ]]; then # Create new file from template local temp_file @@ -502,7 +503,7 @@ update_agent_file() { log_error "Failed to create temporary file" return 1 } - + if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then if mv "$temp_file" "$target_file"; then log_success "Created new $agent_name context file" @@ -522,12 +523,12 @@ update_agent_file() { log_error "Cannot read existing file: $target_file" return 1 fi - + if [[ ! -w "$target_file" ]]; then log_error "Cannot write to existing file: $target_file" return 1 fi - + if update_existing_agent_file "$target_file" "$current_date"; then log_success "Updated existing $agent_name context file" else @@ -535,7 +536,7 @@ update_agent_file() { return 1 fi fi - + return 0 } @@ -545,7 +546,7 @@ update_agent_file() { update_specific_agent() { local agent_type="$1" - + case "$agent_type" in claude) update_agent_file "$CLAUDE_FILE" "Claude Code" @@ -580,9 +581,12 @@ update_specific_agent() { roo) update_agent_file "$ROO_FILE" "Roo Code" ;; + droid) + update_agent_file "$DROID_FILE" "Factory CLI" + ;; *) log_error "Unknown agent type '$agent_type'" - log_error "Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo" + log_error "Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo|droid" exit 1 ;; esac @@ -590,43 +594,43 @@ update_specific_agent() { update_all_existing_agents() { local found_agent=false - + # Check each possible agent file and update if it exists if [[ -f "$CLAUDE_FILE" ]]; then update_agent_file "$CLAUDE_FILE" "Claude Code" found_agent=true fi - + if [[ -f "$GEMINI_FILE" ]]; then update_agent_file "$GEMINI_FILE" "Gemini CLI" found_agent=true fi - + if [[ -f "$COPILOT_FILE" ]]; then update_agent_file "$COPILOT_FILE" "GitHub Copilot" found_agent=true fi - + if [[ -f "$CURSOR_FILE" ]]; then update_agent_file "$CURSOR_FILE" "Cursor IDE" found_agent=true fi - + if [[ -f "$QWEN_FILE" ]]; then update_agent_file "$QWEN_FILE" "Qwen Code" found_agent=true fi - + if [[ -f "$AGENTS_FILE" ]]; then update_agent_file "$AGENTS_FILE" "Codex/opencode" found_agent=true fi - + if [[ -f "$WINDSURF_FILE" ]]; then update_agent_file "$WINDSURF_FILE" "Windsurf" found_agent=true fi - + if [[ -f "$KILOCODE_FILE" ]]; then update_agent_file "$KILOCODE_FILE" "Kilo Code" found_agent=true @@ -636,12 +640,17 @@ update_all_existing_agents() { update_agent_file "$AUGGIE_FILE" "Auggie CLI" found_agent=true fi - + if [[ -f "$ROO_FILE" ]]; then update_agent_file "$ROO_FILE" "Roo Code" found_agent=true fi - + + if [[ -f "$DROID_FILE" ]]; then + update_agent_file "$DROID_FILE" "Factory CLI" + found_agent=true + fi + # If no agent files exist, create a default Claude file if [[ "$found_agent" == false ]]; then log_info "No existing agent files found, creating default Claude file..." @@ -651,21 +660,21 @@ update_all_existing_agents() { print_summary() { echo log_info "Summary of changes:" - + if [[ -n "$NEW_LANG" ]]; then echo " - Added language: $NEW_LANG" fi - + if [[ -n "$NEW_FRAMEWORK" ]]; then echo " - Added framework: $NEW_FRAMEWORK" fi - + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then echo " - Added database: $NEW_DB" fi - + echo - log_info "Usage: $0 [claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo]" + log_info "Usage: $0 [claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo|droid]" } #============================================================================== @@ -675,18 +684,18 @@ print_summary() { main() { # Validate environment before proceeding validate_environment - + log_info "=== Updating agent context files for feature $CURRENT_BRANCH ===" - + # Parse the plan file to extract project information if ! parse_plan_data "$NEW_PLAN"; then log_error "Failed to parse plan data" exit 1 fi - + # Process based on agent type argument local success=true - + if [[ -z "$AGENT_TYPE" ]]; then # No specific agent provided - update all existing agent files log_info "No agent specified, updating all existing agent files..." @@ -700,10 +709,10 @@ main() { success=false fi fi - + # Print summary print_summary - + if [[ "$success" == true ]]; then log_success "Agent context update completed successfully" exit 0 diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 index 8f4830a95..585a8f66c 100644 --- a/scripts/powershell/update-agent-context.ps1 +++ b/scripts/powershell/update-agent-context.ps1 @@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh: 2. Plan Data Extraction 3. Agent File Management (create from template or update existing) 4. Content Generation (technology stack, recent changes, timestamp) - 5. Multi-Agent Support (claude, gemini, copilot, cursor, qwen, opencode, codex, windsurf) + 5. Multi-Agent Support (claude, gemini, copilot, cursor, qwen, opencode, codex, windsurf, droid) .PARAMETER AgentType Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist). @@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1 #> param( [Parameter(Position=0)] - [ValidateSet('claude','gemini','copilot','cursor','qwen','opencode','codex','windsurf','kilocode','auggie','roo')] + [ValidateSet('claude','gemini','copilot','cursor','qwen','opencode','codex','windsurf','kilocode','auggie','roo','droid')] [string]$AgentType ) @@ -54,6 +54,7 @@ $WINDSURF_FILE = Join-Path $REPO_ROOT '.windsurf/rules/specify-rules.md' $KILOCODE_FILE = Join-Path $REPO_ROOT '.kilocode/rules/specify-rules.md' $AUGGIE_FILE = Join-Path $REPO_ROOT '.augment/rules/specify-rules.md' $ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md' +$DROID_FILE = Join-Path $REPO_ROOT 'AGENTS.md' $TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md' @@ -63,36 +64,36 @@ $script:NEW_FRAMEWORK = '' $script:NEW_DB = '' $script:NEW_PROJECT_TYPE = '' -function Write-Info { +function Write-Info { param( [Parameter(Mandatory=$true)] [string]$Message ) - Write-Host "INFO: $Message" + Write-Host "INFO: $Message" } -function Write-Success { +function Write-Success { param( [Parameter(Mandatory=$true)] [string]$Message ) - Write-Host "$([char]0x2713) $Message" + Write-Host "$([char]0x2713) $Message" } -function Write-WarningMsg { +function Write-WarningMsg { param( [Parameter(Mandatory=$true)] [string]$Message ) - Write-Warning $Message + Write-Warning $Message } -function Write-Err { +function Write-Err { param( [Parameter(Mandatory=$true)] [string]$Message ) - Write-Host "ERROR: $Message" -ForegroundColor Red + Write-Host "ERROR: $Message" -ForegroundColor Red } function Validate-Environment { @@ -125,7 +126,7 @@ function Extract-PlanField { # Lines like **Language/Version**: Python 3.12 $regex = "^\*\*$([Regex]::Escape($FieldPattern))\*\*: (.+)$" Get-Content -LiteralPath $PlanFile | ForEach-Object { - if ($_ -match $regex) { + if ($_ -match $regex) { $val = $Matches[1].Trim() if ($val -notin @('NEEDS CLARIFICATION','N/A')) { return $val } } @@ -165,15 +166,15 @@ function Format-TechnologyStack { return ($parts -join ' + ') } -function Get-ProjectStructure { +function Get-ProjectStructure { param( [Parameter(Mandatory=$false)] [string]$ProjectType ) - if ($ProjectType -match 'web') { return "backend/`nfrontend/`ntests/" } else { return "src/`ntests/" } + if ($ProjectType -match 'web') { return "backend/`nfrontend/`ntests/" } else { return "src/`ntests/" } } -function Get-CommandsForLanguage { +function Get-CommandsForLanguage { param( [Parameter(Mandatory=$false)] [string]$Lang @@ -186,12 +187,12 @@ function Get-CommandsForLanguage { } } -function Get-LanguageConventions { +function Get-LanguageConventions { param( [Parameter(Mandatory=$false)] [string]$Lang ) - if ($Lang) { "${Lang}: Follow standard conventions" } else { 'General: Follow standard conventions' } + if ($Lang) { "${Lang}: Follow standard conventions" } else { 'General: Follow standard conventions' } } function New-AgentFile { @@ -218,7 +219,7 @@ function New-AgentFile { $content = Get-Content -LiteralPath $temp -Raw $content = $content -replace '\[PROJECT NAME\]',$ProjectName $content = $content -replace '\[DATE\]',$Date.ToString('yyyy-MM-dd') - + # Build the technology stack string safely $techStackForTemplate = "" if ($escaped_lang -and $escaped_framework) { @@ -228,7 +229,7 @@ function New-AgentFile { } elseif ($escaped_framework) { $techStackForTemplate = "- $escaped_framework ($escaped_branch)" } - + $content = $content -replace '\[EXTRACTED FROM ALL PLAN.MD FILES\]',$techStackForTemplate # For project structure we manually embed (keep newlines) $escapedStructure = [Regex]::Escape($projectStructure) @@ -236,7 +237,7 @@ function New-AgentFile { # Replace escaped newlines placeholder after all replacements $content = $content -replace '\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]',$commands $content = $content -replace '\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]',$languageConventions - + # Build the recent changes string safely $recentChangesForTemplate = "" if ($escaped_lang -and $escaped_framework) { @@ -246,7 +247,7 @@ function New-AgentFile { } elseif ($escaped_framework) { $recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_framework}" } - + $content = $content -replace '\[LAST 3 FEATURES AND WHAT THEY ADDED\]',$recentChangesForTemplate # Convert literal \n sequences introduced by Escape to real newlines $content = $content -replace '\\n',[Environment]::NewLine @@ -271,14 +272,14 @@ function Update-ExistingAgentFile { $newTechEntries = @() if ($techStack) { $escapedTechStack = [Regex]::Escape($techStack) - if (-not (Select-String -Pattern $escapedTechStack -Path $TargetFile -Quiet)) { - $newTechEntries += "- $techStack ($CURRENT_BRANCH)" + if (-not (Select-String -Pattern $escapedTechStack -Path $TargetFile -Quiet)) { + $newTechEntries += "- $techStack ($CURRENT_BRANCH)" } } if ($NEW_DB -and $NEW_DB -notin @('N/A','NEEDS CLARIFICATION')) { $escapedDB = [Regex]::Escape($NEW_DB) - if (-not (Select-String -Pattern $escapedDB -Path $TargetFile -Quiet)) { - $newTechEntries += "- $NEW_DB ($CURRENT_BRANCH)" + if (-not (Select-String -Pattern $escapedDB -Path $TargetFile -Quiet)) { + $newTechEntries += "- $NEW_DB ($CURRENT_BRANCH)" } } $newChangeEntry = '' @@ -376,7 +377,8 @@ function Update-SpecificAgent { 'kilocode' { Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code' } 'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' } 'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' } - default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo'; return $false } + 'droid' { Update-AgentFile -TargetFile $DROID_FILE -AgentName 'Factory CLI' } + default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo|droid'; return $false } } } @@ -393,6 +395,7 @@ function Update-AllExistingAgents { if (Test-Path $KILOCODE_FILE) { if (-not (Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }; $found = $true } if (Test-Path $AUGGIE_FILE) { if (-not (Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }; $found = $true } if (Test-Path $ROO_FILE) { if (-not (Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }; $found = $true } + if (Test-Path $DROID_FILE) { if (-not (Update-AgentFile -TargetFile $DROID_FILE -AgentName 'Factory CLI')) { $ok = $false }; $found = $true } if (-not $found) { Write-Info 'No existing agent files found, creating default Claude file...' if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false } @@ -407,7 +410,7 @@ function Print-Summary { if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" } if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" } Write-Host '' - Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo]' + Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo|droid]' } function Main { diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index a5be99d74..3925738b9 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -77,6 +77,7 @@ def _github_auth_headers(cli_token: str | None = None) -> dict: "kilocode": "Kilo Code", "auggie": "Auggie CLI", "roo": "Roo Code", + "droid": "Factory CLI", } # Add script type choices SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} @@ -88,10 +89,10 @@ def _github_auth_headers(cli_token: str | None = None) -> dict: BANNER = """ ███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗ ██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝ -███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝ -╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝ -███████║██║ ███████╗╚██████╗██║██║ ██║ -╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝ +███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝ +╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝ +███████║██║ ███████╗╚██████╗██║██║ ██║ +╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝ """ TAGLINE = "GitHub Spec Kit - Spec-Driven Development Toolkit" @@ -186,27 +187,27 @@ def render(self): MINI_BANNER = """ ╔═╗╔═╗╔═╗╔═╗╦╔═╗╦ ╦ ╚═╗╠═╝║╣ ║ ║╠╣ ╚╦╝ -╚═╝╩ ╚═╝╚═╝╩╚ ╩ +╚═╝╩ ╚═╝╚═╝╩╚ ╩ """ def get_key(): """Get a single keypress in a cross-platform way using readchar.""" key = readchar.readkey() - + # Arrow keys if key == readchar.key.UP or key == readchar.key.CTRL_P: return 'up' if key == readchar.key.DOWN or key == readchar.key.CTRL_N: return 'down' - + # Enter/Return if key == readchar.key.ENTER: return 'enter' - + # Escape if key == readchar.key.ESC: return 'escape' - + # Ctrl+C if key == readchar.key.CTRL_C: raise KeyboardInterrupt @@ -218,12 +219,12 @@ def get_key(): def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str: """ Interactive selection using arrow keys with Rich Live display. - + Args: options: Dict with keys as option keys and values as descriptions prompt_text: Text to show above the options default_key: Default option key to start with - + Returns: Selected option key """ @@ -232,7 +233,7 @@ def select_with_arrows(options: dict, prompt_text: str = "Select an option", def selected_index = option_keys.index(default_key) else: selected_index = 0 - + selected_key = None def create_selection_panel(): @@ -240,23 +241,23 @@ def create_selection_panel(): table = Table.grid(padding=(0, 2)) table.add_column(style="cyan", justify="left", width=3) table.add_column(style="white", justify="left") - + for i, key in enumerate(option_keys): if i == selected_index: table.add_row("▶", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]") else: table.add_row(" ", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]") - + table.add_row("", "") table.add_row("", "[dim]Use ↑/↓ to navigate, Enter to select, Esc to cancel[/dim]") - + return Panel( table, title=f"[bold]{prompt_text}[/bold]", border_style="cyan", padding=(1, 2) ) - + console.print() def run_selection_loop(): @@ -275,7 +276,7 @@ def run_selection_loop(): elif key == 'escape': console.print("\n[yellow]Selection cancelled[/yellow]") raise typer.Exit(1) - + live.update(create_selection_panel(), refresh=True) except KeyboardInterrupt: @@ -298,7 +299,7 @@ def run_selection_loop(): class BannerGroup(TyperGroup): """Custom group that shows banner before help.""" - + def format_help(self, ctx, formatter): # Show banner before help show_banner() @@ -319,12 +320,12 @@ def show_banner(): # Create gradient effect with different colors banner_lines = BANNER.strip().split('\n') colors = ["bright_blue", "blue", "cyan", "bright_cyan", "white", "bright_white"] - + styled_banner = Text() for i, line in enumerate(banner_lines): color = colors[i % len(colors)] styled_banner.append(line + "\n", style=color) - + console.print(Align.center(styled_banner)) console.print(Align.center(Text(TAGLINE, style="italic bright_yellow"))) console.print() @@ -372,7 +373,7 @@ def check_tool_for_tracker(tool: str, tracker: StepTracker) -> bool: 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 @@ -381,7 +382,7 @@ def check_tool(tool: str, install_hint: str) -> bool: if tool == "claude": if CLAUDE_LOCAL_PATH.exists() and CLAUDE_LOCAL_PATH.is_file(): return True - + if shutil.which(tool): return True else: @@ -392,7 +393,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 @@ -424,7 +425,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool: if not quiet: console.print("[green]✓[/green] Git repository initialized") return True - + except subprocess.CalledProcessError as e: if not quiet: console.print(f"[red]Error initializing git repository:[/red] {e}") @@ -438,11 +439,11 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri repo_name = "spec-kit" if client is None: client = httpx.Client(verify=ssl_context) - + if verbose: console.print("[cyan]Fetching latest release information...[/cyan]") api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" - + try: response = client.get( api_url, @@ -464,7 +465,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri console.print(f"[red]Error fetching release information[/red]") console.print(Panel(str(e), title="Fetch Error", border_style="red")) raise typer.Exit(1) - + # Find the template asset for the specified AI assistant assets = release_data.get("assets", []) pattern = f"spec-kit-template-{ai_assistant}-{script_type}" @@ -484,7 +485,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri download_url = asset["browser_download_url"] filename = asset["name"] file_size = asset["size"] - + if verbose: console.print(f"[cyan]Found template:[/cyan] {filename}") console.print(f"[cyan]Size:[/cyan] {file_size:,} bytes") @@ -493,7 +494,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]") - + try: with client.stream( "GET", @@ -550,7 +551,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup) """ current_dir = Path.cwd() - + # Step: fetch + download combined if tracker: tracker.start("fetch", "contacting GitHub API") @@ -576,18 +577,18 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ if verbose: console.print(f"[red]Error downloading template:[/red] {e}") raise - + if tracker: tracker.add("extract", "Extract template") tracker.start("extract") elif verbose: console.print("Extracting template...") - + try: # Create project directory only if not using current directory if not is_current_dir: project_path.mkdir(parents=True) - + with zipfile.ZipFile(zip_path, 'r') as zip_ref: # List all files in the ZIP for debugging zip_contents = zip_ref.namelist() @@ -596,13 +597,13 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ tracker.complete("zip-list", f"{len(zip_contents)} entries") elif verbose: console.print(f"[cyan]ZIP contains {len(zip_contents)} items[/cyan]") - + # For current directory, extract to a temp location first if is_current_dir: with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) zip_ref.extractall(temp_path) - + # Check what was extracted extracted_items = list(temp_path.iterdir()) if tracker: @@ -610,7 +611,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ tracker.complete("extracted-summary", f"temp {len(extracted_items)} items") elif verbose: console.print(f"[cyan]Extracted {len(extracted_items)} items to temp location[/cyan]") - + # Handle GitHub-style ZIP with a single root directory source_dir = temp_path if len(extracted_items) == 1 and extracted_items[0].is_dir(): @@ -620,7 +621,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ tracker.complete("flatten") elif verbose: console.print(f"[cyan]Found nested directory structure[/cyan]") - + # Copy contents to current directory for item in source_dir.iterdir(): dest_path = project_path / item.name @@ -646,7 +647,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ else: # Extract directly to project directory (original behavior) zip_ref.extractall(project_path) - + # Check what was extracted extracted_items = list(project_path.iterdir()) if tracker: @@ -656,7 +657,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ console.print(f"[cyan]Extracted {len(extracted_items)} items to {project_path}:[/cyan]") for item in extracted_items: console.print(f" - {item.name} ({'dir' if item.is_dir() else 'file'})") - + # Handle GitHub-style ZIP with a single root directory if len(extracted_items) == 1 and extracted_items[0].is_dir(): # Move contents up one level @@ -673,7 +674,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ tracker.complete("flatten") elif verbose: console.print(f"[cyan]Flattened nested directory structure[/cyan]") - + except Exception as e: if tracker: tracker.error("extract", str(e)) @@ -699,7 +700,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ tracker.complete("cleanup") elif verbose: console.print(f"Cleaned up: {zip_path.name}") - + return project_path @@ -750,7 +751,7 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = @app.command() def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"), - ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor, qwen, opencode, codex, windsurf, kilocode, or auggie"), + ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor, qwen, opencode, codex, windsurf, kilocode, auggie, or droid"), 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"), @@ -762,15 +763,15 @@ def init( ): """ Initialize a new Specify project from the latest template. - + This command will: 1. Check that required tools are installed (git is optional) - 2. Let you choose your AI assistant (Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, or Auggie CLI) + 2. Let you choose your AI assistant (Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, or Factory CLI) 3. Download the appropriate template from GitHub 4. Extract the template to a new project directory or current directory 5. Initialize a fresh git repository (if not --no-git and no existing repo) 6. Optionally set up AI assistant commands - + Examples: specify init my-project specify init my-project --ai claude @@ -782,6 +783,7 @@ def init( specify init my-project --ai codex specify init my-project --ai windsurf specify init my-project --ai auggie + specify init my-project --ai droid specify init --ignore-agent-tools my-project specify init . --ai claude # Initialize in current directory specify init . # Initialize in current directory (interactive AI selection) @@ -792,26 +794,26 @@ def init( """ # Show banner first show_banner() - + # Handle '.' as shorthand for current directory (equivalent to --here) if project_name == ".": here = True project_name = None # Clear project_name to use existing validation logic - + # Validate arguments if here and project_name: console.print("[red]Error:[/red] Cannot specify both project name and --here flag") raise typer.Exit(1) - + if not here and not project_name: console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag") raise typer.Exit(1) - + # Determine project directory if here: project_name = Path.cwd().name project_path = Path.cwd() - + # Check if current directory has any files existing_items = list(project_path.iterdir()) if existing_items: @@ -839,23 +841,23 @@ def init( console.print() console.print(error_panel) raise typer.Exit(1) - + # Create formatted setup info with column alignment current_dir = Path.cwd() - + setup_lines = [ "[cyan]Specify Project Setup[/cyan]", "", f"{'Project':<15} [green]{project_path.name}[/green]", f"{'Working Path':<15} [dim]{current_dir}[/dim]", ] - + # Add target path only if different from working dir if not here: setup_lines.append(f"{'Target Path':<15} [dim]{project_path}[/dim]") - + console.print(Panel("\n".join(setup_lines), border_style="cyan", padding=(1, 2))) - + # Check git only if we might need it (not --no-git) # Only set to True if the user wants it and the tool is available should_init_git = False @@ -873,11 +875,11 @@ def init( else: # Use arrow-key selection interface selected_ai = select_with_arrows( - AI_CHOICES, - "Choose your AI assistant:", + AI_CHOICES, + "Choose your AI assistant:", "copilot" ) - + # Check agent tools unless ignored if not ignore_agent_tools: agent_tool_missing = False @@ -906,6 +908,10 @@ def init( if not check_tool("auggie", "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli"): install_url = "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli" agent_tool_missing = True + elif selected_ai == "droid": + if not check_tool("droid", "https://docs.factory.ai/cli/getting-started/quickstart"): + install_url = "https://docs.factory.ai/cli/getting-started/quickstart" + agent_tool_missing = True # GitHub Copilot and Cursor checks are not needed as they're typically available in supported IDEs if agent_tool_missing: @@ -921,7 +927,7 @@ def init( console.print() console.print(error_panel) raise typer.Exit(1) - + # Determine script type (explicit, interactive, or OS default) if script_type: if script_type not in SCRIPT_TYPE_CHOICES: @@ -936,10 +942,10 @@ def init( selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script) else: selected_script = default_script - + console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}") console.print(f"[cyan]Selected script type:[/cyan] {selected_script}") - + # Download and set up project # New tree-based progress (no emojis); include earlier substeps tracker = StepTracker("Initialize Specify Project") @@ -1017,7 +1023,7 @@ def init( # Final static tree (ensures finished state visible after Live context ends) console.print(tracker.render()) console.print("\n[bold green]Project ready.[/bold green]") - + # Agent folder security notice agent_folder_map = { "claude": ".claude/", @@ -1030,9 +1036,10 @@ def init( "kilocode": ".kilocode/", "auggie": ".augment/", "copilot": ".github/", - "roo": ".roo/" + "roo": ".roo/", + "droid": ".factory/" } - + if selected_ai in agent_folder_map: agent_folder = agent_folder_map[selected_ai] security_notice = Panel( @@ -1044,7 +1051,7 @@ def init( ) console.print() console.print(security_notice) - + # Boxed "Next steps" section steps_lines = [] if not here: @@ -1062,7 +1069,7 @@ def init( cmd = f"setx CODEX_HOME {quoted_path}" else: # Unix-like systems cmd = f"export CODEX_HOME={quoted_path}" - + steps_lines.append(f"{step_num}. Set [cyan]CODEX_HOME[/cyan] environment variable before running Codex: [cyan]{cmd}[/cyan]") step_num += 1 @@ -1094,7 +1101,7 @@ def init( Custom prompts do not yet support arguments in Codex. You may need to manually specify additional project instructions directly in prompt files located in [cyan].codex/prompts/[/cyan]. For more information, see: [cyan]https://github.com/openai/codex/issues/2890[/cyan]""" - + warning_panel = Panel(warning_text, title="Slash Commands in Codex", border_style="yellow", padding=(1,2)) console.print() console.print(warning_panel) @@ -1106,7 +1113,7 @@ def check(): console.print("[bold]Checking for installed tools...[/bold]\n") tracker = StepTracker("Check Available Tools") - + tracker.add("git", "Git version control") tracker.add("claude", "Claude Code CLI") tracker.add("gemini", "Gemini CLI") @@ -1119,9 +1126,10 @@ def check(): tracker.add("opencode", "opencode") tracker.add("codex", "Codex CLI") tracker.add("auggie", "Auggie CLI") - + tracker.add("droid", "Factory CLI") + git_ok = check_tool_for_tracker("git", tracker) - claude_ok = check_tool_for_tracker("claude", tracker) + claude_ok = check_tool_for_tracker("claude", tracker) gemini_ok = check_tool_for_tracker("gemini", tracker) qwen_ok = check_tool_for_tracker("qwen", tracker) code_ok = check_tool_for_tracker("code", tracker) @@ -1132,6 +1140,7 @@ def check(): opencode_ok = check_tool_for_tracker("opencode", tracker) codex_ok = check_tool_for_tracker("codex", tracker) auggie_ok = check_tool_for_tracker("auggie", tracker) + droid_ok = check_tool_for_tracker("droid", tracker) console.print(tracker.render()) @@ -1139,7 +1148,7 @@ def check(): if not git_ok: console.print("[dim]Tip: Install git for repository management[/dim]") - if not (claude_ok or gemini_ok or cursor_ok or qwen_ok or windsurf_ok or kilocode_ok or opencode_ok or codex_ok or auggie_ok): + if not (claude_ok or gemini_ok or cursor_ok or qwen_ok or windsurf_ok or kilocode_ok or opencode_ok or codex_ok or auggie_ok or droid_ok): console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]") diff --git a/templates/plan-template.md b/templates/plan-template.md index 283ffd4b3..92eb4d9b1 100644 --- a/templates/plan-template.md +++ b/templates/plan-template.md @@ -24,7 +24,7 @@ scripts: → Update Progress Tracking: Initial Constitution Check 5. Execute Phase 0 → research.md → If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns" -6. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, `GEMINI.md` for Gemini CLI, `QWEN.md` for Qwen Code or `AGENTS.md` for opencode). +6. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, `GEMINI.md` for Gemini CLI, `QWEN.md` for Qwen Code or `AGENTS.md` for opencode and Factory CLI). 7. Re-evaluate Constitution Check section → If new violations: Refactor design, return to Phase 1 → Update Progress Tracking: Post-Design Constitution Check @@ -40,14 +40,14 @@ scripts: [Extract from feature spec: primary requirement + technical approach from research] ## Technical Context -**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] -**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] -**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] -**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] +**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] +**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] +**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] **Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] -**Project Type**: [single/web/mobile - determines source structure] -**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] -**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] +**Project Type**: [single/web/mobile - determines source structure] +**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] +**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] **Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] ## Constitution Check @@ -175,12 +175,12 @@ directories captured above] - Load `.specify/templates/tasks-template.md` as base - Generate tasks from Phase 1 design docs (contracts, data model, quickstart) - Each contract → contract test task [P] -- Each entity → model creation task [P] +- Each entity → model creation task [P] - Each user story → integration test task - Implementation tasks to make tests pass **Ordering Strategy**: -- TDD order: Tests before implementation +- TDD order: Tests before implementation - Dependency order: Models before services before UI - Mark [P] for parallel execution (independent files) @@ -191,8 +191,8 @@ directories captured above] ## Phase 3+: Future Implementation *These phases are beyond the scope of the /plan command* -**Phase 3**: Task execution (/tasks command creates tasks.md) -**Phase 4**: Implementation (execute tasks.md following constitutional principles) +**Phase 3**: Task execution (/tasks command creates tasks.md) +**Phase 4**: Implementation (execute tasks.md following constitutional principles) **Phase 5**: Validation (run tests, execute quickstart.md, performance validation) ## Complexity Tracking From bc583bd32a908c220bef79fc4b66597175be7589 Mon Sep 17 00:00:00 2001 From: Andrew Gower Date: Mon, 29 Sep 2025 19:59:08 +0100 Subject: [PATCH 2/4] docs: Update AGENTS.md to include Factory CLI support - Added Factory CLI to the list of supported agents. - Updated AI_CHOICES and command file formats to reflect Factory CLI integration. - Fixed minor formatting inconsistencies in the documentation. --- AGENTS.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 59b995668..b9994fb2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,7 @@ Specify supports multiple AI agents by generating agent-specific command files a | **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI | | **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI | | **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows | +| **Factory CLI** | `.factory/commands/` | Markdown | `droid` | Factory CLI | ### Step-by-Step Integration Guide @@ -50,7 +51,7 @@ Add the new agent to the `AI_CHOICES` dictionary in `src/specify_cli/__init__.py ```python AI_CHOICES = { "copilot": "GitHub Copilot", - "claude": "Claude Code", + "claude": "Claude Code", "gemini": "Gemini CLI", "cursor": "Cursor", "qwen": "Qwen Code", @@ -138,7 +139,7 @@ Add to case statement: case "$AGENT_TYPE" in # ... existing cases ... windsurf) update_agent_file "$WINDSURF_FILE" "Windsurf" ;; - "") + "") # ... existing checks ... [ -f "$WINDSURF_FILE" ] && update_agent_file "$WINDSURF_FILE" "Windsurf"; # Update default creation condition @@ -193,10 +194,11 @@ elif selected_ai == "windsurf": ### CLI-Based Agents Require a command-line tool to be installed: - **Claude Code**: `claude` CLI -- **Gemini CLI**: `gemini` CLI +- **Gemini CLI**: `gemini` CLI - **Cursor**: `cursor-agent` CLI - **Qwen Code**: `qwen` CLI - **opencode**: `opencode` CLI +- **Factory CLI**: `droid` CLI ### IDE-Based Agents Work within integrated development environments: @@ -206,7 +208,7 @@ Work within integrated development environments: ## Command File Formats ### Markdown Format -Used by: Claude, Cursor, opencode, Windsurf +Used by: Claude, Cursor, opencode, Windsurf, Factory CLI ```markdown --- @@ -269,4 +271,4 @@ When adding new agents: --- -*This documentation should be updated whenever new agents are added to maintain accuracy and completeness.* \ No newline at end of file +*This documentation should be updated whenever new agents are added to maintain accuracy and completeness.* From 05bdfec560d5399e56384db31c702b9642f75643 Mon Sep 17 00:00:00 2001 From: Andrew Gower Date: Mon, 29 Sep 2025 19:59:33 +0100 Subject: [PATCH 3/4] docs: Update README.md to include Factory CLI support - Added Factory CLI to the list of supported AI agents. - Updated CLI tool check and initialization options to reflect Factory CLI integration. - Revised prerequisites section to include Factory CLI as an AI coding agent. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b4e91c377..b727ac047 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c | [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | | | [Roo Code](https://roocode.com/) | ✅ | | | [Codex CLI](https://github.com/openai/codex) | ⚠️ | Codex [does not support](https://github.com/openai/codex/issues/2890) custom arguments for slash commands. | +| [Factory CLI](https://factory.ai/) | ✅ | | ## 🔧 Specify CLI Reference @@ -144,14 +145,14 @@ The `specify` command supports the following options: | Command | Description | |-------------|----------------------------------------------------------------| | `init` | Initialize a new Specify project from the latest template | -| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`) | +| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `droid`) | ### `specify init` Arguments & Options | Argument/Option | Type | Description | |------------------------|----------|------------------------------------------------------------------------------| | `` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) | -| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, or `roo` | +| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, or `droid` | | `--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 | @@ -186,7 +187,7 @@ specify init --here --ai copilot # Force merge into current (non-empty) directory without confirmation specify init . --force --ai copilot -# or +# or specify init --here --force --ai copilot # Skip git initialization @@ -268,7 +269,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), [Cursor](https://cursor.sh/), [Qwen CLI](https://github.com/QwenLM/qwen-code), [opencode](https://opencode.ai/), [Codex CLI](https://github.com/openai/codex), or [Windsurf](https://windsurf.com/) +- 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/), [Qwen CLI](https://github.com/QwenLM/qwen-code), [opencode](https://opencode.ai/), [Codex CLI](https://github.com/openai/codex), [Windsurf](https://windsurf.com/), or [Factory CLI](https://factory.ai/) - [uv](https://docs.astral.sh/uv/) for package management - [Python 3.11+](https://www.python.org/downloads/) - [Git](https://git-scm.com/downloads) From b124f4b60030666e89323c5c2f68e6494be4d59d Mon Sep 17 00:00:00 2001 From: Andrew Gower Date: Mon, 29 Sep 2025 19:59:41 +0100 Subject: [PATCH 4/4] chore: Bump version to 0.0.18 and update CHANGELOG.md - Added support for Factory CLI (`droid`) agent, including necessary CLI help, packaging scripts, context updaters, and documentation updates. - Implemented fallback to locally built templates when a release archive for a new agent is not yet published. --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9485cb4c..fed21f12e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for using `.` as a shorthand for current directory in `specify init .` command, equivalent to `--here` flag but more intuitive for users +## [0.0.18] - 2025-09-23 + +### Added + +- Factory CLI (`droid`) agent support, including CLI help, packaging scripts, context updaters, and documentation updates. +- CLI fallback to locally built templates when a release archive for a new agent is not yet published. + ## [0.0.17] - 2025-09-22 ### Added diff --git a/pyproject.toml b/pyproject.toml index 559bad2d5..86f9abba1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "specify-cli" -version = "0.0.17" +version = "0.0.18" description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)." requires-python = ">=3.11" dependencies = [