Skip to content

Conversation

tony
Copy link
Member

@tony tony commented Oct 19, 2025

Modernize CLI: Align with DevOps Tool Conventions

Summary

This PR modernizes vcspull's CLI to align with modern DevOps tool conventions (Terraform, Cargo, Ruff, Biome), providing a more intuitive developer experience and better automation support.

⚠️ This is a breaking change release (v1.39.x)

Motivation

The previous CLI lacked:

  • Read-only introspection - No way to list or check repo status without syncing
  • Dry-run support - Users couldn't preview changes before applying them
  • Machine-readable output - No JSON/NDJSON for CI/CD pipelines
  • Consistent flags - Mixed flag naming (-c vs -f, verbose --workspace-root)
  • Clear command separation - Single import command tried to do two different things

Changes

🎯 New Commands

vcspull list - List Configured Repositories

vcspull list                    # All repos
vcspull list "django-*"         # Filter by pattern
vcspull list --tree             # Group by workspace
vcspull list --json             # Machine-readable

vcspull status - Check Repository Health

vcspull status                  # Quick status check
vcspull status --detailed       # Show branches, ahead/behind
vcspull status --json           # Machine-readable

vcspull add - Add Single Repository

vcspull add mylib https://github.com/example/mylib.git
vcspull add mylib URL -w ~/code
vcspull add mylib URL --dry-run

vcspull discover - Scan Filesystem for Repositories

vcspull discover ~/code
vcspull discover ~/code --recursive --yes
vcspull discover ~/code --dry-run

🔄 Breaking Changes

Change Old New
Command vcspull import NAME URL vcspull add NAME URL
Command vcspull import --scan DIR vcspull discover DIR
Flag -c/--config -f/--file
Flag --workspace-root -w/--workspace (all 3 aliases supported)

✨ New Features

Dry-Run Mode

Action commands support --dry-run/-n for safe previewing:

vcspull sync --dry-run "*"      # Terraform-style plan
vcspull add mylib URL --dry-run # Preview add
vcspull discover ~/code -n      # Preview discovery

Structured Output

Machine-readable output for automation:

vcspull list --json             # Single JSON array
vcspull list --ndjson           # NDJSON stream (one per line)
vcspull status --json           # Status as JSON
vcspull sync --dry-run --ndjson "*"  # Stream sync plan

Semantic Colors

  • --color {auto,always,never} - Control color output
  • Respects NO_COLOR environment variable
  • Semantic colors: green (success), yellow (warning), red (error)

Short Flags

  • -w for workspace (instead of verbose --workspace-root)
  • -n for dry-run
  • -f for file (matches kubectl, docker-compose conventions)

📁 File Structure

New Files:

src/vcspull/cli/
├── _colors.py       # Semantic colors and NO_COLOR support
├── _output.py       # OutputFormatter (JSON/NDJSON/human)
├── _workspaces.py   # Shared workspace filtering
├── add.py           # Add single repository
├── discover.py      # Discover repos from filesystem
├── list.py          # List configured repos
└── status.py        # Check repo health

tests/cli/
├── test_add.py              # 8 tests for add command (299 lines)
├── test_discover.py         # 17 tests for discover (688 lines)
├── test_list.py             # 8 tests for list (267 lines)
├── test_status.py           # 13 tests for status (549 lines)
├── test_plan_output_helpers.py  # 7 tests for sync plan helpers
└── test_sync_plan_helpers.py    # 15 tests for sync planning

Deleted Files:

  • src/vcspull/cli/_import.py (split into add.py + discover.py)
  • tests/cli/test_import.py (replaced by test_add.py + test_discover.py)

Modified Files:

  • src/vcspull/cli/__init__.py - Wire new commands, update examples
  • src/vcspull/cli/sync.py - Add -f, -w, --dry-run, --json/--ndjson, async planning
  • src/vcspull/cli/fmt.py - Change -c to -f
  • tests/test_cli.py - Update for new command structure
  • tests/test_log.py - Update logger names for new modules
  • README.md - Update all examples with new commands
  • CHANGES - Document breaking changes with migration guide
  • docs/ - Comprehensive documentation for all new commands

Migration Guide

Command Changes

# Old → New
vcspull import NAME URL              → vcspull add NAME URL
vcspull import --scan DIR            → vcspull discover DIR

Flag Changes

