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 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
- // Validate environment variables
23
- (0, utils_1.validateEnvVars)();
24
- // Helper function to wrap commands with permission check
25
- function withPermission(commandName, commandFn) {
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
- // Create CLI program
37
- const program = new commander_1.Command();
48
+ // Auth command (always allowed, skips validation)
38
49
  program
39
- .name('jira-ai')
40
- .description('CLI tool for interacting with Atlassian Jira')
41
- .version('1.0.0');
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')
@@ -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(' Settings are managed in settings.yaml');
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
+ }
@@ -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
- jiraClient = new jira_js_1.Version3Client({
20
- host: process.env.JIRA_HOST,
21
- authentication: {
22
- basic: {
23
- email: process.env.JIRA_USER_EMAIL,
24
- apiToken: process.env.JIRA_API_TOKEN,
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
  */
@@ -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
- 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
+ // 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(settingsPath, 'utf8');
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('Error loading settings.yaml:', 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('✗ 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');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI tool for interacting with Atlassian Jira",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
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
- // Validate environment variables
21
- validateEnvVars();
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
- // Create CLI program
37
- const program = new Command();
38
-
51
+ // Auth command (always allowed, skips validation)
39
52
  program
40
- .name('jira-ai')
41
- .description('CLI tool for interacting with Atlassian Jira')
42
- .version('1.0.0');
53
+ .command('auth')
54
+ .description('Set up Jira authentication credentials')
55
+ .action(() => authCommand());
43
56
 
44
57
  // Me command
45
58
  program
@@ -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(' Settings are managed in settings.yaml');
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
+ }
@@ -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
- jiraClient = new Version3Client({
96
- host: process.env.JIRA_HOST!,
97
- authentication: {
98
- basic: {
99
- email: process.env.JIRA_USER_EMAIL!,
100
- apiToken: process.env.JIRA_API_TOKEN!,
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
  */
@@ -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
- const settingsPath = path.join(process.cwd(), 'settings.yaml');
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(settingsPath)) {
20
- console.warn('Warning: settings.yaml not found. Using default settings (all allowed).');
21
- cachedSettings = {
22
- projects: ['all'],
23
- commands: ['all']
24
- };
25
- return cachedSettings;
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(settingsPath, 'utf8');
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('Error loading settings.yaml:', 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('✗ Missing required environment variables:\n'));
19
- missing.forEach((key) => console.error(chalk.red(` - ${key}`)));
20
- console.log('\nPlease create a .env file with the following variables:');
21
- console.log(' JIRA_HOST=https://your-domain.atlassian.net');
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
+ });
package/to-do.md CHANGED
@@ -3,7 +3,6 @@
3
3
  - edit task description from .md file - done
4
4
  - add comment from .md file - done
5
5
  - get all parent and nested features - done
6
-
6
+ - auth - done
7
7
 
8
8
  - create task from {with-template} using parent
9
- - auth