forgexa-cli 1.15.6__tar.gz → 1.16.0__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.15.6 → forgexa_cli-1.16.0}/PKG-INFO +2 -3
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli/daemon.py +20 -3
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli/main.py +35 -22
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli.egg-info/PKG-INFO +2 -3
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/pyproject.toml +3 -4
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/tests/test_auth_and_runtime_commands.py +59 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/README.md +0 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.15.6 → forgexa_cli-1.16.0}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgexa-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
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
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://forgexa.net
|
|
8
8
|
Project-URL: Documentation, https://docs.forgexa.net
|
|
9
9
|
Project-URL: Repository, https://github.com/forgexa/forgexa
|
|
@@ -12,7 +12,6 @@ Keywords: forgexa,ai,software-factory,cli,devops,agent
|
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
15
|
Classifier: Operating System :: OS Independent
|
|
17
16
|
Classifier: Programming Language :: Python :: 3
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.16.0"
|
|
@@ -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.
|
|
526
|
+
DAEMON_VERSION = "1.16.0"
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
def _detect_client_type() -> str:
|
|
@@ -558,7 +558,9 @@ _CLIENT_TYPE = _detect_client_type()
|
|
|
558
558
|
# Set DAEMON_NO_CONSOLE_LOG=1 to suppress the stream handler even on a TTY
|
|
559
559
|
# (used by `forgexa daemon start` foreground mode for clean UX).
|
|
560
560
|
_log_dir = Path.home() / ".forgexa" / "daemon"
|
|
561
|
-
_log_dir.mkdir(parents=True, exist_ok=True)
|
|
561
|
+
_log_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
562
|
+
# Ensure directory has restricted permissions even if it already existed
|
|
563
|
+
_log_dir.chmod(0o700)
|
|
562
564
|
DAEMON_LOG_PATH = _log_dir / "daemon.log"
|
|
563
565
|
|
|
564
566
|
_log_handlers: list[logging.Handler] = [
|
|
@@ -568,6 +570,9 @@ _log_handlers: list[logging.Handler] = [
|
|
|
568
570
|
backupCount=5,
|
|
569
571
|
),
|
|
570
572
|
]
|
|
573
|
+
# L-4: Restrict log file to owner-read/write only (shared-host protection)
|
|
574
|
+
if DAEMON_LOG_PATH.exists():
|
|
575
|
+
DAEMON_LOG_PATH.chmod(0o600)
|
|
571
576
|
if sys.stderr.isatty() and not os.environ.get("DAEMON_NO_CONSOLE_LOG"):
|
|
572
577
|
_log_handlers.append(logging.StreamHandler(sys.stderr))
|
|
573
578
|
|
|
@@ -878,6 +883,15 @@ def _daemon_extract_codex_output(raw: str) -> str:
|
|
|
878
883
|
continue
|
|
879
884
|
if not isinstance(data, dict):
|
|
880
885
|
continue
|
|
886
|
+
|
|
887
|
+
if data.get("type") == "item.completed":
|
|
888
|
+
item = data.get("item") or {}
|
|
889
|
+
if item.get("type") == "agent_message":
|
|
890
|
+
text = item.get("text") or ""
|
|
891
|
+
if isinstance(text, str) and text.strip():
|
|
892
|
+
assistant_messages.append(text.strip())
|
|
893
|
+
continue
|
|
894
|
+
|
|
881
895
|
if data.get("type") != "message" or data.get("role") != "assistant":
|
|
882
896
|
continue
|
|
883
897
|
|
|
@@ -3173,7 +3187,10 @@ class WorkspaceManager:
|
|
|
3173
3187
|
key_content = self._ssh_keys.get(project_key)
|
|
3174
3188
|
if key_content:
|
|
3175
3189
|
import stat as stat_mod
|
|
3176
|
-
|
|
3190
|
+
# F-9: Use a dedicated directory with restricted permissions for SSH keys
|
|
3191
|
+
_ssh_dir = Path.home() / ".forgexa" / "ssh"
|
|
3192
|
+
_ssh_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
3193
|
+
fd, key_path = tempfile.mkstemp(prefix="forgexa_ssh_", suffix=".key", dir=str(_ssh_dir))
|
|
3177
3194
|
try:
|
|
3178
3195
|
os.write(fd, key_content.encode() if isinstance(key_content, str) else key_content)
|
|
3179
3196
|
if not key_content.rstrip().endswith("\n"):
|
|
@@ -478,8 +478,8 @@ def _build_upgrade_plan(target_version: str | None) -> UpgradePlan:
|
|
|
478
478
|
# ── Update-check helpers ──────────────────────────────────────────────────────
|
|
479
479
|
#
|
|
480
480
|
# Strategy (borrowed from gh CLI / update-notifier):
|
|
481
|
-
# 1. At the start of main(): launch a
|
|
482
|
-
# writes ~/.forgexa/update-check.json (only when the cache is stale).
|
|
481
|
+
# 1. At the start of main(): launch a detached Python worker that calls PyPI
|
|
482
|
+
# and writes ~/.forgexa/update-check.json (only when the cache is stale).
|
|
483
483
|
# 2. At the end of main(): read the cache (instant) and, if a newer version
|
|
484
484
|
# is available, print one notice to stderr.
|
|
485
485
|
#
|
|
@@ -534,31 +534,44 @@ def _version_is_newer(candidate: str, reference: str) -> bool:
|
|
|
534
534
|
return False
|
|
535
535
|
|
|
536
536
|
|
|
537
|
-
def
|
|
538
|
-
"""
|
|
537
|
+
def _run_update_check() -> None:
|
|
538
|
+
"""Fetch the latest published CLI version and refresh the local cache."""
|
|
539
|
+
try:
|
|
540
|
+
import httpx
|
|
541
|
+
|
|
542
|
+
with httpx.Client(timeout=5) as client:
|
|
543
|
+
resp = client.get("https://pypi.org/pypi/forgexa-cli/json")
|
|
544
|
+
resp.raise_for_status()
|
|
545
|
+
latest = str(resp.json()["info"]["version"])
|
|
546
|
+
_write_update_cache(latest)
|
|
547
|
+
except Exception:
|
|
548
|
+
pass # Network errors are silently ignored
|
|
539
549
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
550
|
+
|
|
551
|
+
def _launch_update_check_process() -> None:
|
|
552
|
+
"""Start a detached worker process that refreshes the update cache.
|
|
553
|
+
|
|
554
|
+
Short-lived commands like `forgexa version` can exit before a daemon thread
|
|
555
|
+
finishes its PyPI request. A separate Python process survives long enough to
|
|
556
|
+
populate the cache without blocking the foreground command.
|
|
543
557
|
"""
|
|
544
558
|
_, checked_at = _read_update_cache()
|
|
545
559
|
if time.time() - checked_at <= _UPDATE_CHECK_TTL:
|
|
546
560
|
return # Cache is fresh — nothing to do
|
|
547
561
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
t.start()
|
|
562
|
+
try:
|
|
563
|
+
subprocess.Popen(
|
|
564
|
+
[
|
|
565
|
+
sys.executable,
|
|
566
|
+
"-c",
|
|
567
|
+
"from forgexa_cli.main import _run_update_check; _run_update_check()",
|
|
568
|
+
],
|
|
569
|
+
stdin=subprocess.DEVNULL,
|
|
570
|
+
stdout=subprocess.DEVNULL,
|
|
571
|
+
stderr=subprocess.DEVNULL,
|
|
572
|
+
)
|
|
573
|
+
except Exception:
|
|
574
|
+
pass
|
|
562
575
|
|
|
563
576
|
|
|
564
577
|
def _print_update_notice_if_available() -> None:
|
|
@@ -1750,7 +1763,7 @@ def main() -> None:
|
|
|
1750
1763
|
and getattr(args, "follow", False)
|
|
1751
1764
|
)
|
|
1752
1765
|
if not _skip_update_check:
|
|
1753
|
-
|
|
1766
|
+
_launch_update_check_process()
|
|
1754
1767
|
|
|
1755
1768
|
dispatch: dict = {
|
|
1756
1769
|
"version": cmd_version,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgexa-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
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
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://forgexa.net
|
|
8
8
|
Project-URL: Documentation, https://docs.forgexa.net
|
|
9
9
|
Project-URL: Repository, https://github.com/forgexa/forgexa
|
|
@@ -12,7 +12,6 @@ Keywords: forgexa,ai,software-factory,cli,devops,agent
|
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
15
|
Classifier: Operating System :: OS Independent
|
|
17
16
|
Classifier: Programming Language :: Python :: 3
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "forgexa-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.16.0"
|
|
4
4
|
description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
|
|
5
5
|
requires-python = ">=3.9"
|
|
6
|
-
license =
|
|
6
|
+
license = "MIT"
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Jason Sun", email = "dev.winds@gmail.com" },
|
|
9
9
|
]
|
|
@@ -13,7 +13,6 @@ classifiers = [
|
|
|
13
13
|
"Development Status :: 4 - Beta",
|
|
14
14
|
"Environment :: Console",
|
|
15
15
|
"Intended Audience :: Developers",
|
|
16
|
-
"License :: OSI Approved :: MIT License",
|
|
17
16
|
"Operating System :: OS Independent",
|
|
18
17
|
"Programming Language :: Python :: 3",
|
|
19
18
|
"Programming Language :: Python :: 3.9",
|
|
@@ -46,7 +45,7 @@ forgexa = "forgexa_cli.main:main"
|
|
|
46
45
|
forgexa-daemon = "forgexa_cli.daemon:main_sync"
|
|
47
46
|
|
|
48
47
|
[build-system]
|
|
49
|
-
requires = ["setuptools>=
|
|
48
|
+
requires = ["setuptools>=77.0.0"]
|
|
50
49
|
build-backend = "setuptools.build_meta"
|
|
51
50
|
|
|
52
51
|
[tool.setuptools.packages.find]
|
|
@@ -207,6 +207,65 @@ def test_normalize_target_version_rejects_invalid_input() -> None:
|
|
|
207
207
|
main._normalize_target_version("../../1.2.3")
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
def test_run_update_check_writes_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
211
|
+
monkeypatch.setattr(main, "_UPDATE_CACHE_PATH", tmp_path / "update-check.json")
|
|
212
|
+
|
|
213
|
+
class FakeResponse:
|
|
214
|
+
def raise_for_status(self) -> None:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
def json(self) -> dict:
|
|
218
|
+
return {"info": {"version": "1.15.7"}}
|
|
219
|
+
|
|
220
|
+
class FakeClient:
|
|
221
|
+
def __init__(self, **kwargs):
|
|
222
|
+
assert kwargs["timeout"] == 5
|
|
223
|
+
|
|
224
|
+
def __enter__(self):
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def __exit__(self, *args):
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def get(self, url: str) -> FakeResponse:
|
|
231
|
+
assert url == "https://pypi.org/pypi/forgexa-cli/json"
|
|
232
|
+
return FakeResponse()
|
|
233
|
+
|
|
234
|
+
monkeypatch.setattr(httpx, "Client", FakeClient)
|
|
235
|
+
|
|
236
|
+
main._run_update_check()
|
|
237
|
+
|
|
238
|
+
payload = json.loads((tmp_path / "update-check.json").read_text())
|
|
239
|
+
assert payload["latest_version"] == "1.15.7"
|
|
240
|
+
assert payload["checked_at"] > 0
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_launch_update_check_process_spawns_background_python_when_cache_is_stale(
|
|
244
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
245
|
+
) -> None:
|
|
246
|
+
calls: list[tuple[list[str], dict]] = []
|
|
247
|
+
|
|
248
|
+
monkeypatch.setattr(main, "_read_update_cache", lambda: (None, 0.0))
|
|
249
|
+
monkeypatch.setattr(main.time, "time", lambda: float(main._UPDATE_CHECK_TTL + 10))
|
|
250
|
+
monkeypatch.setattr(main.sys, "executable", "/usr/bin/python3")
|
|
251
|
+
|
|
252
|
+
def fake_popen(cmd: list[str], **kwargs):
|
|
253
|
+
calls.append((cmd, kwargs))
|
|
254
|
+
return SimpleNamespace()
|
|
255
|
+
|
|
256
|
+
monkeypatch.setattr(main.subprocess, "Popen", fake_popen)
|
|
257
|
+
|
|
258
|
+
main._launch_update_check_process()
|
|
259
|
+
|
|
260
|
+
assert len(calls) == 1
|
|
261
|
+
cmd, kwargs = calls[0]
|
|
262
|
+
assert cmd[:2] == ["/usr/bin/python3", "-c"]
|
|
263
|
+
assert cmd[2] == "from forgexa_cli.main import _run_update_check; _run_update_check()"
|
|
264
|
+
assert kwargs["stdin"] is main.subprocess.DEVNULL
|
|
265
|
+
assert kwargs["stdout"] is main.subprocess.DEVNULL
|
|
266
|
+
assert kwargs["stderr"] is main.subprocess.DEVNULL
|
|
267
|
+
|
|
268
|
+
|
|
210
269
|
def test_build_upgrade_plan_for_pipx_latest(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
211
270
|
monkeypatch.setattr(main, "_cli_distribution", lambda: SimpleNamespace(version="1.12.3"))
|
|
212
271
|
monkeypatch.setattr(main, "_distribution_root", lambda dist: Path("/installed/site-packages"))
|
|
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
|