Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / cli / repl / execution.py: 99%
117 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright © 2025 Bijan Mousavi
4"""Execution helpers for the REPL."""
6from __future__ import annotations
8from contextlib import suppress
9import json
10import os
11import shlex
12import sys
14from bijux_cli.cli.core.constants import OPT_FORMAT, OPT_QUIET, PRETTY_FLAGS
15from bijux_cli.cli.repl.parsing import (
16 _filter_control,
17 _known_commands,
18 _split_segments,
19 _suggest,
20)
21from bijux_cli.core.enums import ExitCode, OutputFormat
22from bijux_cli.core.exit_policy import ExitIntent, ExitIntentError
23from bijux_cli.core.runtime import run_command
25_JSON_CMDS = {
26 "audit",
27 "doctor",
28 "history",
29 "memory",
30 "plugins",
31 "status",
32 "version",
33}
36def _invoke(tokens: list[str], *, repl_quiet: bool) -> int:
37 """Runs a single CLI command invocation within the REPL sandbox."""
38 from importlib import import_module
40 from typer.testing import CliRunner
42 env = {**os.environ, "PS1": ""}
44 head = tokens[0] if tokens else ""
46 if head in _JSON_CMDS and not (set(PRETTY_FLAGS) | set(OPT_FORMAT)) & set(tokens):
47 tokens.append("--no-pretty")
49 if (
50 head == "config"
51 and len(tokens) > 1
52 and tokens[1] == "list"
53 and not (set(PRETTY_FLAGS) & set(tokens))
54 ):
55 tokens.append("--no-pretty")
57 cli_root = import_module("bijux_cli.cli.root")
58 app = getattr(cli_root, "build_app", None)
59 typer_app = app() if callable(app) else cli_root.app
60 result = CliRunner().invoke(typer_app, tokens, env=env)
62 sub_quiet = any(t in OPT_QUIET for t in tokens)
63 should_print = not repl_quiet and not sub_quiet
65 if head == "history":
66 with suppress(Exception):
67 data = json.loads(result.stdout or "{}")
68 if data.get("entries", []) == []:
69 if should_print:
70 from bijux_cli.cli.core.command import resolve_serializer
72 pretty = (
73 resolve_serializer()
74 .dumps(data, fmt=OutputFormat.JSON, pretty=True)
75 .rstrip("\n")
76 + "\n"
77 )
78 sys.stdout.write(pretty)
79 sys.stderr.write(result.stderr or "")
80 return result.exit_code
82 if should_print:
83 sys.stdout.write(result.stdout or "")
84 sys.stderr.write(result.stderr or "")
86 return result.exit_code
89def _run_piped(repl_quiet: bool) -> None:
90 """Processes piped input commands in non-interactive mode."""
91 for raw_line in sys.stdin.read().splitlines():
92 line = raw_line.rstrip()
94 if not line or line.lstrip().startswith("#"):
95 if not repl_quiet:
96 from bijux_cli.cli.repl.ui import get_prompt
98 sys.stderr.write(_filter_control(str(get_prompt())) + "\n")
99 sys.stderr.flush()
100 continue
102 if line.lstrip().startswith(";"):
103 bad = line.lstrip(";").strip()
104 hint = _suggest(bad)
105 msg = f"No such command '{bad}'." + (hint or "")
106 if not repl_quiet:
107 print(msg, file=sys.stderr)
108 continue
110 for seg in _split_segments(line):
111 seg = seg.strip()
112 if not seg or seg.startswith("#"):
113 continue
115 lo = seg.lower()
116 if lo in {"exit", "quit"}:
117 raise ExitIntentError(
118 ExitIntent(
119 code=ExitCode.SUCCESS,
120 stream=None,
121 payload=None,
122 fmt=OutputFormat.JSON,
123 pretty=False,
124 show_traceback=False,
125 )
126 )
128 if seg == "docs":
129 if not repl_quiet:
130 print("Available topics: …")
131 continue
132 if seg.startswith("docs "):
133 if not repl_quiet:
134 print(seg.split(None, 1)[1])
135 continue
137 if seg.startswith("-"):
138 bad = seg.lstrip("-")
139 hint = _suggest(bad)
140 msg = f"No such command '{bad}'." + (hint or "")
141 if not repl_quiet:
142 print(msg, file=sys.stderr)
143 continue
145 try:
146 tokens = shlex.split(seg)
147 except ValueError:
148 continue
149 if not tokens:
150 continue
152 head = tokens[0]
154 if head == "config":
155 sub = tokens[1:]
157 def _emit(
158 msg: str,
159 failure: str,
160 subcommand: list[str] = sub,
161 ) -> None:
162 """Emits a JSON error for a `config` subcommand."""
163 if repl_quiet:
164 return
166 error_obj = {
167 "error": msg,
168 "code": 2,
169 "failure": failure,
170 "command": f"config {subcommand[0] if subcommand else ''}".strip(),
171 "format": "json",
172 }
173 from bijux_cli.cli.core.command import resolve_serializer
175 print(
176 resolve_serializer().dumps(
177 error_obj, fmt=OutputFormat.JSON, pretty=False
178 )
179 )
181 if not sub:
182 pass
183 elif sub[0] == "set" and len(sub) == 1:
184 _emit("Missing argument: KEY=VALUE required", "missing_argument")
185 continue
186 elif sub[0] in {"get", "unset"} and len(sub) == 1: 186 ↛ 190line 186 didn't jump to line 190 because the condition on line 186 was always true
187 _emit("Missing argument: key required", "missing_argument")
188 continue
190 if head not in _known_commands():
191 hint = _suggest(head)
192 msg = f"No such command '{head}'."
193 if hint:
194 msg += hint
195 if not repl_quiet:
196 print(msg, file=sys.stderr)
197 continue
198 else:
199 _invoke(tokens, repl_quiet=repl_quiet)
201 raise ExitIntentError(
202 ExitIntent(
203 code=ExitCode.SUCCESS,
204 stream=None,
205 payload=None,
206 fmt=OutputFormat.JSON,
207 pretty=False,
208 show_traceback=False,
209 )
210 )
213def run_repl_session(*, quiet: bool, stdin_isatty: bool) -> None:
214 """Run the REPL in piped or interactive mode."""
215 if quiet or not stdin_isatty:
216 _run_piped(quiet)
217 else:
218 from bijux_cli.cli.repl.ui import _run_interactive
220 run_command(_run_interactive)