Project Management CLI

Cross-platform command-line tool for managing projects, tasks, and workflows with offline support

Project Management CLI (pmcli)

A powerful, cross-platform command-line tool built with Rust that provides comprehensive project management capabilities including task tracking, time management, and workflow automation with both online and offline support.

OSpec Definition

ospec_version: "1.0.0"
id: "project-management-cli"
name: "Project Management CLI (pmcli)"
description: "Cross-platform CLI tool for project management with task tracking, time logging, and workflow automation"
outcome_type: "cli"

acceptance:
  commands:
    # Core help and version
    - name: "pmcli --help"
      exit_code: 0
      output_contains: ["Usage:", "Commands:", "Options:"]
      max_execution_time_ms: 1000

    - name: "pmcli --version"
      exit_code: 0
      output_format: "semver"
      max_execution_time_ms: 500

    # Project management
    - name: "pmcli project create"
      subcommands:
        - "pmcli project create --name 'Test Project' --description 'A test project'"
          exit_code: 0
          output_contains: ["Created project", "Test Project"]
        
        - "pmcli project create --name 'Test' --template web-app"
          exit_code: 0
          output_contains: ["Created project from template"]

    - name: "pmcli project list"
      exit_code: 0
      output_format: "table"
      columns: ["ID", "Name", "Status", "Tasks", "Created"]

    - name: "pmcli project show <id>"
      exit_code: 0
      output_contains: ["Project Details", "Tasks:", "Statistics:"]

    # Task management
    - name: "pmcli task add"
      subcommands:
        - "pmcli task add --title 'New Task' --priority high"
          exit_code: 0
          output_contains: ["Created task", "New Task"]
        
        - "pmcli task add --title 'Feature' --project 1 --due 2024-12-31"
          exit_code: 0
          output_contains: ["Created task", "Feature"]

    - name: "pmcli task list"
      exit_code: 0
      output_format: "table"
      filters:
        - "--status pending"
        - "--priority high"
        - "--project 1"
        - "--assigned-to me"

    - name: "pmcli task complete <id>"
      exit_code: 0
      output_contains: ["Task completed"]

    # Time tracking
    - name: "pmcli time start <task_id>"
      exit_code: 0
      output_contains: ["Timer started for task"]

    - name: "pmcli time stop"
      exit_code: 0
      output_contains: ["Timer stopped", "Duration:"]

    - name: "pmcli time report"
      exit_code: 0
      output_format: "table"
      columns: ["Date", "Task", "Duration", "Project"]

    # Configuration
    - name: "pmcli config set <key> <value>"
      exit_code: 0
      output_contains: ["Configuration updated"]

    - name: "pmcli config get <key>"
      exit_code: 0
      max_execution_time_ms: 500

    # Sync and backup
    - name: "pmcli sync"
      exit_code: 0
      output_contains: ["Sync completed"]
      timeout_ms: 10000

    - name: "pmcli backup create"
      exit_code: 0
      output_contains: ["Backup created"]

  performance:
    startup_time_ms: 100
    command_response_time_ms: 1000
    large_dataset_response_ms: 5000
    memory_usage_mb_max: 50
    binary_size_mb_max: 20

  cross_platform:
    supported_os: ["linux", "macos", "windows"]
    supported_architectures: ["x86_64", "aarch64"]
    
  offline_support:
    local_database: true
    sync_conflict_resolution: true
    offline_time_tracking: true

  data_formats:
    export_formats: ["json", "csv", "markdown"]
    import_formats: ["json", "csv", "trello", "asana"]

  integrations:
    git_integration: true
    editor_plugins: ["vscode", "vim", "emacs"]
    shell_completions: ["bash", "zsh", "fish", "powershell"]

  tests:
    - file: "tests/unit/**/*.rs"
      type: "unit"
      coverage_threshold: 0.8
    - file: "tests/integration/**/*.rs"
      type: "integration"
      coverage_threshold: 0.7
    - file: "tests/cli/**/*.rs"
      type: "cli"
      coverage_threshold: 0.6

