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.
rexdeck/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """RexDeck's explicit repository-local setup tools."""
rexdeck/cli.py ADDED
@@ -0,0 +1,292 @@
1
+ """One-time native Rex target approval and independent integration lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Never
12
+
13
+ from rexdeck import dependencies, lifecycle, settings
14
+
15
+ _FAILURES = {"conflict", "failed", "fault"}
16
+
17
+
18
+ class _Parser(argparse.ArgumentParser):
19
+ def error(self, message: str) -> Never:
20
+ print(json.dumps({"state": "failed", "error": message}), file=sys.stderr)
21
+ raise SystemExit(2)
22
+
23
+
24
+ def _discover(config: Path) -> str:
25
+ # REX_SERVER is honored only here, never by subsequent saved-target commands.
26
+ env = {
27
+ key: value
28
+ for key, value in os.environ.items()
29
+ if key not in {"REX_SESSION", "REX_BLOCK", "REX_CLIENT"}
30
+ }
31
+ env["REX_CONFIG"] = str(config)
32
+ argv = [lifecycle._executable("rex"), "--autostart=false", "--timeout", "3s"]
33
+ if "REX_SERVER" in env:
34
+ # Native `server status` needs -S even when REX_SERVER is present.
35
+ lifecycle._socket_path(env["REX_SERVER"])
36
+ argv += ["-S", env["REX_SERVER"]]
37
+ result = lifecycle._run([*argv, "server", "status", "--json"], env)
38
+ if result.code != 0:
39
+ raise RuntimeError(
40
+ "Cannot discover the Rex server; start Rex or pass setup --server UNIX_URL: "
41
+ + result.output.decode(errors="replace").strip()
42
+ )
43
+ value = lifecycle._decode(result.output, "Rex server discovery")
44
+ server = value.get("endpoint")
45
+ if (
46
+ not isinstance(server, str)
47
+ or not server
48
+ or server != server.strip()
49
+ or not isinstance(value.get("instance_id"), str)
50
+ or not value["instance_id"]
51
+ ):
52
+ raise RuntimeError("Rex server discovery lacks endpoint/instance identity")
53
+ lifecycle._socket_path(server)
54
+ return server
55
+
56
+
57
+ def _selected_status(setup: lifecycle.Setup) -> None:
58
+ current = lifecycle._server_status(setup, selected=True)
59
+ if current is None:
60
+ raise RuntimeError("Selected Rex server is not running")
61
+ endpoint = current.get("endpoint")
62
+ if not isinstance(endpoint, str) or (
63
+ lifecycle._socket_path(endpoint) != lifecycle._socket_path(setup.server)
64
+ ):
65
+ raise RuntimeError("Selected server answered with a different socket endpoint")
66
+
67
+
68
+ def _inspect(target: settings.SavedTarget) -> dict[str, object]:
69
+ """Inspect the saved target without imposing plugin-specific prerequisites."""
70
+ setup = target.setup()
71
+ _selected_status(setup)
72
+ return lifecycle.inspect_setup(setup, check_tools=False)
73
+
74
+
75
+ def _configure(args: argparse.Namespace, path: Path) -> dict[str, object]:
76
+ """Approve and persist one target without retargeting existing ownership."""
77
+ old = settings.load(path)
78
+ # Validate both XDG roots even when a saved workspace is reused.
79
+ workspace_default = settings.default_workspace()
80
+ settings.xdg_path("XDG_RUNTIME_DIR", Path("/tmp"))
81
+ if (
82
+ old is not None
83
+ and args.config is None
84
+ and args.server is None
85
+ and args.workspace is None
86
+ ):
87
+ if args.real_server and not old.real_server:
88
+ raise RuntimeError("--real-server does not match the saved isolated target")
89
+ result = _inspect(old)
90
+ if result.get("state") in _FAILURES:
91
+ return result
92
+ return {"state": "configured", "settings": str(path), **old.document()}
93
+
94
+ if args.config is not None:
95
+ config = args.config
96
+ elif old is not None:
97
+ config = old.config
98
+ elif "REX_CONFIG" in os.environ:
99
+ configured = os.environ["REX_CONFIG"]
100
+ if not configured or "\x00" in configured:
101
+ raise RuntimeError("REX_CONFIG must name a nonempty config path")
102
+ config = Path(configured)
103
+ else:
104
+ config = (
105
+ settings.xdg_path("XDG_CONFIG_HOME", Path.home() / ".config")
106
+ / "rex/init.lua"
107
+ )
108
+ config = config.parent.resolve(strict=True) / config.name
109
+ config_target = config.resolve(strict=True)
110
+ if not config_target.is_file():
111
+ raise RuntimeError("Selected Rex config must be a regular file")
112
+ workspace = (
113
+ args.workspace.resolve()
114
+ if args.workspace is not None
115
+ else old.workspace
116
+ if old is not None
117
+ else workspace_default
118
+ )
119
+ data_root = old.data_root if old is not None else settings.default_data_root()
120
+ server = lifecycle._canonical_server(
121
+ args.server
122
+ if args.server is not None
123
+ else (old.server if old is not None else _discover(config))
124
+ )
125
+ candidate = settings.SavedTarget(
126
+ workspace=workspace,
127
+ data_root=data_root,
128
+ config=config,
129
+ config_target=config_target,
130
+ server=server,
131
+ real_server=False,
132
+ )
133
+ selected_setup = candidate.setup()
134
+ _selected_status(selected_setup)
135
+ is_real = lifecycle._is_real_endpoint(selected_setup)
136
+ if args.real_server and not is_real:
137
+ raise RuntimeError(
138
+ "--real-server was selected for an isolated server; remove the flag"
139
+ )
140
+ approval_saved = old is not None and (
141
+ old.config,
142
+ old.config_target,
143
+ old.workspace,
144
+ old.data_root,
145
+ old.server,
146
+ old.real_server,
147
+ ) == (config, config_target, workspace, data_root, server, is_real)
148
+ interactive = sys.stdin.isatty()
149
+ if interactive and not approval_saved:
150
+ print(
151
+ f"Rex config: {config}\nResolved config: {config_target}\nRex server: {server}",
152
+ file=sys.stderr,
153
+ )
154
+ if is_real and not args.real_server and not approval_saved:
155
+ if not interactive:
156
+ raise RuntimeError(
157
+ "Real Rex target requires explicit approval: run rexdeck setup interactively "
158
+ "or pass setup --real-server for approved automation"
159
+ )
160
+ print(
161
+ "Approve this real Rex target for later integration writes? Type yes: ",
162
+ end="",
163
+ file=sys.stderr,
164
+ flush=True,
165
+ )
166
+ if sys.stdin.readline().strip() != "yes":
167
+ raise RuntimeError("Real Rex target was not approved; nothing saved")
168
+ candidate = settings.SavedTarget(
169
+ workspace=workspace,
170
+ data_root=data_root,
171
+ config=config,
172
+ config_target=config_target,
173
+ server=server,
174
+ real_server=is_real,
175
+ )
176
+ if old is not None and old != candidate:
177
+ raise RuntimeError(
178
+ "Cannot retarget existing saved integration ownership. Disable every integration "
179
+ "for the saved target, then deliberately remove "
180
+ f"{path} before setup with --workspace pointing to a fresh directory "
181
+ "for the new target; preserve the old ownership workspace"
182
+ )
183
+ result = _inspect(candidate)
184
+ if result.get("state") in _FAILURES:
185
+ return result
186
+ # Recheck the frozen symlink immediately before publishing the approved settings.
187
+ candidate.setup()
188
+ if candidate != old:
189
+ settings.save(path, candidate)
190
+ return {"state": "configured", "settings": str(path), **candidate.document()}
191
+
192
+
193
+ def _enable(path: Path, target: settings.SavedTarget, plugin: str) -> dict[str, object]:
194
+ """Carry one executable identity through both locked ownership transactions."""
195
+ setup = target.setup(plugin)
196
+ _selected_status(setup)
197
+ qualified_tool: lifecycle.QualifiedTool = dependencies.ensure(plugin)
198
+ # Version probes run outside config locks; revalidate the saved target and
199
+ # running server before publishing an integration.
200
+ current = settings.load(path)
201
+ if current is None or current != target:
202
+ raise RuntimeError(
203
+ "Saved Rex target changed during dependency preparation; no integration "
204
+ f"was registered. Inspect {path}, then rerun rexdeck enable {plugin}"
205
+ )
206
+ setup = current.setup(plugin)
207
+ _selected_status(setup)
208
+ result = lifecycle.apply_setup(setup, expected_tool=qualified_tool)
209
+ if result.get("state") == "applied":
210
+ return lifecycle.start(setup, expected_tool=qualified_tool)
211
+ if result.get("state") not in _FAILURES:
212
+ raise RuntimeError(
213
+ f"{plugin} apply did not succeed; integration was not started"
214
+ )
215
+ return result
216
+
217
+
218
+ def main() -> int:
219
+ """Dispatch lifecycle commands and isolate faults in aggregate status rows."""
220
+ parser = _Parser(description=__doc__)
221
+ subcommands = parser.add_subparsers(dest="command", required=True)
222
+ configure = subcommands.add_parser("setup")
223
+ configure.add_argument("--config", type=Path)
224
+ configure.add_argument("--server")
225
+ configure.add_argument(
226
+ "--workspace", type=Path, help="Reuse an existing native Files workspace"
227
+ )
228
+ configure.add_argument(
229
+ "--real-server",
230
+ action="store_true",
231
+ help="Explicitly approve the real Rex target for subsequent integration writes",
232
+ )
233
+ for name in ("enable", "disable", "status"):
234
+ command = subcommands.add_parser(name)
235
+ command.add_argument(
236
+ "plugin", choices=lifecycle.PLUGINS, nargs="?" if name == "status" else None
237
+ )
238
+ args = parser.parse_args()
239
+ try:
240
+ path = settings.settings_path()
241
+ if args.command == "setup":
242
+ result = _configure(args, path)
243
+ else:
244
+ target = settings.load(path)
245
+ if target is None:
246
+ raise RuntimeError("No saved Rex target; run rexdeck setup first")
247
+ if args.command == "enable":
248
+ result = _enable(path, target, args.plugin)
249
+ elif args.command == "disable":
250
+ result = lifecycle.disable(target.setup(args.plugin))
251
+ else:
252
+ if args.plugin:
253
+ result = lifecycle.status(target.setup(args.plugin))
254
+ else:
255
+ plugins: dict[str, dict[str, object]] = {}
256
+ for plugin in lifecycle.PLUGINS:
257
+ try:
258
+ plugins[plugin] = lifecycle.status(target.setup(plugin))
259
+ except (
260
+ OSError,
261
+ ValueError,
262
+ RuntimeError,
263
+ subprocess.SubprocessError,
264
+ ) as error:
265
+ plugins[plugin] = {
266
+ "state": "fault",
267
+ "server": target.server,
268
+ "error": f"Cannot inspect {plugin}: {error}",
269
+ }
270
+ failed = any(
271
+ row.get("state") in _FAILURES for row in plugins.values()
272
+ )
273
+ result = {
274
+ "state": "fault"
275
+ if failed
276
+ else "ready"
277
+ if any(row.get("enabled") for row in plugins.values())
278
+ else "disabled",
279
+ "plugins": plugins,
280
+ }
281
+ print(json.dumps(result, ensure_ascii=True, indent=2))
282
+ return 1 if result.get("state") in _FAILURES else 0
283
+ except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error:
284
+ print(
285
+ json.dumps({"state": "failed", "error": str(error)}, ensure_ascii=True),
286
+ file=sys.stderr,
287
+ )
288
+ return 1
289
+
290
+
291
+ if __name__ == "__main__":
292
+ raise SystemExit(main())
@@ -0,0 +1,92 @@
1
+ """Qualify selected preinstalled tools and explain owner-managed installation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ from rexdeck import lifecycle
12
+
13
+ _TOOLS = {"files": "yazi", "git": "lazygit"}
14
+ _VERSIONS = {
15
+ "yazi": ("26.9.1",),
16
+ "lazygit": ("0.66.2", "0.66.3"),
17
+ }
18
+ _RELEASES = {
19
+ "yazi": "https://github.com/sxyazi/yazi/releases/tag/v26.9.1",
20
+ "lazygit": "https://github.com/jesseduffield/lazygit/releases/tag/v0.66.3",
21
+ }
22
+ _PATTERNS = {
23
+ "yazi": (
24
+ r"^Yazi(?:[ \t]+|[ \t]*\r?\n[ \t]*Version:[ \t]*)"
25
+ r"(\d+\.\d+\.\d+)(?:\s|$)"
26
+ ),
27
+ "lazygit": r"(?:^|,\s*)version=(\d+\.\d+\.\d+)(?=,|\s*$)",
28
+ }
29
+
30
+
31
+ def _resolve(name: str) -> str | None:
32
+ """Search absolute PATH entries while preserving lexical executable identity."""
33
+ search_path = os.pathsep.join(
34
+ entry
35
+ for entry in os.environ.get("PATH", os.defpath).split(os.pathsep)
36
+ if Path(entry).is_absolute()
37
+ )
38
+ found = shutil.which(name, path=search_path) if search_path else None
39
+ return str(Path(found).absolute()) if found is not None else None
40
+
41
+
42
+ def _probe(executable: str, name: str) -> str:
43
+ """Require an explicitly qualified upstream version before registration."""
44
+ result = lifecycle._run([executable, "--version"], dict(os.environ))
45
+ text = result.output.decode(errors="replace").strip()
46
+ version = re.search(_PATTERNS[name], text, re.MULTILINE)
47
+ if result.code != 0 or version is None:
48
+ raise RuntimeError(f"Cannot identify {name} version at {executable}: {text}")
49
+ value = version.group(1)
50
+ if value not in _VERSIONS[name]:
51
+ raise RuntimeError(
52
+ f"Unqualified {name} version {value} at {executable}; "
53
+ f"RexDeck qualifies {name} {' or '.join(_VERSIONS[name])} only"
54
+ )
55
+ return value
56
+
57
+
58
+ def _manual(plugin: str) -> str:
59
+ """Give owner-run, version-qualified installation and exact rerun instructions."""
60
+ tool = _TOOLS[plugin]
61
+ return (
62
+ f"Manually install {tool} {' or '.join(_VERSIONS[tool])} from {_RELEASES[tool]}. "
63
+ f"On macOS with existing Homebrew, inspect brew info {tool}; run brew install "
64
+ f"{tool} only if it selects a qualified version and you accept its dependency, "
65
+ "build and upgrade effects. Otherwise skip Homebrew and use the qualified "
66
+ f"upstream release. Verify with {tool} --version, put the executable in a "
67
+ f"trusted absolute PATH directory, then rerun: rexdeck enable {plugin}. "
68
+ "RexDeck never installs, upgrades or removes upstream tools."
69
+ )
70
+
71
+
72
+ def ensure(plugin: str) -> lifecycle.QualifiedTool:
73
+ """Validate only the selected preinstalled tool; never prompt or run an installer."""
74
+ tool = _TOOLS[plugin]
75
+ if plugin == "git" and _resolve("git") is None:
76
+ raise RuntimeError(
77
+ "Git is a preinstalled prerequisite for the Git integration and was not "
78
+ "found on PATH. Install Git using https://git-scm.com/downloads, make git "
79
+ "available on PATH, then rerun: rexdeck enable git. RexDeck never installs Git"
80
+ )
81
+ executable = _resolve(tool)
82
+ if executable is None:
83
+ raise RuntimeError(
84
+ f"Missing {tool} on PATH; no integration was registered. {_manual(plugin)}"
85
+ )
86
+ try:
87
+ qualified = lifecycle.QualifiedTool.capture(executable)
88
+ _probe(executable, tool)
89
+ qualified.require_current()
90
+ except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
91
+ raise RuntimeError(f"{exc}. {_manual(plugin)}") from exc
92
+ return qualified