jira-ai 1.3.0 → 1.4.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 CHANGED
@@ -48,6 +48,7 @@ program
48
48
  .description('CLI tool for interacting with Atlassian Jira')
49
49
  .version(getVersion())
50
50
  .option('--compact', 'Output as compact JSON')
51
+ .option('--dry-run', 'Preview changes without executing them')
51
52
  .addHelpText('after', () => {
52
53
  const latestVersion = checkForUpdateSync();
53
54
  if (latestVersion) {
@@ -6,6 +6,7 @@ import { CommandError } from '../lib/errors.js';
6
6
  import { validateOptions, CreateTaskSchema } from '../lib/validation.js';
7
7
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
8
8
  import { outputResult } from '../lib/json-mode.js';
9
+ import { isDryRun, formatDryRunResult } from '../lib/dry-run.js';
9
10
  export async function createTaskCommand(options) {
10
11
  validateOptions(CreateTaskSchema, options);
11
12
  const { title, project, issueType, parent, priority, description, descriptionFile, labels, component, fixVersion, dueDate, assignee, customField } = options;
@@ -74,6 +75,17 @@ export async function createTaskCommand(options) {
74
75
  issueFields[fieldId] = isNaN(numValue) ? rawValue : numValue;
75
76
  }
76
77
  }
78
+ if (isDryRun()) {
79
+ const changes = {
80
+ project,
81
+ summary: title,
82
+ issueType,
83
+ ...(parent !== undefined ? { parent } : {}),
84
+ ...issueFields,
85
+ };
86
+ formatDryRunResult('issue.create', project, changes);
87
+ return;
88
+ }
77
89
  try {
78
90
  const result = await createIssue({
79
91
  project,
@@ -5,13 +5,14 @@ import { outputResult } from '../lib/json-mode.js';
5
5
  import { markdownToAdf } from 'marklassian';
6
6
  import { FieldResolver } from '../lib/field-resolver.js';
7
7
  import { processMentionsInADF } from '../lib/adf-mentions.js';
8
+ import { isDryRun, formatDryRunResult } from '../lib/dry-run.js';
8
9
  export async function transitionCommand(taskId, toStatus, options) {
9
10
  // Validate mutual exclusivity of --comment and --comment-file
10
11
  if (options?.comment && options?.commentFile) {
11
12
  throw new CommandError('Cannot use both --comment and --comment-file flags simultaneously.');
12
13
  }
13
14
  // Check permissions and filters
14
- await validateIssuePermissions(taskId, 'transition');
15
+ const currentIssue = await validateIssuePermissions(taskId, 'transition');
15
16
  try {
16
17
  const transitions = await getIssueTransitions(taskId);
17
18
  const matchingTransitions = transitions.filter((t) => t.to.name.toLowerCase() === toStatus.toLowerCase());
@@ -34,6 +35,35 @@ export async function transitionCommand(taskId, toStatus, options) {
34
35
  });
35
36
  }
36
37
  const transition = matchingTransitions[0];
38
+ if (isDryRun()) {
39
+ const changes = {
40
+ status: {
41
+ from: currentIssue?.status?.name,
42
+ to: transition.to.name,
43
+ },
44
+ };
45
+ if (options?.resolution !== undefined) {
46
+ changes.resolution = { to: options.resolution };
47
+ }
48
+ if (options?.assignee !== undefined) {
49
+ changes.assignee = { to: options.assignee };
50
+ }
51
+ if (options?.fixVersion !== undefined) {
52
+ changes.fixVersions = { to: options.fixVersion };
53
+ }
54
+ if (options?.customFields && options.customFields.length > 0) {
55
+ for (const entry of options.customFields) {
56
+ const eqIdx = entry.indexOf('=');
57
+ if (eqIdx === -1)
58
+ continue;
59
+ const fieldId = entry.slice(0, eqIdx).trim();
60
+ const value = entry.slice(eqIdx + 1).trim();
61
+ changes[fieldId] = { to: value };
62
+ }
63
+ }
64
+ formatDryRunResult('issue.transition', taskId, changes);
65
+ return;
66
+ }
37
67
  // Build optional payload if any options were provided
38
68
  let payload;
39
69
  if (options && Object.keys(options).some(k => options[k] !== undefined)) {
@@ -6,6 +6,7 @@ import { CommandError } from '../lib/errors.js';
6
6
  import { validateOptions, UpdateIssueSchema, IssueKeySchema } from '../lib/validation.js';
7
7
  import { isCommandAllowed, isProjectAllowed } from '../lib/settings.js';
8
8
  import { outputResult } from '../lib/json-mode.js';
9
+ import { isDryRun, formatDryRunResult } from '../lib/dry-run.js';
9
10
  export async function updateIssueCommand(issueKey, options) {
10
11
  validateOptions(IssueKeySchema, issueKey);
11
12
  validateOptions(UpdateIssueSchema, options);
@@ -16,7 +17,47 @@ export async function updateIssueCommand(issueKey, options) {
16
17
  if (!isCommandAllowed('update-issue', projectKey)) {
17
18
  throw new CommandError(`Command 'update-issue' is not allowed for project ${projectKey}.`);
18
19
  }
19
- await validateIssuePermissions(issueKey, 'update-issue');
20
+ const currentIssue = await validateIssuePermissions(issueKey, 'update-issue');
21
+ if (isDryRun()) {
22
+ const changes = {};
23
+ if (options.priority !== undefined) {
24
+ changes.priority = { from: currentIssue?.priority, to: options.priority };
25
+ }
26
+ if (options.summary !== undefined) {
27
+ changes.summary = { from: currentIssue?.summary, to: options.summary };
28
+ }
29
+ if (options.labels !== undefined || options.clearLabels) {
30
+ const newLabels = options.clearLabels ? [] : options.labels?.split(',').map(l => l.trim()).filter(Boolean);
31
+ changes.labels = { from: currentIssue?.labels, to: newLabels };
32
+ }
33
+ if (options.component !== undefined) {
34
+ changes.components = { from: currentIssue?.components, to: options.component };
35
+ }
36
+ if (options.fixVersion !== undefined) {
37
+ changes.fixVersions = { from: currentIssue?.fixVersions, to: options.fixVersion };
38
+ }
39
+ if (options.dueDate !== undefined) {
40
+ changes.dueDate = { from: currentIssue?.duedate, to: options.dueDate };
41
+ }
42
+ if (options.assignee !== undefined) {
43
+ changes.assignee = { from: currentIssue?.assignee?.displayName, to: options.assignee };
44
+ }
45
+ if (options.description !== undefined || options.fromFile !== undefined) {
46
+ changes.description = { from: '(current)', to: '(updated)' };
47
+ }
48
+ if (options.customField && options.customField.length > 0) {
49
+ for (const cf of options.customField) {
50
+ const eqIdx = cf.indexOf('=');
51
+ if (eqIdx === -1)
52
+ continue;
53
+ const fieldId = cf.slice(0, eqIdx);
54
+ const value = cf.slice(eqIdx + 1);
55
+ changes[fieldId] = { to: value };
56
+ }
57
+ }
58
+ formatDryRunResult('issue.update', issueKey, changes);
59
+ return;
60
+ }
20
61
  const fields = {};
21
62
  if (options.priority !== undefined) {
22
63
  if (!options.priority)
@@ -0,0 +1,15 @@
1
+ import { outputResult } from './json-mode.js';
2
+ export function isDryRun() {
3
+ return process.argv.includes('--dry-run');
4
+ }
5
+ export function formatDryRunResult(command, target, changes, preview = {}) {
6
+ const result = {
7
+ dryRun: true,
8
+ command,
9
+ target,
10
+ changes,
11
+ preview,
12
+ message: 'No changes were made. Remove --dry-run to execute.',
13
+ };
14
+ outputResult(result);
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",