jira-ai 0.3.15 → 0.3.16

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
@@ -17,7 +17,9 @@ import { createTaskCommand } from './commands/create-task.js';
17
17
  import { getIssueStatisticsCommand } from './commands/get-issue-statistics.js';
18
18
  import { aboutCommand } from './commands/about.js';
19
19
  import { authCommand } from './commands/auth.js';
20
+ import { listOrganizations, useOrganizationCommand, removeOrganizationCommand } from './commands/organization.js';
20
21
  import { isCommandAllowed, getAllowedCommands } from './lib/settings.js';
22
+ import { setOrganizationOverride } from './lib/jira-client.js';
21
23
  import { CliError } from './types/errors.js';
22
24
  import { CommandError } from './lib/errors.js';
23
25
  import { ui } from './lib/ui.js';
@@ -29,7 +31,12 @@ const program = new Command();
29
31
  program
30
32
  .name('jira-ai')
31
33
  .description('CLI tool for interacting with Atlassian Jira')
32
- .version('0.3.12');
34
+ .version('0.3.16')
35
+ .option('-o, --organization <alias>', 'Override the active Jira organization');
36
+ // Hook to handle the global option before any command runs
37
+ program.on('option:organization', (alias) => {
38
+ setOrganizationOverride(alias);
39
+ });
33
40
  // Middleware to validate credentials for commands that need them
34
41
  const validateCredentials = () => {
35
42
  validateEnvVars();
@@ -68,7 +75,29 @@ program
68
75
  .description('Set up Jira authentication credentials')
69
76
  .option('--from-json <json_string>', 'Accepts a raw JSON string with credentials')
70
77
  .option('--from-file <path>', 'Accepts a path to a file (typically .env) with credentials')
78
+ .option('--alias <alias>', 'Alias for this organization')
71
79
  .action((options) => authCommand(options));
80
+ // Organization commands
81
+ const org = program
82
+ .command('organization')
83
+ .alias('org')
84
+ .description('Manage Jira organization profiles');
85
+ org
86
+ .command('list')
87
+ .description('Show all saved organizations')
88
+ .action(() => listOrganizations());
89
+ org
90
+ .command('use <alias>')
91
+ .description('Switch the active organization')
92
+ .action((alias) => useOrganizationCommand(alias));
93
+ org
94
+ .command('remove <alias>')
95
+ .description('Delete an organization credentials')
96
+ .action((alias) => removeOrganizationCommand(alias));
97
+ org
98
+ .command('add <alias>')
99
+ .description('Add a new organization')
100
+ .action((alias) => authCommand({ alias }));
72
101
  // Me command
73
102
  program
74
103
  .command('me')
@@ -6,31 +6,18 @@ import { createTemporaryClient } from '../lib/jira-client.js';
6
6
  import { saveCredentials } from '../lib/auth-storage.js';
7
7
  import { CommandError } from '../lib/errors.js';
8
8
  import { ui } from '../lib/ui.js';
9
- const rl = readline.createInterface({
10
- input: process.stdin,
11
- output: process.stdout,
12
- });
13
- function ask(question) {
14
- return new Promise((resolve) => {
15
- rl.question(question, (answer) => {
16
- resolve(answer.trim());
17
- });
9
+ export async function authCommand(options = {}) {
10
+ const rl = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
18
13
  });
19
- }
20
- function askSecret(question) {
21
- return new Promise((resolve) => {
22
- // Hide input for secret (basic implementation)
23
- const stdin = process.stdin;
24
- process.stdout.write(question);
25
- // We can't easily hide characters in standard readline without some complex logic
26
- // or external libraries. For now, we'll just use regular question but we'll
27
- // mention it's a secret.
28
- rl.question('', (answer) => {
29
- resolve(answer.trim());
14
+ function ask(question) {
15
+ return new Promise((resolve) => {
16
+ rl.question(question, (answer) => {
17
+ resolve(answer.trim());
18
+ });
30
19
  });
31
- });
32
- }
33
- export async function authCommand(options = {}) {
20
+ }
34
21
  let host = '';
35
22
  let email = '';
36
23
  let apiToken = '';
@@ -105,7 +92,7 @@ export async function authCommand(options = {}) {
105
92
  const user = await tempClient.myself.getCurrentUser();
106
93
  ui.succeedSpinner(chalk.green('Authentication successful!'));
107
94
  console.log(chalk.blue(`\nWelcome, ${user.displayName} (${user.emailAddress})`));
108
- saveCredentials({ host, email, apiToken });
95
+ saveCredentials({ host, email, apiToken }, options.alias);
109
96
  console.log(chalk.green('\nCredentials saved successfully to ~/.jira-ai/config.json'));
110
97
  console.log(chalk.gray('These credentials will be used for future commands on this machine.'));
111
98
  }
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { getOrganizations, getCurrentOrganizationAlias, useOrganization, removeOrganization } from '../lib/auth-storage.js';
4
+ import { CommandError } from '../lib/errors.js';
5
+ export async function listOrganizations() {
6
+ const organizations = getOrganizations();
7
+ const currentAlias = getCurrentOrganizationAlias();
8
+ if (Object.keys(organizations).length === 0) {
9
+ console.log(chalk.yellow('No organizations configured. Use "jira-ai auth" to add one.'));
10
+ return;
11
+ }
12
+ const table = new Table({
13
+ head: [chalk.cyan('Status'), chalk.cyan('Alias'), chalk.cyan('Host'), chalk.cyan('Email')],
14
+ style: { head: [], border: [] }
15
+ });
16
+ for (const [alias, creds] of Object.entries(organizations)) {
17
+ const isCurrent = alias === currentAlias;
18
+ table.push([
19
+ isCurrent ? chalk.green('active') : '',
20
+ isCurrent ? chalk.bold(alias) : alias,
21
+ creds.host,
22
+ creds.email
23
+ ]);
24
+ }
25
+ console.log(chalk.cyan('\nJira Organizations:'));
26
+ console.log(table.toString());
27
+ }
28
+ export async function useOrganizationCommand(alias) {
29
+ try {
30
+ useOrganization(alias);
31
+ console.log(chalk.green(`Switched to organization: ${chalk.bold(alias)}`));
32
+ }
33
+ catch (error) {
34
+ throw new CommandError(error.message);
35
+ }
36
+ }
37
+ export async function removeOrganizationCommand(alias) {
38
+ const currentAlias = getCurrentOrganizationAlias();
39
+ removeOrganization(alias);
40
+ console.log(chalk.green(`Removed organization: ${chalk.bold(alias)}`));
41
+ if (currentAlias === alias) {
42
+ const newAlias = getCurrentOrganizationAlias();
43
+ if (newAlias) {
44
+ console.log(chalk.yellow(`Active organization switched to: ${chalk.bold(newAlias)}`));
45
+ }
46
+ else {
47
+ console.log(chalk.yellow('No more organizations configured.'));
48
+ }
49
+ }
50
+ }
@@ -4,36 +4,87 @@ import os from 'os';
4
4
  const CONFIG_DIR = path.join(os.homedir(), '.jira-ai');
5
5
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
6
6
  /**
7
- * Save credentials to local storage
7
+ * Extract default alias from Jira host
8
8
  */
9
- export function saveCredentials(creds) {
9
+ export function extractAliasFromHost(host) {
10
+ try {
11
+ const url = new URL(host.startsWith('http') ? host : `https://${host}`);
12
+ const hostname = url.hostname;
13
+ const parts = hostname.split('.');
14
+ // For xxxx.atlassian.net
15
+ if (parts.length >= 3 && parts[parts.length - 1] === 'net' && parts[parts.length - 2] === 'atlassian') {
16
+ return parts[0];
17
+ }
18
+ return hostname;
19
+ }
20
+ catch {
21
+ return host.replace(/https?:\/\//, '').split(/[./]/)[0] || 'default';
22
+ }
23
+ }
24
+ function loadConfig() {
25
+ if (!fs.existsSync(CONFIG_FILE)) {
26
+ return { organizations: {} };
27
+ }
28
+ try {
29
+ const data = fs.readFileSync(CONFIG_FILE, 'utf8');
30
+ const parsed = JSON.parse(data);
31
+ // Migration logic for old format
32
+ if (parsed.host && parsed.email && parsed.apiToken) {
33
+ const alias = extractAliasFromHost(parsed.host);
34
+ return {
35
+ current: alias,
36
+ organizations: {
37
+ [alias]: {
38
+ host: parsed.host,
39
+ email: parsed.email,
40
+ apiToken: parsed.apiToken,
41
+ },
42
+ },
43
+ };
44
+ }
45
+ return parsed;
46
+ }
47
+ catch (error) {
48
+ return { organizations: {} };
49
+ }
50
+ }
51
+ function saveConfig(config) {
10
52
  if (!fs.existsSync(CONFIG_DIR)) {
11
53
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
12
54
  }
13
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(creds, null, 2), {
55
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
14
56
  mode: 0o600, // Read/write for owner only
15
57
  });
16
58
  }
17
59
  /**
18
- * Load credentials from local storage
60
+ * Save credentials to local storage
19
61
  */
20
- export function loadCredentials() {
21
- if (!fs.existsSync(CONFIG_FILE)) {
22
- return null;
62
+ export function saveCredentials(creds, alias) {
63
+ const config = loadConfig();
64
+ const effectiveAlias = alias || extractAliasFromHost(creds.host);
65
+ config.organizations[effectiveAlias] = creds;
66
+ if (!config.current) {
67
+ config.current = effectiveAlias;
23
68
  }
24
- try {
25
- const data = fs.readFileSync(CONFIG_FILE, 'utf8');
26
- return JSON.parse(data);
27
- }
28
- catch (error) {
69
+ saveConfig(config);
70
+ }
71
+ /**
72
+ * Load credentials from local storage
73
+ */
74
+ export function loadCredentials(alias) {
75
+ const config = loadConfig();
76
+ const targetAlias = alias || config.current;
77
+ if (!targetAlias || !config.organizations[targetAlias]) {
29
78
  return null;
30
79
  }
80
+ return config.organizations[targetAlias];
31
81
  }
32
82
  /**
33
83
  * Check if credentials exist
34
84
  */
35
85
  export function hasCredentials() {
36
- return fs.existsSync(CONFIG_FILE);
86
+ const config = loadConfig();
87
+ return Object.keys(config.organizations).length > 0;
37
88
  }
38
89
  /**
39
90
  * Clear stored credentials
@@ -43,3 +94,53 @@ export function clearCredentials() {
43
94
  fs.unlinkSync(CONFIG_FILE);
44
95
  }
45
96
  }
97
+ /**
98
+ * Save organization credentials
99
+ */
100
+ export function saveOrganization(alias, creds) {
101
+ saveCredentials(creds, alias);
102
+ }
103
+ /**
104
+ * Switch the active organization
105
+ */
106
+ export function useOrganization(alias) {
107
+ const config = loadConfig();
108
+ if (!config.organizations[alias]) {
109
+ throw new Error(`Organization "${alias}" not found.`);
110
+ }
111
+ config.current = alias;
112
+ saveConfig(config);
113
+ }
114
+ /**
115
+ * Alias for useOrganization to match test expectations
116
+ */
117
+ export function setCurrentOrganization(alias) {
118
+ useOrganization(alias);
119
+ }
120
+ /**
121
+ * Remove an organization's credentials
122
+ */
123
+ export function removeOrganization(alias) {
124
+ const config = loadConfig();
125
+ if (config.organizations[alias]) {
126
+ delete config.organizations[alias];
127
+ if (config.current === alias) {
128
+ config.current = Object.keys(config.organizations)[0];
129
+ }
130
+ saveConfig(config);
131
+ }
132
+ }
133
+ /**
134
+ * Get all saved organizations
135
+ */
136
+ export function getOrganizations() {
137
+ const config = loadConfig();
138
+ return config.organizations;
139
+ }
140
+ /**
141
+ * Get the currently active organization alias
142
+ */
143
+ export function getCurrentOrganizationAlias() {
144
+ const config = loadConfig();
145
+ return config.current;
146
+ }
@@ -20,7 +20,7 @@ function createTable(headers, colWidths) {
20
20
  */
21
21
  export function formatUserInfo(user) {
22
22
  const table = createTable(['Property', 'Value'], [20, 50]);
23
- table.push(['Host', process.env.JIRA_HOST || 'N/A'], ['Display Name', user.displayName], ['Email', user.emailAddress], ['Account ID', user.accountId], ['Status', user.active ? chalk.green('Active') : chalk.red('Inactive')], ['Time Zone', user.timeZone]);
23
+ table.push(['Host', user.host], ['Display Name', user.displayName], ['Email', user.emailAddress], ['Account ID', user.accountId], ['Status', user.active ? chalk.green('Active') : chalk.red('Inactive')], ['Time Zone', user.timeZone]);
24
24
  return '\n' + chalk.bold('User Information:') + '\n' + table.toString() + '\n';
25
25
  }
26
26
  /**
@@ -2,6 +2,14 @@ import { Version3Client } from 'jira.js';
2
2
  import { calculateStatusStatistics, convertADFToMarkdown } from './utils.js';
3
3
  import { loadCredentials } from './auth-storage.js';
4
4
  let jiraClient = null;
5
+ let organizationOverride = undefined;
6
+ /**
7
+ * Set a global organization override for the current execution
8
+ */
9
+ export function setOrganizationOverride(alias) {
10
+ organizationOverride = alias;
11
+ jiraClient = null; // Force client recreation
12
+ }
5
13
  /**
6
14
  * Get or create Jira client instance
7
15
  */
@@ -22,7 +30,7 @@ export function getJiraClient() {
22
30
  });
23
31
  }
24
32
  else {
25
- const storedCreds = loadCredentials();
33
+ const storedCreds = loadCredentials(organizationOverride);
26
34
  if (storedCreds) {
27
35
  jiraClient = new Version3Client({
28
36
  host: storedCreds.host,
@@ -35,7 +43,10 @@ export function getJiraClient() {
35
43
  });
36
44
  }
37
45
  else {
38
- throw new Error('Jira credentials not found. Please set environment variables or run "jira-ai auth"');
46
+ const errorMsg = organizationOverride
47
+ ? `Jira credentials for organization "${organizationOverride}" not found.`
48
+ : 'Jira credentials not found. Please set environment variables or run "jira-ai auth"';
49
+ throw new Error(errorMsg);
39
50
  }
40
51
  }
41
52
  }
@@ -61,12 +72,16 @@ export function createTemporaryClient(host, email, apiToken) {
61
72
  export async function getCurrentUser() {
62
73
  const client = getJiraClient();
63
74
  const user = await client.myself.getCurrentUser();
75
+ // Try to extract host from client instance
76
+ // @ts-ignore - accessing internal property to show it in UI
77
+ const host = client.config.host || 'N/A';
64
78
  return {
65
79
  accountId: user.accountId || '',
66
80
  displayName: user.displayName || '',
67
81
  emailAddress: user.emailAddress || '',
68
82
  active: user.active || false,
69
83
  timeZone: user.timeZone || '',
84
+ host,
70
85
  };
71
86
  }
72
87
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",