@remogram/cli 0.1.0-beta.5 → 0.1.0-beta.6
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 +312 -0
- package/cli-doctor.js +211 -0
- package/cli-io.js +36 -0
- package/index.js +6 -575
- package/package.json +7 -7
package/cli-argv.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ERROR_CODES, forgeError } from '@remogram/core';
|
|
2
|
+
|
|
3
|
+
export const REPEATABLE_FLAGS = new Set(['allowed_path']);
|
|
4
|
+
|
|
5
|
+
export function parseAllowedPathFlags(flags) {
|
|
6
|
+
if (flags.allowed_path == null) return undefined;
|
|
7
|
+
return Array.isArray(flags.allowed_path) ? flags.allowed_path : [flags.allowed_path];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parsePositiveInt(value, name) {
|
|
11
|
+
if (value == null) return undefined;
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
14
|
+
throw Object.assign(new Error(`Invalid ${name}`), {
|
|
15
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, `${name} must be a positive integer`),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseCliArgv(argv) {
|
|
22
|
+
const positional = [];
|
|
23
|
+
let asJson = false;
|
|
24
|
+
const flags = {};
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
27
|
+
const arg = argv[i];
|
|
28
|
+
if (arg === '--json') asJson = true;
|
|
29
|
+
else if (arg.startsWith('--')) {
|
|
30
|
+
const key = arg.slice(2).replace(/-/g, '_');
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
if (REPEATABLE_FLAGS.has(key)) {
|
|
33
|
+
if (!flags[key]) flags[key] = [];
|
|
34
|
+
if (next != null && !next.startsWith('--')) {
|
|
35
|
+
flags[key].push(next);
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
} else if (next != null && !next.startsWith('--')) {
|
|
39
|
+
flags[key] = next;
|
|
40
|
+
i += 1;
|
|
41
|
+
} else {
|
|
42
|
+
flags[key] = true;
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
positional.push(arg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { positional, asJson, flags };
|
|
50
|
+
}
|
package/cli-dispatch.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forgePacket,
|
|
3
|
+
PACKET_TYPES,
|
|
4
|
+
ERROR_CODES,
|
|
5
|
+
forgeError,
|
|
6
|
+
assertGitRef,
|
|
7
|
+
assertGitRemote,
|
|
8
|
+
throwIfStaleHeadByNumber,
|
|
9
|
+
FACT_INVENTORY_PACKET_TYPES,
|
|
10
|
+
forgeFactInventoryPacket,
|
|
11
|
+
assertWriteCommandConfigured,
|
|
12
|
+
parseSinceObservedAt,
|
|
13
|
+
normalizeAllowedPaths,
|
|
14
|
+
} from '@remogram/core';
|
|
15
|
+
import { parseAllowedPathFlags, parsePositiveInt } from './cli-argv.js';
|
|
16
|
+
|
|
17
|
+
export async function dispatchForgeCommand({ group, sub, flags, positional, ctx, provider }) {
|
|
18
|
+
if (group === 'provider' && sub === 'capabilities') {
|
|
19
|
+
return forgePacket(
|
|
20
|
+
PACKET_TYPES.PROVIDER_CAPABILITIES,
|
|
21
|
+
ctx,
|
|
22
|
+
await provider.providerCapabilities(ctx),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (group === 'repo' && sub === 'status') {
|
|
26
|
+
return forgePacket(PACKET_TYPES.REPO_STATUS, ctx, await provider.repoStatus(ctx));
|
|
27
|
+
}
|
|
28
|
+
if (group === 'refs' && sub === 'compare') {
|
|
29
|
+
if (!flags.base || !flags.head) {
|
|
30
|
+
throw Object.assign(new Error('--base and --head required'), {
|
|
31
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--base and --head required'),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
assertGitRef(flags.base, '--base');
|
|
35
|
+
assertGitRef(flags.head, '--head');
|
|
36
|
+
return forgePacket(
|
|
37
|
+
PACKET_TYPES.REF_COMPARE,
|
|
38
|
+
ctx,
|
|
39
|
+
await provider.refsCompare(ctx, flags.base, flags.head),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (group === 'refs' && sub === 'inventory') {
|
|
43
|
+
if (typeof provider.refsInventory !== 'function') {
|
|
44
|
+
throw Object.assign(new Error('refs inventory not implemented for provider'), {
|
|
45
|
+
forgeError: forgeError(
|
|
46
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
47
|
+
'refs inventory not implemented for provider',
|
|
48
|
+
),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return forgeFactInventoryPacket(
|
|
52
|
+
FACT_INVENTORY_PACKET_TYPES.REF_INVENTORY,
|
|
53
|
+
ctx,
|
|
54
|
+
await provider.refsInventory(ctx),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (group === 'cr' && sub === 'inventory') {
|
|
58
|
+
if (typeof provider.crInventory !== 'function') {
|
|
59
|
+
throw Object.assign(new Error('cr inventory not implemented for provider'), {
|
|
60
|
+
forgeError: forgeError(
|
|
61
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
62
|
+
'cr inventory not implemented for provider',
|
|
63
|
+
),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const inventoryBody = await provider.crInventory(ctx, {
|
|
67
|
+
slice_ref: flags.slice_ref,
|
|
68
|
+
limit: parsePositiveInt(flags.limit, '--limit'),
|
|
69
|
+
sort: flags.sort,
|
|
70
|
+
});
|
|
71
|
+
if (inventoryBody.list_truncated === true) {
|
|
72
|
+
throw Object.assign(new Error('Open CR list incomplete'), {
|
|
73
|
+
forgeError: forgeError(
|
|
74
|
+
ERROR_CODES.INVENTORY_LIST_INCOMPLETE,
|
|
75
|
+
'Open change request list could not be proved complete within pagination bounds',
|
|
76
|
+
null,
|
|
77
|
+
{
|
|
78
|
+
inventory_list: {
|
|
79
|
+
entry_count: inventoryBody.entry_count,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return forgeFactInventoryPacket(
|
|
86
|
+
FACT_INVENTORY_PACKET_TYPES.CR_INVENTORY_SLICE,
|
|
87
|
+
ctx,
|
|
88
|
+
inventoryBody,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (group === 'cr' && sub === 'files') {
|
|
92
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
93
|
+
if (number == null) {
|
|
94
|
+
throw Object.assign(new Error('--number required'), {
|
|
95
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for cr files'),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (typeof provider.crFiles !== 'function') {
|
|
99
|
+
throw Object.assign(new Error('cr files not implemented for provider'), {
|
|
100
|
+
forgeError: forgeError(
|
|
101
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
102
|
+
'cr files not implemented for provider',
|
|
103
|
+
),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return forgePacket(
|
|
107
|
+
PACKET_TYPES.CR_FILES,
|
|
108
|
+
ctx,
|
|
109
|
+
await provider.crFiles(ctx, { number }),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (group === 'cr' && sub === 'comments') {
|
|
113
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
114
|
+
if (number == null) {
|
|
115
|
+
throw Object.assign(new Error('--number required'), {
|
|
116
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for cr comments'),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (typeof provider.crComments !== 'function') {
|
|
120
|
+
throw Object.assign(new Error('cr comments not implemented for provider'), {
|
|
121
|
+
forgeError: forgeError(
|
|
122
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
123
|
+
'cr comments not implemented for provider',
|
|
124
|
+
),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return forgePacket(
|
|
128
|
+
PACKET_TYPES.CR_COMMENTS,
|
|
129
|
+
ctx,
|
|
130
|
+
await provider.crComments(ctx, { number }),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (group === 'forge' && sub === 'changes') {
|
|
134
|
+
const sinceIso = parseSinceObservedAt(flags.since);
|
|
135
|
+
if (typeof provider.forgeChanges !== 'function') {
|
|
136
|
+
throw Object.assign(new Error('forge changes not implemented for provider'), {
|
|
137
|
+
forgeError: forgeError(
|
|
138
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
139
|
+
'forge changes not implemented for provider',
|
|
140
|
+
),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return forgePacket(
|
|
144
|
+
PACKET_TYPES.FORGE_CHANGES,
|
|
145
|
+
ctx,
|
|
146
|
+
await provider.forgeChanges(ctx, { since: sinceIso }),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (group === 'cr' && sub === 'open') {
|
|
150
|
+
if (typeof provider.crOpen !== 'function') {
|
|
151
|
+
throw Object.assign(new Error('cr open not implemented for provider'), {
|
|
152
|
+
forgeError: forgeError(
|
|
153
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
154
|
+
'cr open not implemented for provider',
|
|
155
|
+
),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!flags.head || !flags.base || !flags.title) {
|
|
159
|
+
throw Object.assign(new Error('--head, --base, and --title required'), {
|
|
160
|
+
forgeError: forgeError(
|
|
161
|
+
ERROR_CODES.INVALID_ARGS,
|
|
162
|
+
'--head, --base, and --title required for cr open',
|
|
163
|
+
),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
assertGitRef(flags.head, '--head');
|
|
167
|
+
assertGitRef(flags.base, '--base');
|
|
168
|
+
assertWriteCommandConfigured(ctx.config, 'cr_open');
|
|
169
|
+
return forgePacket(
|
|
170
|
+
PACKET_TYPES.CHANGE_REQUEST_OPENED,
|
|
171
|
+
ctx,
|
|
172
|
+
await provider.crOpen(ctx, {
|
|
173
|
+
head: flags.head,
|
|
174
|
+
base: flags.base,
|
|
175
|
+
title: flags.title,
|
|
176
|
+
body: flags.body,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (group === 'status' && sub === 'set') {
|
|
181
|
+
if (typeof provider.statusSet !== 'function') {
|
|
182
|
+
throw Object.assign(new Error('status set not implemented for provider'), {
|
|
183
|
+
forgeError: forgeError(
|
|
184
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
185
|
+
'status set not implemented for provider',
|
|
186
|
+
),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
assertWriteCommandConfigured(ctx.config, 'status_set');
|
|
190
|
+
return forgePacket(
|
|
191
|
+
PACKET_TYPES.COMMIT_STATUS_SET,
|
|
192
|
+
ctx,
|
|
193
|
+
await provider.statusSet(ctx, {
|
|
194
|
+
sha: flags.sha,
|
|
195
|
+
context: flags.context,
|
|
196
|
+
state: flags.state,
|
|
197
|
+
target_url: flags.target_url,
|
|
198
|
+
description: flags.description,
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (group === 'pr' && sub === 'view') {
|
|
203
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
204
|
+
if (number == null) {
|
|
205
|
+
throw Object.assign(new Error('--number required'), {
|
|
206
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for pr view'),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const body = await provider.prView(ctx, { number });
|
|
210
|
+
throwIfStaleHeadByNumber(
|
|
211
|
+
ctx,
|
|
212
|
+
PACKET_TYPES.PR_STATUS,
|
|
213
|
+
body,
|
|
214
|
+
body.head_ref,
|
|
215
|
+
body.head_sha,
|
|
216
|
+
);
|
|
217
|
+
return forgePacket(PACKET_TYPES.PR_STATUS, ctx, body);
|
|
218
|
+
}
|
|
219
|
+
if (group === 'pr' && sub === 'checks') {
|
|
220
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
221
|
+
if (number == null && !flags.ref) {
|
|
222
|
+
throw Object.assign(new Error('--number or --ref required'), {
|
|
223
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number or --ref required for pr checks'),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (flags.ref) assertGitRef(flags.ref, '--ref');
|
|
227
|
+
if (number != null && !flags.ref) {
|
|
228
|
+
const view = await provider.prView(ctx, { number });
|
|
229
|
+
throwIfStaleHeadByNumber(
|
|
230
|
+
ctx,
|
|
231
|
+
PACKET_TYPES.PR_CHECKS,
|
|
232
|
+
{ head_sha: view.head_sha },
|
|
233
|
+
view.head_ref,
|
|
234
|
+
view.head_sha,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return forgePacket(
|
|
238
|
+
PACKET_TYPES.PR_CHECKS,
|
|
239
|
+
ctx,
|
|
240
|
+
await provider.prChecks(ctx, { number, ref: flags.ref }),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (group === 'merge' && sub === 'plan') {
|
|
244
|
+
const number = parsePositiveInt(flags.number, '--number');
|
|
245
|
+
if (number == null) {
|
|
246
|
+
throw Object.assign(new Error('--number required'), {
|
|
247
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for merge plan'),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const allowedPaths = normalizeAllowedPaths(parseAllowedPathFlags(flags) ?? []);
|
|
251
|
+
return forgePacket(
|
|
252
|
+
PACKET_TYPES.MERGE_PLAN,
|
|
253
|
+
ctx,
|
|
254
|
+
await provider.mergePlan(ctx, {
|
|
255
|
+
number,
|
|
256
|
+
...(allowedPaths ? { allowed_paths: allowedPaths } : {}),
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (group === 'branch' && sub === 'protection') {
|
|
261
|
+
const branchRef = flags.branch_ref;
|
|
262
|
+
if (!branchRef) {
|
|
263
|
+
throw Object.assign(new Error('--branch-ref required'), {
|
|
264
|
+
forgeError: forgeError(
|
|
265
|
+
ERROR_CODES.INVALID_ARGS,
|
|
266
|
+
'--branch-ref required for branch protection',
|
|
267
|
+
),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
assertGitRef(branchRef, '--branch-ref');
|
|
271
|
+
if (typeof provider.branchProtection !== 'function') {
|
|
272
|
+
throw Object.assign(new Error('branch protection not implemented for provider'), {
|
|
273
|
+
forgeError: forgeError(
|
|
274
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
275
|
+
'branch protection not implemented for provider',
|
|
276
|
+
),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return forgePacket(
|
|
280
|
+
PACKET_TYPES.BRANCH_PROTECTION,
|
|
281
|
+
ctx,
|
|
282
|
+
await provider.branchProtection(ctx, { branchRef }),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (group === 'whoami' && sub == null) {
|
|
286
|
+
if (typeof provider.whoami !== 'function') {
|
|
287
|
+
throw Object.assign(new Error('whoami not implemented for provider'), {
|
|
288
|
+
forgeError: forgeError(
|
|
289
|
+
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
290
|
+
'whoami not implemented for provider',
|
|
291
|
+
),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return forgePacket(PACKET_TYPES.PROVIDER_IDENTITY, ctx, await provider.whoami(ctx));
|
|
295
|
+
}
|
|
296
|
+
if (group === 'sync' && sub === 'plan') {
|
|
297
|
+
const remote = flags.remote || ctx.config.remote;
|
|
298
|
+
assertGitRemote(remote, '--remote');
|
|
299
|
+
return forgePacket(
|
|
300
|
+
PACKET_TYPES.SYNC_PLAN,
|
|
301
|
+
ctx,
|
|
302
|
+
await provider.syncPlan(ctx, remote),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
throw Object.assign(new Error(`Unknown command: ${positional.join(' ')}`), {
|
|
307
|
+
forgeError: forgeError(
|
|
308
|
+
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',
|
|
310
|
+
),
|
|
311
|
+
});
|
|
312
|
+
}
|
package/cli-doctor.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadConfig,
|
|
3
|
+
findConfigPath,
|
|
4
|
+
gitRemoteUrl,
|
|
5
|
+
parseRemoteUrl,
|
|
6
|
+
trustedBaseUrl,
|
|
7
|
+
assertConfigMatchesRemote,
|
|
8
|
+
forgePacket,
|
|
9
|
+
unknownForgeContext,
|
|
10
|
+
PACKET_TYPES,
|
|
11
|
+
ERROR_CODES,
|
|
12
|
+
forgeError,
|
|
13
|
+
sanitizeField,
|
|
14
|
+
assertGitRemote,
|
|
15
|
+
getEffectiveIngestMaxBytes,
|
|
16
|
+
FORGE_INGEST_MAX_BYTES_ENV,
|
|
17
|
+
MAX_FORGE_INGEST_ENV_BYTES,
|
|
18
|
+
} from '@remogram/core';
|
|
19
|
+
import { contextFromConfig } from './cli-io.js';
|
|
20
|
+
|
|
21
|
+
export function doctorCheck(name, status, message, details = null) {
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
status,
|
|
25
|
+
message: sanitizeField(message),
|
|
26
|
+
...(details ? { details } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function doctorSummary(checks) {
|
|
31
|
+
if (checks.some((check) => check.status === 'fail')) return 'fail';
|
|
32
|
+
if (checks.some((check) => check.status === 'warn')) return 'warn';
|
|
33
|
+
return 'pass';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function finalizeDoctorPacket(ctx, checks, providerCapabilities) {
|
|
37
|
+
const summary = doctorSummary(checks);
|
|
38
|
+
const error =
|
|
39
|
+
summary === 'fail'
|
|
40
|
+
? forgeError(ERROR_CODES.CONFIG_INVALID, 'Doctor checks failed')
|
|
41
|
+
: null;
|
|
42
|
+
return forgePacket(
|
|
43
|
+
PACKET_TYPES.PROVIDER_DOCTOR,
|
|
44
|
+
ctx,
|
|
45
|
+
{
|
|
46
|
+
summary,
|
|
47
|
+
checks,
|
|
48
|
+
provider_capabilities: providerCapabilities,
|
|
49
|
+
},
|
|
50
|
+
error,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function buildDoctorPacket(cwd, providers) {
|
|
55
|
+
const checks = [];
|
|
56
|
+
const configPath = findConfigPath(cwd);
|
|
57
|
+
let loaded = null;
|
|
58
|
+
let config = null;
|
|
59
|
+
let parsed = null;
|
|
60
|
+
let ctx = unknownForgeContext();
|
|
61
|
+
let providerCapabilities = null;
|
|
62
|
+
|
|
63
|
+
if (!configPath) {
|
|
64
|
+
checks.push(doctorCheck('config', 'fail', 'No .remogram.json found'));
|
|
65
|
+
return finalizeDoctorPacket(ctx, checks, null);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
loaded = loadConfig(cwd);
|
|
70
|
+
config = loaded.config;
|
|
71
|
+
ctx = contextFromConfig(config, loaded.cwd);
|
|
72
|
+
checks.push(doctorCheck('config', 'pass', '.remogram.json is present and valid'));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
checks.push(doctorCheck('config', 'fail', err.forgeError?.message || err.message));
|
|
75
|
+
return finalizeDoctorPacket(ctx, checks, null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const provider = providers[config.provider];
|
|
79
|
+
if (!provider) {
|
|
80
|
+
checks.push(doctorCheck('provider', 'fail', `Unsupported provider: ${config.provider}`));
|
|
81
|
+
} else {
|
|
82
|
+
if (typeof provider.providerCapabilities === 'function') {
|
|
83
|
+
providerCapabilities = await provider.providerCapabilities(ctx);
|
|
84
|
+
const stubProvider =
|
|
85
|
+
providerCapabilities.commands?.length > 0
|
|
86
|
+
&& providerCapabilities.commands.every((command) => command.implemented === false);
|
|
87
|
+
checks.push(
|
|
88
|
+
doctorCheck(
|
|
89
|
+
'provider',
|
|
90
|
+
stubProvider ? 'warn' : 'pass',
|
|
91
|
+
stubProvider
|
|
92
|
+
? `${config.provider} is not fully supported in v1; use an *-api provider`
|
|
93
|
+
: `${config.provider} is registered`,
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
checks.push(doctorCheck('capabilities', 'pass', 'Provider capabilities are available'));
|
|
97
|
+
} else {
|
|
98
|
+
checks.push(doctorCheck('provider', 'pass', `${config.provider} is registered`));
|
|
99
|
+
checks.push(doctorCheck('capabilities', 'fail', 'Provider capabilities are not implemented'));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
assertGitRemote(config.remote, 'config.remote');
|
|
105
|
+
const remoteUrl = gitRemoteUrl(loaded.cwd, config.remote);
|
|
106
|
+
parsed = parseRemoteUrl(remoteUrl);
|
|
107
|
+
if (!parsed) {
|
|
108
|
+
checks.push(doctorCheck('remote', 'fail', 'Could not parse git remote URL'));
|
|
109
|
+
} else {
|
|
110
|
+
ctx = contextFromConfig(config, loaded.cwd, parsed);
|
|
111
|
+
checks.push(doctorCheck('remote', 'pass', 'Git remote URL parses successfully', {
|
|
112
|
+
host: sanitizeField(parsed.host),
|
|
113
|
+
owner: sanitizeField(parsed.owner),
|
|
114
|
+
repo: sanitizeField(parsed.repo),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
checks.push(doctorCheck('remote', 'fail', err.forgeError?.message || err.message));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (parsed) {
|
|
122
|
+
try {
|
|
123
|
+
assertConfigMatchesRemote(config, parsed);
|
|
124
|
+
checks.push(doctorCheck('repo_match', 'pass', 'Config owner/repo matches git remote'));
|
|
125
|
+
} catch (err) {
|
|
126
|
+
checks.push(doctorCheck('repo_match', 'fail', err.forgeError?.message || err.message));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (config.baseUrl && !trustedBaseUrl(config, parsed.host)) {
|
|
130
|
+
checks.push(
|
|
131
|
+
doctorCheck('host_binding', 'fail', `baseUrl host does not match remote host ${parsed.host}`),
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
checks.push(doctorCheck('host_binding', 'pass', 'Configured host binding is trusted'));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (providerCapabilities) {
|
|
139
|
+
const envNames = providerCapabilities.auth_envs || [];
|
|
140
|
+
const presentEnv = envNames.find((name) => Boolean(process.env[name])) || null;
|
|
141
|
+
checks.push(
|
|
142
|
+
doctorCheck(
|
|
143
|
+
'auth',
|
|
144
|
+
presentEnv ? 'pass' : 'warn',
|
|
145
|
+
presentEnv ? `${presentEnv} is present` : 'No provider auth environment variable is set',
|
|
146
|
+
{ env_names: envNames, present_env: presentEnv },
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
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));
|
|
154
|
+
checks.push(
|
|
155
|
+
doctorCheck(
|
|
156
|
+
'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 },
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!providerCapabilities.check_sources?.length) {
|
|
167
|
+
checks.push(doctorCheck('checks', 'warn', 'Provider does not report forge check sources'));
|
|
168
|
+
} else {
|
|
169
|
+
checks.push(doctorCheck('checks', 'pass', 'Provider reports forge check sources', {
|
|
170
|
+
sources: providerCapabilities.check_sources,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { bytes: ingestCapBytes, envOverride: ingestEnvOverride, invalidEnv: ingestInvalidEnv, clamped: ingestClamped } =
|
|
176
|
+
getEffectiveIngestMaxBytes();
|
|
177
|
+
if (ingestInvalidEnv) {
|
|
178
|
+
checks.push(
|
|
179
|
+
doctorCheck(
|
|
180
|
+
'forge_ingest_cap',
|
|
181
|
+
'warn',
|
|
182
|
+
`${FORGE_INGEST_MAX_BYTES_ENV} is invalid; using default 8192 bytes`,
|
|
183
|
+
{ effective_bytes: ingestCapBytes, env_override: false },
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
} else if (ingestEnvOverride) {
|
|
187
|
+
checks.push(
|
|
188
|
+
doctorCheck(
|
|
189
|
+
'forge_ingest_cap',
|
|
190
|
+
'warn',
|
|
191
|
+
ingestClamped
|
|
192
|
+
? `${FORGE_INGEST_MAX_BYTES_ENV} exceeds max ${MAX_FORGE_INGEST_ENV_BYTES}; clamped — agent-safe guarantee is weakened`
|
|
193
|
+
: `${FORGE_INGEST_MAX_BYTES_ENV} overrides default ingest cap; agent-safe guarantee is weakened`,
|
|
194
|
+
{ effective_bytes: ingestCapBytes, env_override: true, ...(ingestClamped ? { clamped: true } : {}) },
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
} else {
|
|
198
|
+
checks.push(
|
|
199
|
+
doctorCheck(
|
|
200
|
+
'forge_ingest_cap',
|
|
201
|
+
'pass',
|
|
202
|
+
'Forge HTTP ingest cap is default 8192 bytes',
|
|
203
|
+
{ effective_bytes: ingestCapBytes, env_override: false },
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
checks.push(doctorCheck('api_reachability', 'skipped', 'Live API reachability is not checked by default'));
|
|
209
|
+
|
|
210
|
+
return finalizeDoctorPacket(ctx, checks, providerCapabilities);
|
|
211
|
+
}
|
package/cli-io.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { forgePacket, forgeErrorPacket, ERROR_CODES, forgeError } from '@remogram/core';
|
|
2
|
+
|
|
3
|
+
export function output(packet, asJson) {
|
|
4
|
+
console.log(JSON.stringify(packet, null, asJson ? 2 : 0));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
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
|
+
};
|
|
13
|
+
const baseCtx = ctx || {
|
|
14
|
+
providerId: 'unknown',
|
|
15
|
+
remoteName: 'origin',
|
|
16
|
+
repoId: 'unknown/unknown',
|
|
17
|
+
};
|
|
18
|
+
if (err.staleHeadPacket) {
|
|
19
|
+
output(forgePacket(err.staleHeadPacket.type, baseCtx, err.staleHeadPacket.body, fe), asJson);
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
output(forgeErrorPacket(baseCtx, fe), asJson);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function contextFromConfig(config, cwd, parsed = null) {
|
|
28
|
+
return {
|
|
29
|
+
providerId: config.provider,
|
|
30
|
+
remoteName: config.remote,
|
|
31
|
+
repoId: parsed ? `${parsed.owner}/${parsed.repo}` : `${config.owner}/${config.repo}`,
|
|
32
|
+
config,
|
|
33
|
+
cwd,
|
|
34
|
+
parsed,
|
|
35
|
+
};
|
|
36
|
+
}
|
package/index.js
CHANGED
|
@@ -1,34 +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
|
-
throwIfStaleHeadByNumber,
|
|
22
|
-
FACT_INVENTORY_PACKET_TYPES,
|
|
23
|
-
forgeFactInventoryPacket,
|
|
24
|
-
assertWriteCommandConfigured,
|
|
25
|
-
parseSinceObservedAt,
|
|
26
7
|
} from '@remogram/core';
|
|
27
8
|
import { provider as giteaApi } from '@remogram/provider-gitea-api';
|
|
28
9
|
import { provider as githubApi } from '@remogram/provider-github-api';
|
|
29
10
|
import { provider as gitlabApi } from '@remogram/provider-gitlab-api';
|
|
30
11
|
import { provider as giteaTea } from '@remogram/provider-gitea-tea';
|
|
31
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';
|
|
32
17
|
|
|
33
18
|
const PROVIDERS = {
|
|
34
19
|
'gitea-api': giteaApi,
|
|
@@ -38,280 +23,10 @@ const PROVIDERS = {
|
|
|
38
23
|
'github-gh': githubGh,
|
|
39
24
|
};
|
|
40
25
|
|
|
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
|
-
|
|
48
|
-
function parsePositiveInt(value, name) {
|
|
49
|
-
if (value == null) return undefined;
|
|
50
|
-
const n = Number(value);
|
|
51
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
52
|
-
throw Object.assign(new Error(`Invalid ${name}`), {
|
|
53
|
-
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, `${name} must be a positive integer`),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return n;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function output(packet, asJson) {
|
|
60
|
-
console.log(JSON.stringify(packet, null, asJson ? 2 : 0));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function handleError(err, ctx, asJson) {
|
|
64
|
-
const fe = err.forgeError || {
|
|
65
|
-
code: ERROR_CODES.API_ERROR,
|
|
66
|
-
message: err.message,
|
|
67
|
-
status: err.status,
|
|
68
|
-
};
|
|
69
|
-
const baseCtx = ctx || {
|
|
70
|
-
providerId: 'unknown',
|
|
71
|
-
remoteName: 'origin',
|
|
72
|
-
repoId: 'unknown/unknown',
|
|
73
|
-
};
|
|
74
|
-
if (err.staleHeadPacket) {
|
|
75
|
-
output(forgePacket(err.staleHeadPacket.type, baseCtx, err.staleHeadPacket.body, fe), asJson);
|
|
76
|
-
process.exitCode = 1;
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
output(forgeErrorPacket(baseCtx, fe), asJson);
|
|
80
|
-
process.exitCode = 1;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function doctorCheck(name, status, message, details = null) {
|
|
84
|
-
return {
|
|
85
|
-
name,
|
|
86
|
-
status,
|
|
87
|
-
message: sanitizeField(message),
|
|
88
|
-
...(details ? { details } : {}),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function doctorSummary(checks) {
|
|
93
|
-
if (checks.some((check) => check.status === 'fail')) return 'fail';
|
|
94
|
-
if (checks.some((check) => check.status === 'warn')) return 'warn';
|
|
95
|
-
return 'pass';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function contextFromConfig(config, cwd, parsed = null) {
|
|
99
|
-
return {
|
|
100
|
-
providerId: config.provider,
|
|
101
|
-
remoteName: config.remote,
|
|
102
|
-
repoId: parsed ? `${parsed.owner}/${parsed.repo}` : `${config.owner}/${config.repo}`,
|
|
103
|
-
config,
|
|
104
|
-
cwd,
|
|
105
|
-
parsed,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function finalizeDoctorPacket(ctx, checks, providerCapabilities) {
|
|
110
|
-
const summary = doctorSummary(checks);
|
|
111
|
-
const error =
|
|
112
|
-
summary === 'fail'
|
|
113
|
-
? forgeError(ERROR_CODES.CONFIG_INVALID, 'Doctor checks failed')
|
|
114
|
-
: null;
|
|
115
|
-
return forgePacket(
|
|
116
|
-
PACKET_TYPES.PROVIDER_DOCTOR,
|
|
117
|
-
ctx,
|
|
118
|
-
{
|
|
119
|
-
summary,
|
|
120
|
-
checks,
|
|
121
|
-
provider_capabilities: providerCapabilities,
|
|
122
|
-
},
|
|
123
|
-
error,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function buildDoctorPacket(cwd, providers) {
|
|
128
|
-
const checks = [];
|
|
129
|
-
const configPath = findConfigPath(cwd);
|
|
130
|
-
let loaded = null;
|
|
131
|
-
let config = null;
|
|
132
|
-
let parsed = null;
|
|
133
|
-
let ctx = unknownForgeContext();
|
|
134
|
-
let providerCapabilities = null;
|
|
135
|
-
|
|
136
|
-
if (!configPath) {
|
|
137
|
-
checks.push(doctorCheck('config', 'fail', 'No .remogram.json found'));
|
|
138
|
-
return finalizeDoctorPacket(ctx, checks, null);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
loaded = loadConfig(cwd);
|
|
143
|
-
config = loaded.config;
|
|
144
|
-
ctx = contextFromConfig(config, loaded.cwd);
|
|
145
|
-
checks.push(doctorCheck('config', 'pass', '.remogram.json is present and valid'));
|
|
146
|
-
} catch (err) {
|
|
147
|
-
checks.push(doctorCheck('config', 'fail', err.forgeError?.message || err.message));
|
|
148
|
-
return finalizeDoctorPacket(ctx, checks, null);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const provider = providers[config.provider];
|
|
152
|
-
if (!provider) {
|
|
153
|
-
checks.push(doctorCheck('provider', 'fail', `Unsupported provider: ${config.provider}`));
|
|
154
|
-
} else {
|
|
155
|
-
if (typeof provider.providerCapabilities === 'function') {
|
|
156
|
-
providerCapabilities = await provider.providerCapabilities(ctx);
|
|
157
|
-
const stubProvider =
|
|
158
|
-
providerCapabilities.commands?.length > 0
|
|
159
|
-
&& providerCapabilities.commands.every((command) => command.implemented === false);
|
|
160
|
-
checks.push(
|
|
161
|
-
doctorCheck(
|
|
162
|
-
'provider',
|
|
163
|
-
stubProvider ? 'warn' : 'pass',
|
|
164
|
-
stubProvider
|
|
165
|
-
? `${config.provider} is not fully supported in v1; use an *-api provider`
|
|
166
|
-
: `${config.provider} is registered`,
|
|
167
|
-
),
|
|
168
|
-
);
|
|
169
|
-
checks.push(doctorCheck('capabilities', 'pass', 'Provider capabilities are available'));
|
|
170
|
-
} else {
|
|
171
|
-
checks.push(doctorCheck('provider', 'pass', `${config.provider} is registered`));
|
|
172
|
-
checks.push(doctorCheck('capabilities', 'fail', 'Provider capabilities are not implemented'));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let remoteUrl = null;
|
|
177
|
-
try {
|
|
178
|
-
assertGitRemote(config.remote, 'config.remote');
|
|
179
|
-
remoteUrl = gitRemoteUrl(loaded.cwd, config.remote);
|
|
180
|
-
parsed = parseRemoteUrl(remoteUrl);
|
|
181
|
-
if (!parsed) {
|
|
182
|
-
checks.push(doctorCheck('remote', 'fail', 'Could not parse git remote URL'));
|
|
183
|
-
} else {
|
|
184
|
-
ctx = contextFromConfig(config, loaded.cwd, parsed);
|
|
185
|
-
checks.push(doctorCheck('remote', 'pass', 'Git remote URL parses successfully', {
|
|
186
|
-
host: sanitizeField(parsed.host),
|
|
187
|
-
owner: sanitizeField(parsed.owner),
|
|
188
|
-
repo: sanitizeField(parsed.repo),
|
|
189
|
-
}));
|
|
190
|
-
}
|
|
191
|
-
} catch (err) {
|
|
192
|
-
checks.push(doctorCheck('remote', 'fail', err.forgeError?.message || err.message));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (parsed) {
|
|
196
|
-
try {
|
|
197
|
-
assertConfigMatchesRemote(config, parsed);
|
|
198
|
-
checks.push(doctorCheck('repo_match', 'pass', 'Config owner/repo matches git remote'));
|
|
199
|
-
} catch (err) {
|
|
200
|
-
checks.push(doctorCheck('repo_match', 'fail', err.forgeError?.message || err.message));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (config.baseUrl && !trustedBaseUrl(config, parsed.host)) {
|
|
204
|
-
checks.push(
|
|
205
|
-
doctorCheck('host_binding', 'fail', `baseUrl host does not match remote host ${parsed.host}`),
|
|
206
|
-
);
|
|
207
|
-
} else {
|
|
208
|
-
checks.push(doctorCheck('host_binding', 'pass', 'Configured host binding is trusted'));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (providerCapabilities) {
|
|
213
|
-
const envNames = providerCapabilities.auth_envs || [];
|
|
214
|
-
const presentEnv = envNames.find((name) => Boolean(process.env[name])) || null;
|
|
215
|
-
checks.push(
|
|
216
|
-
doctorCheck(
|
|
217
|
-
'auth',
|
|
218
|
-
presentEnv ? 'pass' : 'warn',
|
|
219
|
-
presentEnv ? `${presentEnv} is present` : 'No provider auth environment variable is set',
|
|
220
|
-
{ env_names: envNames, present_env: presentEnv },
|
|
221
|
-
),
|
|
222
|
-
);
|
|
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
|
-
|
|
240
|
-
if (!providerCapabilities.check_sources?.length) {
|
|
241
|
-
checks.push(doctorCheck('checks', 'warn', 'Provider does not report forge check sources'));
|
|
242
|
-
} else {
|
|
243
|
-
checks.push(doctorCheck('checks', 'pass', 'Provider reports forge check sources', {
|
|
244
|
-
sources: providerCapabilities.check_sources,
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const { bytes: ingestCapBytes, envOverride: ingestEnvOverride, invalidEnv: ingestInvalidEnv } =
|
|
250
|
-
getEffectiveIngestMaxBytes();
|
|
251
|
-
if (ingestInvalidEnv) {
|
|
252
|
-
checks.push(
|
|
253
|
-
doctorCheck(
|
|
254
|
-
'forge_ingest_cap',
|
|
255
|
-
'warn',
|
|
256
|
-
`${FORGE_INGEST_MAX_BYTES_ENV} is invalid; using default 8192 bytes`,
|
|
257
|
-
{ effective_bytes: ingestCapBytes, env_override: false },
|
|
258
|
-
),
|
|
259
|
-
);
|
|
260
|
-
} else if (ingestEnvOverride) {
|
|
261
|
-
checks.push(
|
|
262
|
-
doctorCheck(
|
|
263
|
-
'forge_ingest_cap',
|
|
264
|
-
'warn',
|
|
265
|
-
`${FORGE_INGEST_MAX_BYTES_ENV} overrides default ingest cap; agent-safe guarantee is weakened`,
|
|
266
|
-
{ effective_bytes: ingestCapBytes, env_override: true },
|
|
267
|
-
),
|
|
268
|
-
);
|
|
269
|
-
} else {
|
|
270
|
-
checks.push(
|
|
271
|
-
doctorCheck(
|
|
272
|
-
'forge_ingest_cap',
|
|
273
|
-
'pass',
|
|
274
|
-
'Forge HTTP ingest cap is default 8192 bytes',
|
|
275
|
-
{ effective_bytes: ingestCapBytes, env_override: false },
|
|
276
|
-
),
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
checks.push(doctorCheck('api_reachability', 'skipped', 'Live API reachability is not checked by default'));
|
|
281
|
-
|
|
282
|
-
return finalizeDoctorPacket(ctx, checks, providerCapabilities);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
26
|
export async function runCli(argv, options = {}) {
|
|
286
27
|
const cwd = options.cwd ?? process.env.REMOGRAM_CWD ?? process.cwd();
|
|
287
28
|
const providers = options.providers ?? PROVIDERS;
|
|
288
|
-
const positional =
|
|
289
|
-
let asJson = false;
|
|
290
|
-
const flags = {};
|
|
291
|
-
|
|
292
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
293
|
-
const arg = argv[i];
|
|
294
|
-
if (arg === '--json') asJson = true;
|
|
295
|
-
else if (arg.startsWith('--')) {
|
|
296
|
-
const key = arg.slice(2).replace(/-/g, '_');
|
|
297
|
-
const next = argv[i + 1];
|
|
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('--')) {
|
|
305
|
-
flags[key] = next;
|
|
306
|
-
i += 1;
|
|
307
|
-
} else {
|
|
308
|
-
flags[key] = true;
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
positional.push(arg);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
29
|
+
const { positional, asJson, flags } = parseCliArgv(argv);
|
|
315
30
|
const [group, sub] = positional;
|
|
316
31
|
|
|
317
32
|
if (group === 'doctor' && sub == null) {
|
|
@@ -347,291 +62,7 @@ export async function runCli(argv, options = {}) {
|
|
|
347
62
|
}
|
|
348
63
|
|
|
349
64
|
try {
|
|
350
|
-
|
|
351
|
-
if (group === 'provider' && sub === 'capabilities') {
|
|
352
|
-
packet = forgePacket(
|
|
353
|
-
PACKET_TYPES.PROVIDER_CAPABILITIES,
|
|
354
|
-
ctx,
|
|
355
|
-
await provider.providerCapabilities(ctx),
|
|
356
|
-
);
|
|
357
|
-
} else if (group === 'repo' && sub === 'status') {
|
|
358
|
-
packet = forgePacket(PACKET_TYPES.REPO_STATUS, ctx, await provider.repoStatus(ctx));
|
|
359
|
-
} else if (group === 'refs' && sub === 'compare') {
|
|
360
|
-
if (!flags.base || !flags.head) {
|
|
361
|
-
throw Object.assign(new Error('--base and --head required'), {
|
|
362
|
-
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--base and --head required'),
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
assertGitRef(flags.base, '--base');
|
|
366
|
-
assertGitRef(flags.head, '--head');
|
|
367
|
-
packet = forgePacket(
|
|
368
|
-
PACKET_TYPES.REF_COMPARE,
|
|
369
|
-
ctx,
|
|
370
|
-
await provider.refsCompare(ctx, flags.base, flags.head),
|
|
371
|
-
);
|
|
372
|
-
} else if (group === 'refs' && sub === 'inventory') {
|
|
373
|
-
if (typeof provider.refsInventory !== 'function') {
|
|
374
|
-
throw Object.assign(new Error('refs inventory not implemented for provider'), {
|
|
375
|
-
forgeError: forgeError(
|
|
376
|
-
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
377
|
-
'refs inventory not implemented for provider',
|
|
378
|
-
),
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
packet = forgeFactInventoryPacket(
|
|
382
|
-
FACT_INVENTORY_PACKET_TYPES.REF_INVENTORY,
|
|
383
|
-
ctx,
|
|
384
|
-
await provider.refsInventory(ctx),
|
|
385
|
-
);
|
|
386
|
-
} else if (group === 'cr' && sub === 'inventory') {
|
|
387
|
-
if (typeof provider.crInventory !== 'function') {
|
|
388
|
-
throw Object.assign(new Error('cr inventory not implemented for provider'), {
|
|
389
|
-
forgeError: forgeError(
|
|
390
|
-
ERROR_CODES.PROVIDER_UNSUPPORTED,
|
|
391
|
-
'cr inventory not implemented for provider',
|
|
392
|
-
),
|
|
393
|
-
});
|
|
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
|
-
}
|
|
414
|
-
packet = forgeFactInventoryPacket(
|
|
415
|
-
FACT_INVENTORY_PACKET_TYPES.CR_INVENTORY_SLICE,
|
|
416
|
-
ctx,
|
|
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,
|
|
528
|
-
}),
|
|
529
|
-
);
|
|
530
|
-
} else if (group === 'pr' && sub === 'view') {
|
|
531
|
-
const number = parsePositiveInt(flags.number, '--number');
|
|
532
|
-
if (number == null) {
|
|
533
|
-
throw Object.assign(new Error('--number required'), {
|
|
534
|
-
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for pr view'),
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
const body = await provider.prView(ctx, { number });
|
|
538
|
-
throwIfStaleHeadByNumber(
|
|
539
|
-
ctx,
|
|
540
|
-
PACKET_TYPES.PR_STATUS,
|
|
541
|
-
body,
|
|
542
|
-
body.head_ref,
|
|
543
|
-
body.head_sha,
|
|
544
|
-
);
|
|
545
|
-
packet = forgePacket(PACKET_TYPES.PR_STATUS, ctx, body);
|
|
546
|
-
} else if (group === 'pr' && sub === 'checks') {
|
|
547
|
-
const number = parsePositiveInt(flags.number, '--number');
|
|
548
|
-
if (number == null && !flags.ref) {
|
|
549
|
-
throw Object.assign(new Error('--number or --ref required'), {
|
|
550
|
-
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number or --ref required for pr checks'),
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
if (flags.ref) assertGitRef(flags.ref, '--ref');
|
|
554
|
-
if (number != null && !flags.ref) {
|
|
555
|
-
const view = await provider.prView(ctx, { number });
|
|
556
|
-
throwIfStaleHeadByNumber(
|
|
557
|
-
ctx,
|
|
558
|
-
PACKET_TYPES.PR_CHECKS,
|
|
559
|
-
{ head_sha: view.head_sha },
|
|
560
|
-
view.head_ref,
|
|
561
|
-
view.head_sha,
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
packet = forgePacket(
|
|
565
|
-
PACKET_TYPES.PR_CHECKS,
|
|
566
|
-
ctx,
|
|
567
|
-
await provider.prChecks(ctx, { number, ref: flags.ref }),
|
|
568
|
-
);
|
|
569
|
-
} else if (group === 'merge' && sub === 'plan') {
|
|
570
|
-
const number = parsePositiveInt(flags.number, '--number');
|
|
571
|
-
if (number == null) {
|
|
572
|
-
throw Object.assign(new Error('--number required'), {
|
|
573
|
-
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for merge plan'),
|
|
574
|
-
});
|
|
575
|
-
}
|
|
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));
|
|
619
|
-
} else if (group === 'sync' && sub === 'plan') {
|
|
620
|
-
const remote = flags.remote || ctx.config.remote;
|
|
621
|
-
assertGitRemote(remote, '--remote');
|
|
622
|
-
packet = forgePacket(
|
|
623
|
-
PACKET_TYPES.SYNC_PLAN,
|
|
624
|
-
ctx,
|
|
625
|
-
await provider.syncPlan(ctx, remote),
|
|
626
|
-
);
|
|
627
|
-
} else {
|
|
628
|
-
throw Object.assign(new Error(`Unknown command: ${positional.join(' ')}`), {
|
|
629
|
-
forgeError: forgeError(
|
|
630
|
-
ERROR_CODES.INVALID_ARGS,
|
|
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',
|
|
632
|
-
),
|
|
633
|
-
});
|
|
634
|
-
}
|
|
65
|
+
const packet = await dispatchForgeCommand({ group, sub, flags, positional, ctx, provider });
|
|
635
66
|
output(packet, asJson);
|
|
636
67
|
} catch (err) {
|
|
637
68
|
handleError(err, ctx, asJson);
|
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.6",
|
|
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.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"
|
|
36
36
|
}
|
|
37
37
|
}
|