jira-ai 0.2.11 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ const list_issue_types_1 = require("./commands/list-issue-types");
16
16
  const run_jql_1 = require("./commands/run-jql");
17
17
  const update_description_1 = require("./commands/update-description");
18
18
  const add_comment_1 = require("./commands/add-comment");
19
+ const create_task_1 = require("./commands/create-task");
19
20
  const about_1 = require("./commands/about");
20
21
  const auth_1 = require("./commands/auth");
21
22
  const settings_1 = require("./lib/settings");
@@ -95,6 +96,15 @@ program
95
96
  .requiredOption('--file-path <path>', 'Path to Markdown file')
96
97
  .requiredOption('--issue-key <key>', 'Jira issue key (e.g., PS-123)')
97
98
  .action(withPermission('add-comment', add_comment_1.addCommentCommand));
99
+ // Create task command
100
+ program
101
+ .command('create-task')
102
+ .description('Create a new Jira issue')
103
+ .requiredOption('--title <title>', 'Issue title/summary')
104
+ .requiredOption('--project <project>', 'Project key (e.g., PROJ)')
105
+ .requiredOption('--issue-type <type>', 'Issue type (e.g., Task, Epic, Subtask)')
106
+ .option('--parent <key>', 'Parent issue key (required for subtasks)')
107
+ .action(withPermission('create-task', create_task_1.createTaskCommand));
98
108
  // About command (always allowed)
99
109
  program
100
110
  .command('about')
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createTaskCommand = createTaskCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const jira_client_1 = require("../lib/jira-client");
10
+ async function createTaskCommand(options) {
11
+ const { title, project, issueType, parent } = options;
12
+ // Validate required fields
13
+ if (!title || title.trim() === '') {
14
+ console.error(chalk_1.default.red('\nError: Title is required (use --title)'));
15
+ process.exit(1);
16
+ }
17
+ if (!project || project.trim() === '') {
18
+ console.error(chalk_1.default.red('\nError: Project is required (use --project)'));
19
+ process.exit(1);
20
+ }
21
+ if (!issueType || issueType.trim() === '') {
22
+ console.error(chalk_1.default.red('\nError: Issue type is required (use --issue-type)'));
23
+ process.exit(1);
24
+ }
25
+ // Create issue with spinner
26
+ const spinner = (0, ora_1.default)(`Creating ${issueType} in project ${project}...`).start();
27
+ try {
28
+ const result = await (0, jira_client_1.createIssue)(project, title, issueType, parent);
29
+ spinner.succeed(chalk_1.default.green(`Issue created successfully: ${result.key}`));
30
+ console.log(chalk_1.default.gray(`\nTitle: ${title}`));
31
+ console.log(chalk_1.default.gray(`Project: ${project}`));
32
+ console.log(chalk_1.default.gray(`Issue Type: ${issueType}`));
33
+ if (parent) {
34
+ console.log(chalk_1.default.gray(`Parent: ${parent}`));
35
+ }
36
+ console.log(chalk_1.default.cyan(`\nIssue Key: ${result.key}`));
37
+ }
38
+ catch (error) {
39
+ spinner.fail(chalk_1.default.red('Failed to create issue'));
40
+ console.error(chalk_1.default.red('\nError: ' + (error instanceof Error ? error.message : 'Unknown error')));
41
+ // Provide helpful hints based on error
42
+ if (error instanceof Error) {
43
+ if (error.message.includes('project') || error.message.includes('Project')) {
44
+ console.log(chalk_1.default.yellow('\nHint: Check that the project key is correct'));
45
+ console.log(chalk_1.default.yellow('Use "jira-ai projects" to see available projects'));
46
+ }
47
+ else if (error.message.includes('issue type') || error.message.includes('issuetype')) {
48
+ console.log(chalk_1.default.yellow('\nHint: Check that the issue type is correct'));
49
+ console.log(chalk_1.default.yellow(`Use "jira-ai list-issue-types ${project}" to see available issue types`));
50
+ }
51
+ else if (error.message.includes('parent') || error.message.includes('Parent')) {
52
+ console.log(chalk_1.default.yellow('\nHint: Check that the parent issue key is correct'));
53
+ console.log(chalk_1.default.yellow('Parent issues are required for subtasks'));
54
+ }
55
+ else if (error.message.includes('403')) {
56
+ console.log(chalk_1.default.yellow('\nHint: You may not have permission to create issues in this project'));
57
+ }
58
+ }
59
+ process.exit(1);
60
+ }
61
+ }
@@ -10,6 +10,7 @@ exports.searchIssuesByJql = searchIssuesByJql;
10
10
  exports.updateIssueDescription = updateIssueDescription;
