yoru-cli 0.1.3__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/.gitignore +3 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/PKG-INFO +6 -2
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/README.md +5 -1
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/pyproject.toml +1 -1
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/doctor_cmd.py +10 -9
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/init_cmd.py +22 -7
- yoru_cli-0.2.0/src/yoru_cli/skill_template.py +149 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/update_cmd.py +1 -1
- yoru_cli-0.2.0/tests/test_doctor.py +34 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_init.py +24 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_update.py +1 -1
- yoru_cli-0.1.3/tests/test_doctor.py +0 -18
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/LICENSE +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/__init__.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/__main__.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/api.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/cli.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/config.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/hook_template.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/share_cmd.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/tail_cmd.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/transcript_tailer.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/__init__.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_cli.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_hook_template.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_init_settings_merge.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_tail.py +0 -0
- {yoru_cli-0.1.3 → yoru_cli-0.2.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yoru-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Yoru — audit-grade session receipts for autonomous AI coding agents.
|
|
5
5
|
Project-URL: Homepage, https://yoru.sh
|
|
6
6
|
Project-URL: Documentation, https://yoru.sh/docs
|
|
@@ -34,10 +34,14 @@ The Claude Code hook installer for Yoru. One command installs a `PostToolUse` ho
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
|
|
37
|
+
pipx install yoru-cli # or: uv tool install yoru-cli
|
|
38
38
|
yoru init --server https://yoru.acme.com
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
Use `pipx`/`uv` — a bare `pip install yoru-cli` fails on Homebrew/Debian Python with
|
|
42
|
+
`error: externally-managed-environment` (PEP 668). No pipx? A venv works too:
|
|
43
|
+
`python3 -m venv .venv && .venv/bin/pip install yoru-cli`.
|
|
44
|
+
|
|
41
45
|
Yoru is **self-hosted** — point the CLI at your own backend with `--server`. `yoru init` opens a pairing URL in your browser, writes `~/.claude/hooks/yoru.sh`, and registers the hook in `~/.claude/settings.json`. Don't have a server yet? [Stand one up](https://github.com/TsukumoHQ/yoru) — `docker-compose up`.
|
|
42
46
|
|
|
43
47
|
Requires Python 3.10+. One runtime dep: `httpx`.
|
|
@@ -8,10 +8,14 @@ The Claude Code hook installer for Yoru. One command installs a `PostToolUse` ho
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
|
|
11
|
+
pipx install yoru-cli # or: uv tool install yoru-cli
|
|
12
12
|
yoru init --server https://yoru.acme.com
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
Use `pipx`/`uv` — a bare `pip install yoru-cli` fails on Homebrew/Debian Python with
|
|
16
|
+
`error: externally-managed-environment` (PEP 668). No pipx? A venv works too:
|
|
17
|
+
`python3 -m venv .venv && .venv/bin/pip install yoru-cli`.
|
|
18
|
+
|
|
15
19
|
Yoru is **self-hosted** — point the CLI at your own backend with `--server`. `yoru init` opens a pairing URL in your browser, writes `~/.claude/hooks/yoru.sh`, and registers the hook in `~/.claude/settings.json`. Don't have a server yet? [Stand one up](https://github.com/TsukumoHQ/yoru) — `docker-compose up`.
|
|
16
20
|
|
|
17
21
|
Requires Python 3.10+. One runtime dep: `httpx`.
|
|
@@ -6,13 +6,14 @@ Read-only check of the install:
|
|
|
6
6
|
3. hook-token valid (GET /hook-tokens) → else exit 3 on 401
|
|
7
7
|
4. ~/.claude/hooks/yoru.sh is 0755 → else exit 4
|
|
8
8
|
|
|
9
|
-
Prints ✓
|
|
10
|
-
|
|
9
|
+
Prints a ✓ line to stdout as each stage passes, so a failure leaves a visible
|
|
10
|
+
trail of what already worked (✓ config / ✓ backend / token revoked → the break
|
|
11
|
+
is obvious). Failures go to stderr with a short human-readable reason, then a
|
|
12
|
+
non-zero exit. No fixes attempted.
|
|
11
13
|
"""
|
|
12
14
|
from __future__ import annotations
|
|
13
15
|
|
|
14
16
|
import argparse
|
|
15
|
-
import os
|
|
16
17
|
import stat
|
|
17
18
|
import sys
|
|
18
19
|
from pathlib import Path
|
|
@@ -42,6 +43,9 @@ def run(args: argparse.Namespace) -> int: # noqa: ARG001 — argparse hands arg
|
|
|
42
43
|
if not server or not token:
|
|
43
44
|
print("yoru init not run", file=sys.stderr)
|
|
44
45
|
return 1
|
|
46
|
+
# Each stage prints its check the moment it passes, so a later failure
|
|
47
|
+
# leaves a visible trail of what DID work.
|
|
48
|
+
print(f"✓ config at ~/.config/yoru/config.json (token {_token_suffix(token)})")
|
|
45
49
|
|
|
46
50
|
# 2. backend /health/ready
|
|
47
51
|
try:
|
|
@@ -52,6 +56,7 @@ def run(args: argparse.Namespace) -> int: # noqa: ARG001 — argparse hands arg
|
|
|
52
56
|
if r.status_code != 200:
|
|
53
57
|
print(f"backend unreachable at {server}", file=sys.stderr)
|
|
54
58
|
return 2
|
|
59
|
+
print(f"✓ backend {server} reachable")
|
|
55
60
|
|
|
56
61
|
# 3. hook-token validity
|
|
57
62
|
try:
|
|
@@ -72,6 +77,8 @@ def run(args: argparse.Namespace) -> int: # noqa: ARG001 — argparse hands arg
|
|
|
72
77
|
file=sys.stderr,
|
|
73
78
|
)
|
|
74
79
|
return 3
|
|
80
|
+
user = cfg.get("user") or "authenticated"
|
|
81
|
+
print(f"✓ hook-token valid (user: {user})")
|
|
75
82
|
|
|
76
83
|
# 4. hook file + perms
|
|
77
84
|
hook = _hook_path()
|
|
@@ -82,11 +89,5 @@ def run(args: argparse.Namespace) -> int: # noqa: ARG001 — argparse hands arg
|
|
|
82
89
|
if mode != 0o755:
|
|
83
90
|
print("hook file missing or not 0755", file=sys.stderr)
|
|
84
91
|
return 4
|
|
85
|
-
|
|
86
|
-
# all green
|
|
87
|
-
user = cfg.get("user") or "authenticated"
|
|
88
|
-
print(f"\u2713 config at ~/.config/yoru/config.json (token {_token_suffix(token)})")
|
|
89
|
-
print(f"\u2713 backend {server} reachable")
|
|
90
|
-
print(f"\u2713 hook-token valid (user: {user})")
|
|
91
92
|
print("\u2713 hook installed at ~/.claude/hooks/yoru.sh")
|
|
92
93
|
return 0
|
|
@@ -16,6 +16,7 @@ import httpx
|
|
|
16
16
|
from . import config
|
|
17
17
|
from .api import ReceiptClient
|
|
18
18
|
from .hook_template import HOOK_SCRIPT
|
|
19
|
+
from .skill_template import SKILL_MD, SKILL_NAME
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def _default_label() -> str:
|
|
@@ -106,7 +107,7 @@ def _pair_device(server: str, label: str, *, no_browser: bool) -> str | None:
|
|
|
106
107
|
interval = int(start.get("interval", 2))
|
|
107
108
|
|
|
108
109
|
print()
|
|
109
|
-
print(
|
|
110
|
+
print(" Pair this device with your Yoru account:")
|
|
110
111
|
print(f" 1. Open {verify_uri}")
|
|
111
112
|
print(f" 2. Enter {user_code}")
|
|
112
113
|
print()
|
|
@@ -143,11 +144,22 @@ def _pair_device(server: str, label: str, *, no_browser: bool) -> str | None:
|
|
|
143
144
|
return None
|
|
144
145
|
|
|
145
146
|
|
|
146
|
-
def
|
|
147
|
-
"""(Re)write the Claude Code
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
(
|
|
147
|
+
def _install_skill() -> Path:
|
|
148
|
+
"""(Re)write the PUBLIC end-user yoru Claude Code skill so a fresh Claude
|
|
149
|
+
session can drive setup + usage. Lands at ~/.claude/skills/yoru/SKILL.md,
|
|
150
|
+
where Claude Code resolves user-level skills. Idempotent."""
|
|
151
|
+
skill_dir = Path.home() / ".claude" / "skills" / SKILL_NAME
|
|
152
|
+
os.makedirs(skill_dir, exist_ok=True)
|
|
153
|
+
skill_path = skill_dir / "SKILL.md"
|
|
154
|
+
skill_path.write_text(SKILL_MD, encoding="utf-8")
|
|
155
|
+
return skill_path
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def refresh_hook_assets() -> tuple[Path, Path, Path]:
|
|
159
|
+
"""(Re)write the Claude Code hook script + the public yoru skill, and
|
|
160
|
+
(re)register the hook in settings.json. Idempotent — used by `init` (first
|
|
161
|
+
install) and `update` (refresh to the new version + repair the wiring).
|
|
162
|
+
Returns (hook_path, settings_path, skill_path). Does NOT touch config."""
|
|
151
163
|
hook_dir = Path.home() / ".claude" / "hooks"
|
|
152
164
|
hook_path = hook_dir / "yoru.sh"
|
|
153
165
|
os.makedirs(hook_dir, exist_ok=True)
|
|
@@ -156,7 +168,9 @@ def refresh_hook_assets() -> tuple[Path, Path]:
|
|
|
156
168
|
|
|
157
169
|
settings_path = Path.home() / ".claude" / "settings.json"
|
|
158
170
|
_merge_settings_json(settings_path, hook_path)
|
|
159
|
-
|
|
171
|
+
|
|
172
|
+
skill_path = _install_skill()
|
|
173
|
+
return hook_path, settings_path, skill_path
|
|
160
174
|
|
|
161
175
|
|
|
162
176
|
def run(args: argparse.Namespace) -> int:
|
|
@@ -188,5 +202,6 @@ def run(args: argparse.Namespace) -> int:
|
|
|
188
202
|
print("\u2713 config \u2192 ~/.config/yoru/config.json")
|
|
189
203
|
print("\u2713 hook \u2192 ~/.claude/hooks/yoru.sh")
|
|
190
204
|
print("\u2713 settings \u2192 ~/.claude/settings.json (hook registered)")
|
|
205
|
+
print("\u2713 skill \u2192 ~/.claude/skills/yoru/SKILL.md (Claude can drive setup + usage)")
|
|
191
206
|
print("Next: run Claude Code normally; first event streams to /sessions/events.")
|
|
192
207
|
return 0
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Bundled PUBLIC end-user Claude Code skill — written verbatim by `yoru init`
|
|
2
|
+
to ``~/.claude/skills/yoru/SKILL.md`` (and refreshed by `yoru update`).
|
|
3
|
+
|
|
4
|
+
This is the PUBLIC "how to self-host + use yoru" skill, NOT the private
|
|
5
|
+
yoru-dev maintainer persona. It teaches a fresh Claude Code session to drive
|
|
6
|
+
yoru setup + usage for the user ("tout copier à Claude Code, il fait tout").
|
|
7
|
+
|
|
8
|
+
Hard rails baked into the copy (keep them true if you edit):
|
|
9
|
+
- yoru is SELF-HOSTED ONLY — no hosted/cloud/sign-up. `--server` is required.
|
|
10
|
+
- No private paths / internal fleet references (public/AGPL split).
|
|
11
|
+
- Public sessions are opt-in (`yoru share`) and PII-redacted — stay honest
|
|
12
|
+
about the confidentiality model.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
SKILL_NAME = "yoru"
|
|
16
|
+
|
|
17
|
+
SKILL_MD: str = """---
|
|
18
|
+
name: yoru
|
|
19
|
+
description: >-
|
|
20
|
+
Set up and use yoru — self-hosted, audit-grade session receipts for Claude
|
|
21
|
+
Code and other autonomous coding agents. Use when the user wants to install
|
|
22
|
+
or configure yoru, stand up / start their own yoru backend, verify the
|
|
23
|
+
install, record a coding session, share a public session receipt (/s/:id),
|
|
24
|
+
replay a session, or understand the redaction / privacy model. yoru is
|
|
25
|
+
self-hosted only — there is no hosted service or sign-up.
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# yoru — self-hosted session receipts
|
|
29
|
+
|
|
30
|
+
yoru turns every Claude Code session into an audit-grade receipt: every tool
|
|
31
|
+
call, file edit, prompt, and red-flag event streams to a dashboard the user
|
|
32
|
+
runs themselves, plus a per-session A–F grade (Throughput / Reliability /
|
|
33
|
+
Safety). It is **self-hosted only** — the user runs the backend on their own
|
|
34
|
+
box. There is no hosted yoru, no cloud, no sign-up. Never tell the user to
|
|
35
|
+
"sign up" or point them at a managed service.
|
|
36
|
+
|
|
37
|
+
Two pieces:
|
|
38
|
+
- **`yoru` CLI** (PyPI `yoru-cli`, MIT) — installs a Claude Code hook that
|
|
39
|
+
forwards events to a backend.
|
|
40
|
+
- **the backend + dashboard** (AGPL, `github.com/TsukumoHQ/yoru`) — the user
|
|
41
|
+
stands this up themselves; it ingests events, scores sessions, and serves the
|
|
42
|
+
public session pages at `/s/:id`.
|
|
43
|
+
|
|
44
|
+
When this skill is active, drive the user through whichever part they need.
|
|
45
|
+
Run real commands, read their output, and fix problems — don't just narrate.
|
|
46
|
+
|
|
47
|
+
## 1. Is the CLI installed?
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
yoru --version
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If `yoru` is not found, install it (needs Python ≥ 3.10):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pipx install yoru-cli # or: uv tool install yoru-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use `pipx`/`uv`, not a bare `pip install` — on Homebrew/Debian Python the
|
|
60
|
+
latter fails with `error: externally-managed-environment` (PEP 668). If the
|
|
61
|
+
user has neither, a venv works: `python3 -m venv .venv && .venv/bin/pip install
|
|
62
|
+
yoru-cli`.
|
|
63
|
+
|
|
64
|
+
## 2. Does the user have a backend to point at?
|
|
65
|
+
|
|
66
|
+
yoru needs a backend URL — there is **no default server**. Ask the user for
|
|
67
|
+
their yoru server URL (e.g. `https://yoru.example.com`, or
|
|
68
|
+
`http://localhost:8002` for a local one).
|
|
69
|
+
|
|
70
|
+
If they don't have one yet, help them stand it up from the AGPL server repo:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/TsukumoHQ/yoru && cd yoru
|
|
74
|
+
cp backend/.env.example backend/.env # keyless self-host defaults work as-is
|
|
75
|
+
make dev # API on :8002, dashboard on :5173
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
First run shows a wizard (or `make setup` headless) to create an admin account
|
|
79
|
+
and pick a database. Defaults are local-first: SQLite + local auth, zero
|
|
80
|
+
external services. Postgres / SMTP / OAuth are all optional.
|
|
81
|
+
|
|
82
|
+
## 3. Pair this machine + install the hook
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
yoru init --server <THEIR_SERVER_URL>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`--server` is required. `yoru init`:
|
|
89
|
+
- writes `~/.config/yoru/config.json` (server + a paired device token),
|
|
90
|
+
- writes the hook to `~/.claude/hooks/yoru.sh`,
|
|
91
|
+
- registers it in `~/.claude/settings.json` (PostToolUse / SessionStart / Stop).
|
|
92
|
+
|
|
93
|
+
It opens a browser pairing page; the user approves the device there. For a
|
|
94
|
+
headless box, pass a token instead: `yoru init --server <url> --token <token>`
|
|
95
|
+
(or set `YORU_TOKEN`). Re-running is safe — add `--force` to repoint.
|
|
96
|
+
|
|
97
|
+
## 4. Verify
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
yoru doctor
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
It prints a ✓ per stage (config → backend reachable → token valid → hook
|
|
104
|
+
installed). If a stage fails, the line just before the error tells you what
|
|
105
|
+
already worked — fix that stage:
|
|
106
|
+
- *yoru init not run* → run step 3.
|
|
107
|
+
- *backend unreachable* → is the server URL right and the backend up?
|
|
108
|
+
- *token revoked or expired* → re-run `yoru init --force`.
|
|
109
|
+
- *hook file missing* → re-run `yoru init` (or `yoru update`).
|
|
110
|
+
|
|
111
|
+
## 5. Record a session
|
|
112
|
+
|
|
113
|
+
Nothing extra to do — just use Claude Code normally. The hook streams each
|
|
114
|
+
event to the backend, and the session appears in the dashboard automatically
|
|
115
|
+
with its A–F grade. To refresh the hook after a CLI upgrade: `yoru update`.
|
|
116
|
+
|
|
117
|
+
yoru flags six kinds of red-flag events at ingest, so the user can audit them:
|
|
118
|
+
`secret` (leaked keys), `env` (writes to `.env*`), `shell` (`rm`/`dd`/`mkfs`),
|
|
119
|
+
`db` (`DROP`/`TRUNCATE`/unscoped `DELETE`), `migration` (edits under
|
|
120
|
+
`migrations/`), `ci` (edits under `.github/workflows/`, Dockerfile, etc.).
|
|
121
|
+
|
|
122
|
+
## 6. Share a session (and the privacy model)
|
|
123
|
+
|
|
124
|
+
By default every session is **private** — `/s/:id` returns 404 on purpose.
|
|
125
|
+
Sharing is explicit and per-session:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
yoru share <SESSION_ID> --server <THEIR_SERVER_URL>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This flips that one session public and gives a shareable URL (on the user's own
|
|
132
|
+
host, e.g. `https://yoru.example.com/s/<id>`). The public page is the session
|
|
133
|
+
trail + grade, with a replay view of the event timeline.
|
|
134
|
+
|
|
135
|
+
Be honest with the user about what sharing exposes: a public session is
|
|
136
|
+
**PII-redacted on the wire** — the viewer sees a session id, not the user's
|
|
137
|
+
email — but it DOES show the tool calls, file paths, and any flagged content
|
|
138
|
+
captured in that session. Only share sessions that are safe to make public, and
|
|
139
|
+
only ones the user explicitly chooses. Sharing is opt-in and reversible from the
|
|
140
|
+
dashboard.
|
|
141
|
+
|
|
142
|
+
## Honesty rails
|
|
143
|
+
|
|
144
|
+
- Self-hosted only. Never promise hosting, "sign up", "managed", "free
|
|
145
|
+
forever", or cloud pricing.
|
|
146
|
+
- `--server` is always required; there is no default.
|
|
147
|
+
- Don't overstate redaction — emails are stripped, but shared trails still
|
|
148
|
+
contain whatever the agent did. Say so.
|
|
149
|
+
"""
|
|
@@ -141,7 +141,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
141
141
|
# --- refresh side-assets (best-effort; never fails the binary update) ----
|
|
142
142
|
try:
|
|
143
143
|
init_cmd.refresh_hook_assets()
|
|
144
|
-
print("✓ hook + settings refreshed")
|
|
144
|
+
print("✓ hook + settings + skill refreshed")
|
|
145
145
|
except Exception as e: # noqa: BLE001 — side-asset failure must not fail update
|
|
146
146
|
print(f"note: could not refresh the hook assets ({e}); run `yoru init --force`.")
|
|
147
147
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _args() -> argparse.Namespace:
|
|
7
|
+
return argparse.Namespace(cmd="doctor")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_doctor_exits_1_when_config_missing(monkeypatch, tmp_path, capsys):
|
|
11
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
12
|
+
from yoru_cli import config, doctor_cmd
|
|
13
|
+
|
|
14
|
+
assert config.exists() is False
|
|
15
|
+
rc = doctor_cmd.run(_args())
|
|
16
|
+
assert rc == 1
|
|
17
|
+
err = capsys.readouterr().err
|
|
18
|
+
assert "yoru init not run" in err
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_doctor_prints_passed_stages_before_a_failure(monkeypatch, tmp_path, capsys):
|
|
22
|
+
"""A later-stage failure still shows the ✓ trail of what already worked."""
|
|
23
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
24
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / ".config"))
|
|
25
|
+
from yoru_cli import config, doctor_cmd
|
|
26
|
+
|
|
27
|
+
# config stage passes; backend stage fails (nothing is listening).
|
|
28
|
+
config.save({"server": "http://127.0.0.1:1", "token": "rcpt_test_abcd"})
|
|
29
|
+
|
|
30
|
+
rc = doctor_cmd.run(_args())
|
|
31
|
+
out = capsys.readouterr()
|
|
32
|
+
assert rc == 2 # failed at the backend stage
|
|
33
|
+
assert "✓ config" in out.out # but the passed stage was reported first
|
|
34
|
+
assert "backend unreachable" in out.err
|
|
@@ -42,6 +42,30 @@ def test_init_writes_config_and_hook(monkeypatch, tmp_path):
|
|
|
42
42
|
assert "rcpt_ABC" in cfg_path.read_text()
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def test_init_lands_public_skill(monkeypatch, tmp_path):
|
|
46
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
47
|
+
from yoru_cli import init_cmd
|
|
48
|
+
|
|
49
|
+
rc = init_cmd.run(_args(server="http://fake", token="rcpt_ABC"))
|
|
50
|
+
assert rc == 0
|
|
51
|
+
|
|
52
|
+
skill = tmp_path / ".claude" / "skills" / "yoru" / "SKILL.md"
|
|
53
|
+
assert skill.is_file()
|
|
54
|
+
body = skill.read_text()
|
|
55
|
+
# Valid Claude Code skill frontmatter + the public name it resolves by.
|
|
56
|
+
assert body.startswith("---\n")
|
|
57
|
+
assert "name: yoru" in body
|
|
58
|
+
assert "description:" in body
|
|
59
|
+
# Honesty rails baked in (self-hosted only; --server has no default).
|
|
60
|
+
assert "self-hosted" in body.lower()
|
|
61
|
+
assert "--server" in body
|
|
62
|
+
# Public/AGPL split — the shipped skill must not leak private fleet paths
|
|
63
|
+
# or the private maintainer persona.
|
|
64
|
+
assert ".worktrees" not in body
|
|
65
|
+
assert "yoru-dev" not in body
|
|
66
|
+
assert "agent-relay" not in body
|
|
67
|
+
|
|
68
|
+
|
|
45
69
|
def test_init_already_installed_without_force(monkeypatch, tmp_path, capsys):
|
|
46
70
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
47
71
|
from yoru_cli import init_cmd
|
|
@@ -24,7 +24,7 @@ def calls(monkeypatch):
|
|
|
24
24
|
monkeypatch.setattr(update_cmd, "_pip_install", lambda v: rec["pip"].append(v) or 0)
|
|
25
25
|
monkeypatch.setattr(
|
|
26
26
|
init_cmd, "refresh_hook_assets",
|
|
27
|
-
lambda: rec.__setitem__("refresh", rec["refresh"] + 1) or (None, None),
|
|
27
|
+
lambda: rec.__setitem__("refresh", rec["refresh"] + 1) or (None, None, None),
|
|
28
28
|
)
|
|
29
29
|
monkeypatch.setattr(update_cmd, "_fetch_latest_tag", lambda **_: "v0.1.3")
|
|
30
30
|
return rec
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def _args() -> argparse.Namespace:
|
|
7
|
-
return argparse.Namespace(cmd="doctor")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_doctor_exits_1_when_config_missing(monkeypatch, tmp_path, capsys):
|
|
11
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
12
|
-
from yoru_cli import config, doctor_cmd
|
|
13
|
-
|
|
14
|
-
assert config.exists() is False
|
|
15
|
-
rc = doctor_cmd.run(_args())
|
|
16
|
-
assert rc == 1
|
|
17
|
-
err = capsys.readouterr().err
|
|
18
|
-
assert "yoru init not run" in err
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|