rly 0.2.0__py3-none-win_amd64.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.
- rly/__init__.py +5 -0
- rly/__main__.py +5 -0
- rly/_binaries/.gitkeep +6 -0
- rly/_binaries/replylayer.exe +0 -0
- rly/cli.py +215 -0
- rly-0.2.0.dist-info/METADATA +128 -0
- rly-0.2.0.dist-info/RECORD +9 -0
- rly-0.2.0.dist-info/WHEEL +5 -0
- rly-0.2.0.dist-info/entry_points.txt +2 -0
rly/__init__.py
ADDED
rly/__main__.py
ADDED
rly/_binaries/.gitkeep
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Placeholder so rly/_binaries/ exists in source even when empty.
|
|
2
|
+
# The per-platform CI matrix in `.github/workflows/build-cli-binaries.yml`
|
|
3
|
+
# drops the matching `replylayer` (or `replylayer.exe`) binary into this
|
|
4
|
+
# directory before running `python -m build`. The binary is .gitignored
|
|
5
|
+
# (see ../../.gitignore) but Hatch wheel-target `artifacts` ensures it
|
|
6
|
+
# lands in the wheel anyway (see ../../pyproject.toml).
|
|
Binary file
|
rly/cli.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Bundled-binary launcher for the ReplyLayer CLI.
|
|
2
|
+
|
|
3
|
+
Resolution order (see plans/cli-bundled-binary-launcher.md §4.3):
|
|
4
|
+
|
|
5
|
+
1. RLY_FORCE_NPX=1 → legacy `npx replylayer@<pinned>` path (W3 logic,
|
|
6
|
+
RLY_OFFLINE-aware). Reachable BEFORE binary resolution so callers
|
|
7
|
+
on musl/Alpine (where _resolve_bundled_binary returns None) still
|
|
8
|
+
have an out if Node is on PATH.
|
|
9
|
+
2. Bundled binary at `rly/_binaries/replylayer[.exe]` → direct exec.
|
|
10
|
+
3. Missing binary (sdist install on unsupported platform OR corrupted
|
|
11
|
+
wheel) → exit 1 with an actionable error.
|
|
12
|
+
|
|
13
|
+
Env-var contract:
|
|
14
|
+
|
|
15
|
+
RLY_TIMEOUT_SECONDS: opt-in subprocess timeout (carry-over from W3).
|
|
16
|
+
10s default for --help/--version quick commands.
|
|
17
|
+
Unbounded for everything else.
|
|
18
|
+
RLY_FORCE_NPX=1: use the legacy npx fallback (emergency / dev path).
|
|
19
|
+
RLY_OFFLINE=1: no-op for the binary path; in the npx fallback,
|
|
20
|
+
injects `npx --offline` + `npm_config_offline=true`
|
|
21
|
+
(W3 behavior preserved per rev. 5 audit finding 3).
|
|
22
|
+
|
|
23
|
+
This module's `main()` is registered as the `rly` console-script entry
|
|
24
|
+
point via packages/pypi-rly/pyproject.toml [project.scripts].
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from collections.abc import Sequence
|
|
33
|
+
from importlib.metadata import version
|
|
34
|
+
from importlib.resources import files
|
|
35
|
+
|
|
36
|
+
QUICK_COMMAND_TIMEOUT_SECONDS = 10
|
|
37
|
+
QUICK_COMMAND_ARGS = frozenset({"--help", "-h", "--version", "-V"})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _binary_name() -> str:
|
|
41
|
+
return "replylayer.exe" if sys.platform == "win32" else "replylayer"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_bundled_binary() -> str | None:
|
|
45
|
+
"""Return the path to the bundled binary, or None if missing.
|
|
46
|
+
|
|
47
|
+
Missing-bundled-binary cases:
|
|
48
|
+
- sdist install on an unsupported platform (no _binaries/ dir).
|
|
49
|
+
- Editable / source install during local development.
|
|
50
|
+
- Corrupted wheel.
|
|
51
|
+
|
|
52
|
+
Note: this does NOT check the host's libc — on a musl host that
|
|
53
|
+
somehow installed a glibc wheel (e.g. forced via --no-binary), the
|
|
54
|
+
binary file exists but exec'ing it will fail. The launcher returns
|
|
55
|
+
the path here regardless; the caller's subprocess.run() will surface
|
|
56
|
+
the dynamic-loader error and Python exits with the resulting code.
|
|
57
|
+
The fall-clear platform-support message from _no_binary_error() runs
|
|
58
|
+
only when the file is genuinely missing.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
ref = files("rly._binaries").joinpath(_binary_name())
|
|
62
|
+
# importlib.resources returns a Traversable; for a wheel install,
|
|
63
|
+
# this is a real on-disk path. Defensive: handle both is_file()
|
|
64
|
+
# missing (zip-imported edge case) and a stale .gitkeep-only dir.
|
|
65
|
+
if hasattr(ref, "is_file") and ref.is_file():
|
|
66
|
+
return str(ref)
|
|
67
|
+
except (FileNotFoundError, ModuleNotFoundError):
|
|
68
|
+
return None
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _resolve_timeout(args: list[str]) -> int | None:
|
|
73
|
+
env_timeout = os.environ.get("RLY_TIMEOUT_SECONDS")
|
|
74
|
+
if env_timeout:
|
|
75
|
+
try:
|
|
76
|
+
return int(env_timeout)
|
|
77
|
+
except ValueError:
|
|
78
|
+
return None
|
|
79
|
+
if any(a in QUICK_COMMAND_ARGS for a in args):
|
|
80
|
+
return QUICK_COMMAND_TIMEOUT_SECONDS
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _no_binary_error() -> str:
|
|
85
|
+
try:
|
|
86
|
+
pinned = version("rly")
|
|
87
|
+
except Exception:
|
|
88
|
+
pinned = "<unknown>"
|
|
89
|
+
return (
|
|
90
|
+
f"rly: no bundled replylayer binary found for this install.\n"
|
|
91
|
+
f"This usually means a source/sdist install on an unsupported platform.\n"
|
|
92
|
+
f"\n"
|
|
93
|
+
f"Supported platforms (binary wheel published):\n"
|
|
94
|
+
f" - linux x86_64 / aarch64 (glibc 2.28+)\n"
|
|
95
|
+
f" - macOS arm64 / x86_64 (>= 14.0)\n"
|
|
96
|
+
f" - Windows x64 (>= Windows 10 / Server 2016)\n"
|
|
97
|
+
f"\n"
|
|
98
|
+
f"If you have Node 22+ on PATH, you can use the legacy npx path:\n"
|
|
99
|
+
f" RLY_FORCE_NPX=1 rly ...\n"
|
|
100
|
+
f"\n"
|
|
101
|
+
f"To force installation of the binary wheel from PyPI:\n"
|
|
102
|
+
f" pipx install --force --pip-args='--only-binary=:all:' rly=={pinned}\n"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _run_npx_fallback(args: list[str]) -> int:
|
|
107
|
+
"""Emergency fallback to `npx --yes replylayer@<pinned>`.
|
|
108
|
+
|
|
109
|
+
Preserves the W3 hardening (RLY_OFFLINE -> npx --offline +
|
|
110
|
+
npm_config_offline=true) byte-for-byte from the pre-rev-11
|
|
111
|
+
packages/pypi-rly/rly/cli.py:52 (rev. 5 audit finding 3 — the rev. 4
|
|
112
|
+
draft dropped this and lost W3's offline contract).
|
|
113
|
+
"""
|
|
114
|
+
import shutil
|
|
115
|
+
|
|
116
|
+
npx = shutil.which("npx")
|
|
117
|
+
if npx is None:
|
|
118
|
+
print(
|
|
119
|
+
"rly: RLY_FORCE_NPX=1 requested but npx is not on PATH. "
|
|
120
|
+
"Install Node.js 22+ with npm, or remove RLY_FORCE_NPX.",
|
|
121
|
+
file=sys.stderr,
|
|
122
|
+
)
|
|
123
|
+
return 127
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
pinned = version("rly")
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
print(
|
|
129
|
+
f"rly: could not resolve installed package version ({exc}). "
|
|
130
|
+
"Reinstall rly: `pipx reinstall rly`.",
|
|
131
|
+
file=sys.stderr,
|
|
132
|
+
)
|
|
133
|
+
return 1
|
|
134
|
+
|
|
135
|
+
env = os.environ.copy()
|
|
136
|
+
env.setdefault("npm_config_update_notifier", "false")
|
|
137
|
+
|
|
138
|
+
offline = os.environ.get("RLY_OFFLINE") == "1"
|
|
139
|
+
if offline:
|
|
140
|
+
env["npm_config_offline"] = "true"
|
|
141
|
+
|
|
142
|
+
cmd = [npx]
|
|
143
|
+
if offline:
|
|
144
|
+
cmd.append("--offline")
|
|
145
|
+
cmd.extend(["--yes", f"replylayer@{pinned}", *args])
|
|
146
|
+
|
|
147
|
+
timeout_s = _resolve_timeout(args)
|
|
148
|
+
try:
|
|
149
|
+
completed = subprocess.run(cmd, env=env, check=False, timeout=timeout_s)
|
|
150
|
+
return completed.returncode
|
|
151
|
+
except subprocess.TimeoutExpired:
|
|
152
|
+
if offline:
|
|
153
|
+
print(
|
|
154
|
+
f"rly: npx fallback did not complete within {timeout_s}s. "
|
|
155
|
+
f"Offline mode is enabled; replylayer@{pinned} may not be "
|
|
156
|
+
f"cached locally.",
|
|
157
|
+
file=sys.stderr,
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
print(
|
|
161
|
+
f"rly: npx fallback did not complete within {timeout_s}s. "
|
|
162
|
+
"Set RLY_TIMEOUT_SECONDS higher, or remove RLY_FORCE_NPX to "
|
|
163
|
+
"use the bundled binary.",
|
|
164
|
+
file=sys.stderr,
|
|
165
|
+
)
|
|
166
|
+
return 124
|
|
167
|
+
except KeyboardInterrupt:
|
|
168
|
+
return 130
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
172
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
173
|
+
|
|
174
|
+
# Emergency / backward-compat path. Use only if the bundled binary is
|
|
175
|
+
# broken on this platform or missing (sdist install). Honored BEFORE
|
|
176
|
+
# any binary work so callers on Alpine/musl (where the wheel falls
|
|
177
|
+
# back to sdist) still have an escape hatch when Node is on PATH.
|
|
178
|
+
if os.environ.get("RLY_FORCE_NPX") == "1":
|
|
179
|
+
return _run_npx_fallback(args)
|
|
180
|
+
|
|
181
|
+
binary_path = _resolve_bundled_binary()
|
|
182
|
+
if binary_path is None:
|
|
183
|
+
print(_no_binary_error(), file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
# exec the binary. subprocess.run on Windows + Unix gives clean
|
|
187
|
+
# signal / exit-code semantics. RLY_TIMEOUT_SECONDS opt-in carries
|
|
188
|
+
# over from W3; bundled-binary exec is local + fast, but callers may
|
|
189
|
+
# bound it (e.g. CI watchdogs).
|
|
190
|
+
timeout_s = _resolve_timeout(args)
|
|
191
|
+
try:
|
|
192
|
+
completed = subprocess.run(
|
|
193
|
+
[binary_path, *args], check=False, timeout=timeout_s
|
|
194
|
+
)
|
|
195
|
+
return completed.returncode
|
|
196
|
+
except subprocess.TimeoutExpired:
|
|
197
|
+
print(
|
|
198
|
+
f"rly: command did not complete within {timeout_s}s. "
|
|
199
|
+
"Set RLY_TIMEOUT_SECONDS higher.",
|
|
200
|
+
file=sys.stderr,
|
|
201
|
+
)
|
|
202
|
+
return 124
|
|
203
|
+
except FileNotFoundError:
|
|
204
|
+
# Binary was resolvable as a path but exec failed (binary deleted
|
|
205
|
+
# between resolve and exec — rare race; or the wheel's binary is
|
|
206
|
+
# for a different libc and the dynamic loader rejected it).
|
|
207
|
+
print(
|
|
208
|
+
f"rly: bundled binary at {binary_path} could not be executed. "
|
|
209
|
+
"The wheel may be for a different platform than the host. "
|
|
210
|
+
"Reinstall with `pipx install --force rly`.",
|
|
211
|
+
file=sys.stderr,
|
|
212
|
+
)
|
|
213
|
+
return 127
|
|
214
|
+
except KeyboardInterrupt:
|
|
215
|
+
return 130
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rly
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: PyPI launcher for the ReplyLayer CLI
|
|
5
|
+
Project-URL: Homepage, https://replylayer.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/replylayer/ReplyLayer
|
|
7
|
+
Project-URL: Issues, https://github.com/replylayer/ReplyLayer/issues
|
|
8
|
+
Author: ReplyLayer
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agent,ai,cli,email,mailbox,replylayer
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Communications :: Email
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: build==1.2.2.post1; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest==8.3.4; extra == 'dev'
|
|
22
|
+
Requires-Dist: wheel==0.45.1; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# rly
|
|
26
|
+
|
|
27
|
+
`rly` is the PyPI launcher for the ReplyLayer CLI.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pipx install rly
|
|
31
|
+
rly --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
On Debian and Ubuntu systems that enforce PEP 668, plain `pip install rly`
|
|
35
|
+
may fail with `externally-managed-environment`. Use `pipx install rly` for a
|
|
36
|
+
global CLI install, or install inside a virtual environment:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python3 -m venv .venv
|
|
40
|
+
. .venv/bin/activate
|
|
41
|
+
pip install rly
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The installed `rly` command delegates to the official npm package
|
|
45
|
+
`replylayer`, pinned exactly to the matching launcher version (see
|
|
46
|
+
[Strict pin policy](#strict-pin-policy) below).
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- Python 3.10+
|
|
51
|
+
- Node.js 22+ with npm/npx on `PATH`
|
|
52
|
+
- pipx for the recommended global install path
|
|
53
|
+
|
|
54
|
+
## Examples
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
rly auth login
|
|
58
|
+
rly mailbox list
|
|
59
|
+
rly inbox list --mailbox support-bot
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For full CLI documentation, see the ReplyLayer repository:
|
|
63
|
+
https://github.com/replylayer/ReplyLayer
|
|
64
|
+
|
|
65
|
+
## Environment variables
|
|
66
|
+
|
|
67
|
+
### `RLY_TIMEOUT_SECONDS`
|
|
68
|
+
|
|
69
|
+
Opt-in subprocess timeout, in seconds. When set, every `rly` invocation is
|
|
70
|
+
bounded by `subprocess.run(..., timeout=RLY_TIMEOUT_SECONDS)`; if the
|
|
71
|
+
underlying `npx` process does not exit in time the launcher prints a
|
|
72
|
+
diagnostic to stderr and exits with code `124`.
|
|
73
|
+
|
|
74
|
+
By default there is no timeout, with one narrow exception: the help and
|
|
75
|
+
version short-circuits (`--help`, `-h`, `--version`, `-V`) carry a built-in
|
|
76
|
+
10-second timeout. Those commands should never need to touch the network
|
|
77
|
+
beyond an initial registry resolve; bounding them lets you diagnose a
|
|
78
|
+
restricted-network sandbox without affecting legitimate long-poll commands
|
|
79
|
+
like `rly inbox wait --timeout 60`.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
RLY_TIMEOUT_SECONDS=30 rly inbox list --mailbox support-bot
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Set the value high enough to cover the slowest command you reasonably
|
|
86
|
+
expect. Setting it to a non-integer string is treated as "no timeout"
|
|
87
|
+
(graceful fallback rather than a launcher crash).
|
|
88
|
+
|
|
89
|
+
### `RLY_OFFLINE`
|
|
90
|
+
|
|
91
|
+
Set `RLY_OFFLINE=1` to force `npx` to resolve `replylayer` from the local
|
|
92
|
+
cache only. The launcher passes both `--offline` on the command line **and**
|
|
93
|
+
`npm_config_offline=true` in the subprocess environment (belt-and-suspenders;
|
|
94
|
+
`--prefer-offline` is NOT a no-network guarantee).
|
|
95
|
+
|
|
96
|
+
If the pinned `replylayer` version isn't cached locally, `npx` exits fast
|
|
97
|
+
with its own non-zero exit code (typically `1` with `ENOTCACHED`); the
|
|
98
|
+
wrapper passes that exit code through verbatim — it does NOT normalize it
|
|
99
|
+
to `124`. Exit code `124` is reserved for the wrapper's own timeout branch.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Populate the cache once, online:
|
|
103
|
+
rly --version
|
|
104
|
+
# Subsequent calls can run offline:
|
|
105
|
+
RLY_OFFLINE=1 rly --version
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`RLY_OFFLINE=1` is useful in network-restricted CI sandboxes, air-gapped
|
|
109
|
+
deployments, or any environment where you want a hard guarantee that the
|
|
110
|
+
launcher will not reach out to the npm registry.
|
|
111
|
+
|
|
112
|
+
## Strict pin policy
|
|
113
|
+
|
|
114
|
+
`rly@X.Y.Z` always resolves `replylayer@X.Y.Z` exactly — there is no
|
|
115
|
+
`@latest` resolution and no semver range. The launcher computes the pinned
|
|
116
|
+
package string at runtime via `importlib.metadata.version('rly')`, so the
|
|
117
|
+
two artifacts always move together.
|
|
118
|
+
|
|
119
|
+
This means:
|
|
120
|
+
|
|
121
|
+
- A published `rly@X.Y.Z` cannot ride a later patch-level `replylayer`
|
|
122
|
+
release; both packages must be republished together. The matched-pair
|
|
123
|
+
release runbook lives at `docs/runbooks/cli-release.md` in the
|
|
124
|
+
ReplyLayer repository.
|
|
125
|
+
- Source installs from the repository between a launcher-only PR and the
|
|
126
|
+
paired version bump are intentionally unsupported. Use `pipx install rly`
|
|
127
|
+
(PyPI) or `npm install -g replylayer` (npm) as the supported install
|
|
128
|
+
paths.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
rly/__init__.py,sha256=5FlJ4Z6BHHJ3nif_IiZ5G-hdpUrdA9ZvV1erUDV2hNU,119
|
|
2
|
+
rly/__main__.py,sha256=h1QpgvR08tO6TfQHuySKKgQiQVXVIoQRKLEx5sVht_M,67
|
|
3
|
+
rly/cli.py,sha256=V8PJDwg_FgaMR4fcyh5cRsvzHdYkZfn70UpTkDjEYWc,8178
|
|
4
|
+
rly/_binaries/.gitkeep,sha256=up7lTij0VRXRaKJ2tBBLOYP9RwySj9uovZ-sKttm2Bk,419
|
|
5
|
+
rly/_binaries/replylayer.exe,sha256=ic9ryP5Zf4MGH8i8nMN1KCaI8a4g64lAShV3N4zg6yM,80875520
|
|
6
|
+
rly-0.2.0.dist-info/METADATA,sha256=mi4WiO0CNMtnwsHG15hiWkKxh4lYEaEuRGGCfD_IWVI,4450
|
|
7
|
+
rly-0.2.0.dist-info/WHEEL,sha256=fip2IBhkkNvH-S_Xh3PV94_XVqHJB1nn9gTAcldDBj4,94
|
|
8
|
+
rly-0.2.0.dist-info/entry_points.txt,sha256=WTGfCCCFuHNRnhfvvP2y59WS2siy5SzSmXiaQ8t5sPQ,37
|
|
9
|
+
rly-0.2.0.dist-info/RECORD,,
|