@remogram/cli 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/bin/remogram.js +7 -0
  2. package/index.js +329 -0
  3. package/package.json +37 -0
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../index.js';
3
+
4
+ runCli(process.argv.slice(2)).catch((err) => {
5
+ console.error(err.message || String(err));
6
+ process.exit(1);
7
+ });
package/index.js ADDED
@@ -0,0 +1,329 @@
1
+ import {
2
+ loadConfig,
3
+ findConfigPath,
4
+ assertForgeReady,
5
+ gitRemoteUrl,
6
+ parseRemoteUrl,
7
+ trustedBaseUrl,
8
+ assertConfigMatchesRemote,
9
+ forgeContext,
10
+ forgePacket,
11
+ forgeErrorPacket,
12
+ unknownForgeContext,
13
+ PACKET_TYPES,
14
+ ERROR_CODES,
15
+ forgeError,
16
+ sanitizeField,
17
+ assertGitRef,
18
+ assertGitRemote,
19
+ } from '@remogram/core';
20
+ import { provider as giteaApi } from '@remogram/provider-gitea-api';
21
+ import { provider as githubApi } from '@remogram/provider-github-api';
22
+ import { provider as gitlabApi } from '@remogram/provider-gitlab-api';
23
+ import { provider as giteaTea } from '@remogram/provider-gitea-tea';
24
+ import { provider as githubGh } from '@remogram/provider-github-gh';
25
+
26
+ const PROVIDERS = {
27
+ 'gitea-api': giteaApi,
28
+ 'github-api': githubApi,
29
+ 'gitlab-api': gitlabApi,
30
+ 'gitea-tea': giteaTea,
31
+ 'github-gh': githubGh,
32
+ };
33
+
34
+ function parsePositiveInt(value, name) {
35
+ if (value == null) return undefined;
36
+ const n = Number(value);
37
+ if (!Number.isInteger(n) || n <= 0) {
38
+ throw Object.assign(new Error(`Invalid ${name}`), {
39
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, `${name} must be a positive integer`),
40
+ });
41
+ }
42
+ return n;
43
+ }
44
+
45
+ function output(packet, asJson) {
46
+ console.log(JSON.stringify(packet, null, asJson ? 2 : 0));
47
+ }
48
+
49
+ function handleError(err, ctx, asJson) {
50
+ const fe = err.forgeError || {
51
+ code: ERROR_CODES.API_ERROR,
52
+ message: err.message,
53
+ status: err.status,
54
+ };
55
+ const baseCtx = ctx || {
56
+ providerId: 'unknown',
57
+ remoteName: 'origin',
58
+ repoId: 'unknown/unknown',
59
+ };
60
+ output(forgeErrorPacket(baseCtx, fe), asJson);
61
+ process.exitCode = 1;
62
+ }
63
+
64
+ function doctorCheck(name, status, message, details = null) {
65
+ return {
66
+ name,
67
+ status,
68
+ message: sanitizeField(message),
69
+ ...(details ? { details } : {}),
70
+ };
71
+ }
72
+
73
+ function doctorSummary(checks) {
74
+ if (checks.some((check) => check.status === 'fail')) return 'fail';
75
+ if (checks.some((check) => check.status === 'warn')) return 'warn';
76
+ return 'pass';
77
+ }
78
+
79
+ function contextFromConfig(config, cwd, parsed = null) {
80
+ return {
81
+ providerId: config.provider,
82
+ remoteName: config.remote,
83
+ repoId: parsed ? `${parsed.owner}/${parsed.repo}` : `${config.owner}/${config.repo}`,
84
+ config,
85
+ cwd,
86
+ parsed,
87
+ };
88
+ }
89
+
90
+ async function buildDoctorPacket(cwd, providers) {
91
+ const checks = [];
92
+ const configPath = findConfigPath(cwd);
93
+ let loaded = null;
94
+ let config = null;
95
+ let parsed = null;
96
+ let ctx = unknownForgeContext();
97
+ let providerCapabilities = null;
98
+
99
+ if (!configPath) {
100
+ checks.push(doctorCheck('config', 'fail', 'No .remogram.json found'));
101
+ return forgePacket(PACKET_TYPES.PROVIDER_DOCTOR, ctx, {
102
+ summary: doctorSummary(checks),
103
+ checks,
104
+ provider_capabilities: null,
105
+ });
106
+ }
107
+
108
+ try {
109
+ loaded = loadConfig(cwd);
110
+ config = loaded.config;
111
+ ctx = contextFromConfig(config, loaded.cwd);
112
+ checks.push(doctorCheck('config', 'pass', '.remogram.json is present and valid'));
113
+ } catch (err) {
114
+ checks.push(doctorCheck('config', 'fail', err.forgeError?.message || err.message));
115
+ return forgePacket(PACKET_TYPES.PROVIDER_DOCTOR, ctx, {
116
+ summary: doctorSummary(checks),
117
+ checks,
118
+ provider_capabilities: null,
119
+ });
120
+ }
121
+
122
+ const provider = providers[config.provider];
123
+ if (!provider) {
124
+ checks.push(doctorCheck('provider', 'fail', `Unsupported provider: ${config.provider}`));
125
+ } else {
126
+ checks.push(doctorCheck('provider', 'pass', `${config.provider} is registered`));
127
+ if (typeof provider.providerCapabilities === 'function') {
128
+ providerCapabilities = await provider.providerCapabilities(ctx);
129
+ checks.push(doctorCheck('capabilities', 'pass', 'Provider capabilities are available'));
130
+ } else {
131
+ checks.push(doctorCheck('capabilities', 'fail', 'Provider capabilities are not implemented'));
132
+ }
133
+ }
134
+
135
+ let remoteUrl = null;
136
+ try {
137
+ assertGitRemote(config.remote, 'config.remote');
138
+ remoteUrl = gitRemoteUrl(loaded.cwd, config.remote);
139
+ parsed = parseRemoteUrl(remoteUrl);
140
+ if (!parsed) {
141
+ checks.push(doctorCheck('remote', 'fail', 'Could not parse git remote URL'));
142
+ } else {
143
+ ctx = contextFromConfig(config, loaded.cwd, parsed);
144
+ checks.push(doctorCheck('remote', 'pass', 'Git remote URL parses successfully', {
145
+ host: sanitizeField(parsed.host),
146
+ owner: sanitizeField(parsed.owner),
147
+ repo: sanitizeField(parsed.repo),
148
+ }));
149
+ }
150
+ } catch (err) {
151
+ checks.push(doctorCheck('remote', 'fail', err.forgeError?.message || err.message));
152
+ }
153
+
154
+ if (parsed) {
155
+ try {
156
+ assertConfigMatchesRemote(config, parsed);
157
+ checks.push(doctorCheck('repo_match', 'pass', 'Config owner/repo matches git remote'));
158
+ } catch (err) {
159
+ checks.push(doctorCheck('repo_match', 'fail', err.forgeError?.message || err.message));
160
+ }
161
+
162
+ if (config.baseUrl && !trustedBaseUrl(config, parsed.host)) {
163
+ checks.push(
164
+ doctorCheck('host_binding', 'fail', `baseUrl host does not match remote host ${parsed.host}`),
165
+ );
166
+ } else {
167
+ checks.push(doctorCheck('host_binding', 'pass', 'Configured host binding is trusted'));
168
+ }
169
+ }
170
+
171
+ if (providerCapabilities) {
172
+ const envNames = providerCapabilities.auth_envs || [];
173
+ const presentEnv = envNames.find((name) => Boolean(process.env[name])) || null;
174
+ checks.push(
175
+ doctorCheck(
176
+ 'auth',
177
+ presentEnv ? 'pass' : 'warn',
178
+ presentEnv ? `${presentEnv} is present` : 'No provider auth environment variable is set',
179
+ { env_names: envNames, present_env: presentEnv },
180
+ ),
181
+ );
182
+
183
+ if (!providerCapabilities.check_sources?.length) {
184
+ checks.push(doctorCheck('checks', 'warn', 'Provider does not report forge check sources'));
185
+ } else {
186
+ checks.push(doctorCheck('checks', 'pass', 'Provider reports forge check sources', {
187
+ sources: providerCapabilities.check_sources,
188
+ }));
189
+ }
190
+ }
191
+
192
+ checks.push(doctorCheck('api_reachability', 'skipped', 'Live API reachability is not checked by default'));
193
+
194
+ return forgePacket(PACKET_TYPES.PROVIDER_DOCTOR, ctx, {
195
+ summary: doctorSummary(checks),
196
+ checks,
197
+ provider_capabilities: providerCapabilities,
198
+ });
199
+ }
200
+
201
+ export async function runCli(argv, options = {}) {
202
+ const cwd = options.cwd ?? process.env.REMOGRAM_CWD ?? process.cwd();
203
+ const providers = options.providers ?? PROVIDERS;
204
+ const positional = [];
205
+ let asJson = false;
206
+ const flags = {};
207
+
208
+ for (let i = 0; i < argv.length; i += 1) {
209
+ const arg = argv[i];
210
+ if (arg === '--json') asJson = true;
211
+ else if (arg.startsWith('--')) {
212
+ const key = arg.slice(2).replace(/-/g, '_');
213
+ const next = argv[i + 1];
214
+ if (next != null && !next.startsWith('--')) {
215
+ flags[key] = next;
216
+ i += 1;
217
+ } else {
218
+ flags[key] = true;
219
+ }
220
+ } else {
221
+ positional.push(arg);
222
+ }
223
+ }
224
+
225
+ const [group, sub] = positional;
226
+
227
+ if (group === 'doctor' && sub == null) {
228
+ output(await buildDoctorPacket(cwd, providers), asJson);
229
+ return;
230
+ }
231
+
232
+ let ctx;
233
+ try {
234
+ const loaded = assertForgeReady(loadConfig(cwd));
235
+ ctx = forgeContext(loaded);
236
+ } catch (err) {
237
+ handleError(err, null, asJson);
238
+ return;
239
+ }
240
+
241
+ const provider = providers[ctx.config.provider];
242
+ if (!provider) {
243
+ handleError(
244
+ {
245
+ message: `Unsupported provider: ${ctx.config.provider}`,
246
+ forgeError: forgeError(
247
+ ERROR_CODES.PROVIDER_UNSUPPORTED,
248
+ `Unsupported provider: ${ctx.config.provider}`,
249
+ ),
250
+ },
251
+ ctx,
252
+ asJson,
253
+ );
254
+ return;
255
+ }
256
+
257
+ try {
258
+ let packet;
259
+ if (group === 'provider' && sub === 'capabilities') {
260
+ packet = forgePacket(
261
+ PACKET_TYPES.PROVIDER_CAPABILITIES,
262
+ ctx,
263
+ await provider.providerCapabilities(ctx),
264
+ );
265
+ } else if (group === 'repo' && sub === 'status') {
266
+ packet = forgePacket(PACKET_TYPES.REPO_STATUS, ctx, await provider.repoStatus(ctx));
267
+ } else if (group === 'refs' && sub === 'compare') {
268
+ if (!flags.base || !flags.head) {
269
+ throw Object.assign(new Error('--base and --head required'), {
270
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--base and --head required'),
271
+ });
272
+ }
273
+ assertGitRef(flags.base, '--base');
274
+ assertGitRef(flags.head, '--head');
275
+ packet = forgePacket(
276
+ PACKET_TYPES.REF_COMPARE,
277
+ ctx,
278
+ await provider.refsCompare(ctx, flags.base, flags.head),
279
+ );
280
+ } else if (group === 'pr' && sub === 'view') {
281
+ const number = parsePositiveInt(flags.number, '--number');
282
+ if (number == null) {
283
+ throw Object.assign(new Error('--number required'), {
284
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for pr view'),
285
+ });
286
+ }
287
+ packet = forgePacket(PACKET_TYPES.PR_STATUS, ctx, await provider.prView(ctx, { number }));
288
+ } else if (group === 'pr' && sub === 'checks') {
289
+ const number = parsePositiveInt(flags.number, '--number');
290
+ if (number == null && !flags.ref) {
291
+ throw Object.assign(new Error('--number or --ref required'), {
292
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number or --ref required for pr checks'),
293
+ });
294
+ }
295
+ if (flags.ref) assertGitRef(flags.ref, '--ref');
296
+ packet = forgePacket(
297
+ PACKET_TYPES.PR_CHECKS,
298
+ ctx,
299
+ await provider.prChecks(ctx, { number, ref: flags.ref }),
300
+ );
301
+ } else if (group === 'merge' && sub === 'plan') {
302
+ const number = parsePositiveInt(flags.number, '--number');
303
+ if (number == null) {
304
+ throw Object.assign(new Error('--number required'), {
305
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--number required for merge plan'),
306
+ });
307
+ }
308
+ packet = forgePacket(PACKET_TYPES.MERGE_PLAN, ctx, await provider.mergePlan(ctx, { number }));
309
+ } else if (group === 'sync' && sub === 'plan') {
310
+ const remote = flags.remote || ctx.config.remote;
311
+ assertGitRemote(remote, '--remote');
312
+ packet = forgePacket(
313
+ PACKET_TYPES.SYNC_PLAN,
314
+ ctx,
315
+ await provider.syncPlan(ctx, remote),
316
+ );
317
+ } else {
318
+ throw Object.assign(new Error(`Unknown command: ${positional.join(' ')}`), {
319
+ forgeError: forgeError(
320
+ ERROR_CODES.INVALID_ARGS,
321
+ 'Unknown command. Try: provider capabilities, repo status, refs compare, pr view, pr checks, merge plan, sync plan',
322
+ ),
323
+ });
324
+ }
325
+ output(packet, asJson);
326
+ } catch (err) {
327
+ handleError(err, ctx, asJson);
328
+ }
329
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@remogram/cli",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Remogram forge boundary CLI",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/attebury/remogram.git",
10
+ "directory": "packages/remogram-cli"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "*.js",
17
+ "bin/"
18
+ ],
19
+ "bin": {
20
+ "remogram": "bin/remogram.js"
21
+ },
22
+ "exports": {
23
+ ".": "./index.js",
24
+ "./bin/remogram.js": "./bin/remogram.js"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "dependencies": {
30
+ "@remogram/core": "0.1.0-beta.0",
31
+ "@remogram/provider-gitea-api": "0.1.0-beta.0",
32
+ "@remogram/provider-github-api": "0.1.0-beta.0",
33
+ "@remogram/provider-gitlab-api": "0.1.0-beta.0",
34
+ "@remogram/provider-gitea-tea": "0.1.0-beta.0",
35
+ "@remogram/provider-github-gh": "0.1.0-beta.0"
36
+ }
37
+ }