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/self/migrate_cmd.py CHANGED
@@ -32,72 +32,102 @@ def get_migration_status() -> dict:
32
32
  Returns:
33
33
  Dictionary with migration status information
34
34
  """
35
+ from mcli.lib.paths import get_git_root, is_git_repository
36
+
35
37
  mcli_home = Path.home() / ".mcli"
36
38
  old_commands_dir = mcli_home / "commands"
37
39
  new_workflows_dir = mcli_home / "workflows"
38
40
 
39
41
  status = {
40
- "old_dir_exists": old_commands_dir.exists(),
41
- "old_dir_path": str(old_commands_dir),
42
- "new_dir_exists": new_workflows_dir.exists(),
43
- "new_dir_path": str(new_workflows_dir),
44
- "needs_migration": False,
45
- "files_to_migrate": [],
46
- "migration_done": False,
42
+ "global": {
43
+ "old_dir_exists": old_commands_dir.exists(),
44
+ "old_dir_path": str(old_commands_dir),
45
+ "new_dir_exists": new_workflows_dir.exists(),
46
+ "new_dir_path": str(new_workflows_dir),
47
+ "needs_migration": False,
48
+ "files_to_migrate": [],
49
+ "migration_done": False,
50
+ },
51
+ "local": None,
47
52
  }
48
53
 
49
- # Check if migration is needed
54
+ # Check global migration
50
55
  if old_commands_dir.exists():
51
56
  # Count files that need migration (excluding hidden files)
52
57
  files = [
53
- f for f in old_commands_dir.iterdir()
54
- if f.is_file() and not f.name.startswith('.')
58
+ f for f in old_commands_dir.iterdir() if f.is_file() and not f.name.startswith(".")
55
59
  ]
56
- status["files_to_migrate"] = [f.name for f in files]
57
- status["needs_migration"] = len(files) > 0
60
+ status["global"]["files_to_migrate"] = [f.name for f in files]
61
+ status["global"]["needs_migration"] = len(files) > 0
58
62
 
59
- # Check if migration already done
63
+ # Check if global migration already done
60
64
  if new_workflows_dir.exists() and not old_commands_dir.exists():
61
- status["migration_done"] = True
65
+ status["global"]["migration_done"] = True
66
+
67
+ # Check local migration (if in git repo)
68
+ if is_git_repository():
69
+ git_root = get_git_root()
70
+ local_old = git_root / ".mcli" / "commands"
71
+ local_new = git_root / ".mcli" / "workflows"
72
+
73
+ status["local"] = {
74
+ "git_root": str(git_root),
75
+ "old_dir_exists": local_old.exists(),
76
+ "old_dir_path": str(local_old),
77
+ "new_dir_exists": local_new.exists(),
78
+ "new_dir_path": str(local_new),
79
+ "needs_migration": False,
80
+ "files_to_migrate": [],
81
+ "migration_done": False,
82
+ }
83
+
84
+ if local_old.exists():
85
+ files = [
86
+ f for f in local_old.iterdir() if f.is_file() and not f.name.startswith(".")
87
+ ]
88
+ status["local"]["files_to_migrate"] = [f.name for f in files]
89
+ status["local"]["needs_migration"] = len(files) > 0
90
+
91
+ if local_new.exists() and not local_old.exists():
92
+ status["local"]["migration_done"] = True
62
93
 
63
94
  return status
64
95
 
65
96
 
66
- def migrate_commands_to_workflows(dry_run: bool = False, force: bool = False) -> Tuple[bool, str]:
97
+ def migrate_directory(
98
+ old_dir: Path, new_dir: Path, dry_run: bool = False, force: bool = False
99
+ ) -> Tuple[bool, str, List[str], List[str]]:
67
100
  """
68
- Migrate ~/.mcli/commands to ~/.mcli/workflows.
101
+ Migrate a commands directory to workflows directory.
69
102
 
70
103
  Args:
104
+ old_dir: Source directory to migrate from
105
+ new_dir: Target directory to migrate to
71
106
  dry_run: If True, show what would be done without actually doing it
72
107
  force: If True, proceed even if workflows directory exists
73
108
 
74
109
  Returns:
75
- Tuple of (success, message)
110
+ Tuple of (success, message, migrated_files, skipped_files)
76
111
  """
77
- mcli_home = Path.home() / ".mcli"
78
- old_dir = mcli_home / "commands"
79
- new_dir = mcli_home / "workflows"
80
-
81
112
  # Check if old directory exists
82
113
  if not old_dir.exists():
83
- return False, f"Nothing to migrate: {old_dir} does not exist"
114
+ return False, f"Nothing to migrate: {old_dir} does not exist", [], []
84
115
 
85
116
  # Check if new directory already exists
86
117
  if new_dir.exists() and not force:
87
- return False, f"Target directory {new_dir} already exists. Use --force to override."
118
+ return False, f"Target directory {new_dir} already exists. Use --force to override.", [], []
88
119
 
89
120
  # Get list of files to migrate
90
- files_to_migrate = [
91
- f for f in old_dir.iterdir()
92
- if f.is_file() and not f.name.startswith('.')
93
- ]
121
+ files_to_migrate = [f for f in old_dir.iterdir() if f.is_file() and not f.name.startswith(".")]
94
122
 
95
123
  if not files_to_migrate:
96
- return False, f"No files to migrate in {old_dir}"
124
+ return False, f"No files to migrate in {old_dir}", [], []
97
125
 
98
126
  if dry_run:
99
- message = f"[DRY RUN] Would migrate {len(files_to_migrate)} files from {old_dir} to {new_dir}"
100
- return True, message
127
+ message = (
128
+ f"[DRY RUN] Would migrate {len(files_to_migrate)} files from {old_dir} to {new_dir}"
129
+ )
130
+ return True, message, [f.name for f in files_to_migrate], []
101
131
 
102
132
  try:
103
133
  # Create new directory if it doesn't exist
@@ -115,7 +145,9 @@ def migrate_commands_to_workflows(dry_run: bool = False, force: bool = False) ->
115
145
  if target_path.exists():
116
146
  if force:
117
147
  # Backup existing file
118
- backup_path = target_path.with_suffix(f".backup.{datetime.now().strftime('%Y%m%d%H%M%S')}")
148
+ backup_path = target_path.with_suffix(
149
+ f".backup.{datetime.now().strftime('%Y%m%d%H%M%S')}"
150
+ )
119
151
  shutil.move(str(target_path), str(backup_path))
120
152
  logger.info(f"Backed up existing file to {backup_path}")
121
153
  else:
@@ -129,8 +161,7 @@ def migrate_commands_to_workflows(dry_run: bool = False, force: bool = False) ->
129
161
 
130
162
  # Check if old directory is now empty (only hidden files remain)
131
163
  remaining_files = [
132
- f for f in old_dir.iterdir()
133
- if f.is_file() and not f.name.startswith('.')
164
+ f for f in old_dir.iterdir() if f.is_file() and not f.name.startswith(".")
134
165
  ]
135
166
 
136
167
  # If empty, remove old directory
@@ -152,11 +183,62 @@ def migrate_commands_to_workflows(dry_run: bool = False, force: bool = False) ->
152
183
  if remaining_files:
153
184
  report_lines.append(f"Note: {len(remaining_files)} files remain in {old_dir}")
154
185
 
155
- return True, "\n".join(report_lines)
186
+ return True, "\n".join(report_lines), migrated_files, skipped_files
156
187
 
157
188
  except Exception as e:
158
189
  logger.error(f"Migration failed: {e}")
159
- return False, f"Migration failed: {str(e)}"
190
+ return False, f"Migration failed: {str(e)}", [], []
191
+
192
+
193
+ def migrate_commands_to_workflows(
194
+ dry_run: bool = False, force: bool = False, scope: str = "all"
195
+ ) -> Tuple[bool, str]:
196
+ """
197
+ Migrate commands to workflows directories.
198
+
199
+ Args:
200
+ dry_run: If True, show what would be done without actually doing it
201
+ force: If True, proceed even if workflows directory exists
202
+ scope: "global", "local", or "all" to control migration scope
203
+
204
+ Returns:
205
+ Tuple of (success, message)
206
+ """
207
+ from mcli.lib.paths import get_git_root, is_git_repository
208
+
209
+ results = []
210
+ all_success = True
211
+
212
+ # Migrate global
213
+ if scope in ["global", "all"]:
214
+ mcli_home = Path.home() / ".mcli"
215
+ old_dir = mcli_home / "commands"
216
+ new_dir = mcli_home / "workflows"
217
+
218
+ success, message, migrated, skipped = migrate_directory(old_dir, new_dir, dry_run, force)
219
+
220
+ if old_dir.exists():
221
+ results.append(f"[Global] {message}")
222
+ if not success and "does not exist" not in message:
223
+ all_success = False
224
+
225
+ # Migrate local (if in git repo)
226
+ if scope in ["local", "all"] and is_git_repository():
227
+ git_root = get_git_root()
228
+ old_dir = git_root / ".mcli" / "commands"
229
+ new_dir = git_root / ".mcli" / "workflows"
230
+
231
+ success, message, migrated, skipped = migrate_directory(old_dir, new_dir, dry_run, force)
232
+
233
+ if old_dir.exists():
234
+ results.append(f"[Local - {git_root.name}] {message}")
235
+ if not success and "does not exist" not in message:
236
+ all_success = False
237
+
238
+ if not results:
239
+ return False, "No migrations needed"
240
+
241
+ return all_success, "\n".join(results)
160
242
 
161
243
 
162
244
  @click.command(name="migrate", help="Perform system migrations for mcli")
@@ -175,18 +257,27 @@ def migrate_commands_to_workflows(dry_run: bool = False, force: bool = False) ->
175
257
  is_flag=True,
176
258
  help="Show migration status without performing migration",
177
259
  )
178
- def migrate_command(dry_run: bool, force: bool, status: bool):
260
+ @click.option(
261
+ "--scope",
262
+ type=click.Choice(["all", "global", "local"], case_sensitive=False),
263
+ default="all",
264
+ help="Migration scope: all (default), global (~/.mcli), or local (.mcli in current repo)",
265
+ )
266
+ def migrate_command(dry_run: bool, force: bool, status: bool, scope: str):
179
267
  """
180
268
  Migrate mcli configuration and data to new structure.
181
269
 
182
270
  Currently handles:
183
- - Moving ~/.mcli/commands to ~/.mcli/workflows
271
+ - Moving ~/.mcli/commands to ~/.mcli/workflows (global)
272
+ - Moving .mcli/commands to .mcli/workflows (local, in git repos)
184
273
 
185
274
  Examples:
186
- mcli self migrate --status # Check migration status
187
- mcli self migrate --dry-run # See what would be done
188
- mcli self migrate # Perform migration
189
- mcli self migrate --force # Force migration (overwrite existing)
275
+ mcli self migrate --status # Check migration status
276
+ mcli self migrate --dry-run # See what would be done
277
+ mcli self migrate # Perform migration (both global and local)
278
+ mcli self migrate --scope global # Migrate only global
279
+ mcli self migrate --scope local # Migrate only local (current repo)
280
+ mcli self migrate --force # Force migration (overwrite existing)
190
281
  """
191
282
 
192
283
  # Get current status
@@ -195,53 +286,92 @@ def migrate_command(dry_run: bool, force: bool, status: bool):
195
286
  # If --status flag, just show status and exit
196
287
  if status:
197
288
  console.print("\n[bold cyan]Migration Status[/bold cyan]")
198
- console.print(f"\n[bold]Old location:[/bold] {migration_status['old_dir_path']}")
199
- console.print(f" Exists: {'✓ Yes' if migration_status['old_dir_exists'] else '✗ No'}")
200
-
201
- console.print(f"\n[bold]New location:[/bold] {migration_status['new_dir_path']}")
202
- console.print(f" Exists: {'✓ Yes' if migration_status['new_dir_exists'] else '✗ No'}")
203
-
204
- if migration_status['needs_migration']:
205
- console.print(f"\n[yellow]⚠ Migration needed[/yellow]")
206
- console.print(f"Files to migrate: {len(migration_status['files_to_migrate'])}")
207
289
 
208
- if migration_status['files_to_migrate']:
209
- table = Table(title="Files to Migrate")
210
- table.add_column("File Name", style="cyan")
211
-
212
- for filename in sorted(migration_status['files_to_migrate']):
213
- table.add_row(filename)
214
-
215
- console.print(table)
216
-
217
- console.print(f"\n[dim]Run 'mcli self migrate' to perform migration[/dim]")
218
- elif migration_status['migration_done']:
219
- console.print(f"\n[green]✓ Migration already completed[/green]")
290
+ # Show global status
291
+ global_status = migration_status["global"]
292
+ console.print(f"\n[bold]Global (~/.mcli)[/bold]")
293
+ console.print(f" Old location: {global_status['old_dir_path']}")
294
+ console.print(f" Exists: {'✓ Yes' if global_status['old_dir_exists'] else '✗ No'}")
295
+ console.print(f" New location: {global_status['new_dir_path']}")
296
+ console.print(f" Exists: {'✓ Yes' if global_status['new_dir_exists'] else '✗ No'}")
297
+
298
+ if global_status["needs_migration"]:
299
+ console.print(f" [yellow] Migration needed[/yellow]")
300
+ console.print(f" Files to migrate: {len(global_status['files_to_migrate'])}")
301
+ elif global_status["migration_done"]:
302
+ console.print(f" [green]✓ Migration completed[/green]")
220
303
  else:
221
- console.print(f"\n[green]✓ No migration needed[/green]")
304
+ console.print(f" [green]✓ No migration needed[/green]")
305
+
306
+ # Show local status if in git repo
307
+ if migration_status["local"]:
308
+ local_status = migration_status["local"]
309
+ console.print(f"\n[bold]Local (current repository: {local_status['git_root']})[/bold]")
310
+ console.print(f" Old location: {local_status['old_dir_path']}")
311
+ console.print(f" Exists: {'✓ Yes' if local_status['old_dir_exists'] else '✗ No'}")
312
+ console.print(f" New location: {local_status['new_dir_path']}")
313
+ console.print(f" Exists: {'✓ Yes' if local_status['new_dir_exists'] else '✗ No'}")
314
+
315
+ if local_status["needs_migration"]:
316
+ console.print(f" [yellow]⚠ Migration needed[/yellow]")
317
+ console.print(f" Files to migrate: {len(local_status['files_to_migrate'])}")
318
+ elif local_status["migration_done"]:
319
+ console.print(f" [green]✓ Migration completed[/green]")
320
+ else:
321
+ console.print(f" [green]✓ No migration needed[/green]")
322
+
323
+ # Show files to migrate if any
324
+ all_files = global_status.get("files_to_migrate", [])
325
+ if migration_status["local"]:
326
+ all_files.extend(migration_status["local"].get("files_to_migrate", []))
327
+
328
+ if all_files:
329
+ console.print(f"\n[bold]Files to migrate:[/bold]")
330
+ table = Table()
331
+ table.add_column("Location", style="cyan")
332
+ table.add_column("File Name", style="yellow")
333
+
334
+ for filename in sorted(global_status.get("files_to_migrate", [])):
335
+ table.add_row("Global", filename)
336
+
337
+ if migration_status["local"]:
338
+ for filename in sorted(migration_status["local"].get("files_to_migrate", [])):
339
+ table.add_row("Local", filename)
340
+
341
+ console.print(table)
342
+ console.print(f"\n[dim]Run 'mcli self migrate' to perform migration[/dim]")
222
343
 
223
344
  return
224
345
 
225
346
  # Check if migration is needed
226
- if not migration_status['needs_migration']:
227
- if migration_status['migration_done']:
228
- info("Migration already completed")
229
- info(f"Workflows directory: {migration_status['new_dir_path']}")
230
- else:
231
- info("No migration needed")
347
+ needs_any_migration = global_status["needs_migration"]
348
+ if migration_status["local"]:
349
+ needs_any_migration = needs_any_migration or migration_status["local"]["needs_migration"]
350
+
351
+ if not needs_any_migration:
352
+ info("No migration needed")
232
353
  return
233
354
 
234
355
  # Show what will be migrated
235
356
  console.print("\n[bold cyan]Migration Plan[/bold cyan]")
236
- console.print(f"\nSource: [cyan]{migration_status['old_dir_path']}[/cyan]")
237
- console.print(f"Target: [cyan]{migration_status['new_dir_path']}[/cyan]")
238
- console.print(f"Files: [yellow]{len(migration_status['files_to_migrate'])}[/yellow]")
357
+
358
+ if scope in ["global", "all"] and global_status["needs_migration"]:
359
+ console.print(f"\n[bold]Global:[/bold]")
360
+ console.print(f" Source: [cyan]{global_status['old_dir_path']}[/cyan]")
361
+ console.print(f" Target: [cyan]{global_status['new_dir_path']}[/cyan]")
362
+ console.print(f" Files: [yellow]{len(global_status['files_to_migrate'])}[/yellow]")
363
+
364
+ if scope in ["local", "all"] and migration_status["local"] and migration_status["local"]["needs_migration"]:
365
+ console.print(f"\n[bold]Local:[/bold]")
366
+ console.print(f" Source: [cyan]{migration_status['local']['old_dir_path']}[/cyan]")
367
+ console.print(f" Target: [cyan]{migration_status['local']['new_dir_path']}[/cyan]")
368
+ console.print(f" Files: [yellow]{len(migration_status['local']['files_to_migrate'])}[/yellow]")
239
369
 
240
370
  if dry_run:
241
371
  console.print(f"\n[yellow]DRY RUN MODE - No changes will be made[/yellow]")
242
372
 
243
373
  # Perform migration
244
- success_flag, message = migrate_commands_to_workflows(dry_run=dry_run, force=force)
374
+ success_flag, message = migrate_commands_to_workflows(dry_run=dry_run, force=force, scope=scope)
245
375
 
246
376
  if success_flag:
247
377
  if dry_run:
@@ -249,12 +379,15 @@ def migrate_command(dry_run: bool, force: bool, status: bool):
249
379
  else:
250
380
  success(message)
251
381
  console.print("\n[green]✓ Migration completed successfully[/green]")
252
- console.print(f"\nYour workflows are now in: [cyan]{migration_status['new_dir_path']}[/cyan]")
253
- console.print("\n[dim]You can now use 'mcli workflow' to manage and 'mcli workflows' to run them[/dim]")
382
+ console.print(
383
+ "\n[dim]You can now use 'mcli workflow' to manage and 'mcli workflows' to run them[/dim]"
384
+ )
254
385
  else:
255
386
  error(message)
256
387
  if not force and "already exists" in message:
257
- console.print("\n[yellow]Tip: Use --force to proceed anyway (will backup existing files)[/yellow]")
388
+ console.print(
389
+ "\n[yellow]Tip: Use --force to proceed anyway (will backup existing files)[/yellow]"
390
+ )
258
391
 
259
392
 
260
393
  if __name__ == "__main__":
@@ -1,4 +1,5 @@
1
1
  """Secrets workflow - migrated from lib.secrets"""
2
+
2
3
  from .secrets_cmd import secrets
3
4
 
4
5
  __all__ = ["secrets"]
@@ -10,12 +10,11 @@ from typing import Optional
10
10
 
11
11
  import click
12
12
 
13
- from mcli.lib.ui.styling import error, info, success, warning
14
-
15
13
  # Import from the original lib.secrets modules (keeping the implementation)
16
14
  from mcli.lib.secrets.manager import SecretsManager
17
15
  from mcli.lib.secrets.repl import run_repl
18
16
  from mcli.lib.secrets.store import SecretsStore
17
+ from mcli.lib.ui.styling import error, info, success, warning
19
18
 
20
19
 
21
20
  @click.command(name="secrets", help="Secure secrets management with encryption and git sync")
mcli/workflow/workflow.py CHANGED
@@ -17,16 +17,47 @@ class ScopedWorkflowsGroup(click.Group):
17
17
  def list_commands(self, ctx):
18
18
  """List available commands based on scope."""
19
19
  # Get scope from context
20
- is_global = ctx.params.get('is_global', False)
20
+ is_global = ctx.params.get("is_global", False)
21
21
 
22
22
  # Load commands from appropriate directory
23
23
  from mcli.lib.custom_commands import get_command_manager
24
+ from mcli.lib.logger.logger import get_logger
24
25
 
26
+ logger = get_logger()
25
27
  manager = get_command_manager(global_mode=is_global)
26
28
  commands = manager.load_all_commands()
27
29
 
28
- # Filter to only workflow group commands
29
- workflow_commands = [cmd.get('name') for cmd in commands if cmd.get('group') == 'workflow']
30
+ # Filter to only workflow group commands AND validate they can be loaded
31
+ workflow_commands = []
32
+ for cmd_data in commands:
33
+ if cmd_data.get("group") != "workflow":
34
+ continue
35
+
36
+ cmd_name = cmd_data.get("name")
37
+ # Validate the command can be loaded
38
+ temp_group = click.Group()
39
+ language = cmd_data.get("language", "python")
40
+
41
+ try:
42
+ if language == "shell":
43
+ success = manager.register_shell_command_with_click(cmd_data, temp_group)
44
+ else:
45
+ success = manager.register_command_with_click(cmd_data, temp_group)
46
+
47
+ if success and temp_group.commands.get(cmd_name):
48
+ workflow_commands.append(cmd_name)
49
+ else:
50
+ # Log the issue but don't show to user during list (too noisy)
51
+ logger.debug(
52
+ f"Workflow '{cmd_name}' has invalid code and will not be loaded. "
53
+ f"Edit with: mcli workflow edit {cmd_name}"
54
+ )
55
+ except Exception as e:
56
+ # Log the issue but don't show to user during list (too noisy)
57
+ logger.debug(
58
+ f"Failed to load workflow '{cmd_name}': {e}. "
59
+ f"Edit with: mcli workflow edit {cmd_name}"
60
+ )
30
61
 
31
62
  # Also include built-in subcommands
32
63
  builtin_commands = list(super().list_commands(ctx))
@@ -41,7 +72,7 @@ class ScopedWorkflowsGroup(click.Group):
41
72
  return builtin_cmd
42
73
 
43
74
  # Get scope from context
44
- is_global = ctx.params.get('is_global', False)
75
+ is_global = ctx.params.get("is_global", False)
45
76
 
46
77
  # Load the workflow command from appropriate directory
47
78
  from mcli.lib.custom_commands import get_command_manager
@@ -51,7 +82,7 @@ class ScopedWorkflowsGroup(click.Group):
51
82
 
52
83
  # Find the workflow command
53
84
  for command_data in commands:
54
- if command_data.get('name') == cmd_name and command_data.get('group') == 'workflow':
85
+ if command_data.get("name") == cmd_name and command_data.get("group") == "workflow":
55
86
  # Create a temporary group to register the command
56
87
  temp_group = click.Group()
57
88
  language = command_data.get("language", "python")
@@ -85,7 +116,7 @@ def workflows(ctx, is_global):
85
116
  """
86
117
  # Store the is_global flag in the context for subcommands to access
87
118
  ctx.ensure_object(dict)
88
- ctx.obj['is_global'] = is_global
119
+ ctx.obj["is_global"] = is_global
89
120
 
90
121
  # If a subcommand was invoked, the subcommand will handle execution
91
122
  if ctx.invoked_subcommand:
@@ -103,6 +134,7 @@ try:
103
134
  except ImportError as e:
104
135
  # Secrets workflow not available
105
136
  import sys
137
+
106
138
  from mcli.lib.logger.logger import get_logger
107
139
 
108
140
  logger = get_logger()
@@ -116,6 +148,7 @@ try:
116
148
  except ImportError as e:
117
149
  # Notebook commands not available
118
150
  import sys
151
+
119
152
  from mcli.lib.logger.logger import get_logger
120
153
 
121
154
  logger = get_logger()
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcli-framework
3
- Version: 7.11.3
4
- Summary: Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/commands/, version with lockfile, run as daemon or cron job.
3
+ Version: 7.12.0
4
+ Summary: Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/workflows/, version with lockfile, run as daemon or cron job.
5
5
  Author-email: Luis Fernandez de la Vara <luis@lefv.io>
6
6
  Maintainer-email: Luis Fernandez de la Vara <luis@lefv.io>
7
7
  License: MIT
@@ -167,7 +167,7 @@ Dynamic: license-file
167
167
 
168
168
  **Transform any script into a versioned, portable, schedulable workflow command.**
169
169
 
170
- MCLI is a modular CLI framework that lets you write scripts once and run them anywhere - as interactive commands, scheduled jobs, or background daemons. Your workflows live in `~/.mcli/commands/`, are versioned via lockfile, and completely decoupled from the engine source code.
170
+ MCLI is a modular CLI framework that lets you write scripts once and run them anywhere - as interactive commands, scheduled jobs, or background daemons. Your workflows live in `~/.mcli/workflows/`, are versioned via lockfile, and completely decoupled from the engine source code.
171
171
 
172
172
  ## 🎯 Core Philosophy
173
173
 
@@ -195,7 +195,7 @@ Edit your workflow JSON files like Jupyter notebooks with our VSCode extension!
195
195
  code --install-extension gwicho38.mcli-framework
196
196
 
197
197
  # Or install from VSIX
198
- code --install-extension vscode-extension/mcli-framework-1.0.0.vsix
198
+ code --install-extension vscode-extension/mcli-framework-2.0.0.vsix
199
199
  ```
200
200
 
201
201
  **Learn More:**
@@ -220,13 +220,13 @@ uv pip install mcli-framework
220
220
 
221
221
  ```bash
222
222
  # Initialize workflows in current git repository
223
- mcli workflow init
223
+ mcli init
224
224
 
225
225
  # Or initialize global workflows
226
- mcli workflow init --global
226
+ mcli init --global
227
227
 
228
228
  # Initialize with git repository for workflows
229
- mcli workflow init --git
229
+ mcli init --git
230
230
  ```
231
231
 
232
232
  This creates a `.mcli/workflows/` directory (local to your repo) or `~/.mcli/workflows/` (global) with:
@@ -314,10 +314,10 @@ mcli workflow import my-workflows.json
314
314
  mcli workflow export-script my-workflow --output my_workflow.py
315
315
  ```
316
316
 
317
- Your workflows are just JSON files in `~/.mcli/commands/`:
317
+ Your workflows are just JSON files in `~/.mcli/workflows/`:
318
318
 
319
319
  ```bash
320
- $ ls ~/.mcli/commands/
320
+ $ ls ~/.mcli/workflows/
321
321
  pdf-processor.json
322
322
  data-sync.json
323
323
  git-commit.json
@@ -358,7 +358,7 @@ Example `commands.lock.json`:
358
358
 
359
359
  ```bash
360
360
  # Add lockfile to git
361
- git add ~/.mcli/commands/commands.lock.json ~/.mcli/commands/*.json
361
+ git add ~/.mcli/workflows/commands.lock.json ~/.mcli/workflows/*.json
362
362
  git commit -m "Update workflows"
363
363
 
364
364
  # On another machine
@@ -513,7 +513,7 @@ You write scripts. They work. Then:
513
513
 
514
514
  ### The MCLI Solution
515
515
 
516
- - ✅ **Centralized Storage**: All workflows in `~/.mcli/commands/`
516
+ - ✅ **Centralized Storage**: All workflows in `~/.mcli/workflows/`
517
517
  - ✅ **Portable**: Export/import as JSON, share anywhere
518
518
  - ✅ **Versioned**: Lockfile for reproducibility
519
519
  - ✅ **Decoupled**: Zero coupling to engine source code
@@ -3,10 +3,13 @@ mcli/__main__.py,sha256=nKdf3WqtXi5PhWhZGjpXKAT3a2yGUYkYCBgSLxk4hSQ,295
3
3
  mcli/cli.py,sha256=6KTyXn-pmVkAbCDu59PbiNKBwNra5su31ujFFZ6CBOM,389
4
4
  mcli/config.toml,sha256=263yEVvP_W9F2zOLssUBgy7amKaRAFQuBrfxcMhKxaQ,1706
5
5
  mcli/app/__init__.py,sha256=D4RiKk2gOEXwanbe_jXyNSb5zdgNi47kahtskMnEwjY,489
6
- mcli/app/commands_cmd.py,sha256=gyvjkX8HyImUKaO37Du6XwklN6FJwvQX-VD3tNU0TGQ,76191
6
+ mcli/app/commands_cmd.py,sha256=RAwkNQIy2XFUQxn56M6rIKas7Qb9BhIFWB4iOj4zTFA,50255
7
7
  mcli/app/completion_helpers.py,sha256=e62C6w2N-XoD66GYYHgtvKKoD3kYMuIeBBGzVKbuL04,7497
8
- mcli/app/main.py,sha256=4rfj8d68OgUErLmIRRFDu9UKBgJwc-HFgJ8MuAPur8s,19517
8
+ mcli/app/init_cmd.py,sha256=Pueg96Gi10VJB20xdEwfw_tDj4v5A5f_dEzKj_130UU,12451
9
+ mcli/app/lock_cmd.py,sha256=dlMk94Lw7FtKDZC92H4GSOBKJva2sKWExIafIr1ftcQ,10146
10
+ mcli/app/main.py,sha256=wFfvILpaP9JmGvRaLqHoLYIAiUGhvPBzh2L149rn9zo,20561
9
11
  mcli/app/model_cmd.py,sha256=LQQD8FaebFoaJGK3u_kt19wZ3HJyo_ecwSMYyC2xIp8,2497
12
+ mcli/app/store_cmd.py,sha256=Pr2S0G-TEC1UuGSIzyNnJBxPEc1lH42Cs8WCUsZHJ8I,14316
10
13
  mcli/app/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
14
  mcli/app/model/model.py,sha256=EUGu_td-hRlbf4OElkdk1-0p7WyuG7sZmb-Ux2-J9KY,39061
12
15
  mcli/app/video/__init__.py,sha256=PxCZQrtClwNQr1RaF8Q3iXlLeQlnEAI9NO8LGcQQuvk,66
@@ -18,9 +21,9 @@ mcli/chat/enhanced_chat.py,sha256=e3odh5klewDHIjfNOyvifLzCdHrysDc2IvNVHzTPIh4,27
18
21
  mcli/chat/system_controller.py,sha256=SuGvnIh2QObvM1DMicF3gGyeBkbz_xXS-hOOHjWx5j4,39114
19
22
  mcli/chat/system_integration.py,sha256=xQ11thOUswPg8r1HZkId6U3bTCOtMYngt0-mUYYXpt4,40196
20
23
  mcli/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- mcli/lib/custom_commands.py,sha256=oDgNuPHonjZ7ckwX4-38XsqZOcaR05ESpuSbSFMB-s4,21739
24
+ mcli/lib/custom_commands.py,sha256=y_S5Ys58EM2pEBcQjmGZy55Ff8O6ahFzuzNkmtMRUjo,21739
22
25
  mcli/lib/lib.py,sha256=-CFUfmcubYBxt3LDBY0uj9DF232pz8MPDu-Qg0Ocy8M,850
23
- mcli/lib/optional_deps.py,sha256=z6jPjU9mjK2ipMf8gzdc_izYSyqefQRzUZWgj_CumtM,7568
26
+ mcli/lib/optional_deps.py,sha256=Yq5gGu3EB_Ahl68Xw2mtGryIvjf0CoSm6t7Ri-A7NJ0,7554
24
27
  mcli/lib/paths.py,sha256=3-yseYbqQ-711GUSO8qlI3Tnlwx4BFO48IeiUETIY-A,6412
25
28
  mcli/lib/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
29
  mcli/lib/api/api.py,sha256=sPgAIYC8Z7AWV2TCBssNSKotbRggBqNLsbfzbjkhmUY,18558
@@ -199,7 +202,7 @@ mcli/public/oi/oi.py,sha256=SQabQWQ1pE67pWYEHwIDc3R93DARJfB6VHk7qxWx9xo,308
199
202
  mcli/self/__init__.py,sha256=7hCrgaRb4oAgAf-kcyzqhJ5LFpW19jwF5bxowR4LwjY,41
200
203
  mcli/self/completion_cmd.py,sha256=JXJ70p_yyfC8KlC5QifkQWLNeS2hgUFmNw8xdiix5O8,7764
201
204
  mcli/self/logs_cmd.py,sha256=SCzZ4VZs6p42hksun_w4WN33xIZgmq7RjdWX8P2WcT4,15056
202
- mcli/self/migrate_cmd.py,sha256=s9_pU-CNskn4To79pHa5AmRMAQdf6MX4XZBzh4ORMmM,9024
205
+ mcli/self/migrate_cmd.py,sha256=DalSn-pvfez4KdYYaYlYi1hqqtZM6bgS--pNZzfVsE8,14665
203
206
  mcli/self/redis_cmd.py,sha256=Cl0LQ3Mqt27gLeb542_xw6bJBbIE-CBmWyMmaUTSk8c,9426
204
207
  mcli/self/self_cmd.py,sha256=m0GhY8u2o5Xo0ws9QaTO_UbdwloOtNQin0vrQEY0o44,38017
205
208
  mcli/self/store_cmd.py,sha256=O6arjRr4qWQKh1QyVWtzyXq5R7yZEBL87FSI59Db7IY,13320
@@ -209,7 +212,7 @@ mcli/self/zsh_cmd.py,sha256=63jKmfjhJp2zxJL2c37OdtdzDrnOreXXfyARN7TyfzU,8294
209
212
  mcli/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
210
213
  mcli/workflow/doc_convert.py,sha256=X7ZCYbGCBZLv6WkxAgHfVOkD-6k-euo6qIaPgaKiwhA,25972
211
214
  mcli/workflow/lsh_integration.py,sha256=jop80DUjdOSxmqPb-gX_OBep5f1twViv-pXmkcFqBPY,13314
212
- mcli/workflow/workflow.py,sha256=ctnPo6dwSTcwUPPTa6_K_feZJUXkkIc9n9jvQf_4n_Q,4159
215
+ mcli/workflow/workflow.py,sha256=vqT7B1N7wEAbCLQB9xOYppJbdaDabH2mlKA6dSDL2Y0,5507
213
216
  mcli/workflow/daemon/__init__.py,sha256=iDounKzoQhHcgHwK1kFJHnPPhn09JFzvSEpXRZDSnxU,565
214
217
  mcli/workflow/daemon/async_command_database.py,sha256=pvfKYjt0Jg1EPwJ1p2C0M3bsBWvjEs4Ok-Y6-jY0qVI,24873
215
218
  mcli/workflow/daemon/async_process_manager.py,sha256=hDehiYuWnBOv8LbMLTDEi4DVyrwm8YrrYF8Ca5P66A4,21374
@@ -261,16 +264,16 @@ mcli/workflow/scheduler/monitor.py,sha256=63TClZQyxbJyx5Sf3q1v8BgO6YPvWQNZYfCixM
261
264
  mcli/workflow/scheduler/persistence.py,sha256=SU8-F5wTpTercZvTeAXKlGI7gwHyfmYDhX2_KnEYnjs,11592
262
265
  mcli/workflow/scheduler/scheduler.py,sha256=1Ujq9VgL1rSTCAtshuLA2_sodW6HOj0MEZem7Ga-kic,23351
263
266
  mcli/workflow/search/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
264
- mcli/workflow/secrets/__init__.py,sha256=3lGQzhtj3mKuBcHrR3DHPMPBcs3XvWAeXazgzxmUKTA,107
265
- mcli/workflow/secrets/secrets_cmd.py,sha256=jremZqlYmVC4dQmn-M2rWKwheS25ra6o0gdHbXJXoMI,6761
267
+ mcli/workflow/secrets/__init__.py,sha256=RxHMwOzN5vCDSA8KN-glhdX-71kuxd09acs6WxRlqvs,108
268
+ mcli/workflow/secrets/secrets_cmd.py,sha256=bTZDsjeOZb6pg11C1fmbDszkoAc07espJGRlkKpfHEI,6760
266
269
  mcli/workflow/sync/__init__.py,sha256=vYjpzxC1OTFvhp2sgBqzrujUhghb4MtFNxrsPNDkUcc,104
267
270
  mcli/workflow/sync/test_cmd.py,sha256=neVgs9zEnKSxlvzDpFkuCGucqnzjrShm2OvJtHibslg,10009
268
271
  mcli/workflow/videos/__init__.py,sha256=aV3DEoO7qdKJY4odWKoQbOKDQq4ludTeCLnZcupOFIM,25
269
272
  mcli/workflow/wakatime/__init__.py,sha256=wKG8cVIHVtMPhNRFGFtX43bRnocHqOMMkFMkmW-M6pU,2626
270
273
  mcli/workflow/wakatime/wakatime.py,sha256=sEjsUKa3-XyE8Ni6sAb_D3GAY5jDcA30KknW9YTbLTA,142
271
- mcli_framework-7.11.3.dist-info/licenses/LICENSE,sha256=sahwAMfrJv2-V66HNPTp7A9UmMjxtyejwTZZoWQvEcI,1075
272
- mcli_framework-7.11.3.dist-info/METADATA,sha256=3YjC0sbDgOscsVMhVbcOsw8pnvhzlq3MlYBvxekbanY,18655
273
- mcli_framework-7.11.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
274
- mcli_framework-7.11.3.dist-info/entry_points.txt,sha256=dYrZbDIm-KUPsl1wfv600Kx_8sMy89phMkCihbDRgP8,261
275
- mcli_framework-7.11.3.dist-info/top_level.txt,sha256=_bnO8J2EUkliWivey_1le0UrnocFKmyVMQjbQ8iVXjc,5
276
- mcli_framework-7.11.3.dist-info/RECORD,,
274
+ mcli_framework-7.12.0.dist-info/licenses/LICENSE,sha256=sahwAMfrJv2-V66HNPTp7A9UmMjxtyejwTZZoWQvEcI,1075
275
+ mcli_framework-7.12.0.dist-info/METADATA,sha256=OohrTi2RR9BjdVJQlLsX7skMsOYiC5Gfd5EGQVCmmcY,18635
276
+ mcli_framework-7.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
277
+ mcli_framework-7.12.0.dist-info/entry_points.txt,sha256=dYrZbDIm-KUPsl1wfv600Kx_8sMy89phMkCihbDRgP8,261
278
+ mcli_framework-7.12.0.dist-info/top_level.txt,sha256=_bnO8J2EUkliWivey_1le0UrnocFKmyVMQjbQ8iVXjc,5
279
+ mcli_framework-7.12.0.dist-info/RECORD,,