baton-issue-tracker 1.4.0 → 1.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baton-issue-tracker",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "A CLI issue tracker for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
package/source/cli.js CHANGED
@@ -45,26 +45,33 @@ Commands:
45
45
  Options:
46
46
  init --force Re-initialize an existing tracker database
47
47
  init --specs <path> Path to product specs file (overrides default)
48
+ init --json Output as JSON (for AI agents)
48
49
  init <path> Same as --specs <path> (positional)
49
50
  Default specs: docs/specs/project-requirements.md
50
51
  loop --steps <n> Number of autonomous steps (alias: -n)
51
52
  loop -n <n>
52
- view <id>
53
- search <query>
53
+ loop --json Output as JSON (for AI agents)
54
+ next --json Output as JSON (for AI agents)
55
+ status --json Output as JSON (for AI agents)
56
+ view <id> [--json]
57
+ search <query> [--json]
54
58
  list --status <s> Filter by status: open | in-progress | closed
55
59
  list --priority <p> Filter by priority: low | medium | high
56
60
  list --limit <n> Max results (default: 50)
57
61
  list --offset <n> Skip first n results (default: 0)
62
+ list --json Output as JSON (for AI agents)
58
63
  create --title <text> Issue title (defaults to "Issue #<id>" if omitted)
59
64
  create --description <text> Issue description
60
65
  create --priority <level> low | medium | high (default: low)
61
66
  create --token-limit <n> Optional token budget for this issue
62
- approve <id>
67
+ create --json Output as JSON (for AI agents)
68
+ approve <id> [--json]
63
69
  update --title <text> New title
64
70
  update --description <text> New description
65
71
  update --token-limit <n> New token budget
66
72
  update --status <s> open | in-progress | closed
67
- update --priority <level> low | medium | high
73
+ update --priority <level> low | medium | high
74
+ update --json Output as JSON (for AI agents)
68
75
 
69
76
 
70
77
  Examples:
