Providers
Providers are sysdef’s interface to package managers. They abstract away the differences between various package management systems (apt, pacman, bun, etc.) and provide a unified way for modules to declare package dependencies.
Provider Interface
Section titled “Provider Interface”A provider is defined by the Provider interface in sysdef.ts:169:
export interface Provider { readonly name: string; install: (packages: PackageInfo[]) => Promise<void>; uninstall: (packages: string[]) => Promise<void>; getInstalled: () => Promise<PackageInfo[]>; update: (packages: string[]) => Promise<void>; checkInstallation?: () => Promise<void>;}Where PackageInfo is defined in sysdef.ts:102:
export interface PackageInfo { name: string; version: string; provider: string;}Creating a Provider
Section titled “Creating a Provider”Provider files must export a default function that takes a Shell function and returns a Provider object:
import type { ProviderGenerator, PackageInfo } from "../sysdef-src/sysdef";
const generator: ProviderGenerator = (shell) => { return { name: "example-package-manager",
async install(packages: PackageInfo[]) { for (const pkg of packages) { const spec = pkg.version === "_*_" ? pkg.name : `${pkg.name}@${pkg.version}`; await shell(`example-pm install ${spec}`, { displayOutput: true }); } },
async uninstall(packageNames: string[]) { for (const name of packageNames) { await shell(`example-pm remove ${name}`, { displayOutput: true }); } },
async getInstalled(): Promise<PackageInfo[]> { const result = await shell("example-pm list --installed --json", {}); const packages = JSON.parse(result.stdout);
return packages.map((pkg: any) => ({ name: pkg.name, version: pkg.version, provider: "example-package-manager" })); },
async update(packageNames: string[]) { for (const name of packageNames) { await shell(`example-pm update ${name}`, { displayOutput: true }); } },
async checkInstallation() { // throwOnError defaults to true, so this throws if example-pm isn't found await shell("which example-pm", {}); } };};
export default generator;Provider Methods
Section titled “Provider Methods”install(packages)
Section titled “install(packages)”Installs the specified packages. Receives an array of PackageInfo objects containing:
name: Package nameversion: Specific version or_*_for any versionprovider: Provider name (for context)
Implementation should handle version specifications appropriately for the underlying package manager.
uninstall(packageNames)
Section titled “uninstall(packageNames)”Removes packages by name. Receives an array of package names as strings.
getInstalled()
Section titled “getInstalled()”Returns all packages currently installed through this provider. Must return PackageInfo[] with accurate name, version, and provider fields.
This method is crucial for:
- Determining what needs to be installed/removed during sync
- The
sysdef list-installedcommand - Updating the lockfile with current versions
update(packageNames)
Section titled “update(packageNames)”Updates specified packages to their latest versions. Optional functionality depending on the package manager’s capabilities.
checkInstallation() (optional)
Section titled “checkInstallation() (optional)”Verifies that the provider’s underlying package manager is properly installed and configured. Called by:
sysdef syncbefore package operationssysdef providerscommand for status checking
Should throw an error if the package manager is not available or misconfigured.
Shell Integration
Section titled “Shell Integration”Providers receive a Shell function that provides a consistent interface for executing commands:
type Shell = (s: string | string[], options: ShellOptions) => Promise<ShellResult>;
interface ShellOptions { throwOnError?: boolean; // Default: true - throw on non-zero exit stdin?: string; // Input to pipe to the command displayOutput?: boolean; // Show output in real-time asRoot?: boolean; // Run the command as root (via sudo) - see below}
interface ShellResult { code: number; // Exit code stdout: string; // Standard output}The command can be either a string (split on spaces) or a string array. Use the
array form when an argument contains spaces that must not be split — for example the dnf
provider passes ["rpm", "-qa", "--qf", "%{NAME} %{VERSION}-%{RELEASE}\n"] so the format
string stays a single argument.
Running commands as root
Section titled “Running commands as root”Never hard-code sudo into a command string. Instead pass { asRoot: true }:
// correctawait shell(["pacman", "-S", "--noconfirm", ...names], { asRoot: true, displayOutput: true });
// wrong - re-prompts for the password and bypasses sysdef's askpass helperawait shell("sudo pacman -S --noconfirm ...", { displayOutput: true });sysdef sync (and sysdef update) prompt for the sudo password once at the start and
reuse it in-memory for every asRoot command via a SUDO_ASKPASS helper. A hard-coded
sudo bypasses that and can re-prompt repeatedly. See providers/apt.ts and
providers/arch-official.ts for reference implementations.
The shell function handles:
- Dry-run mode: When
--dry-runis used, providers getdryShellwhich only logs commands - Error handling: Throws exceptions on command failures (unless
throwOnError: false) - Output capture: Collects stdout for parsing package lists
Example shell usage patterns:
// Simple command executionawait shell("apt update", { displayOutput: true });
// Capture output for parsingconst result = await shell("apt list --installed", {});const packages = parseAptOutput(result.stdout);
// Non-failing command (for checking if tool exists): pass throwOnError: false// so a non-zero exit is returned instead of thrown, then inspect the codeconst check = await shell("which pacman", { throwOnError: false });if (check.code !== 0) { throw new Error("pacman not found in PATH");}Provider Loading
Section titled “Provider Loading”Providers are loaded by the loadProviders() function in loaders.ts:51. The loading process:
- Scans the
providers/directory for files with valid extensions (.ts,.tsx,.js,.jsx) - Only loads providers listed in
config.yaml’sprovidersarray - Dynamically imports each provider file
- Calls the default exported function with the appropriate
Shellimplementation - Collects all returned
Providerobjects
Package Synchronization
Section titled “Package Synchronization”During sysdef sync, the syncPackages() function in sysdef.ts:270 coordinates package management:
- Inventory: Calls
getInstalled()on each provider - Diff calculation: Determines what packages to install/remove
- User confirmation: Prompts for approval of package operations
- Execution: Calls
install()anduninstall()methods - Lockfile update: Updates
sysdef-lock.jsonwith new versions
The sync process respects:
- Version constraints: Exact versions specified in modules
- ANY_VERSION_STRING (
"_*_"): Accepts any installed version - Version conflicts: Errors if modules request incompatible versions
- Safe mode (
--safe): Skips package removal
Built-in Providers
Section titled “Built-in Providers”Sysdef includes default providers for common package managers:
- apt: Debian/Ubuntu system packages
- arch-official: Arch Linux official repos (pacman)
- aur: Arch User Repository (clones + builds with makepkg)
- dnf: Fedora/RHEL system packages
- bun: global bun (JavaScript) packages
- npm: global npm (JavaScript) packages
- pipx: isolated Python CLI applications
- go:
go installbinaries - cargo: Rust crates
- custom: a stub for your own logic
These serve as both functional providers and reference implementations for creating custom providers.
Error Handling
Section titled “Error Handling”Provider errors are handled by the errorOut() function for consistency:
- Installation check failures: During
sysdef syncandsysdef providers - Command execution failures: When shell commands return non-zero exit codes
- Invalid package specifications: Malformed version constraints
- Provider not found: When config references non-existent providers
Best Practices
Section titled “Best Practices”Version Handling
Section titled “Version Handling”- Support both exact versions (
"1.2.3") and version ranges where possible - Handle
ANY_VERSION_STRINGgracefully by accepting any installed version - Provide meaningful error messages for unsupported version specifications
Output Parsing
Section titled “Output Parsing”- Make
getInstalled()robust against package manager output format changes - Handle edge cases like packages with special characters in names
- Consider locale-specific output formatting
Performance
Section titled “Performance”- Batch package operations when the underlying package manager supports it
- Cache expensive operations like package list queries when appropriate
- Minimize package manager invocations during sync operations
Error Recovery
Section titled “Error Recovery”- Provide specific error messages for common failure modes
- Implement
checkInstallation()to validate provider prerequisites - Handle partial failures gracefully (e.g., some packages install, others fail)