project

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 23, 2025 License: MIT Imports: 14 Imported by: 0

README

Project Package

This package provides comprehensive functionality for managing ccmd project files (ccmd.yaml and ccmd-lock.yaml).

Overview

The project package provides three main components:

  1. Configuration Management (schema.go) - Handles ccmd.yaml files
  2. Lock File Management (lock.go) - Handles ccmd-lock.yaml files
  3. Project Manager (manager.go) - High-level API for project operations

Usage

import "github.com/gifflet/ccmd/pkg/project"

// Create a manager for the current directory
manager := project.NewManager(".")

// Initialize a new project
err := manager.InitializeConfig()
if err != nil {
    log.Fatal(err)
}

// Add commands
err = manager.AddCommand("github/cli", "v2.0.0")
err = manager.AddCommand("junegunn/fzf", "latest")

// Update a command version
err = manager.UpdateCommand("github/cli", "v2.1.0")

// Remove a command
err = manager.RemoveCommand("junegunn/fzf")
Direct Configuration File Operations
// Load from file
config, err := project.LoadConfig("ccmd.yaml")
if err != nil {
    log.Fatal(err)
}

// Save to file
err = project.SaveConfig(config, "ccmd.yaml")
if err != nil {
    log.Fatal(err)
}

// Parse from reader
config, err := project.ParseConfig(reader)
if err != nil {
    log.Fatal(err)
}

// Write to writer
err = project.WriteConfig(config, writer)
if err != nil {
    log.Fatal(err)
}
Lock File Operations
// Create new lock file
lockFile := project.NewLockFile()

// Add a command
cmd := &project.Command{
    Name:         "gh",
    Repository:   "github/cli",  
    Version:      "v2.0.0",
    CommitHash:   "abc123...", // 40 char SHA
    InstalledAt:  time.Now(),
    UpdatedAt:    time.Now(),
    FileSize:     10485760,
    Checksum:     "sha256...", // 64 char SHA256
}
err := lockFile.AddCommand(cmd)

// Save to disk
err = lockFile.SaveToFile("ccmd-lock.yaml")

// Load from disk
lockFile, err = project.LoadFromFile("ccmd-lock.yaml")

Schema Structure

The ccmd.yaml file has a simple structure:

commands:
  - repo: owner/repository
    version: v1.0.0  # optional
Fields
  • commands: List of command declarations (required)
    • repo: Repository in format "owner/repository" (required)
    • version: Version specification (optional, defaults to "latest")
Version Specification

The version field supports:

  • Semantic versions: v1.0.0, 1.2.3, v2.0.0-beta.1
  • Branch names: main, develop, feature/xyz
  • Tag names: release-1.0, stable
  • Special value: latest (default)
Validation Rules
  1. At least one command must be defined
  2. Repository must be in "owner/repo" format
  3. Owner and repo names must contain only alphanumeric characters, hyphens, and underscores
  4. Version format is validated for basic correctness

Lock File Structure

The ccmd-lock.yaml file records installed command metadata:

version: "1.0"
updated_at: 2024-01-01T00:00:00Z
commands:
  gh:
    name: gh
    repository: github/cli
    version: v2.0.0
    commit_hash: 1234567890abcdef1234567890abcdef12345678
    installed_at: 2024-01-01T00:00:00Z
    updated_at: 2024-01-01T00:00:00Z
    file_size: 10485760
    checksum: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
    dependencies: []
    metadata:
      description: GitHub CLI

Examples

Complete ccmd.yaml Example
commands:
  # Semantic version
  - repo: example/tool
    version: v1.2.3
  
  # Latest version (explicit)
  - repo: org/cli
    version: latest
  
  # Branch
  - repo: user/command
    version: develop
  
  # Default to latest (version omitted)
  - repo: tools/formatter
Working with Commands
// Parse owner and repo from command
cmd := config.Commands[0]
owner, repo, err := cmd.ParseOwnerRepo()
fmt.Printf("Owner: %s, Repo: %s\n", owner, repo)

// Check if using semantic version
if cmd.IsSemanticVersion() {
    fmt.Println("Using semantic version:", cmd.Version)
}

// Calculate file checksum
checksum, err := project.CalculateChecksum("/path/to/binary")
if err != nil {
    log.Fatal(err)
}

Features

Atomic File Operations

All file writes are performed atomically to prevent corruption:

  • Write to temporary file
  • Validate write succeeded
  • Rename temp file to target
  • Clean up on failure
Comprehensive Validation
  • Repository format must be owner/repo
  • Owner and repo names validated for allowed characters
  • Version format validated (semantic version, branch, or tag)
  • Lock file entries validated for completeness
  • Checksums must be 64-char SHA256
  • Commit hashes must be 40-char SHA