11
11
  exports.addIssueComment = addIssueComment;
12
12
  exports.getProjectIssueTypes = getProjectIssueTypes;
13
+ exports.createIssue = createIssue;
13
14
  const jira_js_1 = require("jira.js");
14
15
  const utils_1 = require("./utils");
15
16
  const auth_storage_1 = require("./auth-storage");
@@ -270,3 +271,35 @@ async function getProjectIssueTypes(projectIdOrKey) {
270
271
  hierarchyLevel: issueType.hierarchyLevel || 0,
271
272
  })) || [];
272
273
  }
274
+ /**
275
+ * Create a new issue
276
+ * @param projectKey - The project key (e.g., "PROJ")
277
+ * @param summary - The issue title/summary
278
+ * @param issueTypeName - The issue type name (e.g., "Task", "Epic", "Subtask")
279
+ * @param parentKey - Optional parent issue key for subtasks
280
+ */
281
+ async function createIssue(projectKey, summary, issueTypeName, parentKey) {
282
+ const client = getJiraClient();
283
+ const fields = {
284
+ project: {
285
+ key: projectKey,
286
+ },
287
+ summary,
288
+ issuetype: {
289
+ name: issueTypeName,
290
+ },
291
+ };
292
+ // Add parent field if this is a subtask
293
+ if (parentKey) {
294
+ fields.parent = {
295
+ key: parentKey,
296
+ };
297
+ }
298
+ const response = await client.issues.createIssue({
299
+ fields,
300
+ });
301
+ return {
302
+ key: response.key || '',
303
+ id: response.id || '',
304
+ };
305
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.2.11",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for interacting with Atlassian Jira",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
package/settings.yaml CHANGED
@@ -11,7 +11,7 @@ projects:
11
11
  # - all
12
12
 
13
13
  # Commands: List of allowed commands (use "all" to allow all commands)
14
- # Available commands: me, projects, task-with-details, project-statuses, list-issue-types, run-jql, update-description, add-comment, about
14
+ # Available commands: me, projects, task-with-details, project-statuses, list-issue-types, run-jql, update-description, add-comment, create-task, about
15
15
  commands:
16
16
  - me
17
17
  - projects
@@ -23,3 +23,4 @@ commands:
23
23
  - list-issue-types
24
24
  - update-description
25
25
  - add-comment
26
+ - create-task
package/src/cli.ts CHANGED
@@ -12,6 +12,7 @@ import { listIssueTypesCommand } from './commands/list-issue-types';
12
12
  import { runJqlCommand } from './commands/run-jql';
13
13
  import { updateDescriptionCommand } from './commands/update-description';
14
14
  import { addCommentCommand } from './commands/add-comment';
15
+ import { createTaskCommand } from './commands/create-task';
15
16
  import { aboutCommand } from './commands/about';
16
17
  import { authCommand } from './commands/auth';
17
18
  import { isCommandAllowed, getAllowedCommands } from './lib/settings';
@@ -107,6 +108,16 @@ program
107
108
  .requiredOption('--issue-key <key>', 'Jira issue key (e.g., PS-123)')
108
109
  .action(withPermission('add-comment', addCommentCommand));
109
110
 
111
+ // Create task command
112
+ program
113
+ .command('create-task')
114
+ .description('Create a new Jira issue')
115
+ .requiredOption('--title <title>', 'Issue title/summary')
116
+ .requiredOption('--project <project>', 'Project key (e.g., PROJ)')
117
+ .requiredOption('--issue-type <type>', 'Issue type (e.g., Task, Epic, Subtask)')
118
+ .option('--parent <key>', 'Parent issue key (required for subtasks)')
119
+ .action(withPermission('create-task', createTaskCommand));
120
+
110
121
  // About command (always allowed)
111
122
  program
112
123
  .command('about')
@@ -0,0 +1,73 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createIssue } from '../lib/jira-client';
4
+
5
+ export async function createTaskCommand(
6
+ options: {
7
+ title: string;
8
+ project: string;
9
+ issueType: string;
10
+ parent?: string;
11
+ }
12
+ ): Promise<void> {
13
+ const { title, project, issueType, parent } = options;
14
+
15
+ // Validate required fields
16
+ if (!title || title.trim() === '') {
17
+ console.error(chalk.red('\nError: Title is required (use --title)'));
18
+ process.exit(1);
19
+ }
20
+
21
+ if (!project || project.trim() === '') {
22
+ console.error(chalk.red('\nError: Project is required (use --project)'));
23
+ process.exit(1);
24
+ }
25
+
26
+ if (!issueType || issueType.trim() === '') {
27
+ console.error(chalk.red('\nError: Issue type is required (use --issue-type)'));
28
+ process.exit(1);
29
+ }
30
+
31
+ // Create issue with spinner
32
+ const spinner = ora(`Creating ${issueType} in project ${project}...`).start();
33
+
34
+ try {
35
+ const result = await createIssue(project, title, issueType, parent);
36
+
37
+ spinner.succeed(chalk.green(`Issue created successfully: ${result.key}`));
38
+ console.log(chalk.gray(`\nTitle: ${title}`));
39
+ console.log(chalk.gray(`Project: ${project}`));
40
+ console.log(chalk.gray(`Issue Type: ${issueType}`));
41
+ if (parent) {
42
+ console.log(chalk.gray(`Parent: ${parent}`));
43
+ }
44
+ console.log(chalk.cyan(`\nIssue Key: ${result.key}`));
45
+ } catch (error) {
46
+ spinner.fail(chalk.red('Failed to create issue'));
47
+ console.error(
48
+ chalk.red(
49
+ '\nError: ' + (error instanceof Error ? error.message : 'Unknown error')
50
+ )
51
+ );
52
+
53
+ // Provide helpful hints based on error
54
+ if (error instanceof Error) {
55
+ if (error.message.includes('project') || error.message.includes('Project')) {
56
+ console.log(chalk.yellow('\nHint: Check that the project key is correct'));
57
+ console.log(chalk.yellow('Use "jira-ai projects" to see available projects'));
58
+ } else if (error.message.includes('issue type') || error.message.includes('issuetype')) {
59
+ console.log(chalk.yellow('\nHint: Check that the issue type is correct'));
60
+ console.log(chalk.yellow(`Use "jira-ai list-issue-types ${project}" to see available issue types`));
61
+ } else if (error.message.includes('parent') || error.message.includes('Parent')) {
62
+ console.log(chalk.yellow('\nHint: Check that the parent issue key is correct'));
63
+ console.log(chalk.yellow('Parent issues are required for subtasks'));
64
+ } else if (error.message.includes('403')) {
65
+ console.log(
66
+ chalk.yellow('\nHint: You may not have permission to create issues in this project')
67
+ );
68
+ }
69
+ }
70
+
71
+ process.exit(1);
72
+ }
73
+ }
@@ -383,3 +383,45 @@ export async function getProjectIssueTypes(projectIdOrKey: string): Promise<Issu
383
383
  hierarchyLevel: issueType.hierarchyLevel || 0,
