@remogram/cli 0.1.0-beta.1 → 0.1.0-beta.10

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/index.js CHANGED
@@ -1,29 +1,19 @@
1
1
  import {
2
2
  loadConfig,
3
- findConfigPath,
4
3
  assertForgeReady,
5
- gitRemoteUrl,
6
- parseRemoteUrl,
7
- trustedBaseUrl,
8
- assertConfigMatchesRemote,
9
4
  forgeContext,
10
- forgePacket,
11
- forgeErrorPacket,
12
- unknownForgeContext,
13
- PACKET_TYPES,
14
5
  ERROR_CODES,
15
6
  forgeError,
16
- sanitizeField,
17
- assertGitRef,
18
- assertGitRemote,
19
- getEffectiveIngestMaxBytes,
20
- FORGE_INGEST_MAX_BYTES_ENV,
21
7
  } from '@remogram/core';
22
8
  import { provider as giteaApi } from '@remogram/provider-gitea-api';
23
9
  import { provider as githubApi } from '@remogram/provider-github-api';
24
10
  import { provider as gitlabApi } from '@remogram/provider-gitlab-api';
25
11
  import { provider as giteaTea } from '@remogram/provider-gitea-tea';
26
12
  import { provider as githubGh } from '@remogram/provider-github-gh';
13
+ import { output, handleError } from './cli-io.js';
14
+ import { parseCliArgv } from './cli-argv.js';
15
+ import { buildDoctorPacket } from './cli-doctor.js';
16
+ import { dispatchForgeCommand } from './cli-dispatch.js';
27
17
 
28
18
  const PROVIDERS = {
29
19
  'gitea-api': giteaApi,
@@ -33,250 +23,19 @@ const PROVIDERS = {
33
23
  'github-gh': githubGh,
34
24
  };
35
25
 
36
- function parsePositiveInt(value, name) {
37
- if (value == null) return undefined;
38
- const n = Number(value);
39
- if (!Number.isInteger(n) || n <= 0) {
40
- throw Object.assign(new Error(`Invalid ${name}`), {
41
- forgeError: forgeError(ERROR_CODES.INVALID_ARGS, `${name} must be a positive integer`),
42
- });
43
- }
44
- return n;
45
- }
46
-
47
- function output(packet, asJson) {
48
- console.log(JSON.stringify(packet, null, asJson ? 2 : 0));
49
- }
50
-
51
- function handleError(err, ctx, asJson) {
52
- const fe = err.forgeError || {
53
- code: ERROR_CODES.API_ERROR,
54
- message: err.message,
55
- status: err.status,
56
- };
57
- const baseCtx = ctx || {
58
- providerId: 'unknown',
59
- remoteName: 'origin',
60
- repoId: 'unknown/unknown',
61
- };
62
- output(forgeErrorPacket(baseCtx, fe), asJson);
63
- process.exitCode = 1;
64
- }
65
-
66
- function doctorCheck(name, status, message, details = null) {
67
- return {
68
- name,
69
- status,
70
- message: sanitizeField(message),
71
- ...(details ? { details } : {}),
72
- };
73
- }
74
-
75
- function doctorSummary(checks) {
76
- if (checks.some((check) => check.status === 'fail')) return 'fail';
77
- if (checks.some((check) => check.status === 'warn')) return 'warn';
78
- return 'pass';
79
- }
80
-
81
- function contextFromConfig(config, cwd, parsed = null) {
82
- return {
83
- providerId: config.provider,
84
- remoteName: config.remote,
85
- repoId: parsed ? `${parsed.owner}/${parsed.repo}` : `${config.owner}/${config.repo}`,
86
- config,
87
- cwd,
88
- parsed,
89
- };
90
- }
91
-
92
- function finalizeDoctorPacket(ctx, checks, providerCapabilities) {
93
- const summary = doctorSummary(checks);
94
- const error =
95
- summary === 'fail'
96
- ? forgeError(ERROR_CODES.CONFIG_INVALID, 'Doctor checks failed')
97
- : null;
98
- return forgePacket(
99
- PACKET_TYPES.PROVIDER_DOCTOR,
100
- ctx,
101
- {
102
- summary,
103
- checks,
104
- provider_capabilities: providerCapabilities,
105
- },
106
- error,
107
- );
108
- }
109
-
110
- async function buildDoctorPacket(cwd, providers) {
111
- const checks = [];
112
- const configPath = findConfigPath(cwd);
113
- let loaded = null;
114
- let config = null;
115
- let parsed = null;
116
- let ctx = unknownForgeContext();
117
- let providerCapabilities = null;
118
-
119
- if (!configPath) {
120
- checks.push(doctorCheck('config', 'fail', 'No .remogram.json found'));
121
- return finalizeDoctorPacket(ctx, checks, null);
122
- }
123
-
124
- try {
125
- loaded = loadConfig(cwd);
126
- config = loaded.config;
127
- ctx = contextFromConfig(config, loaded.cwd);
128
- checks.push(doctorCheck('config', 'pass', '.remogram.json is present and valid'));
129
- } catch (err) {
130
- checks.push(doctorCheck('config', 'fail', err.forgeError?.message || err.message));
131
- return finalizeDoctorPacket(ctx, checks, null);
132
- }
133
-
134
- const provider = providers[config.provider];
135
- if (!provider) {
136
- checks.push(doctorCheck('provider', 'fail', `Unsupported provider: ${config.provider}`));
137
- } else {
138
- if (typeof provider.providerCapabilities === 'function') {
139
- providerCapabilities = await provider.providerCapabilities(ctx);
140
- const stubProvider =
141
- providerCapabilities.commands?.length > 0
142
- && providerCapabilities.commands.every((command) => command.implemented === false);
143
- checks.push(
144
- doctorCheck(
145
- 'provider',
146
- stubProvider ? 'warn' : 'pass',
147
- stubProvider
148
- ? `${config.provider} is not implemented in v1; use an *-api provider`
149
- : `${config.provider} is registered`,
150
- ),
151
- );
152
- checks.push(doctorCheck('capabilities', 'pass', 'Provider capabilities are available'));
153
- } else {
154
- checks.push(doctorCheck('provider', 'pass', `${config.provider} is registered`));
155
- checks.push(doctorCheck('capabilities', 'fail', 'Provider capabilities are not implemented'));
156
- }
157
- }
158
-
159
- let remoteUrl = null;
160
- try {
161
- assertGitRemote(config.remote, 'config.remote');
162
- remoteUrl = gitRemoteUrl(loaded.cwd, config.remote);
163
- parsed = parseRemoteUrl(remoteUrl);
164
- if (!parsed) {
165
- checks.push(doctorCheck('remote', 'fail', 'Could not parse git remote URL'));
166
- } else {
167
- ctx = contextFromConfig(config, loaded.cwd, parsed);
168
- checks.push(doctorCheck('remote', 'pass', 'Git remote URL parses successfully', {
169
- host: sanitizeField(parsed.host),
170
- owner: sanitizeField(parsed.owner),
171
- repo: sanitizeField(parsed.repo),
172
- }));
173
- }
174
- } catch (err) {
175
- checks.push(doctorCheck('remote', 'fail', err.forgeError?.message || err.message));
176
- }
177
-
178
- if (parsed) {
179
- try {
180
- assertConfigMatchesRemote(config, parsed);
181
- checks.push(doctorCheck('repo_match', 'pass', 'Config owner/repo matches git remote'));
182
- } catch (err) {
183
- checks.push(doctorCheck('repo_match', 'fail', err.forgeError?.message || err.message));
184
- }
185
-
186
- if (config.baseUrl && !trustedBaseUrl(config, parsed.host)) {
187
- checks.push(
188
- doctorCheck('host_binding', 'fail', `baseUrl host does not match remote host ${parsed.host}`),
189
- );
190
- } else {
191
- checks.push(doctorCheck('host_binding', 'pass', 'Configured host binding is trusted'));
192
- }
193
- }
194
-
195
- if (providerCapabilities) {
196
- const envNames = providerCapabilities.auth_envs || [];
197
- const presentEnv = envNames.find((name) => Boolean(process.env[name])) || null;
198
- checks.push(
199
- doctorCheck(
200
- 'auth',
201
- presentEnv ? 'pass' : 'warn',
202
- presentEnv ? `${presentEnv} is present` : 'No provider auth environment variable is set',
203
- { env_names: envNames, present_env: presentEnv },
204
- ),
205
- );
206
-
207
- if (!providerCapabilities.check_sources?.length) {
208
- checks.push(doctorCheck('checks', 'warn', 'Provider does not report forge check sources'));
209
- } else {
210
- checks.push(doctorCheck('checks', 'pass', 'Provider reports forge check sources', {
211
- sources: providerCapabilities.check_sources,
212
- }));
213
- }
214
- }
215
-
216
- const { bytes: ingestCapBytes, envOverride: ingestEnvOverride, invalidEnv: ingestInvalidEnv } =
217
- getEffectiveIngestMaxBytes();
218
- if (ingestInvalidEnv) {
219
- checks.push(
220
- doctorCheck(
221
- 'forge_ingest_cap',
222
- 'warn',
223
- `${FORGE_INGEST_MAX_BYTES_ENV} is invalid; using default 8192 bytes`,
224
- { effective_bytes: ingestCapBytes, env_override: false },
225
- ),
226
- );
227
- } else if (ingestEnvOverride) {
228
- checks.push(
229
- doctorCheck(
230
- 'forge_ingest_cap',
231
- 'warn',
232
- `${FORGE_INGEST_MAX_BYTES_ENV} overrides default ingest cap; agent-safe guarantee is weakened`,
233
- { effective_bytes: ingestCapBytes, env_override: true },
234
- ),
235
- );
236
- } else {
237
- checks.push(
238
- doctorCheck(
239
- 'forge_ingest_cap',
240
- 'pass',
241
- 'Forge HTTP ingest cap is default 8192 bytes',
242
- { effective_bytes: ingestCapBytes, env_override: false },
243
- ),
244
- );
245
- }
246
-
247
- checks.push(doctorCheck('api_reachability', 'skipped', 'Live API reachability is not checked by default'));
248
-
249
- return finalizeDoctorPacket(ctx, checks, providerCapabilities);
250
- }
251
-
252
26
  export async function runCli(argv, options = {}) {
253
27
  const cwd = options.cwd ?? process.env.REMOGRAM_CWD ?? process.cwd();
254
28
  const providers = options.providers ?? PROVIDERS;
255
- const positional = [];
256
- let asJson = false;
257
- const flags = {};
258
-
259
- for (let i = 0; i < argv.length; i += 1) {
260
- const arg = argv[i];
261
- if (arg === '--json') asJson = true;
262
- else if (arg.startsWith('--')) {
263
- const key = arg.slice(2).replace(/-/g, '_');
264
- const next = argv[i + 1];
265
- if (next != null && !next.startsWith('--')) {
266
- flags[key] = next;
267
- i += 1;
268
- } else {
269
- flags[key] = true;
270
- }
271
- } else {
272
- positional.push(arg);
273
- }
274
- }
275
-
29
+ const { positional, asJson, flags } = parseCliArgv(argv);
30
+ const operatorConfigPath =
31
+ options.operatorConfigPath ?? flags.operator_config ?? null;
276
32
  const [group, sub] = positional;
277
33
 
278
34
  if (group === 'doctor' && sub == null) {
279
- const packet = await buildDoctorPacket(cwd, providers);
35
+ const packet = await buildDoctorPacket(cwd, providers, {
36
+ live: flags.live === true,
37
+ operatorConfigPath,
38
+ });
280
39
  output(packet, asJson);
281
40
  if (!packet.ok) process.exitCode = 1;
282
41
  return;
@@ -285,7 +44,7 @@ export async function runCli(argv, options = {}) {
285
44
  let ctx;
286
45
  try {
287
46
  const loaded = assertForgeReady(loadConfig(cwd));
288
- ctx = forgeContext(loaded);
47
+ ctx = forgeContext(loaded, { operatorConfigPath });
289
48
  } catch (err) {
290
49
  handleError(err, null, asJson);
291
50
  return;
@@ -308,74 +67,9 @@ export async function runCli(argv, options = {}) {
308
67
  }
309
68
 
310
69
  try {
311
- let packet;
312
- if (group === 'provider' && sub === 'capabilities') {
313
- packet = forgePacket(
314
- PACKET_TYPES.PROVIDER_CAPABILITIES,
315
- ctx,
316
- await provider.providerCapabilities(ctx),
317
- );
318
- } else if (group === 'repo' && sub === 'status') {
319
- packet = forgePacket(PACKET_TYPES.REPO_STATUS, ctx, await provider.repoStatus(ctx));
320
- } else if (group === 'refs' && sub === 'compare') {
321
- if (!flags.base || !flags.head) {
322
- throw Object.assign(new Error('--base and --head required'), {
323
- forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--base and --head required'),
324
- });
325
- }
326
- assertGitRef(flags.base, '--base');
327
- assertGitRef(flags.head, '--head');
328
- packet = forgePacket(
329
- PACKET_TYPES.REF_COMPARE,
330
- ctx,
331
- await provider.refsCompare(ctx, flags.base, flags.head),
332
- );
333
- } else if (group === 'pr' && sub === 'view') {
334
- const number = parsePositiveInt(flags.number, '--number');
335
- if (number == null) {
336
- throw Object.assign(new Error('--number required'), {
337
- forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for pr view'),
338
- });
339
- }
340
- packet = forgePacket(PACKET_TYPES.PR_STATUS, ctx, await provider.prView(ctx, { number }));
341
- } else if (group === 'pr' && sub === 'checks') {
342
- const number = parsePositiveInt(flags.number, '--number');
343
- if (number == null && !flags.ref) {
344
- throw Object.assign(new Error('--number or --ref required'), {
345
- forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number or --ref required for pr checks'),
346
- });
347
- }
348
- if (flags.ref) assertGitRef(flags.ref, '--ref');
349
- packet = forgePacket(
350
- PACKET_TYPES.PR_CHECKS,
351
- ctx,
352
- await provider.prChecks(ctx, { number, ref: flags.ref }),
353
- );
354
- } else if (group === 'merge' && sub === 'plan') {
355
- const number = parsePositiveInt(flags.number, '--number');
356
- if (number == null) {
357
- throw Object.assign(new Error('--number required'), {
358
- forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for merge plan'),
359
- });
360
- }
361
- packet = forgePacket(PACKET_TYPES.MERGE_PLAN, ctx, await provider.mergePlan(ctx, { number }));
362
- } else if (group === 'sync' && sub === 'plan') {
363
- const remote = flags.remote || ctx.config.remote;
364
- assertGitRemote(remote, '--remote');
365
- packet = forgePacket(
366
- PACKET_TYPES.SYNC_PLAN,
367
- ctx,
368
- await provider.syncPlan(ctx, remote),
369
- );
370
- } else {
371
- throw Object.assign(new Error(`Unknown command: ${positional.join(' ')}`), {
372
- forgeError: forgeError(
373
- ERROR_CODES.INVALID_ARGS,
374
- 'Unknown command. Try: provider capabilities, repo status, refs compare, pr view, pr checks, merge plan, sync plan',
375
- ),
376
- });
377
- }
70
+ const packet = await dispatchForgeCommand({ group, sub, flags, positional, ctx, provider });
378
71
  output(packet, asJson);
72
+ if (!packet.ok) process.exitCode = 1;
379
73
  } catch (err) {
380
74
  handleError(err, ctx, asJson);
381
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remogram/cli",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.10",
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.1",
31
- "@remogram/provider-gitea-api": "0.1.0-beta.1",
32
- "@remogram/provider-github-api": "0.1.0-beta.1",
33
- "@remogram/provider-gitlab-api": "0.1.0-beta.1",
34
- "@remogram/provider-gitea-tea": "0.1.0-beta.1",
35
- "@remogram/provider-github-gh": "0.1.0-beta.1"
30
+ "@remogram/core": "0.1.0-beta.10",
31
+ "@remogram/provider-gitea-api": "0.1.0-beta.10",
32
+ "@remogram/provider-github-api": "0.1.0-beta.10",
33
+ "@remogram/provider-gitlab-api": "0.1.0-beta.10",
34
+ "@remogram/provider-gitea-tea": "0.1.0-beta.10",
35
+ "@remogram/provider-github-gh": "0.1.0-beta.10"
36
36
  }
37
37
  }