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.
Files changed (86) 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 +69 -2
  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/DashboardView.d.ts +3 -2
  39. package/dist/tui/components/DashboardView.d.ts.map +1 -1
  40. package/dist/tui/components/DashboardView.js +50 -4
  41. package/dist/tui/components/DashboardView.js.map +1 -1
  42. package/dist/tui/components/HealthBadge.d.ts +9 -0
  43. package/dist/tui/components/HealthBadge.d.ts.map +1 -0
  44. package/dist/tui/components/HealthBadge.js +56 -0
  45. package/dist/tui/components/HealthBadge.js.map +1 -0
  46. package/dist/tui/components/StatusBadge.d.ts +2 -1
  47. package/dist/tui/components/StatusBadge.d.ts.map +1 -1
  48. package/dist/tui/components/StatusBadge.js +30 -2
  49. package/dist/tui/components/StatusBadge.js.map +1 -1
  50. package/dist/tui/components/index.d.ts +1 -0
  51. package/dist/tui/components/index.d.ts.map +1 -1
  52. package/dist/tui/components/index.js +3 -1
  53. package/dist/tui/components/index.js.map +1 -1
  54. package/docs/BLUEPRINT.md +476 -476
  55. package/docs/ROADMAP.md +125 -107
  56. package/package.json +36 -36
  57. package/src/cli.ts +139 -38
  58. package/src/core/config-store.ts +278 -171
  59. package/src/core/crypto.ts +162 -162
  60. package/src/core/health-log.ts +173 -0
  61. package/src/core/health-oauth.ts +190 -0
  62. package/src/core/health-orchestrator.ts +224 -0
  63. package/src/core/importers/amExport.ts +177 -177
  64. package/src/core/opencode-config.ts +217 -217
  65. package/src/core/paths.ts +10 -6
  66. package/src/core/types.ts +193 -147
  67. package/src/tui/Dashboard.tsx +557 -478
  68. package/src/tui/components/AccountList.tsx +122 -104
  69. package/src/tui/components/ActionPalette.tsx +117 -117
  70. package/src/tui/components/Box.tsx +7 -7
  71. package/src/tui/components/DashboardView.tsx +285 -230
  72. package/src/tui/components/ExportModal.tsx +255 -255
  73. package/src/tui/components/FileBrowser.tsx +393 -393
  74. package/src/tui/components/Header.tsx +26 -26
  75. package/src/tui/components/HealthBadge.tsx +64 -0
  76. package/src/tui/components/ImportModal.tsx +334 -334
  77. package/src/tui/components/McpServerList.tsx +67 -67
  78. package/src/tui/components/Menu.tsx +61 -61
  79. package/src/tui/components/PasswordInput.tsx +159 -159
  80. package/src/tui/components/ProviderList.tsx +59 -59
  81. package/src/tui/components/SectionBox.tsx +35 -35
  82. package/src/tui/components/StatsRow.tsx +33 -33
  83. package/src/tui/components/StatusBadge.tsx +36 -3
  84. package/src/tui/components/index.ts +15 -14
  85. package/test-minimal.js +26 -26
  86. 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
+ }