diff --git a/packages/cli/src/mdl_cli/main.py b/packages/cli/src/mdl_cli/main.py index fe5e6dc..b6c48b1 100644 --- a/packages/cli/src/mdl_cli/main.py +++ b/packages/cli/src/mdl_cli/main.py @@ -510,7 +510,7 @@ def unmanage( days = int(expires.rstrip("d") or "14") try: apply_command(model_dir, "set_unmanaged", {"id": le.id, "unmanaged": True}) - except CommandError as e: + except (CommandError, FileNotFoundError) as e: typer.secho(str(e), fg=typer.colors.RED, err=True) raise typer.Exit(1) from e entry = add_debt(model_dir, entity, reason, days) @@ -679,7 +679,7 @@ def ontology_promote( raise typer.Exit(1) try: apply_command(model_dir, "promote_alignment", {"id": obj.id}) - except CommandError as e: + except (CommandError, FileNotFoundError) as e: typer.secho(str(e), fg=typer.colors.RED, err=True) raise typer.Exit(1) from e typer.secho(f"alignment on {name!r} promoted to accepted", fg=typer.colors.GREEN) @@ -1227,7 +1227,7 @@ def delete_entity( try: apply_command(model_dir, "delete_entity", {"id": le.id, "cascade": cascade}) - except CommandError as e: + except (CommandError, FileNotFoundError) as e: typer.secho(str(e), fg=typer.colors.RED, err=True) raise typer.Exit(1) from e typer.secho(f"deleted entity {name!r}", fg=typer.colors.GREEN) @@ -1250,7 +1250,9 @@ def new_entity( "create_entity", {"name": name, "subject_area": subject_area, "definition": definition, "layer": layer}, ) - except CommandError as e: + except (CommandError, FileNotFoundError) as e: + # FileNotFoundError carries the "no mdl-project.yaml … run from my-model/" + # guidance (issue #7); show it plainly, not as a traceback. typer.secho(str(e), fg=typer.colors.RED, err=True) raise typer.Exit(1) from e typer.secho(f"created entity {name!r} ({result.created_id})", fg=typer.colors.GREEN) @@ -1262,11 +1264,15 @@ def new_subject_area( model_dir: Path = typer.Option(Path("."), "--model-dir", "-m"), definition: str = typer.Option(None, "--definition"), ) -> None: - from mdl_core.commands import apply_command + from mdl_core.commands import CommandError, apply_command - result = apply_command( - model_dir, "create_subject_area", {"name": name, "definition": definition} - ) + try: + result = apply_command( + model_dir, "create_subject_area", {"name": name, "definition": definition} + ) + except (CommandError, FileNotFoundError) as e: + typer.secho(str(e), fg=typer.colors.RED, err=True) + raise typer.Exit(1) from e typer.secho(f"created subject area {name!r} ({result.created_id})", fg=typer.colors.GREEN) @@ -1296,7 +1302,7 @@ def new_term( "alignment": alignment, }, ) - except CommandError as e: + except (CommandError, FileNotFoundError) as e: typer.secho(str(e), fg=typer.colors.RED, err=True) raise typer.Exit(1) from e typer.secho(f"created term {name!r} ({result.created_id})", fg=typer.colors.GREEN) @@ -1980,7 +1986,7 @@ def gov_import( apply_command(model_dir, op, payload) applied += 1 typer.echo(f" {op} {payload.get('id', '')}") - except CommandError as e: + except (CommandError, FileNotFoundError) as e: typer.secho(f"skip {op}: {e}", fg=typer.colors.YELLOW) note = "" if sot == "collibra" else " (note: source_of_truth is git; this is a one-off import)" diff --git a/packages/core/src/mdl_core/commands.py b/packages/core/src/mdl_core/commands.py index 92661fe..d3008b0 100644 --- a/packages/core/src/mdl_core/commands.py +++ b/packages/core/src/mdl_core/commands.py @@ -22,7 +22,7 @@ from mdl_core.diagnostics import Severity from mdl_core.ids import is_ulid, new_ulid -from mdl_core.repo import PROJECT_FILE, ModelRepo +from mdl_core.repo import PROJECT_FILE, ModelRepo, find_project_root from mdl_core.validate import validate # --------------------------------------------------------------------------- @@ -63,7 +63,11 @@ class CommandResult: def apply_command( model_dir: Path, op: str, payload: dict, base_fingerprint: str | None = None ) -> CommandResult: - model_dir = Path(model_dir) + # Resolve to the model dir first, so a command run from a nested dir or the + # project root (not only from the exact folder holding mdl-project.yaml) acts on + # the right tree — and so the fingerprint below matches ModelRepo.load's own + # walk-up (issue #7). + model_dir = find_project_root(Path(model_dir)) if base_fingerprint and dir_fingerprint(model_dir) != base_fingerprint: raise StaleModelError("model changed on disk — refresh before editing") diff --git a/packages/core/src/mdl_core/repo.py b/packages/core/src/mdl_core/repo.py index be5b3b5..ac66181 100644 --- a/packages/core/src/mdl_core/repo.py +++ b/packages/core/src/mdl_core/repo.py @@ -38,6 +38,45 @@ PROJECT_FILE = "mdl-project.yaml" +def find_project_root(start: Path) -> Path: + """The model directory for `start`: `start` itself if it holds an + `mdl-project.yaml`, else the nearest ancestor that does, else `start` unchanged. + + This is what lets every command run from ANYWHERE inside a model — a nested + working dir, or the project root one level above — instead of forcing the user to + `cd` into the exact folder holding the project file (issue #7). Returning `start` + unchanged when nothing is found keeps the caller's "no mdl-project.yaml in