@prads01/blackbox 0.1.0 → 0.1.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/README.md +70 -28
- package/dist/commands/analyze.js +2 -2
- package/dist/commands/setup.js +1 -0
- package/dist/commands/status.js +3 -2
- package/dist/commands/watch.js +2 -2
- package/dist/config.js +4 -4
- package/dist/linking.js +71 -0
- package/dist/tui/app.js +26 -7
- package/dist/tui/data.js +75 -2
- package/dist/tui/keybindings.js +26 -0
- package/dist/tui/render.js +98 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,48 +1,90 @@
|
|
|
1
1
|
# BlackBox CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A CLI for real-time incident analysis, sandboxed fixes, and automated PR generation.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What is BlackBox?
|
|
6
|
+
BlackBox is a local incident operations tool for engineering teams. It reads runtime evidence, identifies contradictions between expected and actual behavior, and produces structured analysis that can be verified in a safe sandbox. It is designed to shorten the path from detection to validated fix.
|
|
6
7
|
|
|
8
|
+
## Overview
|
|
9
|
+
- Analyze local web apps via `/api/analyze`
|
|
10
|
+
- Detect and track incidents
|
|
11
|
+
- Generate sandboxed fixes
|
|
12
|
+
- Verify fixes before applying
|
|
13
|
+
- Create GitHub PRs directly
|
|
14
|
+
- Full TUI + CLI support
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
- Detect runtime contradictions from logs
|
|
18
|
+
- AI-powered root cause analysis
|
|
19
|
+
- Sandbox fix verification (safe, isolated)
|
|
20
|
+
- One-click GitHub PR creation
|
|
21
|
+
- Interactive terminal UI (TUI)
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
7
24
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
npm install -g @prads01/blackbox
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
```bash
|
|
30
|
+
blackbox setup
|
|
31
|
+
blackbox
|
|
12
32
|
```
|
|
13
33
|
|
|
14
|
-
|
|
34
|
+
`blackbox setup` initializes and validates `.blackbox.yaml`.
|
|
35
|
+
`blackbox` launches the interactive TUI.
|
|
15
36
|
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
Run analysis:
|
|
16
40
|
```bash
|
|
17
|
-
blackbox
|
|
41
|
+
blackbox analyze
|
|
18
42
|
```
|
|
19
43
|
|
|
20
|
-
|
|
44
|
+
Watch logs:
|
|
45
|
+
```bash
|
|
46
|
+
blackbox watch
|
|
47
|
+
```
|
|
21
48
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- `blackbox analyze` - run one analysis against `/api/analyze`
|
|
27
|
-
- `blackbox watch` - monitor log file and re-run analysis on changes
|
|
28
|
-
- `blackbox status` - show config and API reachability
|
|
49
|
+
Open TUI:
|
|
50
|
+
```bash
|
|
51
|
+
blackbox
|
|
52
|
+
```
|
|
29
53
|
|
|
30
|
-
|
|
54
|
+
TUI navigation basics:
|
|
55
|
+
- Arrow keys to move
|
|
56
|
+
- Enter to open/execute
|
|
57
|
+
- Backspace to return from detail views
|
|
31
58
|
|
|
32
|
-
|
|
33
|
-
- `
|
|
34
|
-
- `i` incidents
|
|
35
|
-
- `
|
|
36
|
-
- `
|
|
37
|
-
- `
|
|
38
|
-
- `
|
|
39
|
-
- `q` quit
|
|
59
|
+
Key shortcuts:
|
|
60
|
+
- `a` → analyze
|
|
61
|
+
- `i` → incidents
|
|
62
|
+
- `s` → sandbox
|
|
63
|
+
- `x` → run fix
|
|
64
|
+
- `c` → create PR
|
|
65
|
+
- `q` → quit
|
|
40
66
|
|
|
41
|
-
##
|
|
67
|
+
## Workflow
|
|
68
|
+
1. BlackBox detects an incident.
|
|
69
|
+
2. AI analyzes likely root cause.
|
|
70
|
+
3. Sandbox verifies a fix safely.
|
|
71
|
+
4. User creates a GitHub PR.
|
|
72
|
+
5. Incident is marked resolved.
|
|
42
73
|
|
|
74
|
+
## Configuration
|
|
75
|
+
BlackBox uses `.blackbox.yaml` for local runtime configuration. Core fields:
|
|
76
|
+
- project name
|
|
77
|
+
- API base URL
|
|
78
|
+
- log path
|
|
79
|
+
|
|
80
|
+
## Development
|
|
43
81
|
```bash
|
|
82
|
+
git clone <repo>
|
|
44
83
|
cd cli
|
|
84
|
+
npm install
|
|
45
85
|
npm run build
|
|
46
|
-
npm
|
|
47
|
-
npm publish --access public
|
|
86
|
+
npm link
|
|
48
87
|
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
MIT
|
package/dist/commands/analyze.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { callAnalyzeApi } from "../api.js";
|
|
2
|
-
import { readConfig } from "../config.js";
|
|
3
2
|
import { loadEvidenceFromConfig } from "../files.js";
|
|
3
|
+
import { ensureRepoLinkedForCli } from "../linking.js";
|
|
4
4
|
import { printTruthReport } from "../report.js";
|
|
5
5
|
import { colors, section } from "../utils/format.js";
|
|
6
6
|
export async function runAnalyzeOnce() {
|
|
7
|
-
const config = await
|
|
7
|
+
const config = await ensureRepoLinkedForCli();
|
|
8
8
|
const evidence = await loadEvidenceFromConfig(config);
|
|
9
9
|
const payload = {
|
|
10
10
|
project: {
|
package/dist/commands/setup.js
CHANGED
|
@@ -139,6 +139,7 @@ export async function runSetup() {
|
|
|
139
139
|
expected_file: expectedFile,
|
|
140
140
|
config_file: configFile,
|
|
141
141
|
commit_file: base.commit_file || defaults.commit_file,
|
|
142
|
+
linked_repo: base.linked_repo,
|
|
142
143
|
};
|
|
143
144
|
const writeSpinner = ora("Writing .blackbox.yaml").start();
|
|
144
145
|
const configPath = await writeConfig(configToWrite);
|
package/dist/commands/status.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isApiReachable } from "../api.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ensureRepoLinkedForCli } from "../linking.js";
|
|
3
3
|
import { colors, row, section } from "../utils/format.js";
|
|
4
4
|
export async function runStatus() {
|
|
5
5
|
try {
|
|
6
|
-
const config = await
|
|
6
|
+
const config = await ensureRepoLinkedForCli();
|
|
7
7
|
section("BlackBox Status");
|
|
8
8
|
row("Project:", config.project_name);
|
|
9
9
|
row("Environment:", config.environment);
|
|
@@ -12,6 +12,7 @@ export async function runStatus() {
|
|
|
12
12
|
row("Expected file:", config.expected_file);
|
|
13
13
|
row("Config file:", config.config_file);
|
|
14
14
|
row("Commit file:", config.commit_file);
|
|
15
|
+
row("Linked repo:", config.linked_repo ?? colors.warn("not linked"));
|
|
15
16
|
const reachable = await isApiReachable(config);
|
|
16
17
|
row("API reachable:", reachable ? colors.ok("yes") : colors.bad("no"));
|
|
17
18
|
}
|
package/dist/commands/watch.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import chokidar from "chokidar";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureRepoLinkedForCli } from "../linking.js";
|
|
4
4
|
import { runAnalyzeOnce } from "./analyze.js";
|
|
5
5
|
import { colors, row, section } from "../utils/format.js";
|
|
6
6
|
const WATCH_DEBOUNCE_MS = 1200;
|
|
7
7
|
const WATCH_COOLDOWN_MS = 2000;
|
|
8
8
|
export async function runWatch() {
|
|
9
9
|
try {
|
|
10
|
-
const config = await
|
|
10
|
+
const config = await ensureRepoLinkedForCli();
|
|
11
11
|
const absoluteLogPath = path.resolve(process.cwd(), config.log_file);
|
|
12
12
|
let isAnalyzing = false;
|
|
13
13
|
let debounceTimer = null;
|
package/dist/config.js
CHANGED
|
@@ -10,10 +10,10 @@ export function createDefaultConfig(cwd = process.cwd()) {
|
|
|
10
10
|
project_name: path.basename(cwd),
|
|
11
11
|
environment: "local",
|
|
12
12
|
api_base_url: "http://localhost:3000",
|
|
13
|
-
log_file: "
|
|
14
|
-
expected_file: "
|
|
15
|
-
config_file: "
|
|
16
|
-
commit_file: "
|
|
13
|
+
log_file: "demo-system/logs/app.log",
|
|
14
|
+
expected_file: "demo-system/config/service-config.json",
|
|
15
|
+
config_file: "demo-system/state/system-state.json",
|
|
16
|
+
commit_file: "demo-system/config/recent-change.json",
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
function assertConfigShape(value) {
|
package/dist/linking.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readConfig, writeConfig } from "./config.js";
|
|
3
|
+
import { colors } from "./utils/format.js";
|
|
4
|
+
function openBrowser(url) {
|
|
5
|
+
const platform = process.platform;
|
|
6
|
+
if (platform === "darwin") {
|
|
7
|
+
const child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
8
|
+
child.unref();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (platform === "win32") {
|
|
12
|
+
const child = spawn("cmd", ["/c", "start", "", url], {
|
|
13
|
+
detached: true,
|
|
14
|
+
stdio: "ignore",
|
|
15
|
+
});
|
|
16
|
+
child.unref();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
20
|
+
child.unref();
|
|
21
|
+
}
|
|
22
|
+
async function startLinkFlow(config) {
|
|
23
|
+
const response = await fetch(`${config.api_base_url}/api/cli/link/start`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Unable to start CLI link flow (${response.status}).`);
|
|
28
|
+
}
|
|
29
|
+
return (await response.json());
|
|
30
|
+
}
|
|
31
|
+
async function getLinkStatus(config, token) {
|
|
32
|
+
const response = await fetch(`${config.api_base_url}/api/cli/link/${token}`, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Unable to fetch CLI link status (${response.status}).`);
|
|
37
|
+
}
|
|
38
|
+
return (await response.json());
|
|
39
|
+
}
|
|
40
|
+
export async function ensureRepoLinkedForCli() {
|
|
41
|
+
const config = await readConfig();
|
|
42
|
+
if (config.linked_repo && config.linked_repo.trim().length > 0) {
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
console.log(colors.warn("No linked GitHub repo found for this CLI config."));
|
|
46
|
+
console.log(colors.muted("Starting browser-based auth and repo linking flow..."));
|
|
47
|
+
const start = await startLinkFlow(config);
|
|
48
|
+
console.log(colors.title(`Open this URL if browser does not open:\n${start.linkUrl}`));
|
|
49
|
+
try {
|
|
50
|
+
openBrowser(start.linkUrl);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// no-op, URL is already printed
|
|
54
|
+
}
|
|
55
|
+
const maxAttempts = 120;
|
|
56
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
58
|
+
const status = await getLinkStatus(config, start.token);
|
|
59
|
+
if (status.status !== "completed" || !status.repo)
|
|
60
|
+
continue;
|
|
61
|
+
const linkedRepo = status.repo.fullName;
|
|
62
|
+
const nextConfig = {
|
|
63
|
+
...config,
|
|
64
|
+
linked_repo: linkedRepo,
|
|
65
|
+
};
|
|
66
|
+
await writeConfig(nextConfig);
|
|
67
|
+
console.log(colors.ok(`Linked repository: ${linkedRepo}`));
|
|
68
|
+
return nextConfig;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("CLI link timed out. Complete browser auth and retry.");
|
|
71
|
+
}
|
package/dist/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import blessed from "blessed";
|
|
2
|
-
import { fetchApiReachability, fetchIncidents, loadTuiConfig,
|
|
2
|
+
import { fetchAnalysisRun, fetchApiReachability, fetchIncidents, loadTuiConfig, startAnalysisRun, triggerCreatePr, triggerSandboxFix, updateIncidentStatus, } from "./data.js";
|
|
3
3
|
import { attachKeybindings } from "./keybindings.js";
|
|
4
4
|
import { indexToView, palette, renderFooter, renderMain, renderSidebar, viewToIndex, } from "./render.js";
|
|
5
5
|
import { TuiWatchService } from "./watch-service.js";
|
|
@@ -27,6 +27,7 @@ function createInitialState(config) {
|
|
|
27
27
|
incidents: [],
|
|
28
28
|
selectedIncidentIndex: 0,
|
|
29
29
|
incidentsMode: "list",
|
|
30
|
+
contextSourceView: "logs",
|
|
30
31
|
sandboxMode: "list",
|
|
31
32
|
selectedSandboxIndex: 0,
|
|
32
33
|
lastSandboxIncidentId: null,
|
|
@@ -39,6 +40,7 @@ function createInitialState(config) {
|
|
|
39
40
|
sandboxPrError: null,
|
|
40
41
|
sandboxPrInFlight: false,
|
|
41
42
|
analyzeInFlight: false,
|
|
43
|
+
currentAnalysisRun: null,
|
|
42
44
|
busyMessage: null,
|
|
43
45
|
statusMessage: null,
|
|
44
46
|
errorMessage: null,
|
|
@@ -220,12 +222,29 @@ export async function runTui() {
|
|
|
220
222
|
state.analyzeInFlight = true;
|
|
221
223
|
let didSucceed = false;
|
|
222
224
|
await withBusy("Analyzing...", async () => {
|
|
223
|
-
await
|
|
224
|
-
state.
|
|
225
|
-
|
|
226
|
-
state.
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
const run = await startAnalysisRun(config);
|
|
226
|
+
state.currentAnalysisRun = run;
|
|
227
|
+
state.view = "analyze";
|
|
228
|
+
state.selectedNavIndex = viewToIndex("analyze");
|
|
229
|
+
render();
|
|
230
|
+
while (true) {
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
232
|
+
const next = await fetchAnalysisRun(config, run.id);
|
|
233
|
+
state.currentAnalysisRun = next;
|
|
234
|
+
render();
|
|
235
|
+
if (next.status === "running")
|
|
236
|
+
continue;
|
|
237
|
+
if (next.status === "failed") {
|
|
238
|
+
state.errorMessage = next.error ?? "Analysis run failed.";
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
didSucceed = true;
|
|
242
|
+
state.lastAnalysisAt = new Date().toISOString();
|
|
243
|
+
await refreshData();
|
|
244
|
+
state.view = "dashboard";
|
|
245
|
+
state.selectedNavIndex = viewToIndex("dashboard");
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
229
248
|
});
|
|
230
249
|
state.analyzeInFlight = false;
|
|
231
250
|
if (didSucceed) {
|
package/dist/tui/data.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isApiReachable } from "../api.js";
|
|
2
|
-
import { readConfig } from "../config.js";
|
|
3
2
|
import { loadEvidenceFromConfig } from "../files.js";
|
|
3
|
+
import { ensureRepoLinkedForCli } from "../linking.js";
|
|
4
4
|
function assertIncidentsResponse(value) {
|
|
5
5
|
if (!value || typeof value !== "object")
|
|
6
6
|
return false;
|
|
@@ -25,11 +25,21 @@ function assertIncidentsResponse(value) {
|
|
|
25
25
|
typeof i.impact === "string" &&
|
|
26
26
|
typeof i.nextAction === "string" &&
|
|
27
27
|
(i.evidencePoints === undefined || Array.isArray(i.evidencePoints)) &&
|
|
28
|
+
(i.investigation === undefined ||
|
|
29
|
+
(typeof i.investigation === "object" &&
|
|
30
|
+
i.investigation !== null &&
|
|
31
|
+
typeof i.investigation.investigationSummary ===
|
|
32
|
+
"string" &&
|
|
33
|
+
Array.isArray(i.investigation.evidence) &&
|
|
34
|
+
typeof i.investigation.confidence === "number")) &&
|
|
35
|
+
(i.investigationTimeline === undefined || Array.isArray(i.investigationTimeline)) &&
|
|
36
|
+
(i.investigationTools === undefined || Array.isArray(i.investigationTools)) &&
|
|
37
|
+
(i.toolOutputsSummary === undefined || Array.isArray(i.toolOutputsSummary)) &&
|
|
28
38
|
typeof i.timestamp === "string");
|
|
29
39
|
});
|
|
30
40
|
}
|
|
31
41
|
export async function loadTuiConfig() {
|
|
32
|
-
return
|
|
42
|
+
return ensureRepoLinkedForCli();
|
|
33
43
|
}
|
|
34
44
|
export async function fetchApiReachability(config) {
|
|
35
45
|
return isApiReachable(config);
|
|
@@ -80,6 +90,69 @@ export async function triggerAnalysis(config) {
|
|
|
80
90
|
}
|
|
81
91
|
return result;
|
|
82
92
|
}
|
|
93
|
+
function assertAnalysisRun(value) {
|
|
94
|
+
if (!value || typeof value !== "object")
|
|
95
|
+
return false;
|
|
96
|
+
const candidate = value;
|
|
97
|
+
if (typeof candidate.id !== "string" ||
|
|
98
|
+
(candidate.status !== "running" &&
|
|
99
|
+
candidate.status !== "completed" &&
|
|
100
|
+
candidate.status !== "failed") ||
|
|
101
|
+
typeof candidate.startedAt !== "string" ||
|
|
102
|
+
!Array.isArray(candidate.steps)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return candidate.steps.every((step) => {
|
|
106
|
+
if (!step || typeof step !== "object")
|
|
107
|
+
return false;
|
|
108
|
+
const s = step;
|
|
109
|
+
return (typeof s.id === "string" &&
|
|
110
|
+
typeof s.label === "string" &&
|
|
111
|
+
(s.status === "pending" || s.status === "running" || s.status === "completed"));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export async function startAnalysisRun(config) {
|
|
115
|
+
const evidence = await loadEvidenceFromConfig(config);
|
|
116
|
+
const payload = {
|
|
117
|
+
project: {
|
|
118
|
+
name: config.project_name,
|
|
119
|
+
environment: config.environment,
|
|
120
|
+
},
|
|
121
|
+
evidence,
|
|
122
|
+
};
|
|
123
|
+
const response = await fetch(`${config.api_base_url}/api/analyze/run`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify(payload),
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const body = await response.text();
|
|
132
|
+
throw new Error(`Analyze run start failed (${response.status}): ${body}`);
|
|
133
|
+
}
|
|
134
|
+
const payloadJson = (await response.json());
|
|
135
|
+
const run = payloadJson.run;
|
|
136
|
+
if (!assertAnalysisRun(run)) {
|
|
137
|
+
throw new Error("Analyze run start returned invalid payload.");
|
|
138
|
+
}
|
|
139
|
+
return run;
|
|
140
|
+
}
|
|
141
|
+
export async function fetchAnalysisRun(config, runId) {
|
|
142
|
+
const response = await fetch(`${config.api_base_url}/api/analyze/run/${runId}`, {
|
|
143
|
+
method: "GET",
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const body = await response.text();
|
|
147
|
+
throw new Error(`Analyze run fetch failed (${response.status}): ${body}`);
|
|
148
|
+
}
|
|
149
|
+
const payload = (await response.json());
|
|
150
|
+
const run = payload.run;
|
|
151
|
+
if (!assertAnalysisRun(run)) {
|
|
152
|
+
throw new Error("Analyze run status payload invalid.");
|
|
153
|
+
}
|
|
154
|
+
return run;
|
|
155
|
+
}
|
|
83
156
|
function assertSandboxResult(value) {
|
|
84
157
|
if (!value || typeof value !== "object")
|
|
85
158
|
return false;
|
package/dist/tui/keybindings.js
CHANGED
|
@@ -56,6 +56,7 @@ export function attachKeybindings(screen, state, actions) {
|
|
|
56
56
|
}
|
|
57
57
|
if (state.view === "incidents" && state.incidentsMode === "list" && state.incidents.length > 0) {
|
|
58
58
|
state.incidentsMode = "detail";
|
|
59
|
+
state.contextSourceView = "logs";
|
|
59
60
|
actions.render();
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
@@ -82,9 +83,34 @@ export function attachKeybindings(screen, state, actions) {
|
|
|
82
83
|
screen.key(["i"], () => {
|
|
83
84
|
state.view = "incidents";
|
|
84
85
|
state.incidentsMode = "list";
|
|
86
|
+
state.contextSourceView = "logs";
|
|
85
87
|
state.selectedNavIndex = 1;
|
|
86
88
|
actions.render();
|
|
87
89
|
});
|
|
90
|
+
screen.key(["1"], () => {
|
|
91
|
+
if (state.view === "incidents" && state.incidentsMode === "detail") {
|
|
92
|
+
state.contextSourceView = "logs";
|
|
93
|
+
actions.render();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
screen.key(["2"], () => {
|
|
97
|
+
if (state.view === "incidents" && state.incidentsMode === "detail") {
|
|
98
|
+
state.contextSourceView = "config";
|
|
99
|
+
actions.render();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
screen.key(["3"], () => {
|
|
103
|
+
if (state.view === "incidents" && state.incidentsMode === "detail") {
|
|
104
|
+
state.contextSourceView = "commits";
|
|
105
|
+
actions.render();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
screen.key(["4"], () => {
|
|
109
|
+
if (state.view === "incidents" && state.incidentsMode === "detail") {
|
|
110
|
+
state.contextSourceView = "system_state";
|
|
111
|
+
actions.render();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
88
114
|
screen.key(["s"], () => {
|
|
89
115
|
state.view = "sandbox";
|
|
90
116
|
state.sandboxMode = "list";
|
package/dist/tui/render.js
CHANGED
|
@@ -108,6 +108,48 @@ function renderIncidentDetail(state) {
|
|
|
108
108
|
if (!incident) {
|
|
109
109
|
return "Incident not found.";
|
|
110
110
|
}
|
|
111
|
+
const investigation = incident.investigation;
|
|
112
|
+
const evidence = investigation?.evidence && investigation.evidence.length > 0
|
|
113
|
+
? investigation.evidence
|
|
114
|
+
: incident.evidencePoints ?? [];
|
|
115
|
+
const timeline = incident.investigationTimeline ?? [];
|
|
116
|
+
const investigationTools = incident.investigationTools ?? [];
|
|
117
|
+
const context = incident.context;
|
|
118
|
+
const contextHeader = [
|
|
119
|
+
"{bold}{white-fg}CONTEXT SOURCES{/white-fg}{/bold}",
|
|
120
|
+
`[1] Logs [2] Config [3] Commits [4] System State (active: ${state.contextSourceView})`,
|
|
121
|
+
];
|
|
122
|
+
const contextBody = (() => {
|
|
123
|
+
if (!context)
|
|
124
|
+
return ["No context data available for this incident."];
|
|
125
|
+
if (state.contextSourceView === "logs") {
|
|
126
|
+
const logs = context.logs?.slice(-12) ?? [];
|
|
127
|
+
return logs.length > 0 ? logs : ["No log lines available."];
|
|
128
|
+
}
|
|
129
|
+
if (state.contextSourceView === "config") {
|
|
130
|
+
return [
|
|
131
|
+
`Service: ${context.systemState.service || "N/A"}`,
|
|
132
|
+
`Environment: ${context.systemState.environment || "N/A"}`,
|
|
133
|
+
`Expected Topic: ${context.systemState.invalidationTopicExpected || "N/A"}`,
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
if (state.contextSourceView === "commits") {
|
|
137
|
+
const commits = context.recentCommits ?? [];
|
|
138
|
+
if (commits.length === 0)
|
|
139
|
+
return ["No commit context available."];
|
|
140
|
+
return commits.slice(0, 5).flatMap((commit) => [
|
|
141
|
+
`${commit.sha.slice(0, 7)} ${commit.message}`,
|
|
142
|
+
` ${commit.summary}`,
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
return [
|
|
146
|
+
`Cache Version: ${context.systemState.cacheVersion || "N/A"}`,
|
|
147
|
+
`DB Version: ${context.systemState.dbVersion || "N/A"}`,
|
|
148
|
+
`Observed Topic: ${context.systemState.invalidationTopicObserved || "N/A"}`,
|
|
149
|
+
`Expected Topic: ${context.systemState.invalidationTopicExpected || "N/A"}`,
|
|
150
|
+
`Error Rate: ${context.systemState.recentErrorRate || "N/A"}`,
|
|
151
|
+
];
|
|
152
|
+
})();
|
|
111
153
|
return [
|
|
112
154
|
"{bold}{white-fg}Incident Report{/white-fg}{/bold}",
|
|
113
155
|
"{gray-fg}Backspace to return to incidents list.{/gray-fg}",
|
|
@@ -123,6 +165,10 @@ function renderIncidentDetail(state) {
|
|
|
123
165
|
"{bold}{white-fg}Root Cause{/white-fg}{/bold}",
|
|
124
166
|
incident.rootCause,
|
|
125
167
|
"",
|
|
168
|
+
"{bold}{white-fg}Investigation Summary{/white-fg}{/bold}",
|
|
169
|
+
investigation?.investigationSummary ?? "No synthesized investigation summary available.",
|
|
170
|
+
`{bold}Confidence{/bold}: ${investigation ? `${Math.round(investigation.confidence * 100)}%` : "N/A"}`,
|
|
171
|
+
"",
|
|
126
172
|
"{bold}{white-fg}Impact{/white-fg}{/bold}",
|
|
127
173
|
incident.impact,
|
|
128
174
|
"",
|
|
@@ -130,15 +176,37 @@ function renderIncidentDetail(state) {
|
|
|
130
176
|
incident.fixSuggestion?.trim() || "No fix suggestion available.",
|
|
131
177
|
"",
|
|
132
178
|
"{bold}{white-fg}Evidence Points{/white-fg}{/bold}",
|
|
133
|
-
...(
|
|
134
|
-
?
|
|
179
|
+
...(evidence.length
|
|
180
|
+
? evidence.slice(0, 5).map((point) => `- ${point}`)
|
|
135
181
|
: ["- No evidence points available."]),
|
|
182
|
+
"",
|
|
183
|
+
"{bold}{white-fg}Investigation Timeline{/white-fg}{/bold}",
|
|
184
|
+
...(timeline.length
|
|
185
|
+
? timeline.slice(0, 8).map((entry) => {
|
|
186
|
+
const source = entry.source ? ` [${entry.source}]` : "";
|
|
187
|
+
return `${entry.step}. ${entry.title}${source}\n ${entry.detail}`;
|
|
188
|
+
})
|
|
189
|
+
: ["No timeline steps available."]),
|
|
190
|
+
"",
|
|
191
|
+
"{bold}{white-fg}Investigation Tools{/white-fg}{/bold}",
|
|
192
|
+
...(investigationTools.length
|
|
193
|
+
? investigationTools.map((tool) => {
|
|
194
|
+
const status = tool.status === "completed"
|
|
195
|
+
? "{green-fg}completed{/green-fg}"
|
|
196
|
+
: "{red-fg}failed{/red-fg}";
|
|
197
|
+
return `- ${tool.name} [${status}]\n ${tool.summary}`;
|
|
198
|
+
})
|
|
199
|
+
: ["No tool execution records available."]),
|
|
200
|
+
"",
|
|
201
|
+
...contextHeader,
|
|
202
|
+
...contextBody,
|
|
136
203
|
].join("\n");
|
|
137
204
|
}
|
|
138
205
|
function renderIncidents(state) {
|
|
139
206
|
return state.incidentsMode === "detail" ? renderIncidentDetail(state) : renderIncidentList(state);
|
|
140
207
|
}
|
|
141
208
|
function renderAnalyze(state) {
|
|
209
|
+
const run = state.currentAnalysisRun;
|
|
142
210
|
return [
|
|
143
211
|
"{bold}{white-fg}Analyze{/white-fg}{/bold}",
|
|
144
212
|
"",
|
|
@@ -147,6 +215,29 @@ function renderAnalyze(state) {
|
|
|
147
215
|
"",
|
|
148
216
|
"Press {bold}a{/bold} to trigger analysis once.",
|
|
149
217
|
"Repeated keypresses are ignored while analysis is running.",
|
|
218
|
+
"",
|
|
219
|
+
"{bold}{white-fg}Investigation Activity{/white-fg}{/bold}",
|
|
220
|
+
...(run
|
|
221
|
+
? [
|
|
222
|
+
`Run ID: ${run.id}`,
|
|
223
|
+
`Run Status: ${run.status === "completed"
|
|
224
|
+
? "{green-fg}COMPLETED{/green-fg}"
|
|
225
|
+
: run.status === "failed"
|
|
226
|
+
? "{red-fg}FAILED{/red-fg}"
|
|
227
|
+
: "{yellow-fg}RUNNING{/yellow-fg}"}`,
|
|
228
|
+
...(run.incidentId ? [`Incident: ${run.incidentId}`] : []),
|
|
229
|
+
"",
|
|
230
|
+
...run.steps.map((step, index) => {
|
|
231
|
+
const status = step.status === "completed"
|
|
232
|
+
? "{green-fg}completed{/green-fg}"
|
|
233
|
+
: step.status === "running"
|
|
234
|
+
? "{yellow-fg}running{/yellow-fg}"
|
|
235
|
+
: "{gray-fg}pending{/gray-fg}";
|
|
236
|
+
const detail = step.detail ? `\n ${step.detail}` : "";
|
|
237
|
+
return `${index + 1}. ${step.label} [${status}]${detail}`;
|
|
238
|
+
}),
|
|
239
|
+
]
|
|
240
|
+
: ["No active investigation run."]),
|
|
150
241
|
].join("\n");
|
|
151
242
|
}
|
|
152
243
|
function renderWatch(state) {
|
|
@@ -269,9 +360,12 @@ export function renderMain(main, state) {
|
|
|
269
360
|
}
|
|
270
361
|
export function renderFooter(footer, state) {
|
|
271
362
|
const hotkeysBase = "a analyze | w watch | i incidents | d dashboard | s sandbox | x sandbox-fix | f mark-fixed | v mark-investigating";
|
|
363
|
+
const contextHotkeys = state.view === "incidents" && state.incidentsMode === "detail"
|
|
364
|
+
? " | 1 logs | 2 config | 3 commits | 4 system"
|
|
365
|
+
: "";
|
|
272
366
|
const hotkeys = canCreatePrFromSandbox(state)
|
|
273
|
-
? `${hotkeysBase} | c create-pr | r refresh | q quit`
|
|
274
|
-
: `${hotkeysBase} | r refresh | q quit`;
|
|
367
|
+
? `${hotkeysBase}${contextHotkeys} | c create-pr | r refresh | q quit`
|
|
368
|
+
: `${hotkeysBase}${contextHotkeys} | r refresh | q quit`;
|
|
275
369
|
const status = [
|
|
276
370
|
`focus:${state.focusZone.toUpperCase()}`,
|
|
277
371
|
`api:${state.apiReachable ? "up" : "down"}`,
|