mcli-framework 7.11.3__py3-none-any.whl → 7.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/commands_cmd.py +18 -823
- mcli/app/init_cmd.py +391 -0
- mcli/app/lock_cmd.py +288 -0
- mcli/app/main.py +37 -0
- mcli/app/store_cmd.py +448 -0
- mcli/lib/custom_commands.py +3 -3
- mcli/lib/optional_deps.py +1 -3
- mcli/self/migrate_cmd.py +209 -76
- mcli/workflow/secrets/__init__.py +1 -0
- mcli/workflow/secrets/secrets_cmd.py +1 -2
- mcli/workflow/workflow.py +39 -6
- {mcli_framework-7.11.3.dist-info → mcli_framework-7.12.0.dist-info}/METADATA +11 -11
- {mcli_framework-7.11.3.dist-info → mcli_framework-7.12.0.dist-info}/RECORD +17 -14
- {mcli_framework-7.11.3.dist-info → mcli_framework-7.12.0.dist-info}/WHEEL +0 -0
- {mcli_framework-7.11.3.dist-info → mcli_framework-7.12.0.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.11.3.dist-info → mcli_framework-7.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.11.3.dist-info → mcli_framework-7.12.0.dist-info}/top_level.txt +0 -0
mcli/app/main.py
CHANGED
|
@@ -326,6 +326,42 @@ class LazyGroup(click.Group):
|
|
|
326
326
|
|
|
327
327
|
def _add_lazy_commands(app: click.Group):
|
|
328
328
|
"""Add command groups with lazy loading."""
|
|
329
|
+
# Top-level init command
|
|
330
|
+
try:
|
|
331
|
+
from mcli.app.init_cmd import init
|
|
332
|
+
|
|
333
|
+
app.add_command(init, name="init")
|
|
334
|
+
logger.debug("Added init command")
|
|
335
|
+
except ImportError as e:
|
|
336
|
+
logger.debug(f"Could not load init command: {e}")
|
|
337
|
+
|
|
338
|
+
# Top-level teardown command
|
|
339
|
+
try:
|
|
340
|
+
from mcli.app.init_cmd import teardown
|
|
341
|
+
|
|
342
|
+
app.add_command(teardown, name="teardown")
|
|
343
|
+
logger.debug("Added teardown command")
|
|
344
|
+
except ImportError as e:
|
|
345
|
+
logger.debug(f"Could not load teardown command: {e}")
|
|
346
|
+
|
|
347
|
+
# Top-level lock group
|
|
348
|
+
try:
|
|
349
|
+
from mcli.app.lock_cmd import lock
|
|
350
|
+
|
|
351
|
+
app.add_command(lock, name="lock")
|
|
352
|
+
logger.debug("Added lock group")
|
|
353
|
+
except ImportError as e:
|
|
354
|
+
logger.debug(f"Could not load lock group: {e}")
|
|
355
|
+
|
|
356
|
+
# Top-level store group
|
|
357
|
+
try:
|
|
358
|
+
from mcli.app.store_cmd import store
|
|
359
|
+
|
|
360
|
+
app.add_command(store, name="store")
|
|
361
|
+
logger.debug("Added store group")
|
|
362
|
+
except ImportError as e:
|
|
363
|
+
logger.debug(f"Could not load store group: {e}")
|
|
364
|
+
|
|
329
365
|
# Workflow management - load immediately for fast access (renamed from 'commands')
|
|
330
366
|
try:
|
|
331
367
|
from mcli.app.commands_cmd import workflow
|
|
@@ -350,6 +386,7 @@ def _add_lazy_commands(app: click.Group):
|
|
|
350
386
|
# Add workflows group directly (not lazy-loaded) to preserve -g/--global option
|
|
351
387
|
try:
|
|
352
388
|
from mcli.workflow.workflow import workflows as workflows_group
|
|
389
|
+
|
|
353
390
|
app.add_command(workflows_group, name="workflows")
|
|
354
391
|
logger.debug("Added workflows group with -g/--global support")
|
|
355
392
|
except ImportError as e:
|
mcli/app/store_cmd.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Top-level store management commands for MCLI.
|
|
3
|
+
Manages command store - sync ~/.mcli/commands/ to git.
|
|
4
|
+
"""
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from mcli.lib.logger.logger import get_logger
|
|
13
|
+
from mcli.lib.ui.styling import error, info, success, warning
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
# Command store configuration
|
|
18
|
+
DEFAULT_STORE_PATH = Path.home() / "repos" / "mcli-commands"
|
|
19
|
+
COMMANDS_PATH = Path.home() / ".mcli" / "commands"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_store_path() -> Path:
|
|
23
|
+
"""Get store path from config or default"""
|
|
24
|
+
config_file = Path.home() / ".mcli" / "store.conf"
|
|
25
|
+
|
|
26
|
+
if config_file.exists():
|
|
27
|
+
store_path = Path(config_file.read_text().strip())
|
|
28
|
+
if store_path.exists():
|
|
29
|
+
return store_path
|
|
30
|
+
|
|
31
|
+
# Use default
|
|
32
|
+
return DEFAULT_STORE_PATH
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.group(name="store")
|
|
36
|
+
def store():
|
|
37
|
+
"""Manage command store - sync ~/.mcli/commands/ to git"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@store.command(name="init")
|
|
42
|
+
@click.option("--path", "-p", type=click.Path(), help=f"Store path (default: {DEFAULT_STORE_PATH})")
|
|
43
|
+
@click.option("--remote", "-r", help="Git remote URL (optional)")
|
|
44
|
+
def init_store(path, remote):
|
|
45
|
+
"""Initialize command store with git"""
|
|
46
|
+
store_path = Path(path) if path else DEFAULT_STORE_PATH
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# Create store directory
|
|
50
|
+
store_path.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
# Initialize git if not already initialized
|
|
53
|
+
git_dir = store_path / ".git"
|
|
54
|
+
if not git_dir.exists():
|
|
55
|
+
subprocess.run(["git", "init"], cwd=store_path, check=True, capture_output=True)
|
|
56
|
+
success(f"Initialized git repository at {store_path}")
|
|
57
|
+
|
|
58
|
+
# Create .gitignore
|
|
59
|
+
gitignore = store_path / ".gitignore"
|
|
60
|
+
gitignore.write_text("*.backup\n.DS_Store\n")
|
|
61
|
+
|
|
62
|
+
# Create README
|
|
63
|
+
readme = store_path / "README.md"
|
|
64
|
+
readme.write_text(
|
|
65
|
+
f"""# MCLI Commands Store
|
|
66
|
+
|
|
67
|
+
Personal workflow commands for mcli framework.
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
Push commands:
|
|
72
|
+
```bash
|
|
73
|
+
mcli store push
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Pull commands:
|
|
77
|
+
```bash
|
|
78
|
+
mcli store pull
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Sync (bidirectional):
|
|
82
|
+
```bash
|
|
83
|
+
mcli store sync
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Structure
|
|
87
|
+
|
|
88
|
+
All JSON command files from `~/.mcli/commands/` are stored here and version controlled.
|
|
89
|
+
|
|
90
|
+
Last updated: {datetime.now().isoformat()}
|
|
91
|
+
"""
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Add remote if provided
|
|
95
|
+
if remote:
|
|
96
|
+
subprocess.run(
|
|
97
|
+
["git", "remote", "add", "origin", remote], cwd=store_path, check=True
|
|
98
|
+
)
|
|
99
|
+
success(f"Added remote: {remote}")
|
|
100
|
+
else:
|
|
101
|
+
info(f"Git repository already exists at {store_path}")
|
|
102
|
+
|
|
103
|
+
# Save store path to config
|
|
104
|
+
config_file = Path.home() / ".mcli" / "store.conf"
|
|
105
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
config_file.write_text(str(store_path))
|
|
107
|
+
|
|
108
|
+
success(f"Command store initialized at {store_path}")
|
|
109
|
+
info(f"Store path saved to {config_file}")
|
|
110
|
+
|
|
111
|
+
except subprocess.CalledProcessError as e:
|
|
112
|
+
error(f"Git command failed: {e}")
|
|
113
|
+
logger.error(f"Git init failed: {e}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
error(f"Failed to initialize store: {e}")
|
|
116
|
+
logger.exception(e)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@store.command(name="push")
|
|
120
|
+
@click.option("--message", "-m", help="Commit message")
|
|
121
|
+
@click.option("--all", "-a", is_flag=True, help="Push all files (including backups)")
|
|
122
|
+
@click.option(
|
|
123
|
+
"--global", "-g", "is_global", is_flag=True, help="Push global commands instead of local"
|
|
124
|
+
)
|
|
125
|
+
def push_commands(message, all, is_global):
|
|
126
|
+
"""
|
|
127
|
+
Push commands to git store.
|
|
128
|
+
|
|
129
|
+
By default pushes local commands (if in git repo), use --global/-g for global commands.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
store_path = _get_store_path()
|
|
133
|
+
from mcli.lib.paths import get_custom_commands_dir
|
|
134
|
+
|
|
135
|
+
COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
|
|
136
|
+
|
|
137
|
+
# Copy commands to store
|
|
138
|
+
info(f"Copying commands from {COMMANDS_PATH} to {store_path}...")
|
|
139
|
+
|
|
140
|
+
copied_count = 0
|
|
141
|
+
for item in COMMANDS_PATH.glob("*"):
|
|
142
|
+
# Skip backups unless --all specified
|
|
143
|
+
if not all and item.name.endswith(".backup"):
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
dest = store_path / item.name
|
|
147
|
+
if item.is_file():
|
|
148
|
+
shutil.copy2(item, dest)
|
|
149
|
+
copied_count += 1
|
|
150
|
+
elif item.is_dir():
|
|
151
|
+
shutil.copytree(item, dest, dirs_exist_ok=True)
|
|
152
|
+
copied_count += 1
|
|
153
|
+
|
|
154
|
+
success(f"Copied {copied_count} items to store")
|
|
155
|
+
|
|
156
|
+
# Git add, commit, push
|
|
157
|
+
subprocess.run(["git", "add", "."], cwd=store_path, check=True)
|
|
158
|
+
|
|
159
|
+
# Check if there are changes
|
|
160
|
+
result = subprocess.run(
|
|
161
|
+
["git", "status", "--porcelain"], cwd=store_path, capture_output=True, text=True
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if not result.stdout.strip():
|
|
165
|
+
info("No changes to commit")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# Commit with message
|
|
169
|
+
commit_msg = message or f"Update commands {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
170
|
+
subprocess.run(["git", "commit", "-m", commit_msg], cwd=store_path, check=True)
|
|
171
|
+
success(f"Committed changes: {commit_msg}")
|
|
172
|
+
|
|
173
|
+
# Push to remote if configured
|
|
174
|
+
try:
|
|
175
|
+
subprocess.run(["git", "push"], cwd=store_path, check=True, capture_output=True)
|
|
176
|
+
success("Pushed to remote")
|
|
177
|
+
except subprocess.CalledProcessError:
|
|
178
|
+
warning("No remote configured or push failed. Commands committed locally.")
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
error(f"Failed to push commands: {e}")
|
|
182
|
+
logger.exception(e)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@store.command(name="pull")
|
|
186
|
+
@click.option("--force", "-f", is_flag=True, help="Overwrite local commands without backup")
|
|
187
|
+
@click.option(
|
|
188
|
+
"--global", "-g", "is_global", is_flag=True, help="Pull to global commands instead of local"
|
|
189
|
+
)
|
|
190
|
+
def pull_commands(force, is_global):
|
|
191
|
+
"""
|
|
192
|
+
Pull commands from git store.
|
|
193
|
+
|
|
194
|
+
By default pulls to local commands (if in git repo), use --global/-g for global commands.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
store_path = _get_store_path()
|
|
198
|
+
from mcli.lib.paths import get_custom_commands_dir
|
|
199
|
+
|
|
200
|
+
COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
|
|
201
|
+
|
|
202
|
+
# Pull from remote
|
|
203
|
+
try:
|
|
204
|
+
subprocess.run(["git", "pull"], cwd=store_path, check=True)
|
|
205
|
+
success("Pulled latest changes from remote")
|
|
206
|
+
except subprocess.CalledProcessError:
|
|
207
|
+
warning("No remote configured or pull failed. Using local store.")
|
|
208
|
+
|
|
209
|
+
# Backup existing commands if not force
|
|
210
|
+
if not force and COMMANDS_PATH.exists():
|
|
211
|
+
backup_dir = (
|
|
212
|
+
COMMANDS_PATH.parent / f"commands_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
213
|
+
)
|
|
214
|
+
shutil.copytree(COMMANDS_PATH, backup_dir)
|
|
215
|
+
info(f"Backed up existing commands to {backup_dir}")
|
|
216
|
+
|
|
217
|
+
# Copy from store to commands directory
|
|
218
|
+
info(f"Copying commands from {store_path} to {COMMANDS_PATH}...")
|
|
219
|
+
|
|
220
|
+
COMMANDS_PATH.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
|
|
222
|
+
copied_count = 0
|
|
223
|
+
for item in store_path.glob("*"):
|
|
224
|
+
# Skip git directory and README
|
|
225
|
+
if item.name in [".git", "README.md", ".gitignore"]:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
dest = COMMANDS_PATH / item.name
|
|
229
|
+
if item.is_file():
|
|
230
|
+
shutil.copy2(item, dest)
|
|
231
|
+
copied_count += 1
|
|
232
|
+
elif item.is_dir():
|
|
233
|
+
shutil.copytree(item, dest, dirs_exist_ok=True)
|
|
234
|
+
copied_count += 1
|
|
235
|
+
|
|
236
|
+
success(f"Pulled {copied_count} items from store")
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
error(f"Failed to pull commands: {e}")
|
|
240
|
+
logger.exception(e)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@store.command(name="sync")
|
|
244
|
+
@click.option("--message", "-m", help="Commit message if pushing")
|
|
245
|
+
@click.option(
|
|
246
|
+
"--global", "-g", "is_global", is_flag=True, help="Sync global commands instead of local"
|
|
247
|
+
)
|
|
248
|
+
def sync_commands(message, is_global):
|
|
249
|
+
"""
|
|
250
|
+
Sync commands bidirectionally (pull then push if changes).
|
|
251
|
+
|
|
252
|
+
By default syncs local commands (if in git repo), use --global/-g for global commands.
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
store_path = _get_store_path()
|
|
256
|
+
from mcli.lib.paths import get_custom_commands_dir
|
|
257
|
+
|
|
258
|
+
COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
|
|
259
|
+
|
|
260
|
+
# First pull
|
|
261
|
+
info("Pulling latest changes...")
|
|
262
|
+
try:
|
|
263
|
+
subprocess.run(["git", "pull"], cwd=store_path, check=True, capture_output=True)
|
|
264
|
+
success("Pulled from remote")
|
|
265
|
+
except subprocess.CalledProcessError:
|
|
266
|
+
warning("No remote or pull failed")
|
|
267
|
+
|
|
268
|
+
# Then push local changes
|
|
269
|
+
info("Pushing local changes...")
|
|
270
|
+
|
|
271
|
+
# Copy commands
|
|
272
|
+
for item in COMMANDS_PATH.glob("*"):
|
|
273
|
+
if item.name.endswith(".backup"):
|
|
274
|
+
continue
|
|
275
|
+
dest = store_path / item.name
|
|
276
|
+
if item.is_file():
|
|
277
|
+
shutil.copy2(item, dest)
|
|
278
|
+
elif item.is_dir():
|
|
279
|
+
shutil.copytree(item, dest, dirs_exist_ok=True)
|
|
280
|
+
|
|
281
|
+
# Check for changes
|
|
282
|
+
result = subprocess.run(
|
|
283
|
+
["git", "status", "--porcelain"], cwd=store_path, capture_output=True, text=True
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if not result.stdout.strip():
|
|
287
|
+
success("Everything in sync!")
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Commit and push
|
|
291
|
+
subprocess.run(["git", "add", "."], cwd=store_path, check=True)
|
|
292
|
+
commit_msg = message or f"Sync commands {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
293
|
+
subprocess.run(["git", "commit", "-m", commit_msg], cwd=store_path, check=True)
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
subprocess.run(["git", "push"], cwd=store_path, check=True, capture_output=True)
|
|
297
|
+
success("Synced and pushed to remote")
|
|
298
|
+
except subprocess.CalledProcessError:
|
|
299
|
+
success("Synced locally (no remote configured)")
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
error(f"Sync failed: {e}")
|
|
303
|
+
logger.exception(e)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@store.command(name="status")
|
|
307
|
+
def store_status():
|
|
308
|
+
"""Show git status of command store"""
|
|
309
|
+
try:
|
|
310
|
+
store_path = _get_store_path()
|
|
311
|
+
|
|
312
|
+
click.echo(f"\n📦 Store: {store_path}\n")
|
|
313
|
+
|
|
314
|
+
# Git status
|
|
315
|
+
result = subprocess.run(
|
|
316
|
+
["git", "status", "--short", "--branch"], cwd=store_path, capture_output=True, text=True
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if result.stdout:
|
|
320
|
+
click.echo(result.stdout)
|
|
321
|
+
|
|
322
|
+
# Show remote
|
|
323
|
+
result = subprocess.run(
|
|
324
|
+
["git", "remote", "-v"], cwd=store_path, capture_output=True, text=True
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
if result.stdout:
|
|
328
|
+
click.echo("\n🌐 Remotes:")
|
|
329
|
+
click.echo(result.stdout)
|
|
330
|
+
else:
|
|
331
|
+
info("\nNo remote configured")
|
|
332
|
+
|
|
333
|
+
click.echo()
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
error(f"Failed to get status: {e}")
|
|
337
|
+
logger.exception(e)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@store.command(name="config")
|
|
341
|
+
@click.option("--remote", "-r", help="Set git remote URL")
|
|
342
|
+
@click.option("--path", "-p", type=click.Path(), help="Change store path")
|
|
343
|
+
def configure_store(remote, path):
|
|
344
|
+
"""Configure store settings"""
|
|
345
|
+
try:
|
|
346
|
+
store_path = _get_store_path()
|
|
347
|
+
|
|
348
|
+
if path:
|
|
349
|
+
new_path = Path(path).expanduser().resolve()
|
|
350
|
+
config_file = Path.home() / ".mcli" / "store.conf"
|
|
351
|
+
config_file.write_text(str(new_path))
|
|
352
|
+
success(f"Store path updated to: {new_path}")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
if remote:
|
|
356
|
+
# Check if remote exists
|
|
357
|
+
result = subprocess.run(
|
|
358
|
+
["git", "remote"], cwd=store_path, capture_output=True, text=True
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if "origin" in result.stdout:
|
|
362
|
+
subprocess.run(
|
|
363
|
+
["git", "remote", "set-url", "origin", remote], cwd=store_path, check=True
|
|
364
|
+
)
|
|
365
|
+
success(f"Updated remote URL: {remote}")
|
|
366
|
+
else:
|
|
367
|
+
subprocess.run(
|
|
368
|
+
["git", "remote", "add", "origin", remote], cwd=store_path, check=True
|
|
369
|
+
)
|
|
370
|
+
success(f"Added remote URL: {remote}")
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
error(f"Configuration failed: {e}")
|
|
374
|
+
logger.exception(e)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@store.command(name="list")
|
|
378
|
+
@click.option("--store-dir", "-s", is_flag=True, help="List store instead of local")
|
|
379
|
+
def list_commands(store_dir):
|
|
380
|
+
"""List all commands"""
|
|
381
|
+
try:
|
|
382
|
+
if store_dir:
|
|
383
|
+
store_path = _get_store_path()
|
|
384
|
+
path = store_path
|
|
385
|
+
title = f"Commands in store ({store_path})"
|
|
386
|
+
else:
|
|
387
|
+
path = COMMANDS_PATH
|
|
388
|
+
title = f"Local commands ({COMMANDS_PATH})"
|
|
389
|
+
|
|
390
|
+
click.echo(f"\n{title}:\n")
|
|
391
|
+
|
|
392
|
+
if not path.exists():
|
|
393
|
+
warning(f"Directory does not exist: {path}")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
items = sorted(path.glob("*"))
|
|
397
|
+
if not items:
|
|
398
|
+
info("No commands found")
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
for item in items:
|
|
402
|
+
if item.name in [".git", ".gitignore", "README.md"]:
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
if item.is_file():
|
|
406
|
+
size = item.stat().st_size / 1024
|
|
407
|
+
modified = datetime.fromtimestamp(item.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
408
|
+
click.echo(f" 📄 {item.name:<40} {size:>8.1f} KB {modified}")
|
|
409
|
+
elif item.is_dir():
|
|
410
|
+
count = len(list(item.glob("*")))
|
|
411
|
+
click.echo(f" 📁 {item.name:<40} {count:>3} files")
|
|
412
|
+
|
|
413
|
+
click.echo()
|
|
414
|
+
|
|
415
|
+
except Exception as e:
|
|
416
|
+
error(f"Failed to list commands: {e}")
|
|
417
|
+
logger.exception(e)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@store.command(name="show")
|
|
421
|
+
@click.argument("command_name")
|
|
422
|
+
@click.option("--store-dir", "-s", is_flag=True, help="Show from store instead of local")
|
|
423
|
+
def show_command(command_name, store_dir):
|
|
424
|
+
"""Show command file contents"""
|
|
425
|
+
try:
|
|
426
|
+
if store_dir:
|
|
427
|
+
store_path = _get_store_path()
|
|
428
|
+
path = store_path / command_name
|
|
429
|
+
else:
|
|
430
|
+
path = COMMANDS_PATH / command_name
|
|
431
|
+
|
|
432
|
+
if not path.exists():
|
|
433
|
+
error(f"Command not found: {command_name}")
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
if path.is_file():
|
|
437
|
+
click.echo(f"\n📄 {path}:\n")
|
|
438
|
+
click.echo(path.read_text())
|
|
439
|
+
else:
|
|
440
|
+
info(f"{command_name} is a directory")
|
|
441
|
+
for item in sorted(path.glob("*")):
|
|
442
|
+
click.echo(f" {item.name}")
|
|
443
|
+
|
|
444
|
+
click.echo()
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
error(f"Failed to show command: {e}")
|
|
448
|
+
logger.exception(e)
|
mcli/lib/custom_commands.py
CHANGED
|
@@ -21,9 +21,9 @@ import click
|
|
|
21
21
|
from mcli.lib.logger.logger import get_logger, register_subprocess
|
|
22
22
|
from mcli.lib.paths import (
|
|
23
23
|
get_custom_commands_dir,
|
|
24
|
+
get_git_root,
|
|
24
25
|
get_lockfile_path,
|
|
25
26
|
is_git_repository,
|
|
26
|
-
get_git_root,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
logger = get_logger()
|
|
@@ -131,7 +131,7 @@ class CustomCommandManager:
|
|
|
131
131
|
List of command data dictionaries
|
|
132
132
|
"""
|
|
133
133
|
commands = []
|
|
134
|
-
include_test = os.environ.get(
|
|
134
|
+
include_test = os.environ.get("MCLI_INCLUDE_TEST_COMMANDS", "false").lower() == "true"
|
|
135
135
|
|
|
136
136
|
for command_file in self.commands_dir.glob("*.json"):
|
|
137
137
|
# Skip the lockfile
|
|
@@ -139,7 +139,7 @@ class CustomCommandManager:
|
|
|
139
139
|
continue
|
|
140
140
|
|
|
141
141
|
# Skip test commands unless explicitly included
|
|
142
|
-
if not include_test and command_file.stem.startswith((
|
|
142
|
+
if not include_test and command_file.stem.startswith(("test_", "test-")):
|
|
143
143
|
logger.debug(f"Skipping test command: {command_file.name}")
|
|
144
144
|
continue
|
|
145
145
|
|
mcli/lib/optional_deps.py
CHANGED
|
@@ -219,9 +219,7 @@ def check_dependencies(*module_names: str) -> Dict[str, bool]:
|
|
|
219
219
|
>>> print(status)
|
|
220
220
|
{'torch': True, 'transformers': False, 'streamlit': True}
|
|
221
221
|
"""
|
|
222
|
-
return {
|
|
223
|
-
name: OptionalDependency(name).available for name in module_names
|
|
224
|
-
}
|
|
222
|
+
return {name: OptionalDependency(name).available for name in module_names}
|
|
225
223
|
|
|
226
224
|
|
|
227
225
|
# Pre-register common optional dependencies
|