forgexa-cli 1.14.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.14.7
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
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.14.7"
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.7"
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 read-only files on Windows.
1715
-
1716
- On Windows, git object files (.git/objects/**/*) are stored read-only
1717
- (mode 0444). Plain shutil.rmtree(path, ignore_errors=True) silently
1718
- skips those files, leaving the directory intact. A subsequent
1719
- git clone then fails with 'destination path already exists and is
1720
- not an empty directory'.
1721
-
1722
- This method uses the onerror callback to chmod each entry writable
1723
- before retrying the established pattern for handling git repos on
1724
- Windows.
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.14.7
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.14.7"
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" }
File without changes
File without changes