cambria

Cambria CLI Implementation Plan
Login

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):

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:

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:


2. cambria add

Fossil Equivalent: fossil add

Purpose: Add files to version control

Signature:

cambria add [options] <file>...

Flags:

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:

Notes:


3. cambria commit

Fossil Equivalent: fossil commit / fossil ci

Purpose: Create a new commit/manifest from working directory changes

Signature:

cambria commit [options]

Flags:

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:

Notes:


4. cambria checkout

Fossil Equivalent: fossil checkout

Purpose: Check out a specific version to the working directory

Signature:

cambria checkout [options] <version>

Arguments:

Flags:

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:

Notes:


5. cambria status

Fossil Equivalent: fossil status / fossil changes

Purpose: Show the status of the working directory

Signature:

cambria status [options]

Flags:

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:

Notes:


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:

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:

Notes:


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:

  1. Initialize repository → add files → commit → status
  2. Commit → checkout previous version → status
  3. Modify files → diff → commit
  4. 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 ✅

Phase 2.2: Core Commands ✅

Phase 2.3: Status & Checkout ✅

Phase 2.4: Diff Support ✅

Phase 2.5: Polish 🔄

Deferred to Phase 3

These Fossil commands are NOT implemented because the underlying functionality doesn't exist in Cambria yet:

Network Operations

Advanced Version Control

File Operations

History & Information

Server & Web

Administration

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:

Implementation:

2. Command Aliases

Decision: Support common aliases (ci for commit, co for checkout).

Rationale:

3. Diff Output Format

Decision: Use unified diff format by default.

Rationale:

Future Enhancement:

4. Error Handling

Decision: Fail fast with clear error messages.

Rationale:

5. Subcommands vs Flags

Decision: Use subcommands for diff variants, flags for most other options.

Rationale:

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

Documentation

User Documentation

Create doc_cambria/CLI_USAGE.md with:

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:

Status: ✅ All criteria met - Phase 2 Complete!

Implemented Features Summary

Commands Implemented ✅

  1. cambria init - Initialize new repository
  2. cambria add - Add files to version control
  3. cambria commit / ci - Commit changes (with automatic initial commit handling)
  4. cambria checkout / co - Checkout specific versions
  5. cambria status - Show working directory status
  6. cambria diff - Show differences between versions
  7. cambria open - Open repository into working directory (existing)
  8. cambria close - Close working directory (existing)

Key Features ✅

Known Issues

  1. diff for working directory: Fails when diffing modified files because DiffWorkDir attempts to fetch working directory content from database instead of reading from disk. This is a VCS library issue in pkg/vcs/diff.go:150-175.

  2. checkout requires --force: When switching versions with existing files, --force is required. This is intentional for safety.

Future Enhancements (Phase 3+)

Implementation Statistics