stack:
  language: "Rust 1.70+"
  cli_framework: "clap 4.0"
  database: "SQLite with rusqlite"
  serialization: "serde with serde_json"
  http_client: "reqwest"
  async_runtime: "tokio"
  testing: "cargo test + assert_cmd"
  packaging: "cargo"
  cross_compilation: "cross"
  configuration: "config-rs"
  logging: "tracing + tracing-subscriber"

guardrails:
  tests_required: true
  min_test_coverage: 0.8
  lint: true
  security_scan: true
  dependency_check: true
  cross_platform_testing: true
  performance_benchmarks: true
  license_whitelist: ["MIT", "Apache-2.0", "BSD-3-Clause"]

prompts:
  implementer: |
    You are building a professional CLI tool with Rust and clap.
    
    Requirements:
    - Follow Rust best practices and idioms
    - Use proper error handling with Result types
    - Implement comprehensive logging with tracing
    - Create intuitive command structure and help text
    - Support both interactive and scripting modes
    - Include progress bars for long operations
    - Implement proper signal handling (Ctrl+C)
    - Add shell completion generation
    - Use structured logging for debugging
    - Follow semantic versioning
    - Cross-platform compatibility
    - Offline-first design with sync capabilities

scripts:
  setup: "scripts/setup.sh"
  build: "cargo build --release"
  test: "cargo test"
  lint: "cargo clippy -- -D warnings"
  format: "cargo fmt --check"
  benchmark: "cargo bench"
  cross_compile: "scripts/cross-compile.sh"
  package: "scripts/package.sh"
  install: "cargo install --path ."

secrets:
  provider: "env://local"
  optional:
    - "API_TOKEN"
    - "SYNC_ENDPOINT"
    - "ANALYTICS_TOKEN"

metadata:
  estimated_effort: "4-6 days"
  complexity: "intermediate"
  tags: ["cli", "rust", "project-management", "sqlite", "cross-platform"]
  version: "1.0.0"
  maintainers:
    - name: "CLI Team"
      email: "cli-team@company.com"

Features

Core Features

  • Project Management: Create, organize, and track multiple projects
  • Task Management: Full CRUD operations for tasks with priorities and due dates
  • Time Tracking: Built-in timer for tracking work sessions
  • Offline Support: Works completely offline with optional sync
  • Cross-Platform: Native binaries for Linux, macOS, and Windows
  • Git Integration: Automatic task creation from commit messages
  • Export/Import: Multiple format support (JSON, CSV, Markdown)
  • Templates: Project templates for common workflows

Technical Features

  • Fast Performance: Rust’s speed with sub-100ms startup time
  • Low Memory Usage: Efficient memory usage under 50MB
  • Shell Completions: Auto-completion for all major shells
  • Structured Logging: Comprehensive logging with tracing
  • Configuration Management: Flexible configuration system
  • Progress Indicators: Visual progress bars for long operations
  • Error Recovery: Graceful error handling and recovery

Architecture

Project Structure

pmcli/
├── src/
│   ├── main.rs              # CLI entry point and argument parsing
│   ├── lib.rs               # Library exports
│   ├── cli/                 # CLI command handlers
│   │   ├── mod.rs
│   │   ├── project.rs       # Project commands
│   │   ├── task.rs          # Task commands  
│   │   ├── time.rs          # Time tracking commands
│   │   ├── sync.rs          # Sync commands
│   │   ├── config.rs        # Configuration commands
│   │   └── export.rs        # Export/import commands
│   ├── core/                # Core business logic
│   │   ├── mod.rs
│   │   ├── project.rs       # Project management
│   │   ├── task.rs          # Task management
│   │   ├── timer.rs         # Time tracking
│   │   └── sync.rs          # Synchronization
│   ├── db/                  # Database layer
│   │   ├── mod.rs
│   │   ├── connection.rs    # Database connection
│   │   ├── migrations.rs    # Schema migrations
│   │   ├── models.rs        # Data models
│   │   └── queries.rs       # Database queries
│   ├── config/              # Configuration management
│   │   ├── mod.rs
│   │   └── settings.rs
│   ├── sync/                # Synchronization
│   │   ├── mod.rs
│   │   ├── client.rs        # HTTP sync client
│   │   └── conflict.rs      # Conflict resolution
│   ├── utils/               # Utilities
│   │   ├── mod.rs
│   │   ├── error.rs         # Error types
│   │   ├── time.rs          # Time utilities
│   │   └── format.rs        # Output formatting
│   └── integrations/        # External integrations
│       ├── mod.rs
│       ├── git.rs           # Git integration
│       └── editor.rs        # Editor integration
├── tests/
│   ├── unit/
│   ├── integration/
│   └── cli/
├── benches/                 # Performance benchmarks
├── scripts/                 # Build and utility scripts
├── completions/             # Shell completion files
├── docs/
├── Cargo.toml
└── README.md

