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 +30 -1
- package/dist/commands/auth.js +11 -24
- package/dist/commands/organization.js +50 -0
- package/dist/lib/auth-storage.js +114 -13
- package/dist/lib/formatters.js +1 -1
- package/dist/lib/jira-client.js +17 -2
- package/package.json +1 -1
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.
|
|
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')
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
package/dist/lib/auth-storage.js
CHANGED
|
@@ -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
|
-
*
|
|
7
|
+
* Extract default alias from Jira host
|
|
8
8
|
*/
|
|
9
|
-
export function
|
|
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(
|
|
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
|
-
*
|
|
60
|
+
* Save credentials to local storage
|
|
19
61
|
*/
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/lib/formatters.js
CHANGED
|
@@ -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',
|
|
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
|
/**
|
package/dist/lib/jira-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|