@hieplp/pi-account-switcher 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,6 +85,8 @@ After installing, reload Pi and add your first account:
85
85
  | Command | Description |
86
86
  |---|---|
87
87
  | `/system:reset` | Delete all accounts, providers, and state |
88
+ | `/system:export` | Export all accounts, providers, and state to a JSON file |
89
+ | `/system:import` | Import accounts, providers, and state from a JSON file |
88
90
 
89
91
  ---
90
92
 
@@ -157,6 +159,26 @@ Custom providers are stored at `~/.pi/account-switcher/providers.json` and suppo
157
159
 
158
160
  ---
159
161
 
162
+ ## Export / Import
163
+
164
+ Back up or migrate your full configuration with two commands:
165
+
166
+ ```
167
+ /system:export # prompts for a path, defaults to ~/pi-account-switcher-export.json
168
+ /system:export ~/backup.json # export to a specific path
169
+ ```
170
+
171
+ 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):
172
+
173
+ ```
174
+ /system:import # prompts for a path, defaults to ~/pi-account-switcher-export.json
175
+ /system:import ~/backup.json # import from a specific path
176
+ ```
177
+
178
+ > **Warning:** import replaces all existing data. A confirmation prompt is shown before anything is written.
179
+
180
+ ---
181
+
160
182
  ## Config Reference
161
183
 
162
184
  ### 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.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Pi extension for quickly switching between multiple accounts/API keys per provider.",
6
6
  "license": "MIT",
@@ -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 {
@@ -38,7 +38,7 @@ export abstract class BaseCommand implements Command {
38
38
  register(): void {
39
39
  this.pi.registerCommand(this.name, {
40
40
  description: this.description,
41
- handler: (_, ctx) => this.handler(ctx),
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
  }
@@ -0,0 +1,52 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import type { AccountSwitcher } from "@/runtime";
4
+ import { COMMANDS, DEFAULT_EXPORT_PATH, STATE_PATH } from "@/constants";
5
+ import type { AccountSwitcherContext } from "@/types";
6
+ import { BaseCommand } from "../base";
7
+ import { errorUtil, fileUtil } from "@/utils";
8
+
9
+
10
+ export const useExportCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
11
+ new ExportCommand(pi, runtime).register();
12
+ };
13
+
14
+ class ExportCommand extends BaseCommand {
15
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
16
+ super(pi, runtime, COMMANDS.system.export);
17
+ }
18
+
19
+ async handler(ctx: AccountSwitcherContext, args?: string): Promise<void> {
20
+ try {
21
+ const target = args?.trim() || (await ctx.ui.input("Export file (blank for default)", DEFAULT_EXPORT_PATH));
22
+ if (target === undefined) {
23
+ ctx.ui.notify("Export cancelled.", "info");
24
+ return;
25
+ }
26
+
27
+ const state = await loadState();
28
+ const exportData = {
29
+ version: 1,
30
+ exportedAt: new Date().toISOString(),
31
+ accounts: this.runtime.getAccounts(),
32
+ providers: this.runtime.getProviders(),
33
+ state,
34
+ };
35
+
36
+ const path = fileUtil.expandHome(target.trim() || DEFAULT_EXPORT_PATH);
37
+ await fileUtil.writePrivateJson(path, exportData);
38
+ ctx.ui.notify(`Exported account switcher data to ${path}.`, "info");
39
+ } catch (e) {
40
+ ctx.ui.notify(`Failed to export: ${errorUtil.format(e)}`, "error");
41
+ }
42
+ }
43
+ }
44
+
45
+ async function loadState(): Promise<unknown> {
46
+ try {
47
+ return JSON.parse(await readFile(STATE_PATH, "utf8"));
48
+ } catch (error) {
49
+ if (fileUtil.isMissingFileError(error)) return {};
50
+ throw error;
51
+ }
52
+ }
@@ -0,0 +1,96 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import z from "zod";
3
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
+ import type { AccountSwitcher } from "@/runtime";
5
+ import { ACCOUNTS_PATH, COMMANDS, DEFAULT_EXPORT_PATH, PROVIDERS_PATH, STATE_PATH } from "@/constants";
6
+ import { accountSchema, providerSchema } from "@/schemas";
7
+ import type { AccountConfig, AccountSwitcherContext, ProviderConfig } from "@/types";
8
+ import { BaseCommand } from "../base";
9
+ import { errorUtil, fileUtil, uiUtil } from "@/utils";
10
+
11
+ const importStateSchema = z
12
+ .object({
13
+ activeAccountId: z.string().optional(),
14
+ activeModelId: z.string().optional(),
15
+ activeModelProvider: z.string().optional(),
16
+ })
17
+ .default({});
18
+
19
+ const exportBundleSchema = z.object({
20
+ version: z.number().optional(),
21
+ accounts: z.array(accountSchema).default([]),
22
+ providers: z.array(providerSchema.extend({ id: z.string().min(1) })).default([]),
23
+ state: importStateSchema,
24
+ });
25
+
26
+ type ImportBundle = {
27
+ accounts: AccountConfig[];
28
+ providers: ProviderConfig[];
29
+ state: z.infer<typeof importStateSchema>;
30
+ };
31
+
32
+ export const useImportCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
33
+ new ImportCommand(pi, runtime).register();
34
+ };
35
+
36
+ class ImportCommand extends BaseCommand {
37
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
38
+ super(pi, runtime, COMMANDS.system.import);
39
+ }
40
+
41
+ async handler(ctx: AccountSwitcherContext, args?: string): Promise<void> {
42
+ try {
43
+ const source = args?.trim() || (await ctx.ui.input("Import file (blank for default)", DEFAULT_EXPORT_PATH));
44
+ if (source === undefined) {
45
+ ctx.ui.notify("Import cancelled.", "info");
46
+ return;
47
+ }
48
+
49
+ const path = fileUtil.expandHome(source.trim() || DEFAULT_EXPORT_PATH);
50
+ const bundle = parseImportBundle(JSON.parse(await readFile(path, "utf8")));
51
+
52
+ const confirmed = await ctx.ui.confirm(
53
+ "Import account switcher data?",
54
+ `This will replace all existing accounts, providers, and state with data from ${path}.`,
55
+ );
56
+ if (!confirmed) {
57
+ ctx.ui.notify("Import cancelled.", "info");
58
+ return;
59
+ }
60
+
61
+ await fileUtil.writePrivateJson(ACCOUNTS_PATH, { accounts: bundle.accounts });
62
+ await fileUtil.writePrivateJson(PROVIDERS_PATH, { providers: bundle.providers });
63
+ await fileUtil.writePrivateJson(STATE_PATH, bundle.state);
64
+
65
+ await this.runtime.load();
66
+ await this.runtime.init(ctx);
67
+ uiUtil.setAccountStatus(ctx.ui, this.runtime.getActiveAccount()?.label);
68
+
69
+ ctx.ui.notify(
70
+ `Imported ${bundle.accounts.length} accounts and ${bundle.providers.length} providers from ${path}.`,
71
+ "info",
72
+ );
73
+ } catch (e) {
74
+ ctx.ui.notify(`Failed to import: ${errorUtil.format(e)}`, "error");
75
+ }
76
+ }
77
+ }
78
+
79
+ function parseImportBundle(raw: unknown): ImportBundle {
80
+ const parsed = exportBundleSchema.parse(raw);
81
+ assertNoDuplicateIds("account", parsed.accounts.map((account) => account.id));
82
+ assertNoDuplicateIds("provider", parsed.providers.map((provider) => provider.id));
83
+ return parsed;
84
+ }
85
+
86
+ function assertNoDuplicateIds(kind: string, ids: string[]): void {
87
+ const seen = new Set<string>();
88
+ const duplicates = new Set<string>();
89
+ for (const id of ids) {
90
+ if (seen.has(id)) duplicates.add(id);
91
+ else seen.add(id);
92
+ }
93
+ if (duplicates.size > 0) {
94
+ throw new Error(`Duplicate ${kind} ids: ${Array.from(duplicates).sort().join(", ")}`);
95
+ }
96
+ }
@@ -1,7 +1,11 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
+ import { useExportCommand } from "./export";
4
+ import { useImportCommand } from "./import";
3
5
  import { useResetCommand } from "./reset";
4
6
 
5
7
  export default function useSystemCommands(pi: ExtensionAPI, runtime: AccountSwitcher) {
6
8
  useResetCommand(pi, runtime);
9
+ useExportCommand(pi, runtime);
10
+ useImportCommand(pi, runtime);
7
11
  }
@@ -62,5 +62,13 @@ export const COMMANDS = {
62
62
  name: "system:reset",
63
63
  description: "Reset all extension data (accounts, providers, state) to factory defaults",
64
64
  },
65
+ export: {
66
+ name: "system:export",
67
+ description: "Export all extension data (accounts, providers, state) to a JSON file",
68
+ },
69
+ import: {
70
+ name: "system:import",
71
+ description: "Import extension data (accounts, providers, state) from a JSON file",
72
+ },
65
73
  },
66
74
  } as const;
@@ -5,4 +5,5 @@ export const APP_DIR = join(homedir(), ".pi", "account-switcher");
5
5
  export const ACCOUNTS_PATH = join(APP_DIR, "accounts.json");
6
6
  export const PROVIDERS_PATH = join(APP_DIR, "providers.json");
7
7
  export const STATE_PATH = join(APP_DIR, "state.json");
8
- export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
8
+ export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
9
+ export const DEFAULT_EXPORT_PATH = "~/pi-account-switcher-export.json";