jira-ai 0.9.96 → 0.9.97

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 CHANGED
@@ -63,6 +63,35 @@ See all available commands:
63
63
  jira-ai --help
64
64
  ```
65
65
 
66
+ ## JSON Output Mode
67
+
68
+ All commands support structured JSON output via global flags. This is designed for AI agents and scripting integrations where parsed data is more useful than formatted tables.
69
+
70
+ ### `--json`
71
+
72
+ Outputs pretty-printed JSON (2-space indentation):
73
+ ```bash
74
+ jira-ai --json issue get PROJ-123
75
+ jira-ai --json project list
76
+ jira-ai --json issue search "project = PROJ AND status = Open"
77
+ ```
78
+
79
+ ### `--json-compact`
80
+
81
+ Outputs single-line JSON for maximum token efficiency — ideal for AI agent workflows:
82
+ ```bash
83
+ jira-ai --json-compact issue get PROJ-123
84
+ ```
85
+
86
+ ### Error handling in JSON mode
87
+
88
+ Errors are returned as structured JSON to stdout:
89
+ ```json
90
+ { "error": true, "message": "Issue not found", "hints": ["Check the issue key"], "exitCode": 1 }
91
+ ```
92
+
93
+ Default table output is unchanged when neither flag is used.
94
+
66
95
  ### Rich Issue Management
67
96
 
68
97
  Create issues with detailed field support:
package/dist/cli.js CHANGED
@@ -34,6 +34,7 @@ import { checkForUpdate, formatUpdateMessage, checkForUpdateSync } from './lib/u
34
34
  import { CliError } from './types/errors.js';
35
35
  import { CommandError } from './lib/errors.js';
36
36
  import { ui } from './lib/ui.js';
37
+ import { initJsonMode, outputError } from './lib/json-mode.js';
37
38
  import { CreateTaskSchema, UpdateDescriptionSchema, ProjectFieldsSchema, ConfluenceAddCommentSchema, RunJqlSchema, GetPersonWorklogSchema, GetIssueStatisticsSchema, EpicListSchema, EpicCreateSchema, EpicUpdateSchema, EpicLinkSchema, EpicMaxSchema, validateOptions, IssueKeySchema, ProjectKeySchema, TimeframeSchema } from './lib/validation.js';
38
39
  import { realpathSync } from 'fs';
39
40
  // Create CLI program
@@ -42,6 +43,8 @@ program
42
43
  .name('jira-ai')
43
44
  .description('CLI tool for interacting with Atlassian Jira')
44
45
  .version(getVersion())
46
+ .option('--json', 'Output as JSON')
47
+ .option('--json-compact', 'Output as compact JSON')
45
48
  .addHelpText('after', () => {
46
49
  const latestVersion = checkForUpdateSync();
47
50
  if (latestVersion) {
@@ -506,6 +509,7 @@ export function configureCommandVisibility(program) {
506
509
  // Parse command line arguments
507
510
  export async function main() {
508
511
  try {
512
+ initJsonMode();
509
513
  // Background update check (non-blocking for the user)
510
514
  checkForUpdate().catch(() => { });
511
515
  configureCommandVisibility(program);
@@ -514,30 +518,16 @@ export async function main() {
514
518
  catch (error) {
515
519
  ui.failSpinner();
516
520
  if (error instanceof CommandError) {
517
- console.error(chalk.red(`\n❌ Error: ${error.message}`));
518
- if (error.hints.length > 0) {
519
- error.hints.forEach(hint => {
520
- console.error(chalk.yellow(` Hint: ${hint}`));
521
- });
522
- }
523
- console.log(); // Add a newline
524
- process.exit(error.exitCode);
521
+ outputError(error.message, error.hints, error.exitCode);
525
522
  }
526
523
  else if (error instanceof CliError) {
527
- console.error(chalk.red(`\n❌ ${error.message}\n`));
528
- process.exit(1);
524
+ outputError(error.message, [], 1);
529
525
  }
530
526
  else if (error instanceof Error) {
531
- console.error(chalk.red(`\n💥 Unexpected Error: ${error.message}`));
532
- if (process.env.DEBUG) {
533
- console.error(chalk.gray(error.stack));
534
- }
535
- console.error(chalk.gray('\nPlease report this issue if it persists.\n'));
536
- process.exit(1);
527
+ outputError(`Unexpected Error: ${error.message}`, ['Please report this issue if it persists.'], 1);
537
528
  }
538
529
  else {
539
- console.error(chalk.red(`\n💥 An unknown error occurred: ${String(error)}\n`));
540
- process.exit(1);
530
+ outputError(`An unknown error occurred: ${String(error)}`, [], 1);
541
531
  }
542
532
  }
543
533
  }
@@ -1,18 +1,23 @@
1
1
  import chalk from 'chalk';
2
2
  import { getVersion } from '../lib/utils.js';
3
3
  import { checkForUpdate, formatUpdateMessage } from '../lib/update-check.js';
4
+ import { outputResult } from '../lib/json-mode.js';
4
5
  export async function aboutCommand() {
5
- console.log(chalk.bold.cyan('\n📋 Jira AI\n'));
6
- console.log(`${chalk.bold('Version:')} ${getVersion()}`);
7
- console.log(`${chalk.bold('GitHub:')} https://github.com/festoinc/jira-ai\n`);
6
+ const version = getVersion();
7
+ let latestVersion = null;
8
8
  try {
9
- const latestVersion = await checkForUpdate();
10
- if (latestVersion) {
11
- console.log(formatUpdateMessage(latestVersion));
12
- console.log();
13
- }
9
+ latestVersion = await checkForUpdate() ?? null;
14
10
  }
15
11
  catch (error) {
16
12
  // Ignore update check errors in about command
17
13
  }
14
+ outputResult({ version, updateAvailable: latestVersion ?? undefined }, (data) => {
15
+ let out = chalk.bold.cyan('\n📋 Jira AI\n');
16
+ out += `${chalk.bold('Version:')} ${data.version}\n`;
17
+ out += `${chalk.bold('GitHub:')} https://github.com/festoinc/jira-ai\n`;
18
+ if (data.updateAvailable) {
19
+ out += `\n${formatUpdateMessage(data.updateAvailable)}\n`;
20
+ }
21
+ return out;
22
+ });
18
23
  }
@@ -7,6 +7,7 @@ import { processMentionsInADF } from '../lib/adf-mentions.js';
7
7
  import { CommandError } from '../lib/errors.js';
8
8
  import { ui } from '../lib/ui.js';
9
9
  import { validateOptions, AddCommentSchema } from '../lib/validation.js';
