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
1,260 changes: 328 additions & 932 deletions src/sbx/cli.py

Large diffs are not rendered by default.

64 changes: 29 additions & 35 deletions src/sbx/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
"run",
"create",
"recreate",
"remove",
"rm",
"stop",
"shell",
"list",
"ls",
"network",
"image",
Expand Down Expand Up @@ -51,7 +53,6 @@
"--help",
)
SHELL_OPTIONS = (
"--force-start",
"--keep-running",
"--run-user",
"--project-path",
Expand All @@ -62,8 +63,9 @@
)
LS_OPTIONS = ("--all", "-a", "--help")
RM_OPTIONS = ("--force", "--help")
DOCTOR_OPTIONS = ("--fix", "--help")
STOP_OPTIONS = ("--help",)
RUN_OPTIONS = ("--force-start", *START_OPTIONS)
RUN_OPTIONS = START_OPTIONS
AUTH_PORT_OPTIONS = ("--guest-port", "--host-port", "--replace", "--help")
NETWORK_STATUS_OPTIONS = ("--host-port", "--help")
IMAGE_BUILD_DEBIAN_OPTIONS = (
Expand Down Expand Up @@ -114,6 +116,7 @@ def bash_completion() -> str:
shell_options = _words(SHELL_OPTIONS)
ls_options = _words(LS_OPTIONS)
rm_options = _words(RM_OPTIONS)
doctor_options = _words(DOCTOR_OPTIONS)
stop_options = _words(STOP_OPTIONS)
network_commands = _words(NETWORK_COMMANDS)
image_commands = _words(IMAGE_COMMANDS)
Expand Down Expand Up @@ -175,10 +178,10 @@ def bash_completion() -> str:
shell)
COMPREPLY=( $(compgen -W "{shell_options}" -- "$cur") )
;;
ls)
list|ls)
COMPREPLY=( $(compgen -W "{ls_options}" -- "$cur") )
;;
rm)
remove|rm)
COMPREPLY=( $(compgen -W "{rm_options}" -- "$cur") )
;;
stop)
Expand Down Expand Up @@ -220,6 +223,9 @@ def bash_completion() -> str:
COMPREPLY=( $(compgen -W "{image_ls_options}" -- "$cur") )
fi
;;
doctor)
COMPREPLY=( $(compgen -W "{doctor_options}" -- "$cur") )
;;
completion)
COMPREPLY=( $(compgen -W "{shells}" -- "$cur") )
;;
Expand All @@ -237,6 +243,7 @@ def zsh_completion() -> str:
shell_options = _zsh_words(SHELL_OPTIONS)
ls_options = _zsh_words(LS_OPTIONS)
rm_options = _zsh_words(RM_OPTIONS)
doctor_options = _zsh_words(DOCTOR_OPTIONS)
stop_options = _zsh_words(STOP_OPTIONS)
network_commands = _zsh_words(NETWORK_COMMANDS)
auth_port_options = _zsh_words(AUTH_PORT_OPTIONS)
Expand All @@ -249,7 +256,7 @@ def zsh_completion() -> str:
# zsh completion for sbx
_sbx() {{
local -a commands run_options create_options recreate_options shell_options
local -a ls_options rm_options stop_options network_commands
local -a ls_options rm_options doctor_options stop_options network_commands
local -a auth_port_options network_status_options
local -a image_commands image_build_debian_options image_ls_options shells
commands=({commands})
Expand All @@ -259,6 +266,7 @@ def zsh_completion() -> str:
shell_options=({shell_options})
ls_options=({ls_options})
rm_options=({rm_options})
doctor_options=({doctor_options})
stop_options=({stop_options})
network_commands=({network_commands})
auth_port_options=({auth_port_options})
Expand Down Expand Up @@ -286,10 +294,10 @@ def zsh_completion() -> str:
shell)
_describe 'option' shell_options
;;
ls)
list|ls)
_describe 'option' ls_options
;;
rm)
remove|rm)
_describe 'option' rm_options
;;
stop)
Expand Down Expand Up @@ -321,6 +329,9 @@ def zsh_completion() -> str:
_arguments '*: :->args'
fi
;;
doctor)
_describe 'option' doctor_options
;;
completion)
_describe 'shell' shells
;;
Expand All @@ -346,39 +357,26 @@ def fish_completion() -> str:
for option in GLOBAL_OPTIONS:
lines.append(f"complete -c sbx -f -l {option.removeprefix('--')}")
for command in COMMANDS:
lines.append(
"complete -c sbx -f -n '__fish_use_subcommand' "
f"-a {command}"
)
lines.append(f"complete -c sbx -f -n '__fish_use_subcommand' -a {command}")
for command, options in (
("run", RUN_OPTIONS),
("create", START_OPTIONS),
("recreate", ("--force", *START_OPTIONS)),
("shell", SHELL_OPTIONS),
("list", LS_OPTIONS),
("ls", LS_OPTIONS),
("remove", RM_OPTIONS),
("rm", RM_OPTIONS),
("doctor", DOCTOR_OPTIONS),
("stop", STOP_OPTIONS),
):
for option in options:
lines.append(
f"complete -c sbx -f -n '__fish_seen_subcommand_from {command}' "
f"{_fish_flag(option)}"
)
for agent in AGENTS:
lines.append(
"complete -c sbx -f -n '__fish_seen_argument -l agent' "
f"-a {agent}"
)
for option in SHELL_OPTIONS:
lines.append(
f"complete -c sbx -f -n '__fish_seen_subcommand_from shell' {_fish_flag(option)}"
)
for option in LS_OPTIONS:
lines.append(
f"complete -c sbx -f -n '__fish_seen_subcommand_from ls' {_fish_flag(option)}"
)
for option in RM_OPTIONS:
lines.append(f"complete -c sbx -f -n '__fish_seen_subcommand_from rm' {_fish_flag(option)}")
for option in STOP_OPTIONS:
lines.append(
f"complete -c sbx -f -n '__fish_seen_subcommand_from stop' {_fish_flag(option)}"
)
lines.append(f"complete -c sbx -f -n '__fish_seen_argument -l agent' -a {agent}")
network_subcommands = _words(NETWORK_COMMANDS)
for command in NETWORK_COMMANDS:
lines.append(
Expand All @@ -404,14 +402,10 @@ def fish_completion() -> str:
)
for option in IMAGE_BUILD_DEBIAN_OPTIONS:
lines.append(
"complete -c sbx -f -n '__fish_seen_subcommand_from build-debian' "
f"{_fish_flag(option)}"
f"complete -c sbx -f -n '__fish_seen_subcommand_from build-debian' {_fish_flag(option)}"
)
for option in IMAGE_LS_OPTIONS:
lines.append(
"complete -c sbx -f -n '__fish_seen_subcommand_from ls' "
f"{_fish_flag(option)}"
)
lines.append(f"complete -c sbx -f -n '__fish_seen_subcommand_from ls' {_fish_flag(option)}")
for shell in COMPLETION_SHELLS:
lines.append(f"complete -c sbx -f -n '__fish_seen_subcommand_from completion' -a {shell}")
return "\n".join(lines) + "\n"
1 change: 1 addition & 0 deletions src/sbx/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
SBX_STATE_DIR = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) / "sbx"
TUNNELS_FILE = SBX_STATE_DIR / "tunnels.json"
SESSIONS_FILE = SBX_STATE_DIR / "sessions.json"
SBX_VMS_FILE = SBX_STATE_DIR / "vms.json"
SMOLVM_DB_PATH = Path.home() / ".local" / "state" / "smolvm" / "smolvm.db"
102 changes: 102 additions & 0 deletions src/sbx/doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from pathlib import Path

