jira-ai 0.1.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.
Files changed (45) hide show
  1. package/README.md +364 -0
  2. package/dist/cli.js +87 -0
  3. package/dist/commands/about.js +83 -0
  4. package/dist/commands/add-comment.js +109 -0
  5. package/dist/commands/me.js +23 -0
  6. package/dist/commands/project-statuses.js +23 -0
  7. package/dist/commands/projects.js +35 -0
  8. package/dist/commands/run-jql.js +44 -0
  9. package/dist/commands/task-with-details.js +23 -0
  10. package/dist/commands/update-description.js +109 -0
  11. package/dist/lib/formatters.js +193 -0
  12. package/dist/lib/jira-client.js +216 -0
  13. package/dist/lib/settings.js +68 -0
  14. package/dist/lib/utils.js +79 -0
  15. package/jest.config.js +21 -0
  16. package/package.json +47 -0
  17. package/settings.yaml +24 -0
  18. package/src/cli.ts +97 -0
  19. package/src/commands/about.ts +98 -0
  20. package/src/commands/add-comment.ts +94 -0
  21. package/src/commands/me.ts +18 -0
  22. package/src/commands/project-statuses.ts +18 -0
  23. package/src/commands/projects.ts +32 -0
  24. package/src/commands/run-jql.ts +40 -0
  25. package/src/commands/task-with-details.ts +18 -0
  26. package/src/commands/update-description.ts +94 -0
  27. package/src/lib/formatters.ts +224 -0
  28. package/src/lib/jira-client.ts +319 -0
  29. package/src/lib/settings.ts +77 -0
  30. package/src/lib/utils.ts +76 -0
  31. package/src/types/md-to-adf.d.ts +14 -0
  32. package/tests/README.md +97 -0
  33. package/tests/__mocks__/jira.js.ts +4 -0
  34. package/tests/__mocks__/md-to-adf.ts +7 -0
  35. package/tests/__mocks__/mdast-util-from-adf.ts +4 -0
  36. package/tests/__mocks__/mdast-util-to-markdown.ts +1 -0
  37. package/tests/add-comment.test.ts +226 -0
  38. package/tests/cli-permissions.test.ts +156 -0
  39. package/tests/jira-client.test.ts +123 -0
  40. package/tests/projects.test.ts +205 -0
  41. package/tests/settings.test.ts +288 -0
  42. package/tests/task-with-details.test.ts +83 -0
  43. package/tests/update-description.test.ts +262 -0
  44. package/to-do.md +9 -0
  45. package/tsconfig.json +18 -0