Empty Configuration Support

Empty configuration files are valid, making it easy to start new projects:

manager := project.NewManager(".")
err := manager.InitializeConfig() // Creates empty but valid ccmd.yaml

Documentation

Overview

Copyright (c) 2025 Guilherme Silva Sousa Licensed under the MIT License See LICENSE file in the project root for full license information. Package project provides functionality for managing ccmd project files, including parsing and validating ccmd.yaml configuration files and managing the ccmd-lock.yaml lock file format.

Configuration File (ccmd.yaml)

The ccmd.yaml file declares Claude commands required by a project. It should be placed at the root of your project directory.

Schema Format:

The ccmd.yaml file has a simple structure:

commands:
  - owner/repository@v1.0.0
  - owner/repository        # version defaults to latest

Example ccmd.yaml:

commands:
  - example/claude-command@v1.2.3
  - another/command@latest
  - org/tool                      # version omitted, defaults to latest

Command Format:

Commands are specified as strings in the format "owner/repository@version" where:

  • owner/repository: GitHub repository path
  • @version: Optional version specifier (defaults to latest if omitted)

Version Specification:

The version after @ supports:

  • Semantic versions: v1.0.0, 1.2.3, v2.0.0-beta.1
  • Branch names: main, develop, feature/xyz
  • Tag names: release-1.0, stable
  • "latest" or omitted: uses the latest release

Repository Format:

The repository part must be in the format "owner/repository" where:

  • owner: GitHub username or organization
  • repository: Repository name

Both must contain only alphanumeric characters, hyphens, and underscores.

Lock File Format (ccmd-lock.yaml)

The ccmd-lock.yaml file tracks exact versions and metadata for installed commands. It ensures reproducible installations by locking specific commit hashes and providing integrity verification through checksums.

Example ccmd-lock.yaml:

version: "1.0"
updated_at: 2024-01-15T10:30:00Z
commands:
  gh:
    name: gh
    repository: github.com/cli/cli
    version: v2.40.0
    commit_hash: abc123def456abc123def456abc123def456abc1
    installed_at: 2024-01-10T14:22:00Z
    updated_at: 2024-01-15T10:30:00Z
    file_size: 45678901
    checksum: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
    dependencies:
      - git
    metadata:
      arch: amd64
      os: darwin
  cobra-cli:
    name: cobra-cli
    repository: github.com/spf13/cobra-cli
    version: v1.3.0
    commit_hash: def456abc123def456abc123def456abc123def4
    installed_at: 2024-01-12T09:15:00Z
    updated_at: 2024-01-12T09:15:00Z
    file_size: 12345678
    checksum: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890

Lock File Fields:

  • version: Lock file format version (currently "1.0")
  • updated_at: Last modification timestamp of the lock file
  • commands: Map of installed commands by name

Command Fields:

  • name: Command name (must match the map key)
  • repository: Source repository URL
  • version: Version specifier used during installation (tag, branch, or commit)
  • commit_hash: Exact 40-character git commit SHA
  • installed_at: Initial installation timestamp
  • updated_at: Last update timestamp
  • file_size: Size of the installed binary in bytes
  • checksum: SHA256 hash of the installed binary (64 characters)
  • dependencies: Optional list of runtime dependencies
  • metadata: Optional key-value pairs for additional information

The lock file ensures that:

  • Installations can be reproduced exactly using the commit hash
  • Binary integrity can be verified using the checksum
  • Installation history is tracked with timestamps
  • Dependencies and metadata provide context for troubleshooting

Copyright (c) 2025 Guilherme Silva Sousa Licensed under the MIT License See LICENSE file in the project root for full license information.

Copyright (c) 2025 Guilherme Silva Sousa Licensed under the MIT License See LICENSE file in the project root for full license information. Package project provides functionality for managing ccmd project files.

Copyright (c) 2025 Guilherme Silva Sousa Licensed under the MIT License See LICENSE file in the project root for full license information.

Copyright (c) 2025 Guilherme Silva Sousa Licensed under the MIT License See LICENSE file in the project root for full license information.

Copyright (c) 2025 Guilherme Silva Sousa Licensed under the MIT License See LICENSE file in the project root for full license information.

Copyright (c) 2025 Guilherme Silva Sousa Licensed under the MIT License See LICENSE file in the project root for full license information.

Example
package main

import (
	"fmt"
	"log"
	"path/filepath"
	"strings"
	"time"

	"github.com/gifflet/ccmd/internal/fs"
	"github.com/gifflet/ccmd/pkg/project"
)