import sbx.network as network
from sbx import session_state, vm_metadata, vm_state
from sbx.constants import SBX_VMS_FILE
from sbx.runtime import ConfigError


def doctor_metadata(*, fix: bool) -> int:
try:
metadata = vm_metadata.load_vm_metadata()
except ConfigError as exc:
print(f"sbx metadata: {exc}")
if fix and SBX_VMS_FILE.exists():
backup = SBX_VMS_FILE.with_suffix(SBX_VMS_FILE.suffix + ".bak")
SBX_VMS_FILE.replace(backup)
print(f" fixed: moved corrupt metadata to {backup}")
return 0
return 1

if not metadata:
return 0
existing = {str(getattr(vm, "vm_id", "")) for vm in vm_state.smolvm_vms(all_vms=True)}
changed = False
for name, item in list(metadata.items()):
reason = None
if name not in existing:
reason = "VM missing from SmolVM"
elif not Path(item["project_root"]).exists():
reason = f"project path missing: {item['project_root']}"
if reason is None:
continue
print(f"sbx metadata: {name}: {reason}")
if fix:
metadata.pop(name, None)
changed = True
print(f" fixed: removed metadata for {name}")
if changed:
vm_metadata.save_vm_metadata(metadata)
return 0


def doctor_sessions(*, fix: bool) -> None:
data = session_state.load_sessions()
changed = False
for vm_id, vm_data in list(data.items()):
raw = vm_data.get("sessions", []) if isinstance(vm_data, dict) else []
sessions = [item for item in raw if isinstance(item, dict)]
active = session_state.live_sessions(raw)
if active == sessions:
continue
changed = True
print(f"sbx sessions: {vm_id}: stale session record(s)")
if fix:
if active:
data.setdefault(vm_id, {})["sessions"] = active
else:
data.pop(vm_id, None)
if changed and fix:
session_state.save_sessions(data)
print(" fixed: removed stale session record(s)")


def doctor_tunnels(*, fix: bool) -> None:
data = network._load_tunnels()
changed = False
for vm_id, vm_data in list(data.items()):
if not isinstance(vm_data, dict):
data.pop(vm_id, None)
changed = True
continue
tunnel = vm_data.get("auth_port")
pid = tunnel.get("pid") if isinstance(tunnel, dict) else None
if not isinstance(pid, int) or not session_state.pid_is_alive(pid):
print(f"sbx tunnels: {vm_id}: stale auth tunnel record")
if fix:
vm_data.pop("auth_port", None)
if not vm_data:
data.pop(vm_id, None)
changed = True
if changed and fix:
network._save_tunnels(data)
print(" fixed: removed stale tunnel record(s)")


def doctor_error_vms(*, fix: bool) -> None:
for vm in vm_state.smolvm_vms(all_vms=True):
if getattr(getattr(vm, "status", None), "value", getattr(vm, "status", None)) != "error":
continue
name = str(getattr(vm, "vm_id", ""))
print(f"smolvm state: {name}: VM is in error state")
if fix:
vm_state.mark_error_vm_stopped_for_restart(name)
print(f" fixed: {name} marked stopped")


def run_doctor_checks(*, fix: bool) -> int:
meta_rc = doctor_metadata(fix=fix)
doctor_sessions(fix=fix)
doctor_tunnels(fix=fix)
doctor_error_vms(fix=fix)
return meta_rc
Loading