mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-08-31 01:42:25 +08:00
add auto-save and dry-run for all session-based softwares on one-shot cmds; highlight the design in cli-anything skill
This commit is contained in:
@@ -121,8 +121,10 @@ def handle_error(func):
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--project", "project_path", type=str, default=None,
|
||||
help="Path to .audacity-cli.json project file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path):
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
"""Audacity CLI — Stateful audio editing from the command line.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
@@ -140,6 +142,21 @@ def cli(ctx, use_json, project_path):
|
||||
ctx.invoke(repl, project_path=None)
|
||||
|
||||
|
||||
@cli.result_callback()
|
||||
def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs):
|
||||
"""Auto-save project after one-shot commands if state was modified."""
|
||||
if _repl_mode:
|
||||
return
|
||||
if dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
try:
|
||||
sess.save_session()
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Auto-save failed: {e}", err=True)
|
||||
|
||||
|
||||
# -- Project Commands ------------------------------------------------------
|
||||
@cli.group()
|
||||
def project():
|
||||
|
||||
@@ -118,8 +118,10 @@ def handle_error(func):
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--project", "project_path", type=str, default=None,
|
||||
help="Path to .blend-cli.json project file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path):
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
"""Blender CLI — Stateful 3D scene editing from the command line.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
@@ -137,6 +139,21 @@ def cli(ctx, use_json, project_path):
|
||||
ctx.invoke(repl, project_path=None)
|
||||
|
||||
|
||||
@cli.result_callback()
|
||||
def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs):
|
||||
"""Auto-save project after one-shot commands if state was modified."""
|
||||
if _repl_mode:
|
||||
return
|
||||
if dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
try:
|
||||
sess.save_session()
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Auto-save failed: {e}", err=True)
|
||||
|
||||
|
||||
# ── Scene Commands ──────────────────────────────────────────────
|
||||
@cli.group()
|
||||
def scene():
|
||||
|
||||
@@ -119,6 +119,12 @@ This command implements the complete cli-anything methodology to build a product
|
||||
/cli-anything https://github.com/blender/blender
|
||||
```
|
||||
|
||||
## Auto-Save + --dry-run (Required for Session-Based CLIs)
|
||||
|
||||
**Session-based CLIs must auto-save after one-shot mutations.** Without this, one-shot commands silently lose changes because `save_session()` is never called before the process exits. A `--dry-run` flag must also be provided to suppress the save.
|
||||
|
||||
See [`guides/auto-save-dry-run.md`](../guides/auto-save-dry-run.md) for the full pattern, code examples, and when it applies.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The command succeeds when:
|
||||
@@ -132,3 +138,4 @@ The command succeeds when:
|
||||
8. SKILL.md is generated with proper YAML frontmatter and command documentation
|
||||
9. setup.py is created and local installation works
|
||||
10. CLI is available in PATH as `cli-anything-<software>`
|
||||
11. **Session-based CLIs implement auto-save + `--dry-run`** (see [guide](../guides/auto-save-dry-run.md))
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Auto-Save + --dry-run for One-Shot Commands
|
||||
|
||||
Session-based CLIs must auto-save after one-shot mutations and support `--dry-run` to suppress it.
|
||||
|
||||
## Problem
|
||||
|
||||
One-shot commands like `cli-anything-kdenlive --project p.json bin import video.mp4` mutate the in-memory project but never call `save_session()`. The project file on disk is unchanged when the process exits — changes are silently lost.
|
||||
|
||||
## Solution
|
||||
|
||||
Two additions to `<software>_cli.py`:
|
||||
|
||||
### 1. Add `--dry-run` to the main CLI group
|
||||
|
||||
```python
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--project", "project_path", type=str, default=None,
|
||||
help="Path to project file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
...
|
||||
```
|
||||
|
||||
### 2. Add `@cli.result_callback()` after the group
|
||||
|
||||
```python
|
||||
@cli.result_callback()
|
||||
def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs):
|
||||
"""Auto-save project after one-shot commands if state was modified."""
|
||||
if _repl_mode:
|
||||
return
|
||||
if dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
try:
|
||||
sess.save_session()
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Auto-save failed: {e}", err=True)
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- `result_callback` fires once after the CLI group's command chain completes
|
||||
- Checks `_repl_mode` (skip in REPL — user saves manually), `dry_run` (skip if set), and `sess._modified` (skip if nothing changed)
|
||||
- Calls `sess.save_session()` which uses atomic `_locked_save_json` (see [`session-locking.md`](session-locking.md))
|
||||
- Does NOT fire if `sys.exit(1)` was called in `handle_error` (error path) — correct behavior
|
||||
|
||||
### Alternative: `ctx.call_on_close` pattern
|
||||
|
||||
For harnesses that open the session inline (not via a global singleton), use a closure instead:
|
||||
|
||||
```python
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
...
|
||||
if project_path:
|
||||
sess = get_session()
|
||||
proj = proj_mod.open_project(project_path)
|
||||
sess.set_project(proj, project_path)
|
||||
|
||||
def _auto_save():
|
||||
if dry_run:
|
||||
return
|
||||
if sess._modified and sess.project_path and not _repl_mode:
|
||||
sess.save_session()
|
||||
|
||||
ctx.call_on_close(_auto_save)
|
||||
```
|
||||
|
||||
## `--dry-run` semantics
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| One-shot (default) | Command executes, output is printed, project is auto-saved |
|
||||
| One-shot + `--dry-run` | Command executes, output is printed, project is **not** saved |
|
||||
| REPL | `--dry-run` is accepted but ignored (REPL never auto-saves) |
|
||||
|
||||
## When this applies
|
||||
|
||||
**Required** for any harness where:
|
||||
- `core/session.py` exists with `save_session()` and `_modified` tracking
|
||||
- The CLI accepts a `--project` flag to load a file-backed project
|
||||
- Commands call `sess.snapshot()` before mutations
|
||||
|
||||
**Does not apply** to stateless API clients, service wrappers, or harnesses without a persistent project file.
|
||||
@@ -130,8 +130,10 @@ def handle_error(func):
|
||||
@click.option("--json", "json_mode", is_flag=True, help="Output in JSON format")
|
||||
@click.option("--session", "session_id", default=None, help="Session ID to use/resume")
|
||||
@click.option("--project", "project_path", default=None, help="Open a project file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, json_mode, session_id, project_path):
|
||||
def cli(ctx, json_mode, session_id, project_path, dry_run):
|
||||
"""Draw.io CLI — Diagram creation from the command line.
|
||||
|
||||
A stateful CLI for manipulating draw.io diagram files.
|
||||
@@ -153,6 +155,8 @@ def cli(ctx, json_mode, session_id, project_path):
|
||||
# Auto-save on exit when --project was used and project was modified
|
||||
@ctx.call_on_close
|
||||
def _auto_save():
|
||||
if dry_run:
|
||||
return
|
||||
if project_path and _session and _session.is_open and _session.is_modified:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -171,8 +171,10 @@ output_fn = output
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output in JSON format.")
|
||||
@click.option("--project", "-p", type=click.Path(), help="Load project file.")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx: click.Context, use_json: bool, project: Optional[str]) -> None:
|
||||
def cli(ctx: click.Context, use_json: bool, project: Optional[str], dry_run: bool) -> None:
|
||||
"""cli-anything-freecad — CLI harness for FreeCAD 3D CAD modeler."""
|
||||
global _json_output
|
||||
_json_output = use_json
|
||||
@@ -184,6 +186,8 @@ def cli(ctx: click.Context, use_json: bool, project: Optional[str]) -> None:
|
||||
|
||||
# Auto-save after one-shot commands when --project is used
|
||||
def _auto_save():
|
||||
if dry_run:
|
||||
return
|
||||
if sess._modified and sess.project_path and not _repl_mode:
|
||||
sess.save_session()
|
||||
|
||||
|
||||
@@ -118,8 +118,10 @@ def handle_error(func):
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--project", "project_path", type=str, default=None,
|
||||
help="Path to .gimp-cli.json project file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path):
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
"""GIMP CLI — Stateful image editing from the command line.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
@@ -138,12 +140,18 @@ def cli(ctx, use_json, project_path):
|
||||
|
||||
|
||||
@cli.result_callback()
|
||||
def auto_save_on_cli(result, **kwargs):
|
||||
"""Auto-save project after CLI commands when --project is specified."""
|
||||
if not _repl_mode:
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
proj_mod.save_project(sess.get_project(), sess.project_path)
|
||||
def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs):
|
||||
"""Auto-save project after one-shot commands if state was modified."""
|
||||
if _repl_mode:
|
||||
return
|
||||
if dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
try:
|
||||
sess.save_session()
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Auto-save failed: {e}", err=True)
|
||||
|
||||
|
||||
# ── Project Commands ─────────────────────────────────────────────
|
||||
|
||||
@@ -40,6 +40,7 @@ _session: Optional[Session] = None
|
||||
_json_output = False
|
||||
_repl_mode = False
|
||||
_auto_save = False
|
||||
_dry_run = False
|
||||
|
||||
|
||||
def get_session() -> Session:
|
||||
@@ -123,8 +124,10 @@ def handle_error(func):
|
||||
help="Path to .inkscape-cli.json project file")
|
||||
@click.option("-s", "--save", "auto_save", is_flag=True,
|
||||
help="Auto-save project after each mutation command (one-shot mode)")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path, auto_save):
|
||||
def cli(ctx, use_json, project_path, auto_save, dry_run):
|
||||
"""Inkscape CLI — Stateful vector graphics editing from the command line.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
@@ -132,9 +135,10 @@ def cli(ctx, use_json, project_path, auto_save):
|
||||
Use -s/--save to automatically save changes after each mutation command.
|
||||
This is useful in one-shot mode where each command runs in a new process.
|
||||
"""
|
||||
global _json_output, _auto_save
|
||||
global _json_output, _auto_save, _dry_run
|
||||
_json_output = use_json
|
||||
_auto_save = auto_save
|
||||
_dry_run = dry_run
|
||||
|
||||
if project_path:
|
||||
sess = get_session()
|
||||
@@ -151,7 +155,9 @@ def cli(ctx, use_json, project_path, auto_save):
|
||||
|
||||
def _auto_save_callback():
|
||||
"""Auto-save callback that runs after each command."""
|
||||
global _auto_save, _session
|
||||
global _auto_save, _session, _dry_run
|
||||
if _dry_run:
|
||||
return
|
||||
if _auto_save and _session and _session.has_project() and _session._modified:
|
||||
# Don't auto-save if we're in REPL mode (user can explicitly save)
|
||||
if not _repl_mode:
|
||||
|
||||
@@ -125,8 +125,10 @@ def parse_time(value: str) -> float:
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--project", "project_path", type=str, default=None,
|
||||
help="Path to .kdenlive-cli.json project file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path):
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
"""Kdenlive CLI — Stateful video editing from the command line.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
@@ -144,6 +146,21 @@ def cli(ctx, use_json, project_path):
|
||||
ctx.invoke(repl, project_path=None)
|
||||
|
||||
|
||||
@cli.result_callback()
|
||||
def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs):
|
||||
"""Auto-save project after one-shot commands if state was modified."""
|
||||
if _repl_mode:
|
||||
return
|
||||
if dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
try:
|
||||
sess.save_session()
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Auto-save failed: {e}", err=True)
|
||||
|
||||
|
||||
# ── Project Commands ────────────────────────────────────────────
|
||||
@cli.group()
|
||||
def project():
|
||||
|
||||
@@ -113,12 +113,25 @@ def _save_current(ctx: click.Context) -> None:
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.option("--json", "use_json", is_flag=True, default=False, help="Output in JSON format.")
|
||||
@click.option("--project", "-p", type=click.Path(), default=None, help="Path to project JSON file.")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project):
|
||||
def cli(ctx, use_json, project, dry_run):
|
||||
"""cli-anything-krita: CLI harness for Krita digital painting."""
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["json"] = use_json
|
||||
ctx.obj["project"] = project
|
||||
|
||||
# Auto-save after one-shot commands when --project is used
|
||||
is_oneshot = ctx.invoked_subcommand is not None
|
||||
|
||||
@ctx.call_on_close
|
||||
def _auto_save():
|
||||
if dry_run or not is_oneshot:
|
||||
return
|
||||
if _current_project and _current_project_path:
|
||||
save_project(_current_project, _current_project_path)
|
||||
|
||||
if ctx.invoked_subcommand is None:
|
||||
ctx.invoke(repl, project_path=project)
|
||||
|
||||
|
||||
@@ -117,8 +117,10 @@ def handle_error(func):
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--project", "project_path", type=str, default=None,
|
||||
help="Path to .lo-cli.json project file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path):
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
"""LibreOffice CLI -- Stateful document editing from the command line.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
@@ -136,6 +138,21 @@ def cli(ctx, use_json, project_path):
|
||||
ctx.invoke(repl, project_path=None)
|
||||
|
||||
|
||||
@cli.result_callback()
|
||||
def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs):
|
||||
"""Auto-save project after one-shot commands if state was modified."""
|
||||
if _repl_mode:
|
||||
return
|
||||
if dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
try:
|
||||
sess.save_session()
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Auto-save failed: {e}", err=True)
|
||||
|
||||
|
||||
# ── Document Commands ────────────────────────────────────────────
|
||||
@cli.group()
|
||||
def document():
|
||||
|
||||
@@ -40,8 +40,10 @@ def emit(data, message: str | None = None) -> None:
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON")
|
||||
@click.option("--project", "project_path", default=None, help="Open a Mermaid project file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, json_mode: bool, project_path: str | None) -> None:
|
||||
def cli(ctx, json_mode: bool, project_path: str | None, dry_run: bool) -> None:
|
||||
"""CLI harness for Mermaid Live Editor state files and renderer URLs."""
|
||||
global _json_output, _session
|
||||
_json_output = json_mode
|
||||
@@ -51,6 +53,8 @@ def cli(ctx, json_mode: bool, project_path: str | None) -> None:
|
||||
|
||||
@ctx.call_on_close
|
||||
def _auto_save() -> None:
|
||||
if dry_run:
|
||||
return
|
||||
if project_path and _session and _session.is_open and _session.modified:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -98,8 +98,10 @@ def handle_error(func):
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--project", "project_path", type=str, default=None,
|
||||
help="Path to score file to open")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path):
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
"""MuseScore CLI — Music notation from the command line.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
@@ -117,6 +119,21 @@ def cli(ctx, use_json, project_path):
|
||||
ctx.invoke(repl)
|
||||
|
||||
|
||||
@cli.result_callback()
|
||||
def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs):
|
||||
"""Auto-save project after one-shot commands if state was modified."""
|
||||
if _repl_mode:
|
||||
return
|
||||
if dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
try:
|
||||
sess.save_session()
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Auto-save failed: {e}", err=True)
|
||||
|
||||
|
||||
# ── Project Commands ──────────────────────────────────────────────────
|
||||
|
||||
@cli.group()
|
||||
|
||||
@@ -118,8 +118,10 @@ def handle_error(func):
|
||||
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--project", "project_path", type=str, default=None,
|
||||
help="Path to OBS scene collection JSON file")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, use_json, project_path):
|
||||
def cli(ctx, use_json, project_path, dry_run):
|
||||
"""OBS Studio CLI -- Stateful scene collection editing from the command line.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
@@ -137,6 +139,21 @@ def cli(ctx, use_json, project_path):
|
||||
ctx.invoke(repl, project_path=None)
|
||||
|
||||
|
||||
@cli.result_callback()
|
||||
def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs):
|
||||
"""Auto-save project after one-shot commands if state was modified."""
|
||||
if _repl_mode:
|
||||
return
|
||||
if dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
try:
|
||||
sess.save_session()
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Auto-save failed: {e}", err=True)
|
||||
|
||||
|
||||
# -- Project Commands --------------------------------------------------------
|
||||
@cli.group()
|
||||
def project():
|
||||
|
||||
@@ -26,6 +26,7 @@ _session: Optional[Session] = None
|
||||
_json_output = False
|
||||
_repl_mode = False
|
||||
_auto_save = False
|
||||
_dry_run = False
|
||||
|
||||
|
||||
# ── Output helpers ────────────────────────────────────────────────────────
|
||||
@@ -115,8 +116,10 @@ def handle_error(func):
|
||||
@click.option("--session", "session_id", default=None, help="Session ID to resume")
|
||||
@click.option("--project", "project_path", default=None, help="Open project on start")
|
||||
@click.option("-s", "--save", "auto_save", is_flag=True, help="Auto-save after mutations")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, json_mode, session_id, project_path, auto_save):
|
||||
def cli(ctx, json_mode, session_id, project_path, auto_save, dry_run):
|
||||
"""Openscreen CLI — Screen recording editor.
|
||||
|
||||
Edit screen recordings via command line: zoom, speed, trim, crop,
|
||||
@@ -124,9 +127,10 @@ def cli(ctx, json_mode, session_id, project_path, auto_save):
|
||||
|
||||
Run without a subcommand to enter REPL mode.
|
||||
"""
|
||||
global _session, _json_output, _auto_save
|
||||
global _session, _json_output, _auto_save, _dry_run
|
||||
_json_output = json_mode
|
||||
_auto_save = auto_save
|
||||
_dry_run = dry_run
|
||||
_session = Session(session_id)
|
||||
|
||||
if project_path:
|
||||
@@ -212,7 +216,7 @@ def project_set(key, value):
|
||||
pass
|
||||
result = proj_mod.set_setting(_session, key, value)
|
||||
output(result)
|
||||
if _auto_save and _session.project_path:
|
||||
if _auto_save and not _dry_run and _session.project_path:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -246,7 +250,7 @@ def zoom_add(start, end, depth, focus_x, focus_y, focus_mode):
|
||||
_session, start, end, depth, focus_x, focus_y, focus_mode
|
||||
)
|
||||
output(result, "Zoom region added")
|
||||
if _auto_save and _session.project_path:
|
||||
if _auto_save and not _dry_run and _session.project_path:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -257,7 +261,7 @@ def zoom_remove(region_id):
|
||||
"""Remove a zoom region by ID."""
|
||||
result = tl_mod.remove_zoom_region(_session, region_id)
|
||||
output(result)
|
||||
if _auto_save and _session.project_path:
|
||||
if _auto_save and not _dry_run and _session.project_path:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -286,7 +290,7 @@ def speed_add(start, end, spd):
|
||||
"""Add a speed region."""
|
||||
result = tl_mod.add_speed_region(_session, start, end, spd)
|
||||
output(result, "Speed region added")
|
||||
if _auto_save and _session.project_path:
|
||||
if _auto_save and not _dry_run and _session.project_path:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -297,7 +301,7 @@ def speed_remove(region_id):
|
||||
"""Remove a speed region by ID."""
|
||||
result = tl_mod.remove_speed_region(_session, region_id)
|
||||
output(result)
|
||||
if _auto_save and _session.project_path:
|
||||
if _auto_save and not _dry_run and _session.project_path:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -325,7 +329,7 @@ def trim_add(start, end):
|
||||
"""Add a trim region (cuts this section out)."""
|
||||
result = tl_mod.add_trim_region(_session, start, end)
|
||||
output(result, "Trim region added")
|
||||
if _auto_save and _session.project_path:
|
||||
if _auto_save and not _dry_run and _session.project_path:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -364,7 +368,7 @@ def crop_set(x, y, w, h):
|
||||
"""Set crop region (normalized 0-1 coordinates)."""
|
||||
result = tl_mod.set_crop(_session, x, y, w, h)
|
||||
output(result, "Crop updated")
|
||||
if _auto_save and _session.project_path:
|
||||
if _auto_save and not _dry_run and _session.project_path:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
@@ -400,7 +404,7 @@ def annotation_add_text(start, end, text, x, y, font_size, color, bg_color):
|
||||
_session, start, end, text, x, y, font_size, color, bg_color
|
||||
)
|
||||
output(result, "Annotation added")
|
||||
if _auto_save and _session.project_path:
|
||||
if _auto_save and not _dry_run and _session.project_path:
|
||||
_session.save_project()
|
||||
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ def handle_error(func):
|
||||
|
||||
_repl_mode = False
|
||||
_auto_save = False
|
||||
_dry_run = False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -125,23 +126,26 @@ _auto_save = False
|
||||
@click.option("--json", "json_mode", is_flag=True, help="Output in JSON format")
|
||||
@click.option("--session", "session_id", default=None, help="Session ID to use/resume")
|
||||
@click.option("--project", "project_path", default=None, help="Open a project file")
|
||||
@click.option("-s", "--save", "auto_save", is_flag=True,
|
||||
@click.option("-s", "--save", "auto_save", is_flag=True,
|
||||
help="Auto-save project after each mutation command (one-shot mode)")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
|
||||
help="Run command without saving changes to disk")
|
||||
@click.pass_context
|
||||
def cli(ctx, json_mode, session_id, project_path, auto_save):
|
||||
def cli(ctx, json_mode, session_id, project_path, auto_save, dry_run):
|
||||
"""Shotcut CLI — Video editing from the command line.
|
||||
|
||||
A stateful CLI for manipulating Shotcut/MLT video projects.
|
||||
Designed for AI agents and power users.
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
|
||||
|
||||
Use -s/--save to automatically save changes after each mutation command.
|
||||
This is useful in one-shot mode where each command runs in a new process.
|
||||
"""
|
||||
global _json_output, _session, _auto_save
|
||||
global _json_output, _session, _auto_save, _dry_run
|
||||
_json_output = json_mode
|
||||
_auto_save = auto_save
|
||||
_dry_run = dry_run
|
||||
|
||||
if session_id:
|
||||
_session = Session(session_id)
|
||||
@@ -160,7 +164,9 @@ def cli(ctx, json_mode, session_id, project_path, auto_save):
|
||||
|
||||
def _auto_save_callback():
|
||||
"""Auto-save callback that runs after each command."""
|
||||
global _auto_save, _session
|
||||
global _auto_save, _session, _dry_run
|
||||
if _dry_run:
|
||||
return
|
||||
if _auto_save and _session and _session.is_open and _session.is_modified:
|
||||
# Don't auto-save if we're in REPL mode (user can explicitly save)
|
||||
if not _repl_mode:
|
||||
|
||||
Reference in New Issue
Block a user