emdash-cli 0.1.46__py3-none-any.whl → 0.1.70__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.
- emdash_cli/client.py +12 -28
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/constants.py +78 -0
- emdash_cli/commands/agent/handlers/__init__.py +10 -0
- emdash_cli/commands/agent/handlers/agents.py +67 -39
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +119 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +48 -31
- emdash_cli/commands/agent/handlers/sessions.py +1 -1
- emdash_cli/commands/agent/handlers/setup.py +187 -54
- emdash_cli/commands/agent/handlers/skills.py +42 -4
- emdash_cli/commands/agent/handlers/telegram.py +523 -0
- emdash_cli/commands/agent/handlers/todos.py +55 -34
- emdash_cli/commands/agent/handlers/verify.py +10 -5
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +278 -47
- emdash_cli/commands/agent/menus.py +116 -84
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- emdash_cli/commands/skills.py +72 -6
- emdash_cli/design.py +328 -0
- emdash_cli/diff_renderer.py +438 -0
- emdash_cli/integrations/__init__.py +1 -0
- emdash_cli/integrations/telegram/__init__.py +15 -0
- emdash_cli/integrations/telegram/bot.py +402 -0
- emdash_cli/integrations/telegram/bridge.py +980 -0
- emdash_cli/integrations/telegram/config.py +155 -0
- emdash_cli/integrations/telegram/formatter.py +392 -0
- emdash_cli/main.py +52 -2
- emdash_cli/sse_renderer.py +632 -171
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/METADATA +2 -2
- emdash_cli-0.1.70.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.46.dist-info/RECORD +0 -49
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"""First-run onboarding wizard for emdash CLI.
|
|
2
|
+
|
|
3
|
+
Provides an animated, guided setup experience for new users with zen styling.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.live import Live
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from prompt_toolkit import PromptSession
|
|
14
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
15
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
16
|
+
from prompt_toolkit.styles import Style
|
|
17
|
+
from prompt_toolkit import Application
|
|
18
|
+
|
|
19
|
+
from ...design import (
|
|
20
|
+
Colors,
|
|
21
|
+
ANSI,
|
|
22
|
+
STATUS_ACTIVE,
|
|
23
|
+
STATUS_INACTIVE,
|
|
24
|
+
STATUS_ERROR,
|
|
25
|
+
DOT_BULLET,
|
|
26
|
+
DOT_WAITING,
|
|
27
|
+
DOT_ACTIVE,
|
|
28
|
+
ARROW_PROMPT,
|
|
29
|
+
ARROW_RIGHT,
|
|
30
|
+
EM_DASH,
|
|
31
|
+
header,
|
|
32
|
+
footer,
|
|
33
|
+
step_progress,
|
|
34
|
+
SEPARATOR_WIDTH,
|
|
35
|
+
SPINNER_FRAMES,
|
|
36
|
+
LOGO,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
console = Console()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
# Animation Utilities
|
|
44
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def typewriter(text: str, delay: float = 0.02, style: str = "") -> None:
|
|
47
|
+
"""Print text with typewriter animation."""
|
|
48
|
+
for char in text:
|
|
49
|
+
if style:
|
|
50
|
+
console.print(char, end="", style=style)
|
|
51
|
+
else:
|
|
52
|
+
console.print(char, end="")
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
time.sleep(delay)
|
|
55
|
+
console.print() # newline
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def animate_line(text: str, style: str = "", delay: float = 0.03) -> None:
|
|
59
|
+
"""Animate a single line appearing character by character."""
|
|
60
|
+
for i in range(len(text) + 1):
|
|
61
|
+
sys.stdout.write(f"\r{text[:i]}")
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
time.sleep(delay)
|
|
64
|
+
sys.stdout.write("\n")
|
|
65
|
+
sys.stdout.flush()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def animate_dots(message: str, duration: float = 1.0, style: str = "") -> None:
|
|
69
|
+
"""Show animated dots for a duration."""
|
|
70
|
+
frames = len(SPINNER_FRAMES)
|
|
71
|
+
start = time.time()
|
|
72
|
+
i = 0
|
|
73
|
+
while time.time() - start < duration:
|
|
74
|
+
spinner = SPINNER_FRAMES[i % frames]
|
|
75
|
+
sys.stdout.write(f"\r {spinner} {message}")
|
|
76
|
+
sys.stdout.flush()
|
|
77
|
+
time.sleep(0.1)
|
|
78
|
+
i += 1
|
|
79
|
+
sys.stdout.write("\r" + " " * (len(message) + 10) + "\r")
|
|
80
|
+
sys.stdout.flush()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def reveal_lines(lines: list[tuple[str, str]], delay: float = 0.15) -> None:
|
|
84
|
+
"""Reveal lines one by one with a fade-in effect."""
|
|
85
|
+
for text, style in lines:
|
|
86
|
+
if style:
|
|
87
|
+
console.print(text, style=style)
|
|
88
|
+
else:
|
|
89
|
+
console.print(text)
|
|
90
|
+
time.sleep(delay)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def animated_progress_bar(steps: int, current: int, width: int = 30) -> str:
|
|
94
|
+
"""Create an animated-style progress bar."""
|
|
95
|
+
filled = int(width * current / steps)
|
|
96
|
+
empty = width - filled
|
|
97
|
+
bar = f"{'█' * filled}{'░' * empty}"
|
|
98
|
+
return bar
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def pulse_text(text: str, cycles: int = 3) -> None:
|
|
102
|
+
"""Pulse text brightness."""
|
|
103
|
+
styles = [Colors.DIM, Colors.MUTED, Colors.PRIMARY, Colors.MUTED, Colors.DIM]
|
|
104
|
+
for _ in range(cycles):
|
|
105
|
+
for style in styles:
|
|
106
|
+
sys.stdout.write(f"\r [{style}]{text}[/{style}]")
|
|
107
|
+
sys.stdout.flush()
|
|
108
|
+
time.sleep(0.08)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
# First Run Detection
|
|
113
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def is_first_run() -> bool:
|
|
116
|
+
"""Check if this is the first time running emdash."""
|
|
117
|
+
emdash_dir = Path.home() / ".emdash"
|
|
118
|
+
markers = [
|
|
119
|
+
emdash_dir / "cli_history",
|
|
120
|
+
emdash_dir / "config.json",
|
|
121
|
+
emdash_dir / "sessions",
|
|
122
|
+
]
|
|
123
|
+
return not any(m.exists() for m in markers)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
# Animated Welcome Screen
|
|
128
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
def show_welcome_screen() -> bool:
|
|
131
|
+
"""Show clean welcome screen.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if user wants to proceed with onboarding, False to skip.
|
|
135
|
+
"""
|
|
136
|
+
console.print()
|
|
137
|
+
|
|
138
|
+
# Simple, clean header
|
|
139
|
+
console.print(f"[{Colors.MUTED}]{header('emdash', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
140
|
+
console.print()
|
|
141
|
+
console.print(f" [{Colors.PRIMARY} bold]Welcome[/{Colors.PRIMARY} bold]")
|
|
142
|
+
console.print()
|
|
143
|
+
console.print(f" [{Colors.DIM}]Let's get you set up.[/{Colors.DIM}]")
|
|
144
|
+
console.print()
|
|
145
|
+
|
|
146
|
+
# Clean step list
|
|
147
|
+
console.print(f" [{Colors.MUTED}]{DOT_WAITING}[/{Colors.MUTED}] Connect to GitHub [{Colors.DIM}](optional)[/{Colors.DIM}]")
|
|
148
|
+
console.print(f" [{Colors.MUTED}]{DOT_WAITING}[/{Colors.MUTED}] Create your first rule")
|
|
149
|
+
console.print(f" [{Colors.MUTED}]{DOT_WAITING}[/{Colors.MUTED}] Start building")
|
|
150
|
+
console.print()
|
|
151
|
+
|
|
152
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
153
|
+
console.print()
|
|
154
|
+
|
|
155
|
+
# Prompt
|
|
156
|
+
console.print(f" [{Colors.DIM}]Enter to begin · s to skip[/{Colors.DIM}]")
|
|
157
|
+
console.print()
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
session = PromptSession()
|
|
161
|
+
response = session.prompt(f" {ARROW_PROMPT} ").strip().lower()
|
|
162
|
+
return response != 's'
|
|
163
|
+
except (KeyboardInterrupt, EOFError):
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
# Step Headers
|
|
169
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
def show_step_header(step: int, total: int, title: str) -> None:
|
|
172
|
+
"""Show step header with progress indicator."""
|
|
173
|
+
console.print()
|
|
174
|
+
console.print(f"[{Colors.MUTED}]{header(title, SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
175
|
+
console.print(f" [{Colors.DIM}]step {step} of {total}[/{Colors.DIM}]")
|
|
176
|
+
console.print()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def show_step_complete(message: str) -> None:
|
|
180
|
+
"""Show step completion."""
|
|
181
|
+
console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] {message}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
# Step 1: GitHub Authentication
|
|
186
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
def step_github_auth() -> bool:
|
|
189
|
+
"""Step 1: GitHub authentication (optional).
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if completed or skipped, False if cancelled.
|
|
193
|
+
"""
|
|
194
|
+
show_step_header(1, 3, "Connect GitHub")
|
|
195
|
+
|
|
196
|
+
console.print(f" [{Colors.PRIMARY}]GitHub connection enables:[/{Colors.PRIMARY}]")
|
|
197
|
+
console.print()
|
|
198
|
+
console.print(f" [{Colors.MUTED}]{DOT_BULLET} PR reviews and creation[/{Colors.MUTED}]")
|
|
199
|
+
console.print(f" [{Colors.MUTED}]{DOT_BULLET} Issue management[/{Colors.MUTED}]")
|
|
200
|
+
console.print(f" [{Colors.MUTED}]{DOT_BULLET} Repository insights[/{Colors.MUTED}]")
|
|
201
|
+
console.print()
|
|
202
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
203
|
+
console.print()
|
|
204
|
+
|
|
205
|
+
# Interactive menu
|
|
206
|
+
selected_index = [0]
|
|
207
|
+
result = [None]
|
|
208
|
+
|
|
209
|
+
options = [
|
|
210
|
+
("connect", "Connect GitHub account", "Opens browser for OAuth"),
|
|
211
|
+
("skip", "Skip for now", "Can connect later with /auth"),
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
kb = KeyBindings()
|
|
215
|
+
|
|
216
|
+
@kb.add("up")
|
|
217
|
+
@kb.add("k")
|
|
218
|
+
def move_up(event):
|
|
219
|
+
selected_index[0] = (selected_index[0] - 1) % len(options)
|
|
220
|
+
|
|
221
|
+
@kb.add("down")
|
|
222
|
+
@kb.add("j")
|
|
223
|
+
def move_down(event):
|
|
224
|
+
selected_index[0] = (selected_index[0] + 1) % len(options)
|
|
225
|
+
|
|
226
|
+
@kb.add("enter")
|
|
227
|
+
def select(event):
|
|
228
|
+
result[0] = options[selected_index[0]][0]
|
|
229
|
+
event.app.exit()
|
|
230
|
+
|
|
231
|
+
@kb.add("c")
|
|
232
|
+
def connect(event):
|
|
233
|
+
result[0] = "connect"
|
|
234
|
+
event.app.exit()
|
|
235
|
+
|
|
236
|
+
@kb.add("s")
|
|
237
|
+
def skip(event):
|
|
238
|
+
result[0] = "skip"
|
|
239
|
+
event.app.exit()
|
|
240
|
+
|
|
241
|
+
@kb.add("c-c")
|
|
242
|
+
@kb.add("escape")
|
|
243
|
+
def cancel(event):
|
|
244
|
+
result[0] = "cancel"
|
|
245
|
+
event.app.exit()
|
|
246
|
+
|
|
247
|
+
def get_formatted_options():
|
|
248
|
+
lines = []
|
|
249
|
+
for i, (key, desc, hint) in enumerate(options):
|
|
250
|
+
indicator = STATUS_ACTIVE if i == selected_index[0] else STATUS_INACTIVE
|
|
251
|
+
if i == selected_index[0]:
|
|
252
|
+
lines.append(("class:selected", f" {indicator} {desc}\n"))
|
|
253
|
+
lines.append(("class:hint-selected", f" {hint}\n"))
|
|
254
|
+
else:
|
|
255
|
+
lines.append(("class:option", f" {indicator} {desc}\n"))
|
|
256
|
+
lines.append(("class:hint-dim", f" {hint}\n"))
|
|
257
|
+
lines.append(("class:hint", f"\n{ARROW_PROMPT} c connect s skip Esc cancel"))
|
|
258
|
+
return lines
|
|
259
|
+
|
|
260
|
+
style = Style.from_dict({
|
|
261
|
+
"selected": f"{Colors.SUCCESS} bold",
|
|
262
|
+
"hint-selected": Colors.SUCCESS,
|
|
263
|
+
"option": Colors.MUTED,
|
|
264
|
+
"hint-dim": Colors.DIM,
|
|
265
|
+
"hint": f"{Colors.DIM} italic",
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
layout = Layout(
|
|
269
|
+
HSplit([
|
|
270
|
+
Window(
|
|
271
|
+
FormattedTextControl(get_formatted_options),
|
|
272
|
+
height=7,
|
|
273
|
+
),
|
|
274
|
+
])
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
app = Application(
|
|
278
|
+
layout=layout,
|
|
279
|
+
key_bindings=kb,
|
|
280
|
+
style=style,
|
|
281
|
+
full_screen=False,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
app.run()
|
|
286
|
+
except (KeyboardInterrupt, EOFError):
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
if result[0] == "cancel":
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
if result[0] == "connect":
|
|
293
|
+
from ..auth import auth_login
|
|
294
|
+
console.print()
|
|
295
|
+
try:
|
|
296
|
+
auth_login(no_browser=False)
|
|
297
|
+
console.print()
|
|
298
|
+
show_step_complete("GitHub connected successfully")
|
|
299
|
+
except Exception as e:
|
|
300
|
+
console.print(f" [{Colors.ERROR}]{STATUS_ERROR}[/{Colors.ERROR}] Connection failed: {e}")
|
|
301
|
+
console.print(f" [{Colors.DIM}]You can try again later with /auth login[/{Colors.DIM}]")
|
|
302
|
+
else:
|
|
303
|
+
console.print(f" [{Colors.MUTED}]{DOT_BULLET} Skipped {ARROW_RIGHT} run /auth login anytime[/{Colors.MUTED}]")
|
|
304
|
+
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
309
|
+
# Step 2: Create Rule
|
|
310
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
def step_create_rule() -> bool:
|
|
313
|
+
"""Step 2: Create first rule (optional).
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
True if completed or skipped, False if cancelled.
|
|
317
|
+
"""
|
|
318
|
+
show_step_header(2, 3, "Create a Rule")
|
|
319
|
+
|
|
320
|
+
console.print(f" [{Colors.PRIMARY}]Rules guide the agent's behavior:[/{Colors.PRIMARY}]")
|
|
321
|
+
console.print()
|
|
322
|
+
console.print(f" [{Colors.MUTED}]{DOT_BULLET} Coding style preferences[/{Colors.MUTED}]")
|
|
323
|
+
console.print(f" [{Colors.MUTED}]{DOT_BULLET} Project conventions[/{Colors.MUTED}]")
|
|
324
|
+
console.print(f" [{Colors.MUTED}]{DOT_BULLET} Testing requirements[/{Colors.MUTED}]")
|
|
325
|
+
console.print()
|
|
326
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
327
|
+
console.print()
|
|
328
|
+
|
|
329
|
+
selected_index = [0]
|
|
330
|
+
result = [None]
|
|
331
|
+
|
|
332
|
+
options = [
|
|
333
|
+
("template", "Create from template", "Quick start with best practices"),
|
|
334
|
+
("custom", "Write custom rule", "Define your own guidelines"),
|
|
335
|
+
("skip", "Skip for now", "Can create later with /rules"),
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
kb = KeyBindings()
|
|
339
|
+
|
|
340
|
+
@kb.add("up")
|
|
341
|
+
@kb.add("k")
|
|
342
|
+
def move_up(event):
|
|
343
|
+
selected_index[0] = (selected_index[0] - 1) % len(options)
|
|
344
|
+
|
|
345
|
+
@kb.add("down")
|
|
346
|
+
@kb.add("j")
|
|
347
|
+
def move_down(event):
|
|
348
|
+
selected_index[0] = (selected_index[0] + 1) % len(options)
|
|
349
|
+
|
|
350
|
+
@kb.add("enter")
|
|
351
|
+
def select(event):
|
|
352
|
+
result[0] = options[selected_index[0]][0]
|
|
353
|
+
event.app.exit()
|
|
354
|
+
|
|
355
|
+
@kb.add("s")
|
|
356
|
+
def skip(event):
|
|
357
|
+
result[0] = "skip"
|
|
358
|
+
event.app.exit()
|
|
359
|
+
|
|
360
|
+
@kb.add("c-c")
|
|
361
|
+
@kb.add("escape")
|
|
362
|
+
def cancel(event):
|
|
363
|
+
result[0] = "cancel"
|
|
364
|
+
event.app.exit()
|
|
365
|
+
|
|
366
|
+
def get_formatted_options():
|
|
367
|
+
lines = []
|
|
368
|
+
for i, (key, desc, hint) in enumerate(options):
|
|
369
|
+
indicator = STATUS_ACTIVE if i == selected_index[0] else STATUS_INACTIVE
|
|
370
|
+
if i == selected_index[0]:
|
|
371
|
+
lines.append(("class:selected", f" {indicator} {desc}\n"))
|
|
372
|
+
lines.append(("class:hint-selected", f" {hint}\n"))
|
|
373
|
+
else:
|
|
374
|
+
lines.append(("class:option", f" {indicator} {desc}\n"))
|
|
375
|
+
lines.append(("class:hint-dim", f" {hint}\n"))
|
|
376
|
+
lines.append(("class:hint", f"\n{ARROW_PROMPT} Enter select s skip Esc cancel"))
|
|
377
|
+
return lines
|
|
378
|
+
|
|
379
|
+
style = Style.from_dict({
|
|
380
|
+
"selected": f"{Colors.SUCCESS} bold",
|
|
381
|
+
"hint-selected": Colors.SUCCESS,
|
|
382
|
+
"option": Colors.MUTED,
|
|
383
|
+
"hint-dim": Colors.DIM,
|
|
384
|
+
"hint": f"{Colors.DIM} italic",
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
layout = Layout(
|
|
388
|
+
HSplit([
|
|
389
|
+
Window(
|
|
390
|
+
FormattedTextControl(get_formatted_options),
|
|
391
|
+
height=9,
|
|
392
|
+
),
|
|
393
|
+
])
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
app = Application(
|
|
397
|
+
layout=layout,
|
|
398
|
+
key_bindings=kb,
|
|
399
|
+
style=style,
|
|
400
|
+
full_screen=False,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
app.run()
|
|
405
|
+
except (KeyboardInterrupt, EOFError):
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
if result[0] == "cancel":
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
if result[0] == "template":
|
|
412
|
+
console.print()
|
|
413
|
+
create_rule_from_template()
|
|
414
|
+
elif result[0] == "custom":
|
|
415
|
+
console.print()
|
|
416
|
+
create_custom_rule()
|
|
417
|
+
else:
|
|
418
|
+
console.print(f" [{Colors.MUTED}]{DOT_BULLET} Skipped {ARROW_RIGHT} run /rules anytime[/{Colors.MUTED}]")
|
|
419
|
+
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def create_rule_from_template() -> None:
|
|
424
|
+
"""Create a rule from a template."""
|
|
425
|
+
templates = [
|
|
426
|
+
("python", "Python best practices", "Type hints, docstrings, PEP 8"),
|
|
427
|
+
("typescript", "TypeScript standards", "Strict types, ESLint, modern syntax"),
|
|
428
|
+
("testing", "Testing requirements", "Tests for features, 80% coverage"),
|
|
429
|
+
("minimal", "Minimal rule", "Concise, focused responses"),
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
selected_index = [0]
|
|
433
|
+
result = [None]
|
|
434
|
+
|
|
435
|
+
kb = KeyBindings()
|
|
436
|
+
|
|
437
|
+
@kb.add("up")
|
|
438
|
+
@kb.add("k")
|
|
439
|
+
def move_up(event):
|
|
440
|
+
selected_index[0] = (selected_index[0] - 1) % len(templates)
|
|
441
|
+
|
|
442
|
+
@kb.add("down")
|
|
443
|
+
@kb.add("j")
|
|
444
|
+
def move_down(event):
|
|
445
|
+
selected_index[0] = (selected_index[0] + 1) % len(templates)
|
|
446
|
+
|
|
447
|
+
@kb.add("enter")
|
|
448
|
+
def select(event):
|
|
449
|
+
result[0] = templates[selected_index[0]]
|
|
450
|
+
event.app.exit()
|
|
451
|
+
|
|
452
|
+
@kb.add("c-c")
|
|
453
|
+
@kb.add("escape")
|
|
454
|
+
def cancel(event):
|
|
455
|
+
result[0] = None
|
|
456
|
+
event.app.exit()
|
|
457
|
+
|
|
458
|
+
def get_formatted_templates():
|
|
459
|
+
lines = [("class:title", " Select a template:\n\n")]
|
|
460
|
+
for i, (key, title, desc) in enumerate(templates):
|
|
461
|
+
indicator = STATUS_ACTIVE if i == selected_index[0] else STATUS_INACTIVE
|
|
462
|
+
if i == selected_index[0]:
|
|
463
|
+
lines.append(("class:selected", f" {indicator} {title}\n"))
|
|
464
|
+
lines.append(("class:selected-desc", f" {desc}\n"))
|
|
465
|
+
else:
|
|
466
|
+
lines.append(("class:option", f" {indicator} {title}\n"))
|
|
467
|
+
lines.append(("class:desc", f" {desc}\n"))
|
|
468
|
+
lines.append(("class:hint", f"\n{ARROW_PROMPT} ↑↓ select Enter confirm Esc cancel"))
|
|
469
|
+
return lines
|
|
470
|
+
|
|
471
|
+
style = Style.from_dict({
|
|
472
|
+
"title": f"{Colors.PRIMARY} bold",
|
|
473
|
+
"selected": f"{Colors.SUCCESS} bold",
|
|
474
|
+
"selected-desc": Colors.SUCCESS,
|
|
475
|
+
"option": Colors.MUTED,
|
|
476
|
+
"desc": Colors.DIM,
|
|
477
|
+
"hint": f"{Colors.DIM} italic",
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
layout = Layout(
|
|
481
|
+
HSplit([
|
|
482
|
+
Window(
|
|
483
|
+
FormattedTextControl(get_formatted_templates),
|
|
484
|
+
height=len(templates) * 2 + 4,
|
|
485
|
+
),
|
|
486
|
+
])
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
app = Application(
|
|
490
|
+
layout=layout,
|
|
491
|
+
key_bindings=kb,
|
|
492
|
+
style=style,
|
|
493
|
+
full_screen=False,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
app.run()
|
|
498
|
+
except (KeyboardInterrupt, EOFError):
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
if result[0]:
|
|
502
|
+
key, title, desc = result[0]
|
|
503
|
+
|
|
504
|
+
rules_dir = Path.cwd() / ".emdash" / "rules"
|
|
505
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
506
|
+
rule_file = rules_dir / f"{key}.md"
|
|
507
|
+
|
|
508
|
+
rule_content = f"""# {title}
|
|
509
|
+
|
|
510
|
+
{desc}
|
|
511
|
+
|
|
512
|
+
## Guidelines
|
|
513
|
+
|
|
514
|
+
- Follow project conventions
|
|
515
|
+
- Write clean, maintainable code
|
|
516
|
+
- Add appropriate documentation
|
|
517
|
+
"""
|
|
518
|
+
rule_file.write_text(rule_content)
|
|
519
|
+
show_step_complete(f"Created rule: {rule_file.relative_to(Path.cwd())}")
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def create_custom_rule() -> None:
|
|
523
|
+
"""Create a custom rule with user input."""
|
|
524
|
+
console.print(f" [{Colors.DIM}]Enter a name for your rule:[/{Colors.DIM}]")
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
session = PromptSession()
|
|
528
|
+
name = session.prompt(f" {ARROW_PROMPT} ").strip()
|
|
529
|
+
if not name:
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
name = name.lower().replace(" ", "-")
|
|
533
|
+
|
|
534
|
+
console.print()
|
|
535
|
+
console.print(f" [{Colors.DIM}]Describe the rule (one line):[/{Colors.DIM}]")
|
|
536
|
+
desc = session.prompt(f" {ARROW_PROMPT} ").strip()
|
|
537
|
+
|
|
538
|
+
rules_dir = Path.cwd() / ".emdash" / "rules"
|
|
539
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
540
|
+
rule_file = rules_dir / f"{name}.md"
|
|
541
|
+
|
|
542
|
+
rule_content = f"""# {name.replace("-", " ").title()}
|
|
543
|
+
|
|
544
|
+
{desc or "Custom rule"}
|
|
545
|
+
|
|
546
|
+
## Guidelines
|
|
547
|
+
|
|
548
|
+
- Add your specific guidelines here
|
|
549
|
+
"""
|
|
550
|
+
rule_file.write_text(rule_content)
|
|
551
|
+
show_step_complete(f"Created rule: {rule_file.relative_to(Path.cwd())}")
|
|
552
|
+
console.print(f" [{Colors.DIM}]Edit the file to add more details[/{Colors.DIM}]")
|
|
553
|
+
except (KeyboardInterrupt, EOFError):
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
558
|
+
# Step 3: Completion
|
|
559
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
def step_quick_command() -> bool:
|
|
562
|
+
"""Step 3: Show quick commands.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
True to complete onboarding.
|
|
566
|
+
"""
|
|
567
|
+
show_step_header(3, 3, "You're Ready")
|
|
568
|
+
|
|
569
|
+
console.print(f" [{Colors.SUCCESS}]Setup complete![/{Colors.SUCCESS}]")
|
|
570
|
+
console.print()
|
|
571
|
+
|
|
572
|
+
console.print(f" [{Colors.DIM}]Quick commands:[/{Colors.DIM}]")
|
|
573
|
+
console.print()
|
|
574
|
+
console.print(f" [{Colors.PRIMARY}]/help [/{Colors.PRIMARY}] [{Colors.DIM}]Show all commands[/{Colors.DIM}]")
|
|
575
|
+
console.print(f" [{Colors.PRIMARY}]/plan [/{Colors.PRIMARY}] [{Colors.DIM}]Switch to plan mode[/{Colors.DIM}]")
|
|
576
|
+
console.print(f" [{Colors.PRIMARY}]/agents [/{Colors.PRIMARY}] [{Colors.DIM}]Manage custom agents[/{Colors.DIM}]")
|
|
577
|
+
console.print(f" [{Colors.PRIMARY}]/rules [/{Colors.PRIMARY}] [{Colors.DIM}]Configure rules[/{Colors.DIM}]")
|
|
578
|
+
console.print()
|
|
579
|
+
|
|
580
|
+
console.print(f" [{Colors.MUTED}]Or just type your question to get started.[/{Colors.MUTED}]")
|
|
581
|
+
console.print()
|
|
582
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
583
|
+
console.print()
|
|
584
|
+
|
|
585
|
+
return True
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
589
|
+
# Main Onboarding Flow
|
|
590
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
def run_onboarding() -> bool:
|
|
593
|
+
"""Run the complete animated onboarding flow.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
True if onboarding completed, False if cancelled.
|
|
597
|
+
"""
|
|
598
|
+
if not show_welcome_screen():
|
|
599
|
+
console.print(f" [{Colors.DIM}]Skipped onboarding. Run /setup anytime.[/{Colors.DIM}]")
|
|
600
|
+
console.print()
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
# Step 1: GitHub auth
|
|
604
|
+
if not step_github_auth():
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
# Step 2: Create rule
|
|
608
|
+
if not step_create_rule():
|
|
609
|
+
return False
|
|
610
|
+
|
|
611
|
+
# Step 3: Quick commands
|
|
612
|
+
step_quick_command()
|
|
613
|
+
|
|
614
|
+
# Mark onboarding complete
|
|
615
|
+
emdash_dir = Path.home() / ".emdash"
|
|
616
|
+
emdash_dir.mkdir(parents=True, exist_ok=True)
|
|
617
|
+
(emdash_dir / ".onboarding_complete").touch()
|
|
618
|
+
|
|
619
|
+
return True
|