Database Schema

-- SQLite schema for local storage
CREATE TABLE projects (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT,
    status TEXT DEFAULT 'active',
    template TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    sync_id TEXT UNIQUE,
    last_synced DATETIME
);

CREATE TABLE tasks (
    id INTEGER PRIMARY KEY,
    project_id INTEGER REFERENCES projects(id),
    title TEXT NOT NULL,
    description TEXT,
    status TEXT DEFAULT 'pending',
    priority TEXT DEFAULT 'medium',
    due_date DATETIME,
    completed_at DATETIME,
    estimated_hours REAL,
    actual_hours REAL DEFAULT 0.0,
    tags TEXT, -- JSON array
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    sync_id TEXT UNIQUE,
    last_synced DATETIME
);

CREATE TABLE time_entries (
    id INTEGER PRIMARY KEY,
    task_id INTEGER REFERENCES tasks(id),
    start_time DATETIME NOT NULL,
    end_time DATETIME,
    duration_seconds INTEGER,
    description TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    sync_id TEXT UNIQUE,
    last_synced DATETIME
);

CREATE TABLE sync_log (
    id INTEGER PRIMARY KEY,
    operation TEXT NOT NULL,
    table_name TEXT NOT NULL,
    record_id TEXT NOT NULL,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    status TEXT DEFAULT 'pending'
);

-- Indexes for performance
CREATE INDEX idx_tasks_project_id ON tasks(project_id);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_time_entries_task_id ON time_entries(task_id);
CREATE INDEX idx_sync_log_status ON sync_log(status);

Implementation Examples

Main CLI Structure

// src/main.rs
use clap::{Arg, ArgMatches, Command};
use tracing::{info, error};
use pmcli::cli::{project, task, time, config, sync};
use pmcli::utils::error::{Result, CliError};
use pmcli::config::Settings;

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    // Load configuration
    let settings = Settings::load()?;

    // Build CLI
    let app = Command::new("pmcli")
        .version(env!("CARGO_PKG_VERSION"))
        .author("CLI Team <cli-team@company.com>")
        .about("A powerful project management CLI tool")
        .subcommand_required(true)
        .arg_required_else_help(true)
        .subcommand(project::build_command())
        .subcommand(task::build_command())
        .subcommand(time::build_command())
        .subcommand(config::build_command())
        .subcommand(sync::build_command())
        .subcommand(
            Command::new("completions")
                .about("Generate shell completions")
                .arg(
                    Arg::new("shell")
                        .help("Shell to generate completions for")
                        .required(true)
                        .value_parser(["bash", "zsh", "fish", "powershell"])
                )
        );

    let matches = app.get_matches();

    // Route to appropriate handler
    let result = match matches.subcommand() {
        Some(("project", sub_matches)) => project::handle_command(sub_matches, &settings).await,
        Some(("task", sub_matches)) => task::handle_command(sub_matches, &settings).await,
        Some(("time", sub_matches)) => time::handle_command(sub_matches, &settings).await,
        Some(("config", sub_matches)) => config::handle_command(sub_matches, &settings).await,
        Some(("sync", sub_matches)) => sync::handle_command(sub_matches, &settings).await,
        Some(("completions", sub_matches)) => handle_completions(sub_matches),
        _ => unreachable!(),
    };

    match result {
        Ok(_) => {
            info!("Command completed successfully");
            Ok(())
        }
        Err(e) => {
            error!("Command failed: {}", e);
            eprintln!("Error: {}", e);
            std::process::exit(1);
        }
    }
}

fn handle_completions(matches: &ArgMatches) -> Result<()> {
    use clap_complete::{generate, Shell};
    use std::io;
    
    let shell = matches
        .get_one::<String>("shell")
        .unwrap()
        .parse::<Shell>()
        .map_err(|e| CliError::InvalidInput(format!("Invalid shell: {}", e)))?;

    let mut app = Command::new("pmcli"); // Rebuild app for completions
    generate(shell, &mut app, "pmcli", &mut io::stdout());
    Ok(())
}

Task Management Commands

// src/cli/task.rs
use clap::{Arg, ArgMatches, Command};
use tracing::{info, debug};
use crate::core::task::{TaskManager, Task, TaskFilter, Priority, Status};
use crate::config::Settings;
use crate::utils::error::{Result, CliError};
use crate::utils::format::{format_table, format_task_details};

pub fn build_command() -> Command {
    Command::new("task")
        .about("Task management commands")
        .subcommand_required(true)
        .subcommand(
            Command::new("add")
                .about("Create a new task")
                .arg(
                    Arg::new("title")
                        .short('t')
                        .long("title")
                        .help("Task title")
                        .required(true)
                        .value_name("TITLE")
                )
                .arg(
                    Arg::new("description")
                        .short('d')
                        .long("description")
                        .help("Task description")
                        .value_name("DESCRIPTION")
                )
                .arg(
                    Arg::new("priority")
                        .short('p')
                        .long("priority")
                        .help("Task priority")
                        .value_parser(["low", "medium", "high", "urgent"])
                        .default_value("medium")
                )
                .arg(
                    Arg::new("project")
                        .long("project")
                        .help("Project ID or name")
                        .value_name("PROJECT")
                )
                .arg(
                    Arg::new("due")
                        .long("due")
                        .help("Due date (YYYY-MM-DD)")
                        .value_name("DATE")
                )
                .arg(
                    Arg::new("tags")
                        .long("tags")
                        .help("Comma-separated tags")
                        .value_name("TAGS")
                )
        )
        .subcommand(
            Command::new("list")
                .about("List tasks")
                .arg(
                    Arg::new("status")
                        .short('s')
                        .long("status")
                        .help("Filter by status")
                        .value_parser(["pending", "in_progress", "completed"])
                )
                .arg(
                    Arg::new("priority")
                        .short('p')
                        .long("priority")
                        .help("Filter by priority")
                        .value_parser(["low", "medium", "high", "urgent"])
                )
                .arg(
                    Arg::new("project")
                        .long("project")
                        .help("Filter by project ID or name")
                        .value_name("PROJECT")
                )
                .arg(
                    Arg::new("assigned-to")
                        .long("assigned-to")
                        .help("Filter by assignee")
                        .value_name("USER")
                )
                .arg(
                    Arg::new("format")
                        .short('f')
                        .long("format")
                        .help("Output format")
                        .value_parser(["table", "json", "csv"])
                        .default_value("table")
                )
        )
        .subcommand(
            Command::new("show")
                .about("Show task details")
                .arg(
                    Arg::new("id")
                        .help("Task ID")
                        .required(true)
                        .value_name("ID")
                )
        )
        .subcommand(
            Command::new("complete")
                .about("Mark task as completed")
                .arg(
                    Arg::new("id")
                        .help("Task ID")
                        .required(true)
                        .value_name("ID")
                )
        )
        .subcommand(
            Command::new("start")
                .about("Start working on a task (starts timer)")
                .arg(
                    Arg::new("id")
                        .help("Task ID")
                        .required(true)
                        .value_name("ID")
                )
        )
}

pub async fn handle_command(matches: &ArgMatches, settings: &Settings) -> Result<()> {
    let task_manager = TaskManager::new(&settings.database_path).await?;

    match matches.subcommand() {
        Some(("add", sub_matches)) => handle_add_task(sub_matches, &task_manager).await,
        Some(("list", sub_matches)) => handle_list_tasks(sub_matches, &task_manager).await,
        Some(("show", sub_matches)) => handle_show_task(sub_matches, &task_manager).await,
        Some(("complete", sub_matches)) => handle_complete_task(sub_matches, &task_manager).await,
        Some(("start", sub_matches)) => handle_start_task(sub_matches, &task_manager).await,
        _ => unreachable!(),
    }
}

async fn handle_add_task(matches: &ArgMatches, task_manager: &TaskManager) -> Result<()> {
    let title = matches.get_one::<String>("title").unwrap();
    let description = matches.get_one::<String>("description").cloned();
    let priority = matches.get_one::<String>("priority").unwrap();
    let project_ref = matches.get_one::<String>("project");
    let due_date = matches.get_one::<String>("due");
    let tags = matches.get_one::<String>("tags");

    info!("Creating new task: {}", title);

    // Parse project reference
    let project_id = if let Some(proj_ref) = project_ref {
        Some(task_manager.resolve_project_reference(proj_ref).await?)
    } else {
        None
    };

    // Parse due date
    let parsed_due_date = if let Some(date_str) = due_date {
        Some(chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
            .map_err(|e| CliError::InvalidInput(format!("Invalid date format: {}", e)))?
            .and_hms_opt(23, 59, 59)
            .unwrap())
    } else {
        None
    };

    // Parse tags
    let parsed_tags = if let Some(tags_str) = tags {
        tags_str.split(',').map(|s| s.trim().to_string()).collect()
    } else {
        Vec::new()
    };

    let task = Task {
        id: 0, // Will be set by database
        project_id,
        title: title.clone(),
        description,
        status: Status::Pending,
        priority: priority.parse()?,
        due_date: parsed_due_date,
        tags: parsed_tags,
        ..Default::default()
    };

    let created_task = task_manager.create_task(task).await?;
    
    println!("✅ Created task #{}: {}", created_task.id, created_task.title);
    Ok(())
}

async fn handle_list_tasks(matches: &ArgMatches, task_manager: &TaskManager) -> Result<()> {
    let filter = TaskFilter {
        status: matches.get_one::<String>("status").map(|s| s.parse()).transpose()?,
        priority: matches.get_one::<String>("priority").map(|s| s.parse()).transpose()?,
        project_id: if let Some(proj_ref) = matches.get_one::<String>("project") {
            Some(task_manager.resolve_project_reference(proj_ref).await?)
        } else {
            None
        },
        ..Default::default()
    };

    let tasks = task_manager.list_tasks(filter).await?;
    let format = matches.get_one::<String>("format").unwrap();

    match format.as_str() {
        "table" => {
            let headers = vec!["ID", "Title", "Status", "Priority", "Project", "Due"];
            let rows: Vec<Vec<String>> = tasks.iter().map(|task| {
                vec![
                    task.id.to_string(),
                    task.title.clone(),
                    task.status.to_string(),
                    task.priority.to_string(),
                    task.project_id.map(|id| id.to_string()).unwrap_or("-".to_string()),
                    task.due_date.map(|d| d.format("%Y-%m-%d").to_string()).unwrap_or("-".to_string()),
                ]
            }).collect();

            format_table(&headers, &rows);
        }
        "json" => {
            println!("{}", serde_json::to_string_pretty(&tasks)?);
        }
        "csv" => {
            println!("id,title,status,priority,project_id,due_date");
            for task in tasks {
                println!(
                    "{},{},{},{},{},{}",
                    task.id,
                    task.title,
                    task.status,
                    task.priority,
                    task.project_id.map(|id| id.to_string()).unwrap_or_default(),
                    task.due_date.map(|d| d.format("%Y-%m-%d").to_string()).unwrap_or_default()
                );
            }
        }
        _ => unreachable!(),
    }

    println!("\nTotal: {} tasks", tasks.len());
    Ok(())
}

Time Tracking Implementation

// src/core/timer.rs
use chrono::{DateTime, Utc, Duration};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use std::sync::Arc;
use crate::db::Database;
use crate::utils::error::{Result, CliError};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeEntry {
    pub id: i64,
    pub task_id: i64,
    pub start_time: DateTime<Utc>,
    pub end_time: Option<DateTime<Utc>>,
    pub duration_seconds: Option<i64>,
    pub description: Option<String>,
}

#[derive(Debug)]
pub struct Timer {
    db: Arc<Database>,
    active_entry: Arc<RwLock<Option<TimeEntry>>>,
}

