jira-ai 0.9.98 → 0.9.99

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/dist/cli.js CHANGED
@@ -26,6 +26,9 @@ import { getPersonWorklogCommand } from './commands/get-person-worklog.js';
26
26
  import { isCommandAllowed, getAllowedCommands } from './lib/settings.js';
27
27
  import { confluenceGetPageCommand, confluenceListSpacesCommand, confluenceGetSpacePagesHierarchyCommand, confluenceAddCommentCommand, confluenceCreatePageCommand, confluenceUpdateDescriptionCommand, confluenceSearchCommand } from './commands/confluence.js';
28
28
  import { epicListCommand, epicGetCommand, epicCreateCommand, epicUpdateCommand, epicIssuesCommand, epicLinkCommand, epicUnlinkCommand, epicProgressCommand, } from './commands/epic.js';
29
+ import { boardListCommand, boardGetCommand, boardConfigCommand, boardIssuesCommand, boardRankCommand, } from './commands/board.js';
30
+ import { sprintListCommand, sprintGetCommand, sprintCreateCommand, sprintStartCommand, sprintCompleteCommand, sprintUpdateCommand, sprintDeleteCommand, sprintIssuesCommand, sprintMoveCommand, } from './commands/sprint.js';
31
+ import { backlogMoveCommand } from './commands/backlog.js';
29
32
  import { aboutCommand } from './commands/about.js';
30
33
  import { authCommand } from './commands/auth.js';
31
34
  import { settingsCommand } from './commands/settings.js';
@@ -35,7 +38,7 @@ import { CliError } from './types/errors.js';
35
38
  import { CommandError } from './lib/errors.js';
36
39
  import { ui } from './lib/ui.js';
37
40
  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';
41
+ import { CreateTaskSchema, UpdateDescriptionSchema, ProjectFieldsSchema, ConfluenceAddCommentSchema, RunJqlSchema, GetPersonWorklogSchema, GetIssueStatisticsSchema, EpicListSchema, EpicCreateSchema, EpicUpdateSchema, EpicLinkSchema, EpicMaxSchema, validateOptions, IssueKeySchema, ProjectKeySchema, TimeframeSchema, BoardListSchema, BoardIssuesSchema, BoardRankSchema, SprintListSchema, SprintCreateSchema, SprintUpdateSchema, SprintIssuesSchema, SprintMoveSchema, BacklogMoveSchema, } from './lib/validation.js';
39
42
  import { realpathSync } from 'fs';
40
43
  // Create CLI program
41
44
  const program = new Command();
@@ -459,6 +462,106 @@ epic
459
462
  .action(withPermission('epic.progress', epicProgressCommand, {
460
463
  validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
461
464
  }));
