Add initial QVM CLI, Nix flake, scripts and README
This commit is contained in:
parent
25b1cca0e6
commit
8534f7efb9
14 changed files with 2359 additions and 0 deletions
235
bin/qvm-run
Executable file
235
bin/qvm-run
Executable file
|
|
@ -0,0 +1,235 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# qvm-run - Execute a command in the VM workspace
|
||||
#
|
||||
# This script:
|
||||
# 1. Ensures VM is running (auto-starts if needed)
|
||||
# 2. Registers current $PWD as a workspace in workspaces.json
|
||||
# 3. SSHes into VM and executes command in workspace mount point
|
||||
# 4. Streams output and preserves exit code
|
||||
#
|
||||
# Usage: qvm-run <command> [args...]
|
||||
#
|
||||
# Notes:
|
||||
# - Workspaces are pre-mounted at VM start time (no dynamic 9p hotplug)
|
||||
# - If workspace not already registered, warns to restart VM
|
||||
# - Uses workspace hash for mount tag and path
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source common library
|
||||
QVM_LIB_DIR="${QVM_LIB_DIR:-$(dirname "$(readlink -f "$0")")/../lib}"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "$QVM_LIB_DIR/common.sh"
|
||||
|
||||
#
|
||||
# show_usage - Display usage information
|
||||
#
|
||||
show_usage() {
|
||||
cat <<EOF
|
||||
Usage: qvm run <command> [args...]
|
||||
|
||||
Execute a command in the VM at the current workspace directory.
|
||||
|
||||
The current directory (\$PWD) is automatically registered as a workspace
|
||||
and mounted into the VM. Commands run in the mounted workspace directory.
|
||||
|
||||
Examples:
|
||||
qvm run cargo build
|
||||
qvm run npm install
|
||||
qvm run ls -la
|
||||
qvm run bash -c "pwd && ls"
|
||||
|
||||
Notes:
|
||||
- Workspaces are mounted at VM start time
|
||||
- If this workspace is new, you'll need to restart the VM
|
||||
- Command output streams to your terminal
|
||||
- Exit code matches the command's exit code
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
#
|
||||
# register_workspace - Add workspace to registry if not already present
|
||||
# Args: $1 - absolute path to workspace
|
||||
# $2 - workspace hash
|
||||
# Returns: 0 if already registered, 1 if newly added (requires VM restart)
|
||||
#
|
||||
register_workspace() {
|
||||
local workspace_path="$1"
|
||||
local hash="$2"
|
||||
local mount_tag="ws_${hash}"
|
||||
local guest_path="/workspace/${hash}"
|
||||
|
||||
# Create workspaces.json if it doesn't exist
|
||||
if [[ ! -f "$QVM_WORKSPACES_FILE" ]]; then
|
||||
echo '[]' > "$QVM_WORKSPACES_FILE"
|
||||
fi
|
||||
|
||||
# Check if workspace already registered
|
||||
if jq -e --arg path "$workspace_path" '.[] | select(.host_path == $path)' "$QVM_WORKSPACES_FILE" >/dev/null 2>&1; then
|
||||
log_info "Workspace already registered: $workspace_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Add new workspace to registry
|
||||
log_info "Registering new workspace: $workspace_path"
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
|
||||
jq --arg path "$workspace_path" \
|
||||
--arg hash "$hash" \
|
||||
--arg tag "$mount_tag" \
|
||||
--arg guest "$guest_path" \
|
||||
'. += [{
|
||||
host_path: $path,
|
||||
hash: $hash,
|
||||
mount_tag: $tag,
|
||||
guest_path: $guest
|
||||
}]' "$QVM_WORKSPACES_FILE" > "$temp_file"
|
||||
|
||||
mv "$temp_file" "$QVM_WORKSPACES_FILE"
|
||||
log_info "Workspace registered as $mount_tag -> $guest_path"
|
||||
|
||||
return 1 # Indicate new workspace added
|
||||
}
|
||||
|
||||
#
|
||||
# is_workspace_mounted - Check if workspace is actually mounted in VM
|
||||
# Args: $1 - SSH port
|
||||
# $2 - guest path
|
||||
# Returns: 0 if mounted, 1 if not
|
||||
#
|
||||
is_workspace_mounted() {
|
||||
local ssh_port="$1"
|
||||
local guest_path="$2"
|
||||
|
||||
# SSH into VM and check if guest path exists and is a directory
|
||||
if ssh -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o LogLevel=ERROR \
|
||||
-p "$ssh_port" \
|
||||
root@localhost \
|
||||
"test -d '$guest_path'" 2>/dev/null; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# main - Main execution flow
|
||||
#
|
||||
main() {
|
||||
# Show usage if no arguments
|
||||
if [[ $# -eq 0 ]]; then
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Handle help flags
|
||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||||
show_usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get current workspace (absolute path)
|
||||
local workspace_path
|
||||
workspace_path="$(pwd)"
|
||||
|
||||
# Generate workspace hash
|
||||
local hash
|
||||
hash=$(workspace_hash "$workspace_path")
|
||||
|
||||
local guest_path="/workspace/${hash}"
|
||||
|
||||
log_info "Workspace: $workspace_path"
|
||||
log_info "Guest path: $guest_path"
|
||||
|
||||
# Register workspace in registry
|
||||
local newly_added=0
|
||||
if ! register_workspace "$workspace_path" "$hash"; then
|
||||
newly_added=1
|
||||
fi
|
||||
|
||||
# Ensure VM is running
|
||||
if ! is_vm_running; then
|
||||
log_info "VM not running, starting..."
|
||||
|
||||
# Path to qvm-start script
|
||||
local qvm_start="${QVM_LIB_DIR}/../bin/qvm-start"
|
||||
|
||||
if ! "$qvm_start"; then
|
||||
die "Failed to start VM"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get SSH port
|
||||
local ssh_port
|
||||
ssh_port=$(get_ssh_port)
|
||||
|
||||
# Check if workspace is actually mounted in VM
|
||||
if ! is_workspace_mounted "$ssh_port" "$guest_path"; then
|
||||
log_error "Workspace not mounted in VM"
|
||||
echo ""
|
||||
echo "This workspace was just registered but is not mounted in the VM."
|
||||
echo "Workspaces must be mounted at VM start time."
|
||||
echo ""
|
||||
echo "Please restart the VM to mount this workspace:"
|
||||
echo " qvm stop"
|
||||
echo " qvm start"
|
||||
echo ""
|
||||
echo "Then try your command again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build SSH command
|
||||
# - Use -t if stdin is a TTY (for interactive commands)
|
||||
# - Suppress SSH warnings (ephemeral VM, host keys change)
|
||||
# - cd to guest path and execute command
|
||||
local ssh_cmd=(
|
||||
ssh
|
||||
-o StrictHostKeyChecking=no
|
||||
-o UserKnownHostsFile=/dev/null
|
||||
-o LogLevel=ERROR
|
||||
-p "$ssh_port"
|
||||
)
|
||||
|
||||
# Add -t flag if stdin is a TTY
|
||||
if [[ -t 0 ]]; then
|
||||
ssh_cmd+=(-t)
|
||||
fi
|
||||
|
||||
# Add connection target
|
||||
ssh_cmd+=(root@localhost)
|
||||
|
||||
# Build remote command: cd to workspace and execute user's command
|
||||
# Quote each argument properly to handle spaces and special chars
|
||||
local remote_cmd="cd '$guest_path' && "
|
||||
|
||||
# Append user's command with proper quoting
|
||||
local first_arg=1
|
||||
for arg in "$@"; do
|
||||
if [[ $first_arg -eq 1 ]]; then
|
||||
remote_cmd+="$arg"
|
||||
first_arg=0
|
||||
else
|
||||
# Quote arguments that contain spaces or special characters
|
||||
if [[ "$arg" =~ [[:space:]] ]]; then
|
||||
remote_cmd+=" '$arg'"
|
||||
else
|
||||
remote_cmd+=" $arg"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Add the remote command as final SSH argument
|
||||
ssh_cmd+=("$remote_cmd")
|
||||
|
||||
# Execute SSH command (replaces current process)
|
||||
exec "${ssh_cmd[@]}"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue