jira-pilot 2.0.0 → 2.0.2

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.
@@ -1,60 +1,97 @@
1
- import Conf from 'conf';
2
-
3
- const schema = {
4
- jiraUrl: {
5
- type: 'string',
6
- format: 'url'
7
- },
8
- email: {
9
- type: 'string',
10
- format: 'email'
11
- },
12
- apiToken: {
13
- type: 'string'
14
- },
15
- aiKey: {
16
- type: 'string'
17
- },
18
- aiProvider: {
19
- type: 'string',
20
- default: 'openai'
21
- },
22
- aiEnabled: {
23
- type: 'boolean',
24
- default: false
25
- }
26
- };
27
-
28
- const config = new Conf({
29
- projectName: 'jira-pilot',
30
- schema
31
- });
32
-
33
- export const getCredentials = () => {
34
- return {
35
- jiraUrl: config.get('jiraUrl'),
36
- email: config.get('email'),
37
- apiToken: config.get('apiToken'),
38
- aiKey: config.get('aiKey'),
39
- aiProvider: config.get('aiProvider'),
40
- aiEnabled: config.get('aiEnabled')
41
- };
42
- };
43
-
44
- export const setCredentials = ({ jiraUrl, email, apiToken, aiKey, aiProvider, aiEnabled }) => {
45
- if (jiraUrl) config.set('jiraUrl', jiraUrl);
46
- if (email) config.set('email', email);
47
- if (apiToken) config.set('apiToken', apiToken);
48
- if (aiKey) config.set('aiKey', aiKey);
49
- if (aiProvider) config.set('aiProvider', aiProvider);
50
- if (typeof aiEnabled !== 'undefined') config.set('aiEnabled', aiEnabled);
51
- };
52
-
53
- export const clearCredentials = () => {
54
- config.clear();
55
- };
56
-
57
- export const hasCredentials = () => {
58
- const creds = getCredentials();
59
- return !!(creds.jiraUrl && creds.email && creds.apiToken);
60
- };
1
+ import Conf from 'conf';
2
+
3
+ const schema = {
4
+ jiraUrl: {
5
+ type: 'string',
6
+ format: 'url'
7
+ },
8
+ email: {
9
+ type: 'string',
10
+ format: 'email'
11
+ },
12
+ apiToken: {
13
+ type: 'string'
14
+ },
15
+ aiKey: {
16
+ type: 'string'
17
+ },
18
+ aiProvider: {
19
+ type: 'string',
20
+ default: 'openai'
21
+ },
22
+ aiEnabled: {
23
+ type: 'boolean',
24
+ default: false
25
+ },
26
+ githubToken: {
27
+ type: 'string'
28
+ }
29
+ };
30
+
31
+ const config = new Conf({
32
+ projectName: 'jira-pilot',
33
+ schema
34
+ });
35
+
36
+ export const getCredentials = () => {
37
+ return {
38
+ jiraUrl: config.get('jiraUrl'),
39
+ email: config.get('email'),
40
+ apiToken: config.get('apiToken'),
41
+ aiKey: config.get('aiKey'),
42
+ aiProvider: config.get('aiProvider'),
43
+ aiEnabled: config.get('aiEnabled'),
44
+ githubToken: config.get('githubToken')
45
+ };
46
+ };
47
+
48
+ export const setCredentials = ({ jiraUrl, email, apiToken, aiKey, aiProvider, aiEnabled, githubToken }) => {
49
+ if (jiraUrl) config.set('jiraUrl', jiraUrl);
50
+ if (email) config.set('email', email);
51
+ if (apiToken) config.set('apiToken', apiToken);
52
+ if (aiKey) config.set('aiKey', aiKey);
53
+ if (aiProvider) config.set('aiProvider', aiProvider);
54
+ if (typeof aiEnabled !== 'undefined') config.set('aiEnabled', aiEnabled);
55
+ if (githubToken) config.set('githubToken', githubToken);
56
+ };
57
+
58
+ export const clearCredentials = () => {
59
+ config.clear();
60
+ };
61
+
62
+ export const hasCredentials = () => {
63
+ const creds = getCredentials();
64
+ return !!(creds.jiraUrl && creds.email && creds.apiToken);
65
+ };
66
+
67
+ // ── Profile Management ──────────────────────────────────────────────
68
+
69
+ export const saveProfile = (name) => {
70
+ const creds = getCredentials();
71
+ config.set(`profiles.${name}`, creds);
72
+ config.set('activeProfile', name);
73
+ };
74
+
75
+ export const loadProfile = (name) => {
76
+ const profile = config.get(`profiles.${name}`);
77
+ if (!profile) return false;
78
+ setCredentials(profile);
79
+ config.set('activeProfile', name);
80
+ return true;
81
+ };
82
+
83
+ export const deleteProfile = (name) => {
84
+ config.delete(`profiles.${name}`);
85
+ if (config.get('activeProfile') === name) {
86
+ config.delete('activeProfile');
87
+ }
88
+ };
89
+
90
+ export const listProfiles = () => {
91
+ const profiles = config.get('profiles') || {};
92
+ return Object.keys(profiles);
93
+ };
94
+
95
+ export const getActiveProfile = () => {
96
+ return config.get('activeProfile') || null;
97
+ };
@@ -1,41 +1,41 @@
1
- import chalk from 'chalk';
2
-
3
- /**
4
- * Standardized error handler for CLI commands.
5
- * Stops the spinner and prints a formatted error message.
6
- *
7
- * @param {object|null} spinner - Ora spinner instance (will be stopped/failed)
8
- * @param {Error} error - The error object
9
- * @param {string} [context] - Optional context (e.g., "Failed to list issues")
10
- */
11
- export function handleCommandError(spinner, error, context = 'Operation failed') {
12
- // Handle user cancellation (Ctrl+C in enquirer)
13
- if (error === '' || (error && error.message === '')) {
14
- if (spinner) spinner.stop();
15
- console.log(chalk.yellow('\nCancelled.'));
16
- return;
17
- }
18
-
19
- if (spinner) {
20
- spinner.fail(context);
21
- } else {
22
- console.error(chalk.red(`\n${context}:`));
23
- }
24
-
25
- if (error.response) {
26
- const status = error.response.status;
27
- if (status === 404) {
28
- console.error(chalk.red('Resource not found. Check the ID or key.'));
29
- } else if (status === 400) {
30
- const data = error.response.data;
31
- const messages = data?.errorMessages?.join(', ') || data?.errors
32
- ? Object.entries(data.errors).map(([k, v]) => `${k}: ${v}`).join(', ')
33
- : JSON.stringify(data);
34
- console.error(chalk.red(`Bad Request: ${messages}`));
35
- } else {
36
- console.error(chalk.red(`Error ${status}: `), error.response.data);
37
- }
38
- } else {
39
- console.error(chalk.red(error.message));
40
- }
41
- }
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Standardized error handler for CLI commands.
5
+ * Stops the spinner and prints a formatted error message.
6
+ *
7
+ * @param {object|null} spinner - Ora spinner instance (will be stopped/failed)
8
+ * @param {Error} error - The error object
9
+ * @param {string} [context] - Optional context (e.g., "Failed to list issues")
10
+ */
11
+ export function handleCommandError(spinner, error, context = 'Operation failed') {
12
+ // Handle user cancellation (Ctrl+C in enquirer)
13
+ if (error === '' || (error && error.message === '')) {
14
+ if (spinner) spinner.stop();
15
+ console.log(chalk.yellow('\nCancelled.'));
16
+ return;
17
+ }
18
+
19
+ if (spinner) {
20
+ spinner.fail(context);
21
+ } else {
22
+ console.error(chalk.red(`\n${context}:`));
23
+ }
24
+
25
+ if (error.response) {
26
+ const status = error.response.status;
27
+ if (status === 404) {
28
+ console.error(chalk.red('Resource not found. Check the ID or key.'));
29
+ } else if (status === 400) {
30
+ const data = error.response.data;
31
+ const messages = data?.errorMessages?.join(', ') || (data?.errors
32
+ ? Object.entries(data.errors).map(([k, v]) => `${k}: ${v}`).join(', ')
33
+ : JSON.stringify(data));
34
+ console.error(chalk.red(`Bad Request: ${messages}`));
35
+ } else {
36
+ console.error(chalk.red(`Error ${status}: `), error.response.data);
37
+ }
38
+ } else {
39
+ console.error(chalk.red(error.message));
40
+ }
41
+ }
@@ -1,34 +1,34 @@
1
- /**
2
- * Converts plain text to Atlassian Document Format (ADF).
3
- * Splits on newlines to create separate paragraphs.
4
- *
5
- * @param {string} text - Plain text string
6
- * @returns {object} ADF document node
7
- */
8
- export function textToADF(text) {
9
- if (!text || typeof text !== 'string') {
10
- return {
11
- type: 'doc',
12
- version: 1,
13
- content: [
14
- {
15
- type: 'paragraph',
16
- content: []
17
- }
18
- ]
19
- };
20
- }
21
-
22
- const paragraphs = text.split('\n').map(line => ({
23
- type: 'paragraph',
24
- content: line.trim().length > 0
25
- ? [{ type: 'text', text: line }]
26
- : []
27
- }));
28
-
29
- return {
30
- type: 'doc',
31
- version: 1,
32
- content: paragraphs
33
- };
34
- }
1
+ /**
2
+ * Converts plain text to Atlassian Document Format (ADF).
3
+ * Splits on newlines to create separate paragraphs.
4
+ *
5
+ * @param {string} text - Plain text string
6
+ * @returns {object} ADF document node
7
+ */
8
+ export function textToADF(text) {
9
+ if (!text || typeof text !== 'string') {
10
+ return {
11
+ type: 'doc',
12
+ version: 1,
13
+ content: [
14
+ {
15
+ type: 'paragraph',
16
+ content: []
17
+ }
18
+ ]
19
+ };
20
+ }
21
+
22
+ const paragraphs = text.split('\n').map(line => ({
23
+ type: 'paragraph',
24
+ content: line.trim().length > 0
25
+ ? [{ type: 'text', text: line }]
26
+ : []
27
+ }));
28
+
29
+ return {
30
+ type: 'doc',
31
+ version: 1,
32
+ content: paragraphs
33
+ };
34
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Input Validators for CLI commands.
3
+ * Validates user input before API calls to catch errors early.
4
+ */
5
+
6
+ /**
7
+ * Validates a Jira issue key format (e.g., PROJ-123).
8
+ * @param {string} key - The issue key to validate.
9
+ * @returns {{ valid: boolean, message?: string }}
10
+ */
11
+ export function validateIssueKey(key) {
12
+ if (!key || typeof key !== 'string') {
13
+ return { valid: false, message: 'Issue key is required.' };
14
+ }
15
+
16
+ const trimmed = key.trim().toUpperCase();
17
+ const pattern = /^[A-Z][A-Z0-9_]+-\d+$/;
18
+
19
+ if (!pattern.test(trimmed)) {
20
+ return {
21
+ valid: false,
22
+ message: `Invalid issue key "${key}". Expected format: PROJ-123 (letters/numbers, dash, digits).`
23
+ };
24
+ }
25
+
26
+ return { valid: true };
27
+ }
28
+
29
+ /**
30
+ * Validates a Jira project key (e.g., PROJ).
31
+ * @param {string} key - The project key to validate.
32
+ * @returns {{ valid: boolean, message?: string }}
33
+ */
34
+ export function validateProjectKey(key) {
35
+ if (!key || typeof key !== 'string') {
36
+ return { valid: false, message: 'Project key is required.' };
37
+ }
38
+
39
+ const trimmed = key.trim().toUpperCase();
40
+ const pattern = /^[A-Z][A-Z0-9_]+$/;
41
+
42
+ if (!pattern.test(trimmed)) {
43
+ return {
44
+ valid: false,
45
+ message: `Invalid project key "${key}". Must start with a letter and contain only uppercase letters, digits, or underscores.`
46
+ };
47
+ }
48
+
49
+ return { valid: true };
50
+ }
51
+
52
+ /**
53
+ * Validates a Jira site URL.
54
+ * @param {string} url - The URL to validate.
55
+ * @returns {{ valid: boolean, message?: string }}
56
+ */
57
+ export function validateUrl(url) {
58
+ if (!url || typeof url !== 'string') {
59
+ return { valid: false, message: 'URL is required.' };
60
+ }
61
+
62
+ const trimmed = url.trim();
63
+
64
+ try {
65
+ const parsed = new URL(trimmed);
66
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
67
+ return { valid: false, message: 'URL must use http or https protocol.' };
68
+ }
69
+ return { valid: true };
70
+ } catch {
71
+ return { valid: false, message: `Invalid URL: "${url}". Example: https://your-company.atlassian.net` };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Sanitizes a JQL string by escaping potentially dangerous characters.
77
+ * This is a basic sanitization — Jira's API does its own validation too.
78
+ * @param {string} jql - The JQL query string.
79
+ * @returns {string} The sanitized JQL string.
80
+ */
81
+ export function sanitizeJql(jql) {
82
+ if (!jql || typeof jql !== 'string') {
83
+ return '';
84
+ }
85
+
86
+ // Remove null bytes and control characters
87
+ return jql.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').trim();
88
+ }