@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.
- package/README.md +12 -2
- package/bin/confluence +9 -0
- package/bin/confluence-fetch +9 -0
- package/bin/jira +9 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +1 -1
- package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +3 -3
- package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-extensions.md +62 -0
- package/dist/engineering-pa-core-extensions-atlassian/README.md +81 -16
- package/dist/engineering-pa-core-extensions-atlassian/bin/confluence +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/bin/confluence-fetch +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/bin/jira +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/config/jira-workflow-chains.default.json +4 -0
- package/dist/engineering-pa-core-extensions-atlassian/index.mjs +17 -6
- package/dist/engineering-pa-core-extensions-atlassian/package.json +11 -4
- package/dist/engineering-pa-core-extensions-atlassian/src/common/invalidate-client.mjs +137 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/common/pa-name.mjs +58 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/common/secrets.mjs +158 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs +165 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/client.mjs +294 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/auth.mjs +59 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/comment.mjs +174 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/create.mjs +136 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/label.mjs +98 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mine.mjs +105 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +487 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-page.mjs +351 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/page.mjs +127 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/pages.mjs +107 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/recent.mjs +107 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/search.mjs +123 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/spaces.mjs +82 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/test.mjs +155 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/tree.mjs +103 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/update.mjs +134 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/format.mjs +260 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/mirror.mjs +353 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/storage.mjs +880 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs +313 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/fetch.mjs +327 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/adf.mjs +388 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/bug-fields.mjs +294 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs +197 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/client.mjs +350 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/advance.mjs +486 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/attach.mjs +129 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/auth.mjs +53 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/boards.mjs +88 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/bulk-transition.mjs +223 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/cancel.mjs +135 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/comment.mjs +169 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/create.mjs +376 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/issue.mjs +105 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +204 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/mine.mjs +97 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/projects.mjs +91 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/search.mjs +80 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sprint.mjs +111 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +313 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/test.mjs +142 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/transition.mjs +197 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/update.mjs +284 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/worklog.mjs +380 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/format.mjs +267 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/invalidate.mjs +72 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/metadata.mjs +225 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/required-fields.mjs +334 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +405 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +107 -0
- package/dist/engineering-pa-scaffold/README.md +32 -1
- package/dist/engineering-pa-scaffold/src/cli.mjs +83 -6
- package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +234 -0
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +6 -5
- package/dist/engineering-pa-scaffold/src/generator.mjs +240 -4
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +7 -7
- package/dist/engineering-pa-scaffold/templates/operator-README.md.template +4 -4
- package/dist/engineering-pa-scaffold/templates/root-CLAUDE.md.template +13 -0
- package/dist/engineering-pa-types-engineering/index.mjs +14 -6
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/create-a-small-app.md +81 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/for-agents.md +25 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/index.md +34 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/reference.md +16 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/wire-your-agent.md +17 -0
- package/dist/engineering-pa-types-engineering/package.json +2 -2
- package/dist/engineering-pa-types-engineering/roles/rcf-lite-engineer/definition.md +183 -0
- package/dist/engineering-pa-workspace/app/home.html +7 -7
- package/dist/engineering-pa-workspace/app/onboarding.html +4 -4
- package/dist/engineering-pa-workspace/src/api.mjs +1 -1
- package/dist/engineering-pa-workspace/src/server.mjs +1 -1
- package/docs/installing.md +78 -0
- package/docs/onboarding-solo.md +162 -0
- package/docs/what-it-can-do.md +85 -0
- package/package.json +6 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// test.mjs - Live API contract smoke test.
|
|
2
|
+
//
|
|
3
|
+
// Hits key Jira endpoints and validates response shapes. NO MUTATIONS.
|
|
4
|
+
// Used to verify the API still behaves as expected after upgrades,
|
|
5
|
+
// config changes, or permission adjustments.
|
|
6
|
+
//
|
|
7
|
+
// Exit 0 if all pass, 2 if any fail.
|
|
8
|
+
|
|
9
|
+
import { createClient, JiraError } from '../client.mjs';
|
|
10
|
+
import { bold, dim, green, red, yellow } from '../format.mjs';
|
|
11
|
+
|
|
12
|
+
const HELP = `jira test - Live API contract smoke test
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
jira test
|
|
16
|
+
|
|
17
|
+
Runs read-only checks against the Jira API to validate response shapes
|
|
18
|
+
and connectivity. No data is created, modified, or deleted.
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--help, -h Show this help
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run the test command.
|
|
26
|
+
*
|
|
27
|
+
* @param {string[]} argv Remaining args after 'test'
|
|
28
|
+
* @param {object} [deps] Dependency overrides
|
|
29
|
+
* @returns {Promise<number>} Exit code
|
|
30
|
+
*/
|
|
31
|
+
export async function run(argv = [], deps = {}) {
|
|
32
|
+
const log = deps.log || console.log;
|
|
33
|
+
const logError = deps.logError || console.error;
|
|
34
|
+
|
|
35
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
36
|
+
log(HELP);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const client = deps.client || createClient();
|
|
41
|
+
|
|
42
|
+
log(bold('Jira API Contract Tests\n'));
|
|
43
|
+
|
|
44
|
+
const results = [];
|
|
45
|
+
|
|
46
|
+
// ── Test 1: Auth check ───────────────────────────────────────────
|
|
47
|
+
results.push(await runTest(log, 'Auth (GET /rest/api/3/myself)', async () => {
|
|
48
|
+
const { data } = await client.get('/rest/api/3/myself');
|
|
49
|
+
assert(data.accountId, 'missing accountId');
|
|
50
|
+
assert(data.displayName, 'missing displayName');
|
|
51
|
+
return `user: ${data.displayName}`;
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// ── Test 2: Projects ─────────────────────────────────────────────
|
|
55
|
+
results.push(await runTest(log, 'Projects (GET /rest/api/3/project/search)', async () => {
|
|
56
|
+
const { data } = await client.get('/rest/api/3/project/search?maxResults=1');
|
|
57
|
+
assert(Array.isArray(data.values), 'values is not an array');
|
|
58
|
+
return `${data.total ?? data.values.length} projects`;
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// ── Test 3: JQL Search ───────────────────────────────────────────
|
|
62
|
+
let searchIssueKey = null;
|
|
63
|
+
results.push(await runTest(log, 'Search (POST /rest/api/3/search/jql)', async () => {
|
|
64
|
+
const { data } = await client.post('/rest/api/3/search/jql', {
|
|
65
|
+
jql: 'project = AI ORDER BY updated DESC',
|
|
66
|
+
fields: ['summary'],
|
|
67
|
+
maxResults: 1,
|
|
68
|
+
});
|
|
69
|
+
assert(Array.isArray(data.issues), 'issues is not an array');
|
|
70
|
+
if (data.issues.length > 0) {
|
|
71
|
+
searchIssueKey = data.issues[0].key;
|
|
72
|
+
}
|
|
73
|
+
return `${data.issues.length} issue(s) returned${searchIssueKey ? `, first: ${searchIssueKey}` : ''}`;
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// ── Test 4: Issue fetch ──────────────────────────────────────────
|
|
77
|
+
results.push(await runTest(log, 'Issue fetch (GET /rest/api/3/issue/{key})', async () => {
|
|
78
|
+
if (!searchIssueKey) {
|
|
79
|
+
return 'SKIP: no issue from search to fetch';
|
|
80
|
+
}
|
|
81
|
+
const { data } = await client.get(
|
|
82
|
+
`/rest/api/3/issue/${searchIssueKey}?fields=summary,status`
|
|
83
|
+
);
|
|
84
|
+
assert(data.fields?.summary, 'missing fields.summary');
|
|
85
|
+
return `${data.key}: ${data.fields.summary}`;
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// ── Test 5: Boards ──────────────────────────────────────────────
|
|
89
|
+
results.push(await runTest(log, 'Boards (GET /rest/agile/1.0/board)', async () => {
|
|
90
|
+
const { data } = await client.get('/rest/agile/1.0/board?maxResults=1');
|
|
91
|
+
assert(Array.isArray(data.values), 'values is not an array');
|
|
92
|
+
return `${data.total ?? data.values.length} board(s)`;
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// ── Summary ──────────────────────────────────────────────────────
|
|
96
|
+
const passed = results.filter((r) => r.passed).length;
|
|
97
|
+
const total = results.length;
|
|
98
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
99
|
+
|
|
100
|
+
log('');
|
|
101
|
+
const color = passed === total ? green : (passed >= total - 1 ? yellow : red);
|
|
102
|
+
log(color(bold(`${passed}/${total} passed`)) + (skipped ? dim(` (${skipped} skipped)`) : ''));
|
|
103
|
+
|
|
104
|
+
return passed === total ? 0 : 2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Run a single test and print result.
|
|
109
|
+
*
|
|
110
|
+
* @param {Function} log Output function
|
|
111
|
+
* @param {string} name Test name
|
|
112
|
+
* @param {Function} testFn Async function to execute
|
|
113
|
+
* @returns {Promise<{ passed: boolean, skipped: boolean }>}
|
|
114
|
+
*/
|
|
115
|
+
async function runTest(log, name, testFn) {
|
|
116
|
+
const start = Date.now();
|
|
117
|
+
try {
|
|
118
|
+
const detail = await testFn();
|
|
119
|
+
const ms = Date.now() - start;
|
|
120
|
+
const skipped = typeof detail === 'string' && detail.startsWith('SKIP:');
|
|
121
|
+
if (skipped) {
|
|
122
|
+
log(` ${yellow('SKIP')} ${name} ${dim(`${ms}ms`)} ${dim(detail.slice(6))}`);
|
|
123
|
+
return { passed: true, skipped: true };
|
|
124
|
+
}
|
|
125
|
+
log(` ${green('PASS')} ${name} ${dim(`${ms}ms`)} ${dim(detail || '')}`);
|
|
126
|
+
return { passed: true, skipped: false };
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const ms = Date.now() - start;
|
|
129
|
+
const msg = err instanceof JiraError ? err.message : (err.message || String(err));
|
|
130
|
+
log(` ${red('FAIL')} ${name} ${dim(`${ms}ms`)} ${red(msg)}`);
|
|
131
|
+
return { passed: false, skipped: false };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Simple assertion helper - throws on falsy value.
|
|
137
|
+
*/
|
|
138
|
+
function assert(value, message) {
|
|
139
|
+
if (!value) {
|
|
140
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// transition.mjs - Change Jira issue status via transitions.
|
|
2
|
+
//
|
|
3
|
+
// Fetches available transitions, fuzzy-matches the target status,
|
|
4
|
+
// checks for required fields, and executes the transition. Fields can
|
|
5
|
+
// be set on the transition payload itself (per Atlassian docs) - useful
|
|
6
|
+
// when a transition gate requires a customfield to be populated.
|
|
7
|
+
|
|
8
|
+
import { createClient, JiraError } from '../client.mjs';
|
|
9
|
+
import { textToAdf } from '../adf.mjs';
|
|
10
|
+
import { bold, green, yellow, dim } from '../format.mjs';
|
|
11
|
+
import {
|
|
12
|
+
buildRequiredFieldsPatch,
|
|
13
|
+
maybeWrapRequiredFieldError,
|
|
14
|
+
} from '../required-fields.mjs';
|
|
15
|
+
import { notifyWorkspaceAsync } from '../invalidate.mjs';
|
|
16
|
+
|
|
17
|
+
const HELP = `jira transition - Change issue status
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
jira transition <KEY> <STATUS>
|
|
21
|
+
[--field K=V] [--field-adf K=text]
|
|
22
|
+
[--affected-services NAME] [--original-estimate DURATION]
|
|
23
|
+
|
|
24
|
+
Arguments:
|
|
25
|
+
KEY Issue key (e.g. AI-123)
|
|
26
|
+
STATUS Target status name (case-insensitive, partial match)
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--field K=V Set an arbitrary field on the transition payload
|
|
30
|
+
(repeatable). V is JSON-parsed if it looks like
|
|
31
|
+
JSON, otherwise treated as a string.
|
|
32
|
+
--field-adf K=text Set an ADF-typed field; text is converted to ADF.
|
|
33
|
+
(Repeatable.)
|
|
34
|
+
--affected-services NAME Set Affected services on the transition payload
|
|
35
|
+
(resolved to ARI via service-registry).
|
|
36
|
+
--original-estimate DUR Set timetracking.originalEstimate on the
|
|
37
|
+
transition payload (Jira duration syntax).
|
|
38
|
+
|
|
39
|
+
Discovers available transitions for the issue, fuzzy-matches the target
|
|
40
|
+
status, and executes the transition. If the transition declares required
|
|
41
|
+
fields and you haven't supplied them via the flags above, the CLI surfaces
|
|
42
|
+
the gap and exits without proceeding.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
jira transition AI-123 "In Progress"
|
|
46
|
+
jira transition AI-123 done
|
|
47
|
+
jira transition AI-1556 review --original-estimate 8h \\
|
|
48
|
+
--affected-services "Artificial Intelligence Services"
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run the transition command.
|
|
53
|
+
*
|
|
54
|
+
* @param {string[]} argv Remaining args after 'transition'
|
|
55
|
+
* @param {object} [deps] Dependency overrides
|
|
56
|
+
* @returns {Promise<number>} Exit code
|
|
57
|
+
*/
|
|
58
|
+
export async function run(argv = [], deps = {}) {
|
|
59
|
+
const log = deps.log || console.log;
|
|
60
|
+
const logError = deps.logError || console.error;
|
|
61
|
+
const { parseArgs } = await import('../cli.mjs');
|
|
62
|
+
const args = parseArgs(argv);
|
|
63
|
+
|
|
64
|
+
if (args.help || args.h) {
|
|
65
|
+
log(HELP);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const key = args._[0];
|
|
70
|
+
const targetStatus = args._.slice(1).join(' ');
|
|
71
|
+
|
|
72
|
+
if (!key || !targetStatus) {
|
|
73
|
+
logError('Usage: jira transition <KEY> <STATUS> [--field K=V] [--field-adf K=text]');
|
|
74
|
+
logError(' [--affected-services NAME] [--original-estimate DUR]');
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const client = deps.client || createClient();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Get current issue status for display
|
|
82
|
+
const { data: issueData } = await client.get(
|
|
83
|
+
`/rest/api/3/issue/${encodeURIComponent(key)}?fields=status`
|
|
84
|
+
);
|
|
85
|
+
const currentStatus = issueData.fields?.status?.name || 'Unknown';
|
|
86
|
+
|
|
87
|
+
// Discover available transitions
|
|
88
|
+
const { data: transData } = await client.get(
|
|
89
|
+
`/rest/api/3/issue/${encodeURIComponent(key)}/transitions?expand=transitions.fields`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const transitions = transData.transitions || [];
|
|
93
|
+
if (transitions.length === 0) {
|
|
94
|
+
logError(`No transitions available for ${key} (current status: ${currentStatus})`);
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fuzzy match: case-insensitive, partial match
|
|
99
|
+
const target = targetStatus.toLowerCase();
|
|
100
|
+
const matches = transitions.filter((t) => {
|
|
101
|
+
const name = (t.name || '').toLowerCase();
|
|
102
|
+
return name === target || name.includes(target);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (matches.length === 0) {
|
|
106
|
+
logError(`No transition matching "${targetStatus}" found for ${key}.`);
|
|
107
|
+
logError(`Available transitions:`);
|
|
108
|
+
for (const t of transitions) {
|
|
109
|
+
logError(` - ${t.name} (id: ${t.id})`);
|
|
110
|
+
}
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (matches.length > 1) {
|
|
115
|
+
// Check for exact match first
|
|
116
|
+
const exact = matches.find((t) => (t.name || '').toLowerCase() === target);
|
|
117
|
+
if (exact) {
|
|
118
|
+
matches.length = 0;
|
|
119
|
+
matches.push(exact);
|
|
120
|
+
} else {
|
|
121
|
+
logError(`Ambiguous transition "${targetStatus}" - multiple matches:`);
|
|
122
|
+
for (const t of matches) {
|
|
123
|
+
logError(` - ${t.name} (id: ${t.id})`);
|
|
124
|
+
}
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const transition = matches[0];
|
|
130
|
+
|
|
131
|
+
// ── Build the fields patch from the new flags ──────────────────
|
|
132
|
+
// Done up-front so we can satisfy required fields declared by the
|
|
133
|
+
// transition without forcing the operator to a separate `update` call.
|
|
134
|
+
const patch = await buildRequiredFieldsPatch(args, client, textToAdf);
|
|
135
|
+
|
|
136
|
+
// Check for required fields not covered by what was provided.
|
|
137
|
+
const requiredFields = [];
|
|
138
|
+
if (transition.fields) {
|
|
139
|
+
for (const [fieldId, fieldDef] of Object.entries(transition.fields)) {
|
|
140
|
+
if (fieldDef.required) {
|
|
141
|
+
requiredFields.push({
|
|
142
|
+
id: fieldId,
|
|
143
|
+
name: fieldDef.name || fieldId,
|
|
144
|
+
schema: fieldDef.schema?.type || 'unknown',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const stillMissing = requiredFields.filter((f) => !(f.id in patch.fields));
|
|
151
|
+
if (stillMissing.length > 0) {
|
|
152
|
+
logError(yellow(`Transition "${transition.name}" requires additional fields:`));
|
|
153
|
+
for (const field of stillMissing) {
|
|
154
|
+
logError(` - ${bold(field.name)} (${field.id}) - type: ${field.schema}`);
|
|
155
|
+
}
|
|
156
|
+
logError(dim(''));
|
|
157
|
+
logError(dim('Set them on this command via the new flags:'));
|
|
158
|
+
logError(dim(' --affected-services "<service name>" (Affected services)'));
|
|
159
|
+
logError(dim(' --original-estimate 8h (Original Estimate)'));
|
|
160
|
+
logError(dim(' --field <field-id>=<value> (any other required field)'));
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Execute transition. Per Atlassian REST v3, fields go in the same body
|
|
165
|
+
// as the transition selector. We send `fields` only - `update` is used
|
|
166
|
+
// for edit-operations which aren't expressible via the convenience flags.
|
|
167
|
+
const body = { transition: { id: transition.id } };
|
|
168
|
+
if (Object.keys(patch.fields).length > 0) {
|
|
169
|
+
body.fields = patch.fields;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await client.post(
|
|
173
|
+
`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`,
|
|
174
|
+
body
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
log(green(`${bold(key)}: ${currentStatus} → ${transition.name}`));
|
|
178
|
+
if (patch.changed.length > 0) {
|
|
179
|
+
log(` Fields set on transition: ${patch.changed.join(', ')}`);
|
|
180
|
+
}
|
|
181
|
+
// Notify workspace (fail-silent, fire-and-forget). Test paths inject
|
|
182
|
+
// a no-op via deps.notifyWorkspace if needed.
|
|
183
|
+
(deps.notifyWorkspace || notifyWorkspaceAsync)();
|
|
184
|
+
return 0;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
if (err instanceof JiraError) {
|
|
187
|
+
const wrapped = maybeWrapRequiredFieldError(err);
|
|
188
|
+
if (wrapped) {
|
|
189
|
+
logError(`jira transition: ${wrapped}`);
|
|
190
|
+
} else {
|
|
191
|
+
logError(`jira transition: ${err.message}`);
|
|
192
|
+
}
|
|
193
|
+
return err.exitCode;
|
|
194
|
+
}
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// update.mjs - Update Jira issue fields.
|
|
2
|
+
//
|
|
3
|
+
// At least one field must be provided.
|
|
4
|
+
// Supports:
|
|
5
|
+
// -s SUMMARY Replace summary
|
|
6
|
+
// -d DESCRIPTION Replace description (plain text → ADF)
|
|
7
|
+
// --assign USER Assign to "me" | email | accountId
|
|
8
|
+
// --priority P Replace priority (name or ID)
|
|
9
|
+
// --add-label NAME Add a label (repeatable)
|
|
10
|
+
// --remove-label NAME Remove a label (repeatable)
|
|
11
|
+
// --set-labels a,b,c Replace label set (mutually exclusive with add/remove)
|
|
12
|
+
// --add-component NAME Add a component by name (repeatable)
|
|
13
|
+
// --remove-component NAME Remove a component by name (repeatable)
|
|
14
|
+
// --set-components a,b,c Replace component set (mutually exclusive with add/remove)
|
|
15
|
+
// --field K=V Set an arbitrary field (repeatable)
|
|
16
|
+
// --field-adf K=text Set an ADF-typed field (repeatable)
|
|
17
|
+
// --affected-services NAME Set Affected services customfield (resolved to ARI)
|
|
18
|
+
// --original-estimate DUR Set timetracking.originalEstimate (Jira duration)
|
|
19
|
+
//
|
|
20
|
+
// Add/remove use Jira's edit-operations block ({"update": {...}}).
|
|
21
|
+
// Replacement uses {"fields": {...}}.
|
|
22
|
+
|
|
23
|
+
import { createClient, JiraError } from '../client.mjs';
|
|
24
|
+
import { textToAdf } from '../adf.mjs';
|
|
25
|
+
import { bold, green } from '../format.mjs';
|
|
26
|
+
import {
|
|
27
|
+
asList,
|
|
28
|
+
parseCommaList,
|
|
29
|
+
validateLabels,
|
|
30
|
+
resolveComponents,
|
|
31
|
+
resolvePriority,
|
|
32
|
+
resolveAssignee,
|
|
33
|
+
} from '../metadata.mjs';
|
|
34
|
+
import {
|
|
35
|
+
buildRequiredFieldsPatch,
|
|
36
|
+
maybeWrapRequiredFieldError,
|
|
37
|
+
} from '../required-fields.mjs';
|
|
38
|
+
import { notifyWorkspaceAsync } from '../invalidate.mjs';
|
|
39
|
+
|
|
40
|
+
const HELP = `jira update - Update issue fields
|
|
41
|
+
|
|
42
|
+
Usage:
|
|
43
|
+
jira update <KEY> [-s SUMMARY] [-d DESCRIPTION] [--assign USER] [--priority P]
|
|
44
|
+
[--add-label NAME] [--remove-label NAME] [--set-labels a,b,c]
|
|
45
|
+
[--add-component NAME] [--remove-component NAME]
|
|
46
|
+
[--set-components a,b,c]
|
|
47
|
+
[--field K=V] [--field-adf K=text]
|
|
48
|
+
[--affected-services NAME] [--original-estimate DURATION]
|
|
49
|
+
|
|
50
|
+
Arguments:
|
|
51
|
+
KEY Issue key (e.g. AI-123)
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
-s SUMMARY New summary/title
|
|
55
|
+
-d DESCRIPTION New description (plain text, converted to ADF)
|
|
56
|
+
--assign USER Assign to user: "me", email, or accountId
|
|
57
|
+
--priority P Set priority: name (e.g. "High") or ID (e.g. "2")
|
|
58
|
+
--add-label NAME Add a label (repeatable)
|
|
59
|
+
--remove-label NAME Remove a label (repeatable)
|
|
60
|
+
--set-labels a,b,c Replace labels entirely (comma-separated;
|
|
61
|
+
mutually exclusive with --add-label/--remove-label)
|
|
62
|
+
--add-component NAME Add a component by name (repeatable; resolved
|
|
63
|
+
against the target project's component list)
|
|
64
|
+
--remove-component NAME Remove a component by name (repeatable)
|
|
65
|
+
--set-components a,b,c Replace components entirely (comma-separated;
|
|
66
|
+
mutually exclusive with --add-component/--remove-component)
|
|
67
|
+
--field K=V Set an arbitrary field (repeatable). V is parsed
|
|
68
|
+
as JSON if it looks like JSON, otherwise string.
|
|
69
|
+
Reserved keys must use their dedicated flags.
|
|
70
|
+
--field-adf K=text Set an ADF-typed field; text is converted to ADF.
|
|
71
|
+
(Repeatable.)
|
|
72
|
+
--affected-services NAME Set the "Affected services" customfield. NAME is
|
|
73
|
+
the service display name; the CLI resolves it to
|
|
74
|
+
the ARI via the service-registry autocomplete
|
|
75
|
+
endpoint. Required by some workflows.
|
|
76
|
+
--original-estimate DUR Set timetracking.originalEstimate. DUR uses
|
|
77
|
+
Jira duration syntax: "8h", "30m", "1d 4h".
|
|
78
|
+
Required by some workflows.
|
|
79
|
+
|
|
80
|
+
At least one field/operation must be provided. Labels cannot contain whitespace.
|
|
81
|
+
|
|
82
|
+
Priority values:
|
|
83
|
+
Blocker (1), Serious (2), Medium (3), Minor (4), Trivial (5)
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
// Map priority names (lowercase) to Jira priority IDs (kept for back-compat /
|
|
87
|
+
// offline use when the legacy --priority short path is hit without an API call).
|
|
88
|
+
const PRIORITY_MAP = {
|
|
89
|
+
blocker: '1',
|
|
90
|
+
highest: '1',
|
|
91
|
+
serious: '2',
|
|
92
|
+
high: '2',
|
|
93
|
+
medium: '3',
|
|
94
|
+
minor: '4',
|
|
95
|
+
low: '4',
|
|
96
|
+
trivial: '5',
|
|
97
|
+
lowest: '5',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Run the update command.
|
|
102
|
+
*
|
|
103
|
+
* @param {string[]} argv Remaining args after 'update'
|
|
104
|
+
* @param {object} [deps] Dependency overrides
|
|
105
|
+
* @returns {Promise<number>} Exit code
|
|
106
|
+
*/
|
|
107
|
+
export async function run(argv = [], deps = {}) {
|
|
108
|
+
const log = deps.log || console.log;
|
|
109
|
+
const logError = deps.logError || console.error;
|
|
110
|
+
const { parseArgs } = await import('../cli.mjs');
|
|
111
|
+
const args = parseArgs(argv);
|
|
112
|
+
|
|
113
|
+
if (args.help || args.h) {
|
|
114
|
+
log(HELP);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const key = args._[0];
|
|
119
|
+
if (!key) {
|
|
120
|
+
logError('Usage: jira update <KEY> [-s SUMMARY] [-d DESC] [--assign USER] [--priority P]');
|
|
121
|
+
logError(' [--add-label NAME] [--remove-label NAME] [--set-labels a,b,c]');
|
|
122
|
+
logError(' [--add-component NAME] [--remove-component NAME] [--set-components a,b,c]');
|
|
123
|
+
logError(' [--field K=V] [--field-adf K=text]');
|
|
124
|
+
logError(' [--affected-services NAME] [--original-estimate DUR]');
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Gather flag values ─────────────────────────────────────────────
|
|
129
|
+
const summary = args.s || args.summary;
|
|
130
|
+
const description = args.d || args.description;
|
|
131
|
+
const assign = args.assign;
|
|
132
|
+
const priority = args.priority;
|
|
133
|
+
|
|
134
|
+
const addLabels = asList(args['add-label']);
|
|
135
|
+
const removeLabels = asList(args['remove-label']);
|
|
136
|
+
const setLabelsRaw = args['set-labels'];
|
|
137
|
+
const setLabels = setLabelsRaw != null ? parseCommaList(setLabelsRaw) : null;
|
|
138
|
+
|
|
139
|
+
const addComponents = asList(args['add-component']);
|
|
140
|
+
const removeComponents = asList(args['remove-component']);
|
|
141
|
+
const setComponentsRaw = args['set-components'];
|
|
142
|
+
const setComponents = setComponentsRaw != null ? parseCommaList(setComponentsRaw) : null;
|
|
143
|
+
|
|
144
|
+
// ── Mutually-exclusive checks (usage error, exit 1) ────────────────
|
|
145
|
+
if (setLabels !== null && (addLabels.length > 0 || removeLabels.length > 0)) {
|
|
146
|
+
logError('jira update: --set-labels is mutually exclusive with --add-label / --remove-label');
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
if (setComponents !== null && (addComponents.length > 0 || removeComponents.length > 0)) {
|
|
150
|
+
logError('jira update: --set-components is mutually exclusive with --add-component / --remove-component');
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Local label validation (cheap fail-fast) ───────────────────────
|
|
155
|
+
try {
|
|
156
|
+
validateLabels(addLabels);
|
|
157
|
+
validateLabels(removeLabels);
|
|
158
|
+
if (setLabels) validateLabels(setLabels);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (err instanceof JiraError) {
|
|
161
|
+
logError(`jira update: ${err.message}`);
|
|
162
|
+
return err.exitCode;
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Build payload ──────────────────────────────────────────────────
|
|
168
|
+
const fields = {};
|
|
169
|
+
const update = {};
|
|
170
|
+
const changed = [];
|
|
171
|
+
|
|
172
|
+
if (summary) {
|
|
173
|
+
fields.summary = summary;
|
|
174
|
+
changed.push('summary');
|
|
175
|
+
}
|
|
176
|
+
if (description) {
|
|
177
|
+
fields.description = textToAdf(description);
|
|
178
|
+
changed.push('description');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Labels - set (fields) vs add/remove (update)
|
|
182
|
+
if (setLabels !== null) {
|
|
183
|
+
fields.labels = setLabels;
|
|
184
|
+
changed.push('labels (set)');
|
|
185
|
+
} else if (addLabels.length > 0 || removeLabels.length > 0) {
|
|
186
|
+
update.labels = [
|
|
187
|
+
...addLabels.map((v) => ({ add: v })),
|
|
188
|
+
...removeLabels.map((v) => ({ remove: v })),
|
|
189
|
+
];
|
|
190
|
+
if (addLabels.length > 0) changed.push(`labels +${addLabels.length}`);
|
|
191
|
+
if (removeLabels.length > 0) changed.push(`labels -${removeLabels.length}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const client = deps.client || createClient();
|
|
195
|
+
|
|
196
|
+
// Need the project key for component resolution. Derive it from the
|
|
197
|
+
// issue key (everything before the dash). Jira keys are PROJECT-NUM.
|
|
198
|
+
const projectKey = String(key).split('-')[0].toUpperCase();
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// Components - set (fields) vs add/remove (update). Both paths need
|
|
202
|
+
// name → id resolution via the project's components list.
|
|
203
|
+
if (setComponents !== null) {
|
|
204
|
+
const resolved = await resolveComponents(client, projectKey, setComponents);
|
|
205
|
+
fields.components = resolved.map((c) => ({ id: c.id }));
|
|
206
|
+
changed.push('components (set)');
|
|
207
|
+
} else if (addComponents.length > 0 || removeComponents.length > 0) {
|
|
208
|
+
const all = [...addComponents, ...removeComponents];
|
|
209
|
+
const resolved = await resolveComponents(client, projectKey, all);
|
|
210
|
+
const adds = resolved.slice(0, addComponents.length);
|
|
211
|
+
const removes = resolved.slice(addComponents.length);
|
|
212
|
+
update.components = [
|
|
213
|
+
...adds.map((c) => ({ add: { id: c.id } })),
|
|
214
|
+
...removes.map((c) => ({ remove: { id: c.id } })),
|
|
215
|
+
];
|
|
216
|
+
if (adds.length > 0) changed.push(`components +${adds.length}`);
|
|
217
|
+
if (removes.length > 0) changed.push(`components -${removes.length}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Assignee
|
|
221
|
+
if (assign) {
|
|
222
|
+
const accountId = await resolveAssignee(client, assign);
|
|
223
|
+
fields.assignee = { accountId };
|
|
224
|
+
changed.push('assignee');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Priority - try the static name→id map first (no API call). For
|
|
228
|
+
// numeric IDs, pass through directly. Fall back to a live lookup only
|
|
229
|
+
// when the name is unknown locally, so unfamiliar Jira-instance-specific
|
|
230
|
+
// priorities still resolve.
|
|
231
|
+
if (priority) {
|
|
232
|
+
if (/^\d+$/.test(priority)) {
|
|
233
|
+
fields.priority = { id: priority };
|
|
234
|
+
} else {
|
|
235
|
+
const lower = priority.toLowerCase();
|
|
236
|
+
if (PRIORITY_MAP[lower]) {
|
|
237
|
+
fields.priority = { id: PRIORITY_MAP[lower] };
|
|
238
|
+
} else {
|
|
239
|
+
const resolved = await resolvePriority(client, priority);
|
|
240
|
+
fields.priority = { id: resolved.id };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
changed.push('priority');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Generic --field / --field-adf / --affected-services / --original-estimate.
|
|
247
|
+
// These all target `fields` (never `update`), so just merge in.
|
|
248
|
+
const patch = await buildRequiredFieldsPatch(args, client, textToAdf);
|
|
249
|
+
Object.assign(fields, patch.fields);
|
|
250
|
+
changed.push(...patch.changed);
|
|
251
|
+
|
|
252
|
+
// ── Final sanity: at least one operation queued ──────────────────
|
|
253
|
+
if (Object.keys(fields).length === 0 && Object.keys(update).length === 0) {
|
|
254
|
+
logError('No fields to update. Provide at least one of:');
|
|
255
|
+
logError(' -s, -d, --assign, --priority,');
|
|
256
|
+
logError(' --add-label, --remove-label, --set-labels,');
|
|
257
|
+
logError(' --add-component, --remove-component, --set-components,');
|
|
258
|
+
logError(' --field, --field-adf, --affected-services, --original-estimate');
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const payload = {};
|
|
263
|
+
if (Object.keys(fields).length > 0) payload.fields = fields;
|
|
264
|
+
if (Object.keys(update).length > 0) payload.update = update;
|
|
265
|
+
|
|
266
|
+
await client.put(`/rest/api/3/issue/${encodeURIComponent(key)}`, payload);
|
|
267
|
+
log(green(`${bold(key)} updated successfully.`));
|
|
268
|
+
log(` Changes: ${changed.join(', ')}`);
|
|
269
|
+
|
|
270
|
+
(deps.notifyWorkspace || notifyWorkspaceAsync)();
|
|
271
|
+
return 0;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (err instanceof JiraError) {
|
|
274
|
+
const wrapped = maybeWrapRequiredFieldError(err);
|
|
275
|
+
if (wrapped) {
|
|
276
|
+
logError(`jira update: ${wrapped}`);
|
|
277
|
+
} else {
|
|
278
|
+
logError(`jira update: ${err.message}`);
|
|
279
|
+
}
|
|
280
|
+
return err.exitCode;
|
|
281
|
+
}
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
}
|