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.

Files changed (81) hide show
  1. mcli/__init__.py +160 -0
  2. mcli/__main__.py +14 -0
  3. mcli/app/__init__.py +23 -0
  4. mcli/app/commands_cmd.py +18 -823
  5. mcli/app/init_cmd.py +391 -0
  6. mcli/app/lock_cmd.py +288 -0
  7. mcli/app/main.py +37 -0
  8. mcli/app/model/__init__.py +0 -0
  9. mcli/app/store_cmd.py +448 -0
  10. mcli/app/video/__init__.py +5 -0
  11. mcli/chat/__init__.py +34 -0
  12. mcli/lib/__init__.py +0 -0
  13. mcli/lib/api/__init__.py +0 -0
  14. mcli/lib/auth/__init__.py +1 -0
  15. mcli/lib/config/__init__.py +1 -0
  16. mcli/lib/custom_commands.py +3 -3
  17. mcli/lib/erd/__init__.py +25 -0
  18. mcli/lib/files/__init__.py +0 -0
  19. mcli/lib/fs/__init__.py +1 -0
  20. mcli/lib/logger/__init__.py +3 -0
  21. mcli/lib/optional_deps.py +1 -3
  22. mcli/lib/performance/__init__.py +17 -0
  23. mcli/lib/pickles/__init__.py +1 -0
  24. mcli/lib/secrets/__init__.py +10 -0
  25. mcli/lib/shell/__init__.py +0 -0
  26. mcli/lib/toml/__init__.py +1 -0
  27. mcli/lib/watcher/__init__.py +0 -0
  28. mcli/ml/__init__.py +16 -0
  29. mcli/ml/api/__init__.py +30 -0
  30. mcli/ml/api/routers/__init__.py +27 -0
  31. mcli/ml/auth/__init__.py +41 -0
  32. mcli/ml/backtesting/__init__.py +33 -0
  33. mcli/ml/cli/__init__.py +5 -0
  34. mcli/ml/config/__init__.py +33 -0
  35. mcli/ml/configs/__init__.py +16 -0
  36. mcli/ml/dashboard/__init__.py +12 -0
  37. mcli/ml/dashboard/components/__init__.py +7 -0
  38. mcli/ml/dashboard/pages/__init__.py +6 -0
  39. mcli/ml/data_ingestion/__init__.py +29 -0
  40. mcli/ml/database/__init__.py +40 -0
  41. mcli/ml/experimentation/__init__.py +29 -0
  42. mcli/ml/features/__init__.py +39 -0
  43. mcli/ml/mlops/__init__.py +19 -0
  44. mcli/ml/models/__init__.py +90 -0
  45. mcli/ml/monitoring/__init__.py +25 -0
  46. mcli/ml/optimization/__init__.py +27 -0
  47. mcli/ml/predictions/__init__.py +5 -0
  48. mcli/ml/preprocessing/__init__.py +24 -0
  49. mcli/ml/scripts/__init__.py +1 -0
  50. mcli/ml/serving/__init__.py +1 -0
  51. mcli/ml/trading/__init__.py +63 -0
  52. mcli/ml/training/__init__.py +7 -0
  53. mcli/mygroup/__init__.py +3 -0
  54. mcli/public/__init__.py +1 -0
  55. mcli/public/commands/__init__.py +2 -0
  56. mcli/self/__init__.py +3 -0
  57. mcli/self/migrate_cmd.py +209 -76
  58. mcli/workflow/__init__.py +0 -0
  59. mcli/workflow/daemon/__init__.py +15 -0
  60. mcli/workflow/dashboard/__init__.py +5 -0
  61. mcli/workflow/docker/__init__.py +0 -0
  62. mcli/workflow/file/__init__.py +0 -0
  63. mcli/workflow/gcloud/__init__.py +1 -0
  64. mcli/workflow/git_commit/__init__.py +0 -0
  65. mcli/workflow/interview/__init__.py +0 -0
  66. mcli/workflow/notebook/__init__.py +16 -0
  67. mcli/workflow/registry/__init__.py +0 -0
  68. mcli/workflow/repo/__init__.py +0 -0
  69. mcli/workflow/scheduler/__init__.py +25 -0
  70. mcli/workflow/search/__init__.py +0 -0
  71. mcli/workflow/secrets/__init__.py +5 -0
  72. mcli/workflow/secrets/secrets_cmd.py +1 -2
  73. mcli/workflow/sync/__init__.py +5 -0
  74. mcli/workflow/videos/__init__.py +1 -0
  75. mcli/workflow/wakatime/__init__.py +80 -0
  76. {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/METADATA +10 -10
  77. {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/RECORD +81 -12
  78. {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/WHEEL +0 -0
  79. {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/entry_points.txt +0 -0
  80. {mcli_framework-7.11.4.dist-info → mcli_framework-7.12.0.dist-info}/licenses/LICENSE +0 -0
  81. {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
- @workflow.command("init")
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", "-g", "is_global", is_flag=True, help="Use global commands (~/.mcli/commands/) instead of local (.mcli/commands/)"
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(include_groups: bool, daemon_only: bool, custom_only: bool, as_json: bool, is_global: bool):
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", "-g", "is_global", is_flag=True, help="Add to global commands (~/.mcli/commands/) instead of local (.mcli/commands/)"
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
- @workflow.command("verify")
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
- # State management subgroup
1620
- # Moved from mcli.self for better organization
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