jira-ai 0.6.12 → 0.9.0
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 +191 -167
- package/dist/lib/settings.js +70 -26
- package/dist/lib/validation.js +15 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import { checkForUpdate, formatUpdateMessage, checkForUpdateSync } from './lib/u
|
|
|
30
30
|
import { CliError } from './types/errors.js';
|
|
31
31
|
import { CommandError } from './lib/errors.js';
|
|
32
32
|
import { ui } from './lib/ui.js';
|
|
33
|
-
import { CreateTaskSchema,
|
|
33
|
+
import { CreateTaskSchema, UpdateDescriptionSchema, ConfluenceAddCommentSchema, RunJqlSchema, GetPersonWorklogSchema, GetIssueStatisticsSchema, validateOptions, IssueKeySchema, ProjectKeySchema, TimeframeSchema } from './lib/validation.js';
|
|
34
34
|
import { realpathSync } from 'fs';
|
|
35
35
|
// Create CLI program
|
|
36
36
|
const program = new Command();
|
|
@@ -94,78 +94,34 @@ program
|
|
|
94
94
|
.command('logout')
|
|
95
95
|
.description('Logout from all organizations')
|
|
96
96
|
.action(() => authCommand({ logout: true }));
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
.
|
|
106
|
-
org
|
|
107
|
-
.command('use <alias>')
|
|
108
|
-
.description('Switch the active Jira organization profile to the one specified by the alias.')
|
|
109
|
-
.action((alias) => useOrganizationCommand(alias));
|
|
110
|
-
org
|
|
111
|
-
.command('remove <alias>')
|
|
112
|
-
.description('Delete the saved credentials and profile for the specified organization alias.')
|
|
113
|
-
.action((alias) => removeOrganizationCommand(alias));
|
|
114
|
-
org
|
|
115
|
-
.command('add <alias>')
|
|
116
|
-
.description('Interactive prompt to add a new Jira organization profile with the given alias.')
|
|
117
|
-
.action((alias) => authCommand({ alias }));
|
|
118
|
-
// Me command
|
|
119
|
-
program
|
|
120
|
-
.command('me')
|
|
121
|
-
.description('Show profile details for the currently authenticated user, including Jira host, display name, email, account ID, status, and time zone.')
|
|
122
|
-
.action(withPermission('me', meCommand));
|
|
123
|
-
// Projects command
|
|
124
|
-
program
|
|
125
|
-
.command('projects')
|
|
126
|
-
.description('List all accessible Jira projects showing their key, name, ID, type, and project lead.')
|
|
127
|
-
.action(withPermission('projects', projectsCommand));
|
|
128
|
-
// List colleagues command
|
|
129
|
-
program
|
|
130
|
-
.command('list-colleagues [project-key]')
|
|
131
|
-
.description('Search and list users within the organization or a specific project (if project-key is provided). Returns display name, email, and account ID.')
|
|
132
|
-
.action(withPermission('list-colleagues', listColleaguesCommand, {
|
|
133
|
-
validateArgs: (args) => {
|
|
134
|
-
if (args[0]) {
|
|
135
|
-
validateOptions(ProjectKeySchema, args[0]);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}));
|
|
139
|
-
// Task with details command
|
|
140
|
-
program
|
|
141
|
-
.command('task-with-details <task-id>')
|
|
142
|
-
.description('Retrieve comprehensive issue data including key, summary, status (name, category), assignee, reporter, creation/update dates, due date, labels, parent/subtasks, description, and comments. Use --include-detailed-history to fetch a chronological log of all changes including field updates and status transitions.')
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// ISSUE COMMANDS
|
|
99
|
+
// =============================================================================
|
|
100
|
+
const issue = program
|
|
101
|
+
.command('issue')
|
|
102
|
+
.description('Manage Jira issues');
|
|
103
|
+
issue
|
|
104
|
+
.command('get <issue-id>')
|
|
105
|
+
.description('Retrieve comprehensive issue data including key, summary, status, assignee, reporter, dates, labels, description, and comments.')
|
|
143
106
|
.option('--include-detailed-history', 'Include the full history of task actions')
|
|
144
107
|
.option('--history-limit <number>', 'Number of history entries to show (default: 50)', '50')
|
|
145
108
|
.option('--history-offset <number>', 'Number of history entries to skip (default: 0)', '0')
|
|
146
|
-
.action(withPermission('
|
|
109
|
+
.action(withPermission('issue.get', taskWithDetailsCommand, {
|
|
147
110
|
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
148
111
|
}));
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.command('
|
|
159
|
-
.description('
|
|
160
|
-
.action(withPermission('list-issue-types', listIssueTypesCommand, {
|
|
161
|
-
validateArgs: (args) => validateOptions(ProjectKeySchema, args[0])
|
|
162
|
-
}));
|
|
163
|
-
// Run JQL command
|
|
164
|
-
program
|
|
165
|
-
.command('run-jql <jql-query>')
|
|
166
|
-
.description('Execute a Jira Query Language (JQL) search. Returns a list of issues with their key, summary, status, assignee, and priority. Supports limiting results via --limit (default 50).')
|
|
112
|
+
issue
|
|
113
|
+
.command('create')
|
|
114
|
+
.description('Create a new Jira issue with specified title, project key, and issue type.')
|
|
115
|
+
.requiredOption('--title <title>', 'Issue title/summary')
|
|
116
|
+
.requiredOption('--project <project>', 'Project key (e.g., PROJ)')
|
|
117
|
+
.requiredOption('--issue-type <type>', 'Issue type (e.g., Task, Epic, Subtask)')
|
|
118
|
+
.option('--parent <key>', 'Parent issue key (required for subtasks)')
|
|
119
|
+
.action(withPermission('issue.create', createTaskCommand, { schema: CreateTaskSchema }));
|
|
120
|
+
issue
|
|
121
|
+
.command('search <jql-query>')
|
|
122
|
+
.description('Execute a JQL search query. Returns issues with key, summary, status, assignee, and priority.')
|
|
167
123
|
.option('-l, --limit <number>', 'Maximum number of results (default: 50)', '50')
|
|
168
|
-
.action(withPermission('
|
|
124
|
+
.action(withPermission('issue.search', runJqlCommand, {
|
|
169
125
|
schema: RunJqlSchema,
|
|
170
126
|
validateArgs: (args) => {
|
|
171
127
|
if (typeof args[0] !== 'string' || args[0].trim() === '') {
|
|
@@ -173,27 +129,51 @@ program
|
|
|
173
129
|
}
|
|
174
130
|
}
|
|
175
131
|
}));
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.
|
|
179
|
-
.
|
|
132
|
+
issue
|
|
133
|
+
.command('transition <issue-id> <to-status>')
|
|
134
|
+
.description('Change the status of a Jira issue. The <to-status> can be either the status name or ID.')
|
|
135
|
+
.action(withPermission('issue.transition', transitionCommand, {
|
|
136
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
137
|
+
}));
|
|
138
|
+
issue
|
|
139
|
+
.command('update <issue-id>')
|
|
140
|
+
.description('Update a Jira issue\'s description using content from a local Markdown file.')
|
|
180
141
|
.requiredOption('--from-file <path>', 'Path to Markdown file')
|
|
181
|
-
.action(withPermission('update
|
|
142
|
+
.action(withPermission('issue.update', updateDescriptionCommand, {
|
|
182
143
|
schema: UpdateDescriptionSchema,
|
|
183
144
|
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
184
145
|
}));
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.
|
|
188
|
-
.
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.
|
|
146
|
+
issue
|
|
147
|
+
.command('comment <issue-id>')
|
|
148
|
+
.description('Add a new comment to a Jira issue using content from a local Markdown file.')
|
|
149
|
+
.requiredOption('--from-file <path>', 'Path to Markdown file')
|
|
150
|
+
.action(withPermission('issue.comment', (issueKey, options) => {
|
|
151
|
+
return addCommentCommand({ filePath: options.fromFile, issueKey });
|
|
152
|
+
}, {
|
|
153
|
+
schema: UpdateDescriptionSchema,
|
|
154
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
155
|
+
}));
|
|
156
|
+
issue
|
|
157
|
+
.command('stats <issue-ids>')
|
|
158
|
+
.description('Calculate time-based metrics for one or more issues (comma-separated). Shows time logged, estimates, and status breakdown.')
|
|
159
|
+
.option('--full-breakdown', 'Display each status in its own column')
|
|
160
|
+
.action(withPermission('issue.stats', getIssueStatisticsCommand, {
|
|
161
|
+
schema: GetIssueStatisticsSchema
|
|
162
|
+
}));
|
|
163
|
+
issue
|
|
164
|
+
.command('assign <issue-id> <account-id>')
|
|
165
|
+
.description('Assign or reassign a Jira issue to a user. Use "null" as account-id to unassign.')
|
|
166
|
+
.action(withPermission('issue.assign', issueAssignCommand, {
|
|
167
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
168
|
+
}));
|
|
169
|
+
// Issue label subcommands
|
|
170
|
+
const issueLabel = issue
|
|
171
|
+
.command('label')
|
|
172
|
+
.description('Manage issue labels');
|
|
173
|
+
issueLabel
|
|
174
|
+
.command('add <issue-id> <labels>')
|
|
175
|
+
.description('Add one or more labels (comma-separated) to a Jira issue.')
|
|
176
|
+
.action(withPermission('issue.label.add', addLabelCommand, {
|
|
197
177
|
validateArgs: (args) => {
|
|
198
178
|
validateOptions(IssueKeySchema, args[0]);
|
|
199
179
|
if (typeof args[1] !== 'string' || args[1].trim() === '') {
|
|
@@ -201,11 +181,10 @@ program
|
|
|
201
181
|
}
|
|
202
182
|
}
|
|
203
183
|
}));
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
.
|
|
207
|
-
.
|
|
208
|
-
.action(withPermission('delete-label-from-issue', deleteLabelCommand, {
|
|
184
|
+
issueLabel
|
|
185
|
+
.command('remove <issue-id> <labels>')
|
|
186
|
+
.description('Remove one or more labels (comma-separated) from a Jira issue.')
|
|
187
|
+
.action(withPermission('issue.label.remove', deleteLabelCommand, {
|
|
209
188
|
validateArgs: (args) => {
|
|
210
189
|
validateOptions(IssueKeySchema, args[0]);
|
|
211
190
|
if (typeof args[1] !== 'string' || args[1].trim() === '') {
|
|
@@ -213,83 +192,114 @@ program
|
|
|
213
192
|
}
|
|
214
193
|
}
|
|
215
194
|
}));
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
.
|
|
222
|
-
|
|
223
|
-
.
|
|
224
|
-
.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
.command('
|
|
228
|
-
.description('
|
|
229
|
-
.action(withPermission('
|
|
230
|
-
validateArgs: (args) => validateOptions(
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// PROJECT COMMANDS
|
|
197
|
+
// =============================================================================
|
|
198
|
+
const project = program
|
|
199
|
+
.command('project')
|
|
200
|
+
.description('Manage Jira projects');
|
|
201
|
+
project
|
|
202
|
+
.command('list')
|
|
203
|
+
.description('List all accessible Jira projects showing their key, name, ID, type, and project lead.')
|
|
204
|
+
.action(withPermission('project.list', projectsCommand));
|
|
205
|
+
project
|
|
206
|
+
.command('statuses <project-key>')
|
|
207
|
+
.description('Fetch all available workflow statuses for a project (To Do, In Progress, Done).')
|
|
208
|
+
.action(withPermission('project.statuses', projectStatusesCommand, {
|
|
209
|
+
validateArgs: (args) => validateOptions(ProjectKeySchema, args[0])
|
|
231
210
|
}));
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
.
|
|
235
|
-
.
|
|
236
|
-
|
|
237
|
-
.command('assign <task-id> <account-id>')
|
|
238
|
-
.description('Assign or reassign a Jira issue to a specific user using their Account ID. Use "null" as account-id to unassign.')
|
|
239
|
-
.action(withPermission('issue', issueAssignCommand, {
|
|
240
|
-
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
211
|
+
project
|
|
212
|
+
.command('types <project-key>')
|
|
213
|
+
.description('List all issue types (Standard and Subtask) available for a project.')
|
|
214
|
+
.action(withPermission('project.types', listIssueTypesCommand, {
|
|
215
|
+
validateArgs: (args) => validateOptions(ProjectKeySchema, args[0])
|
|
241
216
|
}));
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
.
|
|
247
|
-
.
|
|
248
|
-
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// USER COMMANDS
|
|
219
|
+
// =============================================================================
|
|
220
|
+
const user = program
|
|
221
|
+
.command('user')
|
|
222
|
+
.description('User information and worklogs');
|
|
223
|
+
user
|
|
224
|
+
.command('me')
|
|
225
|
+
.description('Show profile details for the currently authenticated user.')
|
|
226
|
+
.action(withPermission('user.me', meCommand));
|
|
227
|
+
user
|
|
228
|
+
.command('search [project-key]')
|
|
229
|
+
.description('Search and list users within the organization or a specific project.')
|
|
230
|
+
.action(withPermission('user.search', listColleaguesCommand, {
|
|
231
|
+
validateArgs: (args) => {
|
|
232
|
+
if (args[0]) {
|
|
233
|
+
validateOptions(ProjectKeySchema, args[0]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
249
236
|
}));
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
.
|
|
253
|
-
.description('Retrieve worklogs for a specific user over a timeframe (e.g., \'7d\', \'2w\'). Returns a list of entries with date, issue key, summary, time spent, and comments. Supports --group-by-issue.')
|
|
237
|
+
user
|
|
238
|
+
.command('worklog <person> <timeframe>')
|
|
239
|
+
.description('Retrieve worklogs for a user over a timeframe (e.g., "7d", "2w").')
|
|
254
240
|
.option('--group-by-issue', 'Group the output by issue')
|
|
255
|
-
.action(withPermission('
|
|
241
|
+
.action(withPermission('user.worklog', getPersonWorklogCommand, {
|
|
256
242
|
schema: GetPersonWorklogSchema,
|
|
257
243
|
validateArgs: (args) => {
|
|
258
244
|
validateOptions(TimeframeSchema, args[1]);
|
|
259
245
|
}
|
|
260
246
|
}));
|
|
261
|
-
//
|
|
247
|
+
// =============================================================================
|
|
248
|
+
// ORGANIZATION COMMANDS
|
|
249
|
+
// =============================================================================
|
|
250
|
+
const org = program
|
|
251
|
+
.command('org')
|
|
252
|
+
.alias('organization')
|
|
253
|
+
.description('Manage Jira organization profiles');
|
|
254
|
+
org
|
|
255
|
+
.command('list')
|
|
256
|
+
.description('List all saved Jira organization profiles.')
|
|
257
|
+
.action(() => listOrganizations());
|
|
258
|
+
org
|
|
259
|
+
.command('use <alias>')
|
|
260
|
+
.description('Switch the active Jira organization profile.')
|
|
261
|
+
.action((alias) => useOrganizationCommand(alias));
|
|
262
|
+
org
|
|
263
|
+
.command('remove <alias>')
|
|
264
|
+
.description('Delete credentials for the specified organization.')
|
|
265
|
+
.action((alias) => removeOrganizationCommand(alias));
|
|
266
|
+
org
|
|
267
|
+
.command('add <alias>')
|
|
268
|
+
.description('Add a new Jira organization profile.')
|
|
269
|
+
.action((alias) => authCommand({ alias }));
|
|
270
|
+
// =============================================================================
|
|
271
|
+
// CONFLUENCE COMMANDS
|
|
272
|
+
// =============================================================================
|
|
262
273
|
const confl = program
|
|
263
274
|
.command('confl')
|
|
275
|
+
.alias('confluence')
|
|
264
276
|
.description('Interact with Confluence pages and content');
|
|
265
277
|
confl
|
|
266
|
-
.command('get
|
|
278
|
+
.command('get <url>')
|
|
267
279
|
.description('Download and display Confluence page content and comments from a given URL.')
|
|
268
|
-
.action(withPermission('confl', confluenceGetPageCommand, { skipValidation: false }));
|
|
280
|
+
.action(withPermission('confl.get', confluenceGetPageCommand, { skipValidation: false }));
|
|
269
281
|
confl
|
|
270
282
|
.command('spaces')
|
|
271
283
|
.description('List all allowed Confluence spaces.')
|
|
272
|
-
.action(withPermission('confl', confluenceListSpacesCommand, { skipValidation: false }));
|
|
284
|
+
.action(withPermission('confl.spaces', confluenceListSpacesCommand, { skipValidation: false }));
|
|
273
285
|
confl
|
|
274
286
|
.command('pages <space-key>')
|
|
275
287
|
.description('Display a hierarchical tree view of pages within a specific space.')
|
|
276
|
-
.action(withPermission('confl', confluenceGetSpacePagesHierarchyCommand, { skipValidation: false }));
|
|
288
|
+
.action(withPermission('confl.pages', confluenceGetSpacePagesHierarchyCommand, { skipValidation: false }));
|
|
277
289
|
confl
|
|
278
|
-
.command('create
|
|
279
|
-
.description('Create a new Confluence page')
|
|
280
|
-
.action(withPermission('confl', confluenceCreatePageCommand, { skipValidation: false }));
|
|
290
|
+
.command('create <space> <title> [parent-page]')
|
|
291
|
+
.description('Create a new Confluence page.')
|
|
292
|
+
.action(withPermission('confl.create', confluenceCreatePageCommand, { skipValidation: false }));
|
|
281
293
|
confl
|
|
282
|
-
.command('
|
|
283
|
-
.
|
|
284
|
-
.description('Add a new comment to a Confluence page using content from a local Markdown file.')
|
|
294
|
+
.command('comment <url>')
|
|
295
|
+
.description('Add a comment to a Confluence page using content from a Markdown file.')
|
|
285
296
|
.option('-f, --from-file <path>', 'Path to the markdown file containing the comment content.')
|
|
286
|
-
.action(withPermission('confl', confluenceAddCommentCommand, { schema: ConfluenceAddCommentSchema }));
|
|
297
|
+
.action(withPermission('confl.comment', confluenceAddCommentCommand, { schema: ConfluenceAddCommentSchema }));
|
|
287
298
|
confl
|
|
288
|
-
.command('update
|
|
289
|
-
.
|
|
290
|
-
.description('Update the content of an existing Confluence page using a Markdown file.')
|
|
299
|
+
.command('update <url>')
|
|
300
|
+
.description('Update the content of a Confluence page using a Markdown file.')
|
|
291
301
|
.option('-f, --from-file <path>', 'Path to the markdown file containing the new content.')
|
|
292
|
-
.action(withPermission('confl', confluenceUpdateDescriptionCommand, { schema: UpdateDescriptionSchema }));
|
|
302
|
+
.action(withPermission('confl.update', confluenceUpdateDescriptionCommand, { schema: UpdateDescriptionSchema }));
|
|
293
303
|
// About command (always allowed)
|
|
294
304
|
program
|
|
295
305
|
.command('about')
|
|
@@ -298,7 +308,7 @@ program
|
|
|
298
308
|
// Settings command
|
|
299
309
|
program
|
|
300
310
|
.command('settings')
|
|
301
|
-
.description('View, validate, or apply configuration settings.
|
|
311
|
+
.description('View, validate, or apply configuration settings.')
|
|
302
312
|
.option('--apply <path>', 'Validate and apply settings from a YAML file')
|
|
303
313
|
.option('--validate <path>', 'Perform schema and deep validation of a settings YAML file')
|
|
304
314
|
.option('--reset', 'Revert settings to default')
|
|
@@ -308,32 +318,44 @@ Examples:
|
|
|
308
318
|
$ jira-ai settings --validate my-settings.yaml
|
|
309
319
|
$ jira-ai settings --apply my-settings.yaml
|
|
310
320
|
$ jira-ai settings --reset
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
321
|
+
|
|
322
|
+
Settings File Structure:
|
|
323
|
+
defaults:
|
|
324
|
+
allowed-jira-projects:
|
|
325
|
+
- all # Allow all projects
|
|
326
|
+
allowed-commands:
|
|
327
|
+
- all # Allow all commands
|
|
328
|
+
allowed-confluence-spaces:
|
|
329
|
+
- all # Allow all Confluence spaces
|
|
330
|
+
|
|
331
|
+
organizations:
|
|
332
|
+
work:
|
|
314
333
|
allowed-jira-projects:
|
|
315
|
-
-
|
|
334
|
+
- PROJ # Allow specific project
|
|
335
|
+
- key: PM # Project-specific config
|
|
336
|
+
commands:
|
|
337
|
+
- issue.get # Only allow reading issues
|
|
338
|
+
filters:
|
|
339
|
+
participated:
|
|
340
|
+
was_assignee: true
|
|
316
341
|
allowed-commands:
|
|
317
|
-
-
|
|
342
|
+
- issue # All issue commands
|
|
343
|
+
- project.list # Only project list
|
|
344
|
+
- user.me # Only user me
|
|
318
345
|
allowed-confluence-spaces:
|
|
319
|
-
-
|
|
346
|
+
- DOCS
|
|
347
|
+
|
|
348
|
+
Command Groups (use in allowed-commands):
|
|
349
|
+
issue - get, create, search, transition, update, comment, stats, assign, label
|
|
350
|
+
project - list, statuses, types
|
|
351
|
+
user - me, search, worklog
|
|
352
|
+
org - list, use, add, remove
|
|
353
|
+
confl - get, spaces, pages, create, comment, update
|
|
320
354
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
- key: PM # Project-specific configuration
|
|
326
|
-
commands: # Limit commands for this project
|
|
327
|
-
- task-with-details
|
|
328
|
-
filters:
|
|
329
|
-
participated: # Filter by user participation
|
|
330
|
-
was_assignee: true
|
|
331
|
-
was_reporter: true
|
|
332
|
-
allowed-commands:
|
|
333
|
-
- me
|
|
334
|
-
- projects
|
|
335
|
-
allowed-confluence-spaces:
|
|
336
|
-
- SPACE1 # Allow specific Confluence space
|
|
355
|
+
Examples:
|
|
356
|
+
- "issue" → allows all issue subcommands
|
|
357
|
+
- "issue.get" → allows only issue get
|
|
358
|
+
- "issue.label" → allows issue label add and remove
|
|
337
359
|
`)
|
|
338
360
|
.action((options) => settingsCommand(options));
|
|
339
361
|
/**
|
|
@@ -341,9 +363,10 @@ Examples:
|
|
|
341
363
|
*/
|
|
342
364
|
export function configureCommandVisibility(program) {
|
|
343
365
|
const isAuthorized = hasCredentials();
|
|
366
|
+
const alwaysVisibleCommands = ['auth', 'about', 'settings'];
|
|
344
367
|
if (!isAuthorized) {
|
|
345
368
|
program.commands.forEach(cmd => {
|
|
346
|
-
if (
|
|
369
|
+
if (!alwaysVisibleCommands.includes(cmd.name())) {
|
|
347
370
|
cmd._hidden = true;
|
|
348
371
|
}
|
|
349
372
|
});
|
|
@@ -351,8 +374,9 @@ export function configureCommandVisibility(program) {
|
|
|
351
374
|
}
|
|
352
375
|
else {
|
|
353
376
|
program.commands.forEach(cmd => {
|
|
354
|
-
|
|
355
|
-
|
|
377
|
+
if (!alwaysVisibleCommands.includes(cmd.name())) {
|
|
378
|
+
// For hierarchical commands, check if the group is allowed
|
|
379
|
+
// e.g., 'issue' command group is allowed if 'issue' or any 'issue.*' is allowed
|
|
356
380
|
if (!isCommandAllowed(cmd.name())) {
|
|
357
381
|
cmd._hidden = true;
|
|
358
382
|
}
|
package/dist/lib/settings.js
CHANGED
|
@@ -6,27 +6,44 @@ import chalk from 'chalk';
|
|
|
6
6
|
import { CliError } from '../types/errors.js';
|
|
7
7
|
import { SettingsSchema } from './validation.js';
|
|
8
8
|
import { getCurrentOrganizationAlias } from './auth-storage.js';
|
|
9
|
+
// Mapping from old flat command names to new hierarchical paths
|
|
10
|
+
export const LEGACY_COMMAND_MAP = {
|
|
11
|
+
'me': 'user.me',
|
|
12
|
+
'projects': 'project.list',
|
|
13
|
+
'task-with-details': 'issue.get',
|
|
14
|
+
'project-statuses': 'project.statuses',
|
|
15
|
+
'list-issue-types': 'project.types',
|
|
16
|
+
'list-colleagues': 'user.search',
|
|
17
|
+
'run-jql': 'issue.search',
|
|
18
|
+
'update-description': 'issue.update',
|
|
19
|
+
'add-comment': 'issue.comment',
|
|
20
|
+
'add-label-to-issue': 'issue.label.add',
|
|
21
|
+
'delete-label-from-issue': 'issue.label.remove',
|
|
22
|
+
'create-task': 'issue.create',
|
|
23
|
+
'transition': 'issue.transition',
|
|
24
|
+
'get-issue-statistics': 'issue.stats',
|
|
25
|
+
'get-person-worklog': 'user.worklog',
|
|
26
|
+
'organization': 'org',
|
|
27
|
+
'confluence': 'confl',
|
|
28
|
+
// Already hierarchical, keep as-is
|
|
29
|
+
'issue': 'issue',
|
|
30
|
+
'org': 'org',
|
|
31
|
+
'confl': 'confl'
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Migrate legacy command names to new hierarchical format
|
|
35
|
+
*/
|
|
36
|
+
export function migrateCommandNames(commands) {
|
|
37
|
+
return commands.map(cmd => LEGACY_COMMAND_MAP[cmd] || cmd);
|
|
38
|
+
}
|
|
9
39
|
export const DEFAULT_ORG_SETTINGS = {
|
|
10
40
|
'allowed-jira-projects': ['all'],
|
|
11
41
|
'allowed-commands': [
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'project-statuses',
|
|
18
|
-
'create-task',
|
|
19
|
-
'list-colleagues',
|
|
20
|
-
'add-comment',
|
|
21
|
-
'add-label-to-issue',
|
|
22
|
-
'delete-label-from-issue',
|
|
23
|
-
'get-issue-statistics',
|
|
24
|
-
'get-person-worklog',
|
|
25
|
-
'organization',
|
|
26
|
-
'transition',
|
|
27
|
-
'update-description',
|
|
28
|
-
'confluence',
|
|
29
|
-
'issue'
|
|
42
|
+
'issue', // All issue commands (get, create, search, transition, update, comment, stats, assign, label)
|
|
43
|
+
'project', // All project commands (list, statuses, types)
|
|
44
|
+
'user', // All user commands (me, search, worklog)
|
|
45
|
+
'org', // Organization management
|
|
46
|
+
'confl' // Confluence commands
|
|
30
47
|
],
|
|
31
48
|
'allowed-confluence-spaces': ['all']
|
|
32
49
|
};
|
|
@@ -41,6 +58,8 @@ export function getSettingsPath() {
|
|
|
41
58
|
}
|
|
42
59
|
export function migrateSettings(settings) {
|
|
43
60
|
// Migration logic: if old structure exists, move it to defaults
|
|
61
|
+
// Note: We keep command names as-is in storage (don't migrate them here)
|
|
62
|
+
// Command name migration happens at runtime in isCommandAllowed()
|
|
44
63
|
if (settings.projects || settings.commands) {
|
|
45
64
|
const migratedDefaults = {
|
|
46
65
|
'allowed-jira-projects': settings.projects || DEFAULT_ORG_SETTINGS['allowed-jira-projects'],
|
|
@@ -149,6 +168,24 @@ export function isProjectAllowed(projectKey, orgAlias) {
|
|
|
149
168
|
});
|
|
150
169
|
return isAllowed;
|
|
151
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if a command matches any allowed command using hierarchical matching.
|
|
173
|
+
* e.g., if 'issue' is allowed, then 'issue.get', 'issue.label.add' are all allowed.
|
|
174
|
+
* If 'issue.label' is allowed, then 'issue.label.add' and 'issue.label.remove' are allowed.
|
|
175
|
+
*/
|
|
176
|
+
function matchesHierarchicalCommand(commandPath, allowedCommands) {
|
|
177
|
+
// Check for 'all' permission
|
|
178
|
+
if (allowedCommands.includes('all'))
|
|
179
|
+
return true;
|
|
180
|
+
// Check hierarchical permissions (most specific to least specific)
|
|
181
|
+
const parts = commandPath.split('.');
|
|
182
|
+
for (let i = parts.length; i > 0; i--) {
|
|
183
|
+
const checkPath = parts.slice(0, i).join('.');
|
|
184
|
+
if (allowedCommands.includes(checkPath))
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
152
189
|
export function isCommandAllowed(commandName, projectKey, orgAlias) {
|
|
153
190
|
// about, auth, and settings are always allowed
|
|
154
191
|
if (['about', 'auth', 'settings'].includes(commandName)) {
|
|
@@ -157,31 +194,38 @@ export function isCommandAllowed(commandName, projectKey, orgAlias) {
|
|
|
157
194
|
const settings = getEffectiveSettings(orgAlias);
|
|
158
195
|
if (!settings)
|
|
159
196
|
return false;
|
|
197
|
+
// Normalize the command name being checked (convert legacy to new format)
|
|
198
|
+
const normalizedCommandName = LEGACY_COMMAND_MAP[commandName] || commandName;
|
|
199
|
+
// Migrate legacy command names in settings to new format for checking
|
|
200
|
+
const migratedAllowedCommands = migrateCommandNames(settings['allowed-commands']);
|
|
160
201
|
if (projectKey) {
|
|
161
202
|
let project = settings['allowed-jira-projects'].find(p => typeof p !== 'string' && p.key === projectKey);
|
|
162
203
|
if (!project) {
|
|
163
204
|
project = settings['allowed-jira-projects'].find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
|
|
164
205
|
}
|
|
165
206
|
if (project && typeof project !== 'string' && project.commands) {
|
|
166
|
-
|
|
207
|
+
const migratedProjectCommands = migrateCommandNames(project.commands);
|
|
208
|
+
return matchesHierarchicalCommand(normalizedCommandName, migratedProjectCommands);
|
|
167
209
|
}
|
|
168
210
|
}
|
|
169
211
|
else {
|
|
170
212
|
// For visibility/global check: allowed if in global list OR in any project-specific list
|
|
171
|
-
|
|
172
|
-
if (allowedGlobally) {
|
|
213
|
+
if (matchesHierarchicalCommand(normalizedCommandName, migratedAllowedCommands)) {
|
|
173
214
|
return true;
|
|
174
215
|
}
|
|
175
|
-
const allowedInAnyProject = settings['allowed-jira-projects'].some(p =>
|
|
216
|
+
const allowedInAnyProject = settings['allowed-jira-projects'].some(p => {
|
|
217
|
+
if (typeof p !== 'string' && p.commands) {
|
|
218
|
+
const migratedProjectCommands = migrateCommandNames(p.commands);
|
|
219
|
+
return matchesHierarchicalCommand(normalizedCommandName, migratedProjectCommands);
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
});
|
|
176
223
|
if (allowedInAnyProject) {
|
|
177
224
|
return true;
|
|
178
225
|
}
|
|
179
226
|
return false;
|
|
180
227
|
}
|
|
181
|
-
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
return settings['allowed-commands'].includes(commandName);
|
|
228
|
+
return matchesHierarchicalCommand(normalizedCommandName, migratedAllowedCommands);
|
|
185
229
|
}
|
|
186
230
|
export function isConfluenceSpaceAllowed(spaceKey, orgAlias) {
|
|
187
231
|
const settings = getEffectiveSettings(orgAlias);
|
package/dist/lib/validation.js
CHANGED
|
@@ -87,9 +87,22 @@ export const ProjectFiltersSchema = z.object({
|
|
|
87
87
|
}).optional(),
|
|
88
88
|
jql: z.string().optional(),
|
|
89
89
|
});
|
|
90
|
+
/**
|
|
91
|
+
* Schema for hierarchical command names (e.g., "issue", "issue.get", "issue.label.add")
|
|
92
|
+
* Also accepts 'all' for allowing all commands
|
|
93
|
+
*/
|
|
94
|
+
export const HierarchicalCommandSchema = z.string().regex(/^(all|[a-z]+(\.[a-z]+)*)$/, 'Command must be "all" or lowercase dot-separated (e.g., "issue", "issue.get", "issue.label.add")');
|
|
95
|
+
// Default allowed commands using hierarchical structure
|
|
96
|
+
const DEFAULT_ALLOWED_COMMANDS = [
|
|
97
|
+
'issue', // All issue commands
|
|
98
|
+
'project', // All project commands
|
|
99
|
+
'user', // All user commands
|
|
100
|
+
'org', // Organization management
|
|
101
|
+
'confl' // Confluence commands
|
|
102
|
+
];
|
|
90
103
|
export const ProjectConfigSchema = z.object({
|
|
91
104
|
key: z.string().trim().min(1),
|
|
92
|
-
commands: z.array(z.string()).optional(),
|
|
105
|
+
commands: z.array(z.string()).optional(), // Can be legacy or hierarchical
|
|
93
106
|
filters: ProjectFiltersSchema.optional(),
|
|
94
107
|
});
|
|
95
108
|
export const ProjectSettingSchema = z.union([
|
|
@@ -98,7 +111,7 @@ export const ProjectSettingSchema = z.union([
|
|
|
98
111
|
]);
|
|
99
112
|
export const OrganizationSettingsSchema = z.object({
|
|
100
113
|
'allowed-jira-projects': z.array(ProjectSettingSchema).nullish().transform(val => val || ['all']),
|
|
101
|
-
'allowed-commands': z.array(z.string()).nullish().transform(val => val ||
|
|
114
|
+
'allowed-commands': z.array(z.string()).nullish().transform(val => val || DEFAULT_ALLOWED_COMMANDS),
|
|
102
115
|
'allowed-confluence-spaces': z.array(z.string()).nullish().transform(val => val || ['all']),
|
|
103
116
|
});
|
|
104
117
|
export const SettingsSchema = z.object({
|