impl Timer {
    pub async fn new(database_path: &str) -> Result<Self> {
        let db = Arc::new(Database::new(database_path).await?);
        let active_entry = Arc::new(RwLock::new(None));
        
        // Check for any running timers from previous session
        let running_entry = db.get_running_time_entry().await?;
        if let Some(entry) = running_entry {
            *active_entry.write().await = Some(entry);
        }
        
        Ok(Timer { db, active_entry })
    }

    pub async fn start(&self, task_id: i64, description: Option<String>) -> Result<TimeEntry> {
        // Stop any existing timer first
        self.stop().await?;

        let entry = TimeEntry {
            id: 0, // Will be set by database
            task_id,
            start_time: Utc::now(),
            end_time: None,
            duration_seconds: None,
            description,
        };

        let created_entry = self.db.create_time_entry(&entry).await?;
        *self.active_entry.write().await = Some(created_entry.clone());
        
        println!("⏰ Timer started for task #{}", task_id);
        Ok(created_entry)
    }

    pub async fn stop(&self) -> Result<Option<TimeEntry>> {
        let mut active = self.active_entry.write().await;
        
        if let Some(entry) = active.take() {
            let end_time = Utc::now();
            let duration = end_time.signed_duration_since(entry.start_time);
            
            let completed_entry = TimeEntry {
                end_time: Some(end_time),
                duration_seconds: Some(duration.num_seconds()),
                ..entry
            };

            let updated_entry = self.db.update_time_entry(&completed_entry).await?;
            
            // Update task's actual hours
            self.db.add_task_hours(updated_entry.task_id, duration.num_seconds() as f64 / 3600.0).await?;
            
            println!("⏹️  Timer stopped. Duration: {}", format_duration(duration));
            Ok(Some(updated_entry))
        } else {
            println!("No active timer to stop.");
            Ok(None)
        }
    }

    pub async fn status(&self) -> Result<Option<(TimeEntry, Duration)>> {
        let active = self.active_entry.read().await;
        
        if let Some(entry) = &*active {
            let elapsed = Utc::now().signed_duration_since(entry.start_time);
            Ok(Some((entry.clone(), elapsed)))
        } else {
            Ok(None)
        }
    }

    pub async fn report(&self, start_date: Option<DateTime<Utc>>, end_date: Option<DateTime<Utc>>) -> Result<Vec<TimeEntry>> {
        self.db.get_time_entries(start_date, end_date).await
    }
}

fn format_duration(duration: Duration) -> String {
    let hours = duration.num_hours();
    let minutes = duration.num_minutes() % 60;
    let seconds = duration.num_seconds() % 60;
    
    if hours > 0 {
        format!("{}h {}m {}s", hours, minutes, seconds)
    } else if minutes > 0 {
        format!("{}m {}s", minutes, seconds)
    } else {
        format!("{}s", seconds)
    }
}

Configuration Management

