baton-issue-tracker 1.3.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/README.md +13 -0
- package/drizzle/0000_tidy_enchantress.sql +18 -0
- package/drizzle/0001_salty_luke_cage.sql +16 -0
- package/drizzle/meta/0000_snapshot.json +135 -0
- package/drizzle/meta/0001_snapshot.json +156 -0
- package/drizzle/meta/_journal.json +20 -0
- package/package.json +43 -0
- package/source/cli.js +133 -0
- package/source/commands/approve.js +43 -0
- package/source/commands/create.js +48 -0
- package/source/commands/init.js +171 -0
- package/source/commands/list.js +67 -0
- package/source/commands/loop.js +63 -0
- package/source/commands/next.js +46 -0
- package/source/commands/search.js +44 -0
- package/source/commands/status.js +55 -0
- package/source/commands/update.js +74 -0
- package/source/commands/view.js +46 -0
- package/source/db/index.js +72 -0
- package/source/index.js +1 -0
- package/source/models/activityLog.js +23 -0
- package/source/models/issue.js +72 -0
- package/source/models/schema.js +53 -0
- package/source/services/issuesService.js +476 -0
- package/source/temp.md +1 -0
- package/source/util.js +260 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const Status = Object.freeze({
|
|
2
|
+
OPEN: "Open",
|
|
3
|
+
IN_PROGRESS: "In-Progress",
|
|
4
|
+
IN_REVIEW: "In-Review",
|
|
5
|
+
CLOSED: "Closed"
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const Priority = Object.freeze({
|
|
9
|
+
LOW: "Low",
|
|
10
|
+
MEDIUM: "Medium",
|
|
11
|
+
HIGH: "High",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export class Issue {
|
|
15
|
+
constructor({
|
|
16
|
+
// User fields
|
|
17
|
+
title = "Issue #",
|
|
18
|
+
status = Status.OPEN,
|
|
19
|
+
priority = Priority.LOW,
|
|
20
|
+
tokenLimit = null,
|
|
21
|
+
description = null,
|
|
22
|
+
lastUpdated = new Date().toISOString(),
|
|
23
|
+
assignees = null,
|
|
24
|
+
// Auto-generated fields
|
|
25
|
+
id = 0,
|
|
26
|
+
createdAt = new Date().toISOString(),
|
|
27
|
+
attemptNum = 0,
|
|
28
|
+
} = {}) {
|
|
29
|
+
this.title = title;
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.priority = priority;
|
|
32
|
+
this.tokenLimit = tokenLimit;
|
|
33
|
+
this.description = description;
|
|
34
|
+
this.lastUpdated = lastUpdated;
|
|
35
|
+
this.assignees = assignees;
|
|
36
|
+
this.id = id;
|
|
37
|
+
this.createdAt = createdAt;
|
|
38
|
+
this.attemptNum = attemptNum;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validates the issue fields based on project business rules.
|
|
44
|
+
* @returns {{isValid: boolean, errors: string[]}} boolean and an array of errors
|
|
45
|
+
*/
|
|
46
|
+
validate(){
|
|
47
|
+
const errors = [];
|
|
48
|
+
|
|
49
|
+
if (!this.title || typeof this.title !== "string" || this.title.trim() === ""){
|
|
50
|
+
errors.push("Title cannot be empty");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!Object.values(Status).includes(this.status)) {
|
|
54
|
+
errors.push(`Invalid status "${this.status}". Must be one of: ${Object.values(Status).join(", ")}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!Object.values(Priority).includes(this.priority)) {
|
|
58
|
+
errors.push(`Invalid priority "${this.priority}". Must be one of: ${Object.values(Priority).join(", ")}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.tokenLimit != null){
|
|
62
|
+
const tokenLim = Number(this.tokenLimit);
|
|
63
|
+
if (isNaN(tokenLim) || tokenLim <= 0){
|
|
64
|
+
errors.push("tokenLimit must be positive");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {isValid: errors.length == 0, errors: errors}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { check, int, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import { Status, Priority } from "./issue.js";
|
|
4
|
+
import { Action } from "./activityLog.js";
|
|
5
|
+
|
|
6
|
+
export const issuesTable = sqliteTable("issues", {
|
|
7
|
+
id: int().primaryKey({ autoIncrement: true }),
|
|
8
|
+
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
9
|
+
lastUpdated: text("last_updated").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
10
|
+
attemptNum: int("attempt_num").notNull().default(0),
|
|
11
|
+
title: text().notNull().default("PENDING"),
|
|
12
|
+
status: text({ enum: Object.values(Status) }).notNull().default(Status.OPEN),
|
|
13
|
+
priority: text({ enum: Object.values(Priority) }).default(Priority.LOW),
|
|
14
|
+
tokenLimit: int("token_limit"),
|
|
15
|
+
description: text(),
|
|
16
|
+
assignees: text({mode: "json"}).default(sql`'[]'`),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const activityTable = sqliteTable(
|
|
20
|
+
"activity",
|
|
21
|
+
{
|
|
22
|
+
logId: int("log_id").primaryKey({ autoIncrement: true }),
|
|
23
|
+
issueId: int("issue_id"),
|
|
24
|
+
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
25
|
+
action: text({ enum: Object.values(Action) }).notNull(),
|
|
26
|
+
details: text(),
|
|
27
|
+
},
|
|
28
|
+
(table) => [
|
|
29
|
+
check(
|
|
30
|
+
"activity_action_check",
|
|
31
|
+
sql`${table.action} IN (${sql.raw(
|
|
32
|
+
Object.values(Action)
|
|
33
|
+
.map((value) => `'${value}'`)
|
|
34
|
+
.join(", "),
|
|
35
|
+
)})`,
|
|
36
|
+
),
|
|
37
|
+
],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// helper for parsing command line arguments
|
|
41
|
+
export const issueSchema = {
|
|
42
|
+
id: { flag: '--id', type: 'number' },
|
|
43
|
+
attemptNum: { flag: '--attempt', type: 'number' },
|
|
44
|
+
title: { flag: '--title', type: 'string' },
|
|
45
|
+
status: { flag: '--status', type: 'enum', values: Object.values(Status) },
|
|
46
|
+
tokenLimit: { flag: '--token-limit', type: 'number' },
|
|
47
|
+
priority: { flag: '--priority', type: 'enum', values: Object.values(Priority) },
|
|
48
|
+
description:{ flag: '--description', type: 'string' },
|
|
49
|
+
//assignees: { flag: '--assignees', type: 'json'},
|
|
50
|
+
// Pagination options
|
|
51
|
+
limit: { flag: '--limit', type: 'number' },
|
|
52
|
+
offset: { flag: '--offset', type: 'number' }
|
|
53
|
+
};
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { getDB } from '../db/index.js';
|
|
2
|
+
import { eq, and, or, like, sql } from "drizzle-orm";
|
|
3
|
+
import {issuesTable, activityTable} from "../models/schema.js";
|
|
4
|
+
import {
|
|
5
|
+
Issue,
|
|
6
|
+
Status,
|
|
7
|
+
Priority,
|
|
8
|
+
} from "../models/issue.js";
|
|
9
|
+
import { ActivityLog, Action } from '../models/activityLog.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal helper to log actions.
|
|
13
|
+
* @private
|
|
14
|
+
* @param {object} db - The database instance.
|
|
15
|
+
* @param {number} issueId - The ID of the issue.
|
|
16
|
+
* @param {string} action - The action type.
|
|
17
|
+
* @param {string|null} [details=null] - Optional details.
|
|
18
|
+
*/
|
|
19
|
+
function logActivity(db, issueId, action, details = null) {
|
|
20
|
+
db.insert(activityTable)
|
|
21
|
+
.values({ issueId, action, details })
|
|
22
|
+
.run();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert a raw database row to an Issue instance.
|
|
27
|
+
* @private
|
|
28
|
+
* @param {object|null} row - The raw database row.
|
|
29
|
+
* @returns {Issue|null}
|
|
30
|
+
*/
|
|
31
|
+
function rowToIssue(row) {
|
|
32
|
+
return row ? new Issue(row) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert a raw database row to an ActivityLog instance.
|
|
37
|
+
* @private
|
|
38
|
+
* @param {object|null} row - The raw database row.
|
|
39
|
+
* @returns {ActivityLog|null}
|
|
40
|
+
*/
|
|
41
|
+
function rowToLog(row) {
|
|
42
|
+
return row ? new ActivityLog(row) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fetch a raw issue row by ID, throwing if not found.
|
|
47
|
+
* @private
|
|
48
|
+
* @param {object} db - The database instance.
|
|
49
|
+
* @param {number} id - The ID of the issue.
|
|
50
|
+
* @returns {object} The raw database row.
|
|
51
|
+
* @throws {Error} If no issue with the given ID exists.
|
|
52
|
+
*/
|
|
53
|
+
function findById(db, id) {
|
|
54
|
+
const row = db.select()
|
|
55
|
+
.from(issuesTable)
|
|
56
|
+
.where(eq(issuesTable.id, id))
|
|
57
|
+
.get();
|
|
58
|
+
if (!row) throw new Error(`Issue #${id} not found`);
|
|
59
|
+
return row;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} CreateIssueFields
|
|
64
|
+
* @property {string} [title] - The title of the issue.
|
|
65
|
+
* @property {string} [priority] - The priority level.
|
|
66
|
+
* @property {number} [tokenLimit] - The maximum token limit for the issue.
|
|
67
|
+
* @property {string} [description] - The detailed description of the issue.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a new issue.
|
|
72
|
+
* Title defaults to "Issue #<id>" via SQL trigger if not provided.
|
|
73
|
+
* @param {CreateIssueFields} fields - The fields to initialize the issue with.
|
|
74
|
+
* @returns {Issue}
|
|
75
|
+
*/
|
|
76
|
+
export function createIssue({
|
|
77
|
+
title,
|
|
78
|
+
priority,
|
|
79
|
+
tokenLimit,
|
|
80
|
+
description,
|
|
81
|
+
} = {}) {
|
|
82
|
+
|
|
83
|
+
const db = getDB();
|
|
84
|
+
const result = db.insert(issuesTable)
|
|
85
|
+
.values({
|
|
86
|
+
title: title?.trim() || "PENDING",
|
|
87
|
+
priority: priority ?? Priority.LOW,
|
|
88
|
+
tokenLimit: tokenLimit ?? null,
|
|
89
|
+
description: description ?? null,
|
|
90
|
+
})
|
|
91
|
+
.returning({ id: issuesTable.id })
|
|
92
|
+
.get();
|
|
93
|
+
|
|
94
|
+
// re-fetch the issue to get auto-generated fields back
|
|
95
|
+
const issue = rowToIssue(findById(db, result.id));
|
|
96
|
+
logActivity(db, issue.id, Action.CREATION, `"${issue.title}" was created.`);
|
|
97
|
+
|
|
98
|
+
return issue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get a single issue by id. Logs a read event.
|
|
103
|
+
* @param {number} id
|
|
104
|
+
* @returns {Issue}
|
|
105
|
+
*/
|
|
106
|
+
export function getIssue(id) {
|
|
107
|
+
const db = getDB();
|
|
108
|
+
const issue = rowToIssue(findById(db, id));
|
|
109
|
+
logActivity(db, id, Action.READ, `Issue #${id} was accessed.`);
|
|
110
|
+
return issue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @typedef {Object} ListIssuesOptions
|
|
115
|
+
* @property {string} [status] - Filter issues by their current status.
|
|
116
|
+
* @property {string} [priority] - Filter issues by their priority level.
|
|
117
|
+
* @property {number} [limit] - Maximum number of issues to return.
|
|
118
|
+
* @property {number} [offset] - Pagination offset.
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* List issues with optional filters. Does not log activity.
|
|
123
|
+
* @param {ListIssuesOptions} options - Filtering and pagination options.
|
|
124
|
+
* @returns {Issue[]}
|
|
125
|
+
*/
|
|
126
|
+
export async function listIssues({ status, priority, limit, offset } = {}) {
|
|
127
|
+
const db = getDB();
|
|
128
|
+
const filters = [];
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if (status) {
|
|
132
|
+
filters.push(sql`${issuesTable.status} COLLATE NOCASE = ${status}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (priority) {
|
|
136
|
+
filters.push(sql`${issuesTable.priority} COLLATE NOCASE = ${priority}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// set defaults if limit or offset is NULL
|
|
140
|
+
const limitVal = limit ?? 50;
|
|
141
|
+
const offsetVal = offset ?? 0;
|
|
142
|
+
|
|
143
|
+
return db.select()
|
|
144
|
+
.from(issuesTable)
|
|
145
|
+
.where(filters.length > 0 ? and(...filters) : undefined)
|
|
146
|
+
.limit(limitVal)
|
|
147
|
+
.offset(offsetVal)
|
|
148
|
+
.all();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Search issues by title or description. Does not log activity.
|
|
153
|
+
* @param {string} query
|
|
154
|
+
* @returns {Issue[]}
|
|
155
|
+
*/
|
|
156
|
+
export function searchIssues(query) {
|
|
157
|
+
const db = getDB();
|
|
158
|
+
|
|
159
|
+
if (!query || query.trim() == "") {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const searchTerm = `%${query.toLowerCase().trim()}%`;
|
|
164
|
+
|
|
165
|
+
return db.select()
|
|
166
|
+
.from(issuesTable)
|
|
167
|
+
.where(
|
|
168
|
+
or(
|
|
169
|
+
like(issuesTable.title, searchTerm),
|
|
170
|
+
like(issuesTable.description, searchTerm)
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
.all();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @typedef {Object} UpdateIssueFields
|
|
178
|
+
* @property {string} [title] - The updated title.
|
|
179
|
+
* @property {string} [description] - The updated description.
|
|
180
|
+
* @property {number} [tokenLimit] - The updated token limit.
|
|
181
|
+
* @property {string} [status] - The updated status.
|
|
182
|
+
* @property {string} [priority] - The updated priority.
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Update editable fields: title, description, tokenLimit, status, priority.
|
|
187
|
+
* Logs an edit event.
|
|
188
|
+
* @param {number} id
|
|
189
|
+
* @param {Issue} oldIssue - The current data
|
|
190
|
+
* @param {UpdateIssueFields} fields - The fields to update.
|
|
191
|
+
* @returns {Issue}
|
|
192
|
+
*/
|
|
193
|
+
export function updateIssue(id, oldIssue, { title, description, tokenLimit, status, priority } = {}) {
|
|
194
|
+
const db = getDB();
|
|
195
|
+
const updates = {};
|
|
196
|
+
|
|
197
|
+
if (title !== undefined) updates.title = title;
|
|
198
|
+
if (description !== undefined) updates.description = description;
|
|
199
|
+
if (tokenLimit !== undefined) updates.tokenLimit = tokenLimit;
|
|
200
|
+
|
|
201
|
+
// Normalize status argument
|
|
202
|
+
if (status !== undefined) {
|
|
203
|
+
const statusValues = Object.values(Status);
|
|
204
|
+
const toUpdate = statusValues.find(v => v.trim().toLowerCase() === status.trim().toLowerCase());
|
|
205
|
+
updates.status = toUpdate || status;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Normalize priority argument
|
|
209
|
+
if (priority !== undefined) {
|
|
210
|
+
const priorityValues = Object.values(Priority);
|
|
211
|
+
const toUpdate = priorityValues.find(v => v.toLowerCase().trim() === priority.trim().toLowerCase());
|
|
212
|
+
updates.priority = toUpdate || priority;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Validate the new data
|
|
216
|
+
const proposedIssue = new Issue({ ...oldIssue, ...updates });
|
|
217
|
+
const { isValid, errors } = proposedIssue.validate();
|
|
218
|
+
|
|
219
|
+
if (!isValid) {
|
|
220
|
+
throw new Error(`Validation failed: ${errors.join(", ")}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (Object.keys(updates).length > 0) {
|
|
224
|
+
db.update(issuesTable)
|
|
225
|
+
.set(updates)
|
|
226
|
+
.where(eq(issuesTable.id, id))
|
|
227
|
+
.run();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
logActivity(db, id, Action.EDIT, `Issue #${id} was updated.`);
|
|
231
|
+
return getIssue(id);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Change the status of an issue from in-review to closed
|
|
236
|
+
* Logs a closed event.
|
|
237
|
+
* @param {number} id
|
|
238
|
+
* @returns {Issue}
|
|
239
|
+
*/
|
|
240
|
+
export function approveIssue(id) {
|
|
241
|
+
const db = getDB();
|
|
242
|
+
db.update(issuesTable).set({ status: Status.CLOSED }).where(eq(issuesTable.id, id)).run();
|
|
243
|
+
logActivity(db, id, Action.STATE_CHANGE, `Issue #${id} has been closed`);
|
|
244
|
+
return getIssue(id);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Change the status of an issue from in-review to in-progress
|
|
249
|
+
* Logs a reject event and reason.
|
|
250
|
+
* @param {number} id
|
|
251
|
+
* @param {string} reason
|
|
252
|
+
* @returns {Issue}
|
|
253
|
+
*/
|
|
254
|
+
export function rejectIssue(id, reason) {
|
|
255
|
+
const db = getDB();
|
|
256
|
+
db.update(issuesTable).set({ status: Status.IN_PROGRESS }).where(eq(issuesTable.id, id)).run();
|
|
257
|
+
logActivity(db, id, Action.REJECT, `Issue #${id} has been rejected due to "${reason}"`);
|
|
258
|
+
return getIssue(id);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Change the status of an issue to in-review.
|
|
263
|
+
* Logs a state_change_event.
|
|
264
|
+
* @param {number} id
|
|
265
|
+
* @returns {Issue}
|
|
266
|
+
*/
|
|
267
|
+
export function submitForReview(id) {
|
|
268
|
+
const db = getDB();
|
|
269
|
+
db.update(issuesTable).set({ status: Status.IN_REVIEW }).where(eq(issuesTable.id, id)).run();
|
|
270
|
+
logActivity(db, id, Action.STATE_CHANGE, `Issue #${id} was submitted for review.`);
|
|
271
|
+
return getIssue(id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Change the status of an issue (Open / Closed).
|
|
276
|
+
* Logs a state_change event.
|
|
277
|
+
* @param {number} id
|
|
278
|
+
* @param {string} status
|
|
279
|
+
* @returns {Issue}
|
|
280
|
+
*/
|
|
281
|
+
export function setStatus(id, status) {
|
|
282
|
+
const db = getDB();
|
|
283
|
+
db.update(issuesTable).set({ status }).where(eq(issuesTable.id, id)).run();
|
|
284
|
+
logActivity(db, id, Action.STATE_CHANGE, `Issue #${id} status changed to ${status}.`);
|
|
285
|
+
return getIssue(id);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Change the priority of an issue (Low / Medium / High).
|
|
290
|
+
* Logs a priority_change event.
|
|
291
|
+
* @param {number} id
|
|
292
|
+
* @param {string} priority
|
|
293
|
+
* @returns {Issue}
|
|
294
|
+
*/
|
|
295
|
+
export function setPriority(id, priority) {
|
|
296
|
+
const db = getDB();
|
|
297
|
+
db.update(issuesTable).set({ priority }).where(eq(issuesTable.id, id)).run();
|
|
298
|
+
logActivity(db, id, Action.PRIORITY_CHANGE, `Issue #${id} priority changed to ${priority}.`);
|
|
299
|
+
return getIssue(id);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Increment the attempt counter for an issue.
|
|
304
|
+
* Logs an edit event.
|
|
305
|
+
* @param {number} id
|
|
306
|
+
* @returns {Issue}
|
|
307
|
+
*/
|
|
308
|
+
export function incrementAttempt(id) {
|
|
309
|
+
const db = getDB();
|
|
310
|
+
db.update(issuesTable)
|
|
311
|
+
.set({ attemptNum: sql`${issuesTable.attemptNum} + 1` })
|
|
312
|
+
.where(eq(issuesTable.id, id))
|
|
313
|
+
.run();
|
|
314
|
+
logActivity(db, id, Action.EDIT, `Attempt count increased for Issue #${id}.`);
|
|
315
|
+
return getIssue(id);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Delete an issue. Activity log entry is written before deletion
|
|
320
|
+
* to preserve the audit trail per schema spec.
|
|
321
|
+
* @param {number} id
|
|
322
|
+
* @returns {boolean}
|
|
323
|
+
*/
|
|
324
|
+
export function deleteIssue(id) {
|
|
325
|
+
const db = getDB();
|
|
326
|
+
const existing = findById(db, id);
|
|
327
|
+
|
|
328
|
+
logActivity(db, id, Action.DELETION, `"${existing.title}" was deleted.`);
|
|
329
|
+
db.delete(issuesTable).where(eq(issuesTable.id, id)).run();
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get the full activity history for a specific issue.
|
|
335
|
+
* @param {number} issueId
|
|
336
|
+
* @returns {ActivityLog[]}
|
|
337
|
+
*/
|
|
338
|
+
export function getActivityLog(issueId) {
|
|
339
|
+
const db = getDB();
|
|
340
|
+
return db.select().from(activityTable).where(eq(activityTable.issueId, issueId)).all();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get the most recent activity across all issues.
|
|
345
|
+
* @param {RecentActivityOptions} options - Options for retrieving recent activity.
|
|
346
|
+
* @returns {ActivityLog[]}
|
|
347
|
+
*/
|
|
348
|
+
export function getRecentActivity({ limit = 20 } = {}) {
|
|
349
|
+
const db = getDB();
|
|
350
|
+
return db.select().from(activityTable).orderBy(sql`${activityTable.logId} DESC`).limit(limit).all();
|
|
351
|
+
}
|
|
352
|
+
// =============================================================================
|
|
353
|
+
// Tracker operations (CLI: init / next / status / loop)
|
|
354
|
+
// To be edited later if needed.
|
|
355
|
+
// =============================================================================
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* True when both `issues` and `activity` tables exist (matches initDB schema).
|
|
359
|
+
* @returns {boolean}
|
|
360
|
+
*/
|
|
361
|
+
export function isTrackerReady() {
|
|
362
|
+
const db = getDB();
|
|
363
|
+
try {
|
|
364
|
+
// use raw SQL query here, can't use Drizzle
|
|
365
|
+
const row = db.get(sql`
|
|
366
|
+
SELECT COUNT(*) AS table_count
|
|
367
|
+
FROM sqlite_master
|
|
368
|
+
WHERE type = 'table' AND name IN ('issues', 'activity')
|
|
369
|
+
`);
|
|
370
|
+
return (row?.table_count ?? 0) === 2;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Issue counts by status for `baton status` (single round-trip).
|
|
378
|
+
* @returns {{ total: number, open: number, inProgress: number, closed: number }}
|
|
379
|
+
*/
|
|
380
|
+
export function getIssueStats() {
|
|
381
|
+
const db = getDB();
|
|
382
|
+
const row = db.get(sql`
|
|
383
|
+
SELECT
|
|
384
|
+
COUNT(*) AS total,
|
|
385
|
+
COALESCE(SUM(CASE WHEN status = 'Open' THEN 1 ELSE 0 END), 0) AS open_count,
|
|
386
|
+
COALESCE(SUM(CASE WHEN status = 'In-Progress' THEN 1 ELSE 0 END), 0) AS in_progress_count,
|
|
387
|
+
COALESCE(SUM(CASE WHEN status = 'In-Review' THEN 1 ELSE 0 END), 0) AS in_review_count,
|
|
388
|
+
COALESCE(SUM(CASE WHEN status = 'Closed' THEN 1 ELSE 0 END), 0) AS closed_count
|
|
389
|
+
FROM issues
|
|
390
|
+
`);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
total: Number(row?.total ?? 0),
|
|
394
|
+
open: Number(row?.open_count ?? 0),
|
|
395
|
+
inProgress: Number(row?.in_progress_count ?? 0),
|
|
396
|
+
inReview: Number(row?.in_review_count ?? 0),
|
|
397
|
+
closed: Number(row?.closed_count ?? 0),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* All issues ordered by id.
|
|
403
|
+
* @returns {object[]}
|
|
404
|
+
*/
|
|
405
|
+
export function getAllIssues() {
|
|
406
|
+
const db = getDB();
|
|
407
|
+
return db.select().from(issuesTable).orderBy(issuesTable.id).all();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Highest-priority open issue, then lowest id (same ordering as previous JS sort).
|
|
412
|
+
* @returns {object|null}
|
|
413
|
+
*/
|
|
414
|
+
export function selectNextIssue() {
|
|
415
|
+
const db = getDB();
|
|
416
|
+
return db.select()
|
|
417
|
+
.from(issuesTable)
|
|
418
|
+
.where(eq(issuesTable.status, Status.OPEN))
|
|
419
|
+
.orderBy(
|
|
420
|
+
sql`CASE priority WHEN 'High' THEN 0 WHEN 'Medium' THEN 1 WHEN 'Low' THEN 2 ELSE 3 END`,
|
|
421
|
+
issuesTable.id
|
|
422
|
+
)
|
|
423
|
+
.limit(1)
|
|
424
|
+
.get() ?? null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Mark an issue in-progress, increment attempts, and log activity (atomic).
|
|
429
|
+
* Uses existing `findById` / `logActivity` from above; does not modify lines 1–187.
|
|
430
|
+
* @param {number} issueId
|
|
431
|
+
* @returns {object}
|
|
432
|
+
*/
|
|
433
|
+
export function workOnIssue(issueId) {
|
|
434
|
+
const db = getDB();
|
|
435
|
+
const issue = findById(db, issueId);
|
|
436
|
+
|
|
437
|
+
if (issue.status === Status.CLOSED) {
|
|
438
|
+
throw new Error(`Issue #${issueId} is closed and cannot be worked on.`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
db.transaction((tx) => {
|
|
442
|
+
logActivity(tx, issueId, Action.READ, `Agent accessed issue #${issueId}`);
|
|
443
|
+
|
|
444
|
+
tx.update(issuesTable)
|
|
445
|
+
.set({
|
|
446
|
+
status: Status.IN_PROGRESS,
|
|
447
|
+
attemptNum: sql`${issuesTable.attemptNum} + 1`
|
|
448
|
+
})
|
|
449
|
+
.where(eq(issuesTable.id, issueId))
|
|
450
|
+
.run();
|
|
451
|
+
|
|
452
|
+
logActivity(
|
|
453
|
+
tx,
|
|
454
|
+
issueId,
|
|
455
|
+
Action.STATE_CHANGE,
|
|
456
|
+
`Status changed from ${issue.status} to ${Status.IN_PROGRESS}`,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
logActivity(
|
|
460
|
+
tx,
|
|
461
|
+
issueId,
|
|
462
|
+
Action.EDIT,
|
|
463
|
+
`Agent attempt #${issue.attemptNum + 1} on issue #${issueId}`,
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return findById(db, issueId);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Remove all issues (`baton init --force`). Activity rows are kept for audit.
|
|
472
|
+
*/
|
|
473
|
+
export function clearAllIssues() {
|
|
474
|
+
const db = getDB();
|
|
475
|
+
db.delete(issuesTable).run();
|
|
476
|
+
}
|
package/source/temp.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|