@hieplp/pi-account-switcher 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.
Files changed (77) hide show
  1. package/INSTALL_AS_PI_PACKAGE.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/USAGE.md +446 -0
  5. package/package.json +53 -0
  6. package/src/commands/accounts/add.ts +63 -0
  7. package/src/commands/accounts/edit.ts +31 -0
  8. package/src/commands/accounts/index.ts +19 -0
  9. package/src/commands/accounts/list.ts +31 -0
  10. package/src/commands/accounts/oauth.ts +59 -0
  11. package/src/commands/accounts/remove.ts +40 -0
  12. package/src/commands/accounts/shared/base.ts +88 -0
  13. package/src/commands/accounts/shared/index.ts +3 -0
  14. package/src/commands/accounts/shared/prompts.ts +226 -0
  15. package/src/commands/accounts/shared/select.ts +37 -0
  16. package/src/commands/accounts/switch.ts +54 -0
  17. package/src/commands/base.ts +81 -0
  18. package/src/commands/index.ts +16 -0
  19. package/src/commands/models/add.ts +30 -0
  20. package/src/commands/models/index.ts +13 -0
  21. package/src/commands/models/list.ts +45 -0
  22. package/src/commands/models/remove.ts +41 -0
  23. package/src/commands/models/shared/base.ts +44 -0
  24. package/src/commands/models/shared/index.ts +3 -0
  25. package/src/commands/models/shared/prompts.ts +36 -0
  26. package/src/commands/models/shared/select.ts +37 -0
  27. package/src/commands/providers/add.ts +28 -0
  28. package/src/commands/providers/edit.ts +29 -0
  29. package/src/commands/providers/index.ts +15 -0
  30. package/src/commands/providers/list.ts +38 -0
  31. package/src/commands/providers/remove.ts +46 -0
  32. package/src/commands/providers/shared/base.ts +30 -0
  33. package/src/commands/providers/shared/index.ts +3 -0
  34. package/src/commands/providers/shared/prompts.ts +172 -0
  35. package/src/commands/providers/shared/select.ts +24 -0
  36. package/src/commands/system/index.ts +7 -0
  37. package/src/commands/system/reset.ts +36 -0
  38. package/src/constants/commands.ts +66 -0
  39. package/src/constants/config.ts +6 -0
  40. package/src/constants/index.ts +4 -0
  41. package/src/constants/paths.ts +8 -0
  42. package/src/constants/providers.ts +36 -0
  43. package/src/extension.ts +21 -0
  44. package/src/index.ts +20 -0
  45. package/src/runtime/account-switcher-runtime.ts +194 -0
  46. package/src/runtime/account-switcher.ts +32 -0
  47. package/src/runtime/index.ts +11 -0
  48. package/src/schemas/accounts.ts +50 -0
  49. package/src/schemas/common.ts +3 -0
  50. package/src/schemas/config.ts +7 -0
  51. package/src/schemas/index.ts +4 -0
  52. package/src/schemas/providers.ts +57 -0
  53. package/src/services/accounts.ts +116 -0
  54. package/src/services/index.ts +4 -0
  55. package/src/services/models.ts +27 -0
  56. package/src/services/pi-auth.ts +23 -0
  57. package/src/services/providers.ts +123 -0
  58. package/src/storage/accounts.ts +109 -0
  59. package/src/storage/index.ts +4 -0
  60. package/src/storage/paths.ts +8 -0
  61. package/src/storage/pi-auth.ts +37 -0
  62. package/src/storage/providers.ts +85 -0
  63. package/src/storage/state.ts +43 -0
  64. package/src/types/accounts.ts +37 -0
  65. package/src/types/config.ts +6 -0
  66. package/src/types/context.ts +3 -0
  67. package/src/types/index.ts +4 -0
  68. package/src/types/providers.ts +53 -0
  69. package/src/utils/accounts.ts +99 -0
  70. package/src/utils/common.ts +74 -0
  71. package/src/utils/errors.ts +16 -0
  72. package/src/utils/files.ts +25 -0
  73. package/src/utils/filterable-selector.ts +114 -0
  74. package/src/utils/index.ts +7 -0
  75. package/src/utils/models.ts +76 -0
  76. package/src/utils/providers.ts +49 -0
  77. package/src/utils/ui.ts +49 -0
package/USAGE.md ADDED
@@ -0,0 +1,446 @@
1
+ # Pi Account Switcher — Install & Usage
2
+
3
+ This guide explains how to install, run, and use this extension in Pi.
4
+
5
+ ## 1. Install Dependencies
6
+
7
+ From this repository:
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ Optional sanity check:
14
+
15
+ ```bash
16
+ npm run typecheck
17
+ ```
18
+
19
+ ## 2. Run Temporarily for Testing
20
+
21
+ The fastest way to test the extension is with Pi's `-e` / `--extension` flag:
22
+
23
+ ```bash
24
+ pi -e ./src/extension.ts
25
+ ```
26
+
27
+ Then, inside Pi, add your first account:
28
+
29
+ ```txt
30
+ /accounts:add
31
+ ```
32
+
33
+ To reload after manually editing the config file, use Pi's built-in:
34
+
35
+ ```txt
36
+ /reload
37
+ ```
38
+
39
+ ## 3. Install as a Project-local Pi Extension
40
+
41
+ To make the extension auto-load for this project, place it under `.pi/extensions/` or configure it as a package.
42
+
43
+ Recommended project-local setup:
44
+
45
+ ```bash
46
+ mkdir -p .pi/extensions/account-switcher
47
+ cp -R src package.json package-lock.json tsconfig.json .pi/extensions/account-switcher/
48
+ ```
49
+
50
+ Then start Pi from the project directory:
51
+
52
+ ```bash
53
+ pi
54
+ ```
55
+
56
+ If Pi is already running, use:
57
+
58
+ ```txt
59
+ /reload
60
+ ```
61
+
62
+ Pi auto-discovers extensions from:
63
+
64
+ ```txt
65
+ .pi/extensions/*.ts
66
+ .pi/extensions/*/index.ts
67
+ ~/.pi/agent/extensions/*.ts
68
+ ~/.pi/agent/extensions/*/index.ts
69
+ ```
70
+
71
+ The easiest dev command is:
72
+
73
+ ```bash
74
+ pi -e ./src/extension.ts
75
+ ```
76
+
77
+ ## 4. Install Globally for All Pi Projects
78
+
79
+ To use the extension globally:
80
+
81
+ ```bash
82
+ mkdir -p ~/.pi/agent/extensions/account-switcher
83
+ cp -R src package.json package-lock.json tsconfig.json ~/.pi/agent/extensions/account-switcher/
84
+ ```
85
+
86
+ Then start Pi anywhere:
87
+
88
+ ```bash
89
+ pi
90
+ ```
91
+
92
+ Or reload an existing Pi session:
93
+
94
+ ```txt
95
+ /reload
96
+ ```
97
+
98
+ ## 5. OAuth Login Like Pi `/login`
99
+
100
+ For subscription/OAuth providers, use Pi's built-in login first, then import that login as a named switchable account.
101
+
102
+ ```txt
103
+ /login
104
+ /accounts:oauth
105
+ ```
106
+
107
+ To add another OAuth account for the same provider, run `/login` again with the other browser account, then run `/accounts:oauth` again with a different label.
108
+
109
+ Switch OAuth accounts with:
110
+
111
+ ```txt
112
+ /accounts:list
113
+ ```
114
+
115
+ OAuth credentials are captured from Pi's auth file:
116
+
117
+ ```txt
118
+ ~/.pi/agent/auth.json
119
+ ```
120
+
121
+ When switching OAuth accounts, this extension applies the stored credentials to Pi's live auth storage and clears cached provider sessions when Pi exposes cleanup hooks.
122
+
123
+ ## 6. Configure API-key Accounts from Inside Pi
124
+
125
+ You can add API-key accounts directly from Pi without hand-writing JSON.
126
+
127
+ ### Add an account
128
+
129
+ ```txt
130
+ /accounts:add
131
+ ```
132
+
133
+ This opens a wizard for provider, label, id, credential env var, and secret source, then optionally activates the new account. If the id already exists, choose replace, enter a new id, or cancel. For custom model providers, choose a default model, then paste an account API key override or leave it blank to use the provider-level `apiKey`. Switching to that account re-registers the provider key and switches Pi to the account model. If you enter a free-text custom provider, Pi can save it as a reusable provider.
134
+
135
+ The wizard supports secret sources from pasted API key, env var, file, shell command, or 1Password `op://` reference.
136
+
137
+ Warning: if you choose `Paste API key now`, the key is written as plain text to:
138
+
139
+ ```txt
140
+ ~/.pi/account-switcher/accounts.json
141
+ ```
142
+
143
+ Prefer env vars, files with restricted permissions, or 1Password references.
144
+
145
+ ### Manage custom providers
146
+
147
+ ```txt
148
+ /providers:list
149
+ /providers:add
150
+ /providers:edit
151
+ /providers:remove
152
+ ```
153
+
154
+ Custom providers are stored separately from accounts:
155
+
156
+ ```txt
157
+ ~/.pi/account-switcher/providers.json
158
+ ```
159
+
160
+ Built-in providers are read-only. Removing a custom provider is blocked while accounts still use it. Provider entries may also include Pi model-provider fields like `baseUrl`, `api`, `apiKey`, `compat`, and `models`; account-switcher registers those providers with Pi.
161
+
162
+ `apiKey` can be an env var name, shell command (`!op read ...`), or raw key. Raw keys work, but they are stored in plaintext in `providers.json`; prefer env/file/1Password when possible.
163
+
164
+ Example provider config:
165
+
166
+ ```json
167
+ {
168
+ "providers": {
169
+ "acme": {
170
+ "name": "Acme AI",
171
+ "baseUrl": "https://api.acme.test/v1",
172
+ "api": "openai-completions",
173
+ "apiKey": "ACME_API_KEY",
174
+ "envKeys": ["ACME_API_KEY"],
175
+ "aliases": ["acme-ai"],
176
+ "models": [{ "id": "acme-coder", "name": "Acme Coder" }]
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ ## 7. Configure Accounts Manually
183
+
184
+ Account config lives at:
185
+
186
+ ```txt
187
+ ~/.pi/account-switcher/accounts.json
188
+ ```
189
+
190
+ Example config:
191
+
192
+ ```json
193
+ {
194
+ "switchMode": "env",
195
+ "accounts": [
196
+ {
197
+ "id": "claude-work",
198
+ "label": "Claude — Work",
199
+ "provider": "anthropic",
200
+ "env": {
201
+ "ANTHROPIC_API_KEY": { "type": "env", "name": "ANTHROPIC_WORK_API_KEY" }
202
+ }
203
+ },
204
+ {
205
+ "id": "claude-personal",
206
+ "label": "Claude — Personal",
207
+ "provider": "anthropic",
208
+ "env": {
209
+ "ANTHROPIC_API_KEY": { "type": "file", "path": "~/.keys/claude-personal.txt" }
210
+ }
211
+ },
212
+ {
213
+ "id": "codex-client-a",
214
+ "label": "Codex — Client A",
215
+ "provider": "openai",
216
+ "env": {
217
+ "OPENAI_API_KEY": { "type": "op", "reference": "op://AI/CodexClientA/api-key" }
218
+ }
219
+ }
220
+ ]
221
+ }
222
+ ```
223
+
224
+ ## 8. Supported Secret Sources
225
+
226
+ ### Literal value
227
+
228
+ ```json
229
+ {
230
+ "OPENAI_API_KEY": { "type": "literal", "value": "sk-..." }
231
+ }
232
+ ```
233
+
234
+ ### Existing environment variable
235
+
236
+ ```json
237
+ {
238
+ "ANTHROPIC_API_KEY": { "type": "env", "name": "ANTHROPIC_WORK_API_KEY" }
239
+ }
240
+ ```
241
+
242
+ ### File
243
+
244
+ ```json
245
+ {
246
+ "ANTHROPIC_API_KEY": { "type": "file", "path": "~/.keys/claude-work.txt" }
247
+ }
248
+ ```
249
+
250
+ ### Shell command
251
+
252
+ ```json
253
+ {
254
+ "ANTHROPIC_API_KEY": {
255
+ "type": "command",
256
+ "command": "op read op://AI/ClaudeWork/api-key"
257
+ }
258
+ }
259
+ ```
260
+
261
+ ### 1Password reference
262
+
263
+ ```json
264
+ {
265
+ "OPENAI_API_KEY": {
266
+ "type": "op",
267
+ "reference": "op://AI/CodexClientA/api-key"
268
+ }
269
+ }
270
+ ```
271
+
272
+ A plain string is treated as a literal value, except strings beginning with `op://` are resolved using `op read`.
273
+
274
+ ## 9. Commands
275
+
276
+ ### Pick account for current provider
277
+
278
+ ```txt
279
+ /accounts:list
280
+ ```
281
+
282
+ The extension tries to detect the current model provider and shows matching accounts.
283
+
284
+ ### Pick account for a specific provider
285
+
286
+ ```txt
287
+ /accounts:list
288
+ ```
289
+
290
+ Useful if Pi cannot detect the active provider.
291
+
292
+ ### List accounts
293
+
294
+ ```txt
295
+ /accounts:list
296
+ ```
297
+
298
+ ### Import current Pi OAuth login
299
+
300
+ ```txt
301
+ /accounts:oauth
302
+ ```
303
+
304
+ Use this after Pi's built-in `/login`.
305
+
306
+ ### Add account interactively
307
+
308
+ ```txt
309
+ /accounts:add
310
+ ```
311
+
312
+ ### Login/add account and activate it
313
+
314
+ ```txt
315
+ /accounts:add
316
+ ```
317
+
318
+ ### Edit account
319
+
320
+ ```txt
321
+ /accounts:edit
322
+ ```
323
+
324
+ Edit label, provider, id, and env credential source. Blank text input keeps the existing value. Literal secret values are not displayed by default.
325
+
326
+ ### Remove account
327
+
328
+ ```txt
329
+ /accounts:remove
330
+ ```
331
+
332
+ Shows a non-secret summary, asks for confirmation, deletes the account, and clears stale saved selections.
333
+
334
+ ### Manage custom providers
335
+
336
+ ```txt
337
+ /providers:list
338
+ /providers:add
339
+ /providers:edit
340
+ /providers:remove
341
+ ```
342
+
343
+ ## 10. Switching Flow
344
+
345
+ Typical usage:
346
+
347
+ 1. Start Pi:
348
+
349
+ ```bash
350
+ pi -e ./src/extension.ts
351
+ ```
352
+
353
+ 2. For OAuth/subscription accounts, login with Pi and import it:
354
+
355
+ ```txt
356
+ /login
357
+ /accounts:oauth
358
+ ```
359
+
360
+ For API-key accounts, add and activate an account:
361
+
362
+ ```txt
363
+ /accounts:add
364
+ ```
365
+
366
+ 3. Later, switch accounts:
367
+
368
+ ```txt
369
+ /accounts:list
370
+ ```
371
+
372
+ Alternative manual config flow: edit `~/.pi/account-switcher/accounts.json` directly, then reload Pi:
373
+
374
+ 6. If needed, reload Pi runtime:
375
+
376
+ ```txt
377
+ /reload
378
+ ```
379
+
380
+ ## 11. State Persistence
381
+
382
+ Selected accounts are saved at:
383
+
384
+ ```txt
385
+ ~/.pi/account-switcher/state.json
386
+ ```
387
+
388
+ Example:
389
+
390
+ ```json
391
+ {
392
+ "activeAccountId": "claude-work",
393
+ "activeModelId": "claude-sonnet-4",
394
+ "activeModelProvider": "anthropic"
395
+ }
396
+ ```
397
+
398
+ On Pi session start, the extension restores the saved active account and model state.
399
+
400
+ ## 12. Important Note About Credential Caching
401
+
402
+ The extension updates `process.env`, Pi's live runtime API-key overrides, and Pi's live OAuth auth storage when those hooks are available.
403
+
404
+ If a provider still keeps old credentials cached, run `/reload` or restart Pi.
405
+
406
+ ## 13. Troubleshooting
407
+
408
+ ### No accounts configured
409
+
410
+ Run `/accounts:add` to create one interactively, or create `~/.pi/account-switcher/accounts.json` manually.
411
+
412
+ ### No accounts for provider
413
+
414
+ Run explicitly:
415
+
416
+ ```txt
417
+ /accounts:list
418
+ ```
419
+
420
+ Also check that account `provider` values match supported providers:
421
+
422
+ - `anthropic` / `claude`
423
+ - `openai`
424
+ - `openai-codex` / `codex`
425
+ - `google` / `gemini`
426
+ - `xai`
427
+ - `openrouter`
428
+
429
+ ### Secret resolves empty
430
+
431
+ Check the configured secret source:
432
+
433
+ - env var exists
434
+ - file exists and contains the key
435
+ - command works manually
436
+ - `op` CLI is signed in
437
+
438
+ ### Changes do not apply
439
+
440
+ Switch the account again. If the provider still keeps old credentials cached, run:
441
+
442
+ ```txt
443
+ /reload
444
+ ```
445
+
446
+ If it still uses the old account, restart Pi.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@hieplp/pi-account-switcher",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Pi extension for quickly switching between multiple accounts/API keys per provider.",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi-extension",
10
+ "pi",
11
+ "account-switcher"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/hieplp/pi-account-switcher.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/hieplp/pi-account-switcher/issues"
19
+ },
20
+ "homepage": "https://github.com/hieplp/pi-account-switcher#readme",
21
+ "files": [
22
+ "src",
23
+ "!src/**/*.test.ts",
24
+ "README.md",
25
+ "USAGE.md",
26
+ "INSTALL_AS_PI_PACKAGE.md"
27
+ ],
28
+ "scripts": {
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "vitest run"
31
+ },
32
+ "dependencies": {
33
+ "@mariozechner/jiti": "^2.6.5",
34
+ "@mariozechner/pi-tui": "^0.73.1",
35
+ "zod": "^4.4.3"
36
+ },
37
+ "peerDependencies": {
38
+ "@mariozechner/pi-ai": "*",
39
+ "@mariozechner/pi-coding-agent": "*"
40
+ },
41
+ "devDependencies": {
42
+ "@mariozechner/pi-ai": "latest",
43
+ "@mariozechner/pi-coding-agent": "latest",
44
+ "@types/node": "latest",
45
+ "typescript": "latest",
46
+ "vitest": "latest"
47
+ },
48
+ "pi": {
49
+ "extensions": [
50
+ "./src/extension.ts"
51
+ ]
52
+ }
53
+ }
@@ -0,0 +1,63 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import type { AccountConfig, AccountSwitcherContext } from "@/types";
4
+ import { COMMANDS } from "@/constants";
5
+ import { errorUtil, providerUtil } from "@/utils";
6
+ import { AccountCommand } from "./shared";
7
+ import { AccountConfigBuilder } from "./shared/prompts";
8
+
9
+ export const useAddAccountCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
10
+ new AddAccountCommand(pi, runtime).register();
11
+ };
12
+
13
+ class AddAccountCommand extends AccountCommand {
14
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
15
+ super(pi, runtime, COMMANDS.accounts.add);
16
+ }
17
+
18
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
19
+ try {
20
+ await this.runtime.load();
21
+ const providers = this.runtime.getProviders();
22
+
23
+ const account = await new AccountConfigBuilder(ctx.ui, providers).collect();
24
+ if (!account) return;
25
+
26
+ await this.saveProvider(ctx, account);
27
+ const saved = await this.saveAccount(ctx, account);
28
+ if (!saved) return;
29
+
30
+ ctx.ui.notify(`Added account ${saved.label}.`, "info");
31
+
32
+ const activate = await ctx.ui.confirm(
33
+ "Activate now?",
34
+ `Switch ${providerUtil.normalizeProvider(saved.provider)} to ${saved.label} now?`,
35
+ );
36
+ if (activate) {
37
+ const applied = await this.runtime.activateAccount(saved, ctx);
38
+ const detail = applied ? ` (${applied})` : "";
39
+ ctx.ui.notify(`Activated ${saved.label}${detail}.`, "info");
40
+ }
41
+ } catch (error) {
42
+ ctx.ui.notify(`Failed to add account: ${errorUtil.format(error)}`, "error");
43
+ }
44
+ }
45
+
46
+ private async saveProvider(ctx: AccountSwitcherContext, account: AccountConfig): Promise<void> {
47
+ const providerId = providerUtil.normalizeProvider(account.provider);
48
+
49
+ if (providerUtil.isBuiltInProviderId(providerId)) return;
50
+
51
+ if (providerUtil.hasProvider(providerId, this.runtime.getProviders())) return;
52
+
53
+ const save = await ctx.ui.confirm(
54
+ "Save custom provider?",
55
+ `Save ${providerId} as a reusable custom provider for future account setup?`,
56
+ );
57
+ if (!save) return;
58
+
59
+ const provider = { id: providerId, label: providerId, envKeys: Object.keys(account.env ?? {}) };
60
+ await this.runtime.addProvider(provider);
61
+ ctx.ui.notify(`Saved custom provider ${provider.id}.`, "info");
62
+ }
63
+ }
@@ -0,0 +1,31 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import type { AccountSwitcherContext } from "@/types";
4
+ import { COMMANDS } from "@/constants";
5
+ import { errorUtil } from "@/utils";
6
+ import { AccountCommand, AccountConfigBuilder } from "./shared";
7
+
8
+ export const useEditAccountCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
9
+ new EditAccountCommand(pi, runtime).register();
10
+ };
11
+
12
+ class EditAccountCommand extends AccountCommand {
13
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
14
+ super(pi, runtime, COMMANDS.accounts.edit);
15
+ }
16
+
17
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
18
+ try {
19
+ const original = await this.loadAndSelectAccount(ctx, "Select account to edit");
20
+ if (!original) return;
21
+
22
+ const updated = await new AccountConfigBuilder(ctx.ui, this.runtime.getProviders(), original).collect();
23
+ if (!updated) return;
24
+
25
+ await this.runtime.editAccount(original, updated);
26
+ ctx.ui.notify(`Account "${updated.label}" updated.`, "info");
27
+ } catch (e) {
28
+ ctx.ui.notify(`Failed to edit account: ${errorUtil.format(e)}`, "error");
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,19 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import { useAddAccountCommand } from "./add";
4
+ import { useEditAccountCommand } from "./edit";
5
+ import { useListAccountsCommand } from "./list";
6
+ import { useOAuthImportCommand } from "./oauth";
7
+ import { useRemoveAccountCommand } from "./remove";
8
+ import { useSwitchAccountCommand } from "./switch";
9
+
10
+ const useAccountCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
11
+ useAddAccountCommand(pi, runtime);
12
+ useEditAccountCommand(pi, runtime);
13
+ useListAccountsCommand(pi, runtime);
14
+ useOAuthImportCommand(pi, runtime);
15
+ useRemoveAccountCommand(pi, runtime);
16
+ useSwitchAccountCommand(pi, runtime);
17
+ };
18
+
19
+ export default useAccountCommands;
@@ -0,0 +1,31 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import type { AccountSwitcherContext } from "@/types";
4
+ import { COMMANDS } from "@/constants";
5
+ import { errorUtil } from "@/utils";
6
+ import { AccountCommand } from "./shared";
7
+
8
+ export const useListAccountsCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
9
+ new ListAccountsCommand(pi, runtime).register();
10
+ };
11
+
12
+ class ListAccountsCommand extends AccountCommand {
13
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
14
+ super(pi, runtime, COMMANDS.accounts.list);
15
+ }
16
+
17
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
18
+ try {
19
+ const accounts = await this.loadAccounts(ctx);
20
+ if (!accounts) return;
21
+
22
+ const account = await this.pickGroupedAccount(ctx, accounts, "Pick account to activate");
23
+ if (!account) return;
24
+
25
+ const applied = await this.runtime.activateAccount(account, ctx);
26
+ ctx.ui.notify(`Switched to ${account.label} (${applied}).`, "info");
27
+ } catch (error) {
28
+ ctx.ui.notify(`Failed to list accounts: ${errorUtil.format(error)}`, "error");
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,59 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import type { AccountConfig, AccountSwitcherContext } from "@/types";
4
+ import { COMMANDS, OAUTH_PROVIDER_IDS, PI_AUTH_PATH } from "@/constants";
5
+ import { commonUtil, errorUtil } from "@/utils";
6
+ import { AccountCommand } from "./shared";
7
+
8
+ export const useOAuthImportCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
9
+ new OAuthImportCommand(pi, runtime).register();
10
+ };
11
+
12
+ class OAuthImportCommand extends AccountCommand {
13
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
14
+ super(pi, runtime, COMMANDS.accounts.oauth);
15
+ }
16
+
17
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
18
+ try {
19
+ await this.runtime.load();
20
+
21
+ const providerChoice = await ctx.ui.select("Provider logged in with Pi /login", [...OAUTH_PROVIDER_IDS]);
22
+ if (!providerChoice) return;
23
+
24
+ const provider =
25
+ providerChoice === "custom"
26
+ ? (await ctx.ui.input("Pi auth provider id", "provider-id"))?.trim()
27
+ : providerChoice;
28
+ if (!provider) return;
29
+
30
+ const entry = await this.runtime.getPiAuthEntry(provider);
31
+ if (!entry) {
32
+ ctx.ui.notify(`No Pi auth entry for "${provider}". Run /login ${provider} first, then try again.`, "error");
33
+ return;
34
+ }
35
+
36
+ if (!this.runtime.isOAuthEntry(entry)) {
37
+ const ok = await ctx.ui.confirm(
38
+ "Import non-OAuth auth entry?",
39
+ `"${provider}" exists in ${PI_AUTH_PATH} but is not marked as OAuth. Import anyway?`,
40
+ );
41
+ if (!ok) return;
42
+ }
43
+
44
+ const label = (await ctx.ui.input("Account label", `${provider} — Work`))?.trim();
45
+ if (!label) return;
46
+
47
+ const suggested = commonUtil.slugify(label);
48
+ const id = (await ctx.ui.input("Account id", suggested))?.trim() || suggested;
49
+
50
+ const account: AccountConfig = { id, label, provider, piAuth: { provider, entry } };
51
+ const saved = await this.saveAccount(ctx, account);
52
+ if (!saved) return;
53
+
54
+ ctx.ui.notify(`Imported OAuth account "${saved.label}".`, "info");
55
+ } catch (error) {
56
+ ctx.ui.notify(`Failed to import OAuth account: ${errorUtil.format(error)}`, "error");
57
+ }
58
+ }
59
+ }