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.
Files changed (28) hide show
  1. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/.gitignore +3 -0
  2. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/PKG-INFO +6 -2
  3. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/README.md +5 -1
  4. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/pyproject.toml +1 -1
  5. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/doctor_cmd.py +10 -9
  6. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/init_cmd.py +22 -7
  7. yoru_cli-0.2.0/src/yoru_cli/skill_template.py +149 -0
  8. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/update_cmd.py +1 -1
  9. yoru_cli-0.2.0/tests/test_doctor.py +34 -0
  10. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_init.py +24 -0
  11. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_update.py +1 -1
  12. yoru_cli-0.1.3/tests/test_doctor.py +0 -18
  13. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/LICENSE +0 -0
  14. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/__init__.py +0 -0
  15. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/__main__.py +0 -0
  16. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/api.py +0 -0
  17. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/cli.py +0 -0
  18. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/config.py +0 -0
  19. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/hook_template.py +0 -0
  20. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/share_cmd.py +0 -0
  21. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/tail_cmd.py +0 -0
  22. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/src/yoru_cli/transcript_tailer.py +0 -0
  23. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/__init__.py +0 -0
  24. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_cli.py +0 -0
  25. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_hook_template.py +0 -0
  26. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_init_settings_merge.py +0 -0
  27. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/tests/test_tail.py +0 -0
  28. {yoru_cli-0.1.3 → yoru_cli-0.2.0}/uv.lock +0 -0
@@ -77,3 +77,6 @@ backend/data/.auth_jwt_secret
77
77
  # Embedding cache (trovex / fastembed)
78
78
  .fastembed_cache/
79
79
  .vercel
80
+
81
+ # install-smoke pre-release wheel (transient)
82
+ install-smoke/_wheel/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yoru-cli
3
- Version: 0.1.3
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
- pip install yoru-cli
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
- pip install yoru-cli
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`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "yoru-cli"
3
- version = "0.1.3"
3
+ version = "0.2.0"
4
4
  description = "Yoru — audit-grade session receipts for autonomous AI coding agents."
5
5
  license = { text = "MIT" }
6
6
  license-files = ["LICENSE"]
@@ -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 ✓ lines to stdout on success (exit 0). Failures go to stderr with a
10
- short human-readable reason. No fixes attempted.
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(f" Pair this device with your Yoru account:")
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 refresh_hook_assets() -> tuple[Path, Path]:
147
- """(Re)write the Claude Code hook script and (re)register it in
148
- settings.json. Idempotent used by `init` (first install) and `update`
149
- (refresh the hook to the new version + repair the settings wiring). Returns
150
- (hook_path, settings_path). Does NOT touch config (server/token)."""
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
- return hook_path, settings_path
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