forgexa-cli 1.16.3__tar.gz → 1.16.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.16.3
3
+ Version: 1.16.5
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-Expression: MIT
@@ -127,10 +127,11 @@ The command detects how `forgexa-cli` was installed:
127
127
 
128
128
  - `pipx` install: uses `pipx upgrade forgexa-cli`, or `pipx install --force forgexa-cli==<version>` for a pinned version
129
129
  - `pip` install: uses `python -m pip install --upgrade forgexa-cli`
130
+ - non-editable local path install (`pip install /path/to/cli`): follows the same `pip` upgrade path and switches to the published PyPI release
130
131
 
131
- For safety, `forgexa upgrade` stops any running local daemon before upgrading. Restart it afterward with `forgexa daemon start`.
132
+ For safety, `forgexa upgrade` stops any running local daemon before upgrading. If the daemon was previously started in background mode, the CLI starts it again automatically after a successful upgrade; otherwise it prints a manual restart hint.
132
133
 
133
- `forgexa upgrade` is intended for standard PyPI / pipx installs. Editable installs, local path installs, and VCS installs are rejected with a manual upgrade hint.
134
+ `forgexa upgrade` supports standard PyPI / pipx installs and non-editable local path installs. Editable installs, VCS installs, and other direct URL installs are rejected with a manual upgrade hint.
134
135
 
135
136
  ## Configuration
136
137
 
@@ -95,10 +95,11 @@ The command detects how `forgexa-cli` was installed:
95
95
 
96
96
  - `pipx` install: uses `pipx upgrade forgexa-cli`, or `pipx install --force forgexa-cli==<version>` for a pinned version
97
97
  - `pip` install: uses `python -m pip install --upgrade forgexa-cli`
98
+ - non-editable local path install (`pip install /path/to/cli`): follows the same `pip` upgrade path and switches to the published PyPI release
98
99
 
99
- For safety, `forgexa upgrade` stops any running local daemon before upgrading. Restart it afterward with `forgexa daemon start`.
100
+ For safety, `forgexa upgrade` stops any running local daemon before upgrading. If the daemon was previously started in background mode, the CLI starts it again automatically after a successful upgrade; otherwise it prints a manual restart hint.
100
101
 
101
- `forgexa upgrade` is intended for standard PyPI / pipx installs. Editable installs, local path installs, and VCS installs are rejected with a manual upgrade hint.
102
+ `forgexa upgrade` supports standard PyPI / pipx installs and non-editable local path installs. Editable installs, VCS installs, and other direct URL installs are rejected with a manual upgrade hint.
102
103
 
103
104
  ## Configuration
104
105
 
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.16.3"
2
+ __version__ = "1.16.5"
@@ -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.16.3"
526
+ DAEMON_VERSION = "1.16.5"
527
527
 
528
528
 
529
529
  def _detect_client_type() -> str:
@@ -402,7 +402,7 @@ def _unsupported_upgrade_source(direct_url: dict | None) -> str | None:
402
402
  return f"{vcs} checkout"
403
403
  url = str(direct_url.get("url") or "").strip()
404
404
  if url.startswith("file:"):
405
- return "local path install"
405
+ return None
406
406
  if url:
407
407
  return "direct URL install"
408
408
  return "non-standard install source"
@@ -694,6 +694,62 @@ def _stop_daemon_if_running() -> tuple[bool, int | None]:
694
694
  return True, pid
695
695
 
696
696
 