384
384
  })) || [];
385
385
  }
386
+
387
+ /**
388
+ * Create a new issue
389
+ * @param projectKey - The project key (e.g., "PROJ")
390
+ * @param summary - The issue title/summary
391
+ * @param issueTypeName - The issue type name (e.g., "Task", "Epic", "Subtask")
392
+ * @param parentKey - Optional parent issue key for subtasks
393
+ */
394
+ export async function createIssue(
395
+ projectKey: string,
396
+ summary: string,
397
+ issueTypeName: string,
398
+ parentKey?: string
399
+ ): Promise<{ key: string; id: string }> {
400
+ const client = getJiraClient();
401
+
402
+ const fields: any = {
403
+ project: {
404
+ key: projectKey,
405
+ },
406
+ summary,
407
+ issuetype: {
408
+ name: issueTypeName,
409
+ },
410
+ };
411
+
412
+ // Add parent field if this is a subtask
413
+ if (parentKey) {
414
+ fields.parent = {
415
+ key: parentKey,
416
+ };
417
+ }
418
+
419
+ const response = await client.issues.createIssue({
420
+ fields,
421
+ });
422
+
423
+ return {
424
+ key: response.key || '',
425
+ id: response.id || '',
426
+ };
427
+ }
@@ -0,0 +1,199 @@
1
+ import { createTaskCommand } from '../src/commands/create-task';
2
+ import * as jiraClient from '../src/lib/jira-client';
3
+
4
+ // Mock dependencies
5
+ jest.mock('../src/lib/jira-client');
6
+ jest.mock('../src/lib/utils');
7
+ jest.mock('ora', () => {
8
+ return jest.fn(() => ({
9
+ start: jest.fn().mockReturnThis(),
10
+ succeed: jest.fn().mockReturnThis(),
11
+ fail: jest.fn().mockReturnThis()
12
+ }));
13
+ });
14
+
15
+ const mockJiraClient = jiraClient as jest.Mocked<typeof jiraClient>;
16
+
17
+ describe('Create Task Command', () => {
18
+ const mockOptions = {
19
+ title: 'Test Task Title',
20
+ project: 'TEST',
21
+ issueType: 'Task',
22
+ };
23
+
24
+ const mockResponse = {
25
+ key: 'TEST-123',
26
+ id: '10001',
27
+ };
28
+
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ console.log = jest.fn();
32
+ console.error = jest.fn();
33
+
34
+ // Setup default mock
35
+ mockJiraClient.createIssue = jest.fn().mockResolvedValue(mockResponse);
36
+ });
37
+
38
+ it('should successfully create a task', async () => {
39
+ await createTaskCommand(mockOptions);
40
+
41
+ expect(mockJiraClient.createIssue).toHaveBeenCalledWith(
42
+ 'TEST',
43
+ 'Test Task Title',
44
+ 'Task',
45
+ undefined
46
+ );
47
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('TEST-123'));
48
+ });
49
+
50
+ it('should create a task with parent issue', async () => {
51
+ const optionsWithParent = {
52
+ ...mockOptions,
53
+ parent: 'TEST-100',
54
+ };
55
+
56
+ await createTaskCommand(optionsWithParent);
57
+
58
+ expect(mockJiraClient.createIssue).toHaveBeenCalledWith(
59
+ 'TEST',
60
+ 'Test Task Title',
61
+ 'Task',
62
+ 'TEST-100'
63
+ );
64
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Parent: TEST-100'));
65
+ });
66
+
67
+ it('should exit with error when title is empty', async () => {
68
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
69
+ throw new Error('Process exit');
70
+ });
71
+
72
+ await expect(
73
+ createTaskCommand({ ...mockOptions, title: '' })
74
+ ).rejects.toThrow('Process exit');
75
+
76
+ expect(console.error).toHaveBeenCalledWith(
77
+ expect.stringContaining('Title is required')
78
+ );
79
+ expect(processExitSpy).toHaveBeenCalledWith(1);
80
+
81
+ processExitSpy.mockRestore();
82
+ });
83
+
84
+ it('should exit with error when project is empty', async () => {
85
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
86
+ throw new Error('Process exit');
87
+ });
88
+
89
+ await expect(
90
+ createTaskCommand({ ...mockOptions, project: '' })
91
+ ).rejects.toThrow('Process exit');
92
+
93
+ expect(console.error).toHaveBeenCalledWith(
94
+ expect.stringContaining('Project is required')
95
+ );
96
+ expect(processExitSpy).toHaveBeenCalledWith(1);
97
+
98
+ processExitSpy.mockRestore();
99
+ });
100
+
101
+ it('should exit with error when issue type is empty', async () => {
102
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
103
+ throw new Error('Process exit');
104
+ });
105
+
106
+ await expect(
107
+ createTaskCommand({ ...mockOptions, issueType: '' })
108
+ ).rejects.toThrow('Process exit');
109
+
110
+ expect(console.error).toHaveBeenCalledWith(
111
+ expect.stringContaining('Issue type is required')
112
+ );
113
+ expect(processExitSpy).toHaveBeenCalledWith(1);
114
+
115
+ processExitSpy.mockRestore();
116
+ });
117
+
118
+ it('should exit with error and hint when project not found', async () => {
119
+ const apiError = new Error('Project does not exist');
120
+ mockJiraClient.createIssue = jest.fn().mockRejectedValue(apiError);
121
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
122
+ throw new Error('Process exit');
123
+ });
124
+
125
+ await expect(createTaskCommand(mockOptions)).rejects.toThrow('Process exit');
126
+
127
+ expect(console.error).toHaveBeenCalledWith(
128
+ expect.stringContaining('Project does not exist')
129
+ );
130
+ expect(console.log).toHaveBeenCalledWith(
131
+ expect.stringContaining('Check that the project key is correct')
132
+ );
133
+ expect(processExitSpy).toHaveBeenCalledWith(1);
134
+
135
+ processExitSpy.mockRestore();
136
+ });
137
+
138
+ it('should exit with error and hint when issue type is invalid', async () => {
139
+ const apiError = new Error('Invalid issue type specified');
140
+ mockJiraClient.createIssue = jest.fn().mockRejectedValue(apiError);
141
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
142
+ throw new Error('Process exit');
143
+ });
144
+
145
+ await expect(createTaskCommand(mockOptions)).rejects.toThrow('Process exit');
146
+
147
+ expect(console.error).toHaveBeenCalledWith(
148
+ expect.stringContaining('Invalid issue type specified')
149
+ );
150
+ expect(console.log).toHaveBeenCalledWith(
151
+ expect.stringContaining('Check that the issue type is correct')
152
+ );
153
+ expect(processExitSpy).toHaveBeenCalledWith(1);
154
+
155
+ processExitSpy.mockRestore();
156
+ });
157
+
158
+ it('should exit with error and hint when parent issue is invalid', async () => {
159
+ const apiError = new Error('Parent issue not found');
160
+ mockJiraClient.createIssue = jest.fn().mockRejectedValue(apiError);
161
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
162
+ throw new Error('Process exit');
163
+ });
164
+
165
+ await expect(
166
+ createTaskCommand({ ...mockOptions, parent: 'TEST-999' })
167
+ ).rejects.toThrow('Process exit');
168
+
169
+ expect(console.error).toHaveBeenCalledWith(
170
+ expect.stringContaining('Parent issue not found')
171
+ );
172
+ expect(console.log).toHaveBeenCalledWith(
173
+ expect.stringContaining('Check that the parent issue key is correct')
174
+ );
175
+ expect(processExitSpy).toHaveBeenCalledWith(1);
176
+
177
+ processExitSpy.mockRestore();
178
+ });
179
+
180
+ it('should exit with error and hint when permission denied (403)', async () => {
181
+ const apiError = new Error('Permission denied (403)');
182
+ mockJiraClient.createIssue = jest.fn().mockRejectedValue(apiError);
183
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
184
+ throw new Error('Process exit');
185
+ });
186
+
187
+ await expect(createTaskCommand(mockOptions)).rejects.toThrow('Process exit');
188
+
189
+ expect(console.error).toHaveBeenCalledWith(
190
+ expect.stringContaining('Permission denied (403)')
191
+ );
192
+ expect(console.log).toHaveBeenCalledWith(
193
+ expect.stringContaining('You may not have permission to create issues')
194
+ );
195
+ expect(processExitSpy).toHaveBeenCalledWith(1);
196
+
197
+ processExitSpy.mockRestore();
198
+ });
199
+ });