workflow: auto-update cli-commands on release (#6755)

This commit is contained in:
dianed-square
2026-02-12 08:23:47 -08:00
committed by GitHub
parent a00c25c7b5
commit b90484297c
11 changed files with 2517 additions and 0 deletions
+304
View File
@@ -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 <<EOF > ~/.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
@@ -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
@@ -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 <old-file> <new-file> > 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
@@ -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 <commit-hash>: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.
@@ -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"
}
]
}
@@ -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
@@ -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?
@@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""
Compare two CLI structure JSON files and output detected changes.
Usage:
./diff-cli-structures.py <old-file> <new-file> > 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 <old-file> <new-file>", 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()
@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""
Extract CLI command structure from goose binary using --help output.
Usage:
./extract-cli-structure.py <goose-binary-path> > 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 <FILE> 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., <FORMAT>)
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 <goose-binary-path> [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()
@@ -0,0 +1,115 @@
#!/bin/bash
# Extract CLI command structure from goose at a specific version
# Usage: ./extract-cli-structure.sh <version>
# 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"
@@ -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