@remogram/cli 0.1.0-beta.6 → 0.1.0-beta.9
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-dispatch.js +285 -13
- package/cli-doctor.js +77 -22
- package/cli-io.js +24 -7
- package/index.js +2 -1
- package/package.json +7 -7
package/cli-dispatch.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
PACKET_TYPES,
|
|
4
4
|
ERROR_CODES,
|
|
5
5
|
forgeError,
|
|
6
|
+
sanitizeField,
|
|
6
7
|
assertGitRef,
|
|
7
8
|
assertGitRemote,
|
|
8
9
|
throwIfStaleHeadByNumber,
|
|
@@ -10,7 +11,20 @@ import {
|
|
|
10
11
|
forgeFactInventoryPacket,
|
|
11
12
|
assertWriteCommandConfigured,
|
|
12
13
|
parseSinceObservedAt,
|
|
14
|
+
decodeForgeChangesCursor,
|
|
15
|
+
paginateForgeChangesBody,
|
|
16
|
+
DEFAULT_FORGE_CHANGES_PAGE_SIZE,
|
|
13
17
|
normalizeAllowedPaths,
|
|
18
|
+
assertExpectedSha,
|
|
19
|
+
buildMergeExecuteBeforeFacts,
|
|
20
|
+
collectMergeExecuteBlockers,
|
|
21
|
+
buildCrMergeBlockedBody,
|
|
22
|
+
buildCrMergedBody,
|
|
23
|
+
buildMergeExecuteAfterFacts,
|
|
24
|
+
buildMergeExecuteMergeFacts,
|
|
25
|
+
mergeExecuteViewFacts,
|
|
26
|
+
isOpenPrState,
|
|
27
|
+
bindIdempotencyScope,
|
|
14
28
|
} from '@remogram/core';
|
|
15
29
|
import { parseAllowedPathFlags, parsePositiveInt } from './cli-argv.js';
|
|
16
30
|
|
|
@@ -67,8 +81,9 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
67
81
|
slice_ref: flags.slice_ref,
|
|
68
82
|
limit: parsePositiveInt(flags.limit, '--limit'),
|
|
69
83
|
sort: flags.sort,
|
|
84
|
+
cursor: flags.cursor,
|
|
70
85
|
});
|
|
71
|
-
if (inventoryBody.list_truncated === true) {
|
|
86
|
+
if (inventoryBody.list_truncated === true && !flags.cursor) {
|
|
72
87
|
throw Object.assign(new Error('Open CR list incomplete'), {
|
|
73
88
|
forgeError: forgeError(
|
|
74
89
|
ERROR_CODES.INVENTORY_LIST_INCOMPLETE,
|
|
@@ -131,7 +146,6 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
131
146
|
);
|
|
132
147
|
}
|
|
133
148
|
if (group === 'forge' && sub === 'changes') {
|
|
134
|
-
const sinceIso = parseSinceObservedAt(flags.since);
|
|
135
149
|
if (typeof provider.forgeChanges !== 'function') {
|
|
136
150
|
throw Object.assign(new Error('forge changes not implemented for provider'), {
|
|
137
151
|
forgeError: forgeError(
|
|
@@ -140,11 +154,19 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
140
154
|
),
|
|
141
155
|
});
|
|
142
156
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
let sinceIso;
|
|
158
|
+
let cursorOffset = 0;
|
|
159
|
+
const pageLimit = parsePositiveInt(flags.limit, '--limit') ?? DEFAULT_FORGE_CHANGES_PAGE_SIZE;
|
|
160
|
+
if (flags.cursor) {
|
|
161
|
+
const decoded = decodeForgeChangesCursor(flags.cursor, { since: flags.since });
|
|
162
|
+
sinceIso = decoded.since;
|
|
163
|
+
cursorOffset = decoded.offset;
|
|
164
|
+
} else {
|
|
165
|
+
sinceIso = parseSinceObservedAt(flags.since);
|
|
166
|
+
}
|
|
167
|
+
const body = await provider.forgeChanges(ctx, { since: sinceIso });
|
|
168
|
+
const paginated = paginateForgeChangesBody(body, { offset: cursorOffset, limit: pageLimit });
|
|
169
|
+
return forgePacket(PACKET_TYPES.FORGE_CHANGES, ctx, paginated);
|
|
148
170
|
}
|
|
149
171
|
if (group === 'cr' && sub === 'open') {
|
|
150
172
|
if (typeof provider.crOpen !== 'function') {
|
|
@@ -166,6 +188,9 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
166
188
|
assertGitRef(flags.head, '--head');
|
|
167
189
|
assertGitRef(flags.base, '--base');
|
|
168
190
|
assertWriteCommandConfigured(ctx.config, 'cr_open');
|
|
191
|
+
const idempotencyFingerprint = flags.idempotency_key
|
|
192
|
+
? bindIdempotencyScope(ctx.repo_id, flags.idempotency_key, [flags.head, flags.base])
|
|
193
|
+
: null;
|
|
169
194
|
return forgePacket(
|
|
170
195
|
PACKET_TYPES.CHANGE_REQUEST_OPENED,
|
|
171
196
|
ctx,
|
|
@@ -174,6 +199,35 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
174
199
|
base: flags.base,
|
|
175
200
|
title: flags.title,
|
|
176
201
|
body: flags.body,
|
|
202
|
+
idempotencyFingerprint,
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (group === 'issue' && sub === 'open') {
|
|
207
|
+
if (typeof provider.issueOpen !== 'function') {
|
|
208
|
+
throw Object.assign(new Error('issue open not implemented for provider'), {
|
|
209
|
+
forgeError: forgeError(
|
|
210
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
211
|
+
'issue open not implemented for provider',
|
|
212
|
+
),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (!flags.title) {
|
|
216
|
+
throw Object.assign(new Error('--title required'), {
|
|
217
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--title required for issue open'),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
assertWriteCommandConfigured(ctx.config, 'issue_open');
|
|
221
|
+
const idempotencyFingerprint = flags.idempotency_key
|
|
222
|
+
? bindIdempotencyScope(ctx.repo_id, flags.idempotency_key, [flags.title])
|
|
223
|
+
: null;
|
|
224
|
+
return forgePacket(
|
|
225
|
+
PACKET_TYPES.ISSUE_OPENED,
|
|
226
|
+
ctx,
|
|
227
|
+
await provider.issueOpen(ctx, {
|
|
228
|
+
title: flags.title,
|
|
229
|
+
body: flags.body,
|
|
230
|
+
idempotencyFingerprint,
|
|
177
231
|
}),
|
|
178
232
|
);
|
|
179
233
|
}
|
|
@@ -187,6 +241,13 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
187
241
|
});
|
|
188
242
|
}
|
|
189
243
|
assertWriteCommandConfigured(ctx.config, 'status_set');
|
|
244
|
+
const idempotencyFingerprint = flags.idempotency_key
|
|
245
|
+
? bindIdempotencyScope(ctx.repo_id, flags.idempotency_key, [
|
|
246
|
+
flags.sha,
|
|
247
|
+
flags.context,
|
|
248
|
+
flags.state,
|
|
249
|
+
])
|
|
250
|
+
: null;
|
|
190
251
|
return forgePacket(
|
|
191
252
|
PACKET_TYPES.COMMIT_STATUS_SET,
|
|
192
253
|
ctx,
|
|
@@ -196,6 +257,7 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
196
257
|
state: flags.state,
|
|
197
258
|
target_url: flags.target_url,
|
|
198
259
|
description: flags.description,
|
|
260
|
+
idempotencyFingerprint,
|
|
199
261
|
}),
|
|
200
262
|
);
|
|
201
263
|
}
|
|
@@ -211,8 +273,8 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
211
273
|
ctx,
|
|
212
274
|
PACKET_TYPES.PR_STATUS,
|
|
213
275
|
body,
|
|
214
|
-
body.
|
|
215
|
-
body.
|
|
276
|
+
body.forge_source_branch_ref,
|
|
277
|
+
body.forge_source_sha,
|
|
216
278
|
);
|
|
217
279
|
return forgePacket(PACKET_TYPES.PR_STATUS, ctx, body);
|
|
218
280
|
}
|
|
@@ -229,9 +291,9 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
229
291
|
throwIfStaleHeadByNumber(
|
|
230
292
|
ctx,
|
|
231
293
|
PACKET_TYPES.PR_CHECKS,
|
|
232
|
-
{
|
|
233
|
-
view.
|
|
234
|
-
view.
|
|
294
|
+
{ forge_source_sha: view.forge_source_sha },
|
|
295
|
+
view.forge_source_branch_ref,
|
|
296
|
+
view.forge_source_sha,
|
|
235
297
|
);
|
|
236
298
|
}
|
|
237
299
|
return forgePacket(
|
|
@@ -257,6 +319,216 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
257
319
|
}),
|
|
258
320
|
);
|
|
259
321
|
}
|
|
322
|
+
if (group === 'merge' && sub === 'execute') {
|
|
323
|
+
if (typeof provider.mergeExecute !== 'function') {
|
|
324
|
+
throw Object.assign(new Error('merge execute not implemented for provider'), {
|
|
325
|
+
forgeError: forgeError(
|
|
326
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
327
|
+
'merge execute not implemented for provider',
|
|
328
|
+
),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
332
|
+
if (number == null) {
|
|
333
|
+
throw Object.assign(new Error('--number required'), {
|
|
334
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for merge execute'),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (!flags.expected_base_sha || !flags.expected_head_sha) {
|
|
338
|
+
throw Object.assign(new Error('expected SHAs required'), {
|
|
339
|
+
forgeError: forgeError(
|
|
340
|
+
ERROR_CODES.INVALID_ARGS,
|
|
341
|
+
'--expected-base-sha and --expected-head-sha required for merge execute',
|
|
342
|
+
),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const method = flags.method ? String(flags.method).toLowerCase() : 'merge';
|
|
346
|
+
if (method !== 'merge') {
|
|
347
|
+
throw Object.assign(new Error('Unsupported merge method'), {
|
|
348
|
+
forgeError: forgeError(
|
|
349
|
+
ERROR_CODES.INVALID_ARGS,
|
|
350
|
+
'Only --method merge is supported in v1',
|
|
351
|
+
),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const expectedBaseSha = assertExpectedSha(flags.expected_base_sha, '--expected-base-sha');
|
|
355
|
+
const expectedHeadSha = assertExpectedSha(flags.expected_head_sha, '--expected-head-sha');
|
|
356
|
+
assertWriteCommandConfigured(ctx.config, 'merge');
|
|
357
|
+
|
|
358
|
+
const view = await provider.prView(ctx, { number });
|
|
359
|
+
const checks = await provider.prChecks(ctx, { number });
|
|
360
|
+
const mergePlanBody = await provider.mergePlan(ctx, { number });
|
|
361
|
+
const expected = { baseSha: expectedBaseSha, headSha: expectedHeadSha };
|
|
362
|
+
const viewFacts = mergeExecuteViewFacts(view);
|
|
363
|
+
|
|
364
|
+
let forgeHeadRefSha = null;
|
|
365
|
+
const headRef = viewFacts.sourceBranchRef ? String(viewFacts.sourceBranchRef).trim() : '';
|
|
366
|
+
if (!headRef && isOpenPrState(view.state)) {
|
|
367
|
+
const before = buildMergeExecuteBeforeFacts(
|
|
368
|
+
view,
|
|
369
|
+
checks,
|
|
370
|
+
mergePlanBody,
|
|
371
|
+
null,
|
|
372
|
+
ctx.mergePolicy,
|
|
373
|
+
);
|
|
374
|
+
return forgePacket(
|
|
375
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
376
|
+
ctx,
|
|
377
|
+
buildCrMergeBlockedBody({
|
|
378
|
+
prNumber: number,
|
|
379
|
+
expected,
|
|
380
|
+
before,
|
|
381
|
+
blockers: ['head_ref_missing'],
|
|
382
|
+
}),
|
|
383
|
+
forgeError(ERROR_CODES.MERGE_BLOCKED, 'Open change request missing head branch ref'),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (headRef) {
|
|
387
|
+
if (typeof provider.branchHeadSha !== 'function') {
|
|
388
|
+
const before = buildMergeExecuteBeforeFacts(
|
|
389
|
+
view,
|
|
390
|
+
checks,
|
|
391
|
+
mergePlanBody,
|
|
392
|
+
null,
|
|
393
|
+
ctx.mergePolicy,
|
|
394
|
+
);
|
|
395
|
+
return forgePacket(
|
|
396
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
397
|
+
ctx,
|
|
398
|
+
buildCrMergeBlockedBody({
|
|
399
|
+
prNumber: number,
|
|
400
|
+
expected,
|
|
401
|
+
before,
|
|
402
|
+
blockers: ['head_ref_unverified'],
|
|
403
|
+
}),
|
|
404
|
+
forgeError(
|
|
405
|
+
ERROR_CODES.MERGE_BLOCKED,
|
|
406
|
+
'Forge head branch verification not implemented for provider',
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
assertGitRef(headRef, 'head_ref');
|
|
412
|
+
} catch (err) {
|
|
413
|
+
const before = buildMergeExecuteBeforeFacts(
|
|
414
|
+
view,
|
|
415
|
+
checks,
|
|
416
|
+
mergePlanBody,
|
|
417
|
+
null,
|
|
418
|
+
ctx.mergePolicy,
|
|
419
|
+
);
|
|
420
|
+
return forgePacket(
|
|
421
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
422
|
+
ctx,
|
|
423
|
+
buildCrMergeBlockedBody({
|
|
424
|
+
prNumber: number,
|
|
425
|
+
expected,
|
|
426
|
+
before,
|
|
427
|
+
blockers: ['head_ref_invalid'],
|
|
428
|
+
}),
|
|
429
|
+
forgeError(
|
|
430
|
+
ERROR_CODES.INVALID_ARGS,
|
|
431
|
+
sanitizeField(err.forgeError?.message || err.message || err.invalidArgs)
|
|
432
|
+
|| 'Head branch ref invalid',
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
forgeHeadRefSha = await provider.branchHeadSha(ctx, headRef, {
|
|
438
|
+
repoId: view.forge_source_repo_id ?? null,
|
|
439
|
+
});
|
|
440
|
+
} catch (err) {
|
|
441
|
+
if (err.forgeError?.code === ERROR_CODES.INVALID_ARGS) {
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
const before = buildMergeExecuteBeforeFacts(
|
|
445
|
+
view,
|
|
446
|
+
checks,
|
|
447
|
+
mergePlanBody,
|
|
448
|
+
null,
|
|
449
|
+
ctx.mergePolicy,
|
|
450
|
+
);
|
|
451
|
+
return forgePacket(
|
|
452
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
453
|
+
ctx,
|
|
454
|
+
buildCrMergeBlockedBody({
|
|
455
|
+
prNumber: number,
|
|
456
|
+
expected,
|
|
457
|
+
before,
|
|
458
|
+
blockers: ['head_ref_unreadable'],
|
|
459
|
+
}),
|
|
460
|
+
forgeError(
|
|
461
|
+
ERROR_CODES.MERGE_BLOCKED,
|
|
462
|
+
sanitizeField(err.forgeError?.message || err.message) || 'Head branch ref unreadable',
|
|
463
|
+
err.status ?? err.forgeError?.status ?? null,
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const before = buildMergeExecuteBeforeFacts(
|
|
470
|
+
view,
|
|
471
|
+
checks,
|
|
472
|
+
mergePlanBody,
|
|
473
|
+
forgeHeadRefSha,
|
|
474
|
+
ctx.mergePolicy,
|
|
475
|
+
);
|
|
476
|
+
const blockers = collectMergeExecuteBlockers(
|
|
477
|
+
view,
|
|
478
|
+
checks,
|
|
479
|
+
mergePlanBody,
|
|
480
|
+
expected,
|
|
481
|
+
{ forgeHeadRefSha, mergePolicy: ctx.mergePolicy },
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
if (blockers.length > 0) {
|
|
485
|
+
return forgePacket(
|
|
486
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
487
|
+
ctx,
|
|
488
|
+
buildCrMergeBlockedBody({ prNumber: number, expected, before, blockers }),
|
|
489
|
+
forgeError(ERROR_CODES.MERGE_BLOCKED, 'Merge blocked by preflight'),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const providerResult = await provider.mergeExecute(ctx, {
|
|
495
|
+
number,
|
|
496
|
+
method,
|
|
497
|
+
expectedHeadSha: expected.headSha,
|
|
498
|
+
});
|
|
499
|
+
const merge = buildMergeExecuteMergeFacts(method, providerResult);
|
|
500
|
+
const after = buildMergeExecuteAfterFacts(view, providerResult);
|
|
501
|
+
return forgePacket(
|
|
502
|
+
PACKET_TYPES.CR_MERGED,
|
|
503
|
+
ctx,
|
|
504
|
+
buildCrMergedBody({ prNumber: number, expected, before, merge, after }),
|
|
505
|
+
);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
const status = err.status ?? err.forgeError?.status ?? null;
|
|
508
|
+
const blockers =
|
|
509
|
+
Array.isArray(err.mergeBlockedBlockers) && err.mergeBlockedBlockers.length > 0
|
|
510
|
+
? err.mergeBlockedBlockers
|
|
511
|
+
: ['merge_endpoint_failed'];
|
|
512
|
+
const fe =
|
|
513
|
+
err.forgeError
|
|
514
|
+
?? forgeError(
|
|
515
|
+
ERROR_CODES.MERGE_ENDPOINT_FAILED,
|
|
516
|
+
sanitizeField(err.message) || 'Forge merge request failed',
|
|
517
|
+
status,
|
|
518
|
+
);
|
|
519
|
+
return forgePacket(
|
|
520
|
+
PACKET_TYPES.CR_MERGE_BLOCKED,
|
|
521
|
+
ctx,
|
|
522
|
+
buildCrMergeBlockedBody({
|
|
523
|
+
prNumber: number,
|
|
524
|
+
expected,
|
|
525
|
+
before,
|
|
526
|
+
blockers,
|
|
527
|
+
}),
|
|
528
|
+
fe,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
260
532
|
if (group === 'branch' && sub === 'protection') {
|
|
261
533
|
const branchRef = flags.branch_ref;
|
|
262
534
|
if (!branchRef) {
|
|
@@ -306,7 +578,7 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
|
|
|
306
578
|
throw Object.assign(new Error(`Unknown command: ${positional.join(' ')}`), {
|
|
307
579
|
forgeError: forgeError(
|
|
308
580
|
ERROR_CODES.INVALID_ARGS,
|
|
309
|
-
'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, sync plan, whoami, branch protection',
|
|
581
|
+
'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',
|
|
310
582
|
),
|
|
311
583
|
});
|
|
312
584
|
}
|
package/cli-doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
gitRemoteUrl,
|
|
5
5
|
parseRemoteUrl,
|
|
6
6
|
trustedBaseUrl,
|
|
7
|
+
normalizedForgeOrigin,
|
|
7
8
|
assertConfigMatchesRemote,
|
|
8
9
|
forgePacket,
|
|
9
10
|
unknownForgeContext,
|
|
@@ -15,6 +16,12 @@ import {
|
|
|
15
16
|
getEffectiveIngestMaxBytes,
|
|
16
17
|
FORGE_INGEST_MAX_BYTES_ENV,
|
|
17
18
|
MAX_FORGE_INGEST_ENV_BYTES,
|
|
19
|
+
resolveMergePolicy,
|
|
20
|
+
ALLOW_MISSING_CHECKS_ENV,
|
|
21
|
+
ALLOW_PENDING_CHECKS_ENV,
|
|
22
|
+
buildWriteReadiness,
|
|
23
|
+
writeReadinessHasWarnings,
|
|
24
|
+
buildApiReachabilityCheck,
|
|
18
25
|
} from '@remogram/core';
|
|
19
26
|
import { contextFromConfig } from './cli-io.js';
|
|
20
27
|
|
|
@@ -33,25 +40,23 @@ export function doctorSummary(checks) {
|
|
|
33
40
|
return 'pass';
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
function finalizeDoctorPacket(ctx, checks, providerCapabilities) {
|
|
43
|
+
function finalizeDoctorPacket(ctx, checks, providerCapabilities, writeConfig = null) {
|
|
37
44
|
const summary = doctorSummary(checks);
|
|
38
45
|
const error =
|
|
39
46
|
summary === 'fail'
|
|
40
47
|
? forgeError(ERROR_CODES.CONFIG_INVALID, 'Doctor checks failed')
|
|
41
48
|
: null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
50
|
-
error,
|
|
51
|
-
);
|
|
49
|
+
const body = {
|
|
50
|
+
summary,
|
|
51
|
+
checks,
|
|
52
|
+
provider_capabilities: providerCapabilities,
|
|
53
|
+
};
|
|
54
|
+
if (writeConfig) body.write_config = writeConfig;
|
|
55
|
+
return forgePacket(PACKET_TYPES.PROVIDER_DOCTOR, ctx, body, error);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
export async function buildDoctorPacket(cwd, providers) {
|
|
58
|
+
export async function buildDoctorPacket(cwd, providers, options = {}) {
|
|
59
|
+
const { live = false } = options;
|
|
55
60
|
const checks = [];
|
|
56
61
|
const configPath = findConfigPath(cwd);
|
|
57
62
|
let loaded = null;
|
|
@@ -132,12 +137,18 @@ export async function buildDoctorPacket(cwd, providers) {
|
|
|
132
137
|
);
|
|
133
138
|
} else {
|
|
134
139
|
checks.push(doctorCheck('host_binding', 'pass', 'Configured host binding is trusted'));
|
|
140
|
+
if (config.baseUrl) {
|
|
141
|
+
ctx = { ...ctx, baseUrl: normalizedForgeOrigin(config) };
|
|
142
|
+
}
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
146
|
+
let writeConfig = null;
|
|
147
|
+
|
|
138
148
|
if (providerCapabilities) {
|
|
139
149
|
const envNames = providerCapabilities.auth_envs || [];
|
|
140
150
|
const presentEnv = envNames.find((name) => Boolean(process.env[name])) || null;
|
|
151
|
+
const authPresent = Boolean(presentEnv);
|
|
141
152
|
checks.push(
|
|
142
153
|
doctorCheck(
|
|
143
154
|
'auth',
|
|
@@ -148,17 +159,22 @@ export async function buildDoctorPacket(cwd, providers) {
|
|
|
148
159
|
);
|
|
149
160
|
|
|
150
161
|
if (providerCapabilities.write_support) {
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const
|
|
162
|
+
writeConfig = buildWriteReadiness(config, providerCapabilities, { authPresent });
|
|
163
|
+
const warn = writeReadinessHasWarnings(writeConfig);
|
|
164
|
+
const notReady = writeConfig.commands.filter(
|
|
165
|
+
(entry) => entry.provider_supported && !entry.ready,
|
|
166
|
+
);
|
|
167
|
+
const missingConfig = notReady.filter((entry) => !entry.configured).map((entry) => entry.id);
|
|
154
168
|
checks.push(
|
|
155
169
|
doctorCheck(
|
|
156
170
|
'write_config',
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
?
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
warn ? 'warn' : 'pass',
|
|
172
|
+
warn
|
|
173
|
+
? missingConfig.length
|
|
174
|
+
? `Provider supports write commands but .remogram.json write_commands omits: ${missingConfig.join(', ')}. Add ids for Remogram CLI/MCP writes, or use forge/CI tooling for those actions outside Remogram.`
|
|
175
|
+
: 'One or more configured write commands are not ready (check auth or provider support)'
|
|
176
|
+
: 'All provider write commands are configured and ready',
|
|
177
|
+
writeConfig,
|
|
162
178
|
),
|
|
163
179
|
);
|
|
164
180
|
}
|
|
@@ -205,7 +221,46 @@ export async function buildDoctorPacket(cwd, providers) {
|
|
|
205
221
|
);
|
|
206
222
|
}
|
|
207
223
|
|
|
208
|
-
|
|
224
|
+
const mergePolicy = resolveMergePolicy(config);
|
|
225
|
+
if (mergePolicy.allow_missing_checks || mergePolicy.allow_pending_checks) {
|
|
226
|
+
checks.push(
|
|
227
|
+
doctorCheck(
|
|
228
|
+
'merge_policy',
|
|
229
|
+
'warn',
|
|
230
|
+
'Merge policy relaxes check blockers for merge plan and merge execute',
|
|
231
|
+
{
|
|
232
|
+
allow_missing_checks: mergePolicy.allow_missing_checks,
|
|
233
|
+
allow_pending_checks: mergePolicy.allow_pending_checks,
|
|
234
|
+
source: mergePolicy.source,
|
|
235
|
+
env_names: [ALLOW_MISSING_CHECKS_ENV, ALLOW_PENDING_CHECKS_ENV],
|
|
236
|
+
},
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
checks.push(
|
|
241
|
+
doctorCheck(
|
|
242
|
+
'merge_policy',
|
|
243
|
+
'pass',
|
|
244
|
+
'Default merge policy — missing and pending checks block merge',
|
|
245
|
+
{
|
|
246
|
+
allow_missing_checks: false,
|
|
247
|
+
allow_pending_checks: false,
|
|
248
|
+
source: mergePolicy.source,
|
|
249
|
+
},
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const hostBindingPass = checks.some(
|
|
255
|
+
(check) => check.name === 'host_binding' && check.status === 'pass',
|
|
256
|
+
);
|
|
257
|
+
const configPass = checks.some((check) => check.name === 'config' && check.status === 'pass');
|
|
258
|
+
checks.push(
|
|
259
|
+
await buildApiReachabilityCheck(ctx, provider, {
|
|
260
|
+
live,
|
|
261
|
+
prerequisitesPass: live && configPass && hostBindingPass && parsed != null,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
209
264
|
|
|
210
|
-
return finalizeDoctorPacket(ctx, checks, providerCapabilities);
|
|
265
|
+
return finalizeDoctorPacket(ctx, checks, providerCapabilities, writeConfig);
|
|
211
266
|
}
|
package/cli-io.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
forgePacket,
|
|
3
|
+
forgeErrorPacket,
|
|
4
|
+
forgeError,
|
|
5
|
+
ERROR_CODES,
|
|
6
|
+
normalizedForgeOrigin,
|
|
7
|
+
trustedBaseUrl,
|
|
8
|
+
resolveMergePolicy,
|
|
9
|
+
} from '@remogram/core';
|
|
2
10
|
|
|
3
11
|
export function output(packet, asJson) {
|
|
4
12
|
console.log(JSON.stringify(packet, null, asJson ? 2 : 0));
|
|
5
13
|
}
|
|
6
14
|
|
|
7
15
|
export function handleError(err, ctx, asJson) {
|
|
8
|
-
const fe =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
const fe =
|
|
17
|
+
err.forgeError
|
|
18
|
+
|| (err.invalidArgs
|
|
19
|
+
? forgeError(ERROR_CODES.INVALID_ARGS, err.invalidArgs)
|
|
20
|
+
: {
|
|
21
|
+
code: ERROR_CODES.API_ERROR,
|
|
22
|
+
message: err.message,
|
|
23
|
+
status: err.status,
|
|
24
|
+
});
|
|
13
25
|
const baseCtx = ctx || {
|
|
14
26
|
providerId: 'unknown',
|
|
15
27
|
remoteName: 'origin',
|
|
@@ -25,7 +37,7 @@ export function handleError(err, ctx, asJson) {
|
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
export function contextFromConfig(config, cwd, parsed = null) {
|
|
28
|
-
|
|
40
|
+
const ctx = {
|
|
29
41
|
providerId: config.provider,
|
|
30
42
|
remoteName: config.remote,
|
|
31
43
|
repoId: parsed ? `${parsed.owner}/${parsed.repo}` : `${config.owner}/${config.repo}`,
|
|
@@ -33,4 +45,9 @@ export function contextFromConfig(config, cwd, parsed = null) {
|
|
|
33
45
|
cwd,
|
|
34
46
|
parsed,
|
|
35
47
|
};
|
|
48
|
+
if (config.baseUrl && (!parsed || trustedBaseUrl(config, parsed.host))) {
|
|
49
|
+
ctx.baseUrl = normalizedForgeOrigin(config);
|
|
50
|
+
}
|
|
51
|
+
ctx.mergePolicy = resolveMergePolicy(config);
|
|
52
|
+
return ctx;
|
|
36
53
|
}
|
package/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function runCli(argv, options = {}) {
|
|
|
30
30
|
const [group, sub] = positional;
|
|
31
31
|
|
|
32
32
|
if (group === 'doctor' && sub == null) {
|
|
33
|
-
const packet = await buildDoctorPacket(cwd, providers);
|
|
33
|
+
const packet = await buildDoctorPacket(cwd, providers, { live: flags.live === true });
|
|
34
34
|
output(packet, asJson);
|
|
35
35
|
if (!packet.ok) process.exitCode = 1;
|
|
36
36
|
return;
|
|
@@ -64,6 +64,7 @@ export async function runCli(argv, options = {}) {
|
|
|
64
64
|
try {
|
|
65
65
|
const packet = await dispatchForgeCommand({ group, sub, flags, positional, ctx, provider });
|
|
66
66
|
output(packet, asJson);
|
|
67
|
+
if (!packet.ok) process.exitCode = 1;
|
|
67
68
|
} catch (err) {
|
|
68
69
|
handleError(err, ctx, asJson);
|
|
69
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remogram/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.9",
|
|
4
4
|
"description": "Remogram forge boundary CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"node": ">=20"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@remogram/core": "0.1.0-beta.
|
|
31
|
-
"@remogram/provider-gitea-api": "0.1.0-beta.
|
|
32
|
-
"@remogram/provider-github-api": "0.1.0-beta.
|
|
33
|
-
"@remogram/provider-gitlab-api": "0.1.0-beta.
|
|
34
|
-
"@remogram/provider-gitea-tea": "0.1.0-beta.
|
|
35
|
-
"@remogram/provider-github-gh": "0.1.0-beta.
|
|
30
|
+
"@remogram/core": "0.1.0-beta.9",
|
|
31
|
+
"@remogram/provider-gitea-api": "0.1.0-beta.9",
|
|
32
|
+
"@remogram/provider-github-api": "0.1.0-beta.9",
|
|
33
|
+
"@remogram/provider-gitlab-api": "0.1.0-beta.9",
|
|
34
|
+
"@remogram/provider-gitea-tea": "0.1.0-beta.9",
|
|
35
|
+
"@remogram/provider-github-gh": "0.1.0-beta.9"
|
|
36
36
|
}
|
|
37
37
|
}
|