func main() {
	// Create a new lock file
	lockFile := project.NewLockFile()

	// Add a command entry
	ghCmd := &project.CommandLockInfo{
		Name:         "gh",
		Source:       "github.com/cli/cli",
		Version:      "v2.40.0",
		Commit:       strings.Repeat("a", 40), // Example SHA
		Resolved:     "github.com/cli/cli@v2.40.0",
		InstalledAt:  time.Now(),
		UpdatedAt:    time.Now(),
		Dependencies: []string{"git"},
		Metadata: map[string]string{
			"arch": "amd64",
			"os":   "darwin",
		},
	}

	if err := lockFile.AddCommand(ghCmd); err != nil {
		log.Fatal(err)
	}

	// Save to file
	lockFilePath := filepath.Join("/tmp", "ccmd-lock.yaml")
	fileSystem := fs.OS{}
	if err := lockFile.SaveToFile(lockFilePath, fileSystem); err != nil {
		log.Fatal(err)
	}

	// Load from file
	loaded, err := project.LoadFromFile(lockFilePath, fileSystem)
	if err != nil {
		log.Fatal(err)
	}

	// Retrieve a command
	if cmd, exists := loaded.GetCommand("gh"); exists {
		fmt.Printf("Command: %s\n", cmd.Name)
		fmt.Printf("Version: %s\n", cmd.Version)
		fmt.Printf("Repository: %s\n", cmd.Source)
	}

}
Output:
Command: gh
Version: v2.40.0
Repository: github.com/cli/cli

Index

Examples

Constants

View Source
const (
	// ConfigFileName is the default name for the configuration file
	ConfigFileName = "ccmd.yaml"
	// LockFileName is the default name for the lock file
	LockFileName = "ccmd-lock.yaml"
)
View Source
const LockFileVersion = "1.0"

LockFileVersion represents the current version of the lock file format

Variables

This section is empty.

Functions

func CalculateChecksum

func CalculateChecksum(filepath string) (string, error)

CalculateChecksum calculates the SHA256 checksum of a file

Example
package main

import (
	"fmt"

	"github.com/gifflet/ccmd/pkg/project"
)

func main() {
	// In practice, this would be the path to an installed binary
	binaryPath := "/usr/local/bin/gh"

	// Calculate the SHA256 checksum
	checksum, err := project.CalculateChecksum(binaryPath)
	if err != nil {
		// Handle error - file might not exist in this example
		fmt.Println("Error calculating checksum")
		return
	}

	fmt.Printf("Checksum length: %d characters\n", len(checksum))
	// Output would be: Checksum length: 64 characters
}

func SaveConfig

func SaveConfig(config *Config, path string, fileSystem fs.FileSystem) error

SaveConfig saves a Config to a ccmd.yaml file

func WriteConfig

func WriteConfig(config *Config, w io.Writer) error

WriteConfig writes a Config to an io.Writer

Types

type Command

type Command = CommandLockInfo

Command is kept for compatibility during migration

type CommandLockInfo

type CommandLockInfo struct {
	Name         string            `yaml:"name"`
	Version      string            `yaml:"version"`
	Source       string            `yaml:"source"`
	Resolved     string            `yaml:"resolved"`
	Commit       string            `yaml:"commit,omitempty"`
	InstalledAt  time.Time         `yaml:"installed_at"`
	UpdatedAt    time.Time         `yaml:"updated_at"`
	Dependencies []string          `yaml:"dependencies,omitempty"`
	Metadata     map[string]string `yaml:"metadata,omitempty"`
}

CommandLockInfo represents a locked command entry according to PRD

func (*CommandLockInfo) Validate

func (c *CommandLockInfo) Validate() error

Validate validates a command entry

type Config

type Config struct {
	// Project metadata
	Name        string   `yaml:"name,omitempty"`
	Version     string   `yaml:"version,omitempty"`
	Description string   `yaml:"description,omitempty"`
	Author      string   `yaml:"author,omitempty"`
	Repository  string   `yaml:"repository,omitempty"`
	Entry       string   `yaml:"entry,omitempty"`
	Tags        []string `yaml:"tags,omitempty"`

	// Commands can be either strings or ConfigCommand objects
	Commands interface{} `yaml:"commands"`
}

Config represents the ccmd.yaml configuration file structure

func LoadConfig

func LoadConfig(path string, fileSystem fs.FileSystem) (*Config, error)

LoadConfig loads and parses a ccmd.yaml file

func ParseConfig

func ParseConfig(r io.Reader) (*Config, error)

ParseConfig parses ccmd.yaml content from a reader

func (*Config) GetCommands

func (c *Config) GetCommands() ([]ConfigCommand, error)

GetCommands returns a normalized list of ConfigCommand objects

func (*Config) Validate

func (c *Config) Validate() error

Validate performs validation on the Config

type ConfigCommand

type ConfigCommand struct {
	Repo    string `yaml:"repo"`
	Version string `yaml:"version,omitempty"`
}

ConfigCommand represents a single command declaration in ccmd.yaml

func (*ConfigCommand) IsSemanticVersion

func (c *ConfigCommand) IsSemanticVersion() bool

IsSemanticVersion checks if the version is a semantic version

func (*ConfigCommand) ParseOwnerRepo

func (c *ConfigCommand) ParseOwnerRepo() (owner, repo string, err error)

ParseOwnerRepo extracts owner and repo name from the repo field

func (*ConfigCommand) Validate

func (c *ConfigCommand) Validate() error

Validate performs validation on a ConfigCommand

type LockFile

type LockFile struct {
	Version         string                      `yaml:"version"`
	LockfileVersion int                         `yaml:"lockfileVersion"`
	Commands        map[string]*CommandLockInfo `yaml:"commands"`
}

LockFile represents the ccmd-lock.yaml file structure

func LoadFromFile

func LoadFromFile(filepath string, fileSystem fs.FileSystem) (*LockFile, error)

LoadFromFile loads a lock file from disk

func NewLockFile

func NewLockFile() *LockFile

NewLockFile creates a new lock file with the current version

func (*LockFile) AddCommand

func (lf *LockFile) AddCommand(cmd *Command) error

AddCommand adds or updates a command in the lock file

Example
package main

import (
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/gifflet/ccmd/pkg/project"
)

func main() {
	lockFile := project.NewLockFile()

	// Add multiple commands
	commands := []*project.CommandLockInfo{
		{
			Name:        "cobra-cli",
			Source:      "github.com/spf13/cobra-cli",
			Version:     "v1.3.0",
			Commit:      strings.Repeat("c", 40),
			Resolved:    "github.com/spf13/cobra-cli@v1.3.0",
			InstalledAt: time.Now(),
			UpdatedAt:   time.Now(),
		},
		{
			Name:        "golangci-lint",
			Source:      "github.com/golangci/golangci-lint",
			Version:     "v1.55.0",
			Commit:      strings.Repeat("e", 40),
			Resolved:    "github.com/golangci/golangci-lint@v1.55.0",
			InstalledAt: time.Now(),
			UpdatedAt:   time.Now(),
		},
	}

	for _, cmd := range commands {
		if err := lockFile.AddCommand(cmd); err != nil {
			log.Printf("Failed to add %s: %v", cmd.Name, err)
		}
	}

	fmt.Printf("Total commands: %d\n", len(lockFile.Commands))
}
Output:
Total commands: 2

func (*LockFile) GetCommand

func (lf *LockFile) GetCommand(name string) (*Command, bool)

GetCommand retrieves a command by name

func (*LockFile) ListCommands

func (lf *LockFile) ListCommands() ([]*CommandLockInfo, error)

ListCommands returns a list of all commands in the lock file

func (*LockFile) RemoveCommand

func (lf *LockFile) RemoveCommand(name string) bool

RemoveCommand removes a command from the lock file

func (*LockFile) SaveToFile

func (lf *LockFile) SaveToFile(filepath string, fileSystem fs.FileSystem) error

SaveToFile saves the lock file to disk

func (*LockFile) SetCommand

func (lf *LockFile) SetCommand(name string, cmd *CommandLockInfo)

SetCommand sets or updates a command in the lock file (compatibility method)

func (*LockFile) Validate

func (lf *LockFile) Validate() error

Validate validates the lock file structure

type LockManager

type LockManager struct {
	// contains filtered or unexported fields
}

LockManager provides compatibility wrapper for old internal/lock.Manager

func NewLockManager

func NewLockManager(dir string) *LockManager

NewLockManager creates a new lock manager for the given directory

func NewLockManagerWithFS

func NewLockManagerWithFS(dir string, fileSystem fs.FileSystem) *LockManager

NewLockManagerWithFS creates a new lock manager with a file system (for testing)

func (*LockManager) AddCommand

func (m *LockManager) AddCommand(cmd interface{}) error

AddCommand adds a command to the lock file

func (*LockManager) GetCommand

func (m *LockManager) GetCommand(name string) (*CommandLockInfo, error)

GetCommand retrieves a command by name

func (*LockManager) HasCommand

func (m *LockManager) HasCommand(name string) bool

HasCommand checks if a command exists in the lock file

