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.
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/PKG-INFO +1 -1
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli/daemon.py +67 -12
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/pyproject.toml +1 -1
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/README.md +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/setup.cfg +0 -0
- {forgexa_cli-1.14.7 → forgexa_cli-1.14.8}/tests/test_auth_and_runtime_commands.py +0 -0
|
@@ -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,
|
|
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
|
|
File without changes
|
|
File without changes
|