@cokefenta/pi-switch 0.2.0
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.
- package/README.md +341 -0
- package/bin/pi-switch.js +380 -0
- package/extensions/index.ts +559 -0
- package/index.d.ts +0 -0
- package/index.js +28 -0
- package/package.json +55 -0
- package/pi-switch-native.cjs +22 -0
- package/pi-switch-native.darwin-arm64.node +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# pi-switch
|
|
4
|
+
|
|
5
|
+
[](https://github.com/user/pi-switch/releases)
|
|
6
|
+
[](https://github.com/user/pi-switch/releases)
|
|
7
|
+
[](https://www.rust-lang.org/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
**TUI + CLI dual-mode profile switcher for pi agent**
|
|
11
|
+
|
|
12
|
+
Manage provider profiles, switch models.json, and run a local proxy with failover. Interactive TUI with full CRUD, Dracula theme, and bilingual support.
|
|
13
|
+
|
|
14
|
+
[English](#) | [δΈζ](README_ZH.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## π About
|
|
21
|
+
|
|
22
|
+
pi-switch is a lightweight profile switcher for [pi coding agent](https://pi.dev). It manages `~/.pi/agent/models.json` provider profiles β add, edit, remove, and switch between them via CLI or an interactive terminal UI.
|
|
23
|
+
|
|
24
|
+
Built with Rust (napi-rs) as a native Node.js addon. The interactive TUI is modeled after cc-switch.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## πΈ Screenshots
|
|
29
|
+
|
|
30
|
+
<div align="center">
|
|
31
|
+
<h3>Profiles</h3>
|
|
32
|
+
<img src="assets/screenshots/profiles.png" alt="Profiles" width="70%"/>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<br/>
|
|
36
|
+
|
|
37
|
+
<table>
|
|
38
|
+
<tr>
|
|
39
|
+
<th>Home</th>
|
|
40
|
+
<th>Settings</th>
|
|
41
|
+
</tr>
|
|
42
|
+
<tr>
|
|
43
|
+
<td><img src="assets/screenshots/home.png" alt="Home" width="100%"/></td>
|
|
44
|
+
<td><img src="assets/screenshots/settings.png" alt="Settings" width="100%"/></td>
|
|
45
|
+
</tr>
|
|
46
|
+
</table>
|
|
47
|
+
|
|
48
|
+
## π Quick Start
|
|
49
|
+
|
|
50
|
+
**TUI Mode (Recommended)**
|
|
51
|
+
```bash
|
|
52
|
+
pi-switch tui
|
|
53
|
+
```
|
|
54
|
+
Use the full-screen interface to manage providers, browse presets, inspect proxy status, and configure settings.
|
|
55
|
+
|
|
56
|
+
**Command-Line Mode**
|
|
57
|
+
```bash
|
|
58
|
+
pi-switch provider list # List all provider profiles
|
|
59
|
+
pi-switch provider add <name> [--preset <id>] [--api-key <key>] # Add a profile
|
|
60
|
+
pi-switch use <name> # Switch pi to this profile
|
|
61
|
+
pi-switch provider show <name> # Show profile details
|
|
62
|
+
pi-switch provider delete <name> # Delete a profile
|
|
63
|
+
pi-switch presets list # List built-in provider presets
|
|
64
|
+
pi-switch config show # Display current config
|
|
65
|
+
pi-switch config backups # List backup files
|
|
66
|
+
pi-switch stats # View proxy request statistics
|
|
67
|
+
pi-switch doctor # Run environment diagnostics
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## π₯ Installation
|
|
73
|
+
|
|
74
|
+
### npm (Recommended)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install -g pi-switch
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Pi Package
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pi install npm:pi-switch
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Build from Source
|
|
87
|
+
|
|
88
|
+
**Prerequisites:**
|
|
89
|
+
- Node.js >= 20
|
|
90
|
+
- Rust 1.80+ ([install via rustup](https://rustup.rs/))
|
|
91
|
+
|
|
92
|
+
**Build:**
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/user/pi-switch.git
|
|
95
|
+
cd pi-switch
|
|
96
|
+
npm install
|
|
97
|
+
npm run build:native
|
|
98
|
+
node bin/pi-switch.js tui
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## β¨ Features
|
|
104
|
+
|
|
105
|
+
### π Provider Management
|
|
106
|
+
|
|
107
|
+
Manage provider configurations for pi agent. Built-in presets: OpenRouter, Anthropic, DeepSeek, SiliconFlow, OpenAI.
|
|
108
|
+
|
|
109
|
+
**Features:** add, edit, delete, duplicate, current marker, proxy badge, provider ID display, search/filter.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pi-switch provider list # List all provider profiles
|
|
113
|
+
pi-switch provider show <name> # Show profile details
|
|
114
|
+
pi-switch provider add <name> [--preset <preset>]
|
|
115
|
+
pi-switch provider delete <name> # Delete profile
|
|
116
|
+
pi-switch provider duplicate <name> [--as <new-name>]
|
|
117
|
+
pi-switch use <name> [--mode merge|exclusive] # Switch pi to profile
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### π‘ Built-in Presets
|
|
121
|
+
|
|
122
|
+
Ready-to-use provider templates with pre-configured API endpoints and models.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pi-switch presets list # List all presets
|
|
126
|
+
pi-switch presets show <id> # Show preset detail
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
In the TUI: Presets β Enter to create a profile from a preset template.
|
|
130
|
+
|
|
131
|
+
### βοΈ Configuration Management
|
|
132
|
+
|
|
133
|
+
Manage config backups, imports, and exports with encryption.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pi-switch config show # Display full config
|
|
137
|
+
pi-switch config path # Show config file path
|
|
138
|
+
pi-switch config backups # List backup files
|
|
139
|
+
pi-switch config export <passphrase> # Encrypted export (AES-256-CBC)
|
|
140
|
+
pi-switch config import <path> <passphrase> # Encrypted import
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### π Local Proxy
|
|
144
|
+
|
|
145
|
+
OpenAI-compatible proxy with Anthropic auto-conversion, failover chain, and circuit breaker.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pi-switch proxy start [--host <ip>] [--port <port>] [--profile <name>]
|
|
149
|
+
pi-switch proxy stop
|
|
150
|
+
pi-switch proxy status
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Endpoints:
|
|
154
|
+
- `GET /health`
|
|
155
|
+
- `GET /v1/models`
|
|
156
|
+
- `POST /v1/chat/completions` (OpenAI β Anthropic auto-conversion)
|
|
157
|
+
- `POST /v1/messages` (Anthropic native forwarding)
|
|
158
|
+
|
|
159
|
+
### π Usage Statistics
|
|
160
|
+
|
|
161
|
+
Request metrics aggregated from proxy logs.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pi-switch stats
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Displays: total requests, success rate, per-provider breakdown, per-model breakdown, average latency.
|
|
168
|
+
|
|
169
|
+
### π©Ί Diagnostics
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
pi-switch doctor
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Checks: config file existence, models.json validity, JSON structure, profile count, backup directory.
|
|
176
|
+
|
|
177
|
+
### π Multi-language Support
|
|
178
|
+
|
|
179
|
+
Interactive TUI supports English and Chinese. Language is persisted to config.
|
|
180
|
+
|
|
181
|
+
- Default language: English (set `PI_SWITCH_LANG=zh` for initial Chinese)
|
|
182
|
+
- In TUI: βοΈ Settings β Language β `ββ/Space` to switch
|
|
183
|
+
|
|
184
|
+
### π₯οΈ Interactive TUI
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
pi-switch tui
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Full interactive terminal UI built with ratatui:
|
|
191
|
+
|
|
192
|
+
- **Profiles**: table with proxy badge, provider ID, current marker, add/edit/delete/duplicate/switch/search
|
|
193
|
+
- **Presets**: browse preset templates, create profile from preset
|
|
194
|
+
- **Proxy**: start/stop daemon, view status with target/failover/listen info
|
|
195
|
+
- **Stats**: request metrics by provider and model
|
|
196
|
+
- **Backups**: browse config backup history
|
|
197
|
+
- **Settings**: language (English / δΈζ), proxy host/port/target/failover editing
|
|
198
|
+
|
|
199
|
+
Key bindings:
|
|
200
|
+
- `ββ` switch between menu and content
|
|
201
|
+
- `ββ / j k` move selection
|
|
202
|
+
- `Enter` open detail / confirm
|
|
203
|
+
- `?` help overlay
|
|
204
|
+
- `/` filter
|
|
205
|
+
- `q` quit
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## ποΈ Architecture
|
|
210
|
+
|
|
211
|
+
### Core Design
|
|
212
|
+
|
|
213
|
+
- **napi-rs native addon**: Rust core compiled to `.node` binary for Node.js
|
|
214
|
+
- **pi-switch config**: `~/.pi-switch/config.json` with profiles, proxy settings, backup metadata
|
|
215
|
+
- **pi models.json**: `~/.pi/agent/models.json` β the file pi reads for provider definitions
|
|
216
|
+
- **Atomic writes**: Temp file + rename pattern prevents corruption
|
|
217
|
+
- **Backup rotation**: Auto-backup on every mutation, stored in `~/.pi-switch/backups/`
|
|
218
|
+
|
|
219
|
+
### Config Files
|
|
220
|
+
|
|
221
|
+
- `~/.pi-switch/config.json` β Profiles, current selection, proxy settings
|
|
222
|
+
- `~/.pi-switch/backups/` β Timestamped config backups
|
|
223
|
+
- `~/.pi/agent/models.json` β pi agent provider registry (written by `pi-switch use`)
|
|
224
|
+
|
|
225
|
+
### Code Structure
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
pi-switch/
|
|
229
|
+
βββ bin/pi-switch.js # CLI entry point
|
|
230
|
+
βββ index.js # NAPI re-exports
|
|
231
|
+
βββ pi-switch-native.cjs # NAPI loader (auto platform detection)
|
|
232
|
+
βββ src-rust/
|
|
233
|
+
β βββ lib.rs # NAPI function exports
|
|
234
|
+
β βββ config.rs # Config load/save, types
|
|
235
|
+
β βββ ops.rs # Core operations (use/upsert/remove/duplicate)
|
|
236
|
+
β βββ presets.rs # Built-in provider presets
|
|
237
|
+
β βββ daemon.rs # Proxy daemon management
|
|
238
|
+
β βββ stats.rs # Request log aggregation
|
|
239
|
+
β βββ tui/ # Interactive terminal UI
|
|
240
|
+
β βββ app.rs # State machine + key handler
|
|
241
|
+
β βββ form.rs # Provider form state
|
|
242
|
+
β βββ text_edit.rs # Readline-style text input
|
|
243
|
+
β βββ theme.rs # Dracula theme + color fallback
|
|
244
|
+
β βββ route.rs # Navigation routes
|
|
245
|
+
β βββ i18n.rs # Bilingual text (English / δΈζ)
|
|
246
|
+
β βββ ui/ # Rendering (chrome/pages/profiles/overlay)
|
|
247
|
+
βββ package.json
|
|
248
|
+
βββ Cargo.toml
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## β FAQ
|
|
254
|
+
|
|
255
|
+
<details>
|
|
256
|
+
<summary><b>How do I switch pi to a different provider?</b></summary>
|
|
257
|
+
|
|
258
|
+
<br>
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
pi-switch use <name>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
This updates `~/.pi/agent/models.json` so pi picks up the new provider. If pi is already running, use `/model` inside pi to refresh.
|
|
265
|
+
|
|
266
|
+
Alternatively: open the TUI, navigate to Profiles, and press `Space` on any profile.
|
|
267
|
+
|
|
268
|
+
</details>
|
|
269
|
+
|
|
270
|
+
<details>
|
|
271
|
+
<summary><b>How do I add a custom provider?</b></summary>
|
|
272
|
+
|
|
273
|
+
<br>
|
|
274
|
+
|
|
275
|
+
**CLI:**
|
|
276
|
+
```bash
|
|
277
|
+
pi-switch provider add my-provider --api openai-completions --base-url https://api.example.com/v1 --api-key '$MY_API_KEY' --model gpt-4
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**TUI:** Profiles β `a` β fill in form β `Ctrl+S`
|
|
281
|
+
|
|
282
|
+
</details>
|
|
283
|
+
|
|
284
|
+
<details>
|
|
285
|
+
<summary><b>What does the [proxy] badge mean?</b></summary>
|
|
286
|
+
|
|
287
|
+
<br>
|
|
288
|
+
|
|
289
|
+
A profile with the `[proxy]` badge has `"proxy": true` in its config. This means it's configured to route through the local proxy. The proxy can auto-convert between OpenAI and Anthropic formats and apply failover/circuit-breaker policies.
|
|
290
|
+
|
|
291
|
+
</details>
|
|
292
|
+
|
|
293
|
+
<details>
|
|
294
|
+
<summary><b>How do I set up failover?</b></summary>
|
|
295
|
+
|
|
296
|
+
<br>
|
|
297
|
+
|
|
298
|
+
In the TUI: βοΈ Settings β Failover chain β `Enter` β enter comma-separated profile names β `Enter` to save.
|
|
299
|
+
|
|
300
|
+
Or directly in `~/.pi-switch/config.json` under `settings.proxy.failover`.
|
|
301
|
+
|
|
302
|
+
</details>
|
|
303
|
+
|
|
304
|
+
<details>
|
|
305
|
+
<summary><b>Where is my data stored?</b></summary>
|
|
306
|
+
|
|
307
|
+
<br>
|
|
308
|
+
|
|
309
|
+
All pi-switch data is under `~/.pi-switch/`. pi's own provider registry is `~/.pi/agent/models.json`. No data is sent anywhere.
|
|
310
|
+
|
|
311
|
+
</details>
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## π οΈ Development
|
|
316
|
+
|
|
317
|
+
### Requirements
|
|
318
|
+
|
|
319
|
+
- **Node.js**: >= 20
|
|
320
|
+
- **Rust**: 1.80+ ([rustup](https://rustup.rs/))
|
|
321
|
+
- **npm**: bundled with Node.js
|
|
322
|
+
|
|
323
|
+
### Commands
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
cd pi-switch
|
|
327
|
+
|
|
328
|
+
npm run build:native:debug # Build Rust native addon (debug)
|
|
329
|
+
npm run build:native # Build Rust native addon (release)
|
|
330
|
+
node bin/pi-switch.js tui # Run TUI
|
|
331
|
+
|
|
332
|
+
cargo build # Rust-only build
|
|
333
|
+
cargo clippy # Lint
|
|
334
|
+
cargo fmt # Format
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## π License
|
|
340
|
+
|
|
341
|
+
MIT
|
package/bin/pi-switch.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
initConfig, addProvider, listProfiles, showProfile, useProfile, removeProfile,
|
|
4
|
+
listPresets, showPreset, listBackups, doctor,
|
|
5
|
+
daemonStartNative, daemonStopNative, daemonStatusNative,
|
|
6
|
+
getUsageStats, exportConfig, importConfig,
|
|
7
|
+
runNativeTui,
|
|
8
|
+
} from "../index.js";
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
console.log(`pi-switch v0.2.0 β lightweight profile switcher for pi agent
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
pi-switch provider list
|
|
15
|
+
pi-switch provider show <name>
|
|
16
|
+
pi-switch provider add <name> [--preset <preset>] [--api <api>] [--base-url <url>] [--api-key <key>] [--model <id>...]
|
|
17
|
+
pi-switch provider edit <name>
|
|
18
|
+
pi-switch provider delete <name>
|
|
19
|
+
pi-switch provider duplicate <name> [--as <new-name>]
|
|
20
|
+
pi-switch use <name> [--mode merge|exclusive]
|
|
21
|
+
pi-switch presets [list]
|
|
22
|
+
pi-switch presets show <id>
|
|
23
|
+
pi-switch config show
|
|
24
|
+
pi-switch config path
|
|
25
|
+
pi-switch config export <passphrase>
|
|
26
|
+
pi-switch config import <path> <passphrase>
|
|
27
|
+
pi-switch config backups
|
|
28
|
+
pi-switch proxy start [--host <ip>] [--port <port>] [--profile <name>]
|
|
29
|
+
pi-switch proxy stop
|
|
30
|
+
pi-switch proxy status
|
|
31
|
+
pi-switch stats
|
|
32
|
+
pi-switch doctor
|
|
33
|
+
pi-switch tui
|
|
34
|
+
|
|
35
|
+
Aliases: remove β provider delete, rm β provider delete, interactive/ui β tui
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fail(message, code = 1) {
|
|
40
|
+
const clean = message.startsWith("Error: ") ? message : `Error: ${message}`;
|
|
41
|
+
console.error(clean);
|
|
42
|
+
process.exit(code);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseModel(modelArg) {
|
|
46
|
+
const idx = modelArg.indexOf("=");
|
|
47
|
+
const id = idx === -1 ? modelArg.trim() : modelArg.slice(0, idx).trim();
|
|
48
|
+
const name = idx === -1 ? undefined : modelArg.slice(idx + 1).trim();
|
|
49
|
+
if (!id) throw new Error(`invalid --model '${modelArg}'`);
|
|
50
|
+
return { id, ...(name ? { name } : {}), input: ["text"], contextWindow: 128000, maxTokens: 16384, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const out = { _: [] };
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const arg = argv[i];
|
|
57
|
+
if (!arg.startsWith("--")) { out._.push(arg); continue; }
|
|
58
|
+
const eq = arg.indexOf("=");
|
|
59
|
+
const key = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
60
|
+
const raw = eq === -1 ? argv[++i] : arg.slice(eq + 1);
|
|
61
|
+
if (raw === undefined || raw.startsWith("--")) throw new Error(`missing value for --${key}`);
|
|
62
|
+
if (out[key] === undefined) out[key] = raw;
|
|
63
|
+
else if (Array.isArray(out[key])) out[key].push(raw);
|
|
64
|
+
else out[key] = [out[key], raw];
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function asArray(value) {
|
|
70
|
+
if (value === undefined) return [];
|
|
71
|
+
return Array.isArray(value) ? value : [value];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function main() {
|
|
75
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
76
|
+
try {
|
|
77
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") return usage();
|
|
78
|
+
|
|
79
|
+
// βββ Aliases βββββββββββββββββββββββββββββββββββββ
|
|
80
|
+
|
|
81
|
+
const effectiveCmd = (() => {
|
|
82
|
+
if (cmd === "remove" || cmd === "rm") return "provider-delete";
|
|
83
|
+
if (cmd === "interactive" || cmd === "ui") return "tui";
|
|
84
|
+
return cmd;
|
|
85
|
+
})();
|
|
86
|
+
|
|
87
|
+
// βββ Init ββββββββββββββββββββββββββββββββββββββββ
|
|
88
|
+
|
|
89
|
+
if (effectiveCmd === "init") {
|
|
90
|
+
for (const line of initConfig()) console.log(line);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// βββ Provider subcommands ββββββββββββββββββββββββ
|
|
95
|
+
|
|
96
|
+
if (effectiveCmd === "provider" || effectiveCmd === "provider-delete") {
|
|
97
|
+
const sub = effectiveCmd === "provider-delete" ? "delete" : (rest[0] || "list");
|
|
98
|
+
|
|
99
|
+
if (sub === "list") {
|
|
100
|
+
const data = JSON.parse(listProfiles());
|
|
101
|
+
const names = Object.keys(data.profiles);
|
|
102
|
+
if (names.length === 0) { console.log("No profiles. Add one with: pi-switch provider add <name> ..."); return; }
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
const p = data.profiles[name];
|
|
105
|
+
const mark = data.current === name ? "*" : " ";
|
|
106
|
+
const models = (p.models || []).map(m => m.id).join(", ");
|
|
107
|
+
console.log(`${mark} ${name}`);
|
|
108
|
+
console.log(` api: ${p.api} baseUrl: ${p.baseUrl}`);
|
|
109
|
+
if (models) console.log(` models: ${models}`);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (sub === "show" || sub === "info") {
|
|
115
|
+
const name = rest[1];
|
|
116
|
+
if (!name) fail("provider name required");
|
|
117
|
+
const result = JSON.parse(showProfile(name));
|
|
118
|
+
console.log(JSON.stringify(result, null, 2));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (sub === "add") {
|
|
123
|
+
const args = parseArgs(rest.slice(1));
|
|
124
|
+
const name = args._[0];
|
|
125
|
+
if (!name) fail("provider name required");
|
|
126
|
+
const modelArgs = asArray(args.model);
|
|
127
|
+
const result = addProvider({
|
|
128
|
+
name,
|
|
129
|
+
preset: args.preset || undefined,
|
|
130
|
+
api: args.api || undefined,
|
|
131
|
+
baseUrl: args["base-url"] || args.baseUrl || undefined,
|
|
132
|
+
apiKey: args["api-key"] || args.apiKey || undefined,
|
|
133
|
+
models: modelArgs.length ? modelArgs.map(m => typeof m === 'string' ? parseModel(m).id : m) : undefined,
|
|
134
|
+
});
|
|
135
|
+
console.log(`Saved profile '${result.name}' to ~/.pi-switch/config.json`);
|
|
136
|
+
if (result.backup) console.log(`Backup: ${result.backup}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (sub === "edit") {
|
|
141
|
+
// provider edit opens TUI at the edit form for the given provider
|
|
142
|
+
const name = rest[1];
|
|
143
|
+
if (!name) fail("provider name required");
|
|
144
|
+
const data = JSON.parse(showProfile(name));
|
|
145
|
+
console.log(`Current config for '${name}':`);
|
|
146
|
+
console.log(JSON.stringify(data, null, 2));
|
|
147
|
+
console.log(`\nTo edit, use: pi-switch tui β Profiles β ${name} β e (edit)`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (sub === "delete" || sub === "remove" || sub === "rm") {
|
|
152
|
+
const name = rest[1];
|
|
153
|
+
if (!name) fail("provider name required");
|
|
154
|
+
const result = removeProfile(name);
|
|
155
|
+
console.log(`Removed profile '${result.name}'`);
|
|
156
|
+
if (result.backup) console.log(`Backup: ${result.backup}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (sub === "duplicate" || sub === "copy") {
|
|
161
|
+
const name = rest[1];
|
|
162
|
+
if (!name) fail("provider name required");
|
|
163
|
+
const args = parseArgs(rest.slice(2));
|
|
164
|
+
const dst = args.as || args._[0] || `${name}-copy`;
|
|
165
|
+
// Reuse addProvider with the source's config
|
|
166
|
+
const data = JSON.parse(showProfile(name));
|
|
167
|
+
const p = data.profile;
|
|
168
|
+
const result = addProvider({
|
|
169
|
+
name: dst,
|
|
170
|
+
api: p.api,
|
|
171
|
+
baseUrl: p.baseUrl,
|
|
172
|
+
apiKey: p.apiKey,
|
|
173
|
+
models: (p.models || []).map(m => m.id),
|
|
174
|
+
});
|
|
175
|
+
console.log(`Duplicated '${name}' as '${dst}'`);
|
|
176
|
+
if (result.backup) console.log(`Backup: ${result.backup}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fail(`unknown provider subcommand: '${sub}'`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// βββ Use (shortcut) ββββββββββββββββββββββββββββββ
|
|
184
|
+
|
|
185
|
+
if (effectiveCmd === "use") {
|
|
186
|
+
const args = parseArgs(rest);
|
|
187
|
+
const name = args._[0];
|
|
188
|
+
if (!name) fail("profile name required");
|
|
189
|
+
const result = useProfile(name, args.mode || undefined);
|
|
190
|
+
console.log(`Activated '${result.name}' as provider '${result.providerId}'`);
|
|
191
|
+
if (result.modelsBackup) console.log(`Backup: ${result.modelsBackup}`);
|
|
192
|
+
console.log("Open /model in pi to refresh model list if pi is already running.");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// βββ Presets βββββββββββββββββββββββββββββββββββββ
|
|
197
|
+
|
|
198
|
+
if (effectiveCmd === "presets" || effectiveCmd === "preset") {
|
|
199
|
+
const sub = rest[0] || "list";
|
|
200
|
+
if (sub === "list") {
|
|
201
|
+
let first = true;
|
|
202
|
+
for (const p of listPresets()) {
|
|
203
|
+
if (!first) console.log("");
|
|
204
|
+
first = false;
|
|
205
|
+
console.log(`${p.id} β ${p.description}`);
|
|
206
|
+
console.log(` api: ${p.api} baseUrl: ${p.baseUrl}`);
|
|
207
|
+
console.log(` models: ${p.models.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (sub === "show") {
|
|
212
|
+
const id = rest[1];
|
|
213
|
+
if (!id) fail("preset id required");
|
|
214
|
+
console.log(showPreset(id));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
fail("usage: pi-switch presets [list|show <id>]");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// βββ Config subcommands ββββββββββββββββββββββββββ
|
|
221
|
+
|
|
222
|
+
if (effectiveCmd === "config") {
|
|
223
|
+
const sub = rest[0] || "show";
|
|
224
|
+
if (sub === "show" || sub === "list") {
|
|
225
|
+
const data = JSON.parse(listProfiles());
|
|
226
|
+
console.log(JSON.stringify(data, null, 2));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (sub === "path") {
|
|
230
|
+
console.log(JSON.parse(listProfiles())._configPath || "~/.pi-switch/config.json");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (sub === "export") {
|
|
234
|
+
const pw = rest[1];
|
|
235
|
+
if (!pw) fail("passphrase required");
|
|
236
|
+
console.log(exportConfig(pw));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (sub === "import") {
|
|
240
|
+
const filePath = rest[1];
|
|
241
|
+
const passphrase = rest[2];
|
|
242
|
+
if (!filePath || !passphrase) fail("usage: pi-switch config import <path> <passphrase>");
|
|
243
|
+
console.log(importConfig(filePath, passphrase));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (sub === "backups" || sub === "backup") {
|
|
247
|
+
const result = listBackups();
|
|
248
|
+
if (result.length === 0) console.log("No backups found.");
|
|
249
|
+
else for (const file of result) console.log(file);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
fail(`unknown config subcommand: '${sub}'`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// βββ Proxy subcommands βββββββββββββββββββββββββββ
|
|
256
|
+
|
|
257
|
+
if (effectiveCmd === "proxy") {
|
|
258
|
+
const sub = rest[0] || "status";
|
|
259
|
+
|
|
260
|
+
if (sub === "start") {
|
|
261
|
+
const args = {};
|
|
262
|
+
for (let i = 1; i < rest.length; i++) {
|
|
263
|
+
if (rest[i] === "--host") args.host = rest[++i];
|
|
264
|
+
else if (rest[i] === "--port") args.port = parseInt(rest[++i], 10);
|
|
265
|
+
else if (rest[i] === "--profile") args.profile = rest[++i];
|
|
266
|
+
}
|
|
267
|
+
const result = JSON.parse(daemonStartNative(args.host || null, args.port || null));
|
|
268
|
+
console.log(result.message);
|
|
269
|
+
if (result.pid) console.log(`PID: ${result.pid}`);
|
|
270
|
+
if (args.profile) {
|
|
271
|
+
const useResult = useProfile(args.profile);
|
|
272
|
+
console.log(`Using profile '${useResult.name}' as provider '${useResult.providerId}'`);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (sub === "stop") {
|
|
278
|
+
const result = JSON.parse(daemonStopNative());
|
|
279
|
+
console.log(result.message);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (sub === "status") {
|
|
284
|
+
const result = JSON.parse(daemonStatusNative());
|
|
285
|
+
if (result.running) {
|
|
286
|
+
console.log(`Proxy daemon is running (PID ${result.pid})`);
|
|
287
|
+
console.log(`Listen: http://${result.host}:${result.port}`);
|
|
288
|
+
if (result.target) console.log(`Target: ${result.target}`);
|
|
289
|
+
if (result.failover?.length) console.log(`Failover: ${result.failover.join(" β ")}`);
|
|
290
|
+
} else {
|
|
291
|
+
console.log(result.message);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fail("usage: pi-switch proxy [start|stop|status]");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// βββ Stats βββββββββββββββββββββββββββββββββββββββ
|
|
300
|
+
|
|
301
|
+
if (effectiveCmd === "stats") {
|
|
302
|
+
const stats = JSON.parse(getUsageStats());
|
|
303
|
+
if (stats.totalRequests === 0) {
|
|
304
|
+
console.log("No request data. Start the proxy and make some requests first.");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
console.log(`Total: ${stats.totalRequests} | OK: ${stats.okRequests} | Fail: ${stats.failedRequests} | Rate: ${stats.successRate}`);
|
|
308
|
+
if (stats.avgLatencyMs) console.log(`Avg latency: ${stats.avgLatencyMs}ms`);
|
|
309
|
+
if (Object.keys(stats.byProvider).length) {
|
|
310
|
+
console.log("\nBy Provider:");
|
|
311
|
+
for (const [name, ps] of Object.entries(stats.byProvider)) {
|
|
312
|
+
const rate = ps.total > 0 ? ((ps.ok / ps.total) * 100).toFixed(0) + "%" : "0%";
|
|
313
|
+
console.log(` ${name}: ${ps.total} req, ${ps.ok} ok (${rate})`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// βββ Doctor ββββββββββββββββββββββββββββββββββββββ
|
|
320
|
+
|
|
321
|
+
if (effectiveCmd === "doctor") {
|
|
322
|
+
const checks = doctor();
|
|
323
|
+
let failed = 0;
|
|
324
|
+
for (const c of checks) {
|
|
325
|
+
console.log(`${c.ok ? "β" : "β"} ${c.msg}`);
|
|
326
|
+
if (!c.ok) failed++;
|
|
327
|
+
}
|
|
328
|
+
if (failed) process.exit(1);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// βββ TUI βββββββββββββββββββββββββββββββββββββββββ
|
|
333
|
+
|
|
334
|
+
if (effectiveCmd === "tui") {
|
|
335
|
+
runNativeTui();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// βββ Legacy flat commands (backward compat) ββββββ
|
|
340
|
+
|
|
341
|
+
if (cmd === "list") {
|
|
342
|
+
const data = JSON.parse(listProfiles());
|
|
343
|
+
const names = Object.keys(data.profiles);
|
|
344
|
+
if (names.length === 0) { console.log("No profiles. Add one with: pi-switch provider add <name> ..."); return; }
|
|
345
|
+
for (const name of names) {
|
|
346
|
+
const p = data.profiles[name];
|
|
347
|
+
const mark = data.current === name ? "*" : " ";
|
|
348
|
+
const models = (p.models || []).map(m => m.id).join(", ");
|
|
349
|
+
console.log(`${mark} ${name}`);
|
|
350
|
+
console.log(` api: ${p.api} baseUrl: ${p.baseUrl}`);
|
|
351
|
+
if (models) console.log(` models: ${models}`);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (cmd === "add") {
|
|
357
|
+
const args = parseArgs(rest);
|
|
358
|
+
const name = args._[0];
|
|
359
|
+
if (!name) fail("provider name required");
|
|
360
|
+
const modelArgs = asArray(args.model);
|
|
361
|
+
const result = addProvider({
|
|
362
|
+
name,
|
|
363
|
+
preset: args.preset || undefined,
|
|
364
|
+
api: args.api || undefined,
|
|
365
|
+
baseUrl: args["base-url"] || args.baseUrl || undefined,
|
|
366
|
+
apiKey: args["api-key"] || args.apiKey || undefined,
|
|
367
|
+
models: modelArgs.length ? modelArgs.map(m => typeof m === 'string' ? parseModel(m).id : m) : undefined,
|
|
368
|
+
});
|
|
369
|
+
console.log(`Saved profile '${result.name}' to ~/.pi-switch/config.json`);
|
|
370
|
+
if (result.backup) console.log(`Backup: ${result.backup}`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fail(`unknown command '${cmd}'. Run 'pi-switch help' for usage.`);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
fail(err.stack || err.message);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
main();
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { add as addProfile, doctor, installProxyProvider, list, preset, remove as removeProfile, setProxyTarget, update as updateProfile, use } from "../src/commands.js";
|
|
3
|
+
import { loadConfig, profileToPiProvider, providerIdFor, saveConfig } from "../src/core.js";
|
|
4
|
+
import { getCircuitState, resetCircuitState, daemonStart, daemonStop, daemonStatus } from "../src/proxy.js";
|
|
5
|
+
import { getStats, formatStatsText } from "../src/stats.js";
|
|
6
|
+
import { exportConfig, importConfig, getProfileInfo, openProvider } from "../src/sync.js";
|
|
7
|
+
|
|
8
|
+
async function registerConfiguredProviders(pi: ExtensionAPI): Promise<string[]> {
|
|
9
|
+
const config = await loadConfig();
|
|
10
|
+
const registered: string[] = [];
|
|
11
|
+
|
|
12
|
+
for (const [name, profile] of Object.entries(config.profiles || {})) {
|
|
13
|
+
const providerId = providerIdFor(config, name);
|
|
14
|
+
pi.registerProvider(providerId, profileToPiProvider(profile as any) as any);
|
|
15
|
+
registered.push(providerId);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return registered;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatProfileList(result: Awaited<ReturnType<typeof list>>): string {
|
|
22
|
+
const names = Object.keys(result.profiles);
|
|
23
|
+
if (names.length === 0) return "No profiles. Add one with /piswitch add.";
|
|
24
|
+
return names
|
|
25
|
+
.map((name) => {
|
|
26
|
+
const p = result.profiles[name];
|
|
27
|
+
const mark = result.current === name ? "*" : " ";
|
|
28
|
+
const models = (p.models || []).map((m: any) => m.id).join(", ");
|
|
29
|
+
return `${mark} ${name}\n api: ${p.api}\n baseUrl: ${p.baseUrl}\n models: ${models}`;
|
|
30
|
+
})
|
|
31
|
+
.join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function showDoctor(ctx: ExtensionContext): Promise<void> {
|
|
35
|
+
const checks = await doctor();
|
|
36
|
+
const text = checks.map((c) => `${c.ok ? "β" : "β"} ${c.msg}`).join("\n");
|
|
37
|
+
ctx.ui.notify(text, checks.some((c) => !c.ok) ? "warning" : "info");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function chooseProfile(ctx: ExtensionContext): Promise<string | null> {
|
|
41
|
+
const result = await list();
|
|
42
|
+
const names = Object.keys(result.profiles);
|
|
43
|
+
if (names.length === 0) {
|
|
44
|
+
ctx.ui.notify("No profiles. Add one with /piswitch add.", "warning");
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const selected = await ctx.ui.select(
|
|
48
|
+
"pi-switch profiles",
|
|
49
|
+
names.map((name) => {
|
|
50
|
+
const p = result.profiles[name];
|
|
51
|
+
const mark = result.current === name ? "* " : " ";
|
|
52
|
+
return `${mark}${name} (${p.api})`;
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
if (!selected) return null;
|
|
56
|
+
return selected.replace(/^\*?\s*/, "").split(/\s+/)[0] || null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function promptAddProvider(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
60
|
+
const presetResult = await preset(["list"]);
|
|
61
|
+
const presetItems = presetResult.presets.map((p: any) => `${p.id} β ${p.description}`);
|
|
62
|
+
const selected = await ctx.ui.select("Choose provider preset", [
|
|
63
|
+
...presetItems,
|
|
64
|
+
"custom-openai β Custom OpenAI-compatible endpoint",
|
|
65
|
+
"custom-anthropic β Custom Anthropic-compatible endpoint",
|
|
66
|
+
]);
|
|
67
|
+
if (!selected) return;
|
|
68
|
+
|
|
69
|
+
const presetId = selected.split(" β ")[0];
|
|
70
|
+
const isCustomOpenAI = presetId === "custom-openai";
|
|
71
|
+
const isCustomAnthropic = presetId === "custom-anthropic";
|
|
72
|
+
const isCustom = isCustomOpenAI || isCustomAnthropic;
|
|
73
|
+
|
|
74
|
+
const defaultName = isCustom ? (isCustomOpenAI ? "custom-openai" : "custom-anthropic") : presetId;
|
|
75
|
+
const name = (await ctx.ui.input("Provider name", defaultName))?.trim();
|
|
76
|
+
if (!name) return;
|
|
77
|
+
|
|
78
|
+
let baseUrl: string | undefined;
|
|
79
|
+
let api: string | undefined;
|
|
80
|
+
let model: string | undefined;
|
|
81
|
+
|
|
82
|
+
if (isCustom) {
|
|
83
|
+
api = isCustomOpenAI ? "openai" : "anthropic";
|
|
84
|
+
const defaultUrl = isCustomOpenAI ? "https://api.example.com/v1" : "https://api.example.com";
|
|
85
|
+
baseUrl = (await ctx.ui.input("Base URL", defaultUrl))?.trim();
|
|
86
|
+
if (!baseUrl) return;
|
|
87
|
+
const defaultModel = isCustomOpenAI ? "model-id" : "claude-sonnet-4-5";
|
|
88
|
+
model = (await ctx.ui.input("Model id", defaultModel))?.trim();
|
|
89
|
+
if (!model) return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const defaultKey = isCustom ? "$MY_API_KEY" : `$${presetId.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
93
|
+
const apiKey = (await ctx.ui.input("API key or env var", defaultKey))?.trim();
|
|
94
|
+
if (!apiKey) return;
|
|
95
|
+
|
|
96
|
+
const summary = isCustom
|
|
97
|
+
? `Name: ${name}\nAPI: ${api}\nBase URL: ${baseUrl}\nAPI Key: ${apiKey}\nModel: ${model}`
|
|
98
|
+
: `Name: ${name}\nPreset: ${presetId}\nAPI Key: ${apiKey}`;
|
|
99
|
+
const ok = await ctx.ui.confirm("Create pi-switch provider?", summary);
|
|
100
|
+
if (!ok) return;
|
|
101
|
+
|
|
102
|
+
const argv = isCustom
|
|
103
|
+
? [name, "--api", api!, "--base-url", baseUrl!, "--api-key", apiKey, "--model", model!]
|
|
104
|
+
: [name, "--preset", presetId, "--api-key", apiKey];
|
|
105
|
+
|
|
106
|
+
const result = await addProfile(argv);
|
|
107
|
+
const registered = await registerConfiguredProviders(pi);
|
|
108
|
+
const activate = await ctx.ui.confirm(
|
|
109
|
+
"Provider created",
|
|
110
|
+
`Created '${result.name}'.\nRegistered ${registered.length} provider(s).\n\nActivate it now?`,
|
|
111
|
+
);
|
|
112
|
+
if (activate) {
|
|
113
|
+
const activated = await use([result.name]);
|
|
114
|
+
await registerConfiguredProviders(pi);
|
|
115
|
+
ctx.ui.notify(`Activated '${activated.name}' as '${activated.providerId}'. Open /model to select it.`, "info");
|
|
116
|
+
} else {
|
|
117
|
+
ctx.ui.notify(`Created '${result.name}'. Use /piswitch use ${result.name} when ready.`, "info");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function promptEditProvider(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
122
|
+
const name = await chooseProfile(ctx);
|
|
123
|
+
if (!name) return;
|
|
124
|
+
const result = await list();
|
|
125
|
+
const profile = result.profiles[name];
|
|
126
|
+
if (!profile) return;
|
|
127
|
+
|
|
128
|
+
const action = await ctx.ui.select(`Edit ${name}`, [
|
|
129
|
+
"Edit API key",
|
|
130
|
+
"Edit Base URL",
|
|
131
|
+
"Edit models",
|
|
132
|
+
"Delete profile",
|
|
133
|
+
]);
|
|
134
|
+
if (!action) return;
|
|
135
|
+
|
|
136
|
+
if (action === "Edit API key") {
|
|
137
|
+
const apiKey = (await ctx.ui.input("API key or env var", profile.apiKey || "$API_KEY"))?.trim();
|
|
138
|
+
if (!apiKey) return;
|
|
139
|
+
await updateProfile(name, { apiKey });
|
|
140
|
+
await registerConfiguredProviders(pi);
|
|
141
|
+
ctx.ui.notify(`Updated API key for '${name}'.`, "info");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action === "Edit Base URL") {
|
|
146
|
+
const baseUrl = (await ctx.ui.input("Base URL", profile.baseUrl || "https://api.example.com/v1"))?.trim();
|
|
147
|
+
if (!baseUrl) return;
|
|
148
|
+
await updateProfile(name, { baseUrl });
|
|
149
|
+
await registerConfiguredProviders(pi);
|
|
150
|
+
ctx.ui.notify(`Updated Base URL for '${name}'.`, "info");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (action === "Edit models") {
|
|
155
|
+
const current = (profile.models || []).map((m: any) => (m.name ? `${m.id}=${m.name}` : m.id)).join(", ");
|
|
156
|
+
const text = (await ctx.ui.input("Models, comma separated. Use id or id=name", current))?.trim();
|
|
157
|
+
if (!text) return;
|
|
158
|
+
const models = text.split(",").map((s) => s.trim()).filter(Boolean).map((item) => {
|
|
159
|
+
const idx = item.indexOf("=");
|
|
160
|
+
const id = idx === -1 ? item : item.slice(0, idx).trim();
|
|
161
|
+
const modelName = idx === -1 ? undefined : item.slice(idx + 1).trim();
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
...(modelName ? { name: modelName } : {}),
|
|
165
|
+
input: ["text"],
|
|
166
|
+
contextWindow: 128000,
|
|
167
|
+
maxTokens: 16384,
|
|
168
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
await updateProfile(name, { models });
|
|
172
|
+
await registerConfiguredProviders(pi);
|
|
173
|
+
ctx.ui.notify(`Updated models for '${name}'.`, "info");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (action === "Delete profile") {
|
|
178
|
+
const ok = await ctx.ui.confirm("Delete provider?", `Delete '${name}' from pi-switch config?`);
|
|
179
|
+
if (!ok) return;
|
|
180
|
+
await removeProfile(name);
|
|
181
|
+
await registerConfiguredProviders(pi);
|
|
182
|
+
ctx.ui.notify(`Deleted '${name}'.`, "info");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function promptShowProvider(ctx: ExtensionContext): Promise<void> {
|
|
187
|
+
const name = await chooseProfile(ctx);
|
|
188
|
+
if (!name) return;
|
|
189
|
+
const result = await list();
|
|
190
|
+
const profile = result.profiles[name];
|
|
191
|
+
if (!profile) return;
|
|
192
|
+
const models = (profile.models || []).map((m: any) => `- ${m.id}${m.name ? ` (${m.name})` : ""}`).join("\n");
|
|
193
|
+
ctx.ui.notify(
|
|
194
|
+
`Name: ${name}\nAPI: ${profile.api}\nBase URL: ${profile.baseUrl}\nAPI Key: ${profile.apiKey}\nPreset: ${profile.preset || "custom"}\nModels:\n${models || "- none"}`,
|
|
195
|
+
"info",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function promptRemoveProvider(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
200
|
+
const name = await chooseProfile(ctx);
|
|
201
|
+
if (!name) return;
|
|
202
|
+
const ok = await ctx.ui.confirm("Delete provider?", `Delete '${name}' from pi-switch config?`);
|
|
203
|
+
if (!ok) return;
|
|
204
|
+
await removeProfile(name);
|
|
205
|
+
await registerConfiguredProviders(pi);
|
|
206
|
+
ctx.ui.notify(`Deleted '${name}'.`, "info");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function promptRawConfig(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
210
|
+
const config = await loadConfig();
|
|
211
|
+
const edited = await ctx.ui.editor("Edit ~/.pi-switch/config.json", JSON.stringify(config, null, 2));
|
|
212
|
+
if (edited === undefined) return;
|
|
213
|
+
let parsed: any;
|
|
214
|
+
try {
|
|
215
|
+
parsed = JSON.parse(edited);
|
|
216
|
+
} catch (err: any) {
|
|
217
|
+
ctx.ui.notify(`Invalid JSON: ${err.message}`, "error");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const ok = await ctx.ui.confirm("Save raw config?", "This will overwrite ~/.pi-switch/config.json.");
|
|
221
|
+
if (!ok) return;
|
|
222
|
+
await saveConfig(parsed);
|
|
223
|
+
const registered = await registerConfiguredProviders(pi);
|
|
224
|
+
ctx.ui.notify(`Saved raw config. Registered ${registered.length} provider(s).`, "info");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function promptInstallProxy(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
228
|
+
const action = await ctx.ui.select("Proxy", ["Install proxy provider", "Set target profile", "Circuit status", "Reset circuit", "Daemon start", "Daemon stop", "Daemon status"]);
|
|
229
|
+
if (!action) return;
|
|
230
|
+
|
|
231
|
+
if (action === "Daemon start") {
|
|
232
|
+
try {
|
|
233
|
+
const result = await daemonStart({});
|
|
234
|
+
ctx.ui.notify(result.message + (result.pid ? `\nPID: ${result.pid}\nLog: ${result.logPath}` : ""), "info");
|
|
235
|
+
} catch (err: any) {
|
|
236
|
+
ctx.ui.notify(`Daemon start failed: ${err.message}`, "error");
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (action === "Daemon stop") {
|
|
242
|
+
const result = await daemonStop();
|
|
243
|
+
ctx.ui.notify(result.message, "info");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (action === "Daemon status") {
|
|
248
|
+
const result = await daemonStatus();
|
|
249
|
+
if (result.running) {
|
|
250
|
+
ctx.ui.notify(`Proxy daemon running\nPID: ${result.pid}\nListen: http://${result.host}:${result.port}\nTarget: ${result.target || "none"}\nFailover: ${result.failover?.join(" -> ") || "none"}`, "info");
|
|
251
|
+
} else {
|
|
252
|
+
ctx.ui.notify(result.message, "warning");
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (action === "Set target profile") {
|
|
258
|
+
const target = await chooseNonProxyProfile(ctx);
|
|
259
|
+
if (!target) return;
|
|
260
|
+
const result = await list();
|
|
261
|
+
const others = Object.keys(result.profiles).filter((name) => name !== target && !result.profiles[name]?.proxy);
|
|
262
|
+
const failoverText = others.length
|
|
263
|
+
? await ctx.ui.input("Failover profiles, comma separated (optional)", others.join(","))
|
|
264
|
+
: undefined;
|
|
265
|
+
const failover = failoverText?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
266
|
+
const saved = await setProxyTarget(target, failover);
|
|
267
|
+
ctx.ui.notify(
|
|
268
|
+
`Proxy target set to '${saved.target}'.${saved.failover.length ? `\nFailover: ${saved.failover.join(" -> ")}` : ""}`,
|
|
269
|
+
"info",
|
|
270
|
+
);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (action === "Circuit status") {
|
|
275
|
+
ctx.ui.notify(JSON.stringify(await getCircuitState(), null, 2), "info");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (action === "Reset circuit") {
|
|
280
|
+
await resetCircuitState();
|
|
281
|
+
ctx.ui.notify("Circuit breaker state reset.", "info");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const name = (await ctx.ui.input("Proxy provider name", "proxy"))?.trim();
|
|
286
|
+
if (!name) return;
|
|
287
|
+
const host = (await ctx.ui.input("Proxy host", "127.0.0.1"))?.trim();
|
|
288
|
+
if (!host) return;
|
|
289
|
+
const port = (await ctx.ui.input("Proxy port", "43112"))?.trim();
|
|
290
|
+
if (!port) return;
|
|
291
|
+
const result = await installProxyProvider([name, "--host", host, "--port", port]);
|
|
292
|
+
await registerConfiguredProviders(pi);
|
|
293
|
+
const activate = await ctx.ui.confirm(
|
|
294
|
+
"Proxy provider installed",
|
|
295
|
+
`Installed '${result.name}' -> http://${host}:${port}/v1.\n\nActivate it now?`,
|
|
296
|
+
);
|
|
297
|
+
if (activate) {
|
|
298
|
+
const activated = await use([result.name]);
|
|
299
|
+
ctx.ui.notify(`Activated '${activated.name}'. Start the proxy with: pi-switch proxy start`, "info");
|
|
300
|
+
} else {
|
|
301
|
+
ctx.ui.notify(`Installed '${result.name}'. Start proxy with: pi-switch proxy start`, "info");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function chooseNonProxyProfile(ctx: ExtensionContext): Promise<string | null> {
|
|
306
|
+
const result = await list();
|
|
307
|
+
const names = Object.keys(result.profiles).filter((name) => !result.profiles[name]?.proxy);
|
|
308
|
+
if (names.length === 0) {
|
|
309
|
+
ctx.ui.notify("No non-proxy profiles. Add an upstream provider first with /piswitch add.", "warning");
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const selected = await ctx.ui.select(
|
|
313
|
+
"Proxy target profile",
|
|
314
|
+
names.map((name) => `${name} (${result.profiles[name].api})`),
|
|
315
|
+
);
|
|
316
|
+
return selected ? selected.split(/\s+/)[0] || null : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default async function piSwitchExtension(pi: ExtensionAPI) {
|
|
320
|
+
try {
|
|
321
|
+
await registerConfiguredProviders(pi);
|
|
322
|
+
} catch {
|
|
323
|
+
// Avoid breaking pi startup if pi-switch has not been initialized yet.
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
pi.registerCommand("piswitch", {
|
|
327
|
+
description: "Manage pi-switch profiles",
|
|
328
|
+
getArgumentCompletions: (prefix) => {
|
|
329
|
+
const commands = ["add", "edit", "show", "remove", "raw", "proxy", "list", "use", "doctor", "reload", "presets", "stats", "info", "open", "export", "import", "menu"];
|
|
330
|
+
const matches = commands.filter((c) => c.startsWith(prefix.trim()));
|
|
331
|
+
return matches.length ? matches.map((value) => ({ value, label: value })) : null;
|
|
332
|
+
},
|
|
333
|
+
handler: async (args, ctx) => {
|
|
334
|
+
const [subcommand, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
335
|
+
const cmd = subcommand || "menu";
|
|
336
|
+
|
|
337
|
+
if (cmd === "add") {
|
|
338
|
+
await promptAddProvider(pi, ctx);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (cmd === "edit") {
|
|
343
|
+
await promptEditProvider(pi, ctx);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (cmd === "show") {
|
|
348
|
+
await promptShowProvider(ctx);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (cmd === "remove" || cmd === "rm") {
|
|
353
|
+
await promptRemoveProvider(pi, ctx);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (cmd === "raw") {
|
|
358
|
+
await promptRawConfig(pi, ctx);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (cmd === "proxy") {
|
|
363
|
+
await promptInstallProxy(pi, ctx);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (cmd === "list") {
|
|
368
|
+
ctx.ui.notify(formatProfileList(await list()), "info");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (cmd === "doctor") {
|
|
373
|
+
await showDoctor(ctx);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (cmd === "presets" || cmd === "preset") {
|
|
378
|
+
const result = await preset(["list"]);
|
|
379
|
+
ctx.ui.notify(
|
|
380
|
+
result.presets.map((p: any) => `${p.id}: ${p.description}\n ${p.baseUrl}`).join("\n"),
|
|
381
|
+
"info",
|
|
382
|
+
);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (cmd === "reload") {
|
|
387
|
+
const registered = await registerConfiguredProviders(pi);
|
|
388
|
+
ctx.ui.notify(`Registered ${registered.length} provider(s): ${registered.join(", ") || "none"}`, "info");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (cmd === "stats") {
|
|
393
|
+
const format = rest[0] || "summary";
|
|
394
|
+
const valid = ["summary", "by-provider", "by-model", "hourly", "full"];
|
|
395
|
+
if (!valid.includes(format)) {
|
|
396
|
+
ctx.ui.notify(`Invalid format '${format}'. Use: ${valid.join("|")}`, "warning");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const stats = await getStats();
|
|
400
|
+
ctx.ui.notify(formatStatsText(stats, format), "info");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (cmd === "info") {
|
|
405
|
+
const name = rest[0] || (await chooseProfile(ctx));
|
|
406
|
+
if (!name) return;
|
|
407
|
+
try {
|
|
408
|
+
const result = await getProfileInfo(name);
|
|
409
|
+
const links = [];
|
|
410
|
+
if (result.links.manageKeys) links.push(`Manage keys: ${result.links.manageKeys}`);
|
|
411
|
+
links.push(`API docs: ${result.links.docs}`);
|
|
412
|
+
ctx.ui.notify(
|
|
413
|
+
`Name: ${result.name}\nAPI: ${result.api}\nBase URL: ${result.baseUrl}\nModels: ${result.models.join(", ")}\n\n${links.join("\n")}`,
|
|
414
|
+
"info",
|
|
415
|
+
);
|
|
416
|
+
} catch (err: any) {
|
|
417
|
+
ctx.ui.notify(err.message, "error");
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (cmd === "open") {
|
|
423
|
+
const target = rest[0] || (await chooseProfile(ctx));
|
|
424
|
+
if (!target) return;
|
|
425
|
+
const result = await openProvider(target);
|
|
426
|
+
if (result?.opened) {
|
|
427
|
+
ctx.ui.notify(`Opened ${result.label}`, "info");
|
|
428
|
+
} else if (result?.url) {
|
|
429
|
+
ctx.ui.notify(`${result.label}: ${result.url}`, "info");
|
|
430
|
+
} else {
|
|
431
|
+
ctx.ui.notify(`No link for '${target}'`, "warning");
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (cmd === "export") {
|
|
437
|
+
const passphrase = await ctx.ui.input("Passphrase (min 8 chars)", "");
|
|
438
|
+
if (!passphrase) return;
|
|
439
|
+
try {
|
|
440
|
+
const result = await exportConfig(passphrase.trim());
|
|
441
|
+
ctx.ui.notify(result.message, "info");
|
|
442
|
+
} catch (err: any) {
|
|
443
|
+
ctx.ui.notify(`Export failed: ${err.message}`, "error");
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (cmd === "import") {
|
|
449
|
+
const filePath = await ctx.ui.input("Export file path", "");
|
|
450
|
+
if (!filePath) return;
|
|
451
|
+
const passphrase = await ctx.ui.input("Passphrase", "");
|
|
452
|
+
if (!passphrase) return;
|
|
453
|
+
try {
|
|
454
|
+
const result = await importConfig(filePath.trim(), passphrase.trim());
|
|
455
|
+
ctx.ui.notify(result.message + (result.sanitizedKeys ? `\n${result.sanitizedKeys} key(s) sanitized β env vars` : ""), "info");
|
|
456
|
+
await registerConfiguredProviders(pi);
|
|
457
|
+
} catch (err: any) {
|
|
458
|
+
ctx.ui.notify(`Import failed: ${err.message}`, "error");
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (cmd === "use") {
|
|
464
|
+
const name = rest[0] || (await chooseProfile(ctx));
|
|
465
|
+
if (!name) return;
|
|
466
|
+
const result = await use([name]);
|
|
467
|
+
await registerConfiguredProviders(pi);
|
|
468
|
+
ctx.ui.notify(
|
|
469
|
+
`Activated '${result.name}' as '${result.providerId}'. Open /model to refresh/select the model.`,
|
|
470
|
+
"info",
|
|
471
|
+
);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (cmd !== "menu") {
|
|
476
|
+
ctx.ui.notify("Usage: /piswitch [add|edit|show|remove|raw|list|use|doctor|presets|reload|menu]", "warning");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const selected = await ctx.ui.select("pi-switch", [
|
|
481
|
+
"Add provider",
|
|
482
|
+
"Edit provider",
|
|
483
|
+
"Show provider",
|
|
484
|
+
"Remove provider",
|
|
485
|
+
"Raw config editor",
|
|
486
|
+
"Install proxy provider",
|
|
487
|
+
"Use profile",
|
|
488
|
+
"List profiles",
|
|
489
|
+
"Preset list",
|
|
490
|
+
"Doctor",
|
|
491
|
+
"Reload registered providers",
|
|
492
|
+
"Usage stats",
|
|
493
|
+
"Open dashboard",
|
|
494
|
+
"Export config",
|
|
495
|
+
"Import config",
|
|
496
|
+
]);
|
|
497
|
+
if (selected === "Add provider") {
|
|
498
|
+
await promptAddProvider(pi, ctx);
|
|
499
|
+
} else if (selected === "Edit provider") {
|
|
500
|
+
await promptEditProvider(pi, ctx);
|
|
501
|
+
} else if (selected === "Show provider") {
|
|
502
|
+
await promptShowProvider(ctx);
|
|
503
|
+
} else if (selected === "Remove provider") {
|
|
504
|
+
await promptRemoveProvider(pi, ctx);
|
|
505
|
+
} else if (selected === "Raw config editor") {
|
|
506
|
+
await promptRawConfig(pi, ctx);
|
|
507
|
+
} else if (selected === "Install proxy provider") {
|
|
508
|
+
await promptInstallProxy(pi, ctx);
|
|
509
|
+
} else if (selected === "Use profile") {
|
|
510
|
+
const name = await chooseProfile(ctx);
|
|
511
|
+
if (!name) return;
|
|
512
|
+
const result = await use([name]);
|
|
513
|
+
await registerConfiguredProviders(pi);
|
|
514
|
+
ctx.ui.notify(`Activated '${result.name}' as '${result.providerId}'. Open /model to refresh/select it.`, "info");
|
|
515
|
+
} else if (selected === "List profiles") {
|
|
516
|
+
ctx.ui.notify(formatProfileList(await list()), "info");
|
|
517
|
+
} else if (selected === "Preset list") {
|
|
518
|
+
const result = await preset(["list"]);
|
|
519
|
+
ctx.ui.notify(result.presets.map((p: any) => `${p.id}: ${p.description}\n ${p.baseUrl}`).join("\n"), "info");
|
|
520
|
+
} else if (selected === "Doctor") {
|
|
521
|
+
await showDoctor(ctx);
|
|
522
|
+
} else if (selected === "Reload registered providers") {
|
|
523
|
+
const registered = await registerConfiguredProviders(pi);
|
|
524
|
+
ctx.ui.notify(`Registered ${registered.length} provider(s): ${registered.join(", ") || "none"}`, "info");
|
|
525
|
+
} else if (selected === "Usage stats") {
|
|
526
|
+
const stats = await getStats();
|
|
527
|
+
ctx.ui.notify(formatStatsText(stats, "summary"), "info");
|
|
528
|
+
} else if (selected === "Open dashboard") {
|
|
529
|
+
const name = await chooseProfile(ctx);
|
|
530
|
+
if (!name) return;
|
|
531
|
+
const result = await openProvider(name);
|
|
532
|
+
if (result?.opened) ctx.ui.notify(`Opened ${result.label}`, "info");
|
|
533
|
+
else if (result?.url) ctx.ui.notify(`${result.label}: ${result.url}`, "info");
|
|
534
|
+
else ctx.ui.notify(`No link for '${name}'`, "warning");
|
|
535
|
+
} else if (selected === "Export config") {
|
|
536
|
+
const passphrase = await ctx.ui.input("Passphrase (min 8 chars)", "");
|
|
537
|
+
if (!passphrase) return;
|
|
538
|
+
try {
|
|
539
|
+
const result = await exportConfig(passphrase.trim());
|
|
540
|
+
ctx.ui.notify(result.message, "info");
|
|
541
|
+
} catch (err: any) {
|
|
542
|
+
ctx.ui.notify(`Export failed: ${err.message}`, "error");
|
|
543
|
+
}
|
|
544
|
+
} else if (selected === "Import config") {
|
|
545
|
+
const filePath = await ctx.ui.input("Export file path", "");
|
|
546
|
+
if (!filePath) return;
|
|
547
|
+
const passphrase = await ctx.ui.input("Passphrase", "");
|
|
548
|
+
if (!passphrase) return;
|
|
549
|
+
try {
|
|
550
|
+
const result = await importConfig(filePath.trim(), passphrase.trim());
|
|
551
|
+
ctx.ui.notify(result.message + (result.sanitizedKeys ? `\n${result.sanitizedKeys} key(s) sanitized` : ""), "info");
|
|
552
|
+
await registerConfiguredProviders(pi);
|
|
553
|
+
} catch (err: any) {
|
|
554
|
+
ctx.ui.notify(`Import failed: ${err.message}`, "error");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
}
|
package/index.d.ts
ADDED
|
File without changes
|
package/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// ESM wrapper for pi-switch native addon
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const native = require('./pi-switch-native.cjs');
|
|
5
|
+
|
|
6
|
+
// Re-export all native functions
|
|
7
|
+
export const {
|
|
8
|
+
initConfig,
|
|
9
|
+
listPresets,
|
|
10
|
+
showPreset,
|
|
11
|
+
addProvider,
|
|
12
|
+
listProfiles,
|
|
13
|
+
showProfile,
|
|
14
|
+
useProfile,
|
|
15
|
+
removeProfile,
|
|
16
|
+
listBackups,
|
|
17
|
+
doctor,
|
|
18
|
+
daemonStartNative,
|
|
19
|
+
daemonStopNative,
|
|
20
|
+
daemonStatusNative,
|
|
21
|
+
getUsageStats,
|
|
22
|
+
exportConfig,
|
|
23
|
+
importConfig,
|
|
24
|
+
exportDir,
|
|
25
|
+
runNativeTui,
|
|
26
|
+
} = native;
|
|
27
|
+
|
|
28
|
+
export default native;
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cokefenta/pi-switch",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Lightweight profile switcher for pi models.json",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pi-switch": "./bin/pi-switch.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./index.js",
|
|
10
|
+
"napi": {
|
|
11
|
+
"name": "pi-switch-native",
|
|
12
|
+
"targets": [
|
|
13
|
+
"x86_64-apple-darwin",
|
|
14
|
+
"aarch64-apple-darwin",
|
|
15
|
+
"x86_64-unknown-linux-gnu",
|
|
16
|
+
"x86_64-pc-windows-msvc"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"index.js",
|
|
22
|
+
"index.d.ts",
|
|
23
|
+
"pi-switch-native.cjs",
|
|
24
|
+
"pi-switch-native.darwin-arm64.node",
|
|
25
|
+
"pi-switch-native.darwin-x64.node",
|
|
26
|
+
"pi-switch-native.linux-x64-gnu.node",
|
|
27
|
+
"pi-switch-native.win32-x64-msvc.node",
|
|
28
|
+
"extensions"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"artifacts": "napi artifacts",
|
|
32
|
+
"build:native": "napi build --platform --release",
|
|
33
|
+
"build:native:debug": "napi build --platform",
|
|
34
|
+
"doctor": "node ./bin/pi-switch.js doctor",
|
|
35
|
+
"test": "node --test"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"pi",
|
|
39
|
+
"pi-package",
|
|
40
|
+
"models",
|
|
41
|
+
"switch"
|
|
42
|
+
],
|
|
43
|
+
"pi": {
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./extensions"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@napi-rs/cli": "^3.7.1"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Auto-generated loader for pi-switch-native
|
|
2
|
+
const { existsSync } = require('fs');
|
|
3
|
+
const { join, resolve } = require('path');
|
|
4
|
+
|
|
5
|
+
const { platform, arch } = process;
|
|
6
|
+
|
|
7
|
+
function getBinaryName() {
|
|
8
|
+
const parts = [platform];
|
|
9
|
+
parts.push(arch);
|
|
10
|
+
if (platform === 'linux') parts.push('gnu');
|
|
11
|
+
else if (platform === 'win32') parts.push('msvc');
|
|
12
|
+
return `pi-switch-native.${parts.join('-')}.node`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const binaryName = getBinaryName();
|
|
16
|
+
const localPath = resolve(__dirname, binaryName);
|
|
17
|
+
|
|
18
|
+
if (!existsSync(localPath)) {
|
|
19
|
+
throw new Error(`Native binding not found: ${localPath}. Run: npm run build:native`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = require(localPath);
|
|
Binary file
|