Skip to content

Commit e943de1

Browse files
claudesimonw
authored andcommitted
Add -a/--output-auto flag to all commands
This flag creates output in a subdirectory named after: - The session ID for web command - The file stem for json and local commands Uses -o as parent directory if specified, otherwise current directory. When -a is used, auto-open browser is disabled.
1 parent d2b429c commit e943de1

File tree

2 files changed

+242
-31
lines changed

2 files changed

+242
-31
lines changed

src/claude_code_publish/__init__.py

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,7 +1082,13 @@ def cli():
10821082
"-o",
10831083
"--output",
10841084
type=click.Path(),
1085-
help="Output directory (default: temp dir, or '.' with -o .)",
1085+
help="Output directory. If not specified, writes to temp dir and opens in browser.",
1086+
)
1087+
@click.option(
1088+
"-a",
1089+
"--output-auto",
1090+
is_flag=True,
1091+
help="Auto-name output subdirectory based on session filename (uses -o as parent, or current dir).",
10861092
)
10871093
@click.option(
10881094
"--repo",
@@ -1103,14 +1109,14 @@ def cli():
11031109
"--open",
11041110
"open_browser",
11051111
is_flag=True,
1106-
help="Open the generated index.html in your default browser.",
1112+
help="Open the generated index.html in your default browser (default if no -o specified).",
11071113
)
11081114
@click.option(
11091115
"--limit",
11101116
default=10,
11111117
help="Maximum number of sessions to show (default: 10)",
11121118
)
1113-
def local_cmd(output, repo, gist, include_json, open_browser, limit):
1119+
def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit):
11141120
"""Select and convert a local Claude Code session to HTML."""
11151121
from datetime import datetime
11161122

@@ -1152,15 +1158,22 @@ def local_cmd(output, repo, gist, include_json, open_browser, limit):
11521158

11531159
session_file = selected
11541160

1155-
# Determine output directory
1156-
if (gist or open_browser) and output is None:
1157-
output = Path(tempfile.gettempdir()) / session_file.stem
1161+
# Determine output directory and whether to open browser
1162+
# If no -o specified, use temp dir and open browser by default
1163+
auto_open = output is None and not gist and not output_auto
1164+
if output_auto:
1165+
# Use -o as parent dir (or current dir), with auto-named subdirectory
1166+
parent_dir = Path(output) if output else Path(".")
1167+
output = parent_dir / session_file.stem
11581168
elif output is None:
1159-
output = Path(tempfile.gettempdir()) / session_file.stem
1169+
output = Path(tempfile.gettempdir()) / f"claude-session-{session_file.stem}"
11601170

11611171
output = Path(output)
11621172
generate_html(session_file, output, github_repo=repo)
11631173

1174+
# Show output directory
1175+
click.echo(f"Output: {output.resolve()}")
1176+
11641177
# Copy JSONL file to output directory if requested
11651178
if include_json:
11661179
output.mkdir(exist_ok=True)
@@ -1177,9 +1190,8 @@ def local_cmd(output, repo, gist, include_json, open_browser, limit):
11771190
preview_url = f"https://gistpreview.github.io/?{gist_id}/index.html"
11781191
click.echo(f"Gist: {gist_url}")
11791192
click.echo(f"Preview: {preview_url}")
1180-
click.echo(f"Files: {output}")
11811193

1182-
if open_browser:
1194+
if open_browser or auto_open:
11831195
index_url = (output / "index.html").resolve().as_uri()
11841196
webbrowser.open(index_url)
11851197

@@ -1190,7 +1202,13 @@ def local_cmd(output, repo, gist, include_json, open_browser, limit):
11901202
"-o",
11911203
"--output",
11921204
type=click.Path(),
1193-
help="Output directory (default: current directory, or temp dir with --gist/--open)",
1205+
help="Output directory. If not specified, writes to temp dir and opens in browser.",
1206+
)
1207+
@click.option(
1208+
"-a",
1209+
"--output-auto",
1210+
is_flag=True,
1211+
help="Auto-name output subdirectory based on filename (uses -o as parent, or current dir).",
11941212
)
11951213
@click.option(
11961214
"--repo",
@@ -1211,23 +1229,26 @@ def local_cmd(output, repo, gist, include_json, open_browser, limit):
12111229
"--open",
12121230
"open_browser",
12131231
is_flag=True,
1214-
help="Open the generated index.html in your default browser.",
1232+
help="Open the generated index.html in your default browser (default if no -o specified).",
12151233
)
1216-
def json_cmd(json_file, output, repo, gist, include_json, open_browser):
1234+
def json_cmd(json_file, output, output_auto, repo, gist, include_json, open_browser):
12171235
"""Convert a Claude Code session JSON/JSONL file to HTML."""
1218-
# Determine output directory
1219-
if (gist or open_browser) and output is None:
1220-
# Extract session ID from JSON file for temp directory name
1221-
with open(json_file, "r") as f:
1222-
data = json.load(f)
1223-
session_id = data.get("sessionId", Path(json_file).stem)
1224-
output = Path(tempfile.gettempdir()) / session_id
1236+
# Determine output directory and whether to open browser
1237+
# If no -o specified, use temp dir and open browser by default
1238+
auto_open = output is None and not gist and not output_auto
1239+
if output_auto:
1240+
# Use -o as parent dir (or current dir), with auto-named subdirectory
1241+
parent_dir = Path(output) if output else Path(".")
1242+
output = parent_dir / Path(json_file).stem
12251243
elif output is None:
1226-
output = "."
1244+
output = Path(tempfile.gettempdir()) / f"claude-session-{Path(json_file).stem}"
12271245

12281246
output = Path(output)
12291247
generate_html(json_file, output, github_repo=repo)
12301248

1249+
# Show output directory
1250+
click.echo(f"Output: {output.resolve()}")
1251+
12311252
# Copy JSON file to output directory if requested
12321253
if include_json:
12331254
output.mkdir(exist_ok=True)
@@ -1245,9 +1266,8 @@ def json_cmd(json_file, output, repo, gist, include_json, open_browser):
12451266
preview_url = f"https://gistpreview.github.io/?{gist_id}/index.html"
12461267
click.echo(f"Gist: {gist_url}")
12471268
click.echo(f"Preview: {preview_url}")
1248-
click.echo(f"Files: {output}")
12491269

1250-
if open_browser:
1270+
if open_browser or auto_open:
12511271
index_url = (output / "index.html").resolve().as_uri()
12521272
webbrowser.open(index_url)
12531273

@@ -1491,7 +1511,13 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
14911511
"-o",
14921512
"--output",
14931513
type=click.Path(),
1494-
help="Output directory (default: creates folder with session ID, or temp dir with --gist/--open)",
1514+
help="Output directory. If not specified, writes to temp dir and opens in browser.",
1515+
)
1516+
@click.option(
1517+
"-a",
1518+
"--output-auto",
1519+
is_flag=True,
1520+
help="Auto-name output subdirectory based on session ID (uses -o as parent, or current dir).",
14951521
)
14961522
@click.option("--token", help="API access token (auto-detected from keychain on macOS)")
14971523
@click.option(
@@ -1516,10 +1542,18 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
15161542
"--open",
15171543
"open_browser",
15181544
is_flag=True,
1519-
help="Open the generated index.html in your default browser.",
1545+
help="Open the generated index.html in your default browser (default if no -o specified).",
15201546
)
15211547
def web_cmd(
1522-
session_id, output, token, org_uuid, repo, gist, include_json, open_browser
1548+
session_id,
1549+
output,
1550+
output_auto,
1551+
token,
1552+
org_uuid,
1553+
repo,
1554+
gist,
1555+
include_json,
1556+
open_browser,
15231557
):
15241558
"""Select and convert a web session from the Claude API to HTML.
15251559
@@ -1579,16 +1613,23 @@ def web_cmd(
15791613
except httpx.RequestError as e:
15801614
raise click.ClickException(f"Network error: {e}")
15811615

1582-
# Determine output directory
1583-
if (gist or open_browser) and output is None:
1584-
output = Path(tempfile.gettempdir()) / session_id
1616+
# Determine output directory and whether to open browser
1617+
# If no -o specified, use temp dir and open browser by default
1618+
auto_open = output is None and not gist and not output_auto
1619+
if output_auto:
1620+
# Use -o as parent dir (or current dir), with auto-named subdirectory
1621+
parent_dir = Path(output) if output else Path(".")
1622+
output = parent_dir / session_id
15851623
elif output is None:
1586-
output = session_id
1624+
output = Path(tempfile.gettempdir()) / f"claude-session-{session_id}"
15871625

15881626
output = Path(output)
15891627
click.echo(f"Generating HTML in {output}/...")
15901628
generate_html_from_session_data(session_data, output, github_repo=repo)
15911629

1630+
# Show output directory
1631+
click.echo(f"Output: {output.resolve()}")
1632+
15921633
# Save JSON session data if requested
15931634
if include_json:
15941635
output.mkdir(exist_ok=True)
@@ -1606,9 +1647,8 @@ def web_cmd(
16061647
preview_url = f"https://gistpreview.github.io/?{gist_id}/index.html"
16071648
click.echo(f"Gist: {gist_url}")
16081649
click.echo(f"Preview: {preview_url}")
1609-
click.echo(f"Files: {output}")
16101650

1611-
if open_browser:
1651+
if open_browser or auto_open:
16121652
index_url = (output / "index.html").resolve().as_uri()
16131653
webbrowser.open(index_url)
16141654

tests/test_generate_html.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,3 +1192,174 @@ def ask(self):
11921192

11931193
assert result.exit_code == 0
11941194
assert "No session selected" in result.output
1195+
1196+
1197+
class TestOutputAutoOption:
1198+
"""Tests for the -a/--output-auto flag."""
1199+
1200+
def test_json_output_auto_creates_subdirectory(self, tmp_path):
1201+
"""Test that json -a creates output subdirectory named after file stem."""
1202+
from click.testing import CliRunner
1203+
from claude_code_publish import cli
1204+
1205+
fixture_path = Path(__file__).parent / "sample_session.json"
1206+
1207+
runner = CliRunner()
1208+
result = runner.invoke(
1209+
cli,
1210+
["json", str(fixture_path), "-a", "-o", str(tmp_path)],
1211+
)
1212+
1213+
assert result.exit_code == 0
1214+
# Output should be in tmp_path/sample_session/
1215+
expected_dir = tmp_path / "sample_session"
1216+
assert expected_dir.exists()
1217+
assert (expected_dir / "index.html").exists()
1218+
1219+
def test_json_output_auto_uses_cwd_when_no_output(self, tmp_path, monkeypatch):
1220+
"""Test that json -a uses current directory when -o not specified."""
1221+
from click.testing import CliRunner
1222+
from claude_code_publish import cli
1223+
import os
1224+
1225+
fixture_path = Path(__file__).parent / "sample_session.json"
1226+
1227+
# Change to tmp_path
1228+
monkeypatch.chdir(tmp_path)
1229+
1230+
runner = CliRunner()
1231+
result = runner.invoke(
1232+
cli,
1233+
["json", str(fixture_path), "-a"],
1234+
)
1235+
1236+
assert result.exit_code == 0
1237+
# Output should be in ./sample_session/
1238+
expected_dir = tmp_path / "sample_session"
1239+
assert expected_dir.exists()
1240+
assert (expected_dir / "index.html").exists()
1241+
1242+
def test_json_output_auto_no_browser_open(self, tmp_path, monkeypatch):
1243+
"""Test that json -a does not auto-open browser."""
1244+
from click.testing import CliRunner
1245+
from claude_code_publish import cli
1246+
1247+
fixture_path = Path(__file__).parent / "sample_session.json"
1248+
1249+
# Track webbrowser.open calls
1250+
opened_urls = []
1251+
1252+
def mock_open(url):
1253+
opened_urls.append(url)
1254+
return True
1255+
1256+
monkeypatch.setattr("claude_code_publish.webbrowser.open", mock_open)
1257+
1258+
runner = CliRunner()
1259+
result = runner.invoke(
1260+
cli,
1261+
["json", str(fixture_path), "-a", "-o", str(tmp_path)],
1262+
)
1263+
1264+
assert result.exit_code == 0
1265+
assert len(opened_urls) == 0 # No browser opened
1266+
1267+
def test_local_output_auto_creates_subdirectory(self, tmp_path, monkeypatch):
1268+
"""Test that local -a creates output subdirectory named after file stem."""
1269+
from click.testing import CliRunner
1270+
from claude_code_publish import cli
1271+
import questionary
1272+
1273+
# Create mock .claude/projects structure
1274+
projects_dir = tmp_path / ".claude" / "projects" / "test-project"
1275+
projects_dir.mkdir(parents=True)
1276+
1277+
session_file = projects_dir / "my-session-file.jsonl"
1278+
session_file.write_text(
1279+
'{"type":"summary","summary":"Test local session"}\n'
1280+
'{"type":"user","timestamp":"2025-01-01T00:00:00Z","message":{"role":"user","content":"Hello"}}\n'
1281+
)
1282+
1283+
output_parent = tmp_path / "output"
1284+
output_parent.mkdir()
1285+
1286+
# Mock Path.home() to return our tmp_path
1287+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
1288+
1289+
# Mock questionary.select to return the session file
1290+
class MockSelect:
1291+
def __init__(self, *args, **kwargs):
1292+
pass
1293+
1294+
def ask(self):
1295+
return session_file
1296+
1297+
monkeypatch.setattr(questionary, "select", MockSelect)
1298+
1299+
runner = CliRunner()
1300+
result = runner.invoke(cli, ["local", "-a", "-o", str(output_parent)])
1301+
1302+
assert result.exit_code == 0
1303+
# Output should be in output_parent/my-session-file/
1304+
expected_dir = output_parent / "my-session-file"
1305+
assert expected_dir.exists()
1306+
assert (expected_dir / "index.html").exists()
1307+
1308+
def test_web_output_auto_creates_subdirectory(self, httpx_mock, tmp_path):
1309+
"""Test that web -a creates output subdirectory named after session ID."""
1310+
from click.testing import CliRunner
1311+
from claude_code_publish import cli
1312+
1313+
# Load sample session to mock API response
1314+
fixture_path = Path(__file__).parent / "sample_session.json"
1315+
with open(fixture_path) as f:
1316+
session_data = json.load(f)
1317+
1318+
httpx_mock.add_response(
1319+
url="https://api.anthropic.com/v1/session_ingress/session/my-web-session-id",
1320+
json=session_data,
1321+
)
1322+
1323+
runner = CliRunner()
1324+
result = runner.invoke(
1325+
cli,
1326+
[
1327+
"web",
1328+
"my-web-session-id",
1329+
"--token",
1330+
"test-token",
1331+
"--org-uuid",
1332+
"test-org",
1333+
"-a",
1334+
"-o",
1335+
str(tmp_path),
1336+
],
1337+
)
1338+
1339+
assert result.exit_code == 0
1340+
# Output should be in tmp_path/my-web-session-id/
1341+
expected_dir = tmp_path / "my-web-session-id"
1342+
assert expected_dir.exists()
1343+
assert (expected_dir / "index.html").exists()
1344+
1345+
def test_output_auto_with_jsonl_uses_stem(self, tmp_path, monkeypatch):
1346+
"""Test that -a with JSONL file uses file stem (without .jsonl extension)."""
1347+
from click.testing import CliRunner
1348+
from claude_code_publish import cli
1349+
1350+
# Create a JSONL file
1351+
fixture_path = Path(__file__).parent / "sample_session.jsonl"
1352+
1353+
monkeypatch.chdir(tmp_path)
1354+
1355+
runner = CliRunner()
1356+
result = runner.invoke(
1357+
cli,
1358+
["json", str(fixture_path), "-a"],
1359+
)
1360+
1361+
assert result.exit_code == 0
1362+
# Output should be in ./sample_session/ (not ./sample_session.jsonl/)
1363+
expected_dir = tmp_path / "sample_session"
1364+
assert expected_dir.exists()
1365+
assert (expected_dir / "index.html").exists()

0 commit comments

Comments
 (0)