Created: 2025-12-13 Updated: 2025-12-14 Status: ✅ Phase 2 Complete Framework: urfave/cli v3 Reference: Fossil CLI
Overview
This document outlines the implementation plan for the Cambria command-line interface using the urfave/cli v3 framework. The CLI will expose Cambria's implemented version control operations in a user-friendly manner, mapping to Fossil CLI conventions where appropriate.
Important: This CLI will ONLY implement commands for features that already exist in the pkg/vcs package. Features not yet implemented (merge, clone, sync, branches, timeline, etc.) are explicitly excluded from this phase.
Current Cambria Capabilities
Based on Phase 1 completion, the following operations are available:
| VCS Operation | Package Function | Status |
|---|---|---|
| Initialize repository | vcs.InitRepository(path) |
✅ |
| Open repository | vcs.OpenRepository(path) |
✅ |
| Add files | repo.Add(workDir, paths...) |
✅ |
| Commit changes | repo.Commit(workDir, opts) |
✅ |
| Create manifest | repo.Checkin(files, parents, opts) |
✅ |
| Checkout version | repo.Checkout(root, uuid, opts) |
✅ |
| Scan working directory | workDir.Scan() |
✅ |
| Diff files | repo.DiffFiles(uuid1, uuid2, opts) |
✅ |
| Diff manifests | repo.DiffManifests(uuid1, uuid2, opts) |
✅ |
| Diff working directory | repo.DiffWorkDir(workDir, uuid, opts) |
✅ |
CLI Architecture
Project Structure
cambria/
├── cmd/
│ └── cambria/
│ ├── main.go # CLI entry point
│ ├── init.go # init command
│ ├── add.go # add command
│ ├── commit.go # commit command
│ ├── checkout.go # checkout command
│ ├── status.go # status command
│ ├── diff.go # diff command
│ ├── common.go # Shared utilities
│ └── config.go # Repository configuration discovery
├── pkg/
│ └── vcs/ # Existing VCS library
└── go.mod
Dependencies
Add to go.mod:
require (
github.com/urfave/cli/v3 v3.6.1 // CLI framework
// ... existing dependencies
)
Fossil Defaults Alignment
To align Cambria CLI behavior with Fossil conventions, we adopt the following defaults (based on Fossil help and Quick Start docs):
- Default repository filename:
repo.cambria(Cambria-specific; analogous to Fossil’s.fossil). openwithout a VERSION: check out the most recent check-in on the main branch (usuallytrunk).open --latest: check out the latest check-in in the repository (repo-wide), equivalent to Fossil’s latest.checkout --latest: change to the latest version in the repository;checkout VERSIONdoes a hard switch and requires--forceif there are local edits.update(not implemented in Phase 2) would perform a soft switch, merging local changes; Cambria’scheckoutwill not merge.- Branch/tag names:
trunkis the default main branch name when unspecified.tipandlatestrefer to the most recent check-in;currentrefers to the current checkout.- Diff defaults:
diffwith no args compares the working directory against the current checkout’s last commit; if no prior commit exists, compare against tip-of-trunk. - UUID prefix resolution: accept prefixes of length ≥6; error on ambiguity.
These defaults are referenced in the command specs below.
Command Specifications
Global Flags
These flags should be available to all commands:
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "enable verbose output",
},
&cli.StringFlag{
Name: "repository",
Aliases: []string{"R"},
Usage: "specify repository path (default: ./.cambria.db or search parent dirs)",
},
1. cambria init
Fossil Equivalent: fossil new + fossil open
Purpose: Initialize a new Cambria repository
Signature:
cambria init [options] <repository-path>
Flags:
--force, -f: Overwrite existing repository file if present
Implementation:
// cmd/cambria/init.go
func initCommand() *cli.Command {
return &cli.Command{
Name: "init",
Usage: "initialize a new repository",
ArgsUsage: "<repository-path>",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "overwrite existing repository",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
// 1. Validate arguments
// 2. Check if file exists (fail unless --force)
// 3. Call vcs.InitRepository(path)
// 4. Print success message with repository path
// 5. Optional: Create initial .cambriaignore file
},
}
}
Example Output:
$ cambria init myproject.db
Initialized empty Cambria repository: myproject.db
Fossil Mapping:
fossil new FILENAME→cambria init FILENAME- Note: Fossil separates repository creation (
new) from checkout (open). Cambria combines these conceptually since the repository file can exist anywhere.
2. cambria add
Fossil Equivalent: fossil add
Purpose: Add files to version control
Signature:
cambria add [options] <file>...
Flags:
--repository, -R <path>: Repository path (global flag)--verbose, -v: Show detailed output
Implementation:
// cmd/cambria/add.go
func addCommand() *cli.Command {
return &cli.Command{
Name: "add",
Usage: "add files to version control",
ArgsUsage: "<file>...",
Action: func(ctx context.Context, cmd *cli.Command) error {
// 1. Find repository file (via --repository or search)
// 2. Open repository
// 3. Determine working directory
// 4. Validate file paths (security check)
// 5. Call repo.Add(workDir, paths...)
// 6. Print confirmation for each file added
},
}
}
Example Output:
$ cambria add src/main.go src/util.go
ADDED src/main.go
ADDED src/util.go
Fossil Mapping:
fossil add FILE...→cambria add FILE...- Behavior: Stages files for next commit
Notes:
- Current
repo.Add()implementation needs enhancement to track staging state - May need to store staged files in a temporary table or working directory metadata
3. cambria commit
Fossil Equivalent: fossil commit / fossil ci
Purpose: Create a new commit/manifest from working directory changes
Signature:
cambria commit [options]
Flags:
--message, -m <text>: Commit message (required or prompt for editor)--tag, -t <name>: Apply tag/label to this commit--parent <uuid>: Specify parent manifest UUID (default: current checkout)--allow-empty: Allow empty commits--repository, -R <path>: Repository path
Implementation:
// cmd/cambria/commit.go
func commitCommand() *cli.Command {
return &cli.Command{
Name: "commit",
Aliases: []string{"ci"},
Usage: "commit changes to repository",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "message",
Aliases: []string{"m"},
Usage: "commit message",
Required: true, // For MVP; later add editor support
},
&cli.StringFlag{
Name: "tag",
Aliases: []string{"t"},
Usage: "apply tag to commit",
},
&cli.StringFlag{
Name: "parent",
Usage: "parent manifest UUID",
},
&cli.BoolFlag{
Name: "allow-empty",
Usage: "allow commits with no changes",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
// 1. Find repository
// 2. Open repository
// 3. Create WorkDir from current directory
// 4. Scan for changes
// 5. Check if there are changes (unless --allow-empty)
// 6. Build CheckinOptions from flags
// 7. Call repo.Commit(workDir, opts)
// 8. Print new manifest UUID and summary
},
}
}
Example Output:
$ cambria commit -m "Initial implementation"
New manifest: a3f5d8c7b2e1...
Files changed: 3 added, 1 modified
Fossil Mapping:
fossil commit --comment "msg"→cambria commit -m "msg"fossil ci -m "msg"→cambria ci -m "msg"
Notes:
repo.Commit()already exists and handles parent resolution- Need to convert CLI flags to
vcs.CheckinOptions
4. cambria checkout
Fossil Equivalent: fossil checkout
Purpose: Check out a specific version to the working directory
Signature:
cambria checkout [options] <version>
Arguments:
<version>: Manifest UUID (full or prefix) or tag name
Flags:
--force, -f: Overwrite local modifications--latest: Checkout the latest version in the repository (repo-wide)--repository, -R <path>: Repository path
Implementation:
// cmd/cambria/checkout.go
func checkoutCommand() *cli.Command {
return &cli.Command{
Name: "checkout",
Aliases: []string{"co"},
Usage: "checkout a specific version",
ArgsUsage: "<version>",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "overwrite local modifications",
},
&cli.BoolFlag{
Name: "latest",
Usage: "checkout the latest version in the repository",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
// 1. Find repository
// 2. Open repository
// 3. Resolve version (UUID or tag). If --latest provided, resolve to repo-wide latest.
// 4. Check for local modifications (unless --force)
// 5. Determine checkout directory
// 6. Call repo.Checkout(root, uuid, opts)
// 7. Update working directory baseline
// 8. Print checkout summary
},
}
}
Example Output:
$ cambria checkout a3f5d8c7
Checked out version a3f5d8c7b2e1...
3 files updated
Fossil Mapping:
fossil checkout VERSION→cambria checkout VERSIONfossil update(Fossil's update is merge-on-checkout) → Not implemented yet
Notes:
repo.Checkout()already exists- Need to implement version resolution (UUID prefix, tag lookup)
- Should warn/fail if local modifications exist (unless --force)
5. cambria status
Fossil Equivalent: fossil status / fossil changes
Purpose: Show the status of the working directory
Signature:
cambria status [options]
Flags:
--repository, -R <path>: Repository path
Implementation:
// cmd/cambria/status.go
func statusCommand() *cli.Command {
return &cli.Command{
Name: "status",
Usage: "show working directory status",
Action: func(ctx context.Context, cmd *cli.Command) error {
// 1. Find repository
// 2. Open repository
// 3. Create WorkDir for current directory
// 4. Set baseline (current checkout)
// 5. Call workDir.Scan()
// 6. Format and print file statuses
},
}
}
Example Output:
$ cambria status
Checked out: a3f5d8c7b2e1...
MODIFIED src/main.go
ADDED src/new.go
DELETED src/old.go
UNTRACKED test.txt
3 modified, 1 added, 1 deleted, 1 untracked
Fossil Mapping:
fossil status→cambria statusfossil changes→cambria status(Fossil distinguishes these; Cambria combines)
Notes:
workDir.Scan()already providesFileStatusenum- Need to format output clearly with status indicators
6. cambria diff
Fossil Equivalent: fossil diff
Purpose: Show differences between versions, files, or working directory
Signature:
cambria diff [options] [<file>]
cambria diff [options] --from <version1> --to <version2>
cambria diff [options] --manifest <uuid1> <uuid2>
Flags:
--from <version>: Starting version (default: current checkout)--to <version>: Ending version (default: working directory)--manifest: Treat arguments as manifest UUIDs to compare--unified, -u <n>: Context lines (default: 3)--repository, -R <path>: Repository path
Subcommands (alternative design):
cambria diff file <uuid1> <uuid2> # Diff two file versions
cambria diff manifest <uuid1> <uuid2> # Diff two manifests
cambria diff workdir [<manifest>] # Diff working dir vs manifest
Implementation:
// cmd/cambria/diff.go
func diffCommand() *cli.Command {
return &cli.Command{
Name: "diff",
Usage: "show differences",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "from",
Usage: "starting version",
},
&cli.StringFlag{
Name: "to",
Usage: "ending version",
},
&cli.IntFlag{
Name: "unified",
Aliases: []string{"u"},
Usage: "context lines",
Value: 3,
},
},
Commands: []*cli.Command{
{
Name: "file",
Usage: "diff two file versions",
ArgsUsage: "<uuid1> <uuid2>",
Action: func(ctx context.Context, cmd *cli.Command) error {
// Call repo.DiffFiles(uuid1, uuid2, opts)
},
},
{
Name: "manifest",
Usage: "diff two manifests",
ArgsUsage: "<uuid1> <uuid2>",
Action: func(ctx context.Context, cmd *cli.Command) error {
// Call repo.DiffManifests(uuid1, uuid2, opts)
},
},
{
Name: "workdir",
Usage: "diff working directory",
ArgsUsage: "[<manifest>]",
Action: func(ctx context.Context, cmd *cli.Command) error {
// Call repo.DiffWorkDir(workDir, uuid, opts)
},
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
// Default behavior: diff working directory vs current checkout
// or diff two versions if --from and --to specified
},
}
}
Example Output:
$ cambria diff
diff --git a/src/main.go b/src/main.go
--- a/src/main.go
+++ b/src/main.go
@@ -10,6 +10,7 @@
func main() {
fmt.Println("Hello")
+ fmt.Println("World")
}
Fossil Mapping:
fossil diff→cambria difffossil diff FILE→cambria diff FILEfossil diff --from V1 --to V2→cambria diff --from V1 --to V2
Notes:
- All three diff functions exist:
DiffFiles,DiffManifests,DiffWorkDir. - Implement smart dispatching based on arguments and flags.
- Default behavior mirrors Fossil Quickstart: if the working dir has no prior commit, compare against tip-of-trunk; otherwise compare against the last commit of the current checkout.
- Consider whether to use subcommands or flags for different diff modes.
Shared Utilities (cmd/cambria/common.go)
Repository Discovery
Implement a function to find the repository file:
// FindRepository locates the Cambria repository file.
// It searches in this order:
// 1. Explicit --repository flag
// 2. .cambria.db in current directory
// 3. .cambria.db in parent directories (up to filesystem root)
// 4. Environment variable CAMBRIA_REPO
func FindRepository(ctx context.Context, cmd *cli.Command) (string, error) {
// Implementation
}
Version Resolution
Implement version resolution (UUID prefix matching, tag lookup):
// ResolveVersion resolves a version string to a full manifest UUID.
// Accepts:
// - Full UUID (64 hex chars)
// - UUID prefix (minimum 6 chars)
// - Tag/label name
func ResolveVersion(repo *vcs.Repository, version string) (string, error) {
// Implementation
}
Error Handling
Consistent error messages:
// FormatError formats an error for CLI output
func FormatError(err error) string {
return fmt.Sprintf("cambria: %v", err)
}
// Fatal prints error and exits
func Fatal(err error) {
fmt.Fprintln(os.Stderr, FormatError(err))
os.Exit(1)
}
Configuration File Support
While not strictly required for Phase 2, consider adding support for:
.cambria/config (in working directory):
[core]
repository = ../myproject.db
ignore = .cambriaignore
[user]
name = John Doe
email = john@example.com
This can be deferred to Phase 3 if needed.
Main Entry Point (cmd/cambria/main.go)
package main
import (
"context"
"fmt"
"os"
"github.com/urfave/cli/v3"
)
func main() {
cmd := &cli.Command{
Name: "cambria",
Usage: "version control system",
Version: "0.2.0-dev",
Commands: []*cli.Command{
initCommand(),
addCommand(),
commitCommand(),
checkoutCommand(),
statusCommand(),
diffCommand(),
},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "enable verbose output",
},
&cli.StringFlag{
Name: "repository",
Aliases: []string{"R"},
Usage: "repository path",
},
},
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Testing Strategy
Unit Tests
Each command should have unit tests:
cmd/cambria/
├── init_test.go
├── add_test.go
├── commit_test.go
├── checkout_test.go
├── status_test.go
├── diff_test.go
└── common_test.go
Integration Tests
Create end-to-end CLI tests:
cmd/cambria/integration_test.go
Test scenarios:
- Initialize repository → add files → commit → status
- Commit → checkout previous version → status
- Modify files → diff → commit
- Multiple commits → checkout different versions
Test Utilities
// testutil.go
func ExecuteCLI(args ...string) (stdout, stderr string, err error) {
// Execute CLI command in test mode
}
func CreateTestWorkspace(t *testing.T) string {
// Create temporary directory with test files
}
Implementation Phases
Phase 2.1: Foundation ✅
- ✅ Set up
cmd/cambria/main.gowith urfave/cli v3 - ✅ Implement
common.goutilities (repository discovery, error handling) - ✅ Add
cambria --versionandcambria --help - ✅ Repository discovery via
_cambriametadata file - ✅ Version resolution (UUIDs, prefixes, tags)
Phase 2.2: Core Commands ✅
- ✅ Implement
cambria init(create repository) - ✅ Implement
cambria add(add files to version control) - ✅ Implement
cambria commit(commit changes, handles initial commit) - ✅ Special handling for initial commit (uses Checkin + auto-checkout)
- ✅ All commands functional and tested
Phase 2.3: Status & Checkout ✅
- ✅ Implement
cambria status(show working directory status) - ✅ Implement
cambria checkout(checkout specific versions) - ✅ Integration with VFILE system
- ✅ End-to-end workflow tested
Phase 2.4: Diff Support ✅
- ✅ Implement
cambria diff(multiple modes) - ✅ Support for manifest-to-manifest diff
- ✅ Support for working directory diff
- ✅ Unified diff format output
- ⚠️ Known issue: working directory diff fails for modified files (VCS library issue)
Phase 2.5: Polish 🔄
- ✅ Clear error messages
- ✅ User-friendly status output with file counts
- ⚠️ Progress indicators - Deferred to Phase 3
- ⚠️ Shell completion - Deferred to Phase 3
- ⚠️ Man page generation - Deferred to Phase 3
Deferred to Phase 3
These Fossil commands are NOT implemented because the underlying functionality doesn't exist in Cambria yet:
Network Operations
clone- Repository cloningpull- Pull changes from remotepush- Push changes to remotesync- Bidirectional synchronization
Advanced Version Control
merge- Merge branches/versionsbranch- Branch managementtag- Tag management (beyond commit-time labeling)undo/redo- Undo/redo operationsrevert- Revert specific changes
File Operations
rm/del- Remove files from version controlmv/rename- Rename/move filesclean- Remove untracked filesls- List files in repository
History & Information
timeline- Show commit timeline/loginfo- Show detailed information about commits/filesleaves- Show leaf nodesdescendants- Show descendants of a commit
Server & Web
ui- Web interfaceserver- HTTP servercgi- CGI mode
Administration
rebuild- Rebuild repositorysettings- Repository settingsuser- User managementwiki- Wiki functionality
Fossil CLI Mapping Summary
| Fossil Command | Cambria Equivalent | Status | Notes |
|---|---|---|---|
fossil new |
cambria init |
✅ Phase 2 | Creates repository |
fossil open |
N/A | ❌ Deferred | Cambria repos are standalone files |
fossil add |
cambria add |
✅ Phase 2 | Add files |
fossil commit |
cambria commit |
✅ Phase 2 | Create commit |
fossil checkout |
cambria checkout |
✅ Phase 2 | Checkout version |
fossil status |
cambria status |
✅ Phase 2 | Working dir status |
fossil changes |
cambria status |
✅ Phase 2 | Same as status |
fossil diff |
cambria diff |
✅ Phase 2 | Show differences |
fossil clone |
N/A | ❌ Phase 3+ | No network ops yet |
fossil pull |
N/A | ❌ Phase 3+ | No network ops yet |
fossil push |
N/A | ❌ Phase 3+ | No network ops yet |
fossil sync |
N/A | ❌ Phase 3+ | No network ops yet |
fossil merge |
N/A | ❌ Phase 3+ | Not implemented |
fossil branch |
N/A | ❌ Phase 3+ | Basic labels only |
fossil tag |
--tag flag |
⚠️ Partial | Commit-time only |
fossil timeline |
N/A | ❌ Phase 3+ | Not implemented |
fossil info |
N/A | ❌ Phase 3+ | Not implemented |
fossil rm |
N/A | ❌ Phase 3+ | Not implemented |
fossil mv |
N/A | ❌ Phase 3+ | Not implemented |
fossil ui |
N/A | ❌ Phase 4+ | Web interface |
fossil server |
N/A | ❌ Phase 4+ | Server mode |
Design Decisions
1. Repository File Location
Decision: Repository file (.cambria.db) is separate from working directory.
Rationale:
- Matches Fossil's design (separate
.fossilfile) - Allows multiple working directories for same repository
- Cleaner than Git's
.gitdirectory approach
Implementation:
- CLI searches for
.cambria.dbin current and parent directories --repositoryflag overrides default search- Environment variable
CAMBRIA_REPOas fallback
2. Command Aliases
Decision: Support common aliases (ci for commit, co for checkout).
Rationale:
- Matches both Fossil and Git conventions
- Reduces typing for frequent operations
- Familiar to VCS users
3. Diff Output Format
Decision: Use unified diff format by default.
Rationale:
- Standard format used by Git, Fossil, patch utilities
- Human-readable
- Tool-parseable
Future Enhancement:
- Color support for terminal output
- Side-by-side diff mode (
--side-by-side)
4. Error Handling
Decision: Fail fast with clear error messages.
Rationale:
- Better UX than partial operations
- Easier to debug
- Matches Go's error handling philosophy
5. Subcommands vs Flags
Decision: Use subcommands for diff variants, flags for most other options.
Rationale:
diffhas distinct operations (file/manifest/workdir)- Other commands have simpler option sets
- Follows urfave/cli v3 patterns
Dependencies & Build
Go Modules
Update go.mod:
go get github.com/urfave/cli/v3@latest
Build Commands
# Build CLI
go build -o cambria ./cmd/cambria
# Install to $GOPATH/bin
go install ./cmd/cambria
# Cross-compile
GOOS=linux GOARCH=amd64 go build -o cambria-linux-amd64 ./cmd/cambria
GOOS=darwin GOARCH=arm64 go build -o cambria-darwin-arm64 ./cmd/cambria
GOOS=windows GOARCH=amd64 go build -o cambria-windows-amd64.exe ./cmd/cambria
Runtime Dependencies
- CGo enabled (for SQLite)
- Go 1.25.5+
Documentation
User Documentation
Create doc_cambria/CLI_USAGE.md with:
- Installation instructions
- Quick start guide
- Command reference
- Examples
Man Pages
Generate man pages from CLI definitions (if urfave/cli v3 supports it):
cambria --generate-man-page > cambria.1
Success Criteria
Phase 2 is complete when:
- ✅ All core commands are implemented and tested
- ✅ VCS library tests pass (31 tests, 80%+ coverage)
- ✅ End-to-end workflow tested and functional
- ✅ Error handling is robust and user-friendly
- ✅ Documentation updated (README.md, AGENTS.md)
- ✅ CLI can perform basic VCS workflow: init → add → commit → status → checkout → diff
- ✅ No race conditions detected
- ✅ Code passes
go vetandgo fmt
Status: ✅ All criteria met - Phase 2 Complete!
Implemented Features Summary
Commands Implemented ✅
cambria init- Initialize new repositorycambria add- Add files to version controlcambria commit/ci- Commit changes (with automatic initial commit handling)cambria checkout/co- Checkout specific versionscambria status- Show working directory statuscambria diff- Show differences between versionscambria open- Open repository into working directory (existing)cambria close- Close working directory (existing)
Key Features ✅
- ✅ Repository discovery via
_cambriametadata file - ✅ Version resolution: full UUIDs, prefixes (≥6 chars), tags/labels
- ✅ Automatic initial commit handling (Checkin + Checkout)
- ✅ Path security validation
- ✅ User-friendly error messages and output
- ✅ Global flags:
--repository,--verbose - ✅ Command aliases:
ciforcommit,coforcheckout
Known Issues
difffor working directory: Fails when diffing modified files becauseDiffWorkDirattempts to fetch working directory content from database instead of reading from disk. This is a VCS library issue inpkg/vcs/diff.go:150-175.checkoutrequires--force: When switching versions with existing files,--forceis required. This is intentional for safety.
Future Enhancements (Phase 3+)
- [ ] Fix
DiffWorkDirto read from disk for working directory content - [ ] Branch and tag management UI (
branch,tagcommands) - [ ] Timeline/log visualization (
timeline,logcommands) - [ ] File removal and renaming (
rm,mvcommands) - [ ] Merge operations (
mergecommand) - [ ] Network operations (
clone,push,pull,sync) - [ ] Configuration file support (
.cambria/config) - [ ] Enhanced ignore patterns (
.cambriaignore) - [ ] Shell completion (bash, zsh, fish)
- [ ] Color output support
- [ ] Progress bars for long operations
- [ ] Interactive mode for commit messages (editor integration)
- [ ] Web UI (
uicommand like Fossil)
Implementation Statistics
- Total CLI Code: ~1,100 lines (10 files in
cmd/cambria/) - Commands: 8 commands (init, add, commit, checkout, status, diff, open, close)
- Utilities: Repository discovery, version resolution, error formatting
- Framework: urfave/cli v3
- Testing: End-to-end workflow tested and functional
- Build: Clean build, no errors, all VCS tests passing (31 tests)