agentworks-cli 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentworks/__init__.py +1 -0
- agentworks/agents/__init__.py +0 -0
- agentworks/agents/manager.py +1095 -0
- agentworks/agents/templates.py +145 -0
- agentworks/catalog.py +264 -0
- agentworks/catalog.toml +131 -0
- agentworks/cli.py +1462 -0
- agentworks/completions/__init__.py +33 -0
- agentworks/completions/bash.py +179 -0
- agentworks/completions/install.py +122 -0
- agentworks/completions/powershell.py +270 -0
- agentworks/completions/spec.py +216 -0
- agentworks/completions/zsh.py +256 -0
- agentworks/config.py +894 -0
- agentworks/db.py +1083 -0
- agentworks/doctor.py +430 -0
- agentworks/git_credentials/__init__.py +0 -0
- agentworks/git_credentials/azdo.py +29 -0
- agentworks/git_credentials/base.py +71 -0
- agentworks/git_credentials/github.py +22 -0
- agentworks/nerf-config.yaml +16 -0
- agentworks/output.py +296 -0
- agentworks/remote_exec.py +286 -0
- agentworks/sample-config.toml +289 -0
- agentworks/sessions/__init__.py +0 -0
- agentworks/sessions/console.py +164 -0
- agentworks/sessions/manager.py +1297 -0
- agentworks/sessions/templates.py +101 -0
- agentworks/sessions/tmux.py +503 -0
- agentworks/sources.py +303 -0
- agentworks/ssh.py +759 -0
- agentworks/ssh_config.py +255 -0
- agentworks/vm_hosts/__init__.py +0 -0
- agentworks/vm_hosts/manager.py +86 -0
- agentworks/vms/__init__.py +0 -0
- agentworks/vms/backup.py +409 -0
- agentworks/vms/base.py +56 -0
- agentworks/vms/bootstrap_script.py +185 -0
- agentworks/vms/cloud_init.py +55 -0
- agentworks/vms/initializer.py +1523 -0
- agentworks/vms/manager.py +1122 -0
- agentworks/vms/provisioners/__init__.py +0 -0
- agentworks/vms/provisioners/azure.py +602 -0
- agentworks/vms/provisioners/lima.py +295 -0
- agentworks/vms/provisioners/proxmox.py +279 -0
- agentworks/vms/provisioners/proxmox_api.py +261 -0
- agentworks/vms/provisioners/wsl2.py +340 -0
- agentworks/vms/templates.py +152 -0
- agentworks/workspaces/__init__.py +0 -0
- agentworks/workspaces/backends/__init__.py +0 -0
- agentworks/workspaces/backends/local.py +119 -0
- agentworks/workspaces/backends/vm.py +175 -0
- agentworks/workspaces/manager.py +1080 -0
- agentworks/workspaces/templates.py +76 -0
- agentworks/workspaces/tmuxinator.py +80 -0
- agentworks_cli-0.2.1.dist-info/METADATA +635 -0
- agentworks_cli-0.2.1.dist-info/RECORD +59 -0
- agentworks_cli-0.2.1.dist-info/WHEEL +4 -0
- agentworks_cli-0.2.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Agentworks configuration
|
|
2
|
+
# Copy to ~/.config/agentworks/config.toml and edit to match your setup.
|
|
3
|
+
#
|
|
4
|
+
# Required fields are uncommented. Everything else shows the default value
|
|
5
|
+
# commented out -- uncomment and change only what you need.
|
|
6
|
+
#
|
|
7
|
+
# Agentworks ships a built-in catalog of common tools (apt sources, apt
|
|
8
|
+
# packages, system install commands, user install commands). Run
|
|
9
|
+
# `agentworks installer list` to see what is available. You can reference
|
|
10
|
+
# catalog entries by name in templates and define your own entries below
|
|
11
|
+
# to extend or override the catalog.
|
|
12
|
+
|
|
13
|
+
# =============================================================================
|
|
14
|
+
# Operator identity and platform settings
|
|
15
|
+
# =============================================================================
|
|
16
|
+
|
|
17
|
+
# --- Operator identity ---
|
|
18
|
+
# These settings identify you (the operator) to agentworks and your VMs.
|
|
19
|
+
|
|
20
|
+
[operator]
|
|
21
|
+
ssh_public_key = "~/.ssh/id_ed25519.pub" # required: used to authorize access to VMs
|
|
22
|
+
ssh_private_key = "~/.ssh/id_ed25519" # required: used for SSH connections to VMs
|
|
23
|
+
# ssh_config = "~/.ssh/config" # SSH config file for VM host entries
|
|
24
|
+
# ssh_config_dir = true # manage VM entries in ~/.ssh/config.d/
|
|
25
|
+
# ssh_host_prefix = "awvm--" # prefix for SSH host aliases (e.g. awvm--myvm)
|
|
26
|
+
# extra_ssh_public_keys = [ # additional public keys for VM authorized_keys
|
|
27
|
+
# "~/.ssh/other_key.pub",
|
|
28
|
+
# ]
|
|
29
|
+
|
|
30
|
+
# --- Paths ---
|
|
31
|
+
|
|
32
|
+
[paths]
|
|
33
|
+
# local_workspaces = "~/workspaces" # local workspace directory
|
|
34
|
+
# vm_workspaces = "/opt/agentworks/workspaces" # workspace directory on VMs
|
|
35
|
+
# vscode_workspaces = "~/aw-vscode-workspaces" # .code-workspace file directory
|
|
36
|
+
# backups = "~/.config/agentworks/backups" # vm backup directory
|
|
37
|
+
|
|
38
|
+
# --- Default values for CLI flags ---
|
|
39
|
+
|
|
40
|
+
[defaults]
|
|
41
|
+
# platform = "lima" # lima, azure, or wsl2
|
|
42
|
+
# vm_host = "mac-studio" # default VM host for remote Lima
|
|
43
|
+
|
|
44
|
+
# --- Git credential providers ---
|
|
45
|
+
# Define how to obtain git tokens. Referenced by name in admin.config and
|
|
46
|
+
# agent_templates. Tokens are prompted during VM init (or read from env vars)
|
|
47
|
+
# and stored via git credential-store on the VM.
|
|
48
|
+
|
|
49
|
+
# [git_credentials.github]
|
|
50
|
+
# type = "github"
|
|
51
|
+
# description = "GitHub full access" # optional, shown in prompts and logs
|
|
52
|
+
# Token: create at https://github.com/settings/tokens (repo scope)
|
|
53
|
+
|
|
54
|
+
# [git_credentials.azdo]
|
|
55
|
+
# type = "azdo"
|
|
56
|
+
# org = "my-org"
|
|
57
|
+
# description = "Azure DevOps for my-org" # optional, shown in prompts and logs
|
|
58
|
+
# Token: create at https://dev.azure.com/{org}/_usersSettings/tokens
|
|
59
|
+
|
|
60
|
+
# --- Azure settings (only needed for --platform azure) ---
|
|
61
|
+
|
|
62
|
+
# [azure]
|
|
63
|
+
# subscription_id = "..." # required for azure platform
|
|
64
|
+
# resource_group = "agentworks-vms" # required for azure platform
|
|
65
|
+
# region = "eastus2" # required for azure platform
|
|
66
|
+
|
|
67
|
+
# --- Proxmox settings (only needed for --platform proxmox) ---
|
|
68
|
+
|
|
69
|
+
# [proxmox]
|
|
70
|
+
# api_url = "https://pve.example.com:8006" # required: Proxmox API URL
|
|
71
|
+
# node = "pve" # required: Proxmox node name
|
|
72
|
+
# token_id = "agentworks@pam!agentworks" # required: API token ID
|
|
73
|
+
# template_vmid = 9000 # required: VMID of Debian 12 cloud-init template
|
|
74
|
+
# storage = "local-lvm" # target storage for cloned disks
|
|
75
|
+
# bridge = "vmbr0" # network bridge
|
|
76
|
+
# pool = "agentworks" # Proxmox resource pool for VMs
|
|
77
|
+
# verify_ssl = true # verify TLS certificate
|
|
78
|
+
# Token secret: set PROXMOX_TOKEN_SECRET environment variable
|
|
79
|
+
|
|
80
|
+
# =============================================================================
|
|
81
|
+
# Templates
|
|
82
|
+
# =============================================================================
|
|
83
|
+
#
|
|
84
|
+
# Agentworks uses templates for VMs, workspaces, agents, and tasks. Each
|
|
85
|
+
# template type supports a "default" entry used when --template is not
|
|
86
|
+
# specified. Templates support inheritance via the "inherits" key:
|
|
87
|
+
# child values override parents, depth-first left-to-right.
|
|
88
|
+
|
|
89
|
+
# --- VM templates ---
|
|
90
|
+
# System-level VM configuration: hardware resources, system packages, and
|
|
91
|
+
# system-wide tools. Selected at vm create time with --template.
|
|
92
|
+
#
|
|
93
|
+
# Agentworks always installs system packages (git, tmux, tmuxinator, acl, jq,
|
|
94
|
+
# unzip) during init, in addition to provisioning packages (openssh-server,
|
|
95
|
+
# curl, sudo, ca-certificates, gnupg) installed at VM create time.
|
|
96
|
+
|
|
97
|
+
[vm_templates.default]
|
|
98
|
+
# -- Provisioning (set once at create time) --
|
|
99
|
+
# cpus = 4 # number of CPUs (Lima)
|
|
100
|
+
# memory = 8 # memory in GiB (Lima)
|
|
101
|
+
# azure_vm_size = "Standard_B2s" # Azure VM size (determines cpu and memory for Azure)
|
|
102
|
+
# disk = 50 # disk size in GiB
|
|
103
|
+
# swap = 4 # swap file size in GiB (0 to disable)
|
|
104
|
+
# -- System-wide initialization (applied on create and reinit) --
|
|
105
|
+
# apt = [ # direct apt packages
|
|
106
|
+
# "zsh",
|
|
107
|
+
# "unzip",
|
|
108
|
+
# "build-essential",
|
|
109
|
+
# "ripgrep",
|
|
110
|
+
# "fzf",
|
|
111
|
+
# "fd-find",
|
|
112
|
+
# ]
|
|
113
|
+
# apt_packages = ["gh", "docker"] # named apt package sets from catalog
|
|
114
|
+
# snap = [] # direct snap packages
|
|
115
|
+
# system_install_commands = ["az-cli"] # system-level commands from catalog
|
|
116
|
+
# -- Nerf tools --
|
|
117
|
+
# nerf_build_claude_plugin = false # build Claude Code nerf tools plugin
|
|
118
|
+
# skip_nerf_defaults = false # skip built-in nerf catalog
|
|
119
|
+
# nerf_addl_manifests = [ # additional manifest files to merge in
|
|
120
|
+
# "~/.config/agentworks/nerf/my-tools/manifest.yaml",
|
|
121
|
+
# ]
|
|
122
|
+
# nerf_home_dir = "/opt/agentworks/nerf" # root dir for nerf artifacts
|
|
123
|
+
|
|
124
|
+
# [vm_templates.heavy]
|
|
125
|
+
# inherits = ["default"]
|
|
126
|
+
# cpus = 16
|
|
127
|
+
# memory = 64
|
|
128
|
+
# disk = 200
|
|
129
|
+
|
|
130
|
+
# --- Admin user configuration ---
|
|
131
|
+
# Per-user settings for the admin (operator) user on VMs. Applied during
|
|
132
|
+
# init/reinit. This is not a template; there is one admin config.
|
|
133
|
+
|
|
134
|
+
# [admin.config]
|
|
135
|
+
# username = "agentworks" # admin username on VMs (default: agentworks)
|
|
136
|
+
# shell = "zsh" # admin shell (default: zsh)
|
|
137
|
+
# git_credentials = ["github"] # git credential providers to configure
|
|
138
|
+
# user_install_commands = ["bun"] # per-user commands from catalog
|
|
139
|
+
# -- Dotfiles (source refs: local path or git::, see docs/guides/source-refs.md) --
|
|
140
|
+
# dotfiles_source = "git::https://github.com/user/dotfiles"
|
|
141
|
+
# dotfiles_destination = "~/.dotfiles" # where to put them on the VM
|
|
142
|
+
# dotfiles_install_cmd = "./install.sh" # command to run after clone/copy
|
|
143
|
+
# -- Mise (see docs/guides/mise.md) --
|
|
144
|
+
# mise_activate = true # add mise activate to shell profile
|
|
145
|
+
# mise_packages = ["terraform@1.14.5", "adr-tools@3.0.0"] # name@version tool declarations
|
|
146
|
+
# mise_lockfile = "~/.config/agentworks/mise.lock" # source ref (wins over dotfiles)
|
|
147
|
+
# mise_allow_unlocked = false # install packages not in lockfile (with warning)
|
|
148
|
+
# mise_install_before = "7d" # reject versions newer than this
|
|
149
|
+
# mise_prune_on_reinit = true # remove stale tool versions on reinit
|
|
150
|
+
# -- Git --
|
|
151
|
+
# git_force_safe_directory = true # set safe.directory '*' for admin and agents
|
|
152
|
+
# -- Nerf tools --
|
|
153
|
+
# nerf_install_claude_plugin = false # install nerf plugin for this user
|
|
154
|
+
# -- Claude Code (requires claude CLI on PATH, e.g. via user_install_commands) --
|
|
155
|
+
# claude_marketplaces = ["https://github.com/WayfarerLabs/nerftools#1.2.0"]
|
|
156
|
+
# claude_plugins = ["nerftools-default@nerftools"]
|
|
157
|
+
|
|
158
|
+
# --- Agent templates ---
|
|
159
|
+
# Per-user settings for agent users. Selected at agent create time with
|
|
160
|
+
# --template. Agents default to nothing unless explicitly configured.
|
|
161
|
+
|
|
162
|
+
# [agent_templates.default]
|
|
163
|
+
# shell = "bash" # agent shell (default: bash)
|
|
164
|
+
# git_credentials = [] # git credential providers (default: none)
|
|
165
|
+
# user_install_commands = [] # per-user commands from catalog
|
|
166
|
+
# -- Dotfiles --
|
|
167
|
+
# dotfiles_source = "git::https://github.com/user/agent-dotfiles"
|
|
168
|
+
# dotfiles_destination = "~/.dotfiles"
|
|
169
|
+
# dotfiles_install_cmd = "./install.sh"
|
|
170
|
+
# -- Mise --
|
|
171
|
+
# mise_activate = true # add mise activate to agent shell profile
|
|
172
|
+
# mise_packages = ["terraform@1.14.5"] # name@version tool declarations
|
|
173
|
+
# mise_lockfile = "~/.config/agentworks/agent-mise.lock"
|
|
174
|
+
# mise_allow_unlocked = false
|
|
175
|
+
# mise_install_before = "7d"
|
|
176
|
+
# mise_prune_on_reinit = true
|
|
177
|
+
# -- Nerf tools --
|
|
178
|
+
# nerf_install_claude_plugin = false # install nerf plugin for this agent
|
|
179
|
+
# -- Claude Code (requires claude CLI on PATH, e.g. via user_install_commands) --
|
|
180
|
+
# claude_marketplaces = ["https://github.com/WayfarerLabs/nerftools#1.2.0"]
|
|
181
|
+
# claude_plugins = ["nerftools-default@nerftools"]
|
|
182
|
+
|
|
183
|
+
# [agent_templates.restricted]
|
|
184
|
+
# inherits = ["default"]
|
|
185
|
+
# user_install_commands = []
|
|
186
|
+
# mise_packages = []
|
|
187
|
+
|
|
188
|
+
# --- Workspace templates ---
|
|
189
|
+
# Selected at workspace create time with --template. The "default" template
|
|
190
|
+
# is used when --template is not specified.
|
|
191
|
+
#
|
|
192
|
+
# Repo URLs: use HTTPS format. Git credentials configured in admin.config
|
|
193
|
+
# provide authentication via personal access tokens. Public repos work
|
|
194
|
+
# without credentials.
|
|
195
|
+
|
|
196
|
+
# [workspace_templates.default]
|
|
197
|
+
# repo = "https://github.com/org/repo.git" # git repo to clone into workspace
|
|
198
|
+
# tmuxinator = true # generate tmuxinator config
|
|
199
|
+
# inherits = [] # list of parent template names
|
|
200
|
+
|
|
201
|
+
# [workspace_templates.myproject]
|
|
202
|
+
# repo = "https://github.com/org/myproject.git"
|
|
203
|
+
# tmuxinator = false
|
|
204
|
+
# inherits = ["default"]
|
|
205
|
+
|
|
206
|
+
# --- Session templates ---
|
|
207
|
+
# Define commands that sessions can run. The built-in "default" template runs a
|
|
208
|
+
# login shell. Templates support inheritance via "inherits".
|
|
209
|
+
#
|
|
210
|
+
# Template commands support {{session_name}} and {{workspace_name}} variables,
|
|
211
|
+
# using double-brace syntax consistent with nerftools manifests.
|
|
212
|
+
#
|
|
213
|
+
# Templates may define a restart_command for `session restart`. If omitted,
|
|
214
|
+
# the regular command is used. This is useful for tools like Claude Code
|
|
215
|
+
# where --resume picks up the previous conversation.
|
|
216
|
+
#
|
|
217
|
+
# env maps are merged during inheritance (child wins on key collision).
|
|
218
|
+
|
|
219
|
+
# [session_templates.default] # override the built-in default
|
|
220
|
+
# command = "claude --name {{session_name}}"
|
|
221
|
+
# restart_command = "claude --resume {{session_name}}"
|
|
222
|
+
# description = "Claude Code interactive session"
|
|
223
|
+
|
|
224
|
+
# [session_templates.claude]
|
|
225
|
+
# inherits = ["default"]
|
|
226
|
+
# command = "claude --name {{session_name}}"
|
|
227
|
+
# restart_command = "claude --resume {{session_name}}"
|
|
228
|
+
# description = "Claude Code interactive session"
|
|
229
|
+
|
|
230
|
+
# [session_templates.custom-tool]
|
|
231
|
+
# command = "my-tool --flag"
|
|
232
|
+
# description = "Custom tool"
|
|
233
|
+
# env = { MY_VAR = "value" } # environment variables to set
|
|
234
|
+
|
|
235
|
+
# --- Session configuration ---
|
|
236
|
+
# Non-template session settings.
|
|
237
|
+
|
|
238
|
+
# [session.config]
|
|
239
|
+
# history_limit = 50000 # tmux scrollback buffer lines per session
|
|
240
|
+
|
|
241
|
+
# =============================================================================
|
|
242
|
+
# Catalog extensions
|
|
243
|
+
# =============================================================================
|
|
244
|
+
#
|
|
245
|
+
# Operator-defined entries extend the built-in catalog. Entries with the same name
|
|
246
|
+
# as a built-in entry override it. Run `agentworks installer list` to see all
|
|
247
|
+
# available entries (built-in and operator-defined).
|
|
248
|
+
|
|
249
|
+
# --- Custom apt sources ---
|
|
250
|
+
# Third-party apt repositories.
|
|
251
|
+
# Use {arch} as a placeholder for the VM architecture (amd64 or arm64).
|
|
252
|
+
|
|
253
|
+
# [apt_sources.my-repo]
|
|
254
|
+
# description = "My internal apt repository"
|
|
255
|
+
# key_url = "https://apt.example.com/key.gpg"
|
|
256
|
+
# key_path = "/etc/apt/keyrings/my-repo.gpg"
|
|
257
|
+
# source = "deb [arch={arch} signed-by=/etc/apt/keyrings/my-repo.gpg] https://apt.example.com/debian bookworm main"
|
|
258
|
+
# source_file = "my-repo.list"
|
|
259
|
+
|
|
260
|
+
# --- Custom apt packages ---
|
|
261
|
+
# Named package sets with optional apt source dependencies.
|
|
262
|
+
|
|
263
|
+
# [apt_packages.my-tool]
|
|
264
|
+
# description = "My internal tool"
|
|
265
|
+
# apt_sources = ["my-repo"]
|
|
266
|
+
# apt = ["my-tool"]
|
|
267
|
+
|
|
268
|
+
# --- Custom system install commands ---
|
|
269
|
+
# Shell commands that install system-wide tooling (run during vm init/reinit).
|
|
270
|
+
# These may be re-run (e.g. during reinit) so they should be idempotent.
|
|
271
|
+
# Optional checks short-circuit installation if the tool is already present.
|
|
272
|
+
# Use only one of: test_exec, test_file, or test_dir.
|
|
273
|
+
|
|
274
|
+
# [system_install_commands.my-system-tool]
|
|
275
|
+
# description = "Custom system-wide tool"
|
|
276
|
+
# command = "curl -fsSL https://example.com/install.sh | sudo bash"
|
|
277
|
+
# test_exec = "my-system-tool" # skip if command is on PATH
|
|
278
|
+
|
|
279
|
+
# --- Custom user install commands ---
|
|
280
|
+
# Shell commands that install per-user tooling (run for admin and agents).
|
|
281
|
+
# These may be re-run so they should be idempotent.
|
|
282
|
+
# Use only one of: test_exec, test_file, or test_dir.
|
|
283
|
+
# For test_file/test_dir, ~ resolves to the target user's home.
|
|
284
|
+
|
|
285
|
+
# [user_install_commands.my-tool]
|
|
286
|
+
# description = "Custom per-user tool"
|
|
287
|
+
# command = "curl -fsSL https://example.com/install.sh | bash"
|
|
288
|
+
# path = ["~/.my-tool/bin"]
|
|
289
|
+
# test_exec = "my-tool" # skip if command is on PATH
|
|
File without changes
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""VM console management.
|
|
2
|
+
|
|
3
|
+
The console is a VM-level tmux session that provides a unified view of all
|
|
4
|
+
sessions running on the VM. It has full tmux controls (the operator can split
|
|
5
|
+
panes, create windows, rearrange layout). Each session appears as a window
|
|
6
|
+
that attaches to the session's locked-down tmux session.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shlex
|
|
12
|
+
import sys
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from agentworks import output
|
|
16
|
+
from agentworks.sessions.tmux import tmux_cmd
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from agentworks.config import Config
|
|
20
|
+
from agentworks.db import Database, SessionRow, VMRow
|
|
21
|
+
from agentworks.sessions.tmux import RunCommand
|
|
22
|
+
|
|
23
|
+
CONSOLE_SESSION_NAME = "vm-console"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def console_exists(*, run_command: RunCommand) -> bool:
|
|
27
|
+
"""Check if the console tmux session exists on the VM."""
|
|
28
|
+
result = run_command(f"tmux has-session -t {CONSOLE_SESSION_NAME} 2>/dev/null", check=False)
|
|
29
|
+
return getattr(result, "ok", False)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_console(
|
|
33
|
+
sessions: list[SessionRow],
|
|
34
|
+
*,
|
|
35
|
+
run_command: RunCommand,
|
|
36
|
+
admin_username: str,
|
|
37
|
+
recreate: bool = False,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Create the VM console session with one window per session.
|
|
40
|
+
|
|
41
|
+
When *recreate* is True, kills any existing console session first.
|
|
42
|
+
"""
|
|
43
|
+
if recreate:
|
|
44
|
+
run_command(f"tmux kill-session -t {CONSOLE_SESSION_NAME}", check=False)
|
|
45
|
+
|
|
46
|
+
# Create the session with a login shell as the initial window
|
|
47
|
+
run_command(
|
|
48
|
+
f"tmux new-session -d -s {CONSOLE_SESSION_NAME} "
|
|
49
|
+
f"-n admin-shell "
|
|
50
|
+
f"{shlex.quote('exec sudo su --login ' + shlex.quote(admin_username))}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Keep windows open when attached session command exits
|
|
54
|
+
run_command(f"tmux set -t {CONSOLE_SESSION_NAME} remain-on-exit on", check=False)
|
|
55
|
+
|
|
56
|
+
# Add a window for each session (wrapper loop handles ended sessions)
|
|
57
|
+
output.info(f"Adding {len(sessions)} session(s) to console...")
|
|
58
|
+
for session in sessions:
|
|
59
|
+
_add_session_window(
|
|
60
|
+
session.name,
|
|
61
|
+
run_command=run_command,
|
|
62
|
+
socket_path=session.socket_path,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _add_session_window(
|
|
67
|
+
session_name: str,
|
|
68
|
+
*,
|
|
69
|
+
run_command: RunCommand,
|
|
70
|
+
socket_path: str | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Add a single session window to the console."""
|
|
73
|
+
q_session = shlex.quote(session_name)
|
|
74
|
+
# Unset TMUX to allow nesting (console -> session), then loop
|
|
75
|
+
# re-attach while the session is alive.
|
|
76
|
+
has_cmd = tmux_cmd(f"has-session -t {q_session}", socket_path)
|
|
77
|
+
attach_cmd = tmux_cmd(f"attach -t {q_session}", socket_path)
|
|
78
|
+
wrapper = (
|
|
79
|
+
f"unset TMUX; "
|
|
80
|
+
f"while {has_cmd} 2>/dev/null; do "
|
|
81
|
+
f"{attach_cmd}; "
|
|
82
|
+
f"sleep 0.5; "
|
|
83
|
+
f"done; "
|
|
84
|
+
f"echo 'Session {q_session} has ended. Press enter to close.'; "
|
|
85
|
+
f"read"
|
|
86
|
+
)
|
|
87
|
+
result = run_command(
|
|
88
|
+
f"tmux new-window -t {CONSOLE_SESSION_NAME} -n {q_session} {shlex.quote(wrapper)}",
|
|
89
|
+
check=False,
|
|
90
|
+
)
|
|
91
|
+
ok = getattr(result, "ok", True)
|
|
92
|
+
stderr = getattr(result, "stderr", "")
|
|
93
|
+
if not ok:
|
|
94
|
+
output.warn(f"failed to add window for '{session_name}': {stderr}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def add_session_to_console(
|
|
98
|
+
session_name: str,
|
|
99
|
+
*,
|
|
100
|
+
run_command: RunCommand,
|
|
101
|
+
socket_path: str | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Add a session window to an existing console (best-effort)."""
|
|
104
|
+
if not console_exists(run_command=run_command):
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
_add_session_window(session_name, run_command=run_command, socket_path=socket_path)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def attach_console(
|
|
111
|
+
db: Database,
|
|
112
|
+
config: Config,
|
|
113
|
+
*,
|
|
114
|
+
vm_name: str,
|
|
115
|
+
recreate: bool = False,
|
|
116
|
+
allow_nesting: bool = False,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Attach to (or create) the VM console."""
|
|
119
|
+
import os
|
|
120
|
+
|
|
121
|
+
if os.environ.get("TMUX") and not allow_nesting:
|
|
122
|
+
raise output.SessionError(
|
|
123
|
+
"already inside a tmux session.\n"
|
|
124
|
+
"Nesting is not recommended (prefix key conflicts,\n"
|
|
125
|
+
"confusing detach behavior).\n"
|
|
126
|
+
"Pass --allow-nesting to override."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
vm = db.get_vm(vm_name)
|
|
130
|
+
if vm is None:
|
|
131
|
+
raise output.VMError(f"VM '{vm_name}' not found")
|
|
132
|
+
|
|
133
|
+
from agentworks.workspaces.manager import _ensure_vm_running
|
|
134
|
+
|
|
135
|
+
_ensure_vm_running(db, config, vm)
|
|
136
|
+
|
|
137
|
+
if vm.tailscale_host is None:
|
|
138
|
+
raise output.VMError(f"VM '{vm_name}' has no Tailscale address")
|
|
139
|
+
|
|
140
|
+
from agentworks.ssh import admin_exec_target, interactive
|
|
141
|
+
|
|
142
|
+
target = admin_exec_target(vm, config)
|
|
143
|
+
|
|
144
|
+
# Get sessions for this VM (console wrapper handles dead sessions)
|
|
145
|
+
vm_sessions = _get_sessions_for_vm(db, vm)
|
|
146
|
+
|
|
147
|
+
if recreate or not console_exists(run_command=target.run):
|
|
148
|
+
create_console(
|
|
149
|
+
vm_sessions,
|
|
150
|
+
run_command=target.run,
|
|
151
|
+
admin_username=vm.admin_username,
|
|
152
|
+
recreate=recreate,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
sys.exit(interactive(target, f"tmux attach -t {CONSOLE_SESSION_NAME}"))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_sessions_for_vm(db: Database, vm: VMRow) -> list[SessionRow]:
|
|
159
|
+
"""Get all sessions across all workspaces on a VM."""
|
|
160
|
+
workspaces = db.list_workspaces(vm_name=vm.name)
|
|
161
|
+
sessions: list[SessionRow] = []
|
|
162
|
+
for ws in workspaces:
|
|
163
|
+
sessions.extend(db.list_sessions(workspace_name=ws.name))
|
|
164
|
+
return sessions
|