opencode-account-manager 0.6.4 → 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.
- package/README.md +235 -216
- package/README_VI.md +235 -216
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/config-store.d.ts +12 -0
- package/dist/core/config-store.d.ts.map +1 -1
- package/dist/core/config-store.js +98 -0
- package/dist/core/config-store.js.map +1 -1
- package/dist/core/health-log.d.ts +9 -0
- package/dist/core/health-log.d.ts.map +1 -0
- package/dist/core/health-log.js +154 -0
- package/dist/core/health-log.js.map +1 -0
- package/dist/core/health-oauth.d.ts +5 -0
- package/dist/core/health-oauth.d.ts.map +1 -0
- package/dist/core/health-oauth.js +147 -0
- package/dist/core/health-oauth.js.map +1 -0
- package/dist/core/health-orchestrator.d.ts +32 -0
- package/dist/core/health-orchestrator.d.ts.map +1 -0
- package/dist/core/health-orchestrator.js +148 -0
- package/dist/core/health-orchestrator.js.map +1 -0
- package/dist/core/health-utils.d.ts +15 -0
- package/dist/core/health-utils.d.ts.map +1 -0
- package/dist/core/health-utils.js +60 -0
- package/dist/core/health-utils.js.map +1 -0
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +4 -0
- package/dist/core/paths.js.map +1 -1
- package/dist/core/types.d.ts +26 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/tui/Dashboard.d.ts.map +1 -1
- package/dist/tui/Dashboard.js +69 -2
- package/dist/tui/Dashboard.js.map +1 -1
- package/dist/tui/components/AccountList.d.ts +5 -3
- package/dist/tui/components/AccountList.d.ts.map +1 -1
- package/dist/tui/components/AccountList.js +9 -3
- package/dist/tui/components/AccountList.js.map +1 -1
- package/dist/tui/components/DashboardView.d.ts +3 -2
- package/dist/tui/components/DashboardView.d.ts.map +1 -1
- package/dist/tui/components/DashboardView.js +50 -4
- package/dist/tui/components/DashboardView.js.map +1 -1
- package/dist/tui/components/HealthBadge.d.ts +9 -0
- package/dist/tui/components/HealthBadge.d.ts.map +1 -0
- package/dist/tui/components/HealthBadge.js +56 -0
- package/dist/tui/components/HealthBadge.js.map +1 -0
- package/dist/tui/components/StatusBadge.d.ts +2 -1
- package/dist/tui/components/StatusBadge.d.ts.map +1 -1
- package/dist/tui/components/StatusBadge.js +30 -2
- package/dist/tui/components/StatusBadge.js.map +1 -1
- package/dist/tui/components/index.d.ts +1 -0
- package/dist/tui/components/index.d.ts.map +1 -1
- package/dist/tui/components/index.js +3 -1
- package/dist/tui/components/index.js.map +1 -1
- package/docs/BLUEPRINT.md +476 -476
- package/docs/ROADMAP.md +125 -107
- package/package.json +36 -36
- package/src/cli.ts +139 -38
- package/src/core/config-store.ts +278 -171
- package/src/core/crypto.ts +162 -162
- package/src/core/health-log.ts +173 -0
- package/src/core/health-oauth.ts +190 -0
- package/src/core/health-orchestrator.ts +224 -0
- package/src/core/importers/amExport.ts +177 -177
- package/src/core/opencode-config.ts +217 -217
- package/src/core/paths.ts +10 -6
- package/src/core/types.ts +193 -147
- package/src/tui/Dashboard.tsx +557 -478
- package/src/tui/components/AccountList.tsx +122 -104
- package/src/tui/components/ActionPalette.tsx +117 -117
- package/src/tui/components/Box.tsx +7 -7
- package/src/tui/components/DashboardView.tsx +285 -230
- package/src/tui/components/ExportModal.tsx +255 -255
- package/src/tui/components/FileBrowser.tsx +393 -393
- package/src/tui/components/Header.tsx +26 -26
- package/src/tui/components/HealthBadge.tsx +64 -0
- package/src/tui/components/ImportModal.tsx +334 -334
- package/src/tui/components/McpServerList.tsx +67 -67
- package/src/tui/components/Menu.tsx +61 -61
- package/src/tui/components/PasswordInput.tsx +159 -159
- package/src/tui/components/ProviderList.tsx +59 -59
- package/src/tui/components/SectionBox.tsx +35 -35
- package/src/tui/components/StatsRow.tsx +33 -33
- package/src/tui/components/StatusBadge.tsx +36 -3
- package/src/tui/components/index.ts +15 -14
- package/test-minimal.js +26 -26
- package/test-with-accounts.js +58 -58
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Account, AccountHealthResult, AccountHealthStatus } from "./types";
|
|
2
|
+
import {
|
|
3
|
+
getHealthCache,
|
|
4
|
+
getHealthSettings,
|
|
5
|
+
normalizeHealthKey,
|
|
6
|
+
setHealthCacheEntry,
|
|
7
|
+
} from "./config-store";
|
|
8
|
+
import { checkAccountHealthOAuth } from "./health-oauth";
|
|
9
|
+
import { collectLogHealthResults, mergeAccountHealth } from "./health-log";
|
|
10
|
+
|
|
11
|
+
export type HealthSkipReason = "no_refresh_token" | "cooldown" | "disabled";
|
|
12
|
+
|
|
13
|
+
export interface HealthCheckOptions {
|
|
14
|
+
emails?: string[];
|
|
15
|
+
force?: boolean;
|
|
16
|
+
includeLogs?: boolean;
|
|
17
|
+
onProgress?: (current: number, total: number, message: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HealthCheckItem {
|
|
21
|
+
email: string;
|
|
22
|
+
result: AccountHealthResult;
|
|
23
|
+
skipped?: boolean;
|
|
24
|
+
skipReason?: HealthSkipReason;
|
|
25
|
+
cached?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HealthCheckResult {
|
|
29
|
+
items: HealthCheckItem[];
|
|
30
|
+
counts: {
|
|
31
|
+
total: number;
|
|
32
|
+
checked: number;
|
|
33
|
+
skipped: number;
|
|
34
|
+
cached: number;
|
|
35
|
+
byStatus: Record<AccountHealthStatus, number>;
|
|
36
|
+
};
|
|
37
|
+
timing: {
|
|
38
|
+
startedAt: number;
|
|
39
|
+
completedAt: number;
|
|
40
|
+
durationMs: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createBaseResult(message: string): AccountHealthResult {
|
|
45
|
+
return {
|
|
46
|
+
status: "not_checked",
|
|
47
|
+
source: "manual",
|
|
48
|
+
checkedAt: Date.now(),
|
|
49
|
+
message,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mergeStatusCounts(
|
|
54
|
+
counts: Record<AccountHealthStatus, number>,
|
|
55
|
+
status: AccountHealthStatus
|
|
56
|
+
) {
|
|
57
|
+
counts[status] = (counts[status] || 0) + 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runWithConcurrency<T, R>(
|
|
61
|
+
items: T[],
|
|
62
|
+
limit: number,
|
|
63
|
+
task: (item: T) => Promise<R>
|
|
64
|
+
): Promise<R[]> {
|
|
65
|
+
const results: R[] = new Array(items.length);
|
|
66
|
+
const executing = new Set<Promise<void>>();
|
|
67
|
+
const tasks: Promise<void>[] = [];
|
|
68
|
+
|
|
69
|
+
const safeLimit = Math.max(1, limit || 1);
|
|
70
|
+
|
|
71
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
72
|
+
const item = items[index];
|
|
73
|
+
const promise = (async () => {
|
|
74
|
+
results[index] = await task(item);
|
|
75
|
+
})();
|
|
76
|
+
tasks.push(promise);
|
|
77
|
+
executing.add(promise);
|
|
78
|
+
|
|
79
|
+
const clean = () => executing.delete(promise);
|
|
80
|
+
promise.finally(clean).catch(clean);
|
|
81
|
+
|
|
82
|
+
if (executing.size >= safeLimit) {
|
|
83
|
+
await Promise.race(executing);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await Promise.all(tasks);
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function checkAccountsHealth(
|
|
92
|
+
accounts: Account[],
|
|
93
|
+
options: HealthCheckOptions = {}
|
|
94
|
+
): Promise<HealthCheckResult> {
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
const includeLogs = options.includeLogs ?? true;
|
|
97
|
+
const force = options.force ?? false;
|
|
98
|
+
const settings = getHealthSettings();
|
|
99
|
+
const cache = getHealthCache();
|
|
100
|
+
const logResults = includeLogs ? collectLogHealthResults() : {};
|
|
101
|
+
|
|
102
|
+
const emailFilter = options.emails?.map((email) => normalizeHealthKey(email));
|
|
103
|
+
const filteredAccounts = emailFilter
|
|
104
|
+
? accounts.filter((acc) => emailFilter.includes(normalizeHealthKey(acc.email)))
|
|
105
|
+
: accounts;
|
|
106
|
+
|
|
107
|
+
const items: HealthCheckItem[] = [];
|
|
108
|
+
const toCheck: Array<{ email: string; refreshToken: string }> = [];
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
|
|
111
|
+
for (const account of filteredAccounts) {
|
|
112
|
+
const email = account.email;
|
|
113
|
+
const key = normalizeHealthKey(email);
|
|
114
|
+
const cached = !force ? cache[key] : undefined;
|
|
115
|
+
|
|
116
|
+
if (cached && now - cached.checkedAt <= settings.ttlMs) {
|
|
117
|
+
const merged = mergeAccountHealth(cached, logResults[key]) || cached;
|
|
118
|
+
items.push({ email, result: merged, cached: true });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!force && cached && now - cached.checkedAt < settings.cooldownMs) {
|
|
123
|
+
const base = createBaseResult("Cooldown active");
|
|
124
|
+
const merged = mergeAccountHealth(base, logResults[key]) || base;
|
|
125
|
+
items.push({
|
|
126
|
+
email,
|
|
127
|
+
result: merged,
|
|
128
|
+
skipped: true,
|
|
129
|
+
skipReason: "cooldown",
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (account.enabled === false) {
|
|
135
|
+
const base = createBaseResult("Account disabled");
|
|
136
|
+
const merged = mergeAccountHealth(base, logResults[key]) || base;
|
|
137
|
+
items.push({
|
|
138
|
+
email,
|
|
139
|
+
result: merged,
|
|
140
|
+
skipped: true,
|
|
141
|
+
skipReason: "disabled",
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!account.refreshToken) {
|
|
147
|
+
const base = createBaseResult("Missing refresh token");
|
|
148
|
+
const merged = mergeAccountHealth(base, logResults[key]) || base;
|
|
149
|
+
items.push({
|
|
150
|
+
email,
|
|
151
|
+
result: merged,
|
|
152
|
+
skipped: true,
|
|
153
|
+
skipReason: "no_refresh_token",
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
toCheck.push({ email, refreshToken: account.refreshToken });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let completed = 0;
|
|
162
|
+
const total = toCheck.length;
|
|
163
|
+
|
|
164
|
+
const checkedResults = await runWithConcurrency(
|
|
165
|
+
toCheck,
|
|
166
|
+
settings.maxConcurrency,
|
|
167
|
+
async (item) => {
|
|
168
|
+
if (options.onProgress) {
|
|
169
|
+
options.onProgress(completed, total, `Checking ${item.email}...`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const oauthResult = await checkAccountHealthOAuth(item.refreshToken);
|
|
173
|
+
const key = normalizeHealthKey(item.email);
|
|
174
|
+
const merged = mergeAccountHealth(oauthResult, logResults[key]) || oauthResult;
|
|
175
|
+
setHealthCacheEntry(item.email, merged);
|
|
176
|
+
|
|
177
|
+
completed++;
|
|
178
|
+
if (options.onProgress) {
|
|
179
|
+
options.onProgress(completed, total, `Finished ${item.email}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { email: item.email, result: merged };
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
for (const entry of checkedResults) {
|
|
187
|
+
items.push({ email: entry.email, result: entry.result });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const counts: HealthCheckResult["counts"] = {
|
|
191
|
+
total: filteredAccounts.length,
|
|
192
|
+
checked: checkedResults.length,
|
|
193
|
+
skipped: items.filter((item) => item.skipped).length,
|
|
194
|
+
cached: items.filter((item) => item.cached).length,
|
|
195
|
+
byStatus: {
|
|
196
|
+
ok: 0,
|
|
197
|
+
verification_required: 0,
|
|
198
|
+
revoked: 0,
|
|
199
|
+
disabled: 0,
|
|
200
|
+
deleted: 0,
|
|
201
|
+
password_changed: 0,
|
|
202
|
+
network_error: 0,
|
|
203
|
+
unknown_error: 0,
|
|
204
|
+
not_checked: 0,
|
|
205
|
+
not_configured: 0,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const item of items) {
|
|
210
|
+
mergeStatusCounts(counts.byStatus, item.result.status);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const completedAt = Date.now();
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
items,
|
|
217
|
+
counts,
|
|
218
|
+
timing: {
|
|
219
|
+
startedAt,
|
|
220
|
+
completedAt,
|
|
221
|
+
durationMs: completedAt - startedAt,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Import accounts from Antigravity Manager EXPORTED files
|
|
3
|
-
*
|
|
4
|
-
* AM Export Format (from app export button):
|
|
5
|
-
* [
|
|
6
|
-
* { "email": "xxx@gmail.com", "refresh_token": "1//..." },
|
|
7
|
-
* ...
|
|
8
|
-
* ]
|
|
9
|
-
*
|
|
10
|
-
* This is different from AM folder structure (accounts.json + accounts/*.json)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import fs from "fs";
|
|
14
|
-
import { Account } from "../types";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Single entry in AM export file
|
|
18
|
-
*/
|
|
19
|
-
export interface AMExportEntry {
|
|
20
|
-
email: string;
|
|
21
|
-
refresh_token: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Type guard to check if data is an AM export file (array of entries)
|
|
26
|
-
*/
|
|
27
|
-
export function isAMExportFile(data: unknown): data is AMExportEntry[] {
|
|
28
|
-
if (!Array.isArray(data)) return false;
|
|
29
|
-
if (data.length === 0) return true; // Empty array is valid
|
|
30
|
-
|
|
31
|
-
// Check first few entries to be sure
|
|
32
|
-
const sample = data.slice(0, 3);
|
|
33
|
-
return sample.every(item =>
|
|
34
|
-
typeof item === "object" &&
|
|
35
|
-
item !== null &&
|
|
36
|
-
typeof (item as Record<string, unknown>).email === "string" &&
|
|
37
|
-
typeof (item as Record<string, unknown>).refresh_token === "string"
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Generate a new fingerprint for imported accounts
|
|
43
|
-
*/
|
|
44
|
-
function generateFingerprint() {
|
|
45
|
-
const randomHex = (len: number) => {
|
|
46
|
-
let result = "";
|
|
47
|
-
for (let i = 0; i < len; i++) {
|
|
48
|
-
result += Math.floor(Math.random() * 16).toString(16);
|
|
49
|
-
}
|
|
50
|
-
return result;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const platforms = ["win32/x64", "win32/arm64", "darwin/x64", "darwin/arm64"];
|
|
54
|
-
const ides = ["ANDROID_STUDIO", "INTELLIJ", "IDE_UNSPECIFIED"];
|
|
55
|
-
const clients = [
|
|
56
|
-
"google-cloud-sdk android-studio/2024.1",
|
|
57
|
-
"google-cloud-sdk intellij/2024.1",
|
|
58
|
-
"google-cloud-sdk vscode/1.87.0",
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const platform = platforms[Math.floor(Math.random() * platforms.length)];
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
deviceId: crypto.randomUUID(),
|
|
65
|
-
sessionToken: randomHex(32),
|
|
66
|
-
userAgent: `antigravity/1.15.8 ${platform}`,
|
|
67
|
-
apiClient: clients[Math.floor(Math.random() * clients.length)],
|
|
68
|
-
clientMetadata: {
|
|
69
|
-
ideType: ides[Math.floor(Math.random() * ides.length)],
|
|
70
|
-
platform: platform.startsWith("darwin") ? "MACOS" : "WINDOWS",
|
|
71
|
-
pluginType: "GEMINI",
|
|
72
|
-
osVersion: platform.startsWith("darwin") ? "14.2.1" : "10.0.19042",
|
|
73
|
-
arch: platform.split("/")[1],
|
|
74
|
-
sqmId: `{${crypto.randomUUID().toUpperCase()}}`,
|
|
75
|
-
},
|
|
76
|
-
quotaUser: `device-${randomHex(16)}`,
|
|
77
|
-
createdAt: Date.now(),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface ImportFromAMExportResult {
|
|
82
|
-
accounts: Account[];
|
|
83
|
-
skipped: string[];
|
|
84
|
-
errors: string[];
|
|
85
|
-
source: "am-export";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Import accounts from AM export file content
|
|
90
|
-
*/
|
|
91
|
-
export function importFromAMExportContent(entries: AMExportEntry[]): ImportFromAMExportResult {
|
|
92
|
-
const result: ImportFromAMExportResult = {
|
|
93
|
-
accounts: [],
|
|
94
|
-
skipped: [],
|
|
95
|
-
errors: [],
|
|
96
|
-
source: "am-export",
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
for (const entry of entries) {
|
|
100
|
-
// Validate email
|
|
101
|
-
if (!entry.email || !entry.email.includes("@")) {
|
|
102
|
-
result.skipped.push(`Invalid email: ${entry.email || "(empty)"}`);
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Validate refresh_token
|
|
107
|
-
if (!entry.refresh_token || !entry.refresh_token.startsWith("1//")) {
|
|
108
|
-
result.skipped.push(`${entry.email} (invalid or missing refresh_token)`);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Convert to Plugin account format
|
|
113
|
-
const account: Account = {
|
|
114
|
-
email: entry.email.trim(),
|
|
115
|
-
refreshToken: entry.refresh_token,
|
|
116
|
-
addedAt: Date.now(),
|
|
117
|
-
lastUsed: Date.now(),
|
|
118
|
-
fingerprint: generateFingerprint(),
|
|
119
|
-
enabled: true,
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
result.accounts.push(account);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Import accounts from AM export file path
|
|
130
|
-
*/
|
|
131
|
-
export function importFromAMExportFile(filePath: string): ImportFromAMExportResult {
|
|
132
|
-
const result: ImportFromAMExportResult = {
|
|
133
|
-
accounts: [],
|
|
134
|
-
skipped: [],
|
|
135
|
-
errors: [],
|
|
136
|
-
source: "am-export",
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// Check if file exists
|
|
140
|
-
if (!fs.existsSync(filePath)) {
|
|
141
|
-
result.errors.push(`File not found: ${filePath}`);
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Read and parse file
|
|
146
|
-
let data: unknown;
|
|
147
|
-
try {
|
|
148
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
149
|
-
data = JSON.parse(content);
|
|
150
|
-
} catch (err) {
|
|
151
|
-
result.errors.push(`Failed to parse file: ${err}`);
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Validate format
|
|
156
|
-
if (!isAMExportFile(data)) {
|
|
157
|
-
result.errors.push("Invalid AM export format. Expected array of {email, refresh_token}");
|
|
158
|
-
return result;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return importFromAMExportContent(data);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Check if a file is an AM export file (by reading and checking format)
|
|
166
|
-
*/
|
|
167
|
-
export function isAMExportFilePath(filePath: string): boolean {
|
|
168
|
-
if (!fs.existsSync(filePath)) return false;
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
172
|
-
const data = JSON.parse(content);
|
|
173
|
-
return isAMExportFile(data);
|
|
174
|
-
} catch {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Import accounts from Antigravity Manager EXPORTED files
|
|
3
|
+
*
|
|
4
|
+
* AM Export Format (from app export button):
|
|
5
|
+
* [
|
|
6
|
+
* { "email": "xxx@gmail.com", "refresh_token": "1//..." },
|
|
7
|
+
* ...
|
|
8
|
+
* ]
|
|
9
|
+
*
|
|
10
|
+
* This is different from AM folder structure (accounts.json + accounts/*.json)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import { Account } from "../types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Single entry in AM export file
|
|
18
|
+
*/
|
|
19
|
+
export interface AMExportEntry {
|
|
20
|
+
email: string;
|
|
21
|
+
refresh_token: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Type guard to check if data is an AM export file (array of entries)
|
|
26
|
+
*/
|
|
27
|
+
export function isAMExportFile(data: unknown): data is AMExportEntry[] {
|
|
28
|
+
if (!Array.isArray(data)) return false;
|
|
29
|
+
if (data.length === 0) return true; // Empty array is valid
|
|
30
|
+
|
|
31
|
+
// Check first few entries to be sure
|
|
32
|
+
const sample = data.slice(0, 3);
|
|
33
|
+
return sample.every(item =>
|
|
34
|
+
typeof item === "object" &&
|
|
35
|
+
item !== null &&
|
|
36
|
+
typeof (item as Record<string, unknown>).email === "string" &&
|
|
37
|
+
typeof (item as Record<string, unknown>).refresh_token === "string"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate a new fingerprint for imported accounts
|
|
43
|
+
*/
|
|
44
|
+
function generateFingerprint() {
|
|
45
|
+
const randomHex = (len: number) => {
|
|
46
|
+
let result = "";
|
|
47
|
+
for (let i = 0; i < len; i++) {
|
|
48
|
+
result += Math.floor(Math.random() * 16).toString(16);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const platforms = ["win32/x64", "win32/arm64", "darwin/x64", "darwin/arm64"];
|
|
54
|
+
const ides = ["ANDROID_STUDIO", "INTELLIJ", "IDE_UNSPECIFIED"];
|
|
55
|
+
const clients = [
|
|
56
|
+
"google-cloud-sdk android-studio/2024.1",
|
|
57
|
+
"google-cloud-sdk intellij/2024.1",
|
|
58
|
+
"google-cloud-sdk vscode/1.87.0",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const platform = platforms[Math.floor(Math.random() * platforms.length)];
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
deviceId: crypto.randomUUID(),
|
|
65
|
+
sessionToken: randomHex(32),
|
|
66
|
+
userAgent: `antigravity/1.15.8 ${platform}`,
|
|
67
|
+
apiClient: clients[Math.floor(Math.random() * clients.length)],
|
|
68
|
+
clientMetadata: {
|
|
69
|
+
ideType: ides[Math.floor(Math.random() * ides.length)],
|
|
70
|
+
platform: platform.startsWith("darwin") ? "MACOS" : "WINDOWS",
|
|
71
|
+
pluginType: "GEMINI",
|
|
72
|
+
osVersion: platform.startsWith("darwin") ? "14.2.1" : "10.0.19042",
|
|
73
|
+
arch: platform.split("/")[1],
|
|
74
|
+
sqmId: `{${crypto.randomUUID().toUpperCase()}}`,
|
|
75
|
+
},
|
|
76
|
+
quotaUser: `device-${randomHex(16)}`,
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ImportFromAMExportResult {
|
|
82
|
+
accounts: Account[];
|
|
83
|
+
skipped: string[];
|
|
84
|
+
errors: string[];
|
|
85
|
+
source: "am-export";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Import accounts from AM export file content
|
|
90
|
+
*/
|
|
91
|
+
export function importFromAMExportContent(entries: AMExportEntry[]): ImportFromAMExportResult {
|
|
92
|
+
const result: ImportFromAMExportResult = {
|
|
93
|
+
accounts: [],
|
|
94
|
+
skipped: [],
|
|
95
|
+
errors: [],
|
|
96
|
+
source: "am-export",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
// Validate email
|
|
101
|
+
if (!entry.email || !entry.email.includes("@")) {
|
|
102
|
+
result.skipped.push(`Invalid email: ${entry.email || "(empty)"}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Validate refresh_token
|
|
107
|
+
if (!entry.refresh_token || !entry.refresh_token.startsWith("1//")) {
|
|
108
|
+
result.skipped.push(`${entry.email} (invalid or missing refresh_token)`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Convert to Plugin account format
|
|
113
|
+
const account: Account = {
|
|
114
|
+
email: entry.email.trim(),
|
|
115
|
+
refreshToken: entry.refresh_token,
|
|
116
|
+
addedAt: Date.now(),
|
|
117
|
+
lastUsed: Date.now(),
|
|
118
|
+
fingerprint: generateFingerprint(),
|
|
119
|
+
enabled: true,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
result.accounts.push(account);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Import accounts from AM export file path
|
|
130
|
+
*/
|
|
131
|
+
export function importFromAMExportFile(filePath: string): ImportFromAMExportResult {
|
|
132
|
+
const result: ImportFromAMExportResult = {
|
|
133
|
+
accounts: [],
|
|
134
|
+
skipped: [],
|
|
135
|
+
errors: [],
|
|
136
|
+
source: "am-export",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Check if file exists
|
|
140
|
+
if (!fs.existsSync(filePath)) {
|
|
141
|
+
result.errors.push(`File not found: ${filePath}`);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Read and parse file
|
|
146
|
+
let data: unknown;
|
|
147
|
+
try {
|
|
148
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
149
|
+
data = JSON.parse(content);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
result.errors.push(`Failed to parse file: ${err}`);
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate format
|
|
156
|
+
if (!isAMExportFile(data)) {
|
|
157
|
+
result.errors.push("Invalid AM export format. Expected array of {email, refresh_token}");
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return importFromAMExportContent(data);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if a file is an AM export file (by reading and checking format)
|
|
166
|
+
*/
|
|
167
|
+
export function isAMExportFilePath(filePath: string): boolean {
|
|
168
|
+
if (!fs.existsSync(filePath)) return false;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
172
|
+
const data = JSON.parse(content);
|
|
173
|
+
return isAMExportFile(data);
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|