Skip to content

Commit 3ff7ff7

Browse files
committed
feat(cli): better CLI help messages
1 parent 100d36d commit 3ff7ff7

File tree

4 files changed

+79
-73
lines changed

4 files changed

+79
-73
lines changed

packages/hop3-cli/src/hop3_cli/commands/help.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -93,61 +93,96 @@ def inject_local_commands_into_help(result: list[dict]) -> list[dict]:
9393
return modified_result
9494

9595

96+
def _collect_server_commands(lines: list[str]) -> set[str]:
97+
"""Collect all command names from server output."""
98+
server_commands: set[str] = set()
99+
for line in lines:
100+
if _is_command_line(line):
101+
cmd_name = _get_command_name(line)
102+
if cmd_name:
103+
server_commands.add(cmd_name)
104+
return server_commands
105+
106+
107+
def _insert_remaining_at_end(
108+
new_lines: list[str],
109+
remaining: list[str],
110+
) -> None:
111+
"""Insert remaining commands after the last command line."""
112+
insert_idx = len(new_lines)
113+
for i in range(len(new_lines) - 1, -1, -1):
114+
if _is_command_line(new_lines[i]):
115+
insert_idx = i + 1
116+
break
117+
for j, cmd_line in enumerate(remaining):
118+
new_lines.insert(insert_idx + j, cmd_line)
119+
120+
96121
def _process_help_text_with_local_commands(
97122
text: str,
98123
local_commands: dict[str, str],
99124
) -> str:
100125
"""Process help text and inject local commands into COMMANDS section."""
101126
lines = text.split("\n")
102-
new_lines = []
127+
new_lines: list[str] = []
103128
in_commands_section = False
104-
injected: set[str] = set()
129+
is_all_commands = False
130+
131+
# Pre-collect server commands to avoid duplicates
132+
injected = _collect_server_commands(lines)
105133

106134
for line in lines:
107-
if line.strip() in {"COMMANDS", "ALL COMMANDS"}:
135+
stripped = line.strip()
136+
137+
# Detect section headers
138+
if stripped in {"ALL COMMANDS", "COMMANDS"}:
108139
in_commands_section = True
140+
is_all_commands = stripped == "ALL COMMANDS"
109141
new_lines.append(line)
110142
continue
111143

112-
if in_commands_section and line.strip() and not line.startswith(" "):
113-
# Leaving COMMANDS section - inject remaining commands first
114-
new_lines.extend(_inject_remaining_commands(local_commands, injected))
144+
# Detect leaving commands section
145+
if in_commands_section and stripped and not line.startswith(" "):
146+
new_lines.extend(
147+
_inject_remaining_commands(local_commands, injected, is_all_commands)
148+
)
115149
in_commands_section = False
116150

151+
# Inject local commands before current command if in section
117152
if in_commands_section and _is_command_line(line):
118153
current_cmd = _get_command_name(line)
119154
if current_cmd:
120155
new_lines.extend(
121-
_inject_commands_before(current_cmd, local_commands, injected)
156+
_inject_commands_before(
157+
current_cmd, local_commands, injected, is_all_commands
158+
)
122159
)
123160

124161
new_lines.append(line)
125162

126-
# If still in commands section at end, inject remaining
163+
# Handle remaining commands at end of section
127164
if in_commands_section:
128-
remaining = _inject_remaining_commands(local_commands, injected)
165+
remaining = _inject_remaining_commands(
166+
local_commands, injected, is_all_commands
167+
)
129168
if remaining:
130-
# Insert after last command line
131-
insert_idx = len(new_lines)
132-
for i in range(len(new_lines) - 1, -1, -1):
133-
if _is_command_line(new_lines[i]):
134-
insert_idx = i + 1
135-
break
136-
for j, cmd_line in enumerate(remaining):
137-
new_lines.insert(insert_idx + j, cmd_line)
169+
_insert_remaining_at_end(new_lines, remaining)
138170

139171
return "\n".join(new_lines)
140172

141173

142174
def _inject_remaining_commands(
143175
local_commands: dict[str, str],
144176
injected: set[str],
177+
is_all_commands: bool = False,
145178
) -> list[str]:
146179
"""Return all local commands not yet injected."""
147180
lines = []
148181
for cmd in sorted(local_commands.keys()):
149182
if cmd not in injected:
150-
lines.append(_format_help_command(cmd, local_commands[cmd]))
183+
lines.append(
184+
_format_help_command(cmd, local_commands[cmd], is_all_commands)
185+
)
151186
injected.add(cmd)
152187
return lines
153188

