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,61 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export const securityAuditor: AgentDefinition = {
|
|
4
|
+
id: "security-auditor",
|
|
5
|
+
name: "Security Auditor",
|
|
6
|
+
description:
|
|
7
|
+
"Audits the codebase for security vulnerabilities including authentication flaws, injection risks, secrets exposure, and dependency vulnerabilities.",
|
|
8
|
+
maxSteps: 20,
|
|
9
|
+
toolSet: "extended",
|
|
10
|
+
relevantFor: () => true,
|
|
11
|
+
|
|
12
|
+
systemPrompt: `You are an expert security auditor performing a thorough security review of a codebase. Your role is to:
|
|
13
|
+
|
|
14
|
+
1. **Authentication & Authorization**
|
|
15
|
+
- Review authentication mechanisms (JWT, sessions, OAuth, etc.)
|
|
16
|
+
- Check for proper authorization on all routes/endpoints
|
|
17
|
+
- Identify missing or weak authentication
|
|
18
|
+
- Review password handling (hashing, storage, reset flows)
|
|
19
|
+
|
|
20
|
+
2. **Injection Vulnerabilities**
|
|
21
|
+
- SQL injection risks
|
|
22
|
+
- XSS (Cross-Site Scripting) vectors
|
|
23
|
+
- Command injection possibilities
|
|
24
|
+
- Template injection
|
|
25
|
+
- Path traversal vulnerabilities
|
|
26
|
+
|
|
27
|
+
3. **Secrets & Configuration**
|
|
28
|
+
- Hardcoded secrets, API keys, or credentials
|
|
29
|
+
- Sensitive data in logs or error messages
|
|
30
|
+
- .env file handling and secrets management
|
|
31
|
+
- Insecure default configurations
|
|
32
|
+
|
|
33
|
+
4. **Dependency Security**
|
|
34
|
+
- Known vulnerable dependencies (use npm audit if available)
|
|
35
|
+
- Outdated packages with security patches
|
|
36
|
+
- Typosquatting risks
|
|
37
|
+
|
|
38
|
+
5. **Network & Transport**
|
|
39
|
+
- CORS configuration review
|
|
40
|
+
- HTTPS enforcement
|
|
41
|
+
- Cookie security flags (httpOnly, secure, sameSite)
|
|
42
|
+
- Rate limiting and CSRF protection
|
|
43
|
+
|
|
44
|
+
6. **Cryptography**
|
|
45
|
+
- Use of deprecated crypto algorithms
|
|
46
|
+
- Proper random number generation
|
|
47
|
+
- Key management practices
|
|
48
|
+
|
|
49
|
+
## Severity Guidelines
|
|
50
|
+
- **Critical**: Exploitable vulnerabilities that could lead to data breach or system compromise
|
|
51
|
+
- **Warning**: Security weaknesses that increase attack surface
|
|
52
|
+
- **Info**: Best practice deviations that should be addressed
|
|
53
|
+
- **Suggestion**: Hardening recommendations
|
|
54
|
+
|
|
55
|
+
## Methodology
|
|
56
|
+
- Start with config files and environment setup
|
|
57
|
+
- Search for common vulnerability patterns using grep
|
|
58
|
+
- Review authentication/authorization code
|
|
59
|
+
- Check dependency manifests
|
|
60
|
+
- Examine API endpoints and middleware`,
|
|
61
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export const testCoverageAnalyst: AgentDefinition = {
|
|
4
|
+
id: "test-coverage-analyst",
|
|
5
|
+
name: "Test Coverage Analyst",
|
|
6
|
+
description:
|
|
7
|
+
"Analyzes test coverage gaps, test quality, testing patterns, and CI integration.",
|
|
8
|
+
maxSteps: 12,
|
|
9
|
+
toolSet: "extended",
|
|
10
|
+
relevantFor: (classification) => classification.hasTests,
|
|
11
|
+
|
|
12
|
+
systemPrompt: `You are an expert test engineer analyzing the testing setup and coverage of a codebase.
|
|
13
|
+
|
|
14
|
+
1. **Test Infrastructure**
|
|
15
|
+
- Test framework identification and configuration
|
|
16
|
+
- Test runner setup
|
|
17
|
+
- Coverage tool configuration
|
|
18
|
+
- CI/CD integration for tests
|
|
19
|
+
|
|
20
|
+
2. **Coverage Analysis**
|
|
21
|
+
- Identify untested modules and files
|
|
22
|
+
- Critical business logic without tests
|
|
23
|
+
- API endpoints without integration tests
|
|
24
|
+
- Missing edge case coverage
|
|
25
|
+
|
|
26
|
+
3. **Test Quality**
|
|
27
|
+
- Test isolation (no shared state)
|
|
28
|
+
- Meaningful assertions (not just "no error")
|
|
29
|
+
- Test naming conventions
|
|
30
|
+
- Arrange-Act-Assert pattern adherence
|
|
31
|
+
- Mock usage appropriateness
|
|
32
|
+
|
|
33
|
+
4. **Test Types**
|
|
34
|
+
- Unit test presence and distribution
|
|
35
|
+
- Integration test coverage
|
|
36
|
+
- E2E test availability
|
|
37
|
+
- Snapshot test appropriateness
|
|
38
|
+
- Performance/load test setup
|
|
39
|
+
|
|
40
|
+
5. **Testing Patterns**
|
|
41
|
+
- Factory/fixture patterns for test data
|
|
42
|
+
- Helper functions and test utilities
|
|
43
|
+
- Custom matchers or assertions
|
|
44
|
+
- Test organization structure
|
|
45
|
+
|
|
46
|
+
6. **CI Integration**
|
|
47
|
+
- Test automation in CI pipeline
|
|
48
|
+
- Coverage reporting and gates
|
|
49
|
+
- Test parallelization
|
|
50
|
+
- Flaky test management
|
|
51
|
+
|
|
52
|
+
## Methodology
|
|
53
|
+
- Read test configuration files
|
|
54
|
+
- Scan test directories and files
|
|
55
|
+
- Compare source files with test files to identify gaps
|
|
56
|
+
- Review test quality in sample tests
|
|
57
|
+
- Check CI configuration for test automation`,
|
|
58
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export const todoGenerator: AgentDefinition = {
|
|
4
|
+
id: "todo-generator",
|
|
5
|
+
name: "Todo List Generator",
|
|
6
|
+
description:
|
|
7
|
+
"Generates a prioritized action plan and todo list from all analysis findings.",
|
|
8
|
+
maxSteps: 1,
|
|
9
|
+
toolSet: "base",
|
|
10
|
+
relevantFor: () => true,
|
|
11
|
+
|
|
12
|
+
systemPrompt: `You are a senior technical project manager. Your role is to synthesize code analysis findings into a clear, prioritized, and actionable todo list.
|
|
13
|
+
|
|
14
|
+
## Prioritization Rules
|
|
15
|
+
|
|
16
|
+
Order items by priority using these rules (highest to lowest):
|
|
17
|
+
1. **Critical severity** findings — immediate action required
|
|
18
|
+
2. **Warning severity + low effort** — quick wins with high impact
|
|
19
|
+
3. **Warning severity + medium effort** — important but need planning
|
|
20
|
+
4. **Warning severity + high effort** — schedule for a sprint
|
|
21
|
+
5. **Info/suggestion severity** — nice-to-have improvements
|
|
22
|
+
|
|
23
|
+
## Output Format
|
|
24
|
+
|
|
25
|
+
Produce a structured report with these sections:
|
|
26
|
+
|
|
27
|
+
### Section 1: "Immediate Actions (Critical)"
|
|
28
|
+
Checklist of all critical findings as actionable tasks. If none, state "No critical issues found."
|
|
29
|
+
|
|
30
|
+
### Section 2: "Quick Wins (High Impact, Low Effort)"
|
|
31
|
+
Checklist of warning-level findings that can be fixed quickly.
|
|
32
|
+
|
|
33
|
+
### Section 3: "Planned Improvements"
|
|
34
|
+
Checklist of medium/high effort items grouped by category (security, performance, code quality, etc.)
|
|
35
|
+
|
|
36
|
+
### Section 4: "Nice to Have"
|
|
37
|
+
Checklist of info-level and suggestion items worth considering.
|
|
38
|
+
|
|
39
|
+
### Section 5: "Summary"
|
|
40
|
+
A brief paragraph with the total count of action items by priority, estimated overall effort, and recommended order of execution.
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
- Each todo item must be a concrete, actionable task (not vague advice)
|
|
44
|
+
- Include the source agent/category in parentheses after each item
|
|
45
|
+
- Reference specific files or components when available
|
|
46
|
+
- Use markdown checkbox format: \`- [ ] Task description\`
|
|
47
|
+
- Do NOT repeat the raw findings — transform them into actionable tasks
|
|
48
|
+
- Group related findings into single tasks when appropriate
|
|
49
|
+
- Estimate effort as (low/medium/high) for each task`,
|
|
50
|
+
};
|
package/src/app/App.tsx
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
|
+
import { Box, useApp } from "ink";
|
|
3
|
+
import { HomeScreen } from "../screens/HomeScreen.js";
|
|
4
|
+
import { GenerationScreen } from "../screens/GenerationScreen.js";
|
|
5
|
+
import { ViewerScreen } from "../screens/ViewerScreen.js";
|
|
6
|
+
import { HistoryScreen } from "../screens/HistoryScreen.js";
|
|
7
|
+
import { ConfigScreen, type ConfigChanges } from "../screens/ConfigScreen.js";
|
|
8
|
+
import { createProviderRegistry } from "../config/providers.js";
|
|
9
|
+
import { saveProjectConfig } from "../config/saver.js";
|
|
10
|
+
import { debugLog } from "../utils/debug.js";
|
|
11
|
+
import type { LanguageModelV1 } from "ai";
|
|
12
|
+
import type { Screen, NavigationState } from "../types/index.js";
|
|
13
|
+
import type { OpenReportConfig } from "../config/schema.js";
|
|
14
|
+
|
|
15
|
+
interface AppProps {
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
initialModel: LanguageModelV1;
|
|
18
|
+
initialModelId: string;
|
|
19
|
+
initialConfig: OpenReportConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function App({
|
|
23
|
+
projectRoot,
|
|
24
|
+
initialModel,
|
|
25
|
+
initialModelId,
|
|
26
|
+
initialConfig,
|
|
27
|
+
}: AppProps) {
|
|
28
|
+
const { exit } = useApp();
|
|
29
|
+
const [nav, setNav] = useState<NavigationState>({ screen: "home" });
|
|
30
|
+
const [config, setConfig] = useState<OpenReportConfig>(initialConfig);
|
|
31
|
+
const [model, setModel] = useState<LanguageModelV1>(initialModel);
|
|
32
|
+
const [modelId, setModelId] = useState<string>(initialModelId);
|
|
33
|
+
|
|
34
|
+
const navigate = useCallback(
|
|
35
|
+
(screen: Screen, params?: Record<string, string>) => {
|
|
36
|
+
setNav({ screen, params });
|
|
37
|
+
},
|
|
38
|
+
[]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const handleQuit = useCallback(() => {
|
|
42
|
+
exit();
|
|
43
|
+
}, [exit]);
|
|
44
|
+
|
|
45
|
+
const handleConfigSave = useCallback(
|
|
46
|
+
(changes: ConfigChanges) => {
|
|
47
|
+
// Update config state
|
|
48
|
+
const newConfig: OpenReportConfig = {
|
|
49
|
+
...config,
|
|
50
|
+
defaultProvider: changes.defaultProvider,
|
|
51
|
+
defaultModel: changes.defaultModel,
|
|
52
|
+
agents: {
|
|
53
|
+
...config.agents,
|
|
54
|
+
maxConcurrency: changes.maxConcurrency,
|
|
55
|
+
temperature: changes.temperature,
|
|
56
|
+
},
|
|
57
|
+
features: {
|
|
58
|
+
...config.features,
|
|
59
|
+
todoList: changes.todoList,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
setConfig(newConfig);
|
|
63
|
+
|
|
64
|
+
// Recreate provider/model
|
|
65
|
+
try {
|
|
66
|
+
const registry = createProviderRegistry(newConfig);
|
|
67
|
+
const newModel = registry.getModel(newConfig);
|
|
68
|
+
setModel(newModel);
|
|
69
|
+
setModelId(changes.defaultModel);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
debugLog("app:configChange", e);
|
|
72
|
+
// Provider may fail if no API key - that's OK, will fail at generation time
|
|
73
|
+
setModelId(changes.defaultModel);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Persist to disk (async — fire-and-forget with error logging)
|
|
77
|
+
saveProjectConfig(projectRoot, {
|
|
78
|
+
defaultProvider: changes.defaultProvider,
|
|
79
|
+
defaultModel: changes.defaultModel,
|
|
80
|
+
agents: {
|
|
81
|
+
maxConcurrency: changes.maxConcurrency,
|
|
82
|
+
temperature: changes.temperature,
|
|
83
|
+
},
|
|
84
|
+
features: {
|
|
85
|
+
todoList: changes.todoList,
|
|
86
|
+
},
|
|
87
|
+
}).catch((e) => debugLog("app:saveConfig", e));
|
|
88
|
+
},
|
|
89
|
+
[config, projectRoot]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
switch (nav.screen) {
|
|
93
|
+
case "home":
|
|
94
|
+
return (
|
|
95
|
+
<HomeScreen
|
|
96
|
+
projectRoot={projectRoot}
|
|
97
|
+
modelName={modelId}
|
|
98
|
+
config={config}
|
|
99
|
+
onNavigate={navigate}
|
|
100
|
+
onQuit={handleQuit}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
case "generation":
|
|
105
|
+
return (
|
|
106
|
+
<GenerationScreen
|
|
107
|
+
projectRoot={projectRoot}
|
|
108
|
+
reportType={nav.params?.reportType || "full-audit"}
|
|
109
|
+
model={model}
|
|
110
|
+
modelId={modelId}
|
|
111
|
+
config={config}
|
|
112
|
+
onNavigate={navigate}
|
|
113
|
+
generateTodoList={nav.params?.todoList === "true"}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
case "viewer":
|
|
118
|
+
return (
|
|
119
|
+
<ViewerScreen
|
|
120
|
+
projectRoot={projectRoot}
|
|
121
|
+
reportId={nav.params?.reportId || ""}
|
|
122
|
+
onNavigate={navigate}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
case "history":
|
|
127
|
+
return (
|
|
128
|
+
<HistoryScreen projectRoot={projectRoot} onNavigate={navigate} />
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
case "config":
|
|
132
|
+
return (
|
|
133
|
+
<ConfigScreen
|
|
134
|
+
config={config}
|
|
135
|
+
onNavigate={navigate}
|
|
136
|
+
onSave={handleConfigSave}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
return (
|
|
142
|
+
<HomeScreen
|
|
143
|
+
projectRoot={projectRoot}
|
|
144
|
+
modelName={modelId}
|
|
145
|
+
config={config}
|
|
146
|
+
onNavigate={navigate}
|
|
147
|
+
onQuit={handleQuit}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/app/theme.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
export const colors = {
|
|
4
|
+
primary: "#7C3AED",
|
|
5
|
+
secondary: "#06B6D4",
|
|
6
|
+
success: "#10B981",
|
|
7
|
+
warning: "#F59E0B",
|
|
8
|
+
error: "#EF4444",
|
|
9
|
+
info: "#3B82F6",
|
|
10
|
+
muted: "#6B7280",
|
|
11
|
+
text: "#E5E7EB",
|
|
12
|
+
bg: "#111827",
|
|
13
|
+
border: "#374151",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const severity = {
|
|
17
|
+
critical: chalk.hex("#EF4444"),
|
|
18
|
+
warning: chalk.hex("#F59E0B"),
|
|
19
|
+
info: chalk.hex("#3B82F6"),
|
|
20
|
+
suggestion: chalk.hex("#8B5CF6"),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const severityBg = {
|
|
24
|
+
critical: chalk.bgHex("#EF4444").white.bold,
|
|
25
|
+
warning: chalk.bgHex("#F59E0B").black.bold,
|
|
26
|
+
info: chalk.bgHex("#3B82F6").white.bold,
|
|
27
|
+
suggestion: chalk.bgHex("#8B5CF6").white.bold,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const agentStatus = {
|
|
31
|
+
pending: chalk.hex("#6B7280"),
|
|
32
|
+
running: chalk.hex("#06B6D4"),
|
|
33
|
+
completed: chalk.hex("#10B981"),
|
|
34
|
+
failed: chalk.hex("#EF4444"),
|
|
35
|
+
skipped: chalk.hex("#9CA3AF"),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const agentStatusIcon = {
|
|
39
|
+
pending: "○",
|
|
40
|
+
running: "◉",
|
|
41
|
+
completed: "✓",
|
|
42
|
+
failed: "✗",
|
|
43
|
+
skipped: "⊘",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const ui = {
|
|
47
|
+
title: chalk.hex("#7C3AED").bold,
|
|
48
|
+
subtitle: chalk.hex("#06B6D4"),
|
|
49
|
+
dim: chalk.hex("#6B7280"),
|
|
50
|
+
highlight: chalk.hex("#F59E0B"),
|
|
51
|
+
link: chalk.hex("#3B82F6").underline,
|
|
52
|
+
bold: chalk.bold,
|
|
53
|
+
border: chalk.hex("#374151"),
|
|
54
|
+
};
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Cli, Command, Option } from "clipanion";
|
|
2
|
+
|
|
3
|
+
class DefaultCommand extends Command {
|
|
4
|
+
static paths = [Command.Default];
|
|
5
|
+
|
|
6
|
+
static usage = Command.Usage({
|
|
7
|
+
description: "Launch the interactive TUI",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
async execute() {
|
|
11
|
+
const { runInteractive } = await import("./commands/interactive.js");
|
|
12
|
+
await runInteractive(process.cwd());
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class InitCommand extends Command {
|
|
17
|
+
static paths = [["init"]];
|
|
18
|
+
|
|
19
|
+
static usage = Command.Usage({
|
|
20
|
+
description: "Initialize OpenReport in the current project",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
yes = Option.Boolean("--yes,-y", false, {
|
|
24
|
+
description: "Skip prompts and use defaults",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async execute() {
|
|
28
|
+
const { runInit } = await import("./commands/init.js");
|
|
29
|
+
await runInit(process.cwd(), { yes: this.yes });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class RunCommand extends Command {
|
|
34
|
+
static paths = [["run"]];
|
|
35
|
+
|
|
36
|
+
static usage = Command.Usage({
|
|
37
|
+
description: "Generate a report in headless mode",
|
|
38
|
+
examples: [
|
|
39
|
+
["Full audit", "$0 run full-audit"],
|
|
40
|
+
["Security review", "$0 run security-review"],
|
|
41
|
+
["With custom model", "$0 run full-audit --model gpt-4o"],
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
type = Option.String({ required: true });
|
|
46
|
+
|
|
47
|
+
model = Option.String("--model,-m", {
|
|
48
|
+
description: "Model to use (e.g., claude-sonnet-4-5-20250929, gpt-4o)",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
provider = Option.String("--provider,-p", {
|
|
52
|
+
description: "AI provider (anthropic, openai, google, mistral)",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
output = Option.String("--output,-o", {
|
|
56
|
+
description: "Custom output file path",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
format = Option.String("--format,-f", {
|
|
60
|
+
description: "Output format (markdown, json)",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
quiet = Option.Boolean("--quiet,-q", false, {
|
|
64
|
+
description: "Suppress progress output",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
todo = Option.Boolean("--todo", false, {
|
|
68
|
+
description: "Generate a prioritized todo list from findings",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
async execute() {
|
|
72
|
+
const { runHeadless } = await import("./commands/run.js");
|
|
73
|
+
await runHeadless(process.cwd(), this.type, {
|
|
74
|
+
model: this.model,
|
|
75
|
+
provider: this.provider,
|
|
76
|
+
output: this.output,
|
|
77
|
+
format: this.format as "markdown" | "json" | undefined,
|
|
78
|
+
quiet: this.quiet,
|
|
79
|
+
todo: this.todo,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class ListCommand extends Command {
|
|
85
|
+
static paths = [["list"], ["ls"]];
|
|
86
|
+
|
|
87
|
+
static usage = Command.Usage({
|
|
88
|
+
description: "List past reports",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
json = Option.Boolean("--json", false, {
|
|
92
|
+
description: "Output as JSON",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
limit = Option.String("--limit,-n", {
|
|
96
|
+
description: "Maximum number of reports to show",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
async execute() {
|
|
100
|
+
const { runList } = await import("./commands/list.js");
|
|
101
|
+
await runList(process.cwd(), {
|
|
102
|
+
json: this.json,
|
|
103
|
+
limit: this.limit ? parseInt(this.limit, 10) : undefined,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class ViewCommand extends Command {
|
|
109
|
+
static paths = [["view"]];
|
|
110
|
+
|
|
111
|
+
static usage = Command.Usage({
|
|
112
|
+
description: "View a report in the TUI viewer",
|
|
113
|
+
examples: [
|
|
114
|
+
["View by ID", "$0 view rpt_V1StGXR8"],
|
|
115
|
+
["Raw markdown output", "$0 view rpt_V1StGXR8 --raw"],
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
id = Option.String({ required: true });
|
|
120
|
+
|
|
121
|
+
raw = Option.Boolean("--raw", false, {
|
|
122
|
+
description: "Output raw markdown instead of TUI",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
async execute() {
|
|
126
|
+
const { runView } = await import("./commands/view.js");
|
|
127
|
+
await runView(process.cwd(), this.id, { raw: this.raw });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createCli() {
|
|
132
|
+
const cli = new Cli({
|
|
133
|
+
binaryLabel: "openreport",
|
|
134
|
+
binaryName: "openreport",
|
|
135
|
+
binaryVersion: "0.1.0",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
cli.register(DefaultCommand);
|
|
139
|
+
cli.register(InitCommand);
|
|
140
|
+
cli.register(RunCommand);
|
|
141
|
+
cli.register(ListCommand);
|
|
142
|
+
cli.register(ViewCommand);
|
|
143
|
+
|
|
144
|
+
return cli;
|
|
145
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
5
|
+
|
|
6
|
+
export async function runInit(projectRoot: string, options: { yes?: boolean }) {
|
|
7
|
+
const configDir = path.join(projectRoot, ".openreport");
|
|
8
|
+
const configFile = path.join(configDir, "config.json");
|
|
9
|
+
const reportsDir = path.join(configDir, "reports");
|
|
10
|
+
|
|
11
|
+
// Check if already initialized
|
|
12
|
+
let configExists = false;
|
|
13
|
+
try {
|
|
14
|
+
await fs.promises.access(configFile);
|
|
15
|
+
configExists = true;
|
|
16
|
+
} catch {
|
|
17
|
+
// File doesn't exist
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (configExists && !options.yes) {
|
|
21
|
+
console.log(
|
|
22
|
+
chalk.yellow("OpenReport is already initialized in this project.")
|
|
23
|
+
);
|
|
24
|
+
console.log(chalk.gray(`Config: ${configFile}`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Create directories
|
|
29
|
+
await fs.promises.mkdir(reportsDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
// Write default config
|
|
32
|
+
const config = {
|
|
33
|
+
defaultProvider: DEFAULT_CONFIG.defaultProvider,
|
|
34
|
+
defaultModel: DEFAULT_CONFIG.defaultModel,
|
|
35
|
+
agents: {
|
|
36
|
+
maxConcurrency: DEFAULT_CONFIG.agents.maxConcurrency,
|
|
37
|
+
temperature: DEFAULT_CONFIG.agents.temperature,
|
|
38
|
+
},
|
|
39
|
+
output: {
|
|
40
|
+
format: DEFAULT_CONFIG.output.format,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
45
|
+
|
|
46
|
+
// Add to .gitignore if it exists
|
|
47
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
48
|
+
let gitignoreExists = false;
|
|
49
|
+
try {
|
|
50
|
+
await fs.promises.access(gitignorePath);
|
|
51
|
+
gitignoreExists = true;
|
|
52
|
+
} catch {
|
|
53
|
+
// No .gitignore
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (gitignoreExists) {
|
|
57
|
+
const gitignore = await fs.promises.readFile(gitignorePath, "utf-8");
|
|
58
|
+
if (!gitignore.includes(".openreport/")) {
|
|
59
|
+
await fs.promises.appendFile(
|
|
60
|
+
gitignorePath,
|
|
61
|
+
"\n# OpenReport\n.openreport/\n",
|
|
62
|
+
"utf-8"
|
|
63
|
+
);
|
|
64
|
+
console.log(chalk.gray("Added .openreport/ to .gitignore"));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(chalk.green("OpenReport initialized successfully!"));
|
|
69
|
+
console.log(chalk.gray(`Config: ${configFile}`));
|
|
70
|
+
console.log(chalk.gray(`Reports: ${reportsDir}`));
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(
|
|
73
|
+
chalk.cyan("Set your API key as an environment variable:")
|
|
74
|
+
);
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.white(" export ANTHROPIC_API_KEY=your-key-here")
|
|
77
|
+
);
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(chalk.cyan("Then run:"));
|
|
80
|
+
console.log(chalk.white(" openreport"));
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { App } from "../app/App.js";
|
|
4
|
+
import { loadConfig } from "../config/loader.js";
|
|
5
|
+
import { createProviderRegistry } from "../config/providers.js";
|
|
6
|
+
import { resolveProvider } from "../config/resolve-provider.js";
|
|
7
|
+
|
|
8
|
+
export async function runInteractive(projectRoot: string) {
|
|
9
|
+
const config = await loadConfig({ projectRoot });
|
|
10
|
+
const registry = createProviderRegistry(config);
|
|
11
|
+
const { model, effectiveProvider, effectiveModel } = await resolveProvider(config, registry);
|
|
12
|
+
|
|
13
|
+
const effectiveConfig = {
|
|
14
|
+
...config,
|
|
15
|
+
defaultProvider: effectiveProvider,
|
|
16
|
+
defaultModel: effectiveModel,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const { waitUntilExit } = render(
|
|
20
|
+
<App
|
|
21
|
+
projectRoot={projectRoot}
|
|
22
|
+
initialModel={model}
|
|
23
|
+
initialModelId={effectiveModel}
|
|
24
|
+
initialConfig={effectiveConfig}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
await waitUntilExit();
|
|
29
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { listReports } from "../storage/report-store.js";
|
|
3
|
+
import { formatDate, formatDuration, formatTokens } from "../utils/format.js";
|
|
4
|
+
import { getGradeTerminalColor } from "../utils/grade-colors.js";
|
|
5
|
+
|
|
6
|
+
const CHALK_COLORS: Record<string, (s: string) => string> = {
|
|
7
|
+
green: chalk.green,
|
|
8
|
+
cyan: chalk.cyan,
|
|
9
|
+
yellow: chalk.yellow,
|
|
10
|
+
red: chalk.red,
|
|
11
|
+
redBright: chalk.redBright,
|
|
12
|
+
white: chalk.white,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface ListOptions {
|
|
16
|
+
json?: boolean;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runList(projectRoot: string, options: ListOptions = {}) {
|
|
21
|
+
const limited = await listReports(projectRoot, options.limit || 50);
|
|
22
|
+
|
|
23
|
+
if (options.json) {
|
|
24
|
+
console.log(JSON.stringify(limited, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (limited.length === 0) {
|
|
29
|
+
console.log(chalk.gray("No reports found."));
|
|
30
|
+
console.log(
|
|
31
|
+
chalk.gray('Run "openreport" to generate your first report.')
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.bold(`Reports (${limited.length}):`));
|
|
37
|
+
console.log();
|
|
38
|
+
|
|
39
|
+
for (const report of limited) {
|
|
40
|
+
const colorName = getGradeTerminalColor(report.grade || "");
|
|
41
|
+
const gradeColor = CHALK_COLORS[colorName] || chalk.white;
|
|
42
|
+
|
|
43
|
+
const totalTokens = report.tokens.input + report.tokens.output;
|
|
44
|
+
|
|
45
|
+
console.log(
|
|
46
|
+
` ${chalk.bold(report.id)} ${gradeColor(`[${report.grade || "?"}]`)} ${chalk.white(report.type)}`
|
|
47
|
+
);
|
|
48
|
+
console.log(
|
|
49
|
+
` ${chalk.gray(formatDate(report.createdAt))} | ${chalk.gray(formatDuration(report.duration))} | ${chalk.gray(report.model)} | ${chalk.gray(formatTokens(totalTokens) + " tokens")}`
|
|
50
|
+
);
|
|
51
|
+
console.log();
|
|
52
|
+
}
|
|
53
|
+
}
|