shellmate-cli 3.0.0__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.
- shellmate/__init__.py +0 -0
- shellmate/adapters/__init__.py +67 -0
- shellmate/adapters/bash_adapter.sh +7 -0
- shellmate/adapters/ble_adapter.sh +350 -0
- shellmate/adapters/powershell_adapter.ps1 +417 -0
- shellmate/adapters/powershell_predictor.ps1 +97 -0
- shellmate/adapters/zsh_adapter.zsh +8 -0
- shellmate/agents/__init__.py +23 -0
- shellmate/agents/base.py +248 -0
- shellmate/agents/chain_patterns.yaml +98 -0
- shellmate/agents/command_chain.py +322 -0
- shellmate/agents/context_gatherer.py +175 -0
- shellmate/agents/environment.py +363 -0
- shellmate/agents/librarian.py +502 -0
- shellmate/agents/query.py +415 -0
- shellmate/agents/repomix_backend.py +171 -0
- shellmate/agents/watcher.py +266 -0
- shellmate/cache/__init__.py +0 -0
- shellmate/cache/semantic_cache.py +41 -0
- shellmate/cli/__init__.py +0 -0
- shellmate/cli/commands/__init__.py +27 -0
- shellmate/cli/commands/analyze.py +5 -0
- shellmate/cli/commands/config.py +319 -0
- shellmate/cli/commands/configure.py +118 -0
- shellmate/cli/commands/disable.py +11 -0
- shellmate/cli/commands/enable.py +49 -0
- shellmate/cli/commands/init.py +412 -0
- shellmate/cli/commands/install.py +242 -0
- shellmate/cli/commands/migrate.py +147 -0
- shellmate/cli/commands/status.py +31 -0
- shellmate/cli/commands/uninstall.py +78 -0
- shellmate/cli/main.py +114 -0
- shellmate/cli/parser.py +184 -0
- shellmate/cli/version.py +1 -0
- shellmate/completion/__init__.py +4 -0
- shellmate/completion/bash_adapter.py +145 -0
- shellmate/config/__init__.py +33 -0
- shellmate/config/credentials.py +120 -0
- shellmate/config/defaults.py +88 -0
- shellmate/config/manager.py +94 -0
- shellmate/config/paths.py +77 -0
- shellmate/config/secrets.py +66 -0
- shellmate/daemon/__init__.py +0 -0
- shellmate/daemon/lifecycle.py +308 -0
- shellmate/daemon/observability.py +82 -0
- shellmate/daemon/pipeline.py +119 -0
- shellmate/daemon/server.py +605 -0
- shellmate/daemon/session.py +170 -0
- shellmate/daemon/startup_ui.py +43 -0
- shellmate/environment/__init__.py +0 -0
- shellmate/environment/detector.py +130 -0
- shellmate/ipc/__init__.py +0 -0
- shellmate/ipc/client.py +210 -0
- shellmate/ipc/ps_client.py +190 -0
- shellmate/orchestration/__init__.py +3 -0
- shellmate/orchestration/analysis_graph.py +1274 -0
- shellmate/orchestration/state.py +35 -0
- shellmate/parser/__init__.py +0 -0
- shellmate/parser/command_parser.py +111 -0
- shellmate/plugins/__init__.py +0 -0
- shellmate/plugins/base.py +80 -0
- shellmate/plugins/loader.py +41 -0
- shellmate/providers/__init__.py +0 -0
- shellmate/providers/base.py +97 -0
- shellmate/providers/groq_command_guide.py +108 -0
- shellmate/providers/groq_provider.py +127 -0
- shellmate/providers/groq_supervisor.py +78 -0
- shellmate/providers/openrouter_provider.py +149 -0
- shellmate/providers/router.py +272 -0
- shellmate/providers/validate_keys.py +98 -0
- shellmate/providers/xai_provider.py +142 -0
- shellmate/rules/__init__.py +0 -0
- shellmate/rules/catastrophic.yaml +96 -0
- shellmate/rules/catastrophic_matcher.py +167 -0
- shellmate/rules/definitions/__init__.py +0 -0
- shellmate/rules/definitions/file_deletion.yaml +54 -0
- shellmate/rules/definitions/filesystem.yaml +43 -0
- shellmate/rules/definitions/network_execution.yaml +46 -0
- shellmate/rules/definitions/permissions.yaml +54 -0
- shellmate/rules/definitions/process_control.yaml +37 -0
- shellmate/rules/definitions/system_destruction.yaml +67 -0
- shellmate/rules/engine.py +200 -0
- shellmate/sanitizer/__init__.py +0 -0
- shellmate/sanitizer/typo_detector.py +114 -0
- shellmate/ui/__init__.py +10 -0
- shellmate/ui/analysis.py +164 -0
- shellmate/ui/annotations.py +232 -0
- shellmate/ui/banner.py +49 -0
- shellmate/ui/progress.py +126 -0
- shellmate/ui/runtime/__init__.py +15 -0
- shellmate/ui/runtime/console_factory.py +65 -0
- shellmate/ui/runtime/live_scene.py +116 -0
- shellmate/ui/runtime/motion_policy.py +69 -0
- shellmate/ui/runtime/tokens.py +47 -0
- shellmate/ui/scenes/__init__.py +24 -0
- shellmate/ui/scenes/enable.py +316 -0
- shellmate/ui/scenes/help.py +53 -0
- shellmate/ui/scenes/stages.py +128 -0
- shellmate/ui/scenes/status.py +181 -0
- shellmate/ui/status.py +3 -0
- shellmate_cli-3.0.0.dist-info/METADATA +352 -0
- shellmate_cli-3.0.0.dist-info/RECORD +106 -0
- shellmate_cli-3.0.0.dist-info/WHEEL +5 -0
- shellmate_cli-3.0.0.dist-info/entry_points.txt +3 -0
- shellmate_cli-3.0.0.dist-info/licenses/LICENSE +21 -0
- shellmate_cli-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared visual tokens — spacing, styles, icons with ASCII fallbacks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
CONTENT_MAX_WIDTH = 96
|
|
6
|
+
NARROW_WIDTH = 80
|
|
7
|
+
MIN_WIDTH = 40
|
|
8
|
+
|
|
9
|
+
ACCENT = "cyan"
|
|
10
|
+
MUTED = "dim"
|
|
11
|
+
SUCCESS = "green"
|
|
12
|
+
WARNING = "yellow"
|
|
13
|
+
ERROR = "red"
|
|
14
|
+
TITLE = "bold"
|
|
15
|
+
|
|
16
|
+
# Unicode icons (interactive modern terminals)
|
|
17
|
+
ICONS_UNICODE = {
|
|
18
|
+
"pending": "○",
|
|
19
|
+
"running": "●",
|
|
20
|
+
"done": "✓",
|
|
21
|
+
"error": "✗",
|
|
22
|
+
"skip": "−",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# ASCII-safe (Windows PS 5.1 / constrained / --no-color plain)
|
|
26
|
+
ICONS_ASCII = {
|
|
27
|
+
"pending": "o",
|
|
28
|
+
"running": "*",
|
|
29
|
+
"done": "+",
|
|
30
|
+
"error": "x",
|
|
31
|
+
"skip": "-",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def icons(ascii_safe: bool = False) -> dict[str, str]:
|
|
36
|
+
return ICONS_ASCII if ascii_safe else ICONS_UNICODE
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def content_width(console_width: int) -> int:
|
|
40
|
+
"""Usable content width: track terminal, clamp to max, never invent fixed 110."""
|
|
41
|
+
w = max(int(console_width or MIN_WIDTH), MIN_WIDTH)
|
|
42
|
+
return min(w, CONTENT_MAX_WIDTH)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def show_logo(console_width: int) -> bool:
|
|
46
|
+
"""Bold logo only when there is room; narrow terminals get wordmark only."""
|
|
47
|
+
return int(console_width or 0) >= NARROW_WIDTH
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""CLI scene composers."""
|
|
2
|
+
|
|
3
|
+
from shellmate.ui.scenes.enable import render_enable_frame, run_enable_scene
|
|
4
|
+
from shellmate.ui.scenes.help import print_help, render_help
|
|
5
|
+
from shellmate.ui.scenes.status import render_status, build_status_renderable
|
|
6
|
+
from shellmate.ui.scenes.stages import (
|
|
7
|
+
CANONICAL_STAGES,
|
|
8
|
+
reduce_enable_state,
|
|
9
|
+
normalize_completed,
|
|
10
|
+
next_reveal_target,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"render_enable_frame",
|
|
15
|
+
"run_enable_scene",
|
|
16
|
+
"print_help",
|
|
17
|
+
"render_help",
|
|
18
|
+
"render_status",
|
|
19
|
+
"build_status_renderable",
|
|
20
|
+
"CANONICAL_STAGES",
|
|
21
|
+
"reduce_enable_state",
|
|
22
|
+
"normalize_completed",
|
|
23
|
+
"next_reveal_target",
|
|
24
|
+
]
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Enable scene — bold logo moment + live stage morph + quiet usage settle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Callable, Iterable, Optional
|
|
7
|
+
|
|
8
|
+
from rich.console import Console, Group, RenderableType
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from shellmate.cli.version import VERSION
|
|
13
|
+
from shellmate.ui.runtime.console_factory import ConsoleFactory
|
|
14
|
+
from shellmate.ui.runtime.live_scene import LiveScene
|
|
15
|
+
from shellmate.ui.runtime.motion_policy import MotionPolicy, motion_from_env
|
|
16
|
+
from shellmate.ui.runtime import tokens
|
|
17
|
+
from shellmate.ui.scenes.stages import (
|
|
18
|
+
CANONICAL_STAGES,
|
|
19
|
+
EnableViewState,
|
|
20
|
+
StageStatus,
|
|
21
|
+
next_reveal_target,
|
|
22
|
+
normalize_completed,
|
|
23
|
+
reduce_enable_state,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Compact logo (fits content max-width); dropped on narrow terminals.
|
|
27
|
+
_LOGO_LINES = [
|
|
28
|
+
r" ____ _ _ _ __ __ _ ",
|
|
29
|
+
r" / ___|| |__ ___| | | \/ | __ _| |_ ___ ",
|
|
30
|
+
r" \___ \| '_ \ / _ \ | | |\/| |/ _` | __/ _ \\",
|
|
31
|
+
r" ___) | | | | __/ | | | | | (_| | || __/",
|
|
32
|
+
r" |____/|_| |_|\___|_|_|_| |_|\__,_|\__\___|",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def render_enable_frame(
|
|
37
|
+
state: EnableViewState,
|
|
38
|
+
*,
|
|
39
|
+
console_width: int,
|
|
40
|
+
ascii_safe: bool = False,
|
|
41
|
+
settled: bool = False,
|
|
42
|
+
) -> RenderableType:
|
|
43
|
+
"""Build one enable-scene frame. Width-aware; no fixed 110 panels."""
|
|
44
|
+
width = tokens.content_width(console_width)
|
|
45
|
+
icon = tokens.icons(ascii_safe=ascii_safe)
|
|
46
|
+
lines: list[Text | str] = []
|
|
47
|
+
|
|
48
|
+
if tokens.show_logo(console_width):
|
|
49
|
+
for row in _LOGO_LINES:
|
|
50
|
+
lines.append(Text(row, style=tokens.ACCENT))
|
|
51
|
+
lines.append("")
|
|
52
|
+
lines.append(Text(f"ShellMate v{VERSION}", style=tokens.MUTED))
|
|
53
|
+
else:
|
|
54
|
+
lines.append(Text(f"ShellMate v{VERSION}", style=f"bold {tokens.ACCENT}"))
|
|
55
|
+
|
|
56
|
+
lines.append("")
|
|
57
|
+
lines.append(Text("Startup", style=tokens.TITLE))
|
|
58
|
+
|
|
59
|
+
for stage in state.stages:
|
|
60
|
+
if stage.status == StageStatus.DONE:
|
|
61
|
+
mark = Text(f" {icon['done']} ", style=tokens.SUCCESS)
|
|
62
|
+
label = Text(stage.label, style=tokens.SUCCESS)
|
|
63
|
+
elif stage.status == StageStatus.RUNNING:
|
|
64
|
+
mark = Text(f" {icon['running']} ", style=tokens.ACCENT)
|
|
65
|
+
label = Text(stage.label, style=tokens.ACCENT)
|
|
66
|
+
elif stage.status == StageStatus.INCOMPLETE:
|
|
67
|
+
mark = Text(f" {icon['pending']} ", style=tokens.MUTED)
|
|
68
|
+
label = Text(stage.label, style=tokens.MUTED)
|
|
69
|
+
else:
|
|
70
|
+
mark = Text(f" {icon['pending']} ", style=tokens.MUTED)
|
|
71
|
+
label = Text(stage.label, style=tokens.MUTED)
|
|
72
|
+
lines.append(Text.assemble(mark, label))
|
|
73
|
+
|
|
74
|
+
if settled:
|
|
75
|
+
lines.append("")
|
|
76
|
+
if state.timed_out:
|
|
77
|
+
lines.append(Text("Startup timed out. Daemon may not be ready.", style=tokens.ERROR))
|
|
78
|
+
lines.append(Text("Run sm status to check daemon state.", style=tokens.MUTED))
|
|
79
|
+
else:
|
|
80
|
+
lines.append(Text("Active features", style=tokens.TITLE))
|
|
81
|
+
lines.append(Text.assemble(
|
|
82
|
+
Text(f" {icon['done']} ", style=tokens.SUCCESS),
|
|
83
|
+
Text("Autocomplete: active", style=tokens.SUCCESS),
|
|
84
|
+
))
|
|
85
|
+
lines.append(Text.assemble(
|
|
86
|
+
Text(f" {icon['done']} ", style=tokens.SUCCESS),
|
|
87
|
+
Text("Consequence preview: active", style=tokens.SUCCESS),
|
|
88
|
+
))
|
|
89
|
+
lines.append("")
|
|
90
|
+
lines.append(Text("Usage", style=tokens.TITLE))
|
|
91
|
+
from rich.table import Table
|
|
92
|
+
usage = Table(show_header=False, box=None, padding=(0, 2), collapse_padding=True)
|
|
93
|
+
usage.add_column(style=tokens.ACCENT, no_wrap=True)
|
|
94
|
+
usage.add_column(overflow="fold")
|
|
95
|
+
usage.add_row("sm status", "Show daemon status")
|
|
96
|
+
usage.add_row("sm disable", "Stop ShellMate")
|
|
97
|
+
lines.append(usage)
|
|
98
|
+
|
|
99
|
+
body = Group(*lines)
|
|
100
|
+
return Panel(
|
|
101
|
+
body,
|
|
102
|
+
title=f"[bold {tokens.ACCENT}]ShellMate[/] [dim]v{VERSION}[/]",
|
|
103
|
+
border_style=tokens.ACCENT,
|
|
104
|
+
padding=(1, 2),
|
|
105
|
+
width=width,
|
|
106
|
+
expand=False,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _hold_last_completed(
|
|
111
|
+
prev: list[str],
|
|
112
|
+
incoming: Optional[Iterable[str]],
|
|
113
|
+
current: Optional[str],
|
|
114
|
+
) -> list[str]:
|
|
115
|
+
"""IPC blip (both None) keeps previous completed set — no reset flicker."""
|
|
116
|
+
if incoming is None and current is None:
|
|
117
|
+
return list(prev)
|
|
118
|
+
# incoming None but current set → legacy expand via normalize
|
|
119
|
+
return normalize_completed(incoming, current)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
StagePoll = Callable[[], tuple[Optional[list[str]], Optional[str]]]
|
|
123
|
+
# Returns (startup_stages_completed | None on blip, startup_stage | None)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_enable_scene(
|
|
127
|
+
*,
|
|
128
|
+
poll: StagePoll,
|
|
129
|
+
console: Optional[Console] = None,
|
|
130
|
+
plain: bool = False,
|
|
131
|
+
ascii_safe: bool = False,
|
|
132
|
+
timeout: float = 30.0,
|
|
133
|
+
policy: Optional[MotionPolicy] = None,
|
|
134
|
+
sleep_fn: Callable[[float], None] = time.sleep,
|
|
135
|
+
poll_interval: float = 0.15,
|
|
136
|
+
) -> bool:
|
|
137
|
+
"""Live enable scene. Returns True if ready, False on timeout.
|
|
138
|
+
|
|
139
|
+
Animation failure never raises — falls back to static settle.
|
|
140
|
+
"""
|
|
141
|
+
factory_plain = plain
|
|
142
|
+
if console is None:
|
|
143
|
+
cf = ConsoleFactory(no_color=plain, force_terminal=False if plain else None)
|
|
144
|
+
console = cf.create()
|
|
145
|
+
factory_plain = cf.is_plain() or plain
|
|
146
|
+
|
|
147
|
+
policy = policy or motion_from_env(plain=factory_plain)
|
|
148
|
+
ascii_safe = ascii_safe or factory_plain
|
|
149
|
+
|
|
150
|
+
scene = LiveScene(console, plain=factory_plain)
|
|
151
|
+
last_completed: list[str] = []
|
|
152
|
+
revealed = 0
|
|
153
|
+
ready = False
|
|
154
|
+
timed_out = False
|
|
155
|
+
artificial_spent_ms = 0.0
|
|
156
|
+
saw_ready_at: Optional[float] = None
|
|
157
|
+
|
|
158
|
+
initial = reduce_enable_state(completed=[], revealed_through=1)
|
|
159
|
+
try:
|
|
160
|
+
scene.start(render_enable_frame(initial, console_width=console.width, ascii_safe=ascii_safe))
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Ensure at least the first stage shows as running while IPC is not up yet
|
|
165
|
+
revealed = 1
|
|
166
|
+
scene.update(
|
|
167
|
+
render_enable_frame(
|
|
168
|
+
reduce_enable_state(completed=[], revealed_through=1),
|
|
169
|
+
console_width=console.width,
|
|
170
|
+
ascii_safe=ascii_safe,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
start = time.monotonic()
|
|
175
|
+
try:
|
|
176
|
+
while time.monotonic() - start < timeout:
|
|
177
|
+
try:
|
|
178
|
+
completed_raw, current = poll()
|
|
179
|
+
except Exception:
|
|
180
|
+
completed_raw, current = None, None
|
|
181
|
+
|
|
182
|
+
last_completed = _hold_last_completed(last_completed, completed_raw, current)
|
|
183
|
+
target = next_reveal_target(last_completed, current)
|
|
184
|
+
# Keep a live tip visible before the daemon answers
|
|
185
|
+
if target == 0:
|
|
186
|
+
target = 1
|
|
187
|
+
|
|
188
|
+
if "ready" in last_completed and saw_ready_at is None:
|
|
189
|
+
saw_ready_at = time.monotonic()
|
|
190
|
+
|
|
191
|
+
# Ordered reveal toward target
|
|
192
|
+
while revealed < target:
|
|
193
|
+
catching_up = target - revealed
|
|
194
|
+
remaining_budget_s = (
|
|
195
|
+
policy.clamp_artificial_budget(artificial_spent_ms)
|
|
196
|
+
if saw_ready_at is not None
|
|
197
|
+
else None
|
|
198
|
+
)
|
|
199
|
+
if saw_ready_at is not None and remaining_budget_s is not None and remaining_budget_s <= 0:
|
|
200
|
+
revealed = target
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if saw_ready_at is not None:
|
|
204
|
+
# Daemon already ready — paced ordered replay, clamped by budget
|
|
205
|
+
step_delay = policy.dwell_seconds() + policy.stagger_seconds()
|
|
206
|
+
step_delay = min(step_delay, remaining_budget_s / catching_up)
|
|
207
|
+
elif catching_up > 1:
|
|
208
|
+
# Skipped ahead while polling — brief stagger between lines
|
|
209
|
+
step_delay = policy.stagger_seconds()
|
|
210
|
+
else:
|
|
211
|
+
# Live follow of real progress — tiny pause so the frame paints
|
|
212
|
+
step_delay = min(0.08, policy.stagger_seconds()) if not factory_plain else 0.0
|
|
213
|
+
|
|
214
|
+
revealed += 1
|
|
215
|
+
state = reduce_enable_state(
|
|
216
|
+
completed=last_completed,
|
|
217
|
+
current=current,
|
|
218
|
+
revealed_through=revealed,
|
|
219
|
+
)
|
|
220
|
+
frame = render_enable_frame(
|
|
221
|
+
state,
|
|
222
|
+
console_width=console.width,
|
|
223
|
+
ascii_safe=ascii_safe,
|
|
224
|
+
)
|
|
225
|
+
scene.update(frame)
|
|
226
|
+
|
|
227
|
+
if step_delay > 0:
|
|
228
|
+
sleep_fn(step_delay)
|
|
229
|
+
if saw_ready_at is not None:
|
|
230
|
+
artificial_spent_ms += step_delay * 1000.0
|
|
231
|
+
|
|
232
|
+
# Refresh current tip even when target unchanged (running spinner feel)
|
|
233
|
+
if not ("ready" in last_completed and revealed >= target):
|
|
234
|
+
state = reduce_enable_state(
|
|
235
|
+
completed=last_completed,
|
|
236
|
+
current=current,
|
|
237
|
+
revealed_through=max(revealed, 1),
|
|
238
|
+
)
|
|
239
|
+
scene.update(
|
|
240
|
+
render_enable_frame(
|
|
241
|
+
state,
|
|
242
|
+
console_width=console.width,
|
|
243
|
+
ascii_safe=ascii_safe,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if "ready" in last_completed:
|
|
248
|
+
if revealed >= target:
|
|
249
|
+
ready = True
|
|
250
|
+
break
|
|
251
|
+
sleep_fn(0.0 if factory_plain else poll_interval)
|
|
252
|
+
|
|
253
|
+
if not ready:
|
|
254
|
+
timed_out = True
|
|
255
|
+
except Exception:
|
|
256
|
+
timed_out = not ready
|
|
257
|
+
|
|
258
|
+
# Never invent success: only keys in last_completed are DONE.
|
|
259
|
+
final_state = reduce_enable_state(
|
|
260
|
+
completed=last_completed,
|
|
261
|
+
timed_out=timed_out,
|
|
262
|
+
revealed_through=len(CANONICAL_STAGES) if (timed_out or ready) else max(revealed, 1),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
scene.settle(
|
|
267
|
+
render_enable_frame(
|
|
268
|
+
final_state,
|
|
269
|
+
console_width=console.width,
|
|
270
|
+
ascii_safe=ascii_safe,
|
|
271
|
+
settled=True,
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
except Exception:
|
|
275
|
+
try:
|
|
276
|
+
console.print("[red]Startup finished with UI fallback.[/]")
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
return ready and not timed_out
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def demo_enable_animation(*, slow: bool = True) -> bool:
|
|
284
|
+
"""Watch the enable morph without a real daemon (for manual testing).
|
|
285
|
+
|
|
286
|
+
Run::
|
|
287
|
+
python -c "from shellmate.ui.scenes.enable import demo_enable_animation; demo_enable_animation()"
|
|
288
|
+
|
|
289
|
+
Or slow real enable::
|
|
290
|
+
$env:SM_UI_SLOW=1; sm disable; sm enable
|
|
291
|
+
"""
|
|
292
|
+
import os
|
|
293
|
+
|
|
294
|
+
keys = [k for k, _ in CANONICAL_STAGES]
|
|
295
|
+
state = {"i": 0, "t0": time.monotonic()}
|
|
296
|
+
|
|
297
|
+
def poll():
|
|
298
|
+
# Reveal one new stage every ~0.4s
|
|
299
|
+
elapsed = time.monotonic() - state["t0"]
|
|
300
|
+
n = min(len(keys), 1 + int(elapsed / 0.4))
|
|
301
|
+
state["i"] = n
|
|
302
|
+
completed = keys[:n]
|
|
303
|
+
return completed, completed[-1]
|
|
304
|
+
|
|
305
|
+
if slow:
|
|
306
|
+
os.environ.setdefault("SM_UI_SLOW", "1")
|
|
307
|
+
|
|
308
|
+
return run_enable_scene(
|
|
309
|
+
poll=poll,
|
|
310
|
+
timeout=20.0,
|
|
311
|
+
poll_interval=0.05,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
demo_enable_animation()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Help / usage scene — static, minimal (no logo)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console, Group
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from shellmate.cli.version import VERSION
|
|
10
|
+
from shellmate.ui.runtime import tokens
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_help(*, console_width: int | None = None) -> Group:
|
|
14
|
+
width_hint = console_width or tokens.CONTENT_MAX_WIDTH
|
|
15
|
+
_ = tokens.content_width(width_hint)
|
|
16
|
+
|
|
17
|
+
usage = Table(show_header=False, box=None, padding=(0, 2))
|
|
18
|
+
usage.add_column(style=tokens.ACCENT, no_wrap=True)
|
|
19
|
+
usage.add_column(overflow="fold")
|
|
20
|
+
rows = [
|
|
21
|
+
("sm install", "Install shell hooks"),
|
|
22
|
+
("sm config", "Configure providers and settings"),
|
|
23
|
+
("sm enable", "Start the daemon for current directory"),
|
|
24
|
+
("sm disable", "Stop the daemon and remove session files"),
|
|
25
|
+
("sm status", "Show daemon, agent, and provider status"),
|
|
26
|
+
("sm uninstall", "Remove shell hooks"),
|
|
27
|
+
("sm --help", "Show this help message"),
|
|
28
|
+
("sm --version", "Show version"),
|
|
29
|
+
("sm --no-color <cmd>", "Plain text output (no animation)"),
|
|
30
|
+
]
|
|
31
|
+
for cmd, desc in rows:
|
|
32
|
+
usage.add_row(cmd, desc)
|
|
33
|
+
|
|
34
|
+
return Group(
|
|
35
|
+
Text(f"ShellMate v{VERSION}", style=f"bold {tokens.ACCENT}"),
|
|
36
|
+
Text("Intelligent command safety layer", style=tokens.MUTED),
|
|
37
|
+
Text(""),
|
|
38
|
+
Text("Usage", style=tokens.TITLE),
|
|
39
|
+
usage,
|
|
40
|
+
Text(""),
|
|
41
|
+
Text("Examples", style=tokens.TITLE),
|
|
42
|
+
Text(" $ sm enable", style=tokens.MUTED),
|
|
43
|
+
Text(" $ sm status", style=tokens.MUTED),
|
|
44
|
+
Text(""),
|
|
45
|
+
Text("Run sm enable to start the daemon.", style=tokens.MUTED),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_help(console: Console | None = None) -> None:
|
|
50
|
+
from shellmate.ui.runtime.console_factory import ConsoleFactory
|
|
51
|
+
|
|
52
|
+
c = console or ConsoleFactory().create()
|
|
53
|
+
c.print(render_help(console_width=c.width))
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Canonical startup stages and pure reducer for enable-scene UI state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Iterable, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Canonical display order (UI labels). Daemon keys must match.
|
|
11
|
+
CANONICAL_STAGES: list[tuple[str, str]] = [
|
|
12
|
+
("binding_ipc", "Binding IPC"),
|
|
13
|
+
("watcher_starting", "Starting Watcher"),
|
|
14
|
+
("watcher_ready", "Watcher ready"),
|
|
15
|
+
("librarian_scanning", "Scanning project files"),
|
|
16
|
+
("librarian_ready", "Librarian ready"),
|
|
17
|
+
("providers_checked", "Checking providers"),
|
|
18
|
+
("ready", "Ready"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
STAGE_KEYS = [k for k, _ in CANONICAL_STAGES]
|
|
22
|
+
STAGE_LABELS = {k: label for k, label in CANONICAL_STAGES}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StageStatus(str, Enum):
|
|
26
|
+
PENDING = "pending"
|
|
27
|
+
RUNNING = "running"
|
|
28
|
+
DONE = "done"
|
|
29
|
+
INCOMPLETE = "incomplete" # timeout — never paint false success
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class StageView:
|
|
34
|
+
key: str
|
|
35
|
+
label: str
|
|
36
|
+
status: StageStatus
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class EnableViewState:
|
|
41
|
+
stages: list[StageView] = field(default_factory=list)
|
|
42
|
+
ready: bool = False
|
|
43
|
+
timed_out: bool = False
|
|
44
|
+
revealed_count: int = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def expand_from_current(current: Optional[str]) -> list[str]:
|
|
48
|
+
"""Legacy single-pointer protocol: current stage implies all prior milestones reached."""
|
|
49
|
+
if not current or current not in STAGE_LABELS:
|
|
50
|
+
return []
|
|
51
|
+
idx = STAGE_KEYS.index(current)
|
|
52
|
+
return list(STAGE_KEYS[: idx + 1])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def normalize_completed(completed: Iterable[str] | None, current: Optional[str] = None) -> list[str]:
|
|
56
|
+
"""Merge completed list + current into canonical-ordered unique keys.
|
|
57
|
+
|
|
58
|
+
If ``completed`` is empty/None but ``current`` is set, expand legacy pointer.
|
|
59
|
+
"""
|
|
60
|
+
raw_list = list(completed) if completed is not None else []
|
|
61
|
+
if not raw_list and current:
|
|
62
|
+
raw_list = expand_from_current(current)
|
|
63
|
+
seen: set[str] = set()
|
|
64
|
+
raw: list[str] = []
|
|
65
|
+
for key in raw_list + ([current] if current else []):
|
|
66
|
+
if key in STAGE_LABELS and key not in seen:
|
|
67
|
+
seen.add(key)
|
|
68
|
+
raw.append(key)
|
|
69
|
+
raw.sort(key=lambda k: STAGE_KEYS.index(k))
|
|
70
|
+
return raw
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def next_reveal_target(completed: Iterable[str] | None, current: Optional[str] = None) -> int:
|
|
74
|
+
"""Exclusive index into CANONICAL_STAGES the UI should reveal up to."""
|
|
75
|
+
ordered = normalize_completed(completed, current)
|
|
76
|
+
if not ordered:
|
|
77
|
+
return 0
|
|
78
|
+
return STAGE_KEYS.index(ordered[-1]) + 1
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def reduce_enable_state(
|
|
82
|
+
*,
|
|
83
|
+
completed: Iterable[str] | None = None,
|
|
84
|
+
current: Optional[str] = None,
|
|
85
|
+
timed_out: bool = False,
|
|
86
|
+
revealed_through: Optional[int] = None,
|
|
87
|
+
) -> EnableViewState:
|
|
88
|
+
"""Pure reducer: daemon facts → UI stage list. Never invents success.
|
|
89
|
+
|
|
90
|
+
``revealed_through`` caps how many canonical rows are visible (ordered animation).
|
|
91
|
+
If None, reveal through the latest known completed/current stage (or all on timeout).
|
|
92
|
+
"""
|
|
93
|
+
completed_ordered = normalize_completed(completed, current)
|
|
94
|
+
completed_set = set(completed_ordered)
|
|
95
|
+
ready = "ready" in completed_set and not timed_out
|
|
96
|
+
|
|
97
|
+
if revealed_through is None:
|
|
98
|
+
if timed_out:
|
|
99
|
+
reveal_idx = len(CANONICAL_STAGES)
|
|
100
|
+
else:
|
|
101
|
+
reveal_idx = next_reveal_target(completed, current)
|
|
102
|
+
if reveal_idx == 0 and current:
|
|
103
|
+
reveal_idx = 1
|
|
104
|
+
else:
|
|
105
|
+
reveal_idx = max(0, min(int(revealed_through), len(CANONICAL_STAGES)))
|
|
106
|
+
|
|
107
|
+
stages: list[StageView] = []
|
|
108
|
+
for i, (key, label) in enumerate(CANONICAL_STAGES):
|
|
109
|
+
if i >= reveal_idx:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if key in completed_set:
|
|
113
|
+
status = StageStatus.DONE
|
|
114
|
+
elif timed_out:
|
|
115
|
+
status = StageStatus.INCOMPLETE
|
|
116
|
+
elif i == reveal_idx - 1:
|
|
117
|
+
status = StageStatus.RUNNING
|
|
118
|
+
else:
|
|
119
|
+
status = StageStatus.PENDING
|
|
120
|
+
|
|
121
|
+
stages.append(StageView(key=key, label=label, status=status))
|
|
122
|
+
|
|
123
|
+
return EnableViewState(
|
|
124
|
+
stages=stages,
|
|
125
|
+
ready=ready,
|
|
126
|
+
timed_out=timed_out,
|
|
127
|
+
revealed_count=reveal_idx,
|
|
128
|
+
)
|