@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/cli-argv.js +50 -0
- package/cli-dispatch.js +585 -0
- package/cli-doctor.js +358 -0
- package/cli-io.js +53 -0
- package/index.js +14 -320
- package/package.json +7 -7
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.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
|
}
|