@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9

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 (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
@@ -2,36 +2,37 @@
2
2
  // Slash-command OAuth flows (login / reauth)
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
+ import { resolveIdentityFromOAuthExchange } from "../account-identity.js";
5
6
  import { authorize, exchange } from "../oauth.js";
6
7
  import { createDefaultStats, loadAccounts, saveAccounts } from "../storage.js";
7
8
 
8
9
  export const PENDING_OAUTH_TTL_MS = 10 * 60 * 1000;
9
10
 
10
11
  export interface PendingOAuthEntry {
11
- mode: "login" | "reauth";
12
- verifier: string;
13
- state?: string;
14
- targetIndex?: number;
15
- createdAt: number;
12
+ mode: "login" | "reauth";
13
+ verifier: string;
14
+ state?: string;
15
+ targetIndex?: number;
16
+ createdAt: number;
16
17
  }
17
18
 
18
19
  export interface OAuthFlowDeps {
19
- pendingSlashOAuth: Map<string, PendingOAuthEntry>;
20
- sendCommandMessage: (sessionID: string, message: string) => Promise<void>;
21
- reloadAccountManagerFromDisk: () => Promise<void>;
22
- persistOpenCodeAuth: (refresh: string, access: string | undefined, expires: number | undefined) => Promise<void>;
20
+ pendingSlashOAuth: Map<string, PendingOAuthEntry>;
21
+ sendCommandMessage: (sessionID: string, message: string) => Promise<void>;
22
+ reloadAccountManagerFromDisk: () => Promise<void>;
23
+ persistOpenCodeAuth: (refresh: string, access: string | undefined, expires: number | undefined) => Promise<void>;
23
24
  }
24
25
 
25
26
  /**
26
27
  * Remove expired pending OAuth entries from the map.
27
28
  */
28
29
  export function pruneExpiredPendingOAuth(pendingSlashOAuth: Map<string, PendingOAuthEntry>): void {
29
- const now = Date.now();
30
- for (const [key, entry] of pendingSlashOAuth) {
31
- if (now - entry.createdAt > PENDING_OAUTH_TTL_MS) {
32
- pendingSlashOAuth.delete(key);
30
+ const now = Date.now();
31
+ for (const [key, entry] of pendingSlashOAuth) {
32
+ if (now - entry.createdAt > PENDING_OAUTH_TTL_MS) {
33
+ pendingSlashOAuth.delete(key);
34
+ }
33
35
  }
34
- }
35
36
  }
36
37
 
37
38
  /**
@@ -39,178 +40,192 @@ export function pruneExpiredPendingOAuth(pendingSlashOAuth: Map<string, PendingO
39
40
  * Sends the authorization URL to the user via sendCommandMessage.
40
41
  */
41
42
  export async function startSlashOAuth(
42
- sessionID: string,
43
- mode: "login" | "reauth",
44
- targetIndex: number | undefined,
45
- deps: OAuthFlowDeps,
43
+ sessionID: string,
44
+ mode: "login" | "reauth",
45
+ targetIndex: number | undefined,
46
+ deps: OAuthFlowDeps,
46
47
  ): Promise<void> {
47
- const { pendingSlashOAuth, sendCommandMessage } = deps;
48
- pruneExpiredPendingOAuth(pendingSlashOAuth);
49
- const { url, verifier, state } = await authorize("max");
50
- pendingSlashOAuth.set(sessionID, {
51
- mode,
52
- verifier,
53
- state,
54
- targetIndex,
55
- createdAt: Date.now(),
56
- });
57
-
58
- const action = mode === "login" ? "login" : `reauth ${(targetIndex ?? 0) + 1}`;
59
- const followup =
60
- mode === "login" ? "/anthropic login complete <code#state>" : "/anthropic reauth complete <code#state>";
61
-
62
- await sendCommandMessage(
63
- sessionID,
64
- [
65
- "▣ Anthropic OAuth",
66
- "",
67
- `Started ${action} flow.`,
68
- "Open this URL in your browser:",
69
- url,
70
- "",
71
- `Then run: ${followup}`,
72
- "(Paste the full authorization code, including #state)",
73
- ].join("\n"),
74
- );
48
+ const { pendingSlashOAuth, sendCommandMessage } = deps;
49
+ pruneExpiredPendingOAuth(pendingSlashOAuth);
50
+ const { url, verifier, state } = await authorize("max");
51
+ pendingSlashOAuth.set(sessionID, {
52
+ mode,
53
+ verifier,
54
+ state,
55
+ targetIndex,
56
+ createdAt: Date.now(),
57
+ });
58
+
59
+ const action = mode === "login" ? "login" : `reauth ${(targetIndex ?? 0) + 1}`;
60
+ const followup =
61
+ mode === "login" ? "/anthropic login complete <code#state>" : "/anthropic reauth complete <code#state>";
62
+
63
+ await sendCommandMessage(
64
+ sessionID,
65
+ [
66
+ "▣ Anthropic OAuth",
67
+ "",
68
+ `Started ${action} flow.`,
69
+ "Open this URL in your browser:",
70
+ url,
71
+ "",
72
+ `Then run: ${followup}`,
73
+ "(Paste the full authorization code, including #state)",
74
+ ].join("\n"),
75
+ );
75
76
  }
76
77
 
77
78
  /**
78
79
  * Complete a pending slash-command OAuth flow.
79
80
  */
80
81
  export async function completeSlashOAuth(
81
- sessionID: string,
82
- code: string,
83
- deps: OAuthFlowDeps,
82
+ sessionID: string,
83
+ code: string,
84
+ deps: OAuthFlowDeps,
84
85
  ): Promise<{ ok: boolean; message: string }> {
85
- const { pendingSlashOAuth, reloadAccountManagerFromDisk, persistOpenCodeAuth } = deps;
86
+ const { pendingSlashOAuth, reloadAccountManagerFromDisk, persistOpenCodeAuth } = deps;
87
+
88
+ const pending = pendingSlashOAuth.get(sessionID);
89
+ if (!pending) {
90
+ pruneExpiredPendingOAuth(pendingSlashOAuth);
91
+ return {
92
+ ok: false,
93
+ message: "No pending OAuth flow. Start with /anthropic login or /anthropic reauth <N>.",
94
+ };
95
+ }
86
96
 
87
- const pending = pendingSlashOAuth.get(sessionID);
88
- if (!pending) {
89
- pruneExpiredPendingOAuth(pendingSlashOAuth);
90
- return {
91
- ok: false,
92
- message: "No pending OAuth flow. Start with /anthropic login or /anthropic reauth <N>.",
93
- };
94
- }
97
+ if (Date.now() - pending.createdAt > PENDING_OAUTH_TTL_MS) {
98
+ pendingSlashOAuth.delete(sessionID);
99
+ return {
100
+ ok: false,
101
+ message: "Pending OAuth flow expired. Start again with /anthropic login or /anthropic reauth <N>.",
102
+ };
103
+ }
95
104
 
96
- if (Date.now() - pending.createdAt > PENDING_OAUTH_TTL_MS) {
97
- pendingSlashOAuth.delete(sessionID);
98
- return {
99
- ok: false,
100
- message: "Pending OAuth flow expired. Start again with /anthropic login or /anthropic reauth <N>.",
101
- };
102
- }
105
+ // Validate state parameter to prevent CSRF attacks
106
+ const splits = code.split("#");
107
+ if (pending.state && splits[1] && splits[1] !== pending.state) {
108
+ pendingSlashOAuth.delete(sessionID);
109
+ return {
110
+ ok: false,
111
+ message: "OAuth state mismatch — possible CSRF attack. Please try again.",
112
+ };
113
+ }
103
114
 
104
- // Validate state parameter to prevent CSRF attacks
105
- const splits = code.split("#");
106
- if (pending.state && splits[1] && splits[1] !== pending.state) {
107
- pendingSlashOAuth.delete(sessionID);
108
- return {
109
- ok: false,
110
- message: "OAuth state mismatch possible CSRF attack. Please try again.",
111
- };
112
- }
115
+ const credentials = await exchange(code, pending.verifier);
116
+ if (credentials.type === "failed") {
117
+ return {
118
+ ok: false,
119
+ message: credentials.details
120
+ ? `Token exchange failed (${credentials.details}).`
121
+ : "Token exchange failed. The code may be invalid or expired.",
122
+ };
123
+ }
113
124
 
114
- const credentials = await exchange(code, pending.verifier);
115
- if (credentials.type === "failed") {
116
- return {
117
- ok: false,
118
- message: credentials.details
119
- ? `Token exchange failed (${credentials.details}).`
120
- : "Token exchange failed. The code may be invalid or expired.",
125
+ const stored = (await loadAccounts()) ?? {
126
+ version: 1,
127
+ accounts: [],
128
+ activeIndex: 0,
121
129
  };
122
- }
123
-
124
- const stored = (await loadAccounts()) ?? {
125
- version: 1,
126
- accounts: [],
127
- activeIndex: 0,
128
- };
129
-
130
- if (pending.mode === "login") {
131
- const existingIdx = stored.accounts.findIndex((acc) => acc.refreshToken === credentials.refresh);
132
- if (existingIdx >= 0) {
133
- const acc = stored.accounts[existingIdx];
134
- acc.access = credentials.access;
135
- acc.expires = credentials.expires;
136
- if (credentials.email) acc.email = credentials.email;
137
- acc.enabled = true;
138
- acc.consecutiveFailures = 0;
139
- acc.lastFailureTime = null;
140
- acc.rateLimitResetTimes = {};
141
- await saveAccounts(stored);
142
- await persistOpenCodeAuth(acc.refreshToken, acc.access, acc.expires);
143
- await reloadAccountManagerFromDisk();
144
- pendingSlashOAuth.delete(sessionID);
145
- const name = acc.email || `Account ${existingIdx + 1}`;
146
- return {
147
- ok: true,
148
- message: `Updated existing account #${existingIdx + 1} (${name}).`,
149
- };
130
+
131
+ if (pending.mode === "login") {
132
+ const existingIdx = stored.accounts.findIndex((acc) => acc.refreshToken === credentials.refresh);
133
+ if (existingIdx >= 0) {
134
+ const acc = stored.accounts[existingIdx];
135
+ const accIsCC = acc.source === "cc-keychain" || acc.source === "cc-file";
136
+ acc.access = credentials.access;
137
+ acc.expires = credentials.expires;
138
+ acc.token_updated_at = Date.now();
139
+ acc.enabled = true;
140
+ acc.consecutiveFailures = 0;
141
+ acc.lastFailureTime = null;
142
+ acc.rateLimitResetTimes = {};
143
+ if (!accIsCC) {
144
+ if (credentials.email) acc.email = credentials.email;
145
+ acc.identity = resolveIdentityFromOAuthExchange(credentials);
146
+ acc.source = acc.source ?? "oauth";
147
+ }
148
+ await saveAccounts(stored);
149
+ await persistOpenCodeAuth(acc.refreshToken, acc.access, acc.expires);
150
+ await reloadAccountManagerFromDisk();
151
+ pendingSlashOAuth.delete(sessionID);
152
+ const name = acc.email || `Account ${existingIdx + 1}`;
153
+ return {
154
+ ok: true,
155
+ message: `Updated existing account #${existingIdx + 1} (${name}).`,
156
+ };
157
+ }
158
+
159
+ if (stored.accounts.length >= 10) {
160
+ return {
161
+ ok: false,
162
+ message: "Maximum of 10 accounts reached. Remove one first.",
163
+ };
164
+ }
165
+
166
+ const now = Date.now();
167
+ stored.accounts.push({
168
+ id: `${now}:${credentials.refresh.slice(0, 12)}`,
169
+ email: credentials.email,
170
+ identity: resolveIdentityFromOAuthExchange(credentials),
171
+ refreshToken: credentials.refresh,
172
+ access: credentials.access,
173
+ expires: credentials.expires,
174
+ token_updated_at: now,
175
+ addedAt: now,
176
+ lastUsed: 0,
177
+ enabled: true,
178
+ rateLimitResetTimes: {},
179
+ consecutiveFailures: 0,
180
+ lastFailureTime: null,
181
+ stats: createDefaultStats(now),
182
+ source: "oauth",
183
+ });
184
+ await saveAccounts(stored);
185
+ const newAccount = stored.accounts[stored.accounts.length - 1];
186
+ await persistOpenCodeAuth(newAccount.refreshToken, newAccount.access, newAccount.expires);
187
+ await reloadAccountManagerFromDisk();
188
+ pendingSlashOAuth.delete(sessionID);
189
+ const label = credentials.email || `Account ${stored.accounts.length}`;
190
+ return {
191
+ ok: true,
192
+ message: `Added account #${stored.accounts.length} (${label}).`,
193
+ };
150
194
  }
151
195
 
152
- if (stored.accounts.length >= 10) {
153
- return {
154
- ok: false,
155
- message: "Maximum of 10 accounts reached. Remove one first.",
156
- };
196
+ // reauth flow
197
+ const idx = pending.targetIndex ?? -1;
198
+ if (idx < 0 || idx >= stored.accounts.length) {
199
+ pendingSlashOAuth.delete(sessionID);
200
+ return {
201
+ ok: false,
202
+ message: "Target account no longer exists. Start reauth again.",
203
+ };
204
+ }
205
+
206
+ const existing = stored.accounts[idx];
207
+ const existingIsCC = existing.source === "cc-keychain" || existing.source === "cc-file";
208
+ existing.refreshToken = credentials.refresh;
209
+ existing.access = credentials.access;
210
+ existing.expires = credentials.expires;
211
+ existing.token_updated_at = Date.now();
212
+ existing.enabled = true;
213
+ existing.consecutiveFailures = 0;
214
+ existing.lastFailureTime = null;
215
+ existing.rateLimitResetTimes = {};
216
+ if (!existingIsCC) {
217
+ if (credentials.email) existing.email = credentials.email;
218
+ existing.identity = resolveIdentityFromOAuthExchange(credentials);
219
+ existing.source = existing.source ?? "oauth";
157
220
  }
158
221
 
159
- const now = Date.now();
160
- stored.accounts.push({
161
- id: `${now}:${credentials.refresh.slice(0, 12)}`,
162
- email: credentials.email,
163
- refreshToken: credentials.refresh,
164
- access: credentials.access,
165
- expires: credentials.expires,
166
- token_updated_at: now,
167
- addedAt: now,
168
- lastUsed: 0,
169
- enabled: true,
170
- rateLimitResetTimes: {},
171
- consecutiveFailures: 0,
172
- lastFailureTime: null,
173
- stats: createDefaultStats(now),
174
- });
175
222
  await saveAccounts(stored);
176
- const newAccount = stored.accounts[stored.accounts.length - 1];
177
- await persistOpenCodeAuth(newAccount.refreshToken, newAccount.access, newAccount.expires);
223
+ await persistOpenCodeAuth(existing.refreshToken, existing.access, existing.expires);
178
224
  await reloadAccountManagerFromDisk();
179
225
  pendingSlashOAuth.delete(sessionID);
180
- const label = credentials.email || `Account ${stored.accounts.length}`;
226
+ const name = existing.email || `Account ${idx + 1}`;
181
227
  return {
182
- ok: true,
183
- message: `Added account #${stored.accounts.length} (${label}).`,
184
- };
185
- }
186
-
187
- // reauth flow
188
- const idx = pending.targetIndex ?? -1;
189
- if (idx < 0 || idx >= stored.accounts.length) {
190
- pendingSlashOAuth.delete(sessionID);
191
- return {
192
- ok: false,
193
- message: "Target account no longer exists. Start reauth again.",
228
+ ok: true,
229
+ message: `Re-authenticated account #${idx + 1} (${name}).`,
194
230
  };
195
- }
196
-
197
- const existing = stored.accounts[idx];
198
- existing.refreshToken = credentials.refresh;
199
- existing.access = credentials.access;
200
- existing.expires = credentials.expires;
201
- if (credentials.email) existing.email = credentials.email;
202
- existing.enabled = true;
203
- existing.consecutiveFailures = 0;
204
- existing.lastFailureTime = null;
205
- existing.rateLimitResetTimes = {};
206
-
207
- await saveAccounts(stored);
208
- await persistOpenCodeAuth(existing.refreshToken, existing.access, existing.expires);
209
- await reloadAccountManagerFromDisk();
210
- pendingSlashOAuth.delete(sessionID);
211
- const name = existing.email || `Account ${idx + 1}`;
212
- return {
213
- ok: true,
214
- message: `Re-authenticated account #${idx + 1} (${name}).`,
215
- };
216
231
  }
@@ -10,82 +10,82 @@ import type { AccountManager } from "../accounts.js";
10
10
  * Show the main account menu and return the user's choice.
11
11
  */
12
12
  export async function promptAccountMenu(
13
- accountManager: AccountManager,
13
+ accountManager: AccountManager,
14
14
  ): Promise<"add" | "fresh" | "manage" | "cancel"> {
15
- const accounts = accountManager.getAccountsSnapshot();
16
- const currentIndex = accountManager.getCurrentIndex();
17
- const rl = createInterface({ input: stdin, output: stdout });
15
+ const accounts = accountManager.getAccountsSnapshot();
16
+ const currentIndex = accountManager.getCurrentIndex();
17
+ const rl = createInterface({ input: stdin, output: stdout });
18
18
 
19
- try {
20
- console.log(`\n${accounts.length} account(s) configured:`);
21
- for (const acc of accounts) {
22
- const name = acc.email || `Account ${acc.index + 1}`;
23
- const active = acc.index === currentIndex ? " (active)" : "";
24
- const disabled = !acc.enabled ? " [disabled]" : "";
25
- console.log(` ${acc.index + 1}. ${name}${active}${disabled}`);
26
- }
27
- console.log("");
19
+ try {
20
+ console.log(`\n${accounts.length} account(s) configured:`);
21
+ for (const acc of accounts) {
22
+ const name = acc.email || `Account ${acc.index + 1}`;
23
+ const active = acc.index === currentIndex ? " (active)" : "";
24
+ const disabled = !acc.enabled ? " [disabled]" : "";
25
+ console.log(` ${acc.index + 1}. ${name}${active}${disabled}`);
26
+ }
27
+ console.log("");
28
28
 
29
- while (true) {
30
- const answer = await rl.question("(a)dd new, (f)resh start, (m)anage, (c)ancel? [a/f/m/c]: ");
31
- const normalized = answer.trim().toLowerCase();
32
- if (normalized === "a" || normalized === "add") return "add";
33
- if (normalized === "f" || normalized === "fresh") return "fresh";
34
- if (normalized === "m" || normalized === "manage") return "manage";
35
- if (normalized === "c" || normalized === "cancel") return "cancel";
36
- console.log("Please enter 'a', 'f', 'm', or 'c'.");
29
+ while (true) {
30
+ const answer = await rl.question("(a)dd new, (f)resh start, (m)anage, (c)ancel? [a/f/m/c]: ");
31
+ const normalized = answer.trim().toLowerCase();
32
+ if (normalized === "a" || normalized === "add") return "add";
33
+ if (normalized === "f" || normalized === "fresh") return "fresh";
34
+ if (normalized === "m" || normalized === "manage") return "manage";
35
+ if (normalized === "c" || normalized === "cancel") return "cancel";
36
+ console.log("Please enter 'a', 'f', 'm', or 'c'.");
37
+ }
38
+ } finally {
39
+ rl.close();
37
40
  }
38
- } finally {
39
- rl.close();
40
- }
41
41
  }
42
42
 
43
43
  /**
44
44
  * Show the manage-accounts sub-menu (toggle/delete accounts).
45
45
  */
46
46
  export async function promptManageAccounts(accountManager: AccountManager): Promise<void> {
47
- const accounts = accountManager.getAccountsSnapshot();
48
- const rl = createInterface({ input: stdin, output: stdout });
47
+ const accounts = accountManager.getAccountsSnapshot();
48
+ const rl = createInterface({ input: stdin, output: stdout });
49
49
 
50
- try {
51
- console.log("\nManage accounts:");
52
- for (const acc of accounts) {
53
- const name = acc.email || `Account ${acc.index + 1}`;
54
- const status = acc.enabled ? "enabled" : "disabled";
55
- console.log(` ${acc.index + 1}. ${name} [${status}]`);
56
- }
57
- console.log("");
50
+ try {
51
+ console.log("\nManage accounts:");
52
+ for (const acc of accounts) {
53
+ const name = acc.email || `Account ${acc.index + 1}`;
54
+ const status = acc.enabled ? "enabled" : "disabled";
55
+ console.log(` ${acc.index + 1}. ${name} [${status}]`);
56
+ }
57
+ console.log("");
58
58
 
59
- while (true) {
60
- const answer = await rl.question("Enter account number to toggle, (d)N to delete (e.g. d1), or (b)ack: ");
61
- const normalized = answer.trim().toLowerCase();
59
+ while (true) {
60
+ const answer = await rl.question("Enter account number to toggle, (d)N to delete (e.g. d1), or (b)ack: ");
61
+ const normalized = answer.trim().toLowerCase();
62
62
 
63
- if (normalized === "b" || normalized === "back") return;
63
+ if (normalized === "b" || normalized === "back") return;
64
64
 
65
- // Delete: d1, d2, etc.
66
- const deleteMatch = normalized.match(/^d(\d+)$/);
67
- if (deleteMatch) {
68
- const idx = parseInt(deleteMatch[1], 10) - 1;
69
- if (idx >= 0 && idx < accounts.length) {
70
- accountManager.removeAccount(idx);
71
- console.log(`Removed account ${idx + 1}.`);
72
- return;
73
- }
74
- console.log("Invalid account number.");
75
- continue;
76
- }
65
+ // Delete: d1, d2, etc.
66
+ const deleteMatch = normalized.match(/^d(\d+)$/);
67
+ if (deleteMatch) {
68
+ const idx = parseInt(deleteMatch[1], 10) - 1;
69
+ if (idx >= 0 && idx < accounts.length) {
70
+ accountManager.removeAccount(idx);
71
+ console.log(`Removed account ${idx + 1}.`);
72
+ return;
73
+ }
74
+ console.log("Invalid account number.");
75
+ continue;
76
+ }
77
77
 
78
- // Toggle: just the number
79
- const num = parseInt(normalized, 10);
80
- if (!isNaN(num) && num >= 1 && num <= accounts.length) {
81
- const newState = accountManager.toggleAccount(num - 1);
82
- console.log(`Account ${num} is now ${newState ? "enabled" : "disabled"}.`);
83
- continue;
84
- }
78
+ // Toggle: just the number
79
+ const num = parseInt(normalized, 10);
80
+ if (!isNaN(num) && num >= 1 && num <= accounts.length) {
81
+ const newState = accountManager.toggleAccount(num - 1);
82
+ console.log(`Account ${num} is now ${newState ? "enabled" : "disabled"}.`);
83
+ continue;
84
+ }
85
85
 
86
- console.log("Invalid input.");
86
+ console.log("Invalid input.");
87
+ }
88
+ } finally {
89
+ rl.close();
87
90
  }
88
- } finally {
89
- rl.close();
90
- }
91
91
  }