# Old → New
vcspull sync -c FILE                 → vcspull sync -f FILE
vcspull sync --workspace-root PATH   → vcspull sync -w PATH
vcspull fmt -c FILE                  → vcspull fmt -f FILE

# Note: --workspace-root still works as an alias
vcspull sync --workspace-root ~/code # Still supported

Examples

Before (v1.38.x)

# Add a repo
vcspull import mylib https://github.com/example/mylib.git

# Scan directory
vcspull import --scan ~/code --recursive --yes

# Sync repos
vcspull sync -c ./repos.yaml "*"

# Format config
vcspull fmt -c ~/.vcspull.yaml --write

# No way to list or check status without syncing

After (v1.39.x)

# Add a repo with preview
vcspull add mylib https://github.com/example/mylib.git --dry-run
vcspull add mylib https://github.com/example/mylib.git

# Scan directory with preview
vcspull discover ~/code --recursive --dry-run
vcspull discover ~/code --recursive --yes

# List what we have
vcspull list --tree
vcspull list --json | jq '.[] | .name'

# Check status
vcspull status
vcspull status --detailed

# Preview sync with Terraform-style plan
vcspull sync --dry-run "*"

# Sync repos
vcspull sync -f ./repos.yaml "*"

# Format config
vcspull fmt -f ~/.vcspull.yaml --write

# Machine-readable output for CI/CD
vcspull list --json | jq '.[] | .name'
vcspull status --ndjson | grep -v '"exists":true'
vcspull sync --dry-run --json "*" | jq '.summary'

Testing

Test Coverage

  • Total Tests: 192 (146 original + 46 new)
  • Pass Rate: 100%
  • New Test Files: 4 files with 46 comprehensive tests (1,803 lines)

New Tests Breakdown

tests/cli/test_add.py              8 tests (299 lines)  ✓
tests/cli/test_discover.py        17 tests (688 lines)  ✓
tests/cli/test_list.py             8 tests (267 lines)  ✓
tests/cli/test_status.py          13 tests (549 lines)  ✓
tests/cli/test_plan_output_helpers.py   7 tests        ✓
tests/cli/test_sync_plan_helpers.py    15 tests        ✓
────────────────────────────────────────────────────────
Total new tests:                  46 tests (1,803 lines)

Testing Patterns

  • ✅ NamedTuple fixtures for parameterized tests
  • ✅ Isolated environments (tmp_path, monkeypatch)
  • ✅ Log capture validation
  • ✅ Real git repo creation for integration tests
  • ✅ Both human and machine-readable output modes tested
  • ✅ Edge cases: missing repos, dirty repos, ahead/behind tracking
  • ✅ Async planning with progress tracking

Validation

✓ uv run ruff check . --fix --show-fixes  # All checks passed
✓ uv run ruff format .                     # All files formatted
✓ uv run mypy                              # No issues in 39 source files
✓ uv run pytest                            # 192/192 passed in 4.82s
✓ uv build                                 # Successfully built wheel + sdist

Design Decisions

Why Split import into add and discover?

Problem: Single command doing two unrelated things is confusing

  • vcspull import NAME URL - adds single repo
  • vcspull import --scan DIR - scans filesystem

Solution: Separate commands with clear purposes

  • vcspull add NAME URL - single repo, explicit
  • vcspull discover DIR - filesystem scan, bulk operation

This matches patterns from other tools:

  • git add <file> (single) vs git add -A (bulk)
  • npm install <package> (single) vs npm install (from package.json)
  • cargo add <crate> (single) vs bulk dependency management

Why -f/--file instead of -c/--config?

Precedent: Matches widely-used DevOps tools

  • kubectl -f/--filename
  • docker-compose -f/--file
  • make -f/--file
  • terraform -chdir=DIR (similar concept)

Benefit: Reduces cognitive load for users already familiar with these tools

Why --dry-run/-n?

Precedent: Universal pattern across DevOps tools

  • terraform plan (preview changes)
  • apt-get -s/--dry-run (simulate)
  • rsync -n/--dry-run (preview)
  • git add -n/--dry-run (preview)
  • ansible-playbook --check (dry-run)

Benefit: Safety-first approach prevents accidental changes

Why --json and --ndjson?

Use Cases:

  • --json: Single operation results (list, status) - complete JSON object
  • --ndjson: Streaming operations (sync progress) - one JSON per line

Precedent:

  • Cargo: --message-format=json (NDJSON to stdout)
  • ripgrep: --json (NDJSON events)
  • Terraform: -json (plan/apply output)
  • Docker: docker events --format '{{json .}}'

