import fs from 'fs' import path from 'path' import { electronUserDataDir, ensureRnxHome, profilesDir } from './home-paths' export const DEFAULT_PROFILE_ID = 'default' const PROFILE_INDEX_FILE = 'profiles.json' const PROFILE_INDEX_VERSION = 1 as const export interface StorageProfile { id: string createdAt: string updatedAt: string } interface ProfileIndex { version: 1 profiles: StorageProfile[] } export function normalizeProfileId(input: string): string { const id = input.trim() if (!id) throw new Error('profile id is required') if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) { throw new Error( 'profile ids must start with a letter or number and contain only letters, numbers, dot, dash, or underscore', ) } if (id === '.' || id === '..') throw new Error(`invalid profile id: ${id}`) return id } export function profileIndexPath(): string { return path.join(profilesDir(), PROFILE_INDEX_FILE) } export function electronProfilePartitionName(profileId: string): string { return `sootsim-profile-${normalizeProfileId(profileId)}` } export function electronProfilePartition(profileId: string): string { return `persist:${electronProfilePartitionName(profileId)}` } export function electronProfilePartitionDir(profileId: string): string { return path.join( electronUserDataDir(), 'Partitions', electronProfilePartitionName(profileId), ) } export function playwrightProfileUserDataDir(profileId: string): string { return path.join(profilesDir(), 'playwright', normalizeProfileId(profileId)) } function readIndexRaw(): ProfileIndex { try { const parsed = JSON.parse( fs.readFileSync(profileIndexPath(), 'utf8'), ) as Partial | null if (!parsed || parsed.version !== PROFILE_INDEX_VERSION) { return { version: PROFILE_INDEX_VERSION, profiles: [] } } const profiles = Array.isArray(parsed.profiles) ? parsed.profiles .filter( (profile): profile is StorageProfile => !!profile && typeof profile.id === 'string' && typeof profile.createdAt === 'string' && typeof profile.updatedAt === 'string', ) .map((profile) => ({ id: normalizeProfileId(profile.id), createdAt: profile.createdAt, updatedAt: profile.updatedAt, })) : [] return { version: PROFILE_INDEX_VERSION, profiles } } catch { return { version: PROFILE_INDEX_VERSION, profiles: [] } } } function sortProfiles(profiles: StorageProfile[]): StorageProfile[] { return [...profiles].sort((a, b) => { if (a.id === DEFAULT_PROFILE_ID) return -1 if (b.id === DEFAULT_PROFILE_ID) return 1 return a.id.localeCompare(b.id) }) } function withDefault(index: ProfileIndex): ProfileIndex { if (index.profiles.some((profile) => profile.id === DEFAULT_PROFILE_ID)) { return { ...index, profiles: sortProfiles(index.profiles) } } const now = new Date().toISOString() return { version: PROFILE_INDEX_VERSION, profiles: sortProfiles([ { id: DEFAULT_PROFILE_ID, createdAt: now, updatedAt: now }, ...index.profiles, ]), } } function writeIndex(index: ProfileIndex): ProfileIndex { ensureRnxHome() const next = withDefault(index) const tmp = `${profileIndexPath()}.tmp` fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8') fs.renameSync(tmp, profileIndexPath()) return next } export function listProfiles(): StorageProfile[] { return writeIndex(readIndexRaw()).profiles } export function getProfile(id: string): StorageProfile | null { const normalized = normalizeProfileId(id) return listProfiles().find((profile) => profile.id === normalized) ?? null } export function ensureProfile(id = DEFAULT_PROFILE_ID): StorageProfile { const normalized = normalizeProfileId(id) const existing = getProfile(normalized) if (existing) return existing return createProfile(normalized) } export function createProfile(id: string): StorageProfile { const normalized = normalizeProfileId(id) const index = withDefault(readIndexRaw()) if (index.profiles.some((profile) => profile.id === normalized)) { throw new Error(`profile already exists: ${normalized}`) } const now = new Date().toISOString() const profile = { id: normalized, createdAt: now, updatedAt: now } writeIndex({ version: PROFILE_INDEX_VERSION, profiles: [...index.profiles, profile] }) return profile } export function nextGeneratedProfileId(): string { const existing = new Set(listProfiles().map((profile) => profile.id)) for (let i = 1; i < 10_000; i++) { const id = `profile-${i}` if (!existing.has(id)) return id } throw new Error('could not allocate a new profile id') } export function deleteProfile(id: string): StorageProfile { const normalized = normalizeProfileId(id) if (normalized === DEFAULT_PROFILE_ID) { throw new Error('the default profile cannot be deleted; clear it instead') } const index = withDefault(readIndexRaw()) const profile = index.profiles.find((entry) => entry.id === normalized) if (!profile) throw new Error(`profile not found: ${normalized}`) writeIndex({ version: PROFILE_INDEX_VERSION, profiles: index.profiles.filter((entry) => entry.id !== normalized), }) clearProfileStorage(normalized) return profile } export function clearProfileStorage(id: string): void { const normalized = normalizeProfileId(id) for (const dir of [ electronProfilePartitionDir(normalized), playwrightProfileUserDataDir(normalized), ]) { try { fs.rmSync(dir, { recursive: true, force: true }) } catch {} } }