diff --git a/.github/workflows/docs-update-cli-ref.yml b/.github/workflows/docs-update-cli-ref.yml new file mode 100644 index 0000000000..23ca92b92f --- /dev/null +++ b/.github/workflows/docs-update-cli-ref.yml @@ -0,0 +1,304 @@ +# Automatically updates the CLI Commands Guide when CLI commands or options +# change between releases. +# +# Triggers: Manual (for testing) or on release (production) +# Testing: Use dry_run mode to review outputs without creating PRs +# See: documentation/automation/cli-command-tracking/TESTING.md + +name: Update CLI Documentation + +on: + workflow_dispatch: # Manual trigger for testing + inputs: + old_version: + description: 'Previous version (e.g., v1.14.0). Leave empty to auto-detect.' + required: false + type: string + new_version: + description: 'New version (e.g., v1.15.0). Leave empty to use HEAD.' + required: false + type: string + dry_run: + description: 'Dry run mode - generate files but do not create PR' + required: false + type: boolean + default: true + + # Automatic triggering on releases + # Uses edited to catch when release-action updates the release with artifacts + release: + types: [edited] + +permissions: + contents: write # Create branches and commit files + pull-requests: write # Create PRs + +jobs: + update-docs: + name: Update CLI Documentation + runs-on: ubuntu-latest + + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 # Fetch all history for version comparison + fetch-tags: true # Fetch all tags so we can checkout version tags + + - name: Fetch upstream tags (for forks) + if: github.repository != 'block/goose' + run: | + # Add upstream remote and fetch tags (only needed when testing in forks) + git remote add upstream https://github.com/block/goose.git || git remote set-url upstream https://github.com/block/goose.git + git fetch upstream --tags --force + echo "✅ Fetched tags from upstream (fork mode)" + echo "Total tags available: $(git tag | wc -l)" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y jq ripgrep + + - name: Set up Rust + uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1 + with: + toolchain: stable + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.11' + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '20' + + - name: Install goose CLI + run: | + mkdir -p /home/runner/.local/bin + curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh \ + | CONFIGURE=false GOOSE_BIN_DIR=/home/runner/.local/bin bash + echo "/home/runner/.local/bin" >> $GITHUB_PATH + goose --version + + - name: Configure goose for CI + env: + GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'anthropic' }} + GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'claude-opus-4-5' }} + run: | + mkdir -p ~/.config/goose + cat < ~/.config/goose/config.yaml + GOOSE_PROVIDER: $GOOSE_PROVIDER + GOOSE_MODEL: $GOOSE_MODEL + keyring: false + EOF + + # Also export into the job environment so later steps can log the values + echo "GOOSE_PROVIDER=$GOOSE_PROVIDER" >> "$GITHUB_ENV" + echo "GOOSE_MODEL=$GOOSE_MODEL" >> "$GITHUB_ENV" + + echo "✅ Created goose config:" + cat ~/.config/goose/config.yaml + + - name: Determine versions to compare + id: versions + env: + GH_TOKEN: ${{ github.token }} + INPUT_OLD_VERSION: ${{ github.event.inputs.old_version }} + INPUT_NEW_VERSION: ${{ github.event.inputs.new_version }} + EVENT_NAME: ${{ github.event_name }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + get_previous_release() { + gh release list --limit 2 --json tagName --jq '.[].tagName' | sed -n '2p' + } + + if [ -n "$INPUT_OLD_VERSION" ]; then + OLD_VERSION="$INPUT_OLD_VERSION" + else + OLD_VERSION=$(get_previous_release) + fi + + if [ -n "$INPUT_NEW_VERSION" ]; then + NEW_VERSION="$INPUT_NEW_VERSION" + elif [ "$EVENT_NAME" = "release" ]; then + NEW_VERSION="$RELEASE_TAG" + else + NEW_VERSION="HEAD" # For testing unreleased changes + fi + + if [ -z "$OLD_VERSION" ] || [ -z "$NEW_VERSION" ]; then + echo "Error: Could not determine versions to compare" + exit 1 + fi + + echo "old_version=$OLD_VERSION" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "OLD_VERSION=$OLD_VERSION" >> $GITHUB_ENV + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + + echo "✅ Comparing $OLD_VERSION → $NEW_VERSION" + + - name: Extract and compare CLI structures + id: extract + timeout-minutes: 30 # Building goose takes time + working-directory: documentation/automation/cli-command-tracking + env: + GOOSE_REPO: ${{ github.workspace }} + run: | + set -o pipefail # Ensure pipeline failures are caught + + mkdir -p output + ./scripts/run-pipeline.sh "$OLD_VERSION" "$NEW_VERSION" 2>&1 | tee output/pipeline.log + + HAS_CHANGES=$(jq -r '.has_changes' output/cli-changes.json) + echo "has_changes=$HAS_CHANGES" >> $GITHUB_OUTPUT + + if [ "$HAS_CHANGES" = "false" ]; then + echo "✅ No changes detected" + else + echo "✅ Changes detected" + fi + + - name: Update goose-cli-commands.md (AI synthesis) + if: steps.extract.outputs.has_changes == 'true' + timeout-minutes: 10 + working-directory: documentation/automation/cli-command-tracking/output + env: + CLI_COMMANDS_PATH: ${{ github.workspace }}/documentation/docs/guides/goose-cli-commands.md + run: | + echo "🔍 Environment diagnostics:" + echo " GOOSE_PROVIDER: $GOOSE_PROVIDER" + echo " GOOSE_MODEL: $GOOSE_MODEL" + echo " ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:0:8}..." # Show first 8 chars only + echo " CLI_COMMANDS_PATH: $CLI_COMMANDS_PATH" + echo " HOME: $HOME" + echo " PATH: $PATH" + echo "" + echo "📁 Goose config file:" + cat ~/.config/goose/config.yaml || echo "Config file not found!" + echo "" + echo "📁 Current directory:" + pwd + ls -la + echo "" + echo "🤖 Applying changes to goose-cli-commands.md..." + goose run --recipe ../recipes/update-cli-commands.yaml + + - name: Upload automation outputs + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: cli-docs-update-${{ steps.versions.outputs.old_version }}-to-${{ steps.versions.outputs.new_version }} + path: | + documentation/automation/cli-command-tracking/output/*.json + documentation/automation/cli-command-tracking/output/*.md + documentation/automation/cli-command-tracking/output/*.log + retention-days: 30 + + - name: Create Pull Request + if: | + steps.extract.outputs.has_changes == 'true' && + (github.event.inputs.dry_run != 'true' || github.event_name == 'release') + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + with: + branch: docs/auto-cli-commands-${{ steps.versions.outputs.new_version }} + delete-branch: true + + commit-message: | + docs: Update CLI commands for ${{ steps.versions.outputs.new_version }} + + Automated update based on CLI changes between ${{ steps.versions.outputs.old_version }} and ${{ steps.versions.outputs.new_version }}. + + title: "docs: Update CLI Commands Guide for ${{ steps.versions.outputs.new_version }}" + body: | + ## Summary + + This PR updates the CLI Commands Guide based on command and option changes detected between **${{ steps.versions.outputs.old_version }}** and **${{ steps.versions.outputs.new_version }}**. + + ### Type of Change + - [x] Documentation + + ### AI Assistance + - [x] This PR was created or reviewed with AI assistance + + #### 🤖 Automation Details + + - **Workflow**: docs-update-cli-ref.yml + - **Triggered by**: ${{ github.event_name }} + - **Previous version**: ${{ steps.versions.outputs.old_version }} + - **New version**: ${{ steps.versions.outputs.new_version }} + + #### 📋 Changes Detected + + Review the workflow artifacts for detailed change analysis: + - cli-changes.json - Structured diff of changes + - cli-changes.md - Human-readable change documentation + - update-summary.md - Summary of documentation updates applied + + Download artifacts from the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). + + ### ✅ Review Checklist + + - [ ] Verify all CLI changes are accurately documented + - [ ] Check that examples are updated correctly + - [ ] Ensure new commands are in the correct sections + - [ ] Confirm no unintended changes were made + + ### 🔗 Related + + - Release: ${{ github.event.release.html_url || 'N/A' }} + + --- + + *This PR was automatically generated by the CLI Documentation Automation workflow.* + + labels: | + documentation + automated + cli-commands + + - name: Workflow summary + if: always() + env: + OLD_VERSION: ${{ steps.versions.outputs.old_version }} + NEW_VERSION: ${{ steps.versions.outputs.new_version }} + HAS_CHANGES: ${{ steps.extract.outputs.has_changes }} + DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} + run: | + echo "## 📊 CLI Documentation Update Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version Comparison**: $OLD_VERSION → $NEW_VERSION" >> $GITHUB_STEP_SUMMARY + echo "**Changes Detected**: $HAS_CHANGES" >> $GITHUB_STEP_SUMMARY + echo "**Dry Run Mode**: $DRY_RUN" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$HAS_CHANGES" = "true" ]; then + echo "### ✅ Documentation Updated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The CLI Commands Guide has been updated to reflect changes in $NEW_VERSION." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$DRY_RUN" = "true" ]; then + echo "**Note**: Running in dry-run mode - no PR was created. Review the artifacts to see the generated changes." >> $GITHUB_STEP_SUMMARY + else + echo "A pull request has been created with the documentation updates." >> $GITHUB_STEP_SUMMARY + fi + else + echo "### ℹ️ No Changes Needed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "No CLI command changes were detected between $OLD_VERSION and $NEW_VERSION." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📦 Artifacts" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Download the workflow artifacts to review:" >> $GITHUB_STEP_SUMMARY + echo "- Extracted CLI structures" >> $GITHUB_STEP_SUMMARY + echo "- Change detection results" >> $GITHUB_STEP_SUMMARY + echo "- Human-readable change documentation" >> $GITHUB_STEP_SUMMARY + echo "- Documentation update summary" >> $GITHUB_STEP_SUMMARY diff --git a/documentation/automation/cli-command-tracking/.gitignore b/documentation/automation/cli-command-tracking/.gitignore new file mode 100644 index 0000000000..a041318648 --- /dev/null +++ b/documentation/automation/cli-command-tracking/.gitignore @@ -0,0 +1,28 @@ +# Output directory - generated files +output/ + +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/documentation/automation/cli-command-tracking/README.md b/documentation/automation/cli-command-tracking/README.md new file mode 100644 index 0000000000..1e706bf446 --- /dev/null +++ b/documentation/automation/cli-command-tracking/README.md @@ -0,0 +1,336 @@ +# CLI Command Tracking + +Automated pipeline for detecting and documenting CLI command changes between goose releases. + +## Overview + +This automation keeps the [CLI Commands Guide](https://block.github.io/goose/docs/guides/goose-cli-commands) synchronized with code changes by: + +1. **Extracting** CLI structure from goose binary using `--help` output (deterministic) +2. **Detecting** changes between versions (deterministic diff) +3. **Synthesizing** human-readable change documentation (AI-powered) +4. **Updating** the CLI Commands Guide (AI-powered) + +The automation runs automatically on new releases via GitHub Actions, or can be run manually for testing. + +## Quick Start + +### Automated (GitHub Actions) + +The automation runs automatically when a new release is published. See [TESTING.md](./TESTING.md) for testing instructions. + +### Manual (Local Testing) + +```bash +# Set the goose repository path +export GOOSE_REPO=/path/to/goose + +# Run the complete pipeline with auto-detected versions +./scripts/run-pipeline.sh + +# Or specify versions explicitly +./scripts/run-pipeline.sh v1.17.0 v1.19.0 + +# Or run individual steps: +# 1. Extract CLI structures +./scripts/extract-cli-structure.sh v1.17.0 > output/old-cli-structure.json +./scripts/extract-cli-structure.sh v1.19.0 > output/new-cli-structure.json + +# 2. Detect changes +python3 scripts/diff-cli-structures.py output/old-cli-structure.json \ + output/new-cli-structure.json \ + > output/cli-changes.json + +# 3. Generate human-readable change documentation +cd output && goose run --recipe ../recipes/synthesize-cli-changes.yaml + +# 4. Update goose-cli-commands.md +cd output && goose run --recipe ../recipes/update-cli-commands.yaml +``` + +### Version Detection + +The pipeline automatically detects versions when not specified: +- **Old version**: Second-most-recent release tag (via `gh release list`) +- **New version**: Most recent release tag, or `RELEASE_TAG` env var (for CI) +- **Fallback**: Uses git tags if `gh` CLI not available + +To test unreleased changes, explicitly pass `HEAD`: +```bash +./scripts/run-pipeline.sh v1.19.0 HEAD +``` + +## Architecture + +### Modular Pipeline Design + +The automation uses a **hybrid approach**: deterministic scripts for data extraction/diffing, AI recipes for analysis and documentation updates. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ EXTRACTION (Deterministic) │ +├─────────────────────────────────────────────────────────────────┤ +│ extract-cli-structure.sh → extract-cli-structure.py │ +│ ↓ │ +│ cli-structure.json (commands, options, subcommands, aliases) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ DIFFING (Deterministic) │ +├─────────────────────────────────────────────────────────────────┤ +│ diff-cli-structures.py │ +│ ↓ │ +│ cli-changes.json (added, removed, modified commands/options) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ SYNTHESIS (AI-Powered) │ +├─────────────────────────────────────────────────────────────────┤ +│ synthesize-cli-changes.yaml │ +│ ↓ │ +│ cli-changes.md (human-readable) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ UPDATE (AI-Powered) │ +├─────────────────────────────────────────────────────────────────┤ +│ update-cli-commands.yaml │ +│ ↓ │ +│ goose-cli-commands.md (updated) + update-summary.md │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Why This Design? + +**Scripts handle deterministic tasks:** +- Building goose from specific git tags +- Running `--help` commands and parsing output +- JSON structure comparison +- No interpretation or inference - direct extraction + +**AI recipes handle synthesis and updates:** +- Analyzing changes and explaining implications +- Generating migration guidance and examples +- Updating documentation with proper formatting and context + +**Benefits:** +- **Reliability**: Extraction is deterministic and reproducible +- **Testability**: Each stage has clear inputs/outputs +- **Maintainability**: Easy to update individual components +- **Transparency**: Intermediate files can be inspected + +### Data Flow + +All stages communicate via JSON/Markdown files in the `output/` directory: + +| File | Producer | Consumer | Purpose | +|------|----------|----------|---------| +| `old-cli-structure.json` | `extract-cli-structure.sh` | `diff-cli-structures.py` | Previous version CLI structure | +| `new-cli-structure.json` | `extract-cli-structure.sh` | `diff-cli-structures.py` | Current version CLI structure | +| `cli-changes.json` | `diff-cli-structures.py` | `synthesize-cli-changes.yaml` | Detected changes (structured) | +| `cli-changes.md` | `synthesize-cli-changes.yaml` | `update-cli-commands.yaml` | Human-readable change documentation | +| `update-summary.md` | `update-cli-commands.yaml` | Human review | Summary of documentation updates | + +## Configuration + +### Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `GOOSE_REPO` | Yes (local) | - | Path to goose repository root | +| `CLI_COMMANDS_PATH` | No | `$GOOSE_REPO/documentation/docs/guides/goose-cli-commands.md` | Full path to target doc file | +| `RELEASE_TAG` | No | - | Used by GitHub Actions to specify the new version | + +**Example:** +```bash +export GOOSE_REPO=/Users/you/goose +# CLI_COMMANDS_PATH is auto-constructed from GOOSE_REPO +``` + +### Skipped Commands + +Some commands are intentionally excluded from extraction and documentation tracking. These are configured in `config/skip-commands.json`: + +```json +{ + "description": "Commands to skip during extraction (not documented intentionally)", + "skip_commands": [ + { + "name": "term", + "reason": "Terminal integration documented via @goose/@g aliases" + } + ] +} +``` + +To add or remove skipped commands, edit the config file - no code changes required. + +## Scripts + +### `extract-cli-structure.sh` + +Builds goose from a specific git tag and extracts CLI structure using `--help` output. + +**Usage:** +```bash +./scripts/extract-cli-structure.sh [version] > output/cli-structure.json +``` + +**Arguments:** +- `version` (optional): Git tag or commit to extract from (default: HEAD) + +**Output:** JSON with complete command tree including options, subcommands, aliases + +**Example:** +```bash +# Extract from current code +./scripts/extract-cli-structure.sh HEAD > output/new-cli-structure.json + +# Extract from specific version +./scripts/extract-cli-structure.sh v1.15.0 > output/old-cli-structure.json +``` + +### `diff-cli-structures.py` + +Compares two CLI structure files and outputs detected changes. + +**Usage:** +```bash +python3 scripts/diff-cli-structures.py > output/cli-changes.json +``` + +**Arguments:** +- `old-file`: Path to old CLI structure JSON +- `new-file`: Path to new CLI structure JSON + +**Output:** JSON with categorized changes: +- `commands.added`: New commands +- `commands.removed`: Deleted commands +- `commands.modified`: Changed commands (options, description, aliases) +- `breaking_changes`: Categorized breaking changes + +**Example:** +```bash +python3 scripts/diff-cli-structures.py \ + output/old-cli-structure.json \ + output/new-cli-structure.json \ + > output/cli-changes.json +``` + +## Recipes + +### `synthesize-cli-changes.yaml` + +Analyzes detected changes and generates human-readable documentation. + +**Inputs:** +- `output/cli-changes.json` - Detected changes from diff script +- `output/old-cli-structure.json` - Previous version structure +- `output/new-cli-structure.json` - Current version structure + +**Output:** +- `output/cli-changes.md` - Human-readable change documentation with: + - Breaking changes with migration guidance + - New commands with usage examples + - Modified commands with details + - Non-breaking changes summary + +**Usage:** +```bash +cd output +goose run --recipe ../recipes/synthesize-cli-changes.yaml +``` + +### `update-cli-commands.yaml` + +Updates the CLI Commands Guide based on synthesized changes. + +**Inputs:** +- `output/cli-changes.md` - Change documentation from synthesis recipe +- `goose-cli-commands.md` - Target documentation file (path from `CLI_COMMANDS_PATH` or `GOOSE_REPO` env var) + +**Outputs:** +- Updated `goose-cli-commands.md` with changes applied +- `output/update-summary.md` - Summary of changes for review + +**Usage:** +```bash +export CLI_COMMANDS_PATH=/path/to/goose-cli-commands.md +cd output +goose run --recipe ../recipes/update-cli-commands.yaml +``` + +## Directory Structure + +``` +cli-command-tracking/ +├── README.md # This file +├── TESTING.md # Testing guide for GitHub Actions workflow +├── .gitignore # Excludes output/ directory +├── config/ # Configuration files +│ └── skip-commands.json # Commands to exclude from tracking +├── scripts/ # Extraction and diff scripts +│ ├── extract-cli-structure.sh # Wrapper that builds goose and runs Python +│ ├── extract-cli-structure.py # Python script to parse --help output +│ ├── diff-cli-structures.py # Compare structures and detect changes +│ └── run-pipeline.sh # End-to-end pipeline runner +├── recipes/ # AI recipes +│ ├── synthesize-cli-changes.yaml # Generate change docs +│ └── update-cli-commands.yaml # Update documentation +├── .github/workflows/ # GitHub Actions workflow +│ └── docs-update-cli-ref.yml # Workflow definition +└── output/ # Generated files (gitignored) + ├── old-cli-structure.json # Previous version structure + ├── new-cli-structure.json # Current version structure + ├── cli-changes.json # Detected changes (structured) + ├── cli-changes.md # Change documentation (human-readable) + ├── update-summary.md # Documentation update summary + └── pipeline.log # Pipeline execution log +``` + +## GitHub Actions Workflow + +The automation runs via `.github/workflows/docs-update-cli-ref.yml`: + +- **Trigger**: Automatically on new releases, or manually for testing +- **Process**: Builds goose for both versions, extracts CLI structures, detects changes, updates documentation +- **Output**: Creates a PR with updated `goose-cli-commands.md` if changes detected +- **Testing**: See [TESTING.md](./TESTING.md) for detailed testing instructions + +## What Gets Tracked + +### Commands +- ✅ Commands added/removed +- ✅ Command descriptions changed +- ✅ Command aliases added/removed +- ✅ Subcommands added/removed + +### Options +- ✅ Options added/removed +- ✅ Option help text changed +- ✅ Default values changed +- ✅ Possible values changed (enums) +- ✅ Short/long flags changed + +### Breaking Changes (Auto-Categorized) +- Command removed (high severity) +- Option removed (high severity) +- Option renamed (high severity) +- Default value changed (medium severity) +- Enum values removed (high severity) +- Alias removed (medium severity) + +## Maintenance + +When modifying the automation: + +1. **Test locally first**: Run `./scripts/run-pipeline.sh` with test versions +2. **Verify outputs**: Check generated files against actual CLI changes +3. **Test in fork**: Use GitHub Actions workflow with dry-run mode +4. **Document changes**: Update this README with design decisions + +## Related Documentation + +- [TESTING.md](./TESTING.md) - How to test the GitHub Actions workflow +- [Automation Overview](../README.md) - All automation projects +- [CLI Commands Guide](../../docs/guides/goose-cli-commands.md) - Target documentation diff --git a/documentation/automation/cli-command-tracking/TESTING.md b/documentation/automation/cli-command-tracking/TESTING.md new file mode 100644 index 0000000000..d3df81957c --- /dev/null +++ b/documentation/automation/cli-command-tracking/TESTING.md @@ -0,0 +1,382 @@ +# Testing Guide + +This guide explains how to test the CLI command tracking automation locally and in GitHub Actions. + +## Prerequisites + +- Python 3.7+ +- Rust toolchain (for building goose) +- jq (for JSON processing) +- goose CLI installed (for running recipes) +- Git with access to goose repository + +## Local Testing + +### Step 1: Set Up Environment + +```bash +cd /path/to/cli-command-tracking + +# Set the goose repository path +export GOOSE_REPO=/path/to/goose + +# Create output directory +mkdir -p output +``` + +### Step 2: Test Extraction Script + +Test the extraction with a specific version: + +```bash +# Test with a release version +./scripts/extract-cli-structure.sh v1.19.0 > output/test-extraction.json + +# Verify output +jq '.version, .commands | length' output/test-extraction.json + +# Inspect a specific command +jq '.commands[] | select(.name == "session")' output/test-extraction.json + +# Verify skipped commands are excluded +jq '.commands[].name' output/test-extraction.json | grep -v term +``` + +**Expected output:** +- Valid JSON structure +- Version number extracted correctly +- All commands captured (14+ commands, excluding skipped ones like `term`) +- Subcommands nested properly +- Options parsed with all fields + +**Common issues:** +- Rust not installed: Install via rustup +- Build fails: Check Cargo.toml dependencies +- Timeout errors: Increase timeout in script if needed +- Keychain prompt: See "Keychain Access" section below + +### Step 3: Test Diff Script + +Compare two CLI structures: + +```bash +# Extract from two versions +./scripts/extract-cli-structure.sh v1.14.0 > output/old-cli-structure.json +./scripts/extract-cli-structure.sh v1.15.0 > output/new-cli-structure.json + +# Run diff +python3 scripts/diff-cli-structures.py \ + output/old-cli-structure.json \ + output/new-cli-structure.json \ + > output/cli-changes.json + +# Check results +jq '.has_changes, .summary' output/cli-changes.json + +# View specific changes +jq '.changes.commands.added' output/cli-changes.json +jq '.changes.commands.modified[0]' output/cli-changes.json +jq '.breaking_changes' output/cli-changes.json +``` + +**Expected output:** +- `has_changes: true` if versions differ +- Summary with counts of changes +- Detailed changes in structured format +- Breaking changes categorized + +### Step 4: Test AI Synthesis Recipe + +Generate human-readable documentation: + +```bash +cd output + +# Run synthesis recipe +goose run --recipe ../recipes/synthesize-cli-changes.yaml + +# Check output +ls -lh cli-changes.md +head -50 cli-changes.md +``` + +**Expected output:** +- `cli-changes.md` file created +- Markdown formatted properly +- Breaking changes listed first +- Examples provided for complex changes +- When testing AI workflows, ensure any content sent via the `store_comment` tool does not contain triple-backtick code fences (```), even though regular backticks in markdown files like `cli-changes.md` are allowed. + +### Step 5: Test Documentation Update Recipe + +Update the actual documentation: + +```bash +cd output + +# Set path to documentation file +export CLI_COMMANDS_PATH=/path/to/goose/documentation/docs/guides/goose-cli-commands.md + +# Run update recipe +goose run --recipe ../recipes/update-cli-commands.yaml + +# Check outputs +ls -lh update-summary.md +cat update-summary.md + +# Verify documentation was updated +git diff $CLI_COMMANDS_PATH +``` + +### Step 6: Test Full Pipeline + +Run the complete end-to-end pipeline: + +```bash +cd /path/to/cli-command-tracking + +# Set documentation path (optional - only needed for update step) +export CLI_COMMANDS_PATH=/path/to/goose/documentation/docs/guides/goose-cli-commands.md + +# Run pipeline +./scripts/run-pipeline.sh v1.14.0 v1.15.0 + +# Check all outputs +ls -lh output/ +``` + +**Expected output:** +- All intermediate files created +- Pipeline completes without errors +- Summary shows changes detected +- `cli-changes.md` generated + +## GitHub Actions Testing + +### Test in Fork + +1. **Fork the repository** (if not already done) + +2. **Copy automation files** to your fork: + ```bash + cp -r /path/to/cli-command-tracking \ + /path/to/forked-goose/documentation/automation/ + + cp /path/to/goose/.github/workflows/docs-update-cli-ref.yml \ + /path/to/forked-goose/.github/workflows/ + ``` + +3. **Set up secrets** in your fork: + - Go to Settings → Secrets and variables → Actions + - Add `ANTHROPIC_API_KEY` secret + +4. **Set up variables** (optional): + - Add `GOOSE_PROVIDER` variable (default: anthropic) + - Add `GOOSE_MODEL` variable (default: claude-opus-4-5) + +5. **Trigger workflow manually**: + - Go to Actions → "Update CLI Documentation" + - Click "Run workflow" + - Set `dry_run: true` for testing + - Optionally specify versions to compare + +### Dry Run Mode + +Test without creating PR: + +1. Trigger workflow with `dry_run: true` +2. Review outputs in workflow logs +3. Download artifacts to inspect generated files +4. Validate changes are correct + +### Workflow Inputs + +| Input | Description | Default | +|-------|-------------|---------| +| `old_version` | Previous version tag | Auto-detect from releases | +| `new_version` | New version tag | HEAD | +| `dry_run` | Generate files but don't create PR | true | + +### Reviewing Artifacts + +After workflow runs: + +1. Go to the workflow run page +2. Download the artifacts ZIP +3. Extract and review: + - `old-cli-structure.json` - Previous CLI structure + - `new-cli-structure.json` - New CLI structure + - `cli-changes.json` - Detected changes + - `cli-changes.md` - Human-readable documentation + - `pipeline.log` - Execution log + +## Testing with Known Changes + +To validate the automation works correctly, test with versions that have known CLI changes. + +### Finding Test Versions + +```bash +cd /path/to/goose + +# Check git history for CLI changes +git log --oneline --all -- crates/goose-cli/src/cli.rs | head -20 + +# Look for commits that added/removed/modified commands +git show :crates/goose-cli/src/cli.rs | grep "enum Command" -A 30 +``` + +### Test Case: New Command Added + +If you know a version added a new command: + +```bash +./scripts/run-pipeline.sh v1.13.0 v1.14.0 +jq '.changes.commands.added' output/cli-changes.json +``` + +### Test Case: Option Modified + +If you know a version modified options: + +```bash +./scripts/run-pipeline.sh v1.14.0 v1.15.0 +jq '.changes.commands.modified' output/cli-changes.json +``` + +### Test Case: No Changes + +Test with same version (should show no changes): + +```bash +./scripts/run-pipeline.sh v1.14.0 v1.14.0 +jq '.has_changes' output/cli-changes.json +# Should output: false +``` + +## Validation Checklist + +Before considering the automation complete: + +### Extraction Script +- [ ] Handles all command types (simple, with subcommands, with aliases) +- [ ] Parses all option types (short, long, with values, flags) +- [ ] Captures defaults and possible values +- [ ] Works with commands that have no description +- [ ] Handles nested subcommands (2+ levels) +- [ ] Builds goose from git tags correctly + +### Diff Script +- [ ] Detects added commands +- [ ] Detects removed commands +- [ ] Detects modified options +- [ ] Detects changed help text +- [ ] Detects changed defaults +- [ ] Detects changed possible values +- [ ] Categorizes breaking changes correctly + +### AI Recipes +- [ ] Generates readable documentation +- [ ] Provides migration guidance +- [ ] Uses correct markdown formatting +- [ ] Avoids backticks (security constraint) +- [ ] Includes relevant examples +- [ ] Uses text_editor tool to write files + +### Pipeline +- [ ] Runs end-to-end without errors +- [ ] Handles "no changes" case +- [ ] Creates all expected output files +- [ ] Filters goose session output correctly + +### GitHub Actions +- [ ] Workflow triggers correctly +- [ ] Builds goose for both versions +- [ ] Uploads artifacts +- [ ] Creates PR when changes detected +- [ ] Respects dry_run mode +- [ ] Works in forks (fetches upstream tags) + +## Troubleshooting + +### Keychain Access (macOS) + +On macOS, running `goose --help` or `goose --version` may prompt for keychain access. This happens because goose tries to access stored credentials on startup. + +**Local workaround:** Allow the keychain access when prompted. + +**CI consideration:** GitHub Actions runners don't have a keychain, so this may need to be handled. Check existing goose workflows for patterns - there may be a `keyring: false` config option or environment variable to disable credential loading. + +**TODO:** Investigate if this blocks CI execution and document the solution. + +### Build fails for old version + +Some old versions may have different dependencies: + +```bash +# Check if version exists +git tag | grep v1.14.0 + +# Try building manually +git worktree add /tmp/goose-test v1.14.0 +cd /tmp/goose-test +cargo build --release +``` + +### Extraction timeout + +Increase timeout in `extract-cli-structure.py`: + +```python +result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) # Increase from 10 +``` + +### Diff shows unexpected changes + +Check if help text formatting changed: + +```bash +# Compare raw help output +./old-goose session --help > old-help.txt +./new-goose session --help > new-help.txt +diff old-help.txt new-help.txt +``` + +### AI recipe fails + +Check input files exist and are valid: + +```bash +ls -lh output/cli-changes.json output/old-cli-structure.json output/new-cli-structure.json +jq empty output/cli-changes.json # Validates JSON +``` + +### Workflow fails in fork + +Ensure: +- `ANTHROPIC_API_KEY` secret is set +- Upstream tags are fetched (workflow does this automatically) +- Rust toolchain is available + +## Manual Verification + +After automation runs, manually verify: + +1. **Accuracy**: Do detected changes match actual CLI changes? +2. **Completeness**: Are all changes captured? +3. **Documentation**: Is the updated documentation accurate and clear? +4. **Examples**: Do all examples still work? +5. **Style**: Is formatting consistent with existing docs? + +## Test Data + +Keep test data for regression testing: + +```bash +# Save known-good outputs +mkdir -p test-data +cp output/cli-changes.json test-data/v1.14.0-to-v1.15.0-changes.json +cp output/cli-changes.md test-data/v1.14.0-to-v1.15.0-changes.md +``` + +Use these to verify future changes don't break existing functionality. diff --git a/documentation/automation/cli-command-tracking/config/skip-commands.json b/documentation/automation/cli-command-tracking/config/skip-commands.json new file mode 100644 index 0000000000..6b8581b8c8 --- /dev/null +++ b/documentation/automation/cli-command-tracking/config/skip-commands.json @@ -0,0 +1,9 @@ +{ + "description": "Commands to skip during extraction (not documented intentionally)", + "skip_commands": [ + { + "name": "term", + "reason": "Terminal integration documented via @goose/@g aliases" + } + ] +} diff --git a/documentation/automation/cli-command-tracking/recipes/synthesize-cli-changes.yaml b/documentation/automation/cli-command-tracking/recipes/synthesize-cli-changes.yaml new file mode 100644 index 0000000000..41e8c77361 --- /dev/null +++ b/documentation/automation/cli-command-tracking/recipes/synthesize-cli-changes.yaml @@ -0,0 +1,208 @@ +version: "2" +title: "Synthesize CLI Changes" +description: "Generate human-readable documentation for CLI command changes between two versions" + +extensions: + - type: builtin + name: developer + +instructions: | + You are a technical documentation specialist creating release notes for CLI command changes. + + ## Your Task + + Analyze the CLI changes between two goose versions and generate clear, user-focused + documentation explaining what changed and why it matters. + + ## Input Files + + You have access to THREE data sources: + + 1. **cli-changes.json** - The diff (what changed): + - commands: added, removed, modified + - breaking_changes: categorized breaking changes + - summary: high-level statistics + + 2. **old-cli-structure.json** - Before state (for context): + - Complete command structure from old version + + 3. **new-cli-structure.json** - After state (for context): + - Complete command structure from new version + + ## Output Format + + Create a Markdown file (cli-changes.md) with this structure: + + # CLI Command Changes + + **From**: {old_version} + **To**: {new_version} + **Analyzed**: {timestamp} + + ## Summary + + Brief overview of changes (2-3 sentences). + + - Commands added: X + - Commands removed: X + - Commands modified: X + - Breaking changes: X + + ## Breaking Changes + + ⚠️ **Important**: These changes may break existing scripts or workflows. + + List all breaking changes with migration guidance: + + ### Command Removed: **command-name** + + - **Impact**: Users can no longer use this command + - **Migration**: Use **alternative-command** instead + - **Example**: + + # Old way + goose old-command --option value + + # New way + goose new-command --option value + + ### Option Removed: **--option-name** from **command** + + - **Impact**: Scripts using this option will fail + - **Migration**: Use **--new-option** instead or adjust workflow + + ### Default Value Changed: **--option** in **command** + + - **Old default**: value1 + - **New default**: value2 + - **Impact**: Behavior changes when option is omitted + - **Migration**: Explicitly specify the value if you need the old behavior + + ## New Commands + + Commands added in this release: + + ### **command-name** + + - **Description**: What this command does + - **Usage**: goose command-name [OPTIONS] + - **Purpose**: Why this command was added + - **Key Options**: + - **--option1**: Description + - **--option2**: Description + - **Example**: + + goose command-name --option1 value + + ## Removed Commands + + Commands removed in this release: + + ### **command-name** + + - **Reason**: Why it was removed (infer from context) + - **Alternative**: What to use instead + - **Migration**: How to update existing usage + + ## Modified Commands + + Commands with changes in this release: + + ### **command-name** + + **Changes**: + + #### Description Updated + + - **Old**: Previous description + - **New**: New description + - **Impact**: Clarifies command purpose + + #### New Options + + - **--new-option** VALUE: Description and purpose + + #### Removed Options + + - **--old-option**: Was used for X, now use **--new-option** instead + + #### Modified Options + + - **--option-name**: + - Help text updated for clarity + - Default changed from X to Y + - Possible values expanded: added Z + + #### Aliases Changed + + - Added alias: **x** + - Removed alias: **y** + + ## Non-Breaking Changes + + Changes that don't break existing usage: + + - New optional options added + - Help text clarifications + - New aliases added + - Expanded enum values (new possible values) + + ## Analysis Guidelines + + 1. **Focus on User Impact**: Explain changes from user perspective, not implementation details + + 2. **Prioritize Breaking Changes**: These go first and need clear migration guidance + + 3. **Provide Examples**: Show before/after for breaking changes + + 4. **Infer Intent**: Use command names, descriptions, and option names to understand why changes were made + + 5. **Be Specific**: Include concrete details about what changed + + 6. **Group Related Changes**: If multiple options changed in one command, group them together + + 7. **Explain Implications**: Don't just list changes, explain what they mean for users + + 8. **Suggest Alternatives**: For removed features, suggest what to use instead + + 9. **Skip Trivial Changes**: Don't document minor help text formatting changes + + 10. **Use Context**: Reference old and new structures to understand relationships + + ## Special Cases + + - **Empty changes arrays**: If a category has no changes, skip that section entirely + + - **Commands with no description**: Some commands (like **diagnostics**) have empty about fields - this is normal + + - **Alias changes**: Removing aliases might break user muscle memory, note this in breaking changes + + - **Default value changes**: These can be subtle breaking changes if users rely on defaults + + - **Enum expansions**: Adding new possible values is non-breaking, but removing them is breaking + + ## File Locations + + - Input 1: ./cli-changes.json (the diff) + - Input 2: ./old-cli-structure.json (before state) + - Input 3: ./new-cli-structure.json (after state) + - Output: ./cli-changes.md + + Start by reading all THREE input files, then generate the CLI changes documentation. + +prompt: | + Please analyze the CLI changes and generate comprehensive release notes. + + Steps: + 1. Read all THREE input files (cli-changes.json, old-cli-structure.json, new-cli-structure.json) + 2. Analyze the changes + 3. Write the documentation to ./cli-changes.md using the text_editor tool + + Focus on: + - User impact (how does this affect CLI users and scripts?) + - Breaking vs non-breaking changes + - Migration guidance for breaking changes + - Clear, actionable documentation + - Examples for complex changes + + IMPORTANT: You MUST use the text_editor tool to write the output to ./cli-changes.md diff --git a/documentation/automation/cli-command-tracking/recipes/update-cli-commands.yaml b/documentation/automation/cli-command-tracking/recipes/update-cli-commands.yaml new file mode 100644 index 0000000000..045523845f --- /dev/null +++ b/documentation/automation/cli-command-tracking/recipes/update-cli-commands.yaml @@ -0,0 +1,223 @@ +version: "2" +title: "Update CLI Commands Documentation" +description: "Apply CLI changes to goose-cli-commands.md based on cli-changes.md" + +extensions: + - type: builtin + name: developer + +instructions: | + You are a technical documentation specialist making targeted updates to the CLI Commands Guide to reflect the CURRENT state of the CLI. + + ## ⚠️ GOAL: Document Current State, NOT Change History + + Update docs to show what the CLI looks like NOW. Do NOT document what changed, was removed, or is deprecated. + + Examples: + - Option removed? Delete it from the docs (don't mention it was removed) + - Option added? Add it to the docs (don't mention it's new) + - Option modified? Update to current state (don't mention what it used to be) + + ## 🚨 ABSOLUTE PROHIBITIONS + + 1. **ONLY delete entire command sections** (like `#### bench`, `#### run`) when cli-changes.md explicitly states "Command X was removed" + 2. **NEVER change section headings** (like `### Task Execution`, `### Session Management`) + 3. **NEVER rename options** unless cli-changes.md explicitly documents the rename (e.g., don't change `--with-streamable-http-extension` to something else) + 4. **NEVER duplicate sections** - if a section exists, update it in place + 5. **NEVER remove horizontal rules** (`---`) between sections + 6. **NEVER rewrite examples** - only update the specific flag/option that changed + + ## ⚠️ CRITICAL RULES + + 1. **Only change what's in cli-changes.md** - If not mentioned, don't touch it + 2. **Preserve structure and content** - Keep all existing headings, section organization, formatting, and content intact + 3. **Make surgical edits** - Smallest possible change to achieve the goal + 4. **Use str_replace with old_str/new_str** (NOT diff format) - Include enough context for unique matching, verify old_str matches EXACTLY + 5. Do not copy or reference "Breaking changes" or "Migration guidance" headings from cli-changes.md. Use them only to determine current-state edits + + ## Your Task + + You will update the CLI Commands documentation based on changes in cli-changes.md: + 1. **Command sections** - Add/remove/update command documentation + 2. **Option lists** - Add/remove/update options within commands + 3. **Examples** - Update examples if behavior changed + 4. **Maintain Consistency** - Match existing documentation style + + ## Input Files + + 1. **cli-changes.md** - The change documentation from the pipeline: + - Located at: ./cli-changes.md + - Contains: Command changes, option changes, breaking changes, migration guidance + + 2. **goose-cli-commands.md** - The target documentation file: + - Located at: ${CLI_COMMANDS_PATH} (environment variable) + - Default: ${GOOSE_REPO}/documentation/docs/guides/goose-cli-commands.md + - Contains: Complete CLI reference documentation + + 3. **update-summary.md** - Output file for change summary: + - Located at: ./update-summary.md + - You will create this file to document what was updated + + ## Target Sections in goose-cli-commands.md + + The CLI Commands Guide has this structure: + - Flag Naming Conventions + - Core Commands (configure, info, version, update) + - Session Management (session and subcommands) + - Task Execution (run, bench, recipe, schedule, mcp, acp) + - Project Management (project, projects) + - Interface (web) + - Interactive Session Features (slash commands, themes, etc.) + + **Keep this structure intact** - only update content within sections. + + ## Update Strategy + + ### 1. Read and Analyze + + Read cli-changes.md completely and identify ALL changes documented. + + **CRITICAL**: Only make changes that are explicitly documented in cli-changes.md. + + ### 2. Apply Updates + + For each change documented in cli-changes.md: + + | Change Type | How to Update | + |-------------|---------------| + | **Command added** | Add new command section in appropriate category. Follow existing format with description, options, usage, examples. | + | **Command removed** | Remove entire command section. Check for references elsewhere. | + | **Command description changed** | Update the description text under the command header. | + | **Option added** | Add to the Options list for that command. Include short flag, long flag, description. | + | **Option removed** | Remove from the Options list. | + | **Option modified** | Update the option description, default value, or possible values. | + | **Alias added** | Update command header to show alias (e.g., "**Alias**: x"). | + | **Alias removed** | Remove alias from command header. | + | **Default changed** | Update the default value in the option description. | + | **Possible values changed** | Update the list of valid values. | + + ### 3. Documentation Style + + Follow the existing style in goose-cli-commands.md: + + - **Command headers**: Use #### for command names + - **Options**: Use bullet lists with bold option names + - **Usage blocks**: Use fenced code blocks with language identifier + - **Examples**: Use fenced code blocks with comments + - **Notes**: Use :::info, :::warning, :::tip, :::caution admonitions + + Example format: + + #### command-name + + Brief description of what the command does. + + **Options:** + - **-f, --format FORMAT**: Output format (text, json). Default: text + - **--verbose**: Enable verbose output + + **Usage:** + + # Basic usage + goose command-name + + # With options + goose command-name --format json --verbose + + ### 4. Verification + + After making updates: + - Verify all changes from cli-changes.md are reflected + - Check that no unintended changes were made + - Ensure examples are still valid + - Confirm formatting is consistent + + ## Output Requirements + + ### 1. Updated goose-cli-commands.md + + Apply all changes from cli-changes.md: + - Add new commands in appropriate sections + - Remove deleted commands + - Update modified commands/options + - Preserve all other content + + ### 2. Create update-summary.md + + Generate a summary document: + + # CLI Documentation Update Summary + + **Date**: {current_date} + **Source**: cli-changes.md ({old_version} → {new_version}) + **Target**: goose-cli-commands.md + + ## Changes Applied + + ### Commands Added + - **command-name**: Added to Section Name + + ### Commands Removed + - **command-name**: Removed from Section Name + + ### Commands Modified + - **command-name**: Updated description, added options X and Y + + ## Sections Updated + + - Core Commands: Added X + - Session Management: Updated options for Y + - Task Execution: Modified examples for Z + + ## Verification Checklist + + - [ ] All new commands documented + - [ ] All removed commands deleted + - [ ] All option changes reflected + - [ ] Examples updated and valid + - [ ] No broken references + - [ ] Style consistency maintained + - [ ] No changes outside of cli-changes.md + + ## Notes + + Any special considerations or decisions made during the update. + + ## File Locations Summary + + - Input 1: ./cli-changes.md (change documentation) + - Input 2: ${CLI_COMMANDS_PATH} or ${GOOSE_REPO}/documentation/docs/guides/goose-cli-commands.md + - Output 1: Same as Input 2 (updated in place) + - Output 2: ./update-summary.md (change summary) + + ## Environment Variables + + - CLI_COMMANDS_PATH: Full path to goose-cli-commands.md file (overrides default) + - GOOSE_REPO: Path to goose repository (used if CLI_COMMANDS_PATH not set) + + Start by reading both input files, then apply the updates and generate the summary. + +prompt: | + Update the CLI Commands Guide based on cli-changes.md. + + IMPORTANT: You MUST use the text_editor tool to: + 1. Read cli-changes.md and goose-cli-commands.md + 2. Update goose-cli-commands.md with str_replace + 3. Write update-summary.md + + Remember: + - Document CURRENT state only (not change history) + - Make SURGICAL edits (smallest change needed) + - Only change what's explicitly in cli-changes.md + - Preserve all structure, headings, and content not mentioned in cli-changes.md + - Use EXACT file path from CLI_COMMANDS_PATH environment variable + + Do NOT: + - Rewrite existing descriptions or reorganize sections + - Make "improvements" to content not in cli-changes.md + + Before finalizing, verify: + 1. Did I only change what's in cli-changes.md? + 2. Are all section headings (###, ####) unchanged? + 3. Did I use str_replace with exact matching? + 4. Did I avoid duplicating sections? diff --git a/documentation/automation/cli-command-tracking/scripts/diff-cli-structures.py b/documentation/automation/cli-command-tracking/scripts/diff-cli-structures.py new file mode 100755 index 0000000000..24532bb0a7 --- /dev/null +++ b/documentation/automation/cli-command-tracking/scripts/diff-cli-structures.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +""" +Compare two CLI structure JSON files and output detected changes. + +Usage: + ./diff-cli-structures.py > output/cli-changes.json + +Example: + ./diff-cli-structures.py output/cli-structure-v1.14.0.json \ + output/cli-structure-v1.15.0.json \ + > output/cli-changes.json +""" + +import sys +import json +from datetime import datetime, timezone +from typing import Dict, List + + +def get_command_path(command: Dict, parent_path: str = "") -> str: + """Get the full path of a command (e.g., 'session list').""" + if parent_path: + return f"{parent_path} {command['name']}" + return command['name'] + + +def flatten_commands(commands: List[Dict], parent_path: str = "") -> Dict[str, Dict]: + """ + Flatten nested command structure into a dict keyed by full command path. + + Returns: + Dict mapping command path to command data + """ + result = {} + + for cmd in commands: + cmd_path = get_command_path(cmd, parent_path) + # Store command without subcommands to avoid recursion in comparisons + cmd_copy = cmd.copy() + subcommands = cmd_copy.pop('subcommands', []) + result[cmd_path] = cmd_copy + + # Recursively flatten subcommands + if subcommands: + result.update(flatten_commands(subcommands, cmd_path)) + + return result + + +def compare_options(old_opts: List[Dict], new_opts: List[Dict]) -> Dict: + """ + Compare two lists of options and detect changes. + + Returns dict with: added, removed, modified + """ + # Create dicts keyed by long flag (or short if no long) + old_opts_dict = {opt.get('long') or opt.get('short'): opt for opt in old_opts} + new_opts_dict = {opt.get('long') or opt.get('short'): opt for opt in new_opts} + + old_keys = set(old_opts_dict.keys()) + new_keys = set(new_opts_dict.keys()) + + added = [] + removed = [] + modified = [] + + # Find added options + for key in new_keys - old_keys: + added.append(new_opts_dict[key]) + + # Find removed options + for key in old_keys - new_keys: + removed.append(old_opts_dict[key]) + + # Find modified options + for key in old_keys & new_keys: + old_opt = old_opts_dict[key] + new_opt = new_opts_dict[key] + + changes = {} + + # Check each field for changes + if old_opt.get('short') != new_opt.get('short'): + changes['short'] = {'old': old_opt.get('short'), 'new': new_opt.get('short')} + + if old_opt.get('long') != new_opt.get('long'): + changes['long'] = {'old': old_opt.get('long'), 'new': new_opt.get('long')} + + if old_opt.get('value_name') != new_opt.get('value_name'): + changes['value_name'] = {'old': old_opt.get('value_name'), 'new': new_opt.get('value_name')} + + if old_opt.get('help') != new_opt.get('help'): + changes['help'] = {'old': old_opt.get('help'), 'new': new_opt.get('help')} + + if old_opt.get('default') != new_opt.get('default'): + changes['default'] = {'old': old_opt.get('default'), 'new': new_opt.get('default')} + + if old_opt.get('possible_values') != new_opt.get('possible_values'): + changes['possible_values'] = {'old': old_opt.get('possible_values'), 'new': new_opt.get('possible_values')} + + if changes: + modified.append({ + 'option': key, + 'changes': changes + }) + + return { + 'added': added, + 'removed': removed, + 'modified': modified + } + + +def compare_commands(old_cmds: Dict[str, Dict], new_cmds: Dict[str, Dict]) -> Dict: + """ + Compare two command dictionaries and detect changes. + + Returns dict with: added, removed, modified + """ + old_paths = set(old_cmds.keys()) + new_paths = set(new_cmds.keys()) + + added = [] + removed = [] + modified = [] + + # Find added commands + for path in new_paths - old_paths: + added.append({ + 'command': path, + 'data': new_cmds[path] + }) + + # Find removed commands + for path in old_paths - new_paths: + removed.append({ + 'command': path, + 'data': old_cmds[path] + }) + + # Find modified commands + for path in old_paths & new_paths: + old_cmd = old_cmds[path] + new_cmd = new_cmds[path] + + changes = {} + + # Check about text + if old_cmd.get('about') != new_cmd.get('about'): + changes['about'] = { + 'old': old_cmd.get('about'), + 'new': new_cmd.get('about') + } + + # Check aliases + old_aliases = set(old_cmd.get('aliases', [])) + new_aliases = set(new_cmd.get('aliases', [])) + if old_aliases != new_aliases: + changes['aliases'] = { + 'old': sorted(old_aliases), + 'new': sorted(new_aliases), + 'added': sorted(new_aliases - old_aliases), + 'removed': sorted(old_aliases - new_aliases) + } + + # Check usage + if old_cmd.get('usage') != new_cmd.get('usage'): + changes['usage'] = { + 'old': old_cmd.get('usage'), + 'new': new_cmd.get('usage') + } + + # Check options + option_changes = compare_options( + old_cmd.get('options', []), + new_cmd.get('options', []) + ) + if any(option_changes.values()): + changes['options'] = option_changes + + if changes: + modified.append({ + 'command': path, + 'changes': changes + }) + + return { + 'added': added, + 'removed': removed, + 'modified': modified + } + + +def categorize_breaking_changes(changes: Dict) -> List[Dict]: + """ + Identify changes that are likely breaking changes. + + Returns list of breaking change descriptions. + """ + breaking = [] + + # Removed commands are breaking + for item in changes['commands']['removed']: + breaking.append({ + 'type': 'command_removed', + 'command': item['command'], + 'severity': 'high', + 'description': f"Command '{item['command']}' was removed" + }) + + # Check modified commands for breaking changes + for item in changes['commands']['modified']: + cmd = item['command'] + cmd_changes = item['changes'] + + # Removed options are breaking + if 'options' in cmd_changes: + for opt in cmd_changes['options']['removed']: + opt_name = f"--{opt.get('long')}" if opt.get('long') else f"-{opt.get('short')}" + breaking.append({ + 'type': 'option_removed', + 'command': cmd, + 'option': opt_name, + 'severity': 'high', + 'description': f"Option '{opt_name}' removed from '{cmd}'" + }) + + # Changed option flags are breaking + for mod in cmd_changes['options']['modified']: + if 'short' in mod['changes'] or 'long' in mod['changes']: + breaking.append({ + 'type': 'option_renamed', + 'command': cmd, + 'option': mod['option'], + 'severity': 'high', + 'description': f"Option flags changed in '{cmd}': {mod['option']}" + }) + + # Changed default values might be breaking + if 'default' in mod['changes']: + breaking.append({ + 'type': 'default_changed', + 'command': cmd, + 'option': mod['option'], + 'severity': 'medium', + 'description': f"Default value changed for '{cmd} --{mod['option']}'" + }) + + # Removed possible values are breaking + if 'possible_values' in mod['changes']: + old_vals = set(mod['changes']['possible_values']['old'] or []) + new_vals = set(mod['changes']['possible_values']['new'] or []) + removed_vals = old_vals - new_vals + if removed_vals: + breaking.append({ + 'type': 'enum_values_removed', + 'command': cmd, + 'option': mod['option'], + 'severity': 'high', + 'description': f"Possible values removed from '{cmd} --{mod['option']}': {', '.join(removed_vals)}" + }) + + # Removed aliases might be breaking (users might rely on them) + if 'aliases' in cmd_changes and cmd_changes['aliases']['removed']: + for alias in cmd_changes['aliases']['removed']: + breaking.append({ + 'type': 'alias_removed', + 'command': cmd, + 'alias': alias, + 'severity': 'medium', + 'description': f"Alias '{alias}' removed from '{cmd}'" + }) + + return breaking + + +def main(): + if len(sys.argv) != 3: + print("Usage: diff-cli-structures.py ", file=sys.stderr) + print("Example: diff-cli-structures.py old.json new.json", file=sys.stderr) + sys.exit(1) + + old_file = sys.argv[1] + new_file = sys.argv[2] + + # Load JSON files + try: + with open(old_file, 'r') as f: + old_data = json.load(f) + except Exception as e: + print(f"Error reading {old_file}: {e}", file=sys.stderr) + sys.exit(1) + + try: + with open(new_file, 'r') as f: + new_data = json.load(f) + except Exception as e: + print(f"Error reading {new_file}: {e}", file=sys.stderr) + sys.exit(1) + + print(f"Comparing {old_data['version']} → {new_data['version']}...", file=sys.stderr) + + # Flatten command structures + old_commands = flatten_commands(old_data['commands']) + new_commands = flatten_commands(new_data['commands']) + + print(f"Old version: {len(old_commands)} commands", file=sys.stderr) + print(f"New version: {len(new_commands)} commands", file=sys.stderr) + + # Compare commands + command_changes = compare_commands(old_commands, new_commands) + + # Categorize breaking changes + breaking_changes = categorize_breaking_changes({'commands': command_changes}) + + # Determine if there are any changes + has_changes = ( + len(command_changes['added']) > 0 or + len(command_changes['removed']) > 0 or + len(command_changes['modified']) > 0 + ) + + # Build output + now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + + output = { + 'old_version': old_data['version'], + 'new_version': new_data['version'], + 'compared_at': now, + 'has_changes': has_changes, + 'summary': { + 'commands_added': len(command_changes['added']), + 'commands_removed': len(command_changes['removed']), + 'commands_modified': len(command_changes['modified']), + 'breaking_changes': len([b for b in breaking_changes if b['severity'] == 'high']) + }, + 'changes': { + 'commands': command_changes + }, + 'breaking_changes': breaking_changes + } + + # Output JSON + print(json.dumps(output, indent=2)) + + # Print summary to stderr + print(f"\nSummary:", file=sys.stderr) + print(f" Commands added: {output['summary']['commands_added']}", file=sys.stderr) + print(f" Commands removed: {output['summary']['commands_removed']}", file=sys.stderr) + print(f" Commands modified: {output['summary']['commands_modified']}", file=sys.stderr) + print(f" Breaking changes: {output['summary']['breaking_changes']}", file=sys.stderr) + + +if __name__ == '__main__': + main() diff --git a/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.py b/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.py new file mode 100755 index 0000000000..47cdace86c --- /dev/null +++ b/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +""" +Extract CLI command structure from goose binary using --help output. + +Usage: + ./extract-cli-structure.py > output/cli-structure.json + +Example: + ./extract-cli-structure.py /path/to/goose > output/new-cli-structure.json +""" + +import sys +import subprocess +import json +import re +import os +from datetime import datetime, timezone +from typing import Dict, List, Optional, Tuple + + +def load_skip_commands() -> List[str]: + """Load the list of commands to skip from config file.""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, '..', 'config', 'skip-commands.json') + + try: + with open(config_path, 'r') as f: + config = json.load(f) + return [cmd['name'] for cmd in config.get('skip_commands', [])] + except (FileNotFoundError, json.JSONDecodeError, KeyError) as e: + print(f"Warning: Could not load skip-commands.json: {e}", file=sys.stderr) + return [] + + +SKIP_COMMANDS = load_skip_commands() + + +def run_help_command(binary_path: str, command_path: List[str], short: bool = False) -> str: + """ + Run --help or -h on a command and return the output. + + Args: + binary_path: Path to goose binary + command_path: List of command parts (e.g., ['session', 'list']) + short: If True, use -h instead of --help + + Returns: + Help text output + """ + cmd = [binary_path] + command_path + (['-h'] if short else ['--help']) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + return result.stdout + except subprocess.TimeoutExpired: + print(f"Warning: Command timed out: {' '.join(cmd)}", file=sys.stderr) + return "" + except Exception as e: + print(f"Warning: Failed to run {' '.join(cmd)}: {e}", file=sys.stderr) + return "" + + +def parse_usage_line(help_text: str) -> Optional[str]: + """Extract the usage line from help text.""" + match = re.search(r'^Usage:\s*(.+)$', help_text, re.MULTILINE) + return match.group(1).strip() if match else None + + +def parse_about(help_text: str) -> str: + """Extract the command description (first line before Usage).""" + lines = help_text.strip().split('\n') + + # Find the Usage: line + usage_index = -1 + for i, line in enumerate(lines): + if line.strip().startswith('Usage:'): + usage_index = i + break + + # If Usage is found, look for description before it + if usage_index > 0: + for i in range(usage_index): + line = lines[i].strip() + if line and not line.startswith('Options:') and not line.startswith('Commands:'): + return line + + return "" + + +def parse_aliases(help_text: str) -> List[str]: + """Extract command aliases from help text.""" + # Look for "[aliases: x, y]" pattern in the first few lines + match = re.search(r'\[aliases?:\s*([^\]]+)\]', help_text[:500]) + if match: + aliases_str = match.group(1) + return [a.strip() for a in aliases_str.split(',')] + return [] + + +def parse_options(help_text: str) -> List[Dict]: + """ + Parse options from the Options: section of help text. + + Returns list of option dicts with: short, long, value_name, help, default, possible_values + """ + options = [] + + # Find the Options: section - goes until Commands: section or end of text + # Note: clap help has blank lines between options, so we can't stop at ^$ + options_match = re.search(r'^Options:\s*\n(.+?)(?=^Commands:\s*$|\Z)', + help_text, re.MULTILINE | re.DOTALL) + if not options_match: + return options + + options_text = options_match.group(1) + + # Split into individual option blocks + # Each option starts with whitespace followed by a dash (short or long flag) + # Use lookahead to split at lines that start a new option + option_blocks = re.split(r'\n(?=\s+-)', options_text) + + for block in option_blocks: + block = block.strip() + if not block or not block.startswith('-'): + continue + + option = parse_option_block(block) + if option: + options.append(option) + + return options + + +def parse_option_block(block: str) -> Optional[Dict]: + """Parse a single option block into structured data.""" + lines = block.split('\n') + if not lines: + return None + + # First line has the flags, optional value name, and sometimes inline help (common clap output) + first_line = lines[0].strip() + inline_help = None + + # Split on 2+ spaces to separate flags from inline help text. + # Example: "-o, --output Write output to file" + parts = re.split(r'\s{2,}', first_line, maxsplit=1) + flags_part = parts[0] + if len(parts) == 2: + inline_help = parts[1].strip() or None + + # Extract short flag (e.g., -f) + short_match = re.search(r'-([a-zA-Z])\b', flags_part) + short = short_match.group(1) if short_match else None + + # Extract long flag (e.g., --format) + long_match = re.search(r'--([a-z][a-z0-9-]*)', flags_part) + long = long_match.group(1) if long_match else None + + # Extract value_name (e.g., ) + value_name_match = re.search(r'<([^>]+)>', flags_part) + value_name = value_name_match.group(1) if value_name_match else None + + # Collect help text from subsequent indented lines + help_lines = [] + for line in lines[1:]: + line = line.strip() + if line and not line.startswith('['): + help_lines.append(line) + elif line.startswith('['): + # This might be [default: ...] or [possible values: ...] + break + + help_text = ' '.join(help_lines) + + if inline_help: + help_text = f"{inline_help} {help_text}".strip() if help_text else inline_help + + # Extract default value + default = None + default_match = re.search(r'\[default:\s*([^\]]+)\]', block) + if default_match: + default = default_match.group(1).strip() + + # Extract possible values + possible_values = None + possible_match = re.search(r'\[possible values:\s*([^\]]+)\]', block) + if possible_match: + values_str = possible_match.group(1) + possible_values = [v.strip() for v in values_str.split(',')] + + return { + 'short': short, + 'long': long, + 'value_name': value_name, + 'help': help_text if help_text else None, + 'default': default, + 'possible_values': possible_values + } + + +def parse_subcommands(help_text: str) -> List[Tuple[str, List[str]]]: + """ + Extract subcommand names and their aliases from the Commands: section. + + Returns: + List of tuples: (command_name, [aliases]) + """ + commands = [] + + # Find the Commands: section + commands_match = re.search(r'^Commands:\s*$(.+?)(?:^Options:|\Z)', + help_text, re.MULTILINE | re.DOTALL) + if not commands_match: + return commands + + commands_text = commands_match.group(1) + + # Each command line starts with the command name (not indented or minimally indented) + for raw_line in commands_text.split('\n'): + # Preserve indentation to avoid mis-parsing wrapped description lines. + # In clap help, actual command entries are typically not indented. + if not raw_line.strip(): + continue + + if raw_line.startswith(' ') or raw_line.startswith('\t'): + continue + + line = raw_line.strip() + + # Extract command name (first word) + parts = line.split() + if parts and not parts[0].startswith('-'): + command_name = parts[0] + # Skip "help" command as it's auto-generated + if command_name == 'help': + continue + + # Extract aliases from [aliases: x, y] pattern + aliases = [] + alias_match = re.search(r'\[aliases?:\s*([^\]]+)\]', line) + if alias_match: + aliases_str = alias_match.group(1) + aliases = [a.strip() for a in aliases_str.split(',')] + + commands.append((command_name, aliases)) + + return commands + + +def extract_command_structure(binary_path: str, command_path: List[str] = None, + parent_aliases: List[str] = None) -> Dict: + """ + Recursively extract command structure starting from a command path. + + Args: + binary_path: Path to goose binary + command_path: Current command path (e.g., ['session', 'list']) + parent_aliases: Aliases passed from parent (since they appear in parent's help) + + Returns: + Dict with command structure + """ + if command_path is None: + command_path = [] + + # Get both short and long help + help_text_long = run_help_command(binary_path, command_path, short=False) + + if not help_text_long: + return None + + # Parse command info + command_name = command_path[-1] if command_path else "goose" + about = parse_about(help_text_long) + # Use parent_aliases if provided, otherwise try to parse from own help + aliases = parent_aliases if parent_aliases is not None else parse_aliases(help_text_long) + usage = parse_usage_line(help_text_long) + options = parse_options(help_text_long) + + # Get subcommands with their aliases and recursively process them + subcommand_info = parse_subcommands(help_text_long) + subcommands = [] + + for subcommand_name, subcommand_aliases in subcommand_info: + # Skip commands in the skip list + if subcommand_name in SKIP_COMMANDS: + print(f"Skipping command: {subcommand_name}", file=sys.stderr) + continue + sub_path = command_path + [subcommand_name] + sub_structure = extract_command_structure(binary_path, sub_path, subcommand_aliases) + if sub_structure: + subcommands.append(sub_structure) + + return { + 'name': command_name, + 'about': about, + 'aliases': aliases, + 'usage': usage, + 'options': options, + 'subcommands': subcommands + } + + +def extract_version(binary_path: str) -> str: + """Extract version from goose --version.""" + try: + result = subprocess.run([binary_path, '--version'], + capture_output=True, text=True, timeout=5) + # Output is typically "goose 1.15.0" or similar + version_match = re.search(r'(\d+\.\d+\.\d+)', result.stdout) + return version_match.group(1) if version_match else "unknown" + except Exception as e: + print(f"Warning: Could not extract version: {e}", file=sys.stderr) + return "unknown" + + +def main(): + if len(sys.argv) < 2: + print("Usage: extract-cli-structure.py [source-version]", file=sys.stderr) + print("Example: extract-cli-structure.py /usr/local/bin/goose v1.15.0", file=sys.stderr) + sys.exit(1) + + binary_path = sys.argv[1] + source_version = sys.argv[2] if len(sys.argv) > 2 else None + + # Verify binary exists and is executable + try: + result = subprocess.run([binary_path, '--version'], + capture_output=True, timeout=5) + if result.returncode != 0: + print(f"Error: {binary_path} is not a valid goose binary", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: Cannot execute {binary_path}: {e}", file=sys.stderr) + sys.exit(1) + + print("Extracting CLI structure...", file=sys.stderr) + + # Extract version + version = extract_version(binary_path) + print(f"Version: {version}", file=sys.stderr) + + # Extract root command structure (recursively includes all subcommands) + root_structure = extract_command_structure(binary_path, []) + + # Build output JSON + # Use timezone-aware UTC datetime (Python 3.7+) + now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + + output = { + 'version': version, + 'source_version': source_version or version, + 'extracted_at': now, + 'binary_path': binary_path, + 'commands': root_structure['subcommands'] if root_structure else [] + } + + # Output JSON + print(json.dumps(output, indent=2)) + print("Extraction complete!", file=sys.stderr) + + +if __name__ == '__main__': + main() diff --git a/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.sh b/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.sh new file mode 100755 index 0000000000..bb6edbf8bc --- /dev/null +++ b/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# Extract CLI command structure from goose at a specific version +# Usage: ./extract-cli-structure.sh +# Example: ./extract-cli-structure.sh v1.15.0 +# +# For tagged releases (v*), downloads pre-built binary from GitHub releases. +# For HEAD or non-release refs, builds from source. + +set -e +set -o pipefail + +VERSION=${1:-"HEAD"} +GOOSE_REPO=${GOOSE_REPO:-"$HOME/Development/goose"} +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Create a temporary directory +TEMP_DIR=$(mktemp -d) +trap "rm -rf $TEMP_DIR" EXIT + +# Check if version is a release tag (starts with 'v' followed by numbers) +is_release_tag() { + [[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +# Download pre-built binary for a release version +download_release_binary() { + local version=$1 + local safe_version=${version//\//-} + local bin_dir="$TEMP_DIR/bin" + mkdir -p "$bin_dir" + + echo "Downloading goose $version from GitHub releases..." >&2 + + # Use the official download script with custom bin dir and specific version + curl -fsSL "https://github.com/block/goose/releases/download/stable/download_cli.sh" | \ + CONFIGURE=false GOOSE_BIN_DIR="$bin_dir" GOOSE_VERSION="$version" bash >&2 2>&1 || { + echo "Error: Failed to download goose $version" >&2 + return 1 + } + + echo "$bin_dir/goose" +} + +# Build goose from source +build_from_source() { + local version=$1 + local safe_version=${version//\//-} + + if [ ! -d "$GOOSE_REPO" ]; then + echo "Error: GOOSE_REPO directory not found: $GOOSE_REPO" >&2 + exit 1 + fi + + cd "$GOOSE_REPO" + + if [ "$version" = "HEAD" ]; then + echo "Building goose from HEAD..." >&2 + cargo build --release --quiet >&2 2>&1 || { + echo "Error: Failed to build goose from HEAD" >&2 + return 1 + } + echo "$GOOSE_REPO/target/release/goose" + else + # Verify version exists + if ! git rev-parse "$version" >/dev/null 2>&1; then + echo "Error: Version $version not found in git history" >&2 + return 1 + fi + + echo "Building goose from $version..." >&2 + + # Create a worktree for the version + local worktree_dir="$TEMP_DIR/goose-$safe_version" + git worktree add --quiet "$worktree_dir" "$version" >&2 2>&1 || { + echo "Error: Failed to create worktree for $version" >&2 + return 1 + } + + cd "$worktree_dir" + cargo build --release --quiet >&2 2>&1 || { + echo "Error: Failed to build goose from $version" >&2 + cd "$GOOSE_REPO" + git worktree remove "$worktree_dir" 2>/dev/null || true + return 1 + } + + # Clean up worktree but keep the binary accessible + local bin_path="$worktree_dir/target/release/goose" + local temp_bin="$TEMP_DIR/goose-$safe_version-bin" + cp "$bin_path" "$temp_bin" + + cd "$GOOSE_REPO" + git worktree remove "$worktree_dir" 2>/dev/null || true + + echo "$temp_bin" + fi +} + +# Get the goose binary +if is_release_tag "$VERSION"; then + GOOSE_BIN=$(download_release_binary "$VERSION") +else + GOOSE_BIN=$(build_from_source "$VERSION") +fi + +if [ -z "$GOOSE_BIN" ] || [ ! -x "$GOOSE_BIN" ]; then + echo "Error: Goose binary not found or not executable" >&2 + exit 1 +fi + +echo "Using binary: $GOOSE_BIN" >&2 +echo "Binary version: $($GOOSE_BIN --version 2>&1)" >&2 + +# Run the Python extraction script +python3 "$SCRIPT_DIR/extract-cli-structure.py" "$GOOSE_BIN" "$VERSION" diff --git a/documentation/automation/cli-command-tracking/scripts/run-pipeline.sh b/documentation/automation/cli-command-tracking/scripts/run-pipeline.sh new file mode 100755 index 0000000000..054e24b153 --- /dev/null +++ b/documentation/automation/cli-command-tracking/scripts/run-pipeline.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# End-to-end pipeline for CLI command tracking +# Usage: ./run-pipeline.sh [old_version] [new_version] +# Example: ./run-pipeline.sh v1.17.0 v1.19.0 +# +# Version detection: +# - If old_version not provided: uses the second-most-recent release tag +# - If new_version not provided: uses the most recent release tag (or RELEASE_TAG env var) +# - HEAD is only used when explicitly passed for testing unreleased changes + +set -e + +GOOSE_REPO=${GOOSE_REPO:-"$HOME/Development/goose"} + +# Function to get release tags using gh CLI +get_latest_release() { + if command -v gh &> /dev/null; then + gh release list --repo block/goose --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null + else + # Fallback: get latest version tag from git + cd "$GOOSE_REPO" && git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 + fi +} + +get_previous_release() { + if command -v gh &> /dev/null; then + gh release list --repo block/goose --limit 2 --json tagName --jq '.[].tagName' 2>/dev/null | sed -n '2p' + else + # Fallback: get second-latest version tag from git + cd "$GOOSE_REPO" && git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed -n '2p' + fi +} + +# Determine versions +if [ -n "$1" ]; then + OLD_VERSION="$1" +else + OLD_VERSION=$(get_previous_release) + if [ -z "$OLD_VERSION" ]; then + echo "Error: Could not determine previous release version" >&2 + exit 1 + fi +fi + +if [ -n "$2" ]; then + NEW_VERSION="$2" +elif [ -n "$RELEASE_TAG" ]; then + # Used by GitHub Actions release trigger + NEW_VERSION="$RELEASE_TAG" +else + NEW_VERSION=$(get_latest_release) + if [ -z "$NEW_VERSION" ]; then + echo "Error: Could not determine latest release version" >&2 + exit 1 + fi +fi + +echo "==========================================" +echo "CLI Command Documentation Pipeline" +echo "==========================================" +echo "Old Version: $OLD_VERSION" +echo "New Version: $NEW_VERSION" +echo "" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Change to output directory +OUTPUT_DIR="$SCRIPT_DIR/../output" +mkdir -p "$OUTPUT_DIR" +cd "$OUTPUT_DIR" + +# Use a per-run temp directory for logs to avoid collisions +LOG_DIR=$(mktemp -d) +trap 'rm -rf "$LOG_DIR"' EXIT + +echo "Step 1: Extracting CLI structure from $OLD_VERSION..." +if ! ../scripts/extract-cli-structure.sh "$OLD_VERSION" > old-cli-structure.json 2>"$LOG_DIR/extract-old.log"; then + echo "✗ Failed to extract CLI structure from $OLD_VERSION" >&2 + echo "Error output:" >&2 + cat "$LOG_DIR/extract-old.log" >&2 + exit 1 +fi +echo "✓ Extracted $(jq '.commands | length' old-cli-structure.json) commands" + +echo "" +echo "Step 2: Extracting CLI structure from $NEW_VERSION..." +if ! ../scripts/extract-cli-structure.sh "$NEW_VERSION" > new-cli-structure.json 2>"$LOG_DIR/extract-new.log"; then + echo "✗ Failed to extract CLI structure from $NEW_VERSION" >&2 + echo "Error output:" >&2 + cat "$LOG_DIR/extract-new.log" >&2 + exit 1 +fi +echo "✓ Extracted $(jq '.commands | length' new-cli-structure.json) commands" + +echo "" +echo "Step 3: Comparing CLI structures..." +python3 ../scripts/diff-cli-structures.py old-cli-structure.json new-cli-structure.json > cli-changes.json 2>"$LOG_DIR/diff.log" + +HAS_CHANGES=$(jq -r '.has_changes' cli-changes.json) +echo "✓ Comparison complete. Has changes: $HAS_CHANGES" + +if [ "$HAS_CHANGES" = "true" ]; then + echo "" + echo "Changes detected:" + echo " - Commands added: $(jq '.summary.commands_added' cli-changes.json)" + echo " - Commands removed: $(jq '.summary.commands_removed' cli-changes.json)" + echo " - Commands modified: $(jq '.summary.commands_modified' cli-changes.json)" + echo " - Breaking changes: $(jq '.summary.breaking_changes' cli-changes.json)" + + echo "" + echo "Step 4: Synthesizing CLI changes documentation..." + + # Run goose and capture output, filtering out session logs + goose run --recipe ../recipes/synthesize-cli-changes.yaml 2>&1 | \ + sed -E 's/\x1B\[[0-9;]*[mK]//g' | \ + grep -v "^starting session" | \ + grep -v "^ session id:" | \ + grep -v "^ working directory:" | \ + grep -v "^─── text_editor" | \ + grep -v "^path:" | \ + grep -v "^command:" | \ + grep -v "^Closing session" | \ + grep -v "^Loading recipe:" | \ + grep -v "^Description:" | \ + cat -s > cli-changes.md.tmp + + # If the pipeline fails, surface the goose error (grep can exit 1 when it matches nothing) + if [ ${PIPESTATUS[0]} -ne 0 ]; then + echo "✗ Failed to synthesize CLI changes (goose run failed)" >&2 + exit 1 + fi + + # Check if we got meaningful content + if [ -s cli-changes.md.tmp ] && grep -q "# CLI Command Changes" cli-changes.md.tmp; then + mv cli-changes.md.tmp cli-changes.md + echo "✓ Generated cli-changes.md ($(wc -l < cli-changes.md) lines)" + elif [ -f cli-changes.md ] && [ -s cli-changes.md ]; then + # File was written directly by goose + rm -f cli-changes.md.tmp + echo "✓ Generated cli-changes.md ($(wc -l < cli-changes.md) lines)" + else + echo "✗ Failed to generate cli-changes.md" + rm -f cli-changes.md.tmp + exit 1 + fi + + echo "" + echo "Step 5: Updating CLI commands documentation..." + + # Set environment variables for the update recipe + export CLI_COMMANDS_PATH="${GOOSE_REPO}/documentation/docs/guides/goose-cli-commands.md" + + # Run the update recipe + goose run --recipe ../recipes/update-cli-commands.yaml 2>&1 | \ + sed -E 's/\x1B\[[0-9;]*[mK]//g' | \ + grep -v "^starting session" | \ + grep -v "^ session id:" | \ + grep -v "^ working directory:" | \ + grep -v "^─── text_editor" | \ + grep -v "^path:" | \ + grep -v "^command:" | \ + grep -v "^Closing session" | \ + grep -v "^Loading recipe:" | \ + grep -v "^Description:" | \ + cat -s + + if [ ${PIPESTATUS[0]} -ne 0 ]; then + echo "✗ Failed to update documentation (goose run failed)" >&2 + exit 1 + fi + + echo "✓ Documentation update complete" + + echo "" + echo "==========================================" + echo "Pipeline Complete!" + echo "==========================================" + echo "" + echo "Output files:" + echo " - old-cli-structure.json" + echo " - new-cli-structure.json" + echo " - cli-changes.json" + echo " - cli-changes.md" + echo "" + echo "Review the changes to the CLI commands documentation." +else + echo "" + echo "==========================================" + echo "No Changes Detected" + echo "==========================================" + echo "" + echo "No CLI changes between $OLD_VERSION and $NEW_VERSION." + echo "Documentation update not needed." +fi