@prads01/blackbox 0.1.0

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 ADDED
@@ -0,0 +1,48 @@
1
+ # BlackBox CLI
2
+
3
+ Developer CLI for running BlackBox analysis against a local web app.
4
+
5
+ ## Install (local dev)
6
+
7
+ ```bash
8
+ cd cli
9
+ npm install
10
+ npm run build
11
+ npm link
12
+ ```
13
+
14
+ Then run:
15
+
16
+ ```bash
17
+ blackbox --help
18
+ ```
19
+
20
+ ## Commands
21
+
22
+ - `blackbox` - launch full-screen BlackBox TUI mode
23
+ - `blackbox ui` - launch full-screen BlackBox TUI mode
24
+ - `blackbox setup` - interactive onboarding and `.blackbox.yaml` generation
25
+ - `blackbox init` - quick default config file creation
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
29
+
30
+ ## TUI shortcuts
31
+
32
+ - `a` run analyze
33
+ - `w` start/stop watch mode
34
+ - `i` incidents view
35
+ - `d` dashboard view
36
+ - `s` sandbox view
37
+ - `x` run sandbox fix for latest incident
38
+ - `r` refresh status/incidents
39
+ - `q` quit
40
+
41
+ ## Publish to npm later
42
+
43
+ ```bash
44
+ cd cli
45
+ npm run build
46
+ npm login
47
+ npm publish --access public
48
+ ```
package/dist/api.js ADDED
@@ -0,0 +1,65 @@
1
+ function isSeverity(value) {
2
+ return value === "low" || value === "medium" || value === "high" || value === "critical";
3
+ }
4
+ function isAnalysisResult(value) {
5
+ if (!value || typeof value !== "object")
6
+ return false;
7
+ const result = value;
8
+ if (typeof result.incidentTitle !== "string")
9
+ return false;
10
+ if (typeof result.impact !== "string")
11
+ return false;
12
+ if (!Array.isArray(result.brokenAssumptions))
13
+ return false;
14
+ if (!result.rootCause || typeof result.rootCause !== "object")
15
+ return false;
16
+ if (!result.recommendedNextAction || typeof result.recommendedNextAction !== "object") {
17
+ return false;
18
+ }
19
+ const assumptions = result.brokenAssumptions;
20
+ const topAssumption = assumptions[0];
21
+ if (topAssumption && !isSeverity(topAssumption.severity))
22
+ return false;
23
+ return true;
24
+ }
25
+ function isAnalyzeApiResponse(value) {
26
+ if (!value || typeof value !== "object")
27
+ return false;
28
+ const payload = value;
29
+ return isAnalysisResult(payload.result);
30
+ }
31
+ export async function callAnalyzeApi(config, requestPayload) {
32
+ const response = await fetch(`${config.api_base_url}/api/analyze`, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ },
37
+ body: JSON.stringify(requestPayload),
38
+ });
39
+ if (!response.ok) {
40
+ throw new Error(`API request failed with status ${response.status}`);
41
+ }
42
+ const payload = (await response.json());
43
+ if (!isAnalyzeApiResponse(payload)) {
44
+ throw new Error("Invalid API response shape");
45
+ }
46
+ return payload.result;
47
+ }
48
+ export async function isApiReachable(config) {
49
+ try {
50
+ const controller = new AbortController();
51
+ const timeout = setTimeout(() => controller.abort(), 4000);
52
+ const response = await fetch(`${config.api_base_url}/api/health`, {
53
+ method: "GET",
54
+ signal: controller.signal,
55
+ });
56
+ clearTimeout(timeout);
57
+ if (!response.ok)
58
+ return false;
59
+ const payload = (await response.json());
60
+ return !!payload && typeof payload === "object" && payload.ok === true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
@@ -0,0 +1,30 @@
1
+ import { callAnalyzeApi } from "../api.js";
2
+ import { readConfig } from "../config.js";
3
+ import { loadEvidenceFromConfig } from "../files.js";
4
+ import { printTruthReport } from "../report.js";
5
+ import { colors, section } from "../utils/format.js";
6
+ export async function runAnalyzeOnce() {
7
+ const config = await readConfig();
8
+ const evidence = await loadEvidenceFromConfig(config);
9
+ const payload = {
10
+ project: {
11
+ name: config.project_name,
12
+ environment: config.environment,
13
+ },
14
+ evidence,
15
+ };
16
+ const result = await callAnalyzeApi(config, payload);
17
+ printTruthReport(result);
18
+ return result;
19
+ }
20
+ export async function runAnalyze() {
21
+ section("Running analysis");
22
+ try {
23
+ await runAnalyzeOnce();
24
+ }
25
+ catch (error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ console.error(colors.bad(`Analyze failed: ${message}`));
28
+ process.exitCode = 1;
29
+ }
30
+ }
@@ -0,0 +1,11 @@
1
+ import { createDefaultConfig, writeConfig } from "../config.js";
2
+ import { colors, row, section } from "../utils/format.js";
3
+ export async function runInit() {
4
+ const config = createDefaultConfig();
5
+ const configPath = await writeConfig(config);
6
+ section("Initialized BlackBox");
7
+ row("Config file:", configPath);
8
+ row("Project:", config.project_name);
9
+ row("Environment:", config.environment);
10
+ console.log(colors.muted("Edit .blackbox.yaml to match your local project paths."));
11
+ }
@@ -0,0 +1,148 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { cancel, intro, isCancel, note, outro, select, text, } from "@clack/prompts";
4
+ import boxen from "boxen";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { createDefaultConfig, readConfig, writeConfig } from "../config.js";
8
+ async function promptText(message, initialValue, validate) {
9
+ const value = await text({
10
+ message,
11
+ initialValue,
12
+ validate,
13
+ });
14
+ if (isCancel(value)) {
15
+ cancel("Setup canceled.");
16
+ process.exit(0);
17
+ }
18
+ return (value ?? "").trim();
19
+ }
20
+ async function detectExistingConfig() {
21
+ try {
22
+ return await readConfig();
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ async function validateOptionalPath(filePath) {
29
+ if (!filePath.trim())
30
+ return "Path cannot be empty.";
31
+ const absolute = path.resolve(process.cwd(), filePath);
32
+ try {
33
+ await fs.access(absolute);
34
+ return null;
35
+ }
36
+ catch {
37
+ return `File not found: ${filePath}`;
38
+ }
39
+ }
40
+ async function checkApiReachability(apiBaseUrl) {
41
+ const controller = new AbortController();
42
+ const timeout = setTimeout(() => controller.abort(), 4000);
43
+ try {
44
+ const response = await fetch(`${apiBaseUrl}/api/health`, {
45
+ method: "GET",
46
+ signal: controller.signal,
47
+ });
48
+ if (!response.ok)
49
+ return false;
50
+ const payload = (await response.json());
51
+ return !!payload && typeof payload === "object" && payload.ok === true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ finally {
57
+ clearTimeout(timeout);
58
+ }
59
+ }
60
+ export async function runSetup() {
61
+ intro(boxen(`${chalk.bold.cyan("BLACKBOX CLI SETUP")}\n${chalk.gray("Initialize local forensic analysis configuration")}`, {
62
+ borderColor: "cyan",
63
+ borderStyle: "round",
64
+ padding: 1,
65
+ }));
66
+ const defaults = createDefaultConfig();
67
+ const existing = await detectExistingConfig();
68
+ const base = existing ?? defaults;
69
+ const projectName = await promptText("Project name", base.project_name, (value) => (value ?? "").trim().length > 0 ? undefined : "Project name is required.");
70
+ const envSelection = await select({
71
+ message: "Environment",
72
+ options: [
73
+ { value: "local", label: "local" },
74
+ { value: "staging", label: "staging" },
75
+ { value: "production", label: "production" },
76
+ { value: "custom", label: "custom" },
77
+ ],
78
+ initialValue: base.environment === "local" ||
79
+ base.environment === "staging" ||
80
+ base.environment === "production"
81
+ ? base.environment
82
+ : "custom",
83
+ });
84
+ if (isCancel(envSelection)) {
85
+ cancel("Setup canceled.");
86
+ process.exit(0);
87
+ }
88
+ const environment = envSelection === "custom"
89
+ ? await promptText("Custom environment name", base.environment, (value) => (value ?? "").trim().length > 0 ? undefined : "Environment is required.")
90
+ : envSelection;
91
+ const apiBaseUrl = await promptText("API base URL", base.api_base_url, (value) => {
92
+ const normalized = (value ?? "").trim();
93
+ if (!normalized)
94
+ return "API base URL is required.";
95
+ try {
96
+ const parsed = new URL(normalized);
97
+ return parsed.protocol === "http:" || parsed.protocol === "https:"
98
+ ? undefined
99
+ : "URL must start with http:// or https://";
100
+ }
101
+ catch {
102
+ return "Enter a valid URL.";
103
+ }
104
+ });
105
+ const logFile = await promptText("Log file path", base.log_file, (value) => (value ?? "").trim().length > 0 ? undefined : "Log file path is required.");
106
+ const expectedFile = await promptText("Expected behavior file path", base.expected_file, (value) => ((value ?? "").trim().length > 0 ? undefined : "Expected file path is required."));
107
+ const configFile = await promptText("Config snapshot file path", base.config_file, (value) => (value ?? "").trim().length > 0 ? undefined : "Config file path is required.");
108
+ const validationSpinner = ora("Validating local file paths").start();
109
+ const pathErrors = [];
110
+ const validations = await Promise.all([
111
+ validateOptionalPath(logFile),
112
+ validateOptionalPath(expectedFile),
113
+ validateOptionalPath(configFile),
114
+ ]);
115
+ for (const maybeError of validations) {
116
+ if (maybeError)
117
+ pathErrors.push(maybeError);
118
+ }
119
+ if (pathErrors.length > 0) {
120
+ validationSpinner.warn("Some files were not found");
121
+ note(pathErrors.join("\n"), "Path Validation");
122
+ }
123
+ else {
124
+ validationSpinner.succeed("Local file paths look good");
125
+ }
126
+ const apiSpinner = ora("Checking API reachability").start();
127
+ const reachable = await checkApiReachability(apiBaseUrl);
128
+ if (reachable) {
129
+ apiSpinner.succeed("BlackBox API is reachable");
130
+ }
131
+ else {
132
+ apiSpinner.warn("Could not reach API at /api/health (continuing)");
133
+ }
134
+ const configToWrite = {
135
+ project_name: projectName,
136
+ environment,
137
+ api_base_url: apiBaseUrl,
138
+ log_file: logFile,
139
+ expected_file: expectedFile,
140
+ config_file: configFile,
141
+ commit_file: base.commit_file || defaults.commit_file,
142
+ };
143
+ const writeSpinner = ora("Writing .blackbox.yaml").start();
144
+ const configPath = await writeConfig(configToWrite);
145
+ writeSpinner.succeed(`Config saved: ${configPath}`);
146
+ note(`${chalk.cyan("blackbox watch")}\n${chalk.cyan("blackbox analyze")}\n${chalk.cyan("blackbox status")}`, "Next Steps");
147
+ outro(chalk.green("BlackBox setup complete."));
148
+ }
@@ -0,0 +1,23 @@
1
+ import { isApiReachable } from "../api.js";
2
+ import { readConfig } from "../config.js";
3
+ import { colors, row, section } from "../utils/format.js";
4
+ export async function runStatus() {
5
+ try {
6
+ const config = await readConfig();
7
+ section("BlackBox Status");
8
+ row("Project:", config.project_name);
9
+ row("Environment:", config.environment);
10
+ row("API base URL:", config.api_base_url);
11
+ row("Log file:", config.log_file);
12
+ row("Expected file:", config.expected_file);
13
+ row("Config file:", config.config_file);
14
+ row("Commit file:", config.commit_file);
15
+ const reachable = await isApiReachable(config);
16
+ row("API reachable:", reachable ? colors.ok("yes") : colors.bad("no"));
17
+ }
18
+ catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ console.error(colors.bad(`Status failed: ${message}`));
21
+ process.exitCode = 1;
22
+ }
23
+ }
@@ -0,0 +1,64 @@
1
+ import chokidar from "chokidar";
2
+ import path from "node:path";
3
+ import { readConfig } from "../config.js";
4
+ import { runAnalyzeOnce } from "./analyze.js";
5
+ import { colors, row, section } from "../utils/format.js";
6
+ const WATCH_DEBOUNCE_MS = 1200;
7
+ const WATCH_COOLDOWN_MS = 2000;
8
+ export async function runWatch() {
9
+ try {
10
+ const config = await readConfig();
11
+ const absoluteLogPath = path.resolve(process.cwd(), config.log_file);
12
+ let isAnalyzing = false;
13
+ let debounceTimer = null;
14
+ let lastRunAt = 0;
15
+ section("BlackBox Watch");
16
+ row("Monitoring:", absoluteLogPath);
17
+ row("API endpoint:", `${config.api_base_url}/api/analyze`);
18
+ console.log(colors.muted("Waiting for log changes...\n"));
19
+ const runAnalysis = async () => {
20
+ const now = Date.now();
21
+ if (isAnalyzing)
22
+ return;
23
+ if (now - lastRunAt < WATCH_COOLDOWN_MS)
24
+ return;
25
+ isAnalyzing = true;
26
+ console.log(colors.title("[DETECTED] Log change detected. Re-running analysis..."));
27
+ try {
28
+ await runAnalyzeOnce();
29
+ }
30
+ catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ console.error(colors.bad(`Analyze failed: ${message}`));
33
+ }
34
+ finally {
35
+ isAnalyzing = false;
36
+ lastRunAt = Date.now();
37
+ }
38
+ };
39
+ const watcher = chokidar.watch(absoluteLogPath, {
40
+ awaitWriteFinish: {
41
+ stabilityThreshold: 1500,
42
+ pollInterval: 100,
43
+ },
44
+ });
45
+ watcher.on("change", () => {
46
+ if (isAnalyzing)
47
+ return;
48
+ if (Date.now() - lastRunAt < WATCH_COOLDOWN_MS)
49
+ return;
50
+ if (debounceTimer) {
51
+ clearTimeout(debounceTimer);
52
+ }
53
+ debounceTimer = setTimeout(() => {
54
+ debounceTimer = null;
55
+ void runAnalysis();
56
+ }, WATCH_DEBOUNCE_MS);
57
+ });
58
+ }
59
+ catch (error) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ console.error(colors.bad(`Watch failed: ${message}`));
62
+ process.exitCode = 1;
63
+ }
64
+ }
package/dist/config.js ADDED
@@ -0,0 +1,51 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import yaml from "js-yaml";
4
+ const CONFIG_FILENAME = ".blackbox.yaml";
5
+ export function getConfigPath(cwd = process.cwd()) {
6
+ return path.join(cwd, CONFIG_FILENAME);
7
+ }
8
+ export function createDefaultConfig(cwd = process.cwd()) {
9
+ return {
10
+ project_name: path.basename(cwd),
11
+ environment: "local",
12
+ api_base_url: "http://localhost:3000",
13
+ log_file: "src/demo/cache-mismatch/logs.txt",
14
+ expected_file: "src/demo/cache-mismatch/expected.md",
15
+ config_file: "src/demo/cache-mismatch/config.json",
16
+ commit_file: "src/demo/cache-mismatch/commit.diff",
17
+ };
18
+ }
19
+ function assertConfigShape(value) {
20
+ if (!value || typeof value !== "object") {
21
+ throw new Error("Invalid .blackbox.yaml: expected object");
22
+ }
23
+ const requiredKeys = [
24
+ "project_name",
25
+ "environment",
26
+ "api_base_url",
27
+ "log_file",
28
+ "expected_file",
29
+ "config_file",
30
+ "commit_file",
31
+ ];
32
+ for (const key of requiredKeys) {
33
+ const field = value[key];
34
+ if (typeof field !== "string" || field.trim() === "") {
35
+ throw new Error(`Invalid .blackbox.yaml: missing or invalid "${key}"`);
36
+ }
37
+ }
38
+ }
39
+ export async function readConfig(cwd = process.cwd()) {
40
+ const configPath = getConfigPath(cwd);
41
+ const raw = await fs.readFile(configPath, "utf8");
42
+ const parsed = yaml.load(raw);
43
+ assertConfigShape(parsed);
44
+ return parsed;
45
+ }
46
+ export async function writeConfig(config, cwd = process.cwd()) {
47
+ const configPath = getConfigPath(cwd);
48
+ const serialized = yaml.dump(config);
49
+ await fs.writeFile(configPath, serialized, "utf8");
50
+ return configPath;
51
+ }
package/dist/files.js ADDED
@@ -0,0 +1,20 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ async function readLocalFile(cwd, filePath) {
4
+ const resolvedPath = path.resolve(cwd, filePath);
5
+ return fs.readFile(resolvedPath, "utf8");
6
+ }
7
+ export async function loadEvidenceFromConfig(config, cwd = process.cwd()) {
8
+ const [logs, expected, configData, commit] = await Promise.all([
9
+ readLocalFile(cwd, config.log_file),
10
+ readLocalFile(cwd, config.expected_file),
11
+ readLocalFile(cwd, config.config_file),
12
+ readLocalFile(cwd, config.commit_file),
13
+ ]);
14
+ return {
15
+ logs,
16
+ expected,
17
+ config: configData,
18
+ commit,
19
+ };
20
+ }
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runAnalyze } from "./commands/analyze.js";
4
+ import { runInit } from "./commands/init.js";
5
+ import { runSetup } from "./commands/setup.js";
6
+ import { runStatus } from "./commands/status.js";
7
+ import { runWatch } from "./commands/watch.js";
8
+ import { runTui } from "./tui/app.js";
9
+ if (process.argv.length <= 2) {
10
+ try {
11
+ await runTui();
12
+ }
13
+ catch (error) {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ console.error(`Unable to launch TUI: ${message}`);
16
+ console.error("Run `blackbox setup` first to create .blackbox.yaml.");
17
+ process.exit(1);
18
+ }
19
+ }
20
+ else {
21
+ const program = new Command();
22
+ program
23
+ .name("blackbox")
24
+ .description("BlackBox local developer CLI")
25
+ .version("0.1.0");
26
+ program
27
+ .command("init")
28
+ .description("Create a .blackbox.yaml file in the current directory")
29
+ .action(async () => {
30
+ await runInit();
31
+ });
32
+ program
33
+ .command("setup")
34
+ .description("Interactive setup for .blackbox.yaml with validation")
35
+ .action(async () => {
36
+ await runSetup();
37
+ });
38
+ program
39
+ .command("ui")
40
+ .description("Launch full-screen BlackBox terminal UI")
41
+ .action(async () => {
42
+ try {
43
+ await runTui();
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ console.error(`Unable to launch TUI: ${message}`);
48
+ console.error("Run `blackbox setup` first to create .blackbox.yaml.");
49
+ process.exitCode = 1;
50
+ }
51
+ });
52
+ program
53
+ .command("analyze")
54
+ .description("Send configured evidence to the local BlackBox analysis API")
55
+ .action(async () => {
56
+ await runAnalyze();
57
+ });
58
+ program
59
+ .command("watch")
60
+ .description("Watch the configured log file and re-run analysis on changes")
61
+ .action(async () => {
62
+ await runWatch();
63
+ });
64
+ program
65
+ .command("status")
66
+ .description("Show active config and API reachability")
67
+ .action(async () => {
68
+ await runStatus();
69
+ });
70
+ await program.parseAsync(process.argv);
71
+ }
package/dist/report.js ADDED
@@ -0,0 +1,36 @@
1
+ import { colors, line, row, section } from "./utils/format.js";
2
+ const SEVERITY_ORDER = {
3
+ low: 1,
4
+ medium: 2,
5
+ high: 3,
6
+ critical: 4,
7
+ };
8
+ function getHighestSeverity(result) {
9
+ const assumptions = result.brokenAssumptions;
10
+ if (assumptions.length === 0)
11
+ return "low";
12
+ return assumptions.reduce((top, item) => {
13
+ return SEVERITY_ORDER[item.severity] > SEVERITY_ORDER[top] ? item.severity : top;
14
+ }, "low");
15
+ }
16
+ function formatSeverity(value) {
17
+ if (value === "critical")
18
+ return colors.bad(value.toUpperCase());
19
+ if (value === "high")
20
+ return colors.warn(value.toUpperCase());
21
+ return colors.muted(value.toUpperCase());
22
+ }
23
+ export function printTruthReport(result) {
24
+ const topAssumption = result.brokenAssumptions[0];
25
+ const severity = getHighestSeverity(result);
26
+ section("BlackBox Truth Report");
27
+ row("Incident:", result.incidentTitle);
28
+ row("Severity:", formatSeverity(severity));
29
+ row("Top contradiction:", topAssumption ? topAssumption.statement : result.summary);
30
+ row("Likely root cause:", result.rootCause.title);
31
+ row("Impact:", result.impact);
32
+ row("Next action:", result.recommendedNextAction.summary);
33
+ line();
34
+ row("Owner:", `${result.recommendedNextAction.owner} (${result.recommendedNextAction.priority})`);
35
+ row("Success criteria:", result.recommendedNextAction.successCriteria);
36
+ }