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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baton-issue-tracker",
3
- "version": "1.4.0",
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
- update Updates an issue's specified fields
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
- view <id>
53
- search <query>
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
- approve <id>
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 (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) {
@@ -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
- // baton create --title "Fix login bug" --priority high
8
- // baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
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
- // --title <text> Issue title (defaults to "Issue #<id>" if omitted)
11
- // --description <text> Issue description
12
- // --priority <level> low | medium | high (default: low)
13
- // --token-limit <n> Optional token budget for this issue
14
- // -h, --help Show this help
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 { getFlagValue, getNumericFlag } from "../util.js";
18
- import { parseArgs } from '../util.js';
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
- const validFlags = ['--title', '--description', '--priority', '--token-limit'];
27
- // Check if user misspelled a flag
28
- for (const arg of args) {
29
- if (arg.startsWith('--')) {
30
- if (!validFlags.includes(arg)) {
31
- throw new Error(`Unknown flag provided: ${arg}. \nFlags: --title <text>, --description <text>, --priority <level>, --token-limit <n>`);
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
- try {
37
- const options = parseArgs(args);
220
+ const issue = await createIssue(options);
221
+ const envelope = { status: "success", issue: serializeIssue(issue) };
38
222
 
39
- const issue = await createIssue(options);
223
+ renderOutput(isJson, envelope, () => {
224
+ console.log(`\nCreated issue #${issue.id}: "${issue.title}"`);
225
+ });
40
226
 
41
- // program reaches this line if issue was successfully created
42
- console.log(`Successfully created issue #${issue.id}: "${issue.title}"`);
43
- return 0;
44
- } catch (error) {
45
- console.error(`Failed to create issue: ${error.message}`);
46
- return 1;
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
  }
@@ -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')) {
@@ -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
+ }