Sindri uses a manifest-based extension system to manage development tools and environments. Extensions follow a standardized API with explicit dependency management and activation control.
Extension API v1.0 provides:
- Manifest-based activation: Control which extensions install via
active-extensions.conf - Standardized API: All extensions implement 6 required functions
- Dependency management: Explicit prerequisites checking before installation
- CLI management:
extension-managertool for activation and installation - Idempotent operations: Safe to re-run installations
- Clean removal: Proper uninstall with dependency warnings
# List all available extensions
extension-manager list
# Install an extension (auto-activates if needed)
extension-manager install nodejs
# Or use interactive mode for guided setup
extension-manager --interactive
# Or manually edit manifest then install all
# Edit: /workspace/scripts/lib/extensions.d/active-extensions.conf
extension-manager install-all
# Check installation status
extension-manager status nodejs
# Validate installation
extension-manager validate nodejsAll extensions must implement these 6 functions:
Purpose: Check system requirements before installation
Returns: 0 if all prerequisites met, 1 otherwise
Checks:
- Required system packages
- Commands available in PATH
- Disk space and memory
- Network connectivity
- Dependent extensions
Example:
prerequisites() {
print_status "Checking prerequisites for ${EXT_NAME}..."
if ! command_exists curl; then
print_error "curl is required but not installed"
return 1
fi
print_success "All prerequisites met"
return 0
}Purpose: Install packages and tools
Returns: 0 on success, 1 on failure
Actions:
- Download and install packages
- Compile from source if needed
- Verify installation success
- Handle already-installed gracefully
Example:
install() {
print_status "Installing ${EXT_NAME}..."
if command_exists rust; then
print_warning "Rust already installed"
return 0
fi
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
print_success "Rust installed successfully"
return 0
}Purpose: Post-installation configuration
Returns: 0 on success, 1 on failure
Actions:
- Add to PATH in .bashrc
- Create configuration files
- Set environment variables
- Create SSH wrappers for non-interactive sessions
Example:
configure() {
print_status "Configuring ${EXT_NAME}..."
if ! grep -q "cargo/bin" "$HOME/.bashrc"; then
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> "$HOME/.bashrc"
print_success "Added cargo to PATH"
fi
return 0
}Purpose: Run smoke tests to verify installation
Returns: 0 if valid, 1 if validation fails
Tests:
- Command availability
- Version checks
- Basic functionality tests
- Configuration validation
Example:
validate() {
print_status "Validating ${EXT_NAME}..."
if ! command_exists rustc; then
print_error "rustc command not found"
return 1
fi
print_success "Rust: $(rustc --version)"
return 0
}Purpose: Display current installation state
Returns: 0 if installed, 1 if not installed
Shows:
- Installed version
- Configuration status
- Component availability
- Helpful diagnostics
Example:
status() {
print_status "Checking ${EXT_NAME} status..."
if ! command_exists rustc; then
print_warning "Rust is not installed"
return 1
fi
print_success "Rust: $(rustc --version)"
print_success "Cargo: $(cargo --version)"
return 0
}Purpose: Uninstall and clean up
Returns: 0 on success, 1 on failure
Actions:
- Check for dependent extensions
- Prompt for confirmation
- Remove packages and files
- Clean up configuration
- Remove PATH modifications
Example:
remove() {
print_warning "Uninstalling ${EXT_NAME}..."
read -p "Remove Rust toolchain? (y/N): " -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
return 1
fi
rustup self uninstall -y
print_success "Rust uninstalled"
return 0
}Extensions are organized by category in the activation manifest.
- workspace-structure - Base /workspace directory structure
- nodejs - Node.js LTS via mise (replaces NVM)
- ssh-environment - SSH wrappers for non-interactive sessions
- claude - Claude Code CLI with developer configuration
- claude-marketplace - Plugin installer for https://claudecodemarketplace.com/
- openskills - OpenSkills CLI for managing Claude Code skills from Anthropic's marketplace
- nodejs-devtools - TypeScript, ESLint, Prettier, nodemon, goalie
- monitoring - System monitoring tools (htop, ncdu, glances)
- playwright - Browser automation testing framework
- tmux-workspace - Tmux session manager
- agent-manager - Claude Code agent management
- context-loader - Context management for Claude
- python - Python 3.13 with pip, venv, uv
- rust - Rust toolchain with cargo, clippy, rustfmt
- golang - Go 1.24 with gopls, delve, golangci-lint
- ruby - Ruby 3.4.7 via mise with Rails, Bundler
- php - PHP 8.4 with Composer, Symfony CLI
- jvm - SDKMAN with Java, Kotlin, Scala, Maven, Gradle
- dotnet - .NET SDK 9.0/8.0 with ASP.NET Core
- docker - Docker Engine with compose, dive, ctop
- infra-tools - Terraform, Ansible, kubectl, Helm, Carvel, Pulumi
- cloud-tools - AWS, Azure, GCP, Oracle, DigitalOcean CLIs
- ai-tools - AI coding assistants (Codex, Gemini, Ollama, Fabric)
Extensions are controlled via docker/lib/extensions.d/active-extensions.conf.example (development)
or active-extensions.ci.conf (CI mode):
# Protected extensions (required, cannot be removed):
workspace-structure
mise-config
ssh-environment
# Foundational languages (recommended):
nodejs
python
# Claude AI
claude
openskills
nodejs-devtools
# Languages
python
golang
# Infrastructure
docker
infra-toolsOrder matters: Extensions execute top to bottom. List dependencies before dependents.
The extension-manager CLI tool manages extension lifecycle:
# Show all available extensions
extension-manager list
# Shows: [ACTIVE] or [inactive] status for each extension# Manually add to manifest (doesn't install yet)
# Edit: docker/lib/extensions.d/active-extensions.conf.example
# Add line: claude
# Or install directly (auto-activates)
extension-manager install claude# Install single extension
extension-manager install nodejs
# Install all activated extensions
extension-manager install-all# Show installation status
extension-manager status nodejs
# Shows version and component availability# Run smoke tests
extension-manager validate nodejs
# Returns 0 if valid, 1 if issues found# Remove extension and clean up
extension-manager uninstall nodejs
# Checks for dependent extensions first# Change execution order in manifest
extension-manager reorder nodejs 5
# Moves nodejs to position 5cp ../../../docs/templates/template.extension my-extension.extension#!/bin/bash
# my-extension.extension - Brief description
# Extension API v2.0
EXT_NAME="my-extension"
EXT_VERSION="1.0.0"
EXT_DESCRIPTION="What this extension provides"
EXT_CATEGORY="utility" # utility, language, infrastructureSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$(dirname "$(dirname "$SCRIPT_DIR")")/extensions-common.sh"
# Initialize extension (loads common.sh and sets up environment)
extension_initAvailable Helper Functions:
The extensions-common.sh library provides these helper functions to eliminate code duplication:
Environment Helpers:
is_ci_mode()- Check if running in CIactivate_mise_environment()- Activate mise in current shell
Prerequisite Checks:
check_mise_prerequisite()- Verify mise is installedcheck_disk_space [mb]- Check available disk space (default 600MB)
Status Helpers:
print_extension_header()- Print standard extension header with metadata
Validation Helpers:
validate_commands <array>- Validate multiple commands with version checks
mise Helpers:
install_mise_config "name"- Install mise TOML configuration (handles CI vs dev selection)remove_mise_config "name"- Remove mise configuration
Git Helpers:
setup_git_aliases "alias:command" ...- Setup git aliasescleanup_git_aliases "alias1" "alias2" ...- Remove git aliases
Cleanup Helpers:
cleanup_bashrc "marker"- Remove extension entries from .bashrcprompt_confirmation "question"- Standardized yes/no promptshow_dependent_extensions_warning "cmd1" "cmd2"- Check and display dependent extensions
Main Execution Wrapper:
extension_main "$@"- Standard main execution block (replaces manual case statement)
Implement all 6 API functions: prerequisites(), install(), configure(), validate(), status(), remove()
Use helper functions to reduce boilerplate:
prerequisites() {
print_status "Checking prerequisites for ${EXT_NAME}..."
check_mise_prerequisite || return 1
check_disk_space 500
print_success "All prerequisites met"
return 0
}
install() {
print_status "Installing ${EXT_NAME}..."
install_mise_config "${EXT_NAME}" || return 1
return 0
}
configure() {
print_status "Configuring ${EXT_NAME}..."
setup_git_aliases "my-test:!mytool test"
print_success "Configuration complete"
return 0
}
validate() {
print_status "Validating ${EXT_NAME}..."
activate_mise_environment
declare -A checks=([mytool]="--version")
validate_commands checks
}
status() {
print_extension_header
if command_exists mytool; then
print_success "Installed: $(mytool --version)"
return 0
else
print_warning "Not installed"
return 1
fi
}
remove() {
print_status "Removing ${EXT_NAME}..."
show_dependent_extensions_warning "mytool"
remove_mise_config "${EXT_NAME}"
cleanup_git_aliases "my-test"
cleanup_bashrc "# ${EXT_NAME} - added by extension"
print_success "Removed successfully"
return 0
}# Use helper instead of manual case statement
extension_main "$@"# Test each function individually
./my-extension.extension prerequisites
./my-extension.extension install
./my-extension.extension configure
./my-extension.extension validate
./my-extension.extension status
# Or use extension-manager (install auto-activates)
extension-manager install my-extension
extension-manager validate my-extensionSee docs/templates/template.extension for comprehensive examples of using all helper functions. Key benefits:
- Less Code: Helper functions eliminate 50-100 lines of boilerplate per extension
- Consistency: All extensions use the same patterns
- Maintainability: Bug fixes in helpers benefit all extensions
- New Features: Get new capabilities automatically (e.g., dependency checking)
- Testing: Shared functions have centralized tests
Extensions must be safe to re-run:
install() {
if command_exists my-tool; then
print_warning "Already installed"
return 0
fi
# Install logic here
}Declare dependencies in prerequisites():
prerequisites() {
if ! command_exists npm; then
print_error "nodejs extension required"
print_status "Run: extension-manager install nodejs"
return 1
fi
return 0
}Don't exit on minor failures:
install() {
npm install -g tool1 || print_warning "tool1 failed"
npm install -g tool2 || print_warning "tool2 failed"
# Return success if at least one tool installed
command_exists tool1 || command_exists tool2
}Use provided print functions:
print_status- Informational messagesprint_success- Success messagesprint_error- Error messagesprint_warning- Warning messagesprint_debug- Debug output (only when DEBUG=true)
Avoid sudo when possible. Use version managers and user-space installation methods:
# Good: Use version managers (recommended pattern)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash # Node.js
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Rust
curl -s "https://get.sdkman.io" | bash # JVM languages
# Good: Language package managers with user flags
# Note: Replace "package" with actual package name (e.g., requests, ripgrep, rails)
pip install --user package # Python
cargo install package # Rust (installs to ~/.cargo/bin)
gem install --user-install package # Ruby
# Avoid: System-wide installation requiring sudo
sudo npm install -g package
sudo pip install package
sudo gem install packageImportant for Node.js/mise: Do NOT set npm prefix when using mise:
# WRONG: Conflicts with mise
npm config set prefix "$HOME/.npm-global"
# CORRECT: Let mise manage global packages
# mise already provides user-space global installs without sudo
# Global packages install to: ~/.local/share/mise/installs/node/<version>/bin
npm install -g package # No sudo needed, installs to mise directoryCreate wrappers for non-interactive SSH:
configure() {
if command_exists create_tool_wrapper 2>/dev/null; then
create_tool_wrapper "my-tool" "$(which my-tool)"
fi
}Add comments when modifying .bashrc:
echo "" >> "$HOME/.bashrc"
echo "# ${EXT_NAME} - description" >> "$HOME/.bashrc"
echo "export PATH=\"$HOME/.local/bin:\$PATH\"" >> "$HOME/.bashrc"Consider dependencies when ordering in manifest:
# Good: nodejs before nodejs-devtools
nodejs
nodejs-devtools
# Bad: nodejs-devtools will fail prerequisites
nodejs-devtools
nodejs
# Note: claude extension has no dependencies and can be placed anywhere
claude# Set DEBUG environment variable
DEBUG=true extension-manager install nodejs
# Or for full vm-configure
DEBUG=true /workspace/scripts/vm-configure.sh# Run specific function
bash -x my-extension.sh.example install
# Check prerequisites only
my-extension.sh.example prerequisites && echo "OK" || echo "FAIL"# Extension-manager logs to stdout/stderr
extension-manager install nodejs 2>&1 | tee install.log
# Check what's activated
cat /workspace/scripts/extensions.d/active-extensions.conf# Check installation state
extension-manager status my-extension
# Run validation tests
extension-manager validate my-extension
# List all active extensions
extension-manager list | grep ACTIVEIf you have custom extensions using the old numbered prefix system (e.g., 50-my-tool.sh):
mv 50-my-tool.sh my-tool.sh.exampleAdd EXT_NAME, EXT_VERSION, EXT_DESCRIPTION, EXT_CATEGORY at the top.
Convert script body into install() function. Add other 5 API functions.
# Install command will auto-activate the extension
extension-manager install my-tool
# Or manually add to active-extensions.conf then run install-allextension-manager validate my-tool# Check file exists
ls -l /workspace/scripts/lib/extensions.d/my-extension.sh.example
# Check file permissions
chmod +x my-extension.sh.example
# Check syntax
bash -n my-extension.sh.example# Run prerequisites manually
./my-extension.sh.example prerequisites
# Check what's missing
extension-manager status dependency-name# Enable debug mode
DEBUG=true extension-manager install my-extension
# Check disk space
df -h /workspace
# Check network
curl -I https://github.com# See what failed
extension-manager validate my-extension
# Check command availability
which expected-command
# Check PATH
echo $PATH- Extension Template:
template.sh.example- Complete reference implementation - Extension Manager:
/workspace/scripts/lib/extension-manager.sh- CLI tool source - Active Manifest:
/workspace/scripts/extensions.d/active-extensions.conf- Activation list - Common Functions:
/workspace/scripts/lib/common.sh- Shared utilities - Documentation:
/workspace/projects/active/sindri/CLAUDE.md- Full project docs