@stravica-ai/digital-operator-pa 0.1.39 → 0.1.41

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.
Files changed (92) hide show
  1. package/README.md +12 -2
  2. package/bin/confluence +9 -0
  3. package/bin/confluence-fetch +9 -0
  4. package/bin/jira +9 -0
  5. package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +1 -1
  6. package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +3 -3
  7. package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-extensions.md +62 -0
  8. package/dist/engineering-pa-core-extensions-atlassian/README.md +81 -16
  9. package/dist/engineering-pa-core-extensions-atlassian/bin/confluence +2 -0
  10. package/dist/engineering-pa-core-extensions-atlassian/bin/confluence-fetch +2 -0
  11. package/dist/engineering-pa-core-extensions-atlassian/bin/jira +2 -0
  12. package/dist/engineering-pa-core-extensions-atlassian/config/jira-workflow-chains.default.json +4 -0
  13. package/dist/engineering-pa-core-extensions-atlassian/index.mjs +17 -6
  14. package/dist/engineering-pa-core-extensions-atlassian/package.json +11 -4
  15. package/dist/engineering-pa-core-extensions-atlassian/src/common/invalidate-client.mjs +137 -0
  16. package/dist/engineering-pa-core-extensions-atlassian/src/common/pa-name.mjs +58 -0
  17. package/dist/engineering-pa-core-extensions-atlassian/src/common/secrets.mjs +158 -0
  18. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs +165 -0
  19. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/client.mjs +294 -0
  20. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/auth.mjs +59 -0
  21. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/comment.mjs +174 -0
  22. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/create.mjs +136 -0
  23. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/label.mjs +98 -0
  24. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mine.mjs +105 -0
  25. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +487 -0
  26. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-page.mjs +351 -0
  27. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/page.mjs +127 -0
  28. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/pages.mjs +107 -0
  29. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/recent.mjs +107 -0
  30. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/search.mjs +123 -0
  31. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/spaces.mjs +82 -0
  32. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/test.mjs +155 -0
  33. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/tree.mjs +103 -0
  34. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/update.mjs +134 -0
  35. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/format.mjs +260 -0
  36. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/mirror.mjs +353 -0
  37. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/storage.mjs +880 -0
  38. package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs +313 -0
  39. package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/fetch.mjs +327 -0
  40. package/dist/engineering-pa-core-extensions-atlassian/src/jira/adf.mjs +388 -0
  41. package/dist/engineering-pa-core-extensions-atlassian/src/jira/bug-fields.mjs +294 -0
  42. package/dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs +197 -0
  43. package/dist/engineering-pa-core-extensions-atlassian/src/jira/client.mjs +350 -0
  44. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/advance.mjs +486 -0
  45. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/attach.mjs +129 -0
  46. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/auth.mjs +53 -0
  47. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/boards.mjs +88 -0
  48. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/bulk-transition.mjs +223 -0
  49. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/cancel.mjs +135 -0
  50. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/comment.mjs +169 -0
  51. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/create.mjs +376 -0
  52. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/issue.mjs +105 -0
  53. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +204 -0
  54. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/mine.mjs +97 -0
  55. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/projects.mjs +91 -0
  56. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/search.mjs +80 -0
  57. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sprint.mjs +111 -0
  58. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +313 -0
  59. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/test.mjs +142 -0
  60. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/transition.mjs +197 -0
  61. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/update.mjs +284 -0
  62. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/worklog.mjs +380 -0
  63. package/dist/engineering-pa-core-extensions-atlassian/src/jira/format.mjs +267 -0
  64. package/dist/engineering-pa-core-extensions-atlassian/src/jira/invalidate.mjs +72 -0
  65. package/dist/engineering-pa-core-extensions-atlassian/src/jira/metadata.mjs +225 -0
  66. package/dist/engineering-pa-core-extensions-atlassian/src/jira/required-fields.mjs +334 -0
  67. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +405 -0
  68. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +107 -0
  69. package/dist/engineering-pa-scaffold/README.md +32 -1
  70. package/dist/engineering-pa-scaffold/src/cli.mjs +83 -6
  71. package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +234 -0
  72. package/dist/engineering-pa-scaffold/src/commands/init.mjs +6 -5
  73. package/dist/engineering-pa-scaffold/src/generator.mjs +240 -4
  74. package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +7 -7
  75. package/dist/engineering-pa-scaffold/templates/operator-README.md.template +4 -4
  76. package/dist/engineering-pa-scaffold/templates/root-CLAUDE.md.template +13 -0
  77. package/dist/engineering-pa-types-engineering/index.mjs +14 -6
  78. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/create-a-small-app.md +81 -0
  79. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/for-agents.md +25 -0
  80. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/index.md +34 -0
  81. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/reference.md +16 -0
  82. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/wire-your-agent.md +17 -0
  83. package/dist/engineering-pa-types-engineering/package.json +2 -2
  84. package/dist/engineering-pa-types-engineering/roles/rcf-lite-engineer/definition.md +183 -0
  85. package/dist/engineering-pa-workspace/app/home.html +7 -7
  86. package/dist/engineering-pa-workspace/app/onboarding.html +4 -4
  87. package/dist/engineering-pa-workspace/src/api.mjs +1 -1
  88. package/dist/engineering-pa-workspace/src/server.mjs +1 -1
  89. package/docs/installing.md +78 -0
  90. package/docs/onboarding-solo.md +162 -0
  91. package/docs/what-it-can-do.md +85 -0
  92. package/package.json +6 -2
