refactor: standardize runtime guard patterns with lodash-es (#154)

* refactor(ui): replace runtime type guards with lodash-es predicates

* refactor(runtime): migrate typeof checks to lodash-es in core modules

* refactor(ipc): standardize runtime guards with lodash-es

* test(refactor): align runtime guard checks with lodash-es

* docs(agents): add lodash-es runtime guard skill summary
This commit is contained in:
vin authored and GitHub committed 2026-04-12 11:41:56 +08:00
1 parent 0223e8d807
commit bc35b96b5d
33 files changed
+303 -208

No files matched your search

+6 -5
View File
@@ -6,6 +6,7 @@ import { logger } from '../../utils/logger';
import { shell } from 'electron';
import fs from 'fs';
import { isEmpty, isString } from 'lodash-es';
import { updateTrayMenu } from '../../ipc/tray/handler';
import {
ensureGlobalOriginalFromCurrentStorage,
@@ -46,7 +47,7 @@ function isEnterpriseClient(clientKey?: string): boolean {
}
function normalizeProjectId(projectId?: string): string | null {
if (typeof projectId !== 'string') {
if (!isString(projectId)) {
return null;
}
const normalized = projectId.trim();
@@ -148,7 +149,7 @@ async function clearAccountStatus(account: CloudAccount): Promise<void> {
function hydrateActiveOAuthClientFromSettings(): void {
const preferredClientKey = CloudAccountRepo.getSetting<string>(ACTIVE_OAUTH_CLIENT_KEY_SETTING, '');
if (preferredClientKey && preferredClientKey.trim() !== '') {
if (isString(preferredClientKey) && !isEmpty(preferredClientKey.trim())) {
try {
GoogleAPIService.setActiveOAuthClientKey(preferredClientKey);
} catch (error) {
@@ -195,7 +196,7 @@ async function backfillMissingOAuthClientKeyForLegacyAccounts(
}
const projectMissing =
typeof account.token.project_id !== 'string' || account.token.project_id.trim() === '';
!isString(account.token.project_id) || isEmpty(account.token.project_id.trim());
if (activeClientKey === ENTERPRISE_OAUTH_CLIENT_KEY && projectMissing) {
skippedEnterpriseGuardCount += 1;
continue;
@@ -232,7 +233,7 @@ export async function addGoogleAccount(
oauthClientKey?: string,
): Promise<CloudAccount> {
try {
if (oauthClientKey && oauthClientKey.trim() !== '') {
if (isString(oauthClientKey) && !isEmpty(oauthClientKey.trim())) {
setActiveOAuthClient(oauthClientKey);
} else {
hydrateActiveOAuthClientFromSettings();
@@ -684,7 +685,7 @@ export async function forcePollCloudMonitor(): Promise<void> {
}
export async function startAuthFlow(oauthClientKey?: string): Promise<void> {
if (oauthClientKey && oauthClientKey.trim() !== '') {
if (isString(oauthClientKey) && !isEmpty(oauthClientKey.trim())) {
setActiveOAuthClient(oauthClientKey);
} else {
hydrateActiveOAuthClientFromSettings();
+33 -31
View File
@@ -4,6 +4,7 @@ import fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
import { desc, eq } from 'drizzle-orm';
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
import { isNumber, isObjectLike, isPlainObject, isString } from 'lodash-es';
import { getCloudAccountsDbPath, getAntigravityDbPaths } from '../../utils/paths';
import { logger } from '../../utils/logger';
import {
@@ -35,14 +36,14 @@ type DrizzleExecutor = Pick<
>;
function isSqliteBusyError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
if (!isObjectLike(error)) {
return false;
}
const err = error as { code?: string; message?: string };
if (err.code && SQLITE_BUSY_CODES.has(err.code)) {
return true;
}
if (typeof err.message === 'string') {
if (isString(err.message)) {
return err.message.includes('SQLITE_BUSY') || err.message.includes('SQLITE_LOCKED');
}
return false;
@@ -168,17 +169,13 @@ interface MigrationStats {
failedFields: number;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function readStringCandidate(
source: Record<string, unknown>,
...keys: string[]
): string | undefined {
for (const key of keys) {
const candidate = source[key];
if (typeof candidate === 'string' && candidate.length > 0) {
if (isString(candidate) && candidate.length > 0) {
return candidate;
}
}
@@ -186,14 +183,15 @@ function readStringCandidate(
}
function normalizeDeviceProfile(value: unknown): DeviceProfile | undefined {
if (!isRecord(value)) {
if (!isPlainObject(value)) {
return undefined;
}
const valueRecord = value as Record<string, unknown>;
const machineId = readStringCandidate(value, 'machineId', 'machine_id');
const macMachineId = readStringCandidate(value, 'macMachineId', 'mac_machine_id');
const devDeviceId = readStringCandidate(value, 'devDeviceId', 'dev_device_id');
const sqmId = readStringCandidate(value, 'sqmId', 'sqm_id');
const machineId = readStringCandidate(valueRecord, 'machineId', 'machine_id');
const macMachineId = readStringCandidate(valueRecord, 'macMachineId', 'mac_machine_id');
const devDeviceId = readStringCandidate(valueRecord, 'devDeviceId', 'dev_device_id');
const sqmId = readStringCandidate(valueRecord, 'sqmId', 'sqm_id');
if (!machineId || !macMachineId || !devDeviceId || !sqmId) {
return undefined;
@@ -217,45 +215,47 @@ function areDeviceProfilesEqual(left: DeviceProfile, right: DeviceProfile): bool
}
function readVersionedProfilePayload(value: unknown): unknown {
if (!isRecord(value)) {
if (!isPlainObject(value)) {
return value;
}
if (!('schemaVersion' in value)) {
const valueRecord = value as Record<string, unknown>;
if (!('schemaVersion' in valueRecord)) {
return value;
}
const schemaVersion = value.schemaVersion;
if (typeof schemaVersion !== 'number' || !Number.isFinite(schemaVersion)) {
const schemaVersion = valueRecord.schemaVersion;
if (!isNumber(schemaVersion) || !Number.isFinite(schemaVersion)) {
throw new Error('invalid_device_profile_schema_version');
}
if (schemaVersion !== DEVICE_PAYLOAD_SCHEMA_VERSION) {
throw new Error(`unsupported_device_profile_schema_version:${schemaVersion}`);
}
if (!('profile' in value)) {
if (!('profile' in valueRecord)) {
throw new Error('invalid_device_profile_payload');
}
return value.profile;
return valueRecord.profile;
}
function readVersionedHistoryPayload(value: unknown): unknown {
if (!isRecord(value)) {
if (!isPlainObject(value)) {
return value;
}
if (!('schemaVersion' in value)) {
const valueRecord = value as Record<string, unknown>;
if (!('schemaVersion' in valueRecord)) {
return value;
}
const schemaVersion = value.schemaVersion;
if (typeof schemaVersion !== 'number' || !Number.isFinite(schemaVersion)) {
const schemaVersion = valueRecord.schemaVersion;
if (!isNumber(schemaVersion) || !Number.isFinite(schemaVersion)) {
throw new Error('invalid_device_history_schema_version');
}
if (schemaVersion !== DEVICE_PAYLOAD_SCHEMA_VERSION) {
throw new Error(`unsupported_device_history_schema_version:${schemaVersion}`);
}
if (!('history' in value)) {
if (!('history' in valueRecord)) {
throw new Error('invalid_device_history_payload');
}
return value.history;
return valueRecord.history;
}
function serializeDeviceProfile(profile: DeviceProfile | undefined): string | null {
@@ -285,23 +285,25 @@ function normalizeDeviceHistory(value: unknown): DeviceProfileVersion[] | undefi
const normalized: DeviceProfileVersion[] = [];
for (const item of value) {
if (!isRecord(item)) {
if (!isPlainObject(item)) {
continue;
}
const itemRecord = item as Record<string, unknown>;
const profile = normalizeDeviceProfile(item.profile);
const profile = normalizeDeviceProfile(itemRecord.profile);
if (!profile) {
continue;
}
const id = typeof item.id === 'string' && item.id.length > 0 ? item.id : uuidv4();
const createdAtCandidate = item.createdAt;
const id = isString(itemRecord.id) && itemRecord.id.length > 0 ? itemRecord.id : uuidv4();
const createdAtCandidate = itemRecord.createdAt;
const createdAt =
typeof createdAtCandidate === 'number' && Number.isFinite(createdAtCandidate)
isNumber(createdAtCandidate) && Number.isFinite(createdAtCandidate)
? Math.floor(createdAtCandidate)
: Math.floor(Date.now() / 1000);
const label = typeof item.label === 'string' && item.label.length > 0 ? item.label : 'legacy';
const isCurrent = item.isCurrent === true;
const label =
isString(itemRecord.label) && itemRecord.label.length > 0 ? itemRecord.label : 'legacy';
const isCurrent = itemRecord.isCurrent === true;
normalized.push({
id,
+4 -3
View File
@@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';
import { eq } from 'drizzle-orm';
import { isString } from 'lodash-es';
import { AccountBackupData, AccountInfo } from '../../types/account';
import { ItemTableValueRowSchema, type ItemTableKey } from '../../types/db';
import { logger } from '../../utils/logger';
@@ -192,8 +193,8 @@ export function getCurrentAccountInfo(): AccountInfo {
// Helper to find email in object
const findEmail = (obj: { email?: string; user?: { email?: string } }): string => {
if (!obj) return '';
if (typeof obj.email === 'string') return obj.email;
if (obj.user && typeof obj.user.email === 'string') return obj.user.email;
if (isString(obj.email)) return obj.email;
if (obj.user && isString(obj.user.email)) return obj.user.email;
return '';
};
@@ -323,7 +324,7 @@ function _restoreSingleDb(dbPath: string, backup: AccountBackupData): boolean {
for (const key of KEYS_TO_BACKUP) {
if (key in backup.data) {
const value = backup.data[key];
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
const stringValue = isString(value) ? value : JSON.stringify(value);
tx.insert(itemTable)
.values({ key, value: stringValue })
.onConflictDoUpdate({
+11 -10
View File
@@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';
import { createHash, randomBytes } from 'crypto';
import { isBoolean, isNumber, isObjectLike, isString } from 'lodash-es';
import { type DeviceProfile } from '../../types/account';
import { logger } from '../../utils/logger';
import { getAgentDir, getAntigravityDbPaths, getAntigravityStoragePaths } from '../../utils/paths';
@@ -76,14 +77,14 @@ const deviceHardeningState: DeviceHardeningState = {
};
function isSqliteBusyError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
if (!isObjectLike(error)) {
return false;
}
const candidate = error as { code?: string; message?: string };
if (candidate.code === 'SQLITE_BUSY' || candidate.code === 'SQLITE_LOCKED') {
return true;
}
if (typeof candidate.message === 'string') {
if (isString(candidate.message)) {
return candidate.message.includes('SQLITE_BUSY') || candidate.message.includes('SQLITE_LOCKED');
}
return false;
@@ -139,8 +140,8 @@ function readLastKnownGoodMarker(): LastKnownGoodMarker | null {
if (
parsed &&
parsed.version === 1 &&
typeof parsed.savedAt === 'number' &&
typeof parsed.hasStateDb === 'boolean'
isNumber(parsed.savedAt) &&
isBoolean(parsed.hasStateDb)
) {
return {
version: 1,
@@ -157,7 +158,7 @@ function readLastKnownGoodMarker(): LastKnownGoodMarker | null {
function parseStorageJson(storagePath: string): Record<string, unknown> {
const content = fs.readFileSync(storagePath, 'utf-8');
const parsed = JSON.parse(content);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
if (!isObjectLike(parsed) || Array.isArray(parsed)) {
throw new Error('storage.json top-level value must be an object');
}
return parsed as Record<string, unknown>;
@@ -165,15 +166,15 @@ function parseStorageJson(storagePath: string): Record<string, unknown> {
function getTelemetryField(storage: Record<string, unknown>, key: string): string | null {
const telemetry = storage.telemetry;
if (telemetry && typeof telemetry === 'object' && !Array.isArray(telemetry)) {
if (isObjectLike(telemetry) && !Array.isArray(telemetry)) {
const nestedValue = (telemetry as Record<string, unknown>)[key];
if (typeof nestedValue === 'string' && nestedValue.length > 0) {
if (isString(nestedValue) && nestedValue.length > 0) {
return nestedValue;
}
}
const flatValue = storage[`telemetry.${key}`];
if (typeof flatValue === 'string' && flatValue.length > 0) {
if (isString(flatValue) && flatValue.length > 0) {
return flatValue;
}
@@ -183,7 +184,7 @@ function getTelemetryField(storage: Record<string, unknown>, key: string): strin
function ensureTelemetryObject(storage: Record<string, unknown>): Record<string, unknown> {
if (
!storage.telemetry ||
typeof storage.telemetry !== 'object' ||
!isObjectLike(storage.telemetry) ||
Array.isArray(storage.telemetry)
) {
storage.telemetry = {};
@@ -223,7 +224,7 @@ function readStateServiceMachineIdValue(dbPath: string): string | null {
const row = db
.prepare("SELECT value FROM ItemTable WHERE key = 'storage.serviceMachineId' LIMIT 1")
.get() as { value?: unknown } | undefined;
if (!row || typeof row.value !== 'string' || row.value.length === 0) {
if (!row || !isString(row.value) || row.value.length === 0) {
return null;
}
return row.value;
+2 -1
View File
@@ -1,4 +1,5 @@
import { IPC_CHANNELS } from '@/constants';
import { isString } from 'lodash-es';
// Custom IPC Client that correctly builds method paths
function createIPCClient(port: MessagePort) {
@@ -10,7 +11,7 @@ function createIPCClient(port: MessagePort) {
port.onmessage = (event) => {
try {
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
const data = isString(event.data) ? JSON.parse(event.data) : event.data;
const id = data.i || data.id;
const pending = pendingRequests.get(id);
+2 -1
View File
@@ -1,6 +1,7 @@
import { exec, execSync, spawn } from 'child_process';
import { promisify } from 'util';
import findProcess, { ProcessInfo } from 'find-process';
import { isNumber } from 'lodash-es';
import { getAntigravityExecutablePath, isWsl } from '../../utils/paths';
import { logger } from '../../utils/logger';
@@ -98,7 +99,7 @@ export async function isProcessRunning(): Promise<boolean> {
const processMap = new Map<number, ProcessInfo>();
for (const proc of allMatches) {
if (typeof proc.pid === 'number') {
if (isNumber(proc.pid)) {
processMap.set(proc.pid, proc);
}
}