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
package/source/util.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// util.js
|
|
2
|
+
// AI was consulted for small portions of this file.
|
|
3
|
+
// utility functions for the issue tracker transferred over from other files
|
|
4
|
+
// centralizing these functions here to avoid duplication and improve code organization.
|
|
5
|
+
|
|
6
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
7
|
+
import { issueSchema } from '../source/models/schema.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Formats a timestamp as `HH:MM:SS YYYY-MM-DD`.
|
|
11
|
+
* @param {Date | string | number | null | undefined} value
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function formatTimestamp(value) {
|
|
15
|
+
if (!value) {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
const date = value instanceof Date ? value : new Date(String(value).replace(' ', 'T'));
|
|
19
|
+
if (Number.isNaN(date.getTime())) {
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
23
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())} ${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if the user wants help by checking if the --help or -h flag is present in the command line arguments.
|
|
28
|
+
* @param {string[]} args
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
export function wantsHelp(args) {
|
|
32
|
+
return args.includes('--help') || args.includes('-h');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the user has a specific flag in the command line arguments.
|
|
37
|
+
* @param {string[]} args
|
|
38
|
+
* @param {string} flag
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
export function hasFlag(args, flag) {
|
|
42
|
+
return args.includes(flag);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gets the value of a specific flag in the command line arguments.
|
|
47
|
+
* @param {string[]} args
|
|
48
|
+
* @param {string} flag
|
|
49
|
+
* @returns {string | null}
|
|
50
|
+
* @throws {Error} If the flag is present without a following value
|
|
51
|
+
*/
|
|
52
|
+
export function getFlagValue(args, flag) {
|
|
53
|
+
const index = args.indexOf(flag);
|
|
54
|
+
if (index === -1) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let endIdx = args.length;
|
|
59
|
+
// Finds beginning of next flag
|
|
60
|
+
for (let i = index + 1; i < args.length; i++) {
|
|
61
|
+
if (args[i].startsWith('--')) {
|
|
62
|
+
endIdx = i;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (index === args.length - 1) {
|
|
68
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// join the elements into one sentence
|
|
72
|
+
return args.slice(index + 1, endIdx).join(' ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Gets a positive integer flag value from the command line arguments.
|
|
77
|
+
* @param {string[]} args
|
|
78
|
+
* @param {string} flag
|
|
79
|
+
* @returns {number | null} The parsed value, or null if the flag is not present
|
|
80
|
+
* @throws {Error} If the flag is present without a following value or the value is invalid
|
|
81
|
+
*/
|
|
82
|
+
export function getNumericFlag(args, flag) {
|
|
83
|
+
const index = args.indexOf(flag);
|
|
84
|
+
if (index === -1) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (index === args.length - 1) {
|
|
88
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const raw = args[index + 1];
|
|
92
|
+
const value = Number(raw);
|
|
93
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Invalid value for ${flag}: expected a positive integer, got "${raw}".`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Represents the options for issue fields entered by the user.
|
|
104
|
+
* @typedef {Object} parsedOptions
|
|
105
|
+
* @property {string} [title] The title of an issue
|
|
106
|
+
* @property {string} [status] The issue's status: open | in-progress | closed
|
|
107
|
+
* @property {string} [priority] The issue's priority: low | medium | high
|
|
108
|
+
* @property {number} [tokenLimit] The token limit for an AI agent
|
|
109
|
+
* @property {string} [description] Description of the issue
|
|
110
|
+
* @property {string} [assignee] The agent assigned to the issue
|
|
111
|
+
*/
|
|
112
|
+
/**
|
|
113
|
+
* Parses command line arguments and extracts values for any flags / data fields
|
|
114
|
+
* @param {string[]} args The command line arguemnts
|
|
115
|
+
* @returns {parsedOptions} Object containing the extracted fields
|
|
116
|
+
*/
|
|
117
|
+
export function parseArgs(args) {
|
|
118
|
+
const options = {};
|
|
119
|
+
for (const [key, config] of Object.entries(issueSchema)) {
|
|
120
|
+
if (hasFlag(args, config.flag)) {
|
|
121
|
+
// Use numericFlag helper if the argument type is a number
|
|
122
|
+
const value = config.type === 'number'
|
|
123
|
+
? getNumericFlag(args, config.flag)
|
|
124
|
+
: getFlagValue(args, config.flag);
|
|
125
|
+
// Only add value if user used the corresponding flag
|
|
126
|
+
if (value !== null) {
|
|
127
|
+
options[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Represents the options for parsing the first positional argument.
|
|
135
|
+
* @typedef {Object} FirstPositionalArgOptions
|
|
136
|
+
* @property {string[]} [valueFlags]
|
|
137
|
+
* @property {string[]} [ignoreFlags]
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* First positional argument, skipping flag tokens and values consumed by value flags.
|
|
142
|
+
* @param {string[]} args
|
|
143
|
+
* @param {FirstPositionalArgOptions} [options]
|
|
144
|
+
* @returns {string | null}
|
|
145
|
+
*/
|
|
146
|
+
export function getFirstPositionalArg(
|
|
147
|
+
args,
|
|
148
|
+
{ valueFlags = [], ignoreFlags = [] } = {},
|
|
149
|
+
) {
|
|
150
|
+
const skipIndices = new Set();
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
153
|
+
const arg = args[i];
|
|
154
|
+
if (ignoreFlags.includes(arg)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (valueFlags.includes(arg)) {
|
|
158
|
+
skipIndices.add(i);
|
|
159
|
+
skipIndices.add(i + 1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
164
|
+
if (skipIndices.has(i) || args[i].startsWith('-')) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
return args[i];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolves a user path or falls back to a default (relative to cwd when needed).
|
|
175
|
+
* @param {string | null | undefined} userPath
|
|
176
|
+
* @param {string} defaultPath
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
export function resolvePath(userPath, defaultPath) {
|
|
180
|
+
const target = userPath ?? defaultPath;
|
|
181
|
+
return isAbsolute(target) ? target : resolve(process.cwd(), target);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Prints the standard error when the tracker is not initialized.
|
|
186
|
+
*/
|
|
187
|
+
export function reportTrackerNotReady() {
|
|
188
|
+
console.error('Error: No tracker found in this directory.');
|
|
189
|
+
console.error('Run `baton init` first.');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Configuration for table column widths.
|
|
194
|
+
*/
|
|
195
|
+
export const WIDTHS = {
|
|
196
|
+
id: 5,
|
|
197
|
+
title: 20,
|
|
198
|
+
status: 15,
|
|
199
|
+
priority: 10,
|
|
200
|
+
//assignees: 10,
|
|
201
|
+
description: 50
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Prints the table header row and separator line.
|
|
206
|
+
* @returns {void}
|
|
207
|
+
*/
|
|
208
|
+
export function printTableHeader() {
|
|
209
|
+
console.log(
|
|
210
|
+
"ID".padEnd(WIDTHS.id) + " │ " +
|
|
211
|
+
"TITLE".padEnd(WIDTHS.title) + " │ " +
|
|
212
|
+
"STATUS".padEnd(WIDTHS.status) + " │ " +
|
|
213
|
+
"PRIORITY".padEnd(WIDTHS.priority) + " │ " +
|
|
214
|
+
//"ASSIGNEE".padEnd(WIDTHS.assignees) + " │ " +
|
|
215
|
+
"DESCRIPTION".padEnd(WIDTHS.description)
|
|
216
|
+
);
|
|
217
|
+
console.log(
|
|
218
|
+
"─".repeat(WIDTHS.id) + "─┼─" +
|
|
219
|
+
"─".repeat(WIDTHS.title) + "─┼─" +
|
|
220
|
+
"─".repeat(WIDTHS.status) + "─┼─" +
|
|
221
|
+
"─".repeat(WIDTHS.priority) + "─┼─" +
|
|
222
|
+
//"─".repeat(WIDTHS.assignees) + "─┼─" +
|
|
223
|
+
"─".repeat(WIDTHS.description)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Helper for printIssueTable
|
|
228
|
+
/**
|
|
229
|
+
* Truncates a string to a specific length and adds ellipsis if it exceeds the specified length
|
|
230
|
+
* @param {string} str The string to be truncated
|
|
231
|
+
* @param {number} length The maximum length of the string
|
|
232
|
+
* @returns {string} The truncated string
|
|
233
|
+
*/
|
|
234
|
+
export function truncate(str, length) {
|
|
235
|
+
if (str === null || str === undefined || str === '') return "N/A";
|
|
236
|
+
const s = String(str);
|
|
237
|
+
if (s.length > length) {
|
|
238
|
+
return s.substring(0, length - 3) + "...";
|
|
239
|
+
}
|
|
240
|
+
return s;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Helper function used to format and print Issues in a table
|
|
245
|
+
* @param {Object} issue
|
|
246
|
+
* @returns {void}
|
|
247
|
+
*/
|
|
248
|
+
export function printIssueTable(issue) {
|
|
249
|
+
const idVal = String(issue.id).padEnd(WIDTHS.id);
|
|
250
|
+
const titleVal = truncate(issue.title || `Issue #${issue.id}`, WIDTHS.title).padEnd(WIDTHS.title);
|
|
251
|
+
const statusVal = truncate(issue.status, WIDTHS.status).padEnd(WIDTHS.status);
|
|
252
|
+
const priorityVal = truncate(issue.priority, WIDTHS.priority).padEnd(WIDTHS.priority);
|
|
253
|
+
const descVal = truncate(issue.description, WIDTHS.description).padEnd(WIDTHS.description);
|
|
254
|
+
|
|
255
|
+
// Handling the assignee array
|
|
256
|
+
//const assigneesVal = Array.isArray(issue.assignees) ? issue.assignees.join(', ') : 'None';
|
|
257
|
+
//const assigneeVal = truncate(assigneesVal, WIDTHS.assignees).padEnd(WIDTHS.assignees);
|
|
258
|
+
|
|
259
|
+
console.log(`${idVal} │ ${titleVal} │ ${statusVal} │ ${priorityVal} │ ${descVal}`);
|
|
260
|
+
}
|