@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 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
- return forgePacket(
144
- PACKET_TYPES.FORGE_CHANGES,
145
- ctx,
146
- await provider.forgeChanges(ctx, { since: sinceIso }),
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.head_ref,
215
- body.head_sha,
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
- { head_sha: view.head_sha },
233
- view.head_ref,
234
- view.head_sha,
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
- return forgePacket(
43
- PACKET_TYPES.PROVIDER_DOCTOR,
44
- ctx,
45
- {
46
- summary,
47
- checks,
48
- provider_capabilities: providerCapabilities,
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
- const providerWrites = (providerCapabilities.write_commands || []).filter(Boolean);
152
- const configuredWrites = Array.isArray(config?.write_commands) ? config.write_commands : [];
153
- const missing = providerWrites.filter((name) => !configuredWrites.includes(name));
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
- missing.length ? 'warn' : 'pass',
158
- missing.length
159
- ? `Provider supports write commands but .remogram.json write_commands omits: ${missing.join(', ')}. Add ids for Remogram CLI/MCP writes, or use forge/CI tooling for those actions outside Remogram.`
160
- : 'Consumer write_commands matches provider write surface',
161
- { provider_write_commands: providerWrites, configured_write_commands: configuredWrites },
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
- checks.push(doctorCheck('api_reachability', 'skipped', 'Live API reachability is not checked by default'));
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 { forgePacket, forgeErrorPacket, ERROR_CODES, forgeError } from '@remogram/core';
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 = err.forgeError || {
9
- code: ERROR_CODES.API_ERROR,
10
- message: err.message,
11
- status: err.status,
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
- return {
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.6",
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.6",
31
- "@remogram/provider-gitea-api": "0.1.0-beta.6",
32
- "@remogram/provider-github-api": "0.1.0-beta.6",
33
- "@remogram/provider-gitlab-api": "0.1.0-beta.6",
34
- "@remogram/provider-gitea-tea": "0.1.0-beta.6",
35
- "@remogram/provider-github-gh": "0.1.0-beta.6"
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
  }