// src/config/settings.rs
use config::{Config, ConfigError, Environment, File};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Settings {
    pub database_path: String,
    pub sync: SyncSettings,
    pub ui: UiSettings,
    pub integrations: IntegrationSettings,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct SyncSettings {
    pub enabled: bool,
    pub endpoint: Option<String>,
    pub api_token: Option<String>,
    pub auto_sync_interval_minutes: u64,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct UiSettings {
    pub default_format: String,
    pub color_output: bool,
    pub page_size: usize,
    pub timezone: String,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct IntegrationSettings {
    pub git_enabled: bool,
    pub git_auto_create_tasks: bool,
    pub editor_command: Option<String>,
}

impl Settings {
    pub fn load() -> Result<Self, ConfigError> {
        let config_dir = Self::config_dir()?;
        let config_file = config_dir.join("config.toml");

        let s = Config::builder()
            // Start with default values
            .set_default("database_path", Self::default_database_path())?
            .set_default("sync.enabled", false)?
            .set_default("sync.auto_sync_interval_minutes", 30)?
            .set_default("ui.default_format", "table")?
            .set_default("ui.color_output", true)?
            .set_default("ui.page_size", 20)?
            .set_default("ui.timezone", "UTC")?
            .set_default("integrations.git_enabled", true)?
            .set_default("integrations.git_auto_create_tasks", false)?
            // Add config file if it exists
            .add_source(
                File::from(config_file)
                    .required(false)
            )
            // Add environment variables with PMCLI prefix
            .add_source(
                Environment::with_prefix("PMCLI")
                    .separator("__")
            )
            .build()?;

        s.try_deserialize()
    }

    pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
        let config_dir = Self::config_dir()?;
        std::fs::create_dir_all(&config_dir)?;
        
        let config_file = config_dir.join("config.toml");
        let toml_string = toml::to_string_pretty(self)?;
        std::fs::write(config_file, toml_string)?;
        
        Ok(())
    }

    fn config_dir() -> Result<PathBuf, ConfigError> {
        dirs::config_dir()
            .ok_or_else(|| ConfigError::NotFound("Could not find config directory".into()))?
            .join("pmcli")
            .pipe(Ok)
    }

    fn default_database_path() -> String {
        dirs::data_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join("pmcli")
            .join("pmcli.db")
            .to_string_lossy()
            .to_string()
    }
}

Testing Strategy

CLI Testing with assert_cmd

// tests/cli/task_commands.rs
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::tempdir;

#[test]
fn test_task_add_success() {
    let temp_dir = tempdir().unwrap();
    let db_path = temp_dir.path().join("test.db");

    let mut cmd = Command::cargo_bin("pmcli").unwrap();
    cmd.env("PMCLI__DATABASE_PATH", db_path)
        .args(&["task", "add", "--title", "Test Task", "--priority", "high"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Created task"))
        .stdout(predicate::str::contains("Test Task"));
}

#[test]
fn test_task_list_format_json() {
    let temp_dir = tempdir().unwrap();
    let db_path = temp_dir.path().join("test.db");

    // First create a task
    Command::cargo_bin("pmcli").unwrap()
        .env("PMCLI__DATABASE_PATH", &db_path)
        .args(&["task", "add", "--title", "JSON Test Task"])
        .assert()
        .success();

    // Then list in JSON format
    let mut cmd = Command::cargo_bin("pmcli").unwrap();
    cmd.env("PMCLI__DATABASE_PATH", db_path)
        .args(&["task", "list", "--format", "json"])
        .assert()
        .success()
        .stdout(predicate::str::is_json())
        .stdout(predicate::str::contains("JSON Test Task"));
}

#[test]
fn test_invalid_priority_error() {
    let temp_dir = tempdir().unwrap();
    let db_path = temp_dir.path().join("test.db");

    let mut cmd = Command::cargo_bin("pmcli").unwrap();
    cmd.env("PMCLI__DATABASE_PATH", db_path)
        .args(&["task", "add", "--title", "Test", "--priority", "invalid"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("invalid value 'invalid'"));
}

Integration Testing

// tests/integration/timer_integration.rs
use pmcli::core::{TaskManager, Timer};
use tempfile::tempdir;
use tokio::time::{sleep, Duration};

#[tokio::test]
async fn test_timer_workflow() {
    let temp_dir = tempdir().unwrap();
    let db_path = temp_dir.path().join("test.db").to_str().unwrap().to_string();

    // Set up
    let task_manager = TaskManager::new(&db_path).await.unwrap();
    let timer = Timer::new(&db_path).await.unwrap();

    // Create a test task
    let task = task_manager.create_task(Task {
        title: "Timer Test Task".to_string(),
        ..Default::default()
    }).await.unwrap();

    // Start timer
    let entry = timer.start(task.id, Some("Testing timer".to_string())).await.unwrap();
    assert!(entry.end_time.is_none());
    assert!(entry.duration_seconds.is_none());

    // Check status
    let (active_entry, elapsed) = timer.status().await.unwrap().unwrap();
    assert_eq!(active_entry.task_id, task.id);
    assert!(elapsed.num_seconds() >= 0);

    // Sleep briefly to ensure measurable duration
    sleep(Duration::from_millis(100)).await;

    // Stop timer
    let completed_entry = timer.stop().await.unwrap().unwrap();
    assert!(completed_entry.end_time.is_some());
    assert!(completed_entry.duration_seconds.is_some());
    assert!(completed_entry.duration_seconds.unwrap() > 0);

    // Verify no active timer
    assert!(timer.status().await.unwrap().is_none());
}

Performance Optimizations

Binary Size Optimization

# Cargo.toml
[profile.release]
strip = true        # Remove debug symbols
lto = true         # Link-time optimization
codegen-units = 1  # Better optimization
panic = "abort"    # Smaller binary size

Database Optimization

// src/db/connection.rs
use rusqlite::{Connection, OpenFlags};
use tokio_rusqlite::Connection as AsyncConnection;

pub async fn optimize_database(conn: &AsyncConnection) -> Result<(), rusqlite::Error> {
    conn.call(|conn| {
        // Enable WAL mode for better concurrency
        conn.execute("PRAGMA journal_mode = WAL", [])?;
        
        // Optimize for performance
        conn.execute("PRAGMA synchronous = NORMAL", [])?;
        conn.execute("PRAGMA cache_size = 10000", [])?;
        conn.execute("PRAGMA temp_store = MEMORY", [])?;
        
        // Auto-vacuum for smaller database size
        conn.execute("PRAGMA auto_vacuum = INCREMENTAL", [])?;
        
        Ok(())
    }).await
}

Cross-Platform Build

Build Script

#!/bin/bash
# scripts/cross-compile.sh

set -e

TARGETS=(
    "x86_64-unknown-linux-gnu"
    "x86_64-unknown-linux-musl"
    "x86_64-apple-darwin"
    "aarch64-apple-darwin"
    "x86_64-pc-windows-gnu"
)

VERSION=$(cargo metadata --format-version=1 | jq -r '.packages[] | select(.name == "pmcli") | .version')

echo "Building pmcli v$VERSION for multiple targets..."

for target in "${TARGETS[@]}"; do
    echo "Building for $target..."
    
    if command -v cross &> /dev/null; then
        cross build --release --target "$target"
    else
        cargo build --release --target "$target"
    fi
    
    # Create archive
    case "$target" in
        *windows*)
            archive_name="pmcli-v$VERSION-$target.zip"
            cd "target/$target/release"
            zip "../../../$archive_name" pmcli.exe
            cd "../../.."
            ;;
        *)
            archive_name="pmcli-v$VERSION-$target.tar.gz"
            cd "target/$target/release"
            tar -czf "../../../$archive_name" pmcli
            cd "../../.."
            ;;
    esac
    
    echo "Created $archive_name"
done

echo "All builds completed!"

Installation and Usage

Installation Methods

# From source
git clone https://github.com/nibzard/ospec-examples
cd pmcli
cargo install --path .

# From releases (future)
curl -L https://github.com/nibzard/ospec-examples/releases/latest/download/pmcli-linux.tar.gz | tar xz
sudo mv pmcli /usr/local/bin/

# Via Homebrew (macOS)
brew tap nibzard/tools
brew install pmcli

# Via Cargo
cargo install pmcli

Shell Completions

# Generate completions for your shell
pmcli completions bash > ~/.bash_completion.d/pmcli
pmcli completions zsh > ~/.zfunc/_pmcli
pmcli completions fish > ~/.config/fish/completions/pmcli.fish

Example Usage

Basic Workflow

# Create a new project
pmcli project create --name "Website Redesign" --template web-app

# Add tasks to the project
pmcli task add --title "Design mockups" --priority high --project 1
pmcli task add --title "Implement frontend" --due 2024-06-15 --project 1

# Start working on a task (starts timer)
pmcli task start 1

# List current tasks
pmcli task list --status pending

# Complete a task (stops timer if running)
pmcli task complete 1

# View time report
pmcli time report --project 1

Advanced Usage

# Export data
pmcli export --format csv --output tasks.csv --filter status=completed

# Git integration
pmcli config set integrations.git_auto_create_tasks true
# Now commits with format "fix: #123 description" auto-update task 123

# Sync with remote server
pmcli config set sync.endpoint https://api.yourcompany.com
pmcli config set sync.api_token $API_TOKEN
pmcli sync

This comprehensive CLI tool example demonstrates how OSpec can generate a full-featured, cross-platform command-line application with professional-grade features and performance.