@@ -0,0 +1,68 @@
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.loadSettings = loadSettings;
7
+ exports.isProjectAllowed = isProjectAllowed;
8
+ exports.isCommandAllowed = isCommandAllowed;
9
+ exports.getAllowedProjects = getAllowedProjects;
10
+ exports.getAllowedCommands = getAllowedCommands;
11
+ exports.__resetCache__ = __resetCache__;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const js_yaml_1 = __importDefault(require("js-yaml"));
15
+ let cachedSettings = null;
16
+ function loadSettings() {
17
+ if (cachedSettings) {
18
+ return cachedSettings;
19
+ }
20
+ const settingsPath = path_1.default.join(process.cwd(), 'settings.yaml');
21
+ if (!fs_1.default.existsSync(settingsPath)) {
22
+ console.warn('Warning: settings.yaml not found. Using default settings (all allowed).');
23
+ cachedSettings = {
24
+ projects: ['all'],
25
+ commands: ['all']
26
+ };
27
+ return cachedSettings;
28
+ }
29
+ try {
30
+ const fileContents = fs_1.default.readFileSync(settingsPath, 'utf8');
31
+ const settings = js_yaml_1.default.load(fileContents);
32
+ cachedSettings = {
33
+ projects: settings.projects || ['all'],
34
+ commands: settings.commands || ['all']
35
+ };
36
+ return cachedSettings;
37
+ }
38
+ catch (error) {
39
+ console.error('Error loading settings.yaml:', error);
40
+ process.exit(1);
41
+ }
42
+ }
43
+ function isProjectAllowed(projectKey) {
44
+ const settings = loadSettings();
45
+ if (settings.projects.includes('all')) {
46
+ return true;
47
+ }
48
+ return settings.projects.includes(projectKey);
49
+ }
50
+ function isCommandAllowed(commandName) {
51
+ const settings = loadSettings();
52
+ if (settings.commands.includes('all')) {
53
+ return true;
54
+ }
55
+ return settings.commands.includes(commandName);
56
+ }
57
+ function getAllowedProjects() {
58
+ const settings = loadSettings();
59
+ return settings.projects;
60
+ }
61
+ function getAllowedCommands() {
62
+ const settings = loadSettings();
63
+ return settings.commands;
64
+ }
65
+ // For testing purposes only
66
+ function __resetCache__() {
67
+ cachedSettings = null;
68
+ }
@@ -0,0 +1,79 @@
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.validateEnvVars = validateEnvVars;
7
+ exports.formatTimestamp = formatTimestamp;
8
+ exports.truncate = truncate;
9
+ exports.convertADFToMarkdown = convertADFToMarkdown;
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const mdast_util_from_adf_1 = require("mdast-util-from-adf");
12
+ const mdast_util_to_markdown_1 = require("mdast-util-to-markdown");
13
+ /**
14
+ * Validate required environment variables
15
+ */
16
+ function validateEnvVars() {
17
+ const required = [
18
+ 'JIRA_HOST',
19
+ 'JIRA_USER_EMAIL',
20
+ 'JIRA_API_TOKEN',
21
+ ];
22
+ const missing = required.filter((key) => !process.env[key]);
23
+ if (missing.length > 0) {
24
+ console.error(chalk_1.default.red('✗ Missing required environment variables:\n'));
25
+ missing.forEach((key) => console.error(chalk_1.default.red(` - ${key}`)));
26
+ console.log('\nPlease create a .env file with the following variables:');
27
+ console.log(' JIRA_HOST=https://your-domain.atlassian.net');
28
+ console.log(' JIRA_USER_EMAIL=your-email@example.com');
29
+ console.log(' JIRA_API_TOKEN=your-api-token');
30
+ console.log('\nGet your API token from: https://id.atlassian.com/manage-profile/security/api-tokens');
31
+ process.exit(1);
32
+ }
33
+ }
34
+ /**
35
+ * Format timestamp for display
36
+ */
37
+ function formatTimestamp(date) {
38
+ const d = typeof date === 'string' ? new Date(date) : date;
39
+ return d.toLocaleString('en-US', {
40
+ year: 'numeric',
41
+ month: 'short',
42
+ day: 'numeric',
43
+ hour: '2-digit',
44
+ minute: '2-digit',
45
+ });
46
+ }
47
+ /**
48
+ * Truncate text to max length with ellipsis
49
+ */
50
+ function truncate(text, maxLength) {
51
+ if (text.length <= maxLength)
52
+ return text;
53
+ return text.substring(0, maxLength - 3) + '...';
54
+ }
55
+ /**
56
+ * Convert Atlassian Document Format (ADF) to Markdown
57
+ * Handles both string content and ADF JSON objects
58
+ */
59
+ function convertADFToMarkdown(content) {
60
+ // If content is already a string, return it as-is
61
+ if (typeof content === 'string') {
62
+ return content;
63
+ }
64
+ // If content is null or undefined, return empty string
65
+ if (!content) {
66
+ return '';
67
+ }
68
+ try {
69
+ // Convert ADF to mdast, then mdast to markdown
70
+ const mdastTree = (0, mdast_util_from_adf_1.fromADF)(content);
71
+ const markdown = (0, mdast_util_to_markdown_1.toMarkdown)(mdastTree);
72
+ return markdown.trim();
73
+ }
74
+ catch (error) {
75
+ // If conversion fails, fall back to JSON string representation
76
+ console.error('Failed to convert ADF to Markdown:', error);
77
+ return JSON.stringify(content, null, 2);
78
+ }
79
+ }
package/jest.config.js ADDED
@@ -0,0 +1,21 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src', '<rootDir>/tests'],
5
+ testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
6
+ collectCoverageFrom: [
7
+ 'src/**/*.ts',
8
+ '!src/**/*.d.ts',
9
+ '!src/cli.ts'
10
+ ],
11
+ coverageDirectory: 'coverage',
12
+ coverageReporters: ['text', 'lcov', 'html'],
13
+ moduleFileExtensions: ['ts', 'js', 'json'],
14
+ verbose: true,
15
+ moduleNameMapper: {
16
+ '^jira\\.js$': '<rootDir>/tests/__mocks__/jira.js.ts',
17
+ '^mdast-util-from-adf$': '<rootDir>/tests/__mocks__/mdast-util-from-adf.ts',
18
+ '^mdast-util-to-markdown$': '<rootDir>/tests/__mocks__/mdast-util-to-markdown.ts',
19
+ '^md-to-adf$': '<rootDir>/tests/__mocks__/md-to-adf.ts'
20
+ }
21
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "jira-ai",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for interacting with Atlassian Jira",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "jira-ai": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "ts-node src/cli.ts",
11
+ "build": "tsc && chmod +x dist/cli.js",
12
+ "start": "node dist/cli.js",
13
+ "link": "npm run build && npm link",
14
+ "test": "jest",
15
+ "test:watch": "jest --watch",
16
+ "test:coverage": "jest --coverage",
17
+ "test:verbose": "jest --verbose"
18
+ },
19
+ "keywords": [
20
+ "jira",
21
+ "cli",
22
+ "atlassian"
23
+ ],
24
+ "author": "",
25
+ "license": "ISC",
26
+ "dependencies": {
27
+ "chalk": "^4.1.2",
28
+ "cli-table3": "^0.6.5",
29
+ "commander": "^11.0.0",
30
+ "dotenv": "^17.2.3",
31
+ "jira.js": "^5.2.2",
32
+ "js-yaml": "^4.1.1",
33
+ "md-to-adf": "^0.6.4",
34
+ "mdast-util-from-adf": "^2.2.0",
35
+ "mdast-util-to-markdown": "^2.1.2",
36
+ "ora": "^5.4.1"
37
+ },
38
+ "devDependencies": {
39
+ "@types/jest": "^30.0.0",
40
+ "@types/js-yaml": "^4.0.9",
41
+ "@types/node": "^25.0.3",
42
+ "jest": "^30.2.0",
43
+ "ts-jest": "^29.4.6",
44
+ "ts-node": "^10.9.2",
45
+ "typescript": "^5.9.3"
46
+ }
47
+ }
package/settings.yaml ADDED
@@ -0,0 +1,24 @@
1
+ # Jira AI Settings
2
+
3
+ # Projects: List of allowed projects (use "all" to allow all projects)
4
+ # Examples: BP, PM, PS
5
+ projects:
6
+ - all
7
+ #- BP
8
+ #- PM
9
+ #- PS
10
+ # Uncomment below to allow all projects
11
+ # - all
12
+
13
+ # Commands: List of allowed commands (use "all" to allow all commands)
14
+ # Available commands: me, projects, task-with-details, project-statuses, run-jql, update-description, about
15
+ commands:
16
+ - me
17
+ - projects
18
+ - run-jql
19
+ # Uncomment below to allow all commands
20
+ # - all
21
+ - task-with-details
22
+ # - project-statuses
23
+ - update-description
24
+ - add-comment
package/src/cli.ts ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import dotenv from 'dotenv';
5
+ import chalk from 'chalk';
6
+ import { validateEnvVars } from './lib/utils';
7
+ import { meCommand } from './commands/me';
8
+ import { projectsCommand } from './commands/projects';
9
+ import { taskWithDetailsCommand } from './commands/task-with-details';
10
+ import { projectStatusesCommand } from './commands/project-statuses';
11
+ import { runJqlCommand } from './commands/run-jql';
12
+ import { updateDescriptionCommand } from './commands/update-description';
13
+ import { addCommentCommand } from './commands/add-comment';
14
+ import { aboutCommand } from './commands/about';
15
+ import { isCommandAllowed, getAllowedCommands } from './lib/settings';
16
+
17
+ // Load environment variables
18
+ dotenv.config();
19
+
20
+ // Validate environment variables
21
+ validateEnvVars();
22
+
23
+ // Helper function to wrap commands with permission check
24
+ function withPermission(commandName: string, commandFn: (...args: any[]) => Promise<void>) {
25
+ return async (...args: any[]) => {
26
+ if (!isCommandAllowed(commandName)) {
27
+ console.error(chalk.red(`\n❌ Command '${commandName}' is not allowed.`));
28
+ console.log(chalk.gray('Allowed commands: ' + getAllowedCommands().join(', ')));
29
+ console.log(chalk.gray('Update settings.yaml to enable this command.\n'));
30
+ process.exit(1);
31
+ }
32
+ return commandFn(...args);
33
+ };
34
+ }
35
+
36
+ // Create CLI program
37
+ const program = new Command();
38
+
39
+ program
40
+ .name('jira-ai')
41
+ .description('CLI tool for interacting with Atlassian Jira')
42
+ .version('1.0.0');
43
+
44
+ // Me command
45
+ program
46
+ .command('me')
47
+ .description('Show basic user information')
48
+ .action(withPermission('me', meCommand));
49
+
50
+ // Projects command
51
+ program
52
+ .command('projects')
53
+ .description('Show list of projects')
54
+ .action(withPermission('projects', projectsCommand));
55
+
56
+ // Task with details command
57
+ program
58
+ .command('task-with-details <task-id>')
59
+ .description('Show task title, body, and comments')
60
+ .action(withPermission('task-with-details', taskWithDetailsCommand));
61
+
62
+ // Project statuses command
63
+ program
64
+ .command('project-statuses <project-id>')
65
+ .description('Show all possible statuses for a project')
66
+ .action(withPermission('project-statuses', projectStatusesCommand));
67
+
68
+ // Run JQL command
69
+ program
70
+ .command('run-jql <jql-query>')
71
+ .description('Execute JQL query and display results')
72
+ .option('-l, --limit <number>', 'Maximum number of results (default: 50)', '50')
73
+ .action(withPermission('run-jql', runJqlCommand));
74
+
75
+ // Update description command
76
+ program
77
+ .command('update-description <task-id>')
78
+ .description('Update task description from a Markdown file')
79
+ .requiredOption('--from-file <path>', 'Path to Markdown file')
80
+ .action(withPermission('update-description', updateDescriptionCommand));
81
+
82
+ // Add comment command
83
+ program
84
+ .command('add-comment')
85
+ .description('Add a comment to a Jira issue from a Markdown file')
86
+ .requiredOption('--file-path <path>', 'Path to Markdown file')
87
+ .requiredOption('--issue-key <key>', 'Jira issue key (e.g., PS-123)')
88
+ .action(withPermission('add-comment', addCommentCommand));
89
+
90
+ // About command (always allowed)
91
+ program
92
+ .command('about')
93
+ .description('Show information about available commands')
94
+ .action(aboutCommand);
95
+
96
+ // Parse command line arguments
97
+ program.parse();
@@ -0,0 +1,98 @@
1
+ import chalk from 'chalk';
2
+ import { getAllowedCommands, getAllowedProjects, isCommandAllowed } from '../lib/settings';
3
+
4
+ interface CommandInfo {
5
+ name: string;
6
+ description: string;
7
+ usage: string;
8
+ }
9
+
10
+ const ALL_COMMANDS: CommandInfo[] = [
11
+ {
12
+ name: 'me',
13
+ description: 'Show details of current user',
14
+ usage: 'jira-ai me --help'
15
+ },
16
+ {
17
+ name: 'projects',
18
+ description: 'List available projects',
19
+ usage: 'jira-ai projects --help'
20
+ },
21
+ {
22
+ name: 'task-with-details',
23
+ description: 'Show task title, body, and comments',
24
+ usage: 'jira-ai task-with-details --help'
25
+ },
26
+ {
27
+ name: 'project-statuses',
28
+ description: 'Show all possible statuses for a project',
29
+ usage: 'jira-ai project-statuses --help'
30
+ },
31
+ {
32
+ name: 'run-jql',
33
+ description: 'Execute JQL query and display results',
34
+ usage: 'jira-ai run-jql "<jql-query>" [-l <limit>]'
35
+ },
36
+ {
37
+ name: 'update-description',
38
+ description: 'Update task description from a Markdown file',
39
+ usage: 'jira-ai update-description <task-id> --from-file <path>'
40
+ },
41
+ {
42
+ name: 'add-comment',
43
+ description: 'Add a comment to a Jira issue from a Markdown file',
44
+ usage: 'jira-ai add-comment --file-path <path> --issue-key <key>'
45
+ },
46
+ {
47
+ name: 'about',
48
+ description: 'Show this help message',
49
+ usage: 'jira-ai about'
50
+ }
51
+ ];
52
+
53
+ export async function aboutCommand() {
54
+ console.log(chalk.bold.cyan('\n📋 Jira AI - Available Commands\n'));
55
+
56
+ console.log(chalk.bold('Usage:'));
57
+ console.log(' jira-ai <command> [options]\n');
58
+
59
+ const allowedCommandsList = getAllowedCommands();
60
+ const isAllAllowed = allowedCommandsList.includes('all');
61
+
62
+ // Filter commands based on settings (about is always shown)
63
+ const commandsToShow = ALL_COMMANDS.filter(cmd =>
64
+ cmd.name === 'about' || isAllAllowed || isCommandAllowed(cmd.name)
65
+ );
66
+
67
+ console.log(chalk.bold('Available Commands:\n'));
68
+
69
+ for (const cmd of commandsToShow) {
70
+ console.log(chalk.yellow(` ${cmd.name}`));
71
+ console.log(` ${cmd.description}`);
72
+ console.log(` Usage: ${cmd.usage}\n`);
73
+ }
74
+
75
+ // Show disabled commands if not all are allowed
76
+ if (!isAllAllowed) {
77
+ const disabledCommands = ALL_COMMANDS.filter(cmd =>
78
+ cmd.name !== 'about' && !isCommandAllowed(cmd.name)
79
+ );
80
+
81
+ if (disabledCommands.length > 0) {
82
+ console.log(chalk.bold('Disabled Commands:\n'));
83
+ for (const cmd of disabledCommands) {
84
+ console.log(chalk.gray(` ${cmd.name} - ${cmd.description}`));
85
+ }
86
+ console.log();
87
+ }
88
+ }
89
+
90
+ console.log(chalk.bold('For detailed help on any command, run:'));
91
+ console.log(chalk.green(' jira-ai <command> --help\n'));
92
+
93
+ console.log(chalk.bold('Configuration:'));
94
+ console.log(' Settings are managed in settings.yaml');
95
+ const allowedProjects = getAllowedProjects();
96
+ console.log(` - Projects: ${allowedProjects.includes('all') ? 'All allowed' : allowedProjects.join(', ')}`);
97
+ console.log(` - Commands: ${isAllAllowed ? 'All allowed' : allowedCommandsList.join(', ')}\n`);
98
+ }
@@ -0,0 +1,94 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import mdToAdf from 'md-to-adf';
6
+ import { addIssueComment } from '../lib/jira-client';
7
+
8
+ export async function addCommentCommand(
9
+ options: { filePath: string; issueKey: string }
10
+ ): Promise<void> {
11
+ const { filePath, issueKey } = options;
12
+
13
+ // Validate issueKey
14
+ if (!issueKey || issueKey.trim() === '') {
15
+ console.error(chalk.red('\nError: Issue key is required (use --issue-key)'));
16
+ process.exit(1);
17
+ }
18
+
19
+ // Validate file path
20
+ if (!filePath || filePath.trim() === '') {
21
+ console.error(chalk.red('\nError: File path is required (use --file-path)'));
22
+ process.exit(1);
23
+ }
24
+
25
+ // Resolve file path to absolute
26
+ const absolutePath = path.resolve(filePath);
27
+
28
+ // Check file exists
29
+ if (!fs.existsSync(absolutePath)) {
30
+ console.error(chalk.red(`\nError: File not found: ${absolutePath}`));
31
+ process.exit(1);
32
+ }
33
+
34
+ // Read file
35
+ let markdownContent: string;
36
+ try {
37
+ markdownContent = fs.readFileSync(absolutePath, 'utf-8');
38
+ } catch (error) {
39
+ console.error(
40
+ chalk.red(
41
+ '\nError reading file: ' +
42
+ (error instanceof Error ? error.message : 'Unknown error')
43
+ )
44
+ );
45
+ process.exit(1);
46
+ }
47
+
48
+ // Validate file is not empty
49
+ if (markdownContent.trim() === '') {
50
+ console.error(chalk.red('\nError: File is empty'));
51
+ process.exit(1);
52
+ }
53
+
54
+ // Convert Markdown to ADF
55
+ let adfContent: any;
56
+ try {
57
+ adfContent = mdToAdf(markdownContent);
58
+ } catch (error) {
59
+ console.error(
60
+ chalk.red(
61
+ '\nError converting Markdown to ADF: ' +
62
+ (error instanceof Error ? error.message : 'Unknown error')
63
+ )
64
+ );
65
+ process.exit(1);
66
+ }
67
+
68
+ // Add comment with spinner
69
+ const spinner = ora(`Adding comment to ${issueKey}...`).start();
70
+
71
+ try {
72
+ await addIssueComment(issueKey, adfContent);
73
+ spinner.succeed(chalk.green(`Comment added successfully to ${issueKey}`));
74
+ console.log(chalk.gray(`\nFile: ${absolutePath}`));
75
+ } catch (error) {
76
+ spinner.fail(chalk.red('Failed to add comment'));
77
+ console.error(
78
+ chalk.red(
79
+ '\nError: ' + (error instanceof Error ? error.message : 'Unknown error')
80
+ )
81
+ );
82
+
83
+ // Provide helpful hints based on error
84
+ if (error instanceof Error && error.message.includes('404')) {
85
+ console.log(chalk.yellow('\nHint: Check that the issue key is correct'));
86
+ } else if (error instanceof Error && error.message.includes('403')) {
87
+ console.log(
88
+ chalk.yellow('\nHint: You may not have permission to comment on this issue')
89
+ );
90
+ }
91
+
92
+ process.exit(1);
93
+ }
94
+ }
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getCurrentUser } from '../lib/jira-client';
4
+ import { formatUserInfo } from '../lib/formatters';
5
+
6
+ export async function meCommand(): Promise<void> {
7
+ const spinner = ora('Fetching user information...').start();
8
+
9
+ try {
10
+ const user = await getCurrentUser();
11
+ spinner.succeed(chalk.green('User information retrieved'));
12
+ console.log(formatUserInfo(user));
13
+ } catch (error) {
14
+ spinner.fail(chalk.red('Failed to fetch user information'));
15
+ console.error(chalk.red('\nError: ' + (error instanceof Error ? error.message : 'Unknown error')));
16
+ process.exit(1);
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getProjectStatuses } from '../lib/jira-client';
4
+ import { formatProjectStatuses } from '../lib/formatters';
5
+
6
+ export async function projectStatusesCommand(projectId: string): Promise<void> {
7
+ const spinner = ora(`Fetching statuses for project ${projectId}...`).start();
8
+
9
+ try {
10
+ const statuses = await getProjectStatuses(projectId);
11
+ spinner.succeed(chalk.green('Project statuses retrieved'));
12
+ console.log(formatProjectStatuses(projectId, statuses));
13
+ } catch (error) {
14
+ spinner.fail(chalk.red('Failed to fetch project statuses'));
15
+ console.error(chalk.red('\nError: ' + (error instanceof Error ? error.message : 'Unknown error')));
16
+ process.exit(1);
17
+ }
18
+ }
@@ -0,0 +1,32 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getProjects } from '../lib/jira-client';
4
+ import { formatProjects } from '../lib/formatters';
5
+ import { getAllowedProjects, isProjectAllowed } from '../lib/settings';
6
+
7
+ export async function projectsCommand(): Promise<void> {
8
+ const spinner = ora('Fetching projects...').start();
9
+
10
+ try {
11
+ const allProjects = await getProjects();
12
+ const allowedProjectKeys = getAllowedProjects();
13
+
14
+ // Filter projects based on settings
15
+ const filteredProjects = allowedProjectKeys.includes('all')
16
+ ? allProjects
17
+ : allProjects.filter(project => isProjectAllowed(project.key));
18
+
19
+ spinner.succeed(chalk.green('Projects retrieved'));
20
+
21
+ if (filteredProjects.length === 0) {
22
+ console.log(chalk.yellow('\nNo projects match your settings configuration.'));
23
+ console.log(chalk.gray('Allowed projects: ' + allowedProjectKeys.join(', ')));
24
+ } else {
25
+ console.log(formatProjects(filteredProjects));
26
+ }
27
+ } catch (error) {
28
+ spinner.fail(chalk.red('Failed to fetch projects'));
29
+ console.error(chalk.red('\nError: ' + (error instanceof Error ? error.message : 'Unknown error')));
30
+ process.exit(1);
31
+ }
32
+ }
@@ -0,0 +1,40 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { searchIssuesByJql } from '../lib/jira-client';
4
+ import { formatJqlResults } from '../lib/formatters';
5
+
6
+ export async function runJqlCommand(jqlQuery: string, options: { limit: string }): Promise<void> {
7
+ // Validate JQL query is not empty
8
+ if (!jqlQuery || jqlQuery.trim() === '') {
9
+ console.error(chalk.red('\nError: JQL query cannot be empty'));
10
+ process.exit(1);
11
+ }
12
+
13
+ // Parse and validate limit parameter
14
+ let maxResults = 50; // default
15
+ if (options.limit) {
16
+ const parsedLimit = parseInt(options.limit, 10);
17
+ if (isNaN(parsedLimit) || parsedLimit < 1) {
18
+ console.error(chalk.red('\nError: Limit must be a positive number'));
19
+ process.exit(1);
20
+ }
21
+ if (parsedLimit > 1000) {
22
+ console.warn(chalk.yellow('\nWarning: Limit is very high. Using 1000 as maximum.'));
23
+ maxResults = 1000;
24
+ } else {
25
+ maxResults = parsedLimit;
26
+ }
27
+ }
28
+
29
+ const spinner = ora('Executing JQL query...').start();
30
+
31
+ try {
32
+ const issues = await searchIssuesByJql(jqlQuery, maxResults);
33
+ spinner.succeed(chalk.green('Query executed successfully'));
34
+ console.log(formatJqlResults(issues));
35
+ } catch (error) {
36
+ spinner.fail(chalk.red('Failed to execute JQL query'));
37
+ console.error(chalk.red('\nError: ' + (error instanceof Error ? error.message : 'Unknown error')));
38
+ process.exit(1);
39
+ }
40
+ }
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getTaskWithDetails } from '../lib/jira-client';
4
+ import { formatTaskDetails } from '../lib/formatters';
5
+
6
+ export async function taskWithDetailsCommand(taskId: string): Promise<void> {
7
+ const spinner = ora(`Fetching details for ${taskId}...`).start();
8
+
9
+ try {
10
+ const task = await getTaskWithDetails(taskId);
11
+ spinner.succeed(chalk.green('Task details retrieved'));
12
+ console.log(formatTaskDetails(task));
13
+ } catch (error) {
14
+ spinner.fail(chalk.red('Failed to fetch task details'));
15
+ console.error(chalk.red('\nError: ' + (error instanceof Error ? error.message : 'Unknown error')));
16
+ process.exit(1);
17
+ }
18
+ }