redo-cli 0.1.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.
- main.py +301 -0
- modules/__init__.py +1 -0
- modules/placeholders.py +134 -0
- modules/runner.py +278 -0
- modules/storage.py +515 -0
- modules/ui.py +433 -0
- redo_cli-0.1.0.dist-info/METADATA +231 -0
- redo_cli-0.1.0.dist-info/RECORD +11 -0
- redo_cli-0.1.0.dist-info/WHEEL +5 -0
- redo_cli-0.1.0.dist-info/entry_points.txt +2 -0
- redo_cli-0.1.0.dist-info/top_level.txt +2 -0
modules/ui.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
from rich import box
|
|
2
|
+
from rich.console import Console, Group
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.syntax import Syntax
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from rich.theme import Theme
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ASCII_BANNER = r"""
|
|
11
|
+
/$$$$$$$ /$$
|
|
12
|
+
| $$__ $$ | $$
|
|
13
|
+
| $$ \ $$ /$$$$$$ /$$$$$$$ /$$$$$$
|
|
14
|
+
| $$$$$$$/ /$$__ $$ /$$__ $$ /$$__ $$
|
|
15
|
+
| $$__ $$| $$$$$$$$| $$ | $$| $$ \ $$
|
|
16
|
+
| $$ \ $$| $$_____/| $$ | $$| $$ | $$
|
|
17
|
+
| $$ | $$| $$$$$$$| $$$$$$$| $$$$$$/
|
|
18
|
+
|__/ |__/ \_______/ \_______/ \______/
|
|
19
|
+
""".strip("\n")
|
|
20
|
+
|
|
21
|
+
theme = Theme(
|
|
22
|
+
{
|
|
23
|
+
"brand": "bold steel_blue",
|
|
24
|
+
"muted": "grey58",
|
|
25
|
+
"success": "green4",
|
|
26
|
+
"warning": "dark_orange3",
|
|
27
|
+
"error": "red3",
|
|
28
|
+
"number": "grey82",
|
|
29
|
+
"command": "bright_white",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
console = Console(theme=theme, highlight=False)
|
|
33
|
+
|
|
34
|
+
BRAND = "bold steel_blue"
|
|
35
|
+
MUTED = "grey58"
|
|
36
|
+
SUCCESS = "green4"
|
|
37
|
+
WARNING = "dark_orange3"
|
|
38
|
+
ERROR = "red3"
|
|
39
|
+
NUMBER = "grey82"
|
|
40
|
+
PANEL_BORDER = "grey50"
|
|
41
|
+
TABLE_BORDER = "grey46"
|
|
42
|
+
BANNER_START = "#6f86a8"
|
|
43
|
+
BANNER_END = "#b39ddb"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _hex_to_rgb(value):
|
|
47
|
+
value = value.lstrip("#")
|
|
48
|
+
return tuple(int(value[index : index + 2], 16) for index in (0, 2, 4))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _rgb_to_hex(rgb):
|
|
52
|
+
return "#{:02x}{:02x}{:02x}".format(*rgb)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _blend(start, end, ratio):
|
|
56
|
+
return tuple(round(start[index] + (end[index] - start[index]) * ratio) for index in range(3))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _gradient_text(text, start_color=BANNER_START, end_color=BANNER_END):
|
|
60
|
+
start = _hex_to_rgb(start_color)
|
|
61
|
+
end = _hex_to_rgb(end_color)
|
|
62
|
+
lines = text.splitlines()
|
|
63
|
+
total = max(len(lines) - 1, 1)
|
|
64
|
+
output = Text()
|
|
65
|
+
|
|
66
|
+
for index, line in enumerate(lines):
|
|
67
|
+
color = _rgb_to_hex(_blend(start, end, index / total))
|
|
68
|
+
output.append(line, style=f"bold {color}")
|
|
69
|
+
if index != len(lines) - 1:
|
|
70
|
+
output.append("\n")
|
|
71
|
+
|
|
72
|
+
return output
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _status_line(label, message, style):
|
|
76
|
+
text = Text()
|
|
77
|
+
text.append(f"[{label}] ", style=style)
|
|
78
|
+
text.append(message)
|
|
79
|
+
console.print(text)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _plural(count, singular, plural=None):
|
|
83
|
+
if count == 1:
|
|
84
|
+
return f"{count} {singular}"
|
|
85
|
+
return f"{count} {plural or singular + 's'}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _format_seconds(seconds):
|
|
89
|
+
seconds = int(seconds)
|
|
90
|
+
minutes, remaining_seconds = divmod(seconds, 60)
|
|
91
|
+
hours, remaining_minutes = divmod(minutes, 60)
|
|
92
|
+
|
|
93
|
+
parts = []
|
|
94
|
+
if hours:
|
|
95
|
+
parts.append(f"{hours}h")
|
|
96
|
+
if remaining_minutes:
|
|
97
|
+
parts.append(f"{remaining_minutes}m")
|
|
98
|
+
if remaining_seconds or not parts:
|
|
99
|
+
parts.append(f"{remaining_seconds}s")
|
|
100
|
+
|
|
101
|
+
return " ".join(parts)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _metadata_table(rows):
|
|
105
|
+
table = Table.grid(padding=(0, 2))
|
|
106
|
+
table.add_column(style=MUTED, no_wrap=True)
|
|
107
|
+
table.add_column()
|
|
108
|
+
|
|
109
|
+
for label, value in rows:
|
|
110
|
+
table.add_row(label, str(value))
|
|
111
|
+
|
|
112
|
+
return table
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def print_success(message):
|
|
116
|
+
_status_line("SUCCESS", message, SUCCESS)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def print_error(message):
|
|
120
|
+
_status_line("ERROR", message, ERROR)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def print_warning(message):
|
|
124
|
+
_status_line("WARNING", message, WARNING)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def show_banner():
|
|
128
|
+
console.print(_gradient_text(ASCII_BANNER))
|
|
129
|
+
console.print(Text("Bookmarks for terminal workflows.", style=MUTED))
|
|
130
|
+
console.print(Text("Run redo --help for commands or redo --info for project details.", style=MUTED))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def show_info(version, credit):
|
|
134
|
+
metadata = _metadata_table(
|
|
135
|
+
[
|
|
136
|
+
("Version", version),
|
|
137
|
+
("Credit", credit),
|
|
138
|
+
("Storage", "redo path"),
|
|
139
|
+
("Guide", "redo guide"),
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
console.print(_gradient_text(ASCII_BANNER))
|
|
144
|
+
console.print(
|
|
145
|
+
Panel(
|
|
146
|
+
metadata,
|
|
147
|
+
title="Redo info",
|
|
148
|
+
border_style=PANEL_BORDER,
|
|
149
|
+
box=box.ROUNDED,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def show_help_menu(version):
|
|
155
|
+
console.print(_gradient_text(ASCII_BANNER))
|
|
156
|
+
console.print(Text("Bookmarks for terminal workflows.", style=MUTED))
|
|
157
|
+
|
|
158
|
+
daily = Table(
|
|
159
|
+
title="Daily workflow",
|
|
160
|
+
box=box.ROUNDED,
|
|
161
|
+
border_style=TABLE_BORDER,
|
|
162
|
+
header_style=BRAND,
|
|
163
|
+
)
|
|
164
|
+
daily.add_column("Command", no_wrap=True)
|
|
165
|
+
daily.add_column("Purpose")
|
|
166
|
+
daily.add_row("redo new <name>", "Save a reusable workflow")
|
|
167
|
+
daily.add_row("redo run <name>", "Run a saved workflow")
|
|
168
|
+
daily.add_row("redo run <name> --dry", "Preview commands without executing")
|
|
169
|
+
daily.add_row("redo list", "See every saved workflow")
|
|
170
|
+
daily.add_row("redo show <name>", "Inspect commands and run count")
|
|
171
|
+
|
|
172
|
+
utilities = Table(
|
|
173
|
+
title="Utilities",
|
|
174
|
+
box=box.ROUNDED,
|
|
175
|
+
border_style=TABLE_BORDER,
|
|
176
|
+
header_style=BRAND,
|
|
177
|
+
)
|
|
178
|
+
utilities.add_column("Command", no_wrap=True)
|
|
179
|
+
utilities.add_column("Purpose")
|
|
180
|
+
utilities.add_row("redo guide", "Open the quick-start guide")
|
|
181
|
+
utilities.add_row("redo search <query>", "Find workflows by name, description, or command")
|
|
182
|
+
utilities.add_row("redo copy <source> <target>", "Duplicate a workflow")
|
|
183
|
+
utilities.add_row("redo rename <old> <new>", "Rename a workflow")
|
|
184
|
+
utilities.add_row("redo delete <name>", "Delete one workflow")
|
|
185
|
+
utilities.add_row("redo clearhistory", "Clear all saved workflows")
|
|
186
|
+
|
|
187
|
+
maintenance = Table(
|
|
188
|
+
title="Storage and maintenance",
|
|
189
|
+
box=box.ROUNDED,
|
|
190
|
+
border_style=TABLE_BORDER,
|
|
191
|
+
header_style=BRAND,
|
|
192
|
+
)
|
|
193
|
+
maintenance.add_column("Command", no_wrap=True)
|
|
194
|
+
maintenance.add_column("Purpose")
|
|
195
|
+
maintenance.add_row("redo path", "Show workflow storage location")
|
|
196
|
+
maintenance.add_row("redo doctor", "Check storage and risky saved commands")
|
|
197
|
+
maintenance.add_row("redo autofix", "Repair common storage problems")
|
|
198
|
+
maintenance.add_row("redo export <file>", "Back up workflows")
|
|
199
|
+
maintenance.add_row("redo import <file>", "Import workflows")
|
|
200
|
+
maintenance.add_row("redo --info", f"Show version {version} and credits")
|
|
201
|
+
|
|
202
|
+
console.print(
|
|
203
|
+
Panel(
|
|
204
|
+
Text("Redo command center", style=BRAND),
|
|
205
|
+
border_style=PANEL_BORDER,
|
|
206
|
+
box=box.ROUNDED,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
console.print(daily)
|
|
210
|
+
console.print(utilities)
|
|
211
|
+
console.print(maintenance)
|
|
212
|
+
console.print(Text("Tip: placeholders look like {message} and are filled when you run a workflow.", style=MUTED))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def show_guide():
|
|
216
|
+
intro = Text.assemble(
|
|
217
|
+
("Redo guide\n", BRAND),
|
|
218
|
+
("Save repeated terminal workflows once. Run them again with one command.", MUTED),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
basics = Table.grid(padding=(0, 2))
|
|
222
|
+
basics.add_column(style=MUTED, no_wrap=True)
|
|
223
|
+
basics.add_column()
|
|
224
|
+
basics.add_row("Create", "redo new ship")
|
|
225
|
+
basics.add_row("Run", "redo run ship")
|
|
226
|
+
basics.add_row("Preview", "redo run ship --dry")
|
|
227
|
+
basics.add_row("Inspect", "redo list | redo show ship")
|
|
228
|
+
|
|
229
|
+
example = Syntax(
|
|
230
|
+
'Description: Commit and push code\n'
|
|
231
|
+
'Command: git add .\n'
|
|
232
|
+
'Command: git commit -m "{message}"\n'
|
|
233
|
+
'Command: git push\n'
|
|
234
|
+
'Command: :done',
|
|
235
|
+
"text",
|
|
236
|
+
theme="ansi_dark",
|
|
237
|
+
word_wrap=True,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
placeholders = Table(
|
|
241
|
+
title="Placeholders",
|
|
242
|
+
box=box.ROUNDED,
|
|
243
|
+
border_style=TABLE_BORDER,
|
|
244
|
+
header_style=BRAND,
|
|
245
|
+
)
|
|
246
|
+
placeholders.add_column("Pattern", no_wrap=True)
|
|
247
|
+
placeholders.add_column("What happens")
|
|
248
|
+
placeholders.add_row("{message}", "Redo asks once, then inserts the value everywhere.")
|
|
249
|
+
placeholders.add_row("{project_name}", "Names must use letters, numbers, and underscores.")
|
|
250
|
+
|
|
251
|
+
warnings = Table(
|
|
252
|
+
title="Warnings",
|
|
253
|
+
box=box.ROUNDED,
|
|
254
|
+
border_style=TABLE_BORDER,
|
|
255
|
+
header_style=BRAND,
|
|
256
|
+
)
|
|
257
|
+
warnings.add_column("Tip", no_wrap=True)
|
|
258
|
+
warnings.add_column("Why it matters")
|
|
259
|
+
warnings.add_row("One command per prompt", "Do not separate commands with commas.")
|
|
260
|
+
warnings.add_row('Use git commit -m "{message}"', "Git needs -m for commit messages.")
|
|
261
|
+
warnings.add_row("Use --dry first", "Preview before running risky workflows.")
|
|
262
|
+
|
|
263
|
+
console.print(Panel(intro, border_style=PANEL_BORDER, box=box.ROUNDED))
|
|
264
|
+
console.print(Panel(basics, title="Core commands", border_style=PANEL_BORDER, box=box.ROUNDED))
|
|
265
|
+
console.print(Panel(example, title="Example workflow", border_style=PANEL_BORDER, box=box.ROUNDED))
|
|
266
|
+
console.print(placeholders)
|
|
267
|
+
console.print(warnings)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def show_workflows_table(workflows):
|
|
271
|
+
if not workflows:
|
|
272
|
+
console.print(
|
|
273
|
+
Panel(
|
|
274
|
+
Text("No workflows saved yet.", style=MUTED),
|
|
275
|
+
title="Redo workflows",
|
|
276
|
+
border_style=PANEL_BORDER,
|
|
277
|
+
box=box.ROUNDED,
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
table = Table(
|
|
283
|
+
title="Redo workflows",
|
|
284
|
+
box=box.ROUNDED,
|
|
285
|
+
border_style=TABLE_BORDER,
|
|
286
|
+
header_style=BRAND,
|
|
287
|
+
show_lines=False,
|
|
288
|
+
)
|
|
289
|
+
table.add_column("Name", style="bold", no_wrap=True)
|
|
290
|
+
table.add_column("Description", overflow="fold")
|
|
291
|
+
table.add_column("Commands", justify="right", no_wrap=True)
|
|
292
|
+
table.add_column("Runs", justify="right", style=NUMBER, no_wrap=True)
|
|
293
|
+
|
|
294
|
+
for name, workflow in sorted(workflows.items()):
|
|
295
|
+
command_count = len(workflow.get("commands", []))
|
|
296
|
+
table.add_row(
|
|
297
|
+
name,
|
|
298
|
+
workflow.get("description", "") or "-",
|
|
299
|
+
_plural(command_count, "command"),
|
|
300
|
+
str(workflow.get("runs", 0)),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
console.print(table)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def show_workflow_details(name, workflow):
|
|
307
|
+
commands = workflow.get("commands", [])
|
|
308
|
+
metadata = _metadata_table(
|
|
309
|
+
[
|
|
310
|
+
("Description", workflow.get("description", "") or "-"),
|
|
311
|
+
("Commands", _plural(len(commands), "command")),
|
|
312
|
+
("Runs", workflow.get("runs", 0)),
|
|
313
|
+
]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
console.print(
|
|
317
|
+
Panel(
|
|
318
|
+
metadata,
|
|
319
|
+
title=f"Workflow: {name}",
|
|
320
|
+
border_style=PANEL_BORDER,
|
|
321
|
+
box=box.ROUNDED,
|
|
322
|
+
expand=False,
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
show_commands(commands)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def show_commands(commands):
|
|
329
|
+
if not commands:
|
|
330
|
+
console.print(
|
|
331
|
+
Panel(
|
|
332
|
+
Text("No commands in this workflow.", style=MUTED),
|
|
333
|
+
title="Commands",
|
|
334
|
+
border_style=PANEL_BORDER,
|
|
335
|
+
box=box.ROUNDED,
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
table = Table(
|
|
341
|
+
title="Commands",
|
|
342
|
+
box=box.SIMPLE_HEAVY,
|
|
343
|
+
header_style=BRAND,
|
|
344
|
+
show_edge=False,
|
|
345
|
+
)
|
|
346
|
+
table.add_column("#", justify="right", style=MUTED, no_wrap=True)
|
|
347
|
+
table.add_column("Command")
|
|
348
|
+
|
|
349
|
+
for index, command in enumerate(commands, start=1):
|
|
350
|
+
syntax = Syntax(command, "bash", theme="ansi_dark", word_wrap=True)
|
|
351
|
+
table.add_row(str(index), syntax)
|
|
352
|
+
|
|
353
|
+
console.print(table)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def show_stats(workflows):
|
|
357
|
+
total_workflows = len(workflows)
|
|
358
|
+
total_runs = sum(int(workflow.get("runs", 0)) for workflow in workflows.values())
|
|
359
|
+
total_commands = sum(len(workflow.get("commands", [])) for workflow in workflows.values())
|
|
360
|
+
total_commands_run = sum(
|
|
361
|
+
len(workflow.get("commands", [])) * int(workflow.get("runs", 0))
|
|
362
|
+
for workflow in workflows.values()
|
|
363
|
+
)
|
|
364
|
+
estimated_seconds_saved = total_commands_run * 5
|
|
365
|
+
most_used = "-"
|
|
366
|
+
|
|
367
|
+
if workflows:
|
|
368
|
+
most_used_name, most_used_workflow = max(
|
|
369
|
+
workflows.items(),
|
|
370
|
+
key=lambda item: int(item[1].get("runs", 0)),
|
|
371
|
+
)
|
|
372
|
+
most_used = f"{most_used_name} ({_plural(int(most_used_workflow.get('runs', 0)), 'run')})"
|
|
373
|
+
|
|
374
|
+
summary = _metadata_table(
|
|
375
|
+
[
|
|
376
|
+
("Total workflows", total_workflows),
|
|
377
|
+
("Saved commands", total_commands),
|
|
378
|
+
("Total runs", total_runs),
|
|
379
|
+
("Most used workflow", most_used),
|
|
380
|
+
("Estimated time saved", _format_seconds(estimated_seconds_saved)),
|
|
381
|
+
]
|
|
382
|
+
)
|
|
383
|
+
console.print(
|
|
384
|
+
Panel(
|
|
385
|
+
summary,
|
|
386
|
+
title="Redo stats",
|
|
387
|
+
border_style=PANEL_BORDER,
|
|
388
|
+
box=box.ROUNDED,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def show_doctor_report(report):
|
|
394
|
+
has_warnings = report["dangerous_count"] > 0 or not report["exists"]
|
|
395
|
+
health_text = "Needs attention" if has_warnings else "Ready"
|
|
396
|
+
health_style = WARNING if has_warnings else SUCCESS
|
|
397
|
+
|
|
398
|
+
headline = Text.assemble(
|
|
399
|
+
("Status: ", MUTED),
|
|
400
|
+
(health_text, health_style),
|
|
401
|
+
)
|
|
402
|
+
checks = _metadata_table(
|
|
403
|
+
[
|
|
404
|
+
("Workflow file", report["path"]),
|
|
405
|
+
("File exists", "yes" if report["exists"] else "no"),
|
|
406
|
+
("Total workflows", report["total_workflows"]),
|
|
407
|
+
("Total commands", report["total_commands"]),
|
|
408
|
+
("Placeholder fields", report["placeholder_count"]),
|
|
409
|
+
("Dangerous commands", report["dangerous_count"]),
|
|
410
|
+
]
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
console.print(
|
|
414
|
+
Panel(
|
|
415
|
+
Group(headline, "", checks),
|
|
416
|
+
title="Health check",
|
|
417
|
+
border_style=health_style,
|
|
418
|
+
box=box.ROUNDED,
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if report["dangerous_commands"]:
|
|
423
|
+
table = Table(
|
|
424
|
+
title="Dangerous commands",
|
|
425
|
+
box=box.ROUNDED,
|
|
426
|
+
border_style=WARNING,
|
|
427
|
+
header_style=WARNING,
|
|
428
|
+
)
|
|
429
|
+
table.add_column("Workflow", style="bold", no_wrap=True)
|
|
430
|
+
table.add_column("Command")
|
|
431
|
+
for name, command in report["dangerous_commands"]:
|
|
432
|
+
table.add_row(name, command)
|
|
433
|
+
console.print(table)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: redo-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bookmarks for terminal workflows.
|
|
5
|
+
Project-URL: Homepage, https://github.com/VibeSlayer-code/Redo
|
|
6
|
+
Project-URL: Repository, https://github.com/VibeSlayer-code/Redo
|
|
7
|
+
Project-URL: Issues, https://github.com/VibeSlayer-code/Redo/issues
|
|
8
|
+
Keywords: cli,terminal,workflow,automation,developer-tools
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Topic :: System :: Shells
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: typer>=0.12
|
|
24
|
+
Requires-Dist: rich>=13
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: twine; extra == "dev"
|
|
29
|
+
|
|
30
|
+
# Redo
|
|
31
|
+
|
|
32
|
+
Redo is a CLI tool that saves repeated terminal workflows and runs them again with one command. It is built for developers who are tired of retyping the same setup, build, deploy, and cleanup commands.
|
|
33
|
+
|
|
34
|
+
Think of it as bookmarks for terminal workflows.
|
|
35
|
+
|
|
36
|
+
## Why Redo Exists
|
|
37
|
+
|
|
38
|
+
Developers repeat the same command chains constantly: starting projects, running test suites, cleaning folders, pushing code, building apps, and following long README setup steps.
|
|
39
|
+
|
|
40
|
+
Redo lets you define those workflows once, then replay them whenever you need them. It supports smart placeholders, previews, safety checks for dangerous commands, and simple usage stats.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Install from PyPI:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install redo-cli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For local development, clone the project, create a virtual environment, and install dependencies:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python -m venv .venv
|
|
54
|
+
.venv\Scripts\activate
|
|
55
|
+
pip install -r requirements.txt
|
|
56
|
+
pip install -e .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
You can run Redo locally with either form:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python main.py --help
|
|
63
|
+
redo --help
|
|
64
|
+
redo --info
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Running `redo` with no command shows the Redo ASCII banner. Running `redo --info` shows the banner, version, storage path, and credit.
|
|
68
|
+
|
|
69
|
+
Redo stores its workflow data in:
|
|
70
|
+
|
|
71
|
+
```txt
|
|
72
|
+
%APPDATA%/Redo/workflows.json on Windows, or ~/.redo/workflows.json when APPDATA is unavailable
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Set `REDO_DATA_DIR` to override the storage directory. Run `redo path` to print the exact file Redo is using.
|
|
76
|
+
|
|
77
|
+
Run `redo init` to create the folder and file explicitly, or let Redo create them the first time it needs storage.
|
|
78
|
+
|
|
79
|
+
The first time you run `redo new`, Redo offers to show a quick guide. You can open that guide anytime with:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
redo guide
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
Create a workflow:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
redo new ship
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Enter commands one by one:
|
|
94
|
+
|
|
95
|
+
```txt
|
|
96
|
+
Description: Commit and push code
|
|
97
|
+
Command: git add .
|
|
98
|
+
Command: git commit -m "{message}"
|
|
99
|
+
Command: git push
|
|
100
|
+
Command: :done
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Run it later:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
redo run ship
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Redo shows a live status table while commands run. Successful command output stays quiet by default; if a command fails, Redo stops the workflow and shows a focused error panel with the captured output.
|
|
110
|
+
|
|
111
|
+
Preview it without executing commands:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
redo run ship --dry
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Commands
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
redo init
|
|
121
|
+
redo new <name>
|
|
122
|
+
redo list
|
|
123
|
+
redo show <name>
|
|
124
|
+
redo run <name>
|
|
125
|
+
redo run <name> --dry
|
|
126
|
+
redo delete <name>
|
|
127
|
+
redo clearhistory
|
|
128
|
+
redo stats
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Developer QoL commands:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
redo search <query>
|
|
135
|
+
redo copy <source> <target>
|
|
136
|
+
redo rename <old-name> <new-name>
|
|
137
|
+
redo path
|
|
138
|
+
redo export workflows-backup.json
|
|
139
|
+
redo import workflows-backup.json
|
|
140
|
+
redo import workflows-backup.json --replace
|
|
141
|
+
redo doctor
|
|
142
|
+
redo autofix
|
|
143
|
+
redo guide
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`redo doctor` checks the workflow file, counts saved commands/placeholders, and flags risky commands before they surprise you later.
|
|
147
|
+
|
|
148
|
+
`redo autofix` repairs common storage problems: missing files, blank files, malformed JSON, and workflow entries with missing fields. If JSON is malformed, Redo saves a non-overwriting `workflows.broken.json` backup next to the main file before resetting it.
|
|
149
|
+
|
|
150
|
+
`redo clearhistory` clears every saved workflow from the file shown by `redo path`. Use `redo clearhistory --yes` to skip the confirmation prompt.
|
|
151
|
+
|
|
152
|
+
## Placeholders
|
|
153
|
+
|
|
154
|
+
Use placeholders when part of a command changes each run:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
git commit -m "{message}"
|
|
158
|
+
npm create vite@latest {project_name}
|
|
159
|
+
cd {project_name}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Redo asks once for each unique placeholder, then replaces every occurrence across the workflow.
|
|
163
|
+
|
|
164
|
+
Valid placeholder names use letters, numbers, and underscores, and cannot start with a number:
|
|
165
|
+
|
|
166
|
+
```txt
|
|
167
|
+
{message}
|
|
168
|
+
{project_name}
|
|
169
|
+
{ticket_123}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Placeholder values are quoted before execution so prompt input is treated as one literal value instead of shell syntax. This prevents command separators, variable expansion, and globs from silently changing the command shape.
|
|
173
|
+
|
|
174
|
+
Workflow names cannot be blank or reuse Redo command names such as `run`, `new`, `delete`, or `stats`.
|
|
175
|
+
|
|
176
|
+
## Demo Workflow
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
redo new ship
|
|
180
|
+
redo list
|
|
181
|
+
redo show ship
|
|
182
|
+
redo run ship --dry
|
|
183
|
+
redo run ship
|
|
184
|
+
redo stats
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Example workflow data:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"ship": {
|
|
192
|
+
"description": "Commit and push code",
|
|
193
|
+
"commands": [
|
|
194
|
+
"git add .",
|
|
195
|
+
"git commit -m \"{message}\"",
|
|
196
|
+
"git push"
|
|
197
|
+
],
|
|
198
|
+
"runs": 0
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Safety
|
|
204
|
+
|
|
205
|
+
Redo detects risky commands before running them and asks for confirmation. Examples include:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
rm -rf
|
|
209
|
+
del /s
|
|
210
|
+
format
|
|
211
|
+
sudo
|
|
212
|
+
git reset --hard
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Git Push Tip
|
|
216
|
+
|
|
217
|
+
If Git says the current branch has no upstream branch, run the command Git suggests once:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
git push --set-upstream origin master
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
After that, a workflow containing `git push` can push normally.
|
|
224
|
+
|
|
225
|
+
## Roadmap
|
|
226
|
+
|
|
227
|
+
- Project-local and global workflow stores
|
|
228
|
+
- Tags and search
|
|
229
|
+
- Shell completion
|
|
230
|
+
- Workflow sharing through repository templates
|
|
231
|
+
- More detailed time-saved analytics
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
main.py,sha256=NVRu7YDsoqbRUGfNomRBSLdnjIrz7LH2nW4dp26iLss,9266
|
|
2
|
+
modules/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
3
|
+
modules/placeholders.py,sha256=m0Yvo89cb5IKIxT1O1qPnAdyyo9ynuB5fsUKFed2nkQ,3606
|
|
4
|
+
modules/runner.py,sha256=wihfKp4VMvJbn4t_B0J3bUvVO6Xu8gm3U_ZTiHP6Gag,7967
|
|
5
|
+
modules/storage.py,sha256=bg7fGNVsgIehOXo1i-bEl5ZGwkpG5AXZIbumnl5m0Yk,15132
|
|
6
|
+
modules/ui.py,sha256=tQi0JKDz8VD8BCpXw69zhtlW28I7LrVTSjFF9TwK9VQ,13221
|
|
7
|
+
redo_cli-0.1.0.dist-info/METADATA,sha256=EBk_oFqtG6HbtRsMs-kASsJkowbCG_SSM0P908lpvAQ,6186
|
|
8
|
+
redo_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
redo_cli-0.1.0.dist-info/entry_points.txt,sha256=g5fzs2F79fisWEEssiMcn6s-exrkvPme9CiYBPu7rqU,34
|
|
10
|
+
redo_cli-0.1.0.dist-info/top_level.txt,sha256=AEStq8Ul9X2lAyef8AHIv8n5McZyoG48rZkemhotqrg,13
|
|
11
|
+
redo_cli-0.1.0.dist-info/RECORD,,
|