style(design): apply Minimal Dark Mode design system\n\n- Switched to complete Dark Mode (OLED-friendly)\n- Established standard color palette matching modern technical aesthetics\n- Changed font to Plus Jakarta Sans for increased legibility\n- Enforced dark mode as default in App.tsx\n- Fixed several React cascading render (setState in useEffect) warnings

This commit is contained in:
Gemini Nexus Bot committed 2026-04-28 03:49:34 -03:00
1 parent b80531f8a8
commit bac7b5591e
738 files changed
+138513 -138123

No files matched your search

+483 -483
View File
@@ -1,483 +1,483 @@
import fs from 'fs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { getAccountsFilePath, getBackupsDir } from '../../utils/paths';
import { logger } from '../../utils/logger';
import {
Account,
AccountBackupData,
DeviceProfile,
DeviceProfilesSnapshot,
DeviceProfileVersion,
} from '../../types/account';
import {
backupAccount as dbBackup,
restoreAccount as dbRestore,
getCurrentAccountInfo,
} from '../database/handler';
import {
applyDeviceProfile,
ensureGlobalOriginalFromCurrentStorage,
generateDeviceProfile,
isIdentityProfileApplyEnabled,
loadGlobalOriginalProfile,
readCurrentDeviceProfile,
saveGlobalOriginalProfile,
getStorageDirectoryPath,
} from '../device/handler';
import { runWithSwitchGuard } from '../switchGuard';
import { executeSwitchFlow } from '../switchFlow';
import { shell } from 'electron';
type AccountIndex = Record<string, Account>;
const SWITCH_EXIT_TIMEOUT_MS = 10000;
function getDeviceHistory(account: Account): DeviceProfileVersion[] {
if (!account.deviceHistory) {
account.deviceHistory = [];
}
return account.deviceHistory;
}
function bindDeviceProfileToAccount(
account: Account,
profile: DeviceProfile,
label: string,
addHistory: boolean,
): void {
account.deviceProfile = profile;
if (!addHistory) {
return;
}
const history = getDeviceHistory(account);
for (const version of history) {
version.isCurrent = false;
}
history.push({
id: uuidv4(),
createdAt: Math.floor(Date.now() / 1000),
label,
profile,
isCurrent: true,
});
}
/**
* Loads the accounts index from the file system.
* @returns {AccountIndex} The accounts index.
*/
function loadAccountsIndex(): AccountIndex {
const filePath = getAccountsFilePath();
if (!fs.existsSync(filePath)) {
return {};
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
logger.error('Failed to load accounts index', error);
return {};
}
}
/**
* Saves the accounts index to the file system.
* @param accounts {AccountIndex} The accounts index to save.
* @throws {Error} If the accounts index cannot be saved.
*/
function saveAccountsIndex(accounts: AccountIndex): void {
const filePath = getAccountsFilePath();
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
try {
fs.writeFileSync(filePath, JSON.stringify(accounts, null, 2));
} catch (error) {
logger.error('Failed to save accounts index', error);
throw error;
}
}
/**
* Lists the accounts data.
* @returns {Account[]} The list of accounts.
* @throws {Error} If the accounts index cannot be loaded.
*/
export async function listAccountsData(): Promise<Account[]> {
const accountsObj = loadAccountsIndex();
const accountsList = Object.values(accountsObj);
// NOTE: Sort by last_used descending
accountsList.sort((a, b) => {
const aTime = a.last_used || '';
const bTime = b.last_used || '';
return bTime.localeCompare(aTime);
});
return accountsList;
}
/**
* Adds an account snapshot.
* @returns {Account} The added account.
* @throws {Error} If the account cannot be added.
*/
export async function addAccountSnapshot(): Promise<Account> {
logger.info('Adding account snapshot...');
// NOTE Get current account info from DB
const info = getCurrentAccountInfo();
if (!info.isAuthenticated) {
const errorMsg =
'No authenticated account found. Please ensure Gemini Nexus is running and you are logged in.';
logger.error(errorMsg);
throw new Error(errorMsg);
}
const accounts = loadAccountsIndex();
const now = new Date().toISOString();
// NOTE Find existing account by email
let existingId: string | null = null;
for (const [id, acc] of Object.entries(accounts)) {
if (acc.email === info.email) {
existingId = id;
break;
}
}
let account: Account;
let backupPath: string;
if (existingId) {
// NOTE Update existing account
account = accounts[existingId];
// NOTE Preserve custom name: only update if we have a name from DB AND it's not the default email prefix
// NOTE if not name or name == email.split("@")[0]: name = existing_account.get("name", name)
const defaultName = info.email.split('@')[0];
if (!info.name || info.name === defaultName) {
// NOTE Keep the existing custom name
// (account.name is already set, no change needed)
} else {
// NOTE We have a non-default name from DB, use it
account.name = info.name;
}
account.last_used = now;
// NOTE Use existing backup path if available, otherwise generate new one
backupPath = account.backup_file || path.join(getBackupsDir(), `${account.id}.json`);
logger.info(`Updating existing account: ${info.email}`);
} else {
const accountId = uuidv4();
// NOTE Generate name with edge case handling
let accountName: string;
if (info.name) {
accountName = info.name;
} else if (info.email && info.email !== 'Unknown') {
accountName = info.email.split('@')[0];
} else {
// Edge case: email is "Unknown" or invalid
accountName = `Account_${Date.now()}`;
}
backupPath = path.join(getBackupsDir(), `${accountId}.json`);
account = {
id: accountId,
name: accountName,
email: info.email,
backup_file: backupPath,
deviceHistory: [],
created_at: now,
last_used: now,
};
accounts[accountId] = account;
logger.info(`Creating new account: ${info.email}`);
}
// NOTE Backup data from DB
const backupData = dbBackup(account);
// NOTE Save backup file
const backupsDir = getBackupsDir();
if (!fs.existsSync(backupsDir)) {
fs.mkdirSync(backupsDir, { recursive: true });
}
fs.writeFileSync(backupPath, JSON.stringify(backupData, null, 2));
// NOTE Update backup_file in account object and save
account.backup_file = backupPath;
saveAccountsIndex(accounts);
return account;
}
/**
* Switches to an account.
* @param accountId {string} The ID of the account to switch to.
* @throws {Error} If the account cannot be found or the backup file cannot be found.
*/
export async function switchAccount(accountId: string): Promise<void> {
await runWithSwitchGuard('local-account-switch', async () => {
logger.info(`Switching to account: ${accountId}`);
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
// NOTE Get backup file path from account data
const backupPath = account.backup_file || path.join(getBackupsDir(), `${accountId}.json`);
if (!fs.existsSync(backupPath)) {
throw new Error(`Backup file not found: ${backupPath}`);
}
ensureGlobalOriginalFromCurrentStorage();
if (!account.deviceProfile) {
const generated = generateDeviceProfile();
saveGlobalOriginalProfile(generated);
bindDeviceProfileToAccount(account, generated, 'auto_generated', true);
}
await executeSwitchFlow({
scope: 'local',
targetProfile: account.deviceProfile || null,
applyFingerprint: isIdentityProfileApplyEnabled(),
processExitTimeoutMs: SWITCH_EXIT_TIMEOUT_MS,
performSwitch: async () => {
// NOTE Load backup file
const backupContent = fs.readFileSync(backupPath, 'utf-8');
const backupData: AccountBackupData = JSON.parse(backupContent);
// NOTE Restore data to DB
dbRestore(backupData);
// NOTE Update last used
account.last_used = new Date().toISOString();
saveAccountsIndex(accounts);
},
});
});
}
export async function previewGenerateIdentityProfile(): Promise<DeviceProfile> {
return generateDeviceProfile();
}
export async function getIdentityProfiles(accountId: string): Promise<DeviceProfilesSnapshot> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let currentStorage: DeviceProfile | undefined;
try {
currentStorage = readCurrentDeviceProfile();
} catch (error) {
logger.warn('Failed to read current storage device profile', error);
}
return {
currentStorage,
boundProfile: account.deviceProfile,
history: account.deviceHistory || [],
baseline: loadGlobalOriginalProfile() || undefined,
};
}
export async function bindIdentityProfile(
accountId: string,
mode: 'capture' | 'generate',
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let profile: DeviceProfile;
if (mode === 'capture') {
profile = readCurrentDeviceProfile();
} else {
profile = generateDeviceProfile();
}
ensureGlobalOriginalFromCurrentStorage();
saveGlobalOriginalProfile(profile);
applyDeviceProfile(profile);
bindDeviceProfileToAccount(account, profile, mode, true);
saveAccountsIndex(accounts);
return profile;
}
export async function bindIdentityProfileWithPayload(
accountId: string,
profile: DeviceProfile,
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
ensureGlobalOriginalFromCurrentStorage();
saveGlobalOriginalProfile(profile);
applyDeviceProfile(profile);
bindDeviceProfileToAccount(account, profile, 'generated', true);
saveAccountsIndex(accounts);
return profile;
}
export async function applyBoundIdentityProfile(accountId: string): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
if (!account.deviceProfile) {
throw new Error('Account has no bound device profile');
}
applyDeviceProfile(account.deviceProfile);
account.last_used = new Date().toISOString();
saveAccountsIndex(accounts);
return account.deviceProfile;
}
export async function restoreIdentityProfileRevision(
accountId: string,
versionId: string,
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let targetProfile: DeviceProfile | null = null;
if (versionId === 'baseline') {
targetProfile = loadGlobalOriginalProfile();
if (!targetProfile) {
throw new Error('Global original profile not found');
}
for (const version of getDeviceHistory(account)) {
version.isCurrent = false;
}
} else if (versionId === 'current') {
targetProfile = account.deviceProfile || null;
if (!targetProfile) {
throw new Error('No currently bound profile');
}
} else {
const history = getDeviceHistory(account);
const targetVersion = history.find((version) => version.id === versionId);
if (!targetVersion) {
throw new Error('Device profile version not found');
}
targetProfile = targetVersion.profile;
for (const version of history) {
version.isCurrent = version.id === versionId;
}
}
applyDeviceProfile(targetProfile);
account.deviceProfile = targetProfile;
saveAccountsIndex(accounts);
return targetProfile;
}
export async function deleteIdentityProfileRevision(
accountId: string,
versionId: string,
): Promise<void> {
if (versionId === 'baseline') {
throw new Error('Original profile cannot be deleted');
}
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
const history = getDeviceHistory(account);
if (history.some((version) => version.id === versionId && version.isCurrent)) {
throw new Error('Currently bound profile cannot be deleted');
}
const before = history.length;
account.deviceHistory = history.filter((version) => version.id !== versionId);
if (account.deviceHistory.length === before) {
throw new Error('Historical device profile not found');
}
saveAccountsIndex(accounts);
}
export async function restoreBaselineProfile(accountId: string): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
const baseline = loadGlobalOriginalProfile();
if (!baseline) {
throw new Error('Global original profile not found');
}
account.deviceProfile = baseline;
for (const version of getDeviceHistory(account)) {
version.isCurrent = false;
}
saveAccountsIndex(accounts);
return baseline;
}
export async function openIdentityStorageFolder(): Promise<void> {
const directory = getStorageDirectoryPath();
const result = await shell.openPath(directory);
if (result) {
throw new Error(`Failed to open device folder: ${result}`);
}
}
/**
* Deletes an account.
* @param accountId {string} The ID of the account to delete.
* @throws {Error} If the account cannot be found or the backup file cannot be found.
*/
export async function deleteAccount(accountId: string): Promise<void> {
logger.info(`Deleting account: ${accountId}`);
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
// NOTE Remove backup file using stored path
const backupPath = account.backup_file || path.join(getBackupsDir(), `${accountId}.json`);
if (fs.existsSync(backupPath)) {
try {
fs.unlinkSync(backupPath);
logger.info(`Backup file deleted: ${backupPath}`);
} catch (error) {
logger.warn(`Failed to delete backup file: ${backupPath}`, error);
}
}
// NOTE Remove from index
delete accounts[accountId];
saveAccountsIndex(accounts);
}
import fs from 'fs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { getAccountsFilePath, getBackupsDir } from '../../utils/paths';
import { logger } from '../../utils/logger';
import {
Account,
AccountBackupData,
DeviceProfile,
DeviceProfilesSnapshot,
DeviceProfileVersion,
} from '../../types/account';
import {
backupAccount as dbBackup,
restoreAccount as dbRestore,
getCurrentAccountInfo,
} from '../database/handler';
import {
applyDeviceProfile,
ensureGlobalOriginalFromCurrentStorage,
generateDeviceProfile,
isIdentityProfileApplyEnabled,
loadGlobalOriginalProfile,
readCurrentDeviceProfile,
saveGlobalOriginalProfile,
getStorageDirectoryPath,
} from '../device/handler';
import { runWithSwitchGuard } from '../switchGuard';
import { executeSwitchFlow } from '../switchFlow';
import { shell } from 'electron';
type AccountIndex = Record<string, Account>;
const SWITCH_EXIT_TIMEOUT_MS = 10000;
function getDeviceHistory(account: Account): DeviceProfileVersion[] {
if (!account.deviceHistory) {
account.deviceHistory = [];
}
return account.deviceHistory;
}
function bindDeviceProfileToAccount(
account: Account,
profile: DeviceProfile,
label: string,
addHistory: boolean,
): void {
account.deviceProfile = profile;
if (!addHistory) {
return;
}
const history = getDeviceHistory(account);
for (const version of history) {
version.isCurrent = false;
}
history.push({
id: uuidv4(),
createdAt: Math.floor(Date.now() / 1000),
label,
profile,
isCurrent: true,
});
}
/**
* Loads the accounts index from the file system.
* @returns {AccountIndex} The accounts index.
*/
function loadAccountsIndex(): AccountIndex {
const filePath = getAccountsFilePath();
if (!fs.existsSync(filePath)) {
return {};
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
logger.error('Failed to load accounts index', error);
return {};
}
}
/**
* Saves the accounts index to the file system.
* @param accounts {AccountIndex} The accounts index to save.
* @throws {Error} If the accounts index cannot be saved.
*/
function saveAccountsIndex(accounts: AccountIndex): void {
const filePath = getAccountsFilePath();
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
try {
fs.writeFileSync(filePath, JSON.stringify(accounts, null, 2));
} catch (error) {
logger.error('Failed to save accounts index', error);
throw error;
}
}
/**
* Lists the accounts data.
* @returns {Account[]} The list of accounts.
* @throws {Error} If the accounts index cannot be loaded.
*/
export async function listAccountsData(): Promise<Account[]> {
const accountsObj = loadAccountsIndex();
const accountsList = Object.values(accountsObj);
// NOTE: Sort by last_used descending
accountsList.sort((a, b) => {
const aTime = a.last_used || '';
const bTime = b.last_used || '';
return bTime.localeCompare(aTime);
});
return accountsList;
}
/**
* Adds an account snapshot.
* @returns {Account} The added account.
* @throws {Error} If the account cannot be added.
*/
export async function addAccountSnapshot(): Promise<Account> {
logger.info('Adding account snapshot...');
// NOTE Get current account info from DB
const info = getCurrentAccountInfo();
if (!info.isAuthenticated) {
const errorMsg =
'No authenticated account found. Please ensure Gemini Nexus is running and you are logged in.';
logger.error(errorMsg);
throw new Error(errorMsg);
}
const accounts = loadAccountsIndex();
const now = new Date().toISOString();
// NOTE Find existing account by email
let existingId: string | null = null;
for (const [id, acc] of Object.entries(accounts)) {
if (acc.email === info.email) {
existingId = id;
break;
}
}
let account: Account;
let backupPath: string;
if (existingId) {
// NOTE Update existing account
account = accounts[existingId];
// NOTE Preserve custom name: only update if we have a name from DB AND it's not the default email prefix
// NOTE if not name or name == email.split("@")[0]: name = existing_account.get("name", name)
const defaultName = info.email.split('@')[0];
if (!info.name || info.name === defaultName) {
// NOTE Keep the existing custom name
// (account.name is already set, no change needed)
} else {
// NOTE We have a non-default name from DB, use it
account.name = info.name;
}
account.last_used = now;
// NOTE Use existing backup path if available, otherwise generate new one
backupPath = account.backup_file || path.join(getBackupsDir(), `${account.id}.json`);
logger.info(`Updating existing account: ${info.email}`);
} else {
const accountId = uuidv4();
// NOTE Generate name with edge case handling
let accountName: string;
if (info.name) {
accountName = info.name;
} else if (info.email && info.email !== 'Unknown') {
accountName = info.email.split('@')[0];
} else {
// Edge case: email is "Unknown" or invalid
accountName = `Account_${Date.now()}`;
}
backupPath = path.join(getBackupsDir(), `${accountId}.json`);
account = {
id: accountId,
name: accountName,
email: info.email,
backup_file: backupPath,
deviceHistory: [],
created_at: now,
last_used: now,
};
accounts[accountId] = account;
logger.info(`Creating new account: ${info.email}`);
}
// NOTE Backup data from DB
const backupData = dbBackup(account);
// NOTE Save backup file
const backupsDir = getBackupsDir();
if (!fs.existsSync(backupsDir)) {
fs.mkdirSync(backupsDir, { recursive: true });
}
fs.writeFileSync(backupPath, JSON.stringify(backupData, null, 2));
// NOTE Update backup_file in account object and save
account.backup_file = backupPath;
saveAccountsIndex(accounts);
return account;
}
/**
* Switches to an account.
* @param accountId {string} The ID of the account to switch to.
* @throws {Error} If the account cannot be found or the backup file cannot be found.
*/
export async function switchAccount(accountId: string): Promise<void> {
await runWithSwitchGuard('local-account-switch', async () => {
logger.info(`Switching to account: ${accountId}`);
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
// NOTE Get backup file path from account data
const backupPath = account.backup_file || path.join(getBackupsDir(), `${accountId}.json`);
if (!fs.existsSync(backupPath)) {
throw new Error(`Backup file not found: ${backupPath}`);
}
ensureGlobalOriginalFromCurrentStorage();
if (!account.deviceProfile) {
const generated = generateDeviceProfile();
saveGlobalOriginalProfile(generated);
bindDeviceProfileToAccount(account, generated, 'auto_generated', true);
}
await executeSwitchFlow({
scope: 'local',
targetProfile: account.deviceProfile || null,
applyFingerprint: isIdentityProfileApplyEnabled(),
processExitTimeoutMs: SWITCH_EXIT_TIMEOUT_MS,
performSwitch: async () => {
// NOTE Load backup file
const backupContent = fs.readFileSync(backupPath, 'utf-8');
const backupData: AccountBackupData = JSON.parse(backupContent);
// NOTE Restore data to DB
dbRestore(backupData);
// NOTE Update last used
account.last_used = new Date().toISOString();
saveAccountsIndex(accounts);
},
});
});
}
export async function previewGenerateIdentityProfile(): Promise<DeviceProfile> {
return generateDeviceProfile();
}
export async function getIdentityProfiles(accountId: string): Promise<DeviceProfilesSnapshot> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let currentStorage: DeviceProfile | undefined;
try {
currentStorage = readCurrentDeviceProfile();
} catch (error) {
logger.warn('Failed to read current storage device profile', error);
}
return {
currentStorage,
boundProfile: account.deviceProfile,
history: account.deviceHistory || [],
baseline: loadGlobalOriginalProfile() || undefined,
};
}
export async function bindIdentityProfile(
accountId: string,
mode: 'capture' | 'generate',
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let profile: DeviceProfile;
if (mode === 'capture') {
profile = readCurrentDeviceProfile();
} else {
profile = generateDeviceProfile();
}
ensureGlobalOriginalFromCurrentStorage();
saveGlobalOriginalProfile(profile);
applyDeviceProfile(profile);
bindDeviceProfileToAccount(account, profile, mode, true);
saveAccountsIndex(accounts);
return profile;
}
export async function bindIdentityProfileWithPayload(
accountId: string,
profile: DeviceProfile,
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
ensureGlobalOriginalFromCurrentStorage();
saveGlobalOriginalProfile(profile);
applyDeviceProfile(profile);
bindDeviceProfileToAccount(account, profile, 'generated', true);
saveAccountsIndex(accounts);
return profile;
}
export async function applyBoundIdentityProfile(accountId: string): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
if (!account.deviceProfile) {
throw new Error('Account has no bound device profile');
}
applyDeviceProfile(account.deviceProfile);
account.last_used = new Date().toISOString();
saveAccountsIndex(accounts);
return account.deviceProfile;
}
export async function restoreIdentityProfileRevision(
accountId: string,
versionId: string,
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let targetProfile: DeviceProfile | null = null;
if (versionId === 'baseline') {
targetProfile = loadGlobalOriginalProfile();
if (!targetProfile) {
throw new Error('Global original profile not found');
}
for (const version of getDeviceHistory(account)) {
version.isCurrent = false;
}
} else if (versionId === 'current') {
targetProfile = account.deviceProfile || null;
if (!targetProfile) {
throw new Error('No currently bound profile');
}
} else {
const history = getDeviceHistory(account);
const targetVersion = history.find((version) => version.id === versionId);
if (!targetVersion) {
throw new Error('Device profile version not found');
}
targetProfile = targetVersion.profile;
for (const version of history) {
version.isCurrent = version.id === versionId;
}
}
applyDeviceProfile(targetProfile);
account.deviceProfile = targetProfile;
saveAccountsIndex(accounts);
return targetProfile;
}
export async function deleteIdentityProfileRevision(
accountId: string,
versionId: string,
): Promise<void> {
if (versionId === 'baseline') {
throw new Error('Original profile cannot be deleted');
}
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
const history = getDeviceHistory(account);
if (history.some((version) => version.id === versionId && version.isCurrent)) {
throw new Error('Currently bound profile cannot be deleted');
}
const before = history.length;
account.deviceHistory = history.filter((version) => version.id !== versionId);
if (account.deviceHistory.length === before) {
throw new Error('Historical device profile not found');
}
saveAccountsIndex(accounts);
}
export async function restoreBaselineProfile(accountId: string): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
const baseline = loadGlobalOriginalProfile();
if (!baseline) {
throw new Error('Global original profile not found');
}
account.deviceProfile = baseline;
for (const version of getDeviceHistory(account)) {
version.isCurrent = false;
}
saveAccountsIndex(accounts);
return baseline;
}
export async function openIdentityStorageFolder(): Promise<void> {
const directory = getStorageDirectoryPath();
const result = await shell.openPath(directory);
if (result) {
throw new Error(`Failed to open device folder: ${result}`);
}
}
/**
* Deletes an account.
* @param accountId {string} The ID of the account to delete.
* @throws {Error} If the account cannot be found or the backup file cannot be found.
*/
export async function deleteAccount(accountId: string): Promise<void> {
logger.info(`Deleting account: ${accountId}`);
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
// NOTE Remove backup file using stored path
const backupPath = account.backup_file || path.join(getBackupsDir(), `${accountId}.json`);
if (fs.existsSync(backupPath)) {
try {
fs.unlinkSync(backupPath);
logger.info(`Backup file deleted: ${backupPath}`);
} catch (error) {
logger.warn(`Failed to delete backup file: ${backupPath}`, error);
}
}
// NOTE Remove from index
delete accounts[accountId];
saveAccountsIndex(accounts);
}
+103 -103
View File
@@ -1,103 +1,103 @@
import { z } from 'zod';
import { os } from '@orpc/server';
import {
applyBoundIdentityProfile,
addAccountSnapshot,
bindIdentityProfile,
bindIdentityProfileWithPayload,
deleteAccount,
deleteIdentityProfileRevision,
getIdentityProfiles,
listAccountsData,
previewGenerateIdentityProfile,
restoreBaselineProfile,
restoreIdentityProfileRevision,
switchAccount,
openIdentityStorageFolder,
} from './handler';
import {
AccountSchema,
DeviceProfileSchema,
DeviceProfilesSnapshotSchema,
} from '../../types/account';
export const accountRouter = os.router({
listAccounts: os.output(z.array(AccountSchema)).handler(async () => {
return listAccountsData();
}),
addAccountSnapshot: os.output(AccountSchema).handler(async () => {
return addAccountSnapshot();
}),
switchAccount: os
.input(z.object({ accountId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await switchAccount(input.accountId);
}),
deleteAccount: os
.input(z.object({ accountId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await deleteAccount(input.accountId);
}),
previewGenerateIdentityProfile: os.output(DeviceProfileSchema).handler(async () => {
return previewGenerateIdentityProfile();
}),
getIdentityProfiles: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfilesSnapshotSchema)
.handler(async ({ input }) => {
return getIdentityProfiles(input.accountId);
}),
bindIdentityProfile: os
.input(z.object({ accountId: z.string(), mode: z.enum(['capture', 'generate']) }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return bindIdentityProfile(input.accountId, input.mode);
}),
bindIdentityProfileWithPayload: os
.input(z.object({ accountId: z.string(), profile: DeviceProfileSchema }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return bindIdentityProfileWithPayload(input.accountId, input.profile);
}),
applyBoundIdentityProfile: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return applyBoundIdentityProfile(input.accountId);
}),
restoreIdentityProfileRevision: os
.input(z.object({ accountId: z.string(), versionId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return restoreIdentityProfileRevision(input.accountId, input.versionId);
}),
deleteIdentityProfileRevision: os
.input(z.object({ accountId: z.string(), versionId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await deleteIdentityProfileRevision(input.accountId, input.versionId);
}),
restoreBaselineProfile: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return restoreBaselineProfile(input.accountId);
}),
openIdentityStorageFolder: os.output(z.void()).handler(async () => {
await openIdentityStorageFolder();
}),
});
import { z } from 'zod';
import { os } from '@orpc/server';
import {
applyBoundIdentityProfile,
addAccountSnapshot,
bindIdentityProfile,
bindIdentityProfileWithPayload,
deleteAccount,
deleteIdentityProfileRevision,
getIdentityProfiles,
listAccountsData,
previewGenerateIdentityProfile,
restoreBaselineProfile,
restoreIdentityProfileRevision,
switchAccount,
openIdentityStorageFolder,
} from './handler';
import {
AccountSchema,
DeviceProfileSchema,
DeviceProfilesSnapshotSchema,
} from '../../types/account';
export const accountRouter = os.router({
listAccounts: os.output(z.array(AccountSchema)).handler(async () => {
return listAccountsData();
}),
addAccountSnapshot: os.output(AccountSchema).handler(async () => {
return addAccountSnapshot();
}),
switchAccount: os
.input(z.object({ accountId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await switchAccount(input.accountId);
}),
deleteAccount: os
.input(z.object({ accountId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await deleteAccount(input.accountId);
}),
previewGenerateIdentityProfile: os.output(DeviceProfileSchema).handler(async () => {
return previewGenerateIdentityProfile();
}),
getIdentityProfiles: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfilesSnapshotSchema)
.handler(async ({ input }) => {
return getIdentityProfiles(input.accountId);
}),
bindIdentityProfile: os
.input(z.object({ accountId: z.string(), mode: z.enum(['capture', 'generate']) }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return bindIdentityProfile(input.accountId, input.mode);
}),
bindIdentityProfileWithPayload: os
.input(z.object({ accountId: z.string(), profile: DeviceProfileSchema }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return bindIdentityProfileWithPayload(input.accountId, input.profile);
}),
applyBoundIdentityProfile: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return applyBoundIdentityProfile(input.accountId);
}),
restoreIdentityProfileRevision: os
.input(z.object({ accountId: z.string(), versionId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return restoreIdentityProfileRevision(input.accountId, input.versionId);
}),
deleteIdentityProfileRevision: os
.input(z.object({ accountId: z.string(), versionId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await deleteIdentityProfileRevision(input.accountId, input.versionId);
}),
restoreBaselineProfile: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return restoreBaselineProfile(input.accountId);
}),
openIdentityStorageFolder: os.output(z.void()).handler(async () => {
await openIdentityStorageFolder();
}),
});