opencode-account-manager 0.4.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/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +183 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/accounts.d.ts +19 -0
- package/dist/core/accounts.d.ts.map +1 -0
- package/dist/core/accounts.js +181 -0
- package/dist/core/accounts.js.map +1 -0
- package/dist/core/config-store.d.ts +48 -0
- package/dist/core/config-store.d.ts.map +1 -0
- package/dist/core/config-store.js +206 -0
- package/dist/core/config-store.js.map +1 -0
- package/dist/core/crypto.d.ts +40 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +172 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/importers/amJson.d.ts +17 -0
- package/dist/core/importers/amJson.d.ts.map +1 -0
- package/dist/core/importers/amJson.js +131 -0
- package/dist/core/importers/amJson.js.map +1 -0
- package/dist/core/opencode-config.d.ts +92 -0
- package/dist/core/opencode-config.d.ts.map +1 -0
- package/dist/core/opencode-config.js +148 -0
- package/dist/core/opencode-config.js.map +1 -0
- package/dist/core/paths.d.ts +5 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/types.d.ts +74 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +30 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/utils.d.ts +5 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +35 -0
- package/dist/core/utils.js.map +1 -0
- package/dist/tui/Dashboard.d.ts +7 -0
- package/dist/tui/Dashboard.d.ts.map +1 -0
- package/dist/tui/Dashboard.js +331 -0
- package/dist/tui/Dashboard.js.map +1 -0
- package/dist/tui/components/AccountList.d.ts +18 -0
- package/dist/tui/components/AccountList.d.ts.map +1 -0
- package/dist/tui/components/AccountList.js +92 -0
- package/dist/tui/components/AccountList.js.map +1 -0
- package/dist/tui/components/Box.d.ts +11 -0
- package/dist/tui/components/Box.d.ts.map +1 -0
- package/dist/tui/components/Box.js +15 -0
- package/dist/tui/components/Box.js.map +1 -0
- package/dist/tui/components/ExportModal.d.ts +10 -0
- package/dist/tui/components/ExportModal.d.ts.map +1 -0
- package/dist/tui/components/ExportModal.js +192 -0
- package/dist/tui/components/ExportModal.js.map +1 -0
- package/dist/tui/components/FileBrowser.d.ts +12 -0
- package/dist/tui/components/FileBrowser.d.ts.map +1 -0
- package/dist/tui/components/FileBrowser.js +349 -0
- package/dist/tui/components/FileBrowser.js.map +1 -0
- package/dist/tui/components/Header.d.ts +8 -0
- package/dist/tui/components/Header.d.ts.map +1 -0
- package/dist/tui/components/Header.js +20 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/ImportModal.d.ts +10 -0
- package/dist/tui/components/ImportModal.d.ts.map +1 -0
- package/dist/tui/components/ImportModal.js +215 -0
- package/dist/tui/components/ImportModal.js.map +1 -0
- package/dist/tui/components/McpServerList.d.ts +8 -0
- package/dist/tui/components/McpServerList.d.ts.map +1 -0
- package/dist/tui/components/McpServerList.js +35 -0
- package/dist/tui/components/McpServerList.js.map +1 -0
- package/dist/tui/components/Menu.d.ts +10 -0
- package/dist/tui/components/Menu.d.ts.map +1 -0
- package/dist/tui/components/Menu.js +83 -0
- package/dist/tui/components/Menu.js.map +1 -0
- package/dist/tui/components/PasswordInput.d.ts +12 -0
- package/dist/tui/components/PasswordInput.d.ts.map +1 -0
- package/dist/tui/components/PasswordInput.js +130 -0
- package/dist/tui/components/PasswordInput.js.map +1 -0
- package/dist/tui/components/ProviderList.d.ts +8 -0
- package/dist/tui/components/ProviderList.d.ts.map +1 -0
- package/dist/tui/components/ProviderList.js +37 -0
- package/dist/tui/components/ProviderList.js.map +1 -0
- package/dist/tui/components/SectionBox.d.ts +10 -0
- package/dist/tui/components/SectionBox.d.ts.map +1 -0
- package/dist/tui/components/SectionBox.js +16 -0
- package/dist/tui/components/SectionBox.js.map +1 -0
- package/dist/tui/components/StatsRow.d.ts +13 -0
- package/dist/tui/components/StatsRow.d.ts.map +1 -0
- package/dist/tui/components/StatsRow.js +18 -0
- package/dist/tui/components/StatsRow.js.map +1 -0
- package/dist/tui/components/StatusBadge.d.ts +8 -0
- package/dist/tui/components/StatusBadge.d.ts.map +1 -0
- package/dist/tui/components/StatusBadge.js +30 -0
- package/dist/tui/components/StatusBadge.js.map +1 -0
- package/dist/tui/components/index.d.ts +14 -0
- package/dist/tui/components/index.d.ts.map +1 -0
- package/dist/tui/components/index.js +32 -0
- package/dist/tui/components/index.js.map +1 -0
- package/dist/tui/index.d.ts +5 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +13 -0
- package/dist/tui/index.js.map +1 -0
- package/docs/BLUEPRINT.md +476 -0
- package/docs/ROADMAP.md +74 -0
- package/package.json +38 -0
- package/src/cli.ts +207 -0
- package/src/core/accounts.ts +215 -0
- package/src/core/config-store.ts +212 -0
- package/src/core/crypto.ts +162 -0
- package/src/core/importers/amJson.ts +185 -0
- package/src/core/opencode-config.ts +217 -0
- package/src/core/paths.ts +32 -0
- package/src/core/types.ts +118 -0
- package/src/core/utils.ts +28 -0
- package/src/tui/Dashboard.tsx +431 -0
- package/src/tui/components/AccountList.tsx +155 -0
- package/src/tui/components/Box.tsx +37 -0
- package/src/tui/components/ExportModal.tsx +255 -0
- package/src/tui/components/FileBrowser.tsx +393 -0
- package/src/tui/components/Header.tsx +26 -0
- package/src/tui/components/ImportModal.tsx +288 -0
- package/src/tui/components/McpServerList.tsx +67 -0
- package/src/tui/components/Menu.tsx +103 -0
- package/src/tui/components/PasswordInput.tsx +159 -0
- package/src/tui/components/ProviderList.tsx +61 -0
- package/src/tui/components/SectionBox.tsx +35 -0
- package/src/tui/components/StatsRow.tsx +33 -0
- package/src/tui/components/StatusBadge.tsx +33 -0
- package/src/tui/components/index.ts +13 -0
- package/src/tui/index.tsx +11 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function readJsonFile<T>(filePath: string): T {
|
|
5
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
6
|
+
return JSON.parse(raw) as T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function writeJsonFile(filePath: string, data: unknown): void {
|
|
10
|
+
const dir = path.dirname(filePath);
|
|
11
|
+
if (!fs.existsSync(dir)) {
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
const raw = JSON.stringify(data, null, 2);
|
|
15
|
+
fs.writeFileSync(filePath, raw, "utf8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function safeParseJson(input: string): unknown {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(input);
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function toLowerTrim(input: string): string {
|
|
27
|
+
return input.trim().toLowerCase();
|
|
28
|
+
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import {
|
|
4
|
+
Header,
|
|
5
|
+
StatsRow,
|
|
6
|
+
AccountList,
|
|
7
|
+
MenuBar,
|
|
8
|
+
MenuAction,
|
|
9
|
+
ProviderList,
|
|
10
|
+
McpServerList,
|
|
11
|
+
SectionBox,
|
|
12
|
+
ExportModal,
|
|
13
|
+
ImportModal,
|
|
14
|
+
} from "./components";
|
|
15
|
+
import {
|
|
16
|
+
readPluginAccountsFile,
|
|
17
|
+
createEmptyPluginAccountsFile,
|
|
18
|
+
summarizeAccounts,
|
|
19
|
+
mergeAccounts,
|
|
20
|
+
writePluginAccountsFile,
|
|
21
|
+
} from "../core/accounts";
|
|
22
|
+
import { getPluginAccountsPath, getAmFolderPath } from "../core/paths";
|
|
23
|
+
import { Account, PluginAccountsFile } from "../core/types";
|
|
24
|
+
import { importFromAmFolder } from "../core/importers/amJson";
|
|
25
|
+
import {
|
|
26
|
+
parseOpencodeInfo,
|
|
27
|
+
getConfigSummary,
|
|
28
|
+
OpencodeInfo,
|
|
29
|
+
} from "../core/opencode-config";
|
|
30
|
+
|
|
31
|
+
interface DashboardProps {
|
|
32
|
+
pluginPath?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ActiveSection = "providers" | "accounts" | "mcp";
|
|
36
|
+
type ModalType = "none" | "export" | "import" | "export-selected";
|
|
37
|
+
|
|
38
|
+
function safeReadPluginFile(pluginPath: string): PluginAccountsFile {
|
|
39
|
+
try {
|
|
40
|
+
return readPluginAccountsFile(pluginPath);
|
|
41
|
+
} catch {
|
|
42
|
+
return createEmptyPluginAccountsFile();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function Dashboard({ pluginPath }: DashboardProps) {
|
|
47
|
+
const { exit } = useApp();
|
|
48
|
+
const resolvedPath = getPluginAccountsPath(pluginPath);
|
|
49
|
+
|
|
50
|
+
// OpenCode config state
|
|
51
|
+
const [opencodeInfo, setOpencodeInfo] = useState<OpencodeInfo | null>(null);
|
|
52
|
+
|
|
53
|
+
// Plugin accounts state
|
|
54
|
+
const [accounts, setAccounts] = useState<Account[]>([]);
|
|
55
|
+
const [summary, setSummary] = useState({ total: 0, available: 0, limited: 0 });
|
|
56
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
57
|
+
|
|
58
|
+
// UI state
|
|
59
|
+
const [activeSection, setActiveSection] = useState<ActiveSection>("providers");
|
|
60
|
+
const [selectMode, setSelectMode] = useState(false);
|
|
61
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
62
|
+
const [checkedEmails, setCheckedEmails] = useState<Set<string>>(new Set());
|
|
63
|
+
|
|
64
|
+
// Modal state
|
|
65
|
+
const [activeModal, setActiveModal] = useState<ModalType>("none");
|
|
66
|
+
|
|
67
|
+
const showMessage = (msg: string, duration = 3000) => {
|
|
68
|
+
setMessage(msg);
|
|
69
|
+
setTimeout(() => setMessage(null), duration);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const loadOpencodeConfig = () => {
|
|
73
|
+
const info = parseOpencodeInfo();
|
|
74
|
+
setOpencodeInfo(info);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const loadAccounts = () => {
|
|
78
|
+
const file = safeReadPluginFile(resolvedPath);
|
|
79
|
+
setAccounts(file.accounts);
|
|
80
|
+
setSummary(summarizeAccounts(file.accounts));
|
|
81
|
+
setCheckedEmails(new Set());
|
|
82
|
+
setSelectedIndex(0);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const refresh = () => {
|
|
86
|
+
loadOpencodeConfig();
|
|
87
|
+
loadAccounts();
|
|
88
|
+
showMessage("Refreshed", 2000);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
loadOpencodeConfig();
|
|
93
|
+
loadAccounts();
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
// Keyboard navigation (only when no modal is open)
|
|
97
|
+
useInput((input, key) => {
|
|
98
|
+
if (activeModal !== "none") return;
|
|
99
|
+
|
|
100
|
+
// Section switching with Tab
|
|
101
|
+
if (key.tab) {
|
|
102
|
+
setActiveSection(prev => {
|
|
103
|
+
if (prev === "providers") return "accounts";
|
|
104
|
+
if (prev === "accounts") return "mcp";
|
|
105
|
+
return "providers";
|
|
106
|
+
});
|
|
107
|
+
setSelectMode(false);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Only handle list navigation in accounts section with select mode
|
|
112
|
+
if (activeSection !== "accounts" || !selectMode) return;
|
|
113
|
+
|
|
114
|
+
if (key.upArrow) {
|
|
115
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
116
|
+
}
|
|
117
|
+
if (key.downArrow) {
|
|
118
|
+
setSelectedIndex(prev => Math.min(accounts.length - 1, prev + 1));
|
|
119
|
+
}
|
|
120
|
+
if (input === " ") {
|
|
121
|
+
const email = accounts[selectedIndex]?.email;
|
|
122
|
+
if (email) {
|
|
123
|
+
setCheckedEmails(prev => {
|
|
124
|
+
const next = new Set(prev);
|
|
125
|
+
if (next.has(email)) {
|
|
126
|
+
next.delete(email);
|
|
127
|
+
} else {
|
|
128
|
+
next.add(email);
|
|
129
|
+
}
|
|
130
|
+
return next;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Export completion handler
|
|
137
|
+
const handleExportComplete = (filePath: string) => {
|
|
138
|
+
setActiveModal("none");
|
|
139
|
+
showMessage(`Exported to ${filePath}`, 4000);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Import completion handler
|
|
143
|
+
const handleImportComplete = (importedAccounts: Account[], newCount: number, overwrittenCount: number) => {
|
|
144
|
+
// Merge imported accounts with existing (overwrite mode)
|
|
145
|
+
const file = safeReadPluginFile(resolvedPath);
|
|
146
|
+
const merged = mergeAccounts(file, importedAccounts, "merge");
|
|
147
|
+
writePluginAccountsFile(pluginPath, merged);
|
|
148
|
+
|
|
149
|
+
setActiveModal("none");
|
|
150
|
+
loadAccounts();
|
|
151
|
+
showMessage(`Imported: ${newCount} new, ${overwrittenCount} updated`, 4000);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleImportAM = () => {
|
|
155
|
+
const amPath = getAmFolderPath();
|
|
156
|
+
const result = importFromAmFolder(amPath);
|
|
157
|
+
|
|
158
|
+
if (result.errors.length > 0) {
|
|
159
|
+
showMessage(`Error: ${result.errors[0]}`, 5000);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (result.accounts.length === 0) {
|
|
164
|
+
showMessage(`No accounts found in AM (${result.skipped.length} skipped)`, 4000);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const existingFile = safeReadPluginFile(resolvedPath);
|
|
169
|
+
const merged = mergeAccounts(existingFile, result.accounts, "merge");
|
|
170
|
+
writePluginAccountsFile(pluginPath, merged);
|
|
171
|
+
|
|
172
|
+
const added = merged.accounts.length - existingFile.accounts.length;
|
|
173
|
+
showMessage(
|
|
174
|
+
`Imported from AM: ${result.accounts.length} found, ${added} new. Total: ${merged.accounts.length}`,
|
|
175
|
+
5000
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
loadAccounts();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleEnableSelected = () => {
|
|
182
|
+
if (checkedEmails.size === 0) {
|
|
183
|
+
showMessage("No accounts selected", 2000);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const file = safeReadPluginFile(resolvedPath);
|
|
188
|
+
let count = 0;
|
|
189
|
+
|
|
190
|
+
file.accounts = file.accounts.map(acc => {
|
|
191
|
+
if (checkedEmails.has(acc.email)) {
|
|
192
|
+
count++;
|
|
193
|
+
return { ...acc, enabled: true };
|
|
194
|
+
}
|
|
195
|
+
return acc;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
writePluginAccountsFile(pluginPath, file);
|
|
199
|
+
showMessage(`Enabled ${count} accounts`, 3000);
|
|
200
|
+
loadAccounts();
|
|
201
|
+
setSelectMode(false);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleDisableSelected = () => {
|
|
205
|
+
if (checkedEmails.size === 0) {
|
|
206
|
+
showMessage("No accounts selected", 2000);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const file = safeReadPluginFile(resolvedPath);
|
|
211
|
+
let count = 0;
|
|
212
|
+
|
|
213
|
+
file.accounts = file.accounts.map(acc => {
|
|
214
|
+
if (checkedEmails.has(acc.email)) {
|
|
215
|
+
count++;
|
|
216
|
+
return { ...acc, enabled: false };
|
|
217
|
+
}
|
|
218
|
+
return acc;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
writePluginAccountsFile(pluginPath, file);
|
|
222
|
+
showMessage(`Disabled ${count} accounts`, 3000);
|
|
223
|
+
loadAccounts();
|
|
224
|
+
setSelectMode(false);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleDeleteSelected = () => {
|
|
228
|
+
if (checkedEmails.size === 0) {
|
|
229
|
+
showMessage("No accounts selected", 2000);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const file = safeReadPluginFile(resolvedPath);
|
|
234
|
+
const beforeCount = file.accounts.length;
|
|
235
|
+
|
|
236
|
+
file.accounts = file.accounts.filter(acc => !checkedEmails.has(acc.email));
|
|
237
|
+
const deletedCount = beforeCount - file.accounts.length;
|
|
238
|
+
|
|
239
|
+
writePluginAccountsFile(pluginPath, file);
|
|
240
|
+
showMessage(`Deleted ${deletedCount} accounts`, 3000);
|
|
241
|
+
loadAccounts();
|
|
242
|
+
setSelectMode(false);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handleSelectAll = () => {
|
|
246
|
+
setCheckedEmails(new Set(accounts.map(a => a.email)));
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const handleSelectNone = () => {
|
|
250
|
+
setCheckedEmails(new Set());
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const handleAction = (action: MenuAction) => {
|
|
254
|
+
// Don't handle actions when modal is open
|
|
255
|
+
if (activeModal !== "none") return;
|
|
256
|
+
|
|
257
|
+
switch (action) {
|
|
258
|
+
case "refresh":
|
|
259
|
+
refresh();
|
|
260
|
+
break;
|
|
261
|
+
case "export":
|
|
262
|
+
setActiveModal("export");
|
|
263
|
+
break;
|
|
264
|
+
case "import-file":
|
|
265
|
+
setActiveModal("import");
|
|
266
|
+
break;
|
|
267
|
+
case "import-am":
|
|
268
|
+
handleImportAM();
|
|
269
|
+
break;
|
|
270
|
+
case "toggle-select-mode":
|
|
271
|
+
if (activeSection === "accounts") {
|
|
272
|
+
setSelectMode(prev => !prev);
|
|
273
|
+
setCheckedEmails(new Set());
|
|
274
|
+
setSelectedIndex(0);
|
|
275
|
+
} else {
|
|
276
|
+
showMessage("Switch to Accounts section first (Tab)", 2000);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
case "select-all":
|
|
280
|
+
handleSelectAll();
|
|
281
|
+
break;
|
|
282
|
+
case "select-none":
|
|
283
|
+
handleSelectNone();
|
|
284
|
+
break;
|
|
285
|
+
case "enable-selected":
|
|
286
|
+
handleEnableSelected();
|
|
287
|
+
break;
|
|
288
|
+
case "disable-selected":
|
|
289
|
+
handleDisableSelected();
|
|
290
|
+
break;
|
|
291
|
+
case "delete-selected":
|
|
292
|
+
handleDeleteSelected();
|
|
293
|
+
break;
|
|
294
|
+
case "export-selected":
|
|
295
|
+
if (checkedEmails.size === 0) {
|
|
296
|
+
showMessage("No accounts selected", 2000);
|
|
297
|
+
} else {
|
|
298
|
+
setActiveModal("export-selected");
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
case "quit":
|
|
302
|
+
exit();
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Calculate stats
|
|
308
|
+
const configSummary = opencodeInfo ? getConfigSummary(opencodeInfo) : null;
|
|
309
|
+
|
|
310
|
+
// Get accounts to export (all or selected)
|
|
311
|
+
const getAccountsForExport = (): Account[] => {
|
|
312
|
+
if (activeModal === "export-selected") {
|
|
313
|
+
return accounts.filter(acc => checkedEmails.has(acc.email));
|
|
314
|
+
}
|
|
315
|
+
return accounts;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// If modal is open, render only the modal
|
|
319
|
+
if (activeModal === "export" || activeModal === "export-selected") {
|
|
320
|
+
return (
|
|
321
|
+
<Box flexDirection="column" padding={1}>
|
|
322
|
+
<ExportModal
|
|
323
|
+
accounts={getAccountsForExport()}
|
|
324
|
+
onComplete={handleExportComplete}
|
|
325
|
+
onCancel={() => setActiveModal("none")}
|
|
326
|
+
/>
|
|
327
|
+
</Box>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (activeModal === "import") {
|
|
332
|
+
return (
|
|
333
|
+
<Box flexDirection="column" padding={1}>
|
|
334
|
+
<ImportModal
|
|
335
|
+
existingAccounts={accounts}
|
|
336
|
+
onComplete={handleImportComplete}
|
|
337
|
+
onCancel={() => setActiveModal("none")}
|
|
338
|
+
/>
|
|
339
|
+
</Box>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<Box flexDirection="column" padding={1}>
|
|
345
|
+
<Header title="OpenCode Account Manager" subtitle="Dashboard" />
|
|
346
|
+
|
|
347
|
+
{/* Global Stats */}
|
|
348
|
+
<StatsRow
|
|
349
|
+
stats={[
|
|
350
|
+
{ label: "Providers", value: configSummary?.providers || 0, color: "cyan" },
|
|
351
|
+
{ label: "Models", value: configSummary?.models || 0, color: "yellow" },
|
|
352
|
+
{ label: "MCP On", value: configSummary?.mcpEnabled || 0, color: "green" },
|
|
353
|
+
{ label: "MCP Off", value: configSummary?.mcpDisabled || 0, color: "red" },
|
|
354
|
+
{ label: "Accounts", value: summary.total, color: "white" },
|
|
355
|
+
{ label: "Available", value: summary.available, color: "green" },
|
|
356
|
+
{ label: "Limited", value: summary.limited, color: "yellow" },
|
|
357
|
+
]}
|
|
358
|
+
/>
|
|
359
|
+
|
|
360
|
+
{/* Tab indicator */}
|
|
361
|
+
<Box marginY={1}>
|
|
362
|
+
<Text dimColor>Sections: </Text>
|
|
363
|
+
<Text color={activeSection === "providers" ? "cyan" : "gray"} bold={activeSection === "providers"}>
|
|
364
|
+
[1] Providers
|
|
365
|
+
</Text>
|
|
366
|
+
<Text> </Text>
|
|
367
|
+
<Text color={activeSection === "accounts" ? "cyan" : "gray"} bold={activeSection === "accounts"}>
|
|
368
|
+
[2] Accounts
|
|
369
|
+
</Text>
|
|
370
|
+
<Text> </Text>
|
|
371
|
+
<Text color={activeSection === "mcp" ? "cyan" : "gray"} bold={activeSection === "mcp"}>
|
|
372
|
+
[3] MCP
|
|
373
|
+
</Text>
|
|
374
|
+
<Text dimColor> (Tab to switch)</Text>
|
|
375
|
+
</Box>
|
|
376
|
+
|
|
377
|
+
{/* Providers Section */}
|
|
378
|
+
<SectionBox
|
|
379
|
+
title="PROVIDERS"
|
|
380
|
+
borderColor={activeSection === "providers" ? "cyan" : "gray"}
|
|
381
|
+
collapsed={activeSection !== "providers"}
|
|
382
|
+
>
|
|
383
|
+
{opencodeInfo && <ProviderList providers={opencodeInfo.providers} />}
|
|
384
|
+
</SectionBox>
|
|
385
|
+
|
|
386
|
+
{/* Plugin Accounts Section */}
|
|
387
|
+
<SectionBox
|
|
388
|
+
title={`PLUGIN ACCOUNTS (${opencodeInfo?.plugins[0]?.name || "antigravity-auth"})`}
|
|
389
|
+
borderColor={activeSection === "accounts" ? (selectMode ? "yellow" : "cyan") : "gray"}
|
|
390
|
+
collapsed={activeSection !== "accounts"}
|
|
391
|
+
>
|
|
392
|
+
<AccountList
|
|
393
|
+
accounts={accounts}
|
|
394
|
+
selectedIndex={selectMode ? selectedIndex : -1}
|
|
395
|
+
checkedEmails={checkedEmails}
|
|
396
|
+
showCheckbox={selectMode}
|
|
397
|
+
/>
|
|
398
|
+
</SectionBox>
|
|
399
|
+
|
|
400
|
+
{/* MCP Servers Section */}
|
|
401
|
+
<SectionBox
|
|
402
|
+
title="MCP SERVERS"
|
|
403
|
+
borderColor={activeSection === "mcp" ? "cyan" : "gray"}
|
|
404
|
+
collapsed={activeSection !== "mcp"}
|
|
405
|
+
>
|
|
406
|
+
{opencodeInfo && <McpServerList servers={opencodeInfo.mcpServers} />}
|
|
407
|
+
</SectionBox>
|
|
408
|
+
|
|
409
|
+
{/* Config path */}
|
|
410
|
+
<Box marginTop={1}>
|
|
411
|
+
<Text dimColor>Config: {opencodeInfo?.configPath || "N/A"}</Text>
|
|
412
|
+
</Box>
|
|
413
|
+
|
|
414
|
+
{/* Menu */}
|
|
415
|
+
<Box marginTop={1}>
|
|
416
|
+
<MenuBar
|
|
417
|
+
onSelect={handleAction}
|
|
418
|
+
selectMode={selectMode}
|
|
419
|
+
selectedCount={checkedEmails.size}
|
|
420
|
+
/>
|
|
421
|
+
</Box>
|
|
422
|
+
|
|
423
|
+
{/* Message */}
|
|
424
|
+
{message && (
|
|
425
|
+
<Box marginTop={1}>
|
|
426
|
+
<Text color="green">→ {message}</Text>
|
|
427
|
+
</Box>
|
|
428
|
+
)}
|
|
429
|
+
</Box>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { StatusBadge } from "./StatusBadge";
|
|
4
|
+
import { Account } from "../../core/types";
|
|
5
|
+
|
|
6
|
+
interface AccountRowProps {
|
|
7
|
+
account: Account;
|
|
8
|
+
isSelected?: boolean;
|
|
9
|
+
isChecked?: boolean;
|
|
10
|
+
showCheckbox?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatDuration(ms: number): string {
|
|
14
|
+
if (ms <= 0) return "-";
|
|
15
|
+
const minutes = Math.floor(ms / 60000);
|
|
16
|
+
const hours = Math.floor(minutes / 60);
|
|
17
|
+
const days = Math.floor(hours / 24);
|
|
18
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
19
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
20
|
+
return `${minutes}m`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getAccountStatus(account: Account): {
|
|
24
|
+
status: "available" | "limited" | "disabled";
|
|
25
|
+
resetIn: string;
|
|
26
|
+
limitDetails: string[];
|
|
27
|
+
} {
|
|
28
|
+
if (account.enabled === false) {
|
|
29
|
+
return { status: "disabled", resetIn: "-", limitDetails: [] };
|
|
30
|
+
}
|
|
31
|
+
const resets = account.rateLimitResetTimes || {};
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const limitDetails: string[] = [];
|
|
34
|
+
|
|
35
|
+
// Build detailed limit info for each model
|
|
36
|
+
for (const [model, resetTime] of Object.entries(resets)) {
|
|
37
|
+
if (resetTime > now) {
|
|
38
|
+
limitDetails.push(`${model}: ${formatDuration(resetTime - now)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (limitDetails.length === 0) {
|
|
43
|
+
return { status: "available", resetIn: "-", limitDetails: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const nextReset = Math.min(...Object.values(resets).filter((v) => v > now));
|
|
47
|
+
return {
|
|
48
|
+
status: "limited",
|
|
49
|
+
resetIn: formatDuration(nextReset - now),
|
|
50
|
+
limitDetails,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function AccountRow({ account, isSelected, isChecked, showCheckbox }: AccountRowProps) {
|
|
55
|
+
const { status, resetIn, limitDetails } = getAccountStatus(account);
|
|
56
|
+
const project = account.projectId || account.managedProjectId || "-";
|
|
57
|
+
|
|
58
|
+
const checkbox = showCheckbox
|
|
59
|
+
? (isChecked ? "[x]" : "[ ]")
|
|
60
|
+
: "";
|
|
61
|
+
|
|
62
|
+
const cursor = isSelected ? ">" : " ";
|
|
63
|
+
const emailColor = status === "disabled" ? "gray" : (isSelected ? "white" : "cyan");
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Box flexDirection="column">
|
|
67
|
+
<Box flexDirection="row" paddingX={1}>
|
|
68
|
+
<Box width={2}>
|
|
69
|
+
<Text color={isSelected ? "yellow" : "white"}>{cursor}</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
{showCheckbox && (
|
|
72
|
+
<Box width={4}>
|
|
73
|
+
<Text color={isChecked ? "green" : "gray"}>{checkbox}</Text>
|
|
74
|
+
</Box>
|
|
75
|
+
)}
|
|
76
|
+
<Box width={28}>
|
|
77
|
+
<Text color={emailColor} strikethrough={status === "disabled"}>
|
|
78
|
+
{account.email}
|
|
79
|
+
</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
<Box width={18}>
|
|
82
|
+
<Text dimColor>{project.slice(0, 16)}</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
<Box width={12}>
|
|
85
|
+
<StatusBadge status={status} />
|
|
86
|
+
</Box>
|
|
87
|
+
<Box width={10}>
|
|
88
|
+
<Text dimColor>{resetIn}</Text>
|
|
89
|
+
</Box>
|
|
90
|
+
</Box>
|
|
91
|
+
{status === "limited" && limitDetails.length > 0 && (
|
|
92
|
+
<Box paddingLeft={showCheckbox ? 8 : 4}>
|
|
93
|
+
<Text color="gray">
|
|
94
|
+
└─ {limitDetails.join(" | ")}
|
|
95
|
+
</Text>
|
|
96
|
+
</Box>
|
|
97
|
+
)}
|
|
98
|
+
</Box>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface AccountListProps {
|
|
103
|
+
accounts: Account[];
|
|
104
|
+
selectedIndex?: number;
|
|
105
|
+
checkedEmails?: Set<string>;
|
|
106
|
+
showCheckbox?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function AccountList({
|
|
110
|
+
accounts,
|
|
111
|
+
selectedIndex = -1,
|
|
112
|
+
checkedEmails = new Set(),
|
|
113
|
+
showCheckbox = false
|
|
114
|
+
}: AccountListProps) {
|
|
115
|
+
return (
|
|
116
|
+
<Box flexDirection="column">
|
|
117
|
+
<Box flexDirection="row" paddingX={1} marginBottom={1}>
|
|
118
|
+
<Box width={2}>
|
|
119
|
+
<Text dimColor> </Text>
|
|
120
|
+
</Box>
|
|
121
|
+
{showCheckbox && (
|
|
122
|
+
<Box width={4}>
|
|
123
|
+
<Text bold dimColor>SEL</Text>
|
|
124
|
+
</Box>
|
|
125
|
+
)}
|
|
126
|
+
<Box width={28}>
|
|
127
|
+
<Text bold dimColor>EMAIL</Text>
|
|
128
|
+
</Box>
|
|
129
|
+
<Box width={18}>
|
|
130
|
+
<Text bold dimColor>PROJECT</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
<Box width={12}>
|
|
133
|
+
<Text bold dimColor>STATUS</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
<Box width={10}>
|
|
136
|
+
<Text bold dimColor>RESET IN</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
</Box>
|
|
139
|
+
{accounts.map((account, index) => (
|
|
140
|
+
<AccountRow
|
|
141
|
+
key={account.email}
|
|
142
|
+
account={account}
|
|
143
|
+
isSelected={index === selectedIndex}
|
|
144
|
+
isChecked={checkedEmails.has(account.email)}
|
|
145
|
+
showCheckbox={showCheckbox}
|
|
146
|
+
/>
|
|
147
|
+
))}
|
|
148
|
+
{accounts.length === 0 && (
|
|
149
|
+
<Box paddingX={1}>
|
|
150
|
+
<Text dimColor>No accounts found</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
)}
|
|
153
|
+
</Box>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box as InkBox, Text } from "ink";
|
|
3
|
+
|
|
4
|
+
interface BoxProps {
|
|
5
|
+
title?: string;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
borderColor?: string;
|
|
8
|
+
width?: number | string;
|
|
9
|
+
padding?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Box({
|
|
13
|
+
title,
|
|
14
|
+
children,
|
|
15
|
+
borderColor = "cyan",
|
|
16
|
+
width,
|
|
17
|
+
padding = 1,
|
|
18
|
+
}: BoxProps) {
|
|
19
|
+
return (
|
|
20
|
+
<InkBox
|
|
21
|
+
flexDirection="column"
|
|
22
|
+
borderStyle="round"
|
|
23
|
+
borderColor={borderColor}
|
|
24
|
+
width={width}
|
|
25
|
+
paddingX={padding}
|
|
26
|
+
>
|
|
27
|
+
{title && (
|
|
28
|
+
<InkBox marginBottom={1}>
|
|
29
|
+
<Text bold color={borderColor}>
|
|
30
|
+
{title}
|
|
31
|
+
</Text>
|
|
32
|
+
</InkBox>
|
|
33
|
+
)}
|
|
34
|
+
{children}
|
|
35
|
+
</InkBox>
|
|
36
|
+
);
|
|
37
|
+
}
|