| 85 | } |
| 86 | |
| 87 | export class ScheduleStore { |
| 88 | private db: Database.Database; |
| 89 | |
| 90 | constructor(dbPath: string = QODEX_SESSION_DB) { |
| 91 | this.db = openDatabase(dbPath); |
| 92 | this.db.exec(SCHEMA); |
| 93 | // Migrate DBs created before deliver/recipe existed. ADD COLUMN throws on an existing |
| 94 | // column, so each is guarded — idempotent and safe to run every startup. |
| 95 | for (const col of ['deliver TEXT', 'recipe TEXT']) { |
| 96 | try { this.db.exec(`ALTER TABLE schedules ADD COLUMN ${col}`); } catch { /* already present */ } |
| 97 | } |
| 98 | try { this.db.exec(`ALTER TABLE schedule_runs ADD COLUMN receipt TEXT`); } catch { /* already present */ } |
| 99 | } |
| 100 | |
| 101 | add(input: { |
| 102 | name: string; |
| 103 | cron: string; |
| 104 | prompt: string; |
| 105 | cwd: string; |
| 106 | model?: string; |
| 107 | allowedTools?: string[]; |
| 108 | deliver?: string; |
| 109 | recipe?: string; |
| 110 | }): ScheduleEntry { |
| 111 | const parsed = parseCron(input.cron); // throws on invalid |
| 112 | const next = nextAfter(parsed, new Date()); |
| 113 | const id = uuidv4(); |
| 114 | const allowed = input.allowedTools && input.allowedTools.length > 0 ? JSON.stringify(input.allowedTools) : null; |
| 115 | this.db.prepare(` |
| 116 | INSERT INTO schedules (id, name, cron, prompt, cwd, model, allowed_tools, next_run_at, deliver, recipe) |
| 117 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| 118 | `).run(id, input.name, input.cron, input.prompt, input.cwd, input.model ?? null, allowed, next?.toISOString() ?? null, |
| 119 | input.deliver ?? null, input.recipe ?? null); |
| 120 | return this.get(id)!; |
| 121 | } |
| 122 | |
| 123 | remove(idOrName: string): boolean { |
| 124 | const e = this.resolve(idOrName); |
| 125 | if (!e) return false; |
| 126 | this.db.prepare(`DELETE FROM schedules WHERE id = ?`).run(e.id); |
| 127 | return true; |
| 128 | } |
| 129 | |
| 130 | setEnabled(idOrName: string, enabled: boolean): ScheduleEntry | null { |
| 131 | const e = this.resolve(idOrName); |
| 132 | if (!e) return null; |
| 133 | this.db.prepare(`UPDATE schedules SET enabled = ? WHERE id = ?`).run(enabled ? 1 : 0, e.id); |
| 134 | if (enabled) this.recomputeNext(e.id); |
| 135 | return this.get(e.id) ?? null; |
| 136 | } |
| 137 | |
| 138 | get(id: string): ScheduleEntry | undefined { |
| 139 | return this.db.prepare(`SELECT * FROM schedules WHERE id = ?`).get(id) as ScheduleEntry | undefined; |
| 140 | } |
| 141 | |
| 142 | /** Find by exact id, id prefix (>=4 chars), or exact name. */ |
| 143 | resolve(idOrName: string): ScheduleEntry | undefined { |
| 144 | const exact = this.get(idOrName); |
nothing calls this directly
no outgoing calls
no test coverage detected