Virtual Package Manager — resumable, trackable script orchestration for VPS and local environments
Project description
VPM — Virtual Package Manager
Robust script orchestration for VPS environments. Track, execute, resume, and manage multi-step installation workflows with full logging and dependency resolution.
Table of Contents
- What is VPM?
- Quick Start
- Installation
- The Manifest File
- Commands Reference
- How Tracking Works
- Dependency System
- Logging
- Execution Model
- File Locations
- Shell Completions
- Writing Manifests — Guidelines for AI Agents
- Troubleshooting
- Architecture
What is VPM?
VPM is a single-file Python script that acts as an orchestrator for shell commands on a VPS. Think of it as a declarative way to define "install app X by running commands 1, 2, 3" with:
- Tracking: Every step is recorded. If your SSH drops mid-install, reconnect and run
vpm installagain — it picks up where it left off. - Dependencies: App B can declare it requires App A. VPM resolves the order and skips dependent apps if their requirements failed.
- Full Logging: Every command's stdout/stderr is captured to timestamped log files while still showing output in your terminal.
- Interactive Support: Uses PTY execution so
debconf,ncursesmenus,sudopassword prompts, and any interactive program works normally. - No External Dependencies: Pure Python 3.10+ standard library. No pip packages required.
VPM does not replace apt, dnf, or any package manager. It orchestrates arbitrary shell commands and tracks their success/failure state.
Quick Start
# Install from PyPI
pip install vpmx # or: pipx install vpmx
# Verify
vpm doctor
vpm version
# Create a project
mkdir ~/server-setup && cd ~/server-setup
vpm init
# Edit the manifest
nano vpm-manifest.yaml
# Preview what will run
vpm install --dry-run
# Execute
vpm install
# Check status anytime
vpm status
Installation
Requirements
- Python 3.10 or higher (3.12+ recommended)
- Linux or macOS (PTY support required)
bashorzshavailable on the system
Install Steps
# 1. Download or copy vpm.py to a permanent location
mkdir -p ~/.local/share/vpm
cp vpm.py ~/.local/share/vpm/vpm.py
# 2. Install the `vpm` command (symlinks to ~/.local/bin/vpm)
python3 ~/.local/share/vpm/vpm.py setup --user
# 3. Install shell completions (auto-detects your shell)
vpm completions
# 4. Verify
vpm doctor
vpm version
Global Installation (all users)
sudo python3 vpm.py setup --global
# Installs to /usr/local/bin/vpm
If Python 3.10+ Is Not Available
# Run vpm — it will detect the issue and suggest the right command:
python3 vpm.py doctor
# Typical fix for Ubuntu/Debian:
sudo apt-get update && sudo apt-get install -y python3.12
# For RHEL/CentOS/Fedora:
sudo dnf install -y python3.12
The Manifest File
The manifest file (vpm-manifest.yaml) is where you define what to install and how. It uses a simple, human-readable format that does not require a YAML parser — VPM parses it with its own lightweight parser.
Structure Overview
# Comments start with #
[app_name] Optional human-readable description
requires: dependency1, dependency2
- label: What this step does
run: shell command to execute
- label: Another step
run: |
multi-line
shell command
- run: simple command without explicit label
App Header
An app is defined by a header line enclosed in square brackets:
[my_app] This is my application
my_app— The app identifier. Used in commands likevpm install my_app,vpm retry my_app, etc.- Everything after
]is an optional description shown in status output. - App names are normalized internally: lowercased, special characters replaced with
_. - App names must be unique within a manifest.
Dependencies
Declared on a line after the app header, before any steps:
[web_server] Nginx + Certbot
requires: base_packages, firewall_setup
- Comma-separated list of app names.
- Dependencies must either be defined in the same manifest or already successfully installed from a previous run.
- VPM topologically sorts apps so dependencies always execute first.
- If a dependency fails, all apps that depend on it are automatically skipped.
Steps
Each step is a shell command to execute:
- label: Install Nginx
run: sudo apt-get install -y nginx
label— Human-readable name shown in terminal output and logs. If omitted, auto-generated from the command.run— The shell command to execute. Runs withbash -e(exits on first error).- Steps execute in order within an app.
- If a step fails (non-zero exit code), all subsequent steps in that app are skipped.
- On re-run, successfully completed steps are skipped automatically.
Shorthand (no label)
- sudo apt-get install -y nginx
When only a command is provided without label: or run: keys, VPM treats the entire line after - as the command and auto-generates a label from it.
Multi-line Commands
Use the pipe | character after run: to start a multi-line block:
- label: Configure firewall
run: |
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw --force enable
- All indented lines following
run: |are joined with newlines. - The block ends when a line at the same or lesser indentation level appears (or at end of file).
- The entire block runs as a single
bash -einvocation — if any line fails, execution stops.
Line Continuation
For long single commands, use backslash continuation inside multi-line blocks:
- label: Add Docker repository
run: |
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Comments
Lines starting with # are ignored everywhere:
# This entire app is disabled for now
# [experimental_stuff]
# - run: something risky
[active_app]
# This step is temporarily skipped
# - run: do something optional
- run: do something important
Complete Manifest Example
# ═══════════════════════════════════════════════════════
# Server Setup Manifest
# ═══════════════════════════════════════════════════════
[system_base] Core system packages and configuration
- label: Update package lists
run: sudo apt-get update -y
- label: Upgrade existing packages
run: sudo apt-get upgrade -y
- label: Install essential tools
run: |
sudo apt-get install -y \
curl wget git htop vim unzip jq tree \
net-tools dnsutils mtr-tiny \
build-essential software-properties-common
- label: Set timezone
run: sudo timedatectl set-timezone UTC
- label: Configure automatic security updates
run: |
sudo apt-get install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
[firewall] UFW Firewall Configuration
requires: system_base
- label: Install UFW
run: sudo apt-get install -y ufw
- label: Configure firewall rules
run: |
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw --force enable
- label: Verify firewall status
run: sudo ufw status verbose
[docker] Docker Engine
requires: system_base
- label: Install prerequisites
run: sudo apt-get install -y ca-certificates curl gnupg lsb-release
- label: Add Docker GPG key and repository
run: |
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg --yes
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- label: Install Docker Engine
run: |
sudo apt-get update -y
sudo apt-get install -y \
docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
- label: Add current user to docker group
run: sudo usermod -aG docker $USER
- label: Verify installation
run: sudo docker run --rm hello-world
[app_deployment] Deploy Application Stack
requires: docker, firewall
- label: Create application directory
run: mkdir -p ~/apps/myapp
- label: Clone repository
run: git clone https://github.com/example/myapp.git ~/apps/myapp
- label: Start services
run: |
cd ~/apps/myapp
docker compose up -d
- label: Verify services are running
run: docker compose -f ~/apps/myapp/docker-compose.yml ps
Commands Reference
vpm init
Create a manifest template file with documentation and examples.
vpm init # Creates vpm-manifest.yaml in current directory
vpm init /path/to/project # Creates in specified directory
vpm init --force # Overwrite existing manifest without asking
Output: A vpm-manifest.yaml file pre-filled with commented examples.
vpm install
Execute installation steps from a manifest file.
vpm install # Auto-discover manifest, install all apps
vpm install --file mysetup.yaml # Use specific manifest file
vpm install docker node_js # Install only specific apps
vpm install --file setup.yaml docker # Specific file + specific apps
vpm install --dry-run # Preview without executing
vpm install --force # Reinstall even if already completed
vpm install --yes # Skip confirmation prompts
vpm install -f setup.yaml -y # Combined short flags
Manifest auto-discovery order (when --file is not specified):
./vpm-manifest.yaml./vpm-manifest.yml./.vpm-manifest.yaml~/.config/vpm/manifest.yaml
Behavior:
- Resolves dependencies and computes installation order.
- Skips already-completed steps (resume support).
- Stops remaining steps in an app if one fails.
- Skips dependent apps if their requirements failed.
- Shows real-time progress with step counters and progress bar.
vpm status
Show installation status of tracked apps.
vpm status # Table of all tracked apps
vpm status docker # Detailed view for a specific app (shows each step)
vpm list
Alias for vpm status (shows the summary table).
vpm list
vpm logs
Browse and view execution logs.
vpm logs # List all app log directories with file counts
vpm logs docker # List all log files for docker
vpm logs docker --latest # Show contents of the latest summary log
vpm logs docker --step 0 # Show log for step 0 (first step)
vpm logs docker --step 2 # Show log for step 2 (third step)
vpm logs docker --follow # Tail -f the latest summary log
vpm retry
Resume a failed or partially completed installation.
vpm retry docker
Behavior:
- Resets
failedandskippedsteps topending. - Keeps
successsteps as-is (they won't re-run). - Starts execution from the first non-completed step.
vpm reset
Clear tracking state for an app, allowing fresh reinstallation.
vpm reset docker # Reset tracking for docker
vpm reset docker --clean-logs # Also delete all log files for docker
vpm reset --all # Reset tracking for everything
vpm reset --all --clean-logs # Nuclear option: reset everything + delete all logs
Note: Reset does not undo any system changes made by previous commands. It only clears VPM's tracking data.
vpm setup
Install the vpm command to your PATH.
vpm setup --user # Symlink to ~/.local/bin/vpm (default, no sudo needed)
vpm setup --global # Symlink to /usr/local/bin/vpm (requires sudo)
Also offers to install shell completions automatically.
vpm doctor
Diagnose the VPM environment and suggest fixes.
vpm doctor
Checks performed:
- Python version (3.10+ required, 3.12+ recommended)
- VPM directory structure
- Lock file integrity
- Shell detection
- PATH configuration
- Sudo access
- Required system tools (bash, curl, wget, git)
Can attempt automatic fixes when issues are found.
vpm completions
Generate and install tab-completion for your shell.
vpm completions # Auto-detect shell and install
vpm completions --shell zsh # Force specific shell
vpm completions --shell bash
vpm completions --shell fish
Supported shells: zsh, bash, fish
Completions include:
- All commands and subcommands
- Command-specific flags and options
- Dynamic app name completion (reads from lock file)
- File path completion where appropriate
vpm version
Show version and environment information.
vpm version
Security Scanner
VPM includes a built-in security scanner that analyzes manifest commands before execution.
What It Detects
| Severity | Examples |
|---|---|
| Critical | rm -rf /, fork bombs, disk formatting |
| High | curl | bash, eval $var, chmod 777, SSL bypass |
| Medium | Downloads from unknown URLs, third-party repos, non-HTTPS |
| Low | sudo usage (expected but noted) |
Usage
# Scan without executing
vpm audit
vpm audit --file manifest.yaml
# Auto-scan runs before every install (configurable)
vpm install # scans first, prompts on warnings
vpm install --skip-security # bypass scan (not recommended)
Configuration
In ~/.config/vpm/config.json:
{
"security": {
"level": "warn",
"check_urls": false,
"virustotal_api_key": null,
"allowed_domains": ["github.com", "download.docker.com"]
}
}
Levels: strict (block high+critical), warn (default), permissive, off
Rollback
Steps can define optional rollback commands that undo their changes:
- label: Install Nginx
run: sudo apt-get install -y nginx
rollback: sudo apt-get remove -y nginx
vpm rollback my_app # undo succeeded steps in reverse order
vpm rollback my_app --dry-run # preview what would be undone
Rollback is best-effort: if a rollback step fails, remaining rollbacks still execute.
Only steps with rollback: defined and status: success are undone.
Remote Manifests
Fetch and execute manifests from URLs or GitHub:
vpm run https://example.com/setup.yaml
vpm run github:user/repo # fetches vpm-manifest.yaml from repo root
vpm run github:user/repo/path/manifest.yaml # specific file
Security scanning is mandatory for remote manifests and cannot be skipped.
For AI Agents
See AGENTS.md for a concise reference optimized for AI agent context windows. See examples/ for real-world manifest examples.
How Tracking Works
The Lock File
All state is persisted in ~/.local/share/vpm/vpm-lock.json. This JSON file contains:
{
"_meta": {
"version": "1.0.0",
"updated_at": "2024-01-15T10:30:00",
"user": "deploy",
"hostname": "my-vps"
},
"apps": {
"docker": {
"name": "docker",
"display_name": "docker (Docker Engine)",
"status": "completed",
"requires": ["system_base"],
"steps": [
{
"index": 0,
"label": "Install prerequisites",
"command": "sudo apt-get install -y ...",
"status": "success",
"exit_code": 0,
"started_at": "2024-01-15T10:30:00",
"finished_at": "2024-01-15T10:30:05",
"duration_seconds": 5.2,
"log_file": "/home/user/.local/share/vpm/logs/docker/step_000_...",
"command_hash": "a1b2c3d4e5f6"
}
],
"total_steps": 5,
"completed_steps": 5,
"failed_steps": 0
}
}
}
Step States
| State | Meaning |
|---|---|
pending |
Not yet executed |
running |
Currently executing (set before command starts) |
success |
Completed with exit code 0 |
failed |
Completed with non-zero exit code |
skipped |
Skipped because a previous step failed, a dependency failed, or execution was interrupted |
App States
| State | Meaning |
|---|---|
pending |
No steps have been executed |
in_progress |
Currently being installed |
completed |
All steps finished successfully |
partial |
Some steps succeeded, some failed |
failed |
One or more steps failed, none succeeded (or dependency failed) |
Crash Recovery
If VPM is interrupted (SSH disconnect, Ctrl+C, system crash, kill signal):
- The lock file already has the current step marked as
running. - On next
vpm install, VPM reads the lock file. - Steps marked
successare skipped. - The
runningstep (which was interrupted) is treated as needing re-execution. - Execution resumes from that point.
The lock file is written atomically (write to .tmp, then rename()) so it cannot be corrupted by a crash mid-write.
Change Detection
Each step's command is SHA-256 hashed and stored in the lock file. If you modify a command in the manifest and re-run vpm install:
- VPM detects the hash mismatch.
- It warns you that the manifest has changed.
- It asks whether to re-run with the updated steps.
Dependency System
How Dependencies Resolve
Given this manifest:
[base]
- run: apt-get update
[docker]
requires: base
[app]
requires: docker, base
VPM performs a topological sort:
base(no dependencies)docker(depends onbase)app(depends ondockerandbase)
If docker fails, app is automatically skipped with a clear message:
✖ Skipping 'app': dependency 'docker' is not successfully installed.
Cross-Manifest Dependencies
If App B in today's manifest requires App A that was installed last week from a different manifest:
- VPM checks the lock file for App A's status.
- If App A shows
completedin the lock file, the dependency is satisfied. - If App A is not found or not completed, VPM reports the missing dependency.
Circular Dependency Detection
[a]
requires: b
[b]
requires: a
VPM detects this and exits with:
✖ Dependency error: Circular dependency detected involving: a, b
Logging
Directory Structure
~/.local/share/vpm/
├── vpm-lock.json # State tracking
└── logs/
├── system_base/
│ ├── summary_20240115_103000.log # Overall run summary
│ ├── step_000_update_packages_103000.log
│ ├── step_001_install_tools_103012.log
│ └── step_002_set_timezone_103045.log
├── docker/
│ ├── summary_20240115_103100.log
│ ├── step_000_install_prereqs_103100.log
│ ├── step_001_add_gpg_key_103108.log
│ └── ...
└── app_deployment/
└── ...
Summary Logs
Each vpm install run for an app creates a summary log:
VPM Installation Summary
============================================================
App: docker (Docker Engine)
Started: 2024-01-15T10:31:00
User: deploy
Host: my-vps
Total Steps: 5
============================================================
[OK] Step 1: Install prerequisites (exit=0, 5.2s)
[OK] Step 2: Add Docker GPG key (exit=0, 2.1s)
[OK] Step 3: Add Docker repository (exit=0, 0.8s)
[OK] Step 4: Install Docker Engine (exit=0, 45.3s)
[FAIL] Step 5: Verify installation (exit=1, 0.5s)
Error: Cannot connect to the Docker daemon
============================================================
Finished: 2024-01-15T10:31:54
Duration: 53.9s
Status: partial
Steps: 4/5 succeeded, 1 failed
============================================================
Step Logs
Each step gets its own log file with full command output:
VPM Step Execution Log
────────────────────────────────────────────────────────────
Step: 4 — Install Docker Engine
Started: 2024-01-15T10:31:08
Command:
sudo apt-get update -y
sudo apt-get install -y docker-ce docker-ce-cli ...
────────────────────────────────────────────────────────────
Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease
Get:2 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [119 kB]
...
Setting up docker-ce (5:24.0.7-1~ubuntu.22.04~jammy) ...
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service
────────────────────────────────────────────────────────────
Exit Code: 0
Duration: 45.3s
Finished: 2024-01-15T10:31:53
Execution Model
PTY-Based Execution
VPM uses pseudo-terminal (PTY) execution rather than simple subprocess.PIPE. This means:
- The child process sees a real terminal (as if you typed the command yourself).
- Interactive programs work:
debconfconfiguration screens,ncursesdialogs,sudopassword prompts,less/morepagers, progress bars, colored output. - Your keystrokes (arrows, tab, enter) are forwarded to the child process.
- All output is simultaneously displayed in your terminal AND written to the log file.
Interactive Programs
These all work correctly during VPM execution:
sudopassword promptsdpkg-reconfiguredialogsdebconfpackage configuration screens (e.g., postfix, tzdata)ncurses-based installersmysql_secure_installation- Any program that needs a TTY
Environment Variables
- VPM inherits the current user's full environment.
DEBIAN_FRONTENDis not forced — interactive package configuration works. If you want unattended mode, set it yourself:export DEBIAN_FRONTEND=noninteractivebefore running vpm, or include it in your step command.SHELLis detected from the environment.$USER,$HOME,$PATH— all available as normal.
Shell Selection
Commands are executed with:
$SHELL -e -c "your command here"
- If
$SHELLisbashorzsh, it's used directly. - Otherwise, falls back to
/bin/bash. - The
-eflag means exit on first error — in a multi-line command block, if any line returns non-zero, the entire step fails immediately.
Error Handling
- Non-zero exit code: Step is marked
failed, remaining steps in that app areskipped. - OS error (command not found, permission denied): Step is marked
failedwith the error message captured. - SIGINT (Ctrl+C): Current step is allowed to finish, then remaining steps are marked
skipped. Terminal is restored. - SIGTERM: Same as SIGINT.
- SSH disconnect: The
runningstep in the lock file will be re-executed on next run.
File Locations
| Path | Purpose |
|---|---|
~/.config/vpm/ |
Configuration directory (XDG_CONFIG_HOME) |
~/.config/vpm/config.json |
User configuration (future use) |
~/.config/vpm/completions/ |
Generated shell completion files |
~/.local/share/vpm/ |
Data directory (XDG_DATA_HOME) |
~/.local/share/vpm/vpm-lock.json |
Installation state tracking |
~/.local/share/vpm/logs/ |
All execution logs |
~/.local/share/vpm/logs/<app>/ |
Per-app log directory |
~/.local/bin/vpm |
User-scoped symlink (after vpm setup --user) |
/usr/local/bin/vpm |
Global symlink (after vpm setup --global) |
All paths respect XDG_CONFIG_HOME and XDG_DATA_HOME environment variables if set.
Shell Completions
Supported Shells
| Shell | Completion File | Install Location |
|---|---|---|
| zsh | _vpm |
~/.zsh/completions/_vpm + fpath in .zshrc |
| bash | vpm.bash |
~/.local/share/bash-completion/completions/vpm + source in .bashrc |
| fish | vpm.fish |
~/.config/fish/completions/vpm.fish |
What Gets Completed
- All commands:
init,install,status,list,logs,retry,reset,setup,doctor,completions,version,help - Command-specific flags:
--file,--force,--dry-run,--yes,--global,--user,--shell,--all,--clean-logs,--step,--follow,--latest - Dynamic app names: For
logs,retry,reset,status— reads from the lock file to suggest installed app names. - File paths for
--fileflag. - Directory paths for
initcommand.
Writing Manifests — Guidelines for AI Agents
This section is specifically for AI assistants generating VPM manifest files. Follow these rules precisely.
Rules
-
Format: Use the VPM manifest format (described above), NOT standard YAML.
- App headers use
[square_brackets] - Steps start with
- - Multi-line commands use
run: |followed by indented lines - No YAML anchors, no YAML lists with
- key: valuenesting beyond what's described
- App headers use
-
App names: Use
snake_caseidentifiers. No spaces, no special characters.- Good:
[docker_engine],[node_js],[my_web_app] - Bad:
[Docker Engine],[my-web-app],[MyWebApp]
- Good:
-
Every step must have a descriptive
label. Do not rely on auto-generated labels. -
Each step should be idempotent when possible. If a step runs twice, it should not break things.
- Use
apt-get install -y(the-yand the fact that apt skips already-installed packages makes it idempotent) - Use
mkdir -pinstead ofmkdir - Use
gpg --dearmor -o ... --yesto overwrite existing files - Use
ln -sfinstead ofln -s - Check before creating:
id -u username &>/dev/null || useradd username
- Use
-
Group related commands into logical steps. Don't make every single command its own step, but don't put 50 commands in one step either.
- Good: "Install Docker prerequisites" (one step with 3 apt packages)
- Bad: Separate steps for each
apt-get installof related packages - Bad: One giant step that does everything
-
Use
requires:for dependencies between apps. If App B needs something App A installs, declare it. -
Never use
cdas a separate step. It runs in a subshell and won't persist. Use it within a multi-line block:- label: Build the project run: | cd /path/to/project make build make install -
Use
sudoexplicitly where needed. VPM runs as the current user. -
Do not set
DEBIAN_FRONTEND=noninteractiveunless the user specifically asks for unattended installation. VPM supports interactivedebconfscreens. -
If a command might already be done (like adding a repo that might exist), handle it gracefully:
run: | if ! grep -q "some-repo" /etc/apt/sources.list.d/*; then sudo add-apt-repository -y ppa:some/repo fi -
Always include a verification step as the last step of each app when possible:
- label: Verify Docker installation run: docker --version && docker compose version -
Use absolute paths or
$HOMEinstead of~in commands (tilde expansion is unreliable in non-interactive shells):# Good run: mkdir -p $HOME/apps # Avoid run: mkdir -p ~/apps -
For services that need to be enabled/started, combine into one step:
- label: Enable and start Nginx run: | sudo systemctl enable nginx sudo systemctl start nginx sudo systemctl status nginx --no-pager
Best Practices
Ordering Apps by Dependency
Always declare dependencies explicitly rather than relying on file order:
[system_updates]
- label: Update system
run: sudo apt-get update -y && sudo apt-get upgrade -y
[build_tools]
requires: system_updates
- label: Install build essentials
run: sudo apt-get install -y build-essential gcc g++ make
[node_js]
requires: build_tools
- label: Install Node.js via NVM
run: |
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install --lts
Sourcing Profile in Multi-Step Installs
When installing something that modifies PATH or shell environment (like NVM, RVM, cargo), you must re-source it in subsequent steps because each step runs in a fresh shell:
[rust]
- label: Install Rustup
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- label: Verify Rust installation
run: |
source $HOME/.cargo/env
rustc --version
cargo --version
Handling Services That Need Restart
[nginx_config]
requires: nginx
- label: Copy configuration
run: sudo cp /path/to/nginx.conf /etc/nginx/nginx.conf
- label: Test configuration
run: sudo nginx -t
- label: Reload Nginx
run: sudo systemctl reload nginx
Creating Users and Setting Permissions
[app_user]
- label: Create application user
run: |
id -u appuser &>/dev/null || sudo useradd -m -s /bin/bash appuser
sudo mkdir -p /opt/myapp
sudo chown appuser:appuser /opt/myapp
Common Patterns
Pattern: Repository + Package Install
[custom_repo_app]
- label: Add GPG key
run: |
curl -fsSL https://example.com/gpg.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/example.gpg --yes
- label: Add repository
run: |
echo "deb [signed-by=/etc/apt/keyrings/example.gpg] \
https://packages.example.com/deb stable main" | \
sudo tee /etc/apt/sources.list.d/example.list > /dev/null
- label: Install package
run: |
sudo apt-get update -y
sudo apt-get install -y example-package
- label: Verify
run: example-package --version
Pattern: Download + Extract + Install Binary
[binary_tool]
- label: Download latest release
run: |
LATEST=$(curl -s https://api.github.com/repos/org/tool/releases/latest | jq -r .tag_name)
curl -fsSL "https://github.com/org/tool/releases/download/${LATEST}/tool-linux-amd64.tar.gz" \
-o /tmp/tool.tar.gz
- label: Extract and install
run: |
tar xzf /tmp/tool.tar.gz -C /tmp/
sudo mv /tmp/tool /usr/local/bin/tool
sudo chmod +x /usr/local/bin/tool
rm /tmp/tool.tar.gz
- label: Verify
run: tool version
Pattern: Clone + Build from Source
[from_source]
requires: build_tools
- label: Clone repository
run: |
rm -rf /tmp/project-build
git clone --depth 1 https://github.com/org/project.git /tmp/project-build
- label: Build
run: |
cd /tmp/project-build
./configure --prefix=/usr/local
make -j$(nproc)
- label: Install
run: |
cd /tmp/project-build
sudo make install
- label: Clean up build files
run: rm -rf /tmp/project-build
- label: Verify
run: project --version
Pattern: Docker Compose Application
[my_app]
requires: docker
- label: Create app directory
run: mkdir -p $HOME/apps/myapp
- label: Create docker-compose.yml
run: |
cat > $HOME/apps/myapp/docker-compose.yml << 'EOF'
version: "3.8"
services:
web:
image: nginx:alpine
ports:
- "80:80"
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pgdata:
EOF
- label: Start services
run: |
cd $HOME/apps/myapp
docker compose up -d
- label: Verify services
run: |
cd $HOME/apps/myapp
docker compose ps
sleep 3
curl -sf http://localhost:80 > /dev/null && echo "Web server responding"
Anti-Patterns
DON'T: Use separate steps for trivially related commands
# Bad — too granular
- run: sudo apt-get update
- run: sudo apt-get install -y curl
- run: sudo apt-get install -y wget
- run: sudo apt-get install -y git
# Good — logical grouping
- label: Update and install base tools
run: |
sudo apt-get update -y
sudo apt-get install -y curl wget git
DON'T: Assume environment persists between steps
# Bad — variable set in step 1 is lost in step 2
- run: export MY_VAR="hello"
- run: echo $MY_VAR # This will be empty!
# Good — use within same step
- label: Configure and use variable
run: |
export MY_VAR="hello"
echo $MY_VAR
DON'T: Use cd as a standalone step
# Bad — cd has no effect on the next step
- run: cd /opt/myapp
- run: make build # Runs in $HOME, not /opt/myapp!
# Good — cd within the same step
- label: Build project
run: |
cd /opt/myapp
make build
DON'T: Ignore potential failures silently
# Bad — hides errors
- run: some-command || true
# Good — handle explicitly
- label: Run optional command
run: |
if command -v some-command &>/dev/null; then
some-command
else
echo "some-command not found, skipping"
fi
DON'T: Hardcode user-specific paths
# Bad
- run: cp file /home/deploy/apps/
# Good
- run: cp file $HOME/apps/
Template for AI Agents
When asked to create a VPM manifest, use this template structure:
# ═══════════════════════════════════════════════════════
# VPM Manifest: [Brief description of what this sets up]
# Generated for: [User's described use case]
# Target OS: [Ubuntu 22.04 / Debian 12 / etc.]
# ═══════════════════════════════════════════════════════
#
# Usage:
# vpm install --file this-file.yaml
#
# Prerequisites:
# - [List any manual prerequisites]
#
# After installation:
# - [List any manual post-install steps]
#
[first_app] Description
- label: Descriptive step name
run: command
[second_app] Description
requires: first_app
- label: Descriptive step name
run: |
multi-line
command
Always include:
- A header comment block explaining the manifest's purpose
- Usage instructions in comments
- Prerequisites and post-install notes
- Proper dependency declarations
- Verification steps for each app
- Descriptive labels for every step
Troubleshooting
"App already installed" but something is wrong
# Reset tracking and reinstall
vpm reset my_app
vpm install my_app
SSH disconnected during install
# Just run install again — it resumes automatically
vpm install
Want to see what failed
# Quick status
vpm status my_app
# View the latest summary
vpm logs my_app --latest
# View a specific step's full output
vpm logs my_app --step 3
Terminal is garbled after interrupted command
This shouldn't happen (VPM restores terminal settings), but if it does:
reset
# or
stty sane
Lock file corrupted
VPM auto-detects corruption, creates a backup, and starts fresh. You can also:
# Nuclear reset
vpm reset --all --clean-logs
Need completely unattended installation
Set DEBIAN_FRONTEND in your manifest steps:
- label: Install package non-interactively
run: |
export DEBIAN_FRONTEND=noninteractive
sudo apt-get install -y postfix
Or set it globally before running VPM:
export DEBIAN_FRONTEND=noninteractive
vpm install --yes
Architecture
vpm.py (single file, ~1500 lines)
├── Style — ANSI color/formatting with terminal detection
├── UI — Rich terminal components (headers, tables, progress bars, prompts)
├── Config — XDG-compliant path management
├── LockFile — Atomic JSON state persistence
├── StepRecord — Per-step tracking dataclass
├── AppRecord — Per-app tracking dataclass
├── ManifestParser — Custom format parser (no YAML dependency)
├── ManifestApp — Parsed app representation with dependencies
├── SecurityScanner— Static analysis and URL checking for manifest commands
├── Executor — PTY-based command execution with logging
├── Completions — Shell completion generators (zsh, bash, fish)
├── VPM — Main CLI application with all command handlers
└── main() — Argument parsing and dispatch
Key design decisions:
| Decision | Rationale |
|---|---|
| Single file | Easy to deploy to any VPS — just copy one file |
| No pip dependencies | Nothing to install, no virtual environment needed |
| Custom manifest format | Avoids PyYAML dependency while being human-readable |
| PTY execution | Full interactive support (debconf, ncurses, sudo prompts) |
| Atomic lock file writes | Write to .tmp + rename() prevents corruption |
| XDG Base Directory spec | Config in ~/.config/vpm/, data in ~/.local/share/vpm/ |
| SHA-256 command hashing | Detects manifest changes without re-executing |
| Topological sort | Dependency resolution with cycle detection |
| Per-step tracking | Granular resume — skip only what succeeded |
VPM v1.0.0 — Built for humans who manage servers, and the AI agents who help them.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vpmx-1.1.0.tar.gz.
File metadata
- Download URL: vpmx-1.1.0.tar.gz
- Upload date:
- Size: 75.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6c51c6810c01e2e064e086b9143d67097c579119107db960a0897be5d6909c0
|
|
| MD5 |
4274448c3bd419ae3b26dc97036ed4de
|
|
| BLAKE2b-256 |
8453ad8c656a1d7589c45b2c49698be8e2955b46e0488fa9f0918be69cd4846e
|
Provenance
The following attestation bundles were made for vpmx-1.1.0.tar.gz:
Publisher:
publish.yml on Nao-30/vpm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vpmx-1.1.0.tar.gz -
Subject digest:
a6c51c6810c01e2e064e086b9143d67097c579119107db960a0897be5d6909c0 - Sigstore transparency entry: 1356916590
- Sigstore integration time:
-
Permalink:
Nao-30/vpm@9002ba1210cb145cc1b7a3123f429e38f59f3ade -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Nao-30
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9002ba1210cb145cc1b7a3123f429e38f59f3ade -
Trigger Event:
push
-
Statement type:
File details
Details for the file vpmx-1.1.0-py3-none-any.whl.
File metadata
- Download URL: vpmx-1.1.0-py3-none-any.whl
- Upload date:
- Size: 51.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
68fd3cb6b2cfcfbd171b1a9611cb84bdccde5e8af1222d7607c1e2790ec702b1
|
|
| MD5 |
3f4ee645100061ce6459621497258425
|
|
| BLAKE2b-256 |
94b22f88b7363ff989a5537f3e26749a027ea78beaa06c69227d637e7b9ec4aa
|
Provenance
The following attestation bundles were made for vpmx-1.1.0-py3-none-any.whl:
Publisher:
publish.yml on Nao-30/vpm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vpmx-1.1.0-py3-none-any.whl -
Subject digest:
68fd3cb6b2cfcfbd171b1a9611cb84bdccde5e8af1222d7607c1e2790ec702b1 - Sigstore transparency entry: 1356916597
- Sigstore integration time:
-
Permalink:
Nao-30/vpm@9002ba1210cb145cc1b7a3123f429e38f59f3ade -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Nao-30
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9002ba1210cb145cc1b7a3123f429e38f59f3ade -
Trigger Event:
push
-
Statement type: