jira-ai 0.1.0 → 0.2.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.
- package/dist/cli.js +20 -9
- package/dist/commands/about.js +1 -1
- package/dist/commands/auth.js +77 -0
- package/dist/lib/auth-storage.js +54 -0
- package/dist/lib/jira-client.js +46 -8
- package/dist/lib/settings.js +43 -10
- package/dist/lib/utils.js +7 -9
- package/package.json +1 -1
- package/src/cli.ts +23 -10
- package/src/commands/about.ts +2 -2
- package/src/commands/auth.ts +81 -0
- package/src/lib/auth-storage.ts +57 -0
- package/src/lib/jira-client.ts +46 -8
- package/src/lib/settings.ts +42 -10
- package/src/lib/utils.ts +7 -9
- package/tests/auth-storage.test.ts +64 -0
- package/to-do.md +1 -2
package/dist/cli.js
CHANGED
|
@@ -16,14 +16,26 @@ const run_jql_1 = require("./commands/run-jql");
|
|
|
16
16
|
const update_description_1 = require("./commands/update-description");
|
|
17
17
|
const add_comment_1 = require("./commands/add-comment");
|
|
18
18
|
const about_1 = require("./commands/about");
|
|
19
|
+
const auth_1 = require("./commands/auth");
|
|
19
20
|
const settings_1 = require("./lib/settings");
|
|
20
21
|
// Load environment variables
|
|
21
22
|
dotenv_1.default.config();
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// Create CLI program
|
|
24
|
+
const program = new commander_1.Command();
|
|
25
|
+
program
|
|
26
|
+
.name('jira-ai')
|
|
27
|
+
.description('CLI tool for interacting with Atlassian Jira')
|
|
28
|
+
.version('1.0.0');
|
|
29
|
+
// Middleware to validate credentials for commands that need them
|
|
30
|
+
const validateCredentials = () => {
|
|
31
|
+
(0, utils_1.validateEnvVars)();
|
|
32
|
+
};
|
|
33
|
+
// Helper function to wrap commands with permission check and credential validation
|
|
34
|
+
function withPermission(commandName, commandFn, skipValidation = false) {
|
|
26
35
|
return async (...args) => {
|
|
36
|
+
if (!skipValidation) {
|
|
37
|
+
validateCredentials();
|
|
38
|
+
}
|
|
27
39
|
if (!(0, settings_1.isCommandAllowed)(commandName)) {
|
|
28
40
|
console.error(chalk_1.default.red(`\n❌ Command '${commandName}' is not allowed.`));
|
|
29
41
|
console.log(chalk_1.default.gray('Allowed commands: ' + (0, settings_1.getAllowedCommands)().join(', ')));
|
|
@@ -33,12 +45,11 @@ function withPermission(commandName, commandFn) {
|
|
|
33
45
|
return commandFn(...args);
|
|
34
46
|
};
|
|
35
47
|
}
|
|
36
|
-
//
|
|
37
|
-
const program = new commander_1.Command();
|
|
48
|
+
// Auth command (always allowed, skips validation)
|
|
38
49
|
program
|
|
39
|
-
.
|
|
40
|
-
.description('
|
|
41
|
-
.
|
|
50
|
+
.command('auth')
|
|
51
|
+
.description('Set up Jira authentication credentials')
|
|
52
|
+
.action(() => (0, auth_1.authCommand)());
|
|
42
53
|
// Me command
|
|
43
54
|
program
|
|
44
55
|
.command('me')
|
package/dist/commands/about.js
CHANGED
|
@@ -76,7 +76,7 @@ async function aboutCommand() {
|
|
|
76
76
|
console.log(chalk_1.default.bold('For detailed help on any command, run:'));
|
|
77
77
|
console.log(chalk_1.default.green(' jira-ai <command> --help\n'));
|
|
78
78
|
console.log(chalk_1.default.bold('Configuration:'));
|
|
79
|
-
console.log(
|
|
79
|
+
console.log(` Settings file: ${chalk_1.default.cyan((0, settings_1.getSettingsPath)())}`);
|
|
80
80
|
const allowedProjects = (0, settings_1.getAllowedProjects)();
|
|
81
81
|
console.log(` - Projects: ${allowedProjects.includes('all') ? 'All allowed' : allowedProjects.join(', ')}`);
|
|
82
82
|
console.log(` - Commands: ${isAllAllowed ? 'All allowed' : allowedCommandsList.join(', ')}\n`);
|
|
@@ -0,0 +1,77 @@
|
|
|
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.authCommand = authCommand;
|
|
7
|
+
const readline_1 = __importDefault(require("readline"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
const jira_client_1 = require("../lib/jira-client");
|
|
11
|
+
const auth_storage_1 = require("../lib/auth-storage");
|
|
12
|
+
const rl = readline_1.default.createInterface({
|
|
13
|
+
input: process.stdin,
|
|
14
|
+
output: process.stdout,
|
|
15
|
+
});
|
|
16
|
+
function ask(question) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
rl.question(question, (answer) => {
|
|
19
|
+
resolve(answer.trim());
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function askSecret(question) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
// Hide input for secret (basic implementation)
|
|
26
|
+
const stdin = process.stdin;
|
|
27
|
+
process.stdout.write(question);
|
|
28
|
+
// We can't easily hide characters in standard readline without some complex logic
|
|
29
|
+
// or external libraries. For now, we'll just use regular question but we'll
|
|
30
|
+
// mention it's a secret.
|
|
31
|
+
rl.question('', (answer) => {
|
|
32
|
+
resolve(answer.trim());
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function authCommand() {
|
|
37
|
+
console.log(chalk_1.default.cyan('\n--- Jira Authentication Setup ---\n'));
|
|
38
|
+
try {
|
|
39
|
+
const host = await ask('Jira URL (e.g., https://your-domain.atlassian.net): ');
|
|
40
|
+
if (!host) {
|
|
41
|
+
console.error(chalk_1.default.red('URL is required.'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const email = await ask('Email: ');
|
|
45
|
+
if (!email) {
|
|
46
|
+
console.error(chalk_1.default.red('Email is required.'));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk_1.default.gray('Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens'));
|
|
50
|
+
const apiToken = await ask('API Token: ');
|
|
51
|
+
if (!apiToken) {
|
|
52
|
+
console.error(chalk_1.default.red('API Token is required.'));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const spinner = (0, ora_1.default)('Verifying credentials...').start();
|
|
56
|
+
try {
|
|
57
|
+
const tempClient = (0, jira_client_1.createTemporaryClient)(host, email, apiToken);
|
|
58
|
+
const user = await tempClient.myself.getCurrentUser();
|
|
59
|
+
spinner.succeed(chalk_1.default.green('Authentication successful!'));
|
|
60
|
+
console.log(chalk_1.default.blue(`\nWelcome, ${user.displayName} (${user.emailAddress})`));
|
|
61
|
+
(0, auth_storage_1.saveCredentials)({ host, email, apiToken });
|
|
62
|
+
console.log(chalk_1.default.green('\nCredentials saved successfully to ~/.jira-ai/config.json'));
|
|
63
|
+
console.log(chalk_1.default.gray('These credentials will be used for future commands on this machine.'));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
spinner.fail(chalk_1.default.red('Authentication failed.'));
|
|
67
|
+
console.error(chalk_1.default.red(`Error: ${error.message || 'Invalid credentials'}`));
|
|
68
|
+
if (error.response && error.response.status === 401) {
|
|
69
|
+
console.error(chalk_1.default.yellow('Hint: Check if your email and API token are correct.'));
|
|
70
|
+
}
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
rl.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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.saveCredentials = saveCredentials;
|
|
7
|
+
exports.loadCredentials = loadCredentials;
|
|
8
|
+
exports.hasCredentials = hasCredentials;
|
|
9
|
+
exports.clearCredentials = clearCredentials;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.jira-ai');
|
|
14
|
+
const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
|
|
15
|
+
/**
|
|
16
|
+
* Save credentials to local storage
|
|
17
|
+
*/
|
|
18
|
+
function saveCredentials(creds) {
|
|
19
|
+
if (!fs_1.default.existsSync(CONFIG_DIR)) {
|
|
20
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(creds, null, 2), {
|
|
23
|
+
mode: 0o600, // Read/write for owner only
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load credentials from local storage
|
|
28
|
+
*/
|
|
29
|
+
function loadCredentials() {
|
|
30
|
+
if (!fs_1.default.existsSync(CONFIG_FILE)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const data = fs_1.default.readFileSync(CONFIG_FILE, 'utf8');
|
|
35
|
+
return JSON.parse(data);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if credentials exist
|
|
43
|
+
*/
|
|
44
|
+
function hasCredentials() {
|
|
45
|
+
return fs_1.default.existsSync(CONFIG_FILE);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Clear stored credentials
|
|
49
|
+
*/
|
|
50
|
+
function clearCredentials() {
|
|
51
|
+
if (fs_1.default.existsSync(CONFIG_FILE)) {
|
|
52
|
+
fs_1.default.unlinkSync(CONFIG_FILE);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/lib/jira-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getJiraClient = getJiraClient;
|
|
4
|
+
exports.createTemporaryClient = createTemporaryClient;
|
|
4
5
|
exports.getCurrentUser = getCurrentUser;
|
|
5
6
|
exports.getProjects = getProjects;
|
|
6
7
|
exports.getTaskWithDetails = getTaskWithDetails;
|
|
@@ -10,24 +11,61 @@ exports.updateIssueDescription = updateIssueDescription;
|
|
|
10
11
|
exports.addIssueComment = addIssueComment;
|
|
11
12
|
const jira_js_1 = require("jira.js");
|
|
12
13
|
const utils_1 = require("./utils");
|
|
14
|
+
const auth_storage_1 = require("./auth-storage");
|
|
13
15
|
let jiraClient = null;
|
|
14
16
|
/**
|
|
15
17
|
* Get or create Jira client instance
|
|
16
18
|
*/
|
|
17
19
|
function getJiraClient() {
|
|
18
20
|
if (!jiraClient) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const host = process.env.JIRA_HOST;
|
|
22
|
+
const email = process.env.JIRA_USER_EMAIL;
|
|
23
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
24
|
+
if (host && email && apiToken) {
|
|
25
|
+
jiraClient = new jira_js_1.Version3Client({
|
|
26
|
+
host,
|
|
27
|
+
authentication: {
|
|
28
|
+
basic: {
|
|
29
|
+
email,
|
|
30
|
+
apiToken,
|
|
31
|
+
},
|
|
25
32
|
},
|
|
26
|
-
}
|
|
27
|
-
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const storedCreds = (0, auth_storage_1.loadCredentials)();
|
|
37
|
+
if (storedCreds) {
|
|
38
|
+
jiraClient = new jira_js_1.Version3Client({
|
|
39
|
+
host: storedCreds.host,
|
|
40
|
+
authentication: {
|
|
41
|
+
basic: {
|
|
42
|
+
email: storedCreds.email,
|
|
43
|
+
apiToken: storedCreds.apiToken,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
throw new Error('Jira credentials not found. Please set environment variables or run "jira-ai auth"');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
28
52
|
}
|
|
29
53
|
return jiraClient;
|
|
30
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Initialize a temporary Jira client for verification
|
|
57
|
+
*/
|
|
58
|
+
function createTemporaryClient(host, email, apiToken) {
|
|
59
|
+
return new jira_js_1.Version3Client({
|
|
60
|
+
host,
|
|
61
|
+
authentication: {
|
|
62
|
+
basic: {
|
|
63
|
+
email,
|
|
64
|
+
apiToken,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
31
69
|
/**
|
|
32
70
|
* Get current user information
|
|
33
71
|
*/
|
package/dist/lib/settings.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getSettingsPath = getSettingsPath;
|
|
6
7
|
exports.loadSettings = loadSettings;
|
|
7
8
|
exports.isProjectAllowed = isProjectAllowed;
|
|
8
9
|
exports.isCommandAllowed = isCommandAllowed;
|
|
@@ -11,23 +12,55 @@ exports.getAllowedCommands = getAllowedCommands;
|
|
|
11
12
|
exports.__resetCache__ = __resetCache__;
|
|
12
13
|
const fs_1 = __importDefault(require("fs"));
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const os_1 = __importDefault(require("os"));
|
|
14
16
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
17
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
18
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.jira-ai');
|
|
19
|
+
const SETTINGS_FILE = path_1.default.join(CONFIG_DIR, 'settings.yaml');
|
|
15
20
|
let cachedSettings = null;
|
|
21
|
+
function getSettingsPath() {
|
|
22
|
+
return SETTINGS_FILE;
|
|
23
|
+
}
|
|
16
24
|
function loadSettings() {
|
|
17
25
|
if (cachedSettings) {
|
|
18
26
|
return cachedSettings;
|
|
19
27
|
}
|
|
20
|
-
|
|
21
|
-
if (!fs_1.default.existsSync(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// Ensure config directory exists
|
|
29
|
+
if (!fs_1.default.existsSync(CONFIG_DIR)) {
|
|
30
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
if (!fs_1.default.existsSync(SETTINGS_FILE)) {
|
|
33
|
+
// Check if settings.yaml exists in current working directory (migration/backward compatibility)
|
|
34
|
+
const localSettingsPath = path_1.default.join(process.cwd(), 'settings.yaml');
|
|
35
|
+
if (fs_1.default.existsSync(localSettingsPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const fileContents = fs_1.default.readFileSync(localSettingsPath, 'utf8');
|
|
38
|
+
fs_1.default.writeFileSync(SETTINGS_FILE, fileContents);
|
|
39
|
+
console.log(chalk_1.default?.cyan ? chalk_1.default.cyan(`Migrated settings.yaml to ${SETTINGS_FILE}`) : `Migrated settings.yaml to ${SETTINGS_FILE}`);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error('Error migrating settings.yaml:', error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Create default settings.yaml if it doesn't exist anywhere
|
|
47
|
+
const defaultSettings = {
|
|
48
|
+
projects: ['all'],
|
|
49
|
+
commands: ['all']
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const yamlStr = js_yaml_1.default.dump(defaultSettings);
|
|
53
|
+
fs_1.default.writeFileSync(SETTINGS_FILE, yamlStr);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error('Error creating default settings.yaml:', error);
|
|
57
|
+
}
|
|
58
|
+
cachedSettings = defaultSettings;
|
|
59
|
+
return cachedSettings;
|
|
60
|
+
}
|
|
28
61
|
}
|
|
29
62
|
try {
|
|
30
|
-
const fileContents = fs_1.default.readFileSync(
|
|
63
|
+
const fileContents = fs_1.default.readFileSync(SETTINGS_FILE, 'utf8');
|
|
31
64
|
const settings = js_yaml_1.default.load(fileContents);
|
|
32
65
|
cachedSettings = {
|
|
33
66
|
projects: settings.projects || ['all'],
|
|
@@ -36,7 +69,7 @@ function loadSettings() {
|
|
|
36
69
|
return cachedSettings;
|
|
37
70
|
}
|
|
38
71
|
catch (error) {
|
|
39
|
-
console.error(
|
|
72
|
+
console.error(`Error loading ${SETTINGS_FILE}:`, error);
|
|
40
73
|
process.exit(1);
|
|
41
74
|
}
|
|
42
75
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -10,8 +10,9 @@ exports.convertADFToMarkdown = convertADFToMarkdown;
|
|
|
10
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
11
|
const mdast_util_from_adf_1 = require("mdast-util-from-adf");
|
|
12
12
|
const mdast_util_to_markdown_1 = require("mdast-util-to-markdown");
|
|
13
|
+
const auth_storage_1 = require("./auth-storage");
|
|
13
14
|
/**
|
|
14
|
-
* Validate required environment variables
|
|
15
|
+
* Validate required environment variables or stored credentials
|
|
15
16
|
*/
|
|
16
17
|
function validateEnvVars() {
|
|
17
18
|
const required = [
|
|
@@ -20,14 +21,11 @@ function validateEnvVars() {
|
|
|
20
21
|
'JIRA_API_TOKEN',
|
|
21
22
|
];
|
|
22
23
|
const missing = required.filter((key) => !process.env[key]);
|
|
23
|
-
if (missing.length > 0) {
|
|
24
|
-
console.error(chalk_1.default.red('✗
|
|
25
|
-
|
|
26
|
-
console.log('
|
|
27
|
-
console.log(
|
|
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');
|
|
24
|
+
if (missing.length > 0 && !(0, auth_storage_1.hasCredentials)()) {
|
|
25
|
+
console.error(chalk_1.default.red('✗ Jira credentials not found.\n'));
|
|
26
|
+
console.log('Please run ' + chalk_1.default.cyan('jira-ai auth') + ' to set up your credentials.');
|
|
27
|
+
console.log('Alternatively, you can set the following environment variables:');
|
|
28
|
+
required.forEach((key) => console.log(` - ${key}`));
|
|
31
29
|
process.exit(1);
|
|
32
30
|
}
|
|
33
31
|
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -12,17 +12,32 @@ import { runJqlCommand } from './commands/run-jql';
|
|
|
12
12
|
import { updateDescriptionCommand } from './commands/update-description';
|
|
13
13
|
import { addCommentCommand } from './commands/add-comment';
|
|
14
14
|
import { aboutCommand } from './commands/about';
|
|
15
|
+
import { authCommand } from './commands/auth';
|
|
15
16
|
import { isCommandAllowed, getAllowedCommands } from './lib/settings';
|
|
16
17
|
|
|
17
18
|
// Load environment variables
|
|
18
19
|
dotenv.config();
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
-
|
|
21
|
+
// Create CLI program
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('jira-ai')
|
|
26
|
+
.description('CLI tool for interacting with Atlassian Jira')
|
|
27
|
+
.version('1.0.0');
|
|
28
|
+
|
|
29
|
+
// Middleware to validate credentials for commands that need them
|
|
30
|
+
const validateCredentials = () => {
|
|
31
|
+
validateEnvVars();
|
|
32
|
+
};
|
|
22
33
|
|
|
23
|
-
// Helper function to wrap commands with permission check
|
|
24
|
-
function withPermission(commandName: string, commandFn: (...args: any[]) => Promise<void
|
|
34
|
+
// Helper function to wrap commands with permission check and credential validation
|
|
35
|
+
function withPermission(commandName: string, commandFn: (...args: any[]) => Promise<void>, skipValidation = false) {
|
|
25
36
|
return async (...args: any[]) => {
|
|
37
|
+
if (!skipValidation) {
|
|
38
|
+
validateCredentials();
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
if (!isCommandAllowed(commandName)) {
|
|
27
42
|
console.error(chalk.red(`\n❌ Command '${commandName}' is not allowed.`));
|
|
28
43
|
console.log(chalk.gray('Allowed commands: ' + getAllowedCommands().join(', ')));
|
|
@@ -33,13 +48,11 @@ function withPermission(commandName: string, commandFn: (...args: any[]) => Prom
|
|
|
33
48
|
};
|
|
34
49
|
}
|
|
35
50
|
|
|
36
|
-
//
|
|
37
|
-
const program = new Command();
|
|
38
|
-
|
|
51
|
+
// Auth command (always allowed, skips validation)
|
|
39
52
|
program
|
|
40
|
-
.
|
|
41
|
-
.description('
|
|
42
|
-
.
|
|
53
|
+
.command('auth')
|
|
54
|
+
.description('Set up Jira authentication credentials')
|
|
55
|
+
.action(() => authCommand());
|
|
43
56
|
|
|
44
57
|
// Me command
|
|
45
58
|
program
|
package/src/commands/about.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { getAllowedCommands, getAllowedProjects, isCommandAllowed } from '../lib/settings';
|
|
2
|
+
import { getAllowedCommands, getAllowedProjects, isCommandAllowed, getSettingsPath } from '../lib/settings';
|
|
3
3
|
|
|
4
4
|
interface CommandInfo {
|
|
5
5
|
name: string;
|
|
@@ -91,7 +91,7 @@ export async function aboutCommand() {
|
|
|
91
91
|
console.log(chalk.green(' jira-ai <command> --help\n'));
|
|
92
92
|
|
|
93
93
|
console.log(chalk.bold('Configuration:'));
|
|
94
|
-
console.log(
|
|
94
|
+
console.log(` Settings file: ${chalk.cyan(getSettingsPath())}`);
|
|
95
95
|
const allowedProjects = getAllowedProjects();
|
|
96
96
|
console.log(` - Projects: ${allowedProjects.includes('all') ? 'All allowed' : allowedProjects.join(', ')}`);
|
|
97
97
|
console.log(` - Commands: ${isAllAllowed ? 'All allowed' : allowedCommandsList.join(', ')}\n`);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { createTemporaryClient } from '../lib/jira-client';
|
|
5
|
+
import { saveCredentials } from '../lib/auth-storage';
|
|
6
|
+
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function ask(question: string): Promise<string> {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
resolve(answer.trim());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function askSecret(question: string): Promise<string> {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
// Hide input for secret (basic implementation)
|
|
23
|
+
const stdin = process.stdin;
|
|
24
|
+
process.stdout.write(question);
|
|
25
|
+
|
|
26
|
+
// We can't easily hide characters in standard readline without some complex logic
|
|
27
|
+
// or external libraries. For now, we'll just use regular question but we'll
|
|
28
|
+
// mention it's a secret.
|
|
29
|
+
rl.question('', (answer) => {
|
|
30
|
+
resolve(answer.trim());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function authCommand(): Promise<void> {
|
|
36
|
+
console.log(chalk.cyan('\n--- Jira Authentication Setup ---\n'));
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const host = await ask('Jira URL (e.g., https://your-domain.atlassian.net): ');
|
|
40
|
+
if (!host) {
|
|
41
|
+
console.error(chalk.red('URL is required.'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const email = await ask('Email: ');
|
|
46
|
+
if (!email) {
|
|
47
|
+
console.error(chalk.red('Email is required.'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.gray('Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens'));
|
|
52
|
+
const apiToken = await ask('API Token: ');
|
|
53
|
+
if (!apiToken) {
|
|
54
|
+
console.error(chalk.red('API Token is required.'));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const spinner = ora('Verifying credentials...').start();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const tempClient = createTemporaryClient(host, email, apiToken);
|
|
62
|
+
const user = await tempClient.myself.getCurrentUser();
|
|
63
|
+
|
|
64
|
+
spinner.succeed(chalk.green('Authentication successful!'));
|
|
65
|
+
console.log(chalk.blue(`\nWelcome, ${user.displayName} (${user.emailAddress})`));
|
|
66
|
+
|
|
67
|
+
saveCredentials({ host, email, apiToken });
|
|
68
|
+
console.log(chalk.green('\nCredentials saved successfully to ~/.jira-ai/config.json'));
|
|
69
|
+
console.log(chalk.gray('These credentials will be used for future commands on this machine.'));
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
spinner.fail(chalk.red('Authentication failed.'));
|
|
72
|
+
console.error(chalk.red(`Error: ${error.message || 'Invalid credentials'}`));
|
|
73
|
+
if (error.response && error.response.status === 401) {
|
|
74
|
+
console.error(chalk.yellow('Hint: Check if your email and API token are correct.'));
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
rl.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface AuthCredentials {
|
|
6
|
+
host: string;
|
|
7
|
+
email: string;
|
|
8
|
+
apiToken: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), '.jira-ai');
|
|
12
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Save credentials to local storage
|
|
16
|
+
*/
|
|
17
|
+
export function saveCredentials(creds: AuthCredentials): void {
|
|
18
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
19
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(creds, null, 2), {
|
|
23
|
+
mode: 0o600, // Read/write for owner only
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load credentials from local storage
|
|
29
|
+
*/
|
|
30
|
+
export function loadCredentials(): AuthCredentials | null {
|
|
31
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
37
|
+
return JSON.parse(data) as AuthCredentials;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if credentials exist
|
|
45
|
+
*/
|
|
46
|
+
export function hasCredentials(): boolean {
|
|
47
|
+
return fs.existsSync(CONFIG_FILE);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear stored credentials
|
|
52
|
+
*/
|
|
53
|
+
export function clearCredentials(): void {
|
|
54
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
55
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/lib/jira-client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Version3Client } from 'jira.js';
|
|
2
2
|
import { convertADFToMarkdown } from './utils';
|
|
3
|
+
import { loadCredentials } from './auth-storage';
|
|
3
4
|
|
|
4
5
|
export interface UserInfo {
|
|
5
6
|
accountId: string;
|
|
@@ -8,6 +9,7 @@ export interface UserInfo {
|
|
|
8
9
|
active: boolean;
|
|
9
10
|
timeZone: string;
|
|
10
11
|
}
|
|
12
|
+
// ... (rest of interfaces)
|
|
11
13
|
|
|
12
14
|
export interface Project {
|
|
13
15
|
id: string;
|
|
@@ -92,19 +94,55 @@ let jiraClient: Version3Client | null = null;
|
|
|
92
94
|
*/
|
|
93
95
|
export function getJiraClient(): Version3Client {
|
|
94
96
|
if (!jiraClient) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
const host = process.env.JIRA_HOST;
|
|
98
|
+
const email = process.env.JIRA_USER_EMAIL;
|
|
99
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
100
|
+
|
|
101
|
+
if (host && email && apiToken) {
|
|
102
|
+
jiraClient = new Version3Client({
|
|
103
|
+
host,
|
|
104
|
+
authentication: {
|
|
105
|
+
basic: {
|
|
106
|
+
email,
|
|
107
|
+
apiToken,
|
|
108
|
+
},
|
|
101
109
|
},
|
|
102
|
-
}
|
|
103
|
-
}
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
const storedCreds = loadCredentials();
|
|
113
|
+
if (storedCreds) {
|
|
114
|
+
jiraClient = new Version3Client({
|
|
115
|
+
host: storedCreds.host,
|
|
116
|
+
authentication: {
|
|
117
|
+
basic: {
|
|
118
|
+
email: storedCreds.email,
|
|
119
|
+
apiToken: storedCreds.apiToken,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error('Jira credentials not found. Please set environment variables or run "jira-ai auth"');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
104
127
|
}
|
|
105
128
|
return jiraClient;
|
|
106
129
|
}
|
|
107
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Initialize a temporary Jira client for verification
|
|
133
|
+
*/
|
|
134
|
+
export function createTemporaryClient(host: string, email: string, apiToken: string): Version3Client {
|
|
135
|
+
return new Version3Client({
|
|
136
|
+
host,
|
|
137
|
+
authentication: {
|
|
138
|
+
basic: {
|
|
139
|
+
email,
|
|
140
|
+
apiToken,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
108
146
|
/**
|
|
109
147
|
* Get current user information
|
|
110
148
|
*/
|
package/src/lib/settings.ts
CHANGED
|
@@ -1,32 +1,64 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import yaml from 'js-yaml';
|
|
5
|
+
import chalk from 'chalk';
|
|
4
6
|
|
|
5
7
|
export interface Settings {
|
|
6
8
|
projects: string[];
|
|
7
9
|
commands: string[];
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
const CONFIG_DIR = path.join(os.homedir(), '.jira-ai');
|
|
13
|
+
const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.yaml');
|
|
14
|
+
|
|
10
15
|
let cachedSettings: Settings | null = null;
|
|
11
16
|
|
|
17
|
+
export function getSettingsPath(): string {
|
|
18
|
+
return SETTINGS_FILE;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export function loadSettings(): Settings {
|
|
13
22
|
if (cachedSettings) {
|
|
14
23
|
return cachedSettings;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
// Ensure config directory exists
|
|
27
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
28
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
}
|
|
18
30
|
|
|
19
|
-
if (!fs.existsSync(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
32
|
+
// Check if settings.yaml exists in current working directory (migration/backward compatibility)
|
|
33
|
+
const localSettingsPath = path.join(process.cwd(), 'settings.yaml');
|
|
34
|
+
if (fs.existsSync(localSettingsPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const fileContents = fs.readFileSync(localSettingsPath, 'utf8');
|
|
37
|
+
fs.writeFileSync(SETTINGS_FILE, fileContents);
|
|
38
|
+
console.log(chalk?.cyan ? chalk.cyan(`Migrated settings.yaml to ${SETTINGS_FILE}`) : `Migrated settings.yaml to ${SETTINGS_FILE}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error migrating settings.yaml:', error);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// Create default settings.yaml if it doesn't exist anywhere
|
|
44
|
+
const defaultSettings: Settings = {
|
|
45
|
+
projects: ['all'],
|
|
46
|
+
commands: ['all']
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
const yamlStr = yaml.dump(defaultSettings);
|
|
50
|
+
fs.writeFileSync(SETTINGS_FILE, yamlStr);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error creating default settings.yaml:', error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
cachedSettings = defaultSettings;
|
|
56
|
+
return cachedSettings;
|
|
57
|
+
}
|
|
26
58
|
}
|
|
27
59
|
|
|
28
60
|
try {
|
|
29
|
-
const fileContents = fs.readFileSync(
|
|
61
|
+
const fileContents = fs.readFileSync(SETTINGS_FILE, 'utf8');
|
|
30
62
|
const settings = yaml.load(fileContents) as Settings;
|
|
31
63
|
|
|
32
64
|
cachedSettings = {
|
|
@@ -36,7 +68,7 @@ export function loadSettings(): Settings {
|
|
|
36
68
|
|
|
37
69
|
return cachedSettings;
|
|
38
70
|
} catch (error) {
|
|
39
|
-
console.error(
|
|
71
|
+
console.error(`Error loading ${SETTINGS_FILE}:`, error);
|
|
40
72
|
process.exit(1);
|
|
41
73
|
}
|
|
42
74
|
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { fromADF } from 'mdast-util-from-adf';
|
|
3
3
|
import { toMarkdown } from 'mdast-util-to-markdown';
|
|
4
|
+
import { hasCredentials } from './auth-storage';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Validate required environment variables
|
|
7
|
+
* Validate required environment variables or stored credentials
|
|
7
8
|
*/
|
|
8
9
|
export function validateEnvVars(): void {
|
|
9
10
|
const required = [
|
|
@@ -14,14 +15,11 @@ export function validateEnvVars(): void {
|
|
|
14
15
|
|
|
15
16
|
const missing = required.filter((key) => !process.env[key]);
|
|
16
17
|
|
|
17
|
-
if (missing.length > 0) {
|
|
18
|
-
console.error(chalk.red('✗
|
|
19
|
-
|
|
20
|
-
console.log('
|
|
21
|
-
console.log(
|
|
22
|
-
console.log(' JIRA_USER_EMAIL=your-email@example.com');
|
|
23
|
-
console.log(' JIRA_API_TOKEN=your-api-token');
|
|
24
|
-
console.log('\nGet your API token from: https://id.atlassian.com/manage-profile/security/api-tokens');
|
|
18
|
+
if (missing.length > 0 && !hasCredentials()) {
|
|
19
|
+
console.error(chalk.red('✗ Jira credentials not found.\n'));
|
|
20
|
+
console.log('Please run ' + chalk.cyan('jira-ai auth') + ' to set up your credentials.');
|
|
21
|
+
console.log('Alternatively, you can set the following environment variables:');
|
|
22
|
+
required.forEach((key) => console.log(` - ${key}`));
|
|
25
23
|
process.exit(1);
|
|
26
24
|
}
|
|
27
25
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
// Mock os.homedir()
|
|
6
|
+
const tempDir = path.join(__dirname, 'temp-home');
|
|
7
|
+
const homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempDir);
|
|
8
|
+
|
|
9
|
+
// Now import the module under test
|
|
10
|
+
import { saveCredentials, loadCredentials, hasCredentials, clearCredentials } from '../src/lib/auth-storage';
|
|
11
|
+
|
|
12
|
+
describe('auth-storage', () => {
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
if (!fs.existsSync(tempDir)) {
|
|
15
|
+
fs.mkdirSync(tempDir);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
homedirSpy.mockRestore();
|
|
21
|
+
if (fs.existsSync(tempDir)) {
|
|
22
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
clearCredentials();
|
|
28
|
+
});
|
|
29
|
+
// ...
|
|
30
|
+
|
|
31
|
+
test('should save and load credentials', () => {
|
|
32
|
+
const creds = {
|
|
33
|
+
host: 'https://test.atlassian.net',
|
|
34
|
+
email: 'test@example.com',
|
|
35
|
+
apiToken: 'token123'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
saveCredentials(creds);
|
|
39
|
+
expect(hasCredentials()).toBe(true);
|
|
40
|
+
|
|
41
|
+
const loaded = loadCredentials();
|
|
42
|
+
expect(loaded).toEqual(creds);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should return null if no credentials exist', () => {
|
|
46
|
+
expect(loadCredentials()).toBeNull();
|
|
47
|
+
expect(hasCredentials()).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should clear credentials', () => {
|
|
51
|
+
const creds = {
|
|
52
|
+
host: 'https://test.atlassian.net',
|
|
53
|
+
email: 'test@example.com',
|
|
54
|
+
apiToken: 'token123'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
saveCredentials(creds);
|
|
58
|
+
expect(hasCredentials()).toBe(true);
|
|
59
|
+
|
|
60
|
+
clearCredentials();
|
|
61
|
+
expect(hasCredentials()).toBe(false);
|
|
62
|
+
expect(loadCredentials()).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|