@remogram/cli 0.1.0-beta.5 → 0.1.0-beta.8
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/cli-argv.js +50 -0
- package/cli-dispatch.js +499 -0
- package/cli-doctor.js +215 -0
- package/cli-io.js +51 -0
- package/index.js +7 -575
- package/package.json +7 -7
package/cli-argv.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ERROR_CODES, forgeError } from '@remogram/core';
|
|
2
|
+
|
|
3
|
+
export const REPEATABLE_FLAGS = new Set(['allowed_path']);
|
|
4
|
+
|
|
5
|
+
export function parseAllowedPathFlags(flags) {
|
|
6
|
+
if (flags.allowed_path == null) return undefined;
|
|
7
|
+
return Array.isArray(flags.allowed_path) ? flags.allowed_path : [flags.allowed_path];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parsePositiveInt(value, name) {
|
|
11
|
+
if (value == null) return undefined;
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
14
|
+
throw Object.assign(new Error(`Invalid ${name}`), {
|
|
15
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, `${name} must be a positive integer`),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseCliArgv(argv) {
|
|
22
|
+
const positional = [];
|
|
23
|
+
let asJson = false;
|
|
24
|
+
const flags = {};
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
27
|
+
const arg = argv[i];
|
|
28
|
+
if (arg === '--json') asJson = true;
|
|
29
|
+
else if (arg.startsWith('--')) {
|
|
30
|
+
const key = arg.slice(2).replace(/-/g, '_');
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
if (REPEATABLE_FLAGS.has(key)) {
|
|
33
|
+
if (!flags[key]) flags[key] = [];
|
|
34
|
+
if (next != null && !next.startsWith('--')) {
|
|
35
|
+
flags[key].push(next);
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
} else if (next != null && !next.startsWith('--')) {
|
|
39
|
+
flags[key] = next;
|
|
40
|
+
i += 1;
|
|
41
|
+
} else {
|
|
42
|
+
flags[key] = true;
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
positional.push(arg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { positional, asJson, flags };
|
|
50
|
+
}
|
package/cli-dispatch.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forgePacket,
|
|
3
|
+
PACKET_TYPES,
|
|
4
|
+
ERROR_CODES,
|
|
5
|
+
forgeError,
|
|
6
|
+
sanitizeField,
|
|
7
|
+
assertGitRef,
|
|
8
|
+
assertGitRemote,
|
|
9
|
+
throwIfStaleHeadByNumber,
|
|
10
|
+
FACT_INVENTORY_PACKET_TYPES,
|
|
11
|
+
forgeFactInventoryPacket,
|
|
12
|
+
assertWriteCommandConfigured,
|
|
13
|
+
parseSinceObservedAt,
|
|
14
|
+
normalizeAllowedPaths,
|
|
15
|
+
assertExpectedSha,
|
|
16
|
+
buildMergeExecuteBeforeFacts,
|
|
17
|
+
collectMergeExecuteBlockers,
|
|
18
|
+
buildCrMergeBlockedBody,
|
|
19
|
+
buildCrMergedBody,
|
|
20
|
+
buildMergeExecuteAfterFacts,
|
|
21
|
+
buildMergeExecuteMergeFacts,
|
|
22
|
+
mergeExecuteViewFacts,
|
|
23
|
+
isOpenPrState,
|
|
24
|
+
} from '@remogram/core';
|
|
25
|
+
import { parseAllowedPathFlags, parsePositiveInt } from './cli-argv.js';
|
|
26
|
+
|
|
27
|
+
export async function dispatchForgeCommand({ group, sub, flags, positional, ctx, provider }) {
|
|
28
|
+
if (group === 'provider' && sub === 'capabilities') {
|
|
29
|
+
return forgePacket(
|
|
30
|
+
PACKET_TYPES.PROVIDER_CAPABILITIES,
|
|
31
|
+
ctx,
|
|
32
|
+
await provider.providerCapabilities(ctx),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (group === 'repo' && sub === 'status') {
|
|
36
|
+
return forgePacket(PACKET_TYPES.REPO_STATUS, ctx, await provider.repoStatus(ctx));
|
|
37
|
+
}
|
|
38
|
+
if (group === 'refs' && sub === 'compare') {
|
|
39
|
+
if (!flags.base || !flags.head) {
|
|
40
|
+
throw Object.assign(new Error('--base and --head required'), {
|
|
41
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--base and --head required'),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
assertGitRef(flags.base, '--base');
|
|
45
|
+
assertGitRef(flags.head, '--head');
|
|
46
|
+
return forgePacket(
|
|
47
|
+
PACKET_TYPES.REF_COMPARE,
|
|
48
|
+
ctx,
|
|
49
|
+
await provider.refsCompare(ctx, flags.base, flags.head),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (group === 'refs' && sub === 'inventory') {
|
|
53
|
+
if (typeof provider.refsInventory !== 'function') {
|
|
54
|
+
throw Object.assign(new Error('refs inventory not implemented for provider'), {
|
|
55
|
+
forgeError: forgeError(
|
|
56
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
57
|
+
'refs inventory not implemented for provider',
|
|
58
|
+
),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return forgeFactInventoryPacket(
|
|
62
|
+
FACT_INVENTORY_PACKET_TYPES.REF_INVENTORY,
|
|
63
|
+
ctx,
|
|
64
|
+
await provider.refsInventory(ctx),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (group === 'cr' && sub === 'inventory') {
|
|
68
|
+
if (typeof provider.crInventory !== 'function') {
|
|
69
|
+
throw Object.assign(new Error('cr inventory not implemented for provider'), {
|
|
70
|
+
forgeError: forgeError(
|
|
71
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
72
|
+
'cr inventory not implemented for provider',
|
|
73
|
+
),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const inventoryBody = await provider.crInventory(ctx, {
|
|
77
|
+
slice_ref: flags.slice_ref,
|
|
78
|
+
limit: parsePositiveInt(flags.limit, '--limit'),
|
|
79
|
+
sort: flags.sort,
|
|
80
|
+
});
|
|
81
|
+
if (inventoryBody.list_truncated === true) {
|
|
82
|
+
throw Object.assign(new Error('Open CR list incomplete'), {
|
|
83
|
+
forgeError: forgeError(
|
|
84
|
+
ERROR_CODES.INVENTORY_LIST_INCOMPLETE,
|
|
85
|
+
'Open change request list could not be proved complete within pagination bounds',
|
|
86
|
+
null,
|
|
87
|
+
{
|
|
88
|
+
inventory_list: {
|
|
89
|
+
entry_count: inventoryBody.entry_count,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return forgeFactInventoryPacket(
|
|
96
|
+
FACT_INVENTORY_PACKET_TYPES.CR_INVENTORY_SLICE,
|
|
97
|
+
ctx,
|
|
98
|
+
inventoryBody,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (group === 'cr' && sub === 'files') {
|
|
102
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
103
|
+
if (number == null) {
|
|
104
|
+
throw Object.assign(new Error('--number required'), {
|
|
105
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for cr files'),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (typeof provider.crFiles !== 'function') {
|
|
109
|
+
throw Object.assign(new Error('cr files not implemented for provider'), {
|
|
110
|
+
forgeError: forgeError(
|
|
111
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
112
|
+
'cr files not implemented for provider',
|
|
113
|
+
),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return forgePacket(
|
|
117
|
+
PACKET_TYPES.CR_FILES,
|
|
118
|
+
ctx,
|
|
119
|
+
await provider.crFiles(ctx, { number }),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (group === 'cr' && sub === 'comments') {
|
|
123
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
124
|
+
if (number == null) {
|
|
125
|
+
throw Object.assign(new Error('--number required'), {
|
|
126
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for cr comments'),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (typeof provider.crComments !== 'function') {
|
|
130
|
+
throw Object.assign(new Error('cr comments not implemented for provider'), {
|
|
131
|
+
forgeError: forgeError(
|
|
132
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
133
|
+
'cr comments not implemented for provider',
|
|
134
|
+
),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return forgePacket(
|
|
138
|
+
PACKET_TYPES.CR_COMMENTS,
|
|
139
|
+
ctx,
|
|
140
|
+
await provider.crComments(ctx, { number }),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (group === 'forge' && sub === 'changes') {
|
|
144
|
+
const sinceIso = parseSinceObservedAt(flags.since);
|
|
145
|
+
if (typeof provider.forgeChanges !== 'function') {
|
|
146
|
+
throw Object.assign(new Error('forge changes not implemented for provider'), {
|
|
147
|
+
forgeError: forgeError(
|
|
148
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
149
|
+
'forge changes not implemented for provider',
|
|
150
|
+
),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return forgePacket(
|
|
154
|
+
PACKET_TYPES.FORGE_CHANGES,
|
|
155
|
+
ctx,
|
|
156
|
+
await provider.forgeChanges(ctx, { since: sinceIso }),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (group === 'cr' && sub === 'open') {
|
|
160
|
+
if (typeof provider.crOpen !== 'function') {
|
|
161
|
+
throw Object.assign(new Error('cr open not implemented for provider'), {
|
|
162
|
+
forgeError: forgeError(
|
|
163
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
164
|
+
'cr open not implemented for provider',
|
|
165
|
+
),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (!flags.head || !flags.base || !flags.title) {
|
|
169
|
+
throw Object.assign(new Error('--head, --base, and --title required'), {
|
|
170
|
+
forgeError: forgeError(
|
|
171
|
+
ERROR_CODES.INVALID_ARGS,
|
|
172
|
+
'--head, --base, and --title required for cr open',
|
|
173
|
+
),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
assertGitRef(flags.head, '--head');
|
|
177
|
+
assertGitRef(flags.base, '--base');
|
|
178
|
+
assertWriteCommandConfigured(ctx.config, 'cr_open');
|
|
179
|
+
return forgePacket(
|
|
180
|
+
PACKET_TYPES.CHANGE_REQUEST_OPENED,
|
|
181
|
+
ctx,
|
|
182
|
+
await provider.crOpen(ctx, {
|
|
183
|
+
head: flags.head,
|
|
184
|
+
base: flags.base,
|
|
185
|
+
title: flags.title,
|
|
186
|
+
body: flags.body,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (group === 'status' && sub === 'set') {
|
|
191
|
+
if (typeof provider.statusSet !== 'function') {
|
|
192
|
+
throw Object.assign(new Error('status set not implemented for provider'), {
|
|
193
|
+
forgeError: forgeError(
|
|
194
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
195
|
+
'status set not implemented for provider',
|
|
196
|
+
),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
assertWriteCommandConfigured(ctx.config, 'status_set');
|
|
200
|
+
return forgePacket(
|
|
201
|
+
PACKET_TYPES.COMMIT_STATUS_SET,
|
|
202
|
+
ctx,
|
|
203
|
+
await provider.statusSet(ctx, {
|
|
204
|
+
sha: flags.sha,
|
|
205
|
+
context: flags.context,
|
|
206
|
+
state: flags.state,
|
|
207
|
+
target_url: flags.target_url,
|
|
208
|
+
description: flags.description,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (group === 'pr' && sub === 'view') {
|
|
213
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
214
|
+
if (number == null) {
|
|
215
|
+
throw Object.assign(new Error('--number required'), {
|
|
216
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for pr view'),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const body = await provider.prView(ctx, { number });
|
|
220
|
+
throwIfStaleHeadByNumber(
|
|
221
|
+
ctx,
|
|
222
|
+
PACKET_TYPES.PR_STATUS,
|
|
223
|
+
body,
|
|
224
|
+
body.forge_source_branch_ref,
|
|
225
|
+
body.forge_source_sha,
|
|
226
|
+
);
|
|
227
|
+
return forgePacket(PACKET_TYPES.PR_STATUS, ctx, body);
|
|
228
|
+
}
|
|
229
|
+
if (group === 'pr' && sub === 'checks') {
|
|
230
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
231
|
+
if (number == null && !flags.ref) {
|
|
232
|
+
throw Object.assign(new Error('--number or --ref required'), {
|
|
233
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number or --ref required for pr checks'),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (flags.ref) assertGitRef(flags.ref, '--ref');
|
|
237
|
+
if (number != null && !flags.ref) {
|
|
238
|
+
const view = await provider.prView(ctx, { number });
|
|
239
|
+
throwIfStaleHeadByNumber(
|
|
240
|
+
ctx,
|
|
241
|
+
PACKET_TYPES.PR_CHECKS,
|
|
242
|
+
{ forge_source_sha: view.forge_source_sha },
|
|
243
|
+
view.forge_source_branch_ref,
|
|
244
|
+
view.forge_source_sha,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return forgePacket(
|
|
248
|
+
PACKET_TYPES.PR_CHECKS,
|
|
249
|
+
ctx,
|
|
250
|
+
await provider.prChecks(ctx, { number, ref: flags.ref }),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (group === 'merge' && sub === 'plan') {
|
|
254
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
255
|
+
if (number == null) {
|
|
256
|
+
throw Object.assign(new Error('--number required'), {
|
|
257
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for merge plan'),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const allowedPaths = normalizeAllowedPaths(parseAllowedPathFlags(flags) ?? []);
|
|
261
|
+
return forgePacket(
|
|
262
|
+
PACKET_TYPES.MERGE_PLAN,
|
|
263
|
+
ctx,
|
|
264
|
+
await provider.mergePlan(ctx, {
|
|
265
|
+
number,
|
|
266
|
+
...(allowedPaths ? { allowed_paths: allowedPaths } : {}),
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (group === 'merge' && sub === 'execute') {
|
|
271
|
+
if (typeof provider.mergeExecute !== 'function') {
|
|
272
|
+
throw Object.assign(new Error('merge execute not implemented for provider'), {
|
|
273
|
+
forgeError: forgeError(
|
|
274
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
275
|
+
'merge execute not implemented for provider',
|
|
276
|
+
),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
280
|
+
if (number == null) {
|
|
281
|
+
throw Object.assign(new Error('--number required'), {
|
|
282
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for merge execute'),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (!flags.expected_base_sha || !flags.expected_head_sha) {
|
|
286
|
+
throw Object.assign(new Error('expected SHAs required'), {
|
|
287
|
+
forgeError: forgeError(
|
|
288
|
+
ERROR_CODES.INVALID_ARGS,
|
|
289
|
+
'--expected-base-sha and --expected-head-sha required for merge execute',
|
|
290
|
+
),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const method = flags.method ? String(flags.method).toLowerCase() : 'merge';
|
|
294
|
+
if (method !== 'merge') {
|
|
295
|
+
throw Object.assign(new Error('Unsupported merge method'), {
|
|
296
|
+
forgeError: forgeError(
|
|
297
|
+
ERROR_CODES.INVALID_ARGS,
|
|
298
|
+
'Only --method merge is supported in v1',
|
|
299
|
+
),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const expectedBaseSha = assertExpectedSha(flags.expected_base_sha, '--expected-base-sha');
|
|
303
|
+
const expectedHeadSha = assertExpectedSha(flags.expected_head_sha, '--expected-head-sha');
|
|
304
|
+
assertWriteCommandConfigured(ctx.config, 'merge');
|
|
305
|
+
|
|
306
|
+
const view = await provider.prView(ctx, { number });
|
|
307
|
+
const checks = await provider.prChecks(ctx, { number });
|
|
308
|
+
const mergePlanBody = await provider.mergePlan(ctx, { number });
|
|
309
|
+
const expected = { baseSha: expectedBaseSha, headSha: expectedHeadSha };
|
|
310
|
+
const viewFacts = mergeExecuteViewFacts(view);
|
|
311
|
+
|
|
312
|
+
let forgeHeadRefSha = null;
|
|
313
|
+
const headRef = viewFacts.sourceBranchRef ? String(viewFacts.sourceBranchRef).trim() : '';
|
|
314
|
+
if (!headRef && isOpenPrState(view.state)) {
|
|
315
|
+
const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, null);
|
|
316
|
+
return forgePacket(
|
|
317
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
318
|
+
ctx,
|
|
319
|
+
buildCrMergeBlockedBody({
|
|
320
|
+
prNumber: number,
|
|
321
|
+
expected,
|
|
322
|
+
before,
|
|
323
|
+
blockers: ['head_ref_missing'],
|
|
324
|
+
}),
|
|
325
|
+
forgeError(ERROR_CODES.MERGE_BLOCKED, 'Open change request missing head branch ref'),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (headRef) {
|
|
329
|
+
if (typeof provider.branchHeadSha !== 'function') {
|
|
330
|
+
const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, null);
|
|
331
|
+
return forgePacket(
|
|
332
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
333
|
+
ctx,
|
|
334
|
+
buildCrMergeBlockedBody({
|
|
335
|
+
prNumber: number,
|
|
336
|
+
expected,
|
|
337
|
+
before,
|
|
338
|
+
blockers: ['head_ref_unverified'],
|
|
339
|
+
}),
|
|
340
|
+
forgeError(
|
|
341
|
+
ERROR_CODES.MERGE_BLOCKED,
|
|
342
|
+
'Forge head branch verification not implemented for provider',
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
assertGitRef(headRef, 'head_ref');
|
|
348
|
+
} catch (err) {
|
|
349
|
+
const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, null);
|
|
350
|
+
return forgePacket(
|
|
351
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
352
|
+
ctx,
|
|
353
|
+
buildCrMergeBlockedBody({
|
|
354
|
+
prNumber: number,
|
|
355
|
+
expected,
|
|
356
|
+
before,
|
|
357
|
+
blockers: ['head_ref_invalid'],
|
|
358
|
+
}),
|
|
359
|
+
forgeError(
|
|
360
|
+
ERROR_CODES.INVALID_ARGS,
|
|
361
|
+
sanitizeField(err.forgeError?.message || err.message || err.invalidArgs)
|
|
362
|
+
|| 'Head branch ref invalid',
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
forgeHeadRefSha = await provider.branchHeadSha(ctx, headRef, {
|
|
368
|
+
repoId: view.forge_source_repo_id ?? null,
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, null);
|
|
372
|
+
return forgePacket(
|
|
373
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
374
|
+
ctx,
|
|
375
|
+
buildCrMergeBlockedBody({
|
|
376
|
+
prNumber: number,
|
|
377
|
+
expected,
|
|
378
|
+
before,
|
|
379
|
+
blockers: ['head_ref_unreadable'],
|
|
380
|
+
}),
|
|
381
|
+
forgeError(
|
|
382
|
+
ERROR_CODES.MERGE_BLOCKED,
|
|
383
|
+
sanitizeField(err.forgeError?.message || err.message) || 'Head branch ref unreadable',
|
|
384
|
+
err.status ?? err.forgeError?.status ?? null,
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, forgeHeadRefSha);
|
|
391
|
+
const blockers = collectMergeExecuteBlockers(
|
|
392
|
+
view,
|
|
393
|
+
checks,
|
|
394
|
+
mergePlanBody,
|
|
395
|
+
expected,
|
|
396
|
+
{ forgeHeadRefSha },
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (blockers.length > 0) {
|
|
400
|
+
return forgePacket(
|
|
401
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
402
|
+
ctx,
|
|
403
|
+
buildCrMergeBlockedBody({ prNumber: number, expected, before, blockers }),
|
|
404
|
+
forgeError(ERROR_CODES.MERGE_BLOCKED, 'Merge blocked by preflight'),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const providerResult = await provider.mergeExecute(ctx, {
|
|
410
|
+
number,
|
|
411
|
+
method,
|
|
412
|
+
expectedHeadSha: expected.headSha,
|
|
413
|
+
});
|
|
414
|
+
const merge = buildMergeExecuteMergeFacts(method, providerResult);
|
|
415
|
+
const after = buildMergeExecuteAfterFacts(view, providerResult);
|
|
416
|
+
return forgePacket(
|
|
417
|
+
PACKET_TYPES.CR_MERGED,
|
|
418
|
+
ctx,
|
|
419
|
+
buildCrMergedBody({ prNumber: number, expected, before, merge, after }),
|
|
420
|
+
);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
const status = err.status ?? err.forgeError?.status ?? null;
|
|
423
|
+
const blockers =
|
|
424
|
+
Array.isArray(err.mergeBlockedBlockers) && err.mergeBlockedBlockers.length > 0
|
|
425
|
+
? err.mergeBlockedBlockers
|
|
426
|
+
: ['merge_endpoint_failed'];
|
|
427
|
+
const fe =
|
|
428
|
+
err.forgeError
|
|
429
|
+
?? forgeError(
|
|
430
|
+
ERROR_CODES.MERGE_ENDPOINT_FAILED,
|
|
431
|
+
sanitizeField(err.message) || 'Forge merge request failed',
|
|
432
|
+
status,
|
|
433
|
+
);
|
|
434
|
+
return forgePacket(
|
|
435
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
436
|
+
ctx,
|
|
437
|
+
buildCrMergeBlockedBody({
|
|
438
|
+
prNumber: number,
|
|
439
|
+
expected,
|
|
440
|
+
before,
|
|
441
|
+
blockers,
|
|
442
|
+
}),
|
|
443
|
+
fe,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (group === 'branch' && sub === 'protection') {
|
|
448
|
+
const branchRef = flags.branch_ref;
|
|
449
|
+
if (!branchRef) {
|
|
450
|
+
throw Object.assign(new Error('--branch-ref required'), {
|
|
451
|
+
forgeError: forgeError(
|
|
452
|
+
ERROR_CODES.INVALID_ARGS,
|
|
453
|
+
'--branch-ref required for branch protection',
|
|
454
|
+
),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
assertGitRef(branchRef, '--branch-ref');
|
|
458
|
+
if (typeof provider.branchProtection !== 'function') {
|
|
459
|
+
throw Object.assign(new Error('branch protection not implemented for provider'), {
|
|
460
|
+
forgeError: forgeError(
|
|
461
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
462
|
+
'branch protection not implemented for provider',
|
|
463
|
+
),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return forgePacket(
|
|
467
|
+
PACKET_TYPES.BRANCH_PROTECTION,
|
|
468
|
+
ctx,
|
|
469
|
+
await provider.branchProtection(ctx, { branchRef }),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (group === 'whoami' && sub == null) {
|
|
473
|
+
if (typeof provider.whoami !== 'function') {
|
|
474
|
+
throw Object.assign(new Error('whoami not implemented for provider'), {
|
|
475
|
+
forgeError: forgeError(
|
|
476
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
477
|
+
'whoami not implemented for provider',
|
|
478
|
+
),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return forgePacket(PACKET_TYPES.PROVIDER_IDENTITY, ctx, await provider.whoami(ctx));
|
|
482
|
+
}
|
|
483
|
+
if (group === 'sync' && sub === 'plan') {
|
|
484
|
+
const remote = flags.remote || ctx.config.remote;
|
|
485
|
+
assertGitRemote(remote, '--remote');
|
|
486
|
+
return forgePacket(
|
|
487
|
+
PACKET_TYPES.SYNC_PLAN,
|
|
488
|
+
ctx,
|
|
489
|
+
await provider.syncPlan(ctx, remote),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
throw Object.assign(new Error(`Unknown command: ${positional.join(' ')}`), {
|
|
494
|
+
forgeError: forgeError(
|
|
495
|
+
ERROR_CODES.INVALID_ARGS,
|
|
496
|
+
'Unknown command. Try: provider capabilities, repo status, refs compare, refs inventory, cr inventory, cr files, cr comments, cr open, status set, forge changes, pr view, pr checks, merge plan, merge execute, sync plan, whoami, branch protection',
|
|
497
|
+
),
|
|
498
|
+
});
|
|
499
|
+
}
|