import { app } from 'electron'; import fs from 'fs'; import path from 'path'; import log from 'electron-log'; import type { AutomationScript, ScriptMetaItem, ScriptsMeta, ScriptSaveInput, } from '../../types/script-types'; const META_FILENAME = 'scripts.meta.json'; function getScriptsDir(): string { return app.isPackaged ? path.join(__dirname, 'scripts') : path.join(process.cwd(), 'electron/scripts'); } function ensureScriptsDir(): void { const dir = getScriptsDir(); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } function getMetaPath(): string { return path.join(getScriptsDir(), META_FILENAME); } function readMeta(): ScriptsMeta { const metaPath = getMetaPath(); if (!fs.existsSync(metaPath)) { return { scripts: [] }; } try { const raw = fs.readFileSync(metaPath, 'utf-8'); const parsed = JSON.parse(raw); if (parsed && Array.isArray(parsed.scripts)) { return parsed as ScriptsMeta; } } catch (err) { log.warn('[script-store-service] Failed to read meta:', err); } return { scripts: [] }; } function writeMeta(meta: ScriptsMeta): void { ensureScriptsDir(); const metaPath = getMetaPath(); fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8'); } function sanitizeFilename(name: string): string { return name .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-') .replace(/^-+|-+$/g, '') || 'script'; } function generateUniqueFilename(name: string, existingNames: Set): string { const base = sanitizeFilename(name); let filename = `${base}.mjs`; let counter = 1; while (existingNames.has(filename)) { filename = `${base}-${counter}.mjs`; counter++; } return filename; } function seedScripts(): void { const scriptsDir = getScriptsDir(); const metaPath = getMetaPath(); if (fs.existsSync(metaPath)) { return; } if (!fs.existsSync(scriptsDir)) { log.info('[script-store-service] Scripts directory does not exist, skipping seed.'); return; } const meta: ScriptsMeta = { scripts: [] }; const scriptFiles = fs.readdirSync(scriptsDir).filter((f) => f.endsWith('.mjs')); for (const file of scriptFiles) { try { const name = file.replace(/\.mjs$/, ''); const now = new Date().toISOString(); meta.scripts.push({ id: `seed-${name}`, name, description: '', filename: file, enabled: true, channel: '', createdAt: now, updatedAt: now, }); } catch (err) { log.warn('[script-store-service] Failed to seed script', file, err); } } writeMeta(meta); log.info('[script-store-service] Seeded scripts:', meta.scripts.length); } export function initScriptStoreService(): void { ensureScriptsDir(); seedScripts(); } export function listScripts(): AutomationScript[] { const meta = readMeta(); return meta.scripts .map((item) => enrichWithCode(item)) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); } export function getScript(id: string): AutomationScript | null { const meta = readMeta(); const item = meta.scripts.find((s) => s.id === id); if (!item) return null; return enrichWithCode(item); } export function getScriptPathById(id: string): string | null { const meta = readMeta(); const item = meta.scripts.find((s) => s.id === id); if (!item) return null; return path.join(getScriptsDir(), item.filename); } export function saveScript(input: ScriptSaveInput): AutomationScript { const meta = readMeta(); const scriptsDir = getScriptsDir(); const existingNames = new Set(meta.scripts.map((s) => s.filename)); const now = new Date().toISOString(); if (input.id) { const index = meta.scripts.findIndex((s) => s.id === input.id); if (index >= 0) { const existing = meta.scripts[index]; const filePath = path.join(scriptsDir, existing.filename); fs.writeFileSync(filePath, input.code, 'utf-8'); meta.scripts[index] = { ...existing, name: input.name, description: input.description, channel: input.channel, enabled: input.enabled, updatedAt: now, }; writeMeta(meta); return enrichWithCode(meta.scripts[index]); } } const filename = generateUniqueFilename(input.name, existingNames); const filePath = path.join(scriptsDir, filename); fs.writeFileSync(filePath, input.code, 'utf-8'); const id = `script-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; const item: ScriptMetaItem = { id, name: input.name, description: input.description, filename, enabled: input.enabled, channel: input.channel, createdAt: now, updatedAt: now, }; meta.scripts.push(item); writeMeta(meta); return enrichWithCode(item); } export function deleteScript(id: string): boolean { const meta = readMeta(); const index = meta.scripts.findIndex((s) => s.id === id); if (index === -1) return false; const item = meta.scripts[index]; const filePath = path.join(getScriptsDir(), item.filename); if (fs.existsSync(filePath)) { try { fs.unlinkSync(filePath); } catch (err) { log.warn('[script-store-service] Failed to delete script file:', err); } } meta.scripts.splice(index, 1); writeMeta(meta); return true; } export function toggleScript(id: string, enabled: boolean): boolean { const meta = readMeta(); const index = meta.scripts.findIndex((s) => s.id === id); if (index === -1) return false; meta.scripts[index].enabled = enabled; meta.scripts[index].updatedAt = new Date().toISOString(); writeMeta(meta); return true; } export function updateLastRun(id: string, lastRun: NonNullable): boolean { const meta = readMeta(); const index = meta.scripts.findIndex((s) => s.id === id); if (index === -1) return false; meta.scripts[index].lastRun = lastRun; meta.scripts[index].updatedAt = new Date().toISOString(); writeMeta(meta); return true; } function enrichWithCode(item: ScriptMetaItem): AutomationScript { const scriptsDir = getScriptsDir(); const filePath = path.join(scriptsDir, item.filename); let code = ''; try { if (fs.existsSync(filePath)) { code = fs.readFileSync(filePath, 'utf-8'); } } catch (err) { log.warn('[script-store-service] Failed to read script file:', err); } return { ...item, code, } as AutomationScript; }