diff --git a/.github/workflows/windows-test.yml b/.github/workflows/windows-test.yml new file mode 100644 index 00000000..f2cb84fc --- /dev/null +++ b/.github/workflows/windows-test.yml @@ -0,0 +1,38 @@ +name: Windows Setup Test + +on: + push: + branches: [main] + paths: + - 'setup.sh' + - 'src/aipass/*/apps/handlers/__init__.py' + - 'src/aipass/drone/cli.py' + - 'pyproject.toml' + pull_request: + paths: + - 'setup.sh' + - 'src/aipass/*/apps/handlers/__init__.py' + - 'src/aipass/drone/cli.py' + - 'pyproject.toml' + +jobs: + windows-setup: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Run setup.sh + shell: bash + run: bash setup.sh + + - name: Verify drone CLI + shell: bash + run: | + export PYTHONUTF8=1 + drone --version + drone systems + drone @seedgo --help diff --git a/src/aipass/ai_mail/apps/handlers/__init__.py b/src/aipass/ai_mail/apps/handlers/__init__.py index 358af62a..6fc462bc 100755 --- a/src/aipass/ai_mail/apps/handlers/__init__.py +++ b/src/aipass/ai_mail/apps/handlers/__init__.py @@ -78,7 +78,7 @@ def _guard_branch_access(): return # Allow if truly can't determine # Check if caller is from our branch - if f"/{MY_BRANCH}/" in caller_file: + if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"): return # Same branch, allowed # External caller - block access diff --git a/src/aipass/api/apps/handlers/__init__.py b/src/aipass/api/apps/handlers/__init__.py index 82b8fd6b..b8582eab 100644 --- a/src/aipass/api/apps/handlers/__init__.py +++ b/src/aipass/api/apps/handlers/__init__.py @@ -53,7 +53,7 @@ def _guard_branch_access(): return # Allow command-line Python through return - if f"/{MY_BRANCH}/" in caller_file: + if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"): return caller_branch = _extract_branch_name(caller_file) diff --git a/src/aipass/cli/apps/handlers/__init__.py b/src/aipass/cli/apps/handlers/__init__.py index 7acaaf85..228443ab 100755 --- a/src/aipass/cli/apps/handlers/__init__.py +++ b/src/aipass/cli/apps/handlers/__init__.py @@ -99,7 +99,7 @@ def _guard_branch_access(): return # Allow if truly can't determine # Check if caller is from our branch - if f"/{MY_BRANCH}/" in caller_file: + if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"): return # Same branch, allowed # External caller - block access diff --git a/src/aipass/devpulse/apps/handlers/watchdog/registry.py b/src/aipass/devpulse/apps/handlers/watchdog/registry.py index 1f08285c..00a19654 100644 --- a/src/aipass/devpulse/apps/handlers/watchdog/registry.py +++ b/src/aipass/devpulse/apps/handlers/watchdog/registry.py @@ -40,7 +40,6 @@ Registry file schema (version 1): } """ -import fcntl import json import os import secrets @@ -122,7 +121,7 @@ def _atomic_write_unlocked(storage_path: Path, data: dict) -> None: class _FileLock: - """fcntl.flock-based exclusive lock on a sibling .lock file. + """Exclusive lock on a sibling .lock file (fcntl on Unix, no-op on Windows). Using a sibling avoids racing with the atomic replace of the data file: if we locked the data file itself, os.replace would swap the inode out @@ -134,6 +133,9 @@ class _FileLock: self._fh = None def __enter__(self) -> "_FileLock": + if sys.platform == "win32": + return self # Windows: skip file locking (single-user typical) + import fcntl self._lock_path.parent.mkdir(parents=True, exist_ok=True) # 'a+' so the file is created if missing and lock survives concurrent opens. self._fh = open(self._lock_path, "a+", encoding='utf-8') @@ -143,6 +145,7 @@ class _FileLock: def __exit__(self, exc_type, exc, tb) -> None: if self._fh is not None: try: + import fcntl fcntl.flock(self._fh.fileno(), fcntl.LOCK_UN) finally: self._fh.close() diff --git a/src/aipass/drone/apps/handlers/__init__.py b/src/aipass/drone/apps/handlers/__init__.py index 184e6e87..e6835e4d 100644 --- a/src/aipass/drone/apps/handlers/__init__.py +++ b/src/aipass/drone/apps/handlers/__init__.py @@ -80,7 +80,7 @@ def _guard_branch_access(): # Check if caller is from our branch # MY_BRANCH is "aipass.drone" (dotted), but filesystem uses "/aipass/drone/" branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" - if branch_path in caller_file: + if branch_path in caller_file.replace("\\", "/"): return # Same branch, allowed # External caller - block access diff --git a/src/aipass/flow/apps/flow.py b/src/aipass/flow/apps/flow.py index 8a3d3a01..1cd981e7 100755 --- a/src/aipass/flow/apps/flow.py +++ b/src/aipass/flow/apps/flow.py @@ -26,7 +26,9 @@ import signal from typing import List, Any # Handle broken pipe gracefully (e.g. output piped to head) -signal.signal(signal.SIGPIPE, signal.SIG_DFL) +# SIGPIPE does not exist on Windows +if hasattr(signal, 'SIGPIPE'): + signal.signal(signal.SIGPIPE, signal.SIG_DFL) # Prax logger from aipass.prax.apps.modules.logger import system_logger as logger diff --git a/src/aipass/flow/apps/handlers/__init__.py b/src/aipass/flow/apps/handlers/__init__.py index 1c805ac9..81c53f40 100644 --- a/src/aipass/flow/apps/handlers/__init__.py +++ b/src/aipass/flow/apps/handlers/__init__.py @@ -78,7 +78,7 @@ def _guard_branch_access(): return # Allow if truly can't determine # Check if caller is from our branch - if f"/{MY_BRANCH}/" in caller_file: + if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"): return # Same branch, allowed # External caller - block access diff --git a/src/aipass/memory/apps/handlers/__init__.py b/src/aipass/memory/apps/handlers/__init__.py index e1e29a07..db19c0bb 100644 --- a/src/aipass/memory/apps/handlers/__init__.py +++ b/src/aipass/memory/apps/handlers/__init__.py @@ -80,7 +80,7 @@ def _guard_branch_access(): # Check if caller is from our branch # MY_BRANCH is "aipass.memory" (dotted), but filesystem uses "/aipass/memory/" branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" - if branch_path in caller_file: + if branch_path in caller_file.replace("\\", "/"): return # Same branch, allowed # External caller - block access diff --git a/src/aipass/prax/apps/handlers/__init__.py b/src/aipass/prax/apps/handlers/__init__.py index e0d00a45..2eec073a 100755 --- a/src/aipass/prax/apps/handlers/__init__.py +++ b/src/aipass/prax/apps/handlers/__init__.py @@ -80,7 +80,7 @@ def _guard_branch_access(): # Check if caller is from our branch # MY_BRANCH is "aipass.prax" (dotted), but filesystem uses "/aipass/prax/" branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" - if branch_path in caller_file: + if branch_path in caller_file.replace("\\", "/"): return # Same branch, allowed # External caller - block access diff --git a/src/aipass/seedgo/apps/handlers/__init__.py b/src/aipass/seedgo/apps/handlers/__init__.py index fa95f808..a8c7226e 100644 --- a/src/aipass/seedgo/apps/handlers/__init__.py +++ b/src/aipass/seedgo/apps/handlers/__init__.py @@ -85,7 +85,7 @@ def _guard_branch_access(): # Check if caller is from our branch # MY_BRANCH is "aipass.seedgo" (dotted), but filesystem uses "/aipass/seedgo/" branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" - if branch_path in caller_file: + if branch_path in caller_file.replace("\\", "/"): return # Same branch, allowed # External caller - block access diff --git a/src/aipass/spawn/apps/handlers/__init__.py b/src/aipass/spawn/apps/handlers/__init__.py index 94cfcc89..4f7fb4b9 100644 --- a/src/aipass/spawn/apps/handlers/__init__.py +++ b/src/aipass/spawn/apps/handlers/__init__.py @@ -59,7 +59,7 @@ def _guard_branch_access(): return branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" - if branch_path in caller_file: + if branch_path in caller_file.replace("\\", "/"): return caller_branch = _extract_branch_name(caller_file) diff --git a/src/aipass/spawn/templates/builder/apps/handlers/__init__.py b/src/aipass/spawn/templates/builder/apps/handlers/__init__.py index d8ba28a3..9b578d07 100644 --- a/src/aipass/spawn/templates/builder/apps/handlers/__init__.py +++ b/src/aipass/spawn/templates/builder/apps/handlers/__init__.py @@ -59,7 +59,7 @@ def _guard_branch_access(): return branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" - if branch_path in caller_file: + if branch_path in caller_file.replace("\\", "/"): return caller_branch = _extract_branch_name(caller_file) diff --git a/src/aipass/trigger/apps/config.py b/src/aipass/trigger/apps/config.py index 542cb71a..b61a3f36 100644 --- a/src/aipass/trigger/apps/config.py +++ b/src/aipass/trigger/apps/config.py @@ -13,8 +13,8 @@ Provides package-relative paths for trigger data directories. Works in both pip-installed and development environments. """ -import fcntl import json +import sys import os import tempfile from contextlib import contextmanager @@ -81,12 +81,17 @@ def json_file_lock(path: Path): """ lock_path = path.with_suffix('.lock') lock_path.parent.mkdir(parents=True, exist_ok=True) - with open(lock_path, 'w', encoding='utf-8') as lock_f: - fcntl.flock(lock_f, fcntl.LOCK_EX) - try: - yield - finally: - fcntl.flock(lock_f, fcntl.LOCK_UN) + if sys.platform == "win32": + # Windows: no fcntl, skip file locking (single-user typical) + yield + else: + import fcntl + with open(lock_path, 'w', encoding='utf-8') as lock_f: + fcntl.flock(lock_f, fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(lock_f, fcntl.LOCK_UN) def print_introspection(): diff --git a/src/aipass/trigger/apps/handlers/__init__.py b/src/aipass/trigger/apps/handlers/__init__.py index 40ae30d6..56e8611f 100644 --- a/src/aipass/trigger/apps/handlers/__init__.py +++ b/src/aipass/trigger/apps/handlers/__init__.py @@ -53,7 +53,7 @@ def _guard_branch_access(): return # Allow command-line Python through return - if f"/trigger/" in caller_file: + if f"/trigger/" in caller_file.replace("\\", "/"): return caller_branch = _extract_branch_name(caller_file)