granite-cli 0.1.10

CLI for discovering, configuring, and launching AI workflows powered by IBM Granite models.
# Spec 0014: Version Command Implementation

## Overview

Add a `granite-cli version` command (with `--version`/`-v` flag support) that displays version information derived from git tags and commit state at build time.

## Version Format

The version string follows this semantic format:

```
<base_version>[+dev][+dirty] (commit: <short_hash>[+dirty])
```

### Components

1. **Base Version**: Extracted from most recent `vX.Y.Z` git tag, or `0.0.0` if no tags exist
2. **+dev suffix**: Added if commits exist after the tagged commit
3. **+dirty suffix**: Added if uncommitted changes exist in the working tree at build time
4. **Commit Hash**: Short git hash (8 characters) of the current commit
5. **Hash +dirty**: Added to commit hash if uncommitted changes exist

### Examples

| Scenario | Output |
|----------|--------|
| Clean build at tag v0.1.0 | `0.1.0 (commit: abc12345)` |
| 3 commits after v0.1.0, clean | `0.1.0+dev (commit: def67890)` |
| At tag v0.1.0, with uncommitted changes | `0.1.0+dirty (commit: abc12345+dirty)` |
| 3 commits after v0.1.0, with changes | `0.1.0+dev+dirty (commit: def67890+dirty)` |
| No tags, clean build | `0.0.0+dev (commit: abc12345)` |
| No tags, with uncommitted changes | `0.0.0+dev+dirty (commit: abc12345+dirty)` |

## Implementation Strategy

### 1. Build-Time Code Generation (build.rs)

Extend `build.rs` to capture git version information and generate a Rust module with constants.

#### Git Information to Capture

```rust
// Information needed at build time
struct VersionInfo {
    base_version: String,      // From git tag or "0.0.0"
    commit_hash: String,        // Short hash (8 chars)
    commits_since_tag: u32,     // 0 if at tag, >0 if commits after
    has_uncommitted: bool,      // true if working tree is dirty
}
```

#### Git Commands

```bash
# Get most recent vX.Y.Z tag
git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1

# Get short commit hash
git rev-parse --short=8 HEAD

# Count commits since tag (0 if at tag)
git rev-list <tag>..HEAD --count

# Check for uncommitted changes (tracked files only)
git diff --quiet || echo "dirty"
```

#### Generated Module Structure

Generate `src/version.rs` (git-ignored) with:

```rust
// Auto-generated by build.rs - do not edit
pub const VERSION: &str = "0.1.0";
pub const COMMIT_HASH: &str = "abc12345";
pub const COMMITS_SINCE_TAG: u32 = 0;
pub const HAS_UNCOMMITTED: bool = false;

pub fn version_string() -> String {
    let mut version = VERSION.to_string();
    
    if COMMITS_SINCE_TAG > 0 {
        version.push_str("+dev");
    }
    
    if HAS_UNCOMMITTED {
        version.push_str("+dirty");
    }
    
    let mut commit = format!("commit: {}", COMMIT_HASH);
    if HAS_UNCOMMITTED {
        commit.push_str("+dirty");
    }
    
    format!("{} ({})", version, commit)
}
```

### 2. CLI Integration (main.rs)

Add version command support in two ways:

#### A. Global --version Flag

```rust
#[derive(Parser, Debug)]
#[command(name = "granite-cli")]
#[command(about = "Universal Model Adapter with Capabilities", long_about = None)]
#[command(version = version::version_string())]  // Add this
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}
```

This enables: `granite-cli --version` and `granite-cli -V`

#### B. Version Subcommand

```rust
#[derive(Subcommand, Debug)]
enum Commands {
    // ... existing commands ...
    
    /// Show version information
    Version,
}
```

This enables: `granite-cli version`

### 3. Build Script Changes (build.rs)

#### Add Dependencies

```toml
[build-dependencies]
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
```

No additional dependencies needed - use `std::process::Command` for git.

#### Implementation Steps

1. Add `generate_version_info()` function that:
   - Runs git commands to gather version data
   - Handles errors gracefully (fallback to defaults if not in git repo)
   - Returns `VersionInfo` struct

2. Add `generate_version_module()` function that:
   - Takes `VersionInfo`
   - Generates Rust source code as a string
   - Writes to `src/version.rs`

3. Add `println!("cargo:rerun-if-changed=.git/HEAD")` to rebuild on commits

4. Add `println!("cargo:rerun-if-changed=.git/refs/tags")` to rebuild on new tags

#### Error Handling

- If not in a git repository: use defaults (`0.0.0`, `unknown`, 0 commits, not dirty)
- If git commands fail: log warning and use defaults
- Never fail the build due to version detection issues

### 4. Module Integration (src/main.rs)

```rust
// Add at top of main.rs
mod version;

// In main() or command handler
match cli.command {
    // ... existing commands ...
    Some(Commands::Version) => {
        println!("{}", version::version_string());
        Ok(())
    }
    // ...
}
```

### 5. Git Configuration

#### Update .gitignore

Add to `.gitignore`:
```
# Generated version module
src/version.rs
```

This ensures the build-time generated file is not committed.

## Testing Strategy

### Manual Testing Scenarios

1. **Clean build at tag**
   ```bash
   git tag v0.1.0
   cargo build
   ./target/debug/granite-cli version
   # Expected: 0.1.0 (commit: <hash>)
   ```

2. **Build with commits after tag**
   ```bash
   git tag v0.1.0
   # Make some commits
   cargo build
   ./target/debug/granite-cli version
   # Expected: 0.1.0+dev (commit: <hash>)
   ```

3. **Build with uncommitted changes**
   ```bash
   # Modify a file
   cargo build
   ./target/debug/granite-cli version
   # Expected: <version>+dirty (commit: <hash>+dirty)
   ```

4. **No tags scenario**
   ```bash
   # In repo with no tags
   cargo build
   ./target/debug/granite-cli version
   # Expected: 0.0.0+dev (commit: <hash>)
   ```

5. **Global flag**
   ```bash
   ./target/debug/granite-cli --version
   ./target/debug/granite-cli -V
   # Both should show same output as `version` subcommand
   ```

### Edge Cases

- Not in a git repository → use defaults
- Git commands fail → use defaults with warning
- Detached HEAD state → should still work
- Shallow clone → may not have full history, handle gracefully

## Implementation Checklist

- [ ] Extend `build.rs` with git version detection logic
- [ ] Generate `src/version.rs` module with constants and helper function
- [ ] Add `src/version.rs` to `.gitignore`
- [ ] Add `Version` subcommand to `Commands` enum in `main.rs`
- [ ] Add `version` attribute to `Cli` struct for `--version` flag
- [ ] Implement version command handler
- [ ] Test all scenarios (clean, +dev, +dirty, no tags)
- [ ] Verify `--version`, `-V`, and `version` subcommand all work
- [ ] Document version format in README or user docs

## Compatibility with publish.sh

The version detection logic aligns with `scripts/publish.sh`:

- Both use the same git tag pattern: `^v[0-9]+\.[0-9]+\.[0-9]+$`
- Both sort tags with `-v:refname` for semantic versioning
- Both strip the leading `v` from tags
- The build script adds additional context (+dev, +dirty, commit hash)

The key difference: `publish.sh` requires the tag to point to HEAD, while the version command allows tags on earlier commits (adding `+dev` to indicate this).

## Future Enhancements

- Add build timestamp
- Add build host information
- Add Rust compiler version used
- Support pre-release tags (e.g., `v0.1.0-beta.1`)
- Add `--verbose` flag for extended version info