Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
- Tightened CLI arg validation: `init`, `sync`, `sh`, `ssh-key`, `gpg-key`, `ls`, and
`stop` now reject unexpected extra arguments with a clear error rather than
silently ignoring them.
- Added `dvm stop --all` to stop every DVM-managed Lima instance while preserving VM
disks and config, plus `dvm stop --inactive` / `dvm stop --all --inactive` for
stopping only VMs without a detected active shell, `tmux`/`zellij`, or known DVM
service unit.
- Added an opt-in zsh completion file at `share/dvm/completions/_dvm` for commands,
options, VM names, and bundled `init` templates.
- Tightened `dvm rm` dirty check: when `git` is not installed in the guest, the
check now exits with status 2 and refuses to delete the VM unless `--force` is
passed. Previously the check exited cleanly when `git` was absent, allowing
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ from a temporary snapshot of `bin/dvm` and its shell libraries, so editing or pu
this repo cannot corrupt a long-running `dvm sync`. Bundled recipes, the Lima
template, and example VM configs stay in the repo under `share/dvm`.

For zsh completion, add the in-repo completion directory before `compinit` in
`~/.zshrc`:

```zsh
fpath=(/path/to/dvm/share/dvm/completions $fpath)
autoload -Uz compinit
compinit
```

If your `~/.zshrc` already runs `compinit`, add only the `fpath=...` line above it.

## Commands

```bash
Expand All @@ -47,6 +58,8 @@ dvm ssh-key app
dvm gpg-key app
dvm ls
dvm stop app
dvm stop --all
dvm stop --inactive
dvm rm app --yes
```

Expand All @@ -59,6 +72,11 @@ tools such as AI CLIs across every active VM.
`dvm rm` requires `--yes` and checks nested Git repos for dirty work before deleting.
Use `--force` only when you intentionally want to skip that check.

`dvm stop --all` stops every DVM-managed Lima instance listed with the internal
`dvm-` prefix. This releases VM memory without deleting disks or config.
Use `dvm stop --inactive` to stop only VMs without a detected active shell,
`tmux`/`zellij`, or known DVM service unit.

`dvm ssh-key <name>` creates separate VM-local SSH keys for GitHub access and Git commit
signing. Use the access key as a deploy/authentication key and add the signing key to
your GitHub account's SSH signing keys.
Expand Down
2 changes: 1 addition & 1 deletion bin/dvm
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ main() {
ssh-key) [ "$#" -eq 1 ] || die "ssh-key takes one VM name"; ssh_key_vm "$@" ;;
gpg-key) [ "$#" -eq 1 ] || die "gpg-key takes one VM name"; gpg_key_vm "$@" ;;
ls) [ "$#" -eq 0 ] || die "ls takes no arguments"; list_vms ;;
stop) [ "$#" -eq 1 ] || die "stop takes one VM name"; stop_vm "$@" ;;
stop) stop_command "$@" ;;
rm) [ "$#" -ge 1 ] || die "rm requires a VM name"; rm_vm "$@" ;;
help | -h | --help) usage ;;
*) usage >&2; die "unknown command: $cmd" ;;
Expand Down
41 changes: 40 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ DVM keeps the command surface small. Most day-to-day work should still be `sync`
Use public project names in DVM commands: `app`, `eshlox-net`, `llama`. The `dvm-`
prefix is reserved for internal Lima instance names.

## Zsh Completion

DVM ships an opt-in zsh completion file in the repo:

```zsh
# ~/.zshrc
fpath=(/path/to/dvm/share/dvm/completions $fpath)
autoload -Uz compinit
compinit
```

Zsh loads completion functions from directories in `fpath`, so the path points to the
`completions` directory, not directly to `_dvm`. If your `~/.zshrc` already runs
`compinit`, add only the `fpath=...` line above the existing `compinit` call.

The completion includes DVM commands, command options, VM names from
`$DVM_CONFIG/vms/*.sh`, DVM Lima instances from `limactl list`, and bundled `init`
templates.

## Init

