baton-issue-tracker 1.4.0 → 1.6.0
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/package.json +2 -1
- package/source/cli.js +37 -15
- package/source/commands/approve.js +13 -5
- package/source/commands/create.js +218 -29
- package/source/commands/init.js +22 -10
- package/source/commands/list.js +30 -16
- package/source/commands/log.js +85 -0
- package/source/commands/loop.js +7 -3
- package/source/commands/next.js +20 -13
- package/source/commands/priority.js +102 -0
- package/source/commands/search.js +24 -14
- package/source/commands/status.js +47 -17
- package/source/commands/update.js +231 -41
- package/source/commands/view.js +23 -13
- package/source/util.js +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baton-issue-tracker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A CLI issue tracker for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"node": ">=22.0.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@inquirer/prompts": "^8.5.1",
|
|
40
41
|
"better-sqlite3": "^12.10.0",
|
|
41
42
|
"drizzle-orm": "^0.45.2"
|
|
42
43
|
}
|
package/source/cli.js
CHANGED
|
@@ -23,7 +23,9 @@ import { run as runView} from './commands/view.js';
|
|
|
23
23
|
import { run as runSearch } from './commands/search.js';
|
|
24
24
|
import { run as runList } from './commands/list.js';
|
|
25
25
|
import { run as runCreate } from './commands/create.js'
|
|
26
|
-
import { run as runUpdate } from './commands/update.js'
|
|
26
|
+
import { run as runUpdate } from './commands/update.js';
|
|
27
|
+
import { run as runPriority } from './commands/priority.js';
|
|
28
|
+
import { run as runLog } from './commands/log.js';
|
|
27
29
|
|
|
28
30
|
const HELP = `baton — AI agent issue tracker CLI
|
|
29
31
|
|
|
@@ -40,32 +42,42 @@ Commands:
|
|
|
40
42
|
list Lists issues filtered by status and priority
|
|
41
43
|
create Creates an issue with specified fields
|
|
42
44
|
approve Move an issue from in-review to closed
|
|
43
|
-
|
|
45
|
+
priority Set an issue's priority level
|
|
46
|
+
update Updates an issue's specified fields
|
|
47
|
+
log Show activity history for an issue
|
|
44
48
|
|
|
45
49
|
Options:
|
|
46
50
|
init --force Re-initialize an existing tracker database
|
|
47
51
|
init --specs <path> Path to product specs file (overrides default)
|
|
52
|
+
init --json Output as JSON (for AI agents)
|
|
48
53
|
init <path> Same as --specs <path> (positional)
|
|
49
54
|
Default specs: docs/specs/project-requirements.md
|
|
50
55
|
loop --steps <n> Number of autonomous steps (alias: -n)
|
|
51
56
|
loop -n <n>
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
loop --json Output as JSON (for AI agents)
|
|
58
|
+
next --json Output as JSON (for AI agents)
|
|
59
|
+
status --json Output as JSON (for AI agents)
|
|
60
|
+
view <id> [--json]
|
|
61
|
+
search <query> [--json]
|
|
54
62
|
list --status <s> Filter by status: open | in-progress | closed
|
|
55
63
|
list --priority <p> Filter by priority: low | medium | high
|
|
56
64
|
list --limit <n> Max results (default: 50)
|
|
57
65
|
list --offset <n> Skip first n results (default: 0)
|
|
66
|
+
list --json Output as JSON (for AI agents)
|
|
58
67
|
create --title <text> Issue title (defaults to "Issue #<id>" if omitted)
|
|
59
68
|
create --description <text> Issue description
|
|
60
69
|
create --priority <level> low | medium | high (default: low)
|
|
61
70
|
create --token-limit <n> Optional token budget for this issue
|
|
62
|
-
|
|
71
|
+
create --json Output as JSON (for AI agents)
|
|
72
|
+
approve <id> [--json]
|
|
73
|
+
priority <id> <level> [--json] low | medium | high
|
|
63
74
|
update --title <text> New title
|
|
64
75
|
update --description <text> New description
|
|
65
76
|
update --token-limit <n> New token budget
|
|
66
77
|
update --status <s> open | in-progress | closed
|
|
67
|
-
update --priority <level> low | medium | high
|
|
68
|
-
|
|
78
|
+
update --priority <level> low | medium | high
|
|
79
|
+
update --json Output as JSON (for AI agents)
|
|
80
|
+
log <id> [--json]
|
|
69
81
|
|
|
70
82
|
Examples:
|
|
71
83
|
baton init
|
|
@@ -83,8 +95,11 @@ Examples:
|
|
|
83
95
|
baton create --title "Fix login bug" --priority high
|
|
84
96
|
baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
|
|
85
97
|
baton approve 5
|
|
98
|
+
baton priority 5 high
|
|
99
|
+
baton priority 3 low
|
|
86
100
|
baton update 3 --title "Revised title"
|
|
87
101
|
baton update 7 --status closed --priority medium
|
|
102
|
+
baton log 5
|
|
88
103
|
`;
|
|
89
104
|
|
|
90
105
|
/**
|
|
@@ -94,12 +109,6 @@ Examples:
|
|
|
94
109
|
async function main() {
|
|
95
110
|
const [, , command, ...args] = process.argv;
|
|
96
111
|
|
|
97
|
-
if (!command || command === 'help' || wantsHelp(args) || command === '--help') {
|
|
98
|
-
console.log(HELP);
|
|
99
|
-
process.exit(command ? 0 : 1);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
112
|
const handlers = {
|
|
104
113
|
init: () => runInit(args),
|
|
105
114
|
next: () => runNext(args),
|
|
@@ -109,12 +118,25 @@ async function main() {
|
|
|
109
118
|
search: () => runSearch(args),
|
|
110
119
|
list: () => runList(args),
|
|
111
120
|
approve: () => runApprove(args),
|
|
121
|
+
priority: () => runPriority(args),
|
|
112
122
|
create: () => runCreate(args),
|
|
113
|
-
update: () => runUpdate(args)
|
|
123
|
+
update: () => runUpdate(args),
|
|
124
|
+
log: () => runLog(args),
|
|
114
125
|
};
|
|
115
|
-
|
|
126
|
+
|
|
127
|
+
if (!command || command === 'help' || command === '--help') {
|
|
128
|
+
console.log(HELP);
|
|
129
|
+
process.exit(command ? 0 : 1);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
const handler = handlers[command];
|
|
117
134
|
if (!handler) {
|
|
135
|
+
if (wantsHelp(args)) {
|
|
136
|
+
console.log(HELP);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
118
140
|
console.error(`Error: Unknown command "${command}".`);
|
|
119
141
|
console.error('Run `baton --help` for usage.');
|
|
120
142
|
process.exit(1);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Usage: baton approve <id>
|
|
5
5
|
|
|
6
6
|
import { approveIssue } from '../services/issuesService.js';
|
|
7
|
+
import { hasFlag, renderOutput, serializeIssue } from '../util.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Approves an issue and moves it to the closed state.
|
|
@@ -11,14 +12,17 @@ import { approveIssue } from '../services/issuesService.js';
|
|
|
11
12
|
* @returns {Promise<number>} the exit code: 0 is success, 1 is error
|
|
12
13
|
*/
|
|
13
14
|
export async function run(args) {
|
|
15
|
+
const isJson = hasFlag(args, '--json');
|
|
16
|
+
const idArgs = args.filter((arg) => arg !== '--json');
|
|
17
|
+
|
|
14
18
|
//check if id argument is empty
|
|
15
|
-
if (
|
|
19
|
+
if (idArgs.length == 0) {
|
|
16
20
|
throw new Error(
|
|
17
21
|
'Invalid input: Missing issue ID.\nUsage: baton approve <id>'
|
|
18
22
|
);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
const id =
|
|
25
|
+
const id = idArgs.join(' ');
|
|
22
26
|
|
|
23
27
|
//check if ID argument isn't a number
|
|
24
28
|
if (isNaN(id)) {
|
|
@@ -30,9 +34,13 @@ export async function run(args) {
|
|
|
30
34
|
//try to approve the issue
|
|
31
35
|
try {
|
|
32
36
|
const issue = await approveIssue(id);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
37
|
+
const envelope = { status: 'success', issue: serializeIssue(issue) };
|
|
38
|
+
|
|
39
|
+
renderOutput(isJson, envelope, () => {
|
|
40
|
+
console.log(
|
|
41
|
+
`Issue #${issue.id} approved and moved to ${issue.status}.`
|
|
42
|
+
);
|
|
43
|
+
});
|
|
36
44
|
|
|
37
45
|
return 0;
|
|
38
46
|
} catch (error) {
|
|
@@ -1,48 +1,237 @@
|
|
|
1
1
|
// create.js
|
|
2
2
|
// AI was consulted for some portions of this file.
|
|
3
|
-
// create command allows the user to create an issue
|
|
3
|
+
// create command allows the user to create an issue
|
|
4
4
|
// Usage: baton create --title <text> --description <text> --priority <level> --token-limit <n>
|
|
5
|
-
//
|
|
5
|
+
// baton create (launches interactive prompt mode)
|
|
6
|
+
//
|
|
6
7
|
// Examples:
|
|
7
|
-
//
|
|
8
|
-
//
|
|
8
|
+
// baton create --title "Fix login bug" --priority high
|
|
9
|
+
// baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
|
|
10
|
+
// baton create # interactive mode
|
|
11
|
+
//
|
|
9
12
|
// Options:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// --title <text> Issue title (defaults to "Issue #<id>" if omitted)
|
|
14
|
+
// --description <text> Issue description
|
|
15
|
+
// --priority <level> low | medium | high (default: low)
|
|
16
|
+
// --token-limit <n> Optional token budget for this issue
|
|
17
|
+
// -h, --help Show this help
|
|
15
18
|
|
|
16
19
|
import { createIssue } from "../services/issuesService.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
20
|
+
import { hasFlag, parseArgs, renderOutput, serializeIssue } from "../util.js";
|
|
21
|
+
import { issueSchema } from "../models/schema.js";
|
|
22
|
+
import { input, select, confirm, editor } from "@inquirer/prompts";
|
|
23
|
+
import { spawnSync } from "child_process";
|
|
24
|
+
import { writeFileSync, readFileSync, unlinkSync } from "fs";
|
|
25
|
+
import { tmpdir } from "os";
|
|
26
|
+
import { join } from "path";
|
|
27
|
+
|
|
28
|
+
const ALLOWED_CREATE_FIELDS = ['title', 'priority', 'tokenLimit', 'description'];
|
|
29
|
+
|
|
30
|
+
const VALID_FLAGS = new Set([
|
|
31
|
+
...ALLOWED_CREATE_FIELDS.map(key => issueSchema[key].flag),
|
|
32
|
+
"--json",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Build select choices from issueSchema.priority.values so the list stays in
|
|
36
|
+
// sync with the Priority enum without any duplication.
|
|
37
|
+
const PRIORITY_HINTS = {
|
|
38
|
+
Low: "routine work, no urgency",
|
|
39
|
+
Medium: "should be resolved this week",
|
|
40
|
+
High: "blocking or time-sensitive",
|
|
41
|
+
};
|
|
42
|
+
const PRIORITY_CHOICES = issueSchema.priority.values.map((v) => ({
|
|
43
|
+
name: `${v.padEnd(6)} -- ${PRIORITY_HINTS[v] ?? v}`,
|
|
44
|
+
value: v,
|
|
45
|
+
}));
|
|
46
|
+
const DEFAULT_PRIORITY = issueSchema.priority.values[0];
|
|
47
|
+
|
|
48
|
+
const DESCRIPTION_PLACEHOLDER = [
|
|
49
|
+
"## What is the issue?",
|
|
50
|
+
"",
|
|
51
|
+
"## Steps to reproduce (if applicable)",
|
|
52
|
+
"",
|
|
53
|
+
"## Expected vs actual behaviour",
|
|
54
|
+
"",
|
|
55
|
+
"## Additional context",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Opens the user's $EDITOR with an optional seed template and returns the
|
|
61
|
+
* saved contents, or null if the user left it unchanged / empty.
|
|
62
|
+
*
|
|
63
|
+
* Falls back gracefully: if no $EDITOR is set the inquirer `editor` prompt is
|
|
64
|
+
* used instead (which has its own built-in text area).
|
|
65
|
+
*
|
|
66
|
+
* @param {string} template Initial file content shown to the user.
|
|
67
|
+
* @returns {Promise<string|null>}
|
|
68
|
+
*/
|
|
69
|
+
async function openEditorForDescription(template = DESCRIPTION_PLACEHOLDER) {
|
|
70
|
+
const editorBin = process.env.EDITOR || process.env.VISUAL;
|
|
71
|
+
|
|
72
|
+
if (!editorBin) {
|
|
73
|
+
// If no $EDITOR set, then fall back to @inquirer/prompts built-in editor widget
|
|
74
|
+
const result = await editor({
|
|
75
|
+
message:
|
|
76
|
+
"Description (opens in-terminal editor -- save & quit when done):",
|
|
77
|
+
default: template,
|
|
78
|
+
waitForUseInput: false,
|
|
79
|
+
});
|
|
80
|
+
const cleaned = result.trim();
|
|
81
|
+
return cleaned && cleaned !== template.trim() ? cleaned : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Write template to a temp file, open $EDITOR, read back the result
|
|
85
|
+
const tmpPath = join(tmpdir(), `baton-issue-${Date.now()}.md`);
|
|
86
|
+
writeFileSync(tmpPath, template, "utf8");
|
|
87
|
+
|
|
88
|
+
const result = spawnSync(editorBin, [tmpPath], { stdio: "inherit" });
|
|
89
|
+
|
|
90
|
+
if (result.error) {
|
|
91
|
+
unlinkSync(tmpPath);
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Could not open $EDITOR (${editorBin}): ${result.error.message}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const saved = readFileSync(tmpPath, "utf8");
|
|
98
|
+
unlinkSync(tmpPath);
|
|
99
|
+
|
|
100
|
+
const cleaned = saved.trim();
|
|
101
|
+
// Return null if the user saved without changing anything
|
|
102
|
+
return cleaned && cleaned !== template.trim() ? cleaned : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Guides the user through issue creation with interactive prompts.
|
|
107
|
+
* @returns {Promise<object>} Options object ready to pass to createIssue()
|
|
108
|
+
*/
|
|
109
|
+
async function runInteractiveMode() {
|
|
110
|
+
console.log("\n Baton -- interactive issue creation\n");
|
|
111
|
+
|
|
112
|
+
// Title
|
|
113
|
+
const title = await input({
|
|
114
|
+
message: "Title:",
|
|
115
|
+
required: true,
|
|
116
|
+
validate: (val) => val.trim().length > 0 || "Title cannot be empty.",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Priority
|
|
120
|
+
const priority = await select({
|
|
121
|
+
message: "Priority:",
|
|
122
|
+
choices: PRIORITY_CHOICES,
|
|
123
|
+
default: DEFAULT_PRIORITY,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Token limit
|
|
127
|
+
const wantsTokenLimit = await confirm({
|
|
128
|
+
message: "Set a token limit for this issue?",
|
|
129
|
+
default: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
let tokenLimit = null;
|
|
133
|
+
if (wantsTokenLimit) {
|
|
134
|
+
const raw = await input({
|
|
135
|
+
message: "Token limit (positive integer):",
|
|
136
|
+
validate: (val) => {
|
|
137
|
+
const n = Number(val);
|
|
138
|
+
return (Number.isInteger(n) && n > 0) || "Must be a positive integer.";
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
tokenLimit = Number(raw);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Description
|
|
145
|
+
const wantsDescription = await confirm({
|
|
146
|
+
message: "Add a description?",
|
|
147
|
+
default: true,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
let description = null;
|
|
151
|
+
if (wantsDescription) {
|
|
152
|
+
const editorBin = process.env.EDITOR || process.env.VISUAL;
|
|
153
|
+
const hint = editorBin ? `opens ${editorBin}` : "in-terminal editor";
|
|
154
|
+
console.log(
|
|
155
|
+
` -> ${hint} -- fill in what's relevant, save and quit when done.\n`,
|
|
156
|
+
);
|
|
157
|
+
description = await openEditorForDescription();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Preview & confirm
|
|
161
|
+
console.log("\n" + "-".repeat(48));
|
|
162
|
+
console.log(` Title : ${title}`);
|
|
163
|
+
console.log(` Priority : ${priority}`);
|
|
164
|
+
console.log(` Token limit: ${tokenLimit ?? "(none)"}`);
|
|
165
|
+
if (description) {
|
|
166
|
+
const preview = description.split("\n").slice(0, 3).join(" ").slice(0, 72);
|
|
167
|
+
console.log(
|
|
168
|
+
` Description: ${preview}${description.length > 72 ? "..." : ""}`,
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(` Description: (none)`);
|
|
172
|
+
}
|
|
173
|
+
console.log("-".repeat(48) + "\n");
|
|
174
|
+
|
|
175
|
+
const confirmed = await confirm({
|
|
176
|
+
message: "Create this issue?",
|
|
177
|
+
default: true,
|
|
178
|
+
});
|
|
179
|
+
if (!confirmed) {
|
|
180
|
+
console.log("Aborted -- no issue created.");
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { title, priority, tokenLimit, description };
|
|
185
|
+
}
|
|
19
186
|
|
|
20
187
|
/**
|
|
21
188
|
* Initializes a new issue in the database with the specified fields
|
|
189
|
+
* Drops into interactive mode when no flags are provided
|
|
190
|
+
*
|
|
22
191
|
* @param {string[]} args - The command line arguments
|
|
23
|
-
* @returns {Promise<number>} The exit code: 0 is success, 1 is error
|
|
192
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error
|
|
24
193
|
*/
|
|
25
194
|
export async function run(args) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
195
|
+
const isJson = hasFlag(args, "--json");
|
|
196
|
+
|
|
197
|
+
// Flag validation -- VALID_FLAGS is derived from issueSchema, so this stays
|
|
198
|
+
// current automatically when fields are added or renamed there
|
|
199
|
+
const providedFlags = args.filter((a) => a.startsWith("--"));
|
|
200
|
+
for (const flag of providedFlags) {
|
|
201
|
+
if (!VALID_FLAGS.has(flag)) {
|
|
202
|
+
const knownFlags = [...VALID_FLAGS].join(", ");
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Unknown flag: ${flag}\nValid flags: ${knownFlags}\n` +
|
|
205
|
+
`Tip: run \`baton create\` with no flags to use interactive mode.`,
|
|
206
|
+
);
|
|
34
207
|
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Interactive mode: no flags provided (human at a keyboard)
|
|
212
|
+
// Flag mode: at least one flag present (scripts, CI, power users)
|
|
213
|
+
const nonJsonFlags = providedFlags.filter((f) => f !== "--json");
|
|
214
|
+
const isInteractive = nonJsonFlags.length === 0;
|
|
215
|
+
|
|
216
|
+
const options = isInteractive
|
|
217
|
+
? await runInteractiveMode()
|
|
218
|
+
: parseArgs(args);
|
|
35
219
|
|
|
36
|
-
|
|
37
|
-
|
|
220
|
+
const issue = await createIssue(options);
|
|
221
|
+
const envelope = { status: "success", issue: serializeIssue(issue) };
|
|
38
222
|
|
|
39
|
-
|
|
223
|
+
renderOutput(isJson, envelope, () => {
|
|
224
|
+
console.log(`\nCreated issue #${issue.id}: "${issue.title}"`);
|
|
225
|
+
});
|
|
40
226
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
227
|
+
return 0;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
// when user hits Ctrl+C
|
|
230
|
+
if (error.name === "ExitPromptError") {
|
|
231
|
+
console.log("\nAborted.");
|
|
232
|
+
return 0;
|
|
47
233
|
}
|
|
234
|
+
console.error(`Failed to create issue: ${error.message}`);
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
48
237
|
}
|
package/source/commands/init.js
CHANGED
|
@@ -24,7 +24,9 @@ import {
|
|
|
24
24
|
getFlagValue,
|
|
25
25
|
getFirstPositionalArg,
|
|
26
26
|
hasFlag,
|
|
27
|
+
renderOutput,
|
|
27
28
|
resolvePath,
|
|
29
|
+
serializeIssue,
|
|
28
30
|
} from '../util.js';
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -48,7 +50,7 @@ function parseInitFlags(args) {
|
|
|
48
50
|
const specsFromFlag = getFlagValue(args, '--specs');
|
|
49
51
|
const positionalSpecs = getFirstPositionalArg(args, {
|
|
50
52
|
valueFlags: ['--specs'],
|
|
51
|
-
ignoreFlags: ['--force'],
|
|
53
|
+
ignoreFlags: ['--force', '--json'],
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
if (specsFromFlag && positionalSpecs) {
|
|
@@ -132,6 +134,7 @@ function generateIssuesFromSpecs(specsPath) {
|
|
|
132
134
|
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
133
135
|
*/
|
|
134
136
|
export async function run(args = []) {
|
|
137
|
+
const isJson = hasFlag(args, '--json');
|
|
135
138
|
let flags;
|
|
136
139
|
try {
|
|
137
140
|
flags = parseInitFlags(args);
|
|
@@ -155,17 +158,26 @@ export async function run(args = []) {
|
|
|
155
158
|
|
|
156
159
|
const resolvedSpecsPath = resolvePath(flags.specs, DEFAULT_SPECS_PATH);
|
|
157
160
|
const createdIssues = generateIssuesFromSpecs(flags.specs);
|
|
161
|
+
const envelope = {
|
|
162
|
+
status: 'success',
|
|
163
|
+
db_path: join(process.cwd(), 'data', 'issues.db'),
|
|
164
|
+
specs_path: resolvedSpecsPath,
|
|
165
|
+
count: createdIssues.length,
|
|
166
|
+
issues: createdIssues.map(serializeIssue),
|
|
167
|
+
};
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
renderOutput(isJson, envelope, () => {
|
|
170
|
+
console.log(`Tracker initialized at ${join(process.cwd(), 'data', 'issues.db')}`);
|
|
171
|
+
console.log(`Specs: ${resolvedSpecsPath}`);
|
|
172
|
+
console.log(`Created ${createdIssues.length} issue(s) from product specs.`);
|
|
173
|
+
if (createdIssues.length > 0) {
|
|
174
|
+
console.log('Issues:');
|
|
175
|
+
for (const issue of createdIssues) {
|
|
176
|
+
console.log(` #${issue.id} [${issue.priority}] ${issue.title}`);
|
|
177
|
+
}
|
|
166
178
|
}
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
console.log('Run `baton status` to review progress or `baton next` to start work.');
|
|
180
|
+
});
|
|
169
181
|
|
|
170
182
|
return 0;
|
|
171
183
|
}
|
package/source/commands/list.js
CHANGED
|
@@ -8,11 +8,20 @@
|
|
|
8
8
|
// --priority <p> Filter by priority: low | medium | high
|
|
9
9
|
// --limit <n> Max results (default: 50)
|
|
10
10
|
// --offset <n> Skip first n results (default: 0)
|
|
11
|
+
// --json Output as JSON (for AI agents)
|
|
11
12
|
// -h, --help Show this help
|
|
12
13
|
|
|
13
14
|
import { listIssues } from '../services/issuesService.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
15
|
+
import {
|
|
16
|
+
getFlagValue,
|
|
17
|
+
getNumericFlag,
|
|
18
|
+
hasFlag,
|
|
19
|
+
parseArgs,
|
|
20
|
+
printIssueTable,
|
|
21
|
+
printTableHeader,
|
|
22
|
+
renderOutput,
|
|
23
|
+
serializeIssue,
|
|
24
|
+
} from '../util.js';
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
27
|
* Lists issues matching the filters and pagination settings
|
|
@@ -21,12 +30,13 @@ import { parseArgs, printIssueTable, printTableHeader } from '../util.js';
|
|
|
21
30
|
*/
|
|
22
31
|
|
|
23
32
|
export async function run(args) {
|
|
24
|
-
const
|
|
33
|
+
const isJson = hasFlag(args, '--json');
|
|
34
|
+
const validFlags = ['--status', '--priority', '--limit', '--offset', '--json'];
|
|
25
35
|
// Check if user misspelled a flag
|
|
26
36
|
for (const arg of args) {
|
|
27
37
|
if (arg.startsWith('--')) {
|
|
28
38
|
if (!validFlags.includes(arg)) {
|
|
29
|
-
throw new Error(`Unknown flag provided: ${arg}. \nFlags: --status <s>, --priority <p>, --limit <n>, --offset <n
|
|
39
|
+
throw new Error(`Unknown flag provided: ${arg}. \nFlags: --status <s>, --priority <p>, --limit <n>, --offset <n>, --json`);
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
}
|
|
@@ -35,25 +45,29 @@ export async function run(args) {
|
|
|
35
45
|
const options = parseArgs(args);
|
|
36
46
|
|
|
37
47
|
const result = await listIssues(options);
|
|
48
|
+
const issues = result.map(serializeIssue);
|
|
49
|
+
const envelope = { status: 'success', count: issues.length, issues };
|
|
50
|
+
|
|
51
|
+
renderOutput(isJson, envelope, (data) => {
|
|
52
|
+
if (data.count === 0) {
|
|
53
|
+
console.log('No issues matching those filters were found.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
38
56
|
|
|
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
57
|
const activeFilters = Object.entries(options)
|
|
45
58
|
.map(([key, value]) => `${key}: ${value}`)
|
|
46
59
|
.join(', ');
|
|
47
60
|
|
|
48
|
-
const filterLog = activeFilters ? `matching filters: [${activeFilters}]` :
|
|
49
|
-
console.log(`\nFound ${
|
|
50
|
-
console.log(
|
|
61
|
+
const filterLog = activeFilters ? `matching filters: [${activeFilters}]` : 'with no filters applied';
|
|
62
|
+
console.log(`\nFound ${data.count} issue(s) ${filterLog}.`);
|
|
63
|
+
console.log('');
|
|
51
64
|
|
|
52
65
|
printTableHeader();
|
|
53
|
-
result.forEach(issue => printIssueTable(issue));
|
|
54
|
-
console.log(
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
result.forEach((issue) => printIssueTable(issue));
|
|
67
|
+
console.log('');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return 0;
|
|
57
71
|
} catch (error) {
|
|
58
72
|
// Separate error message for missing value
|
|
59
73
|
if (error.message.includes('Missing value')) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// AI was consulted for some portions of this file.
|
|
2
|
+
// log.js
|
|
3
|
+
// log command which displays the full activity history for an issue
|
|
4
|
+
// Usage: baton log <id>
|
|
5
|
+
//
|
|
6
|
+
// Options:
|
|
7
|
+
// -h, --help Show this help
|
|
8
|
+
// --json Output as JSON (for AI agents)
|
|
9
|
+
//
|
|
10
|
+
// Examples:
|
|
11
|
+
// baton log 5
|
|
12
|
+
|
|
13
|
+
import { getActivityLog } from '../services/issuesService.js';
|
|
14
|
+
import { formatTimestamp, hasFlag } from '../util.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Serializes an activity log entry for JSON output.
|
|
18
|
+
* @param {object} entry
|
|
19
|
+
* @returns {object}
|
|
20
|
+
*/
|
|
21
|
+
function serializeLogEntry(entry) {
|
|
22
|
+
return {
|
|
23
|
+
log_id: entry.logId,
|
|
24
|
+
issue_id: entry.issueId,
|
|
25
|
+
action: entry.action,
|
|
26
|
+
details: entry.details ?? null,
|
|
27
|
+
created_at: entry.createdAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Displays the full activity history for a given issue ID.
|
|
33
|
+
* @param {string[]} args - The issue ID
|
|
34
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
35
|
+
*/
|
|
36
|
+
export async function run(args) {
|
|
37
|
+
const isJson = hasFlag(args, '--json');
|
|
38
|
+
const idArgs = args.filter((arg) => arg !== '--json');
|
|
39
|
+
|
|
40
|
+
if (idArgs.length === 0) {
|
|
41
|
+
throw new Error('Invalid input: Missing Issue ID.\nUsage: baton log <id>');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const id = idArgs.join(' ');
|
|
45
|
+
|
|
46
|
+
if (isNaN(id)) {
|
|
47
|
+
throw new Error('Invalid input: ID must be a number.\nUsage: baton log <id>');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const logs = getActivityLog(Number(id));
|
|
52
|
+
const entries = logs.map(serializeLogEntry);
|
|
53
|
+
const envelope = {
|
|
54
|
+
status: 'success',
|
|
55
|
+
issue_id: Number(id),
|
|
56
|
+
count: entries.length,
|
|
57
|
+
entries,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (isJson) {
|
|
61
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (entries.length === 0) {
|
|
66
|
+
console.log(`No activity history for issue #${id}.`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Activity log for issue #${id}`);
|
|
71
|
+
console.log('──────────────────────────────────────────');
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const timestamp = formatTimestamp(entry.created_at);
|
|
74
|
+
const details = entry.details ?? '';
|
|
75
|
+
console.log(`${timestamp} ${entry.action} ${details}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
return 0;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error: Failed to retrieve activity log.');
|
|
82
|
+
console.error(error.message);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
}
|