@caddis/cli 0.4.16 → 0.4.19

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.
@@ -38,8 +38,22 @@ import shutil
38
38
  import subprocess
39
39
  import sys
40
40
  import time
41
+ import datetime
42
+ import traceback
41
43
  from pathlib import Path
42
44
 
45
+ _DEBUG_LOG = Path.home() / ".caddis" / "statusline_debug.log"
46
+ def _debug_log(msg: str):
47
+ if not os.environ.get("CADDIS_STATUSLINE_DEBUG"):
48
+ return
49
+ try:
50
+ with open(_DEBUG_LOG, "a", encoding="utf-8") as f:
51
+ f.write(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}] [PID {os.getpid()}] {msg}\n")
52
+ except Exception:
53
+ pass
54
+
55
+ _debug_log(f"INVOKE: argv={sys.argv} cwd={os.getcwd()}")
56
+
43
57
  # --------------------------------------------------------------------------------------
44
58
  # Config
45
59
  # --------------------------------------------------------------------------------------
@@ -186,17 +200,30 @@ def term_width() -> int:
186
200
  # Host facts the payload does not carry
187
201
  # --------------------------------------------------------------------------------------
188
202
 
203
+ _GIT_CACHE_FILE = Path.home() / ".caddis" / ".git_status_cache.json"
204
+
205
+
189
206
  def git_facts(cwd: str) -> dict:
190
207
  """One `git status --porcelain=v2 --branch` call -> branch, staged/unstaged/untracked,
191
- ahead/behind. The v2 format gives all of it in a single invocation, which is why the
192
- old bash line dropped its two-calls-plus-grep-plus-awk approach."""
208
+ ahead/behind. Uses a 2-second cache file per cwd to eliminate git process thrashing on rapid
209
+ status updates."""
193
210
  out = {"branch": "", "staged": 0, "unstaged": 0, "untracked": 0, "ahead": 0, "behind": 0}
194
211
  if not cwd or not os.path.isdir(cwd):
195
212
  return out
213
+
214
+ now = time.time()
215
+ try:
216
+ if _GIT_CACHE_FILE.exists():
217
+ cached = json.loads(_GIT_CACHE_FILE.read_text(encoding="utf-8"))
218
+ if cached.get("cwd") == cwd and (now - cached.get("time", 0)) < 2.0:
219
+ return cached.get("facts", out)
220
+ except Exception:
221
+ pass
222
+
196
223
  try:
197
224
  proc = subprocess.run(
198
225
  ["git", "-C", cwd, "--no-optional-locks", "status", "--porcelain=v2", "--branch"],
199
- stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=1.0,
226
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=0.8,
200
227
  )
201
228
  text = proc.stdout.decode("utf-8", "replace")
202
229
  except Exception:
@@ -223,6 +250,12 @@ def git_facts(cwd: str) -> dict:
223
250
  out["unstaged"] += 1
224
251
  elif line.startswith("? "):
225
252
  out["untracked"] += 1
253
+
254
+ try:
255
+ _GIT_CACHE_FILE.write_text(json.dumps({"cwd": cwd, "time": now, "facts": out}), encoding="utf-8")
256
+ except Exception:
257
+ pass
258
+
226
259
  return out
227
260
 
228
261
 
@@ -722,7 +755,19 @@ def wire_host(host: str, settings_path: Path, command: str, dry_run: bool) -> in
722
755
  if host == "agy":
723
756
  entry["enabled"] = True # agy ignores a statusLine block without this
724
757
 
725
- if data.get("statusLine") == entry:
758
+ changed = False
759
+ if data.get("statusLine") != entry:
760
+ data["statusLine"] = entry
761
+ changed = True
762
+
763
+ if host == "agy":
764
+ title_cmd = command.replace("--profile agy", "--title")
765
+ title_entry = {"type": "command", "command": title_cmd, "enabled": True}
766
+ if data.get("title") != title_entry:
767
+ data["title"] = title_entry
768
+ changed = True
769
+
770
+ if not changed:
726
771
  print(" ok %-6s already wired" % host)
727
772
  return 0
728
773
 
@@ -730,7 +775,6 @@ def wire_host(host: str, settings_path: Path, command: str, dry_run: bool) -> in
730
775
  if settings_path.exists():
731
776
  backup = settings_path.with_suffix(settings_path.suffix + ".bak-caddis-statusline")
732
777
  shutil.copyfile(str(settings_path), str(backup))
733
- data["statusLine"] = entry
734
778
  settings_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
735
779
  print(" ok %-6s wired -> %s" % (host, settings_path))
736
780
  return 0
@@ -761,6 +805,12 @@ def check() -> int:
761
805
  else:
762
806
  print(" !! %-6s points elsewhere: %s" % (host, cmd))
763
807
  bad += 1
808
+ if host == "agy":
809
+ t_entry = data.get("title")
810
+ if isinstance(t_entry, dict) and "statusline.py" in str(t_entry.get("command", "")) and "--title" in str(t_entry.get("command", "")):
811
+ print(" ok agy-title %s" % t_entry.get("command"))
812
+ else:
813
+ print(" -- agy-title not wired to statusline.py (run --install to wire)")
764
814
  return bad
765
815
 
766
816
 
@@ -815,6 +865,7 @@ def main(argv=None) -> int:
815
865
  parser.add_argument("--sample", action="store_true", help="render a fake payload (preview / test)")
816
866
  parser.add_argument("--dry-run", action="store_true", help="with --install: print, write nothing")
817
867
  parser.add_argument("--no-color", action="store_true", help="render without ANSI colour")
868
+ parser.add_argument("--title", action="store_true", help="render terminal title (agy)")
818
869
  args = parser.parse_args(argv)
819
870
 
820
871
  # The line is Unicode. On Windows the default cp1252 stdout raises on ◆/⎇/█.
@@ -827,6 +878,21 @@ def main(argv=None) -> int:
827
878
  except Exception:
828
879
  pass
829
880
 
881
+ if args.title:
882
+ cwd = os.getcwd()
883
+ repo = os.path.basename(cwd) or cwd
884
+ facts = git_facts(cwd)
885
+ branch = facts.get("branch", "")
886
+ if branch:
887
+ print(f"agy: {repo} [{branch}]")
888
+ else:
889
+ print(f"agy: {repo}")
890
+ try:
891
+ sys.stdout.flush()
892
+ except Exception:
893
+ pass
894
+ return 0
895
+
830
896
  if args.install:
831
897
  hosts = ["claude", "agy"] if args.host == "both" else [args.host]
832
898
  return 1 if install(hosts, args.dry_run) else 0
@@ -838,31 +904,39 @@ def main(argv=None) -> int:
838
904
  if args.no_color:
839
905
  cfg["color"] = False
840
906
 
907
+ t0 = time.time()
841
908
  if args.sample:
842
909
  data = SAMPLES[args.profile]
843
910
  else:
844
911
  try:
845
912
  raw = sys.stdin.read()
846
- except Exception:
913
+ except Exception as e:
914
+ _debug_log(f"stdin.read exception: {e}")
847
915
  raw = ""
916
+ _debug_log(f"stdin read: {len(raw)} bytes in {time.time()-t0:.3f}s")
848
917
  start = raw.find("{") # strip a UTF-8 BOM or any leading noise
849
918
  try:
850
919
  data = json.loads(raw[start:]) if start >= 0 else {}
851
- except Exception:
920
+ except Exception as e:
921
+ _debug_log(f"json.loads exception: {e}")
852
922
  data = {}
853
923
  if not isinstance(data, dict):
854
924
  data = {}
855
925
 
856
926
  try:
857
927
  line = RENDERERS[args.profile](data, cfg)
858
- except Exception:
928
+ _debug_log(f"Rendered in {time.time()-t0:.3f}s")
929
+ except Exception as e:
930
+ _debug_log(f"Renderer exception: {traceback.format_exc()}")
859
931
  # A status line must never break the host. Degrade to the directory name.
860
932
  line = os.path.basename(os.getcwd())
861
933
 
862
934
  try:
863
935
  print(line)
864
936
  sys.stdout.flush()
865
- except (BrokenPipeError, OSError):
937
+ _debug_log(f"Printed and flushed in {time.time()-t0:.3f}s")
938
+ except (BrokenPipeError, OSError) as e:
939
+ _debug_log(f"BrokenPipeError on flush: {e}")
866
940
  # Parent process cancelled or closed stdout pipe (e.g. agy runner on user prompt/state switch).
867
941
  # Redirecting to devnull prevents Windows Python from throwing unhandled TextIOWrapper errors on exit.
868
942
  try:
@@ -871,7 +945,8 @@ def main(argv=None) -> int:
871
945
  except Exception:
872
946
  pass
873
947
  return 0
874
- except Exception:
948
+ except Exception as e:
949
+ _debug_log(f"Flush exception: {e}")
875
950
  return 0
876
951
 
877
952
  return 0
@@ -880,10 +955,12 @@ def main(argv=None) -> int:
880
955
  if __name__ == "__main__":
881
956
  try:
882
957
  code = main()
883
- except BaseException:
958
+ except BaseException as e:
959
+ _debug_log(f"Unhandled exception in main: {traceback.format_exc()}")
884
960
  code = 0
885
961
  try:
886
962
  sys.stdout.flush()
887
- except Exception:
888
- pass
889
- sys.exit(code)
963
+ except Exception as e:
964
+ _debug_log(f"Final flush exception: {e}")
965
+ _debug_log(f"EXIT with code {code}")
966
+ os._exit(code)
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.109",
3
+ "version": "1.3.111",
4
4
  "description": "caddis — agent-agnostic dev harness as an Antigravity (agy) plugin: TDD skills + commands, single-sourced with the Claude Code plugin."
5
5
  }