```bash
Expand Down Expand Up @@ -136,9 +155,29 @@ internal `dvm-` prefix and are aligned for terminal output.

```bash
dvm stop app
dvm stop --all
dvm stop --all --inactive
dvm stop --inactive
dvm stop --inactive --force
```

Stops the Lima VM.
`stop <name>` stops one Lima VM.

`stop --all` stops every DVM-managed Lima instance listed with the internal `dvm-`
prefix, including instances whose DVM config was later removed. It releases VM memory
without deleting disks or config.

`stop --inactive` is shorthand for `stop --all --inactive`. It probes each running
DVM VM and stops only VMs without a detected interactive shell, `tmux` process,
`zellij` process, or active known DVM service unit (`dvm-cloudflared.service`,
`dvm-llama.service`, `tailscaled.service`). This is intentionally conservative; it
does not prove that arbitrary background jobs or dev servers are idle unless they are
inside one of those detected sessions or services.

Bulk stop commands skip already stopped instances, report failures, and exit non-zero
if any VM failed to stop. With `--inactive`, active instances are skipped too, and
`--force` stops a VM when the activity probe fails; VMs that are successfully detected
as active are still skipped. To stop active VMs too, use plain `dvm stop --all`.

## Remove

Expand Down
1 change: 1 addition & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ init_config() {
rel="${file#"$src"/}"
case "$rel" in
lima.yaml.in) ;;
completions/*) ;;
lib/*) ;;
recipes/*) ;;
vms/*) ;;
Expand Down
203 changes: 203 additions & 0 deletions share/dvm/completions/_dvm
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#compdef dvm

local -a _dvm_commands
_dvm_commands=(
'init:create a VM config from a bundled template'
'sync:create or update a VM'
'sh:open an interactive VM shell'
'ssh:run a command in a VM'
'cp:copy files between host and VM'
'log:show VM journal logs'
'ssh-key:create or show VM-local SSH keys'
'gpg-key:create or show a VM-local GPG key'
'ls:list DVM-managed VMs'
'stop:stop one or more VMs'
'rm:delete a VM'
'help:show help'
)

_dvm_vm_names() {
local config_dir file name
local -a names
config_dir="${DVM_CONFIG:-$HOME/.config/dvm}"

for file in "$config_dir"/vms/*.sh(N); do
names+=("${file:t:r}")
done

if (( $+commands[limactl] )); then
while IFS= read -r name; do
[[ "$name" == dvm-* ]] && names+=("${name#dvm-}")
done < <(limactl list --format '{{.Name}}' 2>/dev/null)
fi

(( $#names )) && print -r -- "${(F)${(u)names}}"
}

_dvm_complete_vm_names() {
local expl
local -a names
names=("${(@f)$(_dvm_vm_names)}")
(( $#names )) && _wanted vms expl 'DVM VM' compadd "$expl[@]" -a names
}

_dvm_share_dirs() {
local dir share
local -a dirs

[[ -n "${DVM_SHARE:-}" && -d "$DVM_SHARE/vms" ]] && dirs+=("$DVM_SHARE")
[[ -n "${DVM_ROOT:-}" && -d "$DVM_ROOT/share/dvm/vms" ]] && dirs+=("$DVM_ROOT/share/dvm")

for dir in $fpath; do
share="${dir:A:h}"
[[ -r "$dir/_dvm" && -d "$share/vms" ]] && dirs+=("$share")
done

(( $#dirs )) && print -r -- "${(F)${(u)dirs}}"
}

_dvm_complete_templates() {
local dir expl file
local -a templates

for dir in "${(@f)$(_dvm_share_dirs)}"; do
for file in "$dir"/vms/*.sh(N); do
templates+=("${file:t:r}")
done
done

templates=("${(@u)templates}")
(( $#templates )) && _wanted templates expl 'DVM template' compadd "$expl[@]" -a templates
}

_dvm_complete_vm_paths() {
local name
local -a names paths
names=("${(@f)$(_dvm_vm_names)}")
for name in $names; do
paths+=("$name:")
done
(( $#paths )) && compadd -S '' -a paths
}

_dvm_describe() {
local tag="$1" label="$2"
shift 2
local -a values
values=("$@")
_describe -t "$tag" "$label" values
}

_dvm_complete_sync() {
local -a opts
opts=('--all:sync every VM config')
_dvm_describe options 'sync option' "$opts[@]"
_dvm_complete_vm_names
}

_dvm_complete_stop() {
local -a opts
opts=(
'--all:stop every DVM-managed VM'
'--inactive:stop only VMs without detected active sessions or services'
'--force:with --inactive, stop when the activity probe fails'
'-f:alias for --force'
)
_dvm_describe options 'stop option' "$opts[@]"
(( ${words[(I)--all]} || ${words[(I)--inactive]} )) || _dvm_complete_vm_names
}

_dvm_complete_rm() {
local -a opts
opts=(
'--yes:confirm VM deletion'
'--force:skip dirty checks'
'-f:alias for --force'
)
_dvm_describe options 'rm option' "$opts[@]"
_dvm_complete_vm_names
}

_dvm_complete_cp() {
case "${words[CURRENT - 1]}" in
--backend)
compadd auto scp rsync
return
;;
esac
case "$PREFIX" in
--backend=*)
compset -P '--backend='
compadd auto scp rsync
return
;;
esac
local -a opts
opts=(
'-r:copy directories recursively'
'--recursive:copy directories recursively'
'-v:show copy details'
'--verbose:show copy details'
'--backend=:select copy backend'
)
_dvm_describe options 'copy option' "$opts[@]"
_dvm_complete_vm_paths
_files
}

_dvm_complete_log() {
local -a units
units=(
'dvm-cloudflared.service:cloudflared service'
'dvm-llama.service:llama service'
'tailscaled.service:tailscale service'
)
if (( CURRENT == 3 )); then
_dvm_complete_vm_names
else
_dvm_describe units 'systemd unit' "$units[@]"
fi
}

local cmd

if (( CURRENT == 2 )); then
_dvm_describe commands 'dvm command' "$_dvm_commands[@]"
return
fi

cmd="$words[2]"
case "$cmd" in
init)
if (( CURRENT == 4 )); then
_dvm_complete_templates
fi
;;
sync)
_dvm_complete_sync
;;
sh | ssh-key | gpg-key)
_dvm_complete_vm_names
;;
ssh)
if (( CURRENT == 3 )); then
_dvm_complete_vm_names
else
_normal
fi
;;
cp)
_dvm_complete_cp
;;
log)
_dvm_complete_log
;;
stop)
_dvm_complete_stop
;;
rm)
_dvm_complete_rm
;;
*)
;;
esac
2 changes: 2 additions & 0 deletions share/dvm/lib/core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ usage:
dvm gpg-key <name>
dvm ls
dvm stop <name>
dvm stop --all [--inactive] [--force]
dvm stop --inactive [--force]
dvm rm <name> --yes [--force]
HELP
}
Expand Down
Loading