rexdeck 0.1.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.
@@ -0,0 +1,218 @@
1
+ """Start unmodified Yazi in a native Rex terminal and restore its exact origin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ import socket
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from urllib.parse import unquote, urlsplit
14
+
15
+
16
+ def directory(raw: str) -> str:
17
+ """Decode only an explicit absolute local directory, never a fallback."""
18
+ if not raw or "\x00" in raw:
19
+ raise ValueError(
20
+ "An explicit local directory without NUL characters is required"
21
+ )
22
+ raw.encode("utf-8", errors="strict")
23
+ if raw.lower().startswith("file:"):
24
+ if any(ord(char) < 32 or ord(char) == 127 for char in raw):
25
+ raise ValueError("File URI contains unescaped control characters")
26
+ uri = urlsplit(raw)
27
+ authority = uri.netloc.casefold()
28
+ if (
29
+ authority not in ("", "localhost")
30
+ and authority != socket.gethostname().casefold()
31
+ and authority != socket.getfqdn().casefold()
32
+ ):
33
+ raise ValueError("Remote or credentialed file URI is not supported")
34
+ if "?" in raw or "#" in raw:
35
+ raise ValueError("File URI must not contain a query or fragment")
36
+ if re.search(r"%(?![0-9a-fA-F]{2})", uri.path):
37
+ raise ValueError("File URI contains invalid percent encoding")
38
+ raw = unquote(uri.path, encoding="utf-8", errors="strict")
39
+ if "\x00" in raw or not raw.startswith("/") or raw.startswith("//"):
40
+ raise ValueError("An absolute local directory is required")
41
+ try:
42
+ path = Path(raw).resolve(strict=True)
43
+ except RuntimeError as error:
44
+ # Python 3.12 reports symlink loops as RuntimeError rather than OSError.
45
+ raise ValueError(str(error)) from error
46
+ if not path.is_dir():
47
+ raise ValueError("The invoking terminal's cwd is not a directory")
48
+ return str(path)
49
+
50
+
51
+ def git_directory(raw: str) -> str:
52
+ """Keep cwd-based Git discovery; reject inherited repository redirection."""
53
+ overrides = {
54
+ "GIT_DIR",
55
+ "GIT_WORK_TREE",
56
+ "GIT_COMMON_DIR",
57
+ "GIT_INDEX_FILE",
58
+ "GIT_OBJECT_DIRECTORY",
59
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
60
+ "GIT_NAMESPACE",
61
+ "GIT_CONFIG",
62
+ "GIT_CONFIG_COUNT",
63
+ "GIT_CONFIG_PARAMETERS",
64
+ "GIT_CEILING_DIRECTORIES",
65
+ "GIT_DISCOVERY_ACROSS_FILESYSTEM",
66
+ }
67
+ selected = sorted(
68
+ key
69
+ for key in os.environ
70
+ if key in overrides or key.startswith(("GIT_CONFIG_KEY_", "GIT_CONFIG_VALUE_"))
71
+ )
72
+ if selected:
73
+ raise ValueError("Ambiguous inherited Git override: " + ", ".join(selected))
74
+ cwd = directory(raw)
75
+ search_path = os.pathsep.join(
76
+ entry
77
+ for entry in os.environ.get("PATH", os.defpath).split(os.pathsep)
78
+ if Path(entry).is_absolute()
79
+ )
80
+ git = shutil.which("git", path=search_path) if search_path else None
81
+ if git is None:
82
+ raise ValueError(
83
+ "Required executable not found in absolute PATH directories: git"
84
+ )
85
+ result = subprocess.run(
86
+ [git, "rev-parse", "--is-inside-work-tree"],
87
+ cwd=cwd,
88
+ capture_output=True,
89
+ text=True,
90
+ timeout=10,
91
+ check=False,
92
+ )
93
+ if result.returncode:
94
+ raise ValueError(result.stderr.strip() or f"Git exited {result.returncode}")
95
+ if result.stdout.strip() != "true":
96
+ raise ValueError("Invoking directory is not a non-bare Git worktree")
97
+ return cwd
98
+
99
+
100
+ def acknowledge() -> None:
101
+ if sys.stdin.isatty():
102
+ try:
103
+ input("Press Enter to close RexDeck: ")
104
+ except (EOFError, KeyboardInterrupt):
105
+ pass
106
+
107
+
108
+ def restore_focus(rex: str, server: str, session: str, origin: str) -> None:
109
+ """Use API identities, not CLI label/suffix/directional target resolution."""
110
+ environment = dict(os.environ)
111
+ environment.pop("REX_SESSION", None)
112
+ environment.pop("REX_BLOCK", None)
113
+ environment.pop("REX_CLIENT", None)
114
+ result = subprocess.run(
115
+ [
116
+ rex,
117
+ "-S",
118
+ server,
119
+ "--autostart=false",
120
+ "--timeout=10s",
121
+ "api",
122
+ "call",
123
+ "session.focus_block",
124
+ json.dumps({"session_id": session, "block_id": origin}),
125
+ ],
126
+ env=environment,
127
+ capture_output=True,
128
+ text=True,
129
+ timeout=15,
130
+ check=False,
131
+ )
132
+ if result.returncode:
133
+ raise RuntimeError(f"Rex exited {result.returncode}: {result.stderr.strip()}")
134
+
135
+
136
+ def run(
137
+ yazi: str,
138
+ rex: str,
139
+ server: str,
140
+ session: str,
141
+ origin: str,
142
+ cwd: str,
143
+ *,
144
+ plugin: str = "files",
145
+ ) -> int:
146
+ """Launch the selected native tool at validated cwd, then restore origin focus."""
147
+ title = "Git" if plugin == "git" else "Files"
148
+ tool = "lazygit" if plugin == "git" else "Yazi"
149
+ exit_code = 1
150
+ try:
151
+ if not all((server, session, origin)):
152
+ raise ValueError(
153
+ "Explicit Rex server, session and origin identities are required"
154
+ )
155
+ if not Path(yazi).is_absolute() or not Path(rex).is_absolute():
156
+ raise ValueError("Absolute tool and Rex executable paths are required")
157
+ checked_cwd = git_directory(cwd) if plugin == "git" else directory(cwd)
158
+ # Inherit the actual pane TTY and the tool's normal configuration.
159
+ exit_code = subprocess.run([yazi], cwd=checked_cwd, check=False).returncode
160
+ if exit_code < 0:
161
+ exit_code = 128 - exit_code
162
+ if exit_code:
163
+ print(f"RexDeck {title}: {tool} exited {exit_code}", file=sys.stderr)
164
+ acknowledge()
165
+ except KeyboardInterrupt:
166
+ exit_code = 130
167
+ print(f"RexDeck {title}: {tool} interrupted", file=sys.stderr)
168
+ acknowledge()
169
+ except (OSError, ValueError, subprocess.SubprocessError) as error:
170
+ print(f"RexDeck {title}: cannot launch {tool}: {error}", file=sys.stderr)
171
+ acknowledge()
172
+ try:
173
+ if not all((server, session, origin)) or not Path(rex).is_absolute():
174
+ raise ValueError(
175
+ "Cannot restore focus without explicit Rex origin and executable"
176
+ )
177
+ restore_focus(rex, server, session, origin)
178
+ except KeyboardInterrupt:
179
+ print(
180
+ f"RexDeck {title}: return focus to original pane {origin!r} interrupted; "
181
+ "focus outcome is unknown. No replacement pane was selected.",
182
+ file=sys.stderr,
183
+ )
184
+ acknowledge()
185
+ return exit_code or 1
186
+ except (OSError, ValueError, RuntimeError, subprocess.TimeoutExpired) as error:
187
+ print(
188
+ f"RexDeck {title}: cannot return focus to original pane {origin!r}: {error}. "
189
+ "No replacement pane was selected.",
190
+ file=sys.stderr,
191
+ )
192
+ acknowledge()
193
+ return exit_code or 1
194
+ return exit_code
195
+
196
+
197
+ def main() -> int:
198
+ """Dispatch standalone cwd validation and native pane launch commands."""
199
+ args = sys.argv[1:]
200
+ if len(args) == 2 and args[0] in ("cwd", "git-cwd"):
201
+ try:
202
+ cwd = git_directory(args[1]) if args[0] == "git-cwd" else directory(args[1])
203
+ print(json.dumps({"cwd": cwd}))
204
+ return 0
205
+ except (OSError, ValueError, subprocess.SubprocessError) as error:
206
+ print(f"RexDeck: invalid invoking cwd: {error}", file=sys.stderr)
207
+ return 1
208
+ if len(args) == 7 and args[0] in ("run", "git-run"):
209
+ return run(*args[1:], plugin="git" if args[0] == "git-run" else "files")
210
+ print(
211
+ "Use [git-]cwd RAW_PWD or [git-]run TOOL REX SERVER SESSION ORIGIN CWD",
212
+ file=sys.stderr,
213
+ )
214
+ return 2
215
+
216
+
217
+ if __name__ == "__main__":
218
+ raise SystemExit(main())
@@ -0,0 +1,92 @@
1
+ -- Native Rex configuration module; also embedded verbatim by RexDeck's installer.
2
+ return function(opts)
3
+ if type(opts) ~= 'table' then error('RexDeck Files requires installation options', 0) end
4
+ local plugin = opts.plugin or 'files'
5
+ if plugin ~= 'files' and plugin ~= 'git' then error('Unknown RexDeck integration', 0) end
6
+ local is_git = plugin == 'git'
7
+ local title = is_git and 'RexDeck Git' or 'RexDeck Files'
8
+ local tool = is_git and 'lazygit' or 'yazi'
9
+ local paths = {}
10
+ for _, key in ipairs({'python', 'rex', tool}) do
11
+ local value = opts[key]
12
+ if type(value) ~= 'string' or value:sub(1, 1) ~= '/' or value:find('%z') then
13
+ error(title .. ' requires an absolute ' .. key .. ' path', 0)
14
+ end
15
+ paths[key] = value
16
+ end
17
+ local launcher = opts.launcher_code
18
+ if type(launcher) ~= 'string' or launcher == '' or launcher:find('%z') then
19
+ error(title .. ' requires its applied Python launcher', 0)
20
+ end
21
+ local server = opts.server
22
+ if type(server) ~= 'string' or server == '' or server:find('%z') then
23
+ error(title .. ' requires an explicit Rex server', 0)
24
+ end
25
+
26
+ rex.action{
27
+ name = 'rexdeck_' .. plugin,
28
+ title = title,
29
+ category = 'RexDeck',
30
+ description = 'Open ' .. tool .. ' in the invoking terminal directory',
31
+ keywords = is_git and 'git lazygit' or 'files yazi explorer',
32
+ run = function(ctx, args)
33
+ -- Freeze identity before any call can change focus or mutate the context.
34
+ local session = type(ctx) == 'table' and ctx.session_id
35
+ local origin = type(ctx) == 'table' and ctx.block_id
36
+ if type(session) ~= 'string' or session == '' or type(origin) ~= 'string' or origin == '' then
37
+ error(title .. ' requires the invoking session and terminal pane', 0)
38
+ end
39
+ local attached, attach_error = rex.call('session.attach', {session_id = session})
40
+ if not attached then
41
+ error(title .. ' cannot attach to the invoking session: ' .. tostring(attach_error), 0)
42
+ end
43
+ local reported, pwd_error = rex.call('com.superlogical.terminal.pwd', {
44
+ session_id = session, block_id = origin, args = {},
45
+ })
46
+ if not reported then
47
+ error(title .. ' cannot read the invoking terminal cwd: ' .. tostring(pwd_error), 0)
48
+ end
49
+ if type(reported.pwd) ~= 'string' or reported.pwd == '' then
50
+ error(title .. ': the invoking terminal has not reported a cwd', 0)
51
+ end
52
+ local checked, exec_error = rex.exec({paths.python, '-I', '-c', launcher,
53
+ is_git and 'git-cwd' or 'cwd', reported.pwd}, {timeout = 15})
54
+ if not checked then
55
+ error(title .. ' cannot validate the invoking cwd: ' .. tostring(exec_error), 0)
56
+ end
57
+ if not checked.ok then
58
+ error(title .. ' refused the invoking cwd: ' .. tostring(checked.stderr), 0)
59
+ end
60
+ local decoded, json_error = rex.json.decode(checked.stdout)
61
+ if json_error or type(decoded) ~= 'table' or type(decoded.cwd) ~= 'string'
62
+ or decoded.cwd:sub(1, 1) ~= '/' or decoded.cwd:find('%z') then
63
+ error(title .. ' received an invalid cwd validation result: ' .. tostring(json_error), 0)
64
+ end
65
+ local cwd = decoded.cwd
66
+ local split, split_error = rex.session.new_split{
67
+ session_id = session,
68
+ anchor_id = origin,
69
+ direction = 'horizontal',
70
+ side = 'after',
71
+ ratio = 0.5,
72
+ focus = true,
73
+ layout = {block = {
74
+ flavor = 'com.superlogical.terminal.shell',
75
+ label = title,
76
+ options = {
77
+ cwd = cwd,
78
+ shell = 'none',
79
+ login = false,
80
+ exit = {on_completion = true},
81
+ command = {paths.python, '-I', '-c', launcher, is_git and 'git-run' or 'run',
82
+ paths[tool], paths.rex, server, session, origin, cwd},
83
+ },
84
+ }},
85
+ }
86
+ if not split then
87
+ error(title .. ' cannot open the tool terminal: ' .. tostring(split_error), 0)
88
+ end
89
+ return split
90
+ end,
91
+ }
92
+ end
rexdeck/settings.py ADDED
@@ -0,0 +1,188 @@
1
+ """Bounded, atomic storage of the explicitly approved native Files target."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import stat
8
+ import tempfile
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import cast
12
+
13
+ from rexdeck import lifecycle
14
+
15
+ _MAX_SETTINGS = 16 * 1024
16
+ _FIELDS = {
17
+ "version",
18
+ "kind",
19
+ "workspace",
20
+ "data_root",
21
+ "config",
22
+ "config_target",
23
+ "server",
24
+ "real_server",
25
+ }
26
+
27
+
28
+ def xdg_path(name: str, default: Path) -> Path:
29
+ value = os.environ.get(name)
30
+ if value is None:
31
+ return default
32
+ if not value or not Path(value).is_absolute() or "\x00" in value:
33
+ raise RuntimeError(f"{name} must be a nonempty absolute path")
34
+ return Path(value)
35
+
36
+
37
+ def settings_path() -> Path:
38
+ return xdg_path("XDG_CONFIG_HOME", Path.home() / ".config") / "rexdeck/config.json"
39
+
40
+
41
+ def default_workspace() -> Path:
42
+ return (
43
+ xdg_path("XDG_STATE_HOME", Path.home() / ".local/state") / "rexdeck/files"
44
+ ).resolve()
45
+
46
+
47
+ def default_data_root() -> Path:
48
+ root = xdg_path("XDG_DATA_HOME", Path.home() / ".local/share") / "rexdeck"
49
+ if root.is_symlink():
50
+ raise RuntimeError(f"Files deployment directory is redirected: {root}")
51
+ return root.resolve()
52
+
53
+
54
+ def _absolute(value: object, name: str) -> Path:
55
+ if not isinstance(value, str) or not value or "\x00" in value:
56
+ raise RuntimeError(f"Saved {name} must be a nonempty absolute path")
57
+ path = Path(value)
58
+ if not path.is_absolute() or str(path) != value or ".." in path.parts:
59
+ raise RuntimeError(f"Saved {name} must be a normalized absolute path")
60
+ return path
61
+
62
+
63
+ def _object(pairs: list[tuple[str, object]]) -> dict[str, object]:
64
+ result: dict[str, object] = {}
65
+ for key, value in pairs:
66
+ if key in result:
67
+ raise RuntimeError(f"Duplicate settings field: {key}")
68
+ result[key] = value
69
+ return result
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class SavedTarget:
74
+ workspace: Path
75
+ data_root: Path
76
+ config: Path
77
+ config_target: Path
78
+ server: str
79
+ real_server: bool
80
+
81
+ def _validate(self, plugin: str | None = None) -> None:
82
+ """Validate shared frozen identity and only an explicitly selected plugin."""
83
+ if self.config.resolve(strict=True) != self.config_target:
84
+ raise RuntimeError(
85
+ "Saved Rex config target changed; restore the selected symlink before "
86
+ "using rexdeck setup or changing Files"
87
+ )
88
+ if self.workspace.resolve() != self.workspace:
89
+ raise RuntimeError("Saved Files workspace target changed; restore its path")
90
+ lifecycle._validate_data_root(self.data_root, plugin)
91
+ if lifecycle._canonical_server(self.server) != self.server:
92
+ raise RuntimeError("Saved Rex socket target changed; restore its path")
93
+
94
+ def setup(self, plugin: str = "files") -> lifecycle.Setup:
95
+ """Validate frozen target paths and select one integration's ownership."""
96
+ self._validate(plugin)
97
+ return lifecycle.Setup(
98
+ project=Path(__file__).resolve().parent,
99
+ workspace=self.workspace,
100
+ data_root=self.data_root,
101
+ config=self.config,
102
+ server=self.server,
103
+ real_server=self.real_server,
104
+ plugin=plugin,
105
+ )
106
+
107
+ def document(self) -> dict[str, object]:
108
+ return {
109
+ "version": 2,
110
+ "kind": "native-files",
111
+ "workspace": str(self.workspace),
112
+ "data_root": str(self.data_root),
113
+ "config": str(self.config),
114
+ "config_target": str(self.config_target),
115
+ "server": self.server,
116
+ "real_server": self.real_server,
117
+ }
118
+
119
+
120
+ def load(path: Path) -> SavedTarget | None:
121
+ """Load bounded settings and validate shared identity before plugin selection."""
122
+ try:
123
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
124
+ except FileNotFoundError:
125
+ return None
126
+ try:
127
+ info = os.fstat(descriptor)
128
+ if not stat.S_ISREG(info.st_mode) or info.st_size > _MAX_SETTINGS:
129
+ raise RuntimeError(
130
+ "RexDeck settings must be a bounded regular non-symlink file"
131
+ )
132
+ with os.fdopen(descriptor, "rb", closefd=False) as stream:
133
+ data = stream.read(_MAX_SETTINGS + 1)
134
+ if len(data) > _MAX_SETTINGS:
135
+ raise RuntimeError("RexDeck settings exceed the size limit")
136
+ finally:
137
+ os.close(descriptor)
138
+ try:
139
+ value: object = json.loads(data, object_pairs_hook=_object)
140
+ except (ValueError, UnicodeError) as exc:
141
+ raise RuntimeError(f"Invalid RexDeck settings JSON: {exc}") from exc
142
+ if not isinstance(value, dict):
143
+ raise RuntimeError("RexDeck settings must be a JSON object")
144
+ fields = cast(dict[str, object], value)
145
+ if (
146
+ set(fields) != _FIELDS
147
+ or type(fields["version"]) is not int
148
+ or fields["version"] != 2
149
+ or fields["kind"] != "native-files"
150
+ or type(fields["real_server"]) is not bool
151
+ ):
152
+ raise RuntimeError("Unsupported or malformed RexDeck settings schema")
153
+ server = fields["server"]
154
+ if not isinstance(server, str) or not server or server != server.strip():
155
+ raise RuntimeError("Saved server must be a nonempty Unix endpoint")
156
+ lifecycle._socket_path(server)
157
+ target = SavedTarget(
158
+ workspace=_absolute(fields["workspace"], "workspace"),
159
+ data_root=_absolute(fields["data_root"], "data_root"),
160
+ config=_absolute(fields["config"], "config"),
161
+ config_target=_absolute(fields["config_target"], "config_target"),
162
+ server=server,
163
+ real_server=fields["real_server"],
164
+ )
165
+ if target.config_target.resolve() != target.config_target:
166
+ raise RuntimeError("Saved config_target must be a resolved absolute path")
167
+ target._validate()
168
+ return target
169
+
170
+
171
+ def save(path: Path, target: SavedTarget) -> None:
172
+ data = (json.dumps(target.document(), ensure_ascii=True, indent=2) + "\n").encode()
173
+ if len(data) > _MAX_SETTINGS:
174
+ raise RuntimeError("Approved RexDeck settings exceed the size limit")
175
+ path.parent.mkdir(parents=True, exist_ok=True)
176
+ descriptor, temporary = tempfile.mkstemp(prefix=".config-", dir=path.parent)
177
+ try:
178
+ with os.fdopen(descriptor, "wb") as stream:
179
+ stream.write(data)
180
+ stream.flush()
181
+ os.fsync(stream.fileno())
182
+ # Publish a complete file without ever replacing a concurrently saved target.
183
+ os.link(temporary, path)
184
+ finally:
185
+ try:
186
+ os.unlink(temporary)
187
+ except FileNotFoundError:
188
+ pass
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.5
2
+ Name: rexdeck
3
+ Version: 0.1.0
4
+ Summary: Native Rex integrations for independently selectable existing tools
5
+ Project-URL: Homepage, https://github.com/Mathews-Tom/RexDeck
6
+ Project-URL: Repository, https://github.com/Mathews-Tom/RexDeck
7
+ Project-URL: Issues, https://github.com/Mathews-Tom/RexDeck/issues
8
+ Project-URL: Changelog, https://github.com/Mathews-Tom/RexDeck/blob/main/CHANGELOG.md
9
+ Author: Tom Mathews
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: lazygit,rex,terminal,yazi
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Operating System :: MacOS :: MacOS X
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/markdown
21
+
22
+ # RexDeck
23
+
24
+ A collection of independently selectable plugins and extensions that bring existing tools into your own Rex application.
25
+
26
+ Files and local Git bring unmodified Yazi and lazygit into native Rex terminals. GitHub workflows, rich viewers, agent/subagent views, and task-provider integrations remain planned. Select the tools you need without installing an unrelated default suite.
27
+
28
+ ## Plugins and extensions
29
+
30
+ | Integration | Purpose | Status |
31
+ | --- | --- | --- |
32
+ | [Files](plugins/files/README.md) | Browse, preview, and manage files using unmodified Yazi in a native Rex terminal | Available |
33
+ | [Git](plugins/git/README.md) | Inspect, stage, and commit in the invoking repository/worktree using unmodified lazygit | Available |
34
+ | GitHub | Pull-request/issue workflows with explicit repository/host/account | Planned |
35
+ | Rich viewers | Rendered Markdown, PDF, and image viewing | Planned |
36
+ | Agents and subagents | Attention, observed state, and navigation to agent work | Planned |
37
+ | Tasks | Independently selectable Linear and ClickUp workflows | Planned |
38
+
39
+ Planned integrations are not implemented or installable yet. RexDeck is not published on PyPI; install the current source package as described below.
40
+
41
+ ## Getting started
42
+
43
+ The qualified platform is **macOS arm64, Rex API revision 10 baseline, and Python 3.12+**. Revision 10 is not an exact-version compatibility gate. Rex must already be installed and running; RexDeck does not bootstrap Rex, Homebrew, Python, or uv. Other platforms, remote hosts, and multi-device operation are not qualified.
44
+
45
+ Until the approved PyPI release is published, install the pinned public source baseline (Files and Git; before S1 dependency onboarding):
46
+
47
+ ```sh
48
+ uv tool install 'git+https://github.com/Mathews-Tom/RexDeck.git@d7f934e31d2becf53bc435ac931fa6e1b984bbd0'
49
+ ```
50
+
51
+ For the S1 candidate, use its reviewed checkout and record the exact commit:
52
+
53
+ ```sh
54
+ uv tool install .
55
+ rexdeck setup
56
+ rexdeck enable files
57
+ rexdeck status
58
+ ```
59
+
60
+ The installed command works from any directory; the package includes the shared native action and Python launcher for Files and Git. No shell wrapper, `RD_*` variables, or developer checkout is needed at runtime. Ensure the tool executable directory reported by `uv` is on `PATH`.
61
+
62
+ `setup` remembers the selected Rex configuration and running server without changing Rex configuration. Interactive setup asks for approval before saving a personal-server target. Use `enable files` or `enable git` independently, `status [files|git]` to inspect ownership/registration, and `disable <id>` to remove only that integration. Unqualified `status` reports both integrations, retaining healthy results alongside an integration-specific fault if its sibling cannot be inspected. See the plugin READMEs for prerequisites, capabilities, updates, and removal.
63
+
64
+ The uv-managed package remains the runtime source. Setup freezes the app data root from `$XDG_DATA_HOME/rexdeck` (default `~/.local/share/rexdeck`); each integration deploys `plugins/<id>/rexdeck_<id>.lua` there with an independently owned tracked loader in Rex's `init.lua`. Approved preferences remain in `$XDG_CONFIG_HOME/rexdeck/config.json`. Files receipt/lock paths remain in the saved `$XDG_STATE_HOME/rexdeck/files` workspace; Git uses its `git/` subdirectory. Later `XDG_DATA_HOME` changes do not retarget the saved installation. Run `disable` for every applied integration before uninstalling RexDeck or deliberately resetting a saved target, including integrations reported as disabled after a failed enable.
65
+
66
+ ### Selected dependencies
67
+
68
+ `setup` and `status` never install packages. `enable files` validates preinstalled Yazi; `enable git` validates preinstalled Git/lazygit. Qualified versions are **Yazi 26.9.1, lazygit 0.66.2 or 0.66.3**. Missing or unqualified selected tools produce actionable nonzero manual-install/version-check/rerun guidance without registration or installation prompts. RexDeck never installs, upgrades, downloads, cancels package transactions, or removes upstream tools, and never probes or executes Homebrew.
69
+
70
+ Executable discovery ignores empty and relative `PATH` entries. Absolute entries, including checkout directories and symlinks to them, remain eligible for version probes and lifecycle lookup. Put only trusted tools in absolute `PATH` directories. RexDeck captures followed-target device/inode/mode/size/nanosecond timestamps/owner before the version probe and rechecks that identity afterward and under its operation locks immediately before applying or enabling an integration. Replacement or modification requires rerunning enable. This is metadata-based detection, not pinned execution or protection against an executable changed after the final check.
71
+
72
+ Install dependencies yourself using qualified upstream releases: [Yazi 26.9.1](https://github.com/sxyazi/yazi/releases/tag/v26.9.1), [lazygit 0.66.2](https://github.com/jesseduffield/lazygit/releases/tag/v0.66.2) or [0.66.3](https://github.com/jesseduffield/lazygit/releases/tag/v0.66.3). Follow [Yazi installation](https://yazi-rs.github.io/docs/installation/) or [lazygit installation](https://github.com/jesseduffield/lazygit#installation); Git must also already be installed for Git integration.
73
+
74
+ If you choose Homebrew, first run `brew info yazi` or `brew info lazygit` yourself and inspect the selected version and dependency/source-build/upgrade effects. **Skip Homebrew if its selected version is not qualified**; use the qualified upstream release instead. Only if the version qualifies and you accept the package effects, run the selected `brew install yazi` or `brew install lazygit` yourself. RexDeck does not qualify a Homebrew version, prefix or transaction; all package effects remain owner-controlled.
75
+
76
+ Check `yazi --version` or `lazygit --version` (and `git --version` for Git), then rerun `rexdeck enable files` or `rexdeck enable git`. The pinned pre-S1 source baseline above requires manual dependency provisioning too, but does not contain S1's selected-tool qualification/guidance behavior.
77
+
78
+ Rex, Yazi, lazygit, Git, Homebrew, Python and uv remain externally managed prerequisites. No install-all, sudo, silent upgrade, tool configuration change, or upstream package removal is performed. `disable` and Python package uninstall leave upstream tools installed.
79
+
80
+ ## Design principles
81
+
82
+ - **Reuse tools, do not rebuild them.** Preserve their normal interfaces, configuration, authentication, and capabilities.
83
+ - **Prefer native Rex integration.** Use native actions and panes where sufficient. LazyRex is optional only where a particular integration needs an existing capability; it is not a collection-wide dependency.
84
+ - **Keep features independently selectable.** Install only the dependencies needed by the chosen integration; no implicit default suite, hooks, or autostart.
85
+ - **Preserve user context and ownership.** Pass explicit launch context and structured arguments, keep unrelated work intact, and make removal reversible.
86
+ - **Disclose capabilities accurately.** External tools run as your OS account and can be write-capable; they are not sandboxed or automatically read-only.
87
+
88
+ ## Development
89
+
90
+ Install LuaJIT before running the suite: native Lua behavior tests require `lua` or `luajit` and otherwise skip. On macOS:
91
+
92
+ ```sh
93
+ brew install luajit
94
+ uv sync --locked
95
+ uv run ruff check .
96
+ uv run ruff format --check .
97
+ uv run mypy --strict
98
+ uv run pytest
99
+ ```
100
+
101
+ Plugin-specific syntax checks and real interaction verification belong in each plugin's README. Keep plugin documentation under `plugins/<name>/README.md` and packaged runtime assets under `src/rexdeck/plugins/<name>/`. Link available integrations from the table above. Do not create empty plugin folders for planned features.
102
+
103
+ ## Release preparation and owner gate
104
+
105
+ Version **0.1.0 is prepared, not evidence of publication**. Do not use a PyPI install command until the release's public registry files and hashes have been verified. A PyPI name lookup returning 404 does not establish ownership, name eligibility, or publishing access.
106
+
107
+ The manual [`publish.yml`](.github/workflows/publish.yml) workflow supports [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) using [uv](https://docs.astral.sh/uv/guides/package/#publishing-your-package). No ordinary push, PR, or tag publishes anything; no release-time rebuild occurs.
108
+
109
+ Before dispatch, the owner must configure a PyPI project or pending publisher for `rexdeck`: GitHub owner `Mathews-Tom`, repository `RexDeck`, workflow `publish.yml`, environment `pypi`. Configure that GitHub environment with `Mathews-Tom` as its **sole required reviewer** and restrict deployment to `main`; GitHub permits any listed reviewer to approve, so additional reviewers do not enforce owner approval. Leave self-review prevention disabled for owner-dispatched runs so the sole owner can approve an assistant-dispatched run under the owner's GitHub identity. These account/environment changes require owner participation and are not performed by setup or this repository. Missing protection or publisher access holds publication.
110
+
111
+ The S1 publication boundary assumes this remains a personal-account repository with Mathews-Tom as its sole administrator. Owner-controlled bypass/configuration is trusted; transfer to an organization requires re-evaluating that gate.
112
+
113
+ 1. Merge only the reviewed candidate. A successful **push on main** `checks.yml` run builds wheel/sdist once and retains the exact pair as `rexdeck-0.1.0`; download and inspect them, record both SHA-256 hashes, immutable artifact ID, full commit SHA, run URL, and runtime/security/review evidence. Artifacts expire; a rebuild requires fresh hashes and approval.
114
+ 2. Obtain explicit owner approval naming that exact commit, artifact ID, both filenames/hashes, and reviewed evidence. Any changed input requires renewed approval. After this approval only, create/verify the annotated `v0.1.0` tag and GitHub Release at that commit, attaching the same files; never rebuild for a tag.
115
+ 3. Dispatch `publish.yml` **from main at that approved commit**, entering the exact SHA, artifact ID, hashes and approval-evidence URL. The protected-environment reviewer must compare the dispatch inputs/run summary against the approval before approving the deployment. The workflow verifies repository/run provenance, tag target and hashes, then publishes only `approved/rexdeck-0.1.0.tar.gz` and `approved/rexdeck-0.1.0-py3-none-any.whl` with `uv publish --trusted-publishing always` (no token secret or broad `dist/*` upload).
116
+ 4. Verify [PyPI JSON](https://pypi.org/pypi/rexdeck/0.1.0/json) exposes both approved hashes. Then run an isolated no-cache public-index install, allowing index propagation, and repeat actual private Rex use/quit/refresh/disable. Only then document `uv tool install 'rexdeck==0.1.0'` as available:
117
+
118
+ ```sh
119
+ uv --no-cache run --isolated --no-project --index https://pypi.org/simple \
120
+ --with rexdeck==0.1.0 rexdeck --help
121
+ ```
122
+
123
+ CI and private endpoint/TUI captures do not prove owner-app palette, glyph, or desktop visual approval. Successful package installation does not prove native interaction; successful local builds do not prove registry publication. Keep those evidence claims separate.
124
+
125
+ ## License
126
+
127
+ RexDeck is [MIT licensed](LICENSE), included in wheel/sdist metadata. External executables are **not bundled**: [Yazi is MIT](https://github.com/sxyazi/yazi/blob/main/LICENSE) and [lazygit is MIT](https://github.com/jesseduffield/lazygit/blob/master/LICENSE), retaining upstream notices and terms. Git retains its own upstream license. Rex is an externally installed, separately licensed application prerequisite; RexDeck's MIT license does not grant rights to Rex or redistribute it.
@@ -0,0 +1,12 @@
1
+ rexdeck/__init__.py,sha256=dta1LrrcdEEoTEdYFAwyXieXsx8AWioPIK2xBwkwSfE,55
2
+ rexdeck/cli.py,sha256=N47pPZF-hb4vzIwKK4Grai5IkbYohMabcx4LfZ66Rcg,11452
3
+ rexdeck/dependencies.py,sha256=SX9kCjm7to6KJj9B-WmfePpGCAa9QRg3YYKmsUw0gQ0,3625
4
+ rexdeck/lifecycle.py,sha256=PJWHaSjJHdgBKpA6NAREGniQ1_c8Pdl60kQoOBJx1tI,47725
5
+ rexdeck/settings.py,sha256=EPuMiteAxxMzyiZm2_f6b1s2DUcE6ORuwbKL1ykKgCU,6687
6
+ rexdeck/plugins/files/launch.py,sha256=GDlL2_3rr_rDOU3OqV6IUcvO6X0nXbmt91V7DMPlGqg,7757
7
+ rexdeck/plugins/files/main.lua,sha256=itAOYwyMlGcsPGlumj47ktdCMZFICj8mqKQYfTSjxME,3981
8
+ rexdeck-0.1.0.dist-info/METADATA,sha256=lKr_vnZXnY72uuUnMKSwyJg6OetyJ4v1B3XaBbZHwic,12599
9
+ rexdeck-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ rexdeck-0.1.0.dist-info/entry_points.txt,sha256=GuflU63f8PxKffblz6oBANdiD36nau_AqJ90s82fCMc,45
11
+ rexdeck-0.1.0.dist-info/licenses/LICENSE,sha256=g6qKmvZAxoXVjOFPeSmV4IaCkMZMuRektS4YVmfl8-k,1068
12
+ rexdeck-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rexdeck = rexdeck.cli:main