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/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
+ }