swixter 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +200 -358
  2. package/dist/cli/index.js +132 -87
  3. package/package.json +5 -6
package/README.md CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/swixter.svg)](https://www.npmjs.com/package/swixter)
4
4
  [![Test Status](https://github.com/dawnswwwww/swixter/actions/workflows/test.yml/badge.svg)](https://github.com/dawnswwwww/swixter/actions/workflows/test.yml)
5
- [![Release Status](https://github.com/dawnswwwww/swixter/actions/workflows/release.yml/badge.svg)](https://github.com/dawnswwwww/swixter/actions/workflows/test.yml)
5
+ [![Release Status](https://github.com/dawnswwwww/swixter/actions/workflows/release.yml/badge.svg)](https://github.com/dawnswwwww/swixter/actions/workflows/release.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![Node.js Version](https://img.shields.io/node/v/swixter.svg)](https://nodejs.org)
8
7
 
9
8
  > Make AI coding tools effortlessly switchable
10
9
 
@@ -12,58 +11,42 @@ A lightweight CLI tool that makes it easy to switch between AI providers for Cla
12
11
 
13
12
  ## Why Swixter?
14
13
 
15
- Working with AI coding tools shouldn't be complicated. Swixter lets you:
16
-
17
14
  - **Switch providers instantly** - Change between Anthropic, Ollama, or custom APIs with one command
18
- - **Manage multiple configs** - Keep separate profiles for work, personal, or experimental setups
19
- - **Support multiple coders** - Works with Claude Code, Codex, Continue, and more
20
- - **Use command aliases** - Ultra-short commands (`r`, `ls`, `sw`) for maximum productivity
21
- - **Add custom providers** - Easily integrate any OpenAI-compatible AI service
22
- - **Stay in control** - All configs stored locally, no cloud dependencies
23
- - **Web UI** - Browser-based interface for visual configuration management
15
+ - **Automatic failover** - Group profiles by priority, auto-retry on provider failure
16
+ - **Local proxy** - Transparent proxy with circuit breaker, your tools won't even notice a provider goes down
17
+ - **Multiple coders** - Works with Claude Code, Codex, Continue
18
+ - **Web UI** - Browser-based interface for visual management
19
+ - **All local** - No cloud dependencies, your keys stay on your machine
24
20
 
25
21
  ## Installation
26
22
 
27
- ### npm (Recommended)
28
-
29
23
  ```bash
24
+ # npm (Recommended)
30
25
  npm install -g swixter
31
- ```
32
-
33
- ### npx (No Install Needed)
34
26
 
35
- ```bash
27
+ # npx (No Install Needed)
36
28
  npx swixter --help
37
29
  ```
38
30
 
39
- ### Platform Support
40
-
41
31
  | Platform | Status | Notes |
42
32
  |----------|--------|-------|
43
- | **Linux** | Full | Full support |
44
- | **macOS** | Full | Full support |
45
- | **Windows 10/11** | Full | Requires Node.js 18+; Docker Desktop + WSL2 for E2E tests |
33
+ | **Linux** | Full | |
34
+ | **macOS** | Full | |
35
+ | **Windows 10/11** | Full | Requires Node.js 18+ |
46
36
 
47
37
  Config stored at:
48
38
  - **Linux/macOS**: `~/.config/swixter/`
49
- - **Windows**: `~/swixter/` (e.g., `C:\Users\YourName\swixter\`)
50
-
51
- See [docs/WINDOWS.md](docs/WINDOWS.md) for detailed Windows guide.
39
+ - **Windows**: `~/swixter/`
52
40
 
53
41
  ## Quick Start
54
42
 
55
43
  ```bash
56
- # Interactive mode - guided setup
57
- swixter
58
-
59
- # Or use commands directly
60
- swixter claude create # Create profile
61
- swixter claude list # List profiles
62
- swixter claude switch my-profile # Switch profile
63
- swixter claude apply # Apply to Claude Code
64
-
65
- # Launch Web UI (browser-based management)
66
- swixter ui
44
+ swixter # Interactive mode
45
+ swixter claude create # Create profile
46
+ swixter claude list # List profiles
47
+ swixter claude switch my-profile # Switch profile
48
+ swixter claude apply # Apply to Claude Code
49
+ swixter ui # Launch Web UI
67
50
  ```
68
51
 
69
52
  ## Built-in Providers
@@ -71,338 +54,257 @@ swixter ui
71
54
  | Provider | API Type | Description |
72
55
  |----------|----------|-------------|
73
56
  | **Anthropic** | responses | Official Claude API |
74
- | **Ollama** | chat | Run Qwen, Llama, and other models locally |
57
+ | **Ollama** | chat | Run models locally |
75
58
  | **OpenAI** | chat | OpenAI API |
76
59
  | **OpenRouter** | chat | Unified access to 100+ models |
77
- | **Custom** | chat | Add any OpenAI-compatible API |
78
-
79
- ## Web UI
60
+ | **Custom** | chat | Any OpenAI-compatible API |
80
61
 
81
- Launch `swixter ui` to open a browser-based interface for managing profiles.
62
+ ## Profile Management
82
63
 
83
- ### Features
64
+ Profiles are configuration templates containing provider, API key, base URL, and model settings. Each coder maintains its own active profile.
84
65
 
85
- - **Dashboard** - View all coders, switch active profiles, apply configurations
86
- - **Profiles** - Create, edit, delete configuration profiles
87
- - **Providers** - Manage custom API providers
88
- - **Settings** - Import/export configurations
89
-
90
- ### Running the Web UI
66
+ ### Commands (take Claude Code as example)
91
67
 
92
68
  ```bash
93
- # Default port (3141)
94
- swixter ui
95
-
96
- # Custom port
97
- swixter ui --port 8080
69
+ swixter claude create # Interactive creation (alias: new)
70
+ swixter claude create --quiet --name my --provider anthropic --api-key sk-ant-xxx # Non-interactive
71
+ swixter claude list # List profiles (alias: ls)
72
+ swixter claude switch <name> # Switch active profile (alias: sw)
73
+ swixter claude apply # Write config to ~/.claude/settings.json
74
+ swixter claude run # Run Claude Code with current profile (alias: r)
75
+ swixter claude edit <name> # Edit profile (alias: update)
76
+ swixter claude current # Show active profile
77
+ swixter claude delete <name> # Delete profile (alias: rm)
78
+ swixter claude install # Install Claude Code CLI
79
+ swixter claude update-cli # Update CLI (alias: upgrade)
98
80
  ```
99
81
 
100
- The UI auto-opens in your browser at `http://localhost:3141`.
82
+ Codex and Qwen/Continue have the same command structure: `swixter codex <command>` / `swixter qwen <command>`.
101
83
 
102
- ### Web UI Dashboard
84
+ ### Model Configuration
103
85
 
104
- The Dashboard shows:
105
- - All installed coders (Claude Code, Codex, Continue)
106
- - Current active profile per coder
107
- - Quick profile switching via dropdown
108
- - One-click Apply to write config to coder's settings file
86
+ **Claude Code** - set per-profile models:
109
87
 
110
- ## Commands
88
+ ```bash
89
+ swixter claude create \
90
+ --name production \
91
+ --provider anthropic \
92
+ --api-key sk-ant-xxx \
93
+ --anthropic-model claude-sonnet-4-20250514 \
94
+ --default-haiku-model claude-haiku-4-20250506 \
95
+ --default-opus-model claude-opus-4-20250514
96
+ ```
111
97
 
112
- ### For Claude Code
98
+ | Flag | Env Variable | Description |
99
+ |------|-------------|-------------|
100
+ | `--anthropic-model` | `ANTHROPIC_MODEL` | Default model |
101
+ | `--default-haiku-model` | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Haiku model |
102
+ | `--default-opus-model` | `ANTHROPIC_DEFAULT_OPUS_MODEL` | Opus model |
103
+ | `--default-sonnet-model` | `ANTHROPIC_DEFAULT_SONNET_MODEL` | Sonnet model |
104
+
105
+ **Codex/Qwen** - single model field:
113
106
 
114
107
  ```bash
115
- swixter claude create # Create new profile (alias: new)
116
- swixter claude list # List all profiles (alias: ls)
117
- swixter claude switch <name> # Switch active profile (alias: sw)
118
- swixter claude apply # Apply to Claude Code
119
- swixter claude run # Run Claude Code with current profile (alias: r)
120
- swixter claude edit <name> # Edit existing profile (alias: update)
121
- swixter claude current # Show current active profile
122
- swixter claude delete <name> # Delete profile (alias: rm)
123
- swixter claude install # Install Claude Code CLI
124
- swixter claude update-cli # Update Claude Code CLI (alias: upgrade)
108
+ swixter codex create --name local --provider ollama --model qwen3:32b
125
109
  ```
126
110
 
127
- ### For Codex
111
+ ### Custom Providers
128
112
 
129
113
  ```bash
130
- swixter codex create # Create new profile
131
- swixter codex list # List all profiles (alias: ls)
132
- swixter codex switch <name> # Switch active profile (alias: sw)
133
- swixter codex apply # Apply to Codex (writes ~/.codex/config.toml)
134
- swixter codex run # Apply + set env + run codex (alias: r)
135
- swixter codex edit <name> # Edit existing profile (alias: update)
136
- swixter codex current # Show current active profile
137
- swixter codex delete <name> # Delete profile (alias: rm)
138
- swixter codex install # Install Codex CLI
139
- swixter codex update-cli # Update Codex CLI (alias: upgrade)
114
+ swixter providers add
115
+ swixter providers list
116
+ swixter providers remove <id>
117
+ swixter providers show <id>
140
118
  ```
141
119
 
142
- **Two ways to use Codex profiles**:
143
-
144
- 1. **Quick way** (recommended): `swixter codex r`
145
- - Automatically applies profile to config.toml
146
- - Sets environment variables
147
- - Runs codex in one command
120
+ ## Groups (Failover)
148
121
 
149
- 2. **Manual way**: `swixter codex apply` set env vars `codex`
150
- - Good for debugging or custom setups
122
+ Groups are ordered lists of profiles used for **automatic failover**. When one provider fails, the next profile in the group is tried automatically.
151
123
 
152
- ### For Qwen/Continue
124
+ ### How It Works
153
125
 
154
- ```bash
155
- swixter qwen create # Create new profile
156
- swixter qwen list # List all profiles (alias: ls)
157
- swixter qwen switch <name> # Switch active profile (alias: sw)
158
- swixter qwen apply # Apply to Continue
159
- swixter qwen run # Run Qwen Code with current profile (alias: r)
160
- swixter qwen edit <name> # Edit existing profile (alias: update)
161
- swixter qwen current # Show current active profile
162
- swixter qwen delete <name> # Delete profile (alias: rm)
163
- swixter qwen install # Install Qwen Code CLI
164
- swixter qwen update-cli # Update Qwen Code CLI (alias: upgrade)
126
+ ```
127
+ Request Proxy Profile 1 (Anthropic) → Fail! → Profile 2 (OpenRouter) → Success → Response
165
128
  ```
166
129
 
167
- ### Provider Management
130
+ Each group defines a priority order. The proxy tries profiles from highest to lowest priority until one succeeds.
131
+
132
+ ### Commands
168
133
 
169
134
  ```bash
170
- swixter providers list # List available providers
171
- swixter providers add # Add custom provider
172
- swixter providers remove <id> # Remove provider
173
- swixter providers show <id> # Show provider details
135
+ swixter group create # Interactive creation (alias: new)
136
+ swixter group list # List groups (alias: ls)
137
+ swixter group show <name> # Show group details with profile order (alias: info)
138
+ swixter group edit <name> # Edit group name and profiles (alias: update)
139
+ swixter group set-default <name> # Set as default group
140
+ swixter group delete <name> # Delete group (alias: rm)
174
141
  ```
175
142
 
176
- ### Configuration
143
+ ### Example
177
144
 
178
145
  ```bash
179
- swixter ui # Launch Web UI
180
- swixter ui --port <port> # Launch Web UI on custom port
181
- swixter export <file> # Export configs
182
- swixter export <file> --sanitize # Export without API keys
183
- swixter import <file> # Import configs
184
- swixter import <file> --overwrite # Overwrite existing profiles
185
- swixter completion bash # Bash completion
186
- swixter completion zsh # Zsh completion
187
- swixter completion fish # Fish completion
188
- ```
146
+ # Create profiles for different providers
147
+ swixter claude create --name anthropic-primary --provider anthropic --api-key sk-ant-xxx
148
+ swixter claude create --name openrouter-backup --provider openrouter --api-key sk-or-xxx
149
+ swixter claude create --name ollama-local --provider ollama
189
150
 
190
- ## Model Configuration
151
+ # Create a group with failover priority
152
+ swixter group create --name ha-group --profiles anthropic-primary,openrouter-backup,ollama-local
191
153
 
192
- ### Claude Code Models
154
+ # Set as default
155
+ swixter group set-default ha-group
156
+ ```
157
+
158
+ ### Web UI
193
159
 
194
- Set specific models per profile:
160
+ Manage groups visually with drag-and-drop profile reordering:
195
161
 
196
162
  ```bash
197
- swixter claude create \
198
- --name production \
199
- --provider anthropic \
200
- --api-key sk-ant-xxx \
201
- --anthropic-model claude-sonnet-4-20250514 \
202
- --default-haiku-model claude-haiku-4-20250506 \
203
- --default-opus-model claude-opus-4-20250514 \
204
- --default-sonnet-model claude-sonnet-4-20250514
163
+ swixter ui # Open Groups page to create/reorder groups
205
164
  ```
206
165
 
207
- | Flag | Environment Variable | Description |
208
- |------|---------------------|-------------|
209
- | `--anthropic-model` | `ANTHROPIC_MODEL` | Default model |
210
- | `--default-haiku-model` | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Haiku model |
211
- | `--default-opus-model` | `ANTHROPIC_DEFAULT_OPUS_MODEL` | Opus model |
212
- | `--default-sonnet-model` | `ANTHROPIC_DEFAULT_SONNET_MODEL` | Sonnet model |
166
+ ## Proxy
213
167
 
214
- ### Codex/Qwen Models
168
+ The local proxy server sits between your AI coding tools and upstream providers, enabling automatic failover, circuit breaking, and unified access.
215
169
 
216
- ```bash
217
- swixter codex create \
218
- --name local-ollama \
219
- --provider ollama \
220
- --base-url http://localhost:11434 \
221
- --model qwen3:32b
222
- ```
170
+ ### How It Works
223
171
 
224
- ## Configuration File
225
-
226
- Configs are stored at:
227
- - **Linux/macOS**: `~/.config/swixter/config.json`
228
- - **Windows**: `~/swixter/config.json`
229
-
230
- ```json
231
- {
232
- "profiles": {
233
- "my-profile": {
234
- "name": "my-profile",
235
- "providerId": "anthropic",
236
- "apiKey": "sk-ant-xxx",
237
- "authToken": "sk-ant-auth-xxx",
238
- "baseURL": "https://api.anthropic.com",
239
- "models": {
240
- "anthropicModel": "claude-sonnet-4-20250514",
241
- "defaultHaikuModel": "claude-haiku-4-20250506",
242
- "defaultOpusModel": "claude-opus-4-20250514",
243
- "defaultSonnetModel": "claude-sonnet-4-20250514"
244
- },
245
- "envKey": "CUSTOM_API_KEY",
246
- "headers": {
247
- "X-Custom-Header": "value"
248
- },
249
- "createdAt": "2024-01-01T00:00:00.000Z",
250
- "updatedAt": "2024-01-01T00:00:00.000Z"
251
- },
252
- "ollama-local": {
253
- "name": "ollama-local",
254
- "providerId": "ollama",
255
- "apiKey": "",
256
- "baseURL": "http://localhost:11434",
257
- "model": "qwen3:32b"
258
- }
259
- },
260
- "coders": {
261
- "claude": {
262
- "activeProfile": "my-profile"
263
- },
264
- "codex": {
265
- "activeProfile": "ollama-local"
266
- },
267
- "qwen": {
268
- "activeProfile": ""
269
- }
270
- }
271
- }
172
+ ```
173
+ ┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
174
+ Claude Code │────▶│ Swixter Proxy │────▶│ Anthropic │
175
+ Codex │ │ (localhost) │─ ─ ▶│ OpenRouter │
176
+ Continue │ │ Circuit Breaker │─ ─ ▶│ Ollama │
177
+ └─────────────┘ └──────────────────┘ └──────────────┘
272
178
  ```
273
179
 
274
- ## Add Custom Providers
180
+ Key capabilities:
275
181
 
276
- ```bash
277
- # Interactive setup
278
- swixter providers add
182
+ - **Failover** - Tries profiles in group priority order, returns first successful response
183
+ - **Circuit Breaker** - Skips providers that failed 3 consecutive times, auto-recovers after 60s
184
+ - **Streaming** - Transparently forwards SSE/NDJSON streaming responses
185
+ - **Multi-format** - Supports both OpenAI Chat (`/v1/chat/completions`) and Anthropic (`/v1/messages`, `/v1/responses`) APIs
186
+ - **Model Rewriting** - Different providers in a group can use different model names; the proxy rewrites them automatically
279
187
 
280
- # With flags
281
- swixter providers add \
282
- --id openrouter \
283
- --name "OpenRouter" \
284
- --display-name "OpenRouter" \
285
- --base-url "https://openrouter.ai/api/v1" \
286
- --auth-type bearer
188
+ ### Commands
287
189
 
288
- # Then create a profile using it
289
- swixter claude create --provider openrouter --api-key sk-or-xxx
190
+ ```bash
191
+ # Start proxy as a background service
192
+ swixter proxy start # Start with default group
193
+ swixter proxy start --group ha-group # Start with specific group
194
+ swixter proxy start --port 8080 # Custom port (default: 15721)
195
+ swixter proxy start --daemon # Run in background
196
+
197
+ # Stop proxy
198
+ swixter proxy stop # Stop default instance
199
+ swixter proxy stop run-15722 # Stop specific instance
200
+
201
+ # View status
202
+ swixter proxy status # Show all running instances
203
+
204
+ # One-shot: start proxy + run coder tool
205
+ swixter proxy run -- claude # Proxy + Claude Code
206
+ swixter proxy run -- codex # Proxy + Codex
207
+ swixter proxy run --group ha-group -- claude # With specific group
290
208
  ```
291
209
 
292
- ## Examples
210
+ ### The `proxy run` Command
211
+
212
+ The easiest way to use the proxy. It:
293
213
 
294
- ### Example 1: Switch between work and personal
214
+ 1. Starts a temporary proxy instance (on next available port)
215
+ 2. Points the coder tool's API URL to the local proxy
216
+ 3. Spawns the coder tool
217
+ 4. Stops the proxy when the coder exits
295
218
 
296
219
  ```bash
297
- # Setup work profile
298
- swixter claude create --name work --provider anthropic --api-key sk-ant-work-xxx
220
+ swixter proxy run -- claude # One command, everything automatic
221
+ ```
299
222
 
300
- # Setup personal profile
301
- swixter claude create --name personal --provider anthropic --api-key sk-ant-personal-xxx
223
+ ### Circuit Breaker
302
224
 
303
- # Switch to work
304
- swixter claude sw work && swixter claude apply
225
+ Per-profile circuit breaker prevents wasting time on failing providers:
305
226
 
306
- # Quick run
307
- swixter claude r
227
+ | State | Behavior | Transition |
228
+ |-------|----------|------------|
229
+ | **Closed** | Requests pass through | 3 consecutive failures → Open |
230
+ | **Open** | Requests skipped | After 60s → Half-Open |
231
+ | **Half-Open** | One probe request allowed | Success → Closed, Failure → Open |
308
232
 
309
- # Switch to personal
310
- swixter claude sw personal && swixter claude apply
311
- swixter claude r
312
- ```
313
-
314
- ### Example 2: Try Qwen locally
233
+ ### Monitoring (Web UI)
315
234
 
316
235
  ```bash
317
- # Add Ollama profile
318
- swixter qwen create \
319
- --name local \
320
- --provider ollama \
321
- --base-url http://localhost:11434
322
-
323
- # Switch and run
324
- swixter qwen sw local
325
- swixter qwen r
236
+ swixter ui # Proxy page shows live status, request counts, and real-time logs
326
237
  ```
327
238
 
328
- ### Example 3: Use Codex with local Ollama
239
+ ## Web UI
329
240
 
330
241
  ```bash
331
- # Create Codex profile for Ollama
332
- swixter codex create \
333
- --name ollama-local \
334
- --provider ollama \
335
- --base-url http://localhost:11434
242
+ swixter ui # Default port 3141
243
+ swixter ui --port 8080 # Custom port
244
+ ```
336
245
 
337
- # Quick way: All-in-one
338
- swixter codex r
246
+ ### Pages
339
247
 
340
- # Manual way
341
- swixter codex apply
342
- export OLLAMA_API_KEY=""
343
- codex
344
- ```
248
+ - **Dashboard** - All coders at a glance, quick switch and apply
249
+ - **Profiles** - CRUD for configuration profiles
250
+ - **Groups** - Manage failover groups with drag-and-drop priority
251
+ - **Proxy** - Start/stop proxy, live logs, status monitoring
252
+ - **Providers** - Manage custom providers
253
+ - **Settings** - Import/export configurations
345
254
 
346
- ### Example 4: Web UI workflow
255
+ ## Other Commands
347
256
 
348
257
  ```bash
349
- # Launch Web UI
350
- swixter ui
351
-
352
- # In browser:
353
- # 1. Go to Profiles Create a new profile
354
- # 2. Go to Dashboard → Select profile from dropdown
355
- # 3. Click APPLY to write config
258
+ swixter export <file> # Export configs
259
+ swixter export <file> --sanitize # Export without API keys
260
+ swixter import <file> # Import configs
261
+ swixter import <file> --overwrite # Overwrite existing
262
+ swixter completion bash # Shell completions (zsh/fish supported)
356
263
  ```
357
264
 
358
- ## Shell Completion
265
+ ## Command Aliases
359
266
 
360
- Enable auto-completion for faster typing:
267
+ | Alias | Full Command | Description |
268
+ |-------|-------------|-------------|
269
+ | `r` | `run` | Execute the AI coder |
270
+ | `ls` | `list` | List profiles/groups |
271
+ | `sw` | `switch` | Switch profiles |
272
+ | `rm` | `delete` | Delete profiles/groups |
273
+ | `new` | `create` | Create new |
274
+ | `update` | `edit` | Edit existing |
275
+ | `upgrade` | `update-cli` | Update CLI tool |
361
276
 
362
- ### Bash
277
+ ## Examples
363
278
 
364
- ```bash
365
- # Install
366
- swixter completion bash > ~/.local/share/bash-completion/completions/swixter
279
+ ### Switch between work and personal
367
280
 
368
- # Reload
369
- source ~/.bashrc
281
+ ```bash
282
+ swixter claude create --name work --provider anthropic --api-key sk-ant-work-xxx
283
+ swixter claude create --name personal --provider anthropic --api-key sk-ant-personal-xxx
284
+ swixter claude sw work && swixter claude apply
370
285
  ```
371
286
 
372
- ### Zsh
287
+ ### Failover setup: Anthropic primary + OpenRouter backup
373
288
 
374
289
  ```bash
375
- # Install
376
- swixter completion zsh > ~/.zfunc/_swixter
290
+ # Create profiles
291
+ swixter claude create --name primary --provider anthropic --api-key sk-ant-xxx
292
+ swixter claude create --name backup --provider openrouter --api-key sk-or-xxx
377
293
 
378
- # Reload
379
- autoload -U compinit && compinit
294
+ # Create failover group
295
+ swixter group create --name failover --profiles primary,backup
296
+
297
+ # Run with proxy (auto-failover)
298
+ swixter proxy run --group failover -- claude
380
299
  ```
381
300
 
382
- ### Fish
301
+ ### Run Codex with local Ollama
383
302
 
384
303
  ```bash
385
- # Install
386
- swixter completion fish > ~/.config/fish/completions/swixter.fish
387
-
388
- # Reload
389
- fish
304
+ swixter codex create --name local --provider ollama --base-url http://localhost:11434
305
+ swixter codex r
390
306
  ```
391
307
 
392
- ## Command Aliases
393
-
394
- Save keystrokes with short aliases:
395
-
396
- | Alias | Full Command | Description |
397
- |-------|-------------|-------------|
398
- | `r` | `run` | Execute the AI coder |
399
- | `ls` | `list` | List all profiles |
400
- | `sw` | `switch` | Switch profiles |
401
- | `rm` | `delete` | Delete profiles |
402
- | `new` | `create` | Create new profile |
403
- | `update` | `edit` | Edit existing profile |
404
- | `upgrade` | `update-cli` | Update CLI tool |
405
-
406
308
  ## Architecture
407
309
 
408
310
  ```
@@ -410,45 +312,34 @@ swixter/
410
312
  ├── src/
411
313
  │ ├── cli/ # CLI command handlers
412
314
  │ ├── config/ # Config file management
413
- │ ├── adapters/ # Coder-specific adapters (Claude, Codex, Continue)
414
- │ ├── providers/ # Provider presets
315
+ │ ├── adapters/ # Coder adapters (Claude, Codex, Continue)
316
+ │ ├── providers/ # Provider presets + user-defined providers
317
+ │ ├── groups/ # Group management (failover profiles)
318
+ │ ├── proxy/ # Local proxy server (failover, circuit breaker)
415
319
  │ ├── server/ # Web UI API server
416
320
  │ └── utils/ # Shared utilities
417
- ├── ui/ # Web UI (React + Vite)
321
+ ├── ui/ # Web UI (React + Vite + Tailwind)
418
322
  └── tests/ # Unit tests
419
323
  ```
420
324
 
421
- **Data Flow**:
422
- 1. `swixter create/switch` → Updates `~/.config/swixter/config.json`
423
- 2. `swixter apply` → Writes to coder config (`~/.claude/settings.json`, etc.)
424
- 3. `swixter run` → Sets env vars + spawns coder CLI
425
-
426
325
  ## Development
427
326
 
428
- Built with modern tools:
429
-
430
327
  ```bash
431
- # Clone repo
432
328
  git clone https://github.com/dawnswwwww/swixter.git
433
329
  cd swixter
434
-
435
- # Install dependencies
436
330
  bun install
331
+ bun run cli:dev # Dev mode with hot reload
332
+ bun test # Run tests
333
+ bun run build # Build all (UI + CLI)
334
+ ```
437
335
 
438
- # Run CLI in dev mode (with hot reload)
439
- bun run cli:dev
440
-
441
- # Run tests
442
- bun test
443
-
444
- # Run specific test
445
- bun test tests/adapters/claude.test.ts
446
-
447
- # E2E tests (requires Docker)
448
- bun run test:e2e
336
+ ### Release
449
337
 
450
- # Build
451
- bun run build
338
+ ```bash
339
+ # Update CHANGELOG.md first, then:
340
+ bun run release:patch # Bug fixes (0.1.0 → 0.1.1)
341
+ bun run release:minor # Features (0.1.0 → 0.2.0)
342
+ bun run release:major # Breaking changes (0.1.0 → 1.0.0)
452
343
  ```
453
344
 
454
345
  ## Tech Stack
@@ -459,7 +350,6 @@ bun run build
459
350
  | **Language** | TypeScript |
460
351
  | **CLI UI** | @clack/prompts |
461
352
  | **Validation** | Zod |
462
- | **Version** | semver |
463
353
  | **Web UI** | React + Vite + Tailwind CSS |
464
354
  | **Testing** | Bun test, Docker E2E |
465
355
 
@@ -467,54 +357,6 @@ bun run build
467
357
 
468
358
  See [CHANGELOG.md](CHANGELOG.md) for version history.
469
359
 
470
- ## Roadmap
471
-
472
- - [ ] Profile templates for common setups
473
- - [ ] Configuration validation and migration tools
474
- - [x] Web UI for profile management (v0.0.9+)
475
- - [ ] Cloud sync for profiles (optional, planned)
476
-
477
- ## Contributing
478
-
479
- Contributions welcome!
480
-
481
- ### Development Setup
482
-
483
- ```bash
484
- git clone https://github.com/dawnswwwww/swixter.git
485
- cd swixter
486
- bun install
487
- bun run cli:dev
488
- bun test
489
- bun run build
490
- ```
491
-
492
- ### Release Process
493
-
494
- 1. Update `CHANGELOG.md` under `[Unreleased]`
495
- 2. Run release command:
496
- ```bash
497
- bun run release:patch # Bug fixes (0.0.10 → 0.0.11)
498
- bun run release:minor # Features (0.0.10 → 0.1.0)
499
- bun run release:major # Breaking changes
500
- ```
501
- 3. GitHub Actions automatically:
502
- - Runs tests on Linux/macOS/Windows
503
- - Publishes to npm
504
- - Creates GitHub Release with changelog
505
-
506
- See [CLAUDE.md](CLAUDE.md) for detailed development documentation.
507
-
508
360
  ## License
509
361
 
510
362
  MIT License - see [LICENSE](LICENSE)
511
-
512
- ## Links
513
-
514
- - [GitHub](https://github.com/dawnswwwww/swixter)
515
- - [npm](https://www.npmjs.com/package/swixter)
516
- - [Issues](https://github.com/dawnswwwww/swixter/issues)
517
-
518
- ---
519
-
520
- **Made with ❤️ to make AI coding tools more accessible**
package/dist/cli/index.js CHANGED
@@ -13950,7 +13950,7 @@ var CONFIG_VERSION = "2.0.0", EXPORT_VERSION = "1.0.0";
13950
13950
  var init_versions2 = () => {};
13951
13951
 
13952
13952
  // src/constants/meta.ts
13953
- var APP_VERSION = "0.1.0";
13953
+ var APP_VERSION = "0.1.1";
13954
13954
  var init_meta = () => {};
13955
13955
 
13956
13956
  // src/constants/install.ts
@@ -19048,8 +19048,8 @@ __export(exports_manager, {
19048
19048
  deleteProfile: () => deleteProfile
19049
19049
  });
19050
19050
  import { existsSync as existsSync5 } from "fs";
19051
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
19052
- import { dirname as dirname5 } from "path";
19051
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, rename } from "fs/promises";
19052
+ import { dirname as dirname5, join as join4 } from "path";
19053
19053
  function getConfigPath2() {
19054
19054
  return process.env.SWIXTER_CONFIG_PATH || getConfigPath("swixter");
19055
19055
  }
@@ -19102,7 +19102,9 @@ async function saveConfig(config2) {
19102
19102
  try {
19103
19103
  ConfigFileSchema.parse(config2);
19104
19104
  const content = JSON.stringify(config2, null, SERIALIZATION.jsonIndent);
19105
- await writeFile5(configPath, content, "utf-8");
19105
+ const tmpPath = join4(dirname5(configPath), `.config.tmp-${Date.now()}`);
19106
+ await writeFile5(tmpPath, content, "utf-8");
19107
+ await rename(tmpPath, configPath);
19106
19108
  } catch (error46) {
19107
19109
  throw new Error(`Failed to save configuration: ${error46}`);
19108
19110
  }
@@ -19156,6 +19158,10 @@ async function deleteProfile(profileName) {
19156
19158
  if (!config2.profiles[profileName]) {
19157
19159
  throw new Error(`Profile "${profileName}" does not exist`);
19158
19160
  }
19161
+ const referencingGroups = Object.values(config2.groups || {}).filter((group) => group.profiles.includes(profileName)).map((group) => group.name);
19162
+ if (referencingGroups.length > 0) {
19163
+ throw new Error(`Profile "${profileName}" is used in group(s): ${referencingGroups.join(", ")}. Remove it from the group(s) first.`);
19164
+ }
19159
19165
  const allCoders = Object.keys(CODER_REGISTRY);
19160
19166
  for (const coder of allCoders) {
19161
19167
  try {
@@ -19168,8 +19174,7 @@ async function deleteProfile(profileName) {
19168
19174
  delete config2.profiles[profileName];
19169
19175
  for (const coder in config2.coders) {
19170
19176
  if (config2.coders[coder].activeProfile === profileName) {
19171
- const remainingProfiles = Object.keys(config2.profiles);
19172
- config2.coders[coder].activeProfile = remainingProfiles.length > 0 ? remainingProfiles[0] : "";
19177
+ config2.coders[coder].activeProfile = "";
19173
19178
  }
19174
19179
  }
19175
19180
  await saveConfig(config2);
@@ -19424,13 +19429,15 @@ var init_commands2 = __esm(() => {
19424
19429
 
19425
19430
  // src/utils/process.ts
19426
19431
  import { spawn } from "child_process";
19432
+ import os from "os";
19427
19433
  function spawnCLI(options) {
19428
19434
  const { command, args, env, displayName, onExit } = options;
19429
19435
  const finalEnv = env ? { ...process.env, ...env } : process.env;
19436
+ const isWin32 = os.platform() === "win32";
19430
19437
  const child = spawn(command, args, {
19431
19438
  env: finalEnv,
19432
19439
  stdio: "inherit",
19433
- shell: true
19440
+ shell: isWin32
19434
19441
  });
19435
19442
  child.on("exit", async (code) => {
19436
19443
  await onExit?.();
@@ -21949,7 +21956,7 @@ async function cmdCreateQuiet(params) {
21949
21956
  console.log(import_picocolors8.default.dim("Usage: swixter claude create --quiet --name <name> --provider <id> [--api-key <key>] [--auth-token <token>] [--base-url <url>] [--anthropic-model <model>] [--default-haiku-model <model>] [--default-opus-model <model>] [--default-sonnet-model <model>] [--apply]"));
21950
21957
  process.exit(1);
21951
21958
  }
21952
- const preset = getPresetById(params.provider);
21959
+ const preset = await getPresetByIdAsync(params.provider);
21953
21960
  if (!preset) {
21954
21961
  console.log(import_picocolors8.default.red(`Error: Unknown provider ID: ${params.provider}`));
21955
21962
  console.log(import_picocolors8.default.dim("Run 'swixter providers' to see all supported providers"));
@@ -22020,7 +22027,7 @@ async function cmdList() {
22020
22027
  console.log(import_picocolors8.default.bold(LABELS.profileList));
22021
22028
  console.log();
22022
22029
  for (const profile of profiles) {
22023
- const preset = getPresetById(profile.providerId);
22030
+ const preset = await getPresetByIdAsync(profile.providerId);
22024
22031
  const isCurrent = current?.name === profile.name;
22025
22032
  const marker = isCurrent ? import_picocolors8.default.green(MARKERS.active) : import_picocolors8.default.dim(MARKERS.inactive);
22026
22033
  const baseUrl = profile.baseURL || preset?.baseURL || MISC_DEFAULTS.baseUrlFallback;
@@ -22042,7 +22049,11 @@ async function cmdSwitch(profileName, args = []) {
22042
22049
  try {
22043
22050
  await setActiveProfileForCoder(CODER_NAME, profileName);
22044
22051
  const profile = await getActiveProfileForCoder(CODER_NAME);
22045
- const preset = getPresetById(profile.providerId);
22052
+ if (!profile) {
22053
+ console.log(import_picocolors8.default.red("Error: Profile not found after switch"));
22054
+ process.exit(1);
22055
+ }
22056
+ const preset = await getPresetByIdAsync(profile.providerId);
22046
22057
  const baseUrl = profile.baseURL || preset?.baseURL || "Default";
22047
22058
  console.log();
22048
22059
  console.log(import_picocolors8.default.green("\u2713") + " Switched successfully!");
@@ -22093,10 +22104,13 @@ async function cmdEdit(profileName) {
22093
22104
  }
22094
22105
  const selected = await ve({
22095
22106
  message: "Select profile to edit",
22096
- options: profiles2.map((profile2) => ({
22097
- value: profile2.name,
22098
- label: profile2.name,
22099
- hint: getPresetById(profile2.providerId)?.displayName || ""
22107
+ options: await Promise.all(profiles2.map(async (profile2) => {
22108
+ const preset = await getPresetByIdAsync(profile2.providerId);
22109
+ return {
22110
+ value: profile2.name,
22111
+ label: profile2.name,
22112
+ hint: preset?.displayName || ""
22113
+ };
22100
22114
  }))
22101
22115
  });
22102
22116
  if (pD(selected)) {
@@ -22117,7 +22131,7 @@ async function cmdEdit(profileName) {
22117
22131
  console.log();
22118
22132
  const { allPresets: allPresets2 } = await Promise.resolve().then(() => (init_presets(), exports_presets));
22119
22133
  const presets = allPresets2;
22120
- const currentPreset = getPresetById(profile.providerId);
22134
+ const currentPreset = await getPresetByIdAsync(profile.providerId);
22121
22135
  const shouldChangeProvider = await ye({
22122
22136
  message: `Change provider? Current: ${currentPreset?.displayName}`,
22123
22137
  initialValue: false
@@ -22288,7 +22302,7 @@ async function cmdApply() {
22288
22302
  }
22289
22303
  try {
22290
22304
  const adapter = getAdapter(CODER_NAME);
22291
- const preset = getPresetById(profile.providerId);
22305
+ const preset = await getPresetByIdAsync(profile.providerId);
22292
22306
  console.log();
22293
22307
  console.log(import_picocolors8.default.dim(`Applying profile to ${adapter.configPath}...`));
22294
22308
  await applyClaudeProfile(profile);
@@ -22318,7 +22332,7 @@ async function cmdCurrent() {
22318
22332
  console.log(import_picocolors8.default.yellow("No active profile"));
22319
22333
  return;
22320
22334
  }
22321
- const preset = getPresetById(profile.providerId);
22335
+ const preset = await getPresetByIdAsync(profile.providerId);
22322
22336
  const baseUrl = profile.baseURL || preset?.baseURL || "Default";
22323
22337
  console.log();
22324
22338
  console.log(import_picocolors8.default.bold("Current active profile:"));
@@ -22402,10 +22416,13 @@ async function cmdSwitchInteractive() {
22402
22416
  const current = await getActiveProfileForCoder(CODER_NAME);
22403
22417
  const profileName = await ve({
22404
22418
  message: "Select profile to switch to",
22405
- options: profiles.map((profile) => ({
22406
- value: profile.name,
22407
- label: profile.name,
22408
- hint: profile.name === current?.name ? "(current)" : getPresetById(profile.providerId)?.displayName || ""
22419
+ options: await Promise.all(profiles.map(async (profile) => {
22420
+ const preset = await getPresetByIdAsync(profile.providerId);
22421
+ return {
22422
+ value: profile.name,
22423
+ label: profile.name,
22424
+ hint: profile.name === current?.name ? "(current)" : preset?.displayName || ""
22425
+ };
22409
22426
  }))
22410
22427
  });
22411
22428
  if (pD(profileName)) {
@@ -22422,10 +22439,13 @@ async function cmdDeleteInteractive() {
22422
22439
  }
22423
22440
  const profileName = await ve({
22424
22441
  message: "Select profile to delete",
22425
- options: profiles.map((profile) => ({
22426
- value: profile.name,
22427
- label: profile.name,
22428
- hint: getPresetById(profile.providerId)?.displayName || ""
22442
+ options: await Promise.all(profiles.map(async (profile) => {
22443
+ const preset = await getPresetByIdAsync(profile.providerId);
22444
+ return {
22445
+ value: profile.name,
22446
+ label: profile.name,
22447
+ hint: preset?.displayName || ""
22448
+ };
22429
22449
  }))
22430
22450
  });
22431
22451
  if (pD(profileName)) {
@@ -22500,7 +22520,7 @@ async function cmdRun(args) {
22500
22520
  process.exit(1);
22501
22521
  }
22502
22522
  }
22503
- const preset = getPresetById(profile.providerId);
22523
+ const preset = await getPresetByIdAsync(profile.providerId);
22504
22524
  const baseURL = profile.baseURL || preset?.baseURL || "";
22505
22525
  const env = {};
22506
22526
  for (const [key, value] of Object.entries(process.env)) {
@@ -22603,7 +22623,7 @@ class ProxyRouter {
22603
22623
 
22604
22624
  // src/proxy/logger.ts
22605
22625
  import { appendFileSync, existsSync as existsSync6, mkdirSync, renameSync, rmSync, statSync } from "fs";
22606
- import { dirname as dirname6, join as join5 } from "path";
22626
+ import { dirname as dirname6, join as join6 } from "path";
22607
22627
  function formatMeta(meta2) {
22608
22628
  return meta2 ? { ...meta2 } : {};
22609
22629
  }
@@ -22628,7 +22648,7 @@ function writeProxyLog(logPath, record2) {
22628
22648
  } catch {}
22629
22649
  }
22630
22650
  function getProxyLogPath(instanceId) {
22631
- return join5(dirname6(getConfigPath2()), `proxy-${instanceId}.log`);
22651
+ return join6(dirname6(getConfigPath2()), `proxy-${instanceId}.log`);
22632
22652
  }
22633
22653
  function createProxyLogger(instanceId) {
22634
22654
  const logPath = getProxyLogPath(instanceId);
@@ -22748,9 +22768,6 @@ class CircuitBreaker {
22748
22768
  }
22749
22769
  isAvailable(profileId) {
22750
22770
  const state = this.getState(profileId);
22751
- if (state.state === "half_open") {
22752
- return true;
22753
- }
22754
22771
  return !state.isOpen;
22755
22772
  }
22756
22773
  recordSuccess(profileId) {
@@ -22797,6 +22814,7 @@ class CircuitBreaker {
22797
22814
  if (!state || state.state !== "open")
22798
22815
  return;
22799
22816
  state.state = "half_open";
22817
+ state.isOpen = false;
22800
22818
  }
22801
22819
  forceHalfOpen(profileId) {
22802
22820
  const state = this.getOrCreateState(profileId);
@@ -23140,9 +23158,9 @@ var init_handler = __esm(() => {
23140
23158
 
23141
23159
  // src/proxy/server.ts
23142
23160
  import { existsSync as existsSync7, readFileSync, writeFileSync } from "fs";
23143
- import { dirname as dirname7, join as join6 } from "path";
23161
+ import { dirname as dirname7, join as join7 } from "path";
23144
23162
  function getRegistryPath() {
23145
- return join6(dirname7(getConfigPath2()), "proxy-instances.json");
23163
+ return join7(dirname7(getConfigPath2()), "proxy-instances.json");
23146
23164
  }
23147
23165
  function loadRegistry() {
23148
23166
  const path = getRegistryPath();
@@ -23191,7 +23209,7 @@ function cleanStaleInstances() {
23191
23209
  saveRegistry(registry2);
23192
23210
  }
23193
23211
  function migrateLegacyRuntime() {
23194
- const legacyPath = join6(dirname7(getConfigPath2()), "proxy-runtime.json");
23212
+ const legacyPath = join7(dirname7(getConfigPath2()), "proxy-runtime.json");
23195
23213
  if (!existsSync7(legacyPath))
23196
23214
  return;
23197
23215
  const registryPath = getRegistryPath();
@@ -24455,7 +24473,7 @@ async function cmdCreateQuiet2(params) {
24455
24473
  console.log(import_picocolors9.default.dim("Usage: swixter qwen create --quiet --name <name> --provider <id> --model <model> [--api-key <key>] [--base-url <url>]"));
24456
24474
  process.exit(1);
24457
24475
  }
24458
- const preset = getPresetById(params.provider);
24476
+ const preset = await getPresetByIdAsync(params.provider);
24459
24477
  if (!preset) {
24460
24478
  console.log(import_picocolors9.default.red(`Error: Unknown provider ID: ${params.provider}`));
24461
24479
  console.log(import_picocolors9.default.dim("Run 'swixter providers' to see all supported providers"));
@@ -24517,7 +24535,7 @@ async function cmdList2() {
24517
24535
  console.log(import_picocolors9.default.bold(LABELS.profileList));
24518
24536
  console.log();
24519
24537
  for (const profile of profiles) {
24520
- const preset = getPresetById(profile.providerId);
24538
+ const preset = await getPresetByIdAsync(profile.providerId);
24521
24539
  const isCurrent = current?.name === profile.name;
24522
24540
  const marker = isCurrent ? import_picocolors9.default.green(MARKERS.active) : import_picocolors9.default.dim(MARKERS.inactive);
24523
24541
  const baseUrl = profile.baseURL || preset?.baseURLChat || preset?.baseURL || MISC_DEFAULTS.baseUrlFallback;
@@ -24539,7 +24557,11 @@ async function cmdSwitch2(profileName, args = []) {
24539
24557
  try {
24540
24558
  await setActiveProfileForCoder(CODER_NAME2, profileName);
24541
24559
  const profile = await getActiveProfileForCoder(CODER_NAME2);
24542
- const preset = getPresetById(profile.providerId);
24560
+ if (!profile) {
24561
+ console.log(import_picocolors9.default.red("Error: Profile not found after switch"));
24562
+ process.exit(1);
24563
+ }
24564
+ const preset = await getPresetByIdAsync(profile.providerId);
24543
24565
  const baseUrl = profile.baseURL || preset?.baseURLChat || preset?.baseURL || "Default";
24544
24566
  console.log();
24545
24567
  console.log(import_picocolors9.default.green("\u2713") + " Switched successfully!");
@@ -24590,10 +24612,13 @@ async function cmdEdit2(profileName) {
24590
24612
  }
24591
24613
  const selected = await ve({
24592
24614
  message: "Select profile to edit",
24593
- options: profiles2.map((profile2) => ({
24594
- value: profile2.name,
24595
- label: profile2.name,
24596
- hint: getPresetById(profile2.providerId)?.displayName || ""
24615
+ options: await Promise.all(profiles2.map(async (profile2) => {
24616
+ const preset = await getPresetByIdAsync(profile2.providerId);
24617
+ return {
24618
+ value: profile2.name,
24619
+ label: profile2.name,
24620
+ hint: preset?.displayName || ""
24621
+ };
24597
24622
  }))
24598
24623
  });
24599
24624
  if (pD(selected)) {
@@ -24614,7 +24639,7 @@ async function cmdEdit2(profileName) {
24614
24639
  console.log();
24615
24640
  const { getProvidersByWireApi: getProvidersByWireApi2 } = await Promise.resolve().then(() => (init_presets(), exports_presets));
24616
24641
  const presets = await getProvidersByWireApi2("chat");
24617
- const currentPreset = getPresetById(profile.providerId);
24642
+ const currentPreset = await getPresetByIdAsync(profile.providerId);
24618
24643
  const shouldChangeProvider = await ye({
24619
24644
  message: `Change provider? Current: ${currentPreset?.displayName}`,
24620
24645
  initialValue: false
@@ -24730,7 +24755,7 @@ async function cmdApply2() {
24730
24755
  }
24731
24756
  try {
24732
24757
  const adapter = getAdapter(CODER_NAME2);
24733
- const preset = getPresetById(profile.providerId);
24758
+ const preset = await getPresetByIdAsync(profile.providerId);
24734
24759
  console.log();
24735
24760
  console.log(import_picocolors9.default.dim(`Applying profile to ${adapter.configPath}...`));
24736
24761
  await adapter.apply(profile);
@@ -24760,7 +24785,7 @@ async function cmdCurrent2() {
24760
24785
  console.log(import_picocolors9.default.yellow("No active profile"));
24761
24786
  return;
24762
24787
  }
24763
- const preset = getPresetById(profile.providerId);
24788
+ const preset = await getPresetByIdAsync(profile.providerId);
24764
24789
  const baseUrl = profile.baseURL || preset?.baseURLChat || preset?.baseURL || "Default";
24765
24790
  console.log();
24766
24791
  console.log(import_picocolors9.default.bold("Current active profile:"));
@@ -24844,10 +24869,13 @@ async function cmdSwitchInteractive2() {
24844
24869
  const current = await getActiveProfileForCoder(CODER_NAME2);
24845
24870
  const profileName = await ve({
24846
24871
  message: "Select profile to switch to",
24847
- options: profiles.map((profile) => ({
24848
- value: profile.name,
24849
- label: profile.name,
24850
- hint: profile.name === current?.name ? "(current)" : getPresetById(profile.providerId)?.displayName || ""
24872
+ options: await Promise.all(profiles.map(async (profile) => {
24873
+ const preset = await getPresetByIdAsync(profile.providerId);
24874
+ return {
24875
+ value: profile.name,
24876
+ label: profile.name,
24877
+ hint: profile.name === current?.name ? "(current)" : preset?.displayName || ""
24878
+ };
24851
24879
  }))
24852
24880
  });
24853
24881
  if (pD(profileName)) {
@@ -24864,10 +24892,13 @@ async function cmdDeleteInteractive2() {
24864
24892
  }
24865
24893
  const profileName = await ve({
24866
24894
  message: "Select profile to delete",
24867
- options: profiles.map((profile) => ({
24868
- value: profile.name,
24869
- label: profile.name,
24870
- hint: getPresetById(profile.providerId)?.displayName || ""
24895
+ options: await Promise.all(profiles.map(async (profile) => {
24896
+ const preset = await getPresetByIdAsync(profile.providerId);
24897
+ return {
24898
+ value: profile.name,
24899
+ label: profile.name,
24900
+ hint: preset?.displayName || ""
24901
+ };
24871
24902
  }))
24872
24903
  });
24873
24904
  if (pD(profileName)) {
@@ -24908,7 +24939,7 @@ async function cmdRun2(args) {
24908
24939
  process.exit(1);
24909
24940
  }
24910
24941
  }
24911
- const preset = getPresetById(profile.providerId);
24942
+ const preset = await getPresetByIdAsync(profile.providerId);
24912
24943
  const baseURL = profile.baseURL || preset?.baseURLChat || preset?.baseURL || "";
24913
24944
  const env = {};
24914
24945
  for (const [key, value] of Object.entries(process.env)) {
@@ -25264,7 +25295,7 @@ async function cmdCreateQuiet3(params) {
25264
25295
  console.log(import_picocolors10.default.dim(`Usage: swixter ${CODER_NAME3} create --quiet --name <name> --provider <id> [--api-key <key>] [--base-url <url>] [--model <model>] [--env-key <var>] [--apply]`));
25265
25296
  process.exit(1);
25266
25297
  }
25267
- const preset = getPresetById(params.provider);
25298
+ const preset = await getPresetByIdAsync(params.provider);
25268
25299
  if (!preset) {
25269
25300
  console.log(import_picocolors10.default.red(`Error: Unknown provider ID: ${params.provider}`));
25270
25301
  console.log(import_picocolors10.default.dim("Run 'swixter providers' to see all supported providers"));
@@ -25337,7 +25368,7 @@ async function cmdList3() {
25337
25368
  console.log(import_picocolors10.default.bold(LABELS.profileList));
25338
25369
  console.log();
25339
25370
  for (const profile of profiles) {
25340
- const preset = getPresetById(profile.providerId);
25371
+ const preset = await getPresetByIdAsync(profile.providerId);
25341
25372
  const isCurrent = current?.name === profile.name;
25342
25373
  const marker = isCurrent ? import_picocolors10.default.green(MARKERS.active) : import_picocolors10.default.dim(MARKERS.inactive);
25343
25374
  const baseUrl = profile.baseURL || preset?.baseURLChat || preset?.baseURL || MISC_DEFAULTS.baseUrlFallback;
@@ -25359,7 +25390,11 @@ async function cmdSwitch3(profileName, args = []) {
25359
25390
  try {
25360
25391
  await setActiveProfileForCoder(CODER_NAME3, profileName);
25361
25392
  const profile = await getActiveProfileForCoder(CODER_NAME3);
25362
- const preset = getPresetById(profile.providerId);
25393
+ if (!profile) {
25394
+ console.log(import_picocolors10.default.red("Error: Profile not found after switch"));
25395
+ process.exit(1);
25396
+ }
25397
+ const preset = await getPresetByIdAsync(profile.providerId);
25363
25398
  const baseUrl = profile.baseURL || preset?.baseURLChat || preset?.baseURL || "Default";
25364
25399
  console.log();
25365
25400
  console.log(import_picocolors10.default.green("\u2713") + " Switched successfully!");
@@ -25410,10 +25445,13 @@ async function cmdEdit3(profileName) {
25410
25445
  }
25411
25446
  const selected = await ve({
25412
25447
  message: "Select profile to edit",
25413
- options: profiles2.map((profile2) => ({
25414
- value: profile2.name,
25415
- label: profile2.name,
25416
- hint: getPresetById(profile2.providerId)?.displayName || ""
25448
+ options: await Promise.all(profiles2.map(async (profile2) => {
25449
+ const preset = await getPresetByIdAsync(profile2.providerId);
25450
+ return {
25451
+ value: profile2.name,
25452
+ label: profile2.name,
25453
+ hint: preset?.displayName || ""
25454
+ };
25417
25455
  }))
25418
25456
  });
25419
25457
  if (pD(selected)) {
@@ -25434,7 +25472,7 @@ async function cmdEdit3(profileName) {
25434
25472
  console.log();
25435
25473
  const { getProvidersByWireApi: getProvidersByWireApi2 } = await Promise.resolve().then(() => (init_presets(), exports_presets));
25436
25474
  const presets = await getProvidersByWireApi2("chat");
25437
- const currentPreset = getPresetById(profile.providerId);
25475
+ const currentPreset = await getPresetByIdAsync(profile.providerId);
25438
25476
  const shouldChangeProvider = await ye({
25439
25477
  message: `Change provider? Current: ${currentPreset?.displayName}`,
25440
25478
  initialValue: false
@@ -25611,7 +25649,7 @@ async function cmdApply3() {
25611
25649
  }
25612
25650
  try {
25613
25651
  const adapter = getAdapter(CODER_NAME3);
25614
- const preset = getPresetById(profile.providerId);
25652
+ const preset = await getPresetByIdAsync(profile.providerId);
25615
25653
  console.log();
25616
25654
  console.log(import_picocolors10.default.dim(`Applying profile to ${adapter.configPath}...`));
25617
25655
  await adapter.apply(profile);
@@ -25654,7 +25692,7 @@ async function cmdCurrent3() {
25654
25692
  console.log(import_picocolors10.default.yellow("No active profile"));
25655
25693
  return;
25656
25694
  }
25657
- const preset = getPresetById(profile.providerId);
25695
+ const preset = await getPresetByIdAsync(profile.providerId);
25658
25696
  const baseUrl = profile.baseURL || preset?.baseURLChat || preset?.baseURL || "Default";
25659
25697
  console.log();
25660
25698
  console.log(import_picocolors10.default.bold("Current active profile:"));
@@ -25738,10 +25776,13 @@ async function cmdSwitchInteractive3() {
25738
25776
  const current = await getActiveProfileForCoder(CODER_NAME3);
25739
25777
  const profileName = await ve({
25740
25778
  message: "Select profile to switch to",
25741
- options: profiles.map((profile) => ({
25742
- value: profile.name,
25743
- label: profile.name,
25744
- hint: profile.name === current?.name ? "(current)" : getPresetById(profile.providerId)?.displayName || ""
25779
+ options: await Promise.all(profiles.map(async (profile) => {
25780
+ const preset = await getPresetByIdAsync(profile.providerId);
25781
+ return {
25782
+ value: profile.name,
25783
+ label: profile.name,
25784
+ hint: profile.name === current?.name ? "(current)" : preset?.displayName || ""
25785
+ };
25745
25786
  }))
25746
25787
  });
25747
25788
  if (pD(profileName)) {
@@ -25758,10 +25799,13 @@ async function cmdDeleteInteractive3() {
25758
25799
  }
25759
25800
  const profileName = await ve({
25760
25801
  message: "Select profile to delete",
25761
- options: profiles.map((profile) => ({
25762
- value: profile.name,
25763
- label: profile.name,
25764
- hint: getPresetById(profile.providerId)?.displayName || ""
25802
+ options: await Promise.all(profiles.map(async (profile) => {
25803
+ const preset = await getPresetByIdAsync(profile.providerId);
25804
+ return {
25805
+ value: profile.name,
25806
+ label: profile.name,
25807
+ hint: preset?.displayName || ""
25808
+ };
25765
25809
  }))
25766
25810
  });
25767
25811
  if (pD(profileName)) {
@@ -25805,7 +25849,7 @@ async function cmdRun3(args) {
25805
25849
  try {
25806
25850
  const adapter = getAdapter(CODER_NAME3);
25807
25851
  await adapter.apply(profile);
25808
- const preset = getPresetById(profile.providerId);
25852
+ const preset = await getPresetByIdAsync(profile.providerId);
25809
25853
  const envKey = profile.envKey || preset?.env_key || "OPENAI_API_KEY";
25810
25854
  const env = {};
25811
25855
  for (const [key, value] of Object.entries(process.env)) {
@@ -25863,9 +25907,8 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
25863
25907
 
25864
25908
  // src/server/index.ts
25865
25909
  var import_picocolors11 = __toESM(require_picocolors(), 1);
25866
- import { dirname as dirname9, join as join8 } from "path";
25910
+ import { dirname as dirname9, join as join9 } from "path";
25867
25911
  import { fileURLToPath } from "url";
25868
- import { exec } from "child_process";
25869
25912
 
25870
25913
  // src/server/middleware.ts
25871
25914
  function corsMiddleware(req, res, next) {
@@ -26150,7 +26193,7 @@ class NodeResShim {
26150
26193
  }
26151
26194
 
26152
26195
  // src/server/bun-static.ts
26153
- import { extname, join as join4 } from "path";
26196
+ import { extname, join as join5 } from "path";
26154
26197
  import { stat } from "fs/promises";
26155
26198
  var MIME_TYPES = {
26156
26199
  ".html": "text/html; charset=utf-8",
@@ -26173,11 +26216,11 @@ var MIME_TYPES = {
26173
26216
  async function serveStaticRequest(request, options) {
26174
26217
  const { root, index = "index.html", spa = true } = options;
26175
26218
  const url2 = new URL(request.url);
26176
- let filePath = join4(root, url2.pathname);
26219
+ let filePath = join5(root, url2.pathname);
26177
26220
  try {
26178
26221
  const stats = await stat(filePath);
26179
26222
  if (stats.isDirectory()) {
26180
- filePath = join4(filePath, index);
26223
+ filePath = join5(filePath, index);
26181
26224
  }
26182
26225
  const file2 = Bun.file(filePath);
26183
26226
  const exists = await file2.exists();
@@ -26190,7 +26233,7 @@ async function serveStaticRequest(request, options) {
26190
26233
  });
26191
26234
  } catch {
26192
26235
  if (spa) {
26193
- const indexPath = join4(root, index);
26236
+ const indexPath = join5(root, index);
26194
26237
  const file2 = Bun.file(indexPath);
26195
26238
  const exists = await file2.exists();
26196
26239
  if (exists) {
@@ -26640,7 +26683,7 @@ init_paths();
26640
26683
  init_export();
26641
26684
  import { existsSync as existsSync9, statSync as statSync2 } from "fs";
26642
26685
  import { readFile as readFile7, writeFile as writeFile7, unlink } from "fs/promises";
26643
- import { join as join7 } from "path";
26686
+ import { join as join8 } from "path";
26644
26687
  async function getVersion(req, res) {
26645
26688
  sendJson(res, {
26646
26689
  appVersion: APP_VERSION,
@@ -26692,7 +26735,7 @@ async function exportConfigFile(req, res) {
26692
26735
  const sanitize = url2.searchParams.get("sanitize") === "true";
26693
26736
  const { exportConfig: exportConfig3 } = await Promise.resolve().then(() => (init_export(), exports_export));
26694
26737
  const tempDir = getConfigDir("swixter");
26695
- const tempPath = join7(tempDir, `.export-${Date.now()}.json`);
26738
+ const tempPath = join8(tempDir, `.export-${Date.now()}.json`);
26696
26739
  try {
26697
26740
  await exportConfig3(tempPath, { sanitizeKeys: sanitize });
26698
26741
  const content = await readFile7(tempPath, "utf-8");
@@ -26717,7 +26760,7 @@ async function importConfigFile(req, res) {
26717
26760
  }
26718
26761
  try {
26719
26762
  const tempDir = getConfigDir("swixter");
26720
- const tempPath = join7(tempDir, `.import-${Date.now()}.json`);
26763
+ const tempPath = join8(tempDir, `.import-${Date.now()}.json`);
26721
26764
  await writeFile7(tempPath, JSON.stringify(body.config), "utf-8");
26722
26765
  try {
26723
26766
  const result = await importConfig(tempPath, { overwrite: body.overwrite !== false });
@@ -26968,18 +27011,20 @@ async function findAvailablePort(startPort = 3141) {
26968
27011
  }
26969
27012
  function openBrowser(url2) {
26970
27013
  const command = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
26971
- exec(`${command} ${url2}`, (error46) => {
26972
- if (error46) {
26973
- console.warn(import_picocolors11.default.yellow(`Could not open browser automatically: ${error46.message}`));
26974
- }
27014
+ import("child_process").then(({ execFile }) => {
27015
+ execFile(command, [url2], (error46) => {
27016
+ if (error46) {
27017
+ console.warn(import_picocolors11.default.yellow(`Could not open browser automatically: ${error46.message}`));
27018
+ }
27019
+ });
26975
27020
  });
26976
27021
  }
26977
27022
  function getUiDir() {
26978
27023
  const isDev = true;
26979
27024
  if (isDev) {
26980
- return join8(__dirname2, "..", "..", "ui", "dist");
27025
+ return join9(__dirname2, "..", "..", "ui", "dist");
26981
27026
  }
26982
- return join8(__dirname2, "..", "..", "ui");
27027
+ return join9(__dirname2, "..", "..", "ui");
26983
27028
  }
26984
27029
  async function startServer(portArg) {
26985
27030
  const port = portArg || await findAvailablePort(3141);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swixter",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "CLI tool for managing AI coding assistant configurations - easily switch between providers (Claude Code, Codex, Continue) with Anthropic, Ollama, or custom APIs",
5
5
  "main": "dist/cli/index.js",
6
6
  "module": "dist/cli/index.js",
@@ -14,13 +14,12 @@
14
14
  "LICENSE"
15
15
  ],
16
16
  "scripts": {
17
- "build": "bun build src/cli/index.ts --outdir dist/cli --target bun --standalone",
18
- "build:ui": "cd ui && npm install && npm run build && mkdir -p ../dist/ui && cp -r dist/* ../dist/ui/",
19
- "build:all": "npm run build:ui && npm run build",
20
- "prepublishOnly": "bun run build:all && bun test",
17
+ "build": "bun run build:ui && bun build src/cli/index.ts --outdir dist/cli --target bun --standalone",
18
+ "build:ui": "cd ui && bun install && bun run build && mkdir -p ../dist/ui && cp -r dist/* ../dist/ui/",
19
+ "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --standalone",
21
20
  "cli": "bun src/cli/index.ts",
22
21
  "cli:dev": "bun --hot src/cli/index.ts",
23
- "ui:dev": "cd ui && npm run dev",
22
+ "ui:dev": "cd ui && bun run dev",
24
23
  "test": "bun test",
25
24
  "test:e2e": "bash test/e2e-docker.sh",
26
25
  "preversion": "bun test",