mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
workflow: auto-update cli-commands on release (#6755)
This commit is contained in:
@@ -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?
|
||||
+355
@@ -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()
|
||||
+363
@@ -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()
|
||||
+115
@@ -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
|
||||
Reference in New Issue
Block a user