mcli-framework 7.3.1__py3-none-any.whl → 7.5.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/app/commands_cmd.py +741 -0
- mcli/lib/auth/aws_manager.py +9 -64
- mcli/lib/auth/azure_manager.py +9 -64
- mcli/lib/auth/credential_manager.py +70 -1
- mcli/lib/auth/gcp_manager.py +11 -64
- mcli/ml/dashboard/app.py +6 -39
- mcli/ml/dashboard/app_integrated.py +288 -117
- mcli/ml/dashboard/app_supabase.py +8 -57
- mcli/ml/dashboard/app_training.py +10 -12
- mcli/ml/dashboard/common.py +167 -0
- mcli/ml/dashboard/overview.py +378 -0
- mcli/ml/dashboard/pages/cicd.py +4 -4
- mcli/ml/dashboard/pages/debug_dependencies.py +406 -0
- mcli/ml/dashboard/pages/gravity_viz.py +783 -0
- mcli/ml/dashboard/pages/monte_carlo_predictions.py +555 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +4 -2
- mcli/ml/dashboard/pages/scrapers_and_logs.py +25 -9
- mcli/ml/dashboard/pages/test_portfolio.py +54 -4
- mcli/ml/dashboard/pages/trading.py +80 -26
- mcli/ml/dashboard/streamlit_extras_utils.py +297 -0
- mcli/ml/dashboard/styles.py +55 -0
- mcli/ml/dashboard/utils.py +7 -0
- mcli/ml/dashboard/warning_suppression.py +34 -0
- mcli/ml/database/session.py +169 -16
- mcli/ml/predictions/monte_carlo.py +428 -0
- mcli/ml/trading/alpaca_client.py +82 -18
- mcli/self/self_cmd.py +182 -737
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/METADATA +2 -3
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/RECORD +33 -87
- mcli/__init__.py +0 -160
- mcli/__main__.py +0 -14
- mcli/app/__init__.py +0 -23
- mcli/app/model/__init__.py +0 -0
- mcli/app/video/__init__.py +0 -5
- mcli/chat/__init__.py +0 -34
- mcli/lib/__init__.py +0 -0
- mcli/lib/api/__init__.py +0 -0
- mcli/lib/auth/__init__.py +0 -1
- mcli/lib/config/__init__.py +0 -1
- mcli/lib/erd/__init__.py +0 -25
- mcli/lib/files/__init__.py +0 -0
- mcli/lib/fs/__init__.py +0 -1
- mcli/lib/logger/__init__.py +0 -3
- mcli/lib/performance/__init__.py +0 -17
- mcli/lib/pickles/__init__.py +0 -1
- mcli/lib/shell/__init__.py +0 -0
- mcli/lib/toml/__init__.py +0 -1
- mcli/lib/watcher/__init__.py +0 -0
- mcli/ml/__init__.py +0 -16
- mcli/ml/api/__init__.py +0 -30
- mcli/ml/api/routers/__init__.py +0 -27
- mcli/ml/auth/__init__.py +0 -45
- mcli/ml/backtesting/__init__.py +0 -39
- mcli/ml/cli/__init__.py +0 -5
- mcli/ml/config/__init__.py +0 -33
- mcli/ml/configs/__init__.py +0 -16
- mcli/ml/dashboard/__init__.py +0 -12
- mcli/ml/dashboard/components/__init__.py +0 -7
- mcli/ml/dashboard/pages/__init__.py +0 -6
- mcli/ml/data_ingestion/__init__.py +0 -39
- mcli/ml/database/__init__.py +0 -47
- mcli/ml/experimentation/__init__.py +0 -29
- mcli/ml/features/__init__.py +0 -39
- mcli/ml/mlops/__init__.py +0 -33
- mcli/ml/models/__init__.py +0 -94
- mcli/ml/monitoring/__init__.py +0 -25
- mcli/ml/optimization/__init__.py +0 -27
- mcli/ml/predictions/__init__.py +0 -5
- mcli/ml/preprocessing/__init__.py +0 -28
- mcli/ml/scripts/__init__.py +0 -1
- mcli/ml/trading/__init__.py +0 -60
- mcli/ml/training/__init__.py +0 -10
- mcli/mygroup/__init__.py +0 -3
- mcli/public/__init__.py +0 -1
- mcli/public/commands/__init__.py +0 -2
- mcli/self/__init__.py +0 -3
- mcli/workflow/__init__.py +0 -0
- mcli/workflow/daemon/__init__.py +0 -15
- mcli/workflow/dashboard/__init__.py +0 -5
- mcli/workflow/docker/__init__.py +0 -0
- mcli/workflow/file/__init__.py +0 -0
- mcli/workflow/gcloud/__init__.py +0 -1
- mcli/workflow/git_commit/__init__.py +0 -0
- mcli/workflow/interview/__init__.py +0 -0
- mcli/workflow/politician_trading/__init__.py +0 -4
- mcli/workflow/registry/__init__.py +0 -0
- mcli/workflow/repo/__init__.py +0 -0
- mcli/workflow/scheduler/__init__.py +0 -25
- mcli/workflow/search/__init__.py +0 -0
- mcli/workflow/sync/__init__.py +0 -5
- mcli/workflow/videos/__init__.py +0 -1
- mcli/workflow/wakatime/__init__.py +0 -80
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/WHEEL +0 -0
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/top_level.txt +0 -0
mcli/self/self_cmd.py
CHANGED
|
@@ -344,8 +344,38 @@ def collect_commands() -> List[Dict[str, Any]]:
|
|
|
344
344
|
module_name = ".".join(relative_path.with_suffix("").parts)
|
|
345
345
|
|
|
346
346
|
try:
|
|
347
|
-
#
|
|
348
|
-
|
|
347
|
+
# Suppress Streamlit warnings and logging during module import
|
|
348
|
+
import warnings
|
|
349
|
+
import logging
|
|
350
|
+
import sys
|
|
351
|
+
import os
|
|
352
|
+
from contextlib import redirect_stderr
|
|
353
|
+
from io import StringIO
|
|
354
|
+
|
|
355
|
+
# Suppress Python warnings
|
|
356
|
+
with warnings.catch_warnings():
|
|
357
|
+
warnings.filterwarnings("ignore", message=".*missing ScriptRunContext.*")
|
|
358
|
+
warnings.filterwarnings("ignore", message=".*No runtime found.*")
|
|
359
|
+
warnings.filterwarnings("ignore", message=".*Session state does not function.*")
|
|
360
|
+
warnings.filterwarnings("ignore", message=".*to view this Streamlit app.*")
|
|
361
|
+
|
|
362
|
+
# Suppress Streamlit logger warnings
|
|
363
|
+
streamlit_logger = logging.getLogger("streamlit")
|
|
364
|
+
original_level = streamlit_logger.level
|
|
365
|
+
streamlit_logger.setLevel(logging.CRITICAL)
|
|
366
|
+
|
|
367
|
+
# Also suppress specific Streamlit sub-loggers
|
|
368
|
+
logging.getLogger("streamlit.runtime.scriptrunner_utils.script_run_context").setLevel(logging.CRITICAL)
|
|
369
|
+
logging.getLogger("streamlit.runtime.caching.cache_data_api").setLevel(logging.CRITICAL)
|
|
370
|
+
|
|
371
|
+
# Redirect stderr to suppress Streamlit warnings
|
|
372
|
+
with redirect_stderr(StringIO()):
|
|
373
|
+
try:
|
|
374
|
+
# Try to import the module
|
|
375
|
+
module = importlib.import_module(module_name)
|
|
376
|
+
finally:
|
|
377
|
+
# Restore original logging level
|
|
378
|
+
streamlit_logger.setLevel(original_level)
|
|
349
379
|
|
|
350
380
|
# Extract command and group objects
|
|
351
381
|
for name, obj in inspect.getmembers(module):
|
|
@@ -387,263 +417,150 @@ def collect_commands() -> List[Dict[str, Any]]:
|
|
|
387
417
|
return commands
|
|
388
418
|
|
|
389
419
|
|
|
390
|
-
|
|
391
|
-
@click.argument("command_name", required=True)
|
|
392
|
-
@click.option("--group", "-g", help="Command group (defaults to 'workflow')", default="workflow")
|
|
393
|
-
@click.option(
|
|
394
|
-
"--description", "-d", help="Description for the command", default="Custom command"
|
|
395
|
-
)
|
|
396
|
-
def add_command(command_name, group, description):
|
|
397
|
-
"""
|
|
398
|
-
Generate a new portable custom command saved to ~/.mcli/commands/.
|
|
399
|
-
|
|
400
|
-
Commands are automatically nested under the 'workflow' group by default,
|
|
401
|
-
making them portable and persistent across updates.
|
|
402
|
-
|
|
403
|
-
Example:
|
|
404
|
-
mcli self add-command my_command
|
|
405
|
-
mcli self add-command analytics --group data
|
|
406
|
-
"""
|
|
407
|
-
command_name = command_name.lower().replace("-", "_")
|
|
408
|
-
|
|
409
|
-
# Validate command name
|
|
410
|
-
if not re.match(r"^[a-z][a-z0-9_]*$", command_name):
|
|
411
|
-
logger.error(
|
|
412
|
-
f"Invalid command name: {command_name}. Use lowercase letters, numbers, and underscores (starting with a letter)."
|
|
413
|
-
)
|
|
414
|
-
click.echo(
|
|
415
|
-
f"Invalid command name: {command_name}. Use lowercase letters, numbers, and underscores (starting with a letter).",
|
|
416
|
-
err=True,
|
|
417
|
-
)
|
|
418
|
-
return 1
|
|
419
|
-
|
|
420
|
-
# Validate group name if provided
|
|
421
|
-
if group:
|
|
422
|
-
command_group = group.lower().replace("-", "_")
|
|
423
|
-
if not re.match(r"^[a-z][a-z0-9_]*$", command_group):
|
|
424
|
-
logger.error(
|
|
425
|
-
f"Invalid group name: {command_group}. Use lowercase letters, numbers, and underscores (starting with a letter)."
|
|
426
|
-
)
|
|
427
|
-
click.echo(
|
|
428
|
-
f"Invalid group name: {command_group}. Use lowercase letters, numbers, and underscores (starting with a letter).",
|
|
429
|
-
err=True,
|
|
430
|
-
)
|
|
431
|
-
return 1
|
|
432
|
-
else:
|
|
433
|
-
command_group = "workflow" # Default to workflow group
|
|
434
|
-
|
|
435
|
-
# Get the command manager
|
|
436
|
-
manager = get_command_manager()
|
|
437
|
-
|
|
438
|
-
# Check if command already exists
|
|
439
|
-
command_file = manager.commands_dir / f"{command_name}.json"
|
|
440
|
-
if command_file.exists():
|
|
441
|
-
logger.warning(f"Custom command already exists: {command_name}")
|
|
442
|
-
should_override = Prompt.ask(
|
|
443
|
-
"Command already exists. Override?", choices=["y", "n"], default="n"
|
|
444
|
-
)
|
|
445
|
-
if should_override.lower() != "y":
|
|
446
|
-
logger.info("Command creation aborted.")
|
|
447
|
-
click.echo("Command creation aborted.")
|
|
448
|
-
return 1
|
|
449
|
-
|
|
450
|
-
# Generate command code
|
|
451
|
-
code = get_command_template(command_name, command_group)
|
|
452
|
-
|
|
453
|
-
# Save the command
|
|
454
|
-
saved_path = manager.save_command(
|
|
455
|
-
name=command_name,
|
|
456
|
-
code=code,
|
|
457
|
-
description=description,
|
|
458
|
-
group=command_group,
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
logger.info(f"Created portable custom command: {command_name}")
|
|
462
|
-
click.echo(f"✅ Created portable custom command: {command_name}")
|
|
463
|
-
click.echo(f"📁 Saved to: {saved_path}")
|
|
464
|
-
click.echo(f"🔄 Command will be automatically loaded on next mcli startup")
|
|
465
|
-
click.echo(
|
|
466
|
-
f"💡 You can share this command by copying {saved_path} to another machine's ~/.mcli/commands/ directory"
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
return 0
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
@self_app.command("list-commands")
|
|
473
|
-
def list_commands():
|
|
474
|
-
"""
|
|
475
|
-
List all custom commands stored in ~/.mcli/commands/.
|
|
476
|
-
"""
|
|
477
|
-
manager = get_command_manager()
|
|
478
|
-
commands = manager.load_all_commands()
|
|
479
|
-
|
|
480
|
-
if not commands:
|
|
481
|
-
click.echo("No custom commands found.")
|
|
482
|
-
click.echo(
|
|
483
|
-
f"Create one with: mcli self add-command <name>"
|
|
484
|
-
)
|
|
485
|
-
return 0
|
|
486
|
-
|
|
487
|
-
table = Table(title="Custom Commands")
|
|
488
|
-
table.add_column("Name", style="green")
|
|
489
|
-
table.add_column("Group", style="blue")
|
|
490
|
-
table.add_column("Description", style="yellow")
|
|
491
|
-
table.add_column("Version", style="cyan")
|
|
492
|
-
table.add_column("Updated", style="dim")
|
|
493
|
-
|
|
494
|
-
for cmd in commands:
|
|
495
|
-
table.add_row(
|
|
496
|
-
cmd["name"],
|
|
497
|
-
cmd.get("group", "-"),
|
|
498
|
-
cmd.get("description", ""),
|
|
499
|
-
cmd.get("version", "1.0"),
|
|
500
|
-
cmd.get("updated_at", "")[:10] if cmd.get("updated_at") else "-",
|
|
501
|
-
)
|
|
502
|
-
|
|
503
|
-
console.print(table)
|
|
504
|
-
click.echo(f"\n📁 Commands directory: {manager.commands_dir}")
|
|
505
|
-
click.echo(f"🔒 Lockfile: {manager.lockfile_path}")
|
|
506
|
-
|
|
507
|
-
return 0
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
@self_app.command("remove-command")
|
|
511
|
-
@click.argument("command_name", required=True)
|
|
512
|
-
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
513
|
-
def remove_command(command_name, yes):
|
|
420
|
+
def open_editor_for_command(command_name: str, command_group: str, description: str) -> Optional[str]:
|
|
514
421
|
"""
|
|
515
|
-
|
|
422
|
+
Open the user's default editor to allow them to write command logic.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
command_name: Name of the command
|
|
426
|
+
command_group: Group for the command
|
|
427
|
+
description: Description of the command
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
The Python code written by the user, or None if cancelled
|
|
516
431
|
"""
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if
|
|
533
|
-
click.echo(
|
|
534
|
-
return
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
click.echo("❌ Failed to export commands.", err=True)
|
|
563
|
-
return 1
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
@self_app.command("import-commands")
|
|
567
|
-
@click.argument("import_file", type=click.Path(exists=True), required=True)
|
|
568
|
-
@click.option("--overwrite", is_flag=True, help="Overwrite existing commands")
|
|
569
|
-
def import_commands(import_file, overwrite):
|
|
570
|
-
"""
|
|
571
|
-
Import custom commands from a JSON file.
|
|
572
|
-
"""
|
|
573
|
-
manager = get_command_manager()
|
|
574
|
-
import_path = Path(import_file)
|
|
575
|
-
|
|
576
|
-
results = manager.import_commands(import_path, overwrite=overwrite)
|
|
577
|
-
|
|
578
|
-
success_count = sum(1 for v in results.values() if v)
|
|
579
|
-
failed_count = len(results) - success_count
|
|
580
|
-
|
|
581
|
-
if success_count > 0:
|
|
582
|
-
click.echo(f"✅ Imported {success_count} command(s)")
|
|
583
|
-
|
|
584
|
-
if failed_count > 0:
|
|
585
|
-
click.echo(
|
|
586
|
-
f"⚠️ Skipped {failed_count} command(s) (already exist, use --overwrite to replace)"
|
|
587
|
-
)
|
|
588
|
-
click.echo("Skipped commands:")
|
|
589
|
-
for name, success in results.items():
|
|
590
|
-
if not success:
|
|
591
|
-
click.echo(f" - {name}")
|
|
592
|
-
|
|
593
|
-
return 0
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
@self_app.command("verify-commands")
|
|
597
|
-
def verify_commands():
|
|
598
|
-
"""
|
|
599
|
-
Verify that custom commands match the lockfile.
|
|
600
|
-
"""
|
|
601
|
-
manager = get_command_manager()
|
|
602
|
-
|
|
603
|
-
# First, ensure lockfile is up to date
|
|
604
|
-
manager.update_lockfile()
|
|
605
|
-
|
|
606
|
-
verification = manager.verify_lockfile()
|
|
607
|
-
|
|
608
|
-
if verification["valid"]:
|
|
609
|
-
click.echo("✅ All custom commands are in sync with the lockfile.")
|
|
610
|
-
return 0
|
|
611
|
-
|
|
612
|
-
click.echo("⚠️ Commands are out of sync with the lockfile:\n")
|
|
613
|
-
|
|
614
|
-
if verification["missing"]:
|
|
615
|
-
click.echo(f"Missing commands (in lockfile but not found):")
|
|
616
|
-
for name in verification["missing"]:
|
|
617
|
-
click.echo(f" - {name}")
|
|
618
|
-
|
|
619
|
-
if verification["extra"]:
|
|
620
|
-
click.echo(f"\nExtra commands (not in lockfile):")
|
|
621
|
-
for name in verification["extra"]:
|
|
622
|
-
click.echo(f" - {name}")
|
|
623
|
-
|
|
624
|
-
if verification["modified"]:
|
|
625
|
-
click.echo(f"\nModified commands:")
|
|
626
|
-
for name in verification["modified"]:
|
|
627
|
-
click.echo(f" - {name}")
|
|
628
|
-
|
|
629
|
-
click.echo(f"\n💡 Run 'mcli self update-lockfile' to sync the lockfile")
|
|
432
|
+
import tempfile
|
|
433
|
+
import subprocess
|
|
434
|
+
import os
|
|
435
|
+
import sys
|
|
436
|
+
from pathlib import Path
|
|
437
|
+
|
|
438
|
+
# Get the user's default editor
|
|
439
|
+
editor = os.environ.get('EDITOR')
|
|
440
|
+
if not editor:
|
|
441
|
+
# Try common editors in order of preference
|
|
442
|
+
for common_editor in ['vim', 'nano', 'code', 'subl', 'atom', 'emacs']:
|
|
443
|
+
if subprocess.run(['which', common_editor], capture_output=True).returncode == 0:
|
|
444
|
+
editor = common_editor
|
|
445
|
+
break
|
|
446
|
+
|
|
447
|
+
if not editor:
|
|
448
|
+
click.echo("❌ No editor found. Please set the EDITOR environment variable or install vim/nano.")
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
# Create a temporary file with the template
|
|
452
|
+
template = get_command_template(command_name, command_group)
|
|
453
|
+
|
|
454
|
+
# Add helpful comments to the template
|
|
455
|
+
enhanced_template = f'''"""
|
|
456
|
+
{command_name} command for mcli.{command_group}.
|
|
457
|
+
|
|
458
|
+
Description: {description}
|
|
459
|
+
|
|
460
|
+
Instructions:
|
|
461
|
+
1. Write your Python command logic below
|
|
462
|
+
2. Use Click decorators for command definition
|
|
463
|
+
3. Save and close the editor to create the command
|
|
464
|
+
4. The command will be automatically converted to JSON format
|
|
465
|
+
|
|
466
|
+
Example Click command structure:
|
|
467
|
+
@click.command()
|
|
468
|
+
@click.argument('name', default='World')
|
|
469
|
+
def my_command(name):
|
|
470
|
+
\"\"\"My custom command.\"\"\"
|
|
471
|
+
click.echo(f"Hello, {{name}}!")
|
|
472
|
+
"""
|
|
473
|
+
import click
|
|
474
|
+
from typing import Optional, List
|
|
475
|
+
from pathlib import Path
|
|
476
|
+
from mcli.lib.logger.logger import get_logger
|
|
630
477
|
|
|
631
|
-
|
|
478
|
+
logger = get_logger()
|
|
632
479
|
|
|
480
|
+
# Write your command logic here:
|
|
481
|
+
# Replace this template with your actual command implementation
|
|
633
482
|
|
|
634
|
-
|
|
635
|
-
def update_lockfile():
|
|
636
|
-
"""
|
|
637
|
-
Update the commands lockfile with current state.
|
|
638
|
-
"""
|
|
639
|
-
manager = get_command_manager()
|
|
483
|
+
{template.split('"""')[2].split('"""')[0] if '"""' in template else ''}
|
|
640
484
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
485
|
+
# Your command implementation goes here:
|
|
486
|
+
# Example:
|
|
487
|
+
# @click.command()
|
|
488
|
+
# @click.argument('name', default='World')
|
|
489
|
+
# def {command_name}_command(name):
|
|
490
|
+
# \"\"\"{description}\"\"\"
|
|
491
|
+
# logger.info(f"Executing {command_name} command with name: {{name}}")
|
|
492
|
+
# click.echo(f"Hello, {{name}}! This is the {command_name} command.")
|
|
493
|
+
'''
|
|
494
|
+
|
|
495
|
+
# Create temporary file
|
|
496
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
|
|
497
|
+
temp_file.write(enhanced_template)
|
|
498
|
+
temp_file_path = temp_file.name
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
# Check if we're in an interactive environment
|
|
502
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
503
|
+
click.echo("❌ Editor requires an interactive terminal. Use --template flag for non-interactive mode.")
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
# Open editor
|
|
507
|
+
click.echo(f"📝 Opening {editor} to edit command logic...")
|
|
508
|
+
click.echo("💡 Write your Python command logic and save the file to continue.")
|
|
509
|
+
click.echo("💡 Press Ctrl+C to cancel command creation.")
|
|
510
|
+
|
|
511
|
+
# Run the editor
|
|
512
|
+
result = subprocess.run([editor, temp_file_path], check=False)
|
|
513
|
+
|
|
514
|
+
if result.returncode != 0:
|
|
515
|
+
click.echo("❌ Editor exited with error. Command creation cancelled.")
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
# Read the edited content
|
|
519
|
+
with open(temp_file_path, 'r') as f:
|
|
520
|
+
edited_code = f.read()
|
|
521
|
+
|
|
522
|
+
# Check if the file was actually edited (not just the template)
|
|
523
|
+
if edited_code.strip() == enhanced_template.strip():
|
|
524
|
+
click.echo("⚠️ No changes detected. Command creation cancelled.")
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
# Extract the actual command code (remove the instructions)
|
|
528
|
+
lines = edited_code.split('\n')
|
|
529
|
+
code_lines = []
|
|
530
|
+
in_code_section = False
|
|
531
|
+
|
|
532
|
+
for line in lines:
|
|
533
|
+
if line.strip().startswith('# Your command implementation goes here:'):
|
|
534
|
+
in_code_section = True
|
|
535
|
+
continue
|
|
536
|
+
if in_code_section:
|
|
537
|
+
code_lines.append(line)
|
|
538
|
+
|
|
539
|
+
if not code_lines or not any(line.strip() for line in code_lines):
|
|
540
|
+
# Fallback: use the entire file content
|
|
541
|
+
code_lines = lines
|
|
542
|
+
|
|
543
|
+
final_code = '\n'.join(code_lines).strip()
|
|
544
|
+
|
|
545
|
+
if not final_code:
|
|
546
|
+
click.echo("❌ No command code found. Command creation cancelled.")
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
click.echo("✅ Command code captured successfully!")
|
|
550
|
+
return final_code
|
|
551
|
+
|
|
552
|
+
except KeyboardInterrupt:
|
|
553
|
+
click.echo("\n❌ Command creation cancelled by user.")
|
|
554
|
+
return None
|
|
555
|
+
except Exception as e:
|
|
556
|
+
click.echo(f"❌ Error opening editor: {e}")
|
|
557
|
+
return None
|
|
558
|
+
finally:
|
|
559
|
+
# Clean up temporary file
|
|
560
|
+
try:
|
|
561
|
+
os.unlink(temp_file_path)
|
|
562
|
+
except OSError:
|
|
563
|
+
pass
|
|
647
564
|
|
|
648
565
|
|
|
649
566
|
@self_app.command("extract-workflow-commands")
|
|
@@ -971,265 +888,21 @@ def hello(name: str):
|
|
|
971
888
|
|
|
972
889
|
|
|
973
890
|
@self_app.command("logs")
|
|
974
|
-
|
|
975
|
-
"--type",
|
|
976
|
-
"-t",
|
|
977
|
-
type=click.Choice(["main", "system", "trace", "all"]),
|
|
978
|
-
default="main",
|
|
979
|
-
help="Type of logs to display",
|
|
980
|
-
)
|
|
981
|
-
@click.option("--lines", "-n", default=50, help="Number of lines to show (default: 50)")
|
|
982
|
-
@click.option("--follow", "-f", is_flag=True, help="Follow log output in real-time")
|
|
983
|
-
@click.option("--date", "-d", help="Show logs for specific date (YYYYMMDD format)")
|
|
984
|
-
@click.option("--grep", "-g", help="Filter logs by pattern")
|
|
985
|
-
@click.option(
|
|
986
|
-
"--level",
|
|
987
|
-
"-l",
|
|
988
|
-
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
|
|
989
|
-
help="Filter logs by minimum level",
|
|
990
|
-
)
|
|
991
|
-
def logs(type: str, lines: int, follow: bool, date: str, grep: str, level: str):
|
|
891
|
+
def logs():
|
|
992
892
|
"""
|
|
993
|
-
Display runtime logs
|
|
893
|
+
[DEPRECATED] Display runtime logs - Use 'mcli logs' instead.
|
|
994
894
|
|
|
995
|
-
|
|
996
|
-
Supports filtering by log type, date, content, and log level.
|
|
997
|
-
|
|
998
|
-
Log files are named as mcli_YYYYMMDD.log, mcli_system_YYYYMMDD.log, mcli_trace_YYYYMMDD.log.
|
|
895
|
+
This command has been moved to 'mcli logs' with enhanced features.
|
|
999
896
|
"""
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
logs_dir = get_logs_dir()
|
|
1010
|
-
|
|
1011
|
-
if not logs_dir.exists():
|
|
1012
|
-
click.echo("❌ Logs directory not found", err=True)
|
|
1013
|
-
click.echo(f"Expected location: {logs_dir}", err=True)
|
|
1014
|
-
return
|
|
1015
|
-
|
|
1016
|
-
# Determine which log files to read
|
|
1017
|
-
log_files = []
|
|
1018
|
-
|
|
1019
|
-
if type == "all":
|
|
1020
|
-
# Get all log files for the specified date or latest
|
|
1021
|
-
if date:
|
|
1022
|
-
# Look for files like mcli_20250709.log, mcli_system_20250709.log, mcli_trace_20250709.log
|
|
1023
|
-
patterns = [f"mcli_{date}.log", f"mcli_system_{date}.log", f"mcli_trace_{date}.log"]
|
|
1024
|
-
else:
|
|
1025
|
-
# Get the most recent log files
|
|
1026
|
-
patterns = ["mcli_*.log"]
|
|
1027
|
-
|
|
1028
|
-
log_files = []
|
|
1029
|
-
for pattern in patterns:
|
|
1030
|
-
files = list(logs_dir.glob(pattern))
|
|
1031
|
-
if files:
|
|
1032
|
-
# Sort by modification time (newest first)
|
|
1033
|
-
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
|
1034
|
-
log_files.extend(files)
|
|
1035
|
-
|
|
1036
|
-
# Remove duplicates and take only the most recent files of each type
|
|
1037
|
-
seen_types = set()
|
|
1038
|
-
filtered_files = []
|
|
1039
|
-
for log_file in log_files:
|
|
1040
|
-
# Extract log type from filename
|
|
1041
|
-
# mcli_20250709 -> main
|
|
1042
|
-
# mcli_system_20250709 -> system
|
|
1043
|
-
# mcli_trace_20250709 -> trace
|
|
1044
|
-
if log_file.name.startswith("mcli_system_"):
|
|
1045
|
-
log_type = "system"
|
|
1046
|
-
elif log_file.name.startswith("mcli_trace_"):
|
|
1047
|
-
log_type = "trace"
|
|
1048
|
-
else:
|
|
1049
|
-
log_type = "main"
|
|
1050
|
-
|
|
1051
|
-
if log_type not in seen_types:
|
|
1052
|
-
seen_types.add(log_type)
|
|
1053
|
-
filtered_files.append(log_file)
|
|
1054
|
-
|
|
1055
|
-
log_files = filtered_files
|
|
1056
|
-
else:
|
|
1057
|
-
# Get specific log type
|
|
1058
|
-
if date:
|
|
1059
|
-
if type == "main":
|
|
1060
|
-
filename = f"mcli_{date}.log"
|
|
1061
|
-
else:
|
|
1062
|
-
filename = f"mcli_{type}_{date}.log"
|
|
1063
|
-
else:
|
|
1064
|
-
# Find the most recent file for this type
|
|
1065
|
-
if type == "main":
|
|
1066
|
-
pattern = "mcli_*.log"
|
|
1067
|
-
# Exclude system and trace files
|
|
1068
|
-
exclude_patterns = ["mcli_system_*.log", "mcli_trace_*.log"]
|
|
1069
|
-
else:
|
|
1070
|
-
pattern = f"mcli_{type}_*.log"
|
|
1071
|
-
exclude_patterns = []
|
|
1072
|
-
|
|
1073
|
-
files = list(logs_dir.glob(pattern))
|
|
1074
|
-
|
|
1075
|
-
# Filter out excluded patterns
|
|
1076
|
-
if exclude_patterns:
|
|
1077
|
-
filtered_files = []
|
|
1078
|
-
for file in files:
|
|
1079
|
-
excluded = False
|
|
1080
|
-
for exclude_pattern in exclude_patterns:
|
|
1081
|
-
if file.match(exclude_pattern):
|
|
1082
|
-
excluded = True
|
|
1083
|
-
break
|
|
1084
|
-
if not excluded:
|
|
1085
|
-
filtered_files.append(file)
|
|
1086
|
-
files = filtered_files
|
|
1087
|
-
|
|
1088
|
-
if files:
|
|
1089
|
-
# Sort by modification time and take the most recent
|
|
1090
|
-
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
|
1091
|
-
filename = files[0].name
|
|
1092
|
-
else:
|
|
1093
|
-
click.echo(f"❌ No {type} log files found", err=True)
|
|
1094
|
-
return
|
|
1095
|
-
|
|
1096
|
-
log_file = logs_dir / filename
|
|
1097
|
-
if log_file.exists():
|
|
1098
|
-
log_files = [log_file]
|
|
1099
|
-
else:
|
|
1100
|
-
click.echo(f"❌ Log file not found: {filename}", err=True)
|
|
1101
|
-
return
|
|
1102
|
-
|
|
1103
|
-
if not log_files:
|
|
1104
|
-
click.echo("❌ No log files found", err=True)
|
|
1105
|
-
return
|
|
1106
|
-
|
|
1107
|
-
# Display log file information
|
|
1108
|
-
click.echo(f"📋 Showing logs from {len(log_files)} file(s):")
|
|
1109
|
-
for log_file in log_files:
|
|
1110
|
-
size_mb = log_file.stat().st_size / (1024 * 1024)
|
|
1111
|
-
modified = datetime.fromtimestamp(log_file.stat().st_mtime)
|
|
1112
|
-
click.echo(
|
|
1113
|
-
f" 📄 {log_file.name} ({size_mb:.1f}MB, modified {modified.strftime('%Y-%m-%d %H:%M:%S')})"
|
|
1114
|
-
)
|
|
1115
|
-
click.echo()
|
|
1116
|
-
|
|
1117
|
-
# Process each log file
|
|
1118
|
-
for log_file in log_files:
|
|
1119
|
-
click.echo(f"🔍 Reading: {log_file.name}")
|
|
1120
|
-
click.echo("─" * 80)
|
|
1121
|
-
|
|
1122
|
-
try:
|
|
1123
|
-
# Read the file content
|
|
1124
|
-
with open(log_file, "r") as f:
|
|
1125
|
-
content = f.readlines()
|
|
1126
|
-
|
|
1127
|
-
# Apply filters
|
|
1128
|
-
filtered_lines = []
|
|
1129
|
-
for line in content:
|
|
1130
|
-
# Apply grep filter
|
|
1131
|
-
if grep and grep.lower() not in line.lower():
|
|
1132
|
-
continue
|
|
1133
|
-
|
|
1134
|
-
# Apply level filter
|
|
1135
|
-
if level:
|
|
1136
|
-
level_pattern = rf"\b{level}\b"
|
|
1137
|
-
if not re.search(level_pattern, line, re.IGNORECASE):
|
|
1138
|
-
# Check if line has a lower level than requested
|
|
1139
|
-
level_order = {
|
|
1140
|
-
"DEBUG": 0,
|
|
1141
|
-
"INFO": 1,
|
|
1142
|
-
"WARNING": 2,
|
|
1143
|
-
"ERROR": 3,
|
|
1144
|
-
"CRITICAL": 4,
|
|
1145
|
-
}
|
|
1146
|
-
requested_level = level_order.get(level.upper(), 0)
|
|
1147
|
-
|
|
1148
|
-
# Check if line contains any log level
|
|
1149
|
-
found_level = None
|
|
1150
|
-
for log_level in level_order:
|
|
1151
|
-
if log_level in line.upper():
|
|
1152
|
-
found_level = level_order[log_level]
|
|
1153
|
-
break
|
|
1154
|
-
|
|
1155
|
-
if found_level is None or found_level < requested_level:
|
|
1156
|
-
continue
|
|
1157
|
-
|
|
1158
|
-
filtered_lines.append(line)
|
|
1159
|
-
|
|
1160
|
-
# Show the last N lines
|
|
1161
|
-
if lines > 0:
|
|
1162
|
-
filtered_lines = filtered_lines[-lines:]
|
|
1163
|
-
|
|
1164
|
-
# Display the lines
|
|
1165
|
-
for line in filtered_lines:
|
|
1166
|
-
# Colorize log levels
|
|
1167
|
-
colored_line = line
|
|
1168
|
-
if "ERROR" in line or "CRITICAL" in line:
|
|
1169
|
-
colored_line = click.style(line, fg="red")
|
|
1170
|
-
elif "WARNING" in line:
|
|
1171
|
-
colored_line = click.style(line, fg="yellow")
|
|
1172
|
-
elif "INFO" in line:
|
|
1173
|
-
colored_line = click.style(line, fg="green")
|
|
1174
|
-
elif "DEBUG" in line:
|
|
1175
|
-
colored_line = click.style(line, fg="blue")
|
|
1176
|
-
|
|
1177
|
-
click.echo(colored_line.rstrip())
|
|
1178
|
-
|
|
1179
|
-
if not filtered_lines:
|
|
1180
|
-
click.echo("(No matching log entries found)")
|
|
1181
|
-
|
|
1182
|
-
except Exception as e:
|
|
1183
|
-
click.echo(f"❌ Error reading log file {log_file.name}: {e}", err=True)
|
|
1184
|
-
|
|
1185
|
-
click.echo()
|
|
1186
|
-
|
|
1187
|
-
if follow:
|
|
1188
|
-
click.echo("🔄 Following log output... (Press Ctrl+C to stop)")
|
|
1189
|
-
try:
|
|
1190
|
-
# Use tail -f for real-time following
|
|
1191
|
-
for log_file in log_files:
|
|
1192
|
-
click.echo(f"📡 Following: {log_file.name}")
|
|
1193
|
-
process = subprocess.Popen(
|
|
1194
|
-
["tail", "-f", str(log_file)],
|
|
1195
|
-
stdout=subprocess.PIPE,
|
|
1196
|
-
stderr=subprocess.PIPE,
|
|
1197
|
-
text=True,
|
|
1198
|
-
)
|
|
1199
|
-
|
|
1200
|
-
try:
|
|
1201
|
-
if process.stdout:
|
|
1202
|
-
for line in process.stdout:
|
|
1203
|
-
# Apply filters to real-time output
|
|
1204
|
-
if grep and grep.lower() not in line.lower():
|
|
1205
|
-
continue
|
|
1206
|
-
|
|
1207
|
-
if level:
|
|
1208
|
-
level_pattern = rf"\b{level}\b"
|
|
1209
|
-
if not re.search(level_pattern, line, re.IGNORECASE):
|
|
1210
|
-
continue
|
|
1211
|
-
|
|
1212
|
-
# Colorize and display
|
|
1213
|
-
colored_line = line
|
|
1214
|
-
if "ERROR" in line or "CRITICAL" in line:
|
|
1215
|
-
colored_line = click.style(line, fg="red")
|
|
1216
|
-
elif "WARNING" in line:
|
|
1217
|
-
colored_line = click.style(line, fg="yellow")
|
|
1218
|
-
elif "INFO" in line:
|
|
1219
|
-
colored_line = click.style(line, fg="green")
|
|
1220
|
-
elif "DEBUG" in line:
|
|
1221
|
-
colored_line = click.style(line, fg="blue")
|
|
1222
|
-
|
|
1223
|
-
click.echo(colored_line.rstrip())
|
|
1224
|
-
|
|
1225
|
-
except KeyboardInterrupt:
|
|
1226
|
-
process.terminate()
|
|
1227
|
-
break
|
|
1228
|
-
|
|
1229
|
-
except KeyboardInterrupt:
|
|
1230
|
-
click.echo("\n🛑 Stopped following logs")
|
|
1231
|
-
except Exception as e:
|
|
1232
|
-
click.echo(f"❌ Error following logs: {e}", err=True)
|
|
897
|
+
console.print("\n[yellow]⚠️ DEPRECATED:[/yellow] This command has been moved.")
|
|
898
|
+
console.print("\n[cyan]New usage:[/cyan]")
|
|
899
|
+
console.print(" mcli logs [bold]stream[/bold] - Stream logs in real-time")
|
|
900
|
+
console.print(" mcli logs [bold]list[/bold] - List available log files")
|
|
901
|
+
console.print(" mcli logs [bold]tail[/bold] - Tail recent log entries")
|
|
902
|
+
console.print(" mcli logs [bold]grep[/bold] - Search in log files")
|
|
903
|
+
console.print(" mcli logs [bold]location[/bold] - Show logs directory")
|
|
904
|
+
console.print(" mcli logs [bold]clear[/bold] - Clear old log files")
|
|
905
|
+
console.print("\n[dim]Run 'mcli logs --help' for more information[/dim]\n")
|
|
1233
906
|
|
|
1234
907
|
|
|
1235
908
|
@self_app.command("performance")
|
|
@@ -1557,234 +1230,6 @@ def update(check: bool, pre: bool, yes: bool, skip_ci_check: bool):
|
|
|
1557
1230
|
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
1558
1231
|
|
|
1559
1232
|
|
|
1560
|
-
@self_app.command("import-script")
|
|
1561
|
-
@click.argument("script_path", type=click.Path(exists=True))
|
|
1562
|
-
@click.option("--name", "-n", help="Command name (defaults to script filename)")
|
|
1563
|
-
@click.option("--group", "-g", default="workflow", help="Command group")
|
|
1564
|
-
@click.option("--description", "-d", help="Command description")
|
|
1565
|
-
@click.option("--interactive", "-i", is_flag=True, help="Open in $EDITOR for review/editing")
|
|
1566
|
-
def import_script(script_path, name, group, description, interactive):
|
|
1567
|
-
"""
|
|
1568
|
-
Import a Python script as a portable JSON command.
|
|
1569
|
-
|
|
1570
|
-
Converts a Python script into a JSON command that can be loaded
|
|
1571
|
-
by mcli. The script should define Click commands.
|
|
1572
|
-
|
|
1573
|
-
Examples:
|
|
1574
|
-
mcli self import-script my_script.py
|
|
1575
|
-
mcli self import-script my_script.py --name custom-cmd --interactive
|
|
1576
|
-
"""
|
|
1577
|
-
import subprocess
|
|
1578
|
-
import tempfile
|
|
1579
|
-
|
|
1580
|
-
script_file = Path(script_path).resolve()
|
|
1581
|
-
|
|
1582
|
-
if not script_file.exists():
|
|
1583
|
-
click.echo(f"❌ Script not found: {script_file}", err=True)
|
|
1584
|
-
return 1
|
|
1585
|
-
|
|
1586
|
-
# Read the script content
|
|
1587
|
-
try:
|
|
1588
|
-
with open(script_file, 'r') as f:
|
|
1589
|
-
code = f.read()
|
|
1590
|
-
except Exception as e:
|
|
1591
|
-
click.echo(f"❌ Failed to read script: {e}", err=True)
|
|
1592
|
-
return 1
|
|
1593
|
-
|
|
1594
|
-
# Determine command name
|
|
1595
|
-
if not name:
|
|
1596
|
-
name = script_file.stem.lower().replace("-", "_")
|
|
1597
|
-
|
|
1598
|
-
# Validate command name
|
|
1599
|
-
if not re.match(r"^[a-z][a-z0-9_]*$", name):
|
|
1600
|
-
click.echo(f"❌ Invalid command name: {name}", err=True)
|
|
1601
|
-
return 1
|
|
1602
|
-
|
|
1603
|
-
# Interactive editing
|
|
1604
|
-
if interactive:
|
|
1605
|
-
editor = os.environ.get('EDITOR', 'vim')
|
|
1606
|
-
click.echo(f"📝 Opening in {editor} for review...")
|
|
1607
|
-
|
|
1608
|
-
# Create temp file with the code
|
|
1609
|
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as tmp:
|
|
1610
|
-
tmp.write(code)
|
|
1611
|
-
tmp_path = tmp.name
|
|
1612
|
-
|
|
1613
|
-
try:
|
|
1614
|
-
subprocess.run([editor, tmp_path], check=True)
|
|
1615
|
-
|
|
1616
|
-
# Read edited content
|
|
1617
|
-
with open(tmp_path, 'r') as f:
|
|
1618
|
-
code = f.read()
|
|
1619
|
-
finally:
|
|
1620
|
-
Path(tmp_path).unlink(missing_ok=True)
|
|
1621
|
-
|
|
1622
|
-
# Get description
|
|
1623
|
-
if not description:
|
|
1624
|
-
# Try to extract from docstring
|
|
1625
|
-
import ast
|
|
1626
|
-
try:
|
|
1627
|
-
tree = ast.parse(code)
|
|
1628
|
-
description = ast.get_docstring(tree) or f"Imported from {script_file.name}"
|
|
1629
|
-
except:
|
|
1630
|
-
description = f"Imported from {script_file.name}"
|
|
1631
|
-
|
|
1632
|
-
# Save as JSON command
|
|
1633
|
-
manager = get_command_manager()
|
|
1634
|
-
|
|
1635
|
-
saved_path = manager.save_command(
|
|
1636
|
-
name=name,
|
|
1637
|
-
code=code,
|
|
1638
|
-
description=description,
|
|
1639
|
-
group=group,
|
|
1640
|
-
metadata={
|
|
1641
|
-
"source": "import-script",
|
|
1642
|
-
"original_file": str(script_file),
|
|
1643
|
-
"imported_at": datetime.now().isoformat()
|
|
1644
|
-
}
|
|
1645
|
-
)
|
|
1646
|
-
|
|
1647
|
-
click.echo(f"✅ Imported script as command: {name}")
|
|
1648
|
-
click.echo(f"📁 Saved to: {saved_path}")
|
|
1649
|
-
click.echo(f"🔄 Use with: mcli {group} {name}")
|
|
1650
|
-
click.echo(f"💡 Command will be available after restart or reload")
|
|
1651
|
-
|
|
1652
|
-
return 0
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
@self_app.command("export-script")
|
|
1656
|
-
@click.argument("command_name")
|
|
1657
|
-
@click.option("--output", "-o", type=click.Path(), help="Output file path")
|
|
1658
|
-
@click.option("--standalone", "-s", is_flag=True, help="Make script standalone (add if __name__ == '__main__')")
|
|
1659
|
-
def export_script(command_name, output, standalone):
|
|
1660
|
-
"""
|
|
1661
|
-
Export a JSON command to a Python script.
|
|
1662
|
-
|
|
1663
|
-
Converts a portable JSON command back to a standalone Python script
|
|
1664
|
-
that can be edited and run independently.
|
|
1665
|
-
|
|
1666
|
-
Examples:
|
|
1667
|
-
mcli self export-script my-command
|
|
1668
|
-
mcli self export-script my-command --output my_script.py --standalone
|
|
1669
|
-
"""
|
|
1670
|
-
manager = get_command_manager()
|
|
1671
|
-
|
|
1672
|
-
# Load the command
|
|
1673
|
-
command_file = manager.commands_dir / f"{command_name}.json"
|
|
1674
|
-
if not command_file.exists():
|
|
1675
|
-
click.echo(f"❌ Command not found: {command_name}", err=True)
|
|
1676
|
-
return 1
|
|
1677
|
-
|
|
1678
|
-
try:
|
|
1679
|
-
with open(command_file, 'r') as f:
|
|
1680
|
-
command_data = json.load(f)
|
|
1681
|
-
except Exception as e:
|
|
1682
|
-
click.echo(f"❌ Failed to load command: {e}", err=True)
|
|
1683
|
-
return 1
|
|
1684
|
-
|
|
1685
|
-
# Get the code
|
|
1686
|
-
code = command_data.get('code', '')
|
|
1687
|
-
|
|
1688
|
-
if not code:
|
|
1689
|
-
click.echo(f"❌ Command has no code: {command_name}", err=True)
|
|
1690
|
-
return 1
|
|
1691
|
-
|
|
1692
|
-
# Add standalone wrapper if requested
|
|
1693
|
-
if standalone:
|
|
1694
|
-
# Check if already has if __name__ == '__main__'
|
|
1695
|
-
if "if __name__" not in code:
|
|
1696
|
-
code += "\n\nif __name__ == '__main__':\n app()\n"
|
|
1697
|
-
|
|
1698
|
-
# Determine output path
|
|
1699
|
-
if not output:
|
|
1700
|
-
output = f"{command_name}.py"
|
|
1701
|
-
|
|
1702
|
-
output_file = Path(output)
|
|
1703
|
-
|
|
1704
|
-
# Write the script
|
|
1705
|
-
try:
|
|
1706
|
-
with open(output_file, 'w') as f:
|
|
1707
|
-
f.write(code)
|
|
1708
|
-
except Exception as e:
|
|
1709
|
-
click.echo(f"❌ Failed to write script: {e}", err=True)
|
|
1710
|
-
return 1
|
|
1711
|
-
|
|
1712
|
-
click.echo(f"✅ Exported command to script: {output_file}")
|
|
1713
|
-
click.echo(f"📝 Source command: {command_name}")
|
|
1714
|
-
|
|
1715
|
-
if standalone:
|
|
1716
|
-
click.echo(f"🚀 Run standalone with: python {output_file}")
|
|
1717
|
-
|
|
1718
|
-
click.echo(f"💡 Edit and re-import with: mcli self import-script {output_file}")
|
|
1719
|
-
|
|
1720
|
-
return 0
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
@self_app.command("edit-command")
|
|
1724
|
-
@click.argument("command_name")
|
|
1725
|
-
@click.option("--editor", "-e", help="Editor to use (defaults to $EDITOR)")
|
|
1726
|
-
def edit_command(command_name, editor):
|
|
1727
|
-
"""
|
|
1728
|
-
Edit a command interactively using $EDITOR.
|
|
1729
|
-
|
|
1730
|
-
Opens the command's Python code in your preferred editor,
|
|
1731
|
-
allows you to make changes, and saves the updated version.
|
|
1732
|
-
|
|
1733
|
-
Examples:
|
|
1734
|
-
mcli self edit-command my-command
|
|
1735
|
-
mcli self edit-command my-command --editor code
|
|
1736
|
-
"""
|
|
1737
|
-
import subprocess
|
|
1738
|
-
import tempfile
|
|
1739
|
-
|
|
1740
|
-
manager = get_command_manager()
|
|
1741
|
-
|
|
1742
|
-
# Load the command
|
|
1743
|
-
command_file = manager.commands_dir / f"{command_name}.json"
|
|
1744
|
-
if not command_file.exists():
|
|
1745
|
-
click.echo(f"❌ Command not found: {command_name}", err=True)
|
|
1746
|
-
return 1
|
|
1747
|
-
|
|
1748
|
-
try:
|
|
1749
|
-
with open(command_file, 'r') as f:
|
|
1750
|
-
command_data = json.load(f)
|
|
1751
|
-
except Exception as e:
|
|
1752
|
-
click.echo(f"❌ Failed to load command: {e}", err=True)
|
|
1753
|
-
return 1
|
|
1754
|
-
|
|
1755
|
-
code = command_data.get('code', '')
|
|
1756
|
-
|
|
1757
|
-
if not code:
|
|
1758
|
-
click.echo(f"❌ Command has no code: {command_name}", err=True)
|
|
1759
|
-
return 1
|
|
1760
|
-
|
|
1761
|
-
# Determine editor
|
|
1762
|
-
if not editor:
|
|
1763
|
-
editor = os.environ.get('EDITOR', 'vim')
|
|
1764
|
-
|
|
1765
|
-
click.echo(f"📝 Opening command in {editor}...")
|
|
1766
|
-
|
|
1767
|
-
# Create temp file with the code
|
|
1768
|
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False,
|
|
1769
|
-
prefix=f"{command_name}_") as tmp:
|
|
1770
|
-
tmp.write(code)
|
|
1771
|
-
tmp_path = tmp.name
|
|
1772
|
-
|
|
1773
|
-
try:
|
|
1774
|
-
# Open in editor
|
|
1775
|
-
result = subprocess.run([editor, tmp_path])
|
|
1776
|
-
|
|
1777
|
-
if result.returncode != 0:
|
|
1778
|
-
click.echo(f"⚠️ Editor exited with code {result.returncode}")
|
|
1779
|
-
|
|
1780
|
-
# Read edited content
|
|
1781
|
-
with open(tmp_path, 'r') as f:
|
|
1782
|
-
new_code = f.read()
|
|
1783
|
-
|
|
1784
|
-
# Check if code changed
|
|
1785
|
-
if new_code.strip() == code.strip():
|
|
1786
|
-
click.echo("ℹ️ No changes made")
|
|
1787
|
-
return 0
|
|
1788
1233
|
|
|
1789
1234
|
# Validate syntax
|
|
1790
1235
|
try:
|