@knowttl/gitea-axi 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +350 -32
- package/axi-credential.schema.json +8 -1
- package/dist/action-values.js +37 -0
- package/dist/api.js +90 -19
- package/dist/attachments.js +171 -0
- package/dist/catalogue.js +65 -0
- package/dist/ci.js +24 -0
- package/dist/credentials.js +78 -6
- package/dist/git.js +89 -0
- package/dist/local-workflows.js +222 -0
- package/dist/main.js +245 -25
- package/dist/private-file.js +3 -1
- package/dist/pulls.js +299 -0
- package/dist/reads.js +15 -9
- package/dist/registry.js +139 -5
- package/dist/releases.js +145 -0
- package/dist/repo-workflows.js +147 -0
- package/dist/search.js +111 -0
- package/dist/stacks.js +486 -0
- package/dist/transport.js +62 -7
- package/dist/writes.js +568 -0
- package/generated/SKILL.md +141 -0
- package/generated/capabilities.json +964 -0
- package/generated/help.txt +5376 -0
- package/generated/host-inventory.json +7000 -151
- package/generated/routes.json +3501 -5
- package/package.json +24 -10
- package/schemas/catalogue.json +2672 -0
package/dist/pulls.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { collect, object, read } from './api.js';
|
|
2
|
+
import { CliError } from './error.js';
|
|
3
|
+
const usage = (message) => { throw new CliError('USAGE', message, 2); };
|
|
4
|
+
const gap = (message) => { throw new CliError('CAPABILITY_GAP', message); };
|
|
5
|
+
const names = (value) => value === undefined ? [] : [...new Set((Array.isArray(value) ? value : [value]).map(String))];
|
|
6
|
+
const sorted = (values) => [...values].sort();
|
|
7
|
+
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
8
|
+
const delta = (previous, add, remove) => [...new Set([...previous.map(String), ...names(add)])].filter(value => !names(remove).includes(value));
|
|
9
|
+
const members = (pull, field, key) => (pull[field] ?? []).map(row => object(row)[key]);
|
|
10
|
+
export function validatePull(operation, options) {
|
|
11
|
+
if (operation === 'pr review') {
|
|
12
|
+
if (['approve', 'request-changes', 'comment'].filter(key => options[key]).length !== 1)
|
|
13
|
+
usage('Choose exactly one of --approve, --request-changes or --comment');
|
|
14
|
+
gap('C043 unresolved: review wire events and atomic stale-head refusal lack vendor evidence; no review will be posted, including with --match-head-commit');
|
|
15
|
+
}
|
|
16
|
+
if (operation === 'pr merge') {
|
|
17
|
+
const styles = ['merge', 'squash', 'rebase'].filter(key => options[key]);
|
|
18
|
+
if (styles.length + Number(options.method !== undefined) !== 1)
|
|
19
|
+
usage('Choose exactly one of --method, --merge, --squash or --rebase');
|
|
20
|
+
if (!['merge', 'squash', 'rebase'].includes(String(options.method ?? styles[0])))
|
|
21
|
+
usage('Supported merge methods: merge, squash, rebase');
|
|
22
|
+
if (!/^[a-f0-9]{40}$/.test(String(options['match-head-commit'] ?? '')))
|
|
23
|
+
usage('--match-head-commit requires the full 40-character head SHA');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function fields(pull) {
|
|
27
|
+
return { title: pull.title, body: pull.body ?? '', state: pull.state, head: object(pull.head).ref, base: object(pull.base).ref,
|
|
28
|
+
assignees: sorted(members(pull, 'assignees', 'login')), labels: sorted(members(pull, 'labels', 'id')),
|
|
29
|
+
milestone: pull.milestone == null ? null : object(pull.milestone).id,
|
|
30
|
+
reviewers: sorted(members(pull, 'requested_reviewers', 'login')), teams: sorted(members(pull, 'requested_reviewers_teams', 'name')) };
|
|
31
|
+
}
|
|
32
|
+
export async function pullPlan(client, operation, base, number, options) {
|
|
33
|
+
const action = operation.split(' ')[1];
|
|
34
|
+
const path = base + '/pulls' + (number ? '/' + number : '');
|
|
35
|
+
const steps = [];
|
|
36
|
+
const preconditions = {};
|
|
37
|
+
const reads = new Set();
|
|
38
|
+
const snapshot = async (path, list = false) => {
|
|
39
|
+
let value;
|
|
40
|
+
if (list) {
|
|
41
|
+
const page = await collect(client, path);
|
|
42
|
+
if (page.truncated)
|
|
43
|
+
throw new CliError('INCOMPLETE_READ', 'A complete collection is required before writing');
|
|
44
|
+
value = page.items;
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
value = await read(client, path);
|
|
48
|
+
preconditions[path] = value;
|
|
49
|
+
reads.add(path);
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
const add = (method, path, payload) => steps.push({ method, path, ...(payload === undefined ? {} : { payload }) });
|
|
53
|
+
const before = action === 'create' ? undefined : object(await snapshot(path));
|
|
54
|
+
const expected = before ? { id: before.id, number: before.number, ...fields(before), merged: before.merged ?? false, headSHA: object(before.head).sha } :
|
|
55
|
+
{ title: '', body: '', state: 'open', merged: false, assignees: [], labels: [], milestone: null, reviewers: [], teams: [] };
|
|
56
|
+
if (action === 'create')
|
|
57
|
+
await snapshot(path + '?state=all', true);
|
|
58
|
+
if (action === 'create' || action === 'edit') {
|
|
59
|
+
for (const [flag, collection, key] of [['assignee', 'assignees', 'login'], ['team-reviewer', 'teams', 'name']]) {
|
|
60
|
+
const requested = action === 'create' ? names(options[flag]) : [...names(options['add-' + flag]), ...names(options['remove-' + flag])];
|
|
61
|
+
if (!requested.length)
|
|
62
|
+
continue;
|
|
63
|
+
const available = await snapshot(base + '/' + collection, true);
|
|
64
|
+
for (const name of requested) {
|
|
65
|
+
const selected = available.find(row => row[key] === name);
|
|
66
|
+
if (!selected)
|
|
67
|
+
usage('Unknown exact ' + flag + ' name: ' + name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const addedReviewers = names(action === 'create' ? options.reviewer : options['add-reviewer']);
|
|
71
|
+
const requestedReviewers = [...new Set([...addedReviewers, ...names(options['remove-reviewer'])])];
|
|
72
|
+
if (requestedReviewers.length) {
|
|
73
|
+
const author = object(action === 'create' ? await snapshot('/api/v1/user') : before.user);
|
|
74
|
+
for (const name of requestedReviewers) {
|
|
75
|
+
let reviewer;
|
|
76
|
+
try {
|
|
77
|
+
reviewer = object(await snapshot('/api/v1/users/' + encodeURIComponent(name)));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof CliError && error.code === 'NOT_FOUND_OR_HIDDEN')
|
|
81
|
+
usage('Unknown exact reviewer name: ' + name);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
if (reviewer.login !== name)
|
|
85
|
+
usage('Unknown exact reviewer name: ' + name);
|
|
86
|
+
if (addedReviewers.includes(name) && reviewer.id === author.id)
|
|
87
|
+
usage('The PR author cannot be requested as a reviewer');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const payload = {};
|
|
91
|
+
for (const field of ['title', 'body', 'base'])
|
|
92
|
+
if (options[field] !== undefined)
|
|
93
|
+
payload[field] = options[field];
|
|
94
|
+
if (action === 'create') {
|
|
95
|
+
for (const field of ['title', 'base', 'head'])
|
|
96
|
+
if (!options[field])
|
|
97
|
+
usage('--' + field + ' is required');
|
|
98
|
+
if (String(options.head).includes(':'))
|
|
99
|
+
gap('Cross-repository PR creation requires separate head repository verification');
|
|
100
|
+
payload.head = options.head;
|
|
101
|
+
payload.assignees = names(options.assignee);
|
|
102
|
+
expected.reviewers = sorted(addedReviewers);
|
|
103
|
+
expected.teams = sorted(names(options['team-reviewer']));
|
|
104
|
+
const head = object(await snapshot(base + '/branches/' + encodeURIComponent(String(options.head))));
|
|
105
|
+
expected.headSHA = object(head.commit).id;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (options['add-assignee'] !== undefined || options['remove-assignee'] !== undefined)
|
|
109
|
+
payload.assignees = delta(expected.assignees, options['add-assignee'], options['remove-assignee']);
|
|
110
|
+
}
|
|
111
|
+
if (options.base !== undefined)
|
|
112
|
+
await snapshot(base + '/branches/' + encodeURIComponent(String(options.base)));
|
|
113
|
+
if (options.milestone !== undefined) {
|
|
114
|
+
const list = await snapshot(base + '/milestones?state=all', true);
|
|
115
|
+
const selected = list.find(row => row.title === options.milestone);
|
|
116
|
+
if (!selected)
|
|
117
|
+
usage('Unknown exact milestone title');
|
|
118
|
+
payload.milestone = selected.id;
|
|
119
|
+
}
|
|
120
|
+
if (options.label !== undefined || options['add-label'] !== undefined || options['remove-label'] !== undefined) {
|
|
121
|
+
const labels = await snapshot(base + '/labels', true);
|
|
122
|
+
const ids = (value) => names(value).map(name => {
|
|
123
|
+
const label = labels.find(row => row.name === name);
|
|
124
|
+
if (!label)
|
|
125
|
+
return usage('Unknown exact label name: ' + name);
|
|
126
|
+
return label.id;
|
|
127
|
+
});
|
|
128
|
+
payload.labels = action === 'create' ? ids(options.label) : [...new Set([...expected.labels, ...ids(options['add-label'])])].filter(id => !ids(options['remove-label']).includes(id));
|
|
129
|
+
}
|
|
130
|
+
for (const field of ['title', 'body', 'base', 'head', 'assignees', 'labels', 'milestone'])
|
|
131
|
+
if (payload[field] !== undefined)
|
|
132
|
+
expected[field] = payload[field];
|
|
133
|
+
if (action === 'create') {
|
|
134
|
+
add('POST', path, payload);
|
|
135
|
+
if (addedReviewers.length || names(options['team-reviewer']).length)
|
|
136
|
+
steps.push({ method: 'POST', path: path + '/0/requested_reviewers',
|
|
137
|
+
payload: { reviewers: addedReviewers, team_reviewers: names(options['team-reviewer']) }, createdPull: true });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
for (const [flag, field, wire] of [['reviewer', 'reviewers', 'reviewers'], ['team-reviewer', 'teams', 'team_reviewers']]) {
|
|
141
|
+
const previous = expected[field];
|
|
142
|
+
const next = delta(previous, options['add-' + flag], options['remove-' + flag]);
|
|
143
|
+
const added = next.filter(name => !previous.includes(name));
|
|
144
|
+
const removed = previous.filter(name => !next.includes(String(name)));
|
|
145
|
+
if (added.length)
|
|
146
|
+
add('POST', path + '/requested_reviewers', { [wire]: added });
|
|
147
|
+
if (removed.length)
|
|
148
|
+
add('DELETE', path + '/requested_reviewers', { [wire]: removed });
|
|
149
|
+
expected[field] = next;
|
|
150
|
+
}
|
|
151
|
+
if (Object.keys(payload).length)
|
|
152
|
+
add('PATCH', path, payload);
|
|
153
|
+
}
|
|
154
|
+
if (!steps.length && !options.attach)
|
|
155
|
+
usage('Choose a PR field or membership to edit');
|
|
156
|
+
}
|
|
157
|
+
else if (action === 'close' || action === 'reopen') {
|
|
158
|
+
expected.state = action === 'close' ? 'closed' : 'open';
|
|
159
|
+
add('PATCH', path, { state: expected.state });
|
|
160
|
+
}
|
|
161
|
+
else if (action === 'merge') {
|
|
162
|
+
if (expected.headSHA !== options['match-head-commit'])
|
|
163
|
+
throw new CliError('STALE_HEAD', 'Selected PR head differs from --match-head-commit');
|
|
164
|
+
if (before.state !== 'open' || before.merged)
|
|
165
|
+
usage('Select an open unmerged PR');
|
|
166
|
+
const style = options.method ?? ['merge', 'squash', 'rebase'].find(key => options[key]);
|
|
167
|
+
const payload = { Do: style, head_commit_id: options['match-head-commit'] };
|
|
168
|
+
if (options.subject !== undefined)
|
|
169
|
+
payload.MergeTitleField = options.subject;
|
|
170
|
+
if (options.body !== undefined)
|
|
171
|
+
payload.MergeMessageField = options.body;
|
|
172
|
+
if (style === 'rebase')
|
|
173
|
+
await snapshot(path + '/commits', true);
|
|
174
|
+
await snapshot(base + '/branches/' + encodeURIComponent(String(expected.base)));
|
|
175
|
+
add('POST', path + '/merge', payload);
|
|
176
|
+
expected.state = 'closed';
|
|
177
|
+
}
|
|
178
|
+
else if (action === 'update-branch') {
|
|
179
|
+
await snapshot(base + '/branches/' + encodeURIComponent(String(expected.base)));
|
|
180
|
+
add('POST', path + '/update');
|
|
181
|
+
}
|
|
182
|
+
if (action === 'comment' || action === 'close' && options.comment !== undefined) {
|
|
183
|
+
const body = action === 'comment' ? options.body : options.comment;
|
|
184
|
+
if (body === undefined || body === '')
|
|
185
|
+
usage('--body is required');
|
|
186
|
+
const comments = base + '/issues/' + number + '/comments';
|
|
187
|
+
await snapshot(comments, true);
|
|
188
|
+
add('POST', comments, { body });
|
|
189
|
+
}
|
|
190
|
+
return { target: path, steps, reads: [...reads], preconditions, pull: { expected, action, repository: base } };
|
|
191
|
+
}
|
|
192
|
+
export async function verifyPull(client, plan, results, observations) {
|
|
193
|
+
const { expected, action, repository: base } = plan.pull;
|
|
194
|
+
const before = object(plan.preconditions);
|
|
195
|
+
const mismatch = () => { throw new CliError('READBACK_MISMATCH', 'PR read-back did not confirm identity, requested fields or commit graph'); };
|
|
196
|
+
const observation = (path) => observations.find(row => row.path === path)?.data;
|
|
197
|
+
let pull;
|
|
198
|
+
if (action === 'create') {
|
|
199
|
+
const result = object(results[0]);
|
|
200
|
+
pull = observation(plan.target + '?state=all').find(row => row.id === result.id && row.number === result.number) ?? mismatch();
|
|
201
|
+
if (before[plan.target + '?state=all'].some(row => row.id === pull.id || row.number === pull.number))
|
|
202
|
+
mismatch();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
pull = object(observation(plan.target));
|
|
206
|
+
if (pull.id !== expected.id || pull.number !== expected.number)
|
|
207
|
+
mismatch();
|
|
208
|
+
}
|
|
209
|
+
const actual = fields(pull);
|
|
210
|
+
for (const field of Object.keys(actual)) {
|
|
211
|
+
const value = ['assignees', 'labels', 'reviewers', 'teams'].includes(field) ? sorted(expected[field]) : expected[field];
|
|
212
|
+
if (!equal(actual[field], value))
|
|
213
|
+
mismatch();
|
|
214
|
+
}
|
|
215
|
+
if (action !== 'update-branch' && object(pull.head).sha !== expected.headSHA)
|
|
216
|
+
mismatch();
|
|
217
|
+
if (action !== 'merge' && (pull.merged ?? false) !== expected.merged)
|
|
218
|
+
mismatch();
|
|
219
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
220
|
+
if (!step.path.endsWith('/comments'))
|
|
221
|
+
continue;
|
|
222
|
+
const comments = observation(step.path);
|
|
223
|
+
const previous = before[step.path];
|
|
224
|
+
const id = object(results[index]).id;
|
|
225
|
+
const added = comments.filter(row => !previous.some(old => old.id === row.id));
|
|
226
|
+
if (added.length !== 1 || added[0].id !== id || added[0].body !== object(step.payload).body || previous.some(old => !comments.some(row => equal(row, old))))
|
|
227
|
+
mismatch();
|
|
228
|
+
}
|
|
229
|
+
if (action !== 'merge' && action !== 'update-branch')
|
|
230
|
+
return;
|
|
231
|
+
const branchPath = base + '/branches/' + encodeURIComponent(String(expected.base));
|
|
232
|
+
const baseSHA = object(object(before[branchPath]).commit).id;
|
|
233
|
+
const commit = async (sha) => {
|
|
234
|
+
if (typeof sha !== 'string' || !/^[a-f0-9]{40}$/.test(sha))
|
|
235
|
+
return mismatch();
|
|
236
|
+
const path = base + '/git/commits/' + sha;
|
|
237
|
+
const data = object(await read(client, path));
|
|
238
|
+
if (data.sha !== sha || !Array.isArray(data.parents))
|
|
239
|
+
mismatch();
|
|
240
|
+
observations.push({ path, data });
|
|
241
|
+
return data;
|
|
242
|
+
};
|
|
243
|
+
if (action === 'update-branch') {
|
|
244
|
+
const head = await commit(object(pull.head).sha);
|
|
245
|
+
const pending = [head], visited = new Set();
|
|
246
|
+
while (pending.length && visited.size < 1000) {
|
|
247
|
+
const current = pending.pop();
|
|
248
|
+
if (visited.has(current.sha))
|
|
249
|
+
continue;
|
|
250
|
+
visited.add(current.sha);
|
|
251
|
+
if (visited.has(baseSHA) && visited.has(expected.headSHA))
|
|
252
|
+
return;
|
|
253
|
+
for (const parent of members(current, 'parents', 'sha')) {
|
|
254
|
+
if (!visited.has(parent))
|
|
255
|
+
pending.push(await commit(parent));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
mismatch();
|
|
259
|
+
}
|
|
260
|
+
if (pull.merged !== true || !pull.merge_commit_sha)
|
|
261
|
+
mismatch();
|
|
262
|
+
const finalSHA = object(object(observation(branchPath)).commit).id;
|
|
263
|
+
if (finalSHA !== pull.merge_commit_sha)
|
|
264
|
+
mismatch();
|
|
265
|
+
const final = await commit(finalSHA);
|
|
266
|
+
const style = object(plan.steps[0].payload).Do;
|
|
267
|
+
if (style === 'merge') {
|
|
268
|
+
if (!equal(members(final, 'parents', 'sha'), [baseSHA, expected.headSHA]))
|
|
269
|
+
mismatch();
|
|
270
|
+
}
|
|
271
|
+
else if (style === 'squash') {
|
|
272
|
+
if (!equal(members(final, 'parents', 'sha'), [baseSHA]) || finalSHA === expected.headSHA)
|
|
273
|
+
mismatch();
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const originals = before[plan.target + '/commits'];
|
|
277
|
+
if (!originals.length)
|
|
278
|
+
mismatch();
|
|
279
|
+
const bySHA = new Map(originals.map(row => [row.sha, row]));
|
|
280
|
+
let original = bySHA.get(expected.headSHA);
|
|
281
|
+
let current = final;
|
|
282
|
+
for (let index = 0; index < originals.length; index++) {
|
|
283
|
+
const parents = members(current, 'parents', 'sha');
|
|
284
|
+
if (!original || parents.length !== 1 || object(current.commit).message !== object(original.commit).message)
|
|
285
|
+
mismatch();
|
|
286
|
+
if (index === originals.length - 1) {
|
|
287
|
+
if (parents[0] !== baseSHA)
|
|
288
|
+
mismatch();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const originalParents = members(original, 'parents', 'sha');
|
|
292
|
+
if (originalParents.length !== 1)
|
|
293
|
+
mismatch();
|
|
294
|
+
original = bySHA.get(originalParents[0]);
|
|
295
|
+
current = await commit(parents[0]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
package/dist/reads.js
CHANGED
|
@@ -12,15 +12,15 @@ export const prFields = [...Object.keys(scalarFields), ...gaps, 'assignees', 'au
|
|
|
12
12
|
'headRefName', 'headRefOid', 'headRepository', 'headRepositoryOwner', 'isCrossRepository', 'labels', 'latestReviews', 'mergeCommit',
|
|
13
13
|
'mergeable', 'mergedBy', 'milestone', 'reactionGroups', 'reviewRequests', 'reviews', 'state', 'statusCheckRollup'];
|
|
14
14
|
export const issueFields = ['number', 'title', 'state', 'body', 'author', 'assignees', 'labels', 'milestone', 'createdAt', 'updatedAt', 'url'];
|
|
15
|
-
export function selectedFields(options, allowed) {
|
|
15
|
+
export function selectedFields(options, allowed, unavailable = gaps) {
|
|
16
16
|
if (typeof options.fields !== 'string')
|
|
17
17
|
return;
|
|
18
18
|
const fields = options.fields.split(',');
|
|
19
19
|
if (fields.some(field => !allowed.includes(field)) || new Set(fields).size !== fields.length)
|
|
20
20
|
throw new CliError('USAGE', 'Unknown or repeated field; choose ' + allowed.join(','), 2);
|
|
21
|
-
const gap = fields.find(field =>
|
|
21
|
+
const gap = fields.find(field => unavailable.includes(field));
|
|
22
22
|
if (gap)
|
|
23
|
-
throw new CliError('CAPABILITY_GAP', gap + ' has no evidenced Gitea equivalent');
|
|
23
|
+
throw new CliError('CAPABILITY_GAP', gap + ' has no evidenced Gitea 1.23.4 equivalent' + (gap === 'id' ? '; fullDatabaseId is the numeric vendor ID, not a GraphQL node ID' : ''));
|
|
24
24
|
if (options.full || options.comments || options.reviews || options.checks)
|
|
25
25
|
throw new CliError('USAGE', '--fields cannot combine with --full, --comments, --reviews or --checks', 2);
|
|
26
26
|
return fields;
|
|
@@ -107,9 +107,9 @@ export async function listIssues(client, base, kind, options) {
|
|
|
107
107
|
return { items: rows.slice(0, limit).map(row => {
|
|
108
108
|
const projection = { ...issue(row), isDraft: row.draft ?? null, headRefName: row.head ? object(row.head).ref : null, baseRefName: row.base ? object(row.base).ref : null };
|
|
109
109
|
return Object.fromEntries(fields.map(field => [field, projection[field]]));
|
|
110
|
-
}), count: Math.min(rows.length, limit), total: page.truncated ? null : rows.length,
|
|
110
|
+
}), count: Math.min(rows.length, limit), total: page.truncated ? null : rows.length, unfiltered_total: page.total,
|
|
111
111
|
truncated: page.truncated || rows.length > limit, next: page.next, continuation: page.truncated || rows.length > limit,
|
|
112
|
-
help: `gitea-axi ${kind} view <number> --repo ${base.split('/').slice(-2).join('/')}
|
|
112
|
+
help: [`gitea-axi ${kind} view <number> --repo ${base.split('/').slice(-2).join('/')}`, ...continuationHelp(page, rows.length, limit)] };
|
|
113
113
|
}
|
|
114
114
|
async function complete(client, path) {
|
|
115
115
|
const page = await collect(client, path);
|
|
@@ -129,7 +129,7 @@ async function reviews(client, path, full) {
|
|
|
129
129
|
}
|
|
130
130
|
export async function checks(client, base, sha) {
|
|
131
131
|
const combined = object(await read(client, base + '/commits/' + encodeURIComponent(sha) + '/status'));
|
|
132
|
-
if (combined.sha !== sha)
|
|
132
|
+
if (combined.sha !== sha && !(combined.sha === '' && combined.total_count === 0 && (combined.statuses === null || Array.isArray(combined.statuses) && !combined.statuses.length)))
|
|
133
133
|
throw new CliError('INVALID_RESPONSE', 'Check response does not match the pull request head SHA');
|
|
134
134
|
const statuses = await complete(client, base + '/commits/' + encodeURIComponent(sha) + '/statuses');
|
|
135
135
|
statuses.sort((a, b) => Number(b.id) - Number(a.id));
|
|
@@ -158,7 +158,7 @@ export async function watchChecks(client, base, number, options) {
|
|
|
158
158
|
if (typeof sha !== 'string' || !sha)
|
|
159
159
|
throw new CliError('INVALID_RESPONSE', 'Pull request head SHA is missing');
|
|
160
160
|
const result = await checks(client, base, sha);
|
|
161
|
-
if (!options.watch || !result.pending || result.failed || Date.now() >= stop || controller.signal.aborted)
|
|
161
|
+
if (!options.watch || result.items.length > 0 && !result.pending || result.failed || Date.now() >= stop || controller.signal.aborted)
|
|
162
162
|
return { ...result, cancelled: controller.signal.aborted };
|
|
163
163
|
await delay(Math.min(interval, Math.max(1, stop - Date.now())), undefined, { signal: controller.signal }).catch(error => {
|
|
164
164
|
if (!controller.signal.aborted)
|
|
@@ -209,7 +209,7 @@ export async function view(client, base, kind, number, options) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
if (options.checks || requested.has('statusCheckRollup'))
|
|
212
|
-
result.statusCheckRollup = await checks(client, base, head.sha);
|
|
212
|
+
result.statusCheckRollup = { ...await checks(client, base, head.sha), source: 'commit-statuses', partial: true };
|
|
213
213
|
if (requested.has('commits'))
|
|
214
214
|
result.commits = (await complete(client, path + '/commits')).map(commit => ({ oid: commit.sha, message: object(commit.commit).message }));
|
|
215
215
|
if (requested.has('files'))
|
|
@@ -223,8 +223,14 @@ export async function view(client, base, kind, number, options) {
|
|
|
223
223
|
}
|
|
224
224
|
return fields ? Object.fromEntries(fields.map(field => [field, result[field] ?? null])) : result;
|
|
225
225
|
}
|
|
226
|
+
export function continuationHelp(page, count, limit) {
|
|
227
|
+
if (!page.truncated && count <= limit)
|
|
228
|
+
return [];
|
|
229
|
+
return [page.truncated ? 'Use gitea-axi api GET "<collection>?limit=50&page=<page>" to page beyond the bounded read (maximum 1000 items).' : 'Raise --limit to see more items (maximum 1000).'];
|
|
230
|
+
}
|
|
226
231
|
export function boundedList(page, limit, fields) {
|
|
227
232
|
return { items: page.items.slice(0, limit).map(row => Object.fromEntries(fields.map(key => [key, row[key] ?? null]))),
|
|
228
233
|
count: Math.min(page.items.length, limit), total: page.total, next: page.next,
|
|
229
|
-
truncated: page.truncated || page.items.length > limit, continuation: page.truncated || page.items.length > limit
|
|
234
|
+
truncated: page.truncated || page.items.length > limit, continuation: page.truncated || page.items.length > limit,
|
|
235
|
+
...(continuationHelp(page, page.items.length, limit).length ? { help: continuationHelp(page, page.items.length, limit) } : {}) };
|
|
230
236
|
}
|
package/dist/registry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { baseline, commandGap, flagGap } from './catalogue.js';
|
|
1
2
|
export const command = {
|
|
2
3
|
path: ['repo', 'view'],
|
|
3
4
|
description: 'Read repository identity and default branch with server contract metadata.',
|
|
@@ -6,7 +7,7 @@ export const command = {
|
|
|
6
7
|
outcomes: ['AC-001', 'AC-002', 'AC-004'],
|
|
7
8
|
effect: 'read',
|
|
8
9
|
flags: {
|
|
9
|
-
repo: { type: 'string', default: 'matching local origin remote', description: 'Repository as OWNER/NAME' },
|
|
10
|
+
repo: { type: 'string', short: 'R', default: 'matching local origin remote', description: 'Repository as OWNER/NAME' },
|
|
10
11
|
origin: { type: 'string', default: 'selected profile', description: 'Gitea origin; HTTPS by default' },
|
|
11
12
|
'allow-insecure-http': {
|
|
12
13
|
type: 'boolean', default: false,
|
|
@@ -24,8 +25,9 @@ const globalFlags = {
|
|
|
24
25
|
origin: command.flags.origin, 'allow-insecure-http': command.flags['allow-insecure-http'], help: command.flags.help,
|
|
25
26
|
config: { type: 'string', description: 'Absolute configuration path' },
|
|
26
27
|
profile: { type: 'string', description: 'Select one file profile' },
|
|
28
|
+
hostname: { type: 'string', description: 'Select exactly one matching configured host:port; otherwise GH_HOST' },
|
|
27
29
|
};
|
|
28
|
-
const repo = { repo: { type: 'string', description: 'OWNER/NAME; otherwise a matching local origin remote' } };
|
|
30
|
+
const repo = { repo: { type: 'string', short: 'R', description: 'OWNER/NAME; otherwise a matching local origin remote' } };
|
|
29
31
|
const list = {
|
|
30
32
|
limit: { type: 'string', description: 'Maximum returned records, default 30, maximum 1000' },
|
|
31
33
|
state: { type: 'string', description: 'open, closed or all; default open' },
|
|
@@ -37,7 +39,50 @@ const detail = {
|
|
|
37
39
|
fields: list.fields, full: { type: 'boolean', description: 'Full body within response ceiling' },
|
|
38
40
|
comments: { type: 'boolean', description: 'Include comments' },
|
|
39
41
|
};
|
|
42
|
+
const writeFlags = {
|
|
43
|
+
'allow-write': { type: 'string', description: 'Comma-separated exact vendor operation IDs authorized for this workflow' },
|
|
44
|
+
preview: { type: 'boolean', description: 'Preview and bind this exact write without dispatch' },
|
|
45
|
+
confirm: { type: 'string', description: 'Execute the unchanged preview confirmation ID once' },
|
|
46
|
+
};
|
|
47
|
+
const text = { type: 'string' };
|
|
48
|
+
const repeated = { type: 'string', multiple: true };
|
|
49
|
+
const issueBody = { body: text, 'body-file': { type: 'string', description: 'Body from an owned file or - for bounded stdin' } };
|
|
50
|
+
const issueEdit = { title: text, ...issueBody, milestone: text };
|
|
51
|
+
const mutationDefinitions = [
|
|
52
|
+
['issue upload', { file: repeated, 'content-type': text }], ['pr upload', { file: repeated, 'content-type': text }], ['release upload', { file: repeated, 'content-type': text }],
|
|
53
|
+
['secret set', { org: text, input: { ...text, description: 'Protected value file or - for stdin; values never accepted in argv' } }],
|
|
54
|
+
['secret delete', { org: text }],
|
|
55
|
+
['variable set', { org: text, body: text, input: { ...text, description: 'Value file or - for stdin; preserves empty values' } }],
|
|
56
|
+
['variable delete', { org: text }],
|
|
57
|
+
['repo create', { public: { type: 'boolean' }, private: { type: 'boolean' }, internal: { type: 'boolean' }, description: text, template: text,
|
|
58
|
+
source: text, push: { type: 'boolean' }, remote: text, clone: { type: 'boolean' } }],
|
|
59
|
+
['repo edit', { description: text, visibility: text, 'default-branch': text, 'enable-issues': { type: 'boolean' }, 'enable-wiki': { type: 'boolean' } }],
|
|
60
|
+
['repo fork', { org: text, clone: { type: 'boolean' }, remote: text, source: text, push: { type: 'boolean' } }],
|
|
61
|
+
['release create', { attach: repeated, 'content-type': text, title: { ...text, short: 't' }, notes: { ...text, short: 'n' }, 'notes-file': { ...text, short: 'F' }, ...issueBody,
|
|
62
|
+
draft: { type: 'boolean', short: 'd' }, prerelease: { type: 'boolean', short: 'p' }, target: text,
|
|
63
|
+
'generate-notes': { type: 'boolean' }, 'notes-start-tag': text, 'notes-from-tag': { type: 'boolean' }, 'verify-tag': { type: 'boolean' },
|
|
64
|
+
'fail-on-no-commits': { type: 'boolean' }, latest: { type: 'boolean' }, 'discussion-category': text }],
|
|
65
|
+
['release edit', { attach: repeated, 'content-type': text, title: text, notes: { ...text, short: 'n' }, 'notes-file': { ...text, short: 'F' }, ...issueBody, draft: { type: 'boolean' }, prerelease: { type: 'boolean' }, latest: { type: 'boolean' } }],
|
|
66
|
+
['release delete', {}],
|
|
67
|
+
['pr create', { ...issueEdit, attach: repeated, 'content-type': text, base: text, head: text, assignee: repeated, label: repeated, reviewer: repeated, 'team-reviewer': repeated }],
|
|
68
|
+
['pr edit', { ...issueEdit, attach: repeated, 'content-type': text, base: text, 'add-label': repeated, 'remove-label': repeated, 'add-assignee': repeated, 'remove-assignee': repeated, 'add-reviewer': repeated, 'remove-reviewer': repeated, 'add-team-reviewer': repeated, 'remove-team-reviewer': repeated }],
|
|
69
|
+
['pr close', { comment: text }], ['pr reopen', {}], ['pr comment', { ...issueBody, attach: repeated, 'content-type': text }],
|
|
70
|
+
['pr update-branch', {}],
|
|
71
|
+
['pr merge', { method: text, merge: { type: 'boolean' }, squash: { type: 'boolean' }, rebase: { type: 'boolean' }, 'match-head-commit': text,
|
|
72
|
+
auto: { type: 'boolean' }, admin: { type: 'boolean' }, 'delete-branch': { type: 'boolean' }, subject: text, ...issueBody }],
|
|
73
|
+
['pr review', { approve: { type: 'boolean' }, 'request-changes': { type: 'boolean' }, comment: { type: 'boolean' }, 'match-head-commit': text, ...issueBody }],
|
|
74
|
+
['issue create', { ...issueEdit, attach: repeated, 'content-type': text, assignee: repeated, label: repeated }],
|
|
75
|
+
['issue edit', { ...issueEdit, attach: repeated, 'content-type': text, 'add-label': repeated, 'remove-label': repeated, 'add-assignee': repeated, 'remove-assignee': repeated }],
|
|
76
|
+
['issue close', { comment: text }], ['issue reopen', {}], ['issue comment', { ...issueBody, attach: repeated, 'content-type': text }],
|
|
77
|
+
['issue delete', {}], ['issue pin', {}], ['issue unpin', {}],
|
|
78
|
+
['label create', { name: text, color: text, description: text }],
|
|
79
|
+
['label edit', { name: text, color: text, description: text }], ['label delete', {}],
|
|
80
|
+
];
|
|
40
81
|
const definitions = [
|
|
82
|
+
['issue download', { ...repo, dir: text, output: text, 'asset-id': text, pattern: repeated, sha256: text, 'content-type': text, clobber: { type: 'boolean' } }],
|
|
83
|
+
['pr download', { ...repo, dir: text, output: text, 'asset-id': text, pattern: repeated, sha256: text, 'content-type': text, clobber: { type: 'boolean' } }],
|
|
84
|
+
['release download', { ...repo, dir: text, output: text, 'asset-id': text, pattern: repeated, sha256: text, 'content-type': text, clobber: { type: 'boolean' } }],
|
|
85
|
+
['secret list', { ...repo, org: text, limit: list.limit }], ['variable list', { ...repo, org: text, limit: list.limit }],
|
|
41
86
|
['dashboard', repo], ['auth status', {}], ['profile list', {}], ['config path', {}], ['doctor', {}],
|
|
42
87
|
['repo list', { limit: list.limit, org: { type: 'string', description: 'List repositories belonging to this organization' } }],
|
|
43
88
|
['org view', {}], ['issue list', { ...repo, ...list, assignee: { type: 'string' }, author: { type: 'string' }, label: { type: 'string' }, milestone: { type: 'string' } }],
|
|
@@ -45,11 +90,100 @@ const definitions = [
|
|
|
45
90
|
['pr list', { ...repo, ...list, assignee: { type: 'string' }, author: { type: 'string' }, label: { type: 'string' }, milestone: { type: 'string' } }],
|
|
46
91
|
['pr view', { ...repo, ...detail, reviews: { type: 'boolean' }, checks: { type: 'boolean' } }],
|
|
47
92
|
['pr checks', { ...repo, watch: { type: 'boolean' }, timeout: { type: 'string', description: 'Watch deadline seconds, required with --watch' }, interval: { type: 'string', description: 'Watch interval seconds, default 5' } }],
|
|
48
|
-
['pr diff', { ...repo, full: detail.full }],
|
|
49
|
-
['release list', { ...repo, limit: list.limit }
|
|
93
|
+
['pr diff', { ...repo, full: detail.full }],
|
|
94
|
+
['release list', { ...repo, limit: list.limit, 'exclude-drafts': { type: 'boolean' }, 'exclude-pre-releases': { type: 'boolean' } }],
|
|
95
|
+
['release view', { ...repo, full: detail.full }], ['api', { ...writeFlags,
|
|
96
|
+
method: { type: 'string', short: 'X', description: 'HTTP method; cannot combine with a positional method' },
|
|
97
|
+
field: { type: 'string', multiple: true, description: 'Repeatable key=value; numbers, booleans and null become JSON values' },
|
|
98
|
+
'raw-field': { type: 'string', multiple: true, description: 'Repeatable key=value string' },
|
|
99
|
+
input: { type: 'string', description: 'Protected JSON request file; use - for stdin' } }],
|
|
100
|
+
['write reconcile', {}],
|
|
50
101
|
];
|
|
51
102
|
export const commands = [
|
|
52
103
|
{ ...command, flags: { ...command.flags, ...globalFlags } },
|
|
53
104
|
...definitions.map(([name, flags]) => ({ path: name.split(' '), description: name === 'dashboard' ? 'Bounded repository dashboard with explicit partial results' : name,
|
|
54
|
-
flags: { ...globalFlags, ...flags }, effect: 'read' })),
|
|
105
|
+
flags: { ...globalFlags, ...flags }, effect: name.endsWith(' download') ? 'download' : 'read' })),
|
|
106
|
+
...mutationDefinitions.map(([name, flags]) => ({ path: name.split(' '), description: name === 'pr review' ? 'pr review (unresolved vendor events and atomic stale-head refusal; no posting)' : name,
|
|
107
|
+
flags: { ...globalFlags, ...repo, ...writeFlags, ...flags }, effect: 'write' })),
|
|
108
|
+
];
|
|
109
|
+
const strings = (...names) => Object.fromEntries(names.map(name => [name, { type: 'string' }]));
|
|
110
|
+
const booleans = (...names) => Object.fromEntries(names.map(name => [name, { type: 'boolean' }]));
|
|
111
|
+
const stageFive = [
|
|
112
|
+
['run list', strings('workflow', 'branch', 'status', 'event', 'user', 'commit', 'limit', 'fields'), 'read'],
|
|
113
|
+
['run view', { ...strings('job', 'conclusion'), ...booleans('log', 'verbose', 'log-failed') }, 'read'],
|
|
114
|
+
['run watch', strings('timeout', 'interval'), 'read'],
|
|
115
|
+
['run rerun', { ...strings('job'), ...booleans('failed', 'debug') }, 'pending-write'],
|
|
116
|
+
['run cancel', {}, 'pending-write'], ['run delete', {}, 'pending-write'],
|
|
117
|
+
['run download', strings('name', 'dir'), 'download'],
|
|
118
|
+
['workflow list', { ...strings('limit'), ...booleans('all') }, 'read'],
|
|
119
|
+
['workflow view', booleans('yaml'), 'read'],
|
|
120
|
+
['workflow run', strings('ref', 'field'), 'pending-write'],
|
|
121
|
+
['workflow enable', {}, 'pending-write'], ['workflow disable', {}, 'pending-write'],
|
|
122
|
+
...['issues', 'prs', 'repos', 'commits', 'code'].map((kind) => [
|
|
123
|
+
'search ' + kind,
|
|
124
|
+
{ ...strings('owner', 'state', 'label', 'assignee', 'author', 'sort', 'order', 'limit', 'fields', 'review', 'language', 'stars', 'revision'), ...booleans('draft') },
|
|
125
|
+
'read',
|
|
126
|
+
]),
|
|
127
|
+
];
|
|
128
|
+
commands.push(...stageFive.map(([name, flags, effect]) => ({ path: name.split(' '), description: name + (effect === 'pending-write' ? ' (pending shared write gate)' : ''), flags: { ...globalFlags, ...repo, ...flags }, effect })));
|
|
129
|
+
const workspace = { workspace: { ...text, description: 'Absolute native Git repository root; clone uses an existing absolute parent directory' } };
|
|
130
|
+
const stackDefinitions = [
|
|
131
|
+
['view', {}], ['init', strings('base')], ['add', { ...strings('message'), ...booleans('all', 'update') }],
|
|
132
|
+
['checkout', {}], ['push', strings('remote')], ['submit', { ...strings('remote'), ...booleans('open') }],
|
|
133
|
+
['sync', { ...strings('remote'), ...booleans('prune') }],
|
|
134
|
+
['rebase', { ...strings('remote'), ...booleans('downstack', 'upstack', 'no-trunk', 'continue', 'abort', 'committer-date-is-author-date', 'preserve-dates') }],
|
|
135
|
+
['link', { ...strings('base', 'remote'), ...booleans('open') }], ['unstack', booleans('local')],
|
|
136
|
+
['merge', { ...strings('merge-method'), ...booleans('merge', 'squash', 'rebase') }],
|
|
137
|
+
...['up', 'down', 'top', 'bottom', 'trunk'].map((name) => [name, {}]),
|
|
138
|
+
];
|
|
139
|
+
commands.push(...stackDefinitions.map(([name, flags]) => ({ path: ['stack', name], description: 'Owned local stack ' + name,
|
|
140
|
+
flags: { ...globalFlags, ...workspace, ...writeFlags, ...flags }, effect: name === 'view' ? 'local-read' : 'local-write' })));
|
|
141
|
+
commands.push(...['repo clone', 'pr checkout', 'pr revert'].map(name => ({ path: name.split(' '), description: name + ' in an explicit standalone workspace',
|
|
142
|
+
flags: { ...globalFlags, ...repo, ...workspace, ...writeFlags }, effect: 'local-write' })));
|
|
143
|
+
for (const command of commands) {
|
|
144
|
+
if (['repo create', 'repo fork'].includes(command.path.join(' ')))
|
|
145
|
+
Object.assign(command.flags, workspace);
|
|
146
|
+
}
|
|
147
|
+
commands.push({ path: ['capabilities'], description: 'Inspect all baseline command IDs, flags, globals and PR fields without claiming full parity', flags: { help: globalFlags.help }, effect: 'local-read' });
|
|
148
|
+
const refusedFlags = [
|
|
149
|
+
['repo create', ['internal']],
|
|
150
|
+
['release create', ['latest', 'discussion-category']],
|
|
151
|
+
['release edit', ['latest']],
|
|
152
|
+
['pr merge', ['auto', 'admin', 'delete-branch']],
|
|
153
|
+
['run list', ['user']],
|
|
154
|
+
['search issues', ['review', 'language', 'stars', 'revision', 'draft']],
|
|
155
|
+
['search prs', ['review', 'language', 'stars', 'revision']],
|
|
156
|
+
['search repos', ['state', 'label', 'assignee', 'author', 'review', 'revision', 'draft']],
|
|
157
|
+
['search commits', ['owner', 'state', 'label', 'assignee', 'review', 'language', 'stars', 'draft']],
|
|
158
|
+
['stack rebase', ['preserve-dates']],
|
|
159
|
+
['stack link', ['open']],
|
|
160
|
+
['stack submit', ['open']],
|
|
55
161
|
];
|
|
162
|
+
for (const [path, names] of refusedFlags) {
|
|
163
|
+
const command = commands.find(command => command.path.join(' ') === path);
|
|
164
|
+
for (const name of names)
|
|
165
|
+
command.flags[name] = { ...command.flags[name], gap: flagGap(path, name) };
|
|
166
|
+
}
|
|
167
|
+
// Retain unsupported leaves and options in the same registry that owns help and admission metadata.
|
|
168
|
+
const booleanFlags = new Set(['comments', 'full', 'public', 'secret', 'files', 'raw', 'web', 'no-type', 'draft', 'failed', 'fail-fast', 'patch', 'undo', 'all', 'prune', 'open', 'local', 'merge', 'squash', 'rebase', 'downstack', 'upstack', 'no-trunk', 'continue', 'abort', 'committer-date-is-author-date', 'preserve-dates', 'check', 'closed', 'clear', 'remove']);
|
|
169
|
+
for (const row of baseline) {
|
|
170
|
+
if (row.path === '--version' || row.path.startsWith('api ') || row.path === 'update --check')
|
|
171
|
+
continue;
|
|
172
|
+
if (!commands.some(command => command.path.join(' ') === row.path)) {
|
|
173
|
+
const gap = commandGap(row.path);
|
|
174
|
+
if (!gap)
|
|
175
|
+
throw new Error('Missing capability decision for ' + row.id);
|
|
176
|
+
commands.push({ path: row.path.split(' '), description: row.path + ' (compatibility gap)', flags: { ...globalFlags, ...repo }, effect: 'gap', gap });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const row of baseline) {
|
|
180
|
+
const path = row.path.startsWith('api ') ? 'api' : row.path === 'update --check' ? 'update' : row.path;
|
|
181
|
+
const command = commands.find(command => command.path.join(' ') === path);
|
|
182
|
+
if (!command)
|
|
183
|
+
continue;
|
|
184
|
+
for (const flag of row.flags) {
|
|
185
|
+
const name = flag.name === '-X' ? 'method' : flag.name.replace(/^--/, '');
|
|
186
|
+
if (!command.flags[name])
|
|
187
|
+
command.flags[name] = { type: booleanFlags.has(name) ? 'boolean' : 'string', description: 'Compatibility gap: ' + flagGap(path, name), gap: flagGap(path, name) };
|
|
188
|
+
}
|
|
189
|
+
}
|