10
+ import { outputResult } from '../lib/json-mode.js';
10
11
  export async function addCommentCommand(options) {
11
12
  // Validate options
12
13
  validateOptions(AddCommentSchema, options);
@@ -47,7 +48,7 @@ export async function addCommentCommand(options) {
47
48
  try {
48
49
  await addIssueComment(issueKey, adfContent);
49
50
  ui.succeedSpinner(chalk.green(`Comment added successfully to ${issueKey}`));
50
- console.log(chalk.gray(`\nFile: ${absolutePath}`));
51
+ outputResult({ success: true, issueKey, file: absolutePath }, (data) => chalk.gray(`\nFile: ${data.file}`));
51
52
  }
52
53
  catch (error) {
53
54
  if (error instanceof CommandError)
@@ -3,6 +3,7 @@ import { addIssueLabels, validateIssuePermissions } from '../lib/jira-client.js'
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { validateOptions, IssueKeySchema } from '../lib/validation.js';
6
+ import { outputResult } from '../lib/json-mode.js';
6
7
  export async function addLabelCommand(taskId, labelsString) {
7
8
  // Validate input
8
9
  validateOptions(IssueKeySchema, taskId);
@@ -21,8 +22,7 @@ export async function addLabelCommand(taskId, labelsString) {
21
22
  try {
22
23
  await addIssueLabels(taskId, labels);
23
24
  ui.succeedSpinner(chalk.green(`Labels added successfully to ${taskId}`));
24
- console.log(chalk.gray(`
25
- Labels: ${labels.join(', ')}`));
25
+ outputResult({ success: true, issueKey: taskId, labels }, (data) => chalk.gray(`\nLabels: ${data.labels.join(', ')}`));
26
26
  }
27
27
  catch (error) {
28
28
  if (error instanceof CommandError)
@@ -6,6 +6,7 @@ import { createTemporaryClient } from '../lib/jira-client.js';
6
6
  import { saveCredentials, clearCredentials } from '../lib/auth-storage.js';
7
7
  import { CommandError } from '../lib/errors.js';
8
8
  import { ui } from '../lib/ui.js';
9
+ import { outputResult, isJsonMode } from '../lib/json-mode.js';
9
10
  /**
10
11
  * Auto-discover the Atlassian Cloud ID for a given site hostname.
11
12
  */
@@ -28,7 +29,7 @@ async function discoverCloudId(host) {
28
29
  }
29
30
  export async function logoutCommand() {
30
31
  clearCredentials();
31
- console.log(chalk.green('Successfully logged out. Authentication credentials cleared.'));
32
+ outputResult({ success: true, message: 'Successfully logged out. Authentication credentials cleared.' }, (data) => chalk.green(data.message));
32
33
  }
33
34
  export async function authCommand(options = {}) {
34
35
  if (options.logout) {
@@ -98,7 +99,9 @@ export async function authCommand(options = {}) {
98
99
  }
99
100
  }
100
101
  if (!host || !email || !apiToken) {
101
- console.log(chalk.cyan('\n--- Jira Authentication Setup ---\n'));
102
+ if (!isJsonMode()) {
103
+ console.log(chalk.cyan('\n--- Jira Authentication Setup ---\n'));
104
+ }
102
105
  try {
103
106
  host = await ask('Jira URL (e.g., https://your-domain.atlassian.net): ');
104
107
  if (!host) {
@@ -139,13 +142,16 @@ export async function authCommand(options = {}) {
139
142
  const tempClient = createTemporaryClient(host, email, apiToken, { authType, cloudId });
140
143
  const user = await tempClient.myself.getCurrentUser();
141
144
  ui.succeedSpinner(chalk.green('Authentication successful!'));
142
- console.log(chalk.blue(`\nWelcome, ${user.displayName} (${user.emailAddress})`));
143
- if (authType === 'service_account') {
144
- console.log(chalk.gray(`Auth type: service_account (via api.atlassian.com gateway)`));
145
- }
146
145
  saveCredentials({ host, email, apiToken, authType, cloudId });
147
- console.log(chalk.green('\nCredentials saved successfully to ~/.jira-ai/config.json'));
148
- console.log(chalk.gray('These credentials will be used for future commands on this machine.'));
146
+ outputResult({ success: true, displayName: user.displayName, email: user.emailAddress, authType }, (data) => {
147
+ let out = chalk.blue(`\nWelcome, ${data.displayName} (${data.email})`);
148
+ if (data.authType === 'service_account') {
149
+ out += `\n${chalk.gray('Auth type: service_account (via api.atlassian.com gateway)')}`;
150
+ }
151
+ out += `\n${chalk.green('\nCredentials saved successfully to ~/.jira-ai/config.json')}`;
152
+ out += `\n${chalk.gray('These credentials will be used for future commands on this machine.')}`;
153
+ return out;
154
+ });
149
155
  }
150
156
  catch (error) {
151
157
  const hints = [];
@@ -7,6 +7,7 @@ import { formatConfluencePage, formatConfluenceSpaces, formatConfluencePageHiera
7
7
  import { ui } from '../lib/ui.js';
8
8
  import { CommandError } from '../lib/errors.js';
9
9
  import { isConfluenceSpaceAllowed } from '../lib/settings.js';
10
+ import { outputResult, isJsonMode } from '../lib/json-mode.js';
10
11
  export async function confluenceCreatePageCommand(space, title, parentPage, options = {}) {
11
12
  // Validate space key
12
13
  if (!isConfluenceSpaceAllowed(space)) {
@@ -16,13 +17,12 @@ export async function confluenceCreatePageCommand(space, title, parentPage, opti
16
17
  try {
17
18
  const result = await createPage(space, title, parentPage, { returnBoth: options.returnBothUrls });
18
19
  ui.succeedSpinner(chalk.green('Confluence page created successfully'));
19
- if (typeof result === 'object') {
20
- console.log(chalk.cyan(`\nFull URL: ${result.url}`));
21
- console.log(chalk.cyan(`Short URL: ${result.shortUrl}`));
22
- }
23
- else {
24
- console.log(chalk.cyan(`\nURL: ${result}`));
25
- }
20
+ outputResult(typeof result === 'object' ? result : { url: result }, (data) => {
21
+ if ('shortUrl' in data) {
22
+ return chalk.cyan(`\nFull URL: ${data.url}\nShort URL: ${data.shortUrl}`);
23
+ }
24
+ return chalk.cyan(`\nURL: ${data.url}`);
25
+ });
26
26
  }
27
27
  catch (error) {
28
28
  ui.failSpinner();
@@ -81,8 +81,7 @@ export async function confluenceAddCommentCommand(url, options) {
81
81
  try {
82
82
  await addPageComment(url, adfContent);
83
83
  ui.succeedSpinner(chalk.green('Comment added successfully to Confluence page'));
84
- console.log(chalk.gray(`\nPage: ${url}`));
85
- console.log(chalk.gray(`File: ${absolutePath}`));
84
+ outputResult({ success: true, page: url, file: absolutePath }, (data) => chalk.gray(`\nPage: ${data.page}\nFile: ${data.file}`));
86
85
  }
87
86
  catch (error) {
88
87
  ui.failSpinner();
@@ -123,7 +122,7 @@ export async function confluenceGetPageCommand(url, options = {}) {
123
122
  throw new CommandError(`Access to Confluence space '${page.space}' is restricted by your settings.`);
124
123
  }
125
124
  ui.succeedSpinner(chalk.green('Confluence page details retrieved'));
126
- console.log(formatConfluencePage(page, comments));
125
+ outputResult({ page, comments }, (data) => formatConfluencePage(data.page, data.comments));
127
126
  }
128
127
  catch (error) {
129
128
  ui.failSpinner();
@@ -152,15 +151,20 @@ export async function confluenceListSpacesCommand() {
152
151
  const allowedSpaces = spaces.filter(space => isConfluenceSpaceAllowed(space.key));
153
152
  if (allowedSpaces.length === 0) {
154
153
  ui.failSpinner('No allowed Confluence spaces found.');
155
- console.log(chalk.yellow('\nHint: Add allowed spaces to your settings.yaml under "allowed-confluence-spaces".'));
156
- console.log(chalk.gray('Example:'));
157
- console.log(chalk.gray(' allowed-confluence-spaces:'));
158
- console.log(chalk.gray(' - SPACE1'));
159
- console.log(chalk.gray(' - SPACE2'));
154
+ if (!isJsonMode()) {
155
+ console.log(chalk.yellow('\nHint: Add allowed spaces to your settings.yaml under "allowed-confluence-spaces".'));
156
+ console.log(chalk.gray('Example:'));
157
+ console.log(chalk.gray(' allowed-confluence-spaces:'));
158
+ console.log(chalk.gray(' - SPACE1'));
159
+ console.log(chalk.gray(' - SPACE2'));
160
+ }
161
+ else {
162
+ outputResult([]);
163
+ }
160
164
  return;
161
165
  }
162
166
  ui.succeedSpinner(chalk.green('Confluence spaces retrieved'));
163
- console.log(formatConfluenceSpaces(allowedSpaces));
167
+ outputResult(allowedSpaces, formatConfluenceSpaces);
164
168
  }
165
169
  catch (error) {
166
170
  ui.failSpinner();
@@ -176,7 +180,7 @@ export async function confluenceGetSpacePagesHierarchyCommand(spaceKey) {
176
180
  try {
177
181
  const hierarchy = await getSpacePagesHierarchy(spaceKey);
178
182
  ui.succeedSpinner(chalk.green('Confluence page hierarchy retrieved'));
179
- console.log(formatConfluencePageHierarchy(hierarchy));
183
+ outputResult(hierarchy, formatConfluencePageHierarchy);
180
184
  }
181
185
  catch (error) {
182
186
  ui.failSpinner();
@@ -224,8 +228,7 @@ export async function confluenceUpdateDescriptionCommand(url, options) {
224
228
  try {
225
229
  await updatePageContent(url, adfContent);
226
230
  ui.succeedSpinner(chalk.green('Confluence page content updated successfully'));
227
- console.log(chalk.gray(`\nPage: ${url}`));
228
- console.log(chalk.gray(`File: ${absolutePath}`));
231
+ outputResult({ success: true, page: url, file: absolutePath }, (data) => chalk.gray(`\nPage: ${data.page}\nFile: ${data.file}`));
229
232
  }
230
233
  catch (error) {
231
234
  ui.failSpinner();
@@ -256,7 +259,7 @@ export async function confluenceSearchCommand(query, options = {}) {
256
259
  return isConfluenceSpaceAllowed(result.space);
257
260
  });
258
261
  ui.succeedSpinner(chalk.green('Confluence search completed'));
259
- console.log(formatConfluenceSearchResults(filteredResults));
262
+ outputResult(filteredResults, formatConfluenceSearchResults);
260
263
  }
261
264
  catch (error) {
262
265
  ui.failSpinner();
@@ -3,6 +3,7 @@ import { createIssueLink, validateIssuePermissions } from '../lib/jira-client.js
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { validateOptions, IssueKeySchema } from '../lib/validation.js';
6
+ import { outputResult } from '../lib/json-mode.js';
6
7
  export async function createIssueLinkCommand(inwardKey, linkType, outwardKey) {
7
8
  validateOptions(IssueKeySchema, inwardKey);
8
9
  validateOptions(IssueKeySchema, outwardKey);
@@ -12,8 +13,7 @@ export async function createIssueLinkCommand(inwardKey, linkType, outwardKey) {
12
13
  try {
13
14
  await createIssueLink(inwardKey, outwardKey, linkType.trim());
14
15
  ui.succeedSpinner(chalk.green(`Link created successfully`));
15
- console.log(chalk.gray(`
16
- ${inwardKey} --[${linkType.trim()}]--> ${outwardKey}`));
16
+ outputResult({ success: true, inwardKey, linkType: linkType.trim(), outwardKey }, (data) => chalk.gray(`\n${data.inwardKey} --[${data.linkType}]--> ${data.outwardKey}`));
17
17
  }
18
18
  catch (error) {
19
19
  if (error instanceof CommandError)
@@ -7,6 +7,7 @@ import { CommandError } from '../lib/errors.js';
7
7
  import { ui } from '../lib/ui.js';
8
8
  import { validateOptions, CreateTaskSchema } from '../lib/validation.js';
9
9
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
10
+ import { outputResult } from '../lib/json-mode.js';
10
11
  export async function createTaskCommand(options) {
11
12
  validateOptions(CreateTaskSchema, options);
12
13
  const { title, project, issueType, parent, priority, description, descriptionFile, labels, component, fixVersion, dueDate, assignee, customField } = options;
@@ -85,13 +86,16 @@ export async function createTaskCommand(options) {
85
86
  ...issueFields,
86
87
  });
87
88
  ui.succeedSpinner(chalk.green(`Issue created successfully: ${result.key}`));
88
- console.log(chalk.gray(`\nTitle: ${title}`));
89
- console.log(chalk.gray(`Project: ${project}`));
90
- console.log(chalk.gray(`Issue Type: ${issueType}`));
91
- if (parent) {
92
- console.log(chalk.gray(`Parent: ${parent}`));
93
- }
94
- console.log(chalk.cyan(`\nIssue Key: ${result.key}`));
89
+ outputResult({ key: result.key, title, project, issueType, parent }, (data) => {
90
+ let out = chalk.gray(`\nTitle: ${data.title}`);
91
+ out += `\n${chalk.gray(`Project: ${data.project}`)}`;
92
+ out += `\n${chalk.gray(`Issue Type: ${data.issueType}`)}`;
93
+ if (data.parent) {
94
+ out += `\n${chalk.gray(`Parent: ${data.parent}`)}`;
95
+ }
96
+ out += `\n${chalk.cyan(`\nIssue Key: ${data.key}`)}`;
97
+ return out;
98
+ });
95
99
  }
96
100
  catch (error) {
97
101
  const errorMsg = error.message?.toLowerCase() || '';
@@ -3,6 +3,7 @@ import { getIssueLinks, deleteIssueLink, validateIssuePermissions } from '../lib
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { validateOptions, IssueKeySchema } from '../lib/validation.js';
6
+ import { outputResult } from '../lib/json-mode.js';
6
7
  export async function deleteIssueLinkCommand(sourceKey, targetKey) {
7
8
  validateOptions(IssueKeySchema, sourceKey);
8
9
  validateOptions(IssueKeySchema, targetKey);
@@ -26,7 +27,7 @@ export async function deleteIssueLinkCommand(sourceKey, targetKey) {
26
27
  ui.startSpinner(`Deleting link ${linkId}...`);
27
28
  await deleteIssueLink(linkId);
28
29
  ui.succeedSpinner(chalk.green(`Link deleted successfully`));
29
- console.log(chalk.gray(`\nRemoved: ${sourceKey} <--> ${targetKey} (${matchingLinks[0].type.name})`));
30
+ outputResult({ success: true, sourceKey, targetKey, linkType: matchingLinks[0].type.name }, (data) => chalk.gray(`\nRemoved: ${data.sourceKey} <--> ${data.targetKey} (${data.linkType})`));
30
31
  }
31
32
  catch (error) {
32
33
  if (error instanceof CommandError)
@@ -3,6 +3,7 @@ import { removeIssueLabels, validateIssuePermissions } from '../lib/jira-client.
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { validateOptions, IssueKeySchema } from '../lib/validation.js';
6
+ import { outputResult } from '../lib/json-mode.js';
6
7
  export async function deleteLabelCommand(taskId, labelsString) {
7
8
  // Validate input
8
9
  validateOptions(IssueKeySchema, taskId);
@@ -21,8 +22,7 @@ export async function deleteLabelCommand(taskId, labelsString) {
21
22
  try {
22
23
  await removeIssueLabels(taskId, labels);
23
24
  ui.succeedSpinner(chalk.green(`Labels removed successfully from ${taskId}`));
24
- console.log(chalk.gray(`
25
- Labels: ${labels.join(', ')}`));
25
+ outputResult({ success: true, issueKey: taskId, labels }, (data) => chalk.gray(`\nLabels: ${data.labels.join(', ')}`));
26
26
  }
27
27
  catch (error) {
28
28
  if (error instanceof CommandError)
@@ -1,20 +1,21 @@
1
1
  import chalk from 'chalk';
2
- import { listEpics, getEpic, createEpic, updateEpic, getEpicIssues, linkIssueToEpic, unlinkIssueFromEpic, getEpicProgress, } from '../lib/jira-client.js';
2
+ import { getEpics, getEpic, createEpic, updateEpic, getEpicIssues, linkIssueToEpic, unlinkIssueFromEpic, getEpicProgress, } from '../lib/jira-client.js';
3
3
  import { formatEpicList, formatEpicDetails, formatEpicProgress, formatEpicIssues, } from '../lib/formatters.js';
4
4
  import { CommandError } from '../lib/errors.js';
5
5
  import { ui } from '../lib/ui.js';
6
+ import { outputResult } from '../lib/json-mode.js';
6
7
  // =============================================================================
7
8
  // epic list <project-key> [--done] [--max <n>]
8
9
  // =============================================================================
9
- export async function epicListCommand(projectKey, options) {
10
+ export async function epicListCommand(projectKey, options = {}) {
10
11
  ui.startSpinner(`Fetching epics for project ${projectKey}...`);
11
12
  try {
12
- const epics = await listEpics(projectKey, {
13
+ const epics = await getEpics(projectKey, {
13
14
  includeDone: !!options.done,
14
15
  max: options.max,
15
16
  });
16
17
  ui.stopSpinner();
17
- console.log(formatEpicList(epics));
18
+ outputResult(epics, formatEpicList);
18
19
  }
19
20
  catch (error) {
20
21
  ui.failSpinner();
@@ -38,7 +39,7 @@ export async function epicGetCommand(epicKey) {
38
39
  try {
39
40
  const epic = await getEpic(epicKey);
40
41
  ui.stopSpinner();
41
- console.log(formatEpicDetails(epic));
42
+ outputResult(epic, formatEpicDetails);
42
43
  }
43
44
  catch (error) {
44
45
  ui.failSpinner();
@@ -68,10 +69,13 @@ export async function epicCreateCommand(projectKey, options) {
68
69
  labels,
69
70
  });
70
71
  ui.succeedSpinner(chalk.green(`Epic created successfully: ${result.key}`));
71
- console.log(chalk.gray(`\nName: ${options.name}`));
72
- console.log(chalk.gray(`Summary: ${options.summary}`));
73
- console.log(chalk.gray(`Project: ${projectKey}`));
74
- console.log(chalk.cyan(`\nEpic Key: ${result.key}`));
72
+ outputResult({ key: result.key, name: options.name, summary: options.summary, project: projectKey }, (data) => {
73
+ let out = chalk.gray(`\nName: ${data.name}`);
74
+ out += `\n${chalk.gray(`Summary: ${data.summary}`)}`;
75
+ out += `\n${chalk.gray(`Project: ${data.project}`)}`;
76
+ out += `\n${chalk.cyan(`\nEpic Key: ${data.key}`)}`;
77
+ return out;
78
+ });
75
79
  }
76
80
  catch (error) {
77
81
  ui.failSpinner();
@@ -99,10 +103,14 @@ export async function epicUpdateCommand(epicKey, options) {
99
103
  try {
100
104
  await updateEpic(epicKey, { name: options.name, summary: options.summary });
101
105
  ui.succeedSpinner(chalk.green(`Epic ${epicKey} updated successfully.`));
102
- if (options.name)
103
- console.log(chalk.gray(`Name: ${options.name}`));
104
- if (options.summary)
105
- console.log(chalk.gray(`Summary: ${options.summary}`));
106
+ outputResult({ success: true, epicKey, name: options.name, summary: options.summary }, (data) => {
107
+ const lines = [];
108
+ if (data.name)
109
+ lines.push(chalk.gray(`Name: ${data.name}`));
110
+ if (data.summary)
111
+ lines.push(chalk.gray(`Summary: ${data.summary}`));
112
+ return lines.join('\n');
113
+ });
106
114
  }
107
115
  catch (error) {
108
116
  ui.failSpinner();
@@ -125,7 +133,7 @@ export async function epicIssuesCommand(epicKey, options) {
125
133
  try {
126
134
  const issues = await getEpicIssues(epicKey, { max: options.max });
127
135
  ui.stopSpinner();
128
- console.log(formatEpicIssues(issues));
136
+ outputResult(issues, formatEpicIssues);
129
137
  }
130
138
  catch (error) {
131
139
  ui.failSpinner();
@@ -196,7 +204,7 @@ export async function epicProgressCommand(epicKey) {
196
204
  try {
197
205
  const progress = await getEpicProgress(epicKey);
198
206
  ui.stopSpinner();
199
- console.log(formatEpicProgress(progress));
207
+ outputResult(progress, formatEpicProgress);
200
208
  }
201
209
  catch (error) {
202
210
  ui.failSpinner();
@@ -2,10 +2,16 @@ import chalk from 'chalk';
2
2
  import { getIssueStatistics, validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { formatIssueStatistics } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
+ import { outputResult, isJsonMode } from '../lib/json-mode.js';
5
6
  export async function getIssueStatisticsCommand(taskIds, options = {}) {
6
7
  const ids = taskIds.split(',').map(id => id.trim()).filter(id => id !== '');
7
8
  if (ids.length === 0) {
8
- console.error(chalk.red('Please provide at least one issue ID.'));
9
+ if (!isJsonMode()) {
10
+ console.error(chalk.red('Please provide at least one issue ID.'));
11
+ }
12
+ else {
13
+ outputResult({ error: true, message: 'Please provide at least one issue ID.' });
14
+ }
9
15
  return;
10
16
  }
11
17
  ui.startSpinner(`Fetching statistics for ${ids.length} issue(s)...`);
@@ -22,19 +28,19 @@ export async function getIssueStatisticsCommand(taskIds, options = {}) {
22
28
  if (!(error instanceof Error))
23
29
  continue;
24
30
  const isPermissionError = error.message.includes('not allowed') || error.message.includes('restricted');
25
- if (isPermissionError) {
26
- console.warn(chalk.yellow(`
27
- Skipping ${id}: ${error.message}`));
28
- }
29
- else {
30
- console.error(chalk.red(`
31
- Failed to fetch statistics for ${id}: ${error.message}`));
31
+ if (!isJsonMode()) {
32
+ if (isPermissionError) {
33
+ console.warn(chalk.yellow(`\nSkipping ${id}: ${error.message}`));
34
+ }
35
+ else {
36
+ console.error(chalk.red(`\nFailed to fetch statistics for ${id}: ${error.message}`));
37
+ }
32
38
  }
33
39
  }
34
40
  }
35
41
  if (results.length > 0) {
36
42
  ui.succeedSpinner(chalk.green('Statistics retrieved'));
37
- console.log(formatIssueStatistics(results, options.fullBreakdown));
43
+ outputResult(results, (data) => formatIssueStatistics(data, options.fullBreakdown));
38
44
  }
39
45
  else {
40
46
  ui.failSpinner('Failed to retrieve statistics or all issues were filtered out');
@@ -4,6 +4,7 @@ import { searchIssuesByJql, getIssueWorklogs } from '../lib/jira-client.js';
4
4
  import { parseTimeframe, formatDateForJql } from '../lib/utils.js';
5
5
  import { formatWorklogs } from '../lib/formatters.js';
6
6
  import { CommandError } from '../lib/errors.js';
7
+ import { outputResult, isJsonMode } from '../lib/json-mode.js';
7
8
  export async function getPersonWorklogCommand(person, timeframe, options) {
8
9
  ui.startSpinner(`Fetching worklogs for ${person}...`);
9
10
  try {
@@ -16,9 +17,12 @@ export async function getPersonWorklogCommand(person, timeframe, options) {
16
17
  const issues = await searchIssuesByJql(jql, 100);
17
18
  if (issues.length === 0) {
18
19
  ui.stopSpinner();
19
- console.log(chalk.yellow(`
20
- No worklogs found for ${person} between ${startJql} and ${endJql}.
21
- `));
20
+ if (!isJsonMode()) {
21
+ console.log(chalk.yellow(`\nNo worklogs found for ${person} between ${startJql} and ${endJql}.\n`));
22
+ }
23
+ else {
24
+ outputResult([]);
25
+ }
22
26
  return;
23
27
  }
24
28
  const allWorklogs = [];
@@ -40,12 +44,15 @@ No worklogs found for ${person} between ${startJql} and ${endJql}.
40
44
  }
41
45
  ui.stopSpinner();
42
46
  if (allWorklogs.length === 0) {
43
- console.log(chalk.yellow(`
44
- No worklogs found for ${person} after detailed filtering.
45
- `));
47
+ if (!isJsonMode()) {
48
+ console.log(chalk.yellow(`\nNo worklogs found for ${person} after detailed filtering.\n`));
49
+ }
50
+ else {
51
+ outputResult([]);
52
+ }
46
53
  return;
47
54
  }
48
- console.log(formatWorklogs(allWorklogs, options.groupByIssue));
55
+ outputResult(allWorklogs, (data) => formatWorklogs(data, options.groupByIssue));
49
56
  }
50
57
  catch (error) {
51
58
  ui.failSpinner(`Failed to fetch worklogs: ${error.message}`);
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { assignIssue, validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
+ import { outputResult } from '../lib/json-mode.js';
5
6
  export async function issueAssignCommand(issueKey, accountId) {
6
7
  const actualAccountId = accountId === 'null' ? null : accountId;
7
8
  // Check permissions and filters
@@ -11,6 +12,7 @@ export async function issueAssignCommand(issueKey, accountId) {
11
12
  try {
12
13
  await assignIssue(issueKey, actualAccountId);
13
14
  ui.succeedSpinner(chalk.green(`Issue ${issueKey} successfully assigned to ${actualAccountId || 'Unassigned'}.`));
15
+ outputResult({ success: true, issueKey, assignee: actualAccountId || null }, (data) => chalk.green(`Issue ${data.issueKey} successfully assigned to ${data.assignee || 'Unassigned'}.`));
14
16
  }
15
17
  catch (error) {
16
18
  if (error instanceof CommandError) {
@@ -4,6 +4,7 @@ import { formatUsers } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
6
6
  import { CommandError } from '../lib/errors.js';
7
+ import { outputResult, isJsonMode } from '../lib/json-mode.js';
7
8
  export async function listColleaguesCommand(projectKey) {
8
9
  if (projectKey) {
9
10
  if (!isProjectAllowed(projectKey)) {
@@ -21,10 +22,15 @@ export async function listColleaguesCommand(projectKey) {
21
22
  const users = await getUsers(projectKey);
22
23
  ui.succeedSpinner(chalk.green('Colleagues retrieved'));
23
24
  if (users.length === 0) {
24
- console.log(chalk.yellow('\nNo active colleagues found.'));
25
+ if (!isJsonMode()) {
26
+ console.log(chalk.yellow('\nNo active colleagues found.'));
27
+ }
28
+ else {
29
+ outputResult(users);
30
+ }
25
31
  }
26
32
  else {
27
- console.log(formatUsers(users));
33
+ outputResult(users, formatUsers);
28
34
  }
29
35
  }
30
36
  catch (error) {
@@ -4,6 +4,7 @@ import { formatIssueLinks } from '../lib/formatters.js';
4
4
  import { CommandError } from '../lib/errors.js';
5
5
  import { ui } from '../lib/ui.js';
6
6
  import { validateOptions, IssueKeySchema } from '../lib/validation.js';
7
+ import { outputResult } from '../lib/json-mode.js';
7
8
  export async function listIssueLinksCommand(issueKey) {
8
9
  validateOptions(IssueKeySchema, issueKey);
9
10
  ui.startSpinner(`Validating permissions for ${issueKey}...`);
@@ -12,7 +13,7 @@ export async function listIssueLinksCommand(issueKey) {
12
13
  try {
13
14
  const links = await getIssueLinks(issueKey);
14
15
  ui.succeedSpinner(chalk.green(`Issue links retrieved for ${issueKey}`));
15
- console.log(formatIssueLinks(issueKey, links));
16
+ outputResult(links, (data) => formatIssueLinks(issueKey, data));
16
17
  }
17
18
  catch (error) {
18
19
  if (error instanceof CommandError)
@@ -4,6 +4,7 @@ import { formatProjectIssueTypes } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
6
6
  import { CommandError } from '../lib/errors.js';
7
+ import { outputResult } from '../lib/json-mode.js';
7
8
  export async function listIssueTypesCommand(projectKey) {
8
9
  // Check if project is allowed
9
10
  if (!isProjectAllowed(projectKey)) {
@@ -16,5 +17,5 @@ export async function listIssueTypesCommand(projectKey) {
16
17
  ui.startSpinner(`Fetching issue types for project ${projectKey}...`);
17
18
  const issueTypes = await getProjectIssueTypes(projectKey);
18
19
  ui.succeedSpinner(chalk.green('Issue types retrieved'));
19
- console.log(formatProjectIssueTypes(projectKey, issueTypes));
20
+ outputResult(issueTypes, (data) => formatProjectIssueTypes(projectKey, data));
20
21
  }
@@ -3,12 +3,13 @@ import { getAvailableLinkTypes } from '../lib/jira-client.js';
3
3
  import { formatLinkTypes } from '../lib/formatters.js';
4
4
  import { CommandError } from '../lib/errors.js';
5
5
  import { ui } from '../lib/ui.js';
6
+ import { outputResult } from '../lib/json-mode.js';
6
7
  export async function listLinkTypesCommand() {
7
8
  ui.startSpinner('Fetching available link types...');
8
9
  try {
9
10
  const linkTypes = await getAvailableLinkTypes();
10
11
  ui.succeedSpinner(chalk.green('Link types retrieved'));
11
- console.log(formatLinkTypes(linkTypes));
12
+ outputResult(linkTypes, formatLinkTypes);
12
13
  }
13
14
  catch (error) {
14
15
  if (error instanceof CommandError)
@@ -2,9 +2,10 @@ import chalk from 'chalk';
2
2
  import { getCurrentUser } from '../lib/jira-client.js';
3
3
  import { formatUserInfo } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
+ import { outputResult } from '../lib/json-mode.js';
5
6
  export async function meCommand() {
6
7
  ui.startSpinner('Fetching user information...');
7
8
  const user = await getCurrentUser();
8
9
  ui.succeedSpinner(chalk.green('User information retrieved'));
9
- console.log(formatUserInfo(user));
10
+ outputResult(user, formatUserInfo);
10
11
  }
@@ -3,6 +3,7 @@ import { getProjectFields } from '../lib/field-resolver.js';
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { validateOptions, ProjectKeySchema } from '../lib/validation.js';
5
5
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
6
+ import { outputResult, isJsonMode } from '../lib/json-mode.js';
6
7
  export async function projectFieldsCommand(projectKey, options) {
7
8
  if (!projectKey) {
8
9
  throw new CommandError('Project key is required');
@@ -24,20 +25,28 @@ export async function projectFieldsCommand(projectKey, options) {
24
25
  fields = fields.filter(f => f.name.toLowerCase().includes(lower) || f.id.toLowerCase().includes(lower));
25
26
  }
26
27
  if (fields.length === 0) {
27
- console.log(chalk.yellow(`No fields found for project ${projectKey}`));
28
+ if (!isJsonMode()) {
29
+ console.log(chalk.yellow(`No fields found for project ${projectKey}`));
30
+ }
31
+ else {
32
+ outputResult(fields);
33
+ }
28
34
  return;
29
35
  }
30
- console.log(chalk.bold(`\nFields for project ${projectKey}:`));
31
- console.log(chalk.gray(`${'ID'.padEnd(30)} ${'Name'.padEnd(30)} ${'Type'.padEnd(20)} Required`));
32
- console.log(chalk.gray('-'.repeat(90)));
33
- for (const field of fields) {
34
- const required = field.required ? chalk.red('yes') : chalk.gray('no');
35
- const idLabel = field.custom ? chalk.cyan(field.id.padEnd(30)) : field.id.padEnd(30);
36
- const nameLabel = field.custom ? chalk.cyan(field.name.padEnd(30)) : field.name.padEnd(30);
37
- const type = (field.schema?.type || 'unknown').padEnd(20);
38
- console.log(`${idLabel} ${nameLabel} ${type} ${required}`);
39
- }
40
- console.log(chalk.gray(`\nTotal: ${fields.length} field(s)`));
36
+ outputResult(fields, (data) => {
37
+ let out = chalk.bold(`\nFields for project ${projectKey}:`);
38
+ out += `\n${chalk.gray(`${'ID'.padEnd(30)} ${'Name'.padEnd(30)} ${'Type'.padEnd(20)} Required`)}`;
39
+ out += `\n${chalk.gray('-'.repeat(90))}`;
40
+ for (const field of data) {
41
+ const required = field.required ? chalk.red('yes') : chalk.gray('no');
42
+ const idLabel = field.custom ? chalk.cyan(field.id.padEnd(30)) : field.id.padEnd(30);
43
+ const nameLabel = field.custom ? chalk.cyan(field.name.padEnd(30)) : field.name.padEnd(30);
44
+ const type = (field.schema?.type || 'unknown').padEnd(20);
45
+ out += `\n${idLabel} ${nameLabel} ${type} ${required}`;
46
+ }
47
+ out += `\n${chalk.gray(`\nTotal: ${data.length} field(s)`)}`;
48
+ return out;
49
+ });
41
50
  }
42
51
  catch (error) {
43
52
  if (error instanceof CommandError)
@@ -4,6 +4,7 @@ import { formatProjectStatuses } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
5
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
6
6
  import { CommandError } from '../lib/errors.js';
7
+ import { outputResult } from '../lib/json-mode.js';
7
8
  export async function projectStatusesCommand(projectIdOrKey) {
8
9
  // Check if project is allowed
9
10
  if (!isProjectAllowed(projectIdOrKey)) {
@@ -17,7 +18,7 @@ export async function projectStatusesCommand(projectIdOrKey) {
17
18
  try {
18
19
  const statuses = await getProjectStatuses(projectIdOrKey);
19
20
  ui.succeedSpinner(chalk.green('Project statuses retrieved'));
20
- console.log(formatProjectStatuses(projectIdOrKey, statuses));
21
+ outputResult(statuses, (data) => formatProjectStatuses(projectIdOrKey, data));
21
22
  }
22
23
  catch (error) {
23
24
  ui.failSpinner(chalk.red('Failed to fetch project statuses'));
@@ -3,6 +3,7 @@ import { getProjects } from '../lib/jira-client.js';
3
3
  import { formatProjects } from '../lib/formatters.js';
4
4
  import { getAllowedProjects, isProjectAllowed } from '../lib/settings.js';
5
5
  import { ui } from '../lib/ui.js';
6
+ import { outputResult, isJsonMode } from '../lib/json-mode.js';
6
7
  export async function projectsCommand() {
7
8
  ui.startSpinner('Fetching projects...');
8
9
  const allProjects = await getProjects();
@@ -13,12 +14,12 @@ export async function projectsCommand() {
13
14
  ? allProjects
14
15
  : allProjects.filter(project => isProjectAllowed(project.key));
15
16
  ui.succeedSpinner(chalk.green('Projects retrieved'));
16
- if (filteredProjects.length === 0) {
17
+ if (!isJsonMode() && filteredProjects.length === 0) {
17
18
  console.log(chalk.yellow('\nNo projects match your settings configuration.'));
18
19
  const displayKeys = allowedProjects.map(p => typeof p === 'string' ? p : p.key);
19
20
  console.log(chalk.gray('Allowed projects: ' + displayKeys.join(', ')));
20
21
  }
21
22
  else {
22
- console.log(formatProjects(filteredProjects));
23
+ outputResult(filteredProjects, formatProjects);
23
24
  }
24
25
  }
@@ -2,15 +2,18 @@ import chalk from 'chalk';
2
2
  import { searchIssuesByJql } from '../lib/jira-client.js';
3
3
  import { formatJqlResults } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
+ import { outputResult, isJsonMode } from '../lib/json-mode.js';
5
6
  export async function runJqlCommand(jqlQuery, options) {
6
7
  // Parse and validate limit parameter
7
8
  let maxResults = options.limit || 50;
8
9
  if (maxResults > 1000) {
9
- console.warn(chalk.yellow('\nWarning: Limit is very high. Using 1000 as maximum.'));
10
+ if (!isJsonMode()) {
11
+ console.warn(chalk.yellow('\nWarning: Limit is very high. Using 1000 as maximum.'));
12
+ }
10
13
  maxResults = 1000;
11
14
  }
12
15
  ui.startSpinner('Executing JQL query...');
13
16
  const issues = await searchIssuesByJql(jqlQuery, maxResults);
14
17
  ui.succeedSpinner(chalk.green('Query executed successfully'));
15
- console.log(formatJqlResults(issues));
18
+ outputResult(issues, formatJqlResults);
16
19
  }
@@ -8,6 +8,7 @@ import { SettingsSchema } from '../lib/validation.js';
8
8
  import { getProjects } from '../lib/jira-client.js';
9
9
  import { CommandError } from '../lib/errors.js';
10
10
  import { validateEnvVars } from '../lib/utils.js';
11
+ import { outputResult } from '../lib/json-mode.js';
11
12
  export async function settingsCommand(options) {
12
13
  if (options.reset) {
13
14
  ui.startSpinner('Resetting settings to default...');
@@ -31,7 +32,7 @@ export async function settingsCommand(options) {
31
32
  }
32
33
  // Default: Show current settings
33
34
  const settings = loadSettings();
34
- console.log(formatSettings(settings));
35
+ outputResult(settings, formatSettings);
35
36
  }
36
37
  async function validateSettingsFile(filePath) {
37
38
  ui.startSpinner(`Validating ${filePath}...`);
@@ -3,6 +3,7 @@ import { validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { formatTaskDetails } from '../lib/formatters.js';
4
4
  import { CommandError } from '../lib/errors.js';
5
5
  import { ui } from '../lib/ui.js';
6
+ import { outputResult } from '../lib/json-mode.js';
6
7
  export async function taskWithDetailsCommand(taskId, options = {}) {
7
8
  ui.startSpinner(`Fetching details for ${taskId}...`);
8
9
  try {
@@ -12,7 +13,7 @@ export async function taskWithDetailsCommand(taskId, options = {}) {
12
13
  historyOffset: options.historyOffset ? parseInt(options.historyOffset, 10) : undefined,
13
14
  });
14
15
  ui.succeedSpinner(chalk.green('Task details retrieved'));
15
- console.log(formatTaskDetails(task));
16
+ outputResult(task, formatTaskDetails);
16
17
  }
17
18
  catch (error) {
18
19
  if (error instanceof CommandError)
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { getIssueTransitions, transitionIssue, validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { CommandError } from '../lib/errors.js';
4
4
  import { ui } from '../lib/ui.js';
5
+ import { outputResult } from '../lib/json-mode.js';
5
6
  export async function transitionCommand(taskId, toStatus) {
6
7
  // Check permissions and filters
7
8
  ui.startSpinner(`Validating permissions for ${taskId}...`);
@@ -33,6 +34,7 @@ export async function transitionCommand(taskId, toStatus) {
33
34
  ui.startSpinner(`Transitioning ${taskId} to ${transition.to.name}...`);
34
35
  await transitionIssue(taskId, transition.id);
35
36
  ui.succeedSpinner(chalk.green(`Issue ${taskId} successfully transitioned to ${transition.to.name}.`));
37
+ outputResult({ success: true, issueKey: taskId, status: transition.to.name }, (data) => chalk.green(`Issue ${data.issueKey} successfully transitioned to ${data.status}.`));
36
38
  }
37
39
  catch (error) {
38
40
  if (error instanceof CommandError) {
@@ -7,6 +7,7 @@ import { processMentionsInADF } from '../lib/adf-mentions.js';
7
7
  import { CommandError } from '../lib/errors.js';
8
8
  import { ui } from '../lib/ui.js';
9
9
  import { validateOptions, UpdateDescriptionSchema, IssueKeySchema } from '../lib/validation.js';
10
+ import { outputResult } from '../lib/json-mode.js';
10
11
  export async function updateDescriptionCommand(taskId, options) {
11
12
  // Validate taskId (positional)
12
13
  validateOptions(IssueKeySchema, taskId);
@@ -49,8 +50,7 @@ export async function updateDescriptionCommand(taskId, options) {
49
50
  try {
50
51
  await updateIssueDescription(taskId, adfContent);
51
52
  ui.succeedSpinner(chalk.green(`Description updated successfully for ${taskId}`));
52
- console.log(chalk.gray(`
53
- File: ${absolutePath}`));
53
+ outputResult({ success: true, issueKey: taskId, file: absolutePath }, (data) => chalk.gray(`\nFile: ${data.file}`));
54
54
  }
55
55
  catch (error) {
56
56
  if (error instanceof CommandError)
@@ -7,6 +7,7 @@ import { CommandError } from '../lib/errors.js';
7
7
  import { ui } from '../lib/ui.js';
8
8
  import { validateOptions, UpdateIssueSchema, IssueKeySchema } from '../lib/validation.js';
9
9
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
10
+ import { outputResult } from '../lib/json-mode.js';
10
11
  export async function updateIssueCommand(issueKey, options) {
11
12
  validateOptions(IssueKeySchema, issueKey);
12
13
  validateOptions(UpdateIssueSchema, options);
@@ -86,6 +87,7 @@ export async function updateIssueCommand(issueKey, options) {
86
87
  try {
87
88
  await updateIssue(issueKey, fields);
88
89
  ui.succeedSpinner(chalk.green(`Issue ${issueKey} updated successfully`));
90
+ outputResult({ success: true, issueKey }, (data) => chalk.green(`Issue ${data.issueKey} updated successfully`));
89
91
  }
90
92
  catch (error) {
91
93
  if (error instanceof CommandError)
@@ -946,3 +946,4 @@ export async function getAvailableLinkTypes() {
946
946
  outward: t.outward,
947
947
  }));
948
948
  }
949
+ export const getEpics = listEpics;
@@ -0,0 +1,45 @@
1
+ import chalk from 'chalk';
2
+ let _jsonMode = false;
3
+ let _compactMode = false;
4
+ export function initJsonMode() {
5
+ _jsonMode = process.argv.includes('--json') || process.argv.includes('--json-compact');
6
+ _compactMode = process.argv.includes('--json-compact');
7
+ }
8
+ export function isJsonMode() {
9
+ return _jsonMode;
10
+ }
11
+ export function isCompactMode() {
12
+ return _compactMode;
13
+ }
14
+ export function outputResult(data, formatter) {
15
+ if (_jsonMode) {
16
+ const output = _compactMode
17
+ ? JSON.stringify(data)
18
+ : JSON.stringify(data, null, 2);
19
+ console.log(output);
20
+ }
21
+ else {
22
+ if (formatter) {
23
+ console.log(formatter(data));
24
+ }
25
+ else if (typeof data === 'object' && data !== null) {
26
+ console.log(JSON.stringify(data, null, 2));
27
+ }
28
+ else {
29
+ console.log(data);
30
+ }
31
+ }
32
+ }
33
+ export function outputError(message, hints = [], exitCode = 1) {
34
+ if (_jsonMode) {
35
+ console.log(JSON.stringify({ error: true, message, hints, exitCode }));
36
+ process.exit(exitCode);
37
+ }
38
+ else {
39
+ console.error(chalk.red(`\n❌ Error: ${message}`));
40
+ hints.forEach(hint => {
41
+ console.error(chalk.yellow(` Hint: ${hint}`));
42
+ });
43
+ process.exit(exitCode);
44
+ }
45
+ }
package/dist/lib/ui.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import ora from 'ora';
2
+ import { isJsonMode } from './json-mode.js';
2
3
  class UI {
3
4
  spinnerInstance = null;
4
5
  startSpinner(message) {
6
+ if (isJsonMode()) {
7
+ return null;
8
+ }
5
9
  if (this.spinnerInstance) {
6
10
  this.spinnerInstance.stop();
7
11
  }
@@ -9,24 +13,36 @@ class UI {
9
13
  return this.spinnerInstance;
10
14
  }
11
15
  stopSpinner() {
16
+ if (isJsonMode()) {
17
+ return;
18
+ }
12
19
  if (this.spinnerInstance) {
13
20
  this.spinnerInstance.stop();
14
21
  this.spinnerInstance = null;
15
22
  }
16
23
  }
17
24
  succeedSpinner(message) {
25
+ if (isJsonMode()) {
26
+ return;
27
+ }
18
28
  if (this.spinnerInstance) {
19
29
  this.spinnerInstance.succeed(message);
20
30
  this.spinnerInstance = null;
21
31
  }
22
32
  }
23
33
  failSpinner(message) {
34
+ if (isJsonMode()) {
35
+ return;
36
+ }
24
37
  if (this.spinnerInstance) {
25
38
  this.spinnerInstance.fail(message);
26
39
  this.spinnerInstance = null;
27
40
  }
28
41
  }
29
42
  updateSpinner(message) {
43
+ if (isJsonMode()) {
44
+ return;
45
+ }
30
46
  if (this.spinnerInstance) {
31
47
  this.spinnerInstance.text = message;
32
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.9.96",
3
+ "version": "0.9.97",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",