@remogram/cli 0.1.0-beta.3 → 0.1.0-beta.5

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.
Files changed (2) hide show
  1. package/index.js +206 -6
  2. package/package.json +7 -7
package/index.js CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  throwIfStaleHeadByNumber,
22
22
  FACT_INVENTORY_PACKET_TYPES,
23
23
  forgeFactInventoryPacket,
24
+ assertWriteCommandConfigured,
25
+ parseSinceObservedAt,
24
26
  } from '@remogram/core';
25
27
  import { provider as giteaApi } from '@remogram/provider-gitea-api';
26
28
  import { provider as githubApi } from '@remogram/provider-github-api';
@@ -36,6 +38,13 @@ const PROVIDERS = {
36
38
  'github-gh': githubGh,
37
39
  };
38
40
 
41
+ const REPEATABLE_FLAGS = new Set(['allowed_path']);
42
+
43
+ function parseAllowedPathFlags(flags) {
44
+ if (flags.allowed_path == null) return undefined;
45
+ return Array.isArray(flags.allowed_path) ? flags.allowed_path : [flags.allowed_path];
46
+ }
47
+
39
48
  function parsePositiveInt(value, name) {
40
49
  if (value == null) return undefined;
41
50
  const n = Number(value);
@@ -212,6 +221,22 @@ async function buildDoctorPacket(cwd, providers) {
212
221
  ),
213
222
  );
214
223
 