Why "Action Commands" vs "Introspection Commands"?

Clarity: Creates clear parallel between command types

  • Action commands (sync, add, discover) - perform mutations, support --dry-run
  • Introspection commands (list, status) - read-only, no side effects

This terminology is clear and doesn't overload the meaning of "write" (which could mean "write to disk" or "write to config").

Documentation Updates

User-Facing Documentation

  • README.md: All examples updated with new commands
  • CHANGES: Breaking changes documented with migration guide
  • CLI Help: Examples updated in --help output for all commands
  • docs/cli/add.md: Complete guide to vcspull add (176 lines)
  • docs/cli/discover.md: Complete guide to vcspull discover (273 lines)
  • docs/cli/list.md: Complete guide to vcspull list (160 lines)
  • docs/cli/status.md: Complete guide to vcspull status (212 lines)
  • docs/cli/import.md: Migration guide from old command
  • docs/cli/sync.md: Updated with new flags and examples
  • docs/cli/fmt.md: Updated with renamed flags

API Documentation

  • Docstrings: NumPy-style docstrings for all new functions
  • Type hints: Full type coverage with mypy validation
  • docs/api/cli/: API reference for all new modules

Backwards Compatibility

What Breaks

  • vcspull import command removed (use add or discover)
  • -c short flag removed (use -f)

What Stays Compatible

  • --workspace-root still works (alias to -w/--workspace)
  • ✅ All sync patterns and filters work identically
  • ✅ Config file format unchanged
  • ✅ All environment variables unchanged
  • ✅ Output format for non-JSON modes mostly unchanged

Alignment with DevOps Tools

This PR brings vcspull in line with modern CLI conventions:

Pattern vcspull Similar Tools
Dry-run --dry-run/-n terraform plan, rsync -n, apt -s
Structured output --json/--ndjson cargo, ripgrep, terraform
File flag -f/--file kubectl, docker-compose, make
Color control --color {auto,always,never} cargo, ruff, git
NO_COLOR Respects env var Rust tools, modern CLIs
Preview + Apply sync --dry-run → sync terraform plan → apply
Introspection list, status kubectl get, docker ps
Async progress Terraform-style plan terraform, cargo

Key Features

Terraform-Style Dry-Run Plans

$ vcspull sync --dry-run "*"
Plan: 1 to clone (+), 3 to update (~), 45 unchanged (✓), 0 blocked (⚠), 0 errors (✗)

~/code/
  + flask  /home/d/code/flask  missing
  ~ django  /home/d/code/django  needs update

Tip: run without --dry-run to apply. Use --show-unchanged to include ✓ rows.

Machine-Readable Output

$ vcspull sync --dry-run --json "*" | jq '.summary'
{
  "format_version": "1",
  "type": "summary",
  "clone": 1,
  "update": 3,
  "unchanged": 45,
  "blocked": 0,
  "errors": 0,
  "total": 49,
  "duration_ms": 342
}

Introspection Commands

$ vcspull list --tree
~/code/
  • flask → /home/d/code/flask
  • django → /home/d/code/django

~/study/rust/
  • ripgrep → /home/d/study/rust/ripgrep

$ vcspull status --detailed
✓ flask: clean (master, ahead: 0, behind: 0)
✗ django: missing
⚠ ripgrep: dirty (feature-branch, ahead: 2, behind: 0)

Commits

This PR contains 17 commits organized by feature area:

Core CLI Infrastructure:

  1. cli(feat): Add shared output formatter for JSON/NDJSON/human-readable modes
  2. cli(feat): Add semantic colors with NO_COLOR support
  3. cli(feat): Add workspace filtering helper

New Commands:
4. cli(list feat): Add list command for viewing configured repositories
5. cli(status feat): Add status command for repository health checks
6. cli(status feat[detailed]): Report branch divergence and ahead/behind
7. cli(add feat): Add command for single repository registration
8. cli(discover feat): Add discover command for filesystem scanning

Sync Enhancements:
9. cli(sync feat): Add dry-run mode with Terraform-style plans
10. cli(sync): Async dry-run plan output with progress tracking
11. cli(sync): Quiet stderr/stdout capture for structured output
12. cli(sync): Suppress progress noise in machine-readable output
13. cli(sync feat[workspace-filter]): Add shared workspace filtering helper

