@remogram/cli 0.1.0-beta.5 → 0.1.0-beta.8

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-doctor.js ADDED
@@ -0,0 +1,215 @@
1
+ import {
2
+ loadConfig,
3
+ findConfigPath,
4
+ gitRemoteUrl,
5
+ parseRemoteUrl,
6
+ trustedBaseUrl,
7
+ normalizedForgeOrigin,
8
+ assertConfigMatchesRemote,
9
+ forgePacket,
10
+ unknownForgeContext,
11
+ PACKET_TYPES,
12
+ ERROR_CODES,
13
+ forgeError,
14
+ sanitizeField,
15
+ assertGitRemote,
16
+ getEffectiveIngestMaxBytes,
17
+ FORGE_INGEST_MAX_BYTES_ENV,
18
+ MAX_FORGE_INGEST_ENV_BYTES,
19
+ } from '@remogram/core';
20
+ import { contextFromConfig } from './cli-io.js';
21
+
22
+ export function doctorCheck(name, status, message, details = null) {
23
+ return {
24
+ name,
25
+ status,
26
+ message: sanitizeField(message),
27
+ ...(details ? { details } : {}),
28
+ };
29
+ }
30
+
31
+ export function doctorSummary(checks) {
32
+ if (checks.some((check) => check.status === 'fail')) return 'fail';
33
+ if (checks.some((check) => check.status === 'warn')) return 'warn';
34
+ return 'pass';
35
+ }
36
+
37
+ function finalizeDoctorPacket(ctx, checks, providerCapabilities) {
38
+ const summary = doctorSummary(checks);
39
+ const error =
40
+ summary === 'fail'
41
+ ? forgeError(ERROR_CODES.CONFIG_INVALID, 'Doctor checks failed')
42
+ : null;
43
+ return forgePacket(
44
+ PACKET_TYPES.PROVIDER_DOCTOR,
45
+ ctx,
46
+ {
47
+ summary,
48
+ checks,
49
+ provider_capabilities: providerCapabilities,
50
+ },
51
+ error,
52
+ );
53
+ }
54
+
55
+ export async function buildDoctorPacket(cwd, providers) {
56
+ const checks = [];
57
+ const configPath = findConfigPath(cwd);
58
+ let loaded = null;
59
+ let config = null;
60
+ let parsed = null;
61
+ let ctx = unknownForgeContext();
62
+ let providerCapabilities = null;
63
+
64
+ if (!configPath) {
65
+ checks.push(doctorCheck('config', 'fail', 'No .remogram.json found'));
66
+ return finalizeDoctorPacket(ctx, checks, null);
67
+ }
68
+
69
+ try {
70
+ loaded = loadConfig(cwd);
71
+ config = loaded.config;
72
+ ctx = contextFromConfig(config, loaded.cwd);
73
+ checks.push(doctorCheck('config', 'pass', '.remogram.json is present and valid'));
74
+ } catch (err) {
75
+ checks.push(doctorCheck('config', 'fail', err.forgeError?.message || err.message));
76
+ return finalizeDoctorPacket(ctx, checks, null);
77
+ }
78
+
79
+ const provider = providers[config.provider];
80
+ if (!provider) {
81
+ checks.push(doctorCheck('provider', 'fail', `Unsupported provider: ${config.provider}`));
82
+ } else {
83
+ if (typeof provider.providerCapabilities === 'function') {
84
+ providerCapabilities = await provider.providerCapabilities(ctx);
85
+ const stubProvider =
86
+ providerCapabilities.commands?.length > 0
87
+ && providerCapabilities.commands.every((command) => command.implemented === false);
88
+ checks.push(
89
+ doctorCheck(
90
+ 'provider',
91
+ stubProvider ? 'warn' : 'pass',
92
+ stubProvider
93
+ ? `${config.provider} is not fully supported in v1; use an *-api provider`
94
+ : `${config.provider} is registered`,
95
+ ),
96
+ );
97
+ checks.push(doctorCheck('capabilities', 'pass', 'Provider capabilities are available'));
98
+ } else {
99
+ checks.push(doctorCheck('provider', 'pass', `${config.provider} is registered`));
100
+ checks.push(doctorCheck('capabilities', 'fail', 'Provider capabilities are not implemented'));
101
+ }
102
+ }
103
+
104
+ try {
105
+ assertGitRemote(config.remote, 'config.remote');
106
+ const remoteUrl = gitRemoteUrl(loaded.cwd, config.remote);
107
+ parsed = parseRemoteUrl(remoteUrl);
108
+ if (!parsed) {
109
+ checks.push(doctorCheck('remote', 'fail', 'Could not parse git remote URL'));
110
+ } else {
111
+ ctx = contextFromConfig(config, loaded.cwd, parsed);
112
+ checks.push(doctorCheck('remote', 'pass', 'Git remote URL parses successfully', {
113
+ host: sanitizeField(parsed.host),
114
+ owner: sanitizeField(parsed.owner),
115
+ repo: sanitizeField(parsed.repo),
116
+ }));
117
+ }
118
+ } catch (err) {
119
+ checks.push(doctorCheck('remote', 'fail', err.forgeError?.message || err.message));
120
+ }
121
+
122
+ if (parsed) {
123
+ try {
124
+ assertConfigMatchesRemote(config, parsed);
125
+ checks.push(doctorCheck('repo_match', 'pass', 'Config owner/repo matches git remote'));
126
+ } catch (err) {
127
+ checks.push(doctorCheck('repo_match', 'fail', err.forgeError?.message || err.message));
128
+ }
129
+
130
+ if (config.baseUrl && !trustedBaseUrl(config, parsed.host)) {
131
+ checks.push(
132
+ doctorCheck('host_binding', 'fail', `baseUrl host does not match remote host ${parsed.host}`),
133
+ );
134
+ } else {
135
+ checks.push(doctorCheck('host_binding', 'pass', 'Configured host binding is trusted'));
136
+ if (config.baseUrl) {
137
+ ctx = { ...ctx, baseUrl: normalizedForgeOrigin(config) };
138
+ }
139
+ }
140
+ }
141
+
142
+ if (providerCapabilities) {
143
+ const envNames = providerCapabilities.auth_envs || [];
144
+ const presentEnv = envNames.find((name) => Boolean(process.env[name])) || null;
145
+ checks.push(
146
+ doctorCheck(
147
+ 'auth',
148
+ presentEnv ? 'pass' : 'warn',
149
+ presentEnv ? `${presentEnv} is present` : 'No provider auth environment variable is set',
150
+ { env_names: envNames, present_env: presentEnv },
151
+ ),
152
+ );
153
+
154
+ if (providerCapabilities.write_support) {
155
+ const providerWrites = (providerCapabilities.write_commands || []).filter(Boolean);
156
+ const configuredWrites = Array.isArray(config?.write_commands) ? config.write_commands : [];
157
+ const missing = providerWrites.filter((name) => !configuredWrites.includes(name));
158
+ checks.push(
159
+ doctorCheck(
160
+ 'write_config',
161
+ missing.length ? 'warn' : 'pass',
162
+ missing.length
163
+ ? `Provider supports write commands but .remogram.json write_commands omits: ${missing.join(', ')}. Add ids for Remogram CLI/MCP writes, or use forge/CI tooling for those actions outside Remogram.`
164
+ : 'Consumer write_commands matches provider write surface',
165
+ { provider_write_commands: providerWrites, configured_write_commands: configuredWrites },
166
+ ),
167
+ );
168
+ }
169
+
170
+ if (!providerCapabilities.check_sources?.length) {
171
+ checks.push(doctorCheck('checks', 'warn', 'Provider does not report forge check sources'));
172
+ } else {
173
+ checks.push(doctorCheck('checks', 'pass', 'Provider reports forge check sources', {
174
+ sources: providerCapabilities.check_sources,
175
+ }));
176
+ }
177
+ }
178
+
179
+ const { bytes: ingestCapBytes, envOverride: ingestEnvOverride, invalidEnv: ingestInvalidEnv, clamped: ingestClamped } =
180
+ getEffectiveIngestMaxBytes();
181
+ if (ingestInvalidEnv) {
182
+ checks.push(
183
+ doctorCheck(
184
+ 'forge_ingest_cap',
185
+ 'warn',
186
+ `${FORGE_INGEST_MAX_BYTES_ENV} is invalid; using default 8192 bytes`,
187
+ { effective_bytes: ingestCapBytes, env_override: false },
188
+ ),
189
+ );
190
+ } else if (ingestEnvOverride) {
191
+ checks.push(
192
+ doctorCheck(
193
+ 'forge_ingest_cap',
194
+ 'warn',
195
+ ingestClamped
196
+ ? `${FORGE_INGEST_MAX_BYTES_ENV} exceeds max ${MAX_FORGE_INGEST_ENV_BYTES}; clamped — agent-safe guarantee is weakened`
197
+ : `${FORGE_INGEST_MAX_BYTES_ENV} overrides default ingest cap; agent-safe guarantee is weakened`,
198
+ { effective_bytes: ingestCapBytes, env_override: true, ...(ingestClamped ? { clamped: true } : {}) },
199
+ ),
200
+ );
201
+ } else {
202
+ checks.push(
203
+ doctorCheck(
204
+ 'forge_ingest_cap',
205
+ 'pass',
206
+ 'Forge HTTP ingest cap is default 8192 bytes',
207
+ { effective_bytes: ingestCapBytes, env_override: false },
208
+ ),
209
+ );
210
+ }
211
+
212
+ checks.push(doctorCheck('api_reachability', 'skipped', 'Live API reachability is not checked by default'));
213
+
214
+ return finalizeDoctorPacket(ctx, checks, providerCapabilities);
215
+ }
package/cli-io.js ADDED
@@ -0,0 +1,51 @@
1
+ import {
2
+ forgePacket,
3
+ forgeErrorPacket,
4
+ forgeError,
5
+ ERROR_CODES,
6
+ normalizedForgeOrigin,
7
+ trustedBaseUrl,
8
+ } from '@remogram/core';
9
+
10
+ export function output(packet, asJson) {
11
+ console.log(JSON.stringify(packet, null, asJson ? 2 : 0));
12
+ }
13
+
14
+ export function handleError(err, ctx, asJson) {
15
+ const fe =
16
+ err.forgeError
17
+ || (err.invalidArgs
18
+ ? forgeError(ERROR_CODES.INVALID_ARGS, err.invalidArgs)
19
+ : {
20
+ code: ERROR_CODES.API_ERROR,
21
+ message: err.message,
22
+ status: err.status,
23
+ });
24
+ const baseCtx = ctx || {
25
+ providerId: 'unknown',
26
+ remoteName: 'origin',
27
+ repoId: 'unknown/unknown',
28
+ };
29
+ if (err.staleHeadPacket) {
30
+ output(forgePacket(err.staleHeadPacket.type, baseCtx, err.staleHeadPacket.body, fe), asJson);
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ output(forgeErrorPacket(baseCtx, fe), asJson);
35
+ process.exitCode = 1;
36
+ }
37
+
38
+ export function contextFromConfig(config, cwd, parsed = null) {
39
+ const ctx = {
40
+ providerId: config.provider,
41
+ remoteName: config.remote,
42
+ repoId: parsed ? `${parsed.owner}/${parsed.repo}` : `${config.owner}/${config.repo}`,
43
+ config,
44
+ cwd,
45
+ parsed,
46
+ };
47
+ if (config.baseUrl && (!parsed || trustedBaseUrl(config, parsed.host))) {
48
+ ctx.baseUrl = normalizedForgeOrigin(config);
49
+ }
50
+ return ctx;
51
+ }