forgexa-cli 1.14.6__tar.gz → 1.14.8__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.
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/PKG-INFO +3 -1
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli/daemon.py +67 -12
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli/main.py +145 -30
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/PKG-INFO +3 -1
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/requires.txt +2 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/pyproject.toml +5 -3
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/README.md +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/setup.cfg +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.8}/tests/test_auth_and_runtime_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgexa-cli
|
|
3
|
-
Version: 1.14.
|
|
3
|
+
Version: 1.14.8
|
|
4
4
|
Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
|
|
5
5
|
Author-email: Jason Sun <dev.winds@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -26,8 +26,10 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
26
26
|
Requires-Python: >=3.9
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
Requires-Dist: httpx>=0.24
|
|
29
|
+
Requires-Dist: socksio>=1.0.0
|
|
29
30
|
Provides-Extra: daemon
|
|
30
31
|
Requires-Dist: httpx>=0.24; extra == "daemon"
|
|
32
|
+
Requires-Dist: socksio>=1.0.0; extra == "daemon"
|
|
31
33
|
|
|
32
34
|
# Forgexa CLI
|
|
33
35
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.14.
|
|
2
|
+
__version__ = "1.14.8"
|
|
@@ -523,7 +523,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
523
523
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
524
524
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
525
525
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
526
|
-
DAEMON_VERSION = "1.14.
|
|
526
|
+
DAEMON_VERSION = "1.14.8"
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
def _detect_client_type() -> str:
|
|
@@ -1711,17 +1711,27 @@ class WorkspaceManager:
|
|
|
1711
1711
|
|
|
1712
1712
|
@staticmethod
|
|
1713
1713
|
def _safe_rmtree(path: Path) -> None:
|
|
1714
|
-
"""Remove a directory tree, handling
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1714
|
+
"""Remove a directory tree, handling two distinct Windows failure modes.
|
|
1715
|
+
|
|
1716
|
+
Mode 1 — Read-only git objects (chmod 0444):
|
|
1717
|
+
Plain shutil.rmtree(path, ignore_errors=True) silently skips
|
|
1718
|
+
read-only files. Fixed by the onerror callback that chmod’s each
|
|
1719
|
+
entry writable before retrying.
|
|
1720
|
+
|
|
1721
|
+
Mode 2 — Windows reserved device names (NUL, CON, AUX, PRN,
|
|
1722
|
+
COM1–COM9, LPT1–LPT9):
|
|
1723
|
+
The Win32 API intercepts these names before they reach the NTFS
|
|
1724
|
+
driver, so os.unlink / shutil.rmtree / PowerShell Remove-Item all
|
|
1725
|
+
fail with a permissions or access error. The solution is the
|
|
1726
|
+
\\?\ extended-length path prefix, which instructs Windows to
|
|
1727
|
+
skip Win32 name parsing and pass the path directly to the NT
|
|
1728
|
+
kernel file-system layer. 'cmd /c rmdir /s /q' reliably handles
|
|
1729
|
+
this when given a \\?\ prefixed path.
|
|
1730
|
+
|
|
1731
|
+
Deletion strategy (each stage only runs if the path still exists):
|
|
1732
|
+
1. shutil.rmtree with onerror chmod — handles read-only objects.
|
|
1733
|
+
2. Windows only: subprocess cmd /c rmdir /s /q \\\\?\\<path> —
|
|
1734
|
+
handles reserved names and any entries missed by stage 1.
|
|
1725
1735
|
"""
|
|
1726
1736
|
import shutil
|
|
1727
1737
|
import stat as _stat
|
|
@@ -1738,6 +1748,39 @@ class WorkspaceManager:
|
|
|
1738
1748
|
|
|
1739
1749
|
shutil.rmtree(path, onerror=_handle_readonly)
|
|
1740
1750
|
|
|
1751
|
+
# Windows fallback: reserved device names survive shutil.rmtree.
|
|
1752
|
+
# cmd /c rmdir /s /q with the \\?\ extended-length prefix bypasses
|
|
1753
|
+
# Win32 name interception and reaches the NTFS layer directly.
|
|
1754
|
+
if path.exists() and sys.platform == "win32":
|
|
1755
|
+
try:
|
|
1756
|
+
import subprocess
|
|
1757
|
+
resolved = str(path.resolve())
|
|
1758
|
+
# Prepend \\?\ extended-length prefix if not already present.
|
|
1759
|
+
# In Python source "\\\\?\\" represents the 4-char string \\?\
|
|
1760
|
+
# which is the Win32 flag to bypass reserved-name interception.
|
|
1761
|
+
if not resolved.startswith("\\\\?\\"):
|
|
1762
|
+
unc = "\\\\?\\" + resolved
|
|
1763
|
+
else:
|
|
1764
|
+
unc = resolved
|
|
1765
|
+
result = subprocess.run(
|
|
1766
|
+
["cmd", "/c", "rmdir", "/s", "/q", unc],
|
|
1767
|
+
capture_output=True,
|
|
1768
|
+
timeout=60,
|
|
1769
|
+
)
|
|
1770
|
+
if result.returncode != 0 and path.exists():
|
|
1771
|
+
logger.warning(
|
|
1772
|
+
"_safe_rmtree: cmd rmdir could not fully remove %s "
|
|
1773
|
+
"(exit %d: %s). Manual deletion may be required.",
|
|
1774
|
+
path,
|
|
1775
|
+
result.returncode,
|
|
1776
|
+
result.stderr.decode(errors="replace").strip()[:200],
|
|
1777
|
+
)
|
|
1778
|
+
except Exception as exc:
|
|
1779
|
+
logger.warning(
|
|
1780
|
+
"_safe_rmtree: Windows rmdir fallback failed for %s: %s",
|
|
1781
|
+
path, exc,
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1741
1784
|
async def _clone_repo(
|
|
1742
1785
|
self,
|
|
1743
1786
|
repo_url: str,
|
|
@@ -4616,6 +4659,18 @@ class ProcessManager:
|
|
|
4616
4659
|
*cmd,
|
|
4617
4660
|
stdout=asyncio.subprocess.PIPE,
|
|
4618
4661
|
stderr=asyncio.subprocess.PIPE,
|
|
4662
|
+
# stdin=DEVNULL is critical on Windows.
|
|
4663
|
+
# Without it, the child process inherits the parent daemon's stdin.
|
|
4664
|
+
# On Windows, the daemon often has a real console handle on stdin
|
|
4665
|
+
# (e.g. from a CMD/PowerShell window or the Desktop app's console).
|
|
4666
|
+
# Copilot CLI detects this via GetConsoleMode(GetStdHandle(STD_INPUT))
|
|
4667
|
+
# and enters interactive/session-check mode, ignoring -p as a task:
|
|
4668
|
+
# it checks the session todo table (empty), reports "no tracked work
|
|
4669
|
+
# items", and waits for user input instead of executing the task.
|
|
4670
|
+
# On Linux/macOS (daemon as service), stdin is typically /dev/null so
|
|
4671
|
+
# the bug doesn't manifest. Explicit DEVNULL ensures non-interactive
|
|
4672
|
+
# mode on all platforms, matching the intended headless operation.
|
|
4673
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
4619
4674
|
cwd=str(cwd),
|
|
4620
4675
|
env=env,
|
|
4621
4676
|
limit=100 * 1024 * 1024,
|
|
@@ -162,21 +162,14 @@ def _refresh_access_token() -> bool:
|
|
|
162
162
|
if not refresh_token:
|
|
163
163
|
return False
|
|
164
164
|
|
|
165
|
-
import
|
|
166
|
-
import urllib.error
|
|
165
|
+
import httpx
|
|
167
166
|
|
|
168
167
|
url = f"{_api_url()}/api/v1/auth/refresh"
|
|
169
|
-
body = json.dumps({"refresh_token": refresh_token}).encode()
|
|
170
|
-
req = urllib.request.Request(
|
|
171
|
-
url,
|
|
172
|
-
data=body,
|
|
173
|
-
headers={"Content-Type": "application/json"},
|
|
174
|
-
method="POST",
|
|
175
|
-
)
|
|
176
168
|
try:
|
|
177
|
-
with
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
with httpx.Client(timeout=15) as client:
|
|
170
|
+
resp = client.post(url, json={"refresh_token": refresh_token})
|
|
171
|
+
payload = resp.json()
|
|
172
|
+
except Exception:
|
|
180
173
|
return False
|
|
181
174
|
|
|
182
175
|
access_token = str(payload.get("access_token") or "").strip()
|
|
@@ -412,6 +405,117 @@ def _build_upgrade_plan(target_version: str | None) -> UpgradePlan:
|
|
|
412
405
|
)
|
|
413
406
|
|
|
414
407
|
|
|
408
|
+
# ── Update-check helpers ──────────────────────────────────────────────────────
|
|
409
|
+
#
|
|
410
|
+
# Strategy (borrowed from gh CLI / update-notifier):
|
|
411
|
+
# 1. At the start of main(): launch a daemon thread that calls PyPI and
|
|
412
|
+
# writes ~/.forgexa/update-check.json (only when the cache is stale).
|
|
413
|
+
# 2. At the end of main(): read the cache (instant) and, if a newer version
|
|
414
|
+
# is available, print one notice to stderr.
|
|
415
|
+
#
|
|
416
|
+
# Zero latency on the critical path — we never wait for the network.
|
|
417
|
+
# Auto-skipped in CI, non-TTY stderr, `forgexa upgrade`, and --format json.
|
|
418
|
+
|
|
419
|
+
_UPDATE_CACHE_PATH: Path = Path.home() / ".forgexa" / "update-check.json"
|
|
420
|
+
_UPDATE_CHECK_TTL: int = 24 * 3600 # seconds
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _read_update_cache() -> tuple[str | None, float]:
|
|
424
|
+
"""Return (latest_version, checked_at_epoch). Never raises."""
|
|
425
|
+
try:
|
|
426
|
+
data = json.loads(_UPDATE_CACHE_PATH.read_text())
|
|
427
|
+
version = str(data.get("latest_version") or "").strip()
|
|
428
|
+
checked_at = float(data.get("checked_at") or 0)
|
|
429
|
+
return (version or None, checked_at)
|
|
430
|
+
except Exception:
|
|
431
|
+
return (None, 0.0)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _write_update_cache(latest_version: str) -> None:
|
|
435
|
+
"""Atomically write the update cache. Never raises."""
|
|
436
|
+
try:
|
|
437
|
+
_UPDATE_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
438
|
+
tmp = _UPDATE_CACHE_PATH.with_suffix(".json.tmp")
|
|
439
|
+
tmp.write_text(
|
|
440
|
+
json.dumps({"latest_version": latest_version, "checked_at": time.time()})
|
|
441
|
+
)
|
|
442
|
+
tmp.replace(_UPDATE_CACHE_PATH)
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _version_is_newer(candidate: str, reference: str) -> bool:
|
|
448
|
+
"""Return True if candidate is strictly newer than reference (numeric tuple comparison)."""
|
|
449
|
+
def _parts(v: str) -> tuple[int, ...]:
|
|
450
|
+
return tuple(int(x) for x in v.split(".")[:4] if x.isdigit())
|
|
451
|
+
try:
|
|
452
|
+
return _parts(candidate) > _parts(reference)
|
|
453
|
+
except Exception:
|
|
454
|
+
return False
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _launch_update_check_thread() -> None:
|
|
458
|
+
"""Start a fire-and-forget daemon thread that fetches the latest PyPI version.
|
|
459
|
+
|
|
460
|
+
The thread only runs if the cache is stale (> TTL). Because it is a daemon
|
|
461
|
+
thread the OS kills it automatically when the main process exits, so it never
|
|
462
|
+
delays shutdown.
|
|
463
|
+
"""
|
|
464
|
+
_, checked_at = _read_update_cache()
|
|
465
|
+
if time.time() - checked_at <= _UPDATE_CHECK_TTL:
|
|
466
|
+
return # Cache is fresh — nothing to do
|
|
467
|
+
|
|
468
|
+
import threading
|
|
469
|
+
|
|
470
|
+
def _worker() -> None:
|
|
471
|
+
try:
|
|
472
|
+
import httpx
|
|
473
|
+
with httpx.Client(timeout=5) as client:
|
|
474
|
+
resp = client.get("https://pypi.org/pypi/forgexa-cli/json")
|
|
475
|
+
latest = str(resp.json()["info"]["version"])
|
|
476
|
+
_write_update_cache(latest)
|
|
477
|
+
except Exception:
|
|
478
|
+
pass # Network errors are silently ignored
|
|
479
|
+
|
|
480
|
+
t = threading.Thread(target=_worker, daemon=True, name="forgexa-update-check")
|
|
481
|
+
t.start()
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _print_update_notice_if_available() -> None:
|
|
485
|
+
"""Print an update notice to stderr when a newer version is cached.
|
|
486
|
+
|
|
487
|
+
Silently skipped when:
|
|
488
|
+
- FORGEXA_NO_UPDATE_CHECK=1 is set
|
|
489
|
+
- stderr is not a TTY (pipes, CI, scripts)
|
|
490
|
+
- The CI env var is set
|
|
491
|
+
- Output format is json (machine-readable)
|
|
492
|
+
"""
|
|
493
|
+
if os.environ.get("FORGEXA_NO_UPDATE_CHECK"):
|
|
494
|
+
return
|
|
495
|
+
if os.environ.get("CI"):
|
|
496
|
+
return
|
|
497
|
+
if not sys.stderr.isatty():
|
|
498
|
+
return
|
|
499
|
+
if _output_format == "json":
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
current = _installed_cli_version()
|
|
503
|
+
if not current:
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
latest, _ = _read_update_cache()
|
|
507
|
+
if not latest or not _version_is_newer(latest, current):
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
print(
|
|
511
|
+
f"\n Update available: forgexa {current} → {latest}\n"
|
|
512
|
+
f" Run: forgexa upgrade\n",
|
|
513
|
+
file=sys.stderr,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
# ── Daemon PID-file helpers ───────────────────────────────────────────────────
|
|
518
|
+
|
|
415
519
|
def _daemon_pid_files() -> list[Path]:
|
|
416
520
|
return [
|
|
417
521
|
Path.home() / ".forgexa-daemon.pid",
|
|
@@ -505,19 +609,16 @@ def _request_json(
|
|
|
505
609
|
timeout: int = 15,
|
|
506
610
|
allow_refresh: bool = True,
|
|
507
611
|
) -> dict | list | None:
|
|
508
|
-
import
|
|
509
|
-
import urllib.error
|
|
612
|
+
import httpx
|
|
510
613
|
|
|
511
614
|
url = f"{_api_url()}/api/v1{path}"
|
|
512
|
-
body = json.dumps(data).encode() if data is not None else None
|
|
513
|
-
req = urllib.request.Request(url, data=body, headers=_headers(), method=method)
|
|
514
615
|
try:
|
|
515
|
-
with
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if e.
|
|
616
|
+
with httpx.Client(headers=_headers(), timeout=timeout) as client:
|
|
617
|
+
resp = client.request(method, url, json=data)
|
|
618
|
+
resp.raise_for_status()
|
|
619
|
+
return resp.json() if resp.content else None
|
|
620
|
+
except httpx.HTTPStatusError as e:
|
|
621
|
+
if e.response.status_code == 401 and allow_refresh and _refresh_access_token():
|
|
521
622
|
return _request_json(
|
|
522
623
|
path,
|
|
523
624
|
method=method,
|
|
@@ -525,10 +626,11 @@ def _request_json(
|
|
|
525
626
|
timeout=timeout,
|
|
526
627
|
allow_refresh=False,
|
|
527
628
|
)
|
|
528
|
-
|
|
629
|
+
body_text = e.response.text
|
|
630
|
+
print(f"Error {e.response.status_code}: {body_text}", file=sys.stderr)
|
|
529
631
|
sys.exit(1)
|
|
530
|
-
except
|
|
531
|
-
print(f"Connection error: {e
|
|
632
|
+
except httpx.RequestError as e:
|
|
633
|
+
print(f"Connection error: {e}\nServer: {_api_url()}", file=sys.stderr)
|
|
532
634
|
sys.exit(1)
|
|
533
635
|
|
|
534
636
|
|
|
@@ -664,15 +766,14 @@ def _get_runtimes(include_all: bool = False) -> list[dict]:
|
|
|
664
766
|
|
|
665
767
|
def _get_runtimes_silent(include_all: bool = False) -> list[dict]:
|
|
666
768
|
"""Like _get_runtimes() but returns [] instead of sys.exit on any error."""
|
|
667
|
-
import
|
|
769
|
+
import httpx
|
|
668
770
|
|
|
669
771
|
path = "/runtimes" if include_all else "/runtimes/me"
|
|
670
772
|
url = f"{_api_url()}/api/v1{path}"
|
|
671
|
-
req = urllib.request.Request(url, headers=_headers(), method="GET")
|
|
672
773
|
try:
|
|
673
|
-
with
|
|
674
|
-
|
|
675
|
-
result = json
|
|
774
|
+
with httpx.Client(headers=_headers(), timeout=10) as client:
|
|
775
|
+
resp = client.get(url)
|
|
776
|
+
result = resp.json() if resp.content else []
|
|
676
777
|
return result if isinstance(result, list) else []
|
|
677
778
|
except Exception:
|
|
678
779
|
return []
|
|
@@ -1535,6 +1636,16 @@ def main() -> None:
|
|
|
1535
1636
|
parser.print_help()
|
|
1536
1637
|
return
|
|
1537
1638
|
|
|
1639
|
+
# Start background update-check now (writes cache if stale; zero latency).
|
|
1640
|
+
# Skip for `upgrade` (user is already updating) and raw log tailing
|
|
1641
|
+
# (`daemon logs -f` / `logs -f`) which never returns.
|
|
1642
|
+
_skip_update_check = cmd == "upgrade" or (
|
|
1643
|
+
cmd in ("daemon", "logs")
|
|
1644
|
+
and getattr(args, "follow", False)
|
|
1645
|
+
)
|
|
1646
|
+
if not _skip_update_check:
|
|
1647
|
+
_launch_update_check_thread()
|
|
1648
|
+
|
|
1538
1649
|
dispatch: dict = {
|
|
1539
1650
|
"version": cmd_version,
|
|
1540
1651
|
"upgrade": cmd_upgrade,
|
|
@@ -1599,6 +1710,10 @@ def main() -> None:
|
|
|
1599
1710
|
else:
|
|
1600
1711
|
parser.print_help()
|
|
1601
1712
|
|
|
1713
|
+
# Show update notice as a footer (reads local cache — no network latency).
|
|
1714
|
+
if not _skip_update_check:
|
|
1715
|
+
_print_update_notice_if_available()
|
|
1716
|
+
|
|
1602
1717
|
|
|
1603
1718
|
if __name__ == "__main__":
|
|
1604
1719
|
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgexa-cli
|
|
3
|
-
Version: 1.14.
|
|
3
|
+
Version: 1.14.8
|
|
4
4
|
Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
|
|
5
5
|
Author-email: Jason Sun <dev.winds@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -26,8 +26,10 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
26
26
|
Requires-Python: >=3.9
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
Requires-Dist: httpx>=0.24
|
|
29
|
+
Requires-Dist: socksio>=1.0.0
|
|
29
30
|
Provides-Extra: daemon
|
|
30
31
|
Requires-Dist: httpx>=0.24; extra == "daemon"
|
|
32
|
+
Requires-Dist: socksio>=1.0.0; extra == "daemon"
|
|
31
33
|
|
|
32
34
|
# Forgexa CLI
|
|
33
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "forgexa-cli"
|
|
3
|
-
version = "1.14.
|
|
3
|
+
version = "1.14.8"
|
|
4
4
|
description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
|
|
5
5
|
requires-python = ">=3.9"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -27,11 +27,13 @@ classifiers = [
|
|
|
27
27
|
]
|
|
28
28
|
# httpx is required for the daemon (core feature). Declaring it here ensures pip/pipx
|
|
29
29
|
# installs it automatically — no fragile runtime auto-install needed.
|
|
30
|
-
|
|
30
|
+
# socksio is required for SOCKS proxy support (httpx picks up ALL_PROXY/SOCKS_PROXY env vars
|
|
31
|
+
# automatically, and raises ImportError if socksio is missing).
|
|
32
|
+
dependencies = ["httpx>=0.24", "socksio>=1.0.0"]
|
|
31
33
|
|
|
32
34
|
[project.optional-dependencies]
|
|
33
35
|
# Keep [daemon] extra for backward compatibility / explicit opt-in.
|
|
34
|
-
daemon = ["httpx>=0.24"]
|
|
36
|
+
daemon = ["httpx>=0.24", "socksio>=1.0.0"]
|
|
35
37
|
|
|
36
38
|
[project.urls]
|
|
37
39
|
Homepage = "https://forgexa.net"
|
|
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
|