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,171 @@
|
|
|
1
|
+
// init.js
|
|
2
|
+
// AI was consulted for large portions of this file.
|
|
3
|
+
// init command for the issue tracker
|
|
4
|
+
// usage: baton init [options] [<specs-path>]
|
|
5
|
+
// options:
|
|
6
|
+
// --force: force initialization even if the tracker is already initialized
|
|
7
|
+
// --specs <path>: path to the specs file (default: docs/specs/project-requirements.md)
|
|
8
|
+
// <specs-path>: optional positional path to specs (same as --specs)
|
|
9
|
+
//
|
|
10
|
+
// examples usage of specs flag:
|
|
11
|
+
// baton init --specs ./path/to/my-specs.md
|
|
12
|
+
// baton init --specs C:\full\path\to\specs.md
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { initDB } from '../db/index.js';
|
|
17
|
+
import { Priority } from '../models/issue.js';
|
|
18
|
+
import {
|
|
19
|
+
createIssue,
|
|
20
|
+
isTrackerReady,
|
|
21
|
+
clearAllIssues,
|
|
22
|
+
} from '../services/issuesService.js';
|
|
23
|
+
import {
|
|
24
|
+
getFlagValue,
|
|
25
|
+
getFirstPositionalArg,
|
|
26
|
+
hasFlag,
|
|
27
|
+
resolvePath,
|
|
28
|
+
} from '../util.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default path to the specs file
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_SPECS_PATH = join('docs', 'specs', 'project-requirements.md');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Represents the parsed command-line flags for initialization.
|
|
37
|
+
* @typedef {Object} InitFlags
|
|
38
|
+
* @property {boolean} force - Whether to force initialization even if already initialized.
|
|
39
|
+
* @property {string | null} specs - The resolved path to the specifications file.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parses the initialization flags.
|
|
44
|
+
* @param {string[]} args
|
|
45
|
+
* @returns {InitFlags}
|
|
46
|
+
*/
|
|
47
|
+
function parseInitFlags(args) {
|
|
48
|
+
const specsFromFlag = getFlagValue(args, '--specs');
|
|
49
|
+
const positionalSpecs = getFirstPositionalArg(args, {
|
|
50
|
+
valueFlags: ['--specs'],
|
|
51
|
+
ignoreFlags: ['--force'],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (specsFromFlag && positionalSpecs) {
|
|
55
|
+
throw new Error('Pass specs path with either --specs <path> or a positional path, not both.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
force: hasFlag(args, '--force'),
|
|
60
|
+
specs: specsFromFlag ?? positionalSpecs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Represents a discrete requirement parsed from the markdown specs.
|
|
66
|
+
* @typedef {Object} Requirement
|
|
67
|
+
* @property {string} title - The title or ID of the requirement (e.g., FR-1).
|
|
68
|
+
* @property {string} description - The detailed breakdown of the requirement.
|
|
69
|
+
* @property {string} priority - The assigned urgency level.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses the must requirements from the markdown file.
|
|
74
|
+
* Likely to be changed in future iterations when AI implementation is added.
|
|
75
|
+
* @param {string} markdown
|
|
76
|
+
* @returns {Requirement[]}
|
|
77
|
+
*/
|
|
78
|
+
function parseMustRequirements(markdown) {
|
|
79
|
+
const issues = [];
|
|
80
|
+
const lines = markdown.split('\n');
|
|
81
|
+
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (!line.includes('| Must |')) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const cells = line.split('|').map((cell) => cell.trim()).filter(Boolean);
|
|
87
|
+
if (cells.length < 3) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const id = cells[0];
|
|
91
|
+
const requirement = cells[2];
|
|
92
|
+
if (!/^FR-/.test(id) || !requirement) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
issues.push({
|
|
96
|
+
title: id,
|
|
97
|
+
description: requirement,
|
|
98
|
+
priority: Priority.MEDIUM,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return issues;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates issues from the specs file.
|
|
107
|
+
* @param {string | null} specsPath
|
|
108
|
+
* @returns {Issue[]}
|
|
109
|
+
*/
|
|
110
|
+
function generateIssuesFromSpecs(specsPath) {
|
|
111
|
+
const resolvedPath = resolvePath(specsPath, DEFAULT_SPECS_PATH);
|
|
112
|
+
|
|
113
|
+
if (!existsSync(resolvedPath)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Specs file not found at ${resolvedPath}. Pass --specs <path> or provide a positional path.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const markdown = readFileSync(resolvedPath, 'utf8');
|
|
120
|
+
const requirements = parseMustRequirements(markdown);
|
|
121
|
+
|
|
122
|
+
if (requirements.length === 0) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return requirements.map((req) => createIssue(req));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Runs the init command
|
|
131
|
+
* @param {string[]} args - The command line arguments
|
|
132
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
133
|
+
*/
|
|
134
|
+
export async function run(args = []) {
|
|
135
|
+
let flags;
|
|
136
|
+
try {
|
|
137
|
+
flags = parseInitFlags(args);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error(`Error: ${error.message}`);
|
|
140
|
+
console.error('Usage: baton init [--force] [--specs <path>] [<specs-path>]');
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (isTrackerReady() && !flags.force) {
|
|
145
|
+
console.error('Error: Tracker already initialized in this directory.');
|
|
146
|
+
console.error('Run `baton init --force` to re-initialize.');
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
initDB();
|
|
151
|
+
|
|
152
|
+
if (flags.force) {
|
|
153
|
+
clearAllIssues();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const resolvedSpecsPath = resolvePath(flags.specs, DEFAULT_SPECS_PATH);
|
|
157
|
+
const createdIssues = generateIssuesFromSpecs(flags.specs);
|
|
158
|
+
|
|
159
|
+
console.log(`Tracker initialized at ${join(process.cwd(), 'data', 'issues.db')}`);
|
|
160
|
+
console.log(`Specs: ${resolvedSpecsPath}`);
|
|
161
|
+
console.log(`Created ${createdIssues.length} issue(s) from product specs.`);
|
|
162
|
+
if (createdIssues.length > 0) {
|
|
163
|
+
console.log('Issues:');
|
|
164
|
+
for (const issue of createdIssues) {
|
|
165
|
+
console.log(` #${issue.id} [${issue.priority}] ${issue.title}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
console.log('Run `baton status` to review progress or `baton next` to start work.');
|
|
169
|
+
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// list.js
|
|
2
|
+
// AI was consulted for some portions of this file.
|
|
3
|
+
// list command which allows the user to view a list of issues matching the filter flags
|
|
4
|
+
// Usage: baton list [--status <s>] [--priority <p>] [--limit <n>] [--offset <n>]
|
|
5
|
+
//
|
|
6
|
+
// Options:
|
|
7
|
+
// --status <s> Filter by status: open | in-progress | closed
|
|
8
|
+
// --priority <p> Filter by priority: low | medium | high
|
|
9
|
+
// --limit <n> Max results (default: 50)
|
|
10
|
+
// --offset <n> Skip first n results (default: 0)
|
|
11
|
+
// -h, --help Show this help
|
|
12
|
+
|
|
13
|
+
import { listIssues } from '../services/issuesService.js';
|
|
14
|
+
import { getFlagValue, getNumericFlag } from '../util.js';
|
|
15
|
+
import { parseArgs, printIssueTable, printTableHeader } from '../util.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Lists issues matching the filters and pagination settings
|
|
19
|
+
* @param {string[]} args - The command line arguments
|
|
20
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export async function run(args) {
|
|
24
|
+
const validFlags = ['--status', '--priority', '--limit', '--offset'];
|
|
25
|
+
// Check if user misspelled a flag
|
|
26
|
+
for (const arg of args) {
|
|
27
|
+
if (arg.startsWith('--')) {
|
|
28
|
+
if (!validFlags.includes(arg)) {
|
|
29
|
+
throw new Error(`Unknown flag provided: ${arg}. \nFlags: --status <s>, --priority <p>, --limit <n>, --offset <n>`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const options = parseArgs(args);
|
|
36
|
+
|
|
37
|
+
const result = await listIssues(options);
|
|
38
|
+
|
|
39
|
+
if (result.length == 0) {
|
|
40
|
+
console.log("No issues matching those filters were found.");
|
|
41
|
+
return 0;
|
|
42
|
+
} else {
|
|
43
|
+
//Logic for table formatting
|
|
44
|
+
const activeFilters = Object.entries(options)
|
|
45
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
46
|
+
.join(', ');
|
|
47
|
+
|
|
48
|
+
const filterLog = activeFilters ? `matching filters: [${activeFilters}]` : "with no filters applied";
|
|
49
|
+
console.log(`\nFound ${result.length} issue(s) ${filterLog}.`);
|
|
50
|
+
console.log("");
|
|
51
|
+
|
|
52
|
+
printTableHeader();
|
|
53
|
+
result.forEach(issue => printIssueTable(issue));
|
|
54
|
+
console.log("");
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Separate error message for missing value
|
|
59
|
+
if (error.message.includes('Missing value')) {
|
|
60
|
+
console.error(`Usage Error: ${error.message}`);
|
|
61
|
+
} else {
|
|
62
|
+
console.error("Error: Failed to retrieve data.");
|
|
63
|
+
console.error(error.message);
|
|
64
|
+
}
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// loop.js
|
|
2
|
+
// AI was consulted for some portions of this file.
|
|
3
|
+
// loop command for the tracker which allows the AI agent to work autonomously for multiple steps.
|
|
4
|
+
// usage: baton loop [options]
|
|
5
|
+
// options:
|
|
6
|
+
// --steps <n>: number of steps to run (default: 1)
|
|
7
|
+
// -n <n>: alias for --steps <n>
|
|
8
|
+
// <n>: same as --steps <n>
|
|
9
|
+
// examples usage of steps flag:
|
|
10
|
+
// baton loop --steps 5
|
|
11
|
+
// baton loop -n 5
|
|
12
|
+
|
|
13
|
+
import { isTrackerReady } from '../services/issuesService.js';
|
|
14
|
+
import { getNumericFlag, reportTrackerNotReady } from '../util.js';
|
|
15
|
+
import { run as runNext } from './next.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parses the flags in the command line argument
|
|
19
|
+
* @param {string[]} args - The command line arguments
|
|
20
|
+
* @returns {{ steps: number }}
|
|
21
|
+
*/
|
|
22
|
+
function parseLoopFlags(args) {
|
|
23
|
+
const stepsFlag = getNumericFlag(args, '--steps') ?? getNumericFlag(args, '-n');
|
|
24
|
+
return { steps: stepsFlag ?? 1 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Runs the loop command
|
|
29
|
+
* @param {string[]} args
|
|
30
|
+
* @returns {Promise<number>}
|
|
31
|
+
*/
|
|
32
|
+
export async function run(args = []) {
|
|
33
|
+
const { steps } = parseLoopFlags(args);
|
|
34
|
+
|
|
35
|
+
if (!isTrackerReady()) {
|
|
36
|
+
reportTrackerNotReady();
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!Number.isInteger(steps) || steps < 1) {
|
|
41
|
+
console.error('Error: --steps must be a positive integer.');
|
|
42
|
+
console.error('Usage: baton loop --steps <n>');
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`Running baton for ${steps} step(s)...\n`);
|
|
47
|
+
|
|
48
|
+
let completed = 0;
|
|
49
|
+
for (let step = 1; step <= steps; step += 1) {
|
|
50
|
+
console.log(`--- Step ${step}/${steps} ---`);
|
|
51
|
+
const code = await runNext();
|
|
52
|
+
if (code !== 0) {
|
|
53
|
+
return code;
|
|
54
|
+
}
|
|
55
|
+
completed += 1;
|
|
56
|
+
if (step < steps) {
|
|
57
|
+
console.log('');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`\nCompleted ${completed} autonomous step(s).`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// next.js
|
|
2
|
+
// AI was consulted for some portions of this file.
|
|
3
|
+
// next command for the issue tracker which allows the user to manually move the AI from issue to issue.
|
|
4
|
+
// usage: baton next
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isTrackerReady,
|
|
8
|
+
selectNextIssue,
|
|
9
|
+
workOnIssue,
|
|
10
|
+
} from '../services/issuesService.js';
|
|
11
|
+
import { formatTimestamp, reportTrackerNotReady } from '../util.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Moves the AI agent to work on the next issue.
|
|
15
|
+
* Checks if the tracker is ready and if there are any open issues.
|
|
16
|
+
* Prompts user to initialize the tracker if it is not ready.
|
|
17
|
+
* Stats are updated on the issue through workOnIssue function from init.js.
|
|
18
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
19
|
+
*/
|
|
20
|
+
export async function run() {
|
|
21
|
+
if (!isTrackerReady()) {
|
|
22
|
+
reportTrackerNotReady();
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const issue = selectNextIssue();
|
|
27
|
+
if (!issue) {
|
|
28
|
+
console.log('No open issues available. All work is complete or the backlog is empty.');
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const updated = workOnIssue(issue.id);
|
|
33
|
+
|
|
34
|
+
console.log('Working on next issue:');
|
|
35
|
+
console.log(` ID: #${updated.id}`);
|
|
36
|
+
console.log(` Title: ${updated.title}`);
|
|
37
|
+
console.log(` Priority: ${updated.priority}`);
|
|
38
|
+
console.log(` Status: ${updated.status}`);
|
|
39
|
+
console.log(` Attempts: ${updated.attemptNum}`);
|
|
40
|
+
console.log(` Created: ${formatTimestamp(updated.createdAt)}`);
|
|
41
|
+
if (updated.description) {
|
|
42
|
+
console.log(` Description: ${updated.description}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// search.js
|
|
2
|
+
// AI was consulted for some portions of this file.
|
|
3
|
+
// search command which allows the user to search for issues by keywords (case insensitive).
|
|
4
|
+
// Usage: baton search "login bug"
|
|
5
|
+
|
|
6
|
+
import { searchIssues } from "../services/issuesService.js";
|
|
7
|
+
import { printIssueTable, printTableHeader } from '../util.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Searches for a title or description matching the command line argument
|
|
11
|
+
* @param {string[]} args - The command line arguments
|
|
12
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
13
|
+
*/
|
|
14
|
+
export async function run(args) {
|
|
15
|
+
if (args.length === 0 || args === '') {
|
|
16
|
+
throw new Error("Invalid input: No search term entered.\nUsage: baton search <query>");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const query = args.join(' ');
|
|
21
|
+
const result = await searchIssues(query);
|
|
22
|
+
|
|
23
|
+
// If no issues with matching title or desc was found
|
|
24
|
+
if (result.length == 0) {
|
|
25
|
+
console.log(`No issues containing "${query}" were found.`);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`\nFound ${result.length} issue(s) containing "${query}":\n`);
|
|
30
|
+
|
|
31
|
+
// Logic for table formatting
|
|
32
|
+
printTableHeader();
|
|
33
|
+
result.forEach(issue => printIssueTable(issue));
|
|
34
|
+
|
|
35
|
+
console.log("");
|
|
36
|
+
return 0;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Failed to query database
|
|
39
|
+
console.error("Error: Failed to retrieve data.");
|
|
40
|
+
console.error(error.message);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// status.js
|
|
2
|
+
// AI was consulted for some portions of this file.
|
|
3
|
+
// status command for the issue tracker which allows the user to see the status of the tracker.
|
|
4
|
+
// usage: baton status
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isTrackerReady,
|
|
8
|
+
getIssueStats,
|
|
9
|
+
getAllIssues,
|
|
10
|
+
} from '../services/issuesService.js';
|
|
11
|
+
import { reportTrackerNotReady } from '../util.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Main function that runs the status command.
|
|
15
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
16
|
+
*/
|
|
17
|
+
export async function run() {
|
|
18
|
+
if (!isTrackerReady()) {
|
|
19
|
+
reportTrackerNotReady();
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const stats = getIssueStats();
|
|
24
|
+
const issues = getAllIssues();
|
|
25
|
+
let progress;
|
|
26
|
+
if (stats.total === 0) {
|
|
27
|
+
progress = 0;
|
|
28
|
+
} else {
|
|
29
|
+
progress = Math.round((stats.closed / stats.total) * 100);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('Issue Tracker Status');
|
|
33
|
+
console.log('──────────────────────────────────────────');
|
|
34
|
+
console.log(`Total issues: ${stats.total}`);
|
|
35
|
+
console.log(`Open: ${stats.open}`);
|
|
36
|
+
console.log(`In progress: ${stats.inProgress}`);
|
|
37
|
+
console.log(`In review: ${stats.inReview}`);
|
|
38
|
+
console.log(`Closed: ${stats.closed}`);
|
|
39
|
+
console.log(`Overall progress: ${progress}% complete`);
|
|
40
|
+
|
|
41
|
+
if (issues.length > 0) {
|
|
42
|
+
console.log('\nOpen issues by priority:');
|
|
43
|
+
const byPriority = { High: 0, Medium: 0, Low: 0 };
|
|
44
|
+
for (const issue of issues) {
|
|
45
|
+
if (issue.status === 'Open') {
|
|
46
|
+
byPriority[issue.priority] += 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
console.log(` High: ${byPriority.High}`);
|
|
50
|
+
console.log(` Medium: ${byPriority.Medium}`);
|
|
51
|
+
console.log(` Low: ${byPriority.Low}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// update.js
|
|
2
|
+
// AI was consulted for some portions of this file.
|
|
3
|
+
// update command allows the user to update data fields for an issue
|
|
4
|
+
// Usage: baton update <id> [options]
|
|
5
|
+
//
|
|
6
|
+
// Options:
|
|
7
|
+
// --title <text> New title
|
|
8
|
+
// --description <text> New description
|
|
9
|
+
// --token-limit <n> New token budget
|
|
10
|
+
// --status <s> open | in-progress | closed
|
|
11
|
+
// --priority <p> low | medium | high
|
|
12
|
+
// -h, --help Show this help
|
|
13
|
+
//
|
|
14
|
+
// Examples:
|
|
15
|
+
// baton update 3 --title "Revised title"
|
|
16
|
+
// baton update 7 --status closed --priority medium
|
|
17
|
+
|
|
18
|
+
import { updateIssue, getIssue } from "../services/issuesService.js";
|
|
19
|
+
import { parseArgs } from '../util.js';
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Updates the specified fields for a given issue ID
|
|
24
|
+
* @param {string[]} args - The command line arguments
|
|
25
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
26
|
+
*/
|
|
27
|
+
export async function run(args) {
|
|
28
|
+
if (args.length === 0 || args === '') {
|
|
29
|
+
throw new Error("Invalid input: No arguments entered.\nUsage: baton update <id> [options] \nOptions: --title <text>, --description <text>, --token-limit <n>, --status <s>, --priority <level>");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Convert id argument from string to base-10 integer
|
|
33
|
+
const id = parseInt(args[0], 10);
|
|
34
|
+
|
|
35
|
+
if (isNaN(id)) {
|
|
36
|
+
throw new Error ("Invalid input: No ID entered. \nUsage: baton update <id> [options]\nOptions: --title <text>, --description <text>, --token-limit <n>, --status <s>, --priority <level>");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const validFlags = ['--title', '--description', '--token-limit', '--status', '--priority'];
|
|
40
|
+
// Check if user misspelled a flag
|
|
41
|
+
for (const arg of args) {
|
|
42
|
+
if (arg.startsWith('--')) {
|
|
43
|
+
if (!validFlags.includes(arg)) {
|
|
44
|
+
throw new Error(`Unknown flag provided: ${arg}. \nFlags: --title <text>, --description <text>, --token-limit <n>, --status <s>, --priority <level>`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const options = parseArgs(args.slice(1));
|
|
51
|
+
|
|
52
|
+
// Store the old issue fields for displaying purposes:
|
|
53
|
+
const oldIssue = getIssue(id);
|
|
54
|
+
|
|
55
|
+
const newIssue = await updateIssue(id, oldIssue, options);
|
|
56
|
+
|
|
57
|
+
console.log("");
|
|
58
|
+
// Compare and print the changes:
|
|
59
|
+
console.log(`Successfully updated issue #${id}:`);
|
|
60
|
+
for (const key in options) {
|
|
61
|
+
if (oldIssue[key] !== newIssue[key]) {
|
|
62
|
+
console.log(` ${key}: "${oldIssue[key]}" -> "${newIssue[key]}"`);
|
|
63
|
+
} else {
|
|
64
|
+
// If the entered argument matches the old data
|
|
65
|
+
console.log(` ${key}: No change (already set to "${newIssue[key]}")`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.log("");
|
|
69
|
+
return 0;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`Failed to update issue: ${error.message}`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// list.js
|
|
2
|
+
// AI was consulted for some portions of this file.
|
|
3
|
+
// view command which allows user to view all data fields for an issue
|
|
4
|
+
// Usage: baton view <id>>
|
|
5
|
+
|
|
6
|
+
import { getIssue } from '../services/issuesService.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Displays all issue fields for a given id #
|
|
10
|
+
* @param {string[]} args - The issue ID #
|
|
11
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
12
|
+
*/
|
|
13
|
+
export async function run(args) {
|
|
14
|
+
// checks if id # argument is empty
|
|
15
|
+
if (args.length == 0) {
|
|
16
|
+
throw new Error(`Invalid input: Missing Issue ID.\nUsage: baton view <id>`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const id = args.join(' ');
|
|
20
|
+
|
|
21
|
+
// checks if ID # argument isn't a number
|
|
22
|
+
if (isNaN(id)) {
|
|
23
|
+
throw new Error(`Invalid input: ID must be a number.\nUsage: baton view <id>`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const issue = await getIssue(id);
|
|
28
|
+
|
|
29
|
+
if (!issue) {
|
|
30
|
+
console.log(`No issue with ID #"${issue}" was found.`);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log("");
|
|
35
|
+
Object.entries(issue).forEach(([key, value]) => {
|
|
36
|
+
console.log(`${key}: ${value}`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.log("");
|
|
40
|
+
return 0;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("Error: Failed to retrieve data.");
|
|
43
|
+
console.error(error.message);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
3
|
+
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
import * as schema from "../models/schema.js";
|
|
8
|
+
|
|
9
|
+
const dataDir = path.join(process.cwd(), ".baton");
|
|
10
|
+
const dbPath = path.join(dataDir, "baton.db");
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(dataDir)) {
|
|
13
|
+
fs.mkdirSync(dataDir);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Initialize better-sqlite3 connection
|
|
17
|
+
const sqliteConnection = new Database(dbPath);
|
|
18
|
+
|
|
19
|
+
// Initialize Drizzle ORM, passing in the schema for relational queries
|
|
20
|
+
// Using let here so we can reassign it in tests with an in-memory database instance.
|
|
21
|
+
let db = drizzle(sqliteConnection, { schema });
|
|
22
|
+
|
|
23
|
+
export function initDB() {
|
|
24
|
+
// Apply Migrations dynamically on CLI startup
|
|
25
|
+
// This looks for a folder named "drizzle" in your project root
|
|
26
|
+
const migrationsFolder = path.join(process.cwd(), "drizzle");
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(migrationsFolder)) {
|
|
29
|
+
migrate(db, { migrationsFolder });
|
|
30
|
+
} else {
|
|
31
|
+
console.warn("No migrations folder found. Tables may not be created.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Execute custom SQLite logic (Triggers)
|
|
35
|
+
// Drizzle does not manage triggers in its schema file, so we keep this raw.
|
|
36
|
+
sqliteConnection.exec(`
|
|
37
|
+
CREATE TRIGGER IF NOT EXISTS set_default_issue_title
|
|
38
|
+
AFTER INSERT ON issues
|
|
39
|
+
FOR EACH ROW
|
|
40
|
+
WHEN NEW.title = 'PENDING' OR NEW.title IS NULL
|
|
41
|
+
BEGIN
|
|
42
|
+
UPDATE issues
|
|
43
|
+
SET title = 'Issue #' || NEW.id
|
|
44
|
+
WHERE id = NEW.id;
|
|
45
|
+
END;
|
|
46
|
+
`);
|
|
47
|
+
|
|
48
|
+
sqliteConnection.exec(`
|
|
49
|
+
CREATE TRIGGER IF NOT EXISTS update_last_updated
|
|
50
|
+
AFTER UPDATE ON issues
|
|
51
|
+
FOR EACH ROW
|
|
52
|
+
WHEN NEW.last_updated IS OLD.last_updated
|
|
53
|
+
BEGIN
|
|
54
|
+
UPDATE issues
|
|
55
|
+
SET last_updated = CURRENT_TIMESTAMP
|
|
56
|
+
WHERE id = OLD.id;
|
|
57
|
+
END;
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getDB() {
|
|
62
|
+
return db;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getSQLiteDB() {
|
|
66
|
+
return sqliteConnection;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// function for the test suite mocking
|
|
70
|
+
export function setTestDB(testDbInstance) {
|
|
71
|
+
db = testDbInstance;
|
|
72
|
+
}
|
package/source/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// temp file for linter to pass
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const Action = Object.freeze({
|
|
2
|
+
STATE_CHANGE: "state_change",
|
|
3
|
+
PRIORITY_CHANGE: "priority_change",
|
|
4
|
+
EDIT: "edit",
|
|
5
|
+
READ: "read",
|
|
6
|
+
CREATION: "creation",
|
|
7
|
+
DELETION: "deletion",
|
|
8
|
+
REJECT: "rejection",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export class ActivityLog {
|
|
12
|
+
constructor({
|
|
13
|
+
logId = null,
|
|
14
|
+
issueId,
|
|
15
|
+
action,
|
|
16
|
+
createdAt = new Date().toISOString(),
|
|
17
|
+
} = {}) {
|
|
18
|
+
this.logId = logId;
|
|
19
|
+
this.issueId = issueId;
|
|
20
|
+
this.action = action;
|
|
21
|
+
this.createdAt = createdAt;
|
|
22
|
+
}
|
|
23
|
+
}
|