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

169
lib/common.sh Normal file
View file

@ -0,0 +1,169 @@
#!/usr/bin/env bash
#
# common.sh - Shared functions and configuration for QVM CLI tool
#
# This file defines XDG-compliant directory paths, constants, and utility
# functions used across all qvm-* commands. It should be sourced by each
# command script via: source "${QVM_LIB_DIR}/common.sh"
#
set -euo pipefail
# XDG-compliant directory paths
readonly QVM_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/qvm"
readonly QVM_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/qvm"
readonly QVM_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/qvm"
readonly QVM_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/qvm"
# Path constants for VM artifacts
readonly QVM_BASE_IMAGE="$QVM_DATA_DIR/base.qcow2"
readonly QVM_OVERLAY="$QVM_STATE_DIR/overlay.qcow2"
readonly QVM_PID_FILE="$QVM_STATE_DIR/vm.pid"
readonly QVM_SSH_PORT_FILE="$QVM_STATE_DIR/ssh.port"
readonly QVM_SERIAL_LOG="$QVM_STATE_DIR/serial.log"
readonly QVM_WORKSPACES_FILE="$QVM_STATE_DIR/workspaces.json"
readonly QVM_USER_FLAKE="$QVM_CONFIG_DIR/flake"
# Cache directories for 9p mounts (shared between host and VM)
readonly QVM_CARGO_HOME="$QVM_CACHE_DIR/cargo-home"
readonly QVM_CARGO_TARGET="$QVM_CACHE_DIR/cargo-target"
readonly QVM_PNPM_STORE="$QVM_CACHE_DIR/pnpm-store"
readonly QVM_SCCACHE="$QVM_CACHE_DIR/sccache"
# Color codes (only used if stdout is a TTY)
if [[ -t 1 ]]; then
readonly COLOR_INFO='\033[0;36m' # Cyan
readonly COLOR_WARN='\033[0;33m' # Yellow
readonly COLOR_ERROR='\033[0;31m' # Red
readonly COLOR_RESET='\033[0m' # Reset
else
readonly COLOR_INFO=''
readonly COLOR_WARN=''
readonly COLOR_ERROR=''
readonly COLOR_RESET=''
fi
#
# log_info - Print informational message in cyan
# Usage: log_info "message"
#
log_info() {
echo -e "${COLOR_INFO}[INFO]${COLOR_RESET} $*" >&2
}
#
# log_warn - Print warning message in yellow
# Usage: log_warn "message"
#
log_warn() {
echo -e "${COLOR_WARN}[WARN]${COLOR_RESET} $*" >&2
}
#
# log_error - Print error message in red
# Usage: log_error "message"
#
log_error() {
echo -e "${COLOR_ERROR}[ERROR]${COLOR_RESET} $*" >&2
}
#
# die - Print error message and exit with status 1
# Usage: die "error message"
#
die() {
log_error "$@"
exit 1
}
#
# ensure_dirs - Create all required QVM directories
# Usage: ensure_dirs
#
ensure_dirs() {
mkdir -p "$QVM_DATA_DIR" \
"$QVM_STATE_DIR" \
"$QVM_CACHE_DIR" \
"$QVM_CONFIG_DIR" \
"$QVM_CARGO_HOME" \
"$QVM_CARGO_TARGET" \
"$QVM_PNPM_STORE" \
"$QVM_SCCACHE"
}
#
# is_vm_running - Check if VM process is running
# Returns: 0 if running, 1 if not
# Usage: if is_vm_running; then ... fi
#
is_vm_running() {
if [[ ! -f "$QVM_PID_FILE" ]]; then
return 1
fi
local pid
pid=$(cat "$QVM_PID_FILE")
# Check if process exists and is a QEMU process
if kill -0 "$pid" 2>/dev/null; then
return 0
else
# Stale PID file, remove it
rm -f "$QVM_PID_FILE"
return 1
fi
}
#
# get_ssh_port - Read SSH port from state file
# Returns: SSH port number on stdout
# Usage: port=$(get_ssh_port)
#
get_ssh_port() {
if [[ ! -f "$QVM_SSH_PORT_FILE" ]]; then
die "SSH port file not found. Is the VM running?"
fi
cat "$QVM_SSH_PORT_FILE"
}
#
# workspace_hash - Generate short hash from absolute path
# Args: $1 - absolute path to workspace
# Returns: 8-character hash on stdout
# Usage: hash=$(workspace_hash "/path/to/workspace")
#
workspace_hash() {
local path="$1"
echo -n "$path" | sha256sum | cut -c1-8
}
#
# wait_for_ssh - Wait for SSH to become available on VM
# Args: $1 - SSH port number
# $2 - timeout in seconds (default: 60)
# Returns: 0 if SSH is available, 1 on timeout
# Usage: wait_for_ssh "$port" 30
#
wait_for_ssh() {
local port="${1:-}"
local timeout="${2:-60}"
local elapsed=0
if [[ -z "$port" ]]; then
die "wait_for_ssh requires port argument"
fi
log_info "Waiting for SSH on port $port (timeout: ${timeout}s)..."
while (( elapsed < timeout )); do
if nc -z -w 1 localhost "$port" 2>/dev/null; then
log_info "SSH is ready"
return 0
fi
sleep 1
(( elapsed++ ))
done
log_error "SSH did not become available within ${timeout}s"
return 1
}