diff --git a/README.md b/README.md index 1c7dda215..3e750ae5d 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ The `specify` command supports the following options: | Command | Description | |-------------|----------------------------------------------------------------| | `init` | Initialize a new Specify project from the latest template | +| `summarize` | Generate a comprehensive summary of an existing project's technology stack, architecture, and code conventions | | `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`) | ### `specify init` Arguments & Options @@ -219,6 +220,9 @@ specify init my-project --ai claude --github-token ghp_your_token_here # Check system requirements specify check + +# Analyze existing project (for brownfield/existing codebases) +specify summarize ``` ### Available Slash Commands @@ -243,6 +247,7 @@ Additional commands for enhanced quality and validation: | Command | Description | |----------------------|-----------------------------------------------------------------------| +| `/speckit.summarize` | Generate comprehensive summary of existing project (technology stack, architecture, code conventions) - ideal for brownfield projects | | `/speckit.clarify` | Clarify underspecified areas (recommended before `/speckit.plan`; formerly `/quizme`) | | `/speckit.analyze` | Cross-artifact consistency & coverage analysis (run after `/speckit.tasks`, before `/speckit.implement`) | | `/speckit.checklist` | Generate custom quality checklists that validate requirements completeness, clarity, and consistency (like "unit tests for English") | diff --git a/scripts/bash/generate-project-summary.sh b/scripts/bash/generate-project-summary.sh new file mode 100755 index 000000000..8e10a5e96 --- /dev/null +++ b/scripts/bash/generate-project-summary.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +set -e + +# Source common utilities +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Parse command line arguments +JSON_OUTPUT=false + +while [[ $# -gt 0 ]]; do + case $1 in + --json) + JSON_OUTPUT=true + shift + ;; + *) + shift + ;; + esac +done + +# Get repository root +REPO_ROOT=$(get_repo_root) + +# Initialize arrays for discovered data +declare -a TECH_FILES=() +declare -a DIRECTORIES=() +declare -a LANGUAGES=() + +# Detect configuration files for different languages/frameworks +detect_config_files() { + local root="$1" + + # JavaScript/TypeScript + [[ -f "$root/package.json" ]] && TECH_FILES+=("package.json") && LANGUAGES+=("JavaScript/TypeScript") + [[ -f "$root/tsconfig.json" ]] && TECH_FILES+=("tsconfig.json") + [[ -f "$root/yarn.lock" ]] && TECH_FILES+=("yarn.lock") + [[ -f "$root/pnpm-lock.yaml" ]] && TECH_FILES+=("pnpm-lock.yaml") + [[ -f "$root/bun.lockb" ]] && TECH_FILES+=("bun.lockb") + + # Python + [[ -f "$root/requirements.txt" ]] && TECH_FILES+=("requirements.txt") && LANGUAGES+=("Python") + [[ -f "$root/pyproject.toml" ]] && TECH_FILES+=("pyproject.toml") && LANGUAGES+=("Python") + [[ -f "$root/setup.py" ]] && TECH_FILES+=("setup.py") && LANGUAGES+=("Python") + [[ -f "$root/Pipfile" ]] && TECH_FILES+=("Pipfile") && LANGUAGES+=("Python") + [[ -f "$root/poetry.lock" ]] && TECH_FILES+=("poetry.lock") + + # Go + [[ -f "$root/go.mod" ]] && TECH_FILES+=("go.mod") && LANGUAGES+=("Go") + [[ -f "$root/go.sum" ]] && TECH_FILES+=("go.sum") + + # Rust + [[ -f "$root/Cargo.toml" ]] && TECH_FILES+=("Cargo.toml") && LANGUAGES+=("Rust") + [[ -f "$root/Cargo.lock" ]] && TECH_FILES+=("Cargo.lock") + + # Java + [[ -f "$root/pom.xml" ]] && TECH_FILES+=("pom.xml") && LANGUAGES+=("Java") + [[ -f "$root/build.gradle" ]] && TECH_FILES+=("build.gradle") && LANGUAGES+=("Java/Kotlin") + [[ -f "$root/build.gradle.kts" ]] && TECH_FILES+=("build.gradle.kts") && LANGUAGES+=("Kotlin") + + # Ruby + [[ -f "$root/Gemfile" ]] && TECH_FILES+=("Gemfile") && LANGUAGES+=("Ruby") + [[ -f "$root/Gemfile.lock" ]] && TECH_FILES+=("Gemfile.lock") + + # PHP + [[ -f "$root/composer.json" ]] && TECH_FILES+=("composer.json") && LANGUAGES+=("PHP") + [[ -f "$root/composer.lock" ]] && TECH_FILES+=("composer.lock") + + # C#/.NET + find "$root" -maxdepth 2 \( -name "*.csproj" -o -name "*.fsproj" -o -name "*.vbproj" \) | while read -r proj; do + TECH_FILES+=("$(basename "$proj")") + LANGUAGES+=("C#/.NET") + done + + # Swift + [[ -f "$root/Package.swift" ]] && TECH_FILES+=("Package.swift") && LANGUAGES+=("Swift") + + # Elixir + [[ -f "$root/mix.exs" ]] && TECH_FILES+=("mix.exs") && LANGUAGES+=("Elixir") + + # C/C++ + [[ -f "$root/CMakeLists.txt" ]] && TECH_FILES+=("CMakeLists.txt") && LANGUAGES+=("C/C++") + [[ -f "$root/Makefile" ]] && TECH_FILES+=("Makefile") + + # Code style/config files + [[ -f "$root/.editorconfig" ]] && TECH_FILES+=(".editorconfig") + [[ -f "$root/.prettierrc" ]] && TECH_FILES+=(".prettierrc") + [[ -f "$root/.prettierrc.json" ]] && TECH_FILES+=(".prettierrc.json") + [[ -f "$root/.eslintrc" ]] && TECH_FILES+=(".eslintrc") + [[ -f "$root/.eslintrc.json" ]] && TECH_FILES+=(".eslintrc.json") + [[ -f "$root/rustfmt.toml" ]] && TECH_FILES+=("rustfmt.toml") +} + +# Detect key directories +detect_directories() { + local root="$1" + + # Source directories + [[ -d "$root/src" ]] && DIRECTORIES+=("src") + [[ -d "$root/lib" ]] && DIRECTORIES+=("lib") + [[ -d "$root/app" ]] && DIRECTORIES+=("app") + [[ -d "$root/pkg" ]] && DIRECTORIES+=("pkg") + [[ -d "$root/internal" ]] && DIRECTORIES+=("internal") + [[ -d "$root/cmd" ]] && DIRECTORIES+=("cmd") + + # Test directories + [[ -d "$root/test" ]] && DIRECTORIES+=("test") + [[ -d "$root/tests" ]] && DIRECTORIES+=("tests") + [[ -d "$root/__tests__" ]] && DIRECTORIES+=("__tests__") + [[ -d "$root/spec" ]] && DIRECTORIES+=("spec") + + # Documentation + [[ -d "$root/docs" ]] && DIRECTORIES+=("docs") + [[ -d "$root/documentation" ]] && DIRECTORIES+=("documentation") + + # Configuration + [[ -d "$root/config" ]] && DIRECTORIES+=("config") + [[ -d "$root/conf" ]] && DIRECTORIES+=("conf") + [[ -d "$root/settings" ]] && DIRECTORIES+=("settings") + + # Frontend/Backend split + [[ -d "$root/frontend" ]] && DIRECTORIES+=("frontend") + [[ -d "$root/client" ]] && DIRECTORIES+=("client") + [[ -d "$root/web" ]] && DIRECTORIES+=("web") + [[ -d "$root/backend" ]] && DIRECTORIES+=("backend") + [[ -d "$root/server" ]] && DIRECTORIES+=("server") + [[ -d "$root/api" ]] && DIRECTORIES+=("api") + + # Database + [[ -d "$root/migrations" ]] && DIRECTORIES+=("migrations") + [[ -d "$root/models" ]] && DIRECTORIES+=("models") + [[ -d "$root/schemas" ]] && DIRECTORIES+=("schemas") + + # Monorepo + [[ -d "$root/packages" ]] && DIRECTORIES+=("packages") + [[ -d "$root/apps" ]] && DIRECTORIES+=("apps") + [[ -d "$root/services" ]] && DIRECTORIES+=("services") + + # Public/Static + [[ -d "$root/public" ]] && DIRECTORIES+=("public") + [[ -d "$root/static" ]] && DIRECTORIES+=("static") + [[ -d "$root/assets" ]] && DIRECTORIES+=("assets") + + # Build/Dist + [[ -d "$root/build" ]] && DIRECTORIES+=("build") + [[ -d "$root/dist" ]] && DIRECTORIES+=("dist") + [[ -d "$root/target" ]] && DIRECTORIES+=("target") +} + +# Detect project type +detect_project_type() { + local type="unknown" + + # Monorepo indicators + if [[ -d "$REPO_ROOT/packages" ]] || [[ -d "$REPO_ROOT/apps" ]] || [[ -d "$REPO_ROOT/services" ]]; then + type="monorepo" + # Frontend + Backend split + elif [[ -d "$REPO_ROOT/frontend" ]] && [[ -d "$REPO_ROOT/backend" ]]; then + type="fullstack-split" + elif [[ -d "$REPO_ROOT/client" ]] && [[ -d "$REPO_ROOT/server" ]]; then + type="fullstack-split" + # Web application + elif [[ -f "$REPO_ROOT/package.json" ]] && grep -q "react\|vue\|angular\|svelte\|next\|nuxt" "$REPO_ROOT/package.json" 2>/dev/null; then + type="web-frontend" + # API/Backend + elif [[ -d "$REPO_ROOT/api" ]] || [[ -d "$REPO_ROOT/server" ]]; then + type="backend-api" + # CLI tool + elif [[ -d "$REPO_ROOT/cmd" ]] || ( [[ -f "$REPO_ROOT/package.json" ]] && grep -q "\"bin\":" "$REPO_ROOT/package.json" 2>/dev/null ); then + type="cli-tool" + # Library + elif [[ -d "$REPO_ROOT/lib" ]] || ( [[ -f "$REPO_ROOT/Cargo.toml" ]] && grep -q "\[lib\]" "$REPO_ROOT/Cargo.toml" 2>/dev/null ); then + type="library" + # Generic application + elif [[ -d "$REPO_ROOT/src" ]]; then + type="application" + fi + + echo "$type" +} + +# Run detection +detect_config_files "$REPO_ROOT" +detect_directories "$REPO_ROOT" +PROJECT_TYPE=$(detect_project_type) + +# Deduplicate languages array +if [ ${#LANGUAGES[@]} -eq 0 ]; then + UNIQUE_LANGUAGES=() +else + UNIQUE_LANGUAGES=($(printf '%s\n' "${LANGUAGES[@]}" | sort -u)) +fi + +# Output as JSON if requested +if [ "$JSON_OUTPUT" = true ]; then + # Convert arrays to JSON arrays (handle empty arrays) + if [ ${#TECH_FILES[@]} -eq 0 ]; then + tech_files_json="[]" + else + tech_files_json=$(printf '%s\n' "${TECH_FILES[@]}" | jq -R . | jq -s .) + fi + + if [ ${#DIRECTORIES[@]} -eq 0 ]; then + directories_json="[]" + else + directories_json=$(printf '%s\n' "${DIRECTORIES[@]}" | jq -R . | jq -s .) + fi + + if [ ${#UNIQUE_LANGUAGES[@]} -eq 0 ]; then + languages_json="[]" + else + languages_json=$(printf '%s\n' "${UNIQUE_LANGUAGES[@]}" | jq -R . | jq -s .) + fi + + # Output JSON + jq -n \ + --arg repo_root "$REPO_ROOT" \ + --arg project_type "$PROJECT_TYPE" \ + --argjson tech_files "$tech_files_json" \ + --argjson directories "$directories_json" \ + --argjson languages "$languages_json" \ + '{ + REPO_ROOT: $repo_root, + PROJECT_TYPE: $project_type, + TECH_FILES: $tech_files, + DIRECTORIES: $directories, + LANGUAGES: $languages + }' +else + # Human-readable output + echo "Repository Root: $REPO_ROOT" + echo "Project Type: $PROJECT_TYPE" + echo "" + echo "Languages:" + if [ ${#UNIQUE_LANGUAGES[@]} -gt 0 ]; then + printf ' - %s\n' "${UNIQUE_LANGUAGES[@]}" + else + echo " (none detected)" + fi + echo "" + echo "Configuration Files:" + if [ ${#TECH_FILES[@]} -gt 0 ]; then + printf ' - %s\n' "${TECH_FILES[@]}" + else + echo " (none detected)" + fi + echo "" + echo "Key Directories:" + if [ ${#DIRECTORIES[@]} -gt 0 ]; then + printf ' - %s\n' "${DIRECTORIES[@]}" + else + echo " (none detected)" + fi +fi diff --git a/scripts/powershell/generate-project-summary.ps1 b/scripts/powershell/generate-project-summary.ps1 new file mode 100644 index 000000000..7e80d9e24 --- /dev/null +++ b/scripts/powershell/generate-project-summary.ps1 @@ -0,0 +1,309 @@ +#!/usr/bin/env pwsh +# Generate project summary by analyzing existing codebase +[CmdletBinding()] +param( + [switch]$Json, + [switch]$Help +) +$ErrorActionPreference = 'Stop' + +# Show help if requested +if ($Help) { + Write-Host "Usage: ./generate-project-summary.ps1 [-Json]" + Write-Host "" + Write-Host "Options:" + Write-Host " -Json Output in JSON format" + Write-Host " -Help Show this help message" + exit 0 +} + +# Find repository root +function Find-RepositoryRoot { + param( + [string]$StartDir = $PWD, + [string[]]$Markers = @('.git', '.specify') + ) + $current = Resolve-Path $StartDir + while ($true) { + foreach ($marker in $Markers) { + if (Test-Path (Join-Path $current $marker)) { + return $current.Path + } + } + $parent = Split-Path $current -Parent + if ($parent -eq $current) { + # Reached filesystem root without finding markers + return $null + } + $current = $parent + } +} + +$repoRoot = Find-RepositoryRoot +if (-not $repoRoot) { + Write-Error "Could not find repository root (no .git or .specify directory found)" + exit 1 +} + +# Initialize arrays +$techFiles = @() +$directories = @() +$languages = @() + +# Detect configuration files for different languages/frameworks +function Detect-ConfigFiles { + param([string]$Root) + + $script:techFiles = @() + $script:languages = @() + + # JavaScript/TypeScript + if (Test-Path "$Root/package.json") { + $script:techFiles += "package.json" + $script:languages += "JavaScript/TypeScript" + } + if (Test-Path "$Root/tsconfig.json") { $script:techFiles += "tsconfig.json" } + if (Test-Path "$Root/yarn.lock") { $script:techFiles += "yarn.lock" } + if (Test-Path "$Root/pnpm-lock.yaml") { $script:techFiles += "pnpm-lock.yaml" } + if (Test-Path "$Root/bun.lockb") { $script:techFiles += "bun.lockb" } + + # Python + if (Test-Path "$Root/requirements.txt") { + $script:techFiles += "requirements.txt" + $script:languages += "Python" + } + if (Test-Path "$Root/pyproject.toml") { + $script:techFiles += "pyproject.toml" + $script:languages += "Python" + } + if (Test-Path "$Root/setup.py") { + $script:techFiles += "setup.py" + $script:languages += "Python" + } + if (Test-Path "$Root/Pipfile") { + $script:techFiles += "Pipfile" + $script:languages += "Python" + } + if (Test-Path "$Root/poetry.lock") { $script:techFiles += "poetry.lock" } + + # Go + if (Test-Path "$Root/go.mod") { + $script:techFiles += "go.mod" + $script:languages += "Go" + } + if (Test-Path "$Root/go.sum") { $script:techFiles += "go.sum" } + + # Rust + if (Test-Path "$Root/Cargo.toml") { + $script:techFiles += "Cargo.toml" + $script:languages += "Rust" + } + if (Test-Path "$Root/Cargo.lock") { $script:techFiles += "Cargo.lock" } + + # Java + if (Test-Path "$Root/pom.xml") { + $script:techFiles += "pom.xml" + $script:languages += "Java" + } + if (Test-Path "$Root/build.gradle") { + $script:techFiles += "build.gradle" + $script:languages += "Java/Kotlin" + } + if (Test-Path "$Root/build.gradle.kts") { + $script:techFiles += "build.gradle.kts" + $script:languages += "Kotlin" + } + + # Ruby + if (Test-Path "$Root/Gemfile") { + $script:techFiles += "Gemfile" + $script:languages += "Ruby" + } + if (Test-Path "$Root/Gemfile.lock") { $script:techFiles += "Gemfile.lock" } + + # PHP + if (Test-Path "$Root/composer.json") { + $script:techFiles += "composer.json" + $script:languages += "PHP" + } + if (Test-Path "$Root/composer.lock") { $script:techFiles += "composer.lock" } + + # C#/.NET + $csprojFiles = Get-ChildItem -Path $Root -Filter "*.csproj" -File -Depth 2 -ErrorAction SilentlyContinue + $fsprojFiles = Get-ChildItem -Path $Root -Filter "*.fsproj" -File -Depth 2 -ErrorAction SilentlyContinue + $vbprojFiles = Get-ChildItem -Path $Root -Filter "*.vbproj" -File -Depth 2 -ErrorAction SilentlyContinue + + if ($csprojFiles -or $fsprojFiles -or $vbprojFiles) { + foreach ($proj in ($csprojFiles + $fsprojFiles + $vbprojFiles)) { + $script:techFiles += $proj.Name + } + $script:languages += "C#/.NET" + } + + # Swift + if (Test-Path "$Root/Package.swift") { + $script:techFiles += "Package.swift" + $script:languages += "Swift" + } + + # Elixir + if (Test-Path "$Root/mix.exs") { + $script:techFiles += "mix.exs" + $script:languages += "Elixir" + } + + # C/C++ + if (Test-Path "$Root/CMakeLists.txt") { + $script:techFiles += "CMakeLists.txt" + $script:languages += "C/C++" + } + if (Test-Path "$Root/Makefile") { $script:techFiles += "Makefile" } + + # Code style/config files + if (Test-Path "$Root/.editorconfig") { $script:techFiles += ".editorconfig" } + if (Test-Path "$Root/.prettierrc") { $script:techFiles += ".prettierrc" } + if (Test-Path "$Root/.prettierrc.json") { $script:techFiles += ".prettierrc.json" } + if (Test-Path "$Root/.eslintrc") { $script:techFiles += ".eslintrc" } + if (Test-Path "$Root/.eslintrc.json") { $script:techFiles += ".eslintrc.json" } + if (Test-Path "$Root/rustfmt.toml") { $script:techFiles += "rustfmt.toml" } +} + +# Detect key directories +function Detect-Directories { + param([string]$Root) + + $script:directories = @() + + # Source directories + if (Test-Path "$Root/src") { $script:directories += "src" } + if (Test-Path "$Root/lib") { $script:directories += "lib" } + if (Test-Path "$Root/app") { $script:directories += "app" } + if (Test-Path "$Root/pkg") { $script:directories += "pkg" } + if (Test-Path "$Root/internal") { $script:directories += "internal" } + if (Test-Path "$Root/cmd") { $script:directories += "cmd" } + + # Test directories + if (Test-Path "$Root/test") { $script:directories += "test" } + if (Test-Path "$Root/tests") { $script:directories += "tests" } + if (Test-Path "$Root/__tests__") { $script:directories += "__tests__" } + if (Test-Path "$Root/spec") { $script:directories += "spec" } + + # Documentation + if (Test-Path "$Root/docs") { $script:directories += "docs" } + if (Test-Path "$Root/documentation") { $script:directories += "documentation" } + + # Configuration + if (Test-Path "$Root/config") { $script:directories += "config" } + if (Test-Path "$Root/conf") { $script:directories += "conf" } + if (Test-Path "$Root/settings") { $script:directories += "settings" } + + # Frontend/Backend split + if (Test-Path "$Root/frontend") { $script:directories += "frontend" } + if (Test-Path "$Root/client") { $script:directories += "client" } + if (Test-Path "$Root/web") { $script:directories += "web" } + if (Test-Path "$Root/backend") { $script:directories += "backend" } + if (Test-Path "$Root/server") { $script:directories += "server" } + if (Test-Path "$Root/api") { $script:directories += "api" } + + # Database + if (Test-Path "$Root/migrations") { $script:directories += "migrations" } + if (Test-Path "$Root/models") { $script:directories += "models" } + if (Test-Path "$Root/schemas") { $script:directories += "schemas" } + + # Monorepo + if (Test-Path "$Root/packages") { $script:directories += "packages" } + if (Test-Path "$Root/apps") { $script:directories += "apps" } + if (Test-Path "$Root/services") { $script:directories += "services" } + + # Public/Static + if (Test-Path "$Root/public") { $script:directories += "public" } + if (Test-Path "$Root/static") { $script:directories += "static" } + if (Test-Path "$Root/assets") { $script:directories += "assets" } + + # Build/Dist + if (Test-Path "$Root/build") { $script:directories += "build" } + if (Test-Path "$Root/dist") { $script:directories += "dist" } + if (Test-Path "$Root/target") { $script:directories += "target" } +} + +# Detect project type +function Detect-ProjectType { + param([string]$Root) + + $type = "unknown" + + # Monorepo indicators + if ((Test-Path "$Root/packages") -or (Test-Path "$Root/apps") -or (Test-Path "$Root/services")) { + $type = "monorepo" + } + # Frontend + Backend split + elseif ((Test-Path "$Root/frontend") -and (Test-Path "$Root/backend")) { + $type = "fullstack-split" + } + elseif ((Test-Path "$Root/client") -and (Test-Path "$Root/server")) { + $type = "fullstack-split" + } + # Web application + elseif ((Test-Path "$Root/package.json") -and + (Select-String -Path "$Root/package.json" -Pattern "react|vue|angular|svelte|next|nuxt" -Quiet)) { + $type = "web-frontend" + } + # API/Backend + elseif ((Test-Path "$Root/api") -or (Test-Path "$Root/server")) { + $type = "backend-api" + } + # CLI tool + elseif ((Test-Path "$Root/cmd") -or + ((Test-Path "$Root/package.json") -and (Select-String -Path "$Root/package.json" -Pattern "`"bin`":" -Quiet))) { + $type = "cli-tool" + } + # Library + elseif ((Test-Path "$Root/lib") -or + ((Test-Path "$Root/Cargo.toml") -and (Select-String -Path "$Root/Cargo.toml" -Pattern "\[lib\]" -Quiet))) { + $type = "library" + } + # Generic application + elseif (Test-Path "$Root/src") { + $type = "application" + } + + return $type +} + +# Run detection +Detect-ConfigFiles $repoRoot +Detect-Directories $repoRoot +$projectType = Detect-ProjectType $repoRoot + +# Deduplicate languages +$uniqueLanguages = $languages | Select-Object -Unique + +# Output +if ($Json) { + $result = @{ + REPO_ROOT = $repoRoot + PROJECT_TYPE = $projectType + TECH_FILES = $techFiles + DIRECTORIES = $directories + LANGUAGES = $uniqueLanguages + } + $result | ConvertTo-Json -Compress +} else { + Write-Host "Repository Root: $repoRoot" + Write-Host "Project Type: $projectType" + Write-Host "" + Write-Host "Languages:" + foreach ($lang in $uniqueLanguages) { + Write-Host " - $lang" + } + Write-Host "" + Write-Host "Configuration Files:" + foreach ($file in $techFiles) { + Write-Host " - $file" + } + Write-Host "" + Write-Host "Key Directories:" + foreach ($dir in $directories) { + Write-Host " - $dir" + } +} \ No newline at end of file diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index a33a1c61a..e0882c77f 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1160,6 +1160,217 @@ def init( console.print() console.print(enhancements_panel) +@app.command() +def summarize(): + """Generate a comprehensive summary of an existing project.""" + show_banner() + console.print("[bold]Analyzing project...[/bold]\n") + + tracker = StepTracker("Project Analysis") + + # Find repository root + try: + tracker.add("find-root", "Locate repository root") + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=False + ) + if result.returncode == 0: + repo_root = Path(result.stdout.strip()) + tracker.complete("find-root", str(repo_root)) + else: + # Fallback to current directory + repo_root = Path.cwd() + # Check for .specify directory + while repo_root != repo_root.parent: + if (repo_root / ".specify").exists(): + break + repo_root = repo_root.parent + else: + repo_root = Path.cwd() + tracker.complete("find-root", str(repo_root)) + except Exception as e: + tracker.error("find-root", str(e)) + console.print(tracker.render()) + raise typer.Exit(1) + + # Determine script type (prefer bash on Unix-like, PowerShell on Windows) + is_windows = sys.platform == "win32" + script_ext = "ps1" if is_windows else "sh" + script_dir = "powershell" if is_windows else "bash" + + # Find script path (could be in installed location or development location) + script_name = f"generate-project-summary.{script_ext}" + + # Try to find script in various locations, for both .ps1 and .sh + if is_windows: + possible_script_names = ["generate-project-summary.ps1", "generate-project-summary.sh"] + else: + possible_script_names = ["generate-project-summary.sh"] + possible_script_paths = [] + for name in possible_script_names: + possible_script_paths.extend([ + repo_root / ".specify" / "scripts" / name, + repo_root / "scripts" / script_dir / name, + Path(__file__).parent.parent.parent / "scripts" / script_dir / name, + ]) + + script_path = None + script_type = None + for path in possible_script_paths: + if path.exists(): + script_path = path + if str(path).endswith('.ps1'): + script_type = 'ps1' + elif str(path).endswith('.sh'): + script_type = 'sh' + break + + if not script_path: + tracker.error("find-script", f"Could not find {script_name} (.ps1 or .sh)") + console.print(tracker.render()) + console.print(f"\n[red]Error: Analysis script not found. Tried:[/red]") + for path in possible_script_paths: + console.print(f" - {path}") + raise typer.Exit(1) + + tracker.add("run-script", f"Run analysis script ({script_path.name})") + + # Run the script with --json flag + try: + if script_type == 'ps1': + cmd = ["pwsh", "-File", str(script_path), "-Json"] + elif script_type == 'sh': + cmd = ["/bin/bash", str(script_path), "--json"] + else: + tracker.error("run-script", f"Unknown script type for {script_path}") + console.print(tracker.render()) + raise typer.Exit(1) + + result = subprocess.run( + cmd, + cwd=str(repo_root), + capture_output=True, + text=True, + check=True + ) + + # Parse JSON output + data = json.loads(result.stdout) + tracker.complete("run-script", f"Found {len(data.get('LANGUAGES', []))} language(s)") + + except subprocess.CalledProcessError as e: + tracker.error("run-script", f"Script failed: {e.stderr}") + console.print(tracker.render()) + raise typer.Exit(1) + except json.JSONDecodeError as e: + tracker.error("run-script", f"Invalid JSON output: {e}") + console.print(tracker.render()) + raise typer.Exit(1) + + console.print(tracker.render()) + + # Display results + console.print("\n[bold cyan]📊 Project Summary[/bold cyan]\n") + + # Project type + console.print(f"[bold]Project Type:[/bold] {data.get('PROJECT_TYPE', 'unknown')}") + console.print(f"[bold]Repository:[/bold] {data.get('REPO_ROOT', '')}\n") + + # Languages + languages = data.get('LANGUAGES', []) + if languages: + console.print("[bold]Languages:[/bold]") + for lang in languages: + console.print(f" • {lang}") + console.print() + + # Configuration files + tech_files = data.get('TECH_FILES', []) + if tech_files: + console.print(f"[bold]Configuration Files:[/bold] ({len(tech_files)} found)") + # Show first 10, then truncate + for file in tech_files[:10]: + console.print(f" • {file}") + if len(tech_files) > 10: + console.print(f" ... and {len(tech_files) - 10} more") + console.print() + + # Directories + directories = data.get('DIRECTORIES', []) + if directories: + console.print(f"[bold]Key Directories:[/bold] ({len(directories)} found)") + for dir in directories[:15]: + console.print(f" • {dir}/") + if len(directories) > 15: + console.print(f" ... and {len(directories) - 15} more") + console.print() + + # Save summary + memory_dir = repo_root / ".specify" / "memory" + summary_file = memory_dir / "project-summary.md" + + # Check if .specify exists + if not (repo_root / ".specify").exists(): + console.print("[yellow]⚠️ .specify directory not found.[/yellow]") + console.print("[dim]Run 'specify init --here --ai ' first to initialize spec-kit.[/dim]\n") + else: + # Ensure memory directory exists + memory_dir.mkdir(parents=True, exist_ok=True) + + # Generate summary content (basic version for now) + from datetime import datetime + summary_content = f"""# Project Summary + +**Generated**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} +**Repository**: {data.get('REPO_ROOT', '')} + +## Technology Stack + +### Languages +{chr(10).join(f'- {lang}' for lang in languages) if languages else '- (none detected)'} + +### Configuration Files +{( + chr(10).join( + [f'- `{file}`' for file in tech_files[:20]] + + ([f'- ... and {len(tech_files) - 20} more'] if len(tech_files) > 20 else []) + ) if tech_files else '- (none detected)' +)} + +## Project Structure + +### Architecture Pattern +{data.get('PROJECT_TYPE', 'unknown')} + +### Key Directories +{chr(10).join(f'- `{dir}/`' for dir in directories) if directories else '- (none detected)'} + +## Notes + +This is a basic summary generated by `specify summarize`. +For a more detailed analysis, use the `/speckit.summarize` command in your AI assistant. + +--- + +*Generated by `specify summarize` - re-run this command to update the summary.* +""" + + summary_file.write_text(summary_content, encoding="utf-8") + console.print(f"[bold green]✅ Summary saved to:[/bold green] {summary_file}\n") + + # Next steps + console.print("[bold]💡 Next Steps:[/bold]") + if not (repo_root / ".specify").exists(): + console.print(" 1. Run [cyan]specify init --here --ai [/cyan] to initialize spec-kit") + else: + console.print(" 1. Review the generated summary") + console.print(" 2. Run [cyan]/speckit.constitution[/cyan] in your AI assistant to define project principles") + console.print(" 3. Start creating feature specs with [cyan]/speckit.specify[/cyan]") + + @app.command() def check(): """Check that all required tools are installed.""" diff --git a/templates/commands/summarize.md b/templates/commands/summarize.md new file mode 100644 index 000000000..ff9d99efa --- /dev/null +++ b/templates/commands/summarize.md @@ -0,0 +1,325 @@ +--- +description: Generate a comprehensive summary of an existing project's technology stack, architecture, and code conventions +scripts: + sh: scripts/bash/generate-project-summary.sh --json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Analyze an existing codebase to extract its technology stack, project structure, code conventions, and testing setup. This command helps bootstrap spec-kit usage in brownfield/existing projects by automatically discovering project characteristics. + +## Operating Constraints + +**STRICTLY READ-ONLY**: Do **not** modify any files. Only analyze existing code and generate a summary document. + +**Progressive Analysis**: Start with configuration files, then analyze directory structure, and finally sample code files for conventions. + +**Smart Detection**: Adapt analysis depth based on project type and available indicators. + +## Execution Steps + +### 1. Initialize Project Scan + +Run `{SCRIPT}` from repo root and parse JSON output for: + +- REPO_ROOT: Project root directory +- TECH_FILES: Discovered configuration files (package.json, pyproject.toml, etc.) +- DIRECTORIES: Key directories found (src/, tests/, etc.) +- PROJECT_TYPE: Detected project type (web, cli, library, monorepo, etc.) + +For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). + +### 2. Analyze Technology Stack + +**Scan for language and framework indicators:** + +Load and analyze configuration files returned by the script: + +**JavaScript/TypeScript:** +- package.json: Extract dependencies, devDependencies, scripts +- tsconfig.json: TypeScript configuration +- Detect frameworks: React, Vue, Angular, Next.js, Express, etc. + +**Python:** +- requirements.txt, pyproject.toml, setup.py, Pipfile +- Detect frameworks: Django, Flask, FastAPI, etc. + +**Go:** +- go.mod, go.sum +- Detect frameworks: Gin, Echo, Chi, etc. + +**Rust:** +- Cargo.toml +- Detect frameworks: Actix, Rocket, Axum, etc. + +**Java:** +- pom.xml, build.gradle +- Detect frameworks: Spring Boot, Quarkus, etc. + +**Other languages:** +- Ruby: Gemfile +- PHP: composer.json +- C#: *.csproj + +**Output consolidated technology stack:** +- Primary language(s) and version(s) +- Main framework(s) +- Key dependencies (top 5-10 most significant) +- Package manager +- Build tool(s) + +### 3. Analyze Project Structure + +**Map directory structure:** + +Scan for common patterns: +- Source code: src/, lib/, app/, pkg/, internal/ +- Tests: test/, tests/, __tests__/, spec/ +- Documentation: docs/, documentation/ +- Configuration: config/, conf/, settings/ +- Frontend: frontend/, client/, web/, public/ +- Backend: backend/, server/, api/ +- Database: migrations/, models/, schemas/ + +**Identify architecture pattern:** +- Monorepo (presence of packages/, apps/, services/) +- Microservices (multiple service directories) +- Frontend + Backend split +- MVC/MVVM (models/, views/, controllers/) +- Clean Architecture (domain/, application/, infrastructure/) +- Modular monolith (modules/, features/) + +**Output:** +- Directory tree (top 2 levels) +- Identified architecture pattern +- Key directories with purpose + +### 4. Analyze Code Conventions + +**Sample representative files** (read 3-5 files from each category): + +**Naming conventions:** +- File naming: camelCase, PascalCase, snake_case, kebab-case +- Variable naming: camelCase, snake_case +- Function naming: camelCase, snake_case, PascalCase +- Class naming: PascalCase, snake_case +- Constant naming: UPPER_CASE, camelCase + +**Code style:** +- Indentation: tabs vs spaces, width (2, 4) +- Line length limits +- Brace style (K&R, Allman, etc.) +- Import/require organization + +**Documentation style:** +- JSDoc, docstrings, rustdoc, GoDoc +- Comment density and patterns +- README structure + +**Look for configuration files:** +- .editorconfig +- .prettierrc, prettier.config.js +- .eslintrc, eslint.config.js +- pyproject.toml [tool.black], [tool.ruff] +- rustfmt.toml +- .clang-format + +**Output:** +- Naming convention summary +- Indentation style +- Code style guide (if configured) +- Documentation patterns + +### 5. Analyze Testing Setup + +**Detect test frameworks:** + +**JavaScript/TypeScript:** +- Jest, Vitest, Mocha, Jasmine, AVA +- Testing Library, Enzyme +- Playwright, Cypress, Puppeteer + +**Python:** +- pytest, unittest, nose +- Coverage.py + +**Go:** +- testing (built-in) +- testify + +**Rust:** +- cargo test (built-in) + +**Java:** +- JUnit, TestNG +- Mockito + +**Look for:** +- Test configuration files +- Test scripts in package.json +- CI/CD configuration (.github/workflows/, .gitlab-ci.yml, etc.) +- Coverage reports configuration + +**Output:** +- Test framework(s) +- Test directory structure +- Test coverage setup (if configured) +- CI/CD integration (if present) + +### 6. Generate Project Summary + +Create a structured summary document at `.specify/memory/project-summary.md`: + +```markdown +# Project Summary + +**Generated**: [CURRENT_DATE] +**Repository**: [REPO_ROOT] + +## Technology Stack + +### Languages +- [LANGUAGE]: [VERSION] + +### Frameworks +- [FRAMEWORK_NAME]: [VERSION] + +### Key Dependencies +- [DEPENDENCY_1]: [VERSION] - [PURPOSE] +- [DEPENDENCY_2]: [VERSION] - [PURPOSE] +... + +### Package Manager +- [PACKAGE_MANAGER] + +### Build Tools +- [BUILD_TOOL] + +## Project Structure + +### Architecture Pattern +[DETECTED_PATTERN] + +### Directory Structure +``` +[DIRECTORY_TREE] +``` + +### Key Directories +- `[DIRECTORY]`: [PURPOSE] +... + +## Code Conventions + +### Naming Conventions +- Files: [FILE_NAMING] +- Variables: [VARIABLE_NAMING] +- Functions: [FUNCTION_NAMING] +- Classes: [CLASS_NAMING] +- Constants: [CONSTANT_NAMING] + +### Code Style +- Indentation: [TABS_OR_SPACES] ([WIDTH]) +- Line length: [MAX_LENGTH] characters +- Style guide: [STYLE_GUIDE_IF_CONFIGURED] + +### Documentation +- Format: [DOC_FORMAT] +- Pattern: [DOC_PATTERN] + +## Testing + +### Test Framework +- [TEST_FRAMEWORK] + +### Test Structure +- Test directory: `[TEST_DIR]` +- Test pattern: [TEST_FILE_PATTERN] + +### Coverage +- Tool: [COVERAGE_TOOL] +- Configuration: [COVERAGE_CONFIG_IF_PRESENT] + +### CI/CD +- Platform: [CI_PLATFORM] +- Configuration: `[CI_CONFIG_FILE]` + +## Notes + +[ANY_ADDITIONAL_OBSERVATIONS] + +--- + +*This summary was generated by `specify summarize`. Update this file as the project evolves or re-run the command to regenerate.* +``` + +### 7. Output Results + +Display a summary to the user: + +1. **Technology Stack Overview**: Brief summary of detected languages and frameworks +2. **Project Type**: Identified architecture pattern +3. **File Location**: Path to generated summary (`.specify/memory/project-summary.md`) +4. **Next Steps**: Suggest using `/speckit.constitution` to define project principles based on discovered conventions + +**Example output:** + +``` +✅ Project analysis complete! + +📊 Summary: + • Language: TypeScript 5.x + • Framework: Next.js 14.x + • Architecture: Frontend + Backend (monorepo) + • Test Framework: Jest + Playwright + +📄 Detailed summary saved to: + .specify/memory/project-summary.md + +💡 Next steps: + 1. Review the generated summary + 2. Run /speckit.constitution to define project principles + 3. Start creating feature specs with /speckit.specify +``` + +## Operating Principles + +### Analysis Strategy + +- **Configuration first**: Start with package managers and build tools +- **Pattern recognition**: Use heuristics to identify common frameworks and patterns +- **Sampling over exhaustive**: Sample representative files rather than reading entire codebase +- **Graceful degradation**: Provide partial summary if some aspects can't be detected + +### Quality Guidelines + +- **Accuracy over completeness**: Only report what can be confidently detected +- **Evidence-based**: Cite specific files as evidence for conclusions +- **Version awareness**: Include version numbers where available +- **Avoid assumptions**: Mark uncertain detections as "likely" or "possible" + +### Context Efficiency + +- **Minimal file reads**: Target specific configuration and sample files +- **Progressive disclosure**: Analyze in stages (config → structure → conventions → tests) +- **Token-efficient**: Summarize findings concisely +- **Reusable data**: Store summary for future commands to reference + +## Error Handling + +- **No .specify directory**: Initialize it first by suggesting `specify init --here --ai [agent]` +- **Unknown project type**: Still generate summary with available information +- **Multiple languages**: List all detected languages with primary language first +- **Conflicting conventions**: Note variations found in different parts of codebase + +## Context + +{ARGS} diff --git a/templates/project-summary-template.md b/templates/project-summary-template.md new file mode 100644 index 000000000..f793f44a3 --- /dev/null +++ b/templates/project-summary-template.md @@ -0,0 +1,78 @@ +# Project Summary + +**Generated**: [CURRENT_DATE] +**Repository**: [REPO_ROOT] + +## Technology Stack + +### Languages +- [LANGUAGE]: [VERSION] + +### Frameworks +- [FRAMEWORK_NAME]: [VERSION] + +### Key Dependencies +- [DEPENDENCY]: [VERSION] - [PURPOSE] +... + +### Package Manager +- [PACKAGE_MANAGER] + +### Build Tools +- [BUILD_TOOL] + +## Project Structure + +### Architecture Pattern +[DETECTED_PATTERN] + +### Directory Structure +``` +[DIRECTORY_TREE] +``` + +### Key Directories +- [DIRECTORY]/ ... + +## Code Conventions + +### Naming Conventions +- Files: [FILE_NAMING] +- Variables: [VARIABLE_NAMING] +- Functions: [FUNCTION_NAMING] +- Classes: [CLASS_NAMING] +- Constants: [CONSTANT_NAMING] + +### Code Style +- Indentation: [TABS_OR_SPACES] ([WIDTH]) +- Line length: [MAX_LENGTH] characters +- Style guide: [STYLE_GUIDE_IF_CONFIGURED] + +### Documentation +- Format: [DOC_FORMAT] +- Pattern: [DOC_PATTERN] + +## Testing + +### Test Framework +- [TEST_FRAMEWORK] + +### Test Structure +- Test directory: `[TEST_DIR]` +- Test pattern: [TEST_FILE_PATTERN] + +### Coverage +- Tool: [COVERAGE_TOOL] +- Configuration: [COVERAGE_CONFIG_IF_PRESENT] + +### CI/CD +- Platform: [CI_PLATFORM] +- Configuration: `[CI_CONFIG_FILE]` + +## Notes + +[ANY_ADDITIONAL_OBSERVATIONS] + +--- + +*This summary was generated by `specify summarize`. Update this file as the project evolves or re-run the command to regenerate.*