@remogram/cli 0.1.0-beta.8 → 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
@@ -11,6 +11,9 @@ import {
11
11
  forgeFactInventoryPacket,
12
12
  assertWriteCommandConfigured,
13
13
  parseSinceObservedAt,
14
+ decodeForgeChangesCursor,
15
+ paginateForgeChangesBody,
16
+ DEFAULT_FORGE_CHANGES_PAGE_SIZE,
14
17
  normalizeAllowedPaths,
15
18
  assertExpectedSha,
16
19
  buildMergeExecuteBeforeFacts,
@@ -21,6 +24,7 @@ import {
21
24
  buildMergeExecuteMergeFacts,
22
25
  mergeExecuteViewFacts,
23
26
  isOpenPrState,
27
+ bindIdempotencyScope,
24
28
  } from '@remogram/core';
25
29
  import { parseAllowedPathFlags, parsePositiveInt } from './cli-argv.js';
26
30
 
@@ -77,8 +81,9 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
77
81
  slice_ref: flags.slice_ref,
78
82
  limit: parsePositiveInt(flags.limit, '--limit'),
79
83
  sort: flags.sort,
84
+ cursor: flags.cursor,
80
85
  });
81
- if (inventoryBody.list_truncated === true) {
86
+ if (inventoryBody.list_truncated === true && !flags.cursor) {
82
87
  throw Object.assign(new Error('Open CR list incomplete'), {
83
88
  forgeError: forgeError(
84
89
  ERROR_CODES.INVENTORY_LIST_INCOMPLETE,
@@ -141,7 +146,6 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
141
146
  );
142
147
  }
143
148
  if (group === 'forge' && sub === 'changes') {
144
- const sinceIso = parseSinceObservedAt(flags.since);
145
149
  if (typeof provider.forgeChanges !== 'function') {
146
150
  throw Object.assign(new Error('forge changes not implemented for provider'), {
147
151
  forgeError: forgeError(
@@ -150,11 +154,19 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
150
154
  ),
151
155
  });
152
156
  }
153
- return forgePacket(
154
- PACKET_TYPES.FORGE_CHANGES,
155
- ctx,
156
- await provider.forgeChanges(ctx, { since: sinceIso }),
157
- );
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);
158
170
  }
159
171
  if (group === 'cr' && sub === 'open') {
160
172
  if (typeof provider.crOpen !== 'function') {
@@ -176,6 +188,9 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
176
188
  assertGitRef(flags.head, '--head');
177
189
  assertGitRef(flags.base, '--base');
178
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;
179
194
  return forgePacket(
180
195
  PACKET_TYPES.CHANGE_REQUEST_OPENED,
181
196
  ctx,
@@ -184,6 +199,35 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
184
199
  base: flags.base,
185
200
  title: flags.title,
186
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,
187
231
  }),
188
232
  );
189
233
  }
@@ -197,6 +241,13 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
197
241
  });
198
242
  }
199
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;
200
251
  return forgePacket(
201
252
  PACKET_TYPES.COMMIT_STATUS_SET,
202
253
  ctx,
@@ -206,6 +257,7 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
206
257
  state: flags.state,
207
258
  target_url: flags.target_url,
208
259
  description: flags.description,
260
+ idempotencyFingerprint,
209
261
  }),
210
262
  );
211
263
  }
@@ -312,7 +364,13 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
312
364
  let forgeHeadRefSha = null;
313
365
  const headRef = viewFacts.sourceBranchRef ? String(viewFacts.sourceBranchRef).trim() : '';
314
366
  if (!headRef && isOpenPrState(view.state)) {
315
- const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, null);
367
+ const before = buildMergeExecuteBeforeFacts(
368
+ view,
369
+ checks,
370
+ mergePlanBody,
371
+ null,
372
+ ctx.mergePolicy,
373
+ );
316
374
  return forgePacket(
317
375
  PACKET_TYPES.CR_MERGE_BLOCKED,
318
376
  ctx,
@@ -327,7 +385,13 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
327
385
  }
328
386
  if (headRef) {
329
387
  if (typeof provider.branchHeadSha !== 'function') {
330
- const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, null);
388
+ const before = buildMergeExecuteBeforeFacts(
389
+ view,
390
+ checks,
391
+ mergePlanBody,
392
+ null,
393
+ ctx.mergePolicy,
394
+ );
331
395
  return forgePacket(
332
396
  PACKET_TYPES.CR_MERGE_BLOCKED,
333
397
  ctx,
@@ -346,7 +410,13 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
346
410
  try {
347
411
  assertGitRef(headRef, 'head_ref');
348
412
  } catch (err) {
349
- const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, null);
413
+ const before = buildMergeExecuteBeforeFacts(
414
+ view,
415
+ checks,
416
+ mergePlanBody,
417
+ null,
418
+ ctx.mergePolicy,
419
+ );
350
420
  return forgePacket(
351
421
  PACKET_TYPES.CR_MERGE_BLOCKED,
352
422
  ctx,
@@ -368,7 +438,16 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
368
438
  repoId: view.forge_source_repo_id ?? null,
369
439
  });
370
440
  } catch (err) {
371
- const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, null);
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
+ );
372
451
  return forgePacket(
373
452
  PACKET_TYPES.CR_MERGE_BLOCKED,
374
453
  ctx,
@@ -387,13 +466,19 @@ export async function dispatchForgeCommand({ group, sub, flags, positional, ctx,
387
466
  }
388
467
  }
389
468
 
390
- const before = buildMergeExecuteBeforeFacts(view, checks, mergePlanBody, forgeHeadRefSha);
469
+ const before = buildMergeExecuteBeforeFacts(
470
+ view,
471
+ checks,
472
+ mergePlanBody,
473
+ forgeHeadRefSha,
474
+ ctx.mergePolicy,
475
+ );
391
476
  const blockers = collectMergeExecuteBlockers(
392
477
  view,
393
478
  checks,
394
479
  mergePlanBody,
395
480
  expected,
396
- { forgeHeadRefSha },
481
+ { forgeHeadRefSha, mergePolicy: ctx.mergePolicy },
397
482
  );
398
483
 
399
484
  if (blockers.length > 0) {
package/cli-doctor.js CHANGED
@@ -16,6 +16,12 @@ import {
16
16
  getEffectiveIngestMaxBytes,
17
17
  FORGE_INGEST_MAX_BYTES_ENV,
18
18
  MAX_FORGE_INGEST_ENV_BYTES,
19
+ resolveMergePolicy,
20
+ ALLOW_MISSING_CHECKS_ENV,
21
+ ALLOW_PENDING_CHECKS_ENV,
22
+ buildWriteReadiness,
23
+ writeReadinessHasWarnings,
24
+ buildApiReachabilityCheck,
19
25
  } from '@remogram/core';
20
26
  import { contextFromConfig } from './cli-io.js';
21
27
 
@@ -34,25 +40,23 @@ export function doctorSummary(checks) {
34
40
  return 'pass';
35
41
  }
36
42
 
37
- function finalizeDoctorPacket(ctx, checks, providerCapabilities) {
43
+ function finalizeDoctorPacket(ctx, checks, providerCapabilities, writeConfig = null) {
38
44
  const summary = doctorSummary(checks);
39
45
  const error =
40
46
  summary === 'fail'
41
47
  ? forgeError(ERROR_CODES.CONFIG_INVALID, 'Doctor checks failed')
42
48
  : null;
43
- return forgePacket(
44
- PACKET_TYPES.PROVIDER_DOCTOR,
45
- ctx,
46
- {
47
- summary,
48
- checks,
49
- provider_capabilities: providerCapabilities,
50
- },
51
- error,
52
- );
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);
53
56
  }
54
57
 
55
- export async function buildDoctorPacket(cwd, providers) {
58
+ export async function buildDoctorPacket(cwd, providers, options = {}) {
59
+ const { live = false } = options;
56
60
  const checks = [];
57
61
  const configPath = findConfigPath(cwd);
58
62
  let loaded = null;
@@ -139,9 +143,12 @@ export async function buildDoctorPacket(cwd, providers) {
139
143
  }
140
144
  }
141
145
 
146
+ let writeConfig = null;
147
+
142
148
  if (providerCapabilities) {
143
149
  const envNames = providerCapabilities.auth_envs || [];
144
150
  const presentEnv = envNames.find((name) => Boolean(process.env[name])) || null;
151
+ const authPresent = Boolean(presentEnv);
145
152
  checks.push(
146
153
  doctorCheck(
147
154
  'auth',
@@ -152,17 +159,22 @@ export async function buildDoctorPacket(cwd, providers) {
152
159
  );
153
160
 
154
161
  if (providerCapabilities.write_support) {
155
- const providerWrites = (providerCapabilities.write_commands || []).filter(Boolean);
156
- const configuredWrites = Array.isArray(config?.write_commands) ? config.write_commands : [];
157
- 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);
158
168
  checks.push(
159
169
  doctorCheck(
160
170
  'write_config',
161
- missing.length ? 'warn' : 'pass',
162
- missing.length
163
- ? `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.`
164
- : 'Consumer write_commands matches provider write surface',
165
- { 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,
166
178
  ),
167
179
  );
168
180
  }
@@ -209,7 +221,46 @@ export async function buildDoctorPacket(cwd, providers) {
209
221
  );
210
222
  }
211
223
 
212
- 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
+ );
213
264
 
214
- return finalizeDoctorPacket(ctx, checks, providerCapabilities);
265
+ return finalizeDoctorPacket(ctx, checks, providerCapabilities, writeConfig);
215
266
  }
package/cli-io.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  ERROR_CODES,
6
6
  normalizedForgeOrigin,
7
7
  trustedBaseUrl,
8
+ resolveMergePolicy,
8
9
  } from '@remogram/core';
9
10
 
10
11
  export function output(packet, asJson) {
@@ -47,5 +48,6 @@ export function contextFromConfig(config, cwd, parsed = null) {
47
48
  if (config.baseUrl && (!parsed || trustedBaseUrl(config, parsed.host))) {
48
49
  ctx.baseUrl = normalizedForgeOrigin(config);
49
50
  }
51
+ ctx.mergePolicy = resolveMergePolicy(config);
50
52
  return ctx;
51
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remogram/cli",
3
- "version": "0.1.0-beta.8",
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.8",
31
- "@remogram/provider-gitea-api": "0.1.0-beta.8",
32
- "@remogram/provider-github-api": "0.1.0-beta.8",
33
- "@remogram/provider-gitlab-api": "0.1.0-beta.8",
34
- "@remogram/provider-gitea-tea": "0.1.0-beta.8",
35
- "@remogram/provider-github-gh": "0.1.0-beta.8"
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
  }