codex-statusline 0.2.2__tar.gz → 0.3.1__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.
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/PKG-INFO +9 -7
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/README.md +8 -6
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/PKG-INFO +9 -7
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/SOURCES.txt +2 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/entry_points.txt +1 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/pyproject.toml +2 -1
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/__init__.py +1 -1
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/cli.py +6 -18
- codex_statusline-0.3.1/src/doctor.py +111 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/launcher.py +6 -17
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/render.py +2 -1
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/update.py +9 -3
- codex_statusline-0.3.1/tests/test_doctor.py +74 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_startup.py +2 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_update.py +2 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/LICENSE +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/dependency_links.txt +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/top_level.txt +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/setup.cfg +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/__main__.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/configure.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/data.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/entry.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/hook.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/manage.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_auto_shells.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_configure.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_data.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_fish.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_management.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_render.py +0 -0
- {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_tmux.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-statusline
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A tmux statusline for OpenAI Codex CLI: context usage, 5h and weekly quota, and session tokens at a glance. Local-only.
|
|
5
5
|
Author: Zimo Xu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -59,7 +59,7 @@ Prefer to do it yourself:
|
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
61
|
uv tool install codex-statusline # or: pipx install codex-statusline
|
|
62
|
-
|
|
62
|
+
cxbar install # add --dry-run to see the plan first
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
|
|
@@ -73,17 +73,19 @@ codex # same as always, now with a status bar
|
|
|
73
73
|
codex resume --last
|
|
74
74
|
codex exec 'task' # non-interactive: passed straight through, no bar
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
cxbar config # pick segments, theme and colors interactively
|
|
77
|
+
cxbar update # upgrade, whichever way you installed it
|
|
78
|
+
cxbar doctor # check your setup; prints the fix for anything wrong
|
|
79
|
+
cxbar uninstall
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
`cxbar` is the short name for `codex-statusline`; both work.
|
|
83
|
+
|
|
82
84
|
Uninstall removes only what this tool added and still matches exactly. If you edited its block, it stops instead of overwriting. Backups live in `~/.local/share/codex-statusline/`.
|
|
83
85
|
|
|
84
86
|
## Configuration
|
|
85
87
|
|
|
86
|
-
Run `
|
|
88
|
+
Run `cxbar config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
|
|
87
89
|
|
|
88
90
|
```text
|
|
89
91
|
[x] ctx context used
|
|
@@ -38,7 +38,7 @@ Prefer to do it yourself:
|
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
40
|
uv tool install codex-statusline # or: pipx install codex-statusline
|
|
41
|
-
|
|
41
|
+
cxbar install # add --dry-run to see the plan first
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
|
|
@@ -52,17 +52,19 @@ codex # same as always, now with a status bar
|
|
|
52
52
|
codex resume --last
|
|
53
53
|
codex exec 'task' # non-interactive: passed straight through, no bar
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
cxbar config # pick segments, theme and colors interactively
|
|
56
|
+
cxbar update # upgrade, whichever way you installed it
|
|
57
|
+
cxbar doctor # check your setup; prints the fix for anything wrong
|
|
58
|
+
cxbar uninstall
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
`cxbar` is the short name for `codex-statusline`; both work.
|
|
62
|
+
|
|
61
63
|
Uninstall removes only what this tool added and still matches exactly. If you edited its block, it stops instead of overwriting. Backups live in `~/.local/share/codex-statusline/`.
|
|
62
64
|
|
|
63
65
|
## Configuration
|
|
64
66
|
|
|
65
|
-
Run `
|
|
67
|
+
Run `cxbar config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
|
|
66
68
|
|
|
67
69
|
```text
|
|
68
70
|
[x] ctx context used
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-statusline
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A tmux statusline for OpenAI Codex CLI: context usage, 5h and weekly quota, and session tokens at a glance. Local-only.
|
|
5
5
|
Author: Zimo Xu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -59,7 +59,7 @@ Prefer to do it yourself:
|
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
61
|
uv tool install codex-statusline # or: pipx install codex-statusline
|
|
62
|
-
|
|
62
|
+
cxbar install # add --dry-run to see the plan first
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
|
|
@@ -73,17 +73,19 @@ codex # same as always, now with a status bar
|
|
|
73
73
|
codex resume --last
|
|
74
74
|
codex exec 'task' # non-interactive: passed straight through, no bar
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
cxbar config # pick segments, theme and colors interactively
|
|
77
|
+
cxbar update # upgrade, whichever way you installed it
|
|
78
|
+
cxbar doctor # check your setup; prints the fix for anything wrong
|
|
79
|
+
cxbar uninstall
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
`cxbar` is the short name for `codex-statusline`; both work.
|
|
83
|
+
|
|
82
84
|
Uninstall removes only what this tool added and still matches exactly. If you edited its block, it stops instead of overwriting. Backups live in `~/.local/share/codex-statusline/`.
|
|
83
85
|
|
|
84
86
|
## Configuration
|
|
85
87
|
|
|
86
|
-
Run `
|
|
88
|
+
Run `cxbar config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
|
|
87
89
|
|
|
88
90
|
```text
|
|
89
91
|
[x] ctx context used
|
|
@@ -11,6 +11,7 @@ src/__main__.py
|
|
|
11
11
|
src/cli.py
|
|
12
12
|
src/configure.py
|
|
13
13
|
src/data.py
|
|
14
|
+
src/doctor.py
|
|
14
15
|
src/entry.py
|
|
15
16
|
src/hook.py
|
|
16
17
|
src/launcher.py
|
|
@@ -20,6 +21,7 @@ src/update.py
|
|
|
20
21
|
tests/test_auto_shells.py
|
|
21
22
|
tests/test_configure.py
|
|
22
23
|
tests/test_data.py
|
|
24
|
+
tests/test_doctor.py
|
|
23
25
|
tests/test_fish.py
|
|
24
26
|
tests/test_management.py
|
|
25
27
|
tests/test_render.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codex-statusline"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "A tmux statusline for OpenAI Codex CLI: context usage, 5h and weekly quota, and session tokens at a glance. Local-only."
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
license = "MIT"
|
|
@@ -23,6 +23,7 @@ Changelog = "https://github.com/Moviw/codex-statusline/blob/main/CHANGELOG.md"
|
|
|
23
23
|
|
|
24
24
|
[project.scripts]
|
|
25
25
|
codex-statusline = "src.cli:main"
|
|
26
|
+
cxbar = "src.cli:main" # short alias
|
|
26
27
|
|
|
27
28
|
[build-system]
|
|
28
29
|
requires = ["setuptools>=77"]
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import shutil
|
|
5
2
|
import sys
|
|
6
3
|
from pathlib import Path
|
|
7
4
|
|
|
@@ -10,7 +7,7 @@ from . import __version__
|
|
|
10
7
|
|
|
11
8
|
def main():
|
|
12
9
|
args = sys.argv[1:]
|
|
13
|
-
from .launcher import child, launch,
|
|
10
|
+
from .launcher import child, launch, raw
|
|
14
11
|
|
|
15
12
|
if args and args[0] in ("launch", "raw"):
|
|
16
13
|
try:
|
|
@@ -25,11 +22,11 @@ def main():
|
|
|
25
22
|
record()
|
|
26
23
|
return 0
|
|
27
24
|
parser = argparse.ArgumentParser(
|
|
28
|
-
prog="codex-statusline",
|
|
25
|
+
prog="cxbar" if Path(sys.argv[0]).name == "cxbar" else "codex-statusline",
|
|
29
26
|
description="A status bar for Codex CLI: context, 5h/weekly quota, and tokens. "
|
|
30
27
|
"Run `codex` as usual once installed.",
|
|
31
28
|
)
|
|
32
|
-
parser.add_argument("--version", action="version", version=__version__)
|
|
29
|
+
parser.add_argument("-V", "-v", "--version", action="version", version=__version__)
|
|
33
30
|
sub = parser.add_subparsers(dest="action", required=True, metavar="<command>", title="commands")
|
|
34
31
|
commands = {
|
|
35
32
|
"install": "hook the status bar into `codex` for your shells",
|
|
@@ -68,18 +65,9 @@ def main():
|
|
|
68
65
|
|
|
69
66
|
return update()
|
|
70
67
|
if ns.action == "doctor":
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"codex": path,
|
|
75
|
-
"codex_version": ".".join(map(str, version(path))),
|
|
76
|
-
"tmux": shutil.which("tmux"),
|
|
77
|
-
"shell": os.environ.get("SHELL"),
|
|
78
|
-
"tested_codex": "0.159.0",
|
|
79
|
-
"credentials_read": False,
|
|
80
|
-
}
|
|
81
|
-
print(json.dumps(data, indent=2))
|
|
82
|
-
return 0 if data["tmux"] and version(path) >= (0, 159, 0) else 1
|
|
68
|
+
from .doctor import doctor
|
|
69
|
+
|
|
70
|
+
return doctor()
|
|
83
71
|
except (OSError, ValueError, RuntimeError, KeyError) as error:
|
|
84
72
|
print(f"codex-statusline: {error}", file=sys.stderr)
|
|
85
73
|
return 1
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""`cxbar doctor`: one line per check, and the command that fixes each problem."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tomllib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .launcher import official, version
|
|
11
|
+
from .manage import STATUS_MESSAGE, hook_group, paths, text, tilde
|
|
12
|
+
from .update import is_newer, latest_version
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def command_name() -> str:
|
|
16
|
+
return "cxbar" if shutil.which("cxbar") else "codex-statusline"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def checks(home: Path, latest=latest_version) -> list[tuple[bool | None, str, str | None]]:
|
|
20
|
+
"""(ok, message, fix): ok is True, False, or None for information only."""
|
|
21
|
+
cmd = command_name()
|
|
22
|
+
out = []
|
|
23
|
+
try:
|
|
24
|
+
found = version(official())
|
|
25
|
+
shown = ".".join(map(str, found))
|
|
26
|
+
out.append((found >= (0, 159, 0), f"Codex CLI {shown}", "upgrade Codex CLI to 0.159.0+"))
|
|
27
|
+
except (RuntimeError, OSError, subprocess.TimeoutExpired):
|
|
28
|
+
out.append((False, "Codex CLI not found on PATH", "install Codex CLI"))
|
|
29
|
+
tmux = shutil.which("tmux")
|
|
30
|
+
if tmux:
|
|
31
|
+
tmux_version = subprocess.run([tmux, "-V"], capture_output=True, text=True).stdout.strip()
|
|
32
|
+
out.append((True, tmux_version or "tmux", None))
|
|
33
|
+
else:
|
|
34
|
+
out.append((False, "tmux not found", "brew install tmux / sudo apt install tmux"))
|
|
35
|
+
|
|
36
|
+
codex_home, _, manifest_path = paths(home, "all")
|
|
37
|
+
try:
|
|
38
|
+
manifest = json.loads(text(manifest_path) or "{}")
|
|
39
|
+
except ValueError:
|
|
40
|
+
manifest = {}
|
|
41
|
+
if not manifest:
|
|
42
|
+
out.append((False, "not hooked into your shell yet", f"{cmd} install"))
|
|
43
|
+
return out + [_update_check(cmd, latest)]
|
|
44
|
+
|
|
45
|
+
if manifest.get("hook_group") != hook_group():
|
|
46
|
+
other = manifest["hook_group"]["hooks"][0]["command"].removesuffix(" hook")
|
|
47
|
+
out.append((False, f"codex runs another copy: {other}", f"{cmd} install"))
|
|
48
|
+
else:
|
|
49
|
+
shells = [Path(item["path"]) for item in manifest.get("shells", [])]
|
|
50
|
+
missing = [p for p in shells if item_block(manifest, p) not in text(p)]
|
|
51
|
+
names = ", ".join(tilde(p, home) for p in shells)
|
|
52
|
+
out.append((not missing, f"codex is hooked into {names}", f"{cmd} install"))
|
|
53
|
+
|
|
54
|
+
hooks_path = Path(manifest["hooks_path"])
|
|
55
|
+
try:
|
|
56
|
+
groups = json.loads(text(hooks_path) or "{}").get("hooks", {}).get("SessionStart", [])
|
|
57
|
+
except ValueError:
|
|
58
|
+
groups = []
|
|
59
|
+
index = next((i for i, g in enumerate(groups) if g == manifest["hook_group"]), None)
|
|
60
|
+
if index is None:
|
|
61
|
+
out.append((False, f"hook missing from {tilde(hooks_path, home)}", f"{cmd} install"))
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
state = (
|
|
65
|
+
tomllib.loads(text(codex_home / "config.toml")).get("hooks", {}).get("state", {})
|
|
66
|
+
)
|
|
67
|
+
except tomllib.TOMLDecodeError:
|
|
68
|
+
state = {}
|
|
69
|
+
# Codex keys trust by the hooks.json path as it sees it; compare resolved paths so a
|
|
70
|
+
# symlinked ~/.codex (or macOS /var -> /private/var) still matches.
|
|
71
|
+
suffix = f":session_start:{index}:0"
|
|
72
|
+
trusted = any(
|
|
73
|
+
key.endswith(suffix)
|
|
74
|
+
and Path(key.removesuffix(suffix)).resolve() == hooks_path.resolve()
|
|
75
|
+
and "trusted_hash" in value
|
|
76
|
+
for key, value in state.items()
|
|
77
|
+
if isinstance(value, dict)
|
|
78
|
+
)
|
|
79
|
+
out.append(
|
|
80
|
+
(True, "hook approved in Codex", None)
|
|
81
|
+
if trusted
|
|
82
|
+
else (
|
|
83
|
+
False,
|
|
84
|
+
"hook not approved in Codex yet",
|
|
85
|
+
f"start codex, approve '{STATUS_MESSAGE}'",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return out + [_update_check(cmd, latest)]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def item_block(manifest: dict, path: Path) -> str:
|
|
92
|
+
return next(i["block"] for i in manifest["shells"] if Path(i["path"]) == path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _update_check(cmd: str, latest) -> tuple[bool | None, str, str | None]:
|
|
96
|
+
found = latest()
|
|
97
|
+
if found is None:
|
|
98
|
+
return (None, "could not check for updates (offline?)", None)
|
|
99
|
+
if is_newer(found):
|
|
100
|
+
return (False, f"{found} available", f"{cmd} update")
|
|
101
|
+
return (True, "latest version", None)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def doctor(home: Path | None = None) -> int:
|
|
105
|
+
results = checks(home or Path.home())
|
|
106
|
+
print(f"codex-statusline {__version__}")
|
|
107
|
+
for ok, message, fix in results:
|
|
108
|
+
print(f" {'✓' if ok else '✗' if ok is False else '·'} {message}")
|
|
109
|
+
if ok is False and fix:
|
|
110
|
+
print(f" → {fix}")
|
|
111
|
+
return 1 if any(ok is False for ok, _, _ in results) else 0
|
|
@@ -143,19 +143,6 @@ def binding_for(bindings, hint, launch_id):
|
|
|
143
143
|
return candidates[0] if len(candidates) == 1 else None
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
def branch(cwd):
|
|
147
|
-
try:
|
|
148
|
-
p = subprocess.run(
|
|
149
|
-
["git", "-C", cwd, "symbolic-ref", "--quiet", "--short", "HEAD"],
|
|
150
|
-
capture_output=True,
|
|
151
|
-
text=True,
|
|
152
|
-
timeout=0.7,
|
|
153
|
-
)
|
|
154
|
-
return p.stdout.strip() if p.returncode == 0 else None
|
|
155
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
156
|
-
return None
|
|
157
|
-
|
|
158
|
-
|
|
159
146
|
def placeholder(width=80):
|
|
160
147
|
"""Use the normal layout before telemetry arrives or while it is unavailable."""
|
|
161
148
|
from .data import newest_quotas, sessions_dir
|
|
@@ -216,6 +203,10 @@ def launch(args):
|
|
|
216
203
|
"set -g allow-rename off\n"
|
|
217
204
|
"set -g set-titles off\n"
|
|
218
205
|
"set -g mouse on\n"
|
|
206
|
+
# Every key belongs to Codex: no prefix (Ctrl-B), no wait after Esc.
|
|
207
|
+
"set -g prefix None\n"
|
|
208
|
+
"unbind C-b\n"
|
|
209
|
+
"set -s escape-time 10\n"
|
|
219
210
|
"set -g history-limit 10000\n"
|
|
220
211
|
"set -g remain-on-exit on\n"
|
|
221
212
|
"set -g exit-empty on\n"
|
|
@@ -247,6 +238,7 @@ def launch(args):
|
|
|
247
238
|
cfg = load_config()
|
|
248
239
|
reader = LogReader()
|
|
249
240
|
latest = {}
|
|
241
|
+
updater = "cxbar" if shutil.which("cxbar") else "codex-statusline"
|
|
250
242
|
if cfg["update_check"]:
|
|
251
243
|
from .update import newer_version
|
|
252
244
|
|
|
@@ -254,7 +246,6 @@ def launch(args):
|
|
|
254
246
|
target=lambda: latest.update(version=newer_version()), daemon=True
|
|
255
247
|
).start()
|
|
256
248
|
sessions, readers, others, scan_at = sessions_dir(), {}, {}, 0.0
|
|
257
|
-
last_branch, branch_at = None, 0
|
|
258
249
|
last_line = None
|
|
259
250
|
while not stop.is_set():
|
|
260
251
|
try:
|
|
@@ -290,9 +281,7 @@ def launch(args):
|
|
|
290
281
|
)
|
|
291
282
|
state["quotas"] = merge_quotas(state["quotas"], others)
|
|
292
283
|
state["update"] = latest.get("version")
|
|
293
|
-
|
|
294
|
-
last_branch, branch_at = branch(state["cwd"]), time.monotonic()
|
|
295
|
-
state["branch"] = last_branch
|
|
284
|
+
state["updater"] = updater
|
|
296
285
|
line = render(
|
|
297
286
|
state,
|
|
298
287
|
int(width or 80),
|
|
@@ -278,7 +278,8 @@ def render(
|
|
|
278
278
|
hint = None
|
|
279
279
|
if isinstance(update, str) and re.fullmatch(r"\d+(\.\d+)*", update):
|
|
280
280
|
arrow = "^" if ascii_only else "↑"
|
|
281
|
-
|
|
281
|
+
tool = "cxbar" if state.get("updater") == "cxbar" else "codex-statusline"
|
|
282
|
+
hint = (f"{arrow} update available: {tool} update", f"{arrow} update", "warning")
|
|
282
283
|
head, rest = parts[0], parts[1:]
|
|
283
284
|
compact_head = (head[1], head[2])
|
|
284
285
|
# Widest first: all detail, compact head, all compact, then drop segments.
|
|
@@ -24,8 +24,8 @@ def is_newer(latest: str, current: str = __version__) -> bool:
|
|
|
24
24
|
return bool(_parse(latest)) and _parse(latest) > _parse(current)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def
|
|
28
|
-
"""Latest release
|
|
27
|
+
def latest_version(timeout: float = 2) -> str | None:
|
|
28
|
+
"""Latest release on PyPI; None on any failure. Sends only a plain GET."""
|
|
29
29
|
try:
|
|
30
30
|
request = urllib.request.Request(
|
|
31
31
|
PYPI, headers={"User-Agent": f"codex-statusline/{__version__}"}
|
|
@@ -34,7 +34,13 @@ def newer_version(timeout: float = 2) -> str | None:
|
|
|
34
34
|
latest = json.load(response)["info"]["version"]
|
|
35
35
|
except Exception:
|
|
36
36
|
return None
|
|
37
|
-
return latest if isinstance(latest, str)
|
|
37
|
+
return latest if isinstance(latest, str) else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def newer_version(timeout: float = 2) -> str | None:
|
|
41
|
+
"""Latest release if newer than this one; None otherwise or on any failure."""
|
|
42
|
+
latest = latest_version(timeout)
|
|
43
|
+
return latest if latest and is_newer(latest) else None
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
def upgrade_command(prefix: str = sys.prefix, package: Path = PACKAGE) -> list[str]:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import contextlib
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
from src import doctor, manage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DoctorTests(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self.temp = tempfile.TemporaryDirectory()
|
|
17
|
+
# A symlinked home, like macOS /var -> /private/var or a linked ~/.codex.
|
|
18
|
+
real = Path(self.temp.name) / "real"
|
|
19
|
+
real.mkdir()
|
|
20
|
+
self.home = Path(self.temp.name) / "link"
|
|
21
|
+
self.home.symlink_to(real)
|
|
22
|
+
env = {"CODEX_HOME": str(self.home / ".codex"), "ZDOTDIR": str(self.home)}
|
|
23
|
+
for p in (
|
|
24
|
+
patch.dict(os.environ, env),
|
|
25
|
+
patch.object(doctor, "official", return_value="/bin/codex"),
|
|
26
|
+
patch.object(doctor, "version", return_value=(0, 159, 0)),
|
|
27
|
+
):
|
|
28
|
+
p.start()
|
|
29
|
+
self.addCleanup(p.stop)
|
|
30
|
+
self.addCleanup(self.temp.cleanup)
|
|
31
|
+
|
|
32
|
+
def run_checks(self, latest="0.0.1"):
|
|
33
|
+
return {msg: (ok, fix) for ok, msg, fix in doctor.checks(self.home, lambda: latest)}
|
|
34
|
+
|
|
35
|
+
def install(self):
|
|
36
|
+
args = argparse.Namespace(home=str(self.home), shell="zsh", yes=True, dry_run=False)
|
|
37
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
38
|
+
manage.installation(args)
|
|
39
|
+
|
|
40
|
+
def test_not_installed_points_to_install(self):
|
|
41
|
+
results = self.run_checks()
|
|
42
|
+
ok, fix = results["not hooked into your shell yet"]
|
|
43
|
+
self.assertFalse(ok)
|
|
44
|
+
self.assertTrue(fix.endswith(" install"))
|
|
45
|
+
|
|
46
|
+
def test_installed_hook_waits_for_approval_then_passes(self):
|
|
47
|
+
self.install()
|
|
48
|
+
self.assertFalse(self.run_checks()["hook not approved in Codex yet"][0])
|
|
49
|
+
hooks = self.home / ".codex" / "hooks.json"
|
|
50
|
+
key = f"{hooks}:session_start:0:0"
|
|
51
|
+
(self.home / ".codex" / "config.toml").write_text(
|
|
52
|
+
f'[hooks.state."{key}"]\ntrusted_hash = "sha256:x"\n'
|
|
53
|
+
)
|
|
54
|
+
results = self.run_checks()
|
|
55
|
+
self.assertTrue(results["hook approved in Codex"][0])
|
|
56
|
+
self.assertTrue(results["codex is hooked into ~/.zshrc"][0])
|
|
57
|
+
self.assertTrue(results["latest version"][0])
|
|
58
|
+
|
|
59
|
+
def test_stale_copy_and_update_are_flagged(self):
|
|
60
|
+
self.install()
|
|
61
|
+
with patch.object(doctor, "hook_group", return_value={"hooks": [{"command": "new"}]}):
|
|
62
|
+
results = self.run_checks(latest="99.0.0")
|
|
63
|
+
stale = [m for m in results if m.startswith("codex runs another copy")]
|
|
64
|
+
self.assertEqual(len(stale), 1)
|
|
65
|
+
self.assertEqual(results["99.0.0 available"][1].split()[-1], "update")
|
|
66
|
+
self.assertIsNone(doctor._update_check("cxbar", lambda: None)[0])
|
|
67
|
+
|
|
68
|
+
def test_doctor_exit_code(self):
|
|
69
|
+
with contextlib.redirect_stdout(io.StringIO()) as out:
|
|
70
|
+
with patch.object(doctor, "latest_version", return_value=None):
|
|
71
|
+
code = doctor.doctor(self.home)
|
|
72
|
+
self.assertEqual(code, 1)
|
|
73
|
+
self.assertIn("✗ not hooked into your shell yet", out.getvalue())
|
|
74
|
+
json.dumps(out.getvalue()) # plain text, printable
|
|
@@ -118,6 +118,8 @@ class StartupTests(unittest.TestCase):
|
|
|
118
118
|
self.assertIn("CTX USED", conf)
|
|
119
119
|
# Codex asks tmux for #{mouse}; off makes the wheel recall old prompts.
|
|
120
120
|
self.assertIn("set -g mouse on", conf)
|
|
121
|
+
self.assertIn("set -g prefix None", conf)
|
|
122
|
+
self.assertIn("set -s escape-time 10", conf)
|
|
121
123
|
self.assertNotIn("binding pending", conf)
|
|
122
124
|
# error.json is written before the fallback status is applied.
|
|
123
125
|
# Faster Linux startup can observe that intermediate state.
|
|
@@ -43,6 +43,8 @@ class UpdateTests(unittest.TestCase):
|
|
|
43
43
|
state = {"context_used": 10, "now": 1000, "update": "0.2.0", "quotas": {}}
|
|
44
44
|
wide = render(state, 240, tmux=False)
|
|
45
45
|
self.assertIn("↑ update available: codex-statusline update", wide)
|
|
46
|
+
short = render(dict(state, updater="cxbar"), 240, tmux=False)
|
|
47
|
+
self.assertIn("↑ update available: cxbar update", short)
|
|
46
48
|
self.assertIn("^ update available", render(state, 240, tmux=False, ascii_only=True))
|
|
47
49
|
self.assertNotIn("update", render(state, 40, tmux=False))
|
|
48
50
|
self.assertNotIn("update", render(dict(state, update="#(evil)"), 240, tmux=False))
|
|
File without changes
|
{codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/dependency_links.txt
RENAMED
|
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
|