yoru-cli 0.2.0__tar.gz → 0.3.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.2.0 → yoru_cli-0.3.0}/.gitignore +3 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/PKG-INFO +13 -2
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/README.md +9 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/pyproject.toml +13 -3
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/api.py +32 -2
- yoru_cli-0.3.0/src/yoru_cli/cli.py +260 -0
- yoru_cli-0.3.0/src/yoru_cli/config.py +297 -0
- yoru_cli-0.3.0/src/yoru_cli/doctor_cmd.py +164 -0
- yoru_cli-0.3.0/src/yoru_cli/dsse_verify.py +183 -0
- yoru_cli-0.3.0/src/yoru_cli/enforce_cmd.py +223 -0
- yoru_cli-0.3.0/src/yoru_cli/enforce_policy.py +107 -0
- yoru_cli-0.3.0/src/yoru_cli/enforce_template.py +34 -0
- yoru_cli-0.3.0/src/yoru_cli/git_hook_run.py +212 -0
- yoru_cli-0.3.0/src/yoru_cli/git_hooks.py +106 -0
- yoru_cli-0.3.0/src/yoru_cli/git_reconcile.py +210 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/hook_template.py +14 -3
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/init_cmd.py +120 -12
- yoru_cli-0.3.0/src/yoru_cli/logout_cmd.py +48 -0
- yoru_cli-0.3.0/src/yoru_cli/rotate_cmd.py +75 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/skill_template.py +2 -2
- yoru_cli-0.3.0/src/yoru_cli/transcript_tailer.py +820 -0
- yoru_cli-0.3.0/src/yoru_cli/use_cmd.py +44 -0
- yoru_cli-0.3.0/src/yoru_cli/verify_export_cmd.py +95 -0
- yoru_cli-0.3.0/tests/test_cli.py +89 -0
- yoru_cli-0.3.0/tests/test_config.py +155 -0
- yoru_cli-0.3.0/tests/test_doctor.py +181 -0
- yoru_cli-0.3.0/tests/test_enforce.py +247 -0
- yoru_cli-0.3.0/tests/test_enforce_hook_script.py +93 -0
- yoru_cli-0.3.0/tests/test_git_hook_run.py +207 -0
- yoru_cli-0.3.0/tests/test_git_hooks.py +81 -0
- yoru_cli-0.3.0/tests/test_git_reconcile.py +400 -0
- yoru_cli-0.3.0/tests/test_hook_template.py +122 -0
- yoru_cli-0.3.0/tests/test_init.py +440 -0
- yoru_cli-0.3.0/tests/test_logout.py +86 -0
- yoru_cli-0.3.0/tests/test_rotate.py +126 -0
- yoru_cli-0.3.0/tests/test_transcript_tailer.py +565 -0
- yoru_cli-0.3.0/tests/test_use.py +84 -0
- yoru_cli-0.3.0/tests/test_verify_export.py +160 -0
- yoru_cli-0.3.0/uv.lock +577 -0
- yoru_cli-0.2.0/src/yoru_cli/cli.py +0 -137
- yoru_cli-0.2.0/src/yoru_cli/config.py +0 -43
- yoru_cli-0.2.0/src/yoru_cli/doctor_cmd.py +0 -93
- yoru_cli-0.2.0/src/yoru_cli/transcript_tailer.py +0 -485
- yoru_cli-0.2.0/tests/test_cli.py +0 -38
- yoru_cli-0.2.0/tests/test_doctor.py +0 -34
- yoru_cli-0.2.0/tests/test_hook_template.py +0 -25
- yoru_cli-0.2.0/tests/test_init.py +0 -161
- yoru_cli-0.2.0/uv.lock +0 -230
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/LICENSE +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/__init__.py +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/__main__.py +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/share_cmd.py +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/tail_cmd.py +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/src/yoru_cli/update_cmd.py +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/tests/__init__.py +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/tests/test_init_settings_merge.py +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/tests/test_tail.py +0 -0
- {yoru_cli-0.2.0 → yoru_cli-0.3.0}/tests/test_update.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: yoru-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -19,9 +19,11 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: cryptography>=42
|
|
22
23
|
Requires-Dist: httpx>=0.27
|
|
23
24
|
Provides-Extra: dev
|
|
24
25
|
Requires-Dist: pytest>=7; extra == 'dev'
|
|
26
|
+
Requires-Dist: yoru-contract; extra == 'dev'
|
|
25
27
|
Description-Content-Type: text/markdown
|
|
26
28
|
|
|
27
29
|
# yoru-cli
|
|
@@ -70,6 +72,15 @@ The installed script is ~20 lines of Bash that `POST`s one JSON event per Claude
|
|
|
70
72
|
|
|
71
73
|
It uses `curl --max-time 2 || true` so a Yoru outage never stalls your terminal — Claude Code doesn't notice the hook is even there.
|
|
72
74
|
|
|
75
|
+
## Multiple devs, multiple identities
|
|
76
|
+
|
|
77
|
+
Each paired identity (server + token) gets its own local slot under `~/.config/yoru/identities/`. The shared tailer and every hook event attribute to whichever identity is **active** — there is no per-process attribution.
|
|
78
|
+
|
|
79
|
+
- **One dev per machine (the normal case):** no ambiguity — one OS user, one identity, all activity is unambiguously yours.
|
|
80
|
+
- **Same OS login, multiple identities paired** (e.g. you re-paired against a second org, or a shared machine): only one identity is active at a time. Concurrent work under that same login attributes to whichever identity is active *regardless of who actually ran it* — this is an explicit, documented limit, not a bug. Switch identities first with `yoru use <label>`.
|
|
81
|
+
|
|
82
|
+
Run `yoru doctor` any time to see the active identity and get warned if other paired identities exist unused on this machine.
|
|
83
|
+
|
|
73
84
|
## Self-host the server
|
|
74
85
|
|
|
75
86
|
The CLI is MIT. The server (backend + dashboard) is AGPL-3.0 and lives at **[github.com/TsukumoHQ/yoru](https://github.com/TsukumoHQ/yoru)**. `docker-compose up`, then `yoru init --server https://your-host` — done. There is no hosted Yoru; you run it.
|
|
@@ -44,6 +44,15 @@ The installed script is ~20 lines of Bash that `POST`s one JSON event per Claude
|
|
|
44
44
|
|
|
45
45
|
It uses `curl --max-time 2 || true` so a Yoru outage never stalls your terminal — Claude Code doesn't notice the hook is even there.
|
|
46
46
|
|
|
47
|
+
## Multiple devs, multiple identities
|
|
48
|
+
|
|
49
|
+
Each paired identity (server + token) gets its own local slot under `~/.config/yoru/identities/`. The shared tailer and every hook event attribute to whichever identity is **active** — there is no per-process attribution.
|
|
50
|
+
|
|
51
|
+
- **One dev per machine (the normal case):** no ambiguity — one OS user, one identity, all activity is unambiguously yours.
|
|
52
|
+
- **Same OS login, multiple identities paired** (e.g. you re-paired against a second org, or a shared machine): only one identity is active at a time. Concurrent work under that same login attributes to whichever identity is active *regardless of who actually ran it* — this is an explicit, documented limit, not a bug. Switch identities first with `yoru use <label>`.
|
|
53
|
+
|
|
54
|
+
Run `yoru doctor` any time to see the active identity and get warned if other paired identities exist unused on this machine.
|
|
55
|
+
|
|
47
56
|
## Self-host the server
|
|
48
57
|
|
|
49
58
|
The CLI is MIT. The server (backend + dashboard) is AGPL-3.0 and lives at **[github.com/TsukumoHQ/yoru](https://github.com/TsukumoHQ/yoru)**. `docker-compose up`, then `yoru init --server https://your-host` — done. There is no hosted Yoru; you run it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "yoru-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Yoru — audit-grade session receipts for autonomous AI coding agents."
|
|
5
5
|
license = { text = "MIT" }
|
|
6
6
|
license-files = ["LICENSE"]
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
19
|
]
|
|
20
20
|
requires-python = ">=3.10"
|
|
21
|
-
dependencies = ["httpx>=0.27"]
|
|
21
|
+
dependencies = ["httpx>=0.27", "cryptography>=42"]
|
|
22
22
|
|
|
23
23
|
[project.urls]
|
|
24
24
|
Homepage = "https://yoru.sh"
|
|
@@ -30,7 +30,17 @@ Issues = "https://github.com/TsukumoHQ/cli-yoru/issues"
|
|
|
30
30
|
yoru = "yoru_cli.cli:main"
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
|
-
dev
|
|
33
|
+
# yoru-contract is dev-only for now: nothing in yoru-cli imports it yet (the
|
|
34
|
+
# CLI-side consumption ticket is a separate follow-up), and it isn't
|
|
35
|
+
# published to PyPI or vendored into the wheel — a hard runtime dependency
|
|
36
|
+
# here would break `pip install yoru-cli` with "No matching distribution
|
|
37
|
+
# found for yoru-contract". Once the CLI actually consumes it, the release
|
|
38
|
+
# mirror step needs to vendor its source into the wheel (or publish it
|
|
39
|
+
# separately) before this can become a real dependency.
|
|
40
|
+
dev = ["pytest>=7", "yoru-contract"]
|
|
41
|
+
|
|
42
|
+
[tool.uv.sources]
|
|
43
|
+
yoru-contract = { path = "../packages/yoru-contract" }
|
|
34
44
|
|
|
35
45
|
[build-system]
|
|
36
46
|
requires = ["hatchling"]
|
|
@@ -10,11 +10,18 @@ class ReceiptClient:
|
|
|
10
10
|
self.base_url = base_url.rstrip("/")
|
|
11
11
|
self.token = token
|
|
12
12
|
|
|
13
|
-
def start_device_code(
|
|
13
|
+
def start_device_code(
|
|
14
|
+
self, label: str | None = None, hostname: str | None = None
|
|
15
|
+
) -> dict[str, Any]:
|
|
14
16
|
"""Begin the device-pairing handshake — no auth needed."""
|
|
17
|
+
body: dict[str, Any] = {}
|
|
18
|
+
if label:
|
|
19
|
+
body["label"] = label
|
|
20
|
+
if hostname:
|
|
21
|
+
body["hostname"] = hostname
|
|
15
22
|
r = httpx.post(
|
|
16
23
|
f"{self.base_url}/api/v1/auth/device-code",
|
|
17
|
-
json=
|
|
24
|
+
json=body,
|
|
18
25
|
timeout=5.0,
|
|
19
26
|
)
|
|
20
27
|
r.raise_for_status()
|
|
@@ -69,6 +76,29 @@ class ReceiptClient:
|
|
|
69
76
|
r.raise_for_status()
|
|
70
77
|
return r.json()
|
|
71
78
|
|
|
79
|
+
def logout(self) -> bool:
|
|
80
|
+
"""Revoke this client's own token server-side (POST /auth/logout).
|
|
81
|
+
|
|
82
|
+
Self-revoke via bearer — no need to know the token's server-side row
|
|
83
|
+
id, which the CLI never learns (the poll response only ever returns
|
|
84
|
+
the raw token). Returns True if the token was revoked, False if it
|
|
85
|
+
was already invalid/revoked (a 401 here just means "nothing to
|
|
86
|
+
revoke", not a failure worth surfacing). Any other error status or
|
|
87
|
+
network failure raises, so the caller can warn loudly instead of
|
|
88
|
+
silently treating a failed revoke as done.
|
|
89
|
+
"""
|
|
90
|
+
if not self.token:
|
|
91
|
+
raise RuntimeError("logout requires authentication")
|
|
92
|
+
r = httpx.post(
|
|
93
|
+
f"{self.base_url}/api/v1/auth/logout",
|
|
94
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
95
|
+
timeout=5.0,
|
|
96
|
+
)
|
|
97
|
+
if r.status_code == 401:
|
|
98
|
+
return False
|
|
99
|
+
r.raise_for_status()
|
|
100
|
+
return True
|
|
101
|
+
|
|
72
102
|
def get_share_consent(self) -> dict[str, Any]:
|
|
73
103
|
"""Returns {consented: bool, at: str|None} for the authenticated user."""
|
|
74
104
|
if not self.token:
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from . import __version__
|
|
6
|
+
from . import (
|
|
7
|
+
config,
|
|
8
|
+
doctor_cmd,
|
|
9
|
+
enforce_cmd,
|
|
10
|
+
git_hook_run,
|
|
11
|
+
init_cmd,
|
|
12
|
+
logout_cmd,
|
|
13
|
+
rotate_cmd,
|
|
14
|
+
share_cmd,
|
|
15
|
+
tail_cmd,
|
|
16
|
+
update_cmd,
|
|
17
|
+
use_cmd,
|
|
18
|
+
verify_export_cmd,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
prog="yoru",
|
|
25
|
+
description="Yoru — audit-grade session receipts for autonomous AI coding agents.",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--version",
|
|
29
|
+
action="version",
|
|
30
|
+
version=f"yoru {__version__}",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
subparsers = parser.add_subparsers(
|
|
34
|
+
dest="cmd",
|
|
35
|
+
required=True,
|
|
36
|
+
metavar="{init,use,logout,rotate,tail,doctor,share,update,verify-export,enforce}",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
p_init = subparsers.add_parser(
|
|
40
|
+
"init",
|
|
41
|
+
help="Install the Claude Code hook and pair a local identity under ~/.config/yoru/.",
|
|
42
|
+
)
|
|
43
|
+
p_init.add_argument(
|
|
44
|
+
"--server",
|
|
45
|
+
required=True,
|
|
46
|
+
help="Backend URL of your Yoru server, e.g. https://yoru.acme.com. "
|
|
47
|
+
"Yoru is self-hosted — there is no default server.",
|
|
48
|
+
)
|
|
49
|
+
p_init.add_argument(
|
|
50
|
+
"--token",
|
|
51
|
+
default=None,
|
|
52
|
+
help="Pre-minted hook token (rcpt_...) — for headless/CI/server setups. "
|
|
53
|
+
"Also read from $YORU_TOKEN. Without this, yoru init launches "
|
|
54
|
+
"interactive device pairing.",
|
|
55
|
+
)
|
|
56
|
+
p_init.add_argument(
|
|
57
|
+
"--label",
|
|
58
|
+
default=None,
|
|
59
|
+
help="Human-readable machine label shown in the dashboard "
|
|
60
|
+
"(default: <hostname> · <os>).",
|
|
61
|
+
)
|
|
62
|
+
p_init.add_argument(
|
|
63
|
+
"--no-browser",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Don't try to auto-open the pairing URL in a browser.",
|
|
66
|
+
)
|
|
67
|
+
p_init.add_argument("--force", action="store_true", help="Overwrite an existing install.")
|
|
68
|
+
p_init.add_argument(
|
|
69
|
+
"--backfill-git",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="OPT-IN: also walk this repo's full pre-existing git history "
|
|
72
|
+
"and spool it (in addition to the normal from-here-on capture). "
|
|
73
|
+
"Off by default — see docs/SELF-HOST.md.",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
p_use = subparsers.add_parser(
|
|
77
|
+
"use",
|
|
78
|
+
help="Switch the active paired identity (no arg: list paired identities).",
|
|
79
|
+
)
|
|
80
|
+
p_use.add_argument(
|
|
81
|
+
"label",
|
|
82
|
+
nargs="?",
|
|
83
|
+
default=None,
|
|
84
|
+
help="identity_label (or identity_id) to switch to. Omit to list.",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
subparsers.add_parser(
|
|
88
|
+
"logout",
|
|
89
|
+
help="Revoke the current token server-side and forget it locally.",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
p_rotate = subparsers.add_parser(
|
|
93
|
+
"rotate",
|
|
94
|
+
help="Replace the current token with a new one, revoking the old "
|
|
95
|
+
"one server-side (same server as the existing pairing).",
|
|
96
|
+
)
|
|
97
|
+
p_rotate.add_argument(
|
|
98
|
+
"--token",
|
|
99
|
+
default=None,
|
|
100
|
+
help="Pre-minted replacement token — for headless/CI/server setups. "
|
|
101
|
+
"Also read from $YORU_TOKEN. Without this, yoru rotate launches "
|
|
102
|
+
"interactive device pairing.",
|
|
103
|
+
)
|
|
104
|
+
p_rotate.add_argument(
|
|
105
|
+
"--label",
|
|
106
|
+
default=None,
|
|
107
|
+
help="Human-readable machine label for the new pairing "
|
|
108
|
+
"(default: <hostname> · <os>).",
|
|
109
|
+
)
|
|
110
|
+
p_rotate.add_argument(
|
|
111
|
+
"--no-browser",
|
|
112
|
+
action="store_true",
|
|
113
|
+
help="Don't try to auto-open the pairing URL in a browser.",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
p_tail = subparsers.add_parser(
|
|
117
|
+
"tail",
|
|
118
|
+
help="Read JSON events from stdin and POST them as a batch (dev/debug).",
|
|
119
|
+
)
|
|
120
|
+
p_tail.add_argument(
|
|
121
|
+
"--server",
|
|
122
|
+
default=None,
|
|
123
|
+
help="Backend URL (default: the server saved in config by `yoru init`).",
|
|
124
|
+
)
|
|
125
|
+
p_tail.add_argument("--session-id", default=None, help="Session id to stamp on events missing one.")
|
|
126
|
+
|
|
127
|
+
subparsers.add_parser(
|
|
128
|
+
"doctor",
|
|
129
|
+
help="Diagnose the install: config, backend, token, hook.",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
p_share = subparsers.add_parser(
|
|
133
|
+
"share",
|
|
134
|
+
help="Flip a session public and copy its yoru.sh/s/<id> URL to the clipboard.",
|
|
135
|
+
)
|
|
136
|
+
p_share.add_argument(
|
|
137
|
+
"session_id",
|
|
138
|
+
help="Session id to share (or revoke). Find it in the dashboard URL or the session header.",
|
|
139
|
+
)
|
|
140
|
+
p_share.add_argument(
|
|
141
|
+
"--revoke",
|
|
142
|
+
action="store_true",
|
|
143
|
+
help="Flip the session back to private instead of sharing it.",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
p_update = subparsers.add_parser(
|
|
147
|
+
"update",
|
|
148
|
+
help="Self-update the CLI to the latest release (pip), and refresh the hook.",
|
|
149
|
+
)
|
|
150
|
+
p_update.add_argument(
|
|
151
|
+
"--force",
|
|
152
|
+
action="store_true",
|
|
153
|
+
help="Bypass the dev-build and no-downgrade guards (explicit reinstall/downgrade).",
|
|
154
|
+
)
|
|
155
|
+
p_update.add_argument(
|
|
156
|
+
"--check",
|
|
157
|
+
action="store_true",
|
|
158
|
+
help="Report whether an update is available and exit — install nothing.",
|
|
159
|
+
)
|
|
160
|
+
p_update.add_argument(
|
|
161
|
+
"--server",
|
|
162
|
+
nargs="?",
|
|
163
|
+
const="",
|
|
164
|
+
default=None,
|
|
165
|
+
metavar="URL",
|
|
166
|
+
help="Check the running SERVER's version (notify-only) instead of self-updating "
|
|
167
|
+
"the CLI. Bare --server uses your configured server; --server URL targets that "
|
|
168
|
+
"instance. Never pulls or restarts the server.",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
p_verify = subparsers.add_parser(
|
|
172
|
+
"verify-export",
|
|
173
|
+
help="Offline-verify a signed audit-export bundle (Ed25519/DSSE + chain).",
|
|
174
|
+
)
|
|
175
|
+
p_verify.add_argument(
|
|
176
|
+
"bundle",
|
|
177
|
+
help="Path to the signed bundle JSON (or '-' to read from stdin).",
|
|
178
|
+
)
|
|
179
|
+
p_verify.add_argument(
|
|
180
|
+
"--pubkey-fingerprint",
|
|
181
|
+
default=None,
|
|
182
|
+
metavar="SHA256_HEX",
|
|
183
|
+
help="Pin the deployer's public-key fingerprint (published out-of-band). "
|
|
184
|
+
"Without it the check is tamper-evident only — the embedded key is "
|
|
185
|
+
"trusted on faith.",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
p_enforce = subparsers.add_parser(
|
|
189
|
+
"enforce",
|
|
190
|
+
help="Manage the OPT-IN enforcement gate that halts dangerous ops "
|
|
191
|
+
"(default OFF; the passive audit trail is unaffected).",
|
|
192
|
+
)
|
|
193
|
+
_eg = p_enforce.add_mutually_exclusive_group()
|
|
194
|
+
_eg.add_argument("--enable", action="store_true",
|
|
195
|
+
help="Install + enable the PreToolUse enforcement gate.")
|
|
196
|
+
_eg.add_argument("--disable", action="store_true",
|
|
197
|
+
help="Disable the gate (no-arg `enforce` shows status).")
|
|
198
|
+
|
|
199
|
+
# Internal: the enforcement hook invokes this with the PreToolUse event on
|
|
200
|
+
# stdin; it emits a deny decision only on a match, else nothing.
|
|
201
|
+
subparsers.add_parser(
|
|
202
|
+
"enforce-check",
|
|
203
|
+
help="(internal) decision endpoint the enforcement hook calls.",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Internal: the installed post-commit/pre-push git hooks (git_hooks.py)
|
|
207
|
+
# shell out to this — a real console-script entrypoint, not
|
|
208
|
+
# `python3 -m yoru_cli...`, so it resolves to the correct interpreter
|
|
209
|
+
# under pipx/uv-tool-install regardless of what `python3` means on PATH
|
|
210
|
+
# (B3 slice1 round-2 review finding).
|
|
211
|
+
p_ghr = subparsers.add_parser(
|
|
212
|
+
"git-hook-run",
|
|
213
|
+
help="(internal) invoked by the installed git hooks — captures commit/push events.",
|
|
214
|
+
)
|
|
215
|
+
p_ghr.add_argument("hook_kind", choices=["post-commit", "pre-push"])
|
|
216
|
+
p_ghr.add_argument("hook_args", nargs="*")
|
|
217
|
+
|
|
218
|
+
return parser
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main(argv: list[str] | None = None) -> int:
|
|
222
|
+
parser = _build_parser()
|
|
223
|
+
args = parser.parse_args(argv)
|
|
224
|
+
|
|
225
|
+
if args.cmd == "tail" and args.server is None:
|
|
226
|
+
cfg = config.load() or {}
|
|
227
|
+
args.server = cfg.get("server")
|
|
228
|
+
if not args.server:
|
|
229
|
+
parser.error(
|
|
230
|
+
"no server configured — run `yoru init --server <url>` first, "
|
|
231
|
+
"or pass --server explicitly."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if args.cmd == "init":
|
|
235
|
+
return init_cmd.run(args)
|
|
236
|
+
if args.cmd == "use":
|
|
237
|
+
return use_cmd.run(args)
|
|
238
|
+
if args.cmd == "logout":
|
|
239
|
+
return logout_cmd.run(args)
|
|
240
|
+
if args.cmd == "rotate":
|
|
241
|
+
return rotate_cmd.run(args)
|
|
242
|
+
if args.cmd == "tail":
|
|
243
|
+
return tail_cmd.run(args)
|
|
244
|
+
if args.cmd == "doctor":
|
|
245
|
+
return doctor_cmd.run(args)
|
|
246
|
+
if args.cmd == "share":
|
|
247
|
+
return share_cmd.run(args)
|
|
248
|
+
if args.cmd == "update":
|
|
249
|
+
return update_cmd.run(args)
|
|
250
|
+
if args.cmd == "verify-export":
|
|
251
|
+
return verify_export_cmd.run(args)
|
|
252
|
+
if args.cmd == "enforce":
|
|
253
|
+
return enforce_cmd.run(args)
|
|
254
|
+
if args.cmd == "enforce-check":
|
|
255
|
+
return enforce_cmd.check()
|
|
256
|
+
if args.cmd == "git-hook-run":
|
|
257
|
+
return git_hook_run.main([args.hook_kind, *args.hook_args])
|
|
258
|
+
|
|
259
|
+
parser.error(f"unknown command: {args.cmd!r}")
|
|
260
|
+
return 2
|