opencode-copilot-account-switcher 0.12.3 → 0.13.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.
@@ -3,52 +3,244 @@ import os from "node:os";
3
3
  import { promises as fs } from "node:fs";
4
4
  import { xdgConfig } from "xdg-basedir";
5
5
  const filename = "codex-store.json";
6
- function pickCodexStore(input) {
6
+ function asRecord(input) {
7
7
  if (!input || typeof input !== "object" || Array.isArray(input))
8
- return {};
9
- const source = input;
10
- const store = {};
11
- if (typeof source.activeProvider === "string")
12
- store.activeProvider = source.activeProvider;
13
- if (typeof source.activeAccountId === "string")
14
- store.activeAccountId = source.activeAccountId;
15
- if (typeof source.activeEmail === "string")
16
- store.activeEmail = source.activeEmail;
17
- if (typeof source.lastStatusRefresh === "number" && !Number.isNaN(source.lastStatusRefresh)) {
18
- store.lastStatusRefresh = source.lastStatusRefresh;
19
- }
20
- if (source.account && typeof source.account === "object" && !Array.isArray(source.account)) {
21
- const account = source.account;
22
- const next = {};
23
- if (typeof account.id === "string")
24
- next.id = account.id;
25
- if (typeof account.email === "string")
26
- next.email = account.email;
27
- if (typeof account.plan === "string")
28
- next.plan = account.plan;
29
- if (Object.keys(next).length > 0)
30
- store.account = next;
8
+ return undefined;
9
+ return input;
10
+ }
11
+ function pickString(input) {
12
+ return typeof input === "string" && input.length > 0 ? input : undefined;
13
+ }
14
+ function pickNumber(input) {
15
+ return typeof input === "number" && Number.isFinite(input) ? input : undefined;
16
+ }
17
+ function pickBoolean(input) {
18
+ return typeof input === "boolean" ? input : undefined;
19
+ }
20
+ function pickUsageWindow(input) {
21
+ const source = asRecord(input);
22
+ if (!source)
23
+ return undefined;
24
+ const next = {};
25
+ if (pickNumber(source.entitlement) !== undefined)
26
+ next.entitlement = pickNumber(source.entitlement);
27
+ if (pickNumber(source.remaining) !== undefined)
28
+ next.remaining = pickNumber(source.remaining);
29
+ if (pickNumber(source.used) !== undefined)
30
+ next.used = pickNumber(source.used);
31
+ if (pickNumber(source.resetAt) !== undefined)
32
+ next.resetAt = pickNumber(source.resetAt);
33
+ return Object.keys(next).length > 0 ? next : undefined;
34
+ }
35
+ function pickSnapshot(input) {
36
+ const source = asRecord(input);
37
+ if (!source)
38
+ return undefined;
39
+ const next = {};
40
+ if (pickString(source.plan))
41
+ next.plan = pickString(source.plan);
42
+ const usage5h = pickUsageWindow(source.usage5h);
43
+ if (usage5h)
44
+ next.usage5h = usage5h;
45
+ const usageWeek = pickUsageWindow(source.usageWeek);
46
+ if (usageWeek)
47
+ next.usageWeek = usageWeek;
48
+ if (pickNumber(source.updatedAt) !== undefined)
49
+ next.updatedAt = pickNumber(source.updatedAt);
50
+ if (pickString(source.error))
51
+ next.error = pickString(source.error);
52
+ return Object.keys(next).length > 0 ? next : undefined;
53
+ }
54
+ function pickEntry(input) {
55
+ const source = asRecord(input);
56
+ if (!source)
57
+ return undefined;
58
+ const next = {};
59
+ if (pickString(source.name))
60
+ next.name = pickString(source.name);
61
+ if (pickString(source.providerId))
62
+ next.providerId = pickString(source.providerId);
63
+ if (pickString(source.refresh))
64
+ next.refresh = pickString(source.refresh);
65
+ if (pickString(source.access))
66
+ next.access = pickString(source.access);
67
+ if (pickNumber(source.expires) !== undefined)
68
+ next.expires = pickNumber(source.expires);
69
+ if (pickString(source.accountId))
70
+ next.accountId = pickString(source.accountId);
71
+ if (pickString(source.email))
72
+ next.email = pickString(source.email);
73
+ if (pickNumber(source.addedAt) !== undefined)
74
+ next.addedAt = pickNumber(source.addedAt);
75
+ if (pickNumber(source.lastUsed) !== undefined)
76
+ next.lastUsed = pickNumber(source.lastUsed);
77
+ if (pickString(source.source))
78
+ next.source = pickString(source.source);
79
+ const snapshot = pickSnapshot(source.snapshot);
80
+ if (snapshot)
81
+ next.snapshot = snapshot;
82
+ return next;
83
+ }
84
+ function pickActiveAccountNames(input, accounts) {
85
+ if (!Array.isArray(input))
86
+ return undefined;
87
+ const seen = new Set();
88
+ const next = [];
89
+ for (const item of input) {
90
+ const name = pickString(item);
91
+ if (!name || seen.has(name) || !accounts[name])
92
+ continue;
93
+ seen.add(name);
94
+ next.push(name);
31
95
  }
32
- if (source.status && typeof source.status === "object" && !Array.isArray(source.status)) {
33
- const status = source.status;
34
- if (status.premium && typeof status.premium === "object" && !Array.isArray(status.premium)) {
35
- const premium = status.premium;
36
- const nextPremium = {};
37
- if (typeof premium.entitlement === "number" && !Number.isNaN(premium.entitlement)) {
38
- nextPremium.entitlement = premium.entitlement;
39
- }
40
- if (typeof premium.remaining === "number" && !Number.isNaN(premium.remaining)) {
41
- nextPremium.remaining = premium.remaining;
42
- }
43
- if (Object.keys(nextPremium).length > 0)
44
- store.status = { premium: nextPremium };
96
+ return next.length > 0 ? next : undefined;
97
+ }
98
+ function normalizeNewStore(source) {
99
+ const accounts = {};
100
+ const sourceAccounts = asRecord(source.accounts);
101
+ if (sourceAccounts) {
102
+ for (const [name, value] of Object.entries(sourceAccounts)) {
103
+ const entry = pickEntry(value);
104
+ if (!entry)
105
+ continue;
106
+ accounts[name] = {
107
+ ...entry,
108
+ ...(entry.name ? {} : { name }),
109
+ };
45
110
  }
46
111
  }
112
+ const store = { accounts };
113
+ const active = pickString(source.active);
114
+ if (active && accounts[active])
115
+ store.active = active;
116
+ const activeNames = pickActiveAccountNames(source.activeAccountNames, accounts);
117
+ if (activeNames)
118
+ store.activeAccountNames = activeNames;
119
+ if (pickBoolean(source.autoRefresh) !== undefined)
120
+ store.autoRefresh = pickBoolean(source.autoRefresh);
121
+ if (pickNumber(source.refreshMinutes) !== undefined)
122
+ store.refreshMinutes = pickNumber(source.refreshMinutes);
123
+ if (pickNumber(source.lastSnapshotRefresh) !== undefined)
124
+ store.lastSnapshotRefresh = pickNumber(source.lastSnapshotRefresh);
125
+ if (pickBoolean(source.bootstrapAuthImportTried) !== undefined) {
126
+ store.bootstrapAuthImportTried = pickBoolean(source.bootstrapAuthImportTried);
127
+ }
128
+ if (pickNumber(source.bootstrapAuthImportAt) !== undefined) {
129
+ store.bootstrapAuthImportAt = pickNumber(source.bootstrapAuthImportAt);
130
+ }
47
131
  return store;
48
132
  }
133
+ function normalizeLegacyStore(source) {
134
+ const legacyAccount = asRecord(source.account);
135
+ const legacyStatus = asRecord(source.status);
136
+ const legacyPremium = asRecord(legacyStatus?.premium);
137
+ const accountId = pickString(source.activeAccountId) ?? pickString(legacyAccount?.id);
138
+ const email = pickString(source.activeEmail) ?? pickString(legacyAccount?.email);
139
+ const plan = pickString(legacyAccount?.plan);
140
+ const entitlement = pickNumber(legacyPremium?.entitlement);
141
+ const remaining = pickNumber(legacyPremium?.remaining);
142
+ const updatedAt = pickNumber(source.lastStatusRefresh);
143
+ const hasLegacy = Boolean(accountId
144
+ || email
145
+ || plan
146
+ || entitlement !== undefined
147
+ || remaining !== undefined);
148
+ const store = {
149
+ accounts: {},
150
+ };
151
+ if (pickBoolean(source.bootstrapAuthImportTried) !== undefined) {
152
+ store.bootstrapAuthImportTried = pickBoolean(source.bootstrapAuthImportTried);
153
+ }
154
+ if (pickNumber(source.bootstrapAuthImportAt) !== undefined) {
155
+ store.bootstrapAuthImportAt = pickNumber(source.bootstrapAuthImportAt);
156
+ }
157
+ if (updatedAt !== undefined)
158
+ store.lastSnapshotRefresh = updatedAt;
159
+ if (!hasLegacy)
160
+ return store;
161
+ const name = accountId ?? email ?? "default";
162
+ const snapshot = {};
163
+ if (plan)
164
+ snapshot.plan = plan;
165
+ if (entitlement !== undefined || remaining !== undefined) {
166
+ snapshot.usage5h = {
167
+ ...(entitlement !== undefined ? { entitlement } : {}),
168
+ ...(remaining !== undefined ? { remaining } : {}),
169
+ };
170
+ }
171
+ if (updatedAt !== undefined)
172
+ snapshot.updatedAt = updatedAt;
173
+ store.accounts[name] = {
174
+ name,
175
+ providerId: "codex",
176
+ ...(accountId ? { accountId } : {}),
177
+ ...(email ? { email } : {}),
178
+ ...(Object.keys(snapshot).length > 0 ? { snapshot } : {}),
179
+ };
180
+ store.active = name;
181
+ return store;
182
+ }
183
+ function mergeLegacyIntoStore(store, source) {
184
+ const legacy = normalizeLegacyStore(source);
185
+ if (Object.keys(legacy.accounts).length === 0) {
186
+ return {
187
+ ...store,
188
+ lastSnapshotRefresh: store.lastSnapshotRefresh ?? legacy.lastSnapshotRefresh,
189
+ ...(legacy.bootstrapAuthImportTried !== undefined ? { bootstrapAuthImportTried: legacy.bootstrapAuthImportTried } : {}),
190
+ ...(legacy.bootstrapAuthImportAt !== undefined ? { bootstrapAuthImportAt: legacy.bootstrapAuthImportAt } : {}),
191
+ };
192
+ }
193
+ if (Object.keys(store.accounts).length > 0) {
194
+ return {
195
+ ...store,
196
+ lastSnapshotRefresh: store.lastSnapshotRefresh ?? legacy.lastSnapshotRefresh,
197
+ bootstrapAuthImportTried: store.bootstrapAuthImportTried ?? legacy.bootstrapAuthImportTried,
198
+ bootstrapAuthImportAt: store.bootstrapAuthImportAt ?? legacy.bootstrapAuthImportAt,
199
+ };
200
+ }
201
+ return {
202
+ ...legacy,
203
+ ...store,
204
+ accounts: {
205
+ ...legacy.accounts,
206
+ ...store.accounts,
207
+ },
208
+ active: store.active ?? legacy.active,
209
+ activeAccountNames: store.activeAccountNames ?? legacy.activeAccountNames,
210
+ autoRefresh: store.autoRefresh ?? legacy.autoRefresh,
211
+ refreshMinutes: store.refreshMinutes ?? legacy.refreshMinutes,
212
+ lastSnapshotRefresh: store.lastSnapshotRefresh ?? legacy.lastSnapshotRefresh,
213
+ bootstrapAuthImportTried: store.bootstrapAuthImportTried ?? legacy.bootstrapAuthImportTried,
214
+ bootstrapAuthImportAt: store.bootstrapAuthImportAt ?? legacy.bootstrapAuthImportAt,
215
+ };
216
+ }
217
+ export function normalizeCodexStore(input) {
218
+ const source = asRecord(input);
219
+ if (!source)
220
+ return { accounts: {} };
221
+ if (source.accounts && asRecord(source.accounts)) {
222
+ return mergeLegacyIntoStore(normalizeNewStore(source), source);
223
+ }
224
+ return normalizeLegacyStore(source);
225
+ }
49
226
  export function parseCodexStore(raw) {
50
227
  const parsed = raw ? JSON.parse(raw) : {};
51
- return pickCodexStore(parsed);
228
+ return normalizeCodexStore(parsed);
229
+ }
230
+ export function getActiveCodexAccount(store) {
231
+ if (store.active && store.accounts[store.active]) {
232
+ return {
233
+ name: store.active,
234
+ entry: store.accounts[store.active],
235
+ };
236
+ }
237
+ const first = Object.entries(store.accounts)[0];
238
+ if (!first)
239
+ return undefined;
240
+ return {
241
+ name: first[0],
242
+ entry: first[1],
243
+ };
52
244
  }
53
245
  export function codexStorePath() {
54
246
  const base = xdgConfig ?? path.join(os.homedir(), ".config");
@@ -64,7 +256,7 @@ export async function readCodexStore(filePath = codexStorePath()) {
64
256
  }
65
257
  export async function writeCodexStore(store, options) {
66
258
  const file = options?.filePath ?? codexStorePath();
67
- const next = pickCodexStore(store);
259
+ const next = normalizeCodexStore(store);
68
260
  await fs.mkdir(path.dirname(file), { recursive: true });
69
261
  await fs.writeFile(file, JSON.stringify(next, null, 2), { mode: 0o600 });
70
262
  }
@@ -0,0 +1,69 @@
1
+ type WriteMeta = {
2
+ reason: string;
3
+ source: string;
4
+ actionType?: string;
5
+ };
6
+ export type MenuAccountInfo = {
7
+ id?: string;
8
+ name: string;
9
+ index: number;
10
+ isCurrent?: boolean;
11
+ };
12
+ export type MenuActionAccount = {
13
+ id?: string;
14
+ name: string;
15
+ };
16
+ type SharedActionResult = boolean | {
17
+ changed: boolean;
18
+ persistHandled?: boolean;
19
+ };
20
+ export type MenuAction = {
21
+ type: "add";
22
+ } | {
23
+ type: "cancel";
24
+ } | {
25
+ type: "remove";
26
+ account: MenuActionAccount;
27
+ } | {
28
+ type: "remove-all";
29
+ } | {
30
+ type: "switch";
31
+ account: MenuActionAccount;
32
+ } | {
33
+ type: "provider";
34
+ name: string;
35
+ payload?: unknown;
36
+ };
37
+ export type ProviderMenuAdapter<TStore, TEntry> = {
38
+ key: string;
39
+ loadStore: () => Promise<TStore>;
40
+ writeStore: (store: TStore, meta: WriteMeta) => Promise<void>;
41
+ bootstrapAuthImport: (store: TStore) => Promise<boolean>;
42
+ authorizeNewAccount: (store: TStore) => Promise<TEntry | undefined>;
43
+ refreshSnapshots: (store: TStore) => Promise<void>;
44
+ toMenuInfo: (store: TStore) => Promise<MenuAccountInfo[]>;
45
+ getCurrentEntry: (store: TStore) => TEntry | undefined;
46
+ getRefreshConfig: (store: TStore) => {
47
+ enabled: boolean;
48
+ minutes: number;
49
+ };
50
+ getAccountByName: (store: TStore, name: string) => {
51
+ name: string;
52
+ entry: TEntry;
53
+ } | undefined;
54
+ addAccount?: (store: TStore, entry: TEntry) => Promise<SharedActionResult> | SharedActionResult;
55
+ removeAccount?: (store: TStore, name: string) => Promise<SharedActionResult> | SharedActionResult;
56
+ removeAllAccounts?: (store: TStore) => Promise<SharedActionResult> | SharedActionResult;
57
+ switchAccount: (store: TStore, name: string, entry: TEntry) => Promise<{
58
+ persistHandled?: boolean;
59
+ } | void>;
60
+ applyAction?: (store: TStore, action: Extract<MenuAction, {
61
+ type: "provider";
62
+ }>) => Promise<boolean>;
63
+ };
64
+ export declare function runProviderMenu<TStore, TEntry>(input: {
65
+ adapter: ProviderMenuAdapter<TStore, TEntry>;
66
+ showMenu: (accounts: MenuAccountInfo[], store: TStore) => Promise<MenuAction>;
67
+ now?: () => number;
68
+ }): Promise<TEntry | undefined>;
69
+ export {};
@@ -0,0 +1,108 @@
1
+ function parseSharedActionResult(result) {
2
+ if (typeof result === "object" && result) {
3
+ return {
4
+ changed: result.changed === true,
5
+ persistHandled: result.persistHandled === true,
6
+ };
7
+ }
8
+ return {
9
+ changed: result === true,
10
+ persistHandled: false,
11
+ };
12
+ }
13
+ export async function runProviderMenu(input) {
14
+ const now = input.now ?? Date.now;
15
+ const store = await input.adapter.loadStore();
16
+ if (await input.adapter.bootstrapAuthImport(store)) {
17
+ await input.adapter.writeStore(store, {
18
+ reason: "bootstrap-auth-import",
19
+ source: "menu-runtime",
20
+ actionType: "bootstrap-auth-import",
21
+ });
22
+ }
23
+ let nextRefreshAt = 0;
24
+ while (true) {
25
+ const refresh = input.adapter.getRefreshConfig(store);
26
+ if (refresh.enabled && now() >= nextRefreshAt) {
27
+ await input.adapter.refreshSnapshots(store);
28
+ await input.adapter.writeStore(store, {
29
+ reason: "auto-refresh",
30
+ source: "menu-runtime",
31
+ actionType: "auto-refresh",
32
+ });
33
+ nextRefreshAt = now() + refresh.minutes * 60_000;
34
+ }
35
+ const accounts = await input.adapter.toMenuInfo(store);
36
+ const action = await input.showMenu(accounts, store);
37
+ if (action.type === "cancel")
38
+ return input.adapter.getCurrentEntry(store);
39
+ if (action.type === "add") {
40
+ const entry = await input.adapter.authorizeNewAccount(store);
41
+ const result = !entry ? undefined : await input.adapter.addAccount?.(store, entry);
42
+ const parsed = parseSharedActionResult(result);
43
+ if (!entry || !parsed.changed)
44
+ continue;
45
+ if (!parsed.persistHandled) {
46
+ await input.adapter.writeStore(store, {
47
+ reason: "add-account",
48
+ source: "menu-runtime",
49
+ actionType: "add",
50
+ });
51
+ }
52
+ continue;
53
+ }
54
+ if (action.type === "remove-all") {
55
+ const result = await input.adapter.removeAllAccounts?.(store);
56
+ const parsed = parseSharedActionResult(result);
57
+ if (!parsed.changed)
58
+ continue;
59
+ if (!parsed.persistHandled) {
60
+ await input.adapter.writeStore(store, {
61
+ reason: "remove-all",
62
+ source: "menu-runtime",
63
+ actionType: "remove-all",
64
+ });
65
+ }
66
+ continue;
67
+ }
68
+ if (action.type === "remove") {
69
+ const accountName = action.account.id ?? action.account.name;
70
+ const result = await input.adapter.removeAccount?.(store, accountName);
71
+ const parsed = parseSharedActionResult(result);
72
+ if (!parsed.changed)
73
+ continue;
74
+ if (!parsed.persistHandled) {
75
+ await input.adapter.writeStore(store, {
76
+ reason: "remove-account",
77
+ source: "menu-runtime",
78
+ actionType: "remove",
79
+ });
80
+ }
81
+ continue;
82
+ }
83
+ if (action.type === "switch") {
84
+ const accountName = action.account.id ?? action.account.name;
85
+ const selected = input.adapter.getAccountByName(store, accountName);
86
+ if (!selected)
87
+ continue;
88
+ const switchResult = await input.adapter.switchAccount(store, selected.name, selected.entry);
89
+ if (!switchResult?.persistHandled) {
90
+ await input.adapter.writeStore(store, {
91
+ reason: "persist-account-switch",
92
+ source: "menu-runtime",
93
+ actionType: "switch",
94
+ });
95
+ }
96
+ continue;
97
+ }
98
+ if (!input.adapter.applyAction)
99
+ continue;
100
+ if (!await input.adapter.applyAction(store, action))
101
+ continue;
102
+ await input.adapter.writeStore(store, {
103
+ reason: `provider-action:${action.name}`,
104
+ source: "menu-runtime",
105
+ actionType: action.name,
106
+ });
107
+ }
108
+ }