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.
- package/README.md +49 -0
- package/dist/cli.js +47 -24
- package/dist/commands/about.js +13 -8
- package/dist/commands/add-comment.js +2 -1
- package/dist/commands/add-label.js +2 -2
- package/dist/commands/auth.js +14 -8
- package/dist/commands/confluence.js +23 -20
- package/dist/commands/create-issue-link.js +2 -2
- package/dist/commands/create-task.js +82 -14
- package/dist/commands/delete-issue-link.js +2 -1
- package/dist/commands/delete-label.js +2 -2
- package/dist/commands/epic.js +23 -15
- package/dist/commands/get-issue-statistics.js +15 -9
- package/dist/commands/get-person-worklog.js +14 -7
- package/dist/commands/issue.js +2 -0
- package/dist/commands/list-colleagues.js +8 -2
- package/dist/commands/list-issue-links.js +2 -1
- package/dist/commands/list-issue-types.js +2 -1
- package/dist/commands/list-link-types.js +2 -1
- package/dist/commands/me.js +2 -1
- package/dist/commands/project-fields.js +64 -0
- package/dist/commands/project-statuses.js +2 -1
- package/dist/commands/projects.js +3 -2
- package/dist/commands/run-jql.js +5 -2
- package/dist/commands/settings.js +2 -1
- package/dist/commands/task-with-details.js +2 -1
- package/dist/commands/transition.js +2 -0
- package/dist/commands/update-description.js +2 -2
- package/dist/commands/update-issue.js +105 -0
- package/dist/lib/epic-fields.js +5 -15
- package/dist/lib/field-resolver.js +111 -0
- package/dist/lib/jira-client.js +27 -19
- package/dist/lib/json-mode.js +45 -0
- package/dist/lib/settings.js +1 -0
- package/dist/lib/ui.js +16 -0
- package/dist/lib/validation.js +43 -0
- 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 {
|
|
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 {
|
|
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
|
|
140
|
-
.
|
|
141
|
-
.
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
process.exit(1);
|
|
524
|
+
outputError(error.message, [], 1);
|
|
496
525
|
}
|
|
497
526
|
else if (error instanceof Error) {
|
|
498
|
-
|
|
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
|
-
|
|
507
|
-
process.exit(1);
|
|
530
|
+
outputError(`An unknown error occurred: ${String(error)}`, [], 1);
|
|
508
531
|
}
|
|
509
532
|
}
|
|
510
533
|
}
|
package/dist/commands/about.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|