697
+ _DAEMON_START_CONTEXT_PATH: Path = Path.home() / ".forgexa" / "daemon" / "start-context.json"
698
+
699
+
700
+ def _write_daemon_start_context(*, mode: str, server_url: str | None) -> None:
701
+ try:
702
+ _DAEMON_START_CONTEXT_PATH.parent.mkdir(parents=True, exist_ok=True)
703
+ _DAEMON_START_CONTEXT_PATH.write_text(
704
+ json.dumps(
705
+ {
706
+ "mode": mode,
707
+ "server_url": (server_url or "").rstrip("/") or None,
708
+ }
709
+ )
710
+ )
711
+ except Exception:
712
+ pass
713
+
714
+
715
+ def _read_daemon_start_context() -> dict[str, str] | None:
716
+ try:
717
+ payload = json.loads(_DAEMON_START_CONTEXT_PATH.read_text())
718
+ except Exception:
719
+ return None
720
+ if not isinstance(payload, dict):
721
+ return None
722
+ mode = str(payload.get("mode") or "").strip()
723
+ server_url = str(payload.get("server_url") or "").strip().rstrip("/")
724
+ result: dict[str, str] = {}
725
+ if mode:
726
+ result["mode"] = mode
727
+ if server_url:
728
+ result["server_url"] = server_url
729
+ return result or None
730
+
731
+
732
+ def _start_daemon_after_upgrade() -> tuple[bool, str | None]:
733
+ context = _read_daemon_start_context() or {}
734
+ if context.get("mode") != "background":
735
+ return False, None
736
+
737
+ server_url = context.get("server_url") or None
738
+ try:
739
+ cmd_daemon_start(
740
+ argparse.Namespace(
741
+ detach=True,
742
+ server_url=server_url,
743
+ )
744
+ )
745
+ except SystemExit as exc:
746
+ return False, f"Auto-restart failed with exit code {exc.code}."
747
+ except Exception as exc:
748
+ return False, str(exc)
749
+
750
+ return True, server_url
751
+
752
+
697
753
  def _request_json(
698
754
  path: str,
699
755
  *,
@@ -1120,6 +1176,11 @@ def cmd_daemon_start(args: argparse.Namespace) -> None:
1120
1176
  if token:
1121
1177
  os.environ.setdefault("DAEMON_API_TOKEN", token)
1122
1178
 
1179
+ _write_daemon_start_context(
1180
+ mode="background" if getattr(args, "detach", False) else "foreground",
1181
+ server_url=server_url,
1182
+ )
1183
+
1123
1184
  if getattr(args, "detach", False):
1124
1185
  # ── Background (detach) mode ─────────────────────────────────────────
1125
1186
  import subprocess as sp
@@ -1500,7 +1561,13 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
1500
1561
  installed_version = _installed_cli_version() or "unknown"
1501
1562
  print(f"Upgrade complete. Installed version: {installed_version}")
1502
1563
  if daemon_stopped:
1503
- print("Restart the daemon with: forgexa daemon start")
1564
+ restarted, server_url = _start_daemon_after_upgrade()
1565
+ if restarted:
1566
+ print("Daemon restarted in background.")
1567
+ if server_url:
1568
+ print(f" Server : {server_url}")
1569
+ else:
1570
+ print("Restart the daemon with: forgexa daemon start")
1504
1571
 
1505
1572
 
1506
1573
  # ── Argument parser ──
@@ -1751,10 +1818,6 @@ def main() -> None:
1751
1818
  _output_format = args.format or "table"
1752
1819
 
1753
1820
  cmd = args.command
1754
- if not cmd:
1755
- parser.print_help()
1756
- return
1757
-
1758
1821
  # Start background update-check now (writes cache if stale; zero latency).
1759
1822
  # Skip for `upgrade` (user is already updating) and raw log tailing
1760
1823
  # (`daemon logs -f` / `logs -f`) which never returns.
@@ -1765,6 +1828,11 @@ def main() -> None:
1765
1828
  if not _skip_update_check:
1766
1829
  _launch_update_check_process()
1767
1830
 
1831
+ if not cmd:
1832
+ parser.print_help()
1833
+ _print_update_notice_if_available()
1834
+ return
1835
+
1768
1836
  dispatch: dict = {
1769
1837
  "version": cmd_version,
1770
1838
  "upgrade": cmd_upgrade,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.16.3
3
+ Version: 1.16.5
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-Expression: MIT
@@ -127,10 +127,11 @@ The command detects how `forgexa-cli` was installed:
127
127
 
128
128
  - `pipx` install: uses `pipx upgrade forgexa-cli`, or `pipx install --force forgexa-cli==<version>` for a pinned version
129
129
  - `pip` install: uses `python -m pip install --upgrade forgexa-cli`
130
+ - non-editable local path install (`pip install /path/to/cli`): follows the same `pip` upgrade path and switches to the published PyPI release
130
131
 
131
- For safety, `forgexa upgrade` stops any running local daemon before upgrading. Restart it afterward with `forgexa daemon start`.
132
+ For safety, `forgexa upgrade` stops any running local daemon before upgrading. If the daemon was previously started in background mode, the CLI starts it again automatically after a successful upgrade; otherwise it prints a manual restart hint.
132
133
 
133
- `forgexa upgrade` is intended for standard PyPI / pipx installs. Editable installs, local path installs, and VCS installs are rejected with a manual upgrade hint.
134
+ `forgexa upgrade` supports standard PyPI / pipx installs and non-editable local path installs. Editable installs, VCS installs, and other direct URL installs are rejected with a manual upgrade hint.
134
135
 
135
136
  ## Configuration
136
137
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.16.3"
3
+ version = "1.16.5"
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 = "MIT"
@@ -266,6 +266,29 @@ def test_launch_update_check_process_spawns_background_python_when_cache_is_stal
266
266
  assert kwargs["stderr"] is main.subprocess.DEVNULL
267
267
 
268
268
 
269
+ def test_main_without_command_still_runs_update_notice_flow(
270
+ monkeypatch: pytest.MonkeyPatch,
271
+ ) -> None:
272
+ calls: list[str] = []
273
+
274
+ monkeypatch.setattr(main.sys, "argv", ["forgexa"])
275
+ monkeypatch.setattr(
276
+ main,
277
+ "_launch_update_check_process",
278
+ lambda: calls.append("launch"),
279
+ )
280
+ monkeypatch.setattr(
281
+ main,
282
+ "_print_update_notice_if_available",
283
+ lambda: calls.append("notice"),
284
+ )
285
+
286
+ with contextlib.redirect_stdout(io.StringIO()):
287
+ main.main()
288
+
289
+ assert calls == ["launch", "notice"]
290
+
291
+
269
292
  def test_build_upgrade_plan_for_pipx_latest(monkeypatch: pytest.MonkeyPatch) -> None:
270
293
  monkeypatch.setattr(main, "_cli_distribution", lambda: SimpleNamespace(version="1.12.3"))
271
294
  monkeypatch.setattr(main, "_distribution_root", lambda dist: Path("/installed/site-packages"))
@@ -339,6 +362,41 @@ def test_build_upgrade_plan_adds_break_system_packages_on_pep668(monkeypatch: py
339
362
  assert plan.command[-1] == "forgexa-cli"
340
363
 
341
364
 
365
+ def test_build_upgrade_plan_allows_non_editable_local_path_installs(monkeypatch: pytest.MonkeyPatch) -> None:
366
+ monkeypatch.setattr(main, "_cli_distribution", lambda: SimpleNamespace(version="1.15.7"))
367
+ monkeypatch.setattr(
368
+ main,
369
+ "_distribution_root",
370
+ lambda dist: Path("/home/demo/.local/lib/python3.12/site-packages"),
371
+ )
372
+ monkeypatch.setattr(main, "_running_from_distribution_root", lambda root: True)
373
+ monkeypatch.setattr(
374
+ main,
375
+ "_direct_url_metadata",
376
+ lambda dist: {"dir_info": {}, "url": "file:///opt/forgexa/cli"},
377
+ )
378
+ monkeypatch.setattr(main, "_looks_like_pipx_environment", lambda python=None: False)
379
+ monkeypatch.setattr(main.importlib.util, "find_spec", lambda name: object() if name == "pip" else None)
380
+ monkeypatch.setattr(main, "_is_user_site_install", lambda root: True)
381
+ monkeypatch.setattr(main, "_is_virtualenv", lambda: False)
382
+ monkeypatch.setattr(main, "_is_pep668_environment", lambda: True)
383
+ monkeypatch.setattr(main.sys, "executable", "/usr/bin/python3")
384
+
385
+ plan = main._build_upgrade_plan(None)
386
+
387
+ assert plan.installer == "pip"
388
+ assert plan.command == [
389
+ "/usr/bin/python3",
390
+ "-m",
391
+ "pip",
392
+ "install",
393
+ "--upgrade",
394
+ "--user",
395
+ "--break-system-packages",
396
+ "forgexa-cli",
397
+ ]
398
+
399
+
342
400
  def test_pep668_detection_returns_false_in_normal_venv(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
343
401
  monkeypatch.setattr(main.sys, "prefix", str(tmp_path))
344
402
  import sysconfig as _sc
@@ -378,6 +436,7 @@ def test_cmd_upgrade_runs_planned_command(monkeypatch: pytest.MonkeyPatch) -> No
378
436
  monkeypatch.setattr(main, "_normalize_target_version", lambda value: value)
379
437
  monkeypatch.setattr(main, "_stop_daemon_if_running", lambda: (False, None))
380
438
  monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.12.3")
439
+ monkeypatch.setattr(main, "_start_daemon_after_upgrade", lambda: (False, None))
381
440
 
382
441
  def fake_run(cmd):
383
442
  calls.append(cmd)
@@ -394,6 +453,64 @@ def test_cmd_upgrade_runs_planned_command(monkeypatch: pytest.MonkeyPatch) -> No
394
453
  assert "Upgrade complete. Installed version: 1.12.3" in output
395
454
 
396
455
 
456
+ def test_cmd_upgrade_restarts_background_daemon_after_success(
457
+ monkeypatch: pytest.MonkeyPatch,
458
+ ) -> None:
459
+ plan = main.UpgradePlan(
460
+ installer="pip",
461
+ command=["python3", "-m", "pip", "install", "--upgrade", "forgexa-cli"],
462
+ current_version="1.16.3",
463
+ target_version=None,
464
+ )
465
+ start_calls: list[bool] = []
466
+
467
+ monkeypatch.setattr(main, "_build_upgrade_plan", lambda target: plan)
468
+ monkeypatch.setattr(main, "_normalize_target_version", lambda value: value)
469
+ monkeypatch.setattr(main, "_stop_daemon_if_running", lambda: (True, 4242))
470
+ monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.16.4")
471
+ monkeypatch.setattr(main.subprocess, "run", lambda cmd: SimpleNamespace(returncode=0))
472
+ monkeypatch.setattr(
473
+ main,
474
+ "_start_daemon_after_upgrade",
475
+ lambda: start_calls.append(True) or (True, "https://api.example.com"),
476
+ )
477
+
478
+ buffer = io.StringIO()
479
+ with contextlib.redirect_stdout(buffer):
480
+ main.cmd_upgrade(SimpleNamespace(target_version=None))
481
+
482
+ output = buffer.getvalue()
483
+ assert start_calls == [True]
484
+ assert "Stopped local daemon (PID 4242) before upgrade." in output
485
+ assert "Daemon restarted in background." in output
486
+ assert "Server : https://api.example.com" in output
487
+
488
+
489
+ def test_cmd_upgrade_keeps_manual_restart_hint_when_auto_restart_skips(
490
+ monkeypatch: pytest.MonkeyPatch,
491
+ ) -> None:
492
+ plan = main.UpgradePlan(
493
+ installer="pip",
494
+ command=["python3", "-m", "pip", "install", "--upgrade", "forgexa-cli"],
495
+ current_version="1.16.3",
496
+ target_version=None,
497
+ )
498
+
499
+ monkeypatch.setattr(main, "_build_upgrade_plan", lambda target: plan)
500
+ monkeypatch.setattr(main, "_normalize_target_version", lambda value: value)
501
+ monkeypatch.setattr(main, "_stop_daemon_if_running", lambda: (True, 4242))
502
+ monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.16.4")
503
+ monkeypatch.setattr(main.subprocess, "run", lambda cmd: SimpleNamespace(returncode=0))
504
+ monkeypatch.setattr(main, "_start_daemon_after_upgrade", lambda: (False, None))
505
+
506
+ buffer = io.StringIO()
507
+ with contextlib.redirect_stdout(buffer):
508
+ main.cmd_upgrade(SimpleNamespace(target_version=None))
509
+
510
+ output = buffer.getvalue()
511
+ assert "Restart the daemon with: forgexa daemon start" in output
512
+
513
+
397
514
  def test_cmd_upgrade_noops_when_target_matches_current(monkeypatch: pytest.MonkeyPatch) -> None:
398
515
  plan = main.UpgradePlan(
399
516
  installer="pip",
File without changes