diff --git a/README.md b/README.md index c3dcdf269..f6a0c12a4 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ The `specify` command supports the following options: |-------------|----------------------------------------------------------------| | `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`, `shai`) | +| `config` | Manage Specify project configuration (progress tracking settings, etc.) | +| `version` | Display version and system information | ### `specify init` Arguments & Options @@ -223,6 +225,12 @@ specify init my-project --ai claude --github-token ghp_your_token_here # Check system requirements specify check + +# View and manage configuration +specify config # Show current configuration +specify config --auto-tracking # Enable auto-tracking +specify config --no-auto-tracking # Disable auto-tracking +specify config --get progress.autoTracking # Get specific setting ``` ### Available Slash Commands diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 2fa6455a2..70c547769 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1349,6 +1349,208 @@ def version(): console.print(panel) console.print() +def _get_config_path(project_path: Path = None) -> Path: + """Get the path to .specify/config.json file.""" + if project_path is None: + project_path = Path.cwd() + return project_path / ".specify" / "config.json" + +def _load_config(project_path: Path = None) -> dict: + """Load configuration from .specify/config.json, return default if not exists.""" + config_path = _get_config_path(project_path) + default_config = { + "progress": { + "autoTracking": False, + "updateOnTaskComplete": True, + "updateOnPhaseComplete": True + }, + "version": "1.0" + } + + if not config_path.exists(): + return default_config + + try: + with open(config_path, 'r', encoding='utf-8') as f: + user_config = json.load(f) + # Merge with defaults to ensure all keys exist + merged = default_config.copy() + if "progress" in user_config: + merged["progress"].update(user_config["progress"]) + if "version" in user_config: + merged["version"] = user_config["version"] + return merged + except (json.JSONDecodeError, IOError) as e: + console.print(f"[yellow]Warning:[/yellow] Could not read config file: {e}") + console.print("[dim]Using default configuration[/dim]") + return default_config + +def _save_config(config: dict, project_path: Path = None) -> bool: + """Save configuration to .specify/config.json.""" + config_path = _get_config_path(project_path) + config_dir = config_path.parent + + try: + # Ensure .specify directory exists + config_dir.mkdir(parents=True, exist_ok=True) + + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2) + f.write('\n') + return True + except IOError as e: + console.print(f"[red]Error:[/red] Could not write config file: {e}") + return False + +@app.command() +def config( + show: bool = typer.Option(False, "--show", "-s", help="Show current configuration"), + get: str = typer.Option(None, "--get", "-g", help="Get a specific configuration value (e.g., 'progress.autoTracking')"), + set_key: str = typer.Option(None, "--set", help="Set a configuration key (e.g., 'progress.autoTracking')"), + set_value: str = typer.Option(None, help="Value for --set (true/false for booleans)"), + auto_tracking: bool = typer.Option(False, "--auto-tracking/--no-auto-tracking", help="Enable or disable auto-tracking"), + project_path: str = typer.Option(None, "--project", "-p", help="Project path (default: current directory)"), +): + """ + Manage Specify project configuration. + + View or modify settings for progress tracking and other features. + + Examples: + specify config --show # Show current configuration + specify config --get progress.autoTracking # Get specific setting + specify config --auto-tracking true # Enable auto-tracking + specify config --set progress.autoTracking false # Set specific setting + """ + show_banner() + + # Determine project path + if project_path: + proj_path = Path(project_path).resolve() + if not proj_path.exists(): + console.print(f"[red]Error:[/red] Project path does not exist: {project_path}") + raise typer.Exit(1) + else: + proj_path = Path.cwd() + + # Check if this is a Specify project + specify_dir = proj_path / ".specify" + if not specify_dir.exists(): + console.print("[yellow]Warning:[/yellow] This doesn't appear to be a Specify project.") + console.print("[dim]Configuration will be created in current directory[/dim]\n") + + # Load current config + current_config = _load_config(proj_path) + + # Handle different operations + # Check if --auto-tracking or --no-auto-tracking was explicitly used + # Typer sets the flag to True if --auto-tracking, False if --no-auto-tracking + # We need to detect if user actually used the flag (not just default False) + import sys + auto_tracking_used = "--auto-tracking" in sys.argv or "--no-auto-tracking" in sys.argv + + if auto_tracking_used: + # Quick set for auto-tracking + current_config["progress"]["autoTracking"] = auto_tracking + if _save_config(current_config, proj_path): + status = "enabled" if auto_tracking else "disabled" + console.print(f"[green]✓[/green] Auto-tracking {status}") + console.print(f"[dim]Configuration saved to: {_get_config_path(proj_path)}[/dim]") + else: + raise typer.Exit(1) + return + + if set_key and set_value is not None: + # Set specific key + keys = set_key.split('.') + config_ref = current_config + for key in keys[:-1]: + if key not in config_ref: + config_ref[key] = {} + config_ref = config_ref[key] + + # Convert value based on type + final_key = keys[-1] + if isinstance(config_ref.get(final_key), bool): + # Boolean conversion + if set_value.lower() in ('true', '1', 'yes', 'on'): + config_ref[final_key] = True + elif set_value.lower() in ('false', '0', 'no', 'off'): + config_ref[final_key] = False + else: + console.print(f"[red]Error:[/red] Invalid boolean value: {set_value}") + console.print("[dim]Use: true, false, 1, 0, yes, no, on, off[/dim]") + raise typer.Exit(1) + elif isinstance(config_ref.get(final_key), int): + try: + config_ref[final_key] = int(set_value) + except ValueError: + console.print(f"[red]Error:[/red] Invalid integer value: {set_value}") + raise typer.Exit(1) + else: + config_ref[final_key] = set_value + + if _save_config(current_config, proj_path): + console.print(f"[green]✓[/green] Set {set_key} = {config_ref[final_key]}") + console.print(f"[dim]Configuration saved to: {_get_config_path(proj_path)}[/dim]") + else: + raise typer.Exit(1) + return + + if get: + # Get specific key + keys = get.split('.') + value = current_config + try: + for key in keys: + value = value[key] + console.print(f"[cyan]{get}:[/cyan] {value}") + except KeyError: + console.print(f"[red]Error:[/red] Configuration key not found: {get}") + raise typer.Exit(1) + return + + # Default: show all configuration + config_table = Table(show_header=False, box=None, padding=(0, 2)) + config_table.add_column("Setting", style="cyan", justify="left") + config_table.add_column("Value", style="white", justify="left") + + # Progress settings + config_table.add_row("", "") + config_table.add_row("[bold]Progress Tracking[/bold]", "") + config_table.add_row(" autoTracking", str(current_config["progress"]["autoTracking"])) + config_table.add_row(" updateOnTaskComplete", str(current_config["progress"]["updateOnTaskComplete"])) + config_table.add_row(" updateOnPhaseComplete", str(current_config["progress"]["updateOnPhaseComplete"])) + + # Version + config_table.add_row("", "") + config_table.add_row("[bold]Version[/bold]", current_config.get("version", "1.0")) + + config_panel = Panel( + config_table, + title="[bold cyan]Specify Configuration[/bold cyan]", + border_style="cyan", + padding=(1, 2) + ) + + console.print(config_panel) + console.print() + + # Show file location + config_file = _get_config_path(proj_path) + if config_file.exists(): + console.print(f"[dim]Configuration file: {config_file}[/dim]") + else: + console.print(f"[dim]Configuration file: {config_file} (using defaults)[/dim]") + + console.print() + console.print("[bold]Usage examples:[/bold]") + console.print(" [cyan]specify config --auto-tracking[/cyan] # Enable auto-tracking") + console.print(" [cyan]specify config --no-auto-tracking[/cyan] # Disable auto-tracking") + console.print(" [cyan]specify config --get progress.autoTracking[/cyan] # Get specific setting") + console.print(" [cyan]specify config --set progress.autoTracking true[/cyan] # Set specific setting") + console.print() + def main(): app() diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 39abb1e6c..be02b24a6 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -122,11 +122,15 @@ You **MUST** consider the user input before proceeding (if not empty). 8. Progress tracking and error handling: - Report progress after each completed task + - **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file. + - **Auto-tracking (optional)**: Check `.specify/config.json` for `progress.autoTracking` setting: + - If `autoTracking: true`: After each completed task, update PROGRESS.md and STATUS.md files using the same logic as `/speckit.progress` command + - If `autoTracking: false` or config missing: Skip automatic progress file updates (user can run `/speckit.progress` manually) + - Progress files location: FEATURE_DIR/PROGRESS.md and FEATURE_DIR/STATUS.md - Halt execution if any non-parallel task fails - For parallel tasks [P], continue with successful tasks, report failed ones - Provide clear error messages with context for debugging - Suggest next steps if implementation cannot proceed - - **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file. 9. Completion validation: - Verify all required tasks are completed diff --git a/templates/commands/progress.md b/templates/commands/progress.md new file mode 100644 index 000000000..8523aaf13 --- /dev/null +++ b/templates/commands/progress.md @@ -0,0 +1,131 @@ +--- +description: Generate and update progress tracking files (PROGRESS.md and STATUS.md) from tasks.md +scripts: + sh: scripts/bash/check-prerequisites.sh --json --require-tasks + ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Outline + +1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 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. **Load configuration** (if `.specify/config.json` exists): + - Read `.specify/config.json` to check progress tracking settings + - If file doesn't exist, use default settings (autoTracking: false) + - Extract `progress.autoTracking`, `progress.updateOnTaskComplete`, `progress.updateOnPhaseComplete` values + +3. **Load tasks.md**: + - **REQUIRED**: Read FEATURE_DIR/tasks.md for the complete task list + - Parse all tasks with format: `- [ ]` or `- [X]` or `- [x]` followed by task ID, labels, and description + - Extract phase information from section headers (e.g., "## Phase 1: Setup", "## Phase 2: Foundational", "## Phase 3: User Story 1") + - Count completed vs total tasks per phase + +4. **Calculate progress metrics**: + - **Total tasks**: Count all lines matching task format + - **Completed tasks**: Count lines with `- [X]` or `- [x]` + - **Remaining tasks**: Total - Completed + - **Overall percentage**: (Completed / Total) * 100 + - **Per-phase metrics**: Calculate completed/total/percentage for each phase + - **Current phase**: Identify the phase with the most recent activity (last completed task) + +5. **Generate PROGRESS.md**: + - Use `.specify/templates/progress-template.md` as structure + - Replace placeholders: + - `[FEATURE_NAME]`: Extract from tasks.md header or FEATURE_DIR name + - `[TIMESTAMP]`: Current date/time in ISO format + - `[TOTAL]`: Total task count + - `[COMPLETED]`: Completed task count + - `[REMAINING]`: Remaining task count + - `[PERCENTAGE]`: Overall progress percentage (rounded to 1 decimal) + - `[PHASE_STATUS_LIST]`: Generate list of phases with status indicators: + - ✅ Complete: Phase with 100% completion + - 🟡 In Progress: Phase with some tasks completed but not all + - ⏳ Pending: Phase with 0% completion + - Format: `- [STATUS] Phase N: [Name] ([completed]/[total] - [percentage]%)` + - `[RECENT_ACTIVITY_LIST]`: List last 5-10 completed tasks (if any) with timestamps + - Write to FEATURE_DIR/PROGRESS.md + - If file exists, update it (preserve any manual notes if present) + +6. **Generate STATUS.md** (quick summary): + - Create a concise status file with: + - Current phase name and progress + - Overall progress percentage + - Next steps (next incomplete task or phase) + - Last update timestamp + - Write to FEATURE_DIR/STATUS.md + +7. **Display summary**: + - Show progress summary in console: + - Overall: X/Y tasks completed (Z%) + - Current phase: [Phase Name] - X/Y tasks (Z%) + - Next: [Next task or phase] + - Display file paths to PROGRESS.md and STATUS.md + +## Progress Calculation Rules + +- **Task format detection**: Match lines starting with `- [ ]`, `- [X]`, or `- [x]` followed by task ID (T###) +- **Phase detection**: Match markdown headers `## Phase N:` or `## Phase N: [Name]` +- **Task assignment**: Assign tasks to the phase they appear under (until next phase header) +- **Completion status**: + - `- [ ]` = incomplete + - `- [X]` or `- [x]` = complete +- **Percentage calculation**: Round to 1 decimal place (e.g., 27.3%) + +## File Locations + +- **PROGRESS.md**: FEATURE_DIR/PROGRESS.md +- **STATUS.md**: FEATURE_DIR/STATUS.md +- **Config**: REPO_ROOT/.specify/config.json (optional) + +## Error Handling + +- If tasks.md is missing: Report error and suggest running `/speckit.tasks` first +- If no tasks found: Report "No tasks found in tasks.md" +- If config.json is malformed: Use default settings and continue +- If template is missing: Use inline template structure + +## Example Output + +**PROGRESS.md**: +```markdown +# Progress Tracking: User Authentication + +**Last Updated**: 2025-01-27T14:30:00Z +**Status**: 🟢 Active Development + +## Overall Progress +- **Total Tasks**: 95 +- **Completed**: 26 +- **Remaining**: 69 +- **Progress**: 27.4% + +## Phase Status +- ✅ Phase 1: Setup (5/5 - 100.0%) +- ✅ Phase 2: Foundational (8/8 - 100.0%) +- 🟡 Phase 3: User Story 1 (13/16 - 81.3%) +- ⏳ Phase 4: User Story 2 (0/19 - 0.0%) +``` + +**STATUS.md**: +```markdown +# Implementation Status + +**Current Phase**: Phase 3: User Story 1 +**Overall Progress**: 27.4% (26/95 tasks) +**Phase Progress**: 81.3% (13/16 tasks) + +**Next Steps**: Complete remaining 3 tasks in Phase 3 + +**Last Updated**: 2025-01-27T14:30:00Z +``` + +Note: This command can be run at any time to refresh progress tracking files, regardless of whether implementation is active. + diff --git a/templates/config.json b/templates/config.json new file mode 100644 index 000000000..7190f1b61 --- /dev/null +++ b/templates/config.json @@ -0,0 +1,9 @@ +{ + "progress": { + "autoTracking": false, + "updateOnTaskComplete": true, + "updateOnPhaseComplete": true + }, + "version": "1.0" +} + diff --git a/templates/progress-template.md b/templates/progress-template.md new file mode 100644 index 000000000..b0a9df710 --- /dev/null +++ b/templates/progress-template.md @@ -0,0 +1,24 @@ +# Progress Tracking: [FEATURE_NAME] + +**Last Updated**: [TIMESTAMP] +**Status**: 🟢 Active Development + +## Overall Progress + +- **Total Tasks**: [TOTAL] +- **Completed**: [COMPLETED] +- **Remaining**: [REMAINING] +- **Progress**: [PERCENTAGE]% + +## Phase Status + +[PHASE_STATUS_LIST] + +## Recent Activity + +[RECENT_ACTIVITY_LIST] + +--- + +*This file is automatically generated and updated. Do not edit manually.* +