@moda-ai/cli 1.22.0 → 1.22.2
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/dist/cli-g8qs2tj9.js +9777 -0
- package/dist/cli-pwbvtes8.js +2312 -0
- package/dist/cli-qszcpda3.js +284 -0
- package/dist/cli.js +4775 -1159
- package/dist/index-6h0nc1r5.js +2765 -0
- package/dist/provision-j5gq8cvx.js +72 -0
- package/package.json +3 -2
- package/skills/integration/index.json +2 -2
- package/dist/agent-schemas.d.ts +0 -3
- package/dist/agent-schemas.js +0 -358
- package/dist/agent.d.ts +0 -5
- package/dist/agent.js +0 -477
- package/dist/analyst-tui.d.ts +0 -81
- package/dist/analyst-tui.js +0 -314
- package/dist/api-client.d.ts +0 -46
- package/dist/api-client.js +0 -120
- package/dist/ask-stream.d.ts +0 -146
- package/dist/ask-stream.js +0 -473
- package/dist/auth-storage.d.ts +0 -35
- package/dist/auth-storage.js +0 -159
- package/dist/auth.d.ts +0 -50
- package/dist/auth.js +0 -694
- package/dist/cli-session.d.ts +0 -26
- package/dist/cli-session.js +0 -169
- package/dist/cli.d.ts +0 -44
- package/dist/command-context.d.ts +0 -62
- package/dist/command-context.js +0 -57
- package/dist/command-registry.d.ts +0 -22
- package/dist/command-registry.js +0 -15
- package/dist/config.d.ts +0 -60
- package/dist/config.js +0 -110
- package/dist/doctor.d.ts +0 -79
- package/dist/doctor.js +0 -1182
- package/dist/errors.d.ts +0 -50
- package/dist/errors.js +0 -64
- package/dist/exit-funnel.d.ts +0 -14
- package/dist/exit-funnel.js +0 -53
- package/dist/harness-artifact-bodies.d.ts +0 -28
- package/dist/harness-artifact-bodies.js +0 -276
- package/dist/harness-candidates.d.ts +0 -77
- package/dist/harness-candidates.js +0 -482
- package/dist/harness-remote.d.ts +0 -135
- package/dist/harness-remote.js +0 -540
- package/dist/harness-report.d.ts +0 -145
- package/dist/harness-report.js +0 -1579
- package/dist/harness-sync-limits.d.ts +0 -8
- package/dist/harness-sync-limits.js +0 -21
- package/dist/harness.d.ts +0 -463
- package/dist/harness.js +0 -4009
- package/dist/init/index.d.ts +0 -78
- package/dist/init/index.js +0 -1442
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -27
- package/dist/init/progress-board.d.ts +0 -80
- package/dist/init/progress-board.js +0 -225
- package/dist/init/rules.d.ts +0 -6
- package/dist/init/rules.js +0 -171
- package/dist/init/sdk-catalog.d.ts +0 -10
- package/dist/init/sdk-catalog.js +0 -80
- package/dist/init/sdk-guide.d.ts +0 -19
- package/dist/init/sdk-guide.js +0 -185
- package/dist/init/sdk-integration.d.ts +0 -53
- package/dist/init/sdk-integration.js +0 -835
- package/dist/init/sdk-protocol.d.ts +0 -135
- package/dist/init/sdk-protocol.js +0 -152
- package/dist/init/sdk-verification.d.ts +0 -27
- package/dist/init/sdk-verification.js +0 -123
- package/dist/init/tenant.d.ts +0 -67
- package/dist/init/tenant.js +0 -421
- package/dist/intelligence-schema.d.ts +0 -349
- package/dist/intelligence-schema.js +0 -174
- package/dist/keys.d.ts +0 -47
- package/dist/keys.js +0 -136
- package/dist/manifest.d.ts +0 -5
- package/dist/manifest.js +0 -114
- package/dist/markdown-terminal.d.ts +0 -49
- package/dist/markdown-terminal.js +0 -212
- package/dist/output.d.ts +0 -191
- package/dist/output.js +0 -624
- package/dist/production-intelligence.d.ts +0 -175
- package/dist/production-intelligence.js +0 -1430
- package/dist/profile-commands.d.ts +0 -10
- package/dist/profile-commands.js +0 -342
- package/dist/profiles.d.ts +0 -112
- package/dist/profiles.js +0 -573
- package/dist/prompts-ab.d.ts +0 -71
- package/dist/prompts-ab.js +0 -313
- package/dist/prompts-propose.d.ts +0 -15
- package/dist/prompts-propose.js +0 -212
- package/dist/prompts.d.ts +0 -58
- package/dist/prompts.js +0 -418
- package/dist/provision.d.ts +0 -28
- package/dist/provision.js +0 -104
- package/dist/redact.d.ts +0 -2
- package/dist/redact.js +0 -90
- package/dist/schemas.d.ts +0 -225
- package/dist/schemas.js +0 -126
- package/dist/skills-bundled.d.ts +0 -35
- package/dist/skills-bundled.js +0 -157
- package/dist/skills-manifest.d.ts +0 -33
- package/dist/skills-manifest.js +0 -244
- package/dist/skills.d.ts +0 -57
- package/dist/skills.js +0 -777
- package/dist/sync.d.ts +0 -5
- package/dist/sync.js +0 -36
- package/dist/telemetry.d.ts +0 -56
- package/dist/telemetry.js +0 -167
- package/dist/transforms.d.ts +0 -25
- package/dist/transforms.js +0 -102
- package/dist/tui.d.ts +0 -48
- package/dist/tui.js +0 -393
- package/dist/update-check.d.ts +0 -31
- package/dist/update-check.js +0 -141
- package/dist/workspace.d.ts +0 -12
- package/dist/workspace.js +0 -1294
package/dist/doctor.js
DELETED
|
@@ -1,1182 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { callDataAPI } from './api-client.js';
|
|
6
|
-
import { isAuthSessionValid, loadAuthSession } from './auth.js';
|
|
7
|
-
import { resolveApiKey, resolveIngestUrl, resolveModaBaseUrl, resolveTenantId } from './config.js';
|
|
8
|
-
import { readHarnessSyncState, scanHarness } from './harness.js';
|
|
9
|
-
import { HARNESS_REPORT_APPROVAL_PATH, HARNESS_REPORT_APPROVAL_SCHEMA_VERSION, HARNESS_REPORT_PATH, hashHarnessReport, readHarnessReport, validateHarnessReport, } from './harness-report.js';
|
|
10
|
-
import { ensureSecretFilePermissions, getActiveProfile, getModaPaths, isLoopbackBaseUrl, loadProfileConfig, resolveSecretReference, secretFilePathFromRef, } from './profiles.js';
|
|
11
|
-
import { renderDoctorHuman, renderStatusHuman } from './tui.js';
|
|
12
|
-
export async function runDoctorCommand(context) {
|
|
13
|
-
const report = await buildDoctorReport(context, {
|
|
14
|
-
offline: context.flags.offline === 'true',
|
|
15
|
-
});
|
|
16
|
-
if (context.outputMode === 'human') {
|
|
17
|
-
if (!context.quiet)
|
|
18
|
-
console.log(renderDoctorHuman(report));
|
|
19
|
-
return { exitCode: 0 };
|
|
20
|
-
}
|
|
21
|
-
context.output.writeData(report, {
|
|
22
|
-
status: report.summary.fail > 0 ? 'error' : 'success',
|
|
23
|
-
...doctorAgentWriteOptions(report, 'doctor'),
|
|
24
|
-
});
|
|
25
|
-
return { exitCode: 0 };
|
|
26
|
-
}
|
|
27
|
-
export async function runStatusCommand(context) {
|
|
28
|
-
const report = await buildDoctorReport(context, {
|
|
29
|
-
offline: context.flags.offline === 'true' || context.flags.online !== 'true',
|
|
30
|
-
});
|
|
31
|
-
const topBroken = firstBrokenCheck(report.checks);
|
|
32
|
-
const statusPayload = {
|
|
33
|
-
schemaVersion: 'status.v0.1',
|
|
34
|
-
generatedAt: report.generatedAt,
|
|
35
|
-
status: report.summary.status,
|
|
36
|
-
activeProfile: report.profile.active,
|
|
37
|
-
auth: {
|
|
38
|
-
apiKey: resolveApiKey() ? 'configured' : 'missing',
|
|
39
|
-
},
|
|
40
|
-
workspace: {
|
|
41
|
-
tenantId: resolveTenantId() ?? null,
|
|
42
|
-
baseUrl: resolveModaBaseUrl(),
|
|
43
|
-
ingestUrl: resolveIngestUrl(),
|
|
44
|
-
},
|
|
45
|
-
dataFlow: checkStatus(report.checks, 'cloud_data_flow'),
|
|
46
|
-
harness: checkStatus(report.checks, 'harness_manifest_current'),
|
|
47
|
-
harnessSync: checkStatus(report.checks, 'harness_sync_state_present'),
|
|
48
|
-
harnessReport: checkStatus(report.checks, 'harness_report_approved_synced'),
|
|
49
|
-
shadowSpark: checkStatus(report.checks, 'shadow_spark_workspace_health'),
|
|
50
|
-
prompts: checkStatus(report.checks, 'prompts_discovered'),
|
|
51
|
-
skills: checkStatus(report.checks, 'skills_discovered'),
|
|
52
|
-
topBrokenCheck: topBroken
|
|
53
|
-
? {
|
|
54
|
-
id: topBroken.id,
|
|
55
|
-
status: topBroken.status,
|
|
56
|
-
severity: topBroken.severity,
|
|
57
|
-
nextCommand: topBroken.nextCommand,
|
|
58
|
-
}
|
|
59
|
-
: null,
|
|
60
|
-
nextCommands: report.nextCommands.slice(0, 3),
|
|
61
|
-
};
|
|
62
|
-
if (context.outputMode === 'human') {
|
|
63
|
-
if (!context.quiet)
|
|
64
|
-
console.log(renderStatusHuman(statusPayload));
|
|
65
|
-
return { exitCode: 0 };
|
|
66
|
-
}
|
|
67
|
-
context.output.writeData(statusPayload, {
|
|
68
|
-
status: report.summary.fail > 0 ? 'error' : 'success',
|
|
69
|
-
...doctorAgentWriteOptions(report, 'status'),
|
|
70
|
-
});
|
|
71
|
-
return { exitCode: 0 };
|
|
72
|
-
}
|
|
73
|
-
export async function buildDoctorReport(context, options) {
|
|
74
|
-
const checks = [];
|
|
75
|
-
const profileConfig = loadProfileConfig({ env: context.env });
|
|
76
|
-
const activeProfile = getActiveProfile({ profile: context.profile, env: context.env });
|
|
77
|
-
const paths = getModaPaths({ env: context.env });
|
|
78
|
-
const profileOptions = { profile: context.profile, env: context.env };
|
|
79
|
-
const apiKey = resolveApiKey(profileOptions);
|
|
80
|
-
const tenantId = resolveTenantId(profileOptions);
|
|
81
|
-
const baseUrl = resolveModaBaseUrl(profileOptions);
|
|
82
|
-
const ingestUrl = resolveIngestUrl(profileOptions);
|
|
83
|
-
checks.push({
|
|
84
|
-
id: 'active_profile_resolves',
|
|
85
|
-
group: 'config',
|
|
86
|
-
title: 'Active profile resolves',
|
|
87
|
-
status: activeProfile ? 'pass' : 'fail',
|
|
88
|
-
severity: activeProfile ? 'info' : 'critical',
|
|
89
|
-
evidence: [
|
|
90
|
-
{ label: 'active_profile', value: profileConfig.active_profile },
|
|
91
|
-
{ label: 'environment', value: activeProfile.environment },
|
|
92
|
-
],
|
|
93
|
-
remediation: activeProfile ? undefined : 'Create or select a Moda profile.',
|
|
94
|
-
nextCommand: activeProfile ? undefined : 'moda profiles list',
|
|
95
|
-
});
|
|
96
|
-
checks.push({
|
|
97
|
-
id: 'api_key_resolves',
|
|
98
|
-
group: 'config',
|
|
99
|
-
title: 'API key resolves',
|
|
100
|
-
status: apiKey ? 'pass' : 'fail',
|
|
101
|
-
severity: apiKey ? 'info' : 'critical',
|
|
102
|
-
evidence: [{ label: 'source', value: apiKey ? 'environment/profile/config' : 'none' }],
|
|
103
|
-
remediation: apiKey ? undefined : 'Authenticate and create a tenant API key.',
|
|
104
|
-
nextCommand: apiKey ? undefined : 'moda init',
|
|
105
|
-
});
|
|
106
|
-
checks.push({
|
|
107
|
-
id: 'tenant_resolves',
|
|
108
|
-
group: 'config',
|
|
109
|
-
title: 'Tenant/workspace resolves',
|
|
110
|
-
status: tenantId ? 'pass' : 'warning',
|
|
111
|
-
severity: tenantId ? 'info' : 'warning',
|
|
112
|
-
evidence: [{ label: 'tenant_id', value: tenantId ? 'configured' : 'missing' }],
|
|
113
|
-
remediation: tenantId ? undefined : 'Select a tenant during init or set MODA_TENANT_ID.',
|
|
114
|
-
nextCommand: tenantId ? undefined : 'moda init',
|
|
115
|
-
});
|
|
116
|
-
checks.push(authTokenCheck(context, Boolean(apiKey)));
|
|
117
|
-
checks.push(configPermissionCheck(paths.legacyConfigPath));
|
|
118
|
-
if (activeProfile.api_key_ref) {
|
|
119
|
-
const secretPath = secretFilePathFromRef(activeProfile.api_key_ref);
|
|
120
|
-
if (secretPath)
|
|
121
|
-
checks.push(secretPermissionCheck(secretPath));
|
|
122
|
-
checks.push({
|
|
123
|
-
id: 'profile_api_key_ref_resolves',
|
|
124
|
-
group: 'config',
|
|
125
|
-
title: 'Profile API key reference resolves',
|
|
126
|
-
status: resolveSecretReference(activeProfile.api_key_ref) ? 'pass' : 'warning',
|
|
127
|
-
severity: 'warning',
|
|
128
|
-
evidence: [{ label: 'api_key_ref', value: activeProfile.api_key_ref }],
|
|
129
|
-
remediation: 'Create or update the profile API key reference.',
|
|
130
|
-
nextCommand: 'moda profiles create',
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
checks.push(urlCheck('base_url_valid', 'config', 'Base URL is valid', baseUrl));
|
|
134
|
-
checks.push(urlCheck('ingest_url_valid', 'config', 'Ingest URL is valid', ingestUrl));
|
|
135
|
-
checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
|
|
136
|
-
const manifestPath = join(context.cwd, '.moda', 'harness.json');
|
|
137
|
-
const manifestExists = existsSync(manifestPath);
|
|
138
|
-
checks.push({
|
|
139
|
-
id: 'harness_manifest_exists',
|
|
140
|
-
group: 'harness',
|
|
141
|
-
title: 'Harness manifest exists',
|
|
142
|
-
status: manifestExists ? 'pass' : 'warning',
|
|
143
|
-
severity: manifestExists ? 'info' : 'warning',
|
|
144
|
-
evidence: [{ label: 'path', value: '.moda/harness.json' }],
|
|
145
|
-
remediation: manifestExists ? undefined : 'Run `moda harness scan --summary` to scan the project and write a harness manifest.',
|
|
146
|
-
nextCommand: manifestExists ? undefined : 'moda harness scan --summary',
|
|
147
|
-
});
|
|
148
|
-
checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
|
|
149
|
-
checks.push(harnessSyncStateCheck(context.cwd));
|
|
150
|
-
checks.push(harnessReportStateCheck(context.cwd));
|
|
151
|
-
checks.push(discoveryCheck('prompts_discovered', 'harness', 'Prompts are discovered', context.cwd, [
|
|
152
|
-
'prompts',
|
|
153
|
-
'.moda/prompts.yml',
|
|
154
|
-
], 'moda prompts init'));
|
|
155
|
-
checks.push(discoveryCheck('skills_discovered', 'harness', 'Skills are discovered', context.cwd, [
|
|
156
|
-
'skills',
|
|
157
|
-
'.claude/skills',
|
|
158
|
-
'.cursor/rules',
|
|
159
|
-
], 'moda skills pull'));
|
|
160
|
-
checks.push(discoveryCheck('evals_discovered', 'harness', 'Evals/tests are discovered', context.cwd, [
|
|
161
|
-
'evals',
|
|
162
|
-
'tests',
|
|
163
|
-
'__tests__',
|
|
164
|
-
]));
|
|
165
|
-
checks.push(sdkInitializationCheck(context.cwd));
|
|
166
|
-
checks.push(validationEventPathCheck(ingestUrl));
|
|
167
|
-
if (options.offline || !apiKey) {
|
|
168
|
-
checks.push(...offlineCloudChecks(options.offline));
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
checks.push(...await cloudDataFlowChecks(ingestUrl, apiKey));
|
|
172
|
-
}
|
|
173
|
-
const nextCommands = nextCommandsForChecks(checks);
|
|
174
|
-
const summary = summarizeChecks(checks);
|
|
175
|
-
return {
|
|
176
|
-
schemaVersion: 'doctor.v0.1',
|
|
177
|
-
generatedAt: new Date().toISOString(),
|
|
178
|
-
profile: {
|
|
179
|
-
active: activeProfile.name,
|
|
180
|
-
requested: context.profile,
|
|
181
|
-
},
|
|
182
|
-
summary,
|
|
183
|
-
checks,
|
|
184
|
-
nextCommands,
|
|
185
|
-
fixPlan: checks
|
|
186
|
-
.filter((check) => check.status === 'fail' || check.status === 'warning')
|
|
187
|
-
.map((check) => ({
|
|
188
|
-
checkId: check.id,
|
|
189
|
-
command: check.nextCommand,
|
|
190
|
-
action: check.remediation ?? 'Review this diagnostic.',
|
|
191
|
-
})),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
function authTokenCheck(context, hasApiKey) {
|
|
195
|
-
const session = loadAuthSession({
|
|
196
|
-
profile: context.profile,
|
|
197
|
-
env: context.env,
|
|
198
|
-
includeExpired: true,
|
|
199
|
-
});
|
|
200
|
-
const valid = isAuthSessionValid(session);
|
|
201
|
-
if (!valid && hasApiKey) {
|
|
202
|
-
return {
|
|
203
|
-
id: 'auth_token_valid',
|
|
204
|
-
group: 'config',
|
|
205
|
-
title: 'Browser auth token is valid',
|
|
206
|
-
status: 'skipped',
|
|
207
|
-
severity: 'info',
|
|
208
|
-
evidence: [{ label: 'reason', value: 'api key configured' }],
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
return {
|
|
212
|
-
id: 'auth_token_valid',
|
|
213
|
-
group: 'config',
|
|
214
|
-
title: 'Browser auth token is valid',
|
|
215
|
-
status: valid ? 'pass' : 'warning',
|
|
216
|
-
severity: valid ? 'info' : 'warning',
|
|
217
|
-
evidence: [
|
|
218
|
-
{ label: 'token', value: session ? 'present' : 'missing' },
|
|
219
|
-
...(session ? [{ label: 'expires_at', value: new Date(session.expires_at * 1000).toISOString() }] : []),
|
|
220
|
-
],
|
|
221
|
-
remediation: valid ? undefined : 'Refresh browser authentication.',
|
|
222
|
-
nextCommand: valid ? undefined : 'moda init',
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
function configPermissionCheck(path) {
|
|
226
|
-
if (!existsSync(path)) {
|
|
227
|
-
return {
|
|
228
|
-
id: 'config_file_permissions',
|
|
229
|
-
group: 'config',
|
|
230
|
-
title: 'Config file permissions',
|
|
231
|
-
status: 'skipped',
|
|
232
|
-
severity: 'info',
|
|
233
|
-
evidence: [{ label: 'path', value: path }],
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const permission = ensureSecretFilePermissions(path);
|
|
237
|
-
return {
|
|
238
|
-
id: 'config_file_permissions',
|
|
239
|
-
group: 'config',
|
|
240
|
-
title: 'Config file permissions',
|
|
241
|
-
status: permission.secure ? 'pass' : 'fail',
|
|
242
|
-
severity: permission.secure ? 'info' : 'critical',
|
|
243
|
-
evidence: [
|
|
244
|
-
{ label: 'path', value: path },
|
|
245
|
-
{ label: 'mode', value: permission.mode ?? 'unknown' },
|
|
246
|
-
],
|
|
247
|
-
remediation: permission.secure ? undefined : 'Restrict config file permissions to the current user.',
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function secretPermissionCheck(path) {
|
|
251
|
-
const permission = ensureSecretFilePermissions(path);
|
|
252
|
-
return {
|
|
253
|
-
id: 'secret_file_permissions',
|
|
254
|
-
group: 'config',
|
|
255
|
-
title: 'Secret file permissions',
|
|
256
|
-
status: permission.secure ? 'pass' : 'fail',
|
|
257
|
-
severity: permission.secure ? 'info' : 'critical',
|
|
258
|
-
evidence: [
|
|
259
|
-
{ label: 'path', value: path },
|
|
260
|
-
{ label: 'mode', value: permission.mode ?? 'unknown' },
|
|
261
|
-
],
|
|
262
|
-
remediation: permission.secure ? undefined : 'Restrict secret file permissions to the current user.',
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
function urlCheck(id, group, title, value) {
|
|
266
|
-
let valid = false;
|
|
267
|
-
try {
|
|
268
|
-
const parsed = new URL(value);
|
|
269
|
-
valid = parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
valid = false;
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
id,
|
|
276
|
-
group,
|
|
277
|
-
title,
|
|
278
|
-
status: valid ? 'pass' : 'fail',
|
|
279
|
-
severity: valid ? 'info' : 'critical',
|
|
280
|
-
evidence: [{ label: 'url', value }],
|
|
281
|
-
remediation: valid ? undefined : 'Configure a valid http(s) URL.',
|
|
282
|
-
nextCommand: valid ? undefined : 'moda profiles create',
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Local stacks mount `/auth/cli` and `/api/*` CLI/control proxies on the
|
|
287
|
-
* frontend (`:3000` by default). Pointing `base_url` at the backend (`:3001`)
|
|
288
|
-
* breaks browser login even though some Nest routes still answer there.
|
|
289
|
-
*/
|
|
290
|
-
function localBaseUrlCheck(environment, baseUrl) {
|
|
291
|
-
// Only the `local` profile targets a loopback worktree stack. `dev` defaults to
|
|
292
|
-
// remote `https://moda.dev`, so including it here warns valid remote configs.
|
|
293
|
-
if (environment !== 'local') {
|
|
294
|
-
return {
|
|
295
|
-
id: 'local_base_url_frontend',
|
|
296
|
-
group: 'config',
|
|
297
|
-
title: 'Local base URL targets the frontend',
|
|
298
|
-
status: 'skipped',
|
|
299
|
-
severity: 'info',
|
|
300
|
-
evidence: [{ label: 'environment', value: environment }],
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
let port = null;
|
|
304
|
-
let loopback = false;
|
|
305
|
-
try {
|
|
306
|
-
const parsed = new URL(baseUrl);
|
|
307
|
-
loopback = isLoopbackBaseUrl(baseUrl);
|
|
308
|
-
port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80');
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
return {
|
|
312
|
-
id: 'local_base_url_frontend',
|
|
313
|
-
group: 'config',
|
|
314
|
-
title: 'Local base URL targets the frontend',
|
|
315
|
-
status: 'fail',
|
|
316
|
-
severity: 'critical',
|
|
317
|
-
evidence: [{ label: 'base_url', value: baseUrl }],
|
|
318
|
-
remediation: 'Set the local profile base URL to the frontend origin (http://localhost:3000).',
|
|
319
|
-
nextCommand: 'moda profiles create local --environment local --base-url http://localhost:3000',
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
const looksLikeBackend = loopback && (port === '3001' || port === '3002');
|
|
323
|
-
if (looksLikeBackend) {
|
|
324
|
-
return {
|
|
325
|
-
id: 'local_base_url_frontend',
|
|
326
|
-
group: 'config',
|
|
327
|
-
title: 'Local base URL targets the frontend',
|
|
328
|
-
status: 'warning',
|
|
329
|
-
severity: 'warning',
|
|
330
|
-
evidence: [
|
|
331
|
-
{ label: 'base_url', value: baseUrl },
|
|
332
|
-
{ label: 'port', value: port ?? 'unknown' },
|
|
333
|
-
],
|
|
334
|
-
remediation: 'Local CLI auth and control APIs are served by the frontend. ' +
|
|
335
|
-
'Use http://localhost:3000 (or your worktree frontend port), not the backend :3001.',
|
|
336
|
-
nextCommand: 'moda profiles create local --environment local --base-url http://localhost:3000 --use',
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
if (!loopback) {
|
|
340
|
-
return {
|
|
341
|
-
id: 'local_base_url_frontend',
|
|
342
|
-
group: 'config',
|
|
343
|
-
title: 'Local base URL targets the frontend',
|
|
344
|
-
status: 'warning',
|
|
345
|
-
severity: 'warning',
|
|
346
|
-
evidence: [{ label: 'base_url', value: baseUrl }],
|
|
347
|
-
remediation: 'Local profile should use a loopback frontend URL (http://localhost:3000). ' +
|
|
348
|
-
'Activate with `moda profiles use local` or set MODA_PROFILE=local.',
|
|
349
|
-
nextCommand: 'moda profiles use local',
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
return {
|
|
353
|
-
id: 'local_base_url_frontend',
|
|
354
|
-
group: 'config',
|
|
355
|
-
title: 'Local base URL targets the frontend',
|
|
356
|
-
status: 'pass',
|
|
357
|
-
severity: 'info',
|
|
358
|
-
evidence: [{ label: 'base_url', value: baseUrl }],
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
function harnessCurrentCheck(cwd, manifestPath, manifestExists, tenantId) {
|
|
362
|
-
if (!manifestExists) {
|
|
363
|
-
return {
|
|
364
|
-
id: 'harness_manifest_current',
|
|
365
|
-
group: 'harness',
|
|
366
|
-
title: 'Harness manifest matches scanner output',
|
|
367
|
-
status: 'skipped',
|
|
368
|
-
severity: 'info',
|
|
369
|
-
evidence: [{ label: 'reason', value: 'manifest missing' }],
|
|
370
|
-
nextCommand: 'moda harness scan --summary',
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
try {
|
|
374
|
-
const current = scanHarness({
|
|
375
|
-
rootDir: cwd,
|
|
376
|
-
scannerVersion: 'doctor',
|
|
377
|
-
tenantId,
|
|
378
|
-
tenantSource: tenantId ? 'env_or_config' : undefined,
|
|
379
|
-
});
|
|
380
|
-
const saved = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
381
|
-
const savedHash = stableGraphHash(saved);
|
|
382
|
-
const currentHash = stableGraphHash(current);
|
|
383
|
-
const matches = savedHash === currentHash;
|
|
384
|
-
return {
|
|
385
|
-
id: 'harness_manifest_current',
|
|
386
|
-
group: 'harness',
|
|
387
|
-
title: 'Harness manifest matches scanner output',
|
|
388
|
-
status: matches ? 'pass' : 'warning',
|
|
389
|
-
severity: matches ? 'info' : 'warning',
|
|
390
|
-
evidence: [
|
|
391
|
-
{ label: 'saved_hash', value: savedHash.slice(0, 12) },
|
|
392
|
-
{ label: 'current_hash', value: currentHash.slice(0, 12) },
|
|
393
|
-
...(matches ? [] : [{ label: 'diff', value: 'Manifest hash differs from fresh scanner output.' }]),
|
|
394
|
-
],
|
|
395
|
-
remediation: matches ? undefined : 'Refresh the local harness manifest.',
|
|
396
|
-
nextCommand: matches ? undefined : 'moda harness scan --summary',
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
catch (err) {
|
|
400
|
-
return {
|
|
401
|
-
id: 'harness_manifest_current',
|
|
402
|
-
group: 'harness',
|
|
403
|
-
title: 'Harness manifest matches scanner output',
|
|
404
|
-
status: 'fail',
|
|
405
|
-
severity: 'critical',
|
|
406
|
-
evidence: [{ label: 'error', value: err instanceof Error ? err.message : String(err) }],
|
|
407
|
-
remediation: 'Regenerate the harness manifest.',
|
|
408
|
-
nextCommand: 'moda harness scan --summary',
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
function harnessSyncStateCheck(cwd) {
|
|
413
|
-
const state = readHarnessSyncState(cwd);
|
|
414
|
-
if (!state) {
|
|
415
|
-
return {
|
|
416
|
-
id: 'harness_sync_state_present',
|
|
417
|
-
group: 'harness',
|
|
418
|
-
title: 'Harness sync state exists',
|
|
419
|
-
status: 'warning',
|
|
420
|
-
severity: 'warning',
|
|
421
|
-
evidence: [{ label: 'path', value: '.moda/sync-state.json' }],
|
|
422
|
-
remediation: 'Sync the harness graph so Moda can correlate local state with cloud state.',
|
|
423
|
-
nextCommand: 'moda harness sync --dry-run',
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
return {
|
|
427
|
-
id: 'harness_sync_state_present',
|
|
428
|
-
group: 'harness',
|
|
429
|
-
title: 'Harness sync state exists',
|
|
430
|
-
status: 'pass',
|
|
431
|
-
severity: 'info',
|
|
432
|
-
evidence: [
|
|
433
|
-
{ label: 'updated_at', value: state.updatedAt },
|
|
434
|
-
{ label: 'profile', value: state.profile },
|
|
435
|
-
{ label: 'harness_id', value: state.harnessId },
|
|
436
|
-
{ label: 'remote_version_id', value: state.remote.versionId || 'none' },
|
|
437
|
-
],
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
function harnessReportStateCheck(cwd) {
|
|
441
|
-
const reportPath = join(cwd, HARNESS_REPORT_PATH);
|
|
442
|
-
if (!existsSync(reportPath)) {
|
|
443
|
-
return {
|
|
444
|
-
id: 'harness_report_approved_synced',
|
|
445
|
-
group: 'harness',
|
|
446
|
-
title: 'Harness analyst report is approved and synced',
|
|
447
|
-
status: 'warning',
|
|
448
|
-
severity: 'warning',
|
|
449
|
-
evidence: [{ label: 'path', value: HARNESS_REPORT_PATH }],
|
|
450
|
-
remediation: 'Run a Moda Analyst pass so the harness graph has cited evidence before cloud sync.',
|
|
451
|
-
nextCommand: 'moda harness analyze --summary',
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
try {
|
|
455
|
-
const report = readHarnessReport(reportPath);
|
|
456
|
-
const validation = validateHarnessReport(report, cwd);
|
|
457
|
-
const reportHash = hashHarnessReport(report);
|
|
458
|
-
const evidence = [
|
|
459
|
-
{ label: 'path', value: HARNESS_REPORT_PATH },
|
|
460
|
-
{ label: 'report_hash', value: reportHash.slice(0, 12) },
|
|
461
|
-
{ label: 'validation', value: validation.status },
|
|
462
|
-
];
|
|
463
|
-
if (validation.status !== 'pass') {
|
|
464
|
-
return {
|
|
465
|
-
id: 'harness_report_approved_synced',
|
|
466
|
-
group: 'harness',
|
|
467
|
-
title: 'Harness analyst report is approved and synced',
|
|
468
|
-
status: 'fail',
|
|
469
|
-
severity: 'critical',
|
|
470
|
-
evidence: [
|
|
471
|
-
...evidence,
|
|
472
|
-
{ label: 'errors', value: validation.errors.slice(0, 3).join(' | ') || 'validation failed' },
|
|
473
|
-
],
|
|
474
|
-
remediation: 'Regenerate or fix the harness report before approving or syncing.',
|
|
475
|
-
nextCommand: 'moda harness analyze --summary',
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
const approval = readHarnessReportApproval(cwd);
|
|
479
|
-
const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION &&
|
|
480
|
-
approval.reportId === report.reportId &&
|
|
481
|
-
approval.reportHash === reportHash &&
|
|
482
|
-
approval.approved === true;
|
|
483
|
-
evidence.push({ label: 'approval', value: approved ? 'approved' : approval ? 'stale_or_invalid' : 'missing' });
|
|
484
|
-
if (approval && !approved) {
|
|
485
|
-
return {
|
|
486
|
-
id: 'harness_report_approved_synced',
|
|
487
|
-
group: 'harness',
|
|
488
|
-
title: 'Harness analyst report is approved and synced',
|
|
489
|
-
status: 'fail',
|
|
490
|
-
severity: 'critical',
|
|
491
|
-
evidence,
|
|
492
|
-
remediation: 'Approval no longer matches the current report. Review and approve the report again.',
|
|
493
|
-
nextCommand: 'moda harness approve --yes',
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
if (!approved) {
|
|
497
|
-
return {
|
|
498
|
-
id: 'harness_report_approved_synced',
|
|
499
|
-
group: 'harness',
|
|
500
|
-
title: 'Harness analyst report is approved and synced',
|
|
501
|
-
status: 'warning',
|
|
502
|
-
severity: 'warning',
|
|
503
|
-
evidence,
|
|
504
|
-
remediation: 'Review and approve the validated harness report before syncing.',
|
|
505
|
-
nextCommand: 'moda harness approve --yes',
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
const state = readHarnessSyncState(cwd);
|
|
509
|
-
const synced = state?.source === 'approved_report' && state.report?.reportHash === reportHash;
|
|
510
|
-
evidence.push({ label: 'sync', value: synced ? 'synced' : 'not_synced_to_this_report' }, ...(state?.report?.reportHash ? [{ label: 'sync_report_hash', value: state.report.reportHash.slice(0, 12) }] : []));
|
|
511
|
-
return {
|
|
512
|
-
id: 'harness_report_approved_synced',
|
|
513
|
-
group: 'harness',
|
|
514
|
-
title: 'Harness analyst report is approved and synced',
|
|
515
|
-
status: synced ? 'pass' : 'warning',
|
|
516
|
-
severity: synced ? 'info' : 'warning',
|
|
517
|
-
evidence,
|
|
518
|
-
remediation: synced ? undefined : 'Sync the approved harness report so Moda cloud has the same harness graph.',
|
|
519
|
-
nextCommand: synced ? undefined : 'moda harness sync --from-report .moda/harness-report.json',
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
catch (err) {
|
|
523
|
-
return {
|
|
524
|
-
id: 'harness_report_approved_synced',
|
|
525
|
-
group: 'harness',
|
|
526
|
-
title: 'Harness analyst report is approved and synced',
|
|
527
|
-
status: 'fail',
|
|
528
|
-
severity: 'critical',
|
|
529
|
-
evidence: [
|
|
530
|
-
{ label: 'path', value: HARNESS_REPORT_PATH },
|
|
531
|
-
{ label: 'error', value: err instanceof Error ? err.message : String(err) },
|
|
532
|
-
],
|
|
533
|
-
remediation: 'Regenerate the harness report.',
|
|
534
|
-
nextCommand: 'moda harness analyze --summary',
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
function readHarnessReportApproval(cwd) {
|
|
539
|
-
const approvalPath = join(cwd, HARNESS_REPORT_APPROVAL_PATH);
|
|
540
|
-
if (!existsSync(approvalPath))
|
|
541
|
-
return null;
|
|
542
|
-
try {
|
|
543
|
-
return JSON.parse(readFileSync(approvalPath, 'utf8'));
|
|
544
|
-
}
|
|
545
|
-
catch {
|
|
546
|
-
return {
|
|
547
|
-
schemaVersion: HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
|
|
548
|
-
reportId: 'unreadable',
|
|
549
|
-
reportHash: 'unreadable',
|
|
550
|
-
approved: true,
|
|
551
|
-
approvedAt: '',
|
|
552
|
-
approvedBy: 'local-human',
|
|
553
|
-
reportPath: HARNESS_REPORT_PATH,
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
|
|
558
|
-
const found = relativePaths.filter((path) => existsSync(join(cwd, path)));
|
|
559
|
-
return {
|
|
560
|
-
id,
|
|
561
|
-
group,
|
|
562
|
-
title,
|
|
563
|
-
status: found.length > 0 ? 'pass' : 'warning',
|
|
564
|
-
severity: found.length > 0 ? 'info' : 'warning',
|
|
565
|
-
evidence: [{ label: 'paths', value: found.length > 0 ? found.join(', ') : 'none' }],
|
|
566
|
-
remediation: found.length > 0 ? undefined : `Expected one of: ${relativePaths.join(', ')}.`,
|
|
567
|
-
nextCommand: found.length > 0 ? undefined : nextCommand,
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
function sdkInitializationCheck(cwd) {
|
|
571
|
-
const candidates = ['src', 'app', 'agent_harness'];
|
|
572
|
-
let detected = false;
|
|
573
|
-
for (const candidate of candidates) {
|
|
574
|
-
const path = join(cwd, candidate);
|
|
575
|
-
if (!existsSync(path))
|
|
576
|
-
continue;
|
|
577
|
-
try {
|
|
578
|
-
const stat = statSync(path);
|
|
579
|
-
detected ||= stat.isDirectory() || stat.isFile();
|
|
580
|
-
}
|
|
581
|
-
catch {
|
|
582
|
-
// Ignore unreadable candidate paths; doctor reports higher-level gaps.
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
return {
|
|
586
|
-
id: 'sdk_initialization_detected',
|
|
587
|
-
group: 'harness',
|
|
588
|
-
title: 'SDK initialization is detectable',
|
|
589
|
-
status: detected ? 'warning' : 'skipped',
|
|
590
|
-
severity: 'warning',
|
|
591
|
-
evidence: [{ label: 'status', value: detected ? 'code present, instrumentation not verified' : 'no source candidates' }],
|
|
592
|
-
remediation: 'Use the generated integration guide to add Moda SDK instrumentation.',
|
|
593
|
-
nextCommand: 'moda init',
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
// Unused while coding-agent capture is disabled; kept for re-enablement.
|
|
597
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
598
|
-
function shadowSparkWorkspaceCheck(cwd, env) {
|
|
599
|
-
const repo = detectGitRepo(cwd);
|
|
600
|
-
if (!repo) {
|
|
601
|
-
return {
|
|
602
|
-
id: 'shadow_spark_workspace_health',
|
|
603
|
-
group: 'spark',
|
|
604
|
-
title: 'Shadow Spark workspace health',
|
|
605
|
-
status: 'skipped',
|
|
606
|
-
severity: 'info',
|
|
607
|
-
evidence: [{ label: 'reason', value: 'not inside a git repository' }],
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
const workspaceId = createHash('sha256')
|
|
611
|
-
.update(`${repo.toplevel}:${repo.originUrl || ''}`)
|
|
612
|
-
.digest('hex')
|
|
613
|
-
.slice(0, 16);
|
|
614
|
-
const path = join(getModaPaths({ env }).home, '.moda', 'workspaces', workspaceId);
|
|
615
|
-
const healthy = existsSync(join(path, '.git'));
|
|
616
|
-
return {
|
|
617
|
-
id: 'shadow_spark_workspace_health',
|
|
618
|
-
group: 'spark',
|
|
619
|
-
title: 'Shadow Spark workspace health',
|
|
620
|
-
status: healthy ? 'pass' : 'warning',
|
|
621
|
-
severity: healthy ? 'info' : 'warning',
|
|
622
|
-
evidence: [
|
|
623
|
-
{ label: 'workspace_id', value: workspaceId },
|
|
624
|
-
{ label: 'path', value: path },
|
|
625
|
-
{ label: 'shadow_git', value: healthy ? 'present' : 'missing' },
|
|
626
|
-
],
|
|
627
|
-
remediation: healthy ? undefined : 'Create a Shadow Spark snapshot.',
|
|
628
|
-
nextCommand: healthy ? undefined : 'moda spark snapshot --json',
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
// Unused while coding-agent capture is disabled; kept for re-enablement.
|
|
632
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
633
|
-
function hookCheck(env) {
|
|
634
|
-
const paths = getModaPaths({ env });
|
|
635
|
-
const hookCandidates = [
|
|
636
|
-
join(paths.home, '.claude', 'hooks', 'moda-snapshot.sh'),
|
|
637
|
-
join(paths.home, '.codex', 'hooks.json'),
|
|
638
|
-
join(paths.home, '.cursor', 'hooks.json'),
|
|
639
|
-
];
|
|
640
|
-
const found = hookCandidates.filter((path) => existsSync(path));
|
|
641
|
-
return {
|
|
642
|
-
id: 'hooks_installed',
|
|
643
|
-
group: 'hooks',
|
|
644
|
-
title: 'Coding-agent hooks installed',
|
|
645
|
-
status: found.length > 0 ? 'pass' : 'warning',
|
|
646
|
-
severity: found.length > 0 ? 'info' : 'warning',
|
|
647
|
-
evidence: [{ label: 'installed', value: found.length > 0 ? String(found.length) : 'none' }],
|
|
648
|
-
remediation: found.length > 0 ? undefined : 'Install coding-agent trace hooks.',
|
|
649
|
-
nextCommand: found.length > 0 ? undefined : 'moda hooks install --agent=all',
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
// Unused while coding-agent capture is disabled; kept for re-enablement.
|
|
653
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
654
|
-
function hookTelemetryChecks(env, ingestUrl) {
|
|
655
|
-
const configs = agentTelemetryConfigs(env);
|
|
656
|
-
return [
|
|
657
|
-
hookFilesExecutableCheck(env),
|
|
658
|
-
agentConfigJsonCheck(configs),
|
|
659
|
-
agentConfigApiKeyCheck(configs),
|
|
660
|
-
otlpEndpointCheck(configs, ingestUrl),
|
|
661
|
-
agentConfigApiKeyStorageCheck(configs),
|
|
662
|
-
];
|
|
663
|
-
}
|
|
664
|
-
function agentTelemetryConfigs(env) {
|
|
665
|
-
const paths = getModaPaths({ env });
|
|
666
|
-
const configs = [
|
|
667
|
-
{
|
|
668
|
-
agent: 'claude',
|
|
669
|
-
title: 'Claude Code',
|
|
670
|
-
path: join(paths.home, '.claude', 'settings.json'),
|
|
671
|
-
providerPath: 'claude',
|
|
672
|
-
providerAttribute: 'claude_code',
|
|
673
|
-
},
|
|
674
|
-
{
|
|
675
|
-
agent: 'codex',
|
|
676
|
-
title: 'Codex',
|
|
677
|
-
path: join(paths.home, '.codex', 'hooks.json'),
|
|
678
|
-
providerPath: 'codex',
|
|
679
|
-
providerAttribute: 'codex',
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
agent: 'cursor',
|
|
683
|
-
title: 'Cursor',
|
|
684
|
-
path: join(paths.home, '.cursor', 'hooks.json'),
|
|
685
|
-
providerPath: 'cursor',
|
|
686
|
-
providerAttribute: 'cursor',
|
|
687
|
-
},
|
|
688
|
-
];
|
|
689
|
-
return configs.map((config) => {
|
|
690
|
-
if (!existsSync(config.path))
|
|
691
|
-
return { ...config, exists: false };
|
|
692
|
-
try {
|
|
693
|
-
const parsed = JSON.parse(readFileSync(config.path, 'utf8'));
|
|
694
|
-
if (!isRecord(parsed)) {
|
|
695
|
-
return { ...config, exists: true, error: 'JSON root is not an object.' };
|
|
696
|
-
}
|
|
697
|
-
return { ...config, exists: true, parsed };
|
|
698
|
-
}
|
|
699
|
-
catch (err) {
|
|
700
|
-
return {
|
|
701
|
-
...config,
|
|
702
|
-
exists: true,
|
|
703
|
-
error: err instanceof Error ? err.message : String(err),
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
function hookFilesExecutableCheck(env) {
|
|
709
|
-
const paths = getModaPaths({ env });
|
|
710
|
-
const candidates = [
|
|
711
|
-
join(paths.home, '.claude', 'hooks', 'moda-snapshot.sh'),
|
|
712
|
-
join(paths.home, '.moda', 'hooks', 'moda-snapshot.sh'),
|
|
713
|
-
];
|
|
714
|
-
const found = candidates.filter((path) => existsSync(path));
|
|
715
|
-
const notExecutable = found.filter((path) => {
|
|
716
|
-
try {
|
|
717
|
-
return (statSync(path).mode & 0o111) === 0;
|
|
718
|
-
}
|
|
719
|
-
catch {
|
|
720
|
-
return true;
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
return {
|
|
724
|
-
id: 'hook_files_executable',
|
|
725
|
-
group: 'hooks',
|
|
726
|
-
title: 'Hook files are executable',
|
|
727
|
-
status: found.length === 0 ? 'skipped' : notExecutable.length === 0 ? 'pass' : 'fail',
|
|
728
|
-
severity: notExecutable.length === 0 ? 'info' : 'critical',
|
|
729
|
-
evidence: [
|
|
730
|
-
{ label: 'found', value: found.length > 0 ? found.map(relativeHomePath(paths.home)).join(', ') : 'none' },
|
|
731
|
-
...(notExecutable.length > 0
|
|
732
|
-
? [{ label: 'not_executable', value: notExecutable.map(relativeHomePath(paths.home)).join(', ') }]
|
|
733
|
-
: []),
|
|
734
|
-
],
|
|
735
|
-
remediation: notExecutable.length > 0 ? 'Reinstall hooks so Moda can set executable permissions.' : undefined,
|
|
736
|
-
nextCommand: notExecutable.length > 0 || found.length === 0 ? 'moda hooks install --agent=all' : undefined,
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
function agentConfigJsonCheck(configs) {
|
|
740
|
-
const existing = configs.filter((config) => config.exists);
|
|
741
|
-
const invalid = existing.filter((config) => config.error);
|
|
742
|
-
return {
|
|
743
|
-
id: 'agent_config_json_valid',
|
|
744
|
-
group: 'hooks',
|
|
745
|
-
title: 'Agent config JSON is valid',
|
|
746
|
-
status: existing.length === 0 ? 'skipped' : invalid.length === 0 ? 'pass' : 'fail',
|
|
747
|
-
severity: invalid.length === 0 ? 'info' : 'critical',
|
|
748
|
-
evidence: [
|
|
749
|
-
{ label: 'configs', value: existing.length > 0 ? existing.map((config) => config.agent).join(', ') : 'none' },
|
|
750
|
-
...(invalid.length > 0
|
|
751
|
-
? invalid.map((config) => ({ label: `${config.agent}_error`, value: config.error ?? 'invalid JSON' }))
|
|
752
|
-
: []),
|
|
753
|
-
],
|
|
754
|
-
remediation: invalid.length > 0 ? 'Fix invalid agent hook config JSON.' : undefined,
|
|
755
|
-
nextCommand: invalid.length > 0 ? 'moda hooks install --agent=all' : undefined,
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
function agentConfigApiKeyCheck(configs) {
|
|
759
|
-
const parsed = configs.filter((config) => config.parsed);
|
|
760
|
-
const missing = parsed.filter((config) => !telemetryHeader(config).replace(/^Authorization=Bearer\s*/, '').trim());
|
|
761
|
-
return {
|
|
762
|
-
id: 'agent_config_api_key_resolves',
|
|
763
|
-
group: 'hooks',
|
|
764
|
-
title: 'API key exists in agent telemetry config',
|
|
765
|
-
status: parsed.length === 0 ? 'skipped' : missing.length === 0 ? 'pass' : 'warning',
|
|
766
|
-
severity: missing.length === 0 ? 'info' : 'warning',
|
|
767
|
-
evidence: [
|
|
768
|
-
{ label: 'configs', value: parsed.length > 0 ? parsed.map((config) => config.agent).join(', ') : 'none' },
|
|
769
|
-
...(missing.length > 0 ? [{ label: 'missing_or_empty', value: missing.map((config) => config.agent).join(', ') }] : []),
|
|
770
|
-
],
|
|
771
|
-
remediation: missing.length > 0 ? 'Reinstall hooks after authenticating so telemetry has an API key.' : undefined,
|
|
772
|
-
nextCommand: missing.length > 0 ? 'moda hooks install --agent=all' : undefined,
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
function otlpEndpointCheck(configs, ingestUrl) {
|
|
776
|
-
const parsed = configs.filter((config) => config.parsed);
|
|
777
|
-
const expectedBase = ingestUrl.replace(/\/+$/, '');
|
|
778
|
-
const mismatches = [];
|
|
779
|
-
for (const config of parsed) {
|
|
780
|
-
const env = telemetryEnv(config);
|
|
781
|
-
const expected = {
|
|
782
|
-
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/logs`,
|
|
783
|
-
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/metrics`,
|
|
784
|
-
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/traces`,
|
|
785
|
-
};
|
|
786
|
-
for (const [key, value] of Object.entries(expected)) {
|
|
787
|
-
if (env[key] !== value)
|
|
788
|
-
mismatches.push(`${config.agent}:${key}`);
|
|
789
|
-
}
|
|
790
|
-
const attrs = String(env.OTEL_RESOURCE_ATTRIBUTES ?? '');
|
|
791
|
-
if (!attrs.split(',').some((item) => item.trim() === `provider=${config.providerAttribute}`)) {
|
|
792
|
-
mismatches.push(`${config.agent}:OTEL_RESOURCE_ATTRIBUTES`);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return {
|
|
796
|
-
id: 'otlp_endpoints_match_profile',
|
|
797
|
-
group: 'hooks',
|
|
798
|
-
title: 'OTLP endpoints match active ingest URL',
|
|
799
|
-
status: parsed.length === 0 ? 'skipped' : mismatches.length === 0 ? 'pass' : 'warning',
|
|
800
|
-
severity: mismatches.length === 0 ? 'info' : 'warning',
|
|
801
|
-
evidence: [
|
|
802
|
-
{ label: 'ingest_url', value: expectedBase },
|
|
803
|
-
{ label: 'configs', value: parsed.length > 0 ? parsed.map((config) => config.agent).join(', ') : 'none' },
|
|
804
|
-
...(mismatches.length > 0 ? [{ label: 'mismatches', value: mismatches.slice(0, 8).join(', ') }] : []),
|
|
805
|
-
],
|
|
806
|
-
remediation: mismatches.length > 0 ? 'Reinstall hooks so agent telemetry points at the active Moda ingest URL.' : undefined,
|
|
807
|
-
nextCommand: mismatches.length > 0 ? 'moda hooks install --agent=all' : undefined,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
function agentConfigApiKeyStorageCheck(configs) {
|
|
811
|
-
const withStoredKey = configs
|
|
812
|
-
.filter((config) => config.parsed)
|
|
813
|
-
.filter((config) => telemetryHeader(config).replace(/^Authorization=Bearer\s*/, '').trim().length > 0);
|
|
814
|
-
return {
|
|
815
|
-
id: 'agent_config_api_key_storage',
|
|
816
|
-
group: 'hooks',
|
|
817
|
-
title: 'API key storage in agent config',
|
|
818
|
-
status: withStoredKey.length > 0 ? 'warning' : 'pass',
|
|
819
|
-
severity: withStoredKey.length > 0 ? 'warning' : 'info',
|
|
820
|
-
evidence: [{ label: 'configs_with_key', value: withStoredKey.length > 0 ? withStoredKey.map((config) => config.agent).join(', ') : 'none' }],
|
|
821
|
-
remediation: withStoredKey.length > 0
|
|
822
|
-
? 'Agent configs contain OTLP auth headers; ensure file permissions are restricted and rotate keys if exposed.'
|
|
823
|
-
: undefined,
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
function validationEventPathCheck(ingestUrl) {
|
|
827
|
-
return {
|
|
828
|
-
id: 'validation_event_path_defined',
|
|
829
|
-
group: 'cloud',
|
|
830
|
-
title: 'Validation event path is defined',
|
|
831
|
-
status: 'pass',
|
|
832
|
-
severity: 'info',
|
|
833
|
-
evidence: [{ label: 'path', value: `${ingestUrl.replace(/\/+$/, '')}/v1/otel/validation` }],
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
function telemetryEnv(config) {
|
|
837
|
-
return isRecord(config.parsed?.env) ? config.parsed.env : {};
|
|
838
|
-
}
|
|
839
|
-
function telemetryHeader(config) {
|
|
840
|
-
const env = telemetryEnv(config);
|
|
841
|
-
return typeof env.OTEL_EXPORTER_OTLP_HEADERS === 'string'
|
|
842
|
-
? env.OTEL_EXPORTER_OTLP_HEADERS
|
|
843
|
-
: '';
|
|
844
|
-
}
|
|
845
|
-
function relativeHomePath(home) {
|
|
846
|
-
return (path) => path.startsWith(home) ? `~/${path.slice(home.length).replace(/^\/+/, '')}` : path;
|
|
847
|
-
}
|
|
848
|
-
function offlineCloudChecks(isOffline) {
|
|
849
|
-
const status = isOffline ? 'skipped' : 'warning';
|
|
850
|
-
const evidence = [{ label: 'mode', value: isOffline ? 'offline' : 'missing api key' }];
|
|
851
|
-
const remediation = isOffline
|
|
852
|
-
? 'Rerun without --offline to check Moda Cloud.'
|
|
853
|
-
: 'Configure an API key before checking cloud data flow.';
|
|
854
|
-
const nextCommand = isOffline ? 'moda doctor --online' : 'moda init';
|
|
855
|
-
return [
|
|
856
|
-
{
|
|
857
|
-
id: 'cloud_api_reachable',
|
|
858
|
-
group: 'cloud',
|
|
859
|
-
title: 'Moda Cloud API is reachable',
|
|
860
|
-
status,
|
|
861
|
-
severity: 'warning',
|
|
862
|
-
evidence,
|
|
863
|
-
remediation,
|
|
864
|
-
nextCommand,
|
|
865
|
-
},
|
|
866
|
-
{
|
|
867
|
-
id: 'ingest_endpoint_reachable',
|
|
868
|
-
group: 'cloud',
|
|
869
|
-
title: 'Moda ingest endpoint is reachable',
|
|
870
|
-
status,
|
|
871
|
-
severity: 'warning',
|
|
872
|
-
evidence,
|
|
873
|
-
remediation,
|
|
874
|
-
nextCommand,
|
|
875
|
-
},
|
|
876
|
-
{
|
|
877
|
-
id: 'recent_data_flow',
|
|
878
|
-
group: 'cloud',
|
|
879
|
-
title: 'Recent traces/logs/runs exist',
|
|
880
|
-
status,
|
|
881
|
-
severity: 'warning',
|
|
882
|
-
evidence,
|
|
883
|
-
remediation,
|
|
884
|
-
nextCommand,
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
id: 'cloud_data_flow',
|
|
888
|
-
group: 'cloud',
|
|
889
|
-
title: 'Moda Cloud data flow',
|
|
890
|
-
status,
|
|
891
|
-
severity: 'warning',
|
|
892
|
-
evidence,
|
|
893
|
-
remediation,
|
|
894
|
-
nextCommand,
|
|
895
|
-
},
|
|
896
|
-
];
|
|
897
|
-
}
|
|
898
|
-
async function cloudDataFlowChecks(ingestUrl, apiKey) {
|
|
899
|
-
let overview;
|
|
900
|
-
let cloudApiCheck;
|
|
901
|
-
try {
|
|
902
|
-
overview = await callDataAPI('/overview?days_back=1');
|
|
903
|
-
cloudApiCheck = {
|
|
904
|
-
id: 'cloud_api_reachable',
|
|
905
|
-
group: 'cloud',
|
|
906
|
-
title: 'Moda Cloud API is reachable',
|
|
907
|
-
status: 'pass',
|
|
908
|
-
severity: 'info',
|
|
909
|
-
evidence: [{ label: 'overview', value: 'reachable' }],
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
catch (err) {
|
|
913
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
914
|
-
return [
|
|
915
|
-
{
|
|
916
|
-
id: 'cloud_api_reachable',
|
|
917
|
-
group: 'cloud',
|
|
918
|
-
title: 'Moda Cloud API is reachable',
|
|
919
|
-
status: 'warning',
|
|
920
|
-
severity: 'warning',
|
|
921
|
-
evidence: [{ label: 'error', value: error }],
|
|
922
|
-
remediation: 'Check API key and base URL.',
|
|
923
|
-
nextCommand: 'moda doctor --offline',
|
|
924
|
-
},
|
|
925
|
-
{
|
|
926
|
-
id: 'ingest_endpoint_reachable',
|
|
927
|
-
group: 'cloud',
|
|
928
|
-
title: 'Moda ingest endpoint is reachable',
|
|
929
|
-
status: 'skipped',
|
|
930
|
-
severity: 'warning',
|
|
931
|
-
evidence: [{ label: 'reason', value: 'cloud API unreachable' }],
|
|
932
|
-
},
|
|
933
|
-
{
|
|
934
|
-
id: 'recent_data_flow',
|
|
935
|
-
group: 'cloud',
|
|
936
|
-
title: 'Recent traces/logs/runs exist',
|
|
937
|
-
status: 'skipped',
|
|
938
|
-
severity: 'warning',
|
|
939
|
-
evidence: [{ label: 'reason', value: 'overview unavailable' }],
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
id: 'cloud_data_flow',
|
|
943
|
-
group: 'cloud',
|
|
944
|
-
title: 'Moda Cloud data flow',
|
|
945
|
-
status: 'warning',
|
|
946
|
-
severity: 'warning',
|
|
947
|
-
evidence: [{ label: 'error', value: error }],
|
|
948
|
-
remediation: 'Check API key, base URL, and whether data has reached Moda.',
|
|
949
|
-
nextCommand: 'moda doctor --offline',
|
|
950
|
-
},
|
|
951
|
-
];
|
|
952
|
-
}
|
|
953
|
-
const ingestCheck = await ingestReachabilityCheck(ingestUrl, apiKey);
|
|
954
|
-
const recentCheck = recentDataFlowCheck(overview);
|
|
955
|
-
const cloudStatus = cloudApiCheck.status === 'pass' &&
|
|
956
|
-
ingestCheck.status === 'pass' &&
|
|
957
|
-
recentCheck.status === 'pass'
|
|
958
|
-
? 'pass'
|
|
959
|
-
: 'warning';
|
|
960
|
-
return [
|
|
961
|
-
cloudApiCheck,
|
|
962
|
-
ingestCheck,
|
|
963
|
-
recentCheck,
|
|
964
|
-
{
|
|
965
|
-
id: 'cloud_data_flow',
|
|
966
|
-
group: 'cloud',
|
|
967
|
-
title: 'Moda Cloud data flow',
|
|
968
|
-
status: cloudStatus,
|
|
969
|
-
severity: cloudStatus === 'pass' ? 'info' : 'warning',
|
|
970
|
-
evidence: [
|
|
971
|
-
{ label: 'cloud_api', value: cloudApiCheck.status },
|
|
972
|
-
{ label: 'ingest', value: ingestCheck.status },
|
|
973
|
-
{ label: 'recent_data', value: recentCheck.status },
|
|
974
|
-
],
|
|
975
|
-
remediation: cloudStatus === 'pass' ? undefined : 'Check API key, ingest URL, and whether data has reached Moda.',
|
|
976
|
-
nextCommand: cloudStatus === 'pass' ? undefined : 'moda doctor --offline',
|
|
977
|
-
},
|
|
978
|
-
];
|
|
979
|
-
}
|
|
980
|
-
async function ingestReachabilityCheck(ingestUrl, apiKey) {
|
|
981
|
-
const url = `${ingestUrl.replace(/\/+$/, '')}/`;
|
|
982
|
-
try {
|
|
983
|
-
const response = await fetch(url, {
|
|
984
|
-
method: 'GET',
|
|
985
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
986
|
-
signal: AbortSignal.timeout(10_000),
|
|
987
|
-
});
|
|
988
|
-
return {
|
|
989
|
-
id: 'ingest_endpoint_reachable',
|
|
990
|
-
group: 'cloud',
|
|
991
|
-
title: 'Moda ingest endpoint is reachable',
|
|
992
|
-
status: 'pass',
|
|
993
|
-
severity: 'info',
|
|
994
|
-
evidence: [
|
|
995
|
-
{ label: 'url', value: url },
|
|
996
|
-
{ label: 'http_status', value: String(response.status) },
|
|
997
|
-
],
|
|
998
|
-
};
|
|
999
|
-
}
|
|
1000
|
-
catch (err) {
|
|
1001
|
-
return {
|
|
1002
|
-
id: 'ingest_endpoint_reachable',
|
|
1003
|
-
group: 'cloud',
|
|
1004
|
-
title: 'Moda ingest endpoint is reachable',
|
|
1005
|
-
status: 'warning',
|
|
1006
|
-
severity: 'warning',
|
|
1007
|
-
evidence: [{ label: 'error', value: err instanceof Error ? err.message : String(err) }],
|
|
1008
|
-
remediation: 'Check the configured ingest URL and network reachability.',
|
|
1009
|
-
nextCommand: 'moda profiles create',
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
function recentDataFlowCheck(overview) {
|
|
1014
|
-
const data = isRecord(overview) ? overview : {};
|
|
1015
|
-
const conversations = numberAt(data, ['conversations', 'total']);
|
|
1016
|
-
const toolFailures = numberAt(data, ['tool_failures', 'total']);
|
|
1017
|
-
const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : [];
|
|
1018
|
-
const lastEventAt = lastActivityTimestamp(recentActivity);
|
|
1019
|
-
const hasData = conversations > 0 || toolFailures > 0 || Boolean(lastEventAt);
|
|
1020
|
-
return {
|
|
1021
|
-
id: 'recent_data_flow',
|
|
1022
|
-
group: 'cloud',
|
|
1023
|
-
title: 'Recent traces/logs/runs exist',
|
|
1024
|
-
status: hasData ? 'pass' : 'warning',
|
|
1025
|
-
severity: hasData ? 'info' : 'warning',
|
|
1026
|
-
evidence: [
|
|
1027
|
-
{ label: 'conversations', value: String(conversations) },
|
|
1028
|
-
{ label: 'tool_failures', value: String(toolFailures) },
|
|
1029
|
-
{ label: 'last_event_at', value: lastEventAt ?? 'none' },
|
|
1030
|
-
],
|
|
1031
|
-
remediation: hasData ? undefined : 'No recent Moda data was visible in the overview window.',
|
|
1032
|
-
nextCommand: hasData ? undefined : 'moda hooks install --agent=all',
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
|
-
function numberAt(value, path) {
|
|
1036
|
-
let cursor = value;
|
|
1037
|
-
for (const key of path) {
|
|
1038
|
-
if (!isRecord(cursor))
|
|
1039
|
-
return 0;
|
|
1040
|
-
cursor = cursor[key];
|
|
1041
|
-
}
|
|
1042
|
-
return typeof cursor === 'number' && Number.isFinite(cursor) ? cursor : 0;
|
|
1043
|
-
}
|
|
1044
|
-
function lastActivityTimestamp(items) {
|
|
1045
|
-
const timestamps = items
|
|
1046
|
-
.map((item) => isRecord(item) && typeof item.timestamp === 'string' ? item.timestamp : undefined)
|
|
1047
|
-
.filter((item) => Boolean(item))
|
|
1048
|
-
.sort();
|
|
1049
|
-
return timestamps.at(-1);
|
|
1050
|
-
}
|
|
1051
|
-
function summarizeChecks(checks) {
|
|
1052
|
-
const fail = checks.filter((check) => check.status === 'fail').length;
|
|
1053
|
-
const warning = checks.filter((check) => check.status === 'warning').length;
|
|
1054
|
-
const skipped = checks.filter((check) => check.status === 'skipped').length;
|
|
1055
|
-
const pass = checks.filter((check) => check.status === 'pass').length;
|
|
1056
|
-
return {
|
|
1057
|
-
status: fail > 0 ? 'fail' : warning > 0 ? 'warning' : 'pass',
|
|
1058
|
-
pass,
|
|
1059
|
-
warning,
|
|
1060
|
-
fail,
|
|
1061
|
-
skipped,
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
function nextCommandsForChecks(checks) {
|
|
1065
|
-
return [...new Set(checks
|
|
1066
|
-
.filter((check) => check.status === 'fail' || check.status === 'warning')
|
|
1067
|
-
.map((check) => check.nextCommand)
|
|
1068
|
-
.filter((command) => Boolean(command)))];
|
|
1069
|
-
}
|
|
1070
|
-
function firstBrokenCheck(checks) {
|
|
1071
|
-
return checks.find((check) => check.status === 'fail') ??
|
|
1072
|
-
checks.find((check) => check.status === 'warning');
|
|
1073
|
-
}
|
|
1074
|
-
function checkStatus(checks, id) {
|
|
1075
|
-
return checks.find((check) => check.id === id)?.status ?? 'skipped';
|
|
1076
|
-
}
|
|
1077
|
-
function doctorAgentWriteOptions(report, command) {
|
|
1078
|
-
const broken = report.checks.filter((check) => check.status === 'fail' || check.status === 'warning');
|
|
1079
|
-
return {
|
|
1080
|
-
summary: `${command} completed with status ${report.summary.status}: ${report.summary.fail} failing checks and ${report.summary.warning} warnings.`,
|
|
1081
|
-
findings: broken.slice(0, 16).map((check) => ({
|
|
1082
|
-
id: check.id,
|
|
1083
|
-
title: check.title,
|
|
1084
|
-
status: check.status,
|
|
1085
|
-
severity: check.severity,
|
|
1086
|
-
description: check.remediation ?? `Doctor check ${check.id} is ${check.status}.`,
|
|
1087
|
-
evidence_ref_ids: [`doctor:${check.id}`],
|
|
1088
|
-
})),
|
|
1089
|
-
evidenceRefs: report.checks.map((check) => ({
|
|
1090
|
-
id: `doctor:${check.id}`,
|
|
1091
|
-
kind: 'doctor_check',
|
|
1092
|
-
label: check.title,
|
|
1093
|
-
status: check.status,
|
|
1094
|
-
})),
|
|
1095
|
-
recommendedActions: report.fixPlan.slice(0, 16).map((item, index) => ({
|
|
1096
|
-
id: `doctor_action_${index + 1}_${item.checkId}`,
|
|
1097
|
-
title: item.action,
|
|
1098
|
-
command: item.command,
|
|
1099
|
-
rationale: `Doctor check ${item.checkId} is not passing.`,
|
|
1100
|
-
mutability: item.command ? agentMutabilityForCommand(item.command) : 'unknown',
|
|
1101
|
-
requires_approval: item.command ? agentCommandRequiresApproval(item.command) : true,
|
|
1102
|
-
})),
|
|
1103
|
-
nextCommands: report.nextCommands.slice(0, 16).map((nextCommand) => ({
|
|
1104
|
-
command: nextCommand,
|
|
1105
|
-
purpose: agentPurposeForCommand(nextCommand),
|
|
1106
|
-
mutability: agentMutabilityForCommand(nextCommand),
|
|
1107
|
-
requires_approval: agentCommandRequiresApproval(nextCommand),
|
|
1108
|
-
})),
|
|
1109
|
-
warnings: broken
|
|
1110
|
-
.filter((check) => check.status === 'warning')
|
|
1111
|
-
.slice(0, 16)
|
|
1112
|
-
.map((check) => `${check.title}: ${check.remediation ?? check.status}`),
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
function agentMutabilityForCommand(command) {
|
|
1116
|
-
if (/\bsync\b/.test(command))
|
|
1117
|
-
return 'sync';
|
|
1118
|
-
if (/\b(init|install|approve|pull|gen|promote|remove)\b/.test(command))
|
|
1119
|
-
return 'write';
|
|
1120
|
-
return 'read';
|
|
1121
|
-
}
|
|
1122
|
-
function agentCommandRequiresApproval(command) {
|
|
1123
|
-
return agentMutabilityForCommand(command) !== 'read' && !command.includes('--dry-run');
|
|
1124
|
-
}
|
|
1125
|
-
function agentPurposeForCommand(command) {
|
|
1126
|
-
if (command.includes('doctor'))
|
|
1127
|
-
return 'Re-run Moda diagnostics.';
|
|
1128
|
-
if (command.includes('harness approve'))
|
|
1129
|
-
return 'Approve the validated harness report after review.';
|
|
1130
|
-
if (command.includes('harness sync'))
|
|
1131
|
-
return 'Sync harness state to Moda Cloud.';
|
|
1132
|
-
if (command.includes('harness scan'))
|
|
1133
|
-
return 'Refresh the local harness manifest.';
|
|
1134
|
-
if (command.includes('init'))
|
|
1135
|
-
return 'Run Moda onboarding.';
|
|
1136
|
-
return 'Inspect or repair Moda setup.';
|
|
1137
|
-
}
|
|
1138
|
-
function detectGitRepo(cwd) {
|
|
1139
|
-
const toplevel = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
1140
|
-
cwd,
|
|
1141
|
-
encoding: 'utf8',
|
|
1142
|
-
});
|
|
1143
|
-
if (toplevel.status !== 0 || !toplevel.stdout.trim())
|
|
1144
|
-
return undefined;
|
|
1145
|
-
const root = toplevel.stdout.trim();
|
|
1146
|
-
const origin = spawnSync('git', ['config', '--get', 'remote.origin.url'], {
|
|
1147
|
-
cwd: root,
|
|
1148
|
-
encoding: 'utf8',
|
|
1149
|
-
});
|
|
1150
|
-
return {
|
|
1151
|
-
toplevel: root,
|
|
1152
|
-
originUrl: origin.status === 0 ? origin.stdout.trim() : '',
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
function stableGraphHash(graph) {
|
|
1156
|
-
return createHash('sha256')
|
|
1157
|
-
.update(JSON.stringify(sortJson({
|
|
1158
|
-
...graph,
|
|
1159
|
-
scan: {
|
|
1160
|
-
...graph.scan,
|
|
1161
|
-
createdAt: undefined,
|
|
1162
|
-
scannerVersion: undefined,
|
|
1163
|
-
skipped: undefined,
|
|
1164
|
-
},
|
|
1165
|
-
})))
|
|
1166
|
-
.digest('hex');
|
|
1167
|
-
}
|
|
1168
|
-
function sortJson(value) {
|
|
1169
|
-
if (Array.isArray(value))
|
|
1170
|
-
return value.map((item) => sortJson(item));
|
|
1171
|
-
if (value && typeof value === 'object') {
|
|
1172
|
-
return Object.fromEntries(Object.entries(value)
|
|
1173
|
-
.filter(([, item]) => item !== undefined)
|
|
1174
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
1175
|
-
.map(([key, item]) => [key, sortJson(item)]));
|
|
1176
|
-
}
|
|
1177
|
-
return value;
|
|
1178
|
-
}
|
|
1179
|
-
function isRecord(value) {
|
|
1180
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
1181
|
-
}
|
|
1182
|
-
//# sourceMappingURL=doctor.js.map
|