mcli-framework 7.11.4__py3-none-any.whl → 7.12.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.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/__init__.py +160 -0
- mcli/__main__.py +14 -0
- mcli/app/__init__.py +23 -0
- mcli/app/commands_cmd.py +18 -823
- mcli/app/init_cmd.py +391 -0
- mcli/app/lock_cmd.py +288 -0
- mcli/app/main.py +37 -0
- mcli/app/model/__init__.py +0 -0
- mcli/app/store_cmd.py +448 -0
- mcli/app/video/__init__.py +5 -0
- mcli/chat/__init__.py +34 -0
- mcli/lib/__init__.py +0 -0
- mcli/lib/api/__init__.py +0 -0
- mcli/lib/auth/__init__.py +1 -0
- mcli/lib/config/__init__.py +1 -0
- mcli/lib/custom_commands.py +3 -3
- mcli/lib/erd/__init__.py +25 -0
- mcli/lib/files/__init__.py +0 -0
- mcli/lib/fs/__init__.py +1 -0
- mcli/lib/logger/__init__.py +3 -0
- mcli/lib/optional_deps.py +1 -3
- mcli/lib/performance/__init__.py +17 -0
- mcli/lib/pickles/__init__.py +1 -0
- mcli/lib/secrets/__init__.py +10 -0
- mcli/lib/shell/__init__.py +0 -0
- mcli/lib/toml/__init__.py +1 -0
- mcli/lib/watcher/__init__.py +0 -0
- mcli/ml/__init__.py +16 -0
- mcli/ml/api/__init__.py +30 -0
- mcli/ml/api/routers/__init__.py +27 -0
- mcli/ml/auth/__init__.py +41 -0
- mcli/ml/backtesting/__init__.py +33 -0
- mcli/ml/cli/__init__.py +5 -0
- mcli/ml/config/__init__.py +33 -0
- mcli/ml/configs/__init__.py +16 -0
- mcli/ml/dashboard/__init__.py +12 -0
- mcli/ml/dashboard/components/__init__.py +7 -0
- mcli/ml/dashboard/pages/__init__.py +6 -0
- mcli/ml/data_ingestion/__init__.py +29 -0
- mcli/ml/database/__init__.py +40 -0
- mcli/ml/experimentation/__init__.py +29 -0
- mcli/ml/features/__init__.py +39 -0
- mcli/ml/mlops/__init__.py +19 -0
- mcli/ml/models/__init__.py +90 -0
- mcli/ml/monitoring/__init__.py +25 -0
- mcli/ml/optimization/__init__.py +27 -0
- mcli/ml/predictions/__init__.py +5 -0
- mcli/ml/preprocessing/__init__.py +24 -0
- mcli/ml/scripts/__init__.py +1 -0
- mcli/ml/serving/__init__.py +1 -0
- mcli/ml/trading/__init__.py +63 -0
- mcli/ml/training/__init__.py +7 -0
- mcli/mygroup/__init__.py +3 -0
- mcli/public/__init__.py +1 -0
- mcli/public/commands/__init__.py +2 -0
- mcli/self/__init__.py +3 -0
- mcli/self/migrate_cmd.py +209 -76
- mcli/workflow/__init__.py +0 -0
- mcli/workflow/daemon/__init__.py +15 -0
- mcli/workflow/dashboard/__init__.py +5 -0
- mcli/workflow/docker/__init__.py +0 -0
- mcli/workflow/file/__init__.py +0 -0
- mcli/workflow/gcloud/__init__.py +1 -0
- mcli/workflow/git_commit/__init__.py +0 -0
- mcli/workflow/interview/__init__.py +0 -0
- mcli/workflow/notebook/__init__.py +16 -0
- mcli/workflow/registry/__init__.py +0 -0
- mcli/workflow/repo/__init__.py +0 -0
- mcli/workflow/scheduler/__init__.py +25 -0
- mcli/workflow/search/__init__.py +0 -0
- mcli/workflow/secrets/__init__.py +5 -0
- mcli/workflow/secrets/secrets_cmd.py +1 -2
- mcli/workflow/sync/__init__.py +5 -0
- mcli/workflow/videos/__init__.py +1 -0
- mcli/workflow/wakatime/__init__.py +80 -0
- {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/METADATA +10 -10
- {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/RECORD +81 -12
- {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/WHEEL +0 -0
- {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/top_level.txt +0 -0
mcli/app/commands_cmd.py
CHANGED
|
@@ -182,265 +182,7 @@ def workflow():
|
|
|
182
182
|
commands = workflow
|
|
183
183
|
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
@click.option(
|
|
187
|
-
"--global", "-g", "is_global", is_flag=True, help="Initialize global workflows directory instead of local"
|
|
188
|
-
)
|
|
189
|
-
@click.option(
|
|
190
|
-
"--git", is_flag=True, help="Initialize git repository in workflows directory"
|
|
191
|
-
)
|
|
192
|
-
@click.option(
|
|
193
|
-
"--force", "-f", is_flag=True, help="Force initialization even if directory exists"
|
|
194
|
-
)
|
|
195
|
-
def init_workflows(is_global, git, force):
|
|
196
|
-
"""
|
|
197
|
-
Initialize workflows directory structure.
|
|
198
|
-
|
|
199
|
-
Creates the necessary directories and configuration files for managing
|
|
200
|
-
custom workflows. By default, creates a local .mcli/workflows/ directory
|
|
201
|
-
if in a git repository, otherwise uses ~/.mcli/workflows/.
|
|
202
|
-
|
|
203
|
-
Examples:
|
|
204
|
-
mcli workflow init # Initialize local workflows (if in git repo)
|
|
205
|
-
mcli workflow init --global # Initialize global workflows
|
|
206
|
-
mcli workflow init --git # Also initialize git repository
|
|
207
|
-
"""
|
|
208
|
-
from mcli.lib.paths import (
|
|
209
|
-
get_mcli_home,
|
|
210
|
-
get_local_mcli_dir,
|
|
211
|
-
get_git_root,
|
|
212
|
-
is_git_repository,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# Determine if we're in a git repository
|
|
216
|
-
in_git_repo = is_git_repository() and not is_global
|
|
217
|
-
git_root = get_git_root() if in_git_repo else None
|
|
218
|
-
|
|
219
|
-
# Explicitly create workflows directory (not commands)
|
|
220
|
-
# This bypasses the migration logic that would check for old commands/ directory
|
|
221
|
-
if not is_global and in_git_repo:
|
|
222
|
-
local_mcli = get_local_mcli_dir()
|
|
223
|
-
workflows_dir = local_mcli / "workflows"
|
|
224
|
-
else:
|
|
225
|
-
workflows_dir = get_mcli_home() / "workflows"
|
|
226
|
-
|
|
227
|
-
lockfile_path = workflows_dir / "commands.lock.json"
|
|
228
|
-
|
|
229
|
-
# Check if already initialized
|
|
230
|
-
if workflows_dir.exists() and not force:
|
|
231
|
-
if lockfile_path.exists():
|
|
232
|
-
console.print(f"[yellow]Workflows directory already initialized at:[/yellow] {workflows_dir}")
|
|
233
|
-
console.print(f"[dim]Use --force to reinitialize[/dim]")
|
|
234
|
-
|
|
235
|
-
should_continue = Prompt.ask(
|
|
236
|
-
"Continue anyway?", choices=["y", "n"], default="n"
|
|
237
|
-
)
|
|
238
|
-
if should_continue.lower() != "y":
|
|
239
|
-
return 0
|
|
240
|
-
|
|
241
|
-
# Create workflows directory
|
|
242
|
-
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
243
|
-
console.print(f"[green]✓[/green] Created workflows directory: {workflows_dir}")
|
|
244
|
-
|
|
245
|
-
# Create README.md
|
|
246
|
-
readme_path = workflows_dir / "README.md"
|
|
247
|
-
if not readme_path.exists() or force:
|
|
248
|
-
scope = "local" if in_git_repo else "global"
|
|
249
|
-
scope_desc = f"for repository: {git_root.name}" if in_git_repo else "globally"
|
|
250
|
-
|
|
251
|
-
readme_content = f"""# MCLI Custom Workflows
|
|
252
|
-
|
|
253
|
-
This directory contains custom workflow commands {scope_desc}.
|
|
254
|
-
|
|
255
|
-
## Quick Start
|
|
256
|
-
|
|
257
|
-
### Create a New Workflow
|
|
258
|
-
|
|
259
|
-
```bash
|
|
260
|
-
# Python workflow
|
|
261
|
-
mcli workflow add my-workflow
|
|
262
|
-
|
|
263
|
-
# Shell workflow
|
|
264
|
-
mcli workflow add my-script --language shell
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### List Workflows
|
|
268
|
-
|
|
269
|
-
```bash
|
|
270
|
-
mcli workflow list --custom-only
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### Execute a Workflow
|
|
274
|
-
|
|
275
|
-
```bash
|
|
276
|
-
mcli workflows my-workflow
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Edit a Workflow
|
|
280
|
-
|
|
281
|
-
```bash
|
|
282
|
-
mcli workflow edit my-workflow
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Export/Import Workflows
|
|
286
|
-
|
|
287
|
-
```bash
|
|
288
|
-
# Export all workflows
|
|
289
|
-
mcli workflow export workflows-backup.json
|
|
290
|
-
|
|
291
|
-
# Import workflows
|
|
292
|
-
mcli workflow import workflows-backup.json
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## Directory Structure
|
|
296
|
-
|
|
297
|
-
```
|
|
298
|
-
{workflows_dir.name}/
|
|
299
|
-
├── README.md # This file
|
|
300
|
-
├── commands.lock.json # Lockfile for workflow state
|
|
301
|
-
└── *.json # Individual workflow definitions
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
## Workflow Format
|
|
305
|
-
|
|
306
|
-
Workflows are stored as JSON files with the following structure:
|
|
307
|
-
|
|
308
|
-
```json
|
|
309
|
-
{{
|
|
310
|
-
"name": "workflow-name",
|
|
311
|
-
"description": "Workflow description",
|
|
312
|
-
"code": "Python or shell code",
|
|
313
|
-
"language": "python",
|
|
314
|
-
"group": "workflow",
|
|
315
|
-
"version": "1.0",
|
|
316
|
-
"created_at": "2025-10-30T...",
|
|
317
|
-
"updated_at": "2025-10-30T..."
|
|
318
|
-
}}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
## Scope
|
|
322
|
-
|
|
323
|
-
- **Scope**: {'Local (repository-specific)' if in_git_repo else 'Global (user-wide)'}
|
|
324
|
-
- **Location**: `{workflows_dir}`
|
|
325
|
-
{f"- **Git Repository**: `{git_root}`" if git_root else ""}
|
|
326
|
-
|
|
327
|
-
## Documentation
|
|
328
|
-
|
|
329
|
-
- [MCLI Documentation](https://github.com/gwicho38/mcli)
|
|
330
|
-
- [Workflow Guide](https://github.com/gwicho38/mcli/blob/main/docs/features/LOCAL_VS_GLOBAL_COMMANDS.md)
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
*Initialized: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
|
|
335
|
-
"""
|
|
336
|
-
|
|
337
|
-
readme_path.write_text(readme_content)
|
|
338
|
-
console.print(f"[green]✓[/green] Created README: {readme_path.name}")
|
|
339
|
-
|
|
340
|
-
# Initialize lockfile
|
|
341
|
-
if not lockfile_path.exists() or force:
|
|
342
|
-
lockfile_data = {
|
|
343
|
-
"version": "1.0",
|
|
344
|
-
"initialized_at": datetime.now().isoformat(),
|
|
345
|
-
"scope": "local" if in_git_repo else "global",
|
|
346
|
-
"commands": {}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
with open(lockfile_path, "w") as f:
|
|
350
|
-
json.dump(lockfile_data, f, indent=2)
|
|
351
|
-
|
|
352
|
-
console.print(f"[green]✓[/green] Initialized lockfile: {lockfile_path.name}")
|
|
353
|
-
|
|
354
|
-
# Create .gitignore if in workflows directory
|
|
355
|
-
gitignore_path = workflows_dir / ".gitignore"
|
|
356
|
-
if not gitignore_path.exists() or force:
|
|
357
|
-
gitignore_content = """# Backup files
|
|
358
|
-
*.backup
|
|
359
|
-
*.bak
|
|
360
|
-
|
|
361
|
-
# Temporary files
|
|
362
|
-
*.tmp
|
|
363
|
-
*.temp
|
|
364
|
-
|
|
365
|
-
# OS files
|
|
366
|
-
.DS_Store
|
|
367
|
-
Thumbs.db
|
|
368
|
-
|
|
369
|
-
# Editor files
|
|
370
|
-
*.swp
|
|
371
|
-
*.swo
|
|
372
|
-
*~
|
|
373
|
-
.vscode/
|
|
374
|
-
.idea/
|
|
375
|
-
"""
|
|
376
|
-
gitignore_path.write_text(gitignore_content)
|
|
377
|
-
console.print(f"[green]✓[/green] Created .gitignore")
|
|
378
|
-
|
|
379
|
-
# Initialize git if requested
|
|
380
|
-
if git and not (workflows_dir / ".git").exists():
|
|
381
|
-
try:
|
|
382
|
-
subprocess.run(
|
|
383
|
-
["git", "init"],
|
|
384
|
-
cwd=workflows_dir,
|
|
385
|
-
check=True,
|
|
386
|
-
capture_output=True
|
|
387
|
-
)
|
|
388
|
-
console.print(f"[green]✓[/green] Initialized git repository in workflows directory")
|
|
389
|
-
|
|
390
|
-
# Create initial commit
|
|
391
|
-
subprocess.run(
|
|
392
|
-
["git", "add", "."],
|
|
393
|
-
cwd=workflows_dir,
|
|
394
|
-
check=True,
|
|
395
|
-
capture_output=True
|
|
396
|
-
)
|
|
397
|
-
subprocess.run(
|
|
398
|
-
["git", "commit", "-m", "Initial commit: Initialize workflows directory"],
|
|
399
|
-
cwd=workflows_dir,
|
|
400
|
-
check=True,
|
|
401
|
-
capture_output=True
|
|
402
|
-
)
|
|
403
|
-
console.print(f"[green]✓[/green] Created initial commit")
|
|
404
|
-
|
|
405
|
-
except subprocess.CalledProcessError as e:
|
|
406
|
-
console.print(f"[yellow]⚠[/yellow] Git initialization failed: {e}")
|
|
407
|
-
except FileNotFoundError:
|
|
408
|
-
console.print(f"[yellow]⚠[/yellow] Git not found. Skipping git initialization.")
|
|
409
|
-
|
|
410
|
-
# Summary
|
|
411
|
-
console.print()
|
|
412
|
-
console.print("[bold green]Workflows directory initialized successfully![/bold green]")
|
|
413
|
-
console.print()
|
|
414
|
-
|
|
415
|
-
# Display summary table
|
|
416
|
-
table = Table(title="Initialization Summary", show_header=False)
|
|
417
|
-
table.add_column("Property", style="cyan")
|
|
418
|
-
table.add_column("Value", style="green")
|
|
419
|
-
|
|
420
|
-
table.add_row("Scope", "Local (repository-specific)" if in_git_repo else "Global (user-wide)")
|
|
421
|
-
table.add_row("Location", str(workflows_dir))
|
|
422
|
-
if git_root:
|
|
423
|
-
table.add_row("Git Repository", str(git_root))
|
|
424
|
-
table.add_row("Lockfile", str(lockfile_path))
|
|
425
|
-
table.add_row("Git Initialized", "Yes" if git and (workflows_dir / ".git").exists() else "No")
|
|
426
|
-
|
|
427
|
-
console.print(table)
|
|
428
|
-
console.print()
|
|
429
|
-
|
|
430
|
-
# Next steps
|
|
431
|
-
console.print("[bold]Next Steps:[/bold]")
|
|
432
|
-
console.print(" 1. Create a workflow: [cyan]mcli workflow add my-workflow[/cyan]")
|
|
433
|
-
console.print(" 2. List workflows: [cyan]mcli workflow list --custom-only[/cyan]")
|
|
434
|
-
console.print(" 3. Execute workflow: [cyan]mcli workflows my-workflow[/cyan]")
|
|
435
|
-
console.print(" 4. View README: [cyan]cat {}/README.md[/cyan]".format(workflows_dir))
|
|
436
|
-
console.print()
|
|
437
|
-
|
|
438
|
-
if in_git_repo:
|
|
439
|
-
console.print(f"[dim]Tip: Workflows are local to this repository. Use --global for user-wide workflows.[/dim]")
|
|
440
|
-
else:
|
|
441
|
-
console.print(f"[dim]Tip: Use workflows in any git repository, or create local ones with 'mcli workflow init' inside repos.[/dim]")
|
|
442
|
-
|
|
443
|
-
return 0
|
|
185
|
+
# init command moved to init_cmd.py as top-level command
|
|
444
186
|
|
|
445
187
|
|
|
446
188
|
@workflow.command("list")
|
|
@@ -451,9 +193,15 @@ Thumbs.db
|
|
|
451
193
|
)
|
|
452
194
|
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
453
195
|
@click.option(
|
|
454
|
-
"--global",
|
|
196
|
+
"--global",
|
|
197
|
+
"-g",
|
|
198
|
+
"is_global",
|
|
199
|
+
is_flag=True,
|
|
200
|
+
help="Use global commands (~/.mcli/commands/) instead of local (.mcli/commands/)",
|
|
455
201
|
)
|
|
456
|
-
def list_commands(
|
|
202
|
+
def list_commands(
|
|
203
|
+
include_groups: bool, daemon_only: bool, custom_only: bool, as_json: bool, is_global: bool
|
|
204
|
+
):
|
|
457
205
|
"""
|
|
458
206
|
List all available commands.
|
|
459
207
|
|
|
@@ -985,7 +733,11 @@ logger = get_logger()
|
|
|
985
733
|
help="Shell type for shell commands (defaults to $SHELL)",
|
|
986
734
|
)
|
|
987
735
|
@click.option(
|
|
988
|
-
"--global",
|
|
736
|
+
"--global",
|
|
737
|
+
"-g",
|
|
738
|
+
"is_global",
|
|
739
|
+
is_flag=True,
|
|
740
|
+
help="Add to global commands (~/.mcli/commands/) instead of local (.mcli/commands/)",
|
|
989
741
|
)
|
|
990
742
|
def add_command(command_name, group, description, template, language, shell, is_global):
|
|
991
743
|
"""
|
|
@@ -1454,67 +1206,7 @@ def import_commands(source, script, overwrite, name, group, description, interac
|
|
|
1454
1206
|
return 0
|
|
1455
1207
|
|
|
1456
1208
|
|
|
1457
|
-
|
|
1458
|
-
@click.option(
|
|
1459
|
-
"--global", "-g", "is_global", is_flag=True, help="Verify global commands instead of local"
|
|
1460
|
-
)
|
|
1461
|
-
def verify_commands(is_global):
|
|
1462
|
-
"""
|
|
1463
|
-
Verify that custom commands match the lockfile.
|
|
1464
|
-
|
|
1465
|
-
By default verifies local commands (if in git repo), use --global/-g for global commands.
|
|
1466
|
-
"""
|
|
1467
|
-
manager = get_command_manager(global_mode=is_global)
|
|
1468
|
-
|
|
1469
|
-
# First, ensure lockfile is up to date
|
|
1470
|
-
manager.update_lockfile()
|
|
1471
|
-
|
|
1472
|
-
verification = manager.verify_lockfile()
|
|
1473
|
-
|
|
1474
|
-
if verification["valid"]:
|
|
1475
|
-
console.print("[green]All custom commands are in sync with the lockfile.[/green]")
|
|
1476
|
-
return 0
|
|
1477
|
-
|
|
1478
|
-
console.print("[yellow]Commands are out of sync with the lockfile:[/yellow]\n")
|
|
1479
|
-
|
|
1480
|
-
if verification["missing"]:
|
|
1481
|
-
console.print("Missing commands (in lockfile but not found):")
|
|
1482
|
-
for name in verification["missing"]:
|
|
1483
|
-
console.print(f" - {name}")
|
|
1484
|
-
|
|
1485
|
-
if verification["extra"]:
|
|
1486
|
-
console.print("\nExtra commands (not in lockfile):")
|
|
1487
|
-
for name in verification["extra"]:
|
|
1488
|
-
console.print(f" - {name}")
|
|
1489
|
-
|
|
1490
|
-
if verification["modified"]:
|
|
1491
|
-
console.print("\nModified commands:")
|
|
1492
|
-
for name in verification["modified"]:
|
|
1493
|
-
console.print(f" - {name}")
|
|
1494
|
-
|
|
1495
|
-
console.print("\n[dim]Run 'mcli commands update-lockfile' to sync the lockfile[/dim]")
|
|
1496
|
-
|
|
1497
|
-
return 1
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
@workflow.command("update-lockfile")
|
|
1501
|
-
@click.option(
|
|
1502
|
-
"--global", "-g", "is_global", is_flag=True, help="Update global lockfile instead of local"
|
|
1503
|
-
)
|
|
1504
|
-
def update_lockfile(is_global):
|
|
1505
|
-
"""
|
|
1506
|
-
Update the commands lockfile with current state.
|
|
1507
|
-
|
|
1508
|
-
By default updates local lockfile (if in git repo), use --global/-g for global lockfile.
|
|
1509
|
-
"""
|
|
1510
|
-
manager = get_command_manager(global_mode=is_global)
|
|
1511
|
-
|
|
1512
|
-
if manager.update_lockfile():
|
|
1513
|
-
console.print(f"[green]Updated lockfile: {manager.lockfile_path}[/green]")
|
|
1514
|
-
return 0
|
|
1515
|
-
else:
|
|
1516
|
-
console.print("[red]Failed to update lockfile.[/red]")
|
|
1517
|
-
return 1
|
|
1209
|
+
# verify and update-lockfile commands moved under lock group
|
|
1518
1210
|
|
|
1519
1211
|
|
|
1520
1212
|
@workflow.command("edit")
|
|
@@ -1616,506 +1308,9 @@ def edit_command(command_name, editor, is_global):
|
|
|
1616
1308
|
return 0
|
|
1617
1309
|
|
|
1618
1310
|
|
|
1619
|
-
#
|
|
1620
|
-
#
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
@workflow.group("state")
|
|
1624
|
-
def command_state():
|
|
1625
|
-
"""Manage command state lockfile and history."""
|
|
1626
|
-
pass
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
@command_state.command("list")
|
|
1630
|
-
def list_states():
|
|
1631
|
-
"""List all saved command states (hash, timestamp, #commands)."""
|
|
1632
|
-
states = load_lockfile()
|
|
1633
|
-
if not states:
|
|
1634
|
-
click.echo("No command states found.")
|
|
1635
|
-
return
|
|
1636
|
-
|
|
1637
|
-
table = Table(title="Command States")
|
|
1638
|
-
table.add_column("Hash", style="cyan")
|
|
1639
|
-
table.add_column("Timestamp", style="green")
|
|
1640
|
-
table.add_column("# Commands", style="yellow")
|
|
1641
|
-
|
|
1642
|
-
for state in states:
|
|
1643
|
-
table.add_row(state["hash"][:8], state["timestamp"], str(len(state["commands"])))
|
|
1644
|
-
|
|
1645
|
-
console.print(table)
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
@command_state.command("restore")
|
|
1649
|
-
@click.argument("hash_value")
|
|
1650
|
-
def restore_state(hash_value):
|
|
1651
|
-
"""Restore to a previous command state by hash."""
|
|
1652
|
-
if restore_command_state(hash_value):
|
|
1653
|
-
click.echo(f"Restored to state {hash_value[:8]}")
|
|
1654
|
-
else:
|
|
1655
|
-
click.echo(f"State {hash_value[:8]} not found.", err=True)
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
@command_state.command("write")
|
|
1659
|
-
@click.argument("json_file", required=False, type=click.Path(exists=False))
|
|
1660
|
-
def write_state(json_file):
|
|
1661
|
-
"""Write a new command state to the lockfile from a JSON file or the current app state."""
|
|
1662
|
-
import traceback
|
|
1663
|
-
|
|
1664
|
-
print("[DEBUG] write_state called")
|
|
1665
|
-
print(f"[DEBUG] LOCKFILE_PATH: {LOCKFILE_PATH}")
|
|
1666
|
-
try:
|
|
1667
|
-
if json_file:
|
|
1668
|
-
print(f"[DEBUG] Loading command state from file: {json_file}")
|
|
1669
|
-
with open(json_file, "r") as f:
|
|
1670
|
-
commands = json.load(f)
|
|
1671
|
-
click.echo(f"Loaded command state from {json_file}.")
|
|
1672
|
-
else:
|
|
1673
|
-
print("[DEBUG] Snapshotting current command state.")
|
|
1674
|
-
commands = get_current_command_state()
|
|
1675
|
-
|
|
1676
|
-
state_hash = hash_command_state(commands)
|
|
1677
|
-
new_state = {
|
|
1678
|
-
"hash": state_hash,
|
|
1679
|
-
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
1680
|
-
"commands": commands,
|
|
1681
|
-
}
|
|
1682
|
-
append_lockfile(new_state)
|
|
1683
|
-
print(f"[DEBUG] Wrote new command state {state_hash[:8]} to lockfile at {LOCKFILE_PATH}")
|
|
1684
|
-
click.echo(f"Wrote new command state {state_hash[:8]} to lockfile.")
|
|
1685
|
-
except Exception as e:
|
|
1686
|
-
print(f"[ERROR] Exception in write_state: {e}")
|
|
1687
|
-
print(traceback.format_exc())
|
|
1688
|
-
click.echo(f"[ERROR] Failed to write command state: {e}", err=True)
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
# Store management subgroup
|
|
1692
|
-
# Moved from mcli.self for better organization
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
def _get_store_path() -> Path:
|
|
1696
|
-
"""Get store path from config or default"""
|
|
1697
|
-
config_file = Path.home() / ".mcli" / "store.conf"
|
|
1698
|
-
|
|
1699
|
-
if config_file.exists():
|
|
1700
|
-
store_path = Path(config_file.read_text().strip())
|
|
1701
|
-
if store_path.exists():
|
|
1702
|
-
return store_path
|
|
1703
|
-
|
|
1704
|
-
# Use default
|
|
1705
|
-
return DEFAULT_STORE_PATH
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
@workflow.group("store")
|
|
1709
|
-
def store():
|
|
1710
|
-
"""Manage command store - sync ~/.mcli/commands/ to git"""
|
|
1711
|
-
pass
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
@store.command(name="init")
|
|
1715
|
-
@click.option("--path", "-p", type=click.Path(), help=f"Store path (default: {DEFAULT_STORE_PATH})")
|
|
1716
|
-
@click.option("--remote", "-r", help="Git remote URL (optional)")
|
|
1717
|
-
def init_store(path, remote):
|
|
1718
|
-
"""Initialize command store with git"""
|
|
1719
|
-
store_path = Path(path) if path else DEFAULT_STORE_PATH
|
|
1720
|
-
|
|
1721
|
-
try:
|
|
1722
|
-
# Create store directory
|
|
1723
|
-
store_path.mkdir(parents=True, exist_ok=True)
|
|
1724
|
-
|
|
1725
|
-
# Initialize git if not already initialized
|
|
1726
|
-
git_dir = store_path / ".git"
|
|
1727
|
-
if not git_dir.exists():
|
|
1728
|
-
subprocess.run(["git", "init"], cwd=store_path, check=True, capture_output=True)
|
|
1729
|
-
success(f"Initialized git repository at {store_path}")
|
|
1730
|
-
|
|
1731
|
-
# Create .gitignore
|
|
1732
|
-
gitignore = store_path / ".gitignore"
|
|
1733
|
-
gitignore.write_text("*.backup\n.DS_Store\n")
|
|
1734
|
-
|
|
1735
|
-
# Create README
|
|
1736
|
-
readme = store_path / "README.md"
|
|
1737
|
-
readme.write_text(
|
|
1738
|
-
f"""# MCLI Commands Store
|
|
1739
|
-
|
|
1740
|
-
Personal workflow commands for mcli framework.
|
|
1741
|
-
|
|
1742
|
-
## Usage
|
|
1743
|
-
|
|
1744
|
-
Push commands:
|
|
1745
|
-
```bash
|
|
1746
|
-
mcli commands store push
|
|
1747
|
-
```
|
|
1748
|
-
|
|
1749
|
-
Pull commands:
|
|
1750
|
-
```bash
|
|
1751
|
-
mcli commands store pull
|
|
1752
|
-
```
|
|
1753
|
-
|
|
1754
|
-
Sync (bidirectional):
|
|
1755
|
-
```bash
|
|
1756
|
-
mcli commands store sync
|
|
1757
|
-
```
|
|
1758
|
-
|
|
1759
|
-
## Structure
|
|
1760
|
-
|
|
1761
|
-
All JSON command files from `~/.mcli/commands/` are stored here and version controlled.
|
|
1762
|
-
|
|
1763
|
-
Last updated: {datetime.now().isoformat()}
|
|
1764
|
-
"""
|
|
1765
|
-
)
|
|
1766
|
-
|
|
1767
|
-
# Add remote if provided
|
|
1768
|
-
if remote:
|
|
1769
|
-
subprocess.run(
|
|
1770
|
-
["git", "remote", "add", "origin", remote], cwd=store_path, check=True
|
|
1771
|
-
)
|
|
1772
|
-
success(f"Added remote: {remote}")
|
|
1773
|
-
else:
|
|
1774
|
-
info(f"Git repository already exists at {store_path}")
|
|
1775
|
-
|
|
1776
|
-
# Save store path to config
|
|
1777
|
-
config_file = Path.home() / ".mcli" / "store.conf"
|
|
1778
|
-
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1779
|
-
config_file.write_text(str(store_path))
|
|
1780
|
-
|
|
1781
|
-
success(f"Command store initialized at {store_path}")
|
|
1782
|
-
info(f"Store path saved to {config_file}")
|
|
1783
|
-
|
|
1784
|
-
except subprocess.CalledProcessError as e:
|
|
1785
|
-
error(f"Git command failed: {e}")
|
|
1786
|
-
logger.error(f"Git init failed: {e}")
|
|
1787
|
-
except Exception as e:
|
|
1788
|
-
error(f"Failed to initialize store: {e}")
|
|
1789
|
-
logger.exception(e)
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
@store.command(name="push")
|
|
1793
|
-
@click.option("--message", "-m", help="Commit message")
|
|
1794
|
-
@click.option("--all", "-a", is_flag=True, help="Push all files (including backups)")
|
|
1795
|
-
@click.option(
|
|
1796
|
-
"--global", "-g", "is_global", is_flag=True, help="Push global commands instead of local"
|
|
1797
|
-
)
|
|
1798
|
-
def push_commands(message, all, is_global):
|
|
1799
|
-
"""
|
|
1800
|
-
Push commands to git store.
|
|
1801
|
-
|
|
1802
|
-
By default pushes local commands (if in git repo), use --global/-g for global commands.
|
|
1803
|
-
"""
|
|
1804
|
-
try:
|
|
1805
|
-
store_path = _get_store_path()
|
|
1806
|
-
from mcli.lib.paths import get_custom_commands_dir
|
|
1807
|
-
COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
|
|
1808
|
-
|
|
1809
|
-
# Copy commands to store
|
|
1810
|
-
info(f"Copying commands from {COMMANDS_PATH} to {store_path}...")
|
|
1811
|
-
|
|
1812
|
-
copied_count = 0
|
|
1813
|
-
for item in COMMANDS_PATH.glob("*"):
|
|
1814
|
-
# Skip backups unless --all specified
|
|
1815
|
-
if not all and item.name.endswith(".backup"):
|
|
1816
|
-
continue
|
|
1817
|
-
|
|
1818
|
-
dest = store_path / item.name
|
|
1819
|
-
if item.is_file():
|
|
1820
|
-
shutil.copy2(item, dest)
|
|
1821
|
-
copied_count += 1
|
|
1822
|
-
elif item.is_dir():
|
|
1823
|
-
shutil.copytree(item, dest, dirs_exist_ok=True)
|
|
1824
|
-
copied_count += 1
|
|
1825
|
-
|
|
1826
|
-
success(f"Copied {copied_count} items to store")
|
|
1827
|
-
|
|
1828
|
-
# Git add, commit, push
|
|
1829
|
-
subprocess.run(["git", "add", "."], cwd=store_path, check=True)
|
|
1830
|
-
|
|
1831
|
-
# Check if there are changes
|
|
1832
|
-
result = subprocess.run(
|
|
1833
|
-
["git", "status", "--porcelain"], cwd=store_path, capture_output=True, text=True
|
|
1834
|
-
)
|
|
1835
|
-
|
|
1836
|
-
if not result.stdout.strip():
|
|
1837
|
-
info("No changes to commit")
|
|
1838
|
-
return
|
|
1839
|
-
|
|
1840
|
-
# Commit with message
|
|
1841
|
-
commit_msg = message or f"Update commands {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
1842
|
-
subprocess.run(["git", "commit", "-m", commit_msg], cwd=store_path, check=True)
|
|
1843
|
-
success(f"Committed changes: {commit_msg}")
|
|
1844
|
-
|
|
1845
|
-
# Push to remote if configured
|
|
1846
|
-
try:
|
|
1847
|
-
subprocess.run(["git", "push"], cwd=store_path, check=True, capture_output=True)
|
|
1848
|
-
success("Pushed to remote")
|
|
1849
|
-
except subprocess.CalledProcessError:
|
|
1850
|
-
warning("No remote configured or push failed. Commands committed locally.")
|
|
1851
|
-
|
|
1852
|
-
except Exception as e:
|
|
1853
|
-
error(f"Failed to push commands: {e}")
|
|
1854
|
-
logger.exception(e)
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
@store.command(name="pull")
|
|
1858
|
-
@click.option("--force", "-f", is_flag=True, help="Overwrite local commands without backup")
|
|
1859
|
-
@click.option(
|
|
1860
|
-
"--global", "-g", "is_global", is_flag=True, help="Pull to global commands instead of local"
|
|
1861
|
-
)
|
|
1862
|
-
def pull_commands(force, is_global):
|
|
1863
|
-
"""
|
|
1864
|
-
Pull commands from git store.
|
|
1865
|
-
|
|
1866
|
-
By default pulls to local commands (if in git repo), use --global/-g for global commands.
|
|
1867
|
-
"""
|
|
1868
|
-
try:
|
|
1869
|
-
store_path = _get_store_path()
|
|
1870
|
-
from mcli.lib.paths import get_custom_commands_dir
|
|
1871
|
-
COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
|
|
1872
|
-
|
|
1873
|
-
# Pull from remote
|
|
1874
|
-
try:
|
|
1875
|
-
subprocess.run(["git", "pull"], cwd=store_path, check=True)
|
|
1876
|
-
success("Pulled latest changes from remote")
|
|
1877
|
-
except subprocess.CalledProcessError:
|
|
1878
|
-
warning("No remote configured or pull failed. Using local store.")
|
|
1879
|
-
|
|
1880
|
-
# Backup existing commands if not force
|
|
1881
|
-
if not force and COMMANDS_PATH.exists():
|
|
1882
|
-
backup_dir = (
|
|
1883
|
-
COMMANDS_PATH.parent / f"commands_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
1884
|
-
)
|
|
1885
|
-
shutil.copytree(COMMANDS_PATH, backup_dir)
|
|
1886
|
-
info(f"Backed up existing commands to {backup_dir}")
|
|
1887
|
-
|
|
1888
|
-
# Copy from store to commands directory
|
|
1889
|
-
info(f"Copying commands from {store_path} to {COMMANDS_PATH}...")
|
|
1890
|
-
|
|
1891
|
-
COMMANDS_PATH.mkdir(parents=True, exist_ok=True)
|
|
1892
|
-
|
|
1893
|
-
copied_count = 0
|
|
1894
|
-
for item in store_path.glob("*"):
|
|
1895
|
-
# Skip git directory and README
|
|
1896
|
-
if item.name in [".git", "README.md", ".gitignore"]:
|
|
1897
|
-
continue
|
|
1898
|
-
|
|
1899
|
-
dest = COMMANDS_PATH / item.name
|
|
1900
|
-
if item.is_file():
|
|
1901
|
-
shutil.copy2(item, dest)
|
|
1902
|
-
copied_count += 1
|
|
1903
|
-
elif item.is_dir():
|
|
1904
|
-
shutil.copytree(item, dest, dirs_exist_ok=True)
|
|
1905
|
-
copied_count += 1
|
|
1906
|
-
|
|
1907
|
-
success(f"Pulled {copied_count} items from store")
|
|
1908
|
-
|
|
1909
|
-
except Exception as e:
|
|
1910
|
-
error(f"Failed to pull commands: {e}")
|
|
1911
|
-
logger.exception(e)
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
@store.command(name="sync")
|
|
1915
|
-
@click.option("--message", "-m", help="Commit message if pushing")
|
|
1916
|
-
@click.option(
|
|
1917
|
-
"--global", "-g", "is_global", is_flag=True, help="Sync global commands instead of local"
|
|
1918
|
-
)
|
|
1919
|
-
def sync_commands(message, is_global):
|
|
1920
|
-
"""
|
|
1921
|
-
Sync commands bidirectionally (pull then push if changes).
|
|
1922
|
-
|
|
1923
|
-
By default syncs local commands (if in git repo), use --global/-g for global commands.
|
|
1924
|
-
"""
|
|
1925
|
-
try:
|
|
1926
|
-
store_path = _get_store_path()
|
|
1927
|
-
from mcli.lib.paths import get_custom_commands_dir
|
|
1928
|
-
COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
|
|
1929
|
-
|
|
1930
|
-
# First pull
|
|
1931
|
-
info("Pulling latest changes...")
|
|
1932
|
-
try:
|
|
1933
|
-
subprocess.run(["git", "pull"], cwd=store_path, check=True, capture_output=True)
|
|
1934
|
-
success("Pulled from remote")
|
|
1935
|
-
except subprocess.CalledProcessError:
|
|
1936
|
-
warning("No remote or pull failed")
|
|
1937
|
-
|
|
1938
|
-
# Then push local changes
|
|
1939
|
-
info("Pushing local changes...")
|
|
1940
|
-
|
|
1941
|
-
# Copy commands
|
|
1942
|
-
for item in COMMANDS_PATH.glob("*"):
|
|
1943
|
-
if item.name.endswith(".backup"):
|
|
1944
|
-
continue
|
|
1945
|
-
dest = store_path / item.name
|
|
1946
|
-
if item.is_file():
|
|
1947
|
-
shutil.copy2(item, dest)
|
|
1948
|
-
elif item.is_dir():
|
|
1949
|
-
shutil.copytree(item, dest, dirs_exist_ok=True)
|
|
1950
|
-
|
|
1951
|
-
# Check for changes
|
|
1952
|
-
result = subprocess.run(
|
|
1953
|
-
["git", "status", "--porcelain"], cwd=store_path, capture_output=True, text=True
|
|
1954
|
-
)
|
|
1955
|
-
|
|
1956
|
-
if not result.stdout.strip():
|
|
1957
|
-
success("Everything in sync!")
|
|
1958
|
-
return
|
|
1959
|
-
|
|
1960
|
-
# Commit and push
|
|
1961
|
-
subprocess.run(["git", "add", "."], cwd=store_path, check=True)
|
|
1962
|
-
commit_msg = message or f"Sync commands {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
1963
|
-
subprocess.run(["git", "commit", "-m", commit_msg], cwd=store_path, check=True)
|
|
1964
|
-
|
|
1965
|
-
try:
|
|
1966
|
-
subprocess.run(["git", "push"], cwd=store_path, check=True, capture_output=True)
|
|
1967
|
-
success("Synced and pushed to remote")
|
|
1968
|
-
except subprocess.CalledProcessError:
|
|
1969
|
-
success("Synced locally (no remote configured)")
|
|
1970
|
-
|
|
1971
|
-
except Exception as e:
|
|
1972
|
-
error(f"Sync failed: {e}")
|
|
1973
|
-
logger.exception(e)
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
@store.command(name="status")
|
|
1977
|
-
def store_status():
|
|
1978
|
-
"""Show git status of command store"""
|
|
1979
|
-
try:
|
|
1980
|
-
store_path = _get_store_path()
|
|
1981
|
-
|
|
1982
|
-
click.echo(f"\n📦 Store: {store_path}\n")
|
|
1983
|
-
|
|
1984
|
-
# Git status
|
|
1985
|
-
result = subprocess.run(
|
|
1986
|
-
["git", "status", "--short", "--branch"], cwd=store_path, capture_output=True, text=True
|
|
1987
|
-
)
|
|
1988
|
-
|
|
1989
|
-
if result.stdout:
|
|
1990
|
-
click.echo(result.stdout)
|
|
1991
|
-
|
|
1992
|
-
# Show remote
|
|
1993
|
-
result = subprocess.run(
|
|
1994
|
-
["git", "remote", "-v"], cwd=store_path, capture_output=True, text=True
|
|
1995
|
-
)
|
|
1996
|
-
|
|
1997
|
-
if result.stdout:
|
|
1998
|
-
click.echo("\n🌐 Remotes:")
|
|
1999
|
-
click.echo(result.stdout)
|
|
2000
|
-
else:
|
|
2001
|
-
info("\nNo remote configured")
|
|
2002
|
-
|
|
2003
|
-
click.echo()
|
|
2004
|
-
|
|
2005
|
-
except Exception as e:
|
|
2006
|
-
error(f"Failed to get status: {e}")
|
|
2007
|
-
logger.exception(e)
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
@store.command(name="config")
|
|
2011
|
-
@click.option("--remote", "-r", help="Set git remote URL")
|
|
2012
|
-
@click.option("--path", "-p", type=click.Path(), help="Change store path")
|
|
2013
|
-
def configure_store(remote, path):
|
|
2014
|
-
"""Configure store settings"""
|
|
2015
|
-
try:
|
|
2016
|
-
store_path = _get_store_path()
|
|
2017
|
-
|
|
2018
|
-
if path:
|
|
2019
|
-
new_path = Path(path).expanduser().resolve()
|
|
2020
|
-
config_file = Path.home() / ".mcli" / "store.conf"
|
|
2021
|
-
config_file.write_text(str(new_path))
|
|
2022
|
-
success(f"Store path updated to: {new_path}")
|
|
2023
|
-
return
|
|
2024
|
-
|
|
2025
|
-
if remote:
|
|
2026
|
-
# Check if remote exists
|
|
2027
|
-
result = subprocess.run(
|
|
2028
|
-
["git", "remote"], cwd=store_path, capture_output=True, text=True
|
|
2029
|
-
)
|
|
2030
|
-
|
|
2031
|
-
if "origin" in result.stdout:
|
|
2032
|
-
subprocess.run(
|
|
2033
|
-
["git", "remote", "set-url", "origin", remote], cwd=store_path, check=True
|
|
2034
|
-
)
|
|
2035
|
-
success(f"Updated remote URL: {remote}")
|
|
2036
|
-
else:
|
|
2037
|
-
subprocess.run(
|
|
2038
|
-
["git", "remote", "add", "origin", remote], cwd=store_path, check=True
|
|
2039
|
-
)
|
|
2040
|
-
success(f"Added remote URL: {remote}")
|
|
2041
|
-
|
|
2042
|
-
except Exception as e:
|
|
2043
|
-
error(f"Configuration failed: {e}")
|
|
2044
|
-
logger.exception(e)
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
@store.command(name="list")
|
|
2048
|
-
@click.option("--store", "-s", is_flag=True, help="List store instead of local")
|
|
2049
|
-
def list_commands(store):
|
|
2050
|
-
"""List all commands"""
|
|
2051
|
-
try:
|
|
2052
|
-
if store:
|
|
2053
|
-
store_path = _get_store_path()
|
|
2054
|
-
path = store_path
|
|
2055
|
-
title = f"Commands in store ({store_path})"
|
|
2056
|
-
else:
|
|
2057
|
-
path = COMMANDS_PATH
|
|
2058
|
-
title = f"Local commands ({COMMANDS_PATH})"
|
|
2059
|
-
|
|
2060
|
-
click.echo(f"\n{title}:\n")
|
|
2061
|
-
|
|
2062
|
-
if not path.exists():
|
|
2063
|
-
warning(f"Directory does not exist: {path}")
|
|
2064
|
-
return
|
|
2065
|
-
|
|
2066
|
-
items = sorted(path.glob("*"))
|
|
2067
|
-
if not items:
|
|
2068
|
-
info("No commands found")
|
|
2069
|
-
return
|
|
2070
|
-
|
|
2071
|
-
for item in items:
|
|
2072
|
-
if item.name in [".git", ".gitignore", "README.md"]:
|
|
2073
|
-
continue
|
|
2074
|
-
|
|
2075
|
-
if item.is_file():
|
|
2076
|
-
size = item.stat().st_size / 1024
|
|
2077
|
-
modified = datetime.fromtimestamp(item.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
2078
|
-
click.echo(f" 📄 {item.name:<40} {size:>8.1f} KB {modified}")
|
|
2079
|
-
elif item.is_dir():
|
|
2080
|
-
count = len(list(item.glob("*")))
|
|
2081
|
-
click.echo(f" 📁 {item.name:<40} {count:>3} files")
|
|
2082
|
-
|
|
2083
|
-
click.echo()
|
|
2084
|
-
|
|
2085
|
-
except Exception as e:
|
|
2086
|
-
error(f"Failed to list commands: {e}")
|
|
2087
|
-
logger.exception(e)
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
@store.command(name="show")
|
|
2091
|
-
@click.argument("command_name")
|
|
2092
|
-
@click.option("--store", "-s", is_flag=True, help="Show from store instead of local")
|
|
2093
|
-
def show_command(command_name, store):
|
|
2094
|
-
"""Show command file contents"""
|
|
2095
|
-
try:
|
|
2096
|
-
if store:
|
|
2097
|
-
store_path = _get_store_path()
|
|
2098
|
-
path = store_path / command_name
|
|
2099
|
-
else:
|
|
2100
|
-
path = COMMANDS_PATH / command_name
|
|
2101
|
-
|
|
2102
|
-
if not path.exists():
|
|
2103
|
-
error(f"Command not found: {command_name}")
|
|
2104
|
-
return
|
|
2105
|
-
|
|
2106
|
-
if path.is_file():
|
|
2107
|
-
click.echo(f"\n📄 {path}:\n")
|
|
2108
|
-
click.echo(path.read_text())
|
|
2109
|
-
else:
|
|
2110
|
-
info(f"{command_name} is a directory")
|
|
2111
|
-
for item in sorted(path.glob("*")):
|
|
2112
|
-
click.echo(f" {item.name}")
|
|
2113
|
-
|
|
2114
|
-
click.echo()
|
|
2115
|
-
|
|
2116
|
-
except Exception as e:
|
|
2117
|
-
error(f"Failed to show command: {e}")
|
|
2118
|
-
logger.exception(e)
|
|
1311
|
+
# Lock and Store management groups moved to separate top-level files:
|
|
1312
|
+
# - lock_cmd.py (mcli lock)
|
|
1313
|
+
# - store_cmd.py (mcli store)
|
|
2119
1314
|
|
|
2120
1315
|
|
|
2121
1316
|
# Extract workflow commands command
|