uxon 3.0.0__py3-none-any.whl
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.
- uxon/__init__.py +8 -0
- uxon/__main__.py +12 -0
- uxon/agents.py +154 -0
- uxon/cli.py +2858 -0
- uxon/git_backend_gh.py +152 -0
- uxon/git_backend_token.py +269 -0
- uxon/git_create.py +307 -0
- uxon/git_profiles.py +171 -0
- uxon/settings.py +424 -0
- uxon/tui/__init__.py +64 -0
- uxon/tui/app.py +407 -0
- uxon/tui/context.py +366 -0
- uxon/tui/events.py +115 -0
- uxon/tui/hints.py +14 -0
- uxon/tui/launch.py +70 -0
- uxon/tui/screens/__init__.py +5 -0
- uxon/tui/screens/agents_unavailable.py +76 -0
- uxon/tui/screens/confirm.py +125 -0
- uxon/tui/screens/existing.py +117 -0
- uxon/tui/screens/git_profile.py +105 -0
- uxon/tui/screens/git_remotes.py +52 -0
- uxon/tui/screens/launch_options.py +239 -0
- uxon/tui/screens/main.py +699 -0
- uxon/tui/screens/new_project.py +114 -0
- uxon/tui/screens/settings.py +479 -0
- uxon/tui/state.py +373 -0
- uxon/tui/styles.tcss +149 -0
- uxon/tui/widgets/__init__.py +13 -0
- uxon/tui/widgets/action_row.py +132 -0
- uxon/tui/widgets/session_table.py +177 -0
- uxon-3.0.0.dist-info/METADATA +373 -0
- uxon-3.0.0.dist-info/RECORD +35 -0
- uxon-3.0.0.dist-info/WHEEL +4 -0
- uxon-3.0.0.dist-info/entry_points.txt +2 -0
- uxon-3.0.0.dist-info/licenses/LICENSE +21 -0
uxon/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""uxon — multi-user wrapper for terminal AI coding agent sessions.
|
|
2
|
+
|
|
3
|
+
The single source of truth for the package version. Hatch reads this via
|
|
4
|
+
``[tool.hatch.version].path`` so wheels, sdists, the in-tree CLI, and
|
|
5
|
+
``uxon version`` all agree without a separate ``VERSION`` file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "3.0.0"
|
uxon/__main__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Allow ``python -m uxon`` to invoke the CLI entrypoint.
|
|
2
|
+
|
|
3
|
+
The console-script generated from ``[project.scripts]`` calls
|
|
4
|
+
``uxon.cli:main`` directly; this module exists so the same entrypoint
|
|
5
|
+
works under ``python -m uxon`` (used by the pty test harness and by
|
|
6
|
+
in-tree development without re-installing the script).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from uxon.cli import main
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
raise SystemExit(main())
|
uxon/agents.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Agent catalog: declarative data for claude / codex / cursor-agent.
|
|
2
|
+
|
|
3
|
+
Pure data + small helpers. No textual, no TUI, no cli imports.
|
|
4
|
+
`probe_agents` uses subprocess locally but never at module scope.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import pwd
|
|
11
|
+
import subprocess
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PermissionMode:
|
|
18
|
+
id: str # "normal" | "auto" | "yolo"
|
|
19
|
+
label: str # user-facing in TUI
|
|
20
|
+
flags: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AgentSpec:
|
|
25
|
+
id: str # "claude" | "codex" | "cursor"
|
|
26
|
+
binary: str # executable name on PATH
|
|
27
|
+
session_suffix: str # "@<id>"
|
|
28
|
+
permission_modes: tuple[PermissionMode, ...]
|
|
29
|
+
install_hint: str # shown by doctor
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
CATALOG: dict[str, AgentSpec] = {
|
|
33
|
+
"claude": AgentSpec(
|
|
34
|
+
id="claude",
|
|
35
|
+
binary="claude",
|
|
36
|
+
session_suffix="@claude",
|
|
37
|
+
permission_modes=(
|
|
38
|
+
PermissionMode("normal", "normal", ()),
|
|
39
|
+
PermissionMode("auto", "auto", ("--permission-mode", "auto")),
|
|
40
|
+
PermissionMode("yolo", "yolo (--dsp)", ("--dangerously-skip-permissions",)),
|
|
41
|
+
),
|
|
42
|
+
install_hint="see https://docs.claude.com/claude-code",
|
|
43
|
+
),
|
|
44
|
+
"codex": AgentSpec(
|
|
45
|
+
id="codex",
|
|
46
|
+
binary="codex",
|
|
47
|
+
session_suffix="@codex",
|
|
48
|
+
permission_modes=(
|
|
49
|
+
PermissionMode("normal", "normal", ()),
|
|
50
|
+
PermissionMode("auto", "auto (--full-auto)", ("--full-auto",)),
|
|
51
|
+
PermissionMode(
|
|
52
|
+
"yolo",
|
|
53
|
+
"yolo (--dangerously-bypass-approvals-and-sandbox)",
|
|
54
|
+
("--dangerously-bypass-approvals-and-sandbox",),
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
install_hint="install: npm i -g @openai/codex",
|
|
58
|
+
),
|
|
59
|
+
"cursor": AgentSpec(
|
|
60
|
+
id="cursor",
|
|
61
|
+
binary="cursor-agent",
|
|
62
|
+
session_suffix="@cursor",
|
|
63
|
+
permission_modes=(
|
|
64
|
+
PermissionMode("normal", "normal", ()),
|
|
65
|
+
PermissionMode("yolo", "yolo (--yolo)", ("--yolo",)),
|
|
66
|
+
),
|
|
67
|
+
install_hint="install: curl https://cursor.com/install -fsSL | bash",
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def permission_mode_for(agent: AgentSpec, mode_id: str) -> PermissionMode | None:
|
|
73
|
+
for mode in agent.permission_modes:
|
|
74
|
+
if mode.id == mode_id:
|
|
75
|
+
return mode
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Availability probe ───────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class AgentAvailability:
|
|
84
|
+
status: str # "pending" | "ok" | "missing" | "timeout"
|
|
85
|
+
path: str | None = None
|
|
86
|
+
version: str | None = None
|
|
87
|
+
error: str | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
PROBE_TIMEOUT_SEC = (
|
|
91
|
+
10.0 # cursor-agent --version is slow (~5-8s); probe is async + parallel, non-blocking
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _current_user() -> str:
|
|
96
|
+
return pwd.getpwuid(os.getuid()).pw_name
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _build_probe_cmd(binary: str, launch_user: str | None) -> list[str]:
|
|
100
|
+
if launch_user and launch_user != _current_user():
|
|
101
|
+
# Match the login-env semantics that ``command_prefix_for_user``
|
|
102
|
+
# in ``uxon.cli`` uses for the actual launch (``sudo -iu``). The
|
|
103
|
+
# ``-i`` loads the target user's login shell so ``PATH`` picks
|
|
104
|
+
# up npm-global / nvm / ``~/.local/bin`` entries where agents
|
|
105
|
+
# like ``claude`` and ``cursor-agent`` are typically installed.
|
|
106
|
+
# Without ``-i``, sudo's ``secure_path`` hides them and the
|
|
107
|
+
# probe reports "missing" for agents that the launch can
|
|
108
|
+
# actually run.
|
|
109
|
+
return ["sudo", "-niu", launch_user, "--", binary, "--version"]
|
|
110
|
+
return [binary, "--version"]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _probe_one(binary: str, launch_user: str | None) -> AgentAvailability:
|
|
114
|
+
cmd = _build_probe_cmd(binary, launch_user)
|
|
115
|
+
try:
|
|
116
|
+
cp = subprocess.run(
|
|
117
|
+
cmd,
|
|
118
|
+
capture_output=True,
|
|
119
|
+
text=True,
|
|
120
|
+
timeout=PROBE_TIMEOUT_SEC,
|
|
121
|
+
)
|
|
122
|
+
except FileNotFoundError as exc:
|
|
123
|
+
return AgentAvailability(status="missing", error=str(exc))
|
|
124
|
+
except subprocess.TimeoutExpired:
|
|
125
|
+
return AgentAvailability(status="timeout")
|
|
126
|
+
if cp.returncode != 0:
|
|
127
|
+
err = (cp.stderr or cp.stdout or "").strip()
|
|
128
|
+
return AgentAvailability(status="missing", error=err or f"exit {cp.returncode}")
|
|
129
|
+
version = (cp.stdout or "").strip().splitlines()[0] if cp.stdout else None
|
|
130
|
+
return AgentAvailability(status="ok", version=version)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def probe_agents(
|
|
134
|
+
agent_ids: list[str],
|
|
135
|
+
launch_user: str | None,
|
|
136
|
+
) -> dict[str, AgentAvailability]:
|
|
137
|
+
"""Probe `<binary> --version` for each known agent id, in parallel.
|
|
138
|
+
|
|
139
|
+
Unknown ids are silently dropped. Runs under ``launch_user`` when
|
|
140
|
+
different from the caller, via ``sudo -n -u <user>``.
|
|
141
|
+
"""
|
|
142
|
+
valid = [aid for aid in agent_ids if aid in CATALOG]
|
|
143
|
+
if not valid:
|
|
144
|
+
return {}
|
|
145
|
+
results: dict[str, AgentAvailability] = {}
|
|
146
|
+
with ThreadPoolExecutor(max_workers=max(1, len(valid))) as ex:
|
|
147
|
+
futures = {ex.submit(_probe_one, CATALOG[aid].binary, launch_user): aid for aid in valid}
|
|
148
|
+
for fut in futures:
|
|
149
|
+
aid = futures[fut]
|
|
150
|
+
try:
|
|
151
|
+
results[aid] = fut.result()
|
|
152
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
153
|
+
results[aid] = AgentAvailability(status="missing", error=str(exc))
|
|
154
|
+
return results
|