@tiens.nguyen/gu-cli 1.0.830 → 1.0.831

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.
@@ -7999,6 +7999,111 @@ _HEREDOC_HINT = ("\n\nSHELL QUOTING broke this line. For text with quotes or lin
7999
7999
  "run_command(\"\"\"git commit -F - <<'EOF'\n<message>\nEOF\"\"\") — nothing in the "
8000
8000
  "message needs escaping.")
8001
8001
 
8002
+ # A PIPE HIDES THE EXIT CODE OF EVERYTHING BEFORE IT (#360 review, turn 3c06654e). The model ran
8003
+ # `python3 -m pytest … | tail -5 && git add … && git commit …`; pytest was not installed, `tail`
8004
+ # exited 0, and the commit went in untested. Global `set -o pipefail` would fix that and break
8005
+ # `git log | head -5` (SIGPIPE → 141) everywhere, so the line runs as written and gu READS the
8006
+ # output instead: a pipe line that exited 0 while printing an unmistakable failure is reported
8007
+ # as one. Markers are line-anchored and specific, so ordinary output containing "error" passes.
8008
+ _PIPE_FAIL_RE = re.compile(
8009
+ r"^.*(?:No module named \S+|ModuleNotFoundError: .*|: command not found|: not found$"
8010
+ r"|^Traceback \(most recent call last\):|^fatal: .*|^error: .*|^npm ERR! .*"
8011
+ r"|^E: Unable to locate package .*|: No such file or directory$)",
8012
+ re.M)
8013
+
8014
+
8015
+ def _pipe_hidden_failure(scan, out: str) -> str:
8016
+ """The failure line a pipe hid, or "" — only for a line that has a `|`."""
8017
+ if not scan or not any(t[0] == "op" and t[1] == "|" for t in scan.get("tokens") or []):
8018
+ return ""
8019
+ m = _PIPE_FAIL_RE.search(out or "")
8020
+ return m.group(0).strip()[:200] if m else ""
8021
+
8022
+
8023
+ def _pipe_hidden_failure_note(line: str) -> str:
8024
+ return ("\n\nWARNING — A FAILURE WAS HIDDEN BY A PIPE: the output shows "
8025
+ f"'{line}', but a pipeline reports only its LAST command's exit code, so this "
8026
+ "line counted as success and every part after `&&` RAN ANYWAY. Check what those "
8027
+ "parts did before relying on them. To chain on a check, run it without a pipe, or "
8028
+ "start the line with `set -o pipefail;`.")
8029
+
8030
+
8031
+ # THE PROJECT'S OWN TEST COMMANDS (#360 review). The model guessed `python3 -m pytest` in a repo
8032
+ # whose package.json says `python3 -m unittest discover tests -v && node --test` — pytest was not
8033
+ # even installed. The manifests already say how each part of the project is tested; reading them
8034
+ # costs no tool call. Depth ≤ 3 (a monorepo's packages), at most 8, nearest first.
8035
+ _TEST_SKIP_DIRS = {"node_modules", "dist", "build", "out", "target", "vendor", "venv", ".venv",
8036
+ "__pycache__", "server-bundle", "coverage"}
8037
+
8038
+
8039
+ def _project_test_commands(root: str, max_depth: int = 3, limit: int = 8) -> list:
8040
+ """[(relative folder, command)] from package.json scripts.test, a Makefile `test:` target,
8041
+ pytest config, Cargo.toml and go.mod. Never raises."""
8042
+ import json as _json
8043
+ found = []
8044
+ try:
8045
+ root = os.path.abspath(root)
8046
+ except Exception: # noqa: BLE001
8047
+ return []
8048
+ queue = [(root, 0)]
8049
+ while queue and len(found) < limit:
8050
+ d, depth = queue.pop(0)
8051
+ try:
8052
+ names = set(os.listdir(d))
8053
+ except OSError:
8054
+ continue
8055
+ rel = os.path.relpath(d, root)
8056
+ rel = "." if rel == "." else rel.replace(os.sep, "/")
8057
+ cmd = ""
8058
+ if "package.json" in names:
8059
+ try:
8060
+ with open(os.path.join(d, "package.json"), encoding="utf-8") as fh:
8061
+ t = ((_json.load(fh) or {}).get("scripts") or {}).get("test") or ""
8062
+ if t and "no test specified" not in t:
8063
+ cmd = f"npm test ({_clip(t, 110)})"
8064
+ except Exception: # noqa: BLE001
8065
+ pass
8066
+ if not cmd and "Makefile" in names:
8067
+ try:
8068
+ with open(os.path.join(d, "Makefile"), encoding="utf-8", errors="replace") as fh:
8069
+ if re.search(r"(?m)^test\s*:", fh.read(200_000)):
8070
+ cmd = "make test"
8071
+ except OSError:
8072
+ pass
8073
+ if not cmd and ("pytest.ini" in names or "conftest.py" in names):
8074
+ cmd = "python3 -m pytest"
8075
+ if not cmd and "pyproject.toml" in names:
8076
+ try:
8077
+ with open(os.path.join(d, "pyproject.toml"), encoding="utf-8", errors="replace") as fh:
8078
+ if "[tool.pytest" in fh.read(200_000):
8079
+ cmd = "python3 -m pytest"
8080
+ except OSError:
8081
+ pass
8082
+ if not cmd and "Cargo.toml" in names:
8083
+ cmd = "cargo test"
8084
+ if not cmd and "go.mod" in names:
8085
+ cmd = "go test ./..."
8086
+ if cmd:
8087
+ found.append((rel, cmd))
8088
+ if depth < max_depth:
8089
+ for n in sorted(names):
8090
+ if n.startswith(".") or n in _TEST_SKIP_DIRS:
8091
+ continue
8092
+ p = os.path.join(d, n)
8093
+ if os.path.isdir(p) and not os.path.islink(p):
8094
+ queue.append((p, depth + 1))
8095
+ return found
8096
+
8097
+
8098
+ def _test_commands_line(root: str) -> str:
8099
+ cmds = _project_test_commands(root) if root else []
8100
+ if not cmds:
8101
+ return ""
8102
+ return (" TEST COMMANDS, from this project's own manifests — use these, never guess a runner "
8103
+ "(a missing pytest fails silently behind a pipe): "
8104
+ + "; ".join(f"{d} → {c}" for d, c in cmds) + "\n")
8105
+
8106
+
8002
8107
  # bash's words for a line whose quotes do not pair up.
8003
8108
  _SHELL_QUOTE_ERR_RE = re.compile(
8004
8109
  r"unexpected EOF while looking for matching|syntax error near unexpected token|"
@@ -10418,9 +10523,16 @@ def run_agent_chat(cfg):
10418
10523
  _ws_servers_line += _shells_context_line(_shells_load())
10419
10524
  except Exception as _e: # noqa: BLE001
10420
10525
  _log(f"shell context skip: {_e}")
10526
+ _ws_tests_line = ""
10527
+ if _WS_AVAILABLE and _WS_ACTIVE:
10528
+ try:
10529
+ _ws_tests_line = _test_commands_line(_WS_ACTIVE)
10530
+ except Exception as _e: # noqa: BLE001
10531
+ _log(f"test command scan skip: {_e}")
10421
10532
  _ws_context_block = (
10422
10533
  _ws_current_line
10423
10534
  + f" Other registered workspaces (use only if the user names them): {_ws_names}\n"
10535
+ + _ws_tests_line
10424
10536
  + _ws_servers_line
10425
10537
  + f" Current folder contents:\n"
10426
10538
  + "\n".join(f" {line}" for line in _ws_overview.splitlines()) + "\n"
@@ -14754,6 +14866,11 @@ def run_agent_chat(cfg):
14754
14866
  # `git commit -m '…turn'\''s…'` lost its backslash to Python and bash
14755
14867
  # stopped on the quote; the next step was spent guessing. A heredoc in a
14756
14868
  # triple-quoted string has no quoting to get wrong.
14869
+ _hidden = _pipe_hidden_failure(_scan, out) if (rc == 0 and _use_shell) else ""
14870
+ if _hidden:
14871
+ result += _pipe_hidden_failure_note(_hidden)
14872
+ _emit({"type": "step", "text":
14873
+ f"Command failed behind a pipe → {_trim_words(_hidden)}"})
14757
14874
  if rc != 0 and _use_shell and _SHELL_QUOTE_ERR_RE.search(out or ""):
14758
14875
  result += _HEREDOC_HINT
14759
14876
  if rc != 0 and _WS_PERM_ERR_RE.search(out or ""):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.830",
3
+ "version": "1.0.831",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",