@danielblomma/cortex-mcp 1.2.1 → 1.3.1

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/bin/cortex.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { fileURLToPath } from "node:url";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { spawn } from "node:child_process";
6
6
  import { normalizeProjectRoot } from "./wsl.mjs";
7
7
 
@@ -53,6 +53,7 @@ function printHelp() {
53
53
  console.log(" cortex bootstrap");
54
54
  console.log(" cortex update");
55
55
  console.log(" cortex status");
56
+ console.log(" cortex doctor");
56
57
  console.log(" cortex ingest [--changed] [--verbose]");
57
58
  console.log(" cortex embed [--changed]");
58
59
  console.log(" cortex graph-load [--no-reset]");
@@ -147,7 +148,7 @@ function ensureScaffoldExists() {
147
148
 
148
149
  // Files that should never be overwritten if they already exist in the target.
149
150
  // These contain user-specific configuration that would be lost on re-init.
150
- const PRESERVE_FILES = new Set(["config.yaml", "enterprise.yml", "enterprise.yaml"]);
151
+ const PRESERVE_FILES = new Set(["config.yaml", "enterprise.yml", "enterprise.yaml", "CLAUDE.md"]);
151
152
 
152
153
  function copyDirectory(sourceDir, targetDir) {
153
154
  fs.mkdirSync(targetDir, { recursive: true });
@@ -206,6 +207,13 @@ function installScaffold(targetDir, force) {
206
207
  copyDirectory(sourcePath, targetPath);
207
208
  }
208
209
 
210
+ // Copy CLAUDE.md (skip if already exists to preserve user edits)
211
+ const claudeMdSource = path.join(SCAFFOLD_ROOT, "CLAUDE.md");
212
+ const claudeMdTarget = path.join(targetDir, "CLAUDE.md");
213
+ if (fs.existsSync(claudeMdSource) && !fs.existsSync(claudeMdTarget)) {
214
+ fs.copyFileSync(claudeMdSource, claudeMdTarget);
215
+ }
216
+
209
217
  const docsDir = path.join(targetDir, "docs");
210
218
  fs.mkdirSync(docsDir, { recursive: true });
211
219
  const docsSource = path.join(SCAFFOLD_ROOT, "docs", "architecture.md");
@@ -482,6 +490,78 @@ function canAutoInitialize(targetDir) {
482
490
  return scaffoldPaths.every((entryPath) => !fs.existsSync(entryPath));
483
491
  }
484
492
 
493
+ function isScaffoldOutOfDate(targetDir) {
494
+ const contextScript = path.join(targetDir, "scripts", "context.sh");
495
+ if (!fs.existsSync(contextScript)) {
496
+ return false;
497
+ }
498
+ const doctorScript = path.join(targetDir, "scripts", "doctor.sh");
499
+ if (!fs.existsSync(doctorScript)) {
500
+ return true;
501
+ }
502
+ const mcpPackage = path.join(targetDir, "mcp", "package.json");
503
+ if (!fs.existsSync(mcpPackage)) {
504
+ return true;
505
+ }
506
+ try {
507
+ const contents = fs.readFileSync(contextScript, "utf8");
508
+ if (!/\bdoctor\)\s*\n/.test(contents)) {
509
+ return true;
510
+ }
511
+ } catch {
512
+ return true;
513
+ }
514
+ return false;
515
+ }
516
+
517
+ async function confirmPrompt(message) {
518
+ const { createInterface } = await import("node:readline/promises");
519
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
520
+ try {
521
+ const answer = (await rl.question(message)).trim().toLowerCase();
522
+ return answer === "y" || answer === "yes";
523
+ } finally {
524
+ rl.close();
525
+ }
526
+ }
527
+
528
+ async function maybeMigrateScaffold(targetDir, command) {
529
+ if (!isScaffoldOutOfDate(targetDir)) {
530
+ return;
531
+ }
532
+
533
+ const autoYes = isTruthyEnv(process.env.CORTEX_AUTO_MIGRATE);
534
+ const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
535
+
536
+ console.error(
537
+ `[cortex] scaffold in ${targetDir} is out of date ` +
538
+ `(missing scripts/doctor.sh, mcp/package.json, or doctor subcommand in context.sh).`
539
+ );
540
+
541
+ let proceed = autoYes;
542
+ if (!autoYes) {
543
+ if (!interactive) {
544
+ throw new Error(
545
+ `Cortex CLI ${process.env.CORTEX_CLI_VERSION ?? ""} needs an updated scaffold to run '${command}'. ` +
546
+ `Run 'cortex init --bootstrap' to upgrade, or re-run with CORTEX_AUTO_MIGRATE=true.`
547
+ );
548
+ }
549
+ proceed = await confirmPrompt("[cortex] Upgrade scaffold now (runs 'cortex init --bootstrap')? [y/N] ");
550
+ }
551
+
552
+ if (!proceed) {
553
+ throw new Error("Scaffold upgrade declined. Run 'cortex init --bootstrap' manually to continue.");
554
+ }
555
+
556
+ console.error(`[cortex] migrating scaffold in ${targetDir}`);
557
+ ensureScaffoldExists();
558
+ installScaffold(targetDir, true);
559
+ installAssistantHelpers(targetDir);
560
+ await maybeInstallGitHooks(targetDir);
561
+ await runContextCommand(targetDir, ["bootstrap"]);
562
+ console.error(`[cortex] scaffold upgraded; continuing with '${command}'`);
563
+ }
564
+
485
565
  async function ensureProjectInitializedForMcp(targetDir) {
486
566
  const mcpPackageJson = path.join(targetDir, "mcp", "package.json");
487
567
  const serverEntry = path.join(targetDir, "mcp", "dist", "server.js");
@@ -490,6 +570,13 @@ async function ensureProjectInitializedForMcp(targetDir) {
490
570
  return;
491
571
  }
492
572
 
573
+ if (isScaffoldOutOfDate(targetDir)) {
574
+ await maybeMigrateScaffold(targetDir, "mcp");
575
+ if (fs.existsSync(mcpPackageJson) && fs.existsSync(serverEntry)) {
576
+ return;
577
+ }
578
+ }
579
+
493
580
  if (!isTruthyEnv(process.env.CORTEX_AUTO_BOOTSTRAP_ON_MCP)) {
494
581
  ensureProjectInitialized(targetDir);
495
582
  return;
@@ -635,17 +722,26 @@ async function run() {
635
722
  "watch",
636
723
  "refresh",
637
724
  "memory-compile",
638
- "memory-lint"
725
+ "memory-lint",
726
+ "doctor"
639
727
  ]);
640
728
 
641
729
  if (!passthrough.has(command)) {
642
730
  throw new Error(`Unknown command: ${command}`);
643
731
  }
644
732
 
733
+ await maybeMigrateScaffold(process.cwd(), command);
645
734
  await runContextCommand(process.cwd(), [command, ...rest]);
646
735
  }
647
736
 
648
- run().catch((error) => {
649
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
650
- process.exit(1);
651
- });
737
+ const invokedAsScript =
738
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
739
+
740
+ if (invokedAsScript) {
741
+ run().catch((error) => {
742
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
743
+ process.exit(1);
744
+ });
745
+ }
746
+
747
+ export { isScaffoldOutOfDate };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danielblomma/cortex-mcp",
3
3
  "mcpName": "io.github.DanielBlomma/cortex",
4
- "version": "1.2.1",
4
+ "version": "1.3.1",
5
5
  "description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
6
6
  "type": "module",
7
7
  "author": "Daniel Blomma",
@@ -18,6 +18,7 @@ Commands:
18
18
  graph-load [--no-reset] Build RyuGraph DB from indexed context
19
19
  dashboard [--interval <sec>] Live TUI showing what Cortex adds to your repo
20
20
  status Show latest ingest summary
21
+ doctor Health check — verify config, index, MCP, and enterprise
21
22
  memory-compile [--dry-run] [--verbose]
22
23
  Compile raw memory notes into structured articles
23
24
  memory-lint [--verbose] [--json] Lint compiled memory articles for issues
@@ -58,6 +59,9 @@ case "$COMMAND" in
58
59
  status)
59
60
  "$SCRIPT_DIR/status.sh"
60
61
  ;;
62
+ doctor)
63
+ "$SCRIPT_DIR/doctor.sh"
64
+ ;;
61
65
  memory-compile)
62
66
  "$SCRIPT_DIR/memory-compile.sh" "$@"
63
67
  ;;
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ CONTEXT_DIR="$REPO_ROOT/.context"
6
+ MCP_DIR="$REPO_ROOT/mcp"
7
+
8
+ PASS=0
9
+ FAIL=0
10
+ WARN=0
11
+
12
+ pass() { echo " ✓ $1"; PASS=$((PASS + 1)); }
13
+ fail() { echo " ✗ $1"; FAIL=$((FAIL + 1)); }
14
+ warn() { echo " ! $1"; WARN=$((WARN + 1)); }
15
+ info() { echo " - $1"; }
16
+
17
+ echo ""
18
+ echo "[cortex] Doctor — checking your setup"
19
+
20
+ # ── Config ──────────────────────────────────────────────
21
+
22
+ echo ""
23
+ echo " Config"
24
+
25
+ if [[ -f "$CONTEXT_DIR/config.yaml" ]]; then
26
+ pass ".context/config.yaml found"
27
+ # Show source_paths
28
+ PATHS=$(node -e '
29
+ const fs = require("node:fs");
30
+ const raw = fs.readFileSync(process.argv[1], "utf8");
31
+ const paths = [];
32
+ let inSection = false;
33
+ for (const line of raw.split("\n")) {
34
+ if (/^source_paths:\s*$/.test(line.trim())) { inSection = true; continue; }
35
+ if (!inSection) continue;
36
+ const m = line.match(/^\s*-\s*(.+?)\s*$/);
37
+ if (m) { paths.push(m[1].replace(/^["\x27]|["\x27]$/g, "")); continue; }
38
+ if (line.trim() !== "" && !/^\s/.test(line)) break;
39
+ }
40
+ console.log(paths.join(", ") || "(none)");
41
+ ' "$CONTEXT_DIR/config.yaml" 2>/dev/null || echo "(parse error)")
42
+ info "source_paths: $PATHS"
43
+ else
44
+ fail ".context/config.yaml not found — run: cortex init"
45
+ fi
46
+
47
+ # Enterprise config
48
+ ENTERPRISE_CONFIG=""
49
+ if [[ -f "$CONTEXT_DIR/enterprise.yml" ]]; then
50
+ ENTERPRISE_CONFIG="$CONTEXT_DIR/enterprise.yml"
51
+ elif [[ -f "$CONTEXT_DIR/enterprise.yaml" ]]; then
52
+ ENTERPRISE_CONFIG="$CONTEXT_DIR/enterprise.yaml"
53
+ fi
54
+
55
+ if [[ -n "$ENTERPRISE_CONFIG" ]]; then
56
+ pass "enterprise config found: $(basename "$ENTERPRISE_CONFIG")"
57
+ else
58
+ info "no enterprise config (community mode)"
59
+ fi
60
+
61
+ # ── Index ───────────────────────────────────────────────
62
+
63
+ echo ""
64
+ echo " Index"
65
+
66
+ INGEST_MANIFEST="$CONTEXT_DIR/cache/manifest.json"
67
+ if [[ -f "$INGEST_MANIFEST" ]]; then
68
+ INGEST_INFO=$(node -e '
69
+ const fs = require("node:fs");
70
+ const d = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
71
+ const c = d.counts || {};
72
+ const age = Math.round((Date.now() - new Date(d.generated_at).getTime()) / 60000);
73
+ const ageStr = age < 60 ? age + " min ago" : Math.round(age / 60) + "h ago";
74
+ console.log(`${c.files ?? 0} files, ${c.rules ?? 0} rules (${ageStr})`);
75
+ ' "$INGEST_MANIFEST" 2>/dev/null || echo "parse error")
76
+ pass "Ingest: $INGEST_INFO"
77
+ else
78
+ warn "Ingest manifest missing — run: cortex bootstrap"
79
+ fi
80
+
81
+ GRAPH_MANIFEST="$CONTEXT_DIR/cache/graph-manifest.json"
82
+ if [[ -f "$GRAPH_MANIFEST" ]]; then
83
+ GRAPH_INFO=$(node -e '
84
+ const fs = require("node:fs");
85
+ const d = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
86
+ const c = d.counts || {};
87
+ const age = Math.round((Date.now() - new Date(d.generated_at).getTime()) / 60000);
88
+ const ageStr = age < 60 ? age + " min ago" : Math.round(age / 60) + "h ago";
89
+ console.log(`${c.files ?? 0} files, ${c.constrains ?? 0} constrains, ${c.calls ?? 0} calls (${ageStr})`);
90
+ ' "$GRAPH_MANIFEST" 2>/dev/null || echo "parse error")
91
+ pass "Graph: $GRAPH_INFO"
92
+ else
93
+ warn "Graph manifest missing — run: cortex bootstrap"
94
+ fi
95
+
96
+ EMBED_MANIFEST="$CONTEXT_DIR/embeddings/manifest.json"
97
+ if [[ -f "$EMBED_MANIFEST" ]]; then
98
+ EMBED_INFO=$(node -e '
99
+ const fs = require("node:fs");
100
+ const d = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
101
+ const c = d.counts || {};
102
+ console.log(`${c.entities ?? 0} entities, model ${d.model || "unknown"}`);
103
+ ' "$EMBED_MANIFEST" 2>/dev/null || echo "parse error")
104
+ pass "Embeddings: $EMBED_INFO"
105
+ else
106
+ warn "Embeddings missing — run: cortex bootstrap"
107
+ fi
108
+
109
+ # Freshness
110
+ if [[ -f "$INGEST_MANIFEST" ]] && command -v git &>/dev/null && git -C "$REPO_ROOT" rev-parse --git-dir &>/dev/null; then
111
+ FRESHNESS=$(node -e '
112
+ const fs = require("node:fs");
113
+ const { execSync } = require("node:child_process");
114
+ const d = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
115
+ const sp = Array.isArray(d.source_paths) ? d.source_paths : [];
116
+ const files = Number(d.counts?.files ?? 0);
117
+ let changed = 0;
118
+ try {
119
+ const out = execSync("git status --porcelain", { cwd: process.argv[2], encoding: "utf8", timeout: 3000 });
120
+ for (const line of out.split("\n")) {
121
+ if (!line || line.length < 4) continue;
122
+ const p = line.slice(3).trim().split(" -> ").pop();
123
+ if (p.startsWith(".context/")) continue;
124
+ if (sp.length === 0 || sp.some(s => p === s || p.startsWith(s + "/"))) changed++;
125
+ }
126
+ } catch {}
127
+ const base = Math.max(files, changed, 1);
128
+ const pct = Math.round(Math.max(0, (base - changed) / base) * 100);
129
+ console.log(pct);
130
+ ' "$INGEST_MANIFEST" "$REPO_ROOT" 2>/dev/null || echo "-1")
131
+ if [[ "$FRESHNESS" != "-1" ]]; then
132
+ if [[ "$FRESHNESS" -ge 90 ]]; then
133
+ pass "Freshness: ${FRESHNESS}%"
134
+ elif [[ "$FRESHNESS" -ge 50 ]]; then
135
+ warn "Freshness: ${FRESHNESS}% — run: cortex update"
136
+ else
137
+ fail "Freshness: ${FRESHNESS}% — run: cortex update"
138
+ fi
139
+ fi
140
+ fi
141
+
142
+ # ── MCP Server ──────────────────────────────────────────
143
+
144
+ echo ""
145
+ echo " MCP Server"
146
+
147
+ if [[ -f "$MCP_DIR/dist/server.js" ]]; then
148
+ pass "mcp/dist/server.js exists"
149
+ else
150
+ fail "mcp/dist/server.js missing — run: cd mcp && npm run build"
151
+ fi
152
+
153
+ if [[ -d "$MCP_DIR/node_modules" ]]; then
154
+ pass "mcp/node_modules present"
155
+ else
156
+ fail "mcp/node_modules missing — run: cd mcp && npm install"
157
+ fi
158
+
159
+ # Quick MCP import check
160
+ if [[ -f "$MCP_DIR/dist/server.js" ]] && [[ -d "$MCP_DIR/node_modules" ]]; then
161
+ MCP_CHECK=$(cd "$REPO_ROOT" && timeout 10 node -e '
162
+ const start = Date.now();
163
+ try {
164
+ require("./mcp/dist/graph.js");
165
+ console.log("ok " + (Date.now() - start));
166
+ } catch(e) {
167
+ console.log("fail " + e.message);
168
+ }
169
+ ' 2>/dev/null || echo "fail timeout")
170
+ if [[ "$MCP_CHECK" == ok* ]]; then
171
+ MS="${MCP_CHECK#ok }"
172
+ pass "Graph module loads (${MS}ms)"
173
+ else
174
+ warn "Graph module failed to load: ${MCP_CHECK#fail }"
175
+ fi
176
+ fi
177
+
178
+ # ── Enterprise ──────────────────────────────────────────
179
+
180
+ if [[ -n "$ENTERPRISE_CONFIG" ]]; then
181
+ echo ""
182
+ echo " Enterprise"
183
+
184
+ # Plugin installed?
185
+ ENTERPRISE_PKG="$MCP_DIR/node_modules/@danielblomma/cortex-enterprise/package.json"
186
+ if [[ -f "$ENTERPRISE_PKG" ]]; then
187
+ ENT_VERSION=$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).version)' "$ENTERPRISE_PKG" 2>/dev/null || echo "unknown")
188
+ pass "Plugin installed: v${ENT_VERSION}"
189
+ else
190
+ fail "Plugin not installed — run: cortex bootstrap"
191
+ fi
192
+
193
+ # Parse enterprise config for checks
194
+ TELEMETRY_ENDPOINT=$(node -e '
195
+ const fs = require("node:fs");
196
+ const raw = fs.readFileSync(process.argv[1], "utf8");
197
+ let section = "", fields = {};
198
+ for (const line of raw.split("\n")) {
199
+ const t = line.trimEnd();
200
+ if (!t || t.startsWith("#")) continue;
201
+ const sm = t.match(/^(\w+):\s*$/);
202
+ if (sm) { section = sm[1]; continue; }
203
+ const kv = t.match(/^\s+(\w+):\s*(.+?)\s*$/);
204
+ if (kv && section) fields[section + "." + kv[1]] = kv[2].replace(/^["\x27]|["\x27]$/g, "");
205
+ }
206
+ console.log(fields["telemetry.endpoint"] || "");
207
+ ' "$ENTERPRISE_CONFIG" 2>/dev/null || echo "")
208
+
209
+ POLICY_ENDPOINT=$(node -e '
210
+ const fs = require("node:fs");
211
+ const raw = fs.readFileSync(process.argv[1], "utf8");
212
+ let section = "", fields = {};
213
+ for (const line of raw.split("\n")) {
214
+ const t = line.trimEnd();
215
+ if (!t || t.startsWith("#")) continue;
216
+ const sm = t.match(/^(\w+):\s*$/);
217
+ if (sm) { section = sm[1]; continue; }
218
+ const kv = t.match(/^\s+(\w+):\s*(.+?)\s*$/);
219
+ if (kv && section) fields[section + "." + kv[1]] = kv[2].replace(/^["\x27]|["\x27]$/g, "");
220
+ }
221
+ console.log(fields["policy.endpoint"] || "");
222
+ ' "$ENTERPRISE_CONFIG" 2>/dev/null || echo "")
223
+
224
+ # Telemetry
225
+ if [[ -n "$TELEMETRY_ENDPOINT" ]]; then
226
+ pass "Telemetry: endpoint configured"
227
+ HTTP_CODE=$(curl -so /dev/null -w '%{http_code}' --max-time 5 -X POST \
228
+ -H "Content-Type: application/json" \
229
+ -d '{}' \
230
+ "$TELEMETRY_ENDPOINT" 2>/dev/null | tail -c 3 || echo "000")
231
+ if [[ "$HTTP_CODE" == "000" ]]; then
232
+ fail "Telemetry: endpoint not reachable (timeout/DNS)"
233
+ elif [[ "$HTTP_CODE" =~ ^[23] ]]; then
234
+ pass "Telemetry: endpoint reachable (HTTP ${HTTP_CODE})"
235
+ elif [[ "$HTTP_CODE" == "401" ]]; then
236
+ pass "Telemetry: endpoint reachable (auth required — expected)"
237
+ else
238
+ warn "Telemetry: endpoint returned HTTP ${HTTP_CODE}"
239
+ fi
240
+ else
241
+ warn "Telemetry: no endpoint configured"
242
+ fi
243
+
244
+ # Policy
245
+ POLICY_COUNT=0
246
+ if [[ -f "$CONTEXT_DIR/rules.yaml" ]]; then
247
+ LOCAL_RULES=$(grep -c "^ - id:" "$CONTEXT_DIR/rules.yaml" 2>/dev/null || echo "0")
248
+ POLICY_COUNT=$((POLICY_COUNT + LOCAL_RULES))
249
+ fi
250
+ if [[ -f "$CONTEXT_DIR/policies/org-rules.yaml" ]]; then
251
+ ORG_RULES=$(grep -c "^ - id:" "$CONTEXT_DIR/policies/org-rules.yaml" 2>/dev/null || echo "0")
252
+ POLICY_COUNT=$((POLICY_COUNT + ORG_RULES))
253
+ fi
254
+ if [[ "$POLICY_COUNT" -gt 0 ]]; then
255
+ pass "Policies: ${POLICY_COUNT} loaded"
256
+ else
257
+ info "Policies: none loaded"
258
+ fi
259
+
260
+ if [[ -n "$POLICY_ENDPOINT" ]]; then
261
+ POLICY_HTTP=$(curl -so /dev/null -w '%{http_code}' --max-time 5 "$POLICY_ENDPOINT" 2>/dev/null | tail -c 3 || echo "000")
262
+ if [[ "$POLICY_HTTP" == "000" ]]; then
263
+ fail "Policy: endpoint not reachable (timeout/DNS)"
264
+ elif [[ "$POLICY_HTTP" =~ ^[23] ]] || [[ "$POLICY_HTTP" == "401" ]]; then
265
+ pass "Policy: endpoint reachable (HTTP ${POLICY_HTTP})"
266
+ else
267
+ warn "Policy: endpoint returned HTTP ${POLICY_HTTP}"
268
+ fi
269
+ fi
270
+
271
+ # Audit
272
+ LATEST_AUDIT=$(ls -t "$CONTEXT_DIR/audit/"*.jsonl 2>/dev/null | head -1 || echo "")
273
+ if [[ -n "$LATEST_AUDIT" ]]; then
274
+ AUDIT_AGE=$(node -e '
275
+ const fs = require("node:fs");
276
+ const stat = fs.statSync(process.argv[1]);
277
+ const mins = Math.round((Date.now() - stat.mtimeMs) / 60000);
278
+ if (mins < 60) console.log(mins + " min ago");
279
+ else console.log(Math.round(mins / 60) + "h ago");
280
+ ' "$LATEST_AUDIT" 2>/dev/null || echo "unknown")
281
+ pass "Audit: last entry ${AUDIT_AGE}"
282
+ else
283
+ info "Audit: no entries yet"
284
+ fi
285
+ fi
286
+
287
+ # ── Summary ─────────────────────────────────────────────
288
+
289
+ echo ""
290
+ TOTAL=$((PASS + FAIL + WARN))
291
+ if [[ "$FAIL" -eq 0 ]]; then
292
+ echo "[cortex] ${PASS}/${TOTAL} checks passed"
293
+ else
294
+ echo "[cortex] ${PASS}/${TOTAL} checks passed, ${FAIL} failed, ${WARN} warnings"
295
+ fi
296
+ echo ""
297
+
298
+ exit "$FAIL"