# 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
| 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
# 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)
#### 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
```
2. **Build with commits after tag**
```bash
git tag v0.1.0
cargo build
./target/debug/granite-cli version
```
3. **Build with uncommitted changes**
```bash
cargo build
./target/debug/granite-cli version
```
4. **No tags scenario**
```bash
cargo build
./target/debug/granite-cli version
```
5. **Global flag**
```bash
./target/debug/granite-cli --version
./target/debug/granite-cli -V
```
### 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