machineconfig 5.30__py3-none-any.whl → 5.31__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.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/scripts/python/croshell_helpers/__init__.py +0 -0
- machineconfig/scripts/python/devops.py +13 -122
- machineconfig/scripts/python/devops_helpers/cli_config.py +43 -0
- machineconfig/scripts/python/devops_helpers/cli_data.py +18 -0
- machineconfig/scripts/python/devops_helpers/cli_nw.py +39 -0
- machineconfig/scripts/python/{repos.py → devops_helpers/cli_repos.py} +43 -7
- machineconfig/scripts/python/devops_helpers/cli_self.py +41 -0
- machineconfig/scripts/python/devops_navigator.py +806 -0
- machineconfig/scripts/python/helpers/repo_sync_helpers.py +1 -1
- machineconfig/scripts/python/helpers_repos/__init__.py +0 -0
- machineconfig/scripts/python/{secure_repo.py → helpers_repos/secure_repo.py} +1 -1
- machineconfig/scripts/python/interactive.py +2 -2
- machineconfig/scripts/python/nw/__init__.py +0 -0
- machineconfig/scripts/python/sessions.py +1 -1
- machineconfig/scripts/python/sessions_helpers/__init__.py +0 -0
- machineconfig/utils/code.py +13 -3
- {machineconfig-5.30.dist-info → machineconfig-5.31.dist-info}/METADATA +2 -1
- {machineconfig-5.30.dist-info → machineconfig-5.31.dist-info}/RECORD +38 -34
- {machineconfig-5.30.dist-info → machineconfig-5.31.dist-info}/entry_points.txt +1 -0
- machineconfig/scripts/python/gh_models.py +0 -104
- machineconfig/scripts/python/snapshot.py +0 -25
- machineconfig/scripts/python/start_terminals.py +0 -121
- machineconfig/scripts/windows/choose_wezterm_theme.ps1 +0 -1
- machineconfig/scripts/windows/wifi_conn.ps1 +0 -2
- /machineconfig/scripts/python/{pomodoro.py → croshell_helpers/pomodoro.py} +0 -0
- /machineconfig/scripts/python/{scheduler.py → croshell_helpers/scheduler.py} +0 -0
- /machineconfig/scripts/python/{start_slidev.py → croshell_helpers/start_slidev.py} +0 -0
- /machineconfig/scripts/python/{viewer.py → croshell_helpers/viewer.py} +0 -0
- /machineconfig/scripts/python/{viewer_template.py → croshell_helpers/viewer_template.py} +0 -0
- /machineconfig/scripts/{windows/select_pwsh_theme.ps1 → python/devops_helpers/choose_pwsh_theme.ps1} +0 -0
- /machineconfig/scripts/python/{choose_wezterm_theme.py → devops_helpers/choose_wezterm_theme.py} +0 -0
- /machineconfig/scripts/python/{dotfile.py → devops_helpers/cli_config_dotfile.py} +0 -0
- /machineconfig/scripts/python/{share_terminal.py → devops_helpers/cli_terminal.py} +0 -0
- /machineconfig/scripts/python/{cloud_repo_sync.py → helpers_repos/cloud_repo_sync.py} +0 -0
- /machineconfig/scripts/python/{mount_nfs.py → nw/mount_nfs.py} +0 -0
- /machineconfig/scripts/python/{mount_nw_drive.py → nw/mount_nw_drive.py} +0 -0
- /machineconfig/scripts/python/{mount_ssh.py → nw/mount_ssh.py} +0 -0
- /machineconfig/scripts/python/{onetimeshare.py → nw/onetimeshare.py} +0 -0
- /machineconfig/scripts/python/{wifi_conn.py → nw/wifi_conn.py} +0 -0
- /machineconfig/scripts/python/{wsl_windows_transfer.py → nw/wsl_windows_transfer.py} +0 -0
- /machineconfig/scripts/python/{sessions_multiprocess.py → sessions_helpers/sessions_multiprocess.py} +0 -0
- {machineconfig-5.30.dist-info → machineconfig-5.31.dist-info}/WHEEL +0 -0
- {machineconfig-5.30.dist-info → machineconfig-5.31.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TUI for navigating through machineconfig command structure using Textual.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from textual.app import App, ComposeResult
|
|
10
|
+
from textual.containers import Horizontal, VerticalScroll
|
|
11
|
+
from textual.widgets import Header, Footer, Tree, Static, Input, Label, Button
|
|
12
|
+
from textual.binding import Binding
|
|
13
|
+
from textual.screen import ModalScreen
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CommandInfo:
|
|
20
|
+
"""Information about a CLI command."""
|
|
21
|
+
name: str
|
|
22
|
+
description: str
|
|
23
|
+
command: str
|
|
24
|
+
parent: Optional[str] = None
|
|
25
|
+
is_group: bool = False
|
|
26
|
+
help_text: str = ""
|
|
27
|
+
module_path: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ArgumentInfo:
|
|
32
|
+
"""Information about a command argument."""
|
|
33
|
+
name: str
|
|
34
|
+
is_required: bool
|
|
35
|
+
is_flag: bool
|
|
36
|
+
placeholder: str = ""
|
|
37
|
+
description: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CommandBuilderScreen(ModalScreen[str]):
|
|
41
|
+
"""Modal screen for building command with arguments."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, command_info: CommandInfo) -> None:
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.command_info = command_info
|
|
46
|
+
self.arguments = self._parse_arguments()
|
|
47
|
+
self.input_widgets: dict[str, Input] = {}
|
|
48
|
+
|
|
49
|
+
def _parse_arguments(self) -> list[ArgumentInfo]:
|
|
50
|
+
"""Parse arguments from help_text."""
|
|
51
|
+
args: list[ArgumentInfo] = []
|
|
52
|
+
seen_names: set[str] = set()
|
|
53
|
+
|
|
54
|
+
if not self.command_info.help_text:
|
|
55
|
+
return args
|
|
56
|
+
|
|
57
|
+
help_text = self.command_info.help_text
|
|
58
|
+
|
|
59
|
+
optional_pattern = re.compile(r'--(\w+(?:-\w+)*)\s+<([^>]+)>')
|
|
60
|
+
for match in optional_pattern.finditer(help_text):
|
|
61
|
+
arg_name = match.group(1)
|
|
62
|
+
placeholder = match.group(2)
|
|
63
|
+
if arg_name not in seen_names:
|
|
64
|
+
args.append(ArgumentInfo(name=arg_name, is_required=False, is_flag=False, placeholder=placeholder))
|
|
65
|
+
seen_names.add(arg_name)
|
|
66
|
+
|
|
67
|
+
flag_pattern = re.compile(r'--(\w+(?:-\w+)*)(?:\s|$)')
|
|
68
|
+
for match in flag_pattern.finditer(help_text):
|
|
69
|
+
arg_name = match.group(1)
|
|
70
|
+
if arg_name not in seen_names:
|
|
71
|
+
args.append(ArgumentInfo(name=arg_name, is_required=False, is_flag=True))
|
|
72
|
+
seen_names.add(arg_name)
|
|
73
|
+
|
|
74
|
+
positional_pattern = re.compile(r'<(\w+)>(?!\s*>)')
|
|
75
|
+
for match in positional_pattern.finditer(help_text):
|
|
76
|
+
arg_name = match.group(1)
|
|
77
|
+
if arg_name not in seen_names and not re.search(rf'--\w+\s+<{arg_name}>', help_text):
|
|
78
|
+
args.append(ArgumentInfo(name=arg_name, is_required=True, is_flag=False, placeholder=arg_name))
|
|
79
|
+
seen_names.add(arg_name)
|
|
80
|
+
|
|
81
|
+
return args
|
|
82
|
+
|
|
83
|
+
def compose(self) -> ComposeResult:
|
|
84
|
+
"""Compose the modal screen."""
|
|
85
|
+
with VerticalScroll():
|
|
86
|
+
yield Static(f"[bold cyan]Build Command: {self.command_info.command}[/bold cyan]\n", classes="title")
|
|
87
|
+
|
|
88
|
+
if not self.arguments:
|
|
89
|
+
yield Static("[yellow]No arguments needed for this command[/yellow]\n")
|
|
90
|
+
else:
|
|
91
|
+
for arg in self.arguments:
|
|
92
|
+
if arg.is_flag:
|
|
93
|
+
label_text = f"--{arg.name} (flag, leave empty to skip)"
|
|
94
|
+
yield Label(label_text)
|
|
95
|
+
input_widget = Input(placeholder="yes/no or leave empty", id=f"arg_{arg.name}")
|
|
96
|
+
else:
|
|
97
|
+
required_marker = "[red]*[/red]" if arg.is_required else "[dim](optional)[/dim]"
|
|
98
|
+
label_text = f"--{arg.name} {required_marker}"
|
|
99
|
+
yield Label(label_text)
|
|
100
|
+
input_widget = Input(placeholder=arg.placeholder or arg.name, id=f"arg_{arg.name}")
|
|
101
|
+
|
|
102
|
+
self.input_widgets[arg.name] = input_widget
|
|
103
|
+
yield input_widget
|
|
104
|
+
|
|
105
|
+
with Horizontal(classes="buttons"):
|
|
106
|
+
yield Button("Execute", variant="primary", id="execute")
|
|
107
|
+
yield Button("Copy", variant="success", id="copy")
|
|
108
|
+
yield Button("Cancel", variant="error", id="cancel")
|
|
109
|
+
|
|
110
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
111
|
+
"""Handle button presses."""
|
|
112
|
+
if event.button.id == "cancel":
|
|
113
|
+
self.dismiss("")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
built_command = self._build_command()
|
|
117
|
+
|
|
118
|
+
if event.button.id == "execute":
|
|
119
|
+
self.dismiss(f"EXECUTE:{built_command}")
|
|
120
|
+
elif event.button.id == "copy":
|
|
121
|
+
self.dismiss(f"COPY:{built_command}")
|
|
122
|
+
|
|
123
|
+
def _build_command(self) -> str:
|
|
124
|
+
"""Build the complete command with arguments."""
|
|
125
|
+
parts = [self.command_info.command]
|
|
126
|
+
|
|
127
|
+
for arg in self.arguments:
|
|
128
|
+
input_widget = self.input_widgets.get(arg.name)
|
|
129
|
+
if input_widget:
|
|
130
|
+
value = input_widget.value.strip()
|
|
131
|
+
if value:
|
|
132
|
+
if arg.is_flag:
|
|
133
|
+
if value.lower() in ('yes', 'y', 'true', '1'):
|
|
134
|
+
parts.append(f"--{arg.name}")
|
|
135
|
+
else:
|
|
136
|
+
parts.append(f"--{arg.name} {value}")
|
|
137
|
+
|
|
138
|
+
return " ".join(parts)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CommandTree(Tree[CommandInfo]):
|
|
142
|
+
"""Tree widget for displaying command hierarchy."""
|
|
143
|
+
|
|
144
|
+
def on_mount(self) -> None:
|
|
145
|
+
"""Build the command tree when mounted."""
|
|
146
|
+
self.show_root = False
|
|
147
|
+
self.guide_depth = 2
|
|
148
|
+
self._build_command_tree()
|
|
149
|
+
|
|
150
|
+
def _build_command_tree(self) -> None:
|
|
151
|
+
"""Build the hierarchical command structure."""
|
|
152
|
+
|
|
153
|
+
# Main entry points
|
|
154
|
+
devops_node = self.root.add("🛠️ devops - DevOps operations", data=CommandInfo(
|
|
155
|
+
name="devops",
|
|
156
|
+
description="DevOps operations",
|
|
157
|
+
command="devops",
|
|
158
|
+
is_group=True,
|
|
159
|
+
module_path="machineconfig.scripts.python.devops"
|
|
160
|
+
))
|
|
161
|
+
|
|
162
|
+
# devops subcommands
|
|
163
|
+
devops_node.add("📦 install - Install essential packages", data=CommandInfo(
|
|
164
|
+
name="install",
|
|
165
|
+
description="Install essential packages",
|
|
166
|
+
command="devops install",
|
|
167
|
+
parent="devops",
|
|
168
|
+
help_text="devops install --which <packages> --group <group> --interactive"
|
|
169
|
+
))
|
|
170
|
+
|
|
171
|
+
repos_node = devops_node.add("📁 repos - Manage git repositories", data=CommandInfo(
|
|
172
|
+
name="repos",
|
|
173
|
+
description="Manage git repositories",
|
|
174
|
+
command="devops repos",
|
|
175
|
+
parent="devops",
|
|
176
|
+
is_group=True,
|
|
177
|
+
module_path="machineconfig.scripts.python.repos"
|
|
178
|
+
))
|
|
179
|
+
|
|
180
|
+
# repos subcommands
|
|
181
|
+
sync_node = repos_node.add("🔄 sync - Synchronize repositories", data=CommandInfo(
|
|
182
|
+
name="sync",
|
|
183
|
+
description="Synchronize repositories",
|
|
184
|
+
command="devops repos sync",
|
|
185
|
+
parent="repos",
|
|
186
|
+
is_group=True
|
|
187
|
+
))
|
|
188
|
+
|
|
189
|
+
sync_node.add("📝 capture - Record repositories into repos.json", data=CommandInfo(
|
|
190
|
+
name="capture",
|
|
191
|
+
description="Record repositories into a repos.json specification",
|
|
192
|
+
command="devops repos sync capture",
|
|
193
|
+
parent="sync",
|
|
194
|
+
help_text="devops repos sync capture --directory <dir> --cloud <cloud>"
|
|
195
|
+
))
|
|
196
|
+
|
|
197
|
+
sync_node.add("📥 clone - Clone repositories from repos.json", data=CommandInfo(
|
|
198
|
+
name="clone",
|
|
199
|
+
description="Clone repositories described by repos.json",
|
|
200
|
+
command="devops repos sync clone",
|
|
201
|
+
parent="sync",
|
|
202
|
+
help_text="devops repos sync clone --directory <dir> --cloud <cloud>"
|
|
203
|
+
))
|
|
204
|
+
|
|
205
|
+
sync_node.add("🔀 checkout-to-commit - Check out specific commits", data=CommandInfo(
|
|
206
|
+
name="checkout-to-commit",
|
|
207
|
+
description="Check out specific commits listed in specification",
|
|
208
|
+
command="devops repos sync checkout-to-commit",
|
|
209
|
+
parent="sync",
|
|
210
|
+
help_text="devops repos sync checkout-to-commit --directory <dir> --cloud <cloud>"
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
sync_node.add("🔀 checkout-to-branch - Check out to main branch", data=CommandInfo(
|
|
214
|
+
name="checkout-to-branch",
|
|
215
|
+
description="Check out to the main branch defined in specification",
|
|
216
|
+
command="devops repos sync checkout-to-branch",
|
|
217
|
+
parent="sync",
|
|
218
|
+
help_text="devops repos sync checkout-to-branch --directory <dir> --cloud <cloud>"
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
repos_node.add("🔍 analyze - Analyze repositories", data=CommandInfo(
|
|
222
|
+
name="analyze",
|
|
223
|
+
description="Analyze repositories in directory",
|
|
224
|
+
command="devops repos analyze",
|
|
225
|
+
parent="repos",
|
|
226
|
+
help_text="devops repos analyze --directory <dir>"
|
|
227
|
+
))
|
|
228
|
+
|
|
229
|
+
# config subcommands
|
|
230
|
+
config_node = devops_node.add("⚙️ config - Configuration management", data=CommandInfo(
|
|
231
|
+
name="config",
|
|
232
|
+
description="Configuration subcommands",
|
|
233
|
+
command="devops config",
|
|
234
|
+
parent="devops",
|
|
235
|
+
is_group=True
|
|
236
|
+
))
|
|
237
|
+
|
|
238
|
+
config_node.add("🔗 private - Manage private configuration files", data=CommandInfo(
|
|
239
|
+
name="private",
|
|
240
|
+
description="Manage private configuration files",
|
|
241
|
+
command="devops config private",
|
|
242
|
+
parent="config",
|
|
243
|
+
help_text="devops config private --method <symlink|copy> --on-conflict <action> --which <items> --interactive"
|
|
244
|
+
))
|
|
245
|
+
|
|
246
|
+
config_node.add("🔗 public - Manage public configuration files", data=CommandInfo(
|
|
247
|
+
name="public",
|
|
248
|
+
description="Manage public configuration files",
|
|
249
|
+
command="devops config public",
|
|
250
|
+
parent="config",
|
|
251
|
+
help_text="devops config public --method <symlink|copy> --on-conflict <action> --which <items> --interactive"
|
|
252
|
+
))
|
|
253
|
+
|
|
254
|
+
config_node.add("🔗 dotfile - Manage dotfiles", data=CommandInfo(
|
|
255
|
+
name="dotfile",
|
|
256
|
+
description="Manage dotfiles",
|
|
257
|
+
command="devops config dotfile",
|
|
258
|
+
parent="config",
|
|
259
|
+
help_text="devops config dotfile <file> --overwrite --dest <destination>"
|
|
260
|
+
))
|
|
261
|
+
|
|
262
|
+
config_node.add("🔗 shell - Configure shell profile", data=CommandInfo(
|
|
263
|
+
name="shell",
|
|
264
|
+
description="Configure your shell profile",
|
|
265
|
+
command="devops config shell",
|
|
266
|
+
parent="config",
|
|
267
|
+
help_text="devops config shell <copy|reference>"
|
|
268
|
+
))
|
|
269
|
+
|
|
270
|
+
# data subcommands
|
|
271
|
+
data_node = devops_node.add("💾 data - Data operations", data=CommandInfo(
|
|
272
|
+
name="data",
|
|
273
|
+
description="Data subcommands",
|
|
274
|
+
command="devops data",
|
|
275
|
+
parent="devops",
|
|
276
|
+
is_group=True
|
|
277
|
+
))
|
|
278
|
+
|
|
279
|
+
data_node.add("💾 backup - Backup data", data=CommandInfo(
|
|
280
|
+
name="backup",
|
|
281
|
+
description="Backup data",
|
|
282
|
+
command="devops data backup",
|
|
283
|
+
parent="data",
|
|
284
|
+
help_text="devops data backup"
|
|
285
|
+
))
|
|
286
|
+
|
|
287
|
+
data_node.add("📥 retrieve - Retrieve data", data=CommandInfo(
|
|
288
|
+
name="retrieve",
|
|
289
|
+
description="Retrieve data from backup",
|
|
290
|
+
command="devops data retrieve",
|
|
291
|
+
parent="data",
|
|
292
|
+
help_text="devops data retrieve"
|
|
293
|
+
))
|
|
294
|
+
|
|
295
|
+
# network subcommands
|
|
296
|
+
network_node = devops_node.add("🔐 network - Network operations", data=CommandInfo(
|
|
297
|
+
name="network",
|
|
298
|
+
description="Network subcommands",
|
|
299
|
+
command="devops network",
|
|
300
|
+
parent="devops",
|
|
301
|
+
is_group=True
|
|
302
|
+
))
|
|
303
|
+
|
|
304
|
+
network_node.add("📡 share-terminal - Share terminal via web", data=CommandInfo(
|
|
305
|
+
name="share-terminal",
|
|
306
|
+
description="Share terminal via web browser",
|
|
307
|
+
command="devops network share-terminal",
|
|
308
|
+
parent="network",
|
|
309
|
+
help_text="devops network share-terminal"
|
|
310
|
+
))
|
|
311
|
+
|
|
312
|
+
network_node.add("🔑 add-key - Add SSH public key", data=CommandInfo(
|
|
313
|
+
name="add-key",
|
|
314
|
+
description="SSH add pub key to this machine",
|
|
315
|
+
command="devops network add-key",
|
|
316
|
+
parent="network",
|
|
317
|
+
help_text="devops network add-key"
|
|
318
|
+
))
|
|
319
|
+
|
|
320
|
+
network_node.add("🗝️ add-identity - Add SSH identity", data=CommandInfo(
|
|
321
|
+
name="add-identity",
|
|
322
|
+
description="SSH add identity (private key) to this machine",
|
|
323
|
+
command="devops network add-identity",
|
|
324
|
+
parent="network",
|
|
325
|
+
help_text="devops network add-identity"
|
|
326
|
+
))
|
|
327
|
+
|
|
328
|
+
network_node.add("📡 setup - SSH setup", data=CommandInfo(
|
|
329
|
+
name="setup",
|
|
330
|
+
description="SSH setup",
|
|
331
|
+
command="devops network setup",
|
|
332
|
+
parent="network",
|
|
333
|
+
help_text="devops network setup"
|
|
334
|
+
))
|
|
335
|
+
|
|
336
|
+
# self subcommands
|
|
337
|
+
self_node = devops_node.add("🔄 self - SELF operations", data=CommandInfo(
|
|
338
|
+
name="self",
|
|
339
|
+
description="SELF operations subcommands",
|
|
340
|
+
command="devops self",
|
|
341
|
+
parent="devops",
|
|
342
|
+
is_group=True
|
|
343
|
+
))
|
|
344
|
+
|
|
345
|
+
self_node.add("🔄 update - Update essential repos", data=CommandInfo(
|
|
346
|
+
name="update",
|
|
347
|
+
description="UPDATE essential repos",
|
|
348
|
+
command="devops self update",
|
|
349
|
+
parent="self",
|
|
350
|
+
help_text="devops self update"
|
|
351
|
+
))
|
|
352
|
+
|
|
353
|
+
self_node.add("🤖 interactive - Interactive configuration", data=CommandInfo(
|
|
354
|
+
name="interactive",
|
|
355
|
+
description="INTERACTIVE configuration of machine",
|
|
356
|
+
command="devops self interactive",
|
|
357
|
+
parent="self",
|
|
358
|
+
help_text="devops self interactive"
|
|
359
|
+
))
|
|
360
|
+
|
|
361
|
+
self_node.add("📊 status - Machine status", data=CommandInfo(
|
|
362
|
+
name="status",
|
|
363
|
+
description="STATUS of machine, shell profile, apps, symlinks, dotfiles, etc.",
|
|
364
|
+
command="devops self status",
|
|
365
|
+
parent="self",
|
|
366
|
+
help_text="devops self status"
|
|
367
|
+
))
|
|
368
|
+
|
|
369
|
+
self_node.add("📋 clone - Clone machineconfig", data=CommandInfo(
|
|
370
|
+
name="clone",
|
|
371
|
+
description="CLONE machineconfig locally and incorporate to shell profile",
|
|
372
|
+
command="devops self clone",
|
|
373
|
+
parent="self",
|
|
374
|
+
help_text="devops self clone"
|
|
375
|
+
))
|
|
376
|
+
|
|
377
|
+
# fire command
|
|
378
|
+
self.root.add("🔥 fire - Fire jobs execution", data=CommandInfo(
|
|
379
|
+
name="fire",
|
|
380
|
+
description="Execute Python scripts with Fire",
|
|
381
|
+
command="fire",
|
|
382
|
+
is_group=False,
|
|
383
|
+
module_path="machineconfig.scripts.python.fire_jobs",
|
|
384
|
+
help_text="fire <path> [function] --ve <env> --interactive --jupyter --streamlit --debug --loop --remote --zellij_tab <name>"
|
|
385
|
+
))
|
|
386
|
+
|
|
387
|
+
# agents command
|
|
388
|
+
agents_node = self.root.add("🤖 agents - AI Agents management", data=CommandInfo(
|
|
389
|
+
name="agents",
|
|
390
|
+
description="AI Agents management subcommands",
|
|
391
|
+
command="agents",
|
|
392
|
+
is_group=True,
|
|
393
|
+
module_path="machineconfig.scripts.python.agents"
|
|
394
|
+
))
|
|
395
|
+
|
|
396
|
+
agents_node.add("✨ create - Create AI agent", data=CommandInfo(
|
|
397
|
+
name="create",
|
|
398
|
+
description="Create a new AI agent",
|
|
399
|
+
command="agents create",
|
|
400
|
+
parent="agents",
|
|
401
|
+
help_text="agents create"
|
|
402
|
+
))
|
|
403
|
+
|
|
404
|
+
agents_node.add("📦 collect - Collect agent data", data=CommandInfo(
|
|
405
|
+
name="collect",
|
|
406
|
+
description="Collect agent data",
|
|
407
|
+
command="agents collect",
|
|
408
|
+
parent="agents",
|
|
409
|
+
help_text="agents collect"
|
|
410
|
+
))
|
|
411
|
+
|
|
412
|
+
agents_node.add("📝 create-template - Create agent template", data=CommandInfo(
|
|
413
|
+
name="create-template",
|
|
414
|
+
description="Create a template for fire agents",
|
|
415
|
+
command="agents create-template",
|
|
416
|
+
parent="agents",
|
|
417
|
+
help_text="agents create-template"
|
|
418
|
+
))
|
|
419
|
+
|
|
420
|
+
# sessions command
|
|
421
|
+
sessions_node = self.root.add("🖥️ sessions - Session layouts management", data=CommandInfo(
|
|
422
|
+
name="sessions",
|
|
423
|
+
description="Layouts management subcommands",
|
|
424
|
+
command="sessions",
|
|
425
|
+
is_group=True,
|
|
426
|
+
module_path="machineconfig.scripts.python.sessions"
|
|
427
|
+
))
|
|
428
|
+
|
|
429
|
+
sessions_node.add("✨ create-from-function - Create layout from function", data=CommandInfo(
|
|
430
|
+
name="create-from-function",
|
|
431
|
+
description="Create layout from function",
|
|
432
|
+
command="sessions create-from-function",
|
|
433
|
+
parent="sessions",
|
|
434
|
+
help_text="sessions create-from-function"
|
|
435
|
+
))
|
|
436
|
+
|
|
437
|
+
sessions_node.add("▶️ run - Run session layout", data=CommandInfo(
|
|
438
|
+
name="run",
|
|
439
|
+
description="Run session layout",
|
|
440
|
+
command="sessions run",
|
|
441
|
+
parent="sessions",
|
|
442
|
+
help_text="sessions run"
|
|
443
|
+
))
|
|
444
|
+
|
|
445
|
+
sessions_node.add("⚖️ balance-load - Balance load", data=CommandInfo(
|
|
446
|
+
name="balance-load",
|
|
447
|
+
description="Balance load across sessions",
|
|
448
|
+
command="sessions balance-load",
|
|
449
|
+
parent="sessions",
|
|
450
|
+
help_text="sessions balance-load"
|
|
451
|
+
))
|
|
452
|
+
|
|
453
|
+
# Other utility commands
|
|
454
|
+
utils_node = self.root.add("🔧 Utilities", data=CommandInfo(
|
|
455
|
+
name="utilities",
|
|
456
|
+
description="Utility commands",
|
|
457
|
+
command="",
|
|
458
|
+
is_group=True
|
|
459
|
+
))
|
|
460
|
+
|
|
461
|
+
utils_node.add("☁️ cloud_mount - Mount cloud storage", data=CommandInfo(
|
|
462
|
+
name="cloud_mount",
|
|
463
|
+
description="Mount cloud storage using rclone",
|
|
464
|
+
command="cloud_mount",
|
|
465
|
+
parent="utilities",
|
|
466
|
+
help_text="cloud_mount --cloud <cloud> --destination <path> --network <network>"
|
|
467
|
+
))
|
|
468
|
+
|
|
469
|
+
utils_node.add("☁️ cloud_copy - Copy to/from cloud", data=CommandInfo(
|
|
470
|
+
name="cloud_copy",
|
|
471
|
+
description="Copy files to/from cloud storage",
|
|
472
|
+
command="cloud_copy",
|
|
473
|
+
parent="utilities",
|
|
474
|
+
help_text="cloud_copy"
|
|
475
|
+
))
|
|
476
|
+
|
|
477
|
+
utils_node.add("☁️ cloud_sync - Sync with cloud", data=CommandInfo(
|
|
478
|
+
name="cloud_sync",
|
|
479
|
+
description="Sync files with cloud storage",
|
|
480
|
+
command="cloud_sync",
|
|
481
|
+
parent="utilities",
|
|
482
|
+
help_text="cloud_sync"
|
|
483
|
+
))
|
|
484
|
+
|
|
485
|
+
utils_node.add("🔧 kill_process - Kill processes", data=CommandInfo(
|
|
486
|
+
name="kill_process",
|
|
487
|
+
description="Kill running processes",
|
|
488
|
+
command="kill_process",
|
|
489
|
+
parent="utilities",
|
|
490
|
+
help_text="kill_process"
|
|
491
|
+
))
|
|
492
|
+
|
|
493
|
+
utils_node.add("🎨 choose_wezterm_theme - Choose terminal theme", data=CommandInfo(
|
|
494
|
+
name="choose_wezterm_theme",
|
|
495
|
+
description="Choose WezTerm theme interactively",
|
|
496
|
+
command="choose_wezterm_theme",
|
|
497
|
+
parent="utilities",
|
|
498
|
+
help_text="choose_wezterm_theme"
|
|
499
|
+
))
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class CommandDetail(Static):
|
|
503
|
+
"""Widget for displaying command details."""
|
|
504
|
+
|
|
505
|
+
def __init__(self, *, id: str) -> None: # type: ignore
|
|
506
|
+
super().__init__(id=id)
|
|
507
|
+
self.command_info: Optional[CommandInfo] = None
|
|
508
|
+
|
|
509
|
+
def update_command(self, command_info: Optional[CommandInfo]) -> None:
|
|
510
|
+
"""Update displayed command information."""
|
|
511
|
+
self.command_info = command_info
|
|
512
|
+
if command_info is None:
|
|
513
|
+
self.update("Select a command to view details")
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
content = Text()
|
|
517
|
+
content.append(f"{'🗂️ Group' if command_info.is_group else '⚡ Command'}: ", style="bold cyan")
|
|
518
|
+
content.append(f"{command_info.name}\n\n", style="bold yellow")
|
|
519
|
+
|
|
520
|
+
content.append("Description: ", style="bold green")
|
|
521
|
+
content.append(f"{command_info.description}\n\n", style="white")
|
|
522
|
+
|
|
523
|
+
content.append("Command: ", style="bold blue")
|
|
524
|
+
content.append(f"{command_info.command}\n\n", style="bold white")
|
|
525
|
+
|
|
526
|
+
if command_info.help_text:
|
|
527
|
+
content.append("Usage: ", style="bold magenta")
|
|
528
|
+
content.append(f"{command_info.help_text}\n\n", style="white")
|
|
529
|
+
|
|
530
|
+
if command_info.module_path:
|
|
531
|
+
content.append("Module: ", style="bold red")
|
|
532
|
+
content.append(f"{command_info.module_path}\n", style="white")
|
|
533
|
+
|
|
534
|
+
self.update(Panel(content, title=f"[bold]{command_info.name}[/bold]", border_style="blue"))
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class SearchBar(Horizontal):
|
|
538
|
+
"""Search bar widget."""
|
|
539
|
+
|
|
540
|
+
def compose(self) -> ComposeResult:
|
|
541
|
+
yield Label("🔍 Search: ", classes="search-label")
|
|
542
|
+
yield Input(placeholder="Type to search commands...", id="search-input")
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class CommandNavigatorApp(App[None]):
|
|
546
|
+
"""TUI application for navigating machineconfig commands."""
|
|
547
|
+
|
|
548
|
+
CSS = """
|
|
549
|
+
Screen {
|
|
550
|
+
layout: grid;
|
|
551
|
+
grid-size: 2 3;
|
|
552
|
+
grid-rows: auto 1fr auto;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
Header {
|
|
556
|
+
column-span: 2;
|
|
557
|
+
background: $boost;
|
|
558
|
+
color: $text;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#search-bar {
|
|
562
|
+
column-span: 2;
|
|
563
|
+
padding: 1;
|
|
564
|
+
background: $surface;
|
|
565
|
+
height: auto;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.search-label {
|
|
569
|
+
width: auto;
|
|
570
|
+
padding-right: 1;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#search-input {
|
|
574
|
+
width: 1fr;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#command-tree {
|
|
578
|
+
row-span: 1;
|
|
579
|
+
border: solid $primary;
|
|
580
|
+
padding: 1;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
#command-detail {
|
|
584
|
+
row-span: 1;
|
|
585
|
+
border: solid $primary;
|
|
586
|
+
padding: 1;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
Footer {
|
|
590
|
+
column-span: 2;
|
|
591
|
+
background: $boost;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
Button {
|
|
595
|
+
margin: 1;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
CommandBuilderScreen {
|
|
599
|
+
align: center middle;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
CommandBuilderScreen > VerticalScroll {
|
|
603
|
+
width: 80;
|
|
604
|
+
height: auto;
|
|
605
|
+
max-height: 90%;
|
|
606
|
+
background: $surface;
|
|
607
|
+
border: thick $primary;
|
|
608
|
+
padding: 2;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
CommandBuilderScreen .title {
|
|
612
|
+
margin-bottom: 1;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
CommandBuilderScreen Label {
|
|
616
|
+
margin-top: 1;
|
|
617
|
+
margin-bottom: 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
CommandBuilderScreen Input {
|
|
621
|
+
margin-bottom: 1;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
CommandBuilderScreen .buttons {
|
|
625
|
+
margin-top: 2;
|
|
626
|
+
height: auto;
|
|
627
|
+
align: center middle;
|
|
628
|
+
}
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
BINDINGS = [
|
|
632
|
+
Binding("q", "quit", "Quit", priority=True),
|
|
633
|
+
Binding("c", "copy_command", "Copy Command"),
|
|
634
|
+
Binding("/", "focus_search", "Search"),
|
|
635
|
+
Binding("?", "help", "Help"),
|
|
636
|
+
Binding("r", "run_command", "Run Command"),
|
|
637
|
+
Binding("b", "build_command", "Build Command"),
|
|
638
|
+
]
|
|
639
|
+
|
|
640
|
+
def compose(self) -> ComposeResult:
|
|
641
|
+
"""Create child widgets for the app."""
|
|
642
|
+
yield Header(show_clock=True)
|
|
643
|
+
yield SearchBar(id="search-bar")
|
|
644
|
+
yield CommandTree("📚 machineconfig Commands", id="command-tree")
|
|
645
|
+
yield CommandDetail(id="command-detail")
|
|
646
|
+
yield Footer()
|
|
647
|
+
|
|
648
|
+
def on_mount(self) -> None:
|
|
649
|
+
"""Actions when app is mounted."""
|
|
650
|
+
self.title = "machineconfig Command Navigator"
|
|
651
|
+
self.sub_title = "Navigate and explore all available commands"
|
|
652
|
+
tree = self.query_one(CommandTree)
|
|
653
|
+
tree.focus()
|
|
654
|
+
|
|
655
|
+
def on_tree_node_selected(self, event: Tree.NodeSelected[CommandInfo]) -> None:
|
|
656
|
+
"""Handle tree node selection."""
|
|
657
|
+
command_info = event.node.data
|
|
658
|
+
detail_widget = self.query_one("#command-detail", CommandDetail)
|
|
659
|
+
detail_widget.update_command(command_info)
|
|
660
|
+
|
|
661
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
662
|
+
"""Handle search input changes."""
|
|
663
|
+
if event.input.id != "search-input":
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
search_term = event.value.lower()
|
|
667
|
+
tree = self.query_one(CommandTree)
|
|
668
|
+
|
|
669
|
+
if not search_term:
|
|
670
|
+
# Show all nodes - expand all root children
|
|
671
|
+
for node in tree.root.children:
|
|
672
|
+
node.expand()
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
# Filter nodes based on search term
|
|
676
|
+
def filter_tree(node): # type: ignore
|
|
677
|
+
if node.data and not node.data.is_group:
|
|
678
|
+
match = (search_term in node.data.name.lower() or
|
|
679
|
+
search_term in node.data.description.lower() or
|
|
680
|
+
search_term in node.data.command.lower())
|
|
681
|
+
return match
|
|
682
|
+
return False
|
|
683
|
+
|
|
684
|
+
# Expand parents of matching nodes by walking through all nodes
|
|
685
|
+
def walk_nodes(node): # type: ignore
|
|
686
|
+
"""Recursively walk through tree nodes."""
|
|
687
|
+
yield node
|
|
688
|
+
for child in node.children:
|
|
689
|
+
yield from walk_nodes(child)
|
|
690
|
+
|
|
691
|
+
for node in walk_nodes(tree.root):
|
|
692
|
+
if filter_tree(node):
|
|
693
|
+
parent = node.parent
|
|
694
|
+
while parent and parent != tree.root:
|
|
695
|
+
parent.expand()
|
|
696
|
+
parent = parent.parent # type: ignore
|
|
697
|
+
|
|
698
|
+
def action_copy_command(self) -> None:
|
|
699
|
+
"""Copy the selected command to clipboard."""
|
|
700
|
+
tree = self.query_one(CommandTree)
|
|
701
|
+
if tree.cursor_node and tree.cursor_node.data:
|
|
702
|
+
command = tree.cursor_node.data.command
|
|
703
|
+
try:
|
|
704
|
+
import pyperclip # type: ignore
|
|
705
|
+
pyperclip.copy(command)
|
|
706
|
+
self.notify(f"Copied: {command}", severity="information")
|
|
707
|
+
except ImportError:
|
|
708
|
+
self.notify("Install pyperclip to enable clipboard support", severity="warning")
|
|
709
|
+
|
|
710
|
+
def action_run_command(self) -> None:
|
|
711
|
+
"""Run the selected command without arguments."""
|
|
712
|
+
tree = self.query_one(CommandTree)
|
|
713
|
+
if tree.cursor_node and tree.cursor_node.data:
|
|
714
|
+
command_info = tree.cursor_node.data
|
|
715
|
+
if command_info.is_group:
|
|
716
|
+
self.notify("Cannot run command groups directly", severity="warning")
|
|
717
|
+
return
|
|
718
|
+
|
|
719
|
+
self._execute_command(command_info.command)
|
|
720
|
+
|
|
721
|
+
def action_build_command(self) -> None:
|
|
722
|
+
"""Open command builder for selected command."""
|
|
723
|
+
tree = self.query_one(CommandTree)
|
|
724
|
+
if tree.cursor_node and tree.cursor_node.data:
|
|
725
|
+
command_info = tree.cursor_node.data
|
|
726
|
+
if command_info.is_group:
|
|
727
|
+
self.notify("Cannot build command for groups", severity="warning")
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
self.push_screen(CommandBuilderScreen(command_info), self._handle_builder_result)
|
|
731
|
+
|
|
732
|
+
def _handle_builder_result(self, result: str | None) -> None:
|
|
733
|
+
"""Handle result from command builder."""
|
|
734
|
+
if not result:
|
|
735
|
+
return
|
|
736
|
+
|
|
737
|
+
if result.startswith("EXECUTE:"):
|
|
738
|
+
command = result[8:]
|
|
739
|
+
self._execute_command(command)
|
|
740
|
+
elif result.startswith("COPY:"):
|
|
741
|
+
command = result[5:]
|
|
742
|
+
self._copy_to_clipboard(command)
|
|
743
|
+
|
|
744
|
+
def _execute_command(self, command: str) -> None:
|
|
745
|
+
"""Execute a shell command."""
|
|
746
|
+
try:
|
|
747
|
+
self.notify(f"Executing: {command}", severity="information")
|
|
748
|
+
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)
|
|
749
|
+
|
|
750
|
+
if result.returncode == 0:
|
|
751
|
+
output = result.stdout.strip()
|
|
752
|
+
if output:
|
|
753
|
+
self.notify(f"Success: {output[:100]}...", severity="information", timeout=5)
|
|
754
|
+
else:
|
|
755
|
+
self.notify("Command executed successfully", severity="information")
|
|
756
|
+
else:
|
|
757
|
+
error = result.stderr.strip() or "Unknown error"
|
|
758
|
+
self.notify(f"Error: {error[:100]}...", severity="error", timeout=10)
|
|
759
|
+
except subprocess.TimeoutExpired:
|
|
760
|
+
self.notify("Command timed out after 30 seconds", severity="warning")
|
|
761
|
+
except Exception as e:
|
|
762
|
+
self.notify(f"Failed to execute: {str(e)}", severity="error")
|
|
763
|
+
|
|
764
|
+
def _copy_to_clipboard(self, command: str) -> None:
|
|
765
|
+
"""Copy command to clipboard."""
|
|
766
|
+
try:
|
|
767
|
+
import pyperclip # type: ignore
|
|
768
|
+
pyperclip.copy(command)
|
|
769
|
+
self.notify(f"Copied: {command}", severity="information")
|
|
770
|
+
except ImportError:
|
|
771
|
+
self.notify("Install pyperclip to enable clipboard support", severity="warning")
|
|
772
|
+
|
|
773
|
+
def action_focus_search(self) -> None:
|
|
774
|
+
"""Focus the search input."""
|
|
775
|
+
search_input = self.query_one("#search-input", Input)
|
|
776
|
+
search_input.focus()
|
|
777
|
+
|
|
778
|
+
def action_help(self) -> None:
|
|
779
|
+
"""Show help information."""
|
|
780
|
+
help_text = """
|
|
781
|
+
Navigation:
|
|
782
|
+
- ↑↓: Navigate tree
|
|
783
|
+
- Enter: Expand/collapse node
|
|
784
|
+
- /: Focus search
|
|
785
|
+
- c: Copy command to clipboard
|
|
786
|
+
- r: Run command directly (no args)
|
|
787
|
+
- b: Build command with arguments
|
|
788
|
+
- q: Quit
|
|
789
|
+
- ?: Show this help
|
|
790
|
+
"""
|
|
791
|
+
self.notify(help_text, severity="information", timeout=10)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def main() -> None:
|
|
795
|
+
"""Run the command navigator TUI."""
|
|
796
|
+
app = CommandNavigatorApp()
|
|
797
|
+
app.run()
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def main_from_parser() -> None:
|
|
801
|
+
"""Entry point for CLI."""
|
|
802
|
+
main()
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
if __name__ == "__main__":
|
|
806
|
+
main()
|