@@ -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 (args.length == 0) {
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 = args.join(' ');
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
- console.log(
34
- `Issue #${issue.id} approved and moved to ${issue.status}.`
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) {
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { createIssue } from "../services/issuesService.js";
17
17
  import { getFlagValue, getNumericFlag } from "../util.js";
18
- import { parseArgs } from '../util.js';
18
+ import { hasFlag, parseArgs, renderOutput, serializeIssue } from '../util.js';
19
19
 
20
20
  /**
21
21
  * Initializes a new issue in the database with the specified fields
@@ -23,7 +23,8 @@ import { parseArgs } from '../util.js';
23
23
  * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
24
24
  */
25
25
  export async function run(args) {
26
- const validFlags = ['--title', '--description', '--priority', '--token-limit'];
26
+ const isJson = hasFlag(args, '--json');
27
+ const validFlags = ['--title', '--description', '--priority', '--token-limit', '--json'];
27
28
  // Check if user misspelled a flag
28
29
  for (const arg of args) {
29
30
  if (arg.startsWith('--')) {
@@ -37,9 +38,12 @@ export async function run(args) {
37
38
  const options = parseArgs(args);
38
39
 
39
40
  const issue = await createIssue(options);
41
+ const envelope = { status: 'success', issue: serializeIssue(issue) };
42
+
43
+ renderOutput(isJson, envelope, () => {
44
+ console.log(`Successfully created issue #${issue.id}: "${issue.title}"`);
45
+ });
40
46
 
41
- // program reaches this line if issue was successfully created
42
- console.log(`Successfully created issue #${issue.id}: "${issue.title}"`);
43
47
  return 0;
44
48
  } catch (error) {
45
49
  console.error(`Failed to create issue: ${error.message}`);
@@ -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
- 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}`);
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
- console.log('Run `baton status` to review progress or `baton next` to start work.');
179
+ console.log('Run `baton status` to review progress or `baton next` to start work.');
180
+ });
169
181
 
170
182
  return 0;
171
183
  }
@@ -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 { getFlagValue, getNumericFlag } from '../util.js';
15
- import { parseArgs, printIssueTable, printTableHeader } from '../util.js';
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 validFlags = ['--status', '--priority', '--limit', '--offset'];
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}]` : "with no filters applied";
49
- console.log(`\nFound ${result.length} issue(s) ${filterLog}.`);
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
- return 0;
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')) {
@@ -11,7 +11,7 @@
11
11
  // baton loop -n 5
12
12
 
13
13
  import { isTrackerReady } from '../services/issuesService.js';
14
- import { getNumericFlag, reportTrackerNotReady } from '../util.js';
14
+ import { getNumericFlag, hasFlag, renderOutput, reportTrackerNotReady } from '../util.js';
15
15
  import { run as runNext } from './next.js';
16
16
 
17
17
  /**
@@ -30,7 +30,9 @@ function parseLoopFlags(args) {
30
30
  * @returns {Promise<number>}
31
31
  */
32
32
  export async function run(args = []) {
33
- const { steps } = parseLoopFlags(args);
33
+ const isJson = hasFlag(args, '--json');
34
+ const loopArgs = args.filter((arg) => arg !== '--json');
35
+ const { steps } = parseLoopFlags(loopArgs);
34
36
 
35
37
  if (!isTrackerReady()) {
36
38
  reportTrackerNotReady();
@@ -58,6 +60,8 @@ export async function run(args = []) {
58
60
  }
59
61
  }
60
62
 
61
- console.log(`\nCompleted ${completed} autonomous step(s).`);
63
+ renderOutput(isJson, { status: 'success', steps, completed }, () => {
64
+ console.log(`\nCompleted ${completed} autonomous step(s).`);
65
+ });
62
66
  return 0;
63
67
  }
@@ -8,7 +8,7 @@ import {
8
8
  selectNextIssue,
9
9
  workOnIssue,
10
10
  } from '../services/issuesService.js';
11
- import { formatTimestamp, reportTrackerNotReady } from '../util.js';
11
+ import { formatTimestamp, hasFlag, renderOutput, reportTrackerNotReady, serializeIssue } from '../util.js';
12
12
 
13
13
  /**
14
14
  * Moves the AI agent to work on the next issue.
@@ -17,7 +17,9 @@ import { formatTimestamp, reportTrackerNotReady } from '../util.js';
17
17
  * Stats are updated on the issue through workOnIssue function from init.js.
18
18
  * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
19
19
  */
20
- export async function run() {
20
+ export async function run(args = []) {
21
+ const isJson = hasFlag(args, '--json');
22
+
21
23
  if (!isTrackerReady()) {
22
24
  reportTrackerNotReady();
23
25
  return 1;
@@ -25,22 +27,27 @@ export async function run() {
25
27
 
26
28
  const issue = selectNextIssue();
27
29
  if (!issue) {
28
- console.log('No open issues available. All work is complete or the backlog is empty.');
30
+ renderOutput(isJson, { status: 'success', issue: null }, () => {
31
+ console.log('No open issues available. All work is complete or the backlog is empty.');
32
+ });
29
33
  return 0;
30
34
  }
31
35
 
32
36
  const updated = workOnIssue(issue.id);
37
+ const envelope = { status: 'success', issue: serializeIssue(updated) };
33
38
 
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
- }
39
+ renderOutput(isJson, envelope, () => {
40
+ console.log('Working on next issue:');
41
+ console.log(` ID: #${updated.id}`);
42
+ console.log(` Title: ${updated.title}`);
43
+ console.log(` Priority: ${updated.priority}`);
44
+ console.log(` Status: ${updated.status}`);
45
+ console.log(` Attempts: ${updated.attemptNum}`);
46
+ console.log(` Created: ${formatTimestamp(updated.createdAt)}`);
47
+ if (updated.description) {
48
+ console.log(` Description: ${updated.description}`);
49
+ }
50
+ });
44
51
 
45
52
  return 0;
46
53
  }
@@ -4,7 +4,13 @@
4
4
  // Usage: baton search "login bug"
5
5
 
6
6
  import { searchIssues } from "../services/issuesService.js";
7
- import { printIssueTable, printTableHeader } from '../util.js';
7
+ import {
8
+ hasFlag,
9
+ printIssueTable,
10
+ printTableHeader,
11
+ renderOutput,
12
+ serializeIssue,
13
+ } from '../util.js';
8
14
 
9
15
  /**
10
16
  * Searches for a title or description matching the command line argument
@@ -12,27 +18,31 @@ import { printIssueTable, printTableHeader } from '../util.js';
12
18
  * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
13
19
  */
14
20
  export async function run(args) {
15
- if (args.length === 0 || args === '') {
21
+ const isJson = hasFlag(args, '--json');
22
+ const queryArgs = args.filter((arg) => arg !== '--json');
23
+
24
+ if (queryArgs.length === 0 || queryArgs === '') {
16
25
  throw new Error("Invalid input: No search term entered.\nUsage: baton search <query>");
17
26
  }
18
27
 
19
28
  try {
20
- const query = args.join(' ');
29
+ const query = queryArgs.join(' ');
21
30
  const result = await searchIssues(query);
31
+ const issues = result.map(serializeIssue);
32
+ const envelope = { status: 'success', count: issues.length, issues };
22
33
 
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`);
34
+ renderOutput(isJson, envelope, (data) => {
35
+ if (data.count === 0) {
36
+ console.log(`No issues containing "${query}" were found.`);
37
+ return;
38
+ }
30
39
 
31
- // Logic for table formatting
32
- printTableHeader();
33
- result.forEach(issue => printIssueTable(issue));
40
+ console.log(`\nFound ${data.count} issue(s) containing "${query}":\n`);
41
+ printTableHeader();
42
+ result.forEach((issue) => printIssueTable(issue));
43
+ console.log('');
44
+ });
34
45
 
35
- console.log("");
36
46
  return 0;
37
47
  } catch (error) {
38
48
  // Failed to query database
@@ -8,13 +8,15 @@ import {
8
8
  getIssueStats,
9
9
  getAllIssues,
10
10
  } from '../services/issuesService.js';
11
- import { reportTrackerNotReady } from '../util.js';
11
+ import { hasFlag, renderOutput, reportTrackerNotReady } from '../util.js';
12
12
 
13
13
  /**
14
14
  * Main function that runs the status command.
15
15
  * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
16
16
  */
17
- export async function run() {
17
+ export async function run(args = []) {
18
+ const isJson = hasFlag(args, '--json');
19
+
18
20
  if (!isTrackerReady()) {
19
21
  reportTrackerNotReady();
20
22
  return 1;
@@ -29,27 +31,55 @@ export async function run() {
29
31
  progress = Math.round((stats.closed / stats.total) * 100);
30
32
  }
31
33
 
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 };
34
+ const byPriority = { High: 0, Medium: 0, Low: 0 };
35
+ if (isJson) {
44
36
  for (const issue of issues) {
45
37
  if (issue.status === 'Open') {
46
38
  byPriority[issue.priority] += 1;
47
39
  }
48
40
  }
49
- console.log(` High: ${byPriority.High}`);
50
- console.log(` Medium: ${byPriority.Medium}`);
51
- console.log(` Low: ${byPriority.Low}`);
52
41
  }
53
42
 
43
+ const envelope = {
44
+ status: 'success',
45
+ stats: {
46
+ total: stats.total,
47
+ open: stats.open,
48
+ in_progress: stats.inProgress,
49
+ in_review: stats.inReview,
50
+ closed: stats.closed,
51
+ progress_percent: progress,
52
+ },
53
+ open_by_priority: {
54
+ high: byPriority.High,
55
+ medium: byPriority.Medium,
56
+ low: byPriority.Low,
57
+ },
58
+ };
59
+
60
+ renderOutput(isJson, envelope, () => {
61
+ console.log('Issue Tracker Status');
62
+ console.log('──────────────────────────────────────────');
63
+ console.log(`Total issues: ${stats.total}`);
64
+ console.log(`Open: ${stats.open}`);
65
+ console.log(`In progress: ${stats.inProgress}`);
66
+ console.log(`In review: ${stats.inReview}`);
67
+ console.log(`Closed: ${stats.closed}`);
68
+ console.log(`Overall progress: ${progress}% complete`);
69
+
70
+ if (issues.length > 0) {
71
+ console.log('\nOpen issues by priority:');
72
+ const byPriority = { High: 0, Medium: 0, Low: 0 };
73
+ for (const issue of issues) {
74
+ if (issue.status === 'Open') {
75
+ byPriority[issue.priority] += 1;
76
+ }
77
+ }
78
+ console.log(` High: ${byPriority.High}`);
79
+ console.log(` Medium: ${byPriority.Medium}`);
80
+ console.log(` Low: ${byPriority.Low}`);
81
+ }
82
+ });
83
+
54
84
  return 0;
55
85
  }
@@ -16,7 +16,7 @@
16
16
  // baton update 7 --status closed --priority medium
17
17
 
18
18
  import { updateIssue, getIssue } from "../services/issuesService.js";
19
- import { parseArgs } from '../util.js';
19
+ import { hasFlag, parseArgs, renderOutput, serializeIssue } from '../util.js';
20
20
 
21
21
 
22
22
  /**
@@ -25,20 +25,23 @@ import { parseArgs } from '../util.js';
25
25
  * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
26
26
  */
27
27
  export async function run(args) {
28
- if (args.length === 0 || args === '') {
28
+ const isJson = hasFlag(args, '--json');
29
+ const cmdArgs = args.filter((arg) => arg !== '--json');
30
+
31
+ if (cmdArgs.length === 0 || cmdArgs === '') {
29
32
  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
33
  }
31
34
 
32
35
  // Convert id argument from string to base-10 integer
33
- const id = parseInt(args[0], 10);
36
+ const id = parseInt(cmdArgs[0], 10);
34
37
 
35
38
  if (isNaN(id)) {
36
39
  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
40
  }
38
41
 
39
- const validFlags = ['--title', '--description', '--token-limit', '--status', '--priority'];
42
+ const validFlags = ['--title', '--description', '--token-limit', '--status', '--priority', '--json'];
40
43
  // Check if user misspelled a flag
41
- for (const arg of args) {
44
+ for (const arg of cmdArgs) {
42
45
  if (arg.startsWith('--')) {
43
46
  if (!validFlags.includes(arg)) {
44
47
  throw new Error(`Unknown flag provided: ${arg}. \nFlags: --title <text>, --description <text>, --token-limit <n>, --status <s>, --priority <level>`);
@@ -47,25 +50,29 @@ export async function run(args) {
47
50
  }
48
51
 
49
52
  try {
50
- const options = parseArgs(args.slice(1));
53
+ const options = parseArgs(cmdArgs.slice(1));
51
54
 
52
55
  // Store the old issue fields for displaying purposes:
53
56
  const oldIssue = getIssue(id);
54
57
 
55
58
  const newIssue = await updateIssue(id, oldIssue, options);
59
+ const envelope = { status: 'success', issue: serializeIssue(newIssue) };
56
60
 
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]}")`);
61
+ renderOutput(isJson, envelope, () => {
62
+ console.log('');
63
+ // Compare and print the changes:
64
+ console.log(`Successfully updated issue #${id}:`);
65
+ for (const key in options) {
66
+ if (oldIssue[key] !== newIssue[key]) {
67
+ console.log(` ${key}: "${oldIssue[key]}" -> "${newIssue[key]}"`);
68
+ } else {
69
+ // If the entered argument matches the old data
70
+ console.log(` ${key}: No change (already set to "${newIssue[key]}")`);
71
+ }
66
72
  }
67
- }
68
- console.log("");
73
+ console.log('');
74
+ });
75
+
69
76
  return 0;
70
77
  } catch (error) {
71
78
  console.error(`Failed to update issue: ${error.message}`);
@@ -1,9 +1,10 @@
1
1
  // list.js
2
2
  // AI was consulted for some portions of this file.
3
3
  // view command which allows user to view all data fields for an issue
4
- // Usage: baton view <id>>
4
+ // Usage: baton view <id> [--json]
5
5
 
6
6
  import { getIssue } from '../services/issuesService.js';
7
+ import { hasFlag, renderOutput, serializeIssue } from '../util.js';
7
8
 
8
9
  /**
9
10
  * Displays all issue fields for a given id #
@@ -11,12 +12,15 @@ import { getIssue } 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
  // checks if id # argument is empty
15
- if (args.length == 0) {
19
+ if (idArgs.length == 0) {
16
20
  throw new Error(`Invalid input: Missing Issue ID.\nUsage: baton view <id>`);
17
21
  }
18
22
 
19
- const id = args.join(' ');
23
+ const id = idArgs.join(' ');
20
24
 
21
25
  // checks if ID # argument isn't a number
22
26
  if (isNaN(id)) {
@@ -25,18 +29,24 @@ export async function run(args) {
25
29
 
26
30
  try {
27
31
  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}`);
32
+ const envelope = {
33
+ status: 'success',
34
+ issue: issue ? serializeIssue(issue) : null,
35
+ };
36
+
37
+ renderOutput(isJson, envelope, () => {
38
+ if (!issue) {
39
+ console.log(`No issue with ID #"${issue}" was found.`);
40
+ return;
41
+ }
42
+
43
+ console.log('');
44
+ Object.entries(issue).forEach(([key, value]) => {
45
+ console.log(`${key}: ${value}`);
46
+ });
47
+ console.log('');
37
48
  });
38
49
 
39
- console.log("");
40
50
  return 0;
41
51
  } catch (error) {
42
52
  console.error("Error: Failed to retrieve data.");
package/source/util.js CHANGED
@@ -189,6 +189,67 @@ export function reportTrackerNotReady() {
189
189
  console.error('Run `baton init` first.');
190
190
  }
191
191
 
192
+ /**
193
+ * Converts DB-style enum values to lowercase snake_case for JSON output.
194
+ * e.g. "In-Progress" → "in_progress"
195
+ * @param {string | null | undefined} value
196
+ * @returns {string | null}
197
+ */
198
+ function toJsonEnum(value) {
199
+ return value?.toLowerCase().replace(/-/g, '_') ?? null;
200
+ }
201
+
202
+ /**
203
+ * Normalizes a DB row or Issue instance into a stable JSON-friendly shape.
204
+ * All commands should serialize through this before building their envelope.
205
+ * @param {object} issue
206
+ * @returns {object}
207
+ */
208
+ export function serializeIssue(issue) {
209
+ return {
210
+ id: issue.id,
211
+ title: issue.title,
212
+ status: toJsonEnum(issue.status),
213
+ priority: toJsonEnum(issue.priority),
214
+ description: issue.description ?? null,
215
+ token_limit: issue.tokenLimit ?? issue.token_limit ?? null,
216
+ attempt_num: issue.attemptNum ?? issue.attempt_num ?? 0,
217
+ created_at: issue.createdAt ?? issue.created_at ?? null,
218
+ last_updated: issue.lastUpdated ?? issue.last_updated ?? null,
219
+ assignees: issue.assignees ?? null,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Centralized output renderer.
225
+ * If isJson is true, outputs the envelope as JSON.
226
+ * Otherwise, runs the human-readable callback.
227
+ * @param {boolean} isJson - Whether the user passed the --json flag
228
+ * @param {object} envelope - Structured response payload (e.g. { status, issues })
229
+ * @param {Function} humanOutput - Callback for human terminal styling
230
+ */
231
+ export function renderOutput(isJson, envelope, humanOutput) {
232
+ if (isJson) {
233
+ console.log(JSON.stringify(envelope, null, 2));
234
+ return;
235
+ }
236
+ humanOutput(envelope);
237
+ }
238
+
239
+ /**
240
+ * Prints an error in JSON or plain text depending on output mode.
241
+ * @param {boolean} isJson
242
+ * @param {string} message
243
+ * @param {string} [code='ERROR']
244
+ */
245
+ export function renderError(isJson, message, code = 'ERROR') {
246
+ if (isJson) {
247
+ console.error(JSON.stringify({ status: 'error', code, message }));
248
+ return;
249
+ }
250
+ console.error(`Error: ${message}`);
251
+ }
252
+
192
253
  /**
193
254
  * Configuration for table column widths.
194
255
  */