API Reference
This document provides a reference for scoop’s public API, primarily intended for:
- AI/LLM tools analyzing or modifying the codebase
- Contributors extending scoop’s functionality
- Advanced users integrating scoop into custom tooling
Note: This is an internal API reference. For CLI usage, see Commands.
Core Types
VirtualEnv Module (core/virtualenv/)
VirtualenvInfo
Represents basic information about a virtual environment. Marked
#[non_exhaustive] since Unreleased — external Rust consumers can
no longer construct it with struct-literal syntax. Obtain instances
from VirtualenvService::list() (or any future read API) instead.
#![allow(unused)]
fn main() {
#[non_exhaustive]
pub struct VirtualenvInfo {
pub name: String,
pub path: PathBuf,
pub python_version: Option<String>,
pub created_at: Option<DateTime<Utc>>,
pub last_used: Option<DateTime<Utc>>,
}
}
Fields:
name- Environment name (e.g.,"myproject")path- Absolute path to virtualenv directorypython_version- Python version string if metadata exists (e.g.,Some("3.12.1"))created_at- Creation timestamp from metadata, if present (Unreleased)last_used- Most recentscoop activate/run/shelltouch timestamp, if present (Unreleased).Nonefor legacy envs that pre-date the field and fresh envs that have never been activated.
Example (consume-only, struct-literal construction is no longer permitted):
#![allow(unused)]
fn main() {
let service = VirtualenvService::auto()?;
for info in service.list()? {
println!("{} (last used: {:?})", info.name, info.last_used);
}
}
VirtualenvService
Primary service for virtualenv operations.
#![allow(unused)]
fn main() {
pub struct VirtualenvService {
uv: UvClient,
}
impl VirtualenvService {
/// Creates a new service with custom uv wrapper
pub fn new(uv: UvClient) -> Self
/// Creates a service using system's uv installation
pub fn auto() -> Result<Self>
/// Lists all virtualenvs
pub fn list(&self) -> Result<Vec<VirtualenvInfo>>
/// Creates a new virtualenv
pub fn create(&self, name: &str, python_version: &str) -> Result<PathBuf>
/// Creates a new virtualenv with a specific Python executable
pub fn create_with_python_path(&self, name: &str, python_version: &str, python_path: &Path) -> Result<PathBuf>
/// Deletes a virtualenv
pub fn delete(&self, name: &str) -> Result<()>
/// Checks if a virtualenv exists
pub fn exists(&self, name: &str) -> Result<bool>
/// Gets the path to a virtualenv
pub fn get_path(&self, name: &str) -> Result<PathBuf>
/// Reads metadata for a virtualenv (best-effort; collapses
/// missing and corrupt into `None`).
pub fn read_metadata(&self, path: &Path) -> Option<Metadata>
/// Reads metadata distinguishing missing (`Ok(None)`) from
/// corrupt (`Err(_)`). Touch / gc use this so they can refuse
/// to overwrite garbage. (Unreleased)
pub fn read_metadata_result(&self, path: &Path) -> Result<Option<Metadata>>
/// Writes metadata atomically via tempfile + rename. (Unreleased)
pub fn write_metadata_atomic(&self, path: &Path, m: &Metadata) -> Result<()>
/// Best-effort update to `last_used`. Never returns an error;
/// logs `warn!` on failure. (Unreleased)
pub fn touch_metadata_best_effort(&self, env_name: &str)
}
}
Common Usage Pattern:
#![allow(unused)]
fn main() {
// Initialize service
let service = VirtualenvService::auto()?;
// Check if environment exists
if !service.exists("myenv")? {
// Create with Python 3.12
service.create("myenv", "3.12")?;
}
// Get environment path
let path = service.get_path("myenv")?;
println!("Environment at: {}", path.display());
}
Metadata Module (core/metadata.rs)
Metadata
Stores JSON metadata for each virtualenv.
#![allow(unused)]
fn main() {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Metadata {
pub name: String,
pub python_version: String,
pub created_at: DateTime<Utc>, // Timestamp (ISO 8601 when serialized)
pub created_by: String, // "scoop X.Y.Z" format
pub uv_version: Option<String>, // uv version used
pub python_path: Option<String>, // Custom Python executable path (if --python-path was used)
pub last_used: Option<DateTime<Utc>>, // Last activation timestamp (Unreleased)
}
impl Metadata {
/// Creates new metadata
pub fn new(name: String, python_version: String, uv_version: Option<String>) -> Self
}
}
Storage Location: ~/.scoop/virtualenvs/<name>/.scoop-metadata.json
Example JSON:
{
"name": "myproject",
"python_version": "3.12.1",
"created_at": "2024-01-15T10:30:00Z",
"created_by": "scoop <version>",
"uv_version": "0.1.0"
}
Note:
created_atisDateTime<Utc>in Rust but serializes to ISO 8601 string in JSON.
Doctor Module (core/doctor.rs)
Check Trait
Interface for health checks.
#![allow(unused)]
fn main() {
pub trait Check: Send + Sync {
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn run(&self) -> Vec<CheckResult>; // Returns Vec - a check can produce multiple results
}
}
Implementations:
UvCheck- Verifies uv is installedHomeCheck- ChecksSCOOP_HOMEdirectoryVirtualenvCheck- Validates virtualenvs directorySymlinkCheck- Checks for broken virtualenv Python symlinks (e.g.,<env>/bin/python)ShellCheck- Verifies shell integrationVersionCheck- Validates version files
CheckStatus
Result status for health checks.
#![allow(unused)]
fn main() {
pub enum CheckStatus {
Ok, // ✅ Check passed
Warning(String), // ⚠️ Issue found, but not critical
Error(String), // ❌ Critical issue
}
}
CheckResult
Detailed result from a health check.
#![allow(unused)]
fn main() {
pub struct CheckResult {
pub id: &'static str,
pub name: &'static str,
pub status: CheckStatus,
pub suggestion: Option<String>,
pub details: Option<String>,
}
impl CheckResult {
/// Creates an OK result
pub fn ok(id: &'static str, name: &'static str) -> Self
/// Creates a warning result
pub fn warn(id: &'static str, name: &'static str, message: impl Into<String>) -> Self
/// Creates an error result
pub fn error(id: &'static str, name: &'static str, message: impl Into<String>) -> Self
/// Adds a suggestion for fixing the issue
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self
/// Adds detailed information
pub fn with_details(mut self, details: impl Into<String>) -> Self
// Status checks
pub fn is_ok(&self) -> bool
pub fn is_warning(&self) -> bool
pub fn is_error(&self) -> bool
}
}
Example - Implementing a Custom Check:
#![allow(unused)]
fn main() {
struct MyCustomCheck;
impl Check for MyCustomCheck {
fn id(&self) -> &'static str {
"my_check"
}
fn name(&self) -> &'static str {
"My Custom Check"
}
fn run(&self) -> Vec<CheckResult> {
if some_condition() {
vec![CheckResult::ok(self.id(), self.name())]
} else {
vec![CheckResult::error(self.id(), self.name(), "Error message here")
.with_suggestion("Run: scoop fix-it")
.with_details("Expected X, found Y")]
}
}
}
}
Doctor
Orchestrates all health checks.
#![allow(unused)]
fn main() {
pub struct Doctor {
checks: Vec<Box<dyn Check>>,
}
impl Doctor {
/// Creates a doctor with default checks
pub fn new() -> Self
/// Runs all checks without fixing
pub fn run_all(&self) -> Vec<CheckResult>
/// Runs checks and attempts to fix issues
pub fn run_and_fix(&self, output: &crate::output::Output) -> Vec<CheckResult>
}
impl Default for Doctor {
fn default() -> Self {
Self::new()
}
}
}
Usage:
#![allow(unused)]
fn main() {
let doctor = Doctor::new();
// Run diagnostics
let results = doctor.run_all();
for result in results {
match &result.status {
CheckStatus::Error(msg) => eprintln!("❌ {}: {}", result.name, msg),
CheckStatus::Warning(msg) => println!("⚠️ {}: {}", result.name, msg),
CheckStatus::Ok => println!("✅ {}", result.name),
}
}
// Auto-fix issues (requires Output for progress display)
use scoop::output::Output;
let output = Output::new(0, false, false, false);
let fixed_results = doctor.run_and_fix(&output);
}
Error Handling
ScoopError (error.rs)
Primary error type for all scoop operations.
#![allow(unused)]
fn main() {
#[derive(Error, Debug)]
pub enum ScoopError {
// Virtualenv errors
VirtualenvNotFound { name: String },
VirtualenvExists { name: String },
InvalidEnvName { name: String, reason: String },
// Python errors
PythonNotInstalled { version: String },
PythonInstallFailed { version: String, message: String },
PythonUninstallFailed { version: String, message: String },
InvalidPythonVersion { version: String },
NoPythonVersions { pattern: String },
// uv errors
UvNotFound,
UvCommandFailed { command: String, message: String },
// Path/IO errors
PathError(String), // Tuple variant
HomeNotFound,
Io(#[from] std::io::Error), // Tuple variant with From
Json(#[from] serde_json::Error), // Tuple variant with From
// Config errors
VersionFileNotFound { path: PathBuf },
UnsupportedShell { shell: String },
// CLI errors
InvalidArgument { message: String },
// Migration errors
PyenvNotFound,
PyenvEnvNotFound { name: String },
VenvWrapperEnvNotFound { name: String },
CondaEnvNotFound { name: String },
CorruptedEnvironment { name: String, reason: String },
PackageExtractionFailed { reason: String },
MigrationFailed { reason: String },
MigrationNameConflict { name: String, existing: PathBuf },
// Python path errors
InvalidPythonPath { path: PathBuf, reason: String },
// Cascade errors
CascadeAborted,
}
impl ScoopError {
/// Returns error code string (e.g., "ENV_NOT_FOUND", "UV_COMMAND_FAILED")
pub fn code(&self) -> &'static str
/// Localized message in an explicit locale (bypasses the process-global
/// current locale). `Display` delegates to this with the current locale.
pub fn message_in(&self, locale: &str) -> String
/// Returns user-friendly suggestion in the current locale (if available)
pub fn suggestion(&self) -> Option<String>
/// Locale-explicit sibling of `suggestion()` — useful for deterministic,
/// parallel tests that must not depend on the global locale.
pub fn suggestion_in(&self, locale: &str) -> Option<String>
/// Returns migration-specific exit code
pub fn migration_exit_code(&self) -> MigrationExitCode
}
}
The
*_in(locale)accessors passlocale =to rust-i18n’st!, which bypasses the process-global current locale — this lets tests assert messages deterministically without#[serial].
Error Code String Prefixes:
ENV_*- Environment errors (e.g.,ENV_NOT_FOUND,ENV_ALREADY_EXISTS)PYTHON_*- Python version errors (e.g.,PYTHON_NOT_INSTALLED)UV_*- uv errors (e.g.,UV_NOT_INSTALLED,UV_COMMAND_FAILED)IO_*- Path/IO errors (e.g.,IO_ERROR,PATH_ERROR)CONFIG_*- Config errors (e.g.,CONFIG_VERSION_FILE_NOT_FOUND)SHELL_*- Shell errors (e.g.,SHELL_NOT_SUPPORTED)ARG_*- CLI argument errors (e.g.,ARG_INVALID)SOURCE_*- Migration source errors (e.g.,SOURCE_PYENV_NOT_FOUND)MIGRATE_*- Migration process errors (e.g.,MIGRATE_FAILED)UNINSTALL_*- Uninstall errors (e.g.,UNINSTALL_CASCADE_ABORTED)
Example Error Handling:
#![allow(unused)]
fn main() {
use crate::error::{ScoopError, Result};
fn my_function(name: &str) -> Result<()> {
if !validate_name(name) {
return Err(ScoopError::InvalidEnvName {
name: name.to_string(),
reason: "Must start with a letter".to_string(),
});
}
// ... operation
Ok(())
}
// Usage
match my_function("123invalid") {
Ok(_) => println!("Success"),
Err(e) => {
eprintln!("Error: {}", e);
if let Some(suggestion) = e.suggestion() {
eprintln!("Suggestion: {}", suggestion);
}
eprintln!("Error code: {}", e.code());
std::process::exit(1); // Non-zero exit for error
}
}
}
Shell Integration
Shell Types (cli/mod.rs)
#![allow(unused)]
fn main() {
pub enum ShellType {
Bash,
Zsh,
Fish,
Powershell,
}
// Note: ShellType is a plain enum without methods
// Shell operations are handled by module-level functions:
// - shell::detect_shell() -> ShellType (in shell/mod.rs)
// - shell::bash::init_script() -> &'static str
// - shell::zsh::init_script() -> &'static str
// - shell::fish::init_script() -> &'static str
// - shell::powershell::init_script() -> &'static str
}
Auto-detection Priority:
FISH_VERSION→ FishPSModulePath→ PowerShellZSH_VERSION→ Zsh- Default → Bash
Path Utilities (paths.rs)
#![allow(unused)]
fn main() {
/// Returns scoop home directory (SCOOP_HOME or ~/.scoop)
pub fn scoop_home() -> Result<PathBuf>
/// Returns virtualenvs directory
pub fn virtualenvs_dir() -> Result<PathBuf>
/// Returns global version file path (~/.scoop/version)
pub fn global_version_file() -> Result<PathBuf>
/// Returns local version file path in the given directory
pub fn local_version_file(dir: &std::path::Path) -> PathBuf
}
Version Resolution (core/version.rs)
VersionService
Service for managing version files.
#![allow(unused)]
fn main() {
pub struct VersionService;
impl VersionService {
/// Set the local version for a directory
pub fn set_local(dir: &Path, env_name: &str) -> Result<()>
/// Set the global version
pub fn set_global(env_name: &str) -> Result<()>
/// Get the local version for a directory
pub fn get_local(dir: &Path) -> Option<String>
/// Get the global version
pub fn get_global() -> Option<String>
/// Resolve the version for a directory (local -> parent walk -> global)
pub fn resolve(dir: &Path) -> Option<String>
/// Resolve from current directory
pub fn resolve_current() -> Option<String>
/// Unset local version (removes .scoop-version)
pub fn unset_local(dir: &Path) -> Result<()>
/// Unset global version (removes ~/.scoop/version)
pub fn unset_global() -> Result<()>
}
}
CLI Equivalent (User Workflow):
VersionService::set_global() is exposed by the scoop use <name> --global command.
To set Python 3.11.0 as the global default in practice:
scoop install 3.11.0
scoop create py311 3.11.0
scoop use py311 --global
This writes py311 to ~/.scoop/version. The global value is used when no local
.scoop-version or SCOOP_VERSION override is present.
Resolution Priority Order:
SCOOP_VERSIONenvironment variable (checked at shell hook level, not in VersionService).scoop-versionin current directory.scoop-versionin parent directories (walks up)~/.scoop/version(global default)
Note:
.python-versionis not supported.
Testing Patterns
Property-Based Testing
scoop uses proptest for property-based testing of critical logic:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use proptest::prelude::*;
proptest! {
#[test]
fn env_name_validation_is_consistent(name in "[a-zA-Z][a-zA-Z0-9_-]*") {
assert!(validate_name(&name).is_ok());
}
}
}
}
Integration Testing
Test files follow the pattern:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_env() -> TempDir {
// Test setup
}
#[test]
fn test_something() {
let temp = setup_test_env();
// Test logic
}
}
}
Extending scoop
Adding a New Command
- Define command in
cli/mod.rs:
#![allow(unused)]
fn main() {
#[derive(Subcommand)]
pub enum Commands {
// ... existing commands
MyCommand {
#[arg(help = "Argument description")]
arg: String,
},
}
}
- Create handler in
cli/commands/:
#![allow(unused)]
fn main() {
// cli/commands/my_command.rs
use crate::error::Result;
pub fn execute(arg: &str) -> Result<()> {
// Implementation
Ok(())
}
}
- Wire up in
main.rs:
#![allow(unused)]
fn main() {
Commands::MyCommand { arg } => {
commands::my_command::execute(&arg)?
}
}
Adding a New Health Check
#![allow(unused)]
fn main() {
// In core/doctor.rs
struct MyCheck;
impl Check for MyCheck {
fn id(&self) -> &'static str {
"my_check"
}
fn name(&self) -> &'static str {
"My Custom Check"
}
fn run(&self) -> Vec<CheckResult> {
// Check logic
vec![CheckResult::ok(self.id(), self.name())]
}
}
// Register in Doctor::new()
impl Doctor {
pub fn new() -> Self {
Self {
checks: vec![
// ... existing checks
Box::new(MyCheck),
],
}
}
}
}
Related Documentation
- Architecture - System design and patterns
- Commands - CLI reference
- Contributing - Development guide
- Testing - Testing strategies
For AI/LLM Tools
When analyzing or modifying this codebase:
-
Use symbolic tools for precise navigation:
find_symbolto locate specific functions/typesfind_referencing_symbolsto understand usageget_symbols_overviewfor module structure
-
Follow existing patterns:
- Error handling: Always return
Result<T>withScoopError - Shell output: CLI outputs shell code, wrapper evals it
- Testing: Unit tests + integration tests + property tests
- i18n: Use
t!()macro for all user-facing strings
- Error handling: Always return
-
Preserve conventions:
- Error codes are string constants (e.g., “ENV_NOT_FOUND”, “UV_COMMAND_FAILED”)
- Process exit codes follow a layered contract (see Process exit codes below)
- Path handling via
paths.rsutilities (cross-platform — Unixbin/vs WindowsScripts/) - Shell detection via
shell::detect_shell()function
Process exit codes
scoop centralises exit-code policy in src/error/exit.rs via
ScoopError::exit_code(). Commands that already render their own
report (e.g. verify) opt into ErrorRenderPolicy::Quiet so main.rs
does not append a duplicate error: line.
| Code | Meaning |
|---|---|
0 | Success |
1 | Generic failure or semantic finding (verify failed, generic operational error) |
2 | Migration failure / MigrationNameConflict (and reserved for future probe/tool failures) |
3 | Migration source-discovery error (pyenv/conda/venvwrapper missing or corrupted) |
Per-command exit code table:
| Command | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
verify | always (informational) | — | — | — |
verify --strict | clean | any Fail check | — | — |
migrate @env / migrate all | success | — | failure / conflict | source tool not found |
doctor | clean | warning | error | — |
| Other commands | success | failure | — | — |
- Documentation requirements:
- All
pub fnmust have doc comments - Examples in doctests where applicable
- Error conditions documented
- Shell integration changes require cross-shell testing
- All
Last Updated: 2026-06-02 scoop Version: Unreleased (post-0.12.0)