Add initial QVM CLI, Nix flake, scripts and README

This commit is contained in:
Joshua Bell 2026-01-26 00:16:18 -06:00
parent 25b1cca0e6
commit 8534f7efb9
14 changed files with 2359 additions and 0 deletions

228
bin/qvm-status Executable file
View file

@ -0,0 +1,228 @@
#!/usr/bin/env bash
#
# qvm-status - Display VM state, configuration, and connection information
#
# Shows current VM status including:
# - Running state (PID, uptime, SSH port)
# - Mounted workspaces from workspaces.json
# - Cache directory status
# - Base image and overlay details
# - Connection hints for SSH and run commands
#
# Exit codes:
# 0 - VM is running
# 1 - VM is stopped
set -euo pipefail
# Source common library for shared functions and constants
readonly QVM_LIB_DIR="${QVM_LIB_DIR:-$(dirname "$0")/../lib}"
source "${QVM_LIB_DIR}/common.sh"
# Additional color codes for status display
if [[ -t 1 ]]; then
readonly COLOR_SUCCESS='\033[0;32m' # Green
readonly COLOR_HEADER='\033[1;37m' # Bold White
readonly COLOR_DIM='\033[0;90m' # Dim Gray
else
readonly COLOR_SUCCESS=''
readonly COLOR_HEADER=''
readonly COLOR_DIM=''
fi
#
# format_bytes - Convert bytes to human-readable format
# Args: $1 - size in bytes
# Returns: formatted string (e.g., "1.5G", "256M", "4.0K")
#
format_bytes() {
local bytes="$1"
if (( bytes >= 1073741824 )); then
printf "%.1fG" "$(echo "scale=1; $bytes / 1073741824" | bc)"
elif (( bytes >= 1048576 )); then
printf "%.1fM" "$(echo "scale=1; $bytes / 1048576" | bc)"
elif (( bytes >= 1024 )); then
printf "%.1fK" "$(echo "scale=1; $bytes / 1024" | bc)"
else
printf "%dB" "$bytes"
fi
}
#
# get_uptime - Calculate VM uptime from PID
# Args: $1 - process PID
# Returns: uptime string (e.g., "2h 15m", "45m", "30s")
#
get_uptime() {
local pid="$1"
# Get process start time in seconds since epoch
local start_time
start_time=$(ps -p "$pid" -o lstart= 2>/dev/null | xargs -I{} date -d "{}" +%s)
if [[ -z "$start_time" ]]; then
echo "unknown"
return
fi
local current_time
current_time=$(date +%s)
local uptime_seconds=$((current_time - start_time))
# Format uptime
local hours=$((uptime_seconds / 3600))
local minutes=$(( (uptime_seconds % 3600) / 60 ))
local seconds=$((uptime_seconds % 60))
if (( hours > 0 )); then
printf "%dh %dm" "$hours" "$minutes"
elif (( minutes > 0 )); then
printf "%dm %ds" "$minutes" "$seconds"
else
printf "%ds" "$seconds"
fi
}
#
# show_file_info - Display file status with size and modification time
# Args: $1 - file path
# $2 - label (e.g., "Base Image")
#
show_file_info() {
local file="$1"
local label="$2"
if [[ -f "$file" ]]; then
local size_bytes
size_bytes=$(stat -c %s "$file" 2>/dev/null || echo "0")
local size_human
size_human=$(format_bytes "$size_bytes")
local mod_time
mod_time=$(stat -c %y "$file" 2>/dev/null | cut -d. -f1)
echo -e " ${COLOR_SUCCESS}✓${COLOR_RESET} $label: ${COLOR_INFO}$size_human${COLOR_RESET} ${COLOR_DIM}(modified: $mod_time)${COLOR_RESET}"
else
echo -e " ${COLOR_WARN}✗${COLOR_RESET} $label: ${COLOR_WARN}missing${COLOR_RESET}"
fi
}
#
# show_dir_info - Display directory status
# Args: $1 - directory path
# $2 - label (e.g., "Cargo Home")
#
show_dir_info() {
local dir="$1"
local label="$2"
if [[ -d "$dir" ]]; then
echo -e " ${COLOR_SUCCESS}✓${COLOR_RESET} $label: ${COLOR_DIM}$dir${COLOR_RESET}"
else
echo -e " ${COLOR_WARN}✗${COLOR_RESET} $label: ${COLOR_WARN}not created${COLOR_RESET}"
fi
}
#
# show_workspaces - Display mounted workspaces from workspaces.json
#
show_workspaces() {
if [[ ! -f "$QVM_WORKSPACES_FILE" ]]; then
echo -e " ${COLOR_DIM}No workspaces mounted${COLOR_RESET}"
return
fi
# Check if file is valid JSON and has workspaces
local workspace_count
workspace_count=$(jq 'length' "$QVM_WORKSPACES_FILE" 2>/dev/null || echo "0")
if (( workspace_count == 0 )); then
echo -e " ${COLOR_DIM}No workspaces mounted${COLOR_RESET}"
return
fi
# Parse and display each workspace
jq -r 'to_entries[] | "\(.key)|\(.value.host_path)|\(.value.guest_path)"' "$QVM_WORKSPACES_FILE" 2>/dev/null | while IFS='|' read -r hash host_path guest_path; do
echo -e " ${COLOR_SUCCESS}✓${COLOR_RESET} $hash: ${COLOR_INFO}$host_path${COLOR_RESET} → ${COLOR_DIM}$guest_path${COLOR_RESET}"
done
}
#
# main - Main status display logic
#
main() {
# Header
echo -e "${COLOR_HEADER}QVM Status${COLOR_RESET}"
echo ""
# VM State
echo -e "${COLOR_HEADER}VM State:${COLOR_RESET}"
if is_vm_running; then
local pid
pid=$(cat "$QVM_PID_FILE")
local ssh_port
if [[ -f "$QVM_SSH_PORT_FILE" ]]; then
ssh_port=$(cat "$QVM_SSH_PORT_FILE")
else
ssh_port="unknown"
fi
local uptime
uptime=$(get_uptime "$pid")
echo -e " ${COLOR_SUCCESS}✓${COLOR_RESET} Running"
echo -e " ${COLOR_DIM}PID:${COLOR_RESET} $pid"
echo -e " ${COLOR_DIM}SSH:${COLOR_RESET} localhost:$ssh_port"
echo -e " ${COLOR_DIM}Uptime:${COLOR_RESET} $uptime"
else
echo -e " ${COLOR_WARN}✗${COLOR_RESET} Stopped"
fi
echo ""
# Workspaces
echo -e "${COLOR_HEADER}Mounted Workspaces:${COLOR_RESET}"
show_workspaces
echo ""
# Cache Directories
echo -e "${COLOR_HEADER}Cache Directories:${COLOR_RESET}"
show_dir_info "$QVM_CARGO_HOME" "Cargo Home"
show_dir_info "$QVM_CARGO_TARGET" "Cargo Target"
show_dir_info "$QVM_PNPM_STORE" "PNPM Store"
show_dir_info "$QVM_SCCACHE" "SCCache"
echo ""
# VM Images
echo -e "${COLOR_HEADER}VM Images:${COLOR_RESET}"
show_file_info "$QVM_BASE_IMAGE" "Base Image"
show_file_info "$QVM_OVERLAY" "Overlay"
echo ""
# Connection Hints (only if VM is running)
if is_vm_running; then
local ssh_port
ssh_port=$(cat "$QVM_SSH_PORT_FILE" 2>/dev/null || echo "unknown")
echo -e "${COLOR_HEADER}Connection:${COLOR_RESET}"
echo -e " ${COLOR_INFO}SSH:${COLOR_RESET} qvm ssh"
echo -e " ${COLOR_INFO}Run cmd:${COLOR_RESET} qvm run <command>"
echo -e " ${COLOR_INFO}Direct:${COLOR_RESET} ssh -p $ssh_port root@localhost"
echo ""
# Exit success if running
exit 0
else
echo -e "${COLOR_HEADER}Quick Start:${COLOR_RESET}"
echo -e " ${COLOR_INFO}Start VM:${COLOR_RESET} qvm start"
echo ""
# Exit failure if stopped
exit 1
fi
}
# Execute main
main "$@"