feat(cli): install SessionStart hook chaining self-upgrade then pull
Single hook entry: 'agnes self-upgrade --quiet ... || true; agnes pull --quiet ... || true'. Shell semicolon guarantees ordering across every Claude Code version (no reliance on undocumented multi-hook execution semantics); each segment's || true preserves the original property that an upgrade failure does not abort the pull.
This commit is contained in:
parent
630e224578
commit
be62ce61b8
2 changed files with 31 additions and 5 deletions
|
|
@ -24,7 +24,7 @@ from pathlib import Path
|
||||||
# Substrings that identify "our" hook commands. Includes legacy `da sync`
|
# Substrings that identify "our" hook commands. Includes legacy `da sync`
|
||||||
# so a workspace bootstrapped by an older CLI gets cleanly upgraded on the
|
# so a workspace bootstrapped by an older CLI gets cleanly upgraded on the
|
||||||
# next `agnes init` run.
|
# next `agnes init` run.
|
||||||
_OUR_COMMAND_MARKERS = ("agnes pull", "agnes push", "da sync")
|
_OUR_COMMAND_MARKERS = ("agnes self-upgrade", "agnes pull", "agnes push", "da sync")
|
||||||
|
|
||||||
|
|
||||||
def install_claude_hooks(workspace: Path) -> None:
|
def install_claude_hooks(workspace: Path) -> None:
|
||||||
|
|
@ -60,7 +60,11 @@ def install_claude_hooks(workspace: Path) -> None:
|
||||||
existing.remove(entry)
|
existing.remove(entry)
|
||||||
existing.append({"hooks": [{"type": "command", "command": command}]})
|
existing.append({"hooks": [{"type": "command", "command": command}]})
|
||||||
|
|
||||||
_replace_or_add("SessionStart", "agnes pull --quiet 2>/dev/null || true")
|
_replace_or_add(
|
||||||
|
"SessionStart",
|
||||||
|
"agnes self-upgrade --quiet 2>/dev/null || true; "
|
||||||
|
"agnes pull --quiet 2>/dev/null || true",
|
||||||
|
)
|
||||||
_replace_or_add("SessionEnd", "agnes push --quiet 2>/dev/null || true")
|
_replace_or_add("SessionEnd", "agnes push --quiet 2>/dev/null || true")
|
||||||
|
|
||||||
settings_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
|
settings_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ def _read_settings(workspace: Path) -> dict:
|
||||||
def test_install_creates_settings_file(tmp_path):
|
def test_install_creates_settings_file(tmp_path):
|
||||||
install_claude_hooks(tmp_path)
|
install_claude_hooks(tmp_path)
|
||||||
cfg = _read_settings(tmp_path)
|
cfg = _read_settings(tmp_path)
|
||||||
assert cfg["hooks"]["SessionStart"]
|
cmd = cfg["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
||||||
assert "agnes pull --quiet" in cfg["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
assert "agnes self-upgrade --quiet" in cmd
|
||||||
assert cfg["hooks"]["SessionEnd"]
|
assert "agnes pull --quiet" in cmd
|
||||||
assert "agnes push --quiet" in cfg["hooks"]["SessionEnd"][0]["hooks"][0]["command"]
|
assert "agnes push --quiet" in cfg["hooks"]["SessionEnd"][0]["hooks"][0]["command"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -74,3 +74,25 @@ def test_install_handles_invalid_json(tmp_path, capsys):
|
||||||
install_claude_hooks(tmp_path)
|
install_claude_hooks(tmp_path)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "not valid JSON" in captured.err or "warning" in captured.err.lower()
|
assert "not valid JSON" in captured.err or "warning" in captured.err.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_chains_self_upgrade_then_pull_in_one_entry(tmp_path):
|
||||||
|
install_claude_hooks(tmp_path)
|
||||||
|
cfg = _read_settings(tmp_path)
|
||||||
|
session_start = cfg["hooks"]["SessionStart"]
|
||||||
|
assert len(session_start) == 1, session_start
|
||||||
|
cmd = session_start[0]["hooks"][0]["command"]
|
||||||
|
assert "agnes self-upgrade --quiet" in cmd
|
||||||
|
assert "agnes pull --quiet" in cmd
|
||||||
|
# Order is encoded in the shell — self-upgrade must appear first
|
||||||
|
assert cmd.index("agnes self-upgrade") < cmd.index("agnes pull")
|
||||||
|
# Both segments carry || true so neither failure aborts the line
|
||||||
|
assert cmd.count("|| true") >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_idempotent_chained_entry(tmp_path):
|
||||||
|
install_claude_hooks(tmp_path)
|
||||||
|
install_claude_hooks(tmp_path)
|
||||||
|
cfg = _read_settings(tmp_path)
|
||||||
|
assert len(cfg["hooks"]["SessionStart"]) == 1
|
||||||
|
assert len(cfg["hooks"]["SessionEnd"]) == 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue