jira-ai 0.4.10 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,37 +1,16 @@
1
1
  import chalk from 'chalk';
2
- import { getTaskWithDetails, getCurrentUser } from '../lib/jira-client.js';
2
+ import { validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { formatTaskDetails } from '../lib/formatters.js';
4
4
  import { CommandError } from '../lib/errors.js';
5
5
  import { ui } from '../lib/ui.js';
6
- import { isCommandAllowed, validateIssueAgainstFilters } from '../lib/settings.js';
7
6
  export async function taskWithDetailsCommand(taskId, options = {}) {
8
7
  ui.startSpinner(`Fetching details for ${taskId}...`);
9
8
  try {
10
- const task = await getTaskWithDetails(taskId, {
9
+ const task = await validateIssuePermissions(taskId, 'task-with-details', {
11
10
  includeHistory: options.includeDetailedHistory,
12
11
  historyLimit: options.historyLimit ? parseInt(options.historyLimit, 10) : undefined,
13
12
  historyOffset: options.historyOffset ? parseInt(options.historyOffset, 10) : undefined,
14
13
  });
15
- const projectKey = task.key.split('-')[0];
16
- // Check if command is allowed for this project
17
- if (!isCommandAllowed('task-with-details', projectKey)) {
18
- ui.failSpinner(chalk.red('Command not allowed for this project'));
19
- throw new CommandError(`Command 'task-with-details' is not allowed for project ${projectKey}.`, {
20
- hints: [
21
- `Update settings.yaml to enable this command for this project.`
22
- ]
23
- });
24
- }
25
- // Check granular filters
26
- const currentUser = await getCurrentUser();
27
- if (!validateIssueAgainstFilters(task, currentUser.accountId)) {
28
- ui.failSpinner(chalk.red('Access denied by project filters'));
29
- throw new CommandError(`Access to issue ${taskId} is restricted by project filters.`, {
30
- hints: [
31
- `This project has filters that you do not meet (e.g., participated roles).`
32
- ]
33
- });
34
- }
35
14
  ui.succeedSpinner(chalk.green('Task details retrieved'));
36
15
  console.log(formatTaskDetails(task));
37
16
  }
@@ -1,7 +1,7 @@
1
1
  import { Version3Client } from 'jira.js';
2
2
  import { calculateStatusStatistics, convertADFToMarkdown } from './utils.js';
3
3
  import { loadCredentials } from './auth-storage.js';
4
- import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAgainstFilters } from './settings.js';
4
+ import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAgainstFilters, loadSettings } from './settings.js';
5
5
  import { CommandError } from './errors.js';
6
6
  let jiraClient = null;
7
7
  let organizationOverride = undefined;
@@ -356,8 +356,8 @@ export async function createIssue(projectKey, summary, issueTypeName, parentKey)
356
356
  /**
357
357
  * Validate that the current user has permission to perform a command on an issue
358
358
  */
359
- export async function validateIssuePermissions(issueKey, commandName) {
360
- const task = await getTaskWithDetails(issueKey);
359
+ export async function validateIssuePermissions(issueKey, commandName, options = {}) {
360
+ const task = await getTaskWithDetails(issueKey, options);
361
361
  const projectKey = task.key.split('-')[0];
362
362
  if (!isProjectAllowed(projectKey)) {
363
363
  throw new CommandError(`Project '${projectKey}' is not allowed by your settings.`);
@@ -373,6 +373,28 @@ export async function validateIssuePermissions(issueKey, commandName) {
373
373
  hints: [`This project has filters that you do not meet (e.g., participated roles).`]
374
374
  });
375
375
  }
376
+ // Check JQL filters
377
+ const settings = loadSettings();
378
+ let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
379
+ if (!project) {
380
+ project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
381
+ }
382
+ if (project && typeof project !== 'string' && project.filters?.jql) {
383
+ const client = getJiraClient();
384
+ const jql = `key = "${issueKey}" AND (${project.filters.jql})`;
385
+ const response = await client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
386
+ jql,
387
+ maxResults: 1,
388
+ fields: ['key'],
389
+ });
390
+ if (!response.issues || response.issues.length === 0) {
391
+ throw new CommandError(`Access to issue ${issueKey} is restricted by project filters.`, {
392
+ hints: [
393
+ `This project has a JQL filter that this issue does not meet: ${project.filters.jql}`,
394
+ ]
395
+ });
396
+ }
397
+ }
376
398
  return task;
377
399
  }
378
400
  /**
@@ -32,7 +32,7 @@ export function loadSettings() {
32
32
  console.error('Error migrating settings.yaml:', error);
33
33
  const defaultSettings = {
34
34
  projects: ['all'],
35
- commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
35
+ commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
36
36
  };
37
37
  cachedSettings = defaultSettings;
38
38
  return cachedSettings;
@@ -42,7 +42,7 @@ export function loadSettings() {
42
42
  // Create default settings.yaml if it doesn't exist anywhere
43
43
  const defaultSettings = {
44
44
  projects: ['all'],
45
- commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
45
+ commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
46
46
  };
47
47
  try {
48
48
  const yamlStr = yaml.dump(defaultSettings);
@@ -68,7 +68,7 @@ export function loadSettings() {
68
68
  const settings = rawSettings;
69
69
  cachedSettings = {
70
70
  projects: settings?.projects || ['all'],
71
- commands: settings?.commands || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
71
+ commands: settings?.commands || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
72
72
  };
73
73
  }
74
74
  else {
@@ -111,7 +111,10 @@ export function isCommandAllowed(commandName, projectKey) {
111
111
  return true;
112
112
  }
113
113
  if (projectKey) {
114
- const project = settings.projects.find(p => (typeof p === 'string' ? p : p.key) === projectKey);
114
+ let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
115
+ if (!project) {
116
+ project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
117
+ }
115
118
  if (project && typeof project !== 'string' && project.commands) {
116
119
  return project.commands.includes(commandName);
117
120
  }
@@ -166,11 +169,12 @@ export function applyGlobalFilters(jql) {
166
169
  export function validateIssueAgainstFilters(issue, currentUserId) {
167
170
  const settings = loadSettings();
168
171
  const projectKey = issue.key.split('-')[0];
169
- const project = settings.projects.find(p => {
170
- if (typeof p === 'string')
171
- return p === 'all' || p === projectKey;
172
- return p.key === projectKey;
173
- });
172
+ // Find specific project config first
173
+ let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
174
+ // If not found, look for string match (exact project key or 'all')
175
+ if (!project) {
176
+ project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
177
+ }
174
178
  if (!project) {
175
179
  return false;
176
180
  }
@@ -92,5 +92,5 @@ export const ProjectSettingSchema = z.union([
92
92
  ]);
93
93
  export const SettingsSchema = z.object({
94
94
  projects: z.array(ProjectSettingSchema).nullish().transform(val => val || ['all']),
95
- commands: z.array(z.string()).nullish().transform(val => val || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']),
95
+ commands: z.array(z.string()).nullish().transform(val => val || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']),
96
96
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",