func (*LockManager) ListCommands

func (m *LockManager) ListCommands() ([]*CommandLockInfo, error)

ListCommands returns all commands in the lock file

func (*LockManager) Load

func (m *LockManager) Load() error

Load reads the lock file from disk

func (*LockManager) RemoveCommand

func (m *LockManager) RemoveCommand(name string) error

RemoveCommand removes a command from the lock file

func (*LockManager) Save

func (m *LockManager) Save() error

Save writes the lock file to disk

func (*LockManager) UpdateCommand

func (m *LockManager) UpdateCommand(cmd interface{}) error

UpdateCommand updates an existing command

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager provides high-level operations for project file management

func NewManager

func NewManager(rootDir string) *Manager

NewManager creates a new project manager for the given directory

func NewManagerWithFS

func NewManagerWithFS(rootDir string, fileSystem fs.FileSystem) *Manager

NewManagerWithFS creates a new project manager with a custom filesystem

func (*Manager) AddCommand

func (m *Manager) AddCommand(repo, version string) error

AddCommand adds a new command to the configuration

func (*Manager) CommandExists

func (m *Manager) CommandExists(name string) (bool, error)

CommandExists checks if a command is installed in the project

func (*Manager) ConfigExists

func (m *Manager) ConfigExists() bool

ConfigExists checks if the config file exists

func (*Manager) ConfigPath

func (m *Manager) ConfigPath() string

ConfigPath returns the full path to the config file

func (*Manager) InitializeConfig

func (m *Manager) InitializeConfig() error

InitializeConfig creates a new config file with default values

func (*Manager) InitializeLock

func (m *Manager) InitializeLock() error

InitializeLock creates a new lock file

func (*Manager) LoadConfig

func (m *Manager) LoadConfig() (*Config, error)

LoadConfig loads the project configuration file

func (*Manager) LoadLegacyLockFile

func (m *Manager) LoadLegacyLockFile() (*LockFile, error)

LoadLegacyLockFile loads commands.lock JSON file for migration

func (*Manager) LoadLockFile

func (m *Manager) LoadLockFile() (*LockFile, error)

LoadLockFile loads the project lock file

func (*Manager) LockExists

func (m *Manager) LockExists() bool

LockExists checks if the lock file exists

func (*Manager) LockPath

func (m *Manager) LockPath() string

LockPath returns the full path to the lock file

func (*Manager) MigrateLegacyLockFile

func (m *Manager) MigrateLegacyLockFile() error

MigrateLegacyLockFile migrates from commands.lock to ccmd-lock.yaml

func (*Manager) RemoveCommand

func (m *Manager) RemoveCommand(repo string) error

RemoveCommand removes a command from the configuration

func (*Manager) SaveConfig

func (m *Manager) SaveConfig(config *Config) error

SaveConfig saves the project configuration file

func (*Manager) SaveLockFile

func (m *Manager) SaveLockFile(lockFile *LockFile) error

SaveLockFile saves the project lock file

func (*Manager) Sync

func (m *Manager) Sync() error

Sync ensures the lock file is in sync with the configuration This is a placeholder for future implementation that would: 1. Check all commands in config are in lock file 2. Remove commands from lock that are not in config 3. Update versions as needed

func (*Manager) UpdateCommand

func (m *Manager) UpdateCommand(repo, newVersion string) error

UpdateCommand updates a command's version in the configuration

func (*Manager) UpdateCommandInLockFile

func (m *Manager) UpdateCommandInLockFile(cmd *Command) error

UpdateCommandInLockFile updates command information in the lock file

type ModelCommand

type ModelCommand struct {
	Name         string            `json:"name"`
	Version      string            `json:"version"`
	Source       string            `json:"source"`
	Repository   string            `json:"repository"`
	CommitHash   string            `json:"commit_hash,omitempty"`
	InstalledAt  time.Time         `json:"installed_at"`
	UpdatedAt    time.Time         `json:"updated_at"`
	FileSize     int64             `json:"file_size,omitempty"`
	Checksum     string            `json:"checksum,omitempty"`
	Dependencies []string          `json:"dependencies,omitempty"`
	Metadata     map[string]string `json:"metadata,omitempty"`
}

ModelCommand provides compatibility with internal/models.Command

func FromCommandLockInfo

func FromCommandLockInfo(cmd *CommandLockInfo) *ModelCommand

FromCommandLockInfo creates ModelCommand from CommandLockInfo

func (*ModelCommand) ToCommandLockInfo

func (c *ModelCommand) ToCommandLockInfo() *CommandLockInfo

ToCommandLockInfo converts ModelCommand to CommandLockInfo

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL