jira-ai 0.9.95 → 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.
Files changed (37) hide show
  1. package/README.md +49 -0
  2. package/dist/cli.js +47 -24
  3. package/dist/commands/about.js +13 -8
  4. package/dist/commands/add-comment.js +2 -1
  5. package/dist/commands/add-label.js +2 -2
  6. package/dist/commands/auth.js +14 -8
  7. package/dist/commands/confluence.js +23 -20
  8. package/dist/commands/create-issue-link.js +2 -2
  9. package/dist/commands/create-task.js +82 -14
  10. package/dist/commands/delete-issue-link.js +2 -1
  11. package/dist/commands/delete-label.js +2 -2
  12. package/dist/commands/epic.js +23 -15
  13. package/dist/commands/get-issue-statistics.js +15 -9
  14. package/dist/commands/get-person-worklog.js +14 -7
  15. package/dist/commands/issue.js +2 -0
  16. package/dist/commands/list-colleagues.js +8 -2
  17. package/dist/commands/list-issue-links.js +2 -1
  18. package/dist/commands/list-issue-types.js +2 -1
  19. package/dist/commands/list-link-types.js +2 -1
  20. package/dist/commands/me.js +2 -1
  21. package/dist/commands/project-fields.js +64 -0
  22. package/dist/commands/project-statuses.js +2 -1
  23. package/dist/commands/projects.js +3 -2
  24. package/dist/commands/run-jql.js +5 -2
  25. package/dist/commands/settings.js +2 -1
  26. package/dist/commands/task-with-details.js +2 -1
  27. package/dist/commands/transition.js +2 -0
  28. package/dist/commands/update-description.js +2 -2
  29. package/dist/commands/update-issue.js +105 -0
  30. package/dist/lib/epic-fields.js +5 -15
  31. package/dist/lib/field-resolver.js +111 -0
  32. package/dist/lib/jira-client.js +27 -19
  33. package/dist/lib/json-mode.js +45 -0
  34. package/dist/lib/settings.js +1 -0
  35. package/dist/lib/ui.js +16 -0
  36. package/dist/lib/validation.js +43 -0
  37. package/package.json +1 -1
package/README.md CHANGED
@@ -63,6 +63,55 @@ 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
+
95
+ ### Rich Issue Management
96
+
97
+ Create issues with detailed field support:
98
+ ```bash
99
+ jira-ai issue create --title "New feature" --project PROJ --issue-type Task \
100
+ --priority High --description "Feature details" --labels "feature,backend" \
101
+ --component "api" --fix-version "v2.0" --due-date 2026-04-15 --assignee "John Doe"
102
+ ```
103
+
104
+ Update issues with any combination of fields:
105
+ ```bash
106
+ jira-ai issue update PROJ-123 --priority Critical --summary "Updated title" \
107
+ --labels "urgent" --due-date 2026-05-01
108
+ ```
109
+
110
+ Discover available fields for a project (including custom fields):
111
+ ```bash
112
+ jira-ai project fields PROJ --type Task
113
+ ```
114
+
66
115
  ## Service Account Authentication
67
116
 
68
117
  Atlassian service accounts use scoped API tokens that must authenticate through the `api.atlassian.com` gateway rather than direct site URLs.
package/dist/cli.js CHANGED
@@ -9,7 +9,8 @@ import { projectStatusesCommand } from './commands/project-statuses.js';
9
9
  import { listIssueTypesCommand } from './commands/list-issue-types.js';
10
10
  import { listColleaguesCommand } from './commands/list-colleagues.js';
11
11
  import { runJqlCommand } from './commands/run-jql.js';
12
- import { updateDescriptionCommand } from './commands/update-description.js';
12
+ import { updateIssueCommand } from './commands/update-issue.js';
13
+ import { projectFieldsCommand } from './commands/project-fields.js';
13
14
  import { addCommentCommand } from './commands/add-comment.js';
14
15
  import { addLabelCommand } from './commands/add-label.js';
15
16
  import { deleteLabelCommand } from './commands/delete-label.js';
@@ -33,7 +34,8 @@ import { checkForUpdate, formatUpdateMessage, checkForUpdateSync } from './lib/u
33
34
  import { CliError } from './types/errors.js';
34
35
  import { CommandError } from './lib/errors.js';
35
36
  import { ui } from './lib/ui.js';
36
- import { CreateTaskSchema, UpdateDescriptionSchema, ConfluenceAddCommentSchema, RunJqlSchema, GetPersonWorklogSchema, GetIssueStatisticsSchema, EpicListSchema, EpicCreateSchema, EpicUpdateSchema, EpicLinkSchema, EpicMaxSchema, validateOptions, IssueKeySchema, ProjectKeySchema, TimeframeSchema } from './lib/validation.js';
37
+ import { initJsonMode, outputError } from './lib/json-mode.js';
38
+ import { CreateTaskSchema, UpdateDescriptionSchema, ProjectFieldsSchema, ConfluenceAddCommentSchema, RunJqlSchema, GetPersonWorklogSchema, GetIssueStatisticsSchema, EpicListSchema, EpicCreateSchema, EpicUpdateSchema, EpicLinkSchema, EpicMaxSchema, validateOptions, IssueKeySchema, ProjectKeySchema, TimeframeSchema } from './lib/validation.js';
37
39
  import { realpathSync } from 'fs';
38
40
  // Create CLI program
39
41
  const program = new Command();
@@ -41,6 +43,8 @@ program
41
43
  .name('jira-ai')
42
44
  .description('CLI tool for interacting with Atlassian Jira')
43
45
  .version(getVersion())
46
+ .option('--json', 'Output as JSON')
47
+ .option('--json-compact', 'Output as compact JSON')
44
48
  .addHelpText('after', () => {
45
49
  const latestVersion = checkForUpdateSync();
46
50
  if (latestVersion) {
@@ -115,6 +119,15 @@ issue
115
119
  .requiredOption('--project <project>', 'Project key (e.g., PROJ)')
116
120
  .requiredOption('--issue-type <type>', 'Issue type (e.g., Task, Epic, Subtask)')
117
121
  .option('--parent <key>', 'Parent issue key (required for subtasks)')
122
+ .option('--priority <priority>', 'Issue priority (e.g., High, Medium, Low)')
123
+ .option('--description <text>', 'Issue description as Markdown')
124
+ .option('--description-file <path>', 'Path to a Markdown file for the description')
125
+ .option('--labels <labels>', 'Comma-separated labels')
126
+ .option('--component <component>', 'Comma-separated component names')
127
+ .option('--fix-version <version>', 'Comma-separated fix version names')
128
+ .option('--due-date <date>', 'Due date in YYYY-MM-DD format')
129
+ .option('--assignee <assignee>', 'Assignee (accountid:<id> or display name)')
130
+ .option('--custom-field <field=value>', 'Custom field in fieldId=value format (repeatable)', (val, prev) => [...(prev || []), val], [])
118
131
  .action(withPermission('issue.create', createTaskCommand, { schema: CreateTaskSchema }));
119
132
  issue
120
133
  .command('search <jql-query>')
@@ -136,10 +149,21 @@ issue
136
149
  }));
137
150
  issue
138
151
  .command('update <issue-id>')
139
- .description('Update a Jira issue\'s description using content from a local Markdown file.')
140
- .requiredOption('--from-file <path>', 'Path to Markdown file')
141
- .action(withPermission('issue.update', updateDescriptionCommand, {
142
- schema: UpdateDescriptionSchema,
152
+ .description('Update one or more fields of a Jira issue. Supports priority, summary, description, labels, components, fix versions, due date, assignee, and custom fields.')
153
+ .option('--from-file <path>', 'Update description from a Markdown file (legacy alias for --description-file)')
154
+ .option('--priority <priority>', 'New priority (e.g., High, Medium, Low)')
155
+ .option('--summary <text>', 'New summary/title')
156
+ .option('--description <text>', 'New description as Markdown')
157
+ .option('--labels <labels>', 'Comma-separated labels (replaces existing)')
158
+ .option('--clear-labels', 'Remove all labels from the issue')
159
+ .option('--component <component>', 'Comma-separated component names')
160
+ .option('--fix-version <version>', 'Comma-separated fix version names')
161
+ .option('--due-date <date>', 'Due date in YYYY-MM-DD format')
162
+ .option('--assignee <assignee>', 'Assignee (accountid:<id> or display name)')
163
+ .option('--custom-field <field=value>', 'Custom field in fieldId=value format (repeatable)', (val, prev) => [...(prev || []), val], [])
164
+ .action(withPermission('issue.update', (issueId, options) => {
165
+ return updateIssueCommand(issueId, options);
166
+ }, {
143
167
  validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
144
168
  }));
145
169
  issue
@@ -257,6 +281,18 @@ project
257
281
  .action(withPermission('project.types', listIssueTypesCommand, {
258
282
  validateArgs: (args) => validateOptions(ProjectKeySchema, args[0])
259
283
  }));
284
+ project
285
+ .command('fields <project-key>')
286
+ .description('List all available fields for a project, including custom fields.')
287
+ .option('--type <issue-type>', 'Filter fields by issue type')
288
+ .option('--custom', 'Show only custom fields')
289
+ .option('--search <term>', 'Filter fields by name or ID')
290
+ .action(withPermission('project.fields', (projectKey, options) => {
291
+ return projectFieldsCommand(projectKey, options);
292
+ }, {
293
+ schema: ProjectFieldsSchema,
294
+ validateArgs: (args) => validateOptions(ProjectKeySchema, args[0])
295
+ }));
260
296
  // =============================================================================
261
297
  // USER COMMANDS
262
298
  // =============================================================================
@@ -473,6 +509,7 @@ export function configureCommandVisibility(program) {
473
509
  // Parse command line arguments
474
510
  export async function main() {
475
511
  try {
512
+ initJsonMode();
476
513
  // Background update check (non-blocking for the user)
477
514
  checkForUpdate().catch(() => { });
478
515
  configureCommandVisibility(program);
@@ -481,30 +518,16 @@ export async function main() {
481
518
  catch (error) {
482
519
  ui.failSpinner();
483
520
  if (error instanceof CommandError) {
484
- console.error(chalk.red(`\n❌ Error: ${error.message}`));
485
- if (error.hints.length > 0) {
486
- error.hints.forEach(hint => {
487
- console.error(chalk.yellow(` Hint: ${hint}`));
488
- });
489
- }
490
- console.log(); // Add a newline
491
- process.exit(error.exitCode);
521
+ outputError(error.message, error.hints, error.exitCode);
492
522
  }
493
523
  else if (error instanceof CliError) {
494
- console.error(chalk.red(`\n❌ ${error.message}\n`));
495
- process.exit(1);
524
+ outputError(error.message, [], 1);
496
525
  }
497
526
  else if (error instanceof Error) {
498
- console.error(chalk.red(`\n💥 Unexpected Error: ${error.message}`));
499
- if (process.env.DEBUG) {
500
- console.error(chalk.gray(error.stack));
501
- }
502
- console.error(chalk.gray('\nPlease report this issue if it persists.\n'));
503
- process.exit(1);
527
+ outputError(`Unexpected Error: ${error.message}`, ['Please report this issue if it persists.'], 1);
504
528
  }
505
529
  else {
506
- console.error(chalk.red(`\n💥 An unknown error occurred: ${String(error)}\n`));
507
- process.exit(1);
530
+ outputError(`An unknown error occurred: ${String(error)}`, [], 1);
508
531
  }
509
532
  }
510
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)
@@ -1,33 +1,101 @@
1
1
  import chalk from 'chalk';
2
- import { createIssue } from '../lib/jira-client.js';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { markdownToAdf } from 'marklassian';
5
+ import { createIssue, resolveUserByName } from '../lib/jira-client.js';
3
6
  import { CommandError } from '../lib/errors.js';
4
7
  import { ui } from '../lib/ui.js';
5
8
  import { validateOptions, CreateTaskSchema } from '../lib/validation.js';
6
9
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
10
+ import { outputResult } from '../lib/json-mode.js';
7
11
  export async function createTaskCommand(options) {
8
- // Validate options
9
12
  validateOptions(CreateTaskSchema, options);
10
- const { title, project, issueType, parent } = options;
11
- // Check if project is allowed
13
+ const { title, project, issueType, parent, priority, description, descriptionFile, labels, component, fixVersion, dueDate, assignee, customField } = options;
12
14
  if (!isProjectAllowed(project)) {
13
15
  throw new CommandError(`Project '${project}' is not allowed by your settings.`);
14
16
  }
15
- // Check if command is allowed for this project
16
17
  if (!isCommandAllowed('create-task', project)) {
17
18
  throw new CommandError(`Command 'create-task' is not allowed for project ${project}.`);
18
19
  }
19
- // Create issue with spinner
20
+ // Build issue fields
21
+ const issueFields = {};
22
+ if (priority) {
23
+ issueFields.priority = { name: priority };
24
+ }
25
+ if (description && descriptionFile) {
26
+ throw new CommandError('Cannot use both --description and --description-file at the same time');
27
+ }
28
+ if (description) {
29
+ issueFields.description = markdownToAdf(description);
30
+ }
31
+ else if (descriptionFile) {
32
+ const absPath = path.resolve(descriptionFile);
33
+ try {
34
+ const content = fs.readFileSync(absPath, 'utf-8');
35
+ issueFields.description = markdownToAdf(content);
36
+ }
37
+ catch (err) {
38
+ throw new CommandError(`Error reading description file: ${err.message}`);
39
+ }
40
+ }
41
+ if (labels) {
42
+ issueFields.labels = labels.split(',').map(l => l.trim()).filter(Boolean);
43
+ }
44
+ if (component) {
45
+ issueFields.components = component.split(',').map(n => ({ name: n.trim() })).filter(c => c.name);
46
+ }
47
+ if (fixVersion) {
48
+ issueFields.fixVersions = fixVersion.split(',').map(n => ({ name: n.trim() })).filter(v => v.name);
49
+ }
50
+ if (dueDate) {
51
+ issueFields.duedate = dueDate;
52
+ }
53
+ if (assignee) {
54
+ if (assignee.startsWith('accountid:')) {
55
+ issueFields.assignee = { accountId: assignee.slice('accountid:'.length) };
56
+ }
57
+ else {
58
+ const accountId = await resolveUserByName(assignee);
59
+ if (!accountId) {
60
+ throw new CommandError(`Could not resolve user: ${assignee}`, {
61
+ hints: ['Use "accountid:<id>" format or check the display name.'],
62
+ });
63
+ }
64
+ issueFields.assignee = { accountId };
65
+ }
66
+ }
67
+ if (customField) {
68
+ for (const cf of customField) {
69
+ const eqIdx = cf.indexOf('=');
70
+ if (eqIdx === -1) {
71
+ throw new CommandError(`Invalid custom field format: "${cf}". Use fieldId=value.`);
72
+ }
73
+ const fieldId = cf.slice(0, eqIdx);
74
+ const rawValue = cf.slice(eqIdx + 1);
75
+ const numValue = Number(rawValue);
76
+ issueFields[fieldId] = isNaN(numValue) ? rawValue : numValue;
77
+ }
78
+ }
20
79
  ui.startSpinner(`Creating ${issueType} in project ${project}...`);
21
80
  try {
22
- const result = await createIssue(project, title, issueType, parent);
81
+ const result = await createIssue({
82
+ project,
83
+ summary: title,
84
+ issueType,
85
+ parent,
86
+ ...issueFields,
87
+ });
23
88
  ui.succeedSpinner(chalk.green(`Issue created successfully: ${result.key}`));
24
- console.log(chalk.gray(`\nTitle: ${title}`));
25
- console.log(chalk.gray(`Project: ${project}`));
26
- console.log(chalk.gray(`Issue Type: ${issueType}`));
27
- if (parent) {
28
- console.log(chalk.gray(`Parent: ${parent}`));
29
- }
30
- 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
+ });
31
99
  }
32
100
  catch (error) {
33
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)