opencode-account-manager 0.6.3 → 0.6.5

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 (95) hide show
  1. package/README.md +235 -216
  2. package/README_VI.md +235 -216
  3. package/dist/cli.js +83 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/config-store.d.ts +12 -0
  6. package/dist/core/config-store.d.ts.map +1 -1
  7. package/dist/core/config-store.js +98 -0
  8. package/dist/core/config-store.js.map +1 -1
  9. package/dist/core/health-log.d.ts +9 -0
  10. package/dist/core/health-log.d.ts.map +1 -0
  11. package/dist/core/health-log.js +154 -0
  12. package/dist/core/health-log.js.map +1 -0
  13. package/dist/core/health-oauth.d.ts +5 -0
  14. package/dist/core/health-oauth.d.ts.map +1 -0
  15. package/dist/core/health-oauth.js +147 -0
  16. package/dist/core/health-oauth.js.map +1 -0
  17. package/dist/core/health-orchestrator.d.ts +32 -0
  18. package/dist/core/health-orchestrator.d.ts.map +1 -0
  19. package/dist/core/health-orchestrator.js +148 -0
  20. package/dist/core/health-orchestrator.js.map +1 -0
  21. package/dist/core/health-utils.d.ts +15 -0
  22. package/dist/core/health-utils.d.ts.map +1 -0
  23. package/dist/core/health-utils.js +60 -0
  24. package/dist/core/health-utils.js.map +1 -0
  25. package/dist/core/paths.d.ts +1 -0
  26. package/dist/core/paths.d.ts.map +1 -1
  27. package/dist/core/paths.js +4 -0
  28. package/dist/core/paths.js.map +1 -1
  29. package/dist/core/types.d.ts +26 -0
  30. package/dist/core/types.d.ts.map +1 -1
  31. package/dist/tui/Dashboard.d.ts.map +1 -1
  32. package/dist/tui/Dashboard.js +72 -5
  33. package/dist/tui/Dashboard.js.map +1 -1
  34. package/dist/tui/components/AccountList.d.ts +5 -3
  35. package/dist/tui/components/AccountList.d.ts.map +1 -1
  36. package/dist/tui/components/AccountList.js +9 -3
  37. package/dist/tui/components/AccountList.js.map +1 -1
  38. package/dist/tui/components/Box.js +2 -2
  39. package/dist/tui/components/Box.js.map +1 -1
  40. package/dist/tui/components/DashboardView.d.ts +3 -2
  41. package/dist/tui/components/DashboardView.d.ts.map +1 -1
  42. package/dist/tui/components/DashboardView.js +50 -4
  43. package/dist/tui/components/DashboardView.js.map +1 -1
  44. package/dist/tui/components/ExportModal.js +4 -4
  45. package/dist/tui/components/ExportModal.js.map +1 -1
  46. package/dist/tui/components/FileBrowser.js +1 -1
  47. package/dist/tui/components/HealthBadge.d.ts +9 -0
  48. package/dist/tui/components/HealthBadge.d.ts.map +1 -0
  49. package/dist/tui/components/HealthBadge.js +56 -0
  50. package/dist/tui/components/HealthBadge.js.map +1 -0
  51. package/dist/tui/components/ImportModal.js +4 -4
  52. package/dist/tui/components/ImportModal.js.map +1 -1
  53. package/dist/tui/components/PasswordInput.js +2 -2
  54. package/dist/tui/components/PasswordInput.js.map +1 -1
  55. package/dist/tui/components/StatusBadge.d.ts +2 -1
  56. package/dist/tui/components/StatusBadge.d.ts.map +1 -1
  57. package/dist/tui/components/StatusBadge.js +30 -2
  58. package/dist/tui/components/StatusBadge.js.map +1 -1
  59. package/dist/tui/components/index.d.ts +1 -0
  60. package/dist/tui/components/index.d.ts.map +1 -1
  61. package/dist/tui/components/index.js +3 -1
  62. package/dist/tui/components/index.js.map +1 -1
  63. package/docs/BLUEPRINT.md +476 -476
  64. package/docs/ROADMAP.md +125 -107
  65. package/package.json +36 -36
  66. package/src/cli.ts +139 -38
  67. package/src/core/config-store.ts +278 -171
  68. package/src/core/crypto.ts +162 -162
  69. package/src/core/health-log.ts +173 -0
  70. package/src/core/health-oauth.ts +190 -0
  71. package/src/core/health-orchestrator.ts +224 -0
  72. package/src/core/importers/amExport.ts +177 -177
  73. package/src/core/opencode-config.ts +217 -217
  74. package/src/core/paths.ts +10 -6
  75. package/src/core/types.ts +193 -147
  76. package/src/tui/Dashboard.tsx +557 -478
  77. package/src/tui/components/AccountList.tsx +122 -104
  78. package/src/tui/components/ActionPalette.tsx +117 -117
  79. package/src/tui/components/Box.tsx +2 -2
  80. package/src/tui/components/DashboardView.tsx +285 -230
  81. package/src/tui/components/ExportModal.tsx +255 -255
  82. package/src/tui/components/FileBrowser.tsx +393 -393
  83. package/src/tui/components/Header.tsx +26 -26
  84. package/src/tui/components/HealthBadge.tsx +64 -0
  85. package/src/tui/components/ImportModal.tsx +334 -334
  86. package/src/tui/components/McpServerList.tsx +67 -67
  87. package/src/tui/components/Menu.tsx +61 -61
  88. package/src/tui/components/PasswordInput.tsx +159 -159
  89. package/src/tui/components/ProviderList.tsx +59 -59
  90. package/src/tui/components/SectionBox.tsx +35 -35
  91. package/src/tui/components/StatsRow.tsx +33 -33
  92. package/src/tui/components/StatusBadge.tsx +36 -3
  93. package/src/tui/components/index.ts +15 -14
  94. package/test-minimal.js +26 -26
  95. package/test-with-accounts.js +58 -58
@@ -1,89 +1,92 @@
1
- import React, { useState, useEffect } from "react";
2
- import { Box, Text, useApp, useInput } from "ink";
3
- import {
4
- Header,
5
- StatsRow,
6
- AccountList,
7
- ProviderList,
8
- McpServerList,
9
- SectionBox,
10
- ExportModal,
11
- ImportModal,
12
- ActionPalette,
13
- PaletteAction,
14
- DashboardView,
15
- } from "./components";
16
- import {
17
- readPluginAccountsFile,
18
- createEmptyPluginAccountsFile,
19
- summarizeAccounts,
20
- mergeAccounts,
21
- writePluginAccountsFile,
22
- } from "../core/accounts";
23
- import { getPluginAccountsPath, getAmFolderPath } from "../core/paths";
24
- import { Account, PluginAccountsFile } from "../core/types";
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useApp, useInput } from "ink";
3
+ import {
4
+ Header,
5
+ StatsRow,
6
+ AccountList,
7
+ ProviderList,
8
+ McpServerList,
9
+ SectionBox,
10
+ ExportModal,
11
+ ImportModal,
12
+ ActionPalette,
13
+ PaletteAction,
14
+ DashboardView,
15
+ } from "./components";
16
+ import {
17
+ readPluginAccountsFile,
18
+ createEmptyPluginAccountsFile,
19
+ summarizeAccounts,
20
+ mergeAccounts,
21
+ writePluginAccountsFile,
22
+ } from "../core/accounts";
23
+ import { getPluginAccountsPath, getAmFolderPath } from "../core/paths";
24
+ import { Account, AccountHealthResult, PluginAccountsFile } from "../core/types";
25
25
  import { importFromAmFolder } from "../core/importers/amJson";
26
26
  import {
27
27
  parseOpencodeInfo,
28
28
  getConfigSummary,
29
29
  OpencodeInfo,
30
30
  } from "../core/opencode-config";