@@ -167,16 +202,26 @@ def _inject_commands_before(
167202
current_cmd: str,
168203
local_commands: dict[str, str],
169204
injected: set[str],
205+
is_all_commands: bool = False,
170206
) -> list[str]:
171207
"""Return local commands that should appear before current_cmd alphabetically."""
172208
lines = []
173209
for cmd in sorted(local_commands.keys()):
174210
if cmd not in injected and cmd < current_cmd:
175-
lines.append(_format_help_command(cmd, local_commands[cmd]))
211+
lines.append(
212+
_format_help_command(cmd, local_commands[cmd], is_all_commands)
213+
)
176214
injected.add(cmd)
177215
return lines
178216

179217

180-
def _format_help_command(name: str, description: str) -> str:
181-
"""Format a command entry for help output."""
182-
return f" {name:16} {description}"
218+
def _format_help_command(name: str, description: str, wide: bool = False) -> str:
219+
"""Format a command entry for help output.
220+
221+
Args:
222+
name: Command name
223+
description: Command description
224+
wide: If True, use 24-char width (for --all mode), otherwise 16-char
225+
"""
226+
width = 24 if wide else 16
227+
return f" {name:<{width}} {description}"

packages/hop3-server/src/hop3/commands/backup.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,7 @@ def call(self, *args):
396396
class BackupCmd(Command):
397397
"""Manage application backups.
398398
399-
Commands:
400-
backup:create Create a backup of an application
401-
backup:list List all backups
402-
backup:info Show detailed backup information
403-
backup:restore Restore an application from backup
404-
backup:delete Delete a backup
399+
Use 'hop help backup' to see available subcommands.
405400
"""
406401

407402
name: ClassVar[str] = "backup"

packages/hop3-server/src/hop3/commands/misc.py

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import subprocess
1010
import tempfile
1111
from dataclasses import dataclass
12-
from datetime import datetime, timezone
1312
from importlib.metadata import version as get_version
1413
from pathlib import Path
1514
from typing import TYPE_CHECKING, ClassVar
@@ -49,50 +48,6 @@ def call(self, *args):
4948
return [text(f"hop3-server {server_version}")]
5049

5150

52-
# --- Backup Command ---
53-
54-
55-
@register
56-
@dataclass(frozen=True)
57-
class BackupCmd(Command):
58-
"""Run a backup for an app's source code and virtual environment."""
59-
60-
db_session: Session
61-
name: ClassVar[str] = "backup"
62-
63-
def call(self, *args):
64-
if not args:
65-
msg = "Usage: hop backup <app_name>"
66-
raise ValueError(msg)
67-
app_name = args[0]
68-
app = get_app(self.db_session, app_name)
69-
70-
# POC implementation
71-
path_to_backup = app.app_path
72-
now = datetime.now(timezone.utc)
73-
timestamp = now.strftime("%Y%m%d-%H%M%S")
74-
backup_name = f"{app.name}-{timestamp}.tar.gz"
75-
backup_dir = c.HOP3_ROOT / "backup"
76-
backup_dir.mkdir(parents=True, exist_ok=True)
77-
backup_file_path = backup_dir / backup_name
78-
79-
cmd = [
80-
"tar",
81-
"-zcf",
82-
str(backup_file_path),
83-
"-C",
84-
str(path_to_backup.parent),
85-
path_to_backup.name,
86-
]
87-
with command_context("creating backup", app_name=app_name):
88-
subprocess.run(cmd, check=True, capture_output=True, text=True)
89-
90-
return [
91-
text(f"Backup for {app.name} created successfully."),
92-
text(f"Location: {backup_file_path}"),
93-
]
94-
95-
9651
# --- Plugins Command ---
9752

9853

packages/hop3-server/src/hop3/commands/services.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@
2424
from sqlalchemy.orm import Session
2525

2626

27+
@register
28+
@dataclass(frozen=True)
29+
class AddonsCmd(Command):
30+
"""Manage backing services (databases, caches, etc.).
31+
32+
Use 'hop help addons' to see available subcommands.
33+
"""
34+
35+
name: ClassVar[str] = "addons"
36+
37+
2738
@register
2839
@dataclass(frozen=True)
2940
class AddonsListCmd(Command):

0 commit comments

Comments
 (0)