import { readFileSync, writeFileSync, existsSync } from 'fs'; import path from 'path'; import { logger } from '../util/logger.js'; import type { LinkMode, PostAction } from '../types.js'; export interface SavedLink { url: string; name: string; active: boolean; mode: LinkMode; postAction?: PostAction; addedAt: string; } export interface SavedSettings { paused: boolean; links: SavedLink[]; poe2LogPath: string; poe2WindowTitle: string; browserUserDataDir: string; travelTimeoutMs: number; stashScanTimeoutMs: number; waitForMoreItemsMs: number; betweenTradesDelayMs: number; dashboardPort: number; } const DEFAULTS: SavedSettings = { paused: false, links: [], poe2LogPath: 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Path of Exile 2\\logs\\Client.txt', poe2WindowTitle: 'Path of Exile 2', browserUserDataDir: './browser-data', travelTimeoutMs: 15000, stashScanTimeoutMs: 10000, waitForMoreItemsMs: 20000, betweenTradesDelayMs: 5000, dashboardPort: 3000, }; export class ConfigStore { private filePath: string; private data: SavedSettings; constructor(configPath?: string) { this.filePath = configPath || path.resolve('config.json'); this.data = this.load(); } private load(): SavedSettings { if (!existsSync(this.filePath)) { logger.info({ path: this.filePath }, 'No config.json found, using defaults'); return { ...DEFAULTS }; } try { const raw = readFileSync(this.filePath, 'utf-8'); const parsed = JSON.parse(raw) as Partial; const merged = { ...DEFAULTS, ...parsed }; // Migrate old links: add name/active fields, strip /live from URLs, default postAction merged.links = merged.links.map((l: any) => { const mode: LinkMode = l.mode || 'live'; return { url: l.url.replace(/\/live\/?$/, ''), name: l.name || '', active: l.active !== undefined ? l.active : true, mode, postAction: l.postAction || (mode === 'scrap' ? 'salvage' : 'stash'), addedAt: l.addedAt || new Date().toISOString(), }; }); logger.info({ path: this.filePath, linkCount: merged.links.length }, 'Loaded config.json'); return merged; } catch (err) { logger.warn({ err, path: this.filePath }, 'Failed to read config.json, using defaults'); return { ...DEFAULTS }; } } save(): void { try { writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8'); } catch (err) { logger.error({ err, path: this.filePath }, 'Failed to save config.json'); } } get settings(): SavedSettings { return this.data; } get links(): SavedLink[] { return this.data.links; } addLink(url: string, name: string = '', mode: LinkMode = 'live', postAction?: PostAction): void { url = url.replace(/\/live\/?$/, ''); if (this.data.links.some((l) => l.url === url)) return; this.data.links.push({ url, name, active: true, mode, postAction: postAction || (mode === 'scrap' ? 'salvage' : 'stash'), addedAt: new Date().toISOString(), }); this.save(); } removeLink(url: string): void { this.data.links = this.data.links.filter((l) => l.url !== url); this.save(); } removeLinkById(id: string): void { this.data.links = this.data.links.filter((l) => { const parts = l.url.split('/'); return parts[parts.length - 1] !== id; }); this.save(); } updateLinkById(id: string, updates: { name?: string; active?: boolean; mode?: LinkMode; postAction?: PostAction }): SavedLink | null { const link = this.data.links.find((l) => { const parts = l.url.split('/'); return parts[parts.length - 1] === id; }); if (!link) return null; if (updates.name !== undefined) link.name = updates.name; if (updates.active !== undefined) link.active = updates.active; if (updates.mode !== undefined) link.mode = updates.mode; if (updates.postAction !== undefined) link.postAction = updates.postAction; this.save(); return link; } setPaused(paused: boolean): void { this.data.paused = paused; this.save(); } updateSettings(partial: Record): void { Object.assign(this.data, partial); this.save(); } }