forgexa-cli 1.14.6__tar.gz → 1.14.7__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.7}/PKG-INFO +3 -1
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli/daemon.py +1 -1
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli/main.py +145 -30
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli.egg-info/PKG-INFO +3 -1
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli.egg-info/requires.txt +2 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/pyproject.toml +5 -3
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/README.md +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/setup.cfg +0 -0
- {forgexa_cli-1.14.6 → forgexa_cli-1.14.7}/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.7
|
|
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.7"
|
|
@@ -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.7"
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
def _detect_client_type() -> str:
|
|
@@ -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.7
|
|
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.7"
|
|
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
|