mirrorneuron-cli 1.1.2__tar.gz → 1.1.4__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.4}/PKG-INFO +4 -1
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/README.md +3 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mirrorneuron_cli.egg-info/PKG-INFO +4 -1
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mirrorneuron_cli.egg-info/SOURCES.txt +6 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/libs/blueprint_cmds.py +212 -241
- mirrorneuron_cli-1.1.4/mn_cli/libs/blueprint_observability.py +158 -0
- mirrorneuron_cli-1.1.4/mn_cli/libs/blueprint_repository.py +159 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/libs/run_cmds.py +23 -259
- mirrorneuron_cli-1.1.4/mn_cli/libs/run_logs.py +202 -0
- mirrorneuron_cli-1.1.4/mn_cli/libs/run_manifest.py +253 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/server_cmds.py +13 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/update_cmds.py +17 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/tests/test_blueprint_cmds.py +132 -0
- mirrorneuron_cli-1.1.4/tests/test_blueprint_repository.py +46 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/tests/test_run_cmds.py +6 -2
- mirrorneuron_cli-1.1.4/tests/test_run_helpers.py +155 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/.github/workflows/ci.yml +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/.github/workflows/release.yml +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/.gitignore +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/LICENSE +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/RELEASE.md +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mirrorneuron_cli.egg-info/dependency_links.txt +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mirrorneuron_cli.egg-info/entry_points.txt +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mirrorneuron_cli.egg-info/requires.txt +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mirrorneuron_cli.egg-info/top_level.txt +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/__init__.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/config.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/error_handler.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/libs/__init__.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/libs/job_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/libs/sys_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/libs/ui.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/logging_config.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/main.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/mn_cli/shared.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/pyproject.toml +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/scripts/check-release-artifacts.sh +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/scripts/make-release-zip.sh +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/scripts/validate-version-tag.sh +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/setup.cfg +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/tests/conftest.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/tests/test_job_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/tests/test_server_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/tests/test_sys_cmds.py +0 -0
- {mirrorneuron_cli-1.1.2 → mirrorneuron_cli-1.1.4}/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.4
|
|
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.4
|
|
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
|
|
@@ -5,135 +5,59 @@ import subprocess
|
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, Optional
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
from rich.table import Table
|
|
12
|
+
from mn_cli.libs.blueprint_observability import (
|
|
13
|
+
artifact_headline as _artifact_headline,
|
|
14
|
+
display as _display,
|
|
15
|
+
final_artifact as _final_artifact,
|
|
16
|
+
job_id_from_record as _job_id,
|
|
17
|
+
load_observability_api as _load_observability_api,
|
|
18
|
+
load_run_or_exit as _load_run_or_exit,
|
|
19
|
+
load_web_ui_api as _load_web_ui_api,
|
|
20
|
+
make_blueprint_run_id as _make_blueprint_run_id,
|
|
21
|
+
print_events as _print_events,
|
|
22
|
+
render_markdown_export as _render_markdown_export,
|
|
23
|
+
run_summary as _run_summary,
|
|
24
|
+
web_ui_url as _web_ui_url,
|
|
25
|
+
)
|
|
26
|
+
from mn_cli.libs.blueprint_repository import (
|
|
27
|
+
BLUEPRINT_REPO_CONTEXT_KEY,
|
|
28
|
+
DEFAULT_BLUEPRINT_REPO,
|
|
29
|
+
BlueprintIndexError,
|
|
30
|
+
blueprint_cache_dir_for_repo as _blueprint_cache_dir_for_repo,
|
|
31
|
+
blueprint_storage_dir_for_source as _blueprint_storage_dir_for_source,
|
|
32
|
+
clone_blueprint_repo as _clone_blueprint_repo,
|
|
33
|
+
context_blueprint_repo as _context_blueprint_repo,
|
|
34
|
+
default_blueprint_storage_dir as _default_blueprint_storage_dir,
|
|
35
|
+
ensure_blueprint_source as _ensure_blueprint_source,
|
|
36
|
+
git_checkout as _git_checkout,
|
|
37
|
+
git_fetch as _git_fetch,
|
|
38
|
+
git_pull as _git_pull,
|
|
39
|
+
git_revision as _git_revision,
|
|
40
|
+
load_blueprint_index as _load_blueprint_index,
|
|
41
|
+
)
|
|
12
42
|
from mn_cli.shared import console, logger
|
|
13
43
|
from mn_cli.libs.run_cmds import run_bundle as _run_bundle
|
|
44
|
+
from mn_cli.libs.run_manifest import load_blueprint_config as _load_blueprint_config
|
|
14
45
|
|
|
15
46
|
blueprint_app = typer.Typer(help="Manage and run MirrorNeuron blueprints")
|
|
16
|
-
|
|
47
|
+
_PATCH_COMPAT = (subprocess, _git_checkout, _git_fetch)
|
|
17
48
|
|
|
18
49
|
|
|
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
|
-
|
|
50
|
+
@blueprint_app.callback()
|
|
51
|
+
def blueprint_callback(
|
|
52
|
+
ctx: typer.Context,
|
|
53
|
+
blueprint_repo: Optional[str] = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--blueprint-repo",
|
|
56
|
+
help="Use this blueprint repository URL/path instead of the default catalog.",
|
|
57
|
+
),
|
|
58
|
+
) -> None:
|
|
59
|
+
ctx.obj = dict(ctx.obj or {})
|
|
60
|
+
ctx.obj[BLUEPRINT_REPO_CONTEXT_KEY] = blueprint_repo
|
|
137
61
|
|
|
138
62
|
def _is_python_source_blueprint(manifest: dict[str, Any]) -> bool:
|
|
139
63
|
metadata = manifest.get("metadata") or {}
|
|
@@ -206,6 +130,7 @@ def _run_resolved_blueprint(
|
|
|
206
130
|
if revision:
|
|
207
131
|
console.print(f"Blueprint revision: {revision}")
|
|
208
132
|
bundle_path = _prepare_blueprint_bundle_for_run(blueprint_dir, manifest, shared_run_id)
|
|
133
|
+
config_overrides = _collect_init_config_review_overrides(bundle_path, manifest)
|
|
209
134
|
_run_bundle(
|
|
210
135
|
str(bundle_path),
|
|
211
136
|
follow_seconds=follow_seconds,
|
|
@@ -219,6 +144,7 @@ def _run_resolved_blueprint(
|
|
|
219
144
|
"blueprint_revision": revision,
|
|
220
145
|
"blueprint_source": source_label,
|
|
221
146
|
},
|
|
147
|
+
config_overrides=config_overrides,
|
|
222
148
|
)
|
|
223
149
|
|
|
224
150
|
|
|
@@ -249,59 +175,114 @@ def _run_local_blueprint_target(
|
|
|
249
175
|
return True
|
|
250
176
|
|
|
251
177
|
|
|
252
|
-
def
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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)
|
|
178
|
+
def _collect_init_config_review_overrides(
|
|
179
|
+
bundle_path: Path,
|
|
180
|
+
manifest: dict[str, Any],
|
|
181
|
+
) -> dict[str, Any] | None:
|
|
182
|
+
review = _manifest_init_config_review(manifest)
|
|
183
|
+
if not isinstance(review, dict):
|
|
184
|
+
return None
|
|
185
|
+
fields = review.get("fields")
|
|
186
|
+
if not isinstance(fields, list) or not fields:
|
|
187
|
+
return None
|
|
188
|
+
if _env_flag("MN_BLUEPRINT_SKIP_INIT_CONFIG_REVIEW"):
|
|
189
|
+
return None
|
|
190
|
+
if not sys.stdin.isatty():
|
|
191
|
+
if review.get("required") is True:
|
|
192
|
+
console.print("[yellow]Blueprint config review requested; keeping current config in this non-interactive run.[/yellow]")
|
|
193
|
+
return None
|
|
295
194
|
|
|
195
|
+
config = _load_blueprint_config(bundle_path) or {}
|
|
196
|
+
overrides: dict[str, Any] = {}
|
|
197
|
+
console.print("[bold]Review blueprint config before launch[/bold]")
|
|
198
|
+
instruction = review.get("instruction")
|
|
199
|
+
if isinstance(instruction, str) and instruction.strip():
|
|
200
|
+
console.print(instruction.strip())
|
|
296
201
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
202
|
+
for raw_field in fields:
|
|
203
|
+
if not isinstance(raw_field, dict):
|
|
204
|
+
continue
|
|
205
|
+
path = raw_field.get("path")
|
|
206
|
+
if not isinstance(path, str) or not path.strip():
|
|
207
|
+
continue
|
|
208
|
+
path = path.strip()
|
|
209
|
+
label = str(raw_field.get("label") or path)
|
|
210
|
+
description = raw_field.get("description")
|
|
211
|
+
current = _config_path_get(config, path)
|
|
212
|
+
fallback = raw_field.get("default")
|
|
213
|
+
default_value = current if current is not None else fallback
|
|
214
|
+
if isinstance(description, str) and description.strip():
|
|
215
|
+
console.print(f"{label}: {description.strip()}")
|
|
216
|
+
if default_value is None:
|
|
217
|
+
response = typer.prompt(label, default="", show_default=False)
|
|
218
|
+
if response == "":
|
|
219
|
+
continue
|
|
220
|
+
else:
|
|
221
|
+
response = typer.prompt(label, default=str(default_value), show_default=True)
|
|
222
|
+
parsed = _parse_review_value(response, default_value)
|
|
223
|
+
if parsed != current:
|
|
224
|
+
_config_path_set(overrides, path, parsed)
|
|
225
|
+
|
|
226
|
+
return overrides or None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _manifest_init_config_review(manifest: dict[str, Any]) -> Any:
|
|
230
|
+
if "init_config_review" in manifest:
|
|
231
|
+
return manifest.get("init_config_review")
|
|
232
|
+
metadata = manifest.get("metadata")
|
|
233
|
+
if isinstance(metadata, dict):
|
|
234
|
+
return metadata.get("init_config_review")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _config_path_get(config: dict[str, Any], dotted_path: str) -> Any:
|
|
239
|
+
cursor: Any = config
|
|
240
|
+
for part in dotted_path.split("."):
|
|
241
|
+
if not isinstance(cursor, dict) or part not in cursor:
|
|
242
|
+
return None
|
|
243
|
+
cursor = cursor[part]
|
|
244
|
+
return cursor
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _config_path_set(config: dict[str, Any], dotted_path: str, value: Any) -> None:
|
|
248
|
+
cursor = config
|
|
249
|
+
parts = [part for part in dotted_path.split(".") if part]
|
|
250
|
+
for part in parts[:-1]:
|
|
251
|
+
next_value = cursor.get(part)
|
|
252
|
+
if not isinstance(next_value, dict):
|
|
253
|
+
next_value = {}
|
|
254
|
+
cursor[part] = next_value
|
|
255
|
+
cursor = next_value
|
|
256
|
+
if parts:
|
|
257
|
+
cursor[parts[-1]] = value
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _parse_review_value(value: str, default_value: Any) -> Any:
|
|
261
|
+
if isinstance(default_value, bool):
|
|
262
|
+
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
263
|
+
if isinstance(default_value, int) and not isinstance(default_value, bool):
|
|
264
|
+
try:
|
|
265
|
+
return int(value)
|
|
266
|
+
except ValueError:
|
|
267
|
+
return value
|
|
268
|
+
if isinstance(default_value, float):
|
|
269
|
+
try:
|
|
270
|
+
return float(value)
|
|
271
|
+
except ValueError:
|
|
272
|
+
return value
|
|
273
|
+
if isinstance(default_value, (dict, list)):
|
|
274
|
+
try:
|
|
275
|
+
return json.loads(value)
|
|
276
|
+
except json.JSONDecodeError:
|
|
277
|
+
return value
|
|
278
|
+
return value
|
|
300
279
|
|
|
301
280
|
|
|
302
|
-
def
|
|
303
|
-
|
|
304
|
-
|
|
281
|
+
def _env_flag(name: str) -> bool:
|
|
282
|
+
value = os.environ.get(name)
|
|
283
|
+
if value is None:
|
|
284
|
+
return False
|
|
285
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
305
286
|
|
|
306
287
|
|
|
307
288
|
def _print_run_table(runs: list[dict[str, Any]]) -> None:
|
|
@@ -322,66 +303,28 @@ def _print_run_table(runs: list[dict[str, Any]]) -> None:
|
|
|
322
303
|
)
|
|
323
304
|
|
|
324
305
|
|
|
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
306
|
@blueprint_app.command("list")
|
|
376
|
-
def blueprint_list():
|
|
307
|
+
def blueprint_list(ctx: typer.Context):
|
|
377
308
|
"""List all available blueprints from the local storage shared with mn staff"""
|
|
378
|
-
|
|
379
|
-
if
|
|
380
|
-
|
|
381
|
-
|
|
309
|
+
blueprint_repo = _context_blueprint_repo(ctx)
|
|
310
|
+
if blueprint_repo:
|
|
311
|
+
storage_dir = Path(
|
|
312
|
+
_ensure_blueprint_source(
|
|
313
|
+
source=None,
|
|
314
|
+
blueprint_repo=blueprint_repo,
|
|
315
|
+
update=False,
|
|
316
|
+
offline=False,
|
|
317
|
+
revision=None,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
index_path = storage_dir / "index.json"
|
|
321
|
+
else:
|
|
322
|
+
index_path = Path(os.path.expanduser("~/.mn/blueprints/index.json"))
|
|
323
|
+
if not index_path.exists():
|
|
324
|
+
console.print("[yellow]Blueprint storage not initialized. Run 'mn blueprint run <name>' to initialize.[/yellow]")
|
|
325
|
+
return
|
|
382
326
|
try:
|
|
383
|
-
|
|
384
|
-
blueprints = json.load(f)
|
|
327
|
+
blueprints = _load_blueprint_index(index_path)
|
|
385
328
|
table = Table("ID", "Name", "Job Name", "Description")
|
|
386
329
|
for bp in blueprints:
|
|
387
330
|
table.add_row(
|
|
@@ -391,12 +334,15 @@ def blueprint_list():
|
|
|
391
334
|
bp.get("description", "")
|
|
392
335
|
)
|
|
393
336
|
console.print(table)
|
|
394
|
-
except
|
|
337
|
+
except BlueprintIndexError as e:
|
|
395
338
|
logger.exception("Error reading blueprint index")
|
|
396
339
|
console.print(f"[red]Error reading blueprints index: {e}[/red]")
|
|
340
|
+
if blueprint_repo:
|
|
341
|
+
raise typer.Exit(1)
|
|
397
342
|
|
|
398
343
|
@blueprint_app.command("run")
|
|
399
344
|
def blueprint_run(
|
|
345
|
+
ctx: typer.Context,
|
|
400
346
|
blueprint_path_name: str,
|
|
401
347
|
run_id: Optional[str] = typer.Option(None, "--run-id", help="Use a specific shared blueprint run ID."),
|
|
402
348
|
source: Optional[str] = typer.Option(None, "--source", help="Use a local blueprint repo/path or clone URL instead of ~/.mn/blueprints."),
|
|
@@ -413,19 +359,20 @@ def blueprint_run(
|
|
|
413
359
|
):
|
|
414
360
|
return
|
|
415
361
|
|
|
416
|
-
storage_dir = _ensure_blueprint_source(
|
|
362
|
+
storage_dir = _ensure_blueprint_source(
|
|
363
|
+
source=source,
|
|
364
|
+
blueprint_repo=_context_blueprint_repo(ctx),
|
|
365
|
+
update=update,
|
|
366
|
+
offline=offline,
|
|
367
|
+
revision=revision,
|
|
368
|
+
)
|
|
417
369
|
|
|
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
|
-
|
|
370
|
+
index_path = Path(storage_dir) / "index.json"
|
|
423
371
|
try:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
except Exception as e:
|
|
372
|
+
blueprints = _load_blueprint_index(index_path, require_paths=True)
|
|
373
|
+
except BlueprintIndexError as e:
|
|
427
374
|
logger.exception("Error parsing blueprint index")
|
|
428
|
-
console.print(f"[red]Error
|
|
375
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
429
376
|
raise typer.Exit(1)
|
|
430
377
|
|
|
431
378
|
target_bp = None
|
|
@@ -457,11 +404,18 @@ def blueprint_run(
|
|
|
457
404
|
|
|
458
405
|
@blueprint_app.command("install")
|
|
459
406
|
def blueprint_install(
|
|
460
|
-
|
|
407
|
+
ctx: typer.Context,
|
|
408
|
+
source: Optional[str] = typer.Option(None, "--source", help="Blueprint repository URL or local path."),
|
|
461
409
|
force: bool = typer.Option(False, "--force", help="Replace the existing cached repository."),
|
|
462
410
|
):
|
|
463
411
|
"""Install the blueprint library into ~/.mn/blueprints."""
|
|
464
|
-
|
|
412
|
+
blueprint_repo = _context_blueprint_repo(ctx)
|
|
413
|
+
repo_source = source or blueprint_repo or DEFAULT_BLUEPRINT_REPO
|
|
414
|
+
storage_dir = (
|
|
415
|
+
_blueprint_cache_dir_for_repo(repo_source)
|
|
416
|
+
if source is None and blueprint_repo
|
|
417
|
+
else _default_blueprint_storage_dir()
|
|
418
|
+
)
|
|
465
419
|
if storage_dir.exists() and not force:
|
|
466
420
|
console.print(f"[yellow]Blueprint storage already exists at {storage_dir}. Use --force to replace it.[/yellow]")
|
|
467
421
|
return
|
|
@@ -469,20 +423,37 @@ def blueprint_install(
|
|
|
469
423
|
import shutil
|
|
470
424
|
|
|
471
425
|
shutil.rmtree(storage_dir)
|
|
472
|
-
_clone_blueprint_repo(
|
|
426
|
+
_clone_blueprint_repo(repo_source, storage_dir)
|
|
427
|
+
try:
|
|
428
|
+
_load_blueprint_index(storage_dir / "index.json")
|
|
429
|
+
except BlueprintIndexError as e:
|
|
430
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
431
|
+
raise typer.Exit(1)
|
|
473
432
|
console.print(f"[green]Installed blueprints at {storage_dir}.[/green]")
|
|
474
433
|
|
|
475
434
|
|
|
476
435
|
@blueprint_app.command("update")
|
|
477
436
|
def blueprint_update(
|
|
437
|
+
ctx: typer.Context,
|
|
478
438
|
source: Optional[str] = typer.Option(None, "--source", help="Cached blueprint repo/path to update."),
|
|
479
439
|
):
|
|
480
440
|
"""Update the cached blueprint library explicitly."""
|
|
481
|
-
|
|
441
|
+
blueprint_repo = _context_blueprint_repo(ctx)
|
|
442
|
+
if source:
|
|
443
|
+
storage_dir = Path(source).expanduser()
|
|
444
|
+
elif blueprint_repo:
|
|
445
|
+
storage_dir = _blueprint_storage_dir_for_source(blueprint_repo)
|
|
446
|
+
else:
|
|
447
|
+
storage_dir = _default_blueprint_storage_dir()
|
|
482
448
|
if not storage_dir.exists():
|
|
483
449
|
console.print(f"[red]Blueprint storage not found at {storage_dir}. Run 'mn blueprint install' first.[/red]")
|
|
484
450
|
raise typer.Exit(1)
|
|
485
451
|
_git_pull(storage_dir)
|
|
452
|
+
try:
|
|
453
|
+
_load_blueprint_index(storage_dir / "index.json")
|
|
454
|
+
except BlueprintIndexError as e:
|
|
455
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
456
|
+
raise typer.Exit(1)
|
|
486
457
|
|
|
487
458
|
|
|
488
459
|
@blueprint_app.command("monitor")
|