@@ -0,0 +1,88 @@
1
+ // boards.mjs - List Jira boards.
2
+ //
3
+ // Usage:
4
+ // jira boards [--project KEY]
5
+ //
6
+ // Fetches boards via offset pagination from the Agile API.
7
+ // Optional --project filters server-side by project key or ID.
8
+ // Exit 0 on success, 2 on API error.
9
+
10
+ import { createClient, JiraError } from '../client.mjs';
11
+ import { formatTable, bold, dim } from '../format.mjs';
12
+
13
+ const HELP = `jira boards - List Jira boards
14
+
15
+ Usage:
16
+ jira boards [--project KEY]
17
+
18
+ Options:
19
+ --project KEY Filter boards by project key or ID (server-side)
20
+ --help, -h Show this help
21
+ `;
22
+
23
+ /**
24
+ * Run the boards command.
25
+ *
26
+ * @param {string[]} argv Remaining args after 'boards'
27
+ * @param {object} [deps] Dependency overrides
28
+ * @returns {Promise<number>} Exit code
29
+ */
30
+ export async function run(argv = [], deps = {}) {
31
+ const log = deps.log || console.log;
32
+ const logError = deps.logError || console.error;
33
+
34
+ if (argv.includes('--help') || argv.includes('-h')) {
35
+ log(HELP);
36
+ return 0;
37
+ }
38
+
39
+ // Parse --project
40
+ let projectKey = null;
41
+ const projIdx = argv.indexOf('--project');
42
+ if (projIdx !== -1 && projIdx + 1 < argv.length) {
43
+ projectKey = argv[projIdx + 1];
44
+ }
45
+
46
+ const client = deps.client || createClient();
47
+
48
+ try {
49
+ const params = {};
50
+ if (projectKey) {
51
+ params.projectKeyOrId = projectKey;
52
+ }
53
+
54
+ const boards = [];
55
+ for await (const board of client.paginateOffset('/rest/agile/1.0/board', params)) {
56
+ boards.push(board);
57
+ }
58
+
59
+ if (boards.length === 0) {
60
+ log(dim(projectKey ? `No boards found for project ${projectKey}.` : 'No boards found.'));
61
+ return 0;
62
+ }
63
+
64
+ const rows = boards.map((b) => ({
65
+ id: String(b.id || ''),
66
+ name: b.name || '',
67
+ type: b.type || '',
68
+ project: b.location?.projectKey || b.location?.name || '',
69
+ }));
70
+
71
+ const label = projectKey ? `Boards for ${projectKey}` : 'Boards';
72
+ log(bold(label) + dim(` (${boards.length})\n`));
73
+ log(formatTable(rows, [
74
+ { key: 'id', label: 'ID', width: 8, align: 'right' },
75
+ { key: 'name', label: 'Name', width: 40 },
76
+ { key: 'type', label: 'Type', width: 12 },
77
+ { key: 'project', label: 'Project', width: 16 },
78
+ ]));
79
+
80
+ return 0;
81
+ } catch (err) {
82
+ if (err instanceof JiraError) {
83
+ logError(`Failed to list boards: ${err.message}`);
84
+ return err.exitCode;
85
+ }
86
+ throw err;
87
+ }
88
+ }
@@ -0,0 +1,223 @@
1
+ // bulk-transition.mjs - Bulk-transition Jira issues via the Bulk Transition API.
2
+ //
3
+ // Usage:
4
+ // jira bulk-transition --jql "project = AI AND status = Open" --to "Cancelled"
5
+ // jira bulk-transition --jql "..." --to "Cancelled" --dry-run
6
+ //
7
+ // Flow:
8
+ // 1. JQL search to collect issue IDs
9
+ // 2. GET /rest/api/3/bulk/issues/transition - available common transitions
10
+ // 3. Match target status name to a transition ID
11
+ // 4. POST /rest/api/3/bulk/issues/transition - execute bulk transition
12
+ // 5. Poll GET /rest/api/3/task/{taskId} until complete
13
+ // 6. Report results
14
+ //
15
+ // --dry-run stops after step 3 (shows what would happen without executing).
16
+ //
17
+ // Exit 0 on success, 1 on usage error, 2 on API/logical error.
18
+
19
+ import { createClient, JiraError } from '../client.mjs';
20
+ import { bold, green, yellow, red, dim, cyan } from '../format.mjs';
21
+
22
+ const HELP = `jira bulk-transition - Bulk-transition Jira issues
23
+
24
+ Usage:
25
+ jira bulk-transition --jql <JQL> --to <STATUS>
26
+
27
+ Options:
28
+ --jql <query> JQL to select issues (required)
29
+ --to <status> Target status name (required, case-insensitive match)
30
+ --dry-run Show what would happen without executing the transition
31
+ --help, -h Show this help
32
+
33
+ Flow:
34
+ 1. Search for issues matching the JQL
35
+ 2. Fetch available bulk transitions for those issues
36
+ 3. Match the target status to a transition
37
+ 4. Execute the bulk transition (skipped with --dry-run)
38
+ 5. Poll for completion and report results
39
+
40
+ Examples:
41
+ jira bulk-transition --jql "project = AI AND status = Open" --to "Cancelled"
42
+ jira bulk-transition --jql "project = AI AND status != Done" --to Done --dry-run
43
+ `;
44
+
45
+ // Default poll interval and max attempts for task completion
46
+ const POLL_INTERVAL_MS = 2000;
47
+ const MAX_POLL_ATTEMPTS = 60; // 2 minutes at 2s intervals
48
+
49
+ /**
50
+ * Run the bulk-transition command.
51
+ *
52
+ * @param {string[]} argv Remaining args after 'bulk-transition'
53
+ * @param {object} [deps] Dependency overrides
54
+ * @returns {Promise<number>} Exit code
55
+ */
56
+ export async function run(argv = [], deps = {}) {
57
+ const log = deps.log || console.log;
58
+ const logError = deps.logError || console.error;
59
+ const { parseArgs } = await import('../cli.mjs');
60
+ const args = parseArgs(argv);
61
+
62
+ if (args.help || args.h) {
63
+ log(HELP);
64
+ return 0;
65
+ }
66
+
67
+ const jql = args.jql;
68
+ const targetStatus = args.to;
69
+ const dryRun = !!args['dry-run'];
70
+
71
+ if (!jql || !targetStatus) {
72
+ logError('Usage: jira bulk-transition --jql <JQL> --to <STATUS>');
73
+ logError('Run "jira bulk-transition --help" for details.');
74
+ return 1;
75
+ }
76
+
77
+ const client = deps.client || createClient();
78
+ const sleep = deps.sleep || defaultSleep;
79
+ const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS;
80
+ const maxPollAttempts = deps.maxPollAttempts ?? MAX_POLL_ATTEMPTS;
81
+
82
+ try {
83
+ // Step 1: JQL search to collect issue IDs
84
+ log(dim(`Searching: ${jql}`));
85
+
86
+ const issueIds = [];
87
+ const issueKeys = [];
88
+ for await (const issue of client.paginateToken(
89
+ '/rest/api/3/search/jql',
90
+ { jql, fields: ['summary', 'status'] }
91
+ )) {
92
+ issueIds.push(issue.id);
93
+ issueKeys.push(issue.key);
94
+ }
95
+
96
+ if (issueIds.length === 0) {
97
+ log(yellow('No issues matched the JQL query.'));
98
+ return 0;
99
+ }
100
+
101
+ log(`Found ${bold(String(issueIds.length))} issue(s): ${issueKeys.slice(0, 10).join(', ')}${issueIds.length > 10 ? ` ... (+${issueIds.length - 10} more)` : ''}`);
102
+
103
+ // Step 2: GET available bulk transitions
104
+ const idsParam = issueIds.join(',');
105
+ const { data: transData } = await client.get(
106
+ `/rest/api/3/bulk/issues/transition?issueIds=${encodeURIComponent(idsParam)}`
107
+ );
108
+
109
+ const transitions = transData.transitions || [];
110
+ if (transitions.length === 0) {
111
+ logError(red('No common transitions available for the selected issues.'));
112
+ logError(dim('This can happen when issues span different workflows or are already in a terminal state.'));
113
+ return 2;
114
+ }
115
+
116
+ // Step 3: Match target status to a transition
117
+ const target = targetStatus.toLowerCase();
118
+ const matches = transitions.filter((t) => {
119
+ const name = (t.name || '').toLowerCase();
120
+ return name === target || name.includes(target);
121
+ });
122
+
123
+ let matched;
124
+ if (matches.length === 0) {
125
+ logError(`No transition matching "${targetStatus}" found.`);
126
+ logError('Available transitions:');
127
+ for (const t of transitions) {
128
+ logError(` - ${t.name} (id: ${t.id})`);
129
+ }
130
+ return 1;
131
+ } else if (matches.length > 1) {
132
+ // Prefer exact match
133
+ const exact = matches.find((t) => (t.name || '').toLowerCase() === target);
134
+ if (exact) {
135
+ matched = exact;
136
+ } else {
137
+ logError(`Ambiguous transition "${targetStatus}" - multiple matches:`);
138
+ for (const t of matches) {
139
+ logError(` - ${t.name} (id: ${t.id})`);
140
+ }
141
+ return 1;
142
+ }
143
+ } else {
144
+ matched = matches[0];
145
+ }
146
+
147
+ log(`Matched transition: ${bold(matched.name)} (id: ${matched.id})`);
148
+
149
+ // Dry run stops here
150
+ if (dryRun) {
151
+ log(yellow(`\n[DRY RUN] Would transition ${issueIds.length} issue(s) to "${matched.name}".`));
152
+ log(dim('No changes have been made. Remove --dry-run to execute.'));
153
+ return 0;
154
+ }
155
+
156
+ // Step 4: POST bulk transition
157
+ log(dim(`Executing bulk transition to "${matched.name}"...`));
158
+
159
+ const { data: postData } = await client.post(
160
+ '/rest/api/3/bulk/issues/transition',
161
+ { issueIds, transitionId: matched.id }
162
+ );
163
+
164
+ const taskId = postData.taskId;
165
+ if (!taskId) {
166
+ logError(red('Bulk transition API did not return a taskId.'));
167
+ return 2;
168
+ }
169
+
170
+ log(dim(`Task ID: ${taskId} - polling for completion...`));
171
+
172
+ // Step 5: Poll for completion
173
+ let taskResult = null;
174
+ for (let attempt = 0; attempt < maxPollAttempts; attempt++) {
175
+ await sleep(pollIntervalMs);
176
+
177
+ const { data: taskData } = await client.get(
178
+ `/rest/api/3/task/${encodeURIComponent(taskId)}`
179
+ );
180
+
181
+ if (taskData.status === 'COMPLETE' || taskData.status === 'DEAD' || taskData.status === 'FAILED') {
182
+ taskResult = taskData;
183
+ break;
184
+ }
185
+
186
+ // Show progress if available
187
+ if (taskData.progress !== undefined) {
188
+ log(dim(` Progress: ${taskData.progress}%`));
189
+ }
190
+ }
191
+
192
+ if (!taskResult) {
193
+ logError(yellow('Timed out waiting for bulk transition to complete.'));
194
+ logError(dim(`Task ${taskId} may still be running. Check Jira or poll manually.`));
195
+ return 2;
196
+ }
197
+
198
+ // Step 6: Report results
199
+ if (taskResult.status === 'COMPLETE') {
200
+ log(green(`\nBulk transition complete: ${issueIds.length} issue(s) transitioned to "${matched.name}".`));
201
+ } else {
202
+ logError(red(`\nBulk transition ${taskResult.status.toLowerCase()}.`));
203
+ if (taskResult.message) {
204
+ logError(dim(taskResult.message));
205
+ }
206
+ return 2;
207
+ }
208
+
209
+ return 0;
210
+ } catch (err) {
211
+ if (err instanceof JiraError) {
212
+ logError(`jira bulk-transition: ${err.message}`);
213
+ return err.exitCode;
214
+ }
215
+ throw err;
216
+ }
217
+ }
218
+
219
+ // ── Helpers ──────────────────────────────────────────────────────────
220
+
221
+ function defaultSleep(ms) {
222
+ return new Promise((r) => setTimeout(r, ms));
223
+ }
@@ -0,0 +1,135 @@
1
+ // cancel.mjs - Convenience wrapper that transitions an issue to "Cancelled".
2
+ //
3
+ // Equivalent to `jira transition <KEY> Cancelled`, but with a clearer
4
+ // error message when the current workflow state has no Cancelled transition:
5
+ // exit code 2 (logical), the available transitions are listed, and (when
6
+ // recognisable) a likely terminal alternative is suggested.
7
+
8
+ import { createClient, JiraError } from '../client.mjs';
9
+ import { bold, green, yellow, dim } from '../format.mjs';
10
+ import { notifyWorkspaceAsync } from '../invalidate.mjs';
11
+
12
+ const HELP = `jira cancel - Transition an issue to Cancelled
13
+
14
+ Usage:
15
+ jira cancel <KEY>
16
+
17
+ Arguments:
18
+ KEY Issue key (e.g. DO-1391)
19
+
20
+ Wraps the Cancelled transition. If the issue's current workflow state has
21
+ no Cancelled transition available, exits with code 2 and lists the
22
+ transitions that are available so you can pick the right one.
23
+
24
+ Examples:
25
+ jira cancel DO-1391
26
+ `;
27
+
28
+ // Names that indicate a "terminal / abandoned" outcome - used to suggest an
29
+ // alternative when Cancelled itself isn't reachable from the current state.
30
+ const TERMINAL_HINTS = [
31
+ 'cancelled', 'canceled', 'abandoned', 'rejected', 'closed', 'won\'t do',
32
+ "won't do", 'wontfix', 'won\'t fix', "won't fix", 'declined',
33
+ ];
34
+
35
+ /**
36
+ * Run the cancel command.
37
+ *
38
+ * @param {string[]} argv Remaining args after 'cancel'
39
+ * @param {object} [deps] Dependency overrides
40
+ * @returns {Promise<number>} Exit code
41
+ */
42
+ export async function run(argv = [], deps = {}) {
43
+ const log = deps.log || console.log;
44
+ const logError = deps.logError || console.error;
45
+ const { parseArgs } = await import('../cli.mjs');
46
+ const args = parseArgs(argv);
47
+
48
+ if (args.help || args.h) {
49
+ log(HELP);
50
+ return 0;
51
+ }
52
+
53
+ const key = args._[0];
54
+ if (!key) {
55
+ logError('Usage: jira cancel <KEY>');
56
+ return 1;
57
+ }
58
+
59
+ const client = deps.client || createClient();
60
+
61
+ try {
62
+ // Current status (for the success message)
63
+ const { data: issueData } = await client.get(
64
+ `/rest/api/3/issue/${encodeURIComponent(key)}?fields=status`
65
+ );
66
+ const currentStatus = issueData?.fields?.status?.name || 'Unknown';
67
+
68
+ // Available transitions
69
+ const { data: transData } = await client.get(
70
+ `/rest/api/3/issue/${encodeURIComponent(key)}/transitions`
71
+ );
72
+ const transitions = transData?.transitions || [];
73
+
74
+ if (transitions.length === 0) {
75
+ throw new JiraError(
76
+ `No transitions available for ${key} (current status: ${currentStatus})`,
77
+ 2
78
+ );
79
+ }
80
+
81
+ // Look for a "Cancelled"-named transition (case-insensitive exact match).
82
+ const target = 'cancelled';
83
+ const exact = transitions.find(
84
+ (t) => (t.name || '').toLowerCase() === target
85
+ );
86
+
87
+ if (!exact) {
88
+ // Build a helpful error: list the available transitions and, if any
89
+ // look terminal/abandoned, surface them as suggestions.
90
+ const names = transitions.map((t) => t.name).filter(Boolean);
91
+ const suggestions = names.filter((n) =>
92
+ TERMINAL_HINTS.includes(n.toLowerCase())
93
+ );
94
+
95
+ logError(
96
+ yellow(
97
+ `No "Cancelled" transition available for ${key} ` +
98
+ `(current status: ${currentStatus}).`
99
+ )
100
+ );
101
+ logError('Available transitions:');
102
+ for (const t of transitions) {
103
+ logError(` - ${t.name} (id: ${t.id})`);
104
+ }
105
+ if (suggestions.length > 0) {
106
+ logError(
107
+ dim(
108
+ `\nLikely alternative${suggestions.length > 1 ? 's' : ''}: ` +
109
+ suggestions.map((s) => `jira transition ${key} "${s}"`).join(' | ')
110
+ )
111
+ );
112
+ }
113
+ throw new JiraError(
114
+ `Cancelled is not an available transition for ${key} from "${currentStatus}"`,
115
+ 2
116
+ );
117
+ }
118
+
119
+ // Execute
120
+ await client.post(
121
+ `/rest/api/3/issue/${encodeURIComponent(key)}/transitions`,
122
+ { transition: { id: exact.id } }
123
+ );
124
+
125
+ log(green(`${bold(key)}: ${currentStatus} → ${exact.name}`));
126
+ (deps.notifyWorkspace || notifyWorkspaceAsync)();
127
+ return 0;
128
+ } catch (err) {
129
+ if (err instanceof JiraError) {
130
+ logError(`jira cancel: ${err.message}`);
131
+ return err.exitCode;
132
+ }
133
+ throw err;
134
+ }
135
+ }
@@ -0,0 +1,169 @@
1
+ // comment.mjs - Add or list comments on a Jira issue.
2
+ //
3
+ // Usage:
4
+ // jira comment <KEY> <TEXT...> Add a comment
5
+ // jira comment <KEY> -- <TEXT...> Add a comment (text after --)
6
+ // jira comment <KEY> --list List recent comments
7
+ //
8
+ // Exit 0 on success, 1 on usage error, 2 on API error.
9
+
10
+ import { createClient, JiraError } from '../client.mjs';
11
+ import { textToAdf, adfToText } from '../adf.mjs';
12
+ import { formatComment, bold, dim, green } from '../format.mjs';
13
+ import { notifyWorkspaceAsync } from '../invalidate.mjs';
14
+
15
+ const HELP = `jira comment - Add or list comments on a Jira issue
16
+
17
+ Usage:
18
+ jira comment <KEY> <TEXT> Add a comment to an issue
19
+ jira comment <KEY> -- <TEXT> Add a comment (text after --)
20
+ jira comment <KEY> --list List recent comments
21
+
22
+ Options:
23
+ --list List the 10 most recent comments instead of adding one
24
+ --help, -h Show this help
25
+ `;
26
+
27
+ /**
28
+ * Parse argv for the comment command.
29
+ * Handles -- separator for comment text, --list flag, and positional args.
30
+ *
31
+ * @param {string[]} argv Args after 'comment'
32
+ * @returns {{ key: string|null, text: string|null, list: boolean }}
33
+ */
34
+ function parseCommentArgs(argv) {
35
+ let key = null;
36
+ let list = false;
37
+ const textParts = [];
38
+ let afterDash = false;
39
+
40
+ for (let i = 0; i < argv.length; i++) {
41
+ if (argv[i] === '--') {
42
+ afterDash = true;
43
+ continue;
44
+ }
45
+ if (afterDash) {
46
+ textParts.push(argv[i]);
47
+ continue;
48
+ }
49
+ if (argv[i] === '--list') {
50
+ list = true;
51
+ continue;
52
+ }
53
+ if (argv[i] === '--help' || argv[i] === '-h') {
54
+ return { key: null, text: null, list: false, help: true };
55
+ }
56
+ // First positional is the key
57
+ if (!key) {
58
+ key = argv[i];
59
+ } else {
60
+ textParts.push(argv[i]);
61
+ }
62
+ }
63
+
64
+ const text = textParts.length > 0 ? textParts.join(' ') : null;
65
+ return { key, text, list, help: false };
66
+ }
67
+
68
+ /**
69
+ * Run the comment command.
70
+ *
71
+ * @param {string[]} argv Remaining args after 'comment'
72
+ * @param {object} [deps] Dependency overrides
73
+ * @returns {Promise<number>} Exit code
74
+ */
75
+ export async function run(argv = [], deps = {}) {
76
+ const log = deps.log || console.log;
77
+ const logError = deps.logError || console.error;
78
+
79
+ const { key, text, list, help } = parseCommentArgs(argv);
80
+
81
+ if (help) {
82
+ log(HELP);
83
+ return 0;
84
+ }
85
+
86
+ if (!key) {
87
+ logError('Usage: jira comment <KEY> <TEXT>\n jira comment <KEY> --list');
88
+ return 1;
89
+ }
90
+
91
+ const client = deps.client || createClient();
92
+
93
+ if (list) {
94
+ return listComments(client, key, log, logError);
95
+ }
96
+
97
+ if (!text) {
98
+ logError('Usage: jira comment <KEY> <TEXT>\nProvide comment text as arguments or after --');
99
+ return 1;
100
+ }
101
+
102
+ const code = await addComment(client, key, text, log, logError);
103
+ if (code === 0) {
104
+ (deps.notifyWorkspace || notifyWorkspaceAsync)();
105
+ }
106
+ return code;
107
+ }
108
+
109
+ /**
110
+ * List recent comments on an issue.
111
+ */
112
+ async function listComments(client, key, log, logError) {
113
+ try {
114
+ const { data } = await client.get(
115
+ `/rest/api/3/issue/${encodeURIComponent(key)}/comment?orderBy=-created&maxResults=10`
116
+ );
117
+
118
+ const comments = data.comments || [];
119
+
120
+ if (comments.length === 0) {
121
+ log(dim(`No comments on ${key}.`));
122
+ return 0;
123
+ }
124
+
125
+ log(bold(`Comments on ${key}`) + dim(` (${comments.length} shown)\n`));
126
+
127
+ for (const comment of comments) {
128
+ log(formatComment(comment));
129
+ // Render the ADF body as plain text
130
+ if (comment.body) {
131
+ const bodyText = adfToText(comment.body);
132
+ if (bodyText) {
133
+ log(` ${bodyText.split('\n').join('\n ')}`);
134
+ }
135
+ }
136
+ log('');
137
+ }
138
+
139
+ return 0;
140
+ } catch (err) {
141
+ if (err instanceof JiraError) {
142
+ logError(`Failed to list comments on ${key}: ${err.message}`);
143
+ return err.exitCode;
144
+ }
145
+ throw err;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Add a comment to an issue.
151
+ */
152
+ async function addComment(client, key, text, log, logError) {
153
+ try {
154
+ const adfBody = textToAdf(text);
155
+ const { data } = await client.post(
156
+ `/rest/api/3/issue/${encodeURIComponent(key)}/comment`,
157
+ { body: adfBody }
158
+ );
159
+
160
+ log(green(`Comment added to ${key}.`) + dim(` (id: ${data.id})`));
161
+ return 0;
162
+ } catch (err) {
163
+ if (err instanceof JiraError) {
164
+ logError(`Failed to add comment to ${key}: ${err.message}`);
165
+ return err.exitCode;
166
+ }
167
+ throw err;
168
+ }
169
+ }