@neurcode-ai/cli 0.9.41 โ 0.9.43
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/README.md +9 -3
- package/dist/api-client.d.ts +14 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +10 -0
- package/dist/api-client.js.map +1 -1
- package/dist/commands/bootstrap.d.ts.map +1 -1
- package/dist/commands/bootstrap.js +16 -89
- package/dist/commands/bootstrap.js.map +1 -1
- package/dist/commands/contract.d.ts.map +1 -1
- package/dist/commands/contract.js +179 -20
- package/dist/commands/contract.js.map +1 -1
- package/dist/commands/doctor.d.ts +13 -3
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +476 -95
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/feedback.d.ts.map +1 -1
- package/dist/commands/feedback.js +68 -0
- package/dist/commands/feedback.js.map +1 -1
- package/dist/commands/remediate.d.ts +8 -0
- package/dist/commands/remediate.d.ts.map +1 -1
- package/dist/commands/remediate.js +485 -103
- package/dist/commands/remediate.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +361 -6
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/cli-json.d.ts +54 -0
- package/dist/utils/cli-json.d.ts.map +1 -0
- package/dist/utils/cli-json.js +152 -0
- package/dist/utils/cli-json.js.map +1 -0
- package/dist/utils/policy-compiler.d.ts +3 -1
- package/dist/utils/policy-compiler.d.ts.map +1 -1
- package/dist/utils/policy-compiler.js +8 -0
- package/dist/utils/policy-compiler.js.map +1 -1
- package/dist/utils/runtime-guard.d.ts +3 -1
- package/dist/utils/runtime-guard.d.ts.map +1 -1
- package/dist/utils/runtime-guard.js +8 -0
- package/dist/utils/runtime-guard.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -1,132 +1,513 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Doctor Command -
|
|
3
|
+
* Doctor Command - Enterprise Readiness Diagnostics
|
|
4
4
|
*
|
|
5
|
-
* Verifies
|
|
5
|
+
* Verifies:
|
|
6
|
+
* - CLI configuration and auth
|
|
7
|
+
* - Project/workspace wiring
|
|
8
|
+
* - Deterministic governance artifacts
|
|
9
|
+
* - API health + runtime compatibility handshake
|
|
10
|
+
* - Notifications stream CORS preflight (dashboard critical path)
|
|
6
11
|
*/
|
|
7
12
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
13
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
14
|
};
|
|
10
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
16
|
exports.doctorCommand = doctorCommand;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
17
|
+
const fs_1 = require("fs");
|
|
18
|
+
const path_1 = require("path");
|
|
14
19
|
const chalk_1 = __importDefault(require("chalk"));
|
|
20
|
+
const contracts_1 = require("@neurcode-ai/contracts");
|
|
21
|
+
const api_client_1 = require("../api-client");
|
|
22
|
+
const config_1 = require("../config");
|
|
23
|
+
const change_contract_1 = require("../utils/change-contract");
|
|
24
|
+
const policy_compiler_1 = require("../utils/policy-compiler");
|
|
25
|
+
const policy_packs_1 = require("../utils/policy-packs");
|
|
26
|
+
const project_root_1 = require("../utils/project-root");
|
|
27
|
+
const runtime_guard_1 = require("../utils/runtime-guard");
|
|
15
28
|
const messages_1 = require("../utils/messages");
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
function summarizeChecks(checks) {
|
|
30
|
+
const summary = {
|
|
31
|
+
total: checks.length,
|
|
32
|
+
passed: 0,
|
|
33
|
+
warned: 0,
|
|
34
|
+
failed: 0,
|
|
35
|
+
skipped: 0,
|
|
36
|
+
};
|
|
37
|
+
for (const check of checks) {
|
|
38
|
+
if (check.status === 'pass')
|
|
39
|
+
summary.passed += 1;
|
|
40
|
+
if (check.status === 'warn')
|
|
41
|
+
summary.warned += 1;
|
|
42
|
+
if (check.status === 'fail')
|
|
43
|
+
summary.failed += 1;
|
|
44
|
+
if (check.status === 'skip')
|
|
45
|
+
summary.skipped += 1;
|
|
46
|
+
}
|
|
47
|
+
return summary;
|
|
48
|
+
}
|
|
49
|
+
function iconForStatus(status) {
|
|
50
|
+
if (status === 'pass')
|
|
51
|
+
return 'โ
';
|
|
52
|
+
if (status === 'warn')
|
|
53
|
+
return 'โ ๏ธ ';
|
|
54
|
+
if (status === 'fail')
|
|
55
|
+
return 'โ';
|
|
56
|
+
return 'โญ๏ธ ';
|
|
57
|
+
}
|
|
58
|
+
function colorizeStatus(status, text) {
|
|
59
|
+
if (status === 'pass')
|
|
60
|
+
return chalk_1.default.green(text);
|
|
61
|
+
if (status === 'warn')
|
|
62
|
+
return chalk_1.default.yellow(text);
|
|
63
|
+
if (status === 'fail')
|
|
64
|
+
return chalk_1.default.red(text);
|
|
65
|
+
return chalk_1.default.dim(text);
|
|
66
|
+
}
|
|
67
|
+
function normalizeUrl(input) {
|
|
68
|
+
return input.replace(/\/+$/, '');
|
|
69
|
+
}
|
|
70
|
+
function parseJsonObject(value) {
|
|
71
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
async function fetchWithTimeout(input, init, timeoutMs) {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
|
+
try {
|
|
80
|
+
return await fetch(input, {
|
|
81
|
+
...init,
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function probeHealth(apiUrl) {
|
|
90
|
+
const healthUrl = `${normalizeUrl(apiUrl)}/health`;
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetchWithTimeout(healthUrl, {
|
|
93
|
+
method: 'GET',
|
|
94
|
+
headers: {
|
|
95
|
+
'User-Agent': 'neurcode-cli-doctor',
|
|
96
|
+
},
|
|
97
|
+
}, 5000);
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
statusCode: response.status,
|
|
102
|
+
apiVersion: null,
|
|
103
|
+
compatibility: null,
|
|
104
|
+
error: `Health endpoint returned HTTP ${response.status}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const payload = await response.json().catch(() => null);
|
|
108
|
+
const record = parseJsonObject(payload);
|
|
109
|
+
const compatibility = record ? parseJsonObject(record.compatibility) : null;
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
statusCode: response.status,
|
|
113
|
+
apiVersion: record && typeof record.version === 'string' ? record.version : null,
|
|
114
|
+
compatibility,
|
|
115
|
+
error: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const message = error instanceof Error && error.name === 'AbortError'
|
|
120
|
+
? 'Health endpoint timed out after 5s'
|
|
121
|
+
: error instanceof Error
|
|
122
|
+
? error.message
|
|
123
|
+
: String(error);
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
statusCode: null,
|
|
127
|
+
apiVersion: null,
|
|
128
|
+
compatibility: null,
|
|
129
|
+
error: message,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function probeNotificationsCors(apiUrl) {
|
|
134
|
+
const streamUrl = `${normalizeUrl(apiUrl)}/api/v1/notifications/stream`;
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetchWithTimeout(streamUrl, {
|
|
137
|
+
method: 'OPTIONS',
|
|
138
|
+
headers: {
|
|
139
|
+
Origin: 'https://www.neurcode.com',
|
|
140
|
+
'Access-Control-Request-Method': 'GET',
|
|
141
|
+
'Access-Control-Request-Headers': 'authorization,content-type,x-org-id',
|
|
142
|
+
'User-Agent': 'neurcode-cli-doctor',
|
|
143
|
+
},
|
|
144
|
+
}, 5000);
|
|
145
|
+
const allowedOrigin = response.headers.get('access-control-allow-origin');
|
|
146
|
+
const allowedHeaders = response.headers.get('access-control-allow-headers');
|
|
147
|
+
const headersNormalized = (allowedHeaders || '').toLowerCase();
|
|
148
|
+
const hasRequiredHeaders = headersNormalized.includes('authorization')
|
|
149
|
+
&& headersNormalized.includes('content-type')
|
|
150
|
+
&& headersNormalized.includes('x-org-id');
|
|
151
|
+
const ok = response.ok
|
|
152
|
+
&& allowedOrigin === 'https://www.neurcode.com'
|
|
153
|
+
&& hasRequiredHeaders;
|
|
154
|
+
return {
|
|
155
|
+
ok,
|
|
156
|
+
statusCode: response.status,
|
|
157
|
+
allowedOrigin,
|
|
158
|
+
allowedHeaders,
|
|
159
|
+
error: ok
|
|
160
|
+
? null
|
|
161
|
+
: `Preflight missing required CORS headers (origin=${allowedOrigin || 'none'}, headers=${allowedHeaders || 'none'})`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
statusCode: null,
|
|
168
|
+
allowedOrigin: null,
|
|
169
|
+
allowedHeaders: null,
|
|
170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function readProjectConfig(projectRoot) {
|
|
175
|
+
const path = (0, path_1.join)(projectRoot, '.neurcode', 'config.json');
|
|
176
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
177
|
+
return {
|
|
178
|
+
exists: false,
|
|
179
|
+
projectId: null,
|
|
180
|
+
orgId: null,
|
|
181
|
+
path,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf-8'));
|
|
186
|
+
return {
|
|
187
|
+
exists: true,
|
|
188
|
+
projectId: typeof parsed.projectId === 'string' && parsed.projectId.trim() ? parsed.projectId : null,
|
|
189
|
+
orgId: typeof parsed.orgId === 'string' && parsed.orgId.trim() ? parsed.orgId : null,
|
|
190
|
+
path,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
exists: true,
|
|
196
|
+
projectId: null,
|
|
197
|
+
orgId: null,
|
|
198
|
+
error: error instanceof Error ? error.message : 'Failed to parse config.json',
|
|
199
|
+
path,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function printCheck(check) {
|
|
204
|
+
const head = `${iconForStatus(check.status)} ${check.label}`;
|
|
205
|
+
console.log(colorizeStatus(check.status, head));
|
|
206
|
+
console.log(chalk_1.default.dim(` ${check.message}`));
|
|
207
|
+
if (check.details && check.details.length > 0) {
|
|
208
|
+
for (const line of check.details) {
|
|
209
|
+
console.log(chalk_1.default.dim(` โข ${line}`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (check.recommendation) {
|
|
213
|
+
console.log(chalk_1.default.dim(` โณ ${check.recommendation}`));
|
|
214
|
+
}
|
|
215
|
+
console.log('');
|
|
216
|
+
}
|
|
217
|
+
async function doctorCommand(options = {}) {
|
|
218
|
+
const startedAt = new Date().toISOString();
|
|
23
219
|
const config = (0, config_1.loadConfig)();
|
|
24
|
-
const apiUrl = config.apiUrl || config_1.DEFAULT_API_URL;
|
|
220
|
+
const apiUrl = normalizeUrl(config.apiUrl || config_1.DEFAULT_API_URL);
|
|
25
221
|
const apiKey = (0, config_1.getApiKey)();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
222
|
+
const cliVersion = (options.cliVersion || 'unknown').trim() || 'unknown';
|
|
223
|
+
const localCompatibility = (0, contracts_1.buildRuntimeCompatibilityDescriptor)('cli', cliVersion);
|
|
224
|
+
const checks = [];
|
|
225
|
+
if (!options.json) {
|
|
226
|
+
const userInfo = await (0, messages_1.getUserInfo)();
|
|
227
|
+
const greeting = userInfo ? `, ${userInfo.displayName}` : '';
|
|
228
|
+
await (0, messages_1.printSuccessBanner)('Neurcode Enterprise Doctor', `Running diagnostics${greeting}...`);
|
|
31
229
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
230
|
+
checks.push({
|
|
231
|
+
id: 'config.api_url',
|
|
232
|
+
label: 'API URL',
|
|
233
|
+
status: 'pass',
|
|
234
|
+
message: `Using ${apiUrl}`,
|
|
235
|
+
details: process.env.NEURCODE_API_URL ? [`env override: ${process.env.NEURCODE_API_URL}`] : undefined,
|
|
236
|
+
});
|
|
237
|
+
checks.push({
|
|
238
|
+
id: 'config.auth',
|
|
239
|
+
label: 'Authentication key',
|
|
240
|
+
status: apiKey ? 'pass' : 'fail',
|
|
241
|
+
message: apiKey ? 'API key is configured.' : 'No API key configured.',
|
|
242
|
+
recommendation: apiKey ? undefined : 'Run `neurcode login`.',
|
|
243
|
+
});
|
|
244
|
+
const rootTrace = (0, project_root_1.resolveNeurcodeProjectRootWithTrace)(process.cwd());
|
|
245
|
+
const projectConfig = readProjectConfig(rootTrace.projectRoot);
|
|
246
|
+
checks.push({
|
|
247
|
+
id: 'workspace.root',
|
|
248
|
+
label: 'Workspace root',
|
|
249
|
+
status: projectConfig.exists ? 'pass' : 'warn',
|
|
250
|
+
message: projectConfig.exists
|
|
251
|
+
? `Resolved root: ${rootTrace.projectRoot}`
|
|
252
|
+
: `No linked workspace found at ${rootTrace.projectRoot}`,
|
|
253
|
+
details: [
|
|
254
|
+
`git root: ${rootTrace.gitRoot || 'not in git repo'}`,
|
|
255
|
+
`override status: ${rootTrace.overrideStatus}`,
|
|
256
|
+
],
|
|
257
|
+
recommendation: projectConfig.exists ? undefined : 'Run `neurcode init` in your repository root.',
|
|
258
|
+
});
|
|
259
|
+
checks.push({
|
|
260
|
+
id: 'workspace.project_config',
|
|
261
|
+
label: 'Project binding',
|
|
262
|
+
status: projectConfig.exists && !projectConfig.error && projectConfig.projectId ? 'pass' : 'warn',
|
|
263
|
+
message: projectConfig.exists && !projectConfig.error && projectConfig.projectId
|
|
264
|
+
? `Project bound (${projectConfig.projectId})${projectConfig.orgId ? ` in org ${projectConfig.orgId}` : ''}.`
|
|
265
|
+
: projectConfig.error
|
|
266
|
+
? `Project config parse error (${projectConfig.error}).`
|
|
267
|
+
: 'Project ID not found in .neurcode/config.json.',
|
|
268
|
+
details: [`path: ${projectConfig.path}`],
|
|
269
|
+
recommendation: projectConfig.exists && !projectConfig.error && projectConfig.projectId
|
|
270
|
+
? undefined
|
|
271
|
+
: 'Run `neurcode init` to bind this repo to a project.',
|
|
272
|
+
});
|
|
273
|
+
const policyLock = (0, policy_packs_1.readPolicyLockFile)(rootTrace.projectRoot);
|
|
274
|
+
checks.push({
|
|
275
|
+
id: 'artifact.policy_lock',
|
|
276
|
+
label: 'Policy lock artifact',
|
|
277
|
+
status: policyLock.lock ? 'pass' : policyLock.exists ? 'fail' : 'warn',
|
|
278
|
+
message: policyLock.lock
|
|
279
|
+
? `Policy lock present (effective rules: ${policyLock.lock.effective.ruleCount}).`
|
|
280
|
+
: policyLock.exists
|
|
281
|
+
? `Policy lock invalid (${policyLock.error || 'parse failure'}).`
|
|
282
|
+
: 'Policy lock artifact not found.',
|
|
283
|
+
details: [`path: ${policyLock.path}`],
|
|
284
|
+
recommendation: policyLock.lock || !projectConfig.exists
|
|
285
|
+
? undefined
|
|
286
|
+
: 'Run `neurcode policy install soc2 && neurcode policy compile`.',
|
|
287
|
+
});
|
|
288
|
+
const compiledPolicy = (0, policy_compiler_1.readCompiledPolicyArtifact)(rootTrace.projectRoot);
|
|
289
|
+
checks.push({
|
|
290
|
+
id: 'artifact.compiled_policy',
|
|
291
|
+
label: 'Compiled policy artifact',
|
|
292
|
+
status: compiledPolicy.artifact ? 'pass' : compiledPolicy.exists ? 'fail' : 'warn',
|
|
293
|
+
message: compiledPolicy.artifact
|
|
294
|
+
? `Compiled policy present (deterministic rules: ${compiledPolicy.artifact.compilation.deterministicRuleCount}).`
|
|
295
|
+
: compiledPolicy.exists
|
|
296
|
+
? `Compiled policy invalid (${compiledPolicy.error || 'parse failure'}).`
|
|
297
|
+
: 'Compiled policy artifact not found.',
|
|
298
|
+
details: [`path: ${compiledPolicy.path}`],
|
|
299
|
+
recommendation: compiledPolicy.artifact || !projectConfig.exists
|
|
300
|
+
? undefined
|
|
301
|
+
: 'Run `neurcode policy compile --include-dashboard --out neurcode.policy.compiled.json`.',
|
|
302
|
+
});
|
|
303
|
+
const changeContract = (0, change_contract_1.readChangeContract)(rootTrace.projectRoot);
|
|
304
|
+
checks.push({
|
|
305
|
+
id: 'artifact.change_contract',
|
|
306
|
+
label: 'Change contract artifact',
|
|
307
|
+
status: changeContract.contract ? 'pass' : changeContract.exists ? 'fail' : 'warn',
|
|
308
|
+
message: changeContract.contract
|
|
309
|
+
? `Change contract present (plan ${changeContract.contract.planId}, files ${changeContract.contract.expectedFiles.length}).`
|
|
310
|
+
: changeContract.exists
|
|
311
|
+
? `Change contract invalid (${changeContract.error || 'parse failure'}).`
|
|
312
|
+
: 'Change contract artifact not found.',
|
|
313
|
+
details: [`path: ${changeContract.path}`],
|
|
314
|
+
recommendation: changeContract.contract || !projectConfig.exists
|
|
315
|
+
? undefined
|
|
316
|
+
: 'Run `neurcode plan ...` or `neurcode contract import ...` to create .neurcode/change-contract.json.',
|
|
317
|
+
});
|
|
318
|
+
const runtimeGuard = (0, runtime_guard_1.readRuntimeGuardArtifact)(rootTrace.projectRoot);
|
|
319
|
+
checks.push({
|
|
320
|
+
id: 'artifact.runtime_guard',
|
|
321
|
+
label: 'Runtime guard artifact',
|
|
322
|
+
status: runtimeGuard.artifact ? (runtimeGuard.artifact.active ? 'pass' : 'warn') : runtimeGuard.exists ? 'fail' : 'warn',
|
|
323
|
+
message: runtimeGuard.artifact
|
|
324
|
+
? runtimeGuard.artifact.active
|
|
325
|
+
? `Runtime guard active (${runtimeGuard.artifact.mode}, checks: ${runtimeGuard.artifact.stats.checksRun}).`
|
|
326
|
+
: 'Runtime guard artifact exists but is inactive.'
|
|
327
|
+
: runtimeGuard.exists
|
|
328
|
+
? `Runtime guard invalid (${runtimeGuard.error || 'parse failure'}).`
|
|
329
|
+
: 'Runtime guard artifact not found.',
|
|
330
|
+
details: [`path: ${runtimeGuard.path}`],
|
|
331
|
+
recommendation: runtimeGuard.artifact && runtimeGuard.artifact.active
|
|
332
|
+
? undefined
|
|
333
|
+
: 'Run `neurcode guard start --strict` before coding sessions.',
|
|
334
|
+
});
|
|
335
|
+
const healthProbe = await probeHealth(apiUrl);
|
|
336
|
+
checks.push({
|
|
337
|
+
id: 'api.health',
|
|
338
|
+
label: 'API health endpoint',
|
|
339
|
+
status: healthProbe.ok ? 'pass' : 'fail',
|
|
340
|
+
message: healthProbe.ok
|
|
341
|
+
? `API reachable (${apiUrl}/health).`
|
|
342
|
+
: `API health probe failed (${healthProbe.error || 'unknown error'}).`,
|
|
343
|
+
details: [
|
|
344
|
+
`http status: ${healthProbe.statusCode ?? 'n/a'}`,
|
|
345
|
+
`api version: ${healthProbe.apiVersion || 'unknown'}`,
|
|
346
|
+
],
|
|
347
|
+
recommendation: healthProbe.ok ? undefined : 'Verify API availability and `NEURCODE_API_URL`.',
|
|
348
|
+
});
|
|
349
|
+
if (healthProbe.ok && healthProbe.compatibility) {
|
|
350
|
+
const apiCompat = healthProbe.compatibility;
|
|
351
|
+
const contractId = typeof apiCompat.contractId === 'string' ? apiCompat.contractId.trim() : '';
|
|
352
|
+
const runtimeContractVersion = typeof apiCompat.runtimeContractVersion === 'string'
|
|
353
|
+
? apiCompat.runtimeContractVersion.trim()
|
|
354
|
+
: '';
|
|
355
|
+
const minimumPeers = parseJsonObject(apiCompat.minimumPeerVersions);
|
|
356
|
+
const requiredCli = minimumPeers && typeof minimumPeers.cli === 'string'
|
|
357
|
+
? minimumPeers.cli.trim()
|
|
358
|
+
: null;
|
|
359
|
+
const requiredApi = localCompatibility.minimumPeerVersions.api || null;
|
|
360
|
+
const contractOk = contractId === contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_ID
|
|
361
|
+
&& runtimeContractVersion === contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION;
|
|
362
|
+
const details = [
|
|
363
|
+
`api contract: ${contractId || 'missing'}@${runtimeContractVersion || 'missing'}`,
|
|
364
|
+
`expected contract: ${contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_ID}@${contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION}`,
|
|
365
|
+
];
|
|
366
|
+
let status = contractOk ? 'pass' : 'fail';
|
|
367
|
+
let message = contractOk
|
|
368
|
+
? 'Runtime compatibility contract handshake passed.'
|
|
369
|
+
: 'Runtime compatibility contract mismatch.';
|
|
370
|
+
let recommendation;
|
|
371
|
+
if (requiredCli) {
|
|
372
|
+
details.push(`api requires cli >= ${requiredCli}`);
|
|
373
|
+
const cliCompat = (0, contracts_1.isSemverAtLeast)(cliVersion, requiredCli);
|
|
374
|
+
if (cliCompat === false) {
|
|
375
|
+
status = 'fail';
|
|
376
|
+
message = `CLI version ${cliVersion} is below API minimum ${requiredCli}.`;
|
|
377
|
+
recommendation = 'Upgrade CLI to a compatible release.';
|
|
58
378
|
}
|
|
59
|
-
else {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
allChecksPassed = false;
|
|
379
|
+
else if (cliCompat === null) {
|
|
380
|
+
status = status === 'fail' ? 'fail' : 'warn';
|
|
381
|
+
details.push(`unable to parse semver for CLI version "${cliVersion}"`);
|
|
63
382
|
}
|
|
64
383
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
384
|
+
if (requiredApi && healthProbe.apiVersion) {
|
|
385
|
+
details.push(`cli expects api >= ${requiredApi}`);
|
|
386
|
+
const apiCompatResult = (0, contracts_1.isSemverAtLeast)(healthProbe.apiVersion, requiredApi);
|
|
387
|
+
if (apiCompatResult === false) {
|
|
388
|
+
status = 'fail';
|
|
389
|
+
message = `API version ${healthProbe.apiVersion} is below CLI minimum ${requiredApi}.`;
|
|
390
|
+
recommendation = 'Deploy compatible API version before running strict verify gates.';
|
|
70
391
|
}
|
|
71
|
-
else {
|
|
72
|
-
|
|
73
|
-
|
|
392
|
+
else if (apiCompatResult === null) {
|
|
393
|
+
status = status === 'fail' ? 'fail' : 'warn';
|
|
394
|
+
details.push(`unable to parse semver for API version "${healthProbe.apiVersion}"`);
|
|
74
395
|
}
|
|
75
|
-
allChecksPassed = false;
|
|
76
396
|
}
|
|
397
|
+
checks.push({
|
|
398
|
+
id: 'api.runtime_compatibility',
|
|
399
|
+
label: 'Runtime compatibility handshake',
|
|
400
|
+
status,
|
|
401
|
+
message,
|
|
402
|
+
details,
|
|
403
|
+
recommendation,
|
|
404
|
+
});
|
|
77
405
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
406
|
+
else {
|
|
407
|
+
checks.push({
|
|
408
|
+
id: 'api.runtime_compatibility',
|
|
409
|
+
label: 'Runtime compatibility handshake',
|
|
410
|
+
status: healthProbe.ok ? 'warn' : 'skip',
|
|
411
|
+
message: healthProbe.ok
|
|
412
|
+
? 'API health payload is missing compatibility metadata.'
|
|
413
|
+
: 'Skipped because API health probe failed.',
|
|
414
|
+
recommendation: healthProbe.ok
|
|
415
|
+
? 'Deploy API build that exposes compatibility metadata on /health.'
|
|
416
|
+
: undefined,
|
|
417
|
+
});
|
|
82
418
|
}
|
|
83
|
-
|
|
84
|
-
|
|
419
|
+
const corsProbe = await probeNotificationsCors(apiUrl);
|
|
420
|
+
checks.push({
|
|
421
|
+
id: 'api.notifications_cors',
|
|
422
|
+
label: 'Notifications stream CORS preflight',
|
|
423
|
+
status: corsProbe.ok ? 'pass' : 'warn',
|
|
424
|
+
message: corsProbe.ok
|
|
425
|
+
? 'CORS preflight allows dashboard stream headers.'
|
|
426
|
+
: `Preflight check reported issues (${corsProbe.error || 'unknown'}).`,
|
|
427
|
+
details: [
|
|
428
|
+
`http status: ${corsProbe.statusCode ?? 'n/a'}`,
|
|
429
|
+
`allow-origin: ${corsProbe.allowedOrigin || 'missing'}`,
|
|
430
|
+
`allow-headers: ${corsProbe.allowedHeaders || 'missing'}`,
|
|
431
|
+
],
|
|
432
|
+
recommendation: corsProbe.ok ? undefined : 'Ensure CORS allowlist includes dashboard origin and x-org-id header.',
|
|
433
|
+
});
|
|
85
434
|
if (apiKey) {
|
|
86
|
-
console.log(chalk_1.default.bold.white('๐ Authentication Check:'));
|
|
87
435
|
try {
|
|
88
436
|
const client = new api_client_1.ApiClient(config);
|
|
89
|
-
// Try a simple authenticated request
|
|
90
|
-
console.log(chalk_1.default.dim(' Testing authenticated endpoint...'));
|
|
91
|
-
// Try to get projects list (lightweight endpoint)
|
|
92
437
|
const projects = await client.getProjects();
|
|
93
|
-
|
|
94
|
-
|
|
438
|
+
checks.push({
|
|
439
|
+
id: 'api.authenticated_request',
|
|
440
|
+
label: 'Authenticated API request',
|
|
441
|
+
status: 'pass',
|
|
442
|
+
message: `Authentication valid (projects visible: ${projects.length}).`,
|
|
443
|
+
});
|
|
95
444
|
}
|
|
96
445
|
catch (error) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
allChecksPassed = false;
|
|
446
|
+
checks.push({
|
|
447
|
+
id: 'api.authenticated_request',
|
|
448
|
+
label: 'Authenticated API request',
|
|
449
|
+
status: 'fail',
|
|
450
|
+
message: `Authenticated request failed (${error instanceof Error ? error.message : String(error)}).`,
|
|
451
|
+
recommendation: 'Run `neurcode login` and verify org/project permissions.',
|
|
452
|
+
});
|
|
106
453
|
}
|
|
107
|
-
console.log('');
|
|
108
454
|
}
|
|
109
455
|
else {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
456
|
+
checks.push({
|
|
457
|
+
id: 'api.authenticated_request',
|
|
458
|
+
label: 'Authenticated API request',
|
|
459
|
+
status: 'skip',
|
|
460
|
+
message: 'Skipped because API key is not configured.',
|
|
461
|
+
recommendation: 'Run `neurcode login`.',
|
|
462
|
+
});
|
|
115
463
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
464
|
+
const summary = summarizeChecks(checks);
|
|
465
|
+
const recommendations = [...new Set(checks
|
|
466
|
+
.filter((check) => check.status === 'warn' || check.status === 'fail')
|
|
467
|
+
.map((check) => check.recommendation)
|
|
468
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0))];
|
|
469
|
+
const payload = {
|
|
470
|
+
success: summary.failed === 0,
|
|
471
|
+
timestamp: startedAt,
|
|
472
|
+
cliVersion,
|
|
473
|
+
apiUrl,
|
|
474
|
+
projectRoot: rootTrace.projectRoot,
|
|
475
|
+
summary,
|
|
476
|
+
checks,
|
|
477
|
+
recommendations,
|
|
478
|
+
};
|
|
479
|
+
if (options.json) {
|
|
480
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
119
481
|
}
|
|
120
482
|
else {
|
|
121
|
-
(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
483
|
+
console.log(chalk_1.default.bold.white('\n๐ Diagnostics\n'));
|
|
484
|
+
checks.forEach(printCheck);
|
|
485
|
+
const summaryLine = `Pass ${summary.passed} ยท Warn ${summary.warned} ยท Fail ${summary.failed} ยท Skip ${summary.skipped}`;
|
|
486
|
+
if (summary.failed > 0) {
|
|
487
|
+
console.log(chalk_1.default.red(`โ ${summaryLine}`));
|
|
488
|
+
}
|
|
489
|
+
else if (summary.warned > 0) {
|
|
490
|
+
console.log(chalk_1.default.yellow(`โ ๏ธ ${summaryLine}`));
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
console.log(chalk_1.default.green(`โ
${summaryLine}`));
|
|
494
|
+
}
|
|
495
|
+
console.log('');
|
|
496
|
+
if (recommendations.length > 0) {
|
|
497
|
+
console.log(chalk_1.default.bold.white('Next actions:'));
|
|
498
|
+
for (const recommendation of recommendations) {
|
|
499
|
+
console.log(chalk_1.default.dim(` โข ${recommendation}`));
|
|
500
|
+
}
|
|
501
|
+
console.log('');
|
|
502
|
+
}
|
|
503
|
+
if (summary.failed === 0) {
|
|
504
|
+
await (0, messages_1.printSuccessBanner)('Doctor Complete', summary.warned > 0
|
|
505
|
+
? 'Core checks passed with advisory warnings.'
|
|
506
|
+
: 'All enterprise readiness checks passed.');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (summary.failed > 0) {
|
|
510
|
+
process.exitCode = 1;
|
|
130
511
|
}
|
|
131
512
|
}
|
|
132
513
|
//# sourceMappingURL=doctor.js.map
|