cgh-codegen 0.1.0__py3-none-any.whl
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.
- cgh_codegen/__init__.py +24 -0
- cgh_codegen/backends.py +133 -0
- cgh_codegen/cli.py +295 -0
- cgh_codegen/flow.py +439 -0
- cgh_codegen/gate.py +73 -0
- cgh_codegen/generate.py +193 -0
- cgh_codegen/logview.py +42 -0
- cgh_codegen/mcp_tools.py +94 -0
- cgh_codegen/picker.py +238 -0
- cgh_codegen/py.typed +0 -0
- cgh_codegen-0.1.0.dist-info/METADATA +91 -0
- cgh_codegen-0.1.0.dist-info/RECORD +15 -0
- cgh_codegen-0.1.0.dist-info/WHEEL +5 -0
- cgh_codegen-0.1.0.dist-info/entry_points.txt +2 -0
- cgh_codegen-0.1.0.dist-info/top_level.txt +1 -0
cgh_codegen/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
|
|
2
|
+
# __creation__ = 2026-09-14
|
|
3
|
+
# __author__ = "jndjama (Joy Ndjama)"
|
|
4
|
+
# __copyright__ = "Copyright 2026 ALTIKVA."
|
|
5
|
+
# __licence__ = "MIT"
|
|
6
|
+
# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
|
|
7
|
+
# Description: cgh plugin entry point. Registers the `cgh codegen` CLI
|
|
8
|
+
# verbs (pick, gen) and the codegen_pick / codegen_write MCP
|
|
9
|
+
# tools: pick the file to mirror, then generate the target from
|
|
10
|
+
# a spec with a cheap model, behind the egress gate.
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
CGH_PLUGIN_API = 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(api) -> None:
|
|
18
|
+
from .cli import make_cli_registrar
|
|
19
|
+
|
|
20
|
+
api.register_cli(make_cli_registrar(api.config))
|
|
21
|
+
|
|
22
|
+
from .mcp_tools import make_mcp_registrar
|
|
23
|
+
|
|
24
|
+
api.register_mcp_tools(make_mcp_registrar(api.config))
|
cgh_codegen/backends.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
|
|
2
|
+
# __creation__ = 2026-09-15
|
|
3
|
+
# __author__ = "jndjama (Joy Ndjama)"
|
|
4
|
+
# __copyright__ = "Copyright 2026 ALTIKVA."
|
|
5
|
+
# __licence__ = "MIT"
|
|
6
|
+
# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
|
|
7
|
+
# Description: Two backends. CliBackend shells out to a configured agent CLI
|
|
8
|
+
# (claude, codex, ...), a cloud egress the flow gates. OllamaBackend
|
|
9
|
+
# calls a local Ollama model, so nothing leaves the machine and the
|
|
10
|
+
# gate is skipped, the leanest, free path for boilerplate.
|
|
11
|
+
# resolve_backend picks between them from config. The four-backend
|
|
12
|
+
# matrix from cgh-summarize is not rebuilt; more come with demand.
|
|
13
|
+
# Every call goes through the Backend protocol, so tests use a fake.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
|
|
23
|
+
_OLLAMA_DEFAULT_URL = "http://127.0.0.1:11434/api/generate"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CliBackend:
|
|
27
|
+
"""Generate by invoking a configured agent CLI. The agent reaches a
|
|
28
|
+
cloud model, so this is NOT local: the flow runs each reference through
|
|
29
|
+
the egress gate before calling it."""
|
|
30
|
+
|
|
31
|
+
is_local = False
|
|
32
|
+
|
|
33
|
+
def __init__(self, command: list[str], timeout: float = 120.0) -> None:
|
|
34
|
+
# command is an argv list from config, e.g. ["claude", "-p"]. Never a
|
|
35
|
+
# shell string: no shell=True, so nothing in the prompt is interpreted.
|
|
36
|
+
self._command = list(command)
|
|
37
|
+
self._timeout = timeout
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def name(self) -> str:
|
|
41
|
+
return f"cli:{self._command[0]}" if self._command else "cli"
|
|
42
|
+
|
|
43
|
+
def available(self) -> bool:
|
|
44
|
+
return bool(self._command) and shutil.which(self._command[0]) is not None
|
|
45
|
+
|
|
46
|
+
def generate(self, system: str, user: str) -> tuple[str, float]:
|
|
47
|
+
from codegraph.plugin_api import quiet_subprocess_kwargs
|
|
48
|
+
|
|
49
|
+
prompt = f"{system}\n\n{user}"
|
|
50
|
+
try:
|
|
51
|
+
proc = subprocess.run(
|
|
52
|
+
[*self._command, "--"],
|
|
53
|
+
input=prompt,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
timeout=self._timeout,
|
|
57
|
+
check=False,
|
|
58
|
+
**quiet_subprocess_kwargs(),
|
|
59
|
+
)
|
|
60
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
61
|
+
return "", 0.0
|
|
62
|
+
# Cost is unknown for an agent CLI (it bills its own account); report
|
|
63
|
+
# 0.0 rather than invent a number.
|
|
64
|
+
return proc.stdout or "", 0.0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class OllamaBackend:
|
|
68
|
+
"""Generate with a local Ollama model. Nothing leaves the machine, so
|
|
69
|
+
is_local is True and the flow skips the egress gate. The leanest option:
|
|
70
|
+
no cloud round-trip, no per-token cost."""
|
|
71
|
+
|
|
72
|
+
is_local = True
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self, model: str, url: str = _OLLAMA_DEFAULT_URL, timeout: float = 120.0
|
|
76
|
+
) -> None:
|
|
77
|
+
self._model = model
|
|
78
|
+
self._url = url
|
|
79
|
+
self._timeout = timeout
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def name(self) -> str:
|
|
83
|
+
return f"ollama:{self._model}"
|
|
84
|
+
|
|
85
|
+
def available(self) -> bool:
|
|
86
|
+
return bool(self._model)
|
|
87
|
+
|
|
88
|
+
def generate(self, system: str, user: str) -> tuple[str, float]:
|
|
89
|
+
payload = json.dumps(
|
|
90
|
+
{
|
|
91
|
+
"model": self._model,
|
|
92
|
+
"prompt": f"{system}\n\n{user}",
|
|
93
|
+
"stream": False,
|
|
94
|
+
}
|
|
95
|
+
).encode("utf-8")
|
|
96
|
+
req = urllib.request.Request(
|
|
97
|
+
self._url,
|
|
98
|
+
data=payload,
|
|
99
|
+
headers={"Content-Type": "application/json"},
|
|
100
|
+
method="POST",
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
104
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
105
|
+
except (urllib.error.URLError, TimeoutError, ValueError, OSError):
|
|
106
|
+
return "", 0.0
|
|
107
|
+
# Local generation is free; report 0.0 cost.
|
|
108
|
+
return body.get("response", ""), 0.0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_backend(config: dict):
|
|
112
|
+
"""The backend to generate with, chosen from config. An explicit
|
|
113
|
+
``backend = "ollama"`` (or an ``ollama_model`` with no ``command``) selects
|
|
114
|
+
the local model; otherwise a ``command`` selects the agent CLI. Returns
|
|
115
|
+
None when nothing is configured, so the caller reports a clear "no backend"
|
|
116
|
+
instead of guessing."""
|
|
117
|
+
kind = str(config.get("backend", "")).strip().lower()
|
|
118
|
+
if kind == "ollama" or (not config.get("command") and config.get("ollama_model")):
|
|
119
|
+
model = config.get("ollama_model")
|
|
120
|
+
if not model:
|
|
121
|
+
return None
|
|
122
|
+
return OllamaBackend(
|
|
123
|
+
model,
|
|
124
|
+
config.get("ollama_url", _OLLAMA_DEFAULT_URL),
|
|
125
|
+
timeout=float(config.get("timeout", 120.0)),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
command = config.get("command")
|
|
129
|
+
if isinstance(command, str):
|
|
130
|
+
command = command.split()
|
|
131
|
+
if not command:
|
|
132
|
+
return None
|
|
133
|
+
return CliBackend(command, timeout=float(config.get("timeout", 120.0)))
|
cgh_codegen/cli.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
|
|
2
|
+
# __creation__ = 2026-09-14
|
|
3
|
+
# __author__ = "jndjama (Joy Ndjama)"
|
|
4
|
+
# __copyright__ = "Copyright 2026 ALTIKVA."
|
|
5
|
+
# __licence__ = "MIT"
|
|
6
|
+
# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
|
|
7
|
+
# Description: CLI verbs `cgh codegen pick` (report the reference to mirror),
|
|
8
|
+
# `cgh codegen gen` (generate the file from a spec plus that
|
|
9
|
+
# reference, behind the egress gate, refusing to clobber without
|
|
10
|
+
# --force), and `cgh codegen log` (show what codegen has done in
|
|
11
|
+
# this repo, read from the activity log, at no model-token cost).
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def plugin_config_for_root(root: str | Path, fallback: dict) -> dict:
|
|
20
|
+
"""The [plugin.codegen] table resolved from ``root``.
|
|
21
|
+
|
|
22
|
+
Plugins are loaded once at CLI startup against the current directory, so
|
|
23
|
+
the config captured then belongs to the CWD, not to a --root passed on
|
|
24
|
+
the command line. Re-resolve from the requested root so --root governs
|
|
25
|
+
the backend and the egress posture too, not just where the file lands.
|
|
26
|
+
Falls back to the captured config when the root declares no table.
|
|
27
|
+
"""
|
|
28
|
+
from codegraph.plugin_api import load_config
|
|
29
|
+
|
|
30
|
+
return load_config(root).plugin_tables.get("codegen", fallback)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def make_cli_registrar(config: dict):
|
|
34
|
+
def add_cli(sub) -> None:
|
|
35
|
+
p = sub.add_parser("codegen", help="Pattern-matched code generation helpers")
|
|
36
|
+
actions = p.add_subparsers(dest="cw_action")
|
|
37
|
+
|
|
38
|
+
pick = actions.add_parser(
|
|
39
|
+
"pick", help="Show the reference file to mirror for a target"
|
|
40
|
+
)
|
|
41
|
+
pick.add_argument("--target", required=True, help="File you intend to write")
|
|
42
|
+
pick.add_argument(
|
|
43
|
+
"--reference", default="", help="Force a reference instead of picking one"
|
|
44
|
+
)
|
|
45
|
+
pick.add_argument("--root", default=os.getcwd())
|
|
46
|
+
pick.set_defaults(func=lambda args: _cmd_pick(args, config))
|
|
47
|
+
|
|
48
|
+
gen = actions.add_parser(
|
|
49
|
+
"gen", help="Generate a file from a spec, mirroring a reference"
|
|
50
|
+
)
|
|
51
|
+
gen.add_argument(
|
|
52
|
+
"--spec",
|
|
53
|
+
default="",
|
|
54
|
+
help="What to generate. Use '-' to read the spec from stdin. "
|
|
55
|
+
"Single-quote it in the shell, or use --spec-file, to avoid the "
|
|
56
|
+
"shell running backticks or $(...) inside it.",
|
|
57
|
+
)
|
|
58
|
+
gen.add_argument(
|
|
59
|
+
"--spec-file",
|
|
60
|
+
default="",
|
|
61
|
+
help="Read the spec from this file ('-' for stdin), instead of "
|
|
62
|
+
"--spec. Sidesteps shell quoting for long or backtick-heavy specs.",
|
|
63
|
+
)
|
|
64
|
+
gen.add_argument("--target", required=True, help="File to write")
|
|
65
|
+
gen.add_argument(
|
|
66
|
+
"--reference", default="", help="Force a reference instead of picking one"
|
|
67
|
+
)
|
|
68
|
+
gen.add_argument(
|
|
69
|
+
"--force", action="store_true", help="Overwrite the target if it exists"
|
|
70
|
+
)
|
|
71
|
+
gen.add_argument(
|
|
72
|
+
"--stdout", action="store_true", help="Print the code, do not write a file"
|
|
73
|
+
)
|
|
74
|
+
gen.add_argument(
|
|
75
|
+
"--extend",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Add to an existing target instead of writing a new file; "
|
|
78
|
+
"the model returns only the block to append",
|
|
79
|
+
)
|
|
80
|
+
gen.add_argument(
|
|
81
|
+
"--verify",
|
|
82
|
+
default="",
|
|
83
|
+
help="Shell check (exit 0 = pass) run after writing; drives the "
|
|
84
|
+
"self-correct loop",
|
|
85
|
+
)
|
|
86
|
+
gen.add_argument(
|
|
87
|
+
"--max-attempts",
|
|
88
|
+
type=int,
|
|
89
|
+
default=1,
|
|
90
|
+
help="With --verify, regenerate up to N times feeding the failure back",
|
|
91
|
+
)
|
|
92
|
+
gen.add_argument("--root", default=os.getcwd())
|
|
93
|
+
gen.set_defaults(func=lambda args: _cmd_gen(args, config))
|
|
94
|
+
|
|
95
|
+
log = actions.add_parser(
|
|
96
|
+
"log",
|
|
97
|
+
help="Show what codegen has done here, read from the activity log "
|
|
98
|
+
"(no model tokens)",
|
|
99
|
+
)
|
|
100
|
+
log.add_argument("--limit", type=int, default=20, help="Max entries to show")
|
|
101
|
+
log.add_argument("--root", default=os.getcwd())
|
|
102
|
+
log.set_defaults(func=lambda args: _cmd_log(args, config))
|
|
103
|
+
|
|
104
|
+
return add_cli
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _cmd_log(args, config: dict) -> None:
|
|
108
|
+
from datetime import datetime
|
|
109
|
+
|
|
110
|
+
from rich.console import Console
|
|
111
|
+
from rich.table import Table
|
|
112
|
+
|
|
113
|
+
from .logview import codegen_activity
|
|
114
|
+
|
|
115
|
+
console = Console()
|
|
116
|
+
root = Path(os.path.abspath(args.root))
|
|
117
|
+
rows = codegen_activity(root, limit=args.limit)
|
|
118
|
+
if not rows:
|
|
119
|
+
console.print(
|
|
120
|
+
"[dim]No codegen activity logged in this repo yet. "
|
|
121
|
+
"Runs of cgh codegen gen show up here.[/dim]"
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
table = Table(show_header=True, header_style="bold cyan", box=None, pad_edge=False)
|
|
126
|
+
table.add_column("when", style="dim", no_wrap=True)
|
|
127
|
+
table.add_column("event")
|
|
128
|
+
table.add_column("detail", overflow="fold")
|
|
129
|
+
for ts, event, detail in rows:
|
|
130
|
+
try:
|
|
131
|
+
when = datetime.fromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M")
|
|
132
|
+
except (ValueError, OSError):
|
|
133
|
+
when = "?"
|
|
134
|
+
kind = event.removeprefix("codegen_")
|
|
135
|
+
label = f"[red]{kind}[/red]" if kind == "egress_denied" else kind
|
|
136
|
+
table.add_row(when, label, detail)
|
|
137
|
+
console.print(f"[bold]codegen activity[/bold] [dim]({len(rows)})[/dim]")
|
|
138
|
+
console.print(table)
|
|
139
|
+
console.print(
|
|
140
|
+
"[dim]Read straight from .codegraph/activity.log; "
|
|
141
|
+
"nothing here entered a model's context.[/dim]"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _cmd_pick(args, config: dict) -> None:
|
|
146
|
+
from rich.console import Console
|
|
147
|
+
|
|
148
|
+
from .picker import CodegenError, pick_reference
|
|
149
|
+
|
|
150
|
+
console = Console()
|
|
151
|
+
root = Path(os.path.abspath(args.root))
|
|
152
|
+
try:
|
|
153
|
+
result = pick_reference(root, args.target, args.reference or None)
|
|
154
|
+
except CodegenError as exc:
|
|
155
|
+
console.print(f"[red]{exc}[/red]")
|
|
156
|
+
raise SystemExit(1) from exc
|
|
157
|
+
|
|
158
|
+
if result["reference"] is None:
|
|
159
|
+
console.print(f"[yellow]{result['reason']}[/yellow]")
|
|
160
|
+
if result["graph_available"] is False:
|
|
161
|
+
console.print(
|
|
162
|
+
"[dim]graph unavailable (no index, or an owner holds the "
|
|
163
|
+
"lock); picked from the filesystem only.[/dim]"
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
console.print(f"[bold]reference:[/bold] {result['reference']}")
|
|
168
|
+
console.print(f"[dim]{result['reason']}[/dim]")
|
|
169
|
+
others = [c for c in result["candidates"] if c != result["reference"]]
|
|
170
|
+
if others:
|
|
171
|
+
console.print("[dim]other candidates: " + ", ".join(others) + "[/dim]")
|
|
172
|
+
if result["graph_available"] is False:
|
|
173
|
+
console.print("[dim]graph unavailable; filesystem-only pick.[/dim]")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _resolve_spec(args, console) -> str:
|
|
177
|
+
"""The spec text, from --spec, --spec-file, or stdin ('-' in either).
|
|
178
|
+
|
|
179
|
+
Reading from a file or stdin lets an agent pass a long or backtick-heavy
|
|
180
|
+
spec without the shell running command-substitution inside it (the reported
|
|
181
|
+
footgun of an unquoted --spec). Exits with a clear message on an empty or
|
|
182
|
+
missing spec.
|
|
183
|
+
"""
|
|
184
|
+
import sys
|
|
185
|
+
|
|
186
|
+
spec_file = getattr(args, "spec_file", "") or ""
|
|
187
|
+
spec = getattr(args, "spec", "") or ""
|
|
188
|
+
text: str | None = None
|
|
189
|
+
if spec_file == "-" or spec == "-":
|
|
190
|
+
text = sys.stdin.read()
|
|
191
|
+
elif spec_file:
|
|
192
|
+
try:
|
|
193
|
+
text = Path(spec_file).read_text(encoding="utf-8")
|
|
194
|
+
except OSError as exc:
|
|
195
|
+
console.print(f"[red]cannot read --spec-file {spec_file}: {exc}[/red]")
|
|
196
|
+
raise SystemExit(1) from exc
|
|
197
|
+
elif spec:
|
|
198
|
+
text = spec
|
|
199
|
+
text = (text or "").strip()
|
|
200
|
+
if not text:
|
|
201
|
+
console.print(
|
|
202
|
+
"[red]empty spec.[/red] Pass --spec '...' (single-quoted), "
|
|
203
|
+
"--spec-file <path>, or --spec - to read from stdin."
|
|
204
|
+
)
|
|
205
|
+
raise SystemExit(1)
|
|
206
|
+
return text
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _cmd_gen(args, config: dict) -> None:
|
|
210
|
+
from rich.console import Console
|
|
211
|
+
|
|
212
|
+
from .backends import resolve_backend
|
|
213
|
+
from .flow import run_generation
|
|
214
|
+
from .generate import GenerationError
|
|
215
|
+
from .picker import CodegenError
|
|
216
|
+
|
|
217
|
+
console = Console()
|
|
218
|
+
root = Path(os.path.abspath(args.root))
|
|
219
|
+
config = plugin_config_for_root(root, config)
|
|
220
|
+
|
|
221
|
+
spec = _resolve_spec(args, console)
|
|
222
|
+
|
|
223
|
+
backend = resolve_backend(config)
|
|
224
|
+
if backend is None:
|
|
225
|
+
console.print(
|
|
226
|
+
"[red]no backend configured.[/red] Set a backend command under "
|
|
227
|
+
"plugin.codegen in .codegraph/config.toml "
|
|
228
|
+
'(command = "claude -p").'
|
|
229
|
+
)
|
|
230
|
+
raise SystemExit(1)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
result = run_generation(
|
|
234
|
+
root,
|
|
235
|
+
spec,
|
|
236
|
+
args.target,
|
|
237
|
+
args.reference or None,
|
|
238
|
+
config=config,
|
|
239
|
+
backend=backend,
|
|
240
|
+
force=args.force,
|
|
241
|
+
to_stdout=args.stdout,
|
|
242
|
+
verify=args.verify or None,
|
|
243
|
+
max_attempts=max(1, args.max_attempts),
|
|
244
|
+
extend=args.extend,
|
|
245
|
+
)
|
|
246
|
+
except (CodegenError, GenerationError) as exc:
|
|
247
|
+
console.print(f"[red]{exc}[/red]")
|
|
248
|
+
raise SystemExit(1) from exc
|
|
249
|
+
|
|
250
|
+
if args.stdout:
|
|
251
|
+
# code to stdout stays clean; the notes go to stderr via rich stderr
|
|
252
|
+
stderr = Console(stderr=True)
|
|
253
|
+
if result.get("ref_fallback"):
|
|
254
|
+
stderr.print(f"[yellow]note:[/yellow] [dim]{result['ref_fallback']}[/dim]")
|
|
255
|
+
stderr.print(
|
|
256
|
+
f"[dim]{result['reference']} -> {result['lines']} lines "
|
|
257
|
+
f"({result['backend']}, egress: {result['egress']})[/dim]"
|
|
258
|
+
)
|
|
259
|
+
print(result["code"])
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
if result.get("rolled_back"):
|
|
263
|
+
console.print(
|
|
264
|
+
f"[red]not appended[/red] to {result['target']}: the check never "
|
|
265
|
+
f"passed in {result['attempts']} attempt(s), so the file was left "
|
|
266
|
+
"as it was."
|
|
267
|
+
)
|
|
268
|
+
raise SystemExit(1)
|
|
269
|
+
if result.get("ref_fallback"):
|
|
270
|
+
console.print(f"[yellow]note:[/yellow] [dim]{result['ref_fallback']}[/dim]")
|
|
271
|
+
if result["extended"]:
|
|
272
|
+
console.print(
|
|
273
|
+
f"[green]appended to[/green] {result['target']} "
|
|
274
|
+
f"[dim]({result['lines']} lines added)[/dim]"
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
console.print(
|
|
278
|
+
f"[green]wrote[/green] {result['target']} "
|
|
279
|
+
f"[dim]({result['lines']} lines, mirror of {result['reference']})[/dim]"
|
|
280
|
+
)
|
|
281
|
+
v = result.get("verified")
|
|
282
|
+
if v is True:
|
|
283
|
+
console.print(
|
|
284
|
+
f"[green]check passed[/green] "
|
|
285
|
+
f"[dim]after {result['attempts']} attempt(s)[/dim]"
|
|
286
|
+
)
|
|
287
|
+
elif v is False:
|
|
288
|
+
console.print(
|
|
289
|
+
f"[yellow]check still failing[/yellow] after {result['attempts']} "
|
|
290
|
+
"attempt(s); the written file is the last try, review it."
|
|
291
|
+
)
|
|
292
|
+
console.print(
|
|
293
|
+
f"[dim]backend: {result['backend']} egress: {result['egress']} "
|
|
294
|
+
"verify by running the checks, not by trusting this output.[/dim]"
|
|
295
|
+
)
|