@@ -63,7 +63,7 @@ _agent_vm_ensure_running() {
6363 local vm_name=" $1 "
6464 local host_dir=" $2 "
6565 shift 2
66- local disk=" " memory=" " cpus=" " reset=" " offline=" " rdonly=" " git_ro=" "
66+ local disk=" " memory=" " cpus=" " reset=" " offline=" " rdonly=" " git_ro=" " portforward= " "
6767 while [[ $# -gt 0 ]]; do
6868 case " $1 " in
6969 --disk) disk=" $2 " ; shift 2 ;;
@@ -73,6 +73,7 @@ _agent_vm_ensure_running() {
7373 --offline) offline=1; shift ;;
7474 --readonly) rdonly=1; shift ;;
7575 --git-read-only|--git-ro) git_ro=1; shift ;;
76+ --port-forward) portforward=" $2 " ; shift 2 ;;
7677 * ) shift ;;
7778 esac
7879 done
@@ -112,7 +113,7 @@ _agent_vm_ensure_running() {
112113 if [[ -f " $base_ver " ]]; then
113114 cp " $base_ver " " $AGENT_VM_STATE_DIR /.agent-vm-version-${vm_name} "
114115 fi
115- elif [[ -n " $disk " || -n " $memory " || -n " $cpus " ]]; then
116+ elif [[ -n " $disk " || -n " $memory " || -n " $cpus " || -n " $portforward " ]]; then
116117 # Auto-resize existing VM if --disk, --memory, or --cpus changed
117118 if _agent_vm_running " $vm_name " ; then
118119 echo " VM '$vm_name ' is currently running. It must be stopped to apply new resource settings."
@@ -154,7 +155,9 @@ _agent_vm_ensure_running() {
154155
155156 if ! _agent_vm_running " $vm_name " ; then
156157 echo " Starting VM '$vm_name '..."
157- limactl start " $vm_name " & > /dev/null
158+ local start_args=(" $vm_name " )
159+ [[ -n " $portforward " ]] && start_args+=(--port-forward " $portforward " )
160+ limactl start " ${start_args[@]} " & > /dev/null
158161 fi
159162
160163 # Run per-user runtime script if it exists
@@ -219,6 +222,12 @@ agent-vm() {
219222 vm_opts+=(--git-read-only); shift ;;
220223 --rm)
221224 vm_opts+=(--rm); shift ;;
225+ --port-forward)
226+ vm_opts+=(--port-forward " $2 " ); shift 2 ;;
227+ --port-forward=* )
228+ vm_opts+=(--port-forward " ${1#* =} " ); shift ;;
229+ --background)
230+ vm_opts+=(--background); shift ;;
222231 * )
223232 break ;;
224233 esac
@@ -299,6 +308,8 @@ VM options (for claude, opencode, codex, shell, run):
299308 --readonly Mount the project directory as read-only
300309 --git-read-only Mount .git directory as read-only (allows git diff/log but not commit/stash)
301310 --rm Automatically destroy the VM after the command exits
311+ --port-forward Enable port forwarding (hostfwd)
312+ --background Run in background (detached from terminal)
302313
303314Examples:
304315 agent-vm setup # Create base VM
@@ -311,6 +322,8 @@ Examples:
311322 agent-vm --offline claude # No internet access
312323 agent-vm --readonly shell # Read-only project mount
313324 agent-vm --git-ro claude # Protect .git from writes
325+ agent-vm --port-forward '3000:3000' opencode serve --port 3000 # Forward vm ports to the host
326+ agent-vm --background opencode serve --port 3000 # Run in background
314327 agent-vm shell # Shell into the VM
315328 agent-vm run npm install # Run a command in the VM
316329 agent-vm claude -p "fix lint errors" # Pass args to claude
@@ -331,18 +344,20 @@ _agent_vm_setup() {
331344 local disk=10
332345 local memory=2
333346 local cpus=1
347+ local portforward=" "
334348
335349 while [[ $# -gt 0 ]]; do
336350 case " $1 " in
337351 --help|-h)
338- echo " Usage: agent-vm setup [--disk GB] [--memory GB] [--cpus N]"
352+ echo " Usage: agent-vm setup [--disk GB] [--memory GB] [--cpus N] [--port-forward] "
339353 echo " "
340354 echo " Create a base VM template with dev tools and agents pre-installed."
341355 echo " "
342356 echo " Options:"
343357 echo " --disk GB VM disk size (default: 10)"
344358 echo " --memory GB VM memory (default: 2)"
345359 echo " --cpus N Number of CPUs (default: 1)"
360+ echo " --port-forward Enable port forwarding (hostfwd)"
346361 echo " --help Show this help"
347362 return 0
348363 ;;
@@ -372,6 +387,14 @@ _agent_vm_setup() {
372387 ;;
373388 --reset|--offline|--readonly|--git-read-only|--git-ro)
374389 shift ;;
390+ --port-forward)
391+ portforward=" $2 "
392+ shift 2
393+ ;;
394+ --port-forward=* )
395+ portforward=" ${1#* =} "
396+ shift
397+ ;;
375398 * )
376399 echo " Unknown option: $1 " >&2
377400 echo " Usage: agent-vm setup [--disk GB] [--memory GB] [--cpus N]" >&2
@@ -401,6 +424,7 @@ _agent_vm_setup() {
401424 --tty=false
402425 )
403426 [[ -n " $cpus " ]] && create_args+=(--cpus=" $cpus " )
427+ [[ -n " $portforward " ]] && create_args+=(--port-forward=" $portforward " )
404428 limactl create --name=" $AGENT_VM_TEMPLATE " template:debian-13 \
405429 " ${create_args[@]} " & > /dev/null || { echo " Error: Failed to create base VM." >&2 ; return 1; }
406430
@@ -434,6 +458,7 @@ _agent_vm_claude() {
434458 local vm_opts=()
435459 local args=()
436460 local rm=" "
461+ local background=" "
437462 while [[ $# -gt 0 ]]; do
438463 case " $1 " in
439464 --disk) vm_opts+=(--disk " $2 " ); shift 2 ;;
@@ -444,9 +469,17 @@ _agent_vm_claude() {
444469 --readonly) vm_opts+=(--readonly); shift ;;
445470 --git-read-only|--git-ro) vm_opts+=(--git-read-only); shift ;;
446471 --rm) rm=1; shift ;;
472+ --port-forward) vm_opts+=(--port-forward " $2 " ); shift 2 ;;
473+ --background) background=1; shift ;;
447474 * ) args+=(" $1 " ); shift ;;
448475 esac
449476 done
477+
478+ if [[ -n " $background " && -n " $rm " ]]; then
479+ echo " Error: --background and --rm cannot be used together" >&2
480+ return 1
481+ fi
482+
450483 local host_dir
451484 host_dir=" $( pwd) "
452485 local vm_name
@@ -455,17 +488,27 @@ _agent_vm_claude() {
455488 _agent_vm_ensure_running " $vm_name " " $host_dir " " ${vm_opts[@]} " || return 1
456489 _agent_vm_print_resources " $vm_name "
457490
458- local exit_code=0
459- limactl shell --workdir " $host_dir " " $vm_name " claude --dangerously-skip-permissions " ${args[@]} "
460- exit_code=$?
461- [[ -n " $rm " ]] && { echo " Removing VM..." ; _agent_vm_destroy; }
462- return $exit_code
491+ if [[ -n " $background " ]]; then
492+ ( limactl shell --tty=false --workdir " $host_dir " " $vm_name " claude --dangerously-skip-permissions " ${args[@]} " & > /dev/null & )
493+ echo " Started claude in background"
494+ return 0
495+ elif [[ -n " $rm " ]]; then
496+ limactl shell --workdir " $host_dir " " $vm_name " claude --dangerously-skip-permissions " ${args[@]} "
497+ exit_code=$?
498+ echo " Removing VM..."
499+ _agent_vm_destroy
500+ return $exit_code
501+ else
502+ limactl shell --workdir " $host_dir " " $vm_name " claude --dangerously-skip-permissions " ${args[@]} "
503+ return $?
504+ fi
463505}
464506
465507_agent_vm_opencode () {
466508 local vm_opts=()
467509 local args=()
468510 local rm=" "
511+ local background=" "
469512 while [[ $# -gt 0 ]]; do
470513 case " $1 " in
471514 --disk) vm_opts+=(--disk " $2 " ); shift 2 ;;
@@ -476,9 +519,17 @@ _agent_vm_opencode() {
476519 --readonly) vm_opts+=(--readonly); shift ;;
477520 --git-read-only|--git-ro) vm_opts+=(--git-read-only); shift ;;
478521 --rm) rm=1; shift ;;
522+ --port-forward) vm_opts+=(--port-forward " $2 " ); shift 2 ;;
523+ --background) background=1; shift ;;
479524 * ) args+=(" $1 " ); shift ;;
480525 esac
481526 done
527+
528+ if [[ -n " $background " && -n " $rm " ]]; then
529+ echo " Error: --background and --rm cannot be used together" >&2
530+ return 1
531+ fi
532+
482533 local host_dir
483534 host_dir=" $( pwd) "
484535 local vm_name
@@ -489,17 +540,27 @@ _agent_vm_opencode() {
489540
490541 # TODO: add --dangerously-skip-permissions once released
491542 # (waiting on https://github.com/anomalyco/opencode/pull/11833)
492- local exit_code=0
493- limactl shell --tty --workdir " $host_dir " " $vm_name " opencode " ${args[@]} "
494- exit_code=$?
495- [[ -n " $rm " ]] && { echo " Removing VM..." ; _agent_vm_destroy; }
496- return $exit_code
543+ if [[ -n " $background " ]]; then
544+ ( limactl shell --tty=false --workdir " $host_dir " " $vm_name " opencode " ${args[@]} " & > /dev/null & )
545+ echo " Started opencode in background"
546+ return 0
547+ elif [[ -n " $rm " ]]; then
548+ limactl shell --tty --workdir " $host_dir " " $vm_name " opencode " ${args[@]} "
549+ exit_code=$?
550+ echo " Removing VM..."
551+ _agent_vm_destroy
552+ return $exit_code
553+ else
554+ limactl shell --tty --workdir " $host_dir " " $vm_name " opencode " ${args[@]} "
555+ return $?
556+ fi
497557}
498558
499559_agent_vm_codex () {
500560 local vm_opts=()
501561 local args=()
502562 local rm=" "
563+ local background=" "
503564 while [[ $# -gt 0 ]]; do
504565 case " $1 " in
505566 --disk) vm_opts+=(--disk " $2 " ); shift 2 ;;
@@ -510,9 +571,17 @@ _agent_vm_codex() {
510571 --readonly) vm_opts+=(--readonly); shift ;;
511572 --git-read-only|--git-ro) vm_opts+=(--git-read-only); shift ;;
512573 --rm) rm=1; shift ;;
574+ --port-forward) vm_opts+=(--port-forward " $2 " ); shift 2 ;;
575+ --background) background=1; shift ;;
513576 * ) args+=(" $1 " ); shift ;;
514577 esac
515578 done
579+
580+ if [[ -n " $background " && -n " $rm " ]]; then
581+ echo " Error: --background and --rm cannot be used together" >&2
582+ return 1
583+ fi
584+
516585 local host_dir
517586 host_dir=" $( pwd) "
518587 local vm_name
@@ -521,11 +590,20 @@ _agent_vm_codex() {
521590 _agent_vm_ensure_running " $vm_name " " $host_dir " " ${vm_opts[@]} " || return 1
522591 _agent_vm_print_resources " $vm_name "
523592
524- local exit_code=0
525- limactl shell --workdir " $host_dir " " $vm_name " codex --full-auto " ${args[@]} "
526- exit_code=$?
527- [[ -n " $rm " ]] && { echo " Removing VM..." ; _agent_vm_destroy; }
528- return $exit_code
593+ if [[ -n " $background " ]]; then
594+ ( limactl shell --tty=false --workdir " $host_dir " " $vm_name " codex --full-auto " ${args[@]} " & > /dev/null & )
595+ echo " Started codex in background"
596+ return 0
597+ elif [[ -n " $rm " ]]; then
598+ limactl shell --workdir " $host_dir " " $vm_name " codex --full-auto " ${args[@]} "
599+ exit_code=$?
600+ echo " Removing VM..."
601+ _agent_vm_destroy
602+ return $exit_code
603+ else
604+ limactl shell --workdir " $host_dir " " $vm_name " codex --full-auto " ${args[@]} "
605+ return $?
606+ fi
529607}
530608
531609_agent_vm_shell () {
@@ -540,6 +618,7 @@ _agent_vm_shell() {
540618 --offline) vm_opts+=(--offline); shift ;;
541619 --readonly) vm_opts+=(--readonly); shift ;;
542620 --git-read-only|--git-ro) vm_opts+=(--git-read-only); shift ;;
621+ --port-forward) vm_opts+=(--port-forward " $2 " ); shift 2 ;;
543622 --rm) rm=1; shift ;;
544623 * ) shift ;;
545624 esac
@@ -569,6 +648,7 @@ _agent_vm_run() {
569648 local vm_opts=()
570649 local args=()
571650 local rm=" "
651+ local background=" "
572652 while [[ $# -gt 0 ]]; do
573653 case " $1 " in
574654 --disk) vm_opts+=(--disk " $2 " ); shift 2 ;;
@@ -579,9 +659,16 @@ _agent_vm_run() {
579659 --readonly) vm_opts+=(--readonly); shift ;;
580660 --git-read-only|--git-ro) vm_opts+=(--git-read-only); shift ;;
581661 --rm) rm=1; shift ;;
662+ --port-forward) vm_opts+=(--port-forward " $2 " ); shift 2 ;;
663+ --background) background=1; shift ;;
582664 * ) args+=(" $1 " ); shift ;;
583665 esac
584666 done
667+ if [[ -n " $background " && -n " $rm " ]]; then
668+ echo " Error: --background and --rm cannot be used together" >&2
669+ return 1
670+ fi
671+
585672 if [[ ${# args[@]} -eq 0 ]]; then
586673 echo " Usage: agent-vm run <command> [args]" >&2
587674 return 1
@@ -594,11 +681,20 @@ _agent_vm_run() {
594681 _agent_vm_ensure_running " $vm_name " " $host_dir " " ${vm_opts[@]} " || return 1
595682 _agent_vm_print_resources " $vm_name "
596683
597- local exit_code=0
598- limactl shell --workdir " $host_dir " " $vm_name " " ${args[@]} "
599- exit_code=$?
600- [[ -n " $rm " ]] && { echo " Removing VM..." ; _agent_vm_destroy; }
601- return $exit_code
684+ if [[ -n " $background " ]]; then
685+ ( limactl shell --tty=false --workdir " $host_dir " " $vm_name " " ${args[@]} " & > /dev/null & )
686+ echo " Started '${args[*]} ' in background"
687+ return 0
688+ elif [[ -n " $rm " ]]; then
689+ limactl shell --workdir " $host_dir " " $vm_name " " ${args[@]} "
690+ exit_code=$?
691+ echo " Removing VM..."
692+ _agent_vm_destroy
693+ return $exit_code
694+ else
695+ limactl shell --workdir " $host_dir " " $vm_name " " ${args[@]} "
696+ return $?
697+ fi
602698}
603699
604700_agent_vm_stop () {
0 commit comments