mcli-framework 7.0.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/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
mcli/self/self_cmd.py
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Self-management commands for mcli.
|
|
3
|
+
Provides utilities for maintaining and extending the CLI itself.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import importlib
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
import tomli
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.prompt import Prompt
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import warnings
|
|
25
|
+
|
|
26
|
+
# Suppress the warning about python-Levenshtein
|
|
27
|
+
warnings.filterwarnings("ignore", message="Using slow pure-python SequenceMatcher")
|
|
28
|
+
from fuzzywuzzy import process
|
|
29
|
+
except ImportError:
|
|
30
|
+
process = None
|
|
31
|
+
|
|
32
|
+
from mcli.lib.logger.logger import get_logger
|
|
33
|
+
|
|
34
|
+
logger = get_logger()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Create a Click command group instead of Typer
|
|
38
|
+
@click.group(name="self", help="Manage and extend the mcli application")
|
|
39
|
+
def self_app():
|
|
40
|
+
"""
|
|
41
|
+
Self-management commands for mcli.
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
console = Console()
|
|
47
|
+
|
|
48
|
+
LOCKFILE_PATH = Path.home() / ".local" / "mcli" / "command_lock.json"
|
|
49
|
+
|
|
50
|
+
# Utility functions for command state lockfile
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_current_command_state():
|
|
54
|
+
"""Collect all command metadata (names, groups, etc.)"""
|
|
55
|
+
# This should use your actual command collection logic
|
|
56
|
+
# For now, use the collect_commands() function
|
|
57
|
+
return collect_commands()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def hash_command_state(commands):
|
|
61
|
+
"""Hash the command state for fast comparison."""
|
|
62
|
+
# Sort for deterministic hash
|
|
63
|
+
commands_sorted = sorted(commands, key=lambda c: (c.get("group") or "", c["name"]))
|
|
64
|
+
state_json = json.dumps(commands_sorted, sort_keys=True)
|
|
65
|
+
return hashlib.sha256(state_json.encode("utf-8")).hexdigest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_lockfile():
|
|
69
|
+
if LOCKFILE_PATH.exists():
|
|
70
|
+
with open(LOCKFILE_PATH, "r") as f:
|
|
71
|
+
return json.load(f)
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def save_lockfile(states):
|
|
76
|
+
with open(LOCKFILE_PATH, "w") as f:
|
|
77
|
+
json.dump(states, f, indent=2, default=str)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def append_lockfile(new_state):
|
|
81
|
+
states = load_lockfile()
|
|
82
|
+
states.append(new_state)
|
|
83
|
+
save_lockfile(states)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def find_state_by_hash(hash_value):
|
|
87
|
+
states = load_lockfile()
|
|
88
|
+
for state in states:
|
|
89
|
+
if state["hash"] == hash_value:
|
|
90
|
+
return state
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def restore_command_state(hash_value):
|
|
95
|
+
state = find_state_by_hash(hash_value)
|
|
96
|
+
if not state:
|
|
97
|
+
return False
|
|
98
|
+
# Here you would implement logic to restore the command registry to this state
|
|
99
|
+
# For now, just print the commands
|
|
100
|
+
print(json.dumps(state["commands"], indent=2))
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Create a Click group for all command management
|
|
105
|
+
@self_app.group("commands")
|
|
106
|
+
def commands_group():
|
|
107
|
+
"""Manage CLI commands and command state."""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Move the command-state group under commands_group
|
|
112
|
+
@commands_group.group("state")
|
|
113
|
+
def command_state():
|
|
114
|
+
"""Manage command state lockfile and history."""
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@command_state.command("list")
|
|
119
|
+
def list_states():
|
|
120
|
+
"""List all saved command states (hash, timestamp, #commands)."""
|
|
121
|
+
states = load_lockfile()
|
|
122
|
+
if not states:
|
|
123
|
+
click.echo("No command states found.")
|
|
124
|
+
return
|
|
125
|
+
table = Table(title="Command States")
|
|
126
|
+
table.add_column("Hash", style="cyan")
|
|
127
|
+
table.add_column("Timestamp", style="green")
|
|
128
|
+
table.add_column("# Commands", style="yellow")
|
|
129
|
+
for state in states:
|
|
130
|
+
table.add_row(state["hash"][:8], state["timestamp"], str(len(state["commands"])))
|
|
131
|
+
console.print(table)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@command_state.command("restore")
|
|
135
|
+
@click.argument("hash_value")
|
|
136
|
+
def restore_state(hash_value):
|
|
137
|
+
"""Restore to a previous command state by hash."""
|
|
138
|
+
if restore_command_state(hash_value):
|
|
139
|
+
click.echo(f"Restored to state {hash_value[:8]}")
|
|
140
|
+
else:
|
|
141
|
+
click.echo(f"State {hash_value[:8]} not found.", err=True)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@command_state.command("write")
|
|
145
|
+
@click.argument("json_file", required=False, type=click.Path(exists=False))
|
|
146
|
+
def write_state(json_file):
|
|
147
|
+
"""Write a new command state to the lockfile from a JSON file or the current app state."""
|
|
148
|
+
import traceback
|
|
149
|
+
|
|
150
|
+
print("[DEBUG] write_state called")
|
|
151
|
+
print(f"[DEBUG] LOCKFILE_PATH: {LOCKFILE_PATH}")
|
|
152
|
+
try:
|
|
153
|
+
if json_file:
|
|
154
|
+
print(f"[DEBUG] Loading command state from file: {json_file}")
|
|
155
|
+
with open(json_file, "r") as f:
|
|
156
|
+
commands = json.load(f)
|
|
157
|
+
click.echo(f"Loaded command state from {json_file}.")
|
|
158
|
+
else:
|
|
159
|
+
print("[DEBUG] Snapshotting current command state.")
|
|
160
|
+
commands = get_current_command_state()
|
|
161
|
+
state_hash = hash_command_state(commands)
|
|
162
|
+
new_state = {
|
|
163
|
+
"hash": state_hash,
|
|
164
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
165
|
+
"commands": commands,
|
|
166
|
+
}
|
|
167
|
+
append_lockfile(new_state)
|
|
168
|
+
print(f"[DEBUG] Wrote new command state {state_hash[:8]} to lockfile at {LOCKFILE_PATH}")
|
|
169
|
+
click.echo(f"Wrote new command state {state_hash[:8]} to lockfile.")
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"[ERROR] Exception in write_state: {e}")
|
|
172
|
+
print(traceback.format_exc())
|
|
173
|
+
click.echo(f"[ERROR] Failed to write command state: {e}", err=True)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# On CLI startup, check and update lockfile if needed
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def check_and_update_command_lockfile():
|
|
180
|
+
current_commands = get_current_command_state()
|
|
181
|
+
current_hash = hash_command_state(current_commands)
|
|
182
|
+
states = load_lockfile()
|
|
183
|
+
if states and states[-1]["hash"] == current_hash:
|
|
184
|
+
# No change
|
|
185
|
+
return
|
|
186
|
+
# New state, append
|
|
187
|
+
new_state = {
|
|
188
|
+
"hash": current_hash,
|
|
189
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
190
|
+
"commands": current_commands,
|
|
191
|
+
}
|
|
192
|
+
append_lockfile(new_state)
|
|
193
|
+
logger.info(f"Appended new command state {current_hash[:8]} to lockfile.")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Call this at the top of your CLI entrypoint (main.py or similar)
|
|
197
|
+
# check_and_update_command_lockfile()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_command_template(name: str, group: Optional[str] = None) -> str:
|
|
201
|
+
"""Generate template code for a new command."""
|
|
202
|
+
|
|
203
|
+
if group:
|
|
204
|
+
# Template for a command in a group using Click
|
|
205
|
+
template = f'''"""
|
|
206
|
+
{name} command for mcli.{group}.
|
|
207
|
+
"""
|
|
208
|
+
import click
|
|
209
|
+
from typing import Optional, List
|
|
210
|
+
from pathlib import Path
|
|
211
|
+
from mcli.lib.logger.logger import get_logger
|
|
212
|
+
|
|
213
|
+
logger = get_logger()
|
|
214
|
+
|
|
215
|
+
# Create a Click command group
|
|
216
|
+
@click.group(name="{name}")
|
|
217
|
+
def {name}_group():
|
|
218
|
+
"""Description for {name} command group."""
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
@{name}_group.command("hello")
|
|
222
|
+
@click.argument("name", default="World")
|
|
223
|
+
def hello(name: str):
|
|
224
|
+
"""Example subcommand."""
|
|
225
|
+
logger.info(f"Hello, {{name}}! This is the {name} command.")
|
|
226
|
+
click.echo(f"Hello, {{name}}! This is the {name} command.")
|
|
227
|
+
'''
|
|
228
|
+
else:
|
|
229
|
+
# Template for a command directly under self using Click
|
|
230
|
+
template = f'''"""
|
|
231
|
+
{name} command for mcli.self.
|
|
232
|
+
"""
|
|
233
|
+
import click
|
|
234
|
+
from typing import Optional, List
|
|
235
|
+
from pathlib import Path
|
|
236
|
+
from mcli.lib.logger.logger import get_logger
|
|
237
|
+
|
|
238
|
+
logger = get_logger()
|
|
239
|
+
|
|
240
|
+
def {name}_command(name: str = "World"):
|
|
241
|
+
"""
|
|
242
|
+
{name.capitalize()} command.
|
|
243
|
+
"""
|
|
244
|
+
logger.info(f"Hello, {{name}}! This is the {name} command.")
|
|
245
|
+
click.echo(f"Hello, {{name}}! This is the {name} command.")
|
|
246
|
+
'''
|
|
247
|
+
|
|
248
|
+
return template
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@self_app.command("search")
|
|
252
|
+
@click.argument("query", required=False)
|
|
253
|
+
@click.option("--full", "-f", is_flag=True, help="Show full command paths and descriptions")
|
|
254
|
+
def search(query, full):
|
|
255
|
+
"""
|
|
256
|
+
Search for available commands using fuzzy matching.
|
|
257
|
+
|
|
258
|
+
Similar to telescope in neovim, this allows quick fuzzy searching
|
|
259
|
+
through all available commands in mcli.
|
|
260
|
+
|
|
261
|
+
If no query is provided, lists all commands.
|
|
262
|
+
"""
|
|
263
|
+
# Collect all commands from the application
|
|
264
|
+
commands = collect_commands()
|
|
265
|
+
|
|
266
|
+
# Display the commands in a table
|
|
267
|
+
table = Table(title="mcli Commands")
|
|
268
|
+
table.add_column("Command", style="green")
|
|
269
|
+
table.add_column("Group", style="blue")
|
|
270
|
+
if full:
|
|
271
|
+
table.add_column("Path", style="dim")
|
|
272
|
+
table.add_column("Description", style="yellow")
|
|
273
|
+
|
|
274
|
+
if query:
|
|
275
|
+
filtered_commands = []
|
|
276
|
+
|
|
277
|
+
# Try to use fuzzywuzzy for better matching if available
|
|
278
|
+
if process:
|
|
279
|
+
# Extract command names for matching
|
|
280
|
+
command_names = [
|
|
281
|
+
f"{cmd['group']}.{cmd['name']}" if cmd["group"] else cmd["name"] for cmd in commands
|
|
282
|
+
]
|
|
283
|
+
matches = process.extract(query, command_names, limit=10)
|
|
284
|
+
|
|
285
|
+
# Filter to matched commands
|
|
286
|
+
match_indices = [command_names.index(match[0]) for match in matches if match[1] > 50]
|
|
287
|
+
filtered_commands = [commands[i] for i in match_indices]
|
|
288
|
+
else:
|
|
289
|
+
# Fallback to simple substring matching
|
|
290
|
+
filtered_commands = [
|
|
291
|
+
cmd
|
|
292
|
+
for cmd in commands
|
|
293
|
+
if query.lower() in cmd["name"].lower()
|
|
294
|
+
or (cmd["group"] and query.lower() in cmd["group"].lower())
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
commands = filtered_commands
|
|
298
|
+
|
|
299
|
+
# Sort commands by group then name
|
|
300
|
+
commands.sort(key=lambda c: (c["group"] if c["group"] else "", c["name"]))
|
|
301
|
+
|
|
302
|
+
# Add rows to the table
|
|
303
|
+
for cmd in commands:
|
|
304
|
+
if full:
|
|
305
|
+
table.add_row(
|
|
306
|
+
cmd["name"],
|
|
307
|
+
cmd["group"] if cmd["group"] else "-",
|
|
308
|
+
cmd["path"],
|
|
309
|
+
cmd["help"] if cmd["help"] else "",
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
table.add_row(cmd["name"], cmd["group"] if cmd["group"] else "-")
|
|
313
|
+
|
|
314
|
+
console.print(table)
|
|
315
|
+
|
|
316
|
+
if not commands:
|
|
317
|
+
logger.info("No commands found matching the search query")
|
|
318
|
+
click.echo("No commands found matching the search query")
|
|
319
|
+
|
|
320
|
+
return 0
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def collect_commands() -> List[Dict[str, Any]]:
|
|
324
|
+
"""Collect all commands from the mcli application."""
|
|
325
|
+
commands = []
|
|
326
|
+
|
|
327
|
+
# Look for command modules in the mcli package
|
|
328
|
+
mcli_path = Path(__file__).parent.parent
|
|
329
|
+
|
|
330
|
+
# This finds command groups as directories under mcli
|
|
331
|
+
for item in mcli_path.iterdir():
|
|
332
|
+
if item.is_dir() and not item.name.startswith("__") and not item.name.startswith("."):
|
|
333
|
+
group_name = item.name
|
|
334
|
+
|
|
335
|
+
# Recursively find all Python files that might define commands
|
|
336
|
+
for py_file in item.glob("**/*.py"):
|
|
337
|
+
if py_file.name.startswith("__"):
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
# Convert file path to module path
|
|
341
|
+
relative_path = py_file.relative_to(mcli_path.parent)
|
|
342
|
+
module_name = ".".join(relative_path.with_suffix("").parts)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
# Try to import the module
|
|
346
|
+
module = importlib.import_module(module_name)
|
|
347
|
+
|
|
348
|
+
# Extract command and group objects
|
|
349
|
+
for name, obj in inspect.getmembers(module):
|
|
350
|
+
# Handle Click commands and groups
|
|
351
|
+
if isinstance(obj, click.Command):
|
|
352
|
+
if isinstance(obj, click.Group):
|
|
353
|
+
# Found a Click group
|
|
354
|
+
app_info = {
|
|
355
|
+
"name": obj.name,
|
|
356
|
+
"group": group_name,
|
|
357
|
+
"path": module_name,
|
|
358
|
+
"help": obj.help,
|
|
359
|
+
}
|
|
360
|
+
commands.append(app_info)
|
|
361
|
+
|
|
362
|
+
# Add subcommands if any
|
|
363
|
+
for cmd_name, cmd in obj.commands.items():
|
|
364
|
+
commands.append(
|
|
365
|
+
{
|
|
366
|
+
"name": cmd_name,
|
|
367
|
+
"group": f"{group_name}.{app_info['name']}",
|
|
368
|
+
"path": f"{module_name}.{cmd_name}",
|
|
369
|
+
"help": cmd.help,
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
# Found a standalone Click command
|
|
374
|
+
commands.append(
|
|
375
|
+
{
|
|
376
|
+
"name": obj.name,
|
|
377
|
+
"group": group_name,
|
|
378
|
+
"path": f"{module_name}.{obj.name}",
|
|
379
|
+
"help": obj.help,
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
except (ImportError, AttributeError) as e:
|
|
383
|
+
logger.debug(f"Skipping {module_name}: {e}")
|
|
384
|
+
|
|
385
|
+
return commands
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@self_app.command("add-command")
|
|
389
|
+
@click.argument("command_name", required=True)
|
|
390
|
+
@click.option("--group", "-g", help="Optional command group to create under")
|
|
391
|
+
def add_command(command_name, group):
|
|
392
|
+
"""
|
|
393
|
+
Generate a new command template that can be used by mcli.
|
|
394
|
+
|
|
395
|
+
Example:
|
|
396
|
+
mcli self add my_command
|
|
397
|
+
mcli self add feature_command --group features
|
|
398
|
+
"""
|
|
399
|
+
command_name = command_name.lower().replace("-", "_")
|
|
400
|
+
|
|
401
|
+
# Validate command name
|
|
402
|
+
if not re.match(r"^[a-z][a-z0-9_]*$", command_name):
|
|
403
|
+
logger.error(
|
|
404
|
+
f"Invalid command name: {command_name}. Use lowercase letters, numbers, and underscores (starting with a letter)."
|
|
405
|
+
)
|
|
406
|
+
click.echo(
|
|
407
|
+
f"Invalid command name: {command_name}. Use lowercase letters, numbers, and underscores (starting with a letter).",
|
|
408
|
+
err=True,
|
|
409
|
+
)
|
|
410
|
+
return 1
|
|
411
|
+
|
|
412
|
+
mcli_path = Path(__file__).parent.parent
|
|
413
|
+
|
|
414
|
+
if group:
|
|
415
|
+
# Creating under a specific group
|
|
416
|
+
command_group = group.lower().replace("-", "_")
|
|
417
|
+
|
|
418
|
+
# Validate group name
|
|
419
|
+
if not re.match(r"^[a-z][a-z0-9_]*$", command_group):
|
|
420
|
+
logger.error(
|
|
421
|
+
f"Invalid group name: {command_group}. Use lowercase letters, numbers, and underscores (starting with a letter)."
|
|
422
|
+
)
|
|
423
|
+
click.echo(
|
|
424
|
+
f"Invalid group name: {command_group}. Use lowercase letters, numbers, and underscores (starting with a letter).",
|
|
425
|
+
err=True,
|
|
426
|
+
)
|
|
427
|
+
return 1
|
|
428
|
+
|
|
429
|
+
# Check if group exists, create if needed
|
|
430
|
+
group_path = mcli_path / command_group
|
|
431
|
+
if not group_path.exists():
|
|
432
|
+
# Create group directory and __init__.py
|
|
433
|
+
group_path.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
with open(group_path / "__init__.py", "w") as f:
|
|
435
|
+
f.write(f'"""\n{command_group.capitalize()} commands for mcli.\n"""')
|
|
436
|
+
logger.info(f"Created new command group directory: {command_group}")
|
|
437
|
+
click.echo(f"Created new command group directory: {command_group}")
|
|
438
|
+
|
|
439
|
+
# Create command file
|
|
440
|
+
command_file_path = group_path / f"{command_name}.py"
|
|
441
|
+
if command_file_path.exists():
|
|
442
|
+
logger.warning(f"Command file already exists: {command_file_path}")
|
|
443
|
+
should_override = Prompt.ask(
|
|
444
|
+
"File already exists. Override?", choices=["y", "n"], default="n"
|
|
445
|
+
)
|
|
446
|
+
if should_override.lower() != "y":
|
|
447
|
+
logger.info("Command creation aborted.")
|
|
448
|
+
click.echo("Command creation aborted.")
|
|
449
|
+
return 1
|
|
450
|
+
|
|
451
|
+
# Generate command file
|
|
452
|
+
with open(command_file_path, "w") as f:
|
|
453
|
+
f.write(get_command_template(command_name, command_group))
|
|
454
|
+
|
|
455
|
+
logger.info(f"Created new command: {command_name} in group: {command_group}")
|
|
456
|
+
click.echo(f"Created new command: {command_name} in group: {command_group}")
|
|
457
|
+
click.echo(f"File created: {command_file_path}")
|
|
458
|
+
click.echo(
|
|
459
|
+
f"To use this command, add 'from mcli.{command_group}.{command_name} import {command_name}_group' to your main imports"
|
|
460
|
+
)
|
|
461
|
+
click.echo(
|
|
462
|
+
f"Then add '{command_name}_group to your main CLI group using app.add_command({command_name}_group)'"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
else:
|
|
466
|
+
# Creating directly under self
|
|
467
|
+
command_file_path = mcli_path / "self" / f"{command_name}.py"
|
|
468
|
+
|
|
469
|
+
if command_file_path.exists():
|
|
470
|
+
logger.warning(f"Command file already exists: {command_file_path}")
|
|
471
|
+
should_override = Prompt.ask(
|
|
472
|
+
"File already exists. Override?", choices=["y", "n"], default="n"
|
|
473
|
+
)
|
|
474
|
+
if should_override.lower() != "y":
|
|
475
|
+
logger.info("Command creation aborted.")
|
|
476
|
+
click.echo("Command creation aborted.")
|
|
477
|
+
return 1
|
|
478
|
+
|
|
479
|
+
# Generate command file
|
|
480
|
+
with open(command_file_path, "w") as f:
|
|
481
|
+
f.write(get_command_template(command_name))
|
|
482
|
+
|
|
483
|
+
# Update self_cmd.py to import and register the new command
|
|
484
|
+
with open(Path(__file__), "r") as f:
|
|
485
|
+
content = f.read()
|
|
486
|
+
|
|
487
|
+
# Add import statement if not exists
|
|
488
|
+
import_statement = f"from mcli.self.{command_name} import {command_name}_command"
|
|
489
|
+
if import_statement not in content:
|
|
490
|
+
import_section_end = content.find("logger = get_logger()")
|
|
491
|
+
if import_section_end != -1:
|
|
492
|
+
updated_content = (
|
|
493
|
+
content[:import_section_end]
|
|
494
|
+
+ import_statement
|
|
495
|
+
+ "\n"
|
|
496
|
+
+ content[import_section_end:]
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Add command registration (Click syntax)
|
|
500
|
+
registration = f"@self_app.command('{command_name}')\ndef {command_name}(name=\"World\"):\n return {command_name}_command(name)\n"
|
|
501
|
+
registration_point = updated_content.rfind("def ")
|
|
502
|
+
if registration_point != -1:
|
|
503
|
+
# Find the end of the last function
|
|
504
|
+
last_func_end = updated_content.find("\n\n", registration_point)
|
|
505
|
+
if last_func_end != -1:
|
|
506
|
+
updated_content = (
|
|
507
|
+
updated_content[: last_func_end + 2]
|
|
508
|
+
+ registration
|
|
509
|
+
+ updated_content[last_func_end + 2 :]
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
updated_content += "\n\n" + registration
|
|
513
|
+
|
|
514
|
+
with open(Path(__file__), "w") as f:
|
|
515
|
+
f.write(updated_content)
|
|
516
|
+
|
|
517
|
+
logger.info(f"Created new command: {command_name} in self module")
|
|
518
|
+
click.echo(f"Created new command: {command_name} in self module")
|
|
519
|
+
click.echo(f"File created: {command_file_path}")
|
|
520
|
+
click.echo(f"Command has been automatically registered with self_app")
|
|
521
|
+
|
|
522
|
+
return 0
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@click.group("plugin")
|
|
526
|
+
def plugin():
|
|
527
|
+
"""
|
|
528
|
+
Manage plugins for mcli.
|
|
529
|
+
|
|
530
|
+
Use one of the subcommands: add, remove, update.
|
|
531
|
+
"""
|
|
532
|
+
logger.info("Plugin management commands loaded")
|
|
533
|
+
pass
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@plugin.command("add")
|
|
537
|
+
@click.argument("plugin_name")
|
|
538
|
+
@click.argument("repo_url", required=False)
|
|
539
|
+
def plugin_add(plugin_name, repo_url=None):
|
|
540
|
+
"""Add a new plugin."""
|
|
541
|
+
# First, check for config path in environment variable
|
|
542
|
+
logger.info(f"Adding plugin: {plugin_name} with repo URL: {repo_url}")
|
|
543
|
+
config_env = os.environ.get("MCLI_CONFIG")
|
|
544
|
+
config_path = None
|
|
545
|
+
|
|
546
|
+
if config_env and Path(config_env).expanduser().exists():
|
|
547
|
+
config_path = Path(config_env).expanduser()
|
|
548
|
+
else:
|
|
549
|
+
# Default to $HOME/.config/mcli/config.toml
|
|
550
|
+
home_config = Path.home() / ".config" / "mcli" / "config.toml"
|
|
551
|
+
if home_config.exists():
|
|
552
|
+
config_path = home_config
|
|
553
|
+
else:
|
|
554
|
+
# Fallback to top-level config.toml
|
|
555
|
+
top_level_config = Path(__file__).parent.parent.parent / "config.toml"
|
|
556
|
+
if top_level_config.exists():
|
|
557
|
+
config_path = top_level_config
|
|
558
|
+
|
|
559
|
+
if not config_path or not config_path.exists():
|
|
560
|
+
click.echo(
|
|
561
|
+
"Config file not found in $MCLI_CONFIG, $HOME/.config/mcli/config.toml, or project root.",
|
|
562
|
+
err=True,
|
|
563
|
+
)
|
|
564
|
+
return 1
|
|
565
|
+
|
|
566
|
+
with open(config_path, "rb") as f:
|
|
567
|
+
config = tomli.load(f)
|
|
568
|
+
|
|
569
|
+
# Example: plugins are listed under [plugins]
|
|
570
|
+
plugins = config.get("plugins", {})
|
|
571
|
+
if plugin_name in plugins:
|
|
572
|
+
click.echo(f"Plugin '{plugin_name}' already exists in config.toml.")
|
|
573
|
+
return 1
|
|
574
|
+
|
|
575
|
+
# Determine plugin install path
|
|
576
|
+
plugin_path = None
|
|
577
|
+
# 1. Check config file for plugin location
|
|
578
|
+
plugin_location = config.get("plugin_location")
|
|
579
|
+
if plugin_location:
|
|
580
|
+
plugin_path = Path(plugin_location).expanduser()
|
|
581
|
+
else:
|
|
582
|
+
# 2. Check env variable
|
|
583
|
+
env_plugin_path = os.environ.get("MCLI_PLUGIN_PATH")
|
|
584
|
+
if env_plugin_path:
|
|
585
|
+
plugin_path = Path(env_plugin_path).expanduser()
|
|
586
|
+
else:
|
|
587
|
+
# 3. Default location
|
|
588
|
+
plugin_path = Path.home() / ".config" / "mcli" / "plugins"
|
|
589
|
+
|
|
590
|
+
plugin_path.mkdir(parents=True, exist_ok=True)
|
|
591
|
+
|
|
592
|
+
# Download the repo if a URL is provided
|
|
593
|
+
if repo_url:
|
|
594
|
+
import subprocess
|
|
595
|
+
|
|
596
|
+
dest = plugin_path / plugin_name
|
|
597
|
+
if dest.exists():
|
|
598
|
+
click.echo(f"Plugin directory already exists at {dest}. Aborting download.", err=True)
|
|
599
|
+
return 1
|
|
600
|
+
try:
|
|
601
|
+
click.echo(f"Cloning {repo_url} into {dest} ...")
|
|
602
|
+
subprocess.run(["git", "clone", repo_url, str(dest)], check=True)
|
|
603
|
+
click.echo(f"Plugin '{plugin_name}' cloned to {dest}")
|
|
604
|
+
except Exception as e:
|
|
605
|
+
click.echo(f"Failed to clone repository: {e}", err=True)
|
|
606
|
+
return 1
|
|
607
|
+
else:
|
|
608
|
+
click.echo("No repo URL provided, plugin will not be downloaded.")
|
|
609
|
+
|
|
610
|
+
# TODO: Optionally update config.toml to register the new plugin
|
|
611
|
+
|
|
612
|
+
return 0
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@plugin.command("remove")
|
|
616
|
+
@click.argument("plugin_name")
|
|
617
|
+
def plugin_remove(plugin_name):
|
|
618
|
+
"""Remove an existing plugin."""
|
|
619
|
+
# Determine plugin install path as in plugin_add
|
|
620
|
+
logger.info(f"Removing plugin: {plugin_name}")
|
|
621
|
+
config_env = os.environ.get("MCLI_CONFIG")
|
|
622
|
+
config_path = None
|
|
623
|
+
|
|
624
|
+
if config_env and Path(config_env).expanduser().exists():
|
|
625
|
+
config_path = Path(config_env).expanduser()
|
|
626
|
+
else:
|
|
627
|
+
home_config = Path.home() / ".config" / "mcli" / "config.toml"
|
|
628
|
+
if home_config.exists():
|
|
629
|
+
config_path = home_config
|
|
630
|
+
else:
|
|
631
|
+
top_level_config = Path(__file__).parent.parent.parent / "config.toml"
|
|
632
|
+
if top_level_config.exists():
|
|
633
|
+
config_path = top_level_config
|
|
634
|
+
|
|
635
|
+
if not config_path or not config_path.exists():
|
|
636
|
+
click.echo(
|
|
637
|
+
"Config file not found in $MCLI_CONFIG, $HOME/.config/mcli/config.toml, or project root.",
|
|
638
|
+
err=True,
|
|
639
|
+
)
|
|
640
|
+
return 1
|
|
641
|
+
|
|
642
|
+
with open(config_path, "rb") as f:
|
|
643
|
+
config = tomli.load(f)
|
|
644
|
+
|
|
645
|
+
plugin_location = config.get("plugin_location")
|
|
646
|
+
if plugin_location:
|
|
647
|
+
plugin_path = Path(plugin_location).expanduser()
|
|
648
|
+
else:
|
|
649
|
+
env_plugin_path = os.environ.get("MCLI_PLUGIN_PATH")
|
|
650
|
+
if env_plugin_path:
|
|
651
|
+
plugin_path = Path(env_plugin_path).expanduser()
|
|
652
|
+
else:
|
|
653
|
+
plugin_path = Path.home() / ".config" / "mcli" / "plugins"
|
|
654
|
+
|
|
655
|
+
dest = plugin_path / plugin_name
|
|
656
|
+
if not dest.exists():
|
|
657
|
+
click.echo(f"Plugin directory does not exist at {dest}. Nothing to remove.", err=True)
|
|
658
|
+
return 1
|
|
659
|
+
|
|
660
|
+
import shutil
|
|
661
|
+
|
|
662
|
+
try:
|
|
663
|
+
shutil.rmtree(dest)
|
|
664
|
+
click.echo(f"Plugin '{plugin_name}' removed from {dest}")
|
|
665
|
+
except Exception as e:
|
|
666
|
+
click.echo(f"Failed to remove plugin: {e}", err=True)
|
|
667
|
+
return 1
|
|
668
|
+
|
|
669
|
+
# TODO: Optionally update config.toml to unregister the plugin
|
|
670
|
+
|
|
671
|
+
return 0
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@plugin.command("update")
|
|
675
|
+
@click.argument("plugin_name")
|
|
676
|
+
def plugin_update(plugin_name):
|
|
677
|
+
"""Update an existing plugin (git pull on default branch)."""
|
|
678
|
+
"""Update an existing plugin by pulling the latest changes from its repository."""
|
|
679
|
+
# Determine plugin install path as in plugin_add
|
|
680
|
+
config_env = os.environ.get("MCLI_CONFIG")
|
|
681
|
+
config_path = None
|
|
682
|
+
|
|
683
|
+
# Determine plugin install path as in plugin_add
|
|
684
|
+
config_env = os.environ.get("MCLI_CONFIG")
|
|
685
|
+
config_path = None
|
|
686
|
+
|
|
687
|
+
if config_env and Path(config_env).expanduser().exists():
|
|
688
|
+
config_path = Path(config_env).expanduser()
|
|
689
|
+
else:
|
|
690
|
+
home_config = Path.home() / ".config" / "mcli" / "config.toml"
|
|
691
|
+
if home_config.exists():
|
|
692
|
+
config_path = home_config
|
|
693
|
+
else:
|
|
694
|
+
top_level_config = Path(__file__).parent.parent.parent / "config.toml"
|
|
695
|
+
if top_level_config.exists():
|
|
696
|
+
config_path = top_level_config
|
|
697
|
+
|
|
698
|
+
if not config_path or not config_path.exists():
|
|
699
|
+
click.echo(
|
|
700
|
+
"Config file not found in $MCLI_CONFIG, $HOME/.config/mcli/config.toml, or project root.",
|
|
701
|
+
err=True,
|
|
702
|
+
)
|
|
703
|
+
return 1
|
|
704
|
+
|
|
705
|
+
with open(config_path, "rb") as f:
|
|
706
|
+
config = tomli.load(f)
|
|
707
|
+
|
|
708
|
+
plugin_location = config.get("plugin_location")
|
|
709
|
+
if plugin_location:
|
|
710
|
+
plugin_path = Path(plugin_location).expanduser()
|
|
711
|
+
else:
|
|
712
|
+
env_plugin_path = os.environ.get("MCLI_PLUGIN_PATH")
|
|
713
|
+
if env_plugin_path:
|
|
714
|
+
plugin_path = Path(env_plugin_path).expanduser()
|
|
715
|
+
else:
|
|
716
|
+
plugin_path = Path.home() / ".config" / "mcli" / "plugins"
|
|
717
|
+
|
|
718
|
+
dest = plugin_path / plugin_name
|
|
719
|
+
if not dest.exists():
|
|
720
|
+
click.echo(f"Plugin directory does not exist at {dest}. Cannot update.", err=True)
|
|
721
|
+
return 1
|
|
722
|
+
|
|
723
|
+
import subprocess
|
|
724
|
+
|
|
725
|
+
try:
|
|
726
|
+
click.echo(f"Updating plugin '{plugin_name}' in {dest} ...")
|
|
727
|
+
subprocess.run(["git", "-C", str(dest), "pull"], check=True)
|
|
728
|
+
click.echo(f"Plugin '{plugin_name}' updated (git pull).")
|
|
729
|
+
except Exception as e:
|
|
730
|
+
click.echo(f"Failed to update plugin: {e}", err=True)
|
|
731
|
+
return 1
|
|
732
|
+
|
|
733
|
+
return 0
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@self_app.command("hello")
|
|
737
|
+
@click.argument("name", default="World")
|
|
738
|
+
def hello(name: str):
|
|
739
|
+
"""A simple hello command for testing."""
|
|
740
|
+
message = f"Hello, {name}! This is the MCLI hello command."
|
|
741
|
+
logger.info(message)
|
|
742
|
+
console.print(f"[green]{message}[/green]")
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
@self_app.command("logs")
|
|
746
|
+
@click.option(
|
|
747
|
+
"--type",
|
|
748
|
+
"-t",
|
|
749
|
+
type=click.Choice(["main", "system", "trace", "all"]),
|
|
750
|
+
default="main",
|
|
751
|
+
help="Type of logs to display",
|
|
752
|
+
)
|
|
753
|
+
@click.option("--lines", "-n", default=50, help="Number of lines to show (default: 50)")
|
|
754
|
+
@click.option("--follow", "-f", is_flag=True, help="Follow log output in real-time")
|
|
755
|
+
@click.option("--date", "-d", help="Show logs for specific date (YYYYMMDD format)")
|
|
756
|
+
@click.option("--grep", "-g", help="Filter logs by pattern")
|
|
757
|
+
@click.option(
|
|
758
|
+
"--level",
|
|
759
|
+
"-l",
|
|
760
|
+
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
|
|
761
|
+
help="Filter logs by minimum level",
|
|
762
|
+
)
|
|
763
|
+
def logs(type: str, lines: int, follow: bool, date: str, grep: str, level: str):
|
|
764
|
+
"""
|
|
765
|
+
Display runtime logs of the mcli application.
|
|
766
|
+
|
|
767
|
+
Shows the most recent log entries from the application's logging system.
|
|
768
|
+
Supports filtering by log type, date, content, and log level.
|
|
769
|
+
|
|
770
|
+
Log files are named as mcli_YYYYMMDD.log, mcli_system_YYYYMMDD.log, mcli_trace_YYYYMMDD.log.
|
|
771
|
+
"""
|
|
772
|
+
import re
|
|
773
|
+
import subprocess
|
|
774
|
+
from datetime import datetime
|
|
775
|
+
from pathlib import Path
|
|
776
|
+
|
|
777
|
+
# Find the logs directory
|
|
778
|
+
current_file = Path(__file__)
|
|
779
|
+
# Go up 5 levels: file -> self -> mcli -> src -> repo_root
|
|
780
|
+
project_root = current_file.parents[4]
|
|
781
|
+
logs_dir = project_root / "logs"
|
|
782
|
+
|
|
783
|
+
# Alternative: try current working directory first
|
|
784
|
+
if not logs_dir.exists():
|
|
785
|
+
logs_dir = Path.cwd() / "logs"
|
|
786
|
+
|
|
787
|
+
if not logs_dir.exists():
|
|
788
|
+
click.echo("â Logs directory not found", err=True)
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
# Determine which log files to read
|
|
792
|
+
log_files = []
|
|
793
|
+
|
|
794
|
+
if type == "all":
|
|
795
|
+
# Get all log files for the specified date or latest
|
|
796
|
+
if date:
|
|
797
|
+
# Look for files like mcli_20250709.log, mcli_system_20250709.log, mcli_trace_20250709.log
|
|
798
|
+
patterns = [f"mcli_{date}.log", f"mcli_system_{date}.log", f"mcli_trace_{date}.log"]
|
|
799
|
+
else:
|
|
800
|
+
# Get the most recent log files
|
|
801
|
+
patterns = ["mcli_*.log"]
|
|
802
|
+
|
|
803
|
+
log_files = []
|
|
804
|
+
for pattern in patterns:
|
|
805
|
+
files = list(logs_dir.glob(pattern))
|
|
806
|
+
if files:
|
|
807
|
+
# Sort by modification time (newest first)
|
|
808
|
+
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
|
809
|
+
log_files.extend(files)
|
|
810
|
+
|
|
811
|
+
# Remove duplicates and take only the most recent files of each type
|
|
812
|
+
seen_types = set()
|
|
813
|
+
filtered_files = []
|
|
814
|
+
for log_file in log_files:
|
|
815
|
+
# Extract log type from filename
|
|
816
|
+
# mcli_20250709 -> main
|
|
817
|
+
# mcli_system_20250709 -> system
|
|
818
|
+
# mcli_trace_20250709 -> trace
|
|
819
|
+
if log_file.name.startswith("mcli_system_"):
|
|
820
|
+
log_type = "system"
|
|
821
|
+
elif log_file.name.startswith("mcli_trace_"):
|
|
822
|
+
log_type = "trace"
|
|
823
|
+
else:
|
|
824
|
+
log_type = "main"
|
|
825
|
+
|
|
826
|
+
if log_type not in seen_types:
|
|
827
|
+
seen_types.add(log_type)
|
|
828
|
+
filtered_files.append(log_file)
|
|
829
|
+
|
|
830
|
+
log_files = filtered_files
|
|
831
|
+
else:
|
|
832
|
+
# Get specific log type
|
|
833
|
+
if date:
|
|
834
|
+
if type == "main":
|
|
835
|
+
filename = f"mcli_{date}.log"
|
|
836
|
+
else:
|
|
837
|
+
filename = f"mcli_{type}_{date}.log"
|
|
838
|
+
else:
|
|
839
|
+
# Find the most recent file for this type
|
|
840
|
+
if type == "main":
|
|
841
|
+
pattern = "mcli_*.log"
|
|
842
|
+
# Exclude system and trace files
|
|
843
|
+
exclude_patterns = ["mcli_system_*.log", "mcli_trace_*.log"]
|
|
844
|
+
else:
|
|
845
|
+
pattern = f"mcli_{type}_*.log"
|
|
846
|
+
exclude_patterns = []
|
|
847
|
+
|
|
848
|
+
files = list(logs_dir.glob(pattern))
|
|
849
|
+
|
|
850
|
+
# Filter out excluded patterns
|
|
851
|
+
if exclude_patterns:
|
|
852
|
+
filtered_files = []
|
|
853
|
+
for file in files:
|
|
854
|
+
excluded = False
|
|
855
|
+
for exclude_pattern in exclude_patterns:
|
|
856
|
+
if file.match(exclude_pattern):
|
|
857
|
+
excluded = True
|
|
858
|
+
break
|
|
859
|
+
if not excluded:
|
|
860
|
+
filtered_files.append(file)
|
|
861
|
+
files = filtered_files
|
|
862
|
+
|
|
863
|
+
if files:
|
|
864
|
+
# Sort by modification time and take the most recent
|
|
865
|
+
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
|
866
|
+
filename = files[0].name
|
|
867
|
+
else:
|
|
868
|
+
click.echo(f"â No {type} log files found", err=True)
|
|
869
|
+
return
|
|
870
|
+
|
|
871
|
+
log_file = logs_dir / filename
|
|
872
|
+
if log_file.exists():
|
|
873
|
+
log_files = [log_file]
|
|
874
|
+
else:
|
|
875
|
+
click.echo(f"â Log file not found: {filename}", err=True)
|
|
876
|
+
return
|
|
877
|
+
|
|
878
|
+
if not log_files:
|
|
879
|
+
click.echo("â No log files found", err=True)
|
|
880
|
+
return
|
|
881
|
+
|
|
882
|
+
# Display log file information
|
|
883
|
+
click.echo(f"đ Showing logs from {len(log_files)} file(s):")
|
|
884
|
+
for log_file in log_files:
|
|
885
|
+
size_mb = log_file.stat().st_size / (1024 * 1024)
|
|
886
|
+
modified = datetime.fromtimestamp(log_file.stat().st_mtime)
|
|
887
|
+
click.echo(
|
|
888
|
+
f" đ {log_file.name} ({size_mb:.1f}MB, modified {modified.strftime('%Y-%m-%d %H:%M:%S')})"
|
|
889
|
+
)
|
|
890
|
+
click.echo()
|
|
891
|
+
|
|
892
|
+
# Process each log file
|
|
893
|
+
for log_file in log_files:
|
|
894
|
+
click.echo(f"đ Reading: {log_file.name}")
|
|
895
|
+
click.echo("â" * 80)
|
|
896
|
+
|
|
897
|
+
try:
|
|
898
|
+
# Read the file content
|
|
899
|
+
with open(log_file, "r") as f:
|
|
900
|
+
content = f.readlines()
|
|
901
|
+
|
|
902
|
+
# Apply filters
|
|
903
|
+
filtered_lines = []
|
|
904
|
+
for line in content:
|
|
905
|
+
# Apply grep filter
|
|
906
|
+
if grep and grep.lower() not in line.lower():
|
|
907
|
+
continue
|
|
908
|
+
|
|
909
|
+
# Apply level filter
|
|
910
|
+
if level:
|
|
911
|
+
level_pattern = rf"\b{level}\b"
|
|
912
|
+
if not re.search(level_pattern, line, re.IGNORECASE):
|
|
913
|
+
# Check if line has a lower level than requested
|
|
914
|
+
level_order = {
|
|
915
|
+
"DEBUG": 0,
|
|
916
|
+
"INFO": 1,
|
|
917
|
+
"WARNING": 2,
|
|
918
|
+
"ERROR": 3,
|
|
919
|
+
"CRITICAL": 4,
|
|
920
|
+
}
|
|
921
|
+
requested_level = level_order.get(level.upper(), 0)
|
|
922
|
+
|
|
923
|
+
# Check if line contains any log level
|
|
924
|
+
found_level = None
|
|
925
|
+
for log_level in level_order:
|
|
926
|
+
if log_level in line.upper():
|
|
927
|
+
found_level = level_order[log_level]
|
|
928
|
+
break
|
|
929
|
+
|
|
930
|
+
if found_level is None or found_level < requested_level:
|
|
931
|
+
continue
|
|
932
|
+
|
|
933
|
+
filtered_lines.append(line)
|
|
934
|
+
|
|
935
|
+
# Show the last N lines
|
|
936
|
+
if lines > 0:
|
|
937
|
+
filtered_lines = filtered_lines[-lines:]
|
|
938
|
+
|
|
939
|
+
# Display the lines
|
|
940
|
+
for line in filtered_lines:
|
|
941
|
+
# Colorize log levels
|
|
942
|
+
colored_line = line
|
|
943
|
+
if "ERROR" in line or "CRITICAL" in line:
|
|
944
|
+
colored_line = click.style(line, fg="red")
|
|
945
|
+
elif "WARNING" in line:
|
|
946
|
+
colored_line = click.style(line, fg="yellow")
|
|
947
|
+
elif "INFO" in line:
|
|
948
|
+
colored_line = click.style(line, fg="green")
|
|
949
|
+
elif "DEBUG" in line:
|
|
950
|
+
colored_line = click.style(line, fg="blue")
|
|
951
|
+
|
|
952
|
+
click.echo(colored_line.rstrip())
|
|
953
|
+
|
|
954
|
+
if not filtered_lines:
|
|
955
|
+
click.echo("(No matching log entries found)")
|
|
956
|
+
|
|
957
|
+
except Exception as e:
|
|
958
|
+
click.echo(f"â Error reading log file {log_file.name}: {e}", err=True)
|
|
959
|
+
|
|
960
|
+
click.echo()
|
|
961
|
+
|
|
962
|
+
if follow:
|
|
963
|
+
click.echo("đ Following log output... (Press Ctrl+C to stop)")
|
|
964
|
+
try:
|
|
965
|
+
# Use tail -f for real-time following
|
|
966
|
+
for log_file in log_files:
|
|
967
|
+
click.echo(f"đĄ Following: {log_file.name}")
|
|
968
|
+
process = subprocess.Popen(
|
|
969
|
+
["tail", "-f", str(log_file)],
|
|
970
|
+
stdout=subprocess.PIPE,
|
|
971
|
+
stderr=subprocess.PIPE,
|
|
972
|
+
text=True,
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
try:
|
|
976
|
+
if process.stdout:
|
|
977
|
+
for line in process.stdout:
|
|
978
|
+
# Apply filters to real-time output
|
|
979
|
+
if grep and grep.lower() not in line.lower():
|
|
980
|
+
continue
|
|
981
|
+
|
|
982
|
+
if level:
|
|
983
|
+
level_pattern = rf"\b{level}\b"
|
|
984
|
+
if not re.search(level_pattern, line, re.IGNORECASE):
|
|
985
|
+
continue
|
|
986
|
+
|
|
987
|
+
# Colorize and display
|
|
988
|
+
colored_line = line
|
|
989
|
+
if "ERROR" in line or "CRITICAL" in line:
|
|
990
|
+
colored_line = click.style(line, fg="red")
|
|
991
|
+
elif "WARNING" in line:
|
|
992
|
+
colored_line = click.style(line, fg="yellow")
|
|
993
|
+
elif "INFO" in line:
|
|
994
|
+
colored_line = click.style(line, fg="green")
|
|
995
|
+
elif "DEBUG" in line:
|
|
996
|
+
colored_line = click.style(line, fg="blue")
|
|
997
|
+
|
|
998
|
+
click.echo(colored_line.rstrip())
|
|
999
|
+
|
|
1000
|
+
except KeyboardInterrupt:
|
|
1001
|
+
process.terminate()
|
|
1002
|
+
break
|
|
1003
|
+
|
|
1004
|
+
except KeyboardInterrupt:
|
|
1005
|
+
click.echo("\nđ Stopped following logs")
|
|
1006
|
+
except Exception as e:
|
|
1007
|
+
click.echo(f"â Error following logs: {e}", err=True)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@self_app.command("performance")
|
|
1011
|
+
@click.option("--detailed", "-d", is_flag=True, help="Show detailed performance information")
|
|
1012
|
+
@click.option("--benchmark", "-b", is_flag=True, help="Run performance benchmarks")
|
|
1013
|
+
def performance(detailed: bool, benchmark: bool):
|
|
1014
|
+
"""đ Show performance optimization status and benchmarks"""
|
|
1015
|
+
try:
|
|
1016
|
+
from mcli.lib.performance.optimizer import get_global_optimizer
|
|
1017
|
+
from mcli.lib.performance.rust_bridge import print_performance_summary
|
|
1018
|
+
|
|
1019
|
+
# Always show the performance summary
|
|
1020
|
+
print_performance_summary()
|
|
1021
|
+
|
|
1022
|
+
if detailed:
|
|
1023
|
+
console.print("\nđ Detailed Performance Information:")
|
|
1024
|
+
console.print("â" * 60)
|
|
1025
|
+
|
|
1026
|
+
optimizer = get_global_optimizer()
|
|
1027
|
+
summary = optimizer.get_optimization_summary()
|
|
1028
|
+
|
|
1029
|
+
table = Table(
|
|
1030
|
+
title="Detailed Optimization Results", show_header=True, header_style="bold magenta"
|
|
1031
|
+
)
|
|
1032
|
+
table.add_column("Optimization", style="cyan", width=20)
|
|
1033
|
+
table.add_column("Status", justify="center", width=10)
|
|
1034
|
+
table.add_column("Details", style="white", width=40)
|
|
1035
|
+
|
|
1036
|
+
for name, details in summary["details"].items():
|
|
1037
|
+
status = "â
" if details.get("success") else "â"
|
|
1038
|
+
detail_text = details.get("performance_gain", "N/A")
|
|
1039
|
+
if details.get("optimizations"):
|
|
1040
|
+
opts = details["optimizations"]
|
|
1041
|
+
detail_text += f"\n{len(opts)} optimizations applied"
|
|
1042
|
+
|
|
1043
|
+
table.add_row(name.replace("_", " ").title(), status, detail_text)
|
|
1044
|
+
|
|
1045
|
+
console.print(table)
|
|
1046
|
+
|
|
1047
|
+
console.print(
|
|
1048
|
+
f"\nđ¯ Estimated Performance Gain: {summary['estimated_performance_gain']}"
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
if benchmark:
|
|
1052
|
+
console.print("\nđ Running Performance Benchmarks...")
|
|
1053
|
+
console.print("â" * 60)
|
|
1054
|
+
|
|
1055
|
+
try:
|
|
1056
|
+
from mcli.lib.ui.visual_effects import MCLIProgressBar
|
|
1057
|
+
|
|
1058
|
+
progress = MCLIProgressBar.create_fancy_progress()
|
|
1059
|
+
with progress:
|
|
1060
|
+
# Benchmark task
|
|
1061
|
+
task = progress.add_task("đĨ Running TF-IDF benchmark...", total=100)
|
|
1062
|
+
|
|
1063
|
+
optimizer = get_global_optimizer()
|
|
1064
|
+
|
|
1065
|
+
# Update progress
|
|
1066
|
+
for i in range(20):
|
|
1067
|
+
progress.update(task, advance=5)
|
|
1068
|
+
time.sleep(0.05)
|
|
1069
|
+
|
|
1070
|
+
# Run actual benchmark
|
|
1071
|
+
benchmark_results = optimizer.benchmark_performance("medium")
|
|
1072
|
+
|
|
1073
|
+
progress.update(task, advance=100)
|
|
1074
|
+
|
|
1075
|
+
# Display results
|
|
1076
|
+
if benchmark_results:
|
|
1077
|
+
console.print("\nđ Benchmark Results:")
|
|
1078
|
+
|
|
1079
|
+
tfidf_results = benchmark_results.get("tfidf_benchmark", {})
|
|
1080
|
+
if tfidf_results.get("rust") and tfidf_results.get("python"):
|
|
1081
|
+
speedup = tfidf_results["python"] / tfidf_results["rust"]
|
|
1082
|
+
console.print(f" đĻ Rust TF-IDF: {tfidf_results['rust']:.3f}s")
|
|
1083
|
+
console.print(f" đ Python TF-IDF: {tfidf_results['python']:.3f}s")
|
|
1084
|
+
console.print(f" ⥠Speedup: {speedup:.1f}x faster with Rust!")
|
|
1085
|
+
|
|
1086
|
+
system_info = benchmark_results.get("system_info", {})
|
|
1087
|
+
if system_info:
|
|
1088
|
+
console.print(f"\nđģ System Info:")
|
|
1089
|
+
console.print(f" Platform: {system_info.get('platform', 'Unknown')}")
|
|
1090
|
+
console.print(f" CPUs: {system_info.get('cpu_count', 'Unknown')}")
|
|
1091
|
+
console.print(
|
|
1092
|
+
f" Memory: {system_info.get('memory_total', 0) // (1024**3):.1f}GB"
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
except ImportError:
|
|
1096
|
+
click.echo("đ Benchmark functionality requires additional dependencies")
|
|
1097
|
+
click.echo("đĄ Install with: pip install rich")
|
|
1098
|
+
|
|
1099
|
+
except ImportError as e:
|
|
1100
|
+
click.echo(f"â Performance monitoring not available: {e}")
|
|
1101
|
+
click.echo("đĄ Try installing dependencies: pip install rich psutil")
|
|
1102
|
+
except Exception as e:
|
|
1103
|
+
click.echo(f"â Error showing performance status: {e}")
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
@self_app.command()
|
|
1107
|
+
@click.option("--refresh", "-r", default=2.0, help="Refresh interval in seconds")
|
|
1108
|
+
@click.option("--once", is_flag=True, help="Show dashboard once and exit")
|
|
1109
|
+
def dashboard(refresh: float, once: bool):
|
|
1110
|
+
"""đ Launch live system dashboard"""
|
|
1111
|
+
try:
|
|
1112
|
+
from mcli.lib.ui.visual_effects import LiveDashboard
|
|
1113
|
+
|
|
1114
|
+
dashboard = LiveDashboard()
|
|
1115
|
+
|
|
1116
|
+
if once:
|
|
1117
|
+
# Show dashboard once
|
|
1118
|
+
console.clear()
|
|
1119
|
+
layout = dashboard.create_full_dashboard()
|
|
1120
|
+
console.print(layout)
|
|
1121
|
+
else:
|
|
1122
|
+
# Start live updating dashboard
|
|
1123
|
+
dashboard.start_live_dashboard(refresh_interval=refresh)
|
|
1124
|
+
|
|
1125
|
+
except ImportError as e:
|
|
1126
|
+
console.print("[red]Dashboard module not available[/red]")
|
|
1127
|
+
console.print(f"Error: {e}")
|
|
1128
|
+
except Exception as e:
|
|
1129
|
+
console.print(f"[red]Error launching dashboard: {e}[/red]")
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
@self_app.command()
|
|
1133
|
+
@click.option("--check", is_flag=True, help="Only check for updates, don't install")
|
|
1134
|
+
@click.option("--pre", is_flag=True, help="Include pre-release versions")
|
|
1135
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
1136
|
+
def update(check: bool, pre: bool, yes: bool):
|
|
1137
|
+
"""đ Check for and install mcli updates from PyPI"""
|
|
1138
|
+
import subprocess
|
|
1139
|
+
import sys
|
|
1140
|
+
from importlib.metadata import version as get_version
|
|
1141
|
+
|
|
1142
|
+
try:
|
|
1143
|
+
import requests
|
|
1144
|
+
except ImportError:
|
|
1145
|
+
console.print("[red]â Error: 'requests' module not found[/red]")
|
|
1146
|
+
console.print("[yellow]Install it with: pip install requests[/yellow]")
|
|
1147
|
+
return
|
|
1148
|
+
|
|
1149
|
+
try:
|
|
1150
|
+
# Get current version
|
|
1151
|
+
try:
|
|
1152
|
+
current_version = get_version("mcli-framework")
|
|
1153
|
+
except Exception:
|
|
1154
|
+
console.print("[yellow]â ī¸ Could not determine current version[/yellow]")
|
|
1155
|
+
current_version = "unknown"
|
|
1156
|
+
|
|
1157
|
+
console.print(f"[cyan]Current version:[/cyan] {current_version}")
|
|
1158
|
+
console.print("[cyan]Checking PyPI for updates...[/cyan]")
|
|
1159
|
+
|
|
1160
|
+
# Check PyPI for latest version
|
|
1161
|
+
try:
|
|
1162
|
+
response = requests.get("https://pypi.org/pypi/mcli-framework/json", timeout=10)
|
|
1163
|
+
response.raise_for_status()
|
|
1164
|
+
pypi_data = response.json()
|
|
1165
|
+
except requests.RequestException as e:
|
|
1166
|
+
console.print(f"[red]â Error fetching version info from PyPI: {e}[/red]")
|
|
1167
|
+
return
|
|
1168
|
+
|
|
1169
|
+
# Get latest version
|
|
1170
|
+
if pre:
|
|
1171
|
+
# Include pre-releases
|
|
1172
|
+
all_versions = list(pypi_data["releases"].keys())
|
|
1173
|
+
latest_version = max(all_versions, key=lambda v: [int(x) for x in v.split(".")] if v[0].isdigit() else [0])
|
|
1174
|
+
else:
|
|
1175
|
+
# Only stable releases
|
|
1176
|
+
latest_version = pypi_data["info"]["version"]
|
|
1177
|
+
|
|
1178
|
+
console.print(f"[cyan]Latest version:[/cyan] {latest_version}")
|
|
1179
|
+
|
|
1180
|
+
# Compare versions
|
|
1181
|
+
if current_version == latest_version:
|
|
1182
|
+
console.print("[green]â
You're already on the latest version![/green]")
|
|
1183
|
+
return
|
|
1184
|
+
|
|
1185
|
+
# Parse versions for comparison
|
|
1186
|
+
def parse_version(v):
|
|
1187
|
+
try:
|
|
1188
|
+
return tuple(int(x) for x in v.split(".") if x.isdigit())
|
|
1189
|
+
except:
|
|
1190
|
+
return (0, 0, 0)
|
|
1191
|
+
|
|
1192
|
+
current_parsed = parse_version(current_version)
|
|
1193
|
+
latest_parsed = parse_version(latest_version)
|
|
1194
|
+
|
|
1195
|
+
if current_parsed >= latest_parsed:
|
|
1196
|
+
console.print(f"[green]â
Your version ({current_version}) is up to date or newer[/green]")
|
|
1197
|
+
return
|
|
1198
|
+
|
|
1199
|
+
console.print(f"[yellow]âŦī¸ Update available: {current_version} â {latest_version}[/yellow]")
|
|
1200
|
+
|
|
1201
|
+
# Show release notes if available
|
|
1202
|
+
if "urls" in pypi_data["info"] and pypi_data["info"].get("project_urls"):
|
|
1203
|
+
project_urls = pypi_data["info"]["project_urls"]
|
|
1204
|
+
if "Changelog" in project_urls:
|
|
1205
|
+
console.print(f"[dim]đ Changelog: {project_urls['Changelog']}[/dim]")
|
|
1206
|
+
|
|
1207
|
+
if check:
|
|
1208
|
+
console.print("[cyan]âšī¸ Run 'mcli self update' to install the update[/cyan]")
|
|
1209
|
+
return
|
|
1210
|
+
|
|
1211
|
+
# Ask for confirmation unless --yes flag is used
|
|
1212
|
+
if not yes:
|
|
1213
|
+
from rich.prompt import Confirm
|
|
1214
|
+
if not Confirm.ask(f"[yellow]Install mcli {latest_version}?[/yellow]"):
|
|
1215
|
+
console.print("[yellow]Update cancelled[/yellow]")
|
|
1216
|
+
return
|
|
1217
|
+
|
|
1218
|
+
# Install update
|
|
1219
|
+
console.print(f"[cyan]đĻ Installing mcli {latest_version}...[/cyan]")
|
|
1220
|
+
|
|
1221
|
+
# Use pip to upgrade
|
|
1222
|
+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "mcli-framework"]
|
|
1223
|
+
if pre:
|
|
1224
|
+
cmd.append("--pre")
|
|
1225
|
+
|
|
1226
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
1227
|
+
|
|
1228
|
+
if result.returncode == 0:
|
|
1229
|
+
console.print(f"[green]â
Successfully updated to mcli {latest_version}![/green]")
|
|
1230
|
+
console.print("[yellow]âšī¸ Restart your terminal or run 'hash -r' to use the new version[/yellow]")
|
|
1231
|
+
else:
|
|
1232
|
+
console.print(f"[red]â Update failed:[/red]")
|
|
1233
|
+
console.print(result.stderr)
|
|
1234
|
+
|
|
1235
|
+
except Exception as e:
|
|
1236
|
+
console.print(f"[red]â Error during update: {e}[/red]")
|
|
1237
|
+
import traceback
|
|
1238
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
# Register the plugin group with self_app
|
|
1242
|
+
self_app.add_command(plugin)
|
|
1243
|
+
|
|
1244
|
+
# This part is important to make the command available to the CLI
|
|
1245
|
+
if __name__ == "__main__":
|
|
1246
|
+
self_app()
|