fix(cli): setup summary reflects actual CLAUDE.md write outcome (True/False return)
This commit is contained in:
parent
c0aa278c67
commit
297d07f2a1
2 changed files with 61 additions and 11 deletions
|
|
@ -301,11 +301,15 @@ def _init_claude_workspace(
|
||||||
workspace: Path,
|
workspace: Path,
|
||||||
server_url: str = "",
|
server_url: str = "",
|
||||||
token: str = "",
|
token: str = "",
|
||||||
) -> None:
|
) -> bool:
|
||||||
"""Initialise the .claude/ directory with placeholder files and hooks.
|
"""Initialise the .claude/ directory with placeholder files and hooks.
|
||||||
|
|
||||||
Writes CLAUDE.md from the server (GET /api/welcome) unless ``server_url``
|
Writes CLAUDE.md from the server (GET /api/welcome) unless ``server_url``
|
||||||
or ``token`` are empty, or the request fails (graceful degradation).
|
or ``token`` are empty, or the request fails (graceful degradation).
|
||||||
|
|
||||||
|
Returns True if CLAUDE.md was written from the server, False otherwise
|
||||||
|
(skipped because caller passed empty server_url/token, or fetch failed —
|
||||||
|
in the latter case a warning was already printed to stderr).
|
||||||
"""
|
"""
|
||||||
local_md = workspace / ".claude" / "CLAUDE.local.md"
|
local_md = workspace / ".claude" / "CLAUDE.local.md"
|
||||||
if not local_md.exists():
|
if not local_md.exists():
|
||||||
|
|
@ -326,15 +330,19 @@ def _init_claude_workspace(
|
||||||
|
|
||||||
# Write CLAUDE.md from the server
|
# Write CLAUDE.md from the server
|
||||||
if server_url and token:
|
if server_url and token:
|
||||||
_write_claude_md(workspace, server_url, token)
|
return _write_claude_md(workspace, server_url, token)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _write_claude_md(workspace: Path, server_url: str, token: str) -> None:
|
def _write_claude_md(workspace: Path, server_url: str, token: str) -> bool:
|
||||||
"""Fetch the rendered CLAUDE.md from the server and write it to the workspace.
|
"""Fetch the rendered CLAUDE.md from the server and write it to the workspace.
|
||||||
|
|
||||||
Gracefully handles:
|
Gracefully handles:
|
||||||
- 404: older server without the endpoint — skip with warning.
|
- 404: older server without the endpoint — skip with warning.
|
||||||
- Other HTTP errors / network errors — skip with warning.
|
- Other HTTP errors / network errors — skip with warning.
|
||||||
|
|
||||||
|
Returns True iff a non-empty CLAUDE.md was successfully written; False on
|
||||||
|
any skipped or failed path (the caller already saw a stderr warning).
|
||||||
"""
|
"""
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -353,27 +361,30 @@ def _write_claude_md(workspace: Path, server_url: str, token: str) -> None:
|
||||||
"Warning: server does not support CLAUDE.md generation (older version). Skipping.",
|
"Warning: server does not support CLAUDE.md generation (older version). Skipping.",
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
return
|
return False
|
||||||
if resp.status_code == 401 or resp.status_code == 403:
|
if resp.status_code == 401 or resp.status_code == 403:
|
||||||
typer.echo(
|
typer.echo(
|
||||||
f"Warning: CLAUDE.md fetch failed ({resp.status_code} {resp.reason_phrase}). Skipping.",
|
f"Warning: CLAUDE.md fetch failed ({resp.status_code} {resp.reason_phrase}). Skipping.",
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
return
|
return False
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
content = data.get("content", "")
|
content = data.get("content", "")
|
||||||
if content:
|
if content:
|
||||||
(workspace / "CLAUDE.md").write_text(content, encoding="utf-8")
|
(workspace / "CLAUDE.md").write_text(content, encoding="utf-8")
|
||||||
else:
|
return True
|
||||||
typer.echo("Warning: server returned empty CLAUDE.md content. Skipping.", err=True)
|
typer.echo("Warning: server returned empty CLAUDE.md content. Skipping.", err=True)
|
||||||
|
return False
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
typer.echo(
|
typer.echo(
|
||||||
f"Warning: CLAUDE.md fetch failed (HTTP {e.response.status_code}). Skipping.",
|
f"Warning: CLAUDE.md fetch failed (HTTP {e.response.status_code}). Skipping.",
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
typer.echo(f"Warning: CLAUDE.md fetch failed: {e}. Skipping.", err=True)
|
typer.echo(f"Warning: CLAUDE.md fetch failed: {e}. Skipping.", err=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -443,7 +454,7 @@ def setup(
|
||||||
|
|
||||||
# 7. Initialise Claude workspace (.claude/ hooks + placeholder + CLAUDE.md)
|
# 7. Initialise Claude workspace (.claude/ hooks + placeholder + CLAUDE.md)
|
||||||
typer.echo("Initializing Claude workspace...")
|
typer.echo("Initializing Claude workspace...")
|
||||||
_init_claude_workspace(
|
claude_md_written = _init_claude_workspace(
|
||||||
workspace,
|
workspace,
|
||||||
server_url=server_url if not no_claude_md else "",
|
server_url=server_url if not no_claude_md else "",
|
||||||
token=token if not no_claude_md else "",
|
token=token if not no_claude_md else "",
|
||||||
|
|
@ -456,8 +467,12 @@ def setup(
|
||||||
typer.echo(f" Tables : {n_downloaded} downloaded, {total_rows} total rows")
|
typer.echo(f" Tables : {n_downloaded} downloaded, {total_rows} total rows")
|
||||||
typer.echo(f" Workspace: {workspace}")
|
typer.echo(f" Workspace: {workspace}")
|
||||||
typer.echo(f" Hooks : SessionStart/End installed in {workspace}/.claude/settings.json")
|
typer.echo(f" Hooks : SessionStart/End installed in {workspace}/.claude/settings.json")
|
||||||
if not no_claude_md:
|
if no_claude_md:
|
||||||
|
typer.echo(f" CLAUDE.md: skipped (--no-claude-md)")
|
||||||
|
elif claude_md_written:
|
||||||
typer.echo(f" CLAUDE.md: written from server template")
|
typer.echo(f" CLAUDE.md: written from server template")
|
||||||
|
else:
|
||||||
|
typer.echo(f" CLAUDE.md: skipped (see warnings above)")
|
||||||
typer.echo("")
|
typer.echo("")
|
||||||
typer.echo("Next steps:")
|
typer.echo("Next steps:")
|
||||||
typer.echo(" da sync — refresh data")
|
typer.echo(" da sync — refresh data")
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,8 @@ class TestInitClaudeWorkspace:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_writes_claude_md_when_server_returns_200(self, tmp_workspace):
|
def test_writes_claude_md_when_server_returns_200(self, tmp_workspace):
|
||||||
"""When /api/welcome returns 200, CLAUDE.md is written."""
|
"""When /api/welcome returns 200, CLAUDE.md is written and the helper
|
||||||
|
returns True so the caller can label its summary line accurately."""
|
||||||
from cli.commands.analyst import _create_workspace, _init_claude_workspace
|
from cli.commands.analyst import _create_workspace, _init_claude_workspace
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
@ -165,12 +166,46 @@ class TestInitClaudeWorkspace:
|
||||||
mock_resp.raise_for_status = MagicMock()
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
with patch("cli.commands.analyst.httpx.get", return_value=mock_resp):
|
with patch("cli.commands.analyst.httpx.get", return_value=mock_resp):
|
||||||
_init_claude_workspace(tmp_workspace, server_url="https://example.com", token="tok")
|
written = _init_claude_workspace(
|
||||||
|
tmp_workspace, server_url="https://example.com", token="tok"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert written is True
|
||||||
claude_md = tmp_workspace / "CLAUDE.md"
|
claude_md = tmp_workspace / "CLAUDE.md"
|
||||||
assert claude_md.exists()
|
assert claude_md.exists()
|
||||||
assert "My CLAUDE.md" in claude_md.read_text(encoding="utf-8")
|
assert "My CLAUDE.md" in claude_md.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
def test_returns_false_when_server_returns_404(self, tmp_workspace):
|
||||||
|
"""When the server is too old for /api/welcome (404), the helper
|
||||||
|
returns False so the caller can render the summary as 'skipped' instead
|
||||||
|
of contradicting its own stderr warning."""
|
||||||
|
from cli.commands.analyst import _create_workspace, _init_claude_workspace
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
_create_workspace(tmp_workspace)
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.status_code = 404
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch("cli.commands.analyst.httpx.get", return_value=mock_resp):
|
||||||
|
written = _init_claude_workspace(
|
||||||
|
tmp_workspace, server_url="https://example.com", token="tok"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert written is False
|
||||||
|
assert not (tmp_workspace / "CLAUDE.md").exists()
|
||||||
|
|
||||||
|
def test_returns_false_when_no_server_url(self, tmp_workspace):
|
||||||
|
"""Caller passing empty server_url/token (e.g. --no-claude-md) gets False back."""
|
||||||
|
from cli.commands.analyst import _create_workspace, _init_claude_workspace
|
||||||
|
|
||||||
|
_create_workspace(tmp_workspace)
|
||||||
|
written = _init_claude_workspace(tmp_workspace, server_url="", token="")
|
||||||
|
|
||||||
|
assert written is False
|
||||||
|
assert not (tmp_workspace / "CLAUDE.md").exists()
|
||||||
|
|
||||||
def test_does_not_write_claude_md_when_no_claude_md_flag(self, tmp_workspace):
|
def test_does_not_write_claude_md_when_no_claude_md_flag(self, tmp_workspace):
|
||||||
"""When server_url/token are empty (--no-claude-md path), CLAUDE.md is not written."""
|
"""When server_url/token are empty (--no-claude-md path), CLAUDE.md is not written."""
|
||||||
from cli.commands.analyst import _create_workspace, _init_claude_workspace
|
from cli.commands.analyst import _create_workspace, _init_claude_workspace
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue