@hieplp/pi-account-switcher 0.2.0 → 0.2.2
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 +53 -21
- package/package.json +10 -7
- package/src/commands/accounts/add.ts +1 -1
- package/src/commands/accounts/edit.ts +1 -1
- package/src/commands/accounts/index.ts +3 -1
- package/src/commands/accounts/list.ts +1 -1
- package/src/commands/accounts/oauth.ts +1 -1
- package/src/commands/accounts/remove.ts +1 -1
- package/src/commands/accounts/shared/base.ts +34 -3
- package/src/commands/accounts/shared/prompts.ts +1 -1
- package/src/commands/accounts/switch.ts +6 -3
- package/src/commands/accounts/verify.ts +298 -0
- package/src/commands/base.ts +6 -6
- package/src/commands/index.ts +1 -1
- package/src/commands/models/add.ts +1 -1
- package/src/commands/models/index.ts +1 -1
- package/src/commands/models/list.ts +1 -1
- package/src/commands/models/remove.ts +1 -1
- package/src/commands/models/shared/base.ts +1 -1
- package/src/commands/models/shared/prompts.ts +1 -1
- package/src/commands/models/shared/select.ts +1 -1
- package/src/commands/providers/add.ts +1 -1
- package/src/commands/providers/edit.ts +1 -1
- package/src/commands/providers/index.ts +1 -1
- package/src/commands/providers/list.ts +5 -2
- package/src/commands/providers/remove.ts +1 -1
- package/src/commands/providers/shared/base.ts +1 -1
- package/src/commands/providers/shared/prompts.ts +1 -1
- package/src/commands/providers/shared/select.ts +1 -1
- package/src/commands/system/export.ts +51 -0
- package/src/commands/system/import.ts +102 -0
- package/src/commands/system/index.ts +5 -1
- package/src/commands/system/reset.ts +1 -1
- package/src/constants/commands.ts +13 -0
- package/src/constants/env.ts +1 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/paths.ts +2 -1
- package/src/extension.ts +2 -4
- package/src/index.ts +1 -1
- package/src/runtime/account-switcher-runtime.ts +6 -2
- package/src/runtime/account-switcher.ts +2 -1
- package/src/runtime/index.ts +2 -4
- package/src/schemas/accounts.ts +1 -2
- package/src/schemas/providers.ts +2 -6
- package/src/services/models.ts +2 -2
- package/src/services/providers.ts +3 -9
- package/src/types/context.ts +1 -1
- package/src/types/index.ts +1 -1
- package/src/utils/accounts.ts +2 -2
- package/src/utils/commands.ts +10 -0
- package/src/utils/common.ts +15 -0
- package/src/utils/errors.ts +1 -3
- package/src/utils/filterable-selector.ts +108 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/models.ts +1 -1
- package/src/utils/ui.ts +36 -2
package/README.md
CHANGED
|
@@ -48,43 +48,55 @@ After installing, reload Pi and add your first account:
|
|
|
48
48
|
/accounts:add
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### Local development command prefix
|
|
52
|
+
|
|
53
|
+
If you have the npm package installed and also run a local checkout, set `PI_ACCOUNT_SWITCHER_COMMAND_PREFIX` before launching Pi to avoid command-name collisions:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
PI_ACCOUNT_SWITCHER_COMMAND_PREFIX=dev pi -e ./src/extension.ts
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The local commands will be registered as `/dev:accounts:list`, `/dev:accounts:add`, etc. The prefix may include the trailing colon (`dev:`) or omit it (`dev`).
|
|
60
|
+
|
|
51
61
|
---
|
|
52
62
|
|
|
53
63
|
## Commands
|
|
54
64
|
|
|
55
65
|
### Accounts
|
|
56
66
|
|
|
57
|
-
| Command
|
|
58
|
-
|
|
59
|
-
| `/accounts:add`
|
|
60
|
-
| `/accounts:list`
|
|
61
|
-
| `/accounts:switch` | Switch to another account within the current provider
|
|
62
|
-
| `/accounts:edit`
|
|
63
|
-
| `/accounts:remove` | Delete an account
|
|
64
|
-
| `/accounts:oauth`
|
|
67
|
+
| Command | Description |
|
|
68
|
+
| ------------------ | --------------------------------------------------------------- |
|
|
69
|
+
| `/accounts:add` | Add a new account interactively |
|
|
70
|
+
| `/accounts:list` | List all accounts and activate the selected one |
|
|
71
|
+
| `/accounts:switch` | Switch to another account within the current provider |
|
|
72
|
+
| `/accounts:edit` | Edit label, provider, id, or credential source |
|
|
73
|
+
| `/accounts:remove` | Delete an account |
|
|
74
|
+
| `/accounts:oauth` | Import the current Pi `/login` OAuth session as a named account |
|
|
65
75
|
|
|
66
76
|
### Providers
|
|
67
77
|
|
|
68
|
-
| Command
|
|
69
|
-
|
|
70
|
-
| `/providers:add`
|
|
71
|
-
| `/providers:list`
|
|
72
|
-
| `/providers:edit`
|
|
78
|
+
| Command | Description |
|
|
79
|
+
| ------------------- | -------------------------------- |
|
|
80
|
+
| `/providers:add` | Add a reusable custom provider |
|
|
81
|
+
| `/providers:list` | List custom providers |
|
|
82
|
+
| `/providers:edit` | Edit a custom provider |
|
|
73
83
|
| `/providers:remove` | Remove an unused custom provider |
|
|
74
84
|
|
|
75
85
|
### Models
|
|
76
86
|
|
|
77
|
-
| Command
|
|
78
|
-
|
|
79
|
-
| `/models:list`
|
|
80
|
-
| `/models:add`
|
|
81
|
-
| `/models:remove` | Remove a custom model config
|
|
87
|
+
| Command | Description |
|
|
88
|
+
| ---------------- | -------------------------------------------------------- |
|
|
89
|
+
| `/models:list` | List all available models and switch to the selected one |
|
|
90
|
+
| `/models:add` | Add a custom model config to the current provider |
|
|
91
|
+
| `/models:remove` | Remove a custom model config |
|
|
82
92
|
|
|
83
93
|
### System
|
|
84
94
|
|
|
85
|
-
| Command
|
|
86
|
-
|
|
87
|
-
| `/system:reset`
|
|
95
|
+
| Command | Description |
|
|
96
|
+
| ---------------- | -------------------------------------------------------- |
|
|
97
|
+
| `/system:reset` | Delete all accounts, providers, and state |
|
|
98
|
+
| `/system:export` | Export all accounts, providers, and state to a JSON file |
|
|
99
|
+
| `/system:import` | Import accounts, providers, and state from a JSON file |
|
|
88
100
|
|
|
89
101
|
---
|
|
90
102
|
|
|
@@ -157,6 +169,26 @@ Custom providers are stored at `~/.pi/account-switcher/providers.json` and suppo
|
|
|
157
169
|
|
|
158
170
|
---
|
|
159
171
|
|
|
172
|
+
## Export / Import
|
|
173
|
+
|
|
174
|
+
Back up or migrate your full configuration with two commands:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
/system:export # prompts for a path, defaults to ~/pi-account-switcher-export.json
|
|
178
|
+
/system:export ~/backup.json # export to a specific path
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The export file contains all accounts, providers, and active-selection state as a single JSON bundle. To restore on another machine (or after a reset):
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
/system:import # prompts for a path, defaults to ~/pi-account-switcher-export.json
|
|
185
|
+
/system:import ~/backup.json # import from a specific path
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> **Warning:** import replaces all existing data. A confirmation prompt is shown before anything is written.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
160
192
|
## Config Reference
|
|
161
193
|
|
|
162
194
|
### Accounts — `~/.pi/account-switcher/accounts.json`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hieplp/pi-account-switcher",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi extension for quickly switching between multiple accounts/API keys per provider.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,21 +27,24 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
|
-
"test": "vitest run"
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"format": "prettier --write .",
|
|
32
|
+
"format:check": "prettier --check ."
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
35
|
+
"@earendil-works/pi-tui": "^0.74.0",
|
|
33
36
|
"@mariozechner/jiti": "^2.6.5",
|
|
34
|
-
"@mariozechner/pi-tui": "^0.73.1",
|
|
35
37
|
"zod": "^4.4.3"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
+
"@earendil-works/pi-ai": "*",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
+
"@earendil-works/pi-ai": "latest",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "latest",
|
|
44
46
|
"@types/node": "latest",
|
|
47
|
+
"prettier": "^3.8.3",
|
|
45
48
|
"typescript": "latest",
|
|
46
49
|
"vitest": "latest"
|
|
47
50
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountConfig, AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { useAddAccountCommand } from "./add";
|
|
4
4
|
import { useEditAccountCommand } from "./edit";
|
|
@@ -6,6 +6,7 @@ import { useListAccountsCommand } from "./list";
|
|
|
6
6
|
import { useOAuthImportCommand } from "./oauth";
|
|
7
7
|
import { useRemoveAccountCommand } from "./remove";
|
|
8
8
|
import { useSwitchAccountCommand } from "./switch";
|
|
9
|
+
import { useVerifyAccountsCommand } from "./verify";
|
|
9
10
|
|
|
10
11
|
const useAccountCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
11
12
|
useAddAccountCommand(pi, runtime);
|
|
@@ -14,6 +15,7 @@ const useAccountCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
|
14
15
|
useOAuthImportCommand(pi, runtime);
|
|
15
16
|
useRemoveAccountCommand(pi, runtime);
|
|
16
17
|
useSwitchAccountCommand(pi, runtime);
|
|
18
|
+
useVerifyAccountsCommand(pi, runtime);
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export default useAccountCommands;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountConfig, AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS, OAUTH_PROVIDER_IDS, PI_AUTH_PATH } from "@/constants";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountConfig, AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { uiUtil } from "@/utils";
|
|
@@ -69,6 +69,31 @@ export abstract class AccountCommand extends BaseCommand {
|
|
|
69
69
|
accounts: AccountConfig[],
|
|
70
70
|
label = "Pick account",
|
|
71
71
|
): Promise<AccountConfig | undefined> {
|
|
72
|
+
const { labels, values } = this.buildGroupedAccountSelectItems(accounts, true);
|
|
73
|
+
return this.pickGrouped(ctx, label, labels, values);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected async pickGroupedAccounts(
|
|
77
|
+
ctx: AccountSwitcherContext,
|
|
78
|
+
accounts: AccountConfig[],
|
|
79
|
+
label = "Pick accounts",
|
|
80
|
+
): Promise<AccountConfig[] | undefined> {
|
|
81
|
+
const activeId = this.runtime.getActiveAccount()?.id;
|
|
82
|
+
const { labels, values } = this.buildGroupedAccountSelectItems(accounts, false);
|
|
83
|
+
const firstAccountIndex = values.findIndex((value) => value !== null);
|
|
84
|
+
const initialChecked = values.map((value, index) =>
|
|
85
|
+
activeId ? value?.id === activeId : firstAccountIndex !== -1 && index === firstAccountIndex,
|
|
86
|
+
);
|
|
87
|
+
return uiUtil.multiGroupedSelect(ctx.ui, label, labels, values, initialChecked);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private buildGroupedAccountSelectItems(
|
|
91
|
+
accounts: AccountConfig[],
|
|
92
|
+
includeActiveMarker: boolean,
|
|
93
|
+
): {
|
|
94
|
+
labels: string[];
|
|
95
|
+
values: Array<AccountConfig | null>;
|
|
96
|
+
} {
|
|
72
97
|
const items = buildGroupedItems(accounts, this.runtime.getProviders(), this.runtime.getActiveAccount()?.id);
|
|
73
98
|
|
|
74
99
|
const labels: string[] = [];
|
|
@@ -79,10 +104,16 @@ export abstract class AccountCommand extends BaseCommand {
|
|
|
79
104
|
values.push(null);
|
|
80
105
|
continue;
|
|
81
106
|
}
|
|
82
|
-
labels.push(formatAccountItem(item));
|
|
107
|
+
labels.push(includeActiveMarker ? formatAccountItem(item) : this.formatMultiAccountItem(item));
|
|
83
108
|
values.push(item.account);
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
return
|
|
111
|
+
return { labels, values };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private formatMultiAccountItem(
|
|
115
|
+
item: Extract<ReturnType<typeof buildGroupedItems>[number], { type: "account" }>,
|
|
116
|
+
): string {
|
|
117
|
+
return item.active ? `${item.account.label} (active)` : item.account.label;
|
|
87
118
|
}
|
|
88
119
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionUIContext } from "@
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountConfig, ProviderConfig, SecretSource } from "@/types";
|
|
3
3
|
import { commonUtil, providerUtil, uiUtil } from "@/utils";
|
|
4
4
|
import { ACCOUNTS_PATH } from "@/constants";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|
|
5
|
-
import { errorUtil, providerUtil } from "@/utils";
|
|
5
|
+
import { commandUtil, errorUtil, providerUtil } from "@/utils";
|
|
6
6
|
import { AccountCommand } from "./shared";
|
|
7
7
|
|
|
8
8
|
export const useSwitchAccountCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
@@ -20,7 +20,10 @@ class SwitchAccountCommand extends AccountCommand {
|
|
|
20
20
|
|
|
21
21
|
const active = this.runtime.getActiveAccount();
|
|
22
22
|
if (!active) {
|
|
23
|
-
ctx.ui.notify(
|
|
23
|
+
ctx.ui.notify(
|
|
24
|
+
`No active account. Use ${commandUtil.name(COMMANDS.accounts.list.name)} to activate one first.`,
|
|
25
|
+
"info",
|
|
26
|
+
);
|
|
24
27
|
return;
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { completeSimple, type Api, type Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AuthCredential, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
4
|
+
import type { AccountConfig, AccountSwitcherContext, ProviderConfig, SecretSource } from "@/types";
|
|
5
|
+
import { COMMANDS } from "@/constants";
|
|
6
|
+
import { accountUtil, commonUtil, errorUtil, providerUtil, uiUtil } from "@/utils";
|
|
7
|
+
import { AccountCommand } from "./shared";
|
|
8
|
+
|
|
9
|
+
export const useVerifyAccountsCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
10
|
+
new VerifyAccountsCommand(pi, runtime).register();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type VerifyTestPlan = {
|
|
14
|
+
secrets: boolean;
|
|
15
|
+
ping: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CheckResult = {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
line: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type AccountVerifyReport = {
|
|
24
|
+
account: AccountConfig;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
lines: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TEST_OPTIONS = {
|
|
30
|
+
secrets: "Verify secrets",
|
|
31
|
+
ping: "Send ping",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const MAX_PARALLEL_VERIFY = 3;
|
|
35
|
+
|
|
36
|
+
class VerifyAccountsCommand extends AccountCommand {
|
|
37
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
38
|
+
super(pi, runtime, COMMANDS.accounts.verify);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async handler(ctx: AccountSwitcherContext, args?: string): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
const accounts = await this.loadAccounts(ctx);
|
|
44
|
+
if (!accounts) return;
|
|
45
|
+
|
|
46
|
+
const parts = (args ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
47
|
+
const verifyAll = parts.includes("all");
|
|
48
|
+
const pingArg = parts.includes("ping") || parts.includes("probe") || parts.includes("connect");
|
|
49
|
+
const targets = verifyAll
|
|
50
|
+
? accounts
|
|
51
|
+
: await this.pickGroupedAccounts(ctx, accounts, pingArg ? "Verify and ping accounts" : "Verify accounts");
|
|
52
|
+
|
|
53
|
+
if (!targets || targets.length === 0) return;
|
|
54
|
+
|
|
55
|
+
const plan = pingArg ? { secrets: true, ping: true } : await this.pickTestPlan(ctx);
|
|
56
|
+
if (!plan) return;
|
|
57
|
+
|
|
58
|
+
const selectedTests = this.formatSelectedTests(plan);
|
|
59
|
+
ctx.ui.notify(
|
|
60
|
+
`Testing ${targets.length} account${targets.length === 1 ? "" : "s"} (${selectedTests})...`,
|
|
61
|
+
"info",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const concurrency = plan.ping ? 1 : MAX_PARALLEL_VERIFY;
|
|
65
|
+
const reports = await commonUtil.runWithConcurrency(targets, concurrency, (account) =>
|
|
66
|
+
this.verifyAccount(ctx, account, plan),
|
|
67
|
+
);
|
|
68
|
+
const failed = reports.filter((report) => !report.ok).length;
|
|
69
|
+
|
|
70
|
+
ctx.ui.setEditorText(this.formatReport(plan, reports));
|
|
71
|
+
ctx.ui.notify(
|
|
72
|
+
`Finished testing ${targets.length} account${targets.length === 1 ? "" : "s"}${
|
|
73
|
+
failed ? ` — ${failed} failed` : " — all checks passed"
|
|
74
|
+
}.`,
|
|
75
|
+
failed ? "warning" : "info",
|
|
76
|
+
);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
ctx.ui.notify(`accounts:verify failed: ${errorUtil.format(error)}`, "error");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async pickTestPlan(ctx: AccountSwitcherContext): Promise<VerifyTestPlan | undefined> {
|
|
83
|
+
const selected = await uiUtil.multiSelect(
|
|
84
|
+
ctx.ui,
|
|
85
|
+
"What should be tested?",
|
|
86
|
+
[TEST_OPTIONS.secrets, TEST_OPTIONS.ping],
|
|
87
|
+
[true, false],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!selected) return undefined;
|
|
91
|
+
|
|
92
|
+
const plan = {
|
|
93
|
+
secrets: selected.includes(TEST_OPTIONS.secrets),
|
|
94
|
+
ping: selected.includes(TEST_OPTIONS.ping),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (!plan.secrets && !plan.ping) {
|
|
98
|
+
ctx.ui.notify("No tests selected.", "info");
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return plan;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async verifyAccount(
|
|
106
|
+
ctx: AccountSwitcherContext,
|
|
107
|
+
account: AccountConfig,
|
|
108
|
+
plan: VerifyTestPlan,
|
|
109
|
+
): Promise<AccountVerifyReport> {
|
|
110
|
+
const lines: string[] = [];
|
|
111
|
+
let ok = true;
|
|
112
|
+
|
|
113
|
+
if (plan.secrets) {
|
|
114
|
+
let anyChecked = false;
|
|
115
|
+
|
|
116
|
+
if (account.piAuth) {
|
|
117
|
+
lines.push("✓ secrets: using stored OAuth/piAuth credentials");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (account.usesProviderApiKey && !account.providerApiKey) {
|
|
121
|
+
lines.push("✓ secrets: using provider config apiKey");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const secretChecks: Array<[string, SecretSource]> = [];
|
|
125
|
+
if (account.providerApiKey) secretChecks.push(["providerApiKey", account.providerApiKey]);
|
|
126
|
+
if (!account.piAuth && account.env) {
|
|
127
|
+
for (const [envName, source] of Object.entries(account.env)) secretChecks.push([envName, source]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (secretChecks.length > 0) {
|
|
131
|
+
const results = await Promise.all(secretChecks.map(([key, source]) => this.verifySecret(key, source)));
|
|
132
|
+
for (const result of results) {
|
|
133
|
+
lines.push(result.line);
|
|
134
|
+
ok &&= result.ok;
|
|
135
|
+
}
|
|
136
|
+
} else if (!account.piAuth && !account.usesProviderApiKey) {
|
|
137
|
+
lines.push("ℹ secrets: no secrets configured");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (plan.ping) {
|
|
142
|
+
const result = await this.pingAccount(ctx, account);
|
|
143
|
+
lines.push(result.line);
|
|
144
|
+
ok &&= result.ok;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { account, ok, lines };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async pingAccount(ctx: AccountSwitcherContext, account: AccountConfig): Promise<CheckResult> {
|
|
151
|
+
const prefix = `[${account.label}]`;
|
|
152
|
+
const authProvider = this.resolveAuthProvider(account);
|
|
153
|
+
const model = this.resolveProbeModel(ctx, account, authProvider);
|
|
154
|
+
if (!model) {
|
|
155
|
+
return { ok: false, line: `✗ ping: skipped — no model found for provider ${authProvider}` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const envBackup = new Map<string, string | undefined>();
|
|
159
|
+
let authBackup: AuthCredential | undefined;
|
|
160
|
+
let hadAuth = false;
|
|
161
|
+
let providerToRestore: ProviderConfig | undefined;
|
|
162
|
+
|
|
163
|
+
let requestAuth!: Awaited<ReturnType<typeof ctx.modelRegistry.getApiKeyAndHeaders>>;
|
|
164
|
+
try {
|
|
165
|
+
if (!account.piAuth && account.env) {
|
|
166
|
+
const resolved = await accountUtil.resolveAccountEnv(account);
|
|
167
|
+
for (const [envName, value] of resolved) {
|
|
168
|
+
envBackup.set(envName, process.env[envName]);
|
|
169
|
+
process.env[envName] = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (account.piAuth) {
|
|
174
|
+
hadAuth = ctx.modelRegistry.authStorage.has(authProvider);
|
|
175
|
+
authBackup = ctx.modelRegistry.authStorage.get(authProvider);
|
|
176
|
+
ctx.modelRegistry.authStorage.set(authProvider, account.piAuth.entry);
|
|
177
|
+
ctx.modelRegistry.authStorage.reload();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (account.providerApiKey) {
|
|
181
|
+
const provider = providerUtil.findProvider(account.provider, this.runtime.getProviders());
|
|
182
|
+
if (provider) {
|
|
183
|
+
providerToRestore = provider;
|
|
184
|
+
const apiKey = await accountUtil.resolveSecret(account.providerApiKey);
|
|
185
|
+
if (!apiKey) throw new Error("Resolved empty providerApiKey for ping");
|
|
186
|
+
this.runtime.registerProvider({ ...provider, apiKey });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
requestAuth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
191
|
+
if (!requestAuth.ok) throw new Error(requestAuth.error);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return { ok: false, line: `✗ ping: unable to prepare credentials — ${errorUtil.format(err)}` };
|
|
194
|
+
} finally {
|
|
195
|
+
for (const [envName, previous] of envBackup) {
|
|
196
|
+
if (previous === undefined) delete process.env[envName];
|
|
197
|
+
else process.env[envName] = previous;
|
|
198
|
+
}
|
|
199
|
+
if (account.piAuth) {
|
|
200
|
+
if (hadAuth && authBackup) ctx.modelRegistry.authStorage.set(authProvider, authBackup);
|
|
201
|
+
else ctx.modelRegistry.authStorage.remove(authProvider);
|
|
202
|
+
ctx.modelRegistry.authStorage.reload();
|
|
203
|
+
}
|
|
204
|
+
if (providerToRestore) {
|
|
205
|
+
this.runtime.registerProvider(providerToRestore);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
ctx.ui.notify(`${prefix} ping: sending request via ${model.provider}/${model.id}...`, "info");
|
|
211
|
+
|
|
212
|
+
const response = await completeSimple(
|
|
213
|
+
model,
|
|
214
|
+
{
|
|
215
|
+
systemPrompt: "You are a health-check endpoint. Follow the user instruction exactly.",
|
|
216
|
+
messages: [
|
|
217
|
+
{
|
|
218
|
+
role: "user",
|
|
219
|
+
content: "Health check: reply with exactly OK.",
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
apiKey: requestAuth.apiKey,
|
|
226
|
+
headers: requestAuth.headers,
|
|
227
|
+
maxTokens: 16,
|
|
228
|
+
timeoutMs: 30_000,
|
|
229
|
+
maxRetries: 0,
|
|
230
|
+
reasoning: "minimal",
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (response.stopReason === "error") throw new Error(response.errorMessage ?? "model returned an error");
|
|
235
|
+
const text = response.content.find((block) => block.type === "text")?.text?.trim();
|
|
236
|
+
return { ok: true, line: `✓ ping: OK via ${model.provider}/${model.id}${text ? ` — ${text}` : ""}` };
|
|
237
|
+
} catch (err) {
|
|
238
|
+
return { ok: false, line: `✗ ping: failed — ${errorUtil.format(err)}` };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private resolveAuthProvider(account: AccountConfig): string {
|
|
243
|
+
if (account.piAuth?.provider) return account.piAuth.provider;
|
|
244
|
+
const provider = providerUtil.findProvider(account.provider, this.runtime.getProviders());
|
|
245
|
+
return provider?.piAuthProvider ?? providerUtil.normalizeProvider(account.provider);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private resolveProbeModel(
|
|
249
|
+
ctx: AccountSwitcherContext,
|
|
250
|
+
account: AccountConfig,
|
|
251
|
+
authProvider: string,
|
|
252
|
+
): Model<Api> | undefined {
|
|
253
|
+
const providers = this.runtime.getProviders();
|
|
254
|
+
const normalized = providerUtil.normalizeProviderWithCustom(authProvider, providers);
|
|
255
|
+
if (account.model) {
|
|
256
|
+
const configured = ctx.modelRegistry.find(authProvider, account.model);
|
|
257
|
+
if (configured) return configured;
|
|
258
|
+
}
|
|
259
|
+
if (ctx.model && providerUtil.normalizeProviderWithCustom(ctx.model.provider, providers) === normalized) {
|
|
260
|
+
return ctx.model;
|
|
261
|
+
}
|
|
262
|
+
return ctx.modelRegistry
|
|
263
|
+
.getAll()
|
|
264
|
+
.find((model) => providerUtil.normalizeProviderWithCustom(model.provider, providers) === normalized);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private async verifySecret(key: string, source: SecretSource): Promise<CheckResult> {
|
|
268
|
+
try {
|
|
269
|
+
const value = await accountUtil.resolveSecret(source);
|
|
270
|
+
if (!value) throw new Error("resolved to empty value");
|
|
271
|
+
return { ok: true, line: `✓ secrets: ${key} OK` };
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return { ok: false, line: `✗ secrets: ${key} failed — ${errorUtil.format(err)}` };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private formatReport(plan: VerifyTestPlan, reports: AccountVerifyReport[]): string {
|
|
278
|
+
const selectedTests = this.formatSelectedTests(plan);
|
|
279
|
+
const passed = reports.filter((report) => report.ok).length;
|
|
280
|
+
|
|
281
|
+
return [
|
|
282
|
+
"Account verify results",
|
|
283
|
+
"",
|
|
284
|
+
`Tests: ${selectedTests}`,
|
|
285
|
+
`Accounts: ${reports.length} (${passed} passed, ${reports.length - passed} failed)`,
|
|
286
|
+
"",
|
|
287
|
+
...reports.flatMap((report) => [
|
|
288
|
+
`${report.ok ? "✓" : "✗"} ${report.account.label}`,
|
|
289
|
+
...report.lines.map((line) => ` ${line}`),
|
|
290
|
+
"",
|
|
291
|
+
]),
|
|
292
|
+
].join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private formatSelectedTests(plan: VerifyTestPlan): string {
|
|
296
|
+
return [plan.secrets ? "secrets" : undefined, plan.ping ? "ping" : undefined].filter(Boolean).join(" + ");
|
|
297
|
+
}
|
|
298
|
+
}
|
package/src/commands/base.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountConfig, AccountSwitcherContext, ProviderConfig } from "@/types";
|
|
4
|
-
import { providerUtil, uiUtil } from "@/utils";
|
|
4
|
+
import { commandUtil, providerUtil, uiUtil } from "@/utils";
|
|
5
5
|
|
|
6
6
|
function deduplicateLabels(labels: string[]): string[] {
|
|
7
7
|
const seen = new Map<string, number>();
|
|
@@ -19,7 +19,7 @@ export interface CommandMeta {
|
|
|
19
19
|
|
|
20
20
|
export interface Command extends CommandMeta {
|
|
21
21
|
register(): void;
|
|
22
|
-
handler(ctx: AccountSwitcherContext): Promise<void>;
|
|
22
|
+
handler(ctx: AccountSwitcherContext, args?: string): Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export abstract class BaseCommand implements Command {
|
|
@@ -31,14 +31,14 @@ export abstract class BaseCommand implements Command {
|
|
|
31
31
|
protected readonly runtime: AccountSwitcher,
|
|
32
32
|
meta: CommandMeta,
|
|
33
33
|
) {
|
|
34
|
-
this.name = meta.name;
|
|
34
|
+
this.name = commandUtil.name(meta.name);
|
|
35
35
|
this.description = meta.description;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
register(): void {
|
|
39
39
|
this.pi.registerCommand(this.name, {
|
|
40
40
|
description: this.description,
|
|
41
|
-
handler: (
|
|
41
|
+
handler: (args, ctx) => this.handler(ctx, args),
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -77,5 +77,5 @@ export abstract class BaseCommand implements Command {
|
|
|
77
77
|
return ctx.model?.id === modelId;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
abstract handler(ctx: AccountSwitcherContext): Promise<void>;
|
|
80
|
+
abstract handler(ctx: AccountSwitcherContext, args?: string): Promise<void>;
|
|
81
81
|
}
|
package/src/commands/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import useProviderCommands from "./providers";
|
|
4
4
|
import useAccountCommands from "./accounts";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { useListModelsCommand } from "./list";
|
|
4
4
|
import { useAddModelCommand } from "./add";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|