465
+ // =============================================================================
466
+ // BOARD COMMANDS
467
+ // =============================================================================
468
+ const board = program
469
+ .command('board')
470
+ .description('Manage Jira agile boards');
471
+ board
472
+ .command('list')
473
+ .description('List all boards, optionally filtered by project or type.')
474
+ .option('--project <key>', 'Filter by project key')
475
+ .option('--type <type>', 'Filter by board type (scrum, kanban)')
476
+ .action(withPermission('board.list', boardListCommand, { schema: BoardListSchema }));
477
+ board
478
+ .command('get <board-id>')
479
+ .description('Get details of a specific board.')
480
+ .action(withPermission('board.get', (boardId) => boardGetCommand(Number(boardId))));
481
+ board
482
+ .command('config <board-id>')
483
+ .description('Get configuration for a board including columns and filter.')
484
+ .action(withPermission('board.config', (boardId) => boardConfigCommand(Number(boardId))));
485
+ board
486
+ .command('issues <board-id>')
487
+ .description('List issues on a board.')
488
+ .option('--jql <jql>', 'Additional JQL filter')
489
+ .option('--max <n>', 'Maximum results')
490
+ .action(withPermission('board.issues', (boardId, options) => boardIssuesCommand(Number(boardId), { jql: options.jql, max: options.max ? Number(options.max) : undefined }), { schema: BoardIssuesSchema }));
491
+ board
492
+ .command('rank')
493
+ .description('Rank issues on a board before or after another issue.')
494
+ .requiredOption('--issues <keys...>', 'Issue keys to rank')
495
+ .option('--before <key>', 'Rank before this issue')
496
+ .option('--after <key>', 'Rank after this issue')
497
+ .action(withPermission('board.rank', boardRankCommand, { schema: BoardRankSchema }));
498
+ // =============================================================================
499
+ // SPRINT COMMANDS
500
+ // =============================================================================
501
+ const sprint = program
502
+ .command('sprint')
503
+ .description('Manage Jira sprints');
504
+ sprint
505
+ .command('list <board-id>')
506
+ .description('List sprints for a board.')
507
+ .option('--state <state>', 'Filter by state (future, active, closed)')
508
+ .action(withPermission('sprint.list', (boardId, options) => sprintListCommand(Number(boardId), options), { schema: SprintListSchema }));
509
+ sprint
510
+ .command('get <sprint-id>')
511
+ .description('Get details of a specific sprint.')
512
+ .action(withPermission('sprint.get', (sprintId) => sprintGetCommand(Number(sprintId))));
513
+ sprint
514
+ .command('create <board-id>')
515
+ .description('Create a new sprint on a board.')
516
+ .requiredOption('--name <name>', 'Sprint name')
517
+ .option('--goal <goal>', 'Sprint goal')
518
+ .option('--start <date>', 'Start date (ISO format)')
519
+ .option('--end <date>', 'End date (ISO format)')
520
+ .action(withPermission('sprint.create', (boardId, options) => sprintCreateCommand(Number(boardId), options.name, options), { schema: SprintCreateSchema }));
521
+ sprint
522
+ .command('start <sprint-id>')
523
+ .description('Start a sprint (must be in future state).')
524
+ .action(withPermission('sprint.start', (sprintId) => sprintStartCommand(Number(sprintId))));
525
+ sprint
526
+ .command('complete <sprint-id>')
527
+ .description('Complete a sprint (must be in active state).')
528
+ .action(withPermission('sprint.complete', (sprintId) => sprintCompleteCommand(Number(sprintId))));
529
+ sprint
530
+ .command('update <sprint-id>')
531
+ .description('Update sprint name, goal, or dates.')
532
+ .option('--name <name>', 'New sprint name')
533
+ .option('--goal <goal>', 'New sprint goal')
534
+ .option('--start <date>', 'New start date (ISO format)')
535
+ .option('--end <date>', 'New end date (ISO format)')
536
+ .action(withPermission('sprint.update', (sprintId, options) => sprintUpdateCommand(Number(sprintId), options), { schema: SprintUpdateSchema }));
537
+ sprint
538
+ .command('delete <sprint-id>')
539
+ .description('Delete a sprint.')
540
+ .action(withPermission('sprint.delete', (sprintId) => sprintDeleteCommand(Number(sprintId))));
541
+ sprint
542
+ .command('issues <sprint-id>')
543
+ .description('List issues in a sprint.')
544
+ .option('--jql <jql>', 'Additional JQL filter')
545
+ .option('--max <n>', 'Maximum results')
546
+ .action(withPermission('sprint.issues', (sprintId, options) => sprintIssuesCommand(Number(sprintId), { jql: options.jql, max: options.max ? Number(options.max) : undefined }), { schema: SprintIssuesSchema }));
547
+ sprint
548
+ .command('move <sprint-id>')
549
+ .description('Move issues into a sprint.')
550
+ .requiredOption('--issues <keys...>', 'Issue keys to move')
551
+ .option('--before <key>', 'Rank before this issue')
552
+ .option('--after <key>', 'Rank after this issue')
553
+ .action(withPermission('sprint.move', (sprintId, options) => sprintMoveCommand(Number(sprintId), options), { schema: SprintMoveSchema }));
554
+ // =============================================================================
555
+ // BACKLOG COMMANDS
556
+ // =============================================================================
557
+ const backlog = program
558
+ .command('backlog')
559
+ .description('Manage Jira backlog');
560
+ backlog
561
+ .command('move')
562
+ .description('Move issues to the backlog.')
563
+ .requiredOption('--issues <keys...>', 'Issue keys to move to backlog')
564
+ .action(withPermission('board.backlog', backlogMoveCommand, { schema: BacklogMoveSchema }));
462
565
  // About command (always allowed)
463
566
  program
464
567
  .command('about')
@@ -493,6 +596,9 @@ Command Groups (use in allowed-commands):
493
596
  user - me, search, worklog
494
597
  epic - list, get, create, update, issues, link, unlink, progress
495
598
  confl - get, spaces, pages, create, comment, update
599
+ board - list, get, config, issues, rank
600
+ sprint - list, get, create, start, complete, update, delete, issues, move
601
+ backlog - move
496
602
 
497
603
  Examples:
498
604
  - "issue" → allows all issue subcommands
