Skip to content

Commit 72d8b67

Browse files
zackman0010claude
andcommitted
Extract resolve_output, publish_gist, open_in_browser into commands/__init__.py
Eliminates identical output-resolution, gist-publishing, and browser-opening blocks duplicated across local, file, and web commands. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e94b12d commit 72d8b67

File tree

5 files changed

+178
-70
lines changed

5 files changed

+178
-70
lines changed

src/claude_code_transcripts/commands/__init__.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,60 @@
11
"""Shared Click option decorators and utilities for commands."""
22

33
import shutil
4+
import tempfile
5+
import webbrowser
46
from pathlib import Path
57

68
import click
79

8-
from claude_code_transcripts.html_generation import Project, Session
10+
from claude_code_transcripts.html_generation import (
11+
Project,
12+
Session,
13+
create_gist,
14+
inject_gist_preview_js,
15+
)
916
from claude_code_transcripts.parser import parse_session_file
1017
from claude_code_transcripts.sessions import find_all_sessions, find_cowork_sessions
1118

1219

20+
def resolve_output(output, output_auto, gist, stem):
21+
"""Resolve the output directory and whether to auto-open the browser.
22+
23+
Args:
24+
output: Explicit output path string, or None.
25+
output_auto: Whether to auto-name the output dir using stem.
26+
gist: Whether the gist flag is set (suppresses auto_open).
27+
stem: Filename stem used for default naming.
28+
29+
Returns:
30+
(output_path, auto_open) tuple.
31+
"""
32+
auto_open = output is None and not gist and not output_auto
33+
if output_auto:
34+
parent_dir = Path(output) if output else Path(".")
35+
output = parent_dir / stem
36+
elif output is None:
37+
output = Path(tempfile.gettempdir()) / f"claude-session-{stem}"
38+
return Path(output), auto_open
39+
40+
41+
def publish_gist(output):
42+
"""Inject gist preview JS, upload to GitHub Gist, and print URLs."""
43+
inject_gist_preview_js(output)
44+
click.echo("Creating GitHub gist...")
45+
gist_id, gist_url = create_gist(output)
46+
preview_url = f"https://gisthost.github.io/?{gist_id}/index.html"
47+
click.echo(f"Gist: {gist_url}")
48+
click.echo(f"Preview: {preview_url}")
49+
50+
51+
def open_in_browser(output, open_browser, auto_open):
52+
"""Open output/index.html in the default browser if requested."""
53+
if open_browser or auto_open:
54+
index_url = (output / "index.html").resolve().as_uri()
55+
webbrowser.open(index_url)
56+
57+
1358
def collect_raw_projects(source, include_agents=False):
1459
"""Discover raw project dicts from the given source.
1560

src/claude_code_transcripts/commands/file.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import shutil
22
import tempfile
3-
import webbrowser
43
from pathlib import Path
54

65
import click
76
import httpx
87

98
from claude_code_transcripts.cli import cli
10-
from claude_code_transcripts.commands import interactive_options
9+
from claude_code_transcripts.commands import (
10+
interactive_options,
11+
open_in_browser,
12+
publish_gist,
13+
resolve_output,
14+
)
1115

1216
from claude_code_transcripts.html_generation import (
13-
create_gist,
1417
generate_html,
15-
inject_gist_preview_js,
1618
)
1719
from claude_code_transcripts.parser import parse_session_file
1820

@@ -70,17 +72,9 @@ def file_cmd(json_file, output, output_auto, repo, gist, include_json, open_brow
7072
raise click.ClickException(f"File not found: {json_file}")
7173
url_name = None
7274

73-
auto_open = output is None and not gist and not output_auto
74-
if output_auto:
75-
parent_dir = Path(output) if output else Path(".")
76-
output = parent_dir / (url_name or json_file_path.stem)
77-
elif output is None:
78-
output = (
79-
Path(tempfile.gettempdir())
80-
/ f"claude-session-{url_name or json_file_path.stem}"
81-
)
75+
stem = url_name or json_file_path.stem
76+
output, auto_open = resolve_output(output, output_auto, gist, stem)
8277

83-
output = Path(output)
8478
generate_html(parse_session_file(json_file_path), output, github_repo=repo)
8579

8680
click.echo(f"Output: {output.resolve()}")
@@ -93,13 +87,6 @@ def file_cmd(json_file, output, output_auto, repo, gist, include_json, open_brow
9387
click.echo(f"JSON: {json_dest} ({json_size_kb:.1f} KB)")
9488

9589
if gist:
96-
inject_gist_preview_js(output)
97-
click.echo("Creating GitHub gist...")
98-
gist_id, gist_url = create_gist(output)
99-
preview_url = f"https://gisthost.github.io/?{gist_id}/index.html"
100-
click.echo(f"Gist: {gist_url}")
101-
click.echo(f"Preview: {preview_url}")
102-
103-
if open_browser or auto_open:
104-
index_url = (output / "index.html").resolve().as_uri()
105-
webbrowser.open(index_url)
90+
publish_gist(output)
91+
92+
open_in_browser(output, open_browser, auto_open)

src/claude_code_transcripts/commands/local.py

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import shutil
2-
import tempfile
3-
import webbrowser
42
from datetime import datetime
53
from pathlib import Path
64

75
import click
86
import questionary
97

108
from claude_code_transcripts.cli import cli
11-
from claude_code_transcripts.commands import local_options, source_option
9+
from claude_code_transcripts.commands import (
10+
local_options,
11+
open_in_browser,
12+
publish_gist,
13+
resolve_output,
14+
source_option,
15+
)
1216

1317
from claude_code_transcripts.html_generation import (
14-
create_gist,
1518
generate_html,
16-
inject_gist_preview_js,
1719
)
1820
from claude_code_transcripts.parser import parse_session_file
1921
from claude_code_transcripts.sessions import find_cowork_sessions, find_local_sessions
@@ -100,14 +102,8 @@ def local_cmd(
100102
session_file = selected["session_file"]
101103
transcript_label = selected["transcript_label"]
102104

103-
auto_open = output is None and not gist and not output_auto
104-
if output_auto:
105-
parent_dir = Path(output) if output else Path(".")
106-
output = parent_dir / session_file.stem
107-
elif output is None:
108-
output = Path(tempfile.gettempdir()) / f"claude-session-{session_file.stem}"
105+
output, auto_open = resolve_output(output, output_auto, gist, session_file.stem)
109106

110-
output = Path(output)
111107
generate_html(
112108
parse_session_file(session_file),
113109
output,
@@ -125,13 +121,6 @@ def local_cmd(
125121
click.echo(f"JSONL: {json_dest} ({json_size_kb:.1f} KB)")
126122

127123
if gist:
128-
inject_gist_preview_js(output)
129-
click.echo("Creating GitHub gist...")
130-
gist_id, gist_url = create_gist(output)
131-
preview_url = f"https://gisthost.github.io/?{gist_id}/index.html"
132-
click.echo(f"Gist: {gist_url}")
133-
click.echo(f"Preview: {preview_url}")
134-
135-
if open_browser or auto_open:
136-
index_url = (output / "index.html").resolve().as_uri()
137-
webbrowser.open(index_url)
124+
publish_gist(output)
125+
126+
open_in_browser(output, open_browser, auto_open)

src/claude_code_transcripts/commands/web.py

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import json
22
import platform
3-
import tempfile
4-
import webbrowser
5-
from pathlib import Path
63

74
import click
85
import httpx
96
import questionary
107

118
from claude_code_transcripts.cli import cli
12-
from claude_code_transcripts.commands import interactive_options
9+
from claude_code_transcripts.commands import (
10+
interactive_options,
11+
open_in_browser,
12+
publish_gist,
13+
resolve_output,
14+
)
1315

1416
from claude_code_transcripts.api import (
1517
enrich_sessions_with_repos,
@@ -20,9 +22,7 @@
2022
get_org_uuid_from_config,
2123
)
2224
from claude_code_transcripts.html_generation import (
23-
create_gist,
2425
generate_html,
25-
inject_gist_preview_js,
2626
)
2727

2828

@@ -146,14 +146,8 @@ def web_cmd(
146146
except httpx.RequestError as e:
147147
raise click.ClickException(f"Network error: {e}")
148148

149-
auto_open = output is None and not gist and not output_auto
150-
if output_auto:
151-
parent_dir = Path(output) if output else Path(".")
152-
output = parent_dir / session_id
153-
elif output is None:
154-
output = Path(tempfile.gettempdir()) / f"claude-session-{session_id}"
149+
output, auto_open = resolve_output(output, output_auto, gist, session_id)
155150

156-
output = Path(output)
157151
click.echo(f"Generating HTML in {output}/...")
158152
generate_html(session_data.get("loglines", []), output, github_repo=repo)
159153

@@ -168,13 +162,6 @@ def web_cmd(
168162
click.echo(f"JSON: {json_dest} ({json_size_kb:.1f} KB)")
169163

170164
if gist:
171-
inject_gist_preview_js(output)
172-
click.echo("Creating GitHub gist...")
173-
gist_id, gist_url = create_gist(output)
174-
preview_url = f"https://gisthost.github.io/?{gist_id}/index.html"
175-
click.echo(f"Gist: {gist_url}")
176-
click.echo(f"Preview: {preview_url}")
177-
178-
if open_browser or auto_open:
179-
index_url = (output / "index.html").resolve().as_uri()
180-
webbrowser.open(index_url)
165+
publish_gist(output)
166+
167+
open_in_browser(output, open_browser, auto_open)

tests/test_commands_output.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Tests for shared output utilities in commands/__init__.py."""
2+
3+
import tempfile
4+
import webbrowser
5+
from pathlib import Path
6+
from unittest.mock import patch, MagicMock
7+
8+
import pytest
9+
10+
from claude_code_transcripts.commands import (
11+
open_in_browser,
12+
publish_gist,
13+
resolve_output,
14+
)
15+
16+
17+
class TestResolveOutput:
18+
def test_explicit_output_returned_as_path(self):
19+
output, auto_open = resolve_output("/tmp/mydir", False, False, "session123")
20+
assert output == Path("/tmp/mydir")
21+
assert auto_open is False
22+
23+
def test_output_auto_uses_stem_under_cwd(self):
24+
output, auto_open = resolve_output(None, True, False, "session123")
25+
assert output == Path(".") / "session123"
26+
assert auto_open is False
27+
28+
def test_output_auto_with_explicit_parent(self):
29+
output, auto_open = resolve_output("/tmp/parent", True, False, "session123")
30+
assert output == Path("/tmp/parent") / "session123"
31+
assert auto_open is False
32+
33+
def test_no_output_uses_tempdir(self):
34+
output, auto_open = resolve_output(None, False, False, "session123")
35+
assert output == Path(tempfile.gettempdir()) / "claude-session-session123"
36+
assert auto_open is True
37+
38+
def test_no_output_with_gist_suppresses_auto_open(self):
39+
output, auto_open = resolve_output(None, False, True, "session123")
40+
assert auto_open is False
41+
42+
def test_no_output_with_output_auto_suppresses_auto_open(self):
43+
output, auto_open = resolve_output(None, True, False, "session123")
44+
assert auto_open is False
45+
46+
47+
class TestPublishGist:
48+
def test_calls_inject_and_create_gist(self, tmp_path):
49+
with (
50+
patch(
51+
"claude_code_transcripts.commands.inject_gist_preview_js"
52+
) as mock_inject,
53+
patch("claude_code_transcripts.commands.create_gist") as mock_create,
54+
):
55+
mock_create.return_value = ("abc123", "https://gist.github.com/abc123")
56+
publish_gist(tmp_path)
57+
58+
mock_inject.assert_called_once_with(tmp_path)
59+
mock_create.assert_called_once_with(tmp_path)
60+
61+
def test_prints_gist_and_preview_urls(self, tmp_path, capsys):
62+
with (
63+
patch("claude_code_transcripts.commands.inject_gist_preview_js"),
64+
patch("claude_code_transcripts.commands.create_gist") as mock_create,
65+
):
66+
mock_create.return_value = ("abc123", "https://gist.github.com/abc123")
67+
publish_gist(tmp_path)
68+
69+
out = capsys.readouterr().out
70+
assert "https://gist.github.com/abc123" in out
71+
assert "gisthost.github.io" in out
72+
assert "abc123" in out
73+
74+
75+
class TestOpenInBrowser:
76+
def test_opens_when_open_browser_true(self, tmp_path):
77+
(tmp_path / "index.html").write_text("<html/>")
78+
with patch("webbrowser.open") as mock_open:
79+
open_in_browser(tmp_path, open_browser=True, auto_open=False)
80+
mock_open.assert_called_once()
81+
assert "index.html" in mock_open.call_args[0][0]
82+
83+
def test_opens_when_auto_open_true(self, tmp_path):
84+
(tmp_path / "index.html").write_text("<html/>")
85+
with patch("webbrowser.open") as mock_open:
86+
open_in_browser(tmp_path, open_browser=False, auto_open=True)
87+
mock_open.assert_called_once()
88+
89+
def test_does_not_open_when_both_false(self, tmp_path):
90+
with patch("webbrowser.open") as mock_open:
91+
open_in_browser(tmp_path, open_browser=False, auto_open=False)
92+
mock_open.assert_not_called()
93+
94+
def test_url_points_to_index_html(self, tmp_path):
95+
(tmp_path / "index.html").write_text("<html/>")
96+
with patch("webbrowser.open") as mock_open:
97+
open_in_browser(tmp_path, open_browser=True, auto_open=False)
98+
url = mock_open.call_args[0][0]
99+
assert url.startswith("file://")
100+
assert url.endswith("index.html")

0 commit comments

Comments
 (0)