224
+ if (providerCapabilities.write_support) {
225
+ const providerWrites = (providerCapabilities.write_commands || []).filter(Boolean);
226
+ const configuredWrites = Array.isArray(config?.write_commands) ? config.write_commands : [];
227
+ const missing = providerWrites.filter((name) => !configuredWrites.includes(name));
228
+ checks.push(
229
+ doctorCheck(
230
+ 'write_config',
231
+ missing.length ? 'warn' : 'pass',
232
+ missing.length
233
+ ? `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.`
234
+ : 'Consumer write_commands matches provider write surface',
235
+ { provider_write_commands: providerWrites, configured_write_commands: configuredWrites },
236
+ ),
237
+ );
238
+ }
239
+
215
240
  if (!providerCapabilities.check_sources?.length) {
216
241
  checks.push(doctorCheck('checks', 'warn', 'Provider does not report forge check sources'));
217
242
  } else {
@@ -270,7 +295,13 @@ export async function runCli(argv, options = {}) {
270
295
  else if (arg.startsWith('--')) {
271
296
  const key = arg.slice(2).replace(/-/g, '_');
272
297
  const next = argv[i + 1];
273
- if (next != null && !next.startsWith('--')) {
298
+ if (REPEATABLE_FLAGS.has(key)) {
299
+ if (!flags[key]) flags[key] = [];
300
+ if (next != null && !next.startsWith('--')) {
301
+ flags[key].push(next);
302
+ i += 1;
303
+ }
304
+ } else if (next != null && !next.startsWith('--')) {
274
305
  flags[key] = next;
275
306
  i += 1;
276
307
  } else {
@@ -361,12 +392,139 @@ export async function runCli(argv, options = {}) {
361
392
  ),
362
393
  });
363
394
  }
395
+ const inventoryBody = await provider.crInventory(ctx, {
396
+ slice_ref: flags.slice_ref,
397
+ limit: parsePositiveInt(flags.limit, '--limit'),
398
+ sort: flags.sort,
399
+ });
400
+ if (inventoryBody.list_truncated === true) {
401
+ throw Object.assign(new Error('Open CR list incomplete'), {
402
+ forgeError: forgeError(
403
+ ERROR_CODES.INVENTORY_LIST_INCOMPLETE,
404
+ 'Open change request list could not be proved complete within pagination bounds',
405
+ null,
406
+ {
407
+ inventory_list: {
408
+ entry_count: inventoryBody.entry_count,
409
+ },
410
+ },
411
+ ),
412
+ });
413
+ }
364
414
  packet = forgeFactInventoryPacket(
365
415
  FACT_INVENTORY_PACKET_TYPES.CR_INVENTORY_SLICE,
366
416
  ctx,
367
- await provider.crInventory(ctx, {
368
- slice_ref: flags.slice_ref,
369
- limit: parsePositiveInt(flags.limit, '--limit'),
417
+ inventoryBody,
418
+ );
419
+ } else if (group === 'cr' && sub === 'files') {
420
+ const number = parsePositiveInt(flags.number, '--number');
421
+ if (number == null) {
422
+ throw Object.assign(new Error('--number required'), {
423
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for cr files'),
424
+ });
425
+ }
426
+ if (typeof provider.crFiles !== 'function') {
427
+ throw Object.assign(new Error('cr files not implemented for provider'), {
428
+ forgeError: forgeError(
429
+ ERROR_CODES.PROVIDER_UNSUPPORTED,
430
+ 'cr files not implemented for provider',
431
+ ),
432
+ });
433
+ }
434
+ packet = forgePacket(
435
+ PACKET_TYPES.CR_FILES,
436
+ ctx,
437
+ await provider.crFiles(ctx, { number }),
438
+ );
439
+ } else if (group === 'cr' && sub === 'comments') {
440
+ const number = parsePositiveInt(flags.number, '--number');
441
+ if (number == null) {
442
+ throw Object.assign(new Error('--number required'), {
443
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for cr comments'),
444
+ });
445
+ }
446
+ if (typeof provider.crComments !== 'function') {
447
+ throw Object.assign(new Error('cr comments not implemented for provider'), {
448
+ forgeError: forgeError(
449
+ ERROR_CODES.PROVIDER_UNSUPPORTED,
450
+ 'cr comments not implemented for provider',
451
+ ),
452
+ });
453
+ }
454
+ packet = forgePacket(
455
+ PACKET_TYPES.CR_COMMENTS,
456
+ ctx,
457
+ await provider.crComments(ctx, { number }),
458
+ );
459
+ } else if (group === 'forge' && sub === 'changes') {
460
+ let sinceIso;
461
+ try {
462
+ sinceIso = parseSinceObservedAt(flags.since);
463
+ } catch (err) {
464
+ throw err;
465
+ }
466
+ if (typeof provider.forgeChanges !== 'function') {
467
+ throw Object.assign(new Error('forge changes not implemented for provider'), {
468
+ forgeError: forgeError(
469
+ ERROR_CODES.PROVIDER_UNSUPPORTED,
470
+ 'forge changes not implemented for provider',
471
+ ),
472
+ });
473
+ }
474
+ packet = forgePacket(
475
+ PACKET_TYPES.FORGE_CHANGES,
476
+ ctx,
477
+ await provider.forgeChanges(ctx, { since: sinceIso }),
478
+ );
479
+ } else if (group === 'cr' && sub === 'open') {
480
+ if (typeof provider.crOpen !== 'function') {
481
+ throw Object.assign(new Error('cr open not implemented for provider'), {
482
+ forgeError: forgeError(
483
+ ERROR_CODES.PROVIDER_UNSUPPORTED,
484
+ 'cr open not implemented for provider',
485
+ ),
486
+ });
487
+ }
488
+ if (!flags.head || !flags.base || !flags.title) {
489
+ throw Object.assign(new Error('--head, --base, and --title required'), {
490
+ forgeError: forgeError(
491
+ ERROR_CODES.INVALID_ARGS,
492
+ '--head, --base, and --title required for cr open',
493
+ ),
494
+ });
495
+ }
496
+ assertGitRef(flags.head, '--head');
497
+ assertGitRef(flags.base, '--base');
498
+ assertWriteCommandConfigured(ctx.config, 'cr_open');
499
+ packet = forgePacket(
500
+ PACKET_TYPES.CHANGE_REQUEST_OPENED,
501
+ ctx,
502
+ await provider.crOpen(ctx, {
503
+ head: flags.head,
504
+ base: flags.base,
505
+ title: flags.title,
506
+ body: flags.body,
507
+ }),
508
+ );
509
+ } else if (group === 'status' && sub === 'set') {
510
+ if (typeof provider.statusSet !== 'function') {
511
+ throw Object.assign(new Error('status set not implemented for provider'), {
512
+ forgeError: forgeError(
513
+ ERROR_CODES.PROVIDER_UNSUPPORTED,
514
+ 'status set not implemented for provider',
515
+ ),
516
+ });
517
+ }
518
+ assertWriteCommandConfigured(ctx.config, 'status_set');
519
+ packet = forgePacket(
520
+ PACKET_TYPES.COMMIT_STATUS_SET,
521
+ ctx,
522
+ await provider.statusSet(ctx, {
523
+ sha: flags.sha,
524
+ context: flags.context,
525
+ state: flags.state,
526
+ target_url: flags.target_url,
527
+ description: flags.description,
370
528
  }),
371
529
  );
372
530
  } else if (group === 'pr' && sub === 'view') {
@@ -415,7 +573,49 @@ export async function runCli(argv, options = {}) {
415
573
  forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for merge plan'),
416
574
  });
417
575
  }
418
- packet = forgePacket(PACKET_TYPES.MERGE_PLAN, ctx, await provider.mergePlan(ctx, { number }));
576
+ const allowedPaths = parseAllowedPathFlags(flags);
577
+ packet = forgePacket(
578
+ PACKET_TYPES.MERGE_PLAN,
579
+ ctx,
580
+ await provider.mergePlan(ctx, {
581
+ number,
582
+ ...(allowedPaths ? { allowed_paths: allowedPaths } : {}),
583
+ }),
584
+ );
585
+ } else if (group === 'branch' && sub === 'protection') {
586
+ const branchRef = flags.branch_ref;
587
+ if (!branchRef) {
588
+ throw Object.assign(new Error('--branch-ref required'), {
589
+ forgeError: forgeError(
590
+ ERROR_CODES.INVALID_ARGS,
591
+ '--branch-ref required for branch protection',
592
+ ),
593
+ });
594
+ }
595
+ assertGitRef(branchRef, '--branch-ref');
596
+ if (typeof provider.branchProtection !== 'function') {
597
+ throw Object.assign(new Error('branch protection not implemented for provider'), {
598
+ forgeError: forgeError(
599
+ ERROR_CODES.PROVIDER_UNSUPPORTED,
600
+ 'branch protection not implemented for provider',
601
+ ),
602
+ });
603
+ }
604
+ packet = forgePacket(
605
+ PACKET_TYPES.BRANCH_PROTECTION,
606
+ ctx,
607
+ await provider.branchProtection(ctx, { branchRef }),
608
+ );
609
+ } else if (group === 'whoami' && sub == null) {
610
+ if (typeof provider.whoami !== 'function') {
611
+ throw Object.assign(new Error('whoami not implemented for provider'), {
612
+ forgeError: forgeError(
613
+ ERROR_CODES.PROVIDER_UNSUPPORTED,
614
+ 'whoami not implemented for provider',
615
+ ),
616
+ });
617
+ }
618
+ packet = forgePacket(PACKET_TYPES.PROVIDER_IDENTITY, ctx, await provider.whoami(ctx));
419
619
  } else if (group === 'sync' && sub === 'plan') {
420
620
  const remote = flags.remote || ctx.config.remote;
421
621
  assertGitRemote(remote, '--remote');
@@ -428,7 +628,7 @@ export async function runCli(argv, options = {}) {
428
628
  throw Object.assign(new Error(`Unknown command: ${positional.join(' ')}`), {
429
629
  forgeError: forgeError(
430
630
  ERROR_CODES.INVALID_ARGS,
431
- 'Unknown command. Try: provider capabilities, repo status, refs compare, refs inventory, cr inventory, pr view, pr checks, merge plan, sync plan',
631
+ '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',
432
632
  ),
433
633
  });
434
634
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remogram/cli",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
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.3",
31
- "@remogram/provider-gitea-api": "0.1.0-beta.3",
32
- "@remogram/provider-github-api": "0.1.0-beta.3",
33
- "@remogram/provider-gitlab-api": "0.1.0-beta.3",
34
- "@remogram/provider-gitea-tea": "0.1.0-beta.3",
35
- "@remogram/provider-github-gh": "0.1.0-beta.3"
30
+ "@remogram/core": "0.1.0-beta.5",
31
+ "@remogram/provider-gitea-api": "0.1.0-beta.5",
32
+ "@remogram/provider-github-api": "0.1.0-beta.5",
33
+ "@remogram/provider-gitlab-api": "0.1.0-beta.5",
34
+ "@remogram/provider-gitea-tea": "0.1.0-beta.5",
35
+ "@remogram/provider-github-gh": "0.1.0-beta.5"
36
36
  }
37
37
  }