@@ -0,0 +1,21 @@
1
+ import chalk from 'chalk';
2
+ import { moveIssuesToBacklog } from '../lib/agile-client.js';
3
+ import { requirePermission } from '../lib/permissions.js';
4
+ import { CommandError } from '../lib/errors.js';
5
+ import { ui } from '../lib/ui.js';
6
+ // backlog move --issues <keys>
7
+ export async function backlogMoveCommand(options) {
8
+ requirePermission('board.backlog');
9
+ if (!options.issues || options.issues.length === 0) {
10
+ throw new CommandError('At least one issue key is required.', { hints: ['Provide issue keys with --issues'] });
11
+ }
12
+ if (options.issues.length > 50) {
13
+ throw new CommandError('Cannot move more than 50 issues at once.', {
14
+ hints: ['Split your issue list into batches of 50 or fewer'],
15
+ });
16
+ }
17
+ ui.startSpinner(`Moving ${options.issues.length} issue(s) to backlog...`);
18
+ await moveIssuesToBacklog(options.issues);
19
+ ui.succeedSpinner(chalk.green(`Moved ${options.issues.length} issue(s) to backlog.`));
20
+ console.log();
21
+ }
@@ -0,0 +1,120 @@
1
+ import chalk from 'chalk';
2
+ import { getBoards, getBoard, getBoardConfig, getBoardIssues, rankIssues } from '../lib/agile-client.js';
3
+ import { requirePermission } from '../lib/permissions.js';
4
+ import { CommandError } from '../lib/errors.js';
5
+ import { ui } from '../lib/ui.js';
6
+ // board list [--project <key>] [--type <type>]
7
+ export async function boardListCommand(options) {
8
+ requirePermission('board.list');
9
+ ui.startSpinner('Fetching boards...');
10
+ const params = {};
11
+ if (options?.projectKey)
12
+ params.projectKeyOrId = options.projectKey;
13
+ if (options?.type)
14
+ params.type = options.type;
15
+ const result = await getBoards(Object.keys(params).length ? params : undefined);
16
+ ui.stopSpinner();
17
+ if (!result.values || result.values.length === 0) {
18
+ console.log(chalk.yellow('No boards found.'));
19
+ return;
20
+ }
21
+ console.log(chalk.bold(`\nBoards (${result.values.length} total)\n`));
22
+ result.values.forEach(board => {
23
+ const location = board.location?.displayName ? chalk.gray(` — ${board.location.displayName}`) : '';
24
+ console.log(` ${chalk.cyan(String(board.id))} ${board.name}${location} ${chalk.gray(`[${board.type}]`)}`);
25
+ });
26
+ if (!result.isLast) {
27
+ console.log(chalk.gray('\n (More results available — use --max to fetch more)'));
28
+ }
29
+ console.log();
30
+ }
31
+ // board get <board-id>
32
+ export async function boardGetCommand(boardId) {
33
+ requirePermission('board.get');
34
+ if (boardId == null) {
35
+ throw new CommandError('Board ID is required.', { hints: ['Provide a numeric board ID'] });
36
+ }
37
+ ui.startSpinner(`Fetching board ${boardId}...`);
38
+ const board = await getBoard(boardId);
39
+ ui.stopSpinner();
40
+ console.log(chalk.bold(`\nBoard: ${board.name}`) + '\n');
41
+ console.log(` ID: ${chalk.cyan(String(board.id))}`);
42
+ console.log(` Type: ${board.type}`);
43
+ if (board.location?.projectKey)
44
+ console.log(` Project: ${board.location.projectKey}`);
45
+ if (board.location?.displayName)
46
+ console.log(` Location: ${board.location.displayName}`);
47
+ if (board.isPrivate !== undefined)
48
+ console.log(` Private: ${board.isPrivate ? 'Yes' : 'No'}`);
49
+ console.log();
50
+ }
51
+ // board config <board-id>
52
+ export async function boardConfigCommand(boardId) {
53
+ requirePermission('board.config');
54
+ if (boardId == null) {
55
+ throw new CommandError('Board ID is required.', { hints: ['Provide a numeric board ID'] });
56
+ }
57
+ ui.startSpinner(`Fetching board config for ${boardId}...`);
58
+ const config = await getBoardConfig(boardId);
59
+ ui.stopSpinner();
60
+ console.log(chalk.bold(`\nBoard Config: ${config.name}`) + '\n');
61
+ console.log(` ID: ${chalk.cyan(String(config.id))}`);
62
+ if (config.type)
63
+ console.log(` Type: ${config.type}`);
64
+ if (config.filter)
65
+ console.log(` Filter ID: ${config.filter.id}`);
66
+ if (config.ranking)
67
+ console.log(` Rank Field: ${config.ranking.rankCustomFieldId}`);
68
+ if (config.columnConfig?.columns?.length) {
69
+ console.log(`\n ${chalk.bold('Columns:')}`);
70
+ config.columnConfig.columns.forEach(col => {
71
+ const statuses = col.statuses?.map(s => s.id).join(', ') || 'none';
72
+ console.log(` ${chalk.cyan(col.name)} [statuses: ${statuses}]`);
73
+ });
74
+ }
75
+ console.log();
76
+ }
77
+ // board issues <board-id> [--jql <jql>] [--max <n>]
78
+ export async function boardIssuesCommand(boardId, options) {
79
+ requirePermission('board.issues');
80
+ ui.startSpinner(`Fetching issues for board ${boardId}...`);
81
+ const params = {};
82
+ if (options?.jql)
83
+ params.jql = options.jql;
84
+ if (options?.max)
85
+ params.maxResults = options.max;
86
+ const result = await getBoardIssues(boardId, params);
87
+ ui.stopSpinner();
88
+ if (!result.issues || result.issues.length === 0) {
89
+ console.log(chalk.yellow('No issues found on this board.'));
90
+ return;
91
+ }
92
+ console.log(chalk.bold(`\nBoard Issues (${result.total} total)\n`));
93
+ result.issues.forEach(issue => {
94
+ const status = chalk.gray(`[${issue.fields.status.name}]`);
95
+ const assignee = issue.fields.assignee ? chalk.gray(` @${issue.fields.assignee.displayName}`) : '';
96
+ console.log(` ${chalk.cyan(issue.key)} ${issue.fields.summary} ${status}${assignee}`);
97
+ });
98
+ console.log();
99
+ }
100
+ // board rank --issues <keys> --before <key> | --after <key>
101
+ export async function boardRankCommand(options) {
102
+ requirePermission('board.rank');
103
+ if (!options.issues || options.issues.length === 0) {
104
+ throw new CommandError('At least one issue key is required.', { hints: ['Provide issue keys with --issues'] });
105
+ }
106
+ if (!options.before && !options.after) {
107
+ throw new CommandError('Either --before or --after must be specified.', {
108
+ hints: ['Use --before <issue-key> or --after <issue-key>'],
109
+ });
110
+ }
111
+ ui.startSpinner('Ranking issues...');
112
+ const rankOptions = {};
113
+ if (options.before)
114
+ rankOptions.rankBeforeIssue = options.before;
115
+ if (options.after)
116
+ rankOptions.rankAfterIssue = options.after;
117
+ await rankIssues(options.issues, rankOptions);
118
+ ui.succeedSpinner(chalk.green(`Ranked ${options.issues.length} issue(s) successfully.`));
119
+ console.log();
120
+ }
@@ -0,0 +1,187 @@
1
+ import chalk from 'chalk';
2
+ import { getSprints, getSprint, createSprint, startSprint, completeSprint, updateSprint, deleteSprint, getSprintIssues, moveIssuesToSprint, } from '../lib/agile-client.js';
3
+ import { requirePermission } from '../lib/permissions.js';
4
+ import { CommandError } from '../lib/errors.js';
5
+ import { ui } from '../lib/ui.js';
6
+ // sprint list <board-id> [--state <state>]
7
+ export async function sprintListCommand(boardId, options) {
8
+ requirePermission('sprint.list');
9
+ ui.startSpinner(`Fetching sprints for board ${boardId}...`);
10
+ const params = {};
11
+ if (options?.state)
12
+ params.state = options.state;
13
+ const result = await getSprints(boardId, params);
14
+ ui.stopSpinner();
15
+ if (!result.values || result.values.length === 0) {
16
+ console.log(chalk.yellow('No sprints found for this board.'));
17
+ return;
18
+ }
19
+ console.log(chalk.bold(`\nSprints (${result.values.length} total)\n`));
20
+ result.values.forEach(sprint => {
21
+ const stateColor = sprint.state === 'active' ? chalk.green : sprint.state === 'closed' ? chalk.gray : chalk.yellow;
22
+ const dates = sprint.startDate ? ` ${chalk.gray(`${sprint.startDate} → ${sprint.endDate || '?'}`)}` : '';
23
+ console.log(` ${chalk.cyan(String(sprint.id))} ${sprint.name} ${stateColor(`[${sprint.state}]`)}${dates}`);
24
+ });
25
+ console.log();
26
+ }
27
+ // sprint get <sprint-id>
28
+ export async function sprintGetCommand(sprintId) {
29
+ requirePermission('sprint.get');
30
+ if (sprintId == null) {
31
+ throw new CommandError('Sprint ID is required.', { hints: ['Provide a numeric sprint ID'] });
32
+ }
33
+ ui.startSpinner(`Fetching sprint ${sprintId}...`);
34
+ const sprint = await getSprint(sprintId);
35
+ ui.stopSpinner();
36
+ const stateColor = sprint.state === 'active' ? chalk.green : sprint.state === 'closed' ? chalk.gray : chalk.yellow;
37
+ console.log(chalk.bold(`\nSprint: ${sprint.name}`) + '\n');
38
+ console.log(` ID: ${chalk.cyan(String(sprint.id))}`);
39
+ console.log(` State: ${stateColor(sprint.state)}`);
40
+ if (sprint.startDate)
41
+ console.log(` Start: ${sprint.startDate}`);
42
+ if (sprint.endDate)
43
+ console.log(` End: ${sprint.endDate}`);
44
+ if (sprint.goal)
45
+ console.log(` Goal: ${sprint.goal}`);
46
+ if (sprint.originBoardId)
47
+ console.log(` Board: ${sprint.originBoardId}`);
48
+ console.log();
49
+ }
50
+ // sprint create <board-id> --name <name> [--goal <goal>] [--start <date>] [--end <date>]
51
+ export async function sprintCreateCommand(boardId, name, options) {
52
+ requirePermission('sprint.create');
53
+ if (boardId == null) {
54
+ throw new CommandError('Board ID is required.', { hints: ['Provide a numeric board ID'] });
55
+ }
56
+ if (!name) {
57
+ throw new CommandError('Sprint name is required.', { hints: ['Provide a sprint name with --name'] });
58
+ }
59
+ ui.startSpinner(`Creating sprint "${name}"...`);
60
+ const params = {};
61
+ if (options?.goal)
62
+ params.goal = options.goal;
63
+ if (options?.start)
64
+ params.startDate = options.start;
65
+ if (options?.end)
66
+ params.endDate = options.end;
67
+ const sprint = await createSprint(boardId, name, params);
68
+ ui.succeedSpinner(chalk.green(`Sprint created: ${sprint.name} (ID: ${sprint.id})`));
69
+ console.log();
70
+ }
71
+ // sprint start <sprint-id>
72
+ export async function sprintStartCommand(sprintId) {
73
+ requirePermission('sprint.start');
74
+ if (sprintId == null) {
75
+ throw new CommandError('Sprint ID is required.', { hints: ['Provide a numeric sprint ID'] });
76
+ }
77
+ ui.startSpinner(`Validating sprint ${sprintId}...`);
78
+ const sprintToStart = await getSprint(sprintId);
79
+ if (sprintToStart.state !== 'future') {
80
+ ui.failSpinner();
81
+ throw new CommandError(`Cannot start sprint in state '${sprintToStart.state}'. Only 'future' sprints can be started.`, { hints: ['Use sprint list to see available sprints and their states'] });
82
+ }
83
+ if (!sprintToStart.startDate || !sprintToStart.endDate) {
84
+ ui.failSpinner();
85
+ throw new CommandError('Cannot start sprint: start and end dates are required. Use sprint update <id> --start <date> --end <date> first.', { hints: ['Example: jira-ai sprint update <id> --start 2024-01-01 --end 2024-01-14'] });
86
+ }
87
+ ui.startSpinner(`Starting sprint ${sprintId}...`);
88
+ await startSprint(sprintId);
89
+ ui.succeedSpinner(chalk.green(`Sprint ${sprintId} started successfully.`));
90
+ console.log();
91
+ }
92
+ // sprint complete <sprint-id>
93
+ export async function sprintCompleteCommand(sprintId) {
94
+ requirePermission('sprint.complete');
95
+ if (sprintId == null) {
96
+ throw new CommandError('Sprint ID is required.', { hints: ['Provide a numeric sprint ID'] });
97
+ }
98
+ ui.startSpinner(`Validating sprint ${sprintId}...`);
99
+ const sprintToComplete = await getSprint(sprintId);
100
+ if (sprintToComplete.state !== 'active') {
101
+ ui.failSpinner();
102
+ throw new CommandError(`Cannot complete sprint in state '${sprintToComplete.state}'. Only 'active' sprints can be completed.`, { hints: ['Use sprint list to see available sprints and their states'] });
103
+ }
104
+ ui.startSpinner(`Completing sprint ${sprintId}...`);
105
+ await completeSprint(sprintId);
106
+ ui.succeedSpinner(chalk.green(`Sprint ${sprintId} completed successfully.`));
107
+ console.log();
108
+ }
109
+ // sprint update <sprint-id> [--name <name>] [--goal <goal>]
110
+ export async function sprintUpdateCommand(sprintId, options) {
111
+ requirePermission('sprint.update');
112
+ if (sprintId == null) {
113
+ throw new CommandError('Sprint ID is required.', { hints: ['Provide a numeric sprint ID'] });
114
+ }
115
+ if (!options?.name && !options?.goal && !options?.start && !options?.end) {
116
+ throw new CommandError('At least one field is required.', {
117
+ hints: ['Provide one or more of: --name, --goal, --start, --end'],
118
+ });
119
+ }
120
+ ui.startSpinner(`Updating sprint ${sprintId}...`);
121
+ const updates = {};
122
+ if (options?.name)
123
+ updates.name = options.name;
124
+ if (options?.goal)
125
+ updates.goal = options.goal;
126
+ if (options?.start)
127
+ updates.startDate = options.start;
128
+ if (options?.end)
129
+ updates.endDate = options.end;
130
+ await updateSprint(sprintId, updates);
131
+ ui.succeedSpinner(chalk.green(`Sprint ${sprintId} updated successfully.`));
132
+ console.log();
133
+ }
134
+ // sprint delete <sprint-id>
135
+ export async function sprintDeleteCommand(sprintId) {
136
+ requirePermission('sprint.delete');
137
+ if (sprintId == null) {
138
+ throw new CommandError('Sprint ID is required.', { hints: ['Provide a numeric sprint ID'] });
139
+ }
140
+ ui.startSpinner(`Deleting sprint ${sprintId}...`);
141
+ await deleteSprint(sprintId);
142
+ ui.succeedSpinner(chalk.green(`Sprint ${sprintId} deleted successfully.`));
143
+ console.log();
144
+ }
145
+ // sprint issues <sprint-id> [--jql <jql>] [--max <n>]
146
+ export async function sprintIssuesCommand(sprintId, options) {
147
+ requirePermission('sprint.issues');
148
+ ui.startSpinner(`Fetching issues for sprint ${sprintId}...`);
149
+ const params = {};
150
+ if (options?.jql)
151
+ params.jql = options.jql;
152
+ if (options?.max)
153
+ params.maxResults = options.max;
154
+ const result = await getSprintIssues(sprintId, params);
155
+ ui.stopSpinner();
156
+ if (!result.issues || result.issues.length === 0) {
157
+ console.log(chalk.yellow('No issues found in this sprint.'));
158
+ return;
159
+ }
160
+ console.log(chalk.bold(`\nSprint Issues (${result.total} total)\n`));
161
+ result.issues.forEach(issue => {
162
+ const status = chalk.gray(`[${issue.fields.status.name}]`);
163
+ console.log(` ${chalk.cyan(issue.key)} ${issue.fields.summary} ${status}`);
164
+ });
165
+ console.log();
166
+ }
167
+ // sprint move <sprint-id> --issues <keys> [--before <key>] [--after <key>]
168
+ export async function sprintMoveCommand(sprintId, options) {
169
+ requirePermission('sprint.move');
170
+ if (!options.issues || options.issues.length === 0) {
171
+ throw new CommandError('At least one issue key is required.', { hints: ['Provide issue keys with --issues'] });
172
+ }
173
+ if (options.issues.length > 50) {
174
+ throw new CommandError('Cannot move more than 50 issues at once.', {
175
+ hints: ['Split your issue list into batches of 50 or fewer'],
176
+ });
177
+ }
178
+ ui.startSpinner(`Moving ${options.issues.length} issue(s) to sprint ${sprintId}...`);
179
+ const rankOptions = {};
180
+ if (options.before)
181
+ rankOptions.rankBeforeIssue = options.before;
182
+ if (options.after)
183
+ rankOptions.rankAfterIssue = options.after;
184
+ await moveIssuesToSprint(sprintId, options.issues, rankOptions);
185
+ ui.succeedSpinner(chalk.green(`Moved ${options.issues.length} issue(s) to sprint ${sprintId}.`));
186
+ console.log();
187
+ }
@@ -0,0 +1,186 @@
1
+ import { AgileClient } from 'jira.js';
2
+ import { loadCredentials } from './auth-storage.js';
3
+ import { CommandError } from './errors.js';
4
+ import { resolveHost } from './jira-client.js';
5
+ /**
6
+ * Map HTTP status codes from Jira Agile API to user-friendly CommandErrors.
7
+ */
8
+ function mapAgileError(error, context) {
9
+ const status = error?.response?.status ?? error?.status;
10
+ const msg = error?.message || String(error);
11
+ if (status === 404) {
12
+ throw new CommandError(`${context} not found. Check that the ID is correct.`, {
13
+ hints: ['Use the list command to find valid IDs'],
14
+ });
15
+ }
16
+ if (status === 400) {
17
+ const detail = error?.response?.data?.message || error?.response?.data || '';
18
+ throw new CommandError(`${context} — bad request: ${detail || msg}`, {
19
+ hints: ['Check that all required fields are provided and the resource is in the correct state'],
20
+ });
21
+ }
22
+ if (status === 403) {
23
+ throw new CommandError(`${context} — permission denied. You do not have access to perform this action.`, {
24
+ hints: ['Check your Jira permissions for agile operations'],
25
+ });
26
+ }
27
+ throw error;
28
+ }
29
+ let agileClientInstance = null;
30
+ export function __resetAgileClient__() {
31
+ agileClientInstance = null;
32
+ }
33
+ export async function getAgileClient() {
34
+ if (!agileClientInstance) {
35
+ const creds = await loadCredentials();
36
+ if (!creds) {
37
+ throw new CommandError('Jira credentials not found. Please run "jira-ai auth"');
38
+ }
39
+ const host = resolveHost(creds);
40
+ agileClientInstance = new AgileClient({
41
+ host,
42
+ authentication: {
43
+ basic: {
44
+ email: creds.email,
45
+ apiToken: creds.apiToken,
46
+ },
47
+ },
48
+ });
49
+ }
50
+ return agileClientInstance;
51
+ }
52
+ // Board wrappers
53
+ export async function getBoards(options) {
54
+ const client = await getAgileClient();
55
+ return client.board.getAllBoards({ ...options });
56
+ }
57
+ export async function getBoard(boardId) {
58
+ const client = await getAgileClient();
59
+ try {
60
+ return await client.board.getBoard({ boardId });
61
+ }
62
+ catch (error) {
63
+ mapAgileError(error, `Board ${boardId}`);
64
+ }
65
+ }
66
+ export async function getBoardConfig(boardId) {
67
+ const client = await getAgileClient();
68
+ try {
69
+ return await client.board.getConfiguration({ boardId });
70
+ }
71
+ catch (error) {
72
+ mapAgileError(error, `Board config for ${boardId}`);
73
+ }
74
+ }
75
+ export async function getBoardIssues(boardId, options) {
76
+ const client = await getAgileClient();
77
+ try {
78
+ return await client.board.getIssuesForBoard({ boardId, ...options });
79
+ }
80
+ catch (error) {
81
+ mapAgileError(error, `Board issues for ${boardId}`);
82
+ }
83
+ }
84
+ // Sprint wrappers
85
+ export async function getSprints(boardId, options) {
86
+ const client = await getAgileClient();
87
+ try {
88
+ // getAllSprints is on client.board in jira.js v5.2.2, not client.sprint
89
+ return await client.board.getAllSprints({ boardId, ...options });
90
+ }
91
+ catch (error) {
92
+ const msg = error?.message || '';
93
+ if (msg.toLowerCase().includes('board does not support sprints') || msg.toLowerCase().includes('does not support sprints')) {
94
+ throw new CommandError('This board does not support sprints. Sprint operations require a Scrum board.', {
95
+ hints: ['Use a Scrum board for sprint operations', 'Kanban boards do not support sprints'],
96
+ });
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+ export async function getSprint(sprintId) {
102
+ const client = await getAgileClient();
103
+ try {
104
+ return await client.sprint.getSprint({ sprintId });
105
+ }
106
+ catch (error) {
107
+ mapAgileError(error, `Sprint ${sprintId}`);
108
+ }
109
+ }
110
+ export async function createSprint(boardId, name, options) {
111
+ const client = await getAgileClient();
112
+ return client.sprint.createSprint({
113
+ originBoardId: boardId,
114
+ name,
115
+ ...options,
116
+ });
117
+ }
118
+ export async function startSprint(sprintId) {
119
+ const client = await getAgileClient();
120
+ await client.sprint.partiallyUpdateSprint({ sprintId, state: 'active' });
121
+ }
122
+ export async function completeSprint(sprintId) {
123
+ const client = await getAgileClient();
124
+ await client.sprint.partiallyUpdateSprint({ sprintId, state: 'closed' });
125
+ }
126
+ export async function updateSprint(sprintId, updates) {
127
+ const client = await getAgileClient();
128
+ await client.sprint.partiallyUpdateSprint({ sprintId, ...updates });
129
+ }
130
+ export async function deleteSprint(sprintId) {
131
+ const client = await getAgileClient();
132
+ try {
133
+ await client.sprint.deleteSprint({ sprintId });
134
+ }
135
+ catch (error) {
136
+ mapAgileError(error, `Sprint ${sprintId}`);
137
+ }
138
+ }
139
+ export async function getSprintIssues(sprintId, options) {
140
+ const client = await getAgileClient();
141
+ return client.sprint.getIssuesForSprint({ sprintId, ...options });
142
+ }
143
+ export async function moveIssuesToSprint(sprintId, issues, options) {
144
+ if (issues.length > 50) {
145
+ throw new CommandError('Cannot move more than 50 issues at once.', {
146
+ hints: ['Split your issue list into batches of 50 or fewer'],
147
+ });
148
+ }
149
+ const client = await getAgileClient();
150
+ try {
151
+ await client.sprint.moveIssuesToSprintAndRank({ sprintId, issues, ...options });
152
+ }
153
+ catch (error) {
154
+ mapAgileError(error, `Move issues to sprint ${sprintId}`);
155
+ }
156
+ }
157
+ // Backlog wrapper
158
+ export async function moveIssuesToBacklog(issues) {
159
+ if (issues.length > 50) {
160
+ throw new CommandError('Cannot move more than 50 issues at once.', {
161
+ hints: ['Split your issue list into batches of 50 or fewer'],
162
+ });
163
+ }
164
+ const client = await getAgileClient();
165
+ try {
166
+ await client.backlog.moveIssuesToBacklog({ issues });
167
+ }
168
+ catch (error) {
169
+ mapAgileError(error, `Move issues to backlog`);
170
+ }
171
+ }
172
+ // Rank wrapper
173
+ export async function rankIssues(issues, options) {
174
+ if (issues.length > 50) {
175
+ throw new CommandError('Cannot rank more than 50 issues at once.', {
176
+ hints: ['Split your issue list into batches of 50 or fewer'],
177
+ });
178
+ }
179
+ const client = await getAgileClient();
180
+ try {
181
+ await client.issue.rankIssues({ issues, ...options });
182
+ }
183
+ catch (error) {
184
+ mapAgileError(error, `Rank issues`);
185
+ }
186
+ }
@@ -5,7 +5,7 @@ import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAg
5
5
  import { CommandError } from './errors.js';
6
6
  import { getEpicFields, isNextGenProject } from './epic-fields.js';
7
7
  let jiraClient = null;
8
- function resolveHost(creds) {
8
+ export function resolveHost(creds) {
9
9
  if (creds.authType === 'service_account' && creds.cloudId) {
10
10
  return `https://api.atlassian.com/ex/jira/${creds.cloudId}`;
11
11
  }
@@ -0,0 +1,13 @@
1
+ import { isCommandAllowed, getAllowedCommands } from './settings.js';
2
+ import { CommandError } from './errors.js';
3
+ /**
4
+ * Throws a CommandError if the given command is not allowed by current settings.
5
+ * Shared utility used by board, sprint, backlog, and other command modules.
6
+ */
7
+ export function requirePermission(command) {
8
+ if (!isCommandAllowed(command)) {
9
+ throw new CommandError(`Command '${command}' is not allowed.`, {
10
+ hints: [`Allowed commands: ${getAllowedCommands().join(', ')}`, 'Update settings.yaml to enable this command.'],
11
+ });
12
+ }
13
+ }
@@ -39,7 +39,10 @@ export const DEFAULT_ORG_SETTINGS = {
39
39
  'issue',
40
40
  'project',
41
41
  'user',
42
- 'confl'
42
+ 'confl',
43
+ 'board',
44
+ 'sprint',
45
+ 'backlog',
43
46
  ],
44
47
  'allowed-confluence-spaces': ['all']
45
48
  };
@@ -187,3 +187,56 @@ export const EpicLinkSchema = z.object({
187
187
  export const EpicMaxSchema = z.object({
188
188
  max: NumericStringSchema.optional(),
189
189
  });
190
+ // =============================================================================
191
+ // BOARD VALIDATION SCHEMAS
192
+ // =============================================================================
193
+ export const BoardListSchema = z.object({
194
+ projectKey: z.string().trim().optional(),
195
+ type: z.string().trim().optional(),
196
+ });
197
+ export const BoardIssuesSchema = z.object({
198
+ jql: z.string().trim().optional(),
199
+ max: NumericStringSchema.optional(),
200
+ });
201
+ export const BoardRankSchema = z.object({
202
+ issues: z.array(z.string().trim().min(1)).min(1, 'At least one issue key is required'),
203
+ before: z.string().trim().optional(),
204
+ after: z.string().trim().optional(),
205
+ }).refine(data => data.before || data.after, {
206
+ message: 'Either --before or --after must be specified',
207
+ });
208
+ // =============================================================================
209
+ // SPRINT VALIDATION SCHEMAS
210
+ // =============================================================================
211
+ export const SprintListSchema = z.object({
212
+ state: z.enum(['future', 'active', 'closed']).optional(),
213
+ });
214
+ export const SprintCreateSchema = z.object({
215
+ name: z.string().trim().min(1, 'Sprint name is required'),
216
+ goal: z.string().trim().optional(),
217
+ start: z.string().trim().optional(),
218
+ end: z.string().trim().optional(),
219
+ });
220
+ export const SprintUpdateSchema = z.object({
221
+ name: z.string().trim().min(1).optional(),
222
+ goal: z.string().trim().optional(),
223
+ start: z.string().trim().optional(),
224
+ end: z.string().trim().optional(),
225
+ }).refine(data => data.name || data.goal || data.start || data.end, {
226
+ message: 'At least one of --name, --goal, --start, or --end is required',
227
+ });
228
+ export const SprintIssuesSchema = z.object({
229
+ jql: z.string().trim().optional(),
230
+ max: NumericStringSchema.optional(),
231
+ });
232
+ export const SprintMoveSchema = z.object({
233
+ issues: z.array(z.string().trim().min(1)).min(1, 'At least one issue key is required'),
234
+ before: z.string().trim().optional(),
235
+ after: z.string().trim().optional(),
236
+ });
237
+ // =============================================================================
238
+ // BACKLOG VALIDATION SCHEMAS
239
+ // =============================================================================
240
+ export const BacklogMoveSchema = z.object({
241
+ issues: z.array(z.string().trim().min(1)).min(1, 'At least one issue key is required'),
242
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.9.98",
3
+ "version": "0.9.99",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",