Skip to content

Commit 4d17158

Browse files
committed
Add comprehensive Windows support
- Fix config path lookup to prioritize ~/.config paths on Windows - Add Windows-specific installers (install.bat, install.ps1) - Update README with Windows support documentation and installation instructions - Add Windows compatibility badge and platform support section - Improve cross-platform configuration management This enables full Windows compatibility with unified config paths and dedicated installers.
1 parent c629454 commit 4d17158

File tree

16 files changed

+1148
-32
lines changed

16 files changed

+1148
-32
lines changed

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
[![License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
66
[![Code Quality](https://img.shields.io/badge/code%20quality-A+-brightgreen.svg)](https://github.com/Chat2AnyLLM/code-assistant-manager/actions)
7+
[![Windows Support](https://img.shields.io/badge/Windows-Supported-blue.svg)](https://github.com/Chat2AnyLLM/code-assistant-manager)
78

89
**One CLI to Rule Them All.**
910
<br>
1011
Tired of juggling multiple AI coding assistants? **CAM** is a unified Python CLI to manage configurations, prompts, skills, and plugins for **17 AI assistants** including Claude, Codex, Gemini, Qwen, Copilot, Blackbox, Goose, Continue, and more from a single, polished terminal interface.
1112

13+
**🪟 Windows Support:** Full Windows compatibility with dedicated installers and configuration management.
14+
1215
</div>
1316

1417
---
@@ -24,10 +27,23 @@ Choose your preferred installation method:
2427
pip install code-assistant-manager
2528
```
2629

30+
### Windows Installation
31+
32+
For Windows users, use the dedicated PowerShell installer:
33+
34+
```powershell
35+
# Download and run the PowerShell installer
36+
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Chat2AnyLLM/code-assistant-manager/main/install.ps1" -OutFile "install.ps1"
37+
.\install.ps1
38+
39+
# Or use the batch installer (alternative)
40+
install.bat
41+
```
42+
2743
### Alternative Methods
2844

2945
```bash
30-
# Install using the install script
46+
# Install using the install script (Linux/macOS)
3147
./install.sh
3248

3349
# Or install directly from the web
@@ -41,6 +57,29 @@ pip install -e ".[dev]"
4157

4258
---
4359

60+
## Platform Support
61+
62+
CAM is cross-platform and works on **Linux**, **macOS**, and **Windows**.
63+
64+
### Windows Features
65+
66+
- **Unified Configuration:** Uses `~/.config/code-assistant-manager/` paths (consistent with Unix systems)
67+
- **PowerShell Integration:** Native PowerShell installer with automatic PATH configuration
68+
- **Cross-Platform Compatibility:** All features work identically across platforms
69+
- **Environment Variables:** Full support for Windows environment variable configuration
70+
71+
### Configuration Locations
72+
73+
CAM automatically detects configuration files in the following order:
74+
75+
1. `~/.config/code-assistant-manager/providers.json` (primary location)
76+
2. `%APPDATA%/code-assistant-manager/providers.json` (Windows roaming)
77+
3. `%LOCALAPPDATA%/code-assistant-manager/providers.json` (Windows local)
78+
4. `./providers.json` (current directory)
79+
5. `~/providers.json` (home directory)
80+
81+
---
82+
4483
## Quick Start
4584

4685
1. **Set up your API keys** in a `.env` file:

code_assistant_manager/agents/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .droid import DroidAgentHandler
2121
from .gemini import GeminiAgentHandler
2222
from .opencode import OpenCodeAgentHandler
23+
from .qwen import QwenAgentHandler
2324
from .manager import VALID_APP_TYPES, AgentManager, AGENT_HANDLERS
2425
from .models import Agent, AgentRepo
2526

@@ -44,6 +45,7 @@ def get_handler(app_type: str) -> BaseAgentHandler:
4445
"CodebuddyAgentHandler",
4546
"CopilotAgentHandler",
4647
"OpenCodeAgentHandler",
48+
"QwenAgentHandler",
4749
"get_handler",
4850
"VALID_APP_TYPES",
4951
]

code_assistant_manager/agents/manager.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .droid import DroidAgentHandler
2020
from .gemini import GeminiAgentHandler
2121
from .opencode import OpenCodeAgentHandler
22+
from .qwen import QwenAgentHandler
2223
from .models import Agent, AgentRepo
2324
from ..fetcher import Fetcher
2425

@@ -93,6 +94,7 @@ def _load_agent_repos_from_config(config_dir: Optional[Path] = None) -> List[Dic
9394
"codebuddy": CodebuddyAgentHandler,
9495
"copilot": CopilotAgentHandler,
9596
"opencode": OpenCodeAgentHandler,
97+
"qwen": QwenAgentHandler,
9698
}
9799

98100
# Valid app types for agents
@@ -106,10 +108,23 @@ def __init__(self, config_dir: Optional[Path] = None):
106108
"""Initialize agent manager.
107109
108110
Args:
109-
config_dir: Configuration directory (defaults to ~/.config/code-assistant-manager)
111+
config_dir: Configuration directory (defaults to platform-appropriate location)
110112
"""
111113
if config_dir is None:
112-
config_dir = Path.home() / ".config" / "code-assistant-manager"
114+
# Default to platform-appropriate config directory
115+
import os
116+
if os.name == 'nt': # Windows
117+
# Try Windows locations first
118+
appdata = os.environ.get('APPDATA')
119+
if appdata:
120+
config_dir = Path(appdata) / "code-assistant-manager"
121+
else:
122+
# Fallback to home directory
123+
config_dir = Path.home() / ".config" / "code-assistant-manager"
124+
else:
125+
# Unix-like systems (Linux, macOS)
126+
config_dir = Path.home() / ".config" / "code-assistant-manager"
127+
113128
self.config_dir = Path(config_dir)
114129
self.agents_file = self.config_dir / "agents.json"
115130
self.repos_file = self.config_dir / "agent_repos.json"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Qwen agent handler."""
2+
3+
from pathlib import Path
4+
5+
from .base import BaseAgentHandler
6+
7+
8+
class QwenAgentHandler(BaseAgentHandler):
9+
"""Agent handler for Qwen CLI.
10+
11+
Qwen agents are markdown files stored in:
12+
- Global: ~/.qwen/agents/
13+
- Project: .qwen/agents/
14+
"""
15+
16+
@property
17+
def app_name(self) -> str:
18+
return "qwen"
19+
20+
@property
21+
def _default_agents_dir(self) -> Path:
22+
return Path.home() / ".qwen" / "agents"

code_assistant_manager/cli/doctor.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ def check_warning(message: str, suggestion: str = ""):
8888
Path.home() / ".config" / "code-assistant-manager" / ".env",
8989
]
9090

91+
# Add Windows-specific env file locations
92+
if os.name == 'nt': # Windows
93+
appdata = os.environ.get('APPDATA')
94+
local_appdata = os.environ.get('LOCALAPPDATA')
95+
if appdata:
96+
env_file_paths.append(Path(appdata) / "code-assistant-manager" / ".env")
97+
if local_appdata:
98+
env_file_paths.append(Path(local_appdata) / "code-assistant-manager" / ".env")
99+
91100
# If dotenv can find an env file, include it as well
92101
found_env = find_env_file()
93102
if found_env and found_env not in env_file_paths:
@@ -98,6 +107,15 @@ def check_warning(message: str, suggestion: str = ""):
98107
Path.cwd() / "providers.json",
99108
Path.home() / "providers.json",
100109
]
110+
111+
# Add Windows-specific config file locations
112+
if os.name == 'nt': # Windows
113+
appdata = os.environ.get('APPDATA')
114+
local_appdata = os.environ.get('LOCALAPPDATA')
115+
if appdata:
116+
config_file_paths.insert(1, Path(appdata) / "code-assistant-manager" / "providers.json")
117+
if local_appdata:
118+
config_file_paths.insert(2, Path(local_appdata) / "code-assistant-manager" / "providers.json")
101119
env_found = False
102120
config_found = False
103121

@@ -266,7 +284,18 @@ def check_warning(message: str, suggestion: str = ""):
266284
typer.echo()
267285
typer.echo(f"{Colors.BOLD}Cache Check{Colors.RESET}")
268286
try:
269-
cache_dir = Path.home() / ".cache" / "code-assistant-manager"
287+
# Use platform-appropriate cache directory
288+
if os.name == 'nt': # Windows
289+
# On Windows, use %LOCALAPPDATA% for cache
290+
local_appdata = os.environ.get('LOCALAPPDATA')
291+
if local_appdata:
292+
cache_dir = Path(local_appdata) / "code-assistant-manager" / "cache"
293+
else:
294+
cache_dir = Path.home() / ".cache" / "code-assistant-manager"
295+
else:
296+
# Unix-like systems (Linux, macOS)
297+
cache_dir = Path.home() / ".cache" / "code-assistant-manager"
298+
270299
if cache_dir.exists():
271300
check_passed(f"Cache directory exists: {cache_dir}")
272301
# Check cache size (rough estimate)

code_assistant_manager/cli/uninstall_commands.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,18 @@ def backup_configs(ctx: UninstallContext) -> Optional[Path]:
154154
return None
155155

156156
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
157-
backup_dir = (
158-
Path.home() / f".config/code-assistant-manager/backup/uninstall_{timestamp}"
159-
)
157+
# Use platform-appropriate config directory for backup
158+
import os
159+
if os.name == 'nt': # Windows
160+
appdata = os.environ.get('APPDATA')
161+
if appdata:
162+
config_dir = Path(appdata) / "code-assistant-manager"
163+
else:
164+
config_dir = Path.home() / ".config" / "code-assistant-manager"
165+
else:
166+
config_dir = Path.home() / ".config" / "code-assistant-manager"
167+
168+
backup_dir = config_dir / f"backup/uninstall_{timestamp}"
160169
backup_dir.mkdir(parents=True, exist_ok=True)
161170

162171
typer.echo(f"\n{Colors.BOLD}Backing up configuration files...{Colors.RESET}")

code_assistant_manager/config.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -282,29 +282,40 @@ def __init__(self, config_path: Optional[str] = None):
282282
logger.debug(f"Initializing ConfigManager with config_path: {config_path}")
283283
if config_path is None:
284284
# Lookup order for providers.json (installed location first):
285-
# 1) ~/.config/code-assistant-manager/providers.json
286-
# 2) ./providers.json (current working directory)
287-
# 3) $HOME/providers.json
285+
# 1. ~/.config/code-assistant-manager/providers.json (Linux/Unix/Windows)
286+
# 2. %APPDATA%/code-assistant-manager/providers.json (Windows roaming)
287+
# 3. %LOCALAPPDATA%/code-assistant-manager/providers.json (Windows local)
288+
# 4. ./providers.json (current working directory)
289+
# 5. ~/providers.json (home directory root)
288290
script_dir = Path(__file__).parent
289291
home_config = (
290292
Path.home() / ".config" / "code-assistant-manager" / "providers.json"
291293
)
294+
295+
# Windows-specific config locations (checked after ~/.config)
296+
import os
297+
windows_configs = []
298+
if os.name == 'nt': # Windows
299+
appdata = os.environ.get('APPDATA')
300+
local_appdata = os.environ.get('LOCALAPPDATA')
301+
if appdata:
302+
windows_configs.append(Path(appdata) / "code-assistant-manager" / "providers.json")
303+
if local_appdata:
304+
windows_configs.append(Path(local_appdata) / "code-assistant-manager" / "providers.json")
305+
292306
cwd_config = Path.cwd() / "providers.json"
293307
home_root_config = Path.home() / "providers.json"
294308

295-
logger.debug(
296-
f"Checking config locations: home={home_config}, cwd={cwd_config}, home_root={home_root_config}"
297-
)
309+
# Check all locations in order of preference - ~/.config first for all platforms
310+
config_locations = [home_config] + windows_configs + [cwd_config, home_root_config]
298311

299-
if home_config.exists():
300-
config_path = str(home_config)
301-
logger.debug(f"Using home config: {config_path}")
302-
elif cwd_config.exists():
303-
config_path = str(cwd_config)
304-
logger.debug(f"Using cwd config: {config_path}")
305-
elif home_root_config.exists():
306-
config_path = str(home_root_config)
307-
logger.debug(f"Using home root config: {config_path}")
312+
logger.debug(f"Checking config locations: {[str(p) for p in config_locations]}")
313+
314+
for config_path_obj in config_locations:
315+
if config_path_obj.exists():
316+
config_path = str(config_path_obj)
317+
logger.debug(f"Using config: {config_path}")
318+
break
308319
else:
309320
# Fallback to bundled providers.json in the package
310321
config_path = str(script_dir / "providers.json")
@@ -833,6 +844,16 @@ def get_config_path() -> Optional[Path]:
833844
Path.cwd() / ".code-assistant-manager" / "config.json",
834845
]
835846

847+
# Add Windows-specific locations (after ~/.config)
848+
import os
849+
if os.name == 'nt': # Windows
850+
appdata = os.environ.get('APPDATA')
851+
local_appdata = os.environ.get('LOCALAPPDATA')
852+
if appdata:
853+
locations.insert(1, Path(appdata) / "code-assistant-manager" / "config.json")
854+
if local_appdata:
855+
locations.insert(2, Path(local_appdata) / "code-assistant-manager" / "config.json")
856+
836857
for config_path in locations:
837858
if config_path.exists() and config_path.is_file():
838859
return config_path

code_assistant_manager/env_loader.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ def find_env_file(
9090
Path.home() / ".config" / "code-assistant-manager" / ".env",
9191
]
9292

93+
# Add Windows-specific locations
94+
import os
95+
if os.name == 'nt': # Windows
96+
appdata = os.environ.get('APPDATA')
97+
local_appdata = os.environ.get('LOCALAPPDATA')
98+
if appdata:
99+
locations.append(Path(appdata) / "code-assistant-manager" / ".env")
100+
if local_appdata:
101+
locations.append(Path(local_appdata) / "code-assistant-manager" / ".env")
102+
93103
for env_file in locations:
94104
if env_file.exists() and env_file.is_file():
95105
return env_file

code_assistant_manager/menu/base.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,15 @@ def _get_terminal_size() -> Tuple[int, int]:
6868
@staticmethod
6969
def _clear_screen():
7070
"""Clear the terminal screen."""
71-
subprocess.run(["clear"] if os.name == "posix" else ["cls"], check=False)
71+
try:
72+
if os.name == "posix":
73+
subprocess.run(["clear"], check=False)
74+
else:
75+
# Windows: use cls through shell since it's a builtin
76+
subprocess.run(["cmd", "/c", "cls"], check=False)
77+
except Exception:
78+
# Fallback: just print newlines
79+
print("\n" * 50)
7280

7381
def _calculate_menu_width(self) -> int:
7482
"""Calculate menu width based on content with smarter sizing for long titles."""

code_assistant_manager/plugins/manager.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,19 @@ def __init__(
159159
for testing purposes
160160
"""
161161
if config_dir is None:
162-
config_dir = Path.home() / ".config" / "code-assistant-manager"
162+
# Default to platform-appropriate config directory
163+
import os
164+
if os.name == 'nt': # Windows
165+
# Try Windows locations first
166+
appdata = os.environ.get('APPDATA')
167+
if appdata:
168+
config_dir = Path(appdata) / "code-assistant-manager"
169+
else:
170+
# Fallback to home directory
171+
config_dir = Path.home() / ".config" / "code-assistant-manager"
172+
else:
173+
# Unix-like systems (Linux, macOS)
174+
config_dir = Path.home() / ".config" / "code-assistant-manager"
163175
self.config_dir = Path(config_dir)
164176
self.plugins_file = self.config_dir / "plugins.json"
165177
self.marketplaces_file = self.config_dir / "marketplaces.json"

0 commit comments

Comments
 (0)