Flag Alignment:
14. cli: Align help text and formatter with new flags (-f, -w)

Testing & Documentation:
15. tests(discover/config,cli): Cover edge-path behaviour
16. cli/tests(test[plan-output]): Add coverage for sync planner helpers
17. docs(readme): Document new cli introspection commands
18. docs(CHANGES): Improve accuracy and readability
19. docs(CHANGES): Use "action commands" instead of "write commands"

Impact

For End Users

  • Better DX: Commands are clearer and more intuitive
  • Safer: Dry-run mode prevents mistakes
  • More discoverable: List and status commands for introspection
  • Familiar: Matches patterns from tools they already use
  • Faster feedback: See what will happen before it happens

For Automation/CI/CD

  • Machine-readable output: JSON/NDJSON for scripting
  • Exit codes: Consistent error handling
  • Non-interactive: --yes flag for bulk operations
  • Structured data: Stable schema with format_version

For Maintainers

  • Better separation: Single-purpose commands
  • Extensible: Output formatters make adding formats easy
  • Testable: Comprehensive test coverage
  • Type-safe: Full mypy coverage
  • Documented: Complete user and API documentation

Performance

No performance regressions:

  • Sync operations: Same speed
  • Dry-run planning: Async with progress tracking for better UX
  • List/Status: Fast introspection (no network calls unless --fetch)

Security

No security changes - same git/hg/svn subprocess handling

Checklist

  • All new code follows project style (ruff, mypy)
  • Tests added for new functionality (46 new tests)
  • All tests passing (192/192)
  • Documentation updated (README, CHANGES, docs/)
  • Breaking changes documented
  • Migration guide provided
  • Commit messages follow project convention
  • No regressions in existing functionality
  • Type hints complete
  • Docstrings complete (NumPy style)

References

Copy link

codecov bot commented Oct 19, 2025

Codecov Report

❌ Patch coverage is 78.59477% with 131 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.15%. Comparing base (afa61bc) to head (c87805f).
⚠️ Report is 18 commits behind head on master.

Files with missing lines Patch % Lines
src/vcspull/cli/sync.py 77.88% 40 Missing and 29 partials ⚠️
src/vcspull/cli/add.py 58.58% 32 Missing and 9 partials ⚠️
src/vcspull/cli/status.py 86.25% 10 Missing and 8 partials ⚠️
src/vcspull/cli/list.py 96.66% 1 Missing and 1 partial ⚠️
src/vcspull/config.py 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #472      +/-   ##
==========================================
- Coverage   79.36%   79.15%   -0.22%     
==========================================
  Files           9       14       +5     
  Lines         659     1439     +780     
  Branches      148      309     +161     
==========================================
+ Hits          523     1139     +616     
- Misses         78      185     +107     
- Partials       58      115      +57     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony force-pushed the streamline-commands branch 3 times, most recently from 979f724 to 3283818 Compare October 19, 2025 20:07
tony added 16 commits October 19, 2025 15:25
why: Align vcspull CLI with modern DevOps tool conventions (Terraform,
Cargo, Ruff, Biome) for improved developer experience and automation
support. Previous CLI lacked introspection commands, dry-run support,
and machine-readable output.

what:
Breaking Changes:
- Remove `vcspull import` command (split into `add` and `discover`)
- Rename `-c/--config` to `-f/--file` across all commands
- Add `-w/--workspace/--workspace-root` (all aliases supported)

New Commands:
- `vcspull list` - List configured repos with --tree, --json, --ndjson
- `vcspull status` - Check repo health (exists, clean/dirty, ahead/behind)
- `vcspull add` - Add single repository with --dry-run support
- `vcspull discover` - Scan filesystem for repos with --dry-run support

New Infrastructure:
- Create _output.py for OutputFormatter (JSON/NDJSON/human modes)
- Create _colors.py with semantic colors and NO_COLOR support
- Add --dry-run/-n flag to sync, add, discover commands
- Add --json/--ndjson structured output to sync, list, status
- Add --color {auto,always,never} flag with NO_COLOR env support

Improvements:
- Split _import.py into add.py (single) and discover.py (bulk)
- Update sync.py with new flags and dry-run preview mode
- Update fmt.py to use -f flag instead of -c
- Wire all new commands in __init__.py with updated examples
- Update README.md with new command examples
- Update CHANGES with breaking changes and migration guide
- Update test_log.py for new module structure

Migration Guide in CHANGES:
- vcspull import NAME URL → vcspull add NAME URL
- vcspull import --scan DIR → vcspull discover DIR
- vcspull sync -c FILE → vcspull sync -f FILE
- vcspull sync --workspace-root PATH → vcspull sync -w PATH

refs: All tests pass (109 tests), mypy clean, ruff clean
why: New commands (add, discover, list, status) need test coverage to
ensure reliability and prevent regressions. Tests follow project's
NamedTuple fixture pattern for parameterized testing.

what:
- Create tests/cli/test_add.py with 5 tests:
  * Parameterized tests for add with default/custom workspace
  * Dry-run mode test
  * Duplicate repository warning test
  * New config file creation test

- Create tests/cli/test_discover.py with 6 tests:
  * Parameterized tests for single-level and recursive discovery
  * Dry-run mode test
  * Skip repos without remote URL test
  * Show existing repos test
  * Workspace override test

- Create tests/cli/test_list.py with 6 tests:
  * Parameterized tests for listing all/filtered repos
  * JSON output test
  * Tree mode test
  * Empty config test
  * Pattern no-match test

- Create tests/cli/test_status.py with 7 tests:
  * Parameterized tests for repo status (exists/git/missing)
  * Status all repos test
  * JSON output test
  * Detailed mode test
  * Pattern filter test

Testing patterns:
- Use caplog.set_level(logging.INFO) to capture log output
- Use tmp_path and monkeypatch for isolated test environments
- Follow project's NamedTuple fixture pattern for parameterization
- Test both human and machine-readable output modes

refs: All 133 tests pass (109 original + 24 new), mypy clean, ruff clean
… to repo configs

why: workspace_root field was showing as empty strings in JSON output from
list and status commands. This field is essential for identifying which
workspace section a repository belongs to, especially for automation and
tooling that processes vcspull output.

what:
- Add workspace_root field to ConfigDict TypedDict in types.py
- Set workspace_root in extract_repos() to preserve original directory label
- Include workspace_root in check_repo_status() return value
- Update test fixtures to include workspace_root field for type compliance

The workspace_root now correctly shows labels like "~/study/ai/" or
"~/work/python/" in JSON output, matching the keys from .vcspull.yaml config.
why: The CLI modernization added four new commands (list, status, add,
discover), replaced the import command, and added the workspace_root field
to JSON output. These changes needed comprehensive documentation for users
and API reference updates.

what:
- Create docs/cli/list.md with JSON/NDJSON output examples showing workspace_root
- Create docs/cli/status.md with detailed status and automation examples
- Create docs/cli/add.md documenting single repo addition (replaces import)
- Create docs/cli/discover.md for bulk filesystem scanning (replaces import --scan)
- Update docs/cli/sync.md with --dry-run, --json, --ndjson, -f, -w flags
- Update docs/cli/fmt.md changing -c/--config to -f/--file throughout
- Update docs/cli/import.md with deprecation notice and migration guide
- Update docs/quickstart.md changing import to add/discover, -c to -f
- Create API reference docs for new modules (add, discover, list, status)
- Update docs/cli/index.md and docs/api/cli/index.md with new command toctrees
- Update docs/api/cli/import.md noting module removal

All JSON output examples now show workspace_root field correctly populated
with workspace labels like "~/study/ai/" matching .vcspull.yaml keys.

Migration guides included for users upgrading from import command.
why: Sphinx build was failing because docs/cli/import.md referenced the
import subparser which was removed in the CLI modernization.

what: Remove argparse directive from import.md since the command no longer
exists. Keep the historical documentation for reference but remove the
command reference that tried to introspect the non-existent subparser.
why: Ensure Terraform-style planner and helper utilities stay stable.

what:
- add plan payload and progress tests in tests/cli/test_plan_output_helpers.py
- cover _maybe_fetch and _determine_plan_action edge cases in tests/cli/test_sync_plan_helpers.py
- extend dry-run CLI fixtures to inject deterministic plan results via module monkeypatch
@tony tony force-pushed the streamline-commands branch from 3283818 to 4253b85 Compare October 19, 2025 20:25
@tony tony force-pushed the streamline-commands branch from 4253b85 to c87805f Compare October 19, 2025 20:27
@tony tony merged commit 298f3b8 into master Oct 19, 2025
10 checks passed
@tony tony deleted the streamline-commands branch October 19, 2025 20:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant