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.
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/PKG-INFO +4 -1
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/README.md +3 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/PKG-INFO +4 -1
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/SOURCES.txt +6 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/blueprint_cmds.py +105 -248
- mirrorneuron_cli-1.1.3/mn_cli/libs/blueprint_observability.py +158 -0
- mirrorneuron_cli-1.1.3/mn_cli/libs/blueprint_repository.py +159 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/run_cmds.py +21 -259
- mirrorneuron_cli-1.1.3/mn_cli/libs/run_logs.py +202 -0
- mirrorneuron_cli-1.1.3/mn_cli/libs/run_manifest.py +72 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/server_cmds.py +13 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/update_cmds.py +17 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_blueprint_cmds.py +132 -0
- mirrorneuron_cli-1.1.3/tests/test_blueprint_repository.py +46 -0
- mirrorneuron_cli-1.1.3/tests/test_run_helpers.py +79 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/.github/workflows/ci.yml +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/.github/workflows/release.yml +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/.gitignore +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/LICENSE +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/RELEASE.md +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/dependency_links.txt +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/entry_points.txt +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/requires.txt +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mirrorneuron_cli.egg-info/top_level.txt +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/__init__.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/config.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/error_handler.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/__init__.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/job_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/sys_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/libs/ui.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/logging_config.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/main.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/mn_cli/shared.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/pyproject.toml +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/scripts/check-release-artifacts.sh +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/scripts/make-release-zip.sh +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/scripts/validate-version-tag.sh +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/setup.cfg +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/conftest.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_job_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_run_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_server_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.3}/tests/test_sys_cmds.py +0 -0
- {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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
45
|
+
_PATCH_COMPAT = (subprocess, _git_checkout, _git_fetch)
|
|
17
46
|
|
|
18
47
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
379
|
-
if
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
425
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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")
|