mirrorneuron-cli 1.1.2__tar.gz → 1.1.3__tar.gz

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.
Files changed (45) hide show
  1. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/PKG-INFO +4 -1
  2. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/README.md +3 -0
  3. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/PKG-INFO +4 -1
  4. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/SOURCES.txt +6 -0
  5. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/blueprint_cmds.py +105 -248
  6. mirrorneuron_cli-1.1.3/mn_cli/libs/blueprint_observability.py +158 -0
  7. mirrorneuron_cli-1.1.3/mn_cli/libs/blueprint_repository.py +159 -0
  8. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/run_cmds.py +21 -259
  9. mirrorneuron_cli-1.1.3/mn_cli/libs/run_logs.py +202 -0
  10. mirrorneuron_cli-1.1.3/mn_cli/libs/run_manifest.py +72 -0
  11. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/server_cmds.py +13 -0
  12. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/update_cmds.py +17 -0
  13. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_blueprint_cmds.py +132 -0
  14. mirrorneuron_cli-1.1.3/tests/test_blueprint_repository.py +46 -0
  15. mirrorneuron_cli-1.1.3/tests/test_run_helpers.py +79 -0
  16. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/.github/workflows/ci.yml +0 -0
  17. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/.github/workflows/release.yml +0 -0
  18. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/.gitignore +0 -0
  19. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/LICENSE +0 -0
  20. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/RELEASE.md +0 -0
  21. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/dependency_links.txt +0 -0
  22. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/entry_points.txt +0 -0
  23. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/requires.txt +0 -0
  24. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/top_level.txt +0 -0
  25. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/__init__.py +0 -0
  26. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/config.py +0 -0
  27. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/error_handler.py +0 -0
  28. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/__init__.py +0 -0
  29. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/job_cmds.py +0 -0
  30. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/sys_cmds.py +0 -0
  31. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/ui.py +0 -0
  32. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/logging_config.py +0 -0
  33. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/main.py +0 -0
  34. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/shared.py +0 -0
  35. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/pyproject.toml +0 -0
  36. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/scripts/check-release-artifacts.sh +0 -0
  37. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/scripts/make-release-zip.sh +0 -0
  38. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/scripts/validate-version-tag.sh +0 -0
  39. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/setup.cfg +0 -0
  40. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/conftest.py +0 -0
  41. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_job_cmds.py +0 -0
  42. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_run_cmds.py +0 -0
  43. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_server_cmds.py +0 -0
  44. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_sys_cmds.py +0 -0
  45. {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_update_cmds.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirrorneuron-cli
3
- Version: 1.1.2
3
+ Version: 1.1.3
4
4
  Summary: MirrorNeuron CLI
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3
@@ -122,6 +122,7 @@ mn blueprint list
122
122
  mn blueprint install
123
123
  mn blueprint update
124
124
  mn blueprint run <blueprint_id>
125
+ mn blueprint --blueprint-repo https://github.com/MirrorNeuronLab/customer-blueprints run <blueprint_id>
125
126
  mn blueprint run ./path/to/bundle_or_source_blueprint
126
127
  mn blueprint run <blueprint_id> --offline
127
128
  mn blueprint run <blueprint_id> --revision <git_sha_or_tag>
@@ -140,6 +141,8 @@ mn blueprint export <run_id> --format html
140
141
 
141
142
  Catalog runs use the cached blueprint library by default. Run `mn blueprint update` or pass `--update` when you want to refresh the local cache.
142
143
 
144
+ Use `mn blueprint --blueprint-repo <repo-url> ...` to read catalog commands from a different blueprint repository, including a private repository your Git credentials can access. Custom repositories are cached separately under `~/.mn/blueprint_repos/`, and the repository root must contain a valid `index.json` JSON list of blueprint entries.
145
+
143
146
  Blueprint run artifacts are stored under:
144
147
 
145
148
  ```text
@@ -108,6 +108,7 @@ mn blueprint list
108
108
  mn blueprint install
109
109
  mn blueprint update
110
110
  mn blueprint run <blueprint_id>
111
+ mn blueprint --blueprint-repo https://github.com/MirrorNeuronLab/customer-blueprints run <blueprint_id>
111
112
  mn blueprint run ./path/to/bundle_or_source_blueprint
112
113
  mn blueprint run <blueprint_id> --offline
113
114
  mn blueprint run <blueprint_id> --revision <git_sha_or_tag>
@@ -126,6 +127,8 @@ mn blueprint export <run_id> --format html
126
127
 
127
128
  Catalog runs use the cached blueprint library by default. Run `mn blueprint update` or pass `--update` when you want to refresh the local cache.
128
129
 
130
+ Use `mn blueprint --blueprint-repo <repo-url> ...` to read catalog commands from a different blueprint repository, including a private repository your Git credentials can access. Custom repositories are cached separately under `~/.mn/blueprint_repos/`, and the repository root must contain a valid `index.json` JSON list of blueprint entries.
131
+
129
132
  Blueprint run artifacts are stored under:
130
133
 
131
134
  ```text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirrorneuron-cli
3
- Version: 1.1.2
3
+ Version: 1.1.3
4
4
  Summary: MirrorNeuron CLI
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3
@@ -122,6 +122,7 @@ mn blueprint list
122
122
  mn blueprint install
123
123
  mn blueprint update
124
124
  mn blueprint run <blueprint_id>
125
+ mn blueprint --blueprint-repo https://github.com/MirrorNeuronLab/customer-blueprints run <blueprint_id>
125
126
  mn blueprint run ./path/to/bundle_or_source_blueprint
126
127
  mn blueprint run <blueprint_id> --offline
127
128
  mn blueprint run <blueprint_id> --revision <git_sha_or_tag>
@@ -140,6 +141,8 @@ mn blueprint export <run_id> --format html
140
141
 
141
142
  Catalog runs use the cached blueprint library by default. Run `mn blueprint update` or pass `--update` when you want to refresh the local cache.
142
143
 
144
+ Use `mn blueprint --blueprint-repo <repo-url> ...` to read catalog commands from a different blueprint repository, including a private repository your Git credentials can access. Custom repositories are cached separately under `~/.mn/blueprint_repos/`, and the repository root must contain a valid `index.json` JSON list of blueprint entries.
145
+
143
146
  Blueprint run artifacts are stored under:
144
147
 
145
148
  ```text
@@ -21,8 +21,12 @@ mn_cli/shared.py
21
21
  mn_cli/update_cmds.py
22
22
  mn_cli/libs/__init__.py
23
23
  mn_cli/libs/blueprint_cmds.py
24
+ mn_cli/libs/blueprint_observability.py
25
+ mn_cli/libs/blueprint_repository.py
24
26
  mn_cli/libs/job_cmds.py
25
27
  mn_cli/libs/run_cmds.py
28
+ mn_cli/libs/run_logs.py
29
+ mn_cli/libs/run_manifest.py
26
30
  mn_cli/libs/sys_cmds.py
27
31
  mn_cli/libs/ui.py
28
32
  scripts/check-release-artifacts.sh
@@ -30,8 +34,10 @@ scripts/make-release-zip.sh
30
34
  scripts/validate-version-tag.sh
31
35
  tests/conftest.py
32
36
  tests/test_blueprint_cmds.py
37
+ tests/test_blueprint_repository.py
33
38
  tests/test_job_cmds.py
34
39
  tests/test_run_cmds.py
40
+ tests/test_run_helpers.py
35
41
  tests/test_server_cmds.py
36
42
  tests/test_sys_cmds.py
37
43
  tests/test_update_cmds.py
@@ -2,138 +2,60 @@ import os
2
2
  import json
3
3
  import shutil
4
4
  import subprocess
5
- import sys
6
5
  import time
7
6
  from pathlib import Path
8
- from typing import Any, Callable, Optional
7
+ from typing import Any, Optional
9
8
 
10
9
  import typer
11
10
  from rich.table import Table
11
+ from mn_cli.libs.blueprint_observability import (
12
+ artifact_headline as _artifact_headline,
13
+ display as _display,
14
+ final_artifact as _final_artifact,
15
+ job_id_from_record as _job_id,
16
+ load_observability_api as _load_observability_api,
17
+ load_run_or_exit as _load_run_or_exit,
18
+ load_web_ui_api as _load_web_ui_api,
19
+ make_blueprint_run_id as _make_blueprint_run_id,
20
+ print_events as _print_events,
21
+ render_markdown_export as _render_markdown_export,
22
+ run_summary as _run_summary,
23
+ web_ui_url as _web_ui_url,
24
+ )
25
+ from mn_cli.libs.blueprint_repository import (
26
+ BLUEPRINT_REPO_CONTEXT_KEY,
27
+ DEFAULT_BLUEPRINT_REPO,
28
+ BlueprintIndexError,
29
+ blueprint_cache_dir_for_repo as _blueprint_cache_dir_for_repo,
30
+ blueprint_storage_dir_for_source as _blueprint_storage_dir_for_source,
31
+ clone_blueprint_repo as _clone_blueprint_repo,
32
+ context_blueprint_repo as _context_blueprint_repo,
33
+ default_blueprint_storage_dir as _default_blueprint_storage_dir,
34
+ ensure_blueprint_source as _ensure_blueprint_source,
35
+ git_checkout as _git_checkout,
36
+ git_fetch as _git_fetch,
37
+ git_pull as _git_pull,
38
+ git_revision as _git_revision,
39
+ load_blueprint_index as _load_blueprint_index,
40
+ )
12
41
  from mn_cli.shared import console, logger
13
42
  from mn_cli.libs.run_cmds import run_bundle as _run_bundle
14
43
 
15
44
  blueprint_app = typer.Typer(help="Manage and run MirrorNeuron blueprints")
16
- DEFAULT_BLUEPRINT_REPO = "https://github.com/MirrorNeuronLab/mn-blueprints"
45
+ _PATCH_COMPAT = (subprocess, _git_checkout, _git_fetch)
17
46
 
18
47
 
19
- def _load_observability_api() -> tuple[Callable[..., list[dict[str, Any]]], Callable[..., dict[str, Any]], Callable[..., list[dict[str, Any]]]]:
20
- try:
21
- from mn_blueprint_support.observability import list_runs, load_run, read_run_events
22
- except ModuleNotFoundError:
23
- repo_root = Path(__file__).resolve().parents[3]
24
- support_src = repo_root / "mn-skills" / "blueprint_support_skill" / "src"
25
- if support_src.exists() and str(support_src) not in sys.path:
26
- sys.path.insert(0, str(support_src))
27
- try:
28
- from mn_blueprint_support.observability import list_runs, load_run, read_run_events
29
- except ModuleNotFoundError:
30
- console.print(
31
- "[red]Blueprint observability support is unavailable. "
32
- "Install the blueprint support package or run from the monorepo checkout.[/red]"
33
- )
34
- raise typer.Exit(1)
35
- return list_runs, load_run, read_run_events
36
-
37
-
38
- def _load_web_ui_api() -> Callable[..., Any]:
39
- _load_observability_api()
40
- try:
41
- from mn_blueprint_support.web_ui import write_static_run_report
42
- except ModuleNotFoundError:
43
- console.print("[red]Blueprint web UI support is unavailable.[/red]")
44
- raise typer.Exit(1)
45
- return write_static_run_report
46
-
47
-
48
- def _make_blueprint_run_id(blueprint_id: str) -> str:
49
- try:
50
- _load_observability_api()
51
- from mn_blueprint_support import make_run_id
52
-
53
- return make_run_id(blueprint_id)
54
- except Exception:
55
- import uuid
56
-
57
- return f"{blueprint_id}-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}-{uuid.uuid4().hex[:10]}"
58
-
59
-
60
- def _ensure_blueprint_source(
61
- *,
62
- source: Optional[str],
63
- update: bool,
64
- offline: bool,
65
- revision: Optional[str],
66
- ) -> str:
67
- if source:
68
- source_path = Path(source).expanduser()
69
- if source_path.exists():
70
- storage_dir = source_path
71
- else:
72
- storage_dir = Path(os.path.expanduser("~/.mn/blueprints"))
73
- if offline:
74
- console.print(f"[red]Offline mode cannot clone missing source {source!r}.[/red]")
75
- raise typer.Exit(1)
76
- if not storage_dir.exists():
77
- _clone_blueprint_repo(source, storage_dir)
78
- elif update:
79
- _git_pull(storage_dir)
80
- else:
81
- storage_dir = Path(os.path.expanduser("~/.mn/blueprints"))
82
- if not storage_dir.exists():
83
- if offline:
84
- console.print(f"[red]Blueprint storage not found at {storage_dir}; offline mode cannot clone it.[/red]")
85
- raise typer.Exit(1)
86
- console.print(f"Initializing blueprint storage at {storage_dir}...")
87
- _clone_blueprint_repo(DEFAULT_BLUEPRINT_REPO, storage_dir)
88
- elif update:
89
- _git_pull(storage_dir)
90
- else:
91
- console.print(f"Using cached blueprint storage at {storage_dir}. Run 'mn blueprint update' or pass --update to refresh.")
92
-
93
- if revision:
94
- if offline:
95
- _git_checkout(storage_dir, revision)
96
- else:
97
- _git_fetch(storage_dir)
98
- _git_checkout(storage_dir, revision)
99
- return str(storage_dir)
100
-
101
-
102
- def _clone_blueprint_repo(source: str, storage_dir: Path) -> None:
103
- storage_dir.parent.mkdir(parents=True, exist_ok=True)
104
- res = subprocess.run(["git", "clone", source, str(storage_dir)], capture_output=True, text=True)
105
- if res.returncode != 0:
106
- logger.error("Failed to clone blueprint repository: %s", res.stderr)
107
- console.print(f"[red]Failed to clone blueprint repository: {res.stderr}[/red]")
108
- raise typer.Exit(1)
109
-
110
-
111
- def _git_pull(storage_dir: Path) -> None:
112
- console.print(f"Updating blueprint storage at {storage_dir}...")
113
- res = subprocess.run(["git", "-C", str(storage_dir), "pull", "--ff-only"], capture_output=True, text=True)
114
- if res.returncode != 0:
115
- logger.warning("Failed to update blueprint repository: %s", res.stderr)
116
- console.print(f"[yellow]Warning: Failed to update blueprint repository: {res.stderr}[/yellow]")
117
-
118
-
119
- def _git_fetch(storage_dir: Path) -> None:
120
- subprocess.run(["git", "-C", str(storage_dir), "fetch", "--all", "--tags"], capture_output=True, text=True)
121
-
122
-
123
- def _git_checkout(storage_dir: Path, revision: str) -> None:
124
- res = subprocess.run(["git", "-C", str(storage_dir), "checkout", revision], capture_output=True, text=True)
125
- if res.returncode != 0:
126
- console.print(f"[red]Failed to checkout blueprint revision {revision}: {res.stderr}[/red]")
127
- raise typer.Exit(1)
128
-
129
-
130
- def _git_revision(storage_dir: Path) -> Optional[str]:
131
- res = subprocess.run(["git", "-C", str(storage_dir), "rev-parse", "HEAD"], capture_output=True, text=True)
132
- if res.returncode != 0:
133
- return None
134
- stdout = getattr(res, "stdout", "") or ""
135
- return str(stdout).strip() or None
136
-
48
+ @blueprint_app.callback()
49
+ def blueprint_callback(
50
+ ctx: typer.Context,
51
+ blueprint_repo: Optional[str] = typer.Option(
52
+ None,
53
+ "--blueprint-repo",
54
+ help="Use this blueprint repository URL/path instead of the default catalog.",
55
+ ),
56
+ ) -> None:
57
+ ctx.obj = dict(ctx.obj or {})
58
+ ctx.obj[BLUEPRINT_REPO_CONTEXT_KEY] = blueprint_repo
137
59
 
138
60
  def _is_python_source_blueprint(manifest: dict[str, Any]) -> bool:
139
61
  metadata = manifest.get("metadata") or {}
@@ -249,61 +171,6 @@ def _run_local_blueprint_target(
249
171
  return True
250
172
 
251
173
 
252
- def _display(value: Any, *, max_length: int = 140) -> str:
253
- if value is None:
254
- return ""
255
- if isinstance(value, (dict, list)):
256
- text = json.dumps(value, sort_keys=True)
257
- else:
258
- text = str(value)
259
- return text if len(text) <= max_length else text[: max_length - 1] + "…"
260
-
261
-
262
- def _run_summary(run: dict[str, Any]) -> dict[str, Any]:
263
- return {
264
- "Run ID": run.get("run_id"),
265
- "Blueprint": run.get("blueprint_id"),
266
- "Status": run.get("status"),
267
- "Started": run.get("started_at"),
268
- "Ended": run.get("ended_at"),
269
- "Run Directory": run.get("run_dir"),
270
- }
271
-
272
-
273
- def _run_summary_with_job(record: dict[str, Any]) -> dict[str, Any]:
274
- summary = _run_summary(record.get("run") or record)
275
- job_id = _job_id(record)
276
- if job_id:
277
- summary["Job ID"] = job_id
278
- return summary
279
-
280
-
281
- def _final_artifact(record: dict[str, Any]) -> dict[str, Any]:
282
- final_artifact = record.get("final_artifact") or {}
283
- if final_artifact:
284
- return final_artifact
285
- result = record.get("result") or {}
286
- nested = result.get("final_artifact") if isinstance(result, dict) else None
287
- return nested if isinstance(nested, dict) else {}
288
-
289
-
290
- def _artifact_headline(artifact: dict[str, Any]) -> str:
291
- for key in ("recommended_action", "recommendation", "decision", "risk_level", "priority", "summary"):
292
- if key in artifact:
293
- return _display(artifact[key])
294
- return _display(artifact)
295
-
296
-
297
- def _web_ui_url(record: dict[str, Any]) -> str:
298
- web_ui = record.get("web_ui") or {}
299
- return str(web_ui.get("url") or "")
300
-
301
-
302
- def _job_id(record: dict[str, Any]) -> str:
303
- job = record.get("job") or {}
304
- return str(job.get("job_id") or "")
305
-
306
-
307
174
  def _print_run_table(runs: list[dict[str, Any]]) -> None:
308
175
  if not runs:
309
176
  console.print("[yellow]No blueprint runs found.[/yellow]")
@@ -322,66 +189,28 @@ def _print_run_table(runs: list[dict[str, Any]]) -> None:
322
189
  )
323
190
 
324
191
 
325
- def _load_run_or_exit(run_id: str, runs_root: Optional[str]) -> dict[str, Any]:
326
- _, load_run, _ = _load_observability_api()
327
- try:
328
- return load_run(run_id, runs_root=runs_root)
329
- except FileNotFoundError as exc:
330
- console.print(f"[red]{exc}[/red]")
331
- raise typer.Exit(1)
332
-
333
-
334
- def _print_events(events: list[dict[str, Any]]) -> None:
335
- for event in events:
336
- timestamp = event.get("timestamp") or event.get("time") or event.get("ts") or ""
337
- event_type = event.get("type") or event.get("event") or event.get("name") or "event"
338
- details = {
339
- key: value
340
- for key, value in event.items()
341
- if key not in {"timestamp", "time", "ts", "type", "event", "name"}
342
- }
343
- detail_text = json.dumps(details, sort_keys=True) if details else ""
344
- console.print(f"{_display(timestamp, max_length=36)} {_display(event_type, max_length=48)} {detail_text}", markup=False)
345
-
346
-
347
- def _markdown_table(rows: list[tuple[str, Any]]) -> list[str]:
348
- output = ["| Field | Value |", "|---|---|"]
349
- for key, value in rows:
350
- escaped_value = _display(value).replace("|", "\\|")
351
- output.append(f"| {key} | {escaped_value} |")
352
- return output
353
-
354
-
355
- def _render_markdown_export(record: dict[str, Any]) -> str:
356
- run = record.get("run") or {}
357
- artifact = _final_artifact(record)
358
- lines = [f"# Blueprint Run {run.get('run_id', 'unknown')}", ""]
359
- lines.extend(["## Summary", ""])
360
- lines.extend(_markdown_table(list(_run_summary_with_job(record).items())))
361
- lines.extend(["", "## Final Artifact", "", "```json", json.dumps(artifact, indent=2, sort_keys=True), "```"])
362
- web_ui = record.get("web_ui") or {}
363
- if web_ui:
364
- lines.extend(["", "## Web UI", ""])
365
- lines.extend(_markdown_table([("URL", web_ui.get("url")), ("Adapter", web_ui.get("adapter")), ("Status", web_ui.get("status"))]))
366
- lines.extend(["", "## Result", "", "```json", json.dumps(record.get("result") or {}, indent=2, sort_keys=True), "```"])
367
- lines.extend(["", "## Inputs", "", "```json", json.dumps(record.get("inputs") or {}, indent=2, sort_keys=True), "```"])
368
- lines.extend(["", "## Config", "", "```json", json.dumps(record.get("config") or {}, indent=2, sort_keys=True), "```"])
369
- lines.extend(["", "## Event Tail", "", "```json"])
370
- for event in (record.get("events") or [])[-20:]:
371
- lines.append(json.dumps(event, sort_keys=True))
372
- lines.extend(["```", ""])
373
- return "\n".join(lines)
374
-
375
192
  @blueprint_app.command("list")
376
- def blueprint_list():
193
+ def blueprint_list(ctx: typer.Context):
377
194
  """List all available blueprints from the local storage shared with mn staff"""
378
- index_path = os.path.expanduser("~/.mn/blueprints/index.json")
379
- if not os.path.exists(index_path):
380
- console.print("[yellow]Blueprint storage not initialized. Run 'mn blueprint run <name>' to initialize.[/yellow]")
381
- return
195
+ blueprint_repo = _context_blueprint_repo(ctx)
196
+ if blueprint_repo:
197
+ storage_dir = Path(
198
+ _ensure_blueprint_source(
199
+ source=None,
200
+ blueprint_repo=blueprint_repo,
201
+ update=False,
202
+ offline=False,
203
+ revision=None,
204
+ )
205
+ )
206
+ index_path = storage_dir / "index.json"
207
+ else:
208
+ index_path = Path(os.path.expanduser("~/.mn/blueprints/index.json"))
209
+ if not index_path.exists():
210
+ console.print("[yellow]Blueprint storage not initialized. Run 'mn blueprint run <name>' to initialize.[/yellow]")
211
+ return
382
212
  try:
383
- with open(index_path, "r") as f:
384
- blueprints = json.load(f)
213
+ blueprints = _load_blueprint_index(index_path)
385
214
  table = Table("ID", "Name", "Job Name", "Description")
386
215
  for bp in blueprints:
387
216
  table.add_row(
@@ -391,12 +220,15 @@ def blueprint_list():
391
220
  bp.get("description", "")
392
221
  )
393
222
  console.print(table)
394
- except Exception as e:
223
+ except BlueprintIndexError as e:
395
224
  logger.exception("Error reading blueprint index")
396
225
  console.print(f"[red]Error reading blueprints index: {e}[/red]")
226
+ if blueprint_repo:
227
+ raise typer.Exit(1)
397
228
 
398
229
  @blueprint_app.command("run")
399
230
  def blueprint_run(
231
+ ctx: typer.Context,
400
232
  blueprint_path_name: str,
401
233
  run_id: Optional[str] = typer.Option(None, "--run-id", help="Use a specific shared blueprint run ID."),
402
234
  source: Optional[str] = typer.Option(None, "--source", help="Use a local blueprint repo/path or clone URL instead of ~/.mn/blueprints."),
@@ -413,19 +245,20 @@ def blueprint_run(
413
245
  ):
414
246
  return
415
247
 
416
- storage_dir = _ensure_blueprint_source(source=source, update=update, offline=offline, revision=revision)
248
+ storage_dir = _ensure_blueprint_source(
249
+ source=source,
250
+ blueprint_repo=_context_blueprint_repo(ctx),
251
+ update=update,
252
+ offline=offline,
253
+ revision=revision,
254
+ )
417
255
 
418
- index_path = os.path.join(storage_dir, "index.json")
419
- if not os.path.exists(index_path):
420
- console.print("[red]Error: index.json not found in blueprint storage.[/red]")
421
- raise typer.Exit(1)
422
-
256
+ index_path = Path(storage_dir) / "index.json"
423
257
  try:
424
- with open(index_path, "r") as f:
425
- blueprints = json.load(f)
426
- except Exception as e:
258
+ blueprints = _load_blueprint_index(index_path, require_paths=True)
259
+ except BlueprintIndexError as e:
427
260
  logger.exception("Error parsing blueprint index")
428
- console.print(f"[red]Error parsing index.json: {e}[/red]")
261
+ console.print(f"[red]Error: {e}[/red]")
429
262
  raise typer.Exit(1)
430
263
 
431
264
  target_bp = None
@@ -457,11 +290,18 @@ def blueprint_run(
457
290
 
458
291
  @blueprint_app.command("install")
459
292
  def blueprint_install(
460
- source: str = typer.Option(DEFAULT_BLUEPRINT_REPO, "--source", help="Blueprint repository URL or local path."),
293
+ ctx: typer.Context,
294
+ source: Optional[str] = typer.Option(None, "--source", help="Blueprint repository URL or local path."),
461
295
  force: bool = typer.Option(False, "--force", help="Replace the existing cached repository."),
462
296
  ):
463
297
  """Install the blueprint library into ~/.mn/blueprints."""
464
- storage_dir = Path(os.path.expanduser("~/.mn/blueprints"))
298
+ blueprint_repo = _context_blueprint_repo(ctx)
299
+ repo_source = source or blueprint_repo or DEFAULT_BLUEPRINT_REPO
300
+ storage_dir = (
301
+ _blueprint_cache_dir_for_repo(repo_source)
302
+ if source is None and blueprint_repo
303
+ else _default_blueprint_storage_dir()
304
+ )
465
305
  if storage_dir.exists() and not force:
466
306
  console.print(f"[yellow]Blueprint storage already exists at {storage_dir}. Use --force to replace it.[/yellow]")
467
307
  return
@@ -469,20 +309,37 @@ def blueprint_install(
469
309
  import shutil
470
310
 
471
311
  shutil.rmtree(storage_dir)
472
- _clone_blueprint_repo(source, storage_dir)
312
+ _clone_blueprint_repo(repo_source, storage_dir)
313
+ try:
314
+ _load_blueprint_index(storage_dir / "index.json")
315
+ except BlueprintIndexError as e:
316
+ console.print(f"[red]Error: {e}[/red]")
317
+ raise typer.Exit(1)
473
318
  console.print(f"[green]Installed blueprints at {storage_dir}.[/green]")
474
319
 
475
320
 
476
321
  @blueprint_app.command("update")
477
322
  def blueprint_update(
323
+ ctx: typer.Context,
478
324
  source: Optional[str] = typer.Option(None, "--source", help="Cached blueprint repo/path to update."),
479
325
  ):
480
326
  """Update the cached blueprint library explicitly."""
481
- storage_dir = Path(source).expanduser() if source else Path(os.path.expanduser("~/.mn/blueprints"))
327
+ blueprint_repo = _context_blueprint_repo(ctx)
328
+ if source:
329
+ storage_dir = Path(source).expanduser()
330
+ elif blueprint_repo:
331
+ storage_dir = _blueprint_storage_dir_for_source(blueprint_repo)
332
+ else:
333
+ storage_dir = _default_blueprint_storage_dir()
482
334
  if not storage_dir.exists():
483
335
  console.print(f"[red]Blueprint storage not found at {storage_dir}. Run 'mn blueprint install' first.[/red]")
484
336
  raise typer.Exit(1)
485
337
  _git_pull(storage_dir)
338
+ try:
339
+ _load_blueprint_index(storage_dir / "index.json")
340
+ except BlueprintIndexError as e:
341
+ console.print(f"[red]Error: {e}[/red]")
342
+ raise typer.Exit(1)
486
343
 
487
344
 
488
345
  @blueprint_app.command("monitor")