openreport 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/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/openreport.ts +6 -0
- package/package.json +61 -0
- package/src/agents/api-documentation.ts +66 -0
- package/src/agents/architecture-analyst.ts +46 -0
- package/src/agents/code-quality-reviewer.ts +59 -0
- package/src/agents/dependency-analyzer.ts +51 -0
- package/src/agents/onboarding-guide.ts +59 -0
- package/src/agents/orchestrator.ts +41 -0
- package/src/agents/performance-analyzer.ts +57 -0
- package/src/agents/registry.ts +50 -0
- package/src/agents/security-auditor.ts +61 -0
- package/src/agents/test-coverage-analyst.ts +58 -0
- package/src/agents/todo-generator.ts +50 -0
- package/src/app/App.tsx +151 -0
- package/src/app/theme.ts +54 -0
- package/src/cli.ts +145 -0
- package/src/commands/init.ts +81 -0
- package/src/commands/interactive.tsx +29 -0
- package/src/commands/list.ts +53 -0
- package/src/commands/run.ts +168 -0
- package/src/commands/view.tsx +52 -0
- package/src/components/generation/AgentStatusItem.tsx +125 -0
- package/src/components/generation/AgentStatusList.tsx +70 -0
- package/src/components/generation/ProgressSummary.tsx +107 -0
- package/src/components/generation/StreamingOutput.tsx +154 -0
- package/src/components/layout/Container.tsx +24 -0
- package/src/components/layout/Footer.tsx +52 -0
- package/src/components/layout/Header.tsx +50 -0
- package/src/components/report/MarkdownRenderer.tsx +50 -0
- package/src/components/report/ReportCard.tsx +31 -0
- package/src/components/report/ScrollableView.tsx +164 -0
- package/src/config/cli-detection.ts +130 -0
- package/src/config/cli-model.ts +397 -0
- package/src/config/cli-prompt-formatter.ts +129 -0
- package/src/config/defaults.ts +79 -0
- package/src/config/loader.ts +168 -0
- package/src/config/ollama.ts +48 -0
- package/src/config/providers.ts +199 -0
- package/src/config/resolve-provider.ts +62 -0
- package/src/config/saver.ts +50 -0
- package/src/config/schema.ts +51 -0
- package/src/errors.ts +34 -0
- package/src/hooks/useReportGeneration.ts +199 -0
- package/src/hooks/useTerminalSize.ts +35 -0
- package/src/ingestion/context-selector.ts +247 -0
- package/src/ingestion/file-tree.ts +227 -0
- package/src/ingestion/token-budget.ts +52 -0
- package/src/pipeline/agent-runner.ts +360 -0
- package/src/pipeline/combiner.ts +199 -0
- package/src/pipeline/context.ts +108 -0
- package/src/pipeline/extraction.ts +153 -0
- package/src/pipeline/progress.ts +192 -0
- package/src/pipeline/runner.ts +526 -0
- package/src/report/html-renderer.ts +294 -0
- package/src/report/html-script.ts +123 -0
- package/src/report/html-styles.ts +1127 -0
- package/src/report/md-to-html.ts +153 -0
- package/src/report/open-browser.ts +22 -0
- package/src/schemas/findings.ts +48 -0
- package/src/schemas/report.ts +64 -0
- package/src/screens/ConfigScreen.tsx +271 -0
- package/src/screens/GenerationScreen.tsx +278 -0
- package/src/screens/HistoryScreen.tsx +108 -0
- package/src/screens/HomeScreen.tsx +143 -0
- package/src/screens/ViewerScreen.tsx +82 -0
- package/src/storage/metadata.ts +69 -0
- package/src/storage/report-store.ts +128 -0
- package/src/tools/get-file-tree.ts +157 -0
- package/src/tools/get-git-info.ts +123 -0
- package/src/tools/glob.ts +48 -0
- package/src/tools/grep.ts +149 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-directory.ts +57 -0
- package/src/tools/read-file.ts +52 -0
- package/src/tools/read-package-json.ts +48 -0
- package/src/tools/run-command.ts +154 -0
- package/src/tools/shared-ignore.ts +58 -0
- package/src/types/index.ts +127 -0
- package/src/types/marked-terminal.d.ts +17 -0
- package/src/utils/debug.ts +25 -0
- package/src/utils/file-utils.ts +77 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/grade-colors.ts +43 -0
- package/src/utils/project-detector.ts +296 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import { resolveAndValidatePath } from "../utils/file-utils.js";
|
|
5
|
+
|
|
6
|
+
export function createReadFileTool(projectRoot: string) {
|
|
7
|
+
return tool({
|
|
8
|
+
description:
|
|
9
|
+
"Read the contents of a file. Path must be relative to the project root. Can optionally read a specific range of lines.",
|
|
10
|
+
parameters: z.object({
|
|
11
|
+
path: z.string().describe("Relative file path from project root"),
|
|
12
|
+
maxLines: z
|
|
13
|
+
.number()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Maximum number of lines to read (default: 500)"),
|
|
16
|
+
startLine: z
|
|
17
|
+
.number()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Line number to start reading from (1-indexed)"),
|
|
20
|
+
}),
|
|
21
|
+
execute: async ({ path: filePath, maxLines = 500, startLine = 1 }) => {
|
|
22
|
+
const resolved = resolveAndValidatePath(projectRoot, filePath);
|
|
23
|
+
if (!resolved) {
|
|
24
|
+
return { error: "Path is outside project root" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const stat = await fs.promises.stat(resolved);
|
|
29
|
+
if (stat.isDirectory()) {
|
|
30
|
+
return { error: "Path is a directory, not a file" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const content = await fs.promises.readFile(resolved, "utf-8");
|
|
34
|
+
const lines = content.split("\n");
|
|
35
|
+
const start = Math.max(0, startLine - 1);
|
|
36
|
+
const end = Math.min(lines.length, start + maxLines);
|
|
37
|
+
const selectedLines = lines.slice(start, end);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: selectedLines.join("\n"),
|
|
41
|
+
totalLines: lines.length,
|
|
42
|
+
readFrom: start + 1,
|
|
43
|
+
readTo: end,
|
|
44
|
+
truncated: end < lines.length,
|
|
45
|
+
};
|
|
46
|
+
} catch (err: unknown) {
|
|
47
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
48
|
+
return { error: `Failed to read file: ${msg}` };
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import { resolveAndValidatePath } from "../utils/file-utils.js";
|
|
5
|
+
|
|
6
|
+
export function createReadPackageJsonTool(projectRoot: string) {
|
|
7
|
+
return tool({
|
|
8
|
+
description:
|
|
9
|
+
"Read and parse a package.json (or similar manifest) file. Returns structured data about dependencies, scripts, and metadata.",
|
|
10
|
+
parameters: z.object({
|
|
11
|
+
path: z
|
|
12
|
+
.string()
|
|
13
|
+
.default("package.json")
|
|
14
|
+
.describe("Relative path to manifest file (default: package.json)"),
|
|
15
|
+
}),
|
|
16
|
+
execute: async ({ path: filePath }) => {
|
|
17
|
+
const resolved = resolveAndValidatePath(projectRoot, filePath);
|
|
18
|
+
if (!resolved) {
|
|
19
|
+
return { error: "Path is outside project root" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const content = await fs.promises.readFile(resolved, "utf-8");
|
|
24
|
+
const parsed = JSON.parse(content);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
name: parsed.name,
|
|
28
|
+
version: parsed.version,
|
|
29
|
+
description: parsed.description,
|
|
30
|
+
main: parsed.main,
|
|
31
|
+
type: parsed.type,
|
|
32
|
+
scripts: parsed.scripts || {},
|
|
33
|
+
dependencies: parsed.dependencies || {},
|
|
34
|
+
devDependencies: parsed.devDependencies || {},
|
|
35
|
+
peerDependencies: parsed.peerDependencies || {},
|
|
36
|
+
engines: parsed.engines,
|
|
37
|
+
workspaces: parsed.workspaces,
|
|
38
|
+
dependencyCount:
|
|
39
|
+
Object.keys(parsed.dependencies || {}).length +
|
|
40
|
+
Object.keys(parsed.devDependencies || {}).length,
|
|
41
|
+
};
|
|
42
|
+
} catch (err: unknown) {
|
|
43
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44
|
+
return { error: `Failed to read manifest: ${msg}` };
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { execFile } from "child_process";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { isInsidePath } from "../utils/file-utils.js";
|
|
6
|
+
|
|
7
|
+
const ALLOWED_COMMANDS = new Set([
|
|
8
|
+
"eslint",
|
|
9
|
+
"tsc",
|
|
10
|
+
"npm",
|
|
11
|
+
"bun",
|
|
12
|
+
"yarn",
|
|
13
|
+
"pnpm",
|
|
14
|
+
"node",
|
|
15
|
+
"cat",
|
|
16
|
+
"wc",
|
|
17
|
+
"head",
|
|
18
|
+
"tail",
|
|
19
|
+
"depcheck",
|
|
20
|
+
"license-checker",
|
|
21
|
+
"madge",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const ALLOWED_SUBCOMMANDS: Record<string, Set<string>> = {
|
|
25
|
+
npm: new Set(["audit", "ls", "list", "outdated", "pack", "explain"]),
|
|
26
|
+
yarn: new Set(["audit", "list", "outdated", "info", "why"]),
|
|
27
|
+
pnpm: new Set(["audit", "ls", "list", "outdated", "pack", "explain"]),
|
|
28
|
+
bun: new Set(["pm"]),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
interface ExecError extends Error {
|
|
32
|
+
stdout?: string;
|
|
33
|
+
stderr?: string;
|
|
34
|
+
code?: number | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const FILE_READING_COMMANDS = new Set(["cat", "head", "tail", "wc"]);
|
|
38
|
+
|
|
39
|
+
export function isCommandAllowed(command: string, projectRoot: string): boolean {
|
|
40
|
+
// Block dangerous shell metacharacters
|
|
41
|
+
if (/[;&|`$\n\r><()%]/.test(command) || command.includes("..")) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parts = command.trim().split(/\s+/);
|
|
46
|
+
const base = parts[0];
|
|
47
|
+
|
|
48
|
+
if (!ALLOWED_COMMANDS.has(base)) return false;
|
|
49
|
+
|
|
50
|
+
const subcommands = ALLOWED_SUBCOMMANDS[base];
|
|
51
|
+
if (subcommands) {
|
|
52
|
+
if (parts.length <= 1) return false; // Require a subcommand
|
|
53
|
+
return subcommands.has(parts[1]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// eslint: block --init and --fix (write operations)
|
|
57
|
+
if (base === "eslint") {
|
|
58
|
+
for (const arg of parts.slice(1)) {
|
|
59
|
+
if (arg === "--init" || arg === "--fix" || arg === "--config" || arg === "-c" || arg === "--rulesdir" || arg === "--resolve-plugins-relative-to") return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// tsc: only allow with --noEmit
|
|
64
|
+
if (base === "tsc") {
|
|
65
|
+
if (!parts.includes("--noEmit")) return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// node: only allow --version
|
|
69
|
+
if (base === "node") {
|
|
70
|
+
if (parts.length !== 2 || parts[1] !== "--version") return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate paths for file-reading commands (cat, head, tail)
|
|
74
|
+
if (FILE_READING_COMMANDS.has(base)) {
|
|
75
|
+
const fileArgs = parts.slice(1).filter((a) => !a.startsWith("-"));
|
|
76
|
+
if (fileArgs.length === 0) return false;
|
|
77
|
+
for (const filePath of fileArgs) {
|
|
78
|
+
const resolved = path.resolve(projectRoot, filePath);
|
|
79
|
+
if (!isInsidePath(resolved, projectRoot)) return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createRunCommandTool(projectRoot: string) {
|
|
87
|
+
return tool({
|
|
88
|
+
description:
|
|
89
|
+
"Run an allowed shell command in the project directory. Only safe, read-only commands are permitted (eslint, tsc --noEmit, npm audit, etc.).",
|
|
90
|
+
parameters: z.object({
|
|
91
|
+
command: z.string().describe("The shell command to execute"),
|
|
92
|
+
}),
|
|
93
|
+
execute: async ({ command }) => {
|
|
94
|
+
if (!isCommandAllowed(command, projectRoot)) {
|
|
95
|
+
return {
|
|
96
|
+
error: `Command not allowed. Allowed base commands: ${Array.from(ALLOWED_COMMANDS).join(", ")}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Block dangerous patterns (defense-in-depth, also checked in isCommandAllowed)
|
|
101
|
+
if (/[;&|`$\n\r><()%]/.test(command) || command.includes("..")) {
|
|
102
|
+
return {
|
|
103
|
+
error:
|
|
104
|
+
"Command contains disallowed characters (;, &, |, `, $, \\n, \\r, >, <, (, ), %, ..)",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const parts = command.trim().split(/\s+/);
|
|
110
|
+
const base = parts[0];
|
|
111
|
+
const args = parts.slice(1);
|
|
112
|
+
|
|
113
|
+
const { stdout, stderr } = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
|
114
|
+
execFile(base, args, {
|
|
115
|
+
cwd: projectRoot,
|
|
116
|
+
encoding: "utf-8",
|
|
117
|
+
timeout: 30_000,
|
|
118
|
+
maxBuffer: 1024 * 1024,
|
|
119
|
+
}, (error, stdout, stderr) => {
|
|
120
|
+
if (error) {
|
|
121
|
+
// Attach stdout/stderr to the error for the catch block
|
|
122
|
+
(error as ExecError).stdout = stdout;
|
|
123
|
+
(error as ExecError).stderr = stderr;
|
|
124
|
+
reject(error);
|
|
125
|
+
} else {
|
|
126
|
+
resolve({ stdout, stderr });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
stdout: stdout.slice(0, 10_000),
|
|
133
|
+
truncated: stdout.length > 10_000,
|
|
134
|
+
exitCode: 0,
|
|
135
|
+
};
|
|
136
|
+
} catch (err: unknown) {
|
|
137
|
+
if (err && typeof err === "object" && "stdout" in err) {
|
|
138
|
+
const execErr = err as {
|
|
139
|
+
stdout: string;
|
|
140
|
+
stderr: string;
|
|
141
|
+
code: number | null;
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
stdout: (execErr.stdout || "").slice(0, 5000),
|
|
145
|
+
stderr: (execErr.stderr || "").slice(0, 5000),
|
|
146
|
+
exitCode: execErr.code ?? 1,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
return { error: `Command failed: ${msg}` };
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import ignore from "ignore";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_IGNORE = [
|
|
6
|
+
"node_modules/**",
|
|
7
|
+
".git/**",
|
|
8
|
+
"dist/**",
|
|
9
|
+
"build/**",
|
|
10
|
+
"coverage/**",
|
|
11
|
+
".next/**",
|
|
12
|
+
"*.lock",
|
|
13
|
+
"*.map",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const DEFAULT_IGNORE_GLOBS = DEFAULT_IGNORE.map((p) => new Bun.Glob(p));
|
|
17
|
+
|
|
18
|
+
const gitignoreCache = new Map<string, ReturnType<typeof ignore>>();
|
|
19
|
+
|
|
20
|
+
export async function loadGitignore(projectRoot: string): Promise<ReturnType<typeof ignore>> {
|
|
21
|
+
const cached = gitignoreCache.get(projectRoot);
|
|
22
|
+
if (cached) return cached;
|
|
23
|
+
|
|
24
|
+
const ig = ignore();
|
|
25
|
+
try {
|
|
26
|
+
const content = await fs.promises.readFile(path.join(projectRoot, ".gitignore"), "utf-8");
|
|
27
|
+
ig.add(content);
|
|
28
|
+
} catch {
|
|
29
|
+
// No .gitignore, that's fine
|
|
30
|
+
}
|
|
31
|
+
gitignoreCache.set(projectRoot, ig);
|
|
32
|
+
return ig;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Filter file paths by DEFAULT_IGNORE patterns, gitignore rules, and optional extra patterns.
|
|
37
|
+
* Returns only paths that pass all filters.
|
|
38
|
+
*/
|
|
39
|
+
export async function filterIgnoredFiles(
|
|
40
|
+
files: string[],
|
|
41
|
+
projectRoot: string,
|
|
42
|
+
extraIgnore: string[] = [],
|
|
43
|
+
): Promise<string[]> {
|
|
44
|
+
const ig = await loadGitignore(projectRoot);
|
|
45
|
+
const extraGlobs = extraIgnore.map((p) => new Bun.Glob(p));
|
|
46
|
+
|
|
47
|
+
return files.filter((f) => {
|
|
48
|
+
const normalized = f.replace(/\\/g, "/");
|
|
49
|
+
for (const g of DEFAULT_IGNORE_GLOBS) {
|
|
50
|
+
if (g.match(normalized)) return false;
|
|
51
|
+
}
|
|
52
|
+
for (const g of extraGlobs) {
|
|
53
|
+
if (g.match(normalized)) return false;
|
|
54
|
+
}
|
|
55
|
+
if (ig.ignores(normalized)) return false;
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
FindingSchema,
|
|
4
|
+
SubReportSchema,
|
|
5
|
+
FullReportSchema,
|
|
6
|
+
ReportMetadataSchema,
|
|
7
|
+
} from "../schemas/report.js";
|
|
8
|
+
|
|
9
|
+
// ── Project Classification ──────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface ProjectClassification {
|
|
12
|
+
language: string;
|
|
13
|
+
framework: string | null;
|
|
14
|
+
projectType: "web-app" | "api" | "library" | "cli" | "monorepo" | "other";
|
|
15
|
+
buildTool: string | null;
|
|
16
|
+
packageManager: "npm" | "yarn" | "pnpm" | "bun" | null;
|
|
17
|
+
hasTests: boolean;
|
|
18
|
+
hasCI: boolean;
|
|
19
|
+
hasDocker: boolean;
|
|
20
|
+
testFramework: string | null;
|
|
21
|
+
linter: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Agent Types ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export type AgentId =
|
|
27
|
+
| "architecture-analyst"
|
|
28
|
+
| "security-auditor"
|
|
29
|
+
| "code-quality-reviewer"
|
|
30
|
+
| "dependency-analyzer"
|
|
31
|
+
| "performance-analyzer"
|
|
32
|
+
| "test-coverage-analyst"
|
|
33
|
+
| "api-documentation"
|
|
34
|
+
| "onboarding-guide"
|
|
35
|
+
| "todo-generator";
|
|
36
|
+
|
|
37
|
+
export type AgentRunStatus =
|
|
38
|
+
| "pending"
|
|
39
|
+
| "running"
|
|
40
|
+
| "completed"
|
|
41
|
+
| "failed"
|
|
42
|
+
| "skipped";
|
|
43
|
+
|
|
44
|
+
export interface AgentDefinition {
|
|
45
|
+
id: AgentId;
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
systemPrompt: string;
|
|
49
|
+
maxSteps: number;
|
|
50
|
+
toolSet: "base" | "extended";
|
|
51
|
+
relevantFor: (classification: ProjectClassification) => boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AgentStatus {
|
|
55
|
+
agentId: AgentId;
|
|
56
|
+
agentName: string;
|
|
57
|
+
status: AgentRunStatus;
|
|
58
|
+
statusMessage?: string;
|
|
59
|
+
startedAt?: Date;
|
|
60
|
+
completedAt?: Date;
|
|
61
|
+
tokensUsed?: { input: number; output: number };
|
|
62
|
+
error?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Pipeline Types ──────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export type PipelinePhase =
|
|
68
|
+
| "scanning"
|
|
69
|
+
| "classifying"
|
|
70
|
+
| "pre-reading"
|
|
71
|
+
| "analyzing"
|
|
72
|
+
| "running-agents"
|
|
73
|
+
| "generating-todo"
|
|
74
|
+
| "combining"
|
|
75
|
+
| "saving"
|
|
76
|
+
| "done"
|
|
77
|
+
| "error";
|
|
78
|
+
|
|
79
|
+
export interface PipelineProgress {
|
|
80
|
+
phase: PipelinePhase;
|
|
81
|
+
phaseDetail?: string;
|
|
82
|
+
agents: AgentStatus[];
|
|
83
|
+
totalTokens: { input: number; output: number };
|
|
84
|
+
startedAt: Date;
|
|
85
|
+
estimatedTimeRemaining?: number;
|
|
86
|
+
currentStreamText?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Report Types (inferred from Zod) ────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export type Finding = z.infer<typeof FindingSchema>;
|
|
92
|
+
export type SubReport = z.infer<typeof SubReportSchema>;
|
|
93
|
+
export type FullReport = z.infer<typeof FullReportSchema>;
|
|
94
|
+
export type ReportMetadata = z.infer<typeof ReportMetadataSchema>;
|
|
95
|
+
|
|
96
|
+
// ── Screen Navigation ───────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export type Screen =
|
|
99
|
+
| "home"
|
|
100
|
+
| "config"
|
|
101
|
+
| "generation"
|
|
102
|
+
| "viewer"
|
|
103
|
+
| "history";
|
|
104
|
+
|
|
105
|
+
export interface NavigationState {
|
|
106
|
+
screen: Screen;
|
|
107
|
+
params?: Record<string, string>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Config Types ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export interface ProviderConfig {
|
|
113
|
+
apiKey?: string;
|
|
114
|
+
baseURL?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── File Tree Types ─────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export interface FileTreeNode {
|
|
120
|
+
name: string;
|
|
121
|
+
path: string;
|
|
122
|
+
type: "file" | "directory";
|
|
123
|
+
language?: string;
|
|
124
|
+
size?: number;
|
|
125
|
+
isLarge?: boolean;
|
|
126
|
+
children?: FileTreeNode[];
|
|
127
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare module "marked-terminal" {
|
|
2
|
+
interface TerminalRendererOptions {
|
|
3
|
+
reflowText?: boolean;
|
|
4
|
+
showSectionPrefix?: boolean;
|
|
5
|
+
tab?: number;
|
|
6
|
+
width?: number;
|
|
7
|
+
tableOptions?: {
|
|
8
|
+
chars?: Record<string, string>;
|
|
9
|
+
style?: Record<string, string[]>;
|
|
10
|
+
};
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default class TerminalRenderer {
|
|
15
|
+
constructor(options?: TerminalRendererOptions);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const DEBUG =
|
|
2
|
+
process.env.OPENREPORT_DEBUG === "1" ||
|
|
3
|
+
process.env.OPENREPORT_DEBUG === "true";
|
|
4
|
+
|
|
5
|
+
const REDACT_PATTERNS = [
|
|
6
|
+
/sk-[a-zA-Z0-9]{20,}/g, // OpenAI/Anthropic keys
|
|
7
|
+
/key-[a-zA-Z0-9]{20,}/g, // Generic API keys
|
|
8
|
+
/Bearer\s+[a-zA-Z0-9._-]+/gi, // Bearer tokens
|
|
9
|
+
/AIza[a-zA-Z0-9_-]{35}/g, // Google API keys
|
|
10
|
+
/(?:api[_-]?key|api[_-]?secret|token|secret[_-]?key|access[_-]?key)\s*[:=]\s*["']?[a-zA-Z0-9_-]{20,}["']?/gi, // Generic key=value patterns (covers Mistral and others)
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function sanitize(text: string): string {
|
|
14
|
+
let result = text;
|
|
15
|
+
for (const pattern of REDACT_PATTERNS) {
|
|
16
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function debugLog(context: string, error: unknown): void {
|
|
22
|
+
if (!DEBUG) return;
|
|
23
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
24
|
+
console.error(`[debug] ${context}: ${sanitize(msg)}`);
|
|
25
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { debugLog } from "./debug.js";
|
|
4
|
+
|
|
5
|
+
export function safeReadFile(filePath: string): string | null {
|
|
6
|
+
try {
|
|
7
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
8
|
+
} catch (e) {
|
|
9
|
+
debugLog("file:safeReadFile", e);
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function safeReadFileAsync(filePath: string): Promise<string | null> {
|
|
15
|
+
try {
|
|
16
|
+
return await fs.promises.readFile(filePath, "utf-8");
|
|
17
|
+
} catch (e) {
|
|
18
|
+
debugLog("file:safeReadFileAsync", e);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ensureDir(dirPath: string): void {
|
|
24
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isInsidePath(child: string, parent: string): boolean {
|
|
28
|
+
try {
|
|
29
|
+
const resolvedChild = fs.realpathSync(path.resolve(child));
|
|
30
|
+
const resolvedParent = fs.realpathSync(path.resolve(parent));
|
|
31
|
+
return resolvedChild.startsWith(resolvedParent + path.sep) || resolvedChild === resolvedParent;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveAndValidatePath(projectRoot: string, userPath: string): string | null {
|
|
38
|
+
const resolved = path.resolve(projectRoot, userPath);
|
|
39
|
+
if (!isInsidePath(resolved, projectRoot)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getProjectName(projectRoot: string): string {
|
|
46
|
+
// Try package.json name first
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(
|
|
49
|
+
fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8")
|
|
50
|
+
);
|
|
51
|
+
if (pkg.name) return pkg.name;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
debugLog("file:readPackageJson", e);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fallback to directory name
|
|
57
|
+
return path.basename(projectRoot);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function fileSize(filePath: string): number {
|
|
61
|
+
try {
|
|
62
|
+
return fs.statSync(filePath).size;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
debugLog("file:stat", e);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function fileSizeAsync(filePath: string): Promise<number> {
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.promises.stat(filePath);
|
|
72
|
+
return stat.size;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
debugLog("file:statAsync", e);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function formatDuration(ms: number): string {
|
|
2
|
+
if (ms < 1000) return `${ms}ms`;
|
|
3
|
+
const seconds = Math.floor(ms / 1000);
|
|
4
|
+
if (seconds < 60) return `${seconds}s`;
|
|
5
|
+
const minutes = Math.floor(seconds / 60);
|
|
6
|
+
const remainingSeconds = seconds % 60;
|
|
7
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatTokens(count: number): string {
|
|
11
|
+
if (count < 1000) return String(count);
|
|
12
|
+
if (count < 1_000_000) return `${(count / 1000).toFixed(1)}K`;
|
|
13
|
+
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatDate(date: Date | string): string {
|
|
17
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
18
|
+
return d.toLocaleDateString("en-US", {
|
|
19
|
+
year: "numeric",
|
|
20
|
+
month: "short",
|
|
21
|
+
day: "numeric",
|
|
22
|
+
hour: "2-digit",
|
|
23
|
+
minute: "2-digit",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatRelativeDate(date: Date | string): string {
|
|
28
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
29
|
+
const now = new Date();
|
|
30
|
+
const diffMs = now.getTime() - d.getTime();
|
|
31
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
32
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
33
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
34
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
35
|
+
|
|
36
|
+
if (diffSec < 60) return "just now";
|
|
37
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
38
|
+
if (diffHour < 24) return `${diffHour}h ago`;
|
|
39
|
+
if (diffDay < 7) return `${diffDay}d ago`;
|
|
40
|
+
return formatDate(d);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatFileSize(bytes: number): string {
|
|
44
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
45
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
46
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function truncate(str: string, maxLen: number): string {
|
|
50
|
+
if (str.length <= maxLen) return str;
|
|
51
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getErrorMessage(err: unknown): string {
|
|
55
|
+
return err instanceof Error ? err.message : String(err);
|
|
56
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Terminal color names for Ink/chalk */
|
|
2
|
+
export function getGradeTerminalColor(grade: string): string {
|
|
3
|
+
switch (grade) {
|
|
4
|
+
case "A+":
|
|
5
|
+
case "A":
|
|
6
|
+
return "green";
|
|
7
|
+
case "B+":
|
|
8
|
+
case "B":
|
|
9
|
+
case "B-":
|
|
10
|
+
return "cyan";
|
|
11
|
+
case "C+":
|
|
12
|
+
case "C":
|
|
13
|
+
return "yellow";
|
|
14
|
+
case "D":
|
|
15
|
+
return "red";
|
|
16
|
+
case "F":
|
|
17
|
+
return "redBright";
|
|
18
|
+
default:
|
|
19
|
+
return "white";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Hex color codes for HTML reports */
|
|
24
|
+
export function getGradeHexColor(grade: string): string {
|
|
25
|
+
switch (grade) {
|
|
26
|
+
case "A+":
|
|
27
|
+
case "A":
|
|
28
|
+
return "#00ff88";
|
|
29
|
+
case "B+":
|
|
30
|
+
case "B":
|
|
31
|
+
case "B-":
|
|
32
|
+
return "#22d3ee";
|
|
33
|
+
case "C+":
|
|
34
|
+
case "C":
|
|
35
|
+
return "#fbbf24";
|
|
36
|
+
case "D":
|
|
37
|
+
return "#ff6b6b";
|
|
38
|
+
case "F":
|
|
39
|
+
return "#ff3366";
|
|
40
|
+
default:
|
|
41
|
+
return "#818cf8";
|
|
42
|
+
}
|
|
43
|
+
}
|