Skip to content

Commit 69c2a38

Browse files
fix: address PR review — shared CLI parser, reloader tied to --debug
1 parent 6a2b285 commit 69c2a38

3 files changed

Lines changed: 51 additions & 32 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ python app.py --base-dir /path/to/claude/projects
6363
```
6464

6565
> **Security warning:** Do not use `--host 0.0.0.0` together with `--debug` on untrusted networks.
66-
> That combination exposes Werkzeug's interactive debugger, which allows arbitrary code execution
67-
> from any browser that can reach the server. For typical local browsing, keep the default
68-
> `--host 127.0.0.1` and omit `--debug`.
66+
> That combination exposes [Werkzeug's interactive debugger](https://werkzeug.palletsprojects.com/en/stable/debug/),
67+
> which allows arbitrary code execution from any browser that can reach the server.
68+
> For typical local browsing, keep the default `--host 127.0.0.1` and omit `--debug`.
6969
7070
### CLI Export
7171

app.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def index():
3535
return app
3636

3737

38-
if __name__ == "__main__":
38+
def build_cli_parser():
39+
"""CLI argument parser for ``python app.py`` (stdlib only; safe to import in tests)."""
3940
import argparse
4041

4142
parser = argparse.ArgumentParser(description="Claude Code Chat Browser")
@@ -55,13 +56,18 @@ def index():
5556
help="Path to exclusion rules file (sensitive sessions are omitted). "
5657
"If omitted, uses ~/.claude-code-chat-browser/exclusion-rules.txt if present.",
5758
)
58-
args = parser.parse_args()
59+
return parser
60+
61+
62+
if __name__ == "__main__":
63+
args = build_cli_parser().parse_args()
5964

6065
app = create_app(base_dir=args.base_dir, exclusion_rules_path=args.exclude_rules)
6166
print(f"Claude Code Chat Browser running at http://{args.host}:{args.port}")
67+
# Reloader follows --debug on Unix only (Werkzeug file watcher, not the interactive debugger).
6268
app.run(
6369
host=args.host,
6470
port=args.port,
6571
debug=args.debug,
66-
use_reloader=(sys.platform != "win32"),
72+
use_reloader=args.debug and (sys.platform != "win32"),
6773
)

tests/test_cli_args.py

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
2222
sys.path.insert(0, REPO_ROOT)
2323

24+
from app import build_cli_parser
2425
from scripts.export import build_parser
2526

2627

@@ -61,6 +62,25 @@ def _debug_kwarg_uses_args(call: ast.Call) -> bool:
6162
return False
6263

6364

65+
def _ast_mentions_args_debug(node: ast.AST) -> bool:
66+
for child in ast.walk(node):
67+
if (
68+
isinstance(child, ast.Attribute)
69+
and child.attr == "debug"
70+
and isinstance(child.value, ast.Name)
71+
and child.value.id == "args"
72+
):
73+
return True
74+
return False
75+
76+
77+
def _use_reloader_kwarg_tied_to_debug(call: ast.Call) -> bool:
78+
for kw in call.keywords:
79+
if kw.arg == "use_reloader":
80+
return _ast_mentions_args_debug(kw.value)
81+
return False
82+
83+
6484
# ---------------------------------------------------------------------------
6585
# export.py argument tests
6686
# ---------------------------------------------------------------------------
@@ -252,37 +272,26 @@ def test_help_exits_zero(self):
252272
# ---------------------------------------------------------------------------
253273

254274
class TestAppArgparse:
255-
"""app.py __main__ block must expose the same flags as cursor's app.py."""
256-
257-
def _build_parser(self) -> argparse.ArgumentParser:
258-
"""Re-create the argparse parser from app.py without importing Flask."""
259-
parser = argparse.ArgumentParser(description="Claude Code Chat Browser")
260-
parser.add_argument("--port", type=int, default=5000)
261-
parser.add_argument("--host", default="127.0.0.1")
262-
parser.add_argument("--debug", action="store_true", default=False)
263-
parser.add_argument("--base-dir", default=None)
264-
parser.add_argument("--exclude-rules", "-e", default=None,
265-
metavar="PATH", dest="exclude_rules")
266-
return parser
275+
"""app.py CLI must expose the same flags as cursor's app.py."""
267276

268277
def test_host_default_is_localhost(self):
269278
"""Default host must be 127.0.0.1 to match cursor which binds to localhost only."""
270-
parser = self._build_parser()
279+
parser = build_cli_parser()
271280
args = parser.parse_args([])
272281
assert args.host == "127.0.0.1"
273282

274283
def test_host_override(self):
275-
parser = self._build_parser()
284+
parser = build_cli_parser()
276285
args = parser.parse_args(["--host", "127.0.0.1"])
277286
assert args.host == "127.0.0.1"
278287

279288
def test_debug_default_is_false(self):
280-
parser = self._build_parser()
289+
parser = build_cli_parser()
281290
args = parser.parse_args([])
282291
assert args.debug is False
283292

284293
def test_debug_explicit_true(self):
285-
parser = self._build_parser()
294+
parser = build_cli_parser()
286295
args = parser.parse_args(["--debug"])
287296
assert args.debug is True
288297

@@ -297,38 +306,38 @@ def test_app_py_debug_not_hardcoded_true(self):
297306
assert any(_debug_kwarg_uses_args(c) for c in app_run_calls)
298307

299308
def test_port_default(self):
300-
parser = self._build_parser()
309+
parser = build_cli_parser()
301310
args = parser.parse_args([])
302311
assert args.port == 5000
303312

304313
def test_port_override(self):
305-
parser = self._build_parser()
314+
parser = build_cli_parser()
306315
args = parser.parse_args(["--port", "8080"])
307316
assert args.port == 8080
308317

309318
def test_base_dir_default_none(self):
310-
parser = self._build_parser()
319+
parser = build_cli_parser()
311320
args = parser.parse_args([])
312321
assert args.base_dir is None
313322

314323
def test_base_dir_override(self):
315-
parser = self._build_parser()
324+
parser = build_cli_parser()
316325
args = parser.parse_args(["--base-dir", "/tmp/projects"])
317326
assert args.base_dir == "/tmp/projects"
318327

319328
def test_exclude_rules_default_none(self):
320-
parser = self._build_parser()
329+
parser = build_cli_parser()
321330
args = parser.parse_args([])
322331
assert args.exclude_rules is None
323332

324333
def test_exclude_rules_long_form(self):
325-
parser = self._build_parser()
334+
parser = build_cli_parser()
326335
args = parser.parse_args(["--exclude-rules", "/tmp/rules.txt"])
327336
assert args.exclude_rules == "/tmp/rules.txt"
328337

329338
def test_exclude_rules_short_form(self):
330339
"""Cursor's app.py uses -e as the short form; claude must too."""
331-
parser = self._build_parser()
340+
parser = build_cli_parser()
332341
args = parser.parse_args(["-e", "/tmp/rules.txt"])
333342
assert args.exclude_rules == "/tmp/rules.txt"
334343

@@ -353,11 +362,15 @@ def test_app_py_host_default_is_localhost(self):
353362
assert '"127.0.0.1"' in src
354363

355364
def test_app_py_use_reloader_is_platform_aware(self):
356-
"""use_reloader must depend on sys.platform, not be hardcoded False."""
365+
"""use_reloader must be opt-in via --debug and gated on non-Windows platforms."""
357366
app_path = os.path.join(REPO_ROOT, "app.py")
358-
with open(app_path, "r", encoding="utf-8") as f:
367+
with open(app_path, encoding="utf-8") as f:
368+
tree = ast.parse(f.read(), filename=app_path)
369+
app_run_calls = [n for n in ast.walk(tree) if _is_app_run_call(n)]
370+
assert app_run_calls
371+
assert all(_use_reloader_kwarg_tied_to_debug(c) for c in app_run_calls)
372+
with open(app_path, encoding="utf-8") as f:
359373
src = f.read()
360374
assert "sys.platform" in src
361375
assert "win32" in src
362-
# Must NOT have unconditional use_reloader=False
363376
assert "use_reloader=False" not in src

0 commit comments

Comments
 (0)