/** * Windows Task Scheduler integration for the daily watched-source sync * (ROADMAP-PINCHFLAT.md Phase J). Registers a task that launches AeroFetch with * `--sync` once a day; the app then runs its startup sync of watched sources. * * NOTE: this is OS-level wiring and cannot be exercised in the Vite UI preview or * the unit tests — like the `aerofetch://` protocol registration, it needs a real * install + manual smoke test. `schtasks` is invoked via execFile (no shell), and * the only interpolated value is the trusted `process.execPath`. */ import { execFile } from 'child_process' import type { ScheduledSyncStatus } from '@shared/ipc' const TASK_NAME = 'AeroFetchDailySync' /** The argv flag the scheduled task passes so startup knows it's a sync launch. */ export const SYNC_FLAG = '--sync' function schtasks(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> { return new Promise((resolve) => { execFile('schtasks', args, { windowsHide: true }, (err, stdout, stderr) => { const code = err ? ((err as { code?: number }).code ?? 1) : 0 resolve({ code, stdout: String(stdout), stderr: String(stderr) }) }) }) } /** True when this launch came from the scheduled task (argv carries --sync). */ export function isSyncLaunch(argv: string[]): boolean { return argv.includes(SYNC_FLAG) } /** Whether the daily-sync scheduled task is currently registered. */ export async function getScheduledSync(): Promise { const r = await schtasks(['/Query', '/TN', TASK_NAME]) return { enabled: r.code === 0 } } /** * Register or remove the daily-sync scheduled task. Creating runs AeroFetch with * `--sync` every day at 09:00 (overwriting any prior task of the same name). */ export async function setScheduledSync(enabled: boolean): Promise { if (enabled) { const tr = `"${process.execPath}" ${SYNC_FLAG}` const r = await schtasks([ '/Create', '/F', '/SC', 'DAILY', '/ST', '09:00', '/TN', TASK_NAME, '/TR', tr ]) return { enabled: r.code === 0, error: r.code === 0 ? undefined : r.stderr.trim() || 'Could not create the scheduled task.' } } const r = await schtasks(['/Delete', '/F', '/TN', TASK_NAME]) // A missing task ("cannot find") is success for our purposes — it's already gone. const gone = r.code === 0 || /cannot find|does not exist/i.test(r.stderr) return { enabled: gone ? false : true, error: gone ? undefined : r.stderr.trim() } }