31
-
32
- interface DashboardProps {
33
- pluginPath?: string;
34
- }
35
-
36
- type ModalType = "none" | "export" | "import" | "export-selected" | "palette";
37
- type MainTab = "dashboard" | "settings";
38
-
39
- function safeReadPluginFile(pluginPath: string): PluginAccountsFile {
40
- try {
41
- return readPluginAccountsFile(pluginPath);
42
- } catch {
43
- return createEmptyPluginAccountsFile();
44
- }
45
- }
46
-
47
- export function Dashboard({ pluginPath }: DashboardProps) {
48
- const { exit } = useApp();
49
- const resolvedPath = getPluginAccountsPath(pluginPath);
50
-
51
- // OpenCode config state
52
- const [opencodeInfo, setOpencodeInfo] = useState<OpencodeInfo | null>(null);
53
-
54
- // Plugin accounts state
31
+ import { checkAccountsHealth } from "../core/health-orchestrator";
32
+ import { getHealthCache, normalizeHealthKey } from "../core/config-store";
33
+
34
+ interface DashboardProps {
35
+ pluginPath?: string;
36
+ }
37
+
38
+ type ModalType = "none" | "export" | "import" | "export-selected" | "palette";
39
+ type MainTab = "dashboard" | "settings";
40
+
41
+ function safeReadPluginFile(pluginPath: string): PluginAccountsFile {
42
+ try {
43
+ return readPluginAccountsFile(pluginPath);
44
+ } catch {
45
+ return createEmptyPluginAccountsFile();
46
+ }
47
+ }
48
+
49
+ export function Dashboard({ pluginPath }: DashboardProps) {
50
+ const { exit } = useApp();
51
+ const resolvedPath = getPluginAccountsPath(pluginPath);
52
+
53
+ // OpenCode config state
54
+ const [opencodeInfo, setOpencodeInfo] = useState<OpencodeInfo | null>(null);
55
+
56
+ // Plugin accounts state
55
57
  const [accounts, setAccounts] = useState<Account[]>([]);
56
58
  const [summary, setSummary] = useState({ total: 0, available: 0, limited: 0 });
57
59
  const [message, setMessage] = useState<string | null>(null);
58
-
59
- // Main tab state
60
- const [activeTab, setActiveTab] = useState<MainTab>("dashboard");
61
-
62
- // Dashboard tab state
63
- const [dashboardIndex, setDashboardIndex] = useState(0);
64
-
65
- // Settings tab state
66
- const [expandedSection, setExpandedSection] = useState<"providers" | "accounts" | "mcp">("accounts");
67
- const [settingsNavIndex, setSettingsNavIndex] = useState(0);
68
- const [checkedEmails, setCheckedEmails] = useState<Set<string>>(new Set());
69
-
70
- // Modal state
71
- const [activeModal, setActiveModal] = useState<ModalType>("none");
72
-
73
- // Loading state
74
- const [isLoading, setIsLoading] = useState(false);
75
- const [loadingStep, setLoadingStep] = useState("");
76
-
77
- const showMessage = (msg: string, duration = 3000) => {
78
- setMessage(msg);
79
- setTimeout(() => setMessage(null), duration);
80
- };
81
-
82
- const loadOpencodeConfig = () => {
83
- const info = parseOpencodeInfo();
84
- setOpencodeInfo(info);
85
- };
86
-
60
+ const [healthResults, setHealthResults] = useState<Record<string, AccountHealthResult>>({});
61
+
62
+ // Main tab state
63
+ const [activeTab, setActiveTab] = useState<MainTab>("dashboard");
64
+
65
+ // Dashboard tab state
66
+ const [dashboardIndex, setDashboardIndex] = useState(0);
67
+
68
+ // Settings tab state
69
+ const [expandedSection, setExpandedSection] = useState<"providers" | "accounts" | "mcp">("accounts");
70
+ const [settingsNavIndex, setSettingsNavIndex] = useState(0);
71
+ const [checkedEmails, setCheckedEmails] = useState<Set<string>>(new Set());
72
+
73
+ // Modal state
74
+ const [activeModal, setActiveModal] = useState<ModalType>("none");
75
+
76
+ // Loading state
77
+ const [isLoading, setIsLoading] = useState(false);
78
+ const [loadingStep, setLoadingStep] = useState("");
79
+
80
+ const showMessage = (msg: string, duration = 3000) => {
81
+ setMessage(msg);
82
+ setTimeout(() => setMessage(null), duration);
83
+ };
84
+
85
+ const loadOpencodeConfig = () => {
86
+ const info = parseOpencodeInfo();
87
+ setOpencodeInfo(info);
88
+ };
89
+
87
90
  const loadAccounts = () => {
88
91
  const file = safeReadPluginFile(resolvedPath);
89
92
  setAccounts(file.accounts);
@@ -91,6 +94,10 @@ export function Dashboard({ pluginPath }: DashboardProps) {
91
94
  setCheckedEmails(new Set());
92
95
  };
93
96
 
97
+ const loadHealthCache = () => {
98
+ setHealthResults(getHealthCache());
99
+ };
100
+
94
101
  const refresh = async () => {
95
102
  setIsLoading(true);
96
103
 
@@ -101,6 +108,7 @@ export function Dashboard({ pluginPath }: DashboardProps) {
101
108
  setLoadingStep("Loading accounts...");
102
109
  await new Promise(r => setTimeout(r, 100));
103
110
  loadAccounts();
111
+ loadHealthCache();
104
112
 
105
113
  setLoadingStep("Done!");
106
114
  await new Promise(r => setTimeout(r, 300));
@@ -110,406 +118,474 @@ export function Dashboard({ pluginPath }: DashboardProps) {
110
118
  showMessage("Refreshed", 2000);
111
119
  };
112
120
 
113
- useEffect(() => {
114
- loadOpencodeConfig();
115
- loadAccounts();
116
- }, []);
121
+ const runHealthCheck = async (emails?: string[]) => {
122
+ if (accounts.length === 0) {
123
+ showMessage("No accounts to check", 2000);
124
+ return;
125
+ }
117
126
 
118
- // Build settings navigation items
119
- const buildSettingsNavItems = () => {
120
- const items: Array<{ type: "section" | "account"; id?: string; index?: number; email?: string }> = [
121
- { type: "section", id: "providers" },
122
- { type: "section", id: "accounts" },
123
- ];
127
+ setIsLoading(true);
128
+ setLoadingStep("Starting health check...");
129
+
130
+ try {
131
+ const result = await checkAccountsHealth(accounts, {
132
+ emails,
133
+ includeLogs: true,
134
+ force: false,
135
+ onProgress: (current, total, msg) => {
136
+ setLoadingStep(`[${current}/${total}] ${msg}`);
137
+ },
138
+ });
124
139
 
125
- if (expandedSection === "accounts") {
126
- accounts.forEach((acc, index) => {
127
- items.push({ type: "account", index, email: acc.email });
140
+ setHealthResults((prev) => {
141
+ const next = { ...prev };
142
+ for (const item of result.items) {
143
+ next[normalizeHealthKey(item.email)] = item.result;
144
+ }
145
+ return next;
128
146
  });
129
- }
130
147
 
131
- items.push({ type: "section", id: "mcp" });
132
- return items;
148
+ showMessage(
149
+ `Health check: ${result.counts.checked} checked, ${result.counts.cached} cached, ${result.counts.skipped} skipped`,
150
+ 5000
151
+ );
152
+ } catch (err) {
153
+ showMessage("Health check failed", 3000);
154
+ } finally {
155
+ setIsLoading(false);
156
+ setLoadingStep("");
157
+ }
133
158
  };
134
159
 
135
- const settingsNavItems = buildSettingsNavItems();
136
-
160
+ const handleCheckHealthSelected = () => {
161
+ if (checkedEmails.size === 0) {
162
+ showMessage("No accounts selected", 2000);
163
+ return;
164
+ }
165
+ runHealthCheck(Array.from(checkedEmails));
166
+ };
167
+
168
+ useEffect(() => {
169
+ loadOpencodeConfig();
170
+ loadAccounts();
171
+ loadHealthCache();
172
+ }, []);
173
+
174
+ // Build settings navigation items
175
+ const buildSettingsNavItems = () => {
176
+ const items: Array<{ type: "section" | "account"; id?: string; index?: number; email?: string }> = [
177
+ { type: "section", id: "providers" },
178
+ { type: "section", id: "accounts" },
179
+ ];
180
+
181
+ if (expandedSection === "accounts") {
182
+ accounts.forEach((acc, index) => {
183
+ items.push({ type: "account", index, email: acc.email });
184
+ });
185
+ }
186
+
187
+ items.push({ type: "section", id: "mcp" });
188
+ return items;
189
+ };
190
+
191
+ const settingsNavItems = buildSettingsNavItems();
192
+
137
193
  // Palette actions
138
194
  const paletteActions: PaletteAction[] = [
139
195
  { id: "refresh", label: "Refresh", shortcut: "R" },
196
+ { id: "check-health-all", label: "Check Account Health (All)", shortcut: "H" },
140
197
  { id: "export", label: "Export All Accounts", shortcut: "E" },
141
198
  { id: "import", label: "Import from File", shortcut: "I" },
142
199
  { id: "import-am", label: "Import from Antigravity Manager", shortcut: "A" },
143
200
  ...(checkedEmails.size > 0 ? [
201
+ { id: "check-health-selected", label: `Check Health Selected (${checkedEmails.size})`, shortcut: "Shift+H" },
144
202
  { id: "export-selected", label: `Export Selected (${checkedEmails.size})`, shortcut: "X" },
145
203
  { id: "enable-selected", label: `Enable Selected (${checkedEmails.size})` },
146
204
  { id: "disable-selected", label: `Disable Selected (${checkedEmails.size})` },
147
205
  { id: "delete-selected", label: `Delete Selected (${checkedEmails.size})`, shortcut: "Del" },
148
- { id: "clear-selection", label: "Clear Selection", shortcut: "N" },
149
- ] : []),
150
- { id: "select-all", label: "Select All Accounts", shortcut: "Ctrl+A" },
151
- { id: "quit", label: "Quit", shortcut: "Q" },
152
- ];
153
-
154
- // Keyboard navigation
155
- useInput((input, key) => {
156
- if (activeModal === "palette") return;
157
- if (activeModal !== "none") return;
158
-
159
- // Tab to switch between main tabs
160
- if (key.tab) {
161
- setActiveTab(prev => prev === "dashboard" ? "settings" : "dashboard");
162
- return;
163
- }
164
-
165
- // Open palette with P
166
- if (input === "p" || input === "P") {
167
- setActiveModal("palette");
168
- return;
169
- }
170
-
171
- // Quick shortcuts
172
- if (input === "q" || input === "Q") {
173
- exit();
174
- return;
175
- }
206
+ { id: "clear-selection", label: "Clear Selection", shortcut: "N" },
207
+ ] : []),
208
+ { id: "select-all", label: "Select All Accounts", shortcut: "Ctrl+A" },
209
+ { id: "quit", label: "Quit", shortcut: "Q" },
210
+ ];
211
+
212
+ // Keyboard navigation
213
+ useInput((input, key) => {
214
+ if (activeModal === "palette") return;
215
+ if (activeModal !== "none") return;
216
+
217
+ // Tab to switch between main tabs
218
+ if (key.tab) {
219
+ setActiveTab(prev => prev === "dashboard" ? "settings" : "dashboard");
220
+ return;
221
+ }
222
+
223
+ // Open palette with P
224
+ if (input === "p" || input === "P") {
225
+ setActiveModal("palette");
226
+ return;
227
+ }
228
+
229
+ // Quick shortcuts
230
+ if (input === "q" || input === "Q") {
231
+ exit();
232
+ return;
233
+ }
176
234
  if (input === "r" || input === "R") {
177
235
  refresh();
178
236
  return;
179
237
  }
180
-
181
- // Tab-specific navigation
182
- if (activeTab === "dashboard") {
183
- // Dashboard tab navigation
184
- if (key.upArrow) {
185
- setDashboardIndex(prev => Math.max(0, prev - 1));
186
- return;
187
- }
188
- if (key.downArrow) {
189
- setDashboardIndex(prev => Math.min(accounts.length - 1, prev + 1));
190
- return;
191
- }
192
- // Space/Enter to toggle selection
193
- if (input === " " || key.return) {
194
- const email = accounts[dashboardIndex]?.email;
195
- if (email) {
196
- setCheckedEmails(prev => {
197
- const next = new Set(prev);
198
- if (next.has(email)) {
199
- next.delete(email);
200
- } else {
201
- next.add(email);
202
- }
203
- return next;
204
- });
205
- }
206
- return;
207
- }
208
- } else {
209
- // Settings tab navigation
210
- if (key.upArrow) {
211
- setSettingsNavIndex(prev => Math.max(0, prev - 1));
212
- return;
213
- }
214
- if (key.downArrow) {
215
- setSettingsNavIndex(prev => Math.min(settingsNavItems.length - 1, prev + 1));
216
- return;
217
- }
218
- if (key.return) {
219
- const currentItem = settingsNavItems[settingsNavIndex];
220
- if (currentItem?.type === "section" && currentItem.id) {
221
- setExpandedSection(currentItem.id as "providers" | "accounts" | "mcp");
222
- } else if (currentItem?.type === "account" && currentItem.email) {
223
- const email = currentItem.email;
224
- setCheckedEmails(prev => {
225
- const next = new Set(prev);
226
- if (next.has(email)) {
227
- next.delete(email);
228
- } else {
229
- next.add(email);
230
- }
231
- return next;
232
- });
233
- }
234
- return;
235
- }
236
- if (input === " ") {
237
- const currentItem = settingsNavItems[settingsNavIndex];
238
- if (currentItem?.type === "account" && currentItem.email) {
239
- const email = currentItem.email;
240
- setCheckedEmails(prev => {
241
- const next = new Set(prev);
242
- if (next.has(email)) {
243
- next.delete(email);
244
- } else {
245
- next.add(email);
246
- }
247
- return next;
248
- });
249
- }
250
- return;
251
- }
252
- }
253
-
254
- // Escape to clear selection
255
- if (key.escape) {
256
- if (checkedEmails.size > 0) {
257
- setCheckedEmails(new Set());
258
- showMessage("Selection cleared", 1500);
259
- }
238
+ if (input === "h" || input === "H") {
239
+ runHealthCheck();
260
240
  return;
261
241
  }
262
- });
263
-
264
- // Handle palette action
265
- const handlePaletteAction = (actionId: string) => {
266
- setActiveModal("none");
267
-
242
+
243
+ // Tab-specific navigation
244
+ if (activeTab === "dashboard") {
245
+ // Dashboard tab navigation
246
+ if (key.upArrow) {
247
+ setDashboardIndex(prev => Math.max(0, prev - 1));
248
+ return;
249
+ }
250
+ if (key.downArrow) {
251
+ setDashboardIndex(prev => Math.min(accounts.length - 1, prev + 1));
252
+ return;
253
+ }
254
+ // Space/Enter to toggle selection
255
+ if (input === " " || key.return) {
256
+ const email = accounts[dashboardIndex]?.email;
257
+ if (email) {
258
+ setCheckedEmails(prev => {
259
+ const next = new Set(prev);
260
+ if (next.has(email)) {
261
+ next.delete(email);
262
+ } else {
263
+ next.add(email);
264
+ }
265
+ return next;
266
+ });
267
+ }
268
+ return;
269
+ }
270
+ } else {
271
+ // Settings tab navigation
272
+ if (key.upArrow) {
273
+ setSettingsNavIndex(prev => Math.max(0, prev - 1));
274
+ return;
275
+ }
276
+ if (key.downArrow) {
277
+ setSettingsNavIndex(prev => Math.min(settingsNavItems.length - 1, prev + 1));
278
+ return;
279
+ }
280
+ if (key.return) {
281
+ const currentItem = settingsNavItems[settingsNavIndex];
282
+ if (currentItem?.type === "section" && currentItem.id) {
283
+ setExpandedSection(currentItem.id as "providers" | "accounts" | "mcp");
284
+ } else if (currentItem?.type === "account" && currentItem.email) {
285
+ const email = currentItem.email;
286
+ setCheckedEmails(prev => {
287
+ const next = new Set(prev);
288
+ if (next.has(email)) {
289
+ next.delete(email);
290
+ } else {
291
+ next.add(email);
292
+ }
293
+ return next;
294
+ });
295
+ }
296
+ return;
297
+ }
298
+ if (input === " ") {
299
+ const currentItem = settingsNavItems[settingsNavIndex];
300
+ if (currentItem?.type === "account" && currentItem.email) {
301
+ const email = currentItem.email;
302
+ setCheckedEmails(prev => {
303
+ const next = new Set(prev);
304
+ if (next.has(email)) {
305
+ next.delete(email);
306
+ } else {
307
+ next.add(email);
308
+ }
309
+ return next;
310
+ });
311
+ }
312
+ return;
313
+ }
314
+ }
315
+
316
+ // Escape to clear selection
317
+ if (key.escape) {
318
+ if (checkedEmails.size > 0) {
319
+ setCheckedEmails(new Set());
320
+ showMessage("Selection cleared", 1500);
321
+ }
322
+ return;
323
+ }
324
+ });
325
+
326
+ // Handle palette action
327
+ const handlePaletteAction = (actionId: string) => {
328
+ setActiveModal("none");
329
+
268
330
  switch (actionId) {
269
331
  case "refresh":
270
332
  refresh();
271
333
  break;
272
- case "export":
273
- setActiveModal("export");
334
+ case "check-health-all":
335
+ runHealthCheck();
274
336
  break;
275
- case "import":
276
- setActiveModal("import");
337
+ case "check-health-selected":
338
+ handleCheckHealthSelected();
277
339
  break;
278
- case "import-am":
279
- handleImportAM();
280
- break;
281
- case "export-selected":
282
- if (checkedEmails.size > 0) {
283
- setActiveModal("export-selected");
284
- } else {
285
- showMessage("No accounts selected", 2000);
286
- }
287
- break;
288
- case "enable-selected":
289
- handleEnableSelected();
290
- break;
291
- case "disable-selected":
292
- handleDisableSelected();
293
- break;
294
- case "delete-selected":
295
- handleDeleteSelected();
296
- break;
297
- case "select-all":
298
- setCheckedEmails(new Set(accounts.map(a => a.email)));
299
- showMessage(`Selected ${accounts.length} accounts`, 2000);
300
- break;
301
- case "clear-selection":
302
- setCheckedEmails(new Set());
303
- showMessage("Selection cleared", 1500);
304
- break;
305
- case "quit":
306
- exit();
340
+ case "export":
341
+ setActiveModal("export");
307
342
  break;
308
- }
309
- };
310
-
311
- // Export completion handler
312
- const handleExportComplete = (filePath: string) => {
313
- setActiveModal("none");
314
- showMessage(`Exported to ${filePath}`, 4000);
315
- };
316
-
317
- // Import completion handler
318
- const handleImportComplete = (importedAccounts: Account[], newCount: number, overwrittenCount: number) => {
319
- const file = safeReadPluginFile(resolvedPath);
320
- const merged = mergeAccounts(file, importedAccounts, "merge");
321
- writePluginAccountsFile(pluginPath, merged);
322
-
323
- setActiveModal("none");
324
- loadAccounts();
325
- showMessage(`Imported: ${newCount} new, ${overwrittenCount} updated`, 4000);
326
- };
327
-
328
- const handleImportAM = () => {
329
- const amPath = getAmFolderPath();
330
- const result = importFromAmFolder(amPath);
331
-
332
- if (result.errors.length > 0) {
333
- showMessage(`Error: ${result.errors[0]}`, 5000);
334
- return;
335
- }
336
-
337
- if (result.accounts.length === 0) {
338
- showMessage(`No accounts found in AM (${result.skipped.length} skipped)`, 4000);
339
- return;
340
- }
341
-
342
- const existingFile = safeReadPluginFile(resolvedPath);
343
- const merged = mergeAccounts(existingFile, result.accounts, "merge");
344
- writePluginAccountsFile(pluginPath, merged);
345
-
346
- const added = merged.accounts.length - existingFile.accounts.length;
347
- showMessage(
348
- `Imported from AM: ${result.accounts.length} found, ${added} new`,
349
- 5000
350
- );
351
-
352
- loadAccounts();
353
- };
354
-
355
- const handleEnableSelected = () => {
356
- if (checkedEmails.size === 0) {
357
- showMessage("No accounts selected", 2000);
358
- return;
359
- }
360
-
361
- const file = safeReadPluginFile(resolvedPath);
362
- let count = 0;
363
-
364
- file.accounts = file.accounts.map(acc => {
365
- if (checkedEmails.has(acc.email)) {
366
- count++;
367
- return { ...acc, enabled: true };
368
- }
369
- return acc;
370
- });
371
-
372
- writePluginAccountsFile(pluginPath, file);
373
- showMessage(`Enabled ${count} accounts`, 3000);
374
- loadAccounts();
375
- setCheckedEmails(new Set());
376
- };
377
-
378
- const handleDisableSelected = () => {
379
- if (checkedEmails.size === 0) {
380
- showMessage("No accounts selected", 2000);
381
- return;
382
- }
383
-
384
- const file = safeReadPluginFile(resolvedPath);
385
- let count = 0;
386
-
387
- file.accounts = file.accounts.map(acc => {
388
- if (checkedEmails.has(acc.email)) {
389
- count++;
390
- return { ...acc, enabled: false };
391
- }
392
- return acc;
393
- });
394
-
395
- writePluginAccountsFile(pluginPath, file);
396
- showMessage(`Disabled ${count} accounts`, 3000);
397
- loadAccounts();
398
- setCheckedEmails(new Set());
399
- };
400
-
401
- const handleDeleteSelected = () => {
402
- if (checkedEmails.size === 0) {
403
- showMessage("No accounts selected", 2000);
404
- return;
405
- }
406
-
407
- const file = safeReadPluginFile(resolvedPath);
408
- const beforeCount = file.accounts.length;
409
-
410
- file.accounts = file.accounts.filter(acc => !checkedEmails.has(acc.email));
411
- const deletedCount = beforeCount - file.accounts.length;
412
-
413
- writePluginAccountsFile(pluginPath, file);
414
- showMessage(`Deleted ${deletedCount} accounts`, 3000);
415
- loadAccounts();
416
- setCheckedEmails(new Set());
417
- };
418
-
419
- // Calculate stats
420
- const configSummary = opencodeInfo ? getConfigSummary(opencodeInfo) : null;
421
-
422
- // Get accounts to export
423
- const getAccountsForExport = (): Account[] => {
424
- if (activeModal === "export-selected") {
425
- return accounts.filter(acc => checkedEmails.has(acc.email));
426
- }
427
- return accounts;
428
- };
429
-
430
- // Settings nav helpers
431
- const currentSettingsItem = settingsNavItems[settingsNavIndex];
432
- const isOnSection = (id: string) => currentSettingsItem?.type === "section" && currentSettingsItem.id === id;
433
-
434
- // Render modals
435
- if (activeModal === "export" || activeModal === "export-selected") {
436
- return (
437
- <Box flexDirection="column" padding={1}>
438
- <ExportModal
439
- accounts={getAccountsForExport()}
440
- onComplete={handleExportComplete}
441
- onCancel={() => setActiveModal("none")}
442
- />
443
- </Box>
444
- );
445
- }
446
-
447
- if (activeModal === "import") {
448
- return (
449
- <Box flexDirection="column" padding={1}>
450
- <ImportModal
451
- existingAccounts={accounts}
452
- onComplete={handleImportComplete}
453
- onCancel={() => setActiveModal("none")}
454
- />
455
- </Box>
456
- );
457
- }
458
-
459
- return (
460
- <Box flexDirection="column" padding={1}>
461
- <Header title="OpenCode Account Manager" subtitle={activeTab === "dashboard" ? "Dashboard" : "Settings"} />
462
-
463
- {/* Tab bar */}
464
- <Box marginBottom={1}>
465
- <Text
466
- inverse={activeTab === "dashboard"}
467
- bold={activeTab === "dashboard"}
468
- >
469
- {" DASHBOARD "}
470
- </Text>
471
- <Text> </Text>
472
- <Text
473
- inverse={activeTab === "settings"}
474
- bold={activeTab === "settings"}
475
- >
476
- {" SETTINGS "}
477
- </Text>
478
- <Text dimColor> [Tab]</Text>
479
- </Box>
480
-
343
+ case "import":
344
+ setActiveModal("import");
345
+ break;
346
+ case "import-am":
347
+ handleImportAM();
348
+ break;
349
+ case "export-selected":
350
+ if (checkedEmails.size > 0) {
351
+ setActiveModal("export-selected");
352
+ } else {
353
+ showMessage("No accounts selected", 2000);
354
+ }
355
+ break;
356
+ case "enable-selected":
357
+ handleEnableSelected();
358
+ break;
359
+ case "disable-selected":
360
+ handleDisableSelected();
361
+ break;
362
+ case "delete-selected":
363
+ handleDeleteSelected();
364
+ break;
365
+ case "select-all":
366
+ setCheckedEmails(new Set(accounts.map(a => a.email)));
367
+ showMessage(`Selected ${accounts.length} accounts`, 2000);
368
+ break;
369
+ case "clear-selection":
370
+ setCheckedEmails(new Set());
371
+ showMessage("Selection cleared", 1500);
372
+ break;
373
+ case "quit":
374
+ exit();
375
+ break;
376
+ }
377
+ };
378
+
379
+ // Export completion handler
380
+ const handleExportComplete = (filePath: string) => {
381
+ setActiveModal("none");
382
+ showMessage(`Exported to ${filePath}`, 4000);
383
+ };
384
+
385
+ // Import completion handler
386
+ const handleImportComplete = (importedAccounts: Account[], newCount: number, overwrittenCount: number) => {
387
+ const file = safeReadPluginFile(resolvedPath);
388
+ const merged = mergeAccounts(file, importedAccounts, "merge");
389
+ writePluginAccountsFile(pluginPath, merged);
390
+
391
+ setActiveModal("none");
392
+ loadAccounts();
393
+ showMessage(`Imported: ${newCount} new, ${overwrittenCount} updated`, 4000);
394
+ };
395
+
396
+ const handleImportAM = () => {
397
+ const amPath = getAmFolderPath();
398
+ const result = importFromAmFolder(amPath);
399
+
400
+ if (result.errors.length > 0) {
401
+ showMessage(`Error: ${result.errors[0]}`, 5000);
402
+ return;
403
+ }
404
+
405
+ if (result.accounts.length === 0) {
406
+ showMessage(`No accounts found in AM (${result.skipped.length} skipped)`, 4000);
407
+ return;
408
+ }
409
+
410
+ const existingFile = safeReadPluginFile(resolvedPath);
411
+ const merged = mergeAccounts(existingFile, result.accounts, "merge");
412
+ writePluginAccountsFile(pluginPath, merged);
413
+
414
+ const added = merged.accounts.length - existingFile.accounts.length;
415
+ showMessage(
416
+ `Imported from AM: ${result.accounts.length} found, ${added} new`,
417
+ 5000
418
+ );
419
+
420
+ loadAccounts();
421
+ };
422
+
423
+ const handleEnableSelected = () => {
424
+ if (checkedEmails.size === 0) {
425
+ showMessage("No accounts selected", 2000);
426
+ return;
427
+ }
428
+
429
+ const file = safeReadPluginFile(resolvedPath);
430
+ let count = 0;
431
+
432
+ file.accounts = file.accounts.map(acc => {
433
+ if (checkedEmails.has(acc.email)) {
434
+ count++;
435
+ return { ...acc, enabled: true };
436
+ }
437
+ return acc;
438
+ });
439
+
440
+ writePluginAccountsFile(pluginPath, file);
441
+ showMessage(`Enabled ${count} accounts`, 3000);
442
+ loadAccounts();
443
+ setCheckedEmails(new Set());
444
+ };
445
+
446
+ const handleDisableSelected = () => {
447
+ if (checkedEmails.size === 0) {
448
+ showMessage("No accounts selected", 2000);
449
+ return;
450
+ }
451
+
452
+ const file = safeReadPluginFile(resolvedPath);
453
+ let count = 0;
454
+
455
+ file.accounts = file.accounts.map(acc => {
456
+ if (checkedEmails.has(acc.email)) {
457
+ count++;
458
+ return { ...acc, enabled: false };
459
+ }
460
+ return acc;
461
+ });
462
+
463
+ writePluginAccountsFile(pluginPath, file);
464
+ showMessage(`Disabled ${count} accounts`, 3000);
465
+ loadAccounts();
466
+ setCheckedEmails(new Set());
467
+ };
468
+
469
+ const handleDeleteSelected = () => {
470
+ if (checkedEmails.size === 0) {
471
+ showMessage("No accounts selected", 2000);
472
+ return;
473
+ }
474
+
475
+ const file = safeReadPluginFile(resolvedPath);
476
+ const beforeCount = file.accounts.length;
477
+
478
+ file.accounts = file.accounts.filter(acc => !checkedEmails.has(acc.email));
479
+ const deletedCount = beforeCount - file.accounts.length;
480
+
481
+ writePluginAccountsFile(pluginPath, file);
482
+ showMessage(`Deleted ${deletedCount} accounts`, 3000);
483
+ loadAccounts();
484
+ setCheckedEmails(new Set());
485
+ };
486
+
487
+ // Calculate stats
488
+ const configSummary = opencodeInfo ? getConfigSummary(opencodeInfo) : null;
489
+
490
+ // Get accounts to export
491
+ const getAccountsForExport = (): Account[] => {
492
+ if (activeModal === "export-selected") {
493
+ return accounts.filter(acc => checkedEmails.has(acc.email));
494
+ }
495
+ return accounts;
496
+ };
497
+
498
+ // Settings nav helpers
499
+ const currentSettingsItem = settingsNavItems[settingsNavIndex];
500
+ const isOnSection = (id: string) => currentSettingsItem?.type === "section" && currentSettingsItem.id === id;
501
+
502
+ // Render modals
503
+ if (activeModal === "export" || activeModal === "export-selected") {
504
+ return (
505
+ <Box flexDirection="column" padding={1}>
506
+ <ExportModal
507
+ accounts={getAccountsForExport()}
508
+ onComplete={handleExportComplete}
509
+ onCancel={() => setActiveModal("none")}
510
+ />
511
+ </Box>
512
+ );
513
+ }
514
+
515
+ if (activeModal === "import") {
516
+ return (
517
+ <Box flexDirection="column" padding={1}>
518
+ <ImportModal
519
+ existingAccounts={accounts}
520
+ onComplete={handleImportComplete}
521
+ onCancel={() => setActiveModal("none")}
522
+ />
523
+ </Box>
524
+ );
525
+ }
526
+
527
+ return (
528
+ <Box flexDirection="column" padding={1}>
529
+ <Header title="OpenCode Account Manager" subtitle={activeTab === "dashboard" ? "Dashboard" : "Settings"} />
530
+
531
+ {/* Tab bar */}
532
+ <Box marginBottom={1}>
533
+ <Text
534
+ inverse={activeTab === "dashboard"}
535
+ bold={activeTab === "dashboard"}
536
+ >
537
+ {" DASHBOARD "}
538
+ </Text>
539
+ <Text> </Text>
540
+ <Text
541
+ inverse={activeTab === "settings"}
542
+ bold={activeTab === "settings"}
543
+ >
544
+ {" SETTINGS "}
545
+ </Text>
546
+ <Text dimColor> [Tab]</Text>
547
+ </Box>
548
+
481
549
  {/* Global Stats */}
482
550
  <StatsRow
483
551
  stats={[
484
552
  { label: "Accounts", value: summary.total, color: "white" },
485
553
  { label: "Available", value: summary.available, color: "white" },
486
554
  { label: "Limited", value: summary.limited, color: "gray" },
555
+ {
556
+ label: "Need Verify",
557
+ value: Object.values(healthResults).filter(r => r.status === "verification_required").length,
558
+ color: "yellow"
559
+ },
487
560
  { label: "Providers", value: configSummary?.providers || 0, color: "white" },
488
561
  { label: "MCP", value: configSummary?.mcpEnabled || 0, color: "white" },
489
562
  ]}
490
563
  />
491
564
 
565
+
492
566
  {/* Help bar */}
493
567
  <Box marginY={1}>
494
568
  <Text dimColor>↑↓ navigate • Space select • </Text>
495
569
  <Text bold>R</Text>
496
570
  <Text dimColor> refresh • </Text>
571
+ <Text bold>H</Text>
572
+ <Text dimColor> health • </Text>
497
573
  <Text bold>P</Text>
498
574
  <Text dimColor> actions • </Text>
499
575
  <Text bold>Q</Text>
500
576
  <Text dimColor> quit</Text>
501
- {checkedEmails.size > 0 ? (
502
- <Text> • {checkedEmails.size} selected</Text>
503
- ) : null}
504
- </Box>
505
-
506
- {/* Loading indicator */}
507
- {isLoading && loadingStep ? (
508
- <Box marginBottom={1} paddingX={1}>
509
- <Text dimColor>⟳ {loadingStep}</Text>
510
- </Box>
511
- ) : null}
512
-
577
+ {checkedEmails.size > 0 ? (
578
+ <Text> • {checkedEmails.size} selected</Text>
579
+ ) : null}
580
+ </Box>
581
+
582
+ {/* Loading indicator */}
583
+ {isLoading && loadingStep ? (
584
+ <Box marginBottom={1} paddingX={1}>
585
+ <Text dimColor>⟳ {loadingStep}</Text>
586
+ </Box>
587
+ ) : null}
588
+
513
589
  {/* Tab content */}
514
590
  {activeTab === "dashboard" ? (
515
591
  // Dashboard Tab - Rate limits view
@@ -517,64 +593,67 @@ export function Dashboard({ pluginPath }: DashboardProps) {
517
593
  <DashboardView
518
594
  accounts={accounts}
519
595
  selectedIndex={dashboardIndex}
596
+ healthResults={healthResults}
520
597
  />
521
598
  </Box>
522
599
  ) : (
523
- // Settings Tab - Original sections view
524
- <>
525
- <SectionBox
526
- title="PROVIDERS"
527
- borderColor={isOnSection("providers") ? "cyan" : (expandedSection === "providers" ? "white" : "gray")}
528
- collapsed={expandedSection !== "providers"}
529
- >
530
- {opencodeInfo && <ProviderList providers={opencodeInfo.providers} />}
531
- </SectionBox>
532
600
 
533
- <SectionBox
534
- title={`ACCOUNTS (${opencodeInfo?.plugins[0]?.name || "antigravity-auth"})`}
535
- borderColor={isOnSection("accounts") || (currentSettingsItem?.type === "account") ? "cyan" : (expandedSection === "accounts" ? "white" : "gray")}
536
- collapsed={expandedSection !== "accounts"}
537
- >
601
+ // Settings Tab - Original sections view
602
+ <>
603
+ <SectionBox
604
+ title="PROVIDERS"
605
+ borderColor={isOnSection("providers") ? "white" : (expandedSection === "providers" ? "gray" : "gray")}
606
+ collapsed={expandedSection !== "providers"}
607
+ >
608
+ {opencodeInfo && <ProviderList providers={opencodeInfo.providers} />}
609
+ </SectionBox>
610
+
611
+ <SectionBox
612
+ title={`ACCOUNTS (${opencodeInfo?.plugins[0]?.name || "antigravity-auth"})`}
613
+ borderColor={isOnSection("accounts") || (currentSettingsItem?.type === "account") ? "white" : "gray"}
614
+ collapsed={expandedSection !== "accounts"}
615
+ >
538
616
  <AccountList
539
617
  accounts={accounts}
540
618
  selectedIndex={currentSettingsItem?.type === "account" ? (currentSettingsItem.index ?? -1) : -1}
541
619
  checkedEmails={checkedEmails}
542
620
  showCheckbox={true}
621
+ healthResults={healthResults}
543
622
  />
544
- </SectionBox>
545
-
546
- <SectionBox
547
- title="MCP SERVERS"
548
- borderColor={isOnSection("mcp") ? "cyan" : (expandedSection === "mcp" ? "white" : "gray")}
549
- collapsed={expandedSection !== "mcp"}
550
- >
551
- {opencodeInfo && <McpServerList servers={opencodeInfo.mcpServers} />}
552
- </SectionBox>
553
- </>
554
- )}
555
-
556
- {/* Config path */}
557
- <Box marginTop={1}>
558
- <Text dimColor>Config: {opencodeInfo?.configPath || "N/A"}</Text>
559
- </Box>
560
-
561
- {/* Message */}
562
- {message ? (
563
- <Box marginTop={1}>
564
- <Text dimColor>→ {message}</Text>
565
- </Box>
566
- ) : null}
567
-
568
- {/* Action Palette overlay */}
569
- {activeModal === "palette" && (
570
- <Box position="absolute" marginTop={3} marginLeft={10}>
571
- <ActionPalette
572
- actions={paletteActions}
573
- onSelect={handlePaletteAction}
574
- onClose={() => setActiveModal("none")}
575
- />
576
- </Box>
577
- )}
578
- </Box>
579
- );
580
- }
623
+ </SectionBox>
624
+
625
+ <SectionBox
626
+ title="MCP SERVERS"
627
+ borderColor={isOnSection("mcp") ? "white" : "gray"}
628
+ collapsed={expandedSection !== "mcp"}
629
+ >
630
+ {opencodeInfo && <McpServerList servers={opencodeInfo.mcpServers} />}
631
+ </SectionBox>
632
+ </>
633
+ )}
634
+
635
+ {/* Config path */}
636
+ <Box marginTop={1}>
637
+ <Text dimColor>Config: {opencodeInfo?.configPath || "N/A"}</Text>
638
+ </Box>
639
+
640
+ {/* Message */}
641
+ {message ? (
642
+ <Box marginTop={1}>
643
+ <Text dimColor>→ {message}</Text>
644
+ </Box>
645
+ ) : null}
646
+
647
+ {/* Action Palette overlay */}
648
+ {activeModal === "palette" && (
649
+ <Box position="absolute" marginTop={3} marginLeft={10}>
650
+ <ActionPalette
651
+ actions={paletteActions}
652
+ onSelect={handlePaletteAction}
653
+ onClose={() => setActiveModal("none")}
654
+ />
655
+ </Box>
656
+ )}
657
+ </Box>
658
+ );
659
+ }