prodboard 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/bin/prodboard.ts +4 -0
- package/config.schema.json +100 -0
- package/package.json +47 -6
- package/src/commands/comments.ts +86 -0
- package/src/commands/daemon.ts +112 -0
- package/src/commands/init.ts +83 -0
- package/src/commands/install.ts +127 -0
- package/src/commands/issues.ts +268 -0
- package/src/commands/schedules.ts +276 -0
- package/src/config.ts +155 -0
- package/src/confirm.ts +14 -0
- package/src/cron.ts +121 -0
- package/src/db.ts +157 -0
- package/src/format.ts +99 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +227 -0
- package/src/invocation.ts +102 -0
- package/src/logger.ts +84 -0
- package/src/mcp.ts +543 -0
- package/src/queries/comments.ts +31 -0
- package/src/queries/issues.ts +155 -0
- package/src/queries/runs.ts +159 -0
- package/src/queries/schedules.ts +115 -0
- package/src/scheduler.ts +411 -0
- package/src/templates.ts +43 -0
- package/src/types.ts +82 -0
- package/templates/CLAUDE.md +12 -0
- package/templates/config.jsonc +38 -0
- package/templates/mcp.json +8 -0
- package/templates/system-prompt-nogit.md +33 -0
- package/templates/system-prompt.md +31 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import type { Issue, Config } from "../types.ts";
|
|
3
|
+
import { generateId } from "../ids.ts";
|
|
4
|
+
|
|
5
|
+
export function createIssue(
|
|
6
|
+
db: Database,
|
|
7
|
+
opts: { title: string; description?: string; status?: string }
|
|
8
|
+
): Issue {
|
|
9
|
+
const id = generateId();
|
|
10
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
11
|
+
const status = opts.status ?? "todo";
|
|
12
|
+
const description = opts.description ?? "";
|
|
13
|
+
|
|
14
|
+
db.query(
|
|
15
|
+
"INSERT INTO issues (id, title, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
16
|
+
).run(id, opts.title, description, status, now, now);
|
|
17
|
+
|
|
18
|
+
return { id, title: opts.title, description, status, created_at: now, updated_at: now };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getIssue(db: Database, id: string): Issue | null {
|
|
22
|
+
return db.query("SELECT * FROM issues WHERE id = ?").get(id) as Issue | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getIssueByPrefix(db: Database, prefix: string): Issue {
|
|
26
|
+
// Try exact match first
|
|
27
|
+
const exact = getIssue(db, prefix);
|
|
28
|
+
if (exact) return exact;
|
|
29
|
+
|
|
30
|
+
const escaped = prefix.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
31
|
+
const matches = db
|
|
32
|
+
.query("SELECT * FROM issues WHERE id LIKE ? || '%' ESCAPE '\\'")
|
|
33
|
+
.all(escaped) as Issue[];
|
|
34
|
+
|
|
35
|
+
if (matches.length === 0) {
|
|
36
|
+
throw new Error(`Issue not found: ${prefix}`);
|
|
37
|
+
}
|
|
38
|
+
if (matches.length > 1) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Ambiguous prefix '${prefix}': matches ${matches.map((m) => m.id).join(", ")}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return matches[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveIssueId(db: Database, idOrPrefix: string): string {
|
|
47
|
+
return getIssueByPrefix(db, idOrPrefix).id;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function listIssues(
|
|
51
|
+
db: Database,
|
|
52
|
+
opts?: {
|
|
53
|
+
status?: string[];
|
|
54
|
+
search?: string;
|
|
55
|
+
sort?: string;
|
|
56
|
+
order?: string;
|
|
57
|
+
includeArchived?: boolean;
|
|
58
|
+
limit?: number;
|
|
59
|
+
}
|
|
60
|
+
): { issues: Issue[]; total: number } {
|
|
61
|
+
const conditions: string[] = [];
|
|
62
|
+
const params: any[] = [];
|
|
63
|
+
|
|
64
|
+
if (!opts?.includeArchived) {
|
|
65
|
+
conditions.push("status != 'archived'");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (opts?.status && opts.status.length > 0) {
|
|
69
|
+
const placeholders = opts.status.map(() => "?").join(", ");
|
|
70
|
+
conditions.push(`status IN (${placeholders})`);
|
|
71
|
+
params.push(...opts.status);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (opts?.search) {
|
|
75
|
+
conditions.push("(title LIKE '%' || ? || '%' OR description LIKE '%' || ? || '%')");
|
|
76
|
+
params.push(opts.search, opts.search);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
|
|
80
|
+
|
|
81
|
+
const VALID_SORT = new Set(["updated_at", "created_at", "title", "status"]);
|
|
82
|
+
const VALID_ORDER = new Set(["ASC", "DESC"]);
|
|
83
|
+
const sort = VALID_SORT.has(opts?.sort ?? "") ? opts!.sort! : "updated_at";
|
|
84
|
+
const order = VALID_ORDER.has((opts?.order ?? "").toUpperCase()) ? (opts!.order!).toUpperCase() : "DESC";
|
|
85
|
+
const limit = opts?.limit ?? 50;
|
|
86
|
+
|
|
87
|
+
const countResult = db.query(`SELECT COUNT(*) as count FROM issues ${where}`).get(...params) as { count: number };
|
|
88
|
+
|
|
89
|
+
const issues = db
|
|
90
|
+
.query(`SELECT * FROM issues ${where} ORDER BY ${sort} ${order} LIMIT ?`)
|
|
91
|
+
.all(...params, limit) as Issue[];
|
|
92
|
+
|
|
93
|
+
return { issues, total: countResult.count };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function updateIssue(
|
|
97
|
+
db: Database,
|
|
98
|
+
id: string,
|
|
99
|
+
fields: { title?: string; description?: string; status?: string }
|
|
100
|
+
): Issue {
|
|
101
|
+
const issue = getIssue(db, id);
|
|
102
|
+
if (!issue) throw new Error(`Issue not found: ${id}`);
|
|
103
|
+
|
|
104
|
+
const hasFields = fields.title !== undefined || fields.description !== undefined || fields.status !== undefined;
|
|
105
|
+
if (!hasFields) return issue;
|
|
106
|
+
|
|
107
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
108
|
+
const sets: string[] = ["updated_at = ?"];
|
|
109
|
+
const params: any[] = [now];
|
|
110
|
+
|
|
111
|
+
if (fields.title !== undefined) {
|
|
112
|
+
sets.push("title = ?");
|
|
113
|
+
params.push(fields.title);
|
|
114
|
+
}
|
|
115
|
+
if (fields.description !== undefined) {
|
|
116
|
+
sets.push("description = ?");
|
|
117
|
+
params.push(fields.description);
|
|
118
|
+
}
|
|
119
|
+
if (fields.status !== undefined) {
|
|
120
|
+
sets.push("status = ?");
|
|
121
|
+
params.push(fields.status);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
params.push(id);
|
|
125
|
+
db.query(`UPDATE issues SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
126
|
+
|
|
127
|
+
return getIssue(db, id)!;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function deleteIssue(db: Database, id: string): void {
|
|
131
|
+
const result = db.query("DELETE FROM issues WHERE id = ?").run(id);
|
|
132
|
+
if (result.changes === 0) {
|
|
133
|
+
throw new Error(`Issue not found: ${id}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getIssueCounts(db: Database): Record<string, number> {
|
|
138
|
+
const rows = db
|
|
139
|
+
.query("SELECT status, COUNT(*) as count FROM issues GROUP BY status")
|
|
140
|
+
.all() as { status: string; count: number }[];
|
|
141
|
+
|
|
142
|
+
const counts: Record<string, number> = {};
|
|
143
|
+
for (const row of rows) {
|
|
144
|
+
counts[row.status] = row.count;
|
|
145
|
+
}
|
|
146
|
+
return counts;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function validateStatus(status: string, config: Config): void {
|
|
150
|
+
if (!config.general.statuses.includes(status)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Invalid status '${status}'. Valid statuses: ${config.general.statuses.join(", ")}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import type { Run } from "../types.ts";
|
|
3
|
+
import { generateId } from "../ids.ts";
|
|
4
|
+
|
|
5
|
+
export function createRun(
|
|
6
|
+
db: Database,
|
|
7
|
+
opts: { schedule_id: string; prompt_used: string; pid?: number }
|
|
8
|
+
): Run {
|
|
9
|
+
const id = generateId();
|
|
10
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
11
|
+
|
|
12
|
+
db.query(`
|
|
13
|
+
INSERT INTO runs (id, schedule_id, status, prompt_used, pid, started_at)
|
|
14
|
+
VALUES (?, ?, 'running', ?, ?, ?)
|
|
15
|
+
`).run(id, opts.schedule_id, opts.prompt_used, opts.pid ?? null, now);
|
|
16
|
+
|
|
17
|
+
return db.query("SELECT * FROM runs WHERE id = ?").get(id) as Run;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function updateRun(
|
|
21
|
+
db: Database,
|
|
22
|
+
id: string,
|
|
23
|
+
fields: Partial<Omit<Run, "id" | "schedule_id" | "started_at">>
|
|
24
|
+
): void {
|
|
25
|
+
const sets: string[] = [];
|
|
26
|
+
const params: any[] = [];
|
|
27
|
+
|
|
28
|
+
const fieldMap: Record<string, string> = {
|
|
29
|
+
status: "status", finished_at: "finished_at", exit_code: "exit_code",
|
|
30
|
+
stdout_tail: "stdout_tail", stderr_tail: "stderr_tail",
|
|
31
|
+
session_id: "session_id", worktree_path: "worktree_path",
|
|
32
|
+
tokens_in: "tokens_in", tokens_out: "tokens_out", cost_usd: "cost_usd",
|
|
33
|
+
tools_used: "tools_used", issues_touched: "issues_touched",
|
|
34
|
+
prompt_used: "prompt_used", pid: "pid",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (const [key, col] of Object.entries(fieldMap)) {
|
|
38
|
+
if ((fields as any)[key] !== undefined) {
|
|
39
|
+
sets.push(`${col} = ?`);
|
|
40
|
+
params.push((fields as any)[key]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (sets.length === 0) return;
|
|
45
|
+
|
|
46
|
+
params.push(id);
|
|
47
|
+
db.query(`UPDATE runs SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function listRuns(
|
|
51
|
+
db: Database,
|
|
52
|
+
opts?: { schedule_id?: string; status?: string; limit?: number }
|
|
53
|
+
): Run[] {
|
|
54
|
+
const conditions: string[] = [];
|
|
55
|
+
const params: any[] = [];
|
|
56
|
+
|
|
57
|
+
if (opts?.schedule_id) {
|
|
58
|
+
conditions.push("r.schedule_id = ?");
|
|
59
|
+
params.push(opts.schedule_id);
|
|
60
|
+
}
|
|
61
|
+
if (opts?.status) {
|
|
62
|
+
conditions.push("r.status = ?");
|
|
63
|
+
params.push(opts.status);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
|
|
67
|
+
const limit = opts?.limit ?? 50;
|
|
68
|
+
|
|
69
|
+
return db.query(`
|
|
70
|
+
SELECT r.*, s.name as schedule_name
|
|
71
|
+
FROM runs r
|
|
72
|
+
LEFT JOIN schedules s ON r.schedule_id = s.id
|
|
73
|
+
${where}
|
|
74
|
+
ORDER BY r.started_at DESC
|
|
75
|
+
LIMIT ?
|
|
76
|
+
`).all(...params, limit) as Run[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getRunningRuns(db: Database): Run[] {
|
|
80
|
+
return db.query("SELECT * FROM runs WHERE status = 'running'").all() as Run[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getLastRun(db: Database, scheduleId: string): Run | null {
|
|
84
|
+
return db.query(
|
|
85
|
+
"SELECT * FROM runs WHERE schedule_id = ? ORDER BY started_at DESC LIMIT 1"
|
|
86
|
+
).get(scheduleId) as Run | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getLastSessionId(db: Database, scheduleId: string): string | null {
|
|
90
|
+
const run = db.query(
|
|
91
|
+
"SELECT session_id FROM runs WHERE schedule_id = ? AND session_id IS NOT NULL ORDER BY started_at DESC LIMIT 1"
|
|
92
|
+
).get(scheduleId) as { session_id: string } | null;
|
|
93
|
+
return run?.session_id ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getScheduleStats(
|
|
97
|
+
db: Database,
|
|
98
|
+
scheduleId?: string,
|
|
99
|
+
days?: number
|
|
100
|
+
): {
|
|
101
|
+
total: number;
|
|
102
|
+
success: number;
|
|
103
|
+
failed: number;
|
|
104
|
+
success_rate: number;
|
|
105
|
+
avg_tokens_in: number;
|
|
106
|
+
avg_tokens_out: number;
|
|
107
|
+
total_cost: number;
|
|
108
|
+
} {
|
|
109
|
+
const conditions: string[] = [];
|
|
110
|
+
const params: any[] = [];
|
|
111
|
+
|
|
112
|
+
if (scheduleId) {
|
|
113
|
+
conditions.push("schedule_id = ?");
|
|
114
|
+
params.push(scheduleId);
|
|
115
|
+
}
|
|
116
|
+
if (days) {
|
|
117
|
+
conditions.push("started_at >= datetime('now', ? || ' days')");
|
|
118
|
+
params.push(-days);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
|
|
122
|
+
|
|
123
|
+
const stats = db.query(`
|
|
124
|
+
SELECT
|
|
125
|
+
COUNT(*) as total,
|
|
126
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
|
|
127
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
|
|
128
|
+
AVG(tokens_in) as avg_tokens_in,
|
|
129
|
+
AVG(tokens_out) as avg_tokens_out,
|
|
130
|
+
SUM(COALESCE(cost_usd, 0)) as total_cost
|
|
131
|
+
FROM runs ${where}
|
|
132
|
+
`).get(...params) as any;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
total: stats.total ?? 0,
|
|
136
|
+
success: stats.success ?? 0,
|
|
137
|
+
failed: stats.failed ?? 0,
|
|
138
|
+
success_rate: stats.total > 0 ? (stats.success ?? 0) / stats.total : 0,
|
|
139
|
+
avg_tokens_in: stats.avg_tokens_in ?? 0,
|
|
140
|
+
avg_tokens_out: stats.avg_tokens_out ?? 0,
|
|
141
|
+
total_cost: stats.total_cost ?? 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function pruneOldRuns(db: Database, retentionDays: number): number {
|
|
146
|
+
const result = db.query(
|
|
147
|
+
"DELETE FROM runs WHERE started_at < datetime('now', ? || ' days')"
|
|
148
|
+
).run(-retentionDays);
|
|
149
|
+
return result.changes;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getSessionRunCount(db: Database, scheduleId: string): number {
|
|
153
|
+
const lastSessionId = getLastSessionId(db, scheduleId);
|
|
154
|
+
if (!lastSessionId) return 0;
|
|
155
|
+
const result = db.query(
|
|
156
|
+
"SELECT COUNT(*) as count FROM runs WHERE schedule_id = ? AND session_id = ?"
|
|
157
|
+
).get(scheduleId, lastSessionId) as { count: number };
|
|
158
|
+
return result.count;
|
|
159
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import type { Schedule } from "../types.ts";
|
|
3
|
+
import { generateId } from "../ids.ts";
|
|
4
|
+
|
|
5
|
+
export function createSchedule(
|
|
6
|
+
db: Database,
|
|
7
|
+
opts: {
|
|
8
|
+
name: string;
|
|
9
|
+
cron: string;
|
|
10
|
+
prompt: string;
|
|
11
|
+
workdir?: string;
|
|
12
|
+
max_turns?: number;
|
|
13
|
+
allowed_tools?: string;
|
|
14
|
+
use_worktree?: boolean;
|
|
15
|
+
inject_context?: boolean;
|
|
16
|
+
persist_session?: boolean;
|
|
17
|
+
agents_json?: string;
|
|
18
|
+
source?: string;
|
|
19
|
+
}
|
|
20
|
+
): Schedule {
|
|
21
|
+
const id = generateId();
|
|
22
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
23
|
+
|
|
24
|
+
db.query(`
|
|
25
|
+
INSERT INTO schedules (id, name, cron, prompt, workdir, max_turns, allowed_tools,
|
|
26
|
+
use_worktree, inject_context, persist_session, agents_json, source, created_at, updated_at)
|
|
27
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
28
|
+
`).run(
|
|
29
|
+
id, opts.name, opts.cron, opts.prompt,
|
|
30
|
+
opts.workdir ?? ".",
|
|
31
|
+
opts.max_turns ?? null,
|
|
32
|
+
opts.allowed_tools ?? null,
|
|
33
|
+
opts.use_worktree !== false ? 1 : 0,
|
|
34
|
+
opts.inject_context !== false ? 1 : 0,
|
|
35
|
+
opts.persist_session ? 1 : 0,
|
|
36
|
+
opts.agents_json ?? null,
|
|
37
|
+
opts.source ?? "cli",
|
|
38
|
+
now, now
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return getSchedule(db, id)!;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getSchedule(db: Database, id: string): Schedule | null {
|
|
45
|
+
return db.query("SELECT * FROM schedules WHERE id = ?").get(id) as Schedule | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getScheduleByPrefix(db: Database, prefix: string): Schedule {
|
|
49
|
+
const exact = getSchedule(db, prefix);
|
|
50
|
+
if (exact) return exact;
|
|
51
|
+
|
|
52
|
+
const escaped = prefix.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
53
|
+
const matches = db
|
|
54
|
+
.query("SELECT * FROM schedules WHERE id LIKE ? || '%' ESCAPE '\\'")
|
|
55
|
+
.all(escaped) as Schedule[];
|
|
56
|
+
|
|
57
|
+
if (matches.length === 0) throw new Error(`Schedule not found: ${prefix}`);
|
|
58
|
+
if (matches.length > 1) throw new Error(`Ambiguous prefix '${prefix}': matches ${matches.map((m) => m.id).join(", ")}`);
|
|
59
|
+
return matches[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function listSchedules(
|
|
63
|
+
db: Database,
|
|
64
|
+
opts?: { includeDisabled?: boolean }
|
|
65
|
+
): Schedule[] {
|
|
66
|
+
if (opts?.includeDisabled) {
|
|
67
|
+
return db.query("SELECT * FROM schedules ORDER BY created_at DESC").all() as Schedule[];
|
|
68
|
+
}
|
|
69
|
+
return db.query("SELECT * FROM schedules WHERE enabled = 1 ORDER BY created_at DESC").all() as Schedule[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function updateSchedule(
|
|
73
|
+
db: Database,
|
|
74
|
+
id: string,
|
|
75
|
+
fields: Partial<Omit<Schedule, "id" | "created_at">>
|
|
76
|
+
): Schedule {
|
|
77
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
78
|
+
const sets: string[] = ["updated_at = ?"];
|
|
79
|
+
const params: any[] = [now];
|
|
80
|
+
|
|
81
|
+
const fieldMap: Record<string, string> = {
|
|
82
|
+
name: "name", cron: "cron", prompt: "prompt", workdir: "workdir",
|
|
83
|
+
enabled: "enabled", max_turns: "max_turns", allowed_tools: "allowed_tools",
|
|
84
|
+
use_worktree: "use_worktree", inject_context: "inject_context",
|
|
85
|
+
persist_session: "persist_session", agents_json: "agents_json",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let hasRealFields = false;
|
|
89
|
+
for (const [key, col] of Object.entries(fieldMap)) {
|
|
90
|
+
if ((fields as any)[key] !== undefined) {
|
|
91
|
+
sets.push(`${col} = ?`);
|
|
92
|
+
params.push((fields as any)[key]);
|
|
93
|
+
hasRealFields = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!hasRealFields) return getSchedule(db, id)!;
|
|
98
|
+
|
|
99
|
+
params.push(id);
|
|
100
|
+
db.query(`UPDATE schedules SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
101
|
+
return getSchedule(db, id)!;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deleteSchedule(db: Database, id: string): void {
|
|
105
|
+
const result = db.query("DELETE FROM schedules WHERE id = ?").run(id);
|
|
106
|
+
if (result.changes === 0) throw new Error(`Schedule not found: ${id}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function enableSchedule(db: Database, id: string): void {
|
|
110
|
+
db.query("UPDATE schedules SET enabled = 1, updated_at = datetime('now') WHERE id = ?").run(id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function disableSchedule(db: Database, id: string): void {
|
|
114
|
+
db.query("UPDATE schedules SET enabled = 0, updated_at = datetime('now') WHERE id = ?").run(id);
|
|
115
|
+
}
|