codehere 0.8.0 → 1.0.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 +37 -184
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +329 -0
- package/dist/index.js +74 -1004
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
- package/dist/presentation/cli/commands/check-command.js +251 -0
- package/dist/presentation/cli/commands/help-command.js +22 -286
- package/dist/ui.js +19 -438
- package/dist/utils/version.js +0 -12
- package/package.json +18 -27
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/generate-final-report.js +0 -134
- package/dist/application/use-cases/generate-narration.js +0 -128
- package/dist/application/use-cases/generate-thread-title.js +0 -178
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/thread-export.js +0 -155
- package/dist/application/use-cases/verify-claim.js +0 -274
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/entities/task.js +0 -61
- package/dist/domain/services/codebase-detector.js +0 -91
- package/dist/error-handler.js +0 -251
- package/dist/infrastructure/completion/bash-completion.js +0 -25
- package/dist/infrastructure/completion/fish-completion.js +0 -22
- package/dist/infrastructure/completion/zsh-completion.js +0 -30
- package/dist/infrastructure/config/config-manager.js +0 -173
- package/dist/infrastructure/config/product-context.js +0 -67
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -163
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -119
- package/dist/infrastructure/context/registry-bootstrap.js +0 -99
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -113
- package/dist/infrastructure/cost/cost-router.js +0 -104
- package/dist/infrastructure/cost/spend-aggregator.js +0 -147
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
- package/dist/infrastructure/hooks/hook-mode.js +0 -71
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
- package/dist/infrastructure/orchestration/context-handoff.js +0 -144
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -77
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
- package/dist/infrastructure/orchestration/graphify-context.js +0 -254
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1091
- package/dist/infrastructure/orchestration/session-persistence.js +0 -258
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/protocols/verify-page.js +0 -983
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/security/permissions-config.js +0 -234
- package/dist/infrastructure/security/trust-scorer.js +0 -657
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
- package/dist/infrastructure/storage/project-thread-reader.js +0 -172
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/infrastructure/storage/task-repository.js +0 -206
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -126
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -198
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -578
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -116
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -387
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
- package/dist/presentation/cli/commands/setup-command.js +0 -307
- package/dist/presentation/cli/commands/status-command.js +0 -661
- package/dist/presentation/cli/commands/thread-command.js +0 -288
- package/dist/presentation/cli/commands/trust-command.js +0 -306
- package/dist/presentation/cli/commands/verify-command.js +0 -233
- package/dist/presentation/cli/error-display.js +0 -336
- package/dist/presentation/cli/repl.js +0 -797
- package/dist/presentation/cli/task-deliverable.js +0 -127
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -166
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* F3 Task 4 — repo_map.json builder (v0.5 TS-native MVP).
|
|
3
|
-
*
|
|
4
|
-
* v0.5 SCOPE:
|
|
5
|
-
* - Walks `git ls-files` (respects .gitignore via git itself).
|
|
6
|
-
* - For .ts/.tsx/.js/.jsx/.py files, regex-extracts top-level public-surface
|
|
7
|
-
* symbols (export function/class/const/interface/type, def/class for Python).
|
|
8
|
-
* - Reference count via `git grep -c` per symbol name.
|
|
9
|
-
* - Writes JSON to .codehere/memory/episodic/repo_map.json.
|
|
10
|
-
*
|
|
11
|
-
* v0.6 PLAN:
|
|
12
|
-
* - Replace the regex extractor with proper tree-sitter parsing
|
|
13
|
-
* (Aider's repomap.py MIT port — Python helper + TS shell-out, OR
|
|
14
|
-
* a TS-native tree-sitter library evaluation).
|
|
15
|
-
* - Add PageRank-style symbol importance (currently raw reference count).
|
|
16
|
-
* - Incremental rebuild via the per-file `hash` field already in the schema.
|
|
17
|
-
*
|
|
18
|
-
* Schema is forward-compatible — only the extractor implementation changes.
|
|
19
|
-
*
|
|
20
|
-
* Why regex MVP for v0.5:
|
|
21
|
-
* - Zero new deps (tree-sitter would mean Python + 5 packages OR a 30+ MB
|
|
22
|
-
* TS dep). codehere is bootstrap-zero-cost; every dep is friction.
|
|
23
|
-
* - Closes the F3 episodic/repo_map.json layer enough for spawned agents
|
|
24
|
-
* to read it as a starting context block.
|
|
25
|
-
* - Honest about limitations — see schema._note in the output JSON.
|
|
26
|
-
*/
|
|
27
|
-
import { spawnSync } from 'child_process';
|
|
28
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
|
|
29
|
-
import { createHash } from 'crypto';
|
|
30
|
-
import { join, dirname, extname } from 'path';
|
|
31
|
-
const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py']);
|
|
32
|
-
/**
|
|
33
|
-
* Per-language extractors. v0.5 regex-based — line-anchored to avoid
|
|
34
|
-
* matching identifiers inside string literals. Returns 1-indexed line numbers.
|
|
35
|
-
*
|
|
36
|
-
* v0.6 will swap these for AST-based extractors. The interface stays the
|
|
37
|
-
* same: (body) → RepoMapSymbol[].
|
|
38
|
-
*/
|
|
39
|
-
function extractTsLikeSymbols(body) {
|
|
40
|
-
const lines = body.split('\n');
|
|
41
|
-
const symbols = [];
|
|
42
|
-
// export function NAME( | export async function NAME(
|
|
43
|
-
// export class NAME
|
|
44
|
-
// export const NAME = | export let NAME = | export var NAME =
|
|
45
|
-
// export interface NAME { | export type NAME =
|
|
46
|
-
const PATTERNS = [
|
|
47
|
-
[/^export\s+(?:async\s+)?function\s+(\w+)\s*[<(]/, 'function'],
|
|
48
|
-
[/^export\s+(?:abstract\s+)?class\s+(\w+)/, 'class'],
|
|
49
|
-
[/^export\s+(?:const|let|var)\s+(\w+)\s*[:=]/, 'const'],
|
|
50
|
-
[/^export\s+interface\s+(\w+)/, 'interface'],
|
|
51
|
-
[/^export\s+type\s+(\w+)\s*=/, 'type'],
|
|
52
|
-
[/^export\s+enum\s+(\w+)/, 'enum'],
|
|
53
|
-
];
|
|
54
|
-
for (let i = 0; i < lines.length; i++) {
|
|
55
|
-
for (const [re, kind] of PATTERNS) {
|
|
56
|
-
const m = lines[i].match(re);
|
|
57
|
-
if (m) {
|
|
58
|
-
symbols.push({ kind, name: m[1], line: i + 1 });
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return symbols;
|
|
64
|
-
}
|
|
65
|
-
function extractPythonSymbols(body) {
|
|
66
|
-
const lines = body.split('\n');
|
|
67
|
-
const symbols = [];
|
|
68
|
-
// Top-level (column 0) def NAME( | class NAME( | class NAME:
|
|
69
|
-
const PATTERNS = [
|
|
70
|
-
[/^def\s+(\w+)\s*\(/, 'def'],
|
|
71
|
-
[/^class\s+(\w+)\s*[(:]/, 'class'],
|
|
72
|
-
];
|
|
73
|
-
for (let i = 0; i < lines.length; i++) {
|
|
74
|
-
for (const [re, kind] of PATTERNS) {
|
|
75
|
-
const m = lines[i].match(re);
|
|
76
|
-
if (m) {
|
|
77
|
-
symbols.push({ kind, name: m[1], line: i + 1 });
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return symbols;
|
|
83
|
-
}
|
|
84
|
-
function extractSymbols(filePath, body) {
|
|
85
|
-
const ext = extname(filePath);
|
|
86
|
-
if (ext === '.py')
|
|
87
|
-
return extractPythonSymbols(body);
|
|
88
|
-
if (SUPPORTED_EXTENSIONS.has(ext))
|
|
89
|
-
return extractTsLikeSymbols(body);
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Hash file content for incremental-rebuild support in v0.6.
|
|
94
|
-
* Format: `sha256:<64-hex>` so the algorithm is self-describing.
|
|
95
|
-
*/
|
|
96
|
-
function hashContent(body) {
|
|
97
|
-
return 'sha256:' + createHash('sha256').update(body).digest('hex');
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* `git ls-files` returns paths relative to the repo root. Filters out
|
|
101
|
-
* non-supported extensions in one pass. If `git` isn't a repo, returns null
|
|
102
|
-
* so the caller can refuse with a clear message.
|
|
103
|
-
*/
|
|
104
|
-
function gitListFiles(repoRoot) {
|
|
105
|
-
const r = spawnSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf-8' });
|
|
106
|
-
if (r.status !== 0)
|
|
107
|
-
return null;
|
|
108
|
-
return r.stdout
|
|
109
|
-
.split('\0')
|
|
110
|
-
.filter((p) => p.length > 0)
|
|
111
|
-
.filter((p) => SUPPORTED_EXTENSIONS.has(extname(p)));
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Count references to a symbol name across the repo via git grep.
|
|
115
|
-
* `-c` returns "<file>:<count>" per matching file; sum the counts.
|
|
116
|
-
*
|
|
117
|
-
* Uses word-boundary regex so `popular` doesn't match `unpopular`. The grep
|
|
118
|
-
* finds DEFINITIONS too, so subtract the definition count to get pure refs.
|
|
119
|
-
*/
|
|
120
|
-
function countReferences(repoRoot, symbolName) {
|
|
121
|
-
// -F = fixed string, -w = word boundary. Combined with grep itself.
|
|
122
|
-
const r = spawnSync('git', ['grep', '-c', '-w', '-F', '--', symbolName], { cwd: repoRoot, encoding: 'utf-8' });
|
|
123
|
-
if (r.status !== 0 && r.status !== 1)
|
|
124
|
-
return 0;
|
|
125
|
-
let total = 0;
|
|
126
|
-
for (const line of r.stdout.split('\n')) {
|
|
127
|
-
const m = line.match(/:(\d+)$/);
|
|
128
|
-
if (m)
|
|
129
|
-
total += parseInt(m[1], 10);
|
|
130
|
-
}
|
|
131
|
-
// Subtract 1 for the definition line itself (best-effort — undercounts if
|
|
132
|
-
// the symbol is defined in multiple files; v0.6 with AST will be accurate).
|
|
133
|
-
return Math.max(0, total - 1);
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Build the repo_map for the repo at `repoRoot`. Returns the JSON object —
|
|
137
|
-
* caller writes to disk. Pure-ish (still spawns git), no codehere-specific
|
|
138
|
-
* paths inside.
|
|
139
|
-
*
|
|
140
|
-
* The `_stats` block in the returned RepoMap persists to disk so downstream
|
|
141
|
-
* agents know whether the map is incomplete (e.g. >1MB files skipped during
|
|
142
|
-
* walk).
|
|
143
|
-
*/
|
|
144
|
-
export function buildRepoMap(repoRoot) {
|
|
145
|
-
if (!existsSync(join(repoRoot, '.git'))) {
|
|
146
|
-
return { error: 'not a git repo (run `git init` first)' };
|
|
147
|
-
}
|
|
148
|
-
const files = gitListFiles(repoRoot);
|
|
149
|
-
if (files === null) {
|
|
150
|
-
return { error: 'git ls-files failed' };
|
|
151
|
-
}
|
|
152
|
-
const fileMap = {};
|
|
153
|
-
const allSymbols = [];
|
|
154
|
-
let skippedLargeFiles = 0;
|
|
155
|
-
for (const rel of files) {
|
|
156
|
-
const abs = join(repoRoot, rel);
|
|
157
|
-
// Skip giant files (>1MB) — surfaced in the result so users see the count.
|
|
158
|
-
try {
|
|
159
|
-
if (statSync(abs).size > 1_000_000) {
|
|
160
|
-
skippedLargeFiles++;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
let body;
|
|
168
|
-
try {
|
|
169
|
-
body = readFileSync(abs, 'utf-8');
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// File deleted between ls-files and read — skip.
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
const symbols = extractSymbols(rel, body);
|
|
176
|
-
fileMap[rel] = {
|
|
177
|
-
hash: hashContent(body),
|
|
178
|
-
symbols,
|
|
179
|
-
};
|
|
180
|
-
for (const s of symbols) {
|
|
181
|
-
allSymbols.push({ name: s.name, file: rel, line: s.line });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// Rank by reference count.
|
|
185
|
-
//
|
|
186
|
-
// codex Round 1 fix: the previous version applied .slice(0, 200) BEFORE
|
|
187
|
-
// grepping, which meant "first 200 discovered names" not "top 200
|
|
188
|
-
// candidates." Symbols defined in files traversed late silently got
|
|
189
|
-
// references=0 and dropped out of topSymbols, regardless of how heavily
|
|
190
|
-
// they were referenced. Now we grep ALL unique names. O(N) git greps is
|
|
191
|
-
// acceptable for v0.5 — codehere's own 249-symbol repo runs in ~5s.
|
|
192
|
-
// v0.6's tree-sitter port will swap raw grep for AST-aware reference
|
|
193
|
-
// resolution + PageRank, which is faster AND more accurate.
|
|
194
|
-
const uniqueNames = Array.from(new Set(allSymbols.map((s) => s.name)));
|
|
195
|
-
const refCounts = new Map();
|
|
196
|
-
for (const name of uniqueNames) {
|
|
197
|
-
refCounts.set(name, countReferences(repoRoot, name));
|
|
198
|
-
}
|
|
199
|
-
const topSymbols = allSymbols
|
|
200
|
-
.map((s) => ({ ...s, references: refCounts.get(s.name) ?? 0 }))
|
|
201
|
-
// Stable sort: references DESC, then name ASC for tie-breaking.
|
|
202
|
-
.sort((a, b) => b.references - a.references || a.name.localeCompare(b.name))
|
|
203
|
-
.slice(0, 50);
|
|
204
|
-
return {
|
|
205
|
-
version: 'v0.5',
|
|
206
|
-
generatedAt: new Date().toISOString(),
|
|
207
|
-
rootPath: repoRoot,
|
|
208
|
-
_note: 'TS-native regex MVP — KNOWN MISSES (v0.5, addressed in v0.6 tree-sitter port): ' +
|
|
209
|
-
'`export default function/class` (no name on the export line), ' +
|
|
210
|
-
'`export { foo, bar }` blocks, `export { foo } from "..."` re-exports, ' +
|
|
211
|
-
'`export * from "..."` star re-exports, multi-line `export { ... }` blocks, ' +
|
|
212
|
-
'`export declare`, CommonJS `module.exports` / `exports.X` assignments, ' +
|
|
213
|
-
'symbols not at column 0 in Python. Reference counts via word-bounded ' +
|
|
214
|
-
'git grep undercount when a name is defined in multiple files. ' +
|
|
215
|
-
'Read `_stats.skippedLargeFiles` to detect files left out of the walk.',
|
|
216
|
-
_stats: { skippedLargeFiles },
|
|
217
|
-
files: fileMap,
|
|
218
|
-
topSymbols,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Disk-side wrapper: build + write to .codehere/memory/episodic/repo_map.json.
|
|
223
|
-
*
|
|
224
|
-
* `_stats` is preserved in the on-disk JSON (codex Round 2 fix) so spawned
|
|
225
|
-
* agents reading the map can tell "no symbols in this file" from "this file
|
|
226
|
-
* was skipped during the walk." The CLI also surfaces it on the return value.
|
|
227
|
-
*/
|
|
228
|
-
export function writeRepoMap(repoRoot, codehereDir, options = {}) {
|
|
229
|
-
const targetPath = join(codehereDir, 'memory/episodic/repo_map.json');
|
|
230
|
-
if (existsSync(targetPath) && !options.rebuild) {
|
|
231
|
-
return { result: 'exists', path: targetPath };
|
|
232
|
-
}
|
|
233
|
-
const built = buildRepoMap(repoRoot);
|
|
234
|
-
if ('error' in built) {
|
|
235
|
-
return { result: 'failed', path: targetPath, error: built.error };
|
|
236
|
-
}
|
|
237
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
238
|
-
writeFileSync(targetPath, JSON.stringify(built, null, 2) + '\n');
|
|
239
|
-
return {
|
|
240
|
-
result: 'written',
|
|
241
|
-
path: targetPath,
|
|
242
|
-
fileCount: Object.keys(built.files).length,
|
|
243
|
-
symbolCount: Object.values(built.files).reduce((acc, f) => acc + f.symbols.length, 0),
|
|
244
|
-
skippedLargeFiles: built._stats.skippedLargeFiles,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
//# sourceMappingURL=repo-map-builder.js.map
|
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Command: report
|
|
3
|
-
*
|
|
4
|
-
* Generates a shareable audit narrative — not just a score.
|
|
5
|
-
* The viral artifact: something developers post in PRs, tweet, and put in READMEs.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* codehere report # scan current dir, print markdown
|
|
9
|
-
* codehere report src/ # scan a subdirectory
|
|
10
|
-
* codehere report --out report.md # write to file
|
|
11
|
-
* codehere report --json # machine-readable JSON
|
|
12
|
-
* codehere report --diff HEAD # scan only changed files
|
|
13
|
-
*
|
|
14
|
-
* Output is a markdown document with:
|
|
15
|
-
* - Executive summary with score and grade
|
|
16
|
-
* - Per-severity finding breakdowns with context
|
|
17
|
-
* - Actionable recommendations in plain English
|
|
18
|
-
* - A shareable badge and attribution
|
|
19
|
-
*/
|
|
20
|
-
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
21
|
-
import { basename, relative, join, isAbsolute } from 'path';
|
|
22
|
-
import { colors, success, error, newline, info } from '../../../ui.js';
|
|
23
|
-
import { scoreFile } from '../../../infrastructure/security/trust-scorer.js';
|
|
24
|
-
import { collectCodeFiles } from '../../../infrastructure/security/file-collection.js';
|
|
25
|
-
import { loadProductContext, adjustRecommendation } from '../../../infrastructure/config/product-context.js';
|
|
26
|
-
// ── Markdown generation ───────────────────────────────────────────────────────
|
|
27
|
-
function gradeEmoji(grade) {
|
|
28
|
-
return grade === 'A' ? '🟢' : grade === 'B' ? '🟡' : grade === 'C' ? '🟠' : grade === 'D' ? '🔴' : '🚨';
|
|
29
|
-
}
|
|
30
|
-
function severityLabel(severity) {
|
|
31
|
-
return severity === 'critical'
|
|
32
|
-
? '🚨 Critical'
|
|
33
|
-
: severity === 'high'
|
|
34
|
-
? '⚠️ High'
|
|
35
|
-
: severity === 'medium'
|
|
36
|
-
? '🔵 Medium'
|
|
37
|
-
: '⬇️ Low';
|
|
38
|
-
}
|
|
39
|
-
function badgeUrl(score, grade) {
|
|
40
|
-
const color = grade === 'A'
|
|
41
|
-
? 'brightgreen'
|
|
42
|
-
: grade === 'B'
|
|
43
|
-
? 'green'
|
|
44
|
-
: grade === 'C'
|
|
45
|
-
? 'yellow'
|
|
46
|
-
: grade === 'D'
|
|
47
|
-
? 'orange'
|
|
48
|
-
: 'red';
|
|
49
|
-
return `https://img.shields.io/badge/codehere%20trust-${score}%25%20${grade}-${color}?logo=shield&logoColor=white`;
|
|
50
|
-
}
|
|
51
|
-
function narrativeSummary(report) {
|
|
52
|
-
const { overallScore, criticalCount, highCount, mediumCount, files } = report;
|
|
53
|
-
if (criticalCount === 0 && highCount === 0) {
|
|
54
|
-
if (overallScore >= 90) {
|
|
55
|
-
return (`Clean codebase. ${files.length} file${files.length !== 1 ? 's' : ''} scanned, nothing blocking a merge. ` +
|
|
56
|
-
(mediumCount > 0
|
|
57
|
-
? `${mediumCount} medium-severity pattern${mediumCount !== 1 ? 's' : ''} worth reviewing in a future sprint.`
|
|
58
|
-
: 'No significant findings.'));
|
|
59
|
-
}
|
|
60
|
-
return `Solid codebase with some medium/low findings. ${overallScore}/100 — worth a review pass but not blocking.`;
|
|
61
|
-
}
|
|
62
|
-
if (criticalCount > 0) {
|
|
63
|
-
return (`${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} found that should be resolved before this merges. ` +
|
|
64
|
-
`${highCount > 0 ? `Also ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} to address.` : ''}`);
|
|
65
|
-
}
|
|
66
|
-
return `No critical issues, but ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} worth addressing before merge.`;
|
|
67
|
-
}
|
|
68
|
-
function recommendationsMarkdown(report) {
|
|
69
|
-
const { criticalCount, highCount, mediumCount, lowCount } = report;
|
|
70
|
-
const lines = [];
|
|
71
|
-
if (criticalCount > 0) {
|
|
72
|
-
lines.push(`1. **Resolve all ${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} before merging.** These represent active security risks (credentials in code, eval injection, SQL without parameterization, etc.). They're usually quick fixes.`);
|
|
73
|
-
}
|
|
74
|
-
if (highCount > 0) {
|
|
75
|
-
lines.push(`${lines.length + 1}. **Address the ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} in this sprint.** Not blocking, but worth fixing while the code is fresh.`);
|
|
76
|
-
}
|
|
77
|
-
if (mediumCount > 0) {
|
|
78
|
-
lines.push(`${lines.length + 1}. **${mediumCount} medium-severity item${mediumCount !== 1 ? 's' : ''} can be queued for a future cleanup pass.** Mostly defensive hygiene — redirect hardening, input validation gaps.`);
|
|
79
|
-
}
|
|
80
|
-
if (lowCount > 0) {
|
|
81
|
-
lines.push(`${lines.length + 1}. **${lowCount} low-severity note${lowCount !== 1 ? 's' : ''} for informational purposes.** These rarely cause issues but keep the score from being perfect.`);
|
|
82
|
-
}
|
|
83
|
-
if (lines.length === 0) {
|
|
84
|
-
lines.push('1. **Nothing blocking.** Keep it up — this is what a healthy codebase looks like.');
|
|
85
|
-
}
|
|
86
|
-
return lines.join('\n\n');
|
|
87
|
-
}
|
|
88
|
-
function findingsMarkdown(files) {
|
|
89
|
-
const sections = [];
|
|
90
|
-
for (const severity of ['critical', 'high', 'medium', 'low']) {
|
|
91
|
-
const allFindings = [];
|
|
92
|
-
for (const f of files) {
|
|
93
|
-
for (const finding of f.findings.filter((x) => x.severity === severity)) {
|
|
94
|
-
allFindings.push({ filepath: f.filepath, ...finding });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (allFindings.length === 0)
|
|
98
|
-
continue;
|
|
99
|
-
sections.push(`### ${severityLabel(severity)}\n`);
|
|
100
|
-
// Cap per-severity at 10 most impactful findings
|
|
101
|
-
for (const f of allFindings.slice(0, 10)) {
|
|
102
|
-
const loc = f.line ? `:${f.line}` : '';
|
|
103
|
-
const cwe = f.cwe ? ` · ${f.cwe}` : '';
|
|
104
|
-
const matched = f.matched ? `\n> Matched: \`${f.matched.substring(0, 80)}\`` : '';
|
|
105
|
-
sections.push(`**${f.category}** · \`${f.filepath}${loc}\`${cwe} \n> ${f.description}${matched}\n`);
|
|
106
|
-
}
|
|
107
|
-
if (allFindings.length > 10) {
|
|
108
|
-
sections.push(`*... and ${allFindings.length - 10} more ${severity}-severity findings*\n`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return sections.length > 0 ? sections.join('\n') : '*No findings.*\n';
|
|
112
|
-
}
|
|
113
|
-
function generateMarkdown(report, target, cwd) {
|
|
114
|
-
const { overallScore, grade, files, criticalCount, highCount, mediumCount, lowCount, scannedAt } = report;
|
|
115
|
-
const targetLabel = basename(target) || '.';
|
|
116
|
-
const dateStr = new Date(scannedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
117
|
-
const badge = badgeUrl(overallScore, grade);
|
|
118
|
-
const summary = narrativeSummary(report);
|
|
119
|
-
const gradeIcon = gradeEmoji(grade);
|
|
120
|
-
const productCtx = loadProductContext(cwd);
|
|
121
|
-
const contextLine = productCtx
|
|
122
|
-
? `\nProject: **${productCtx.name}** · Risk level: **${productCtx.riskLevel}**${productCtx.productionApp ? ' · Production' : ''}\n`
|
|
123
|
-
: '';
|
|
124
|
-
const recommendation = adjustRecommendation(report.recommendation, criticalCount, highCount, overallScore, productCtx);
|
|
125
|
-
return `# Security Audit Report · ${targetLabel}
|
|
126
|
-
|
|
127
|
-
${gradeIcon} **Score: ${overallScore}/100 · Grade ${grade}**
|
|
128
|
-
Generated ${dateStr} by [codehere](https://github.com/codehere-dev/codehere)
|
|
129
|
-
${contextLine}
|
|
130
|
-
> ${recommendation}
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## Summary
|
|
135
|
-
|
|
136
|
-
${summary}
|
|
137
|
-
|
|
138
|
-
| | |
|
|
139
|
-
|---|---|
|
|
140
|
-
| Files scanned | ${files.length} |
|
|
141
|
-
| Critical | ${criticalCount > 0 ? `**${criticalCount}** 🚨` : criticalCount} |
|
|
142
|
-
| High | ${highCount > 0 ? `**${highCount}** ⚠️` : highCount} |
|
|
143
|
-
| Medium | ${mediumCount} |
|
|
144
|
-
| Low | ${lowCount} |
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## Findings
|
|
149
|
-
|
|
150
|
-
${findingsMarkdown(files)}
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## Recommendations
|
|
155
|
-
|
|
156
|
-
${recommendationsMarkdown(report)}
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
## Add to your README
|
|
161
|
-
|
|
162
|
-
\`\`\`markdown
|
|
163
|
-

|
|
164
|
-
\`\`\`
|
|
165
|
-
|
|
166
|
-
Renders as: 
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
*Generated with [codehere](https://github.com/codehere-dev/codehere) — every AI action, traced. Every agent, verified.*
|
|
171
|
-
`;
|
|
172
|
-
}
|
|
173
|
-
// ── Command ───────────────────────────────────────────────────────────────────
|
|
174
|
-
// ── Simple (non-technical) output ────────────────────────────────────────────
|
|
175
|
-
function generateSimpleReport(report, target) {
|
|
176
|
-
const { overallScore, grade, criticalCount, highCount, mediumCount, files } = report;
|
|
177
|
-
const targetLabel = basename(target) || 'your project';
|
|
178
|
-
const lines = [];
|
|
179
|
-
// Plain English headline
|
|
180
|
-
if (criticalCount > 0) {
|
|
181
|
-
lines.push(`Your AI assistant found ${criticalCount} serious problem${criticalCount !== 1 ? 's' : ''} in ${targetLabel} that need${criticalCount === 1 ? 's' : ''} fixing before you share this code.`);
|
|
182
|
-
}
|
|
183
|
-
else if (highCount > 0) {
|
|
184
|
-
lines.push(`${targetLabel} looks mostly safe. There ${highCount === 1 ? 'is' : 'are'} ${highCount} thing${highCount !== 1 ? 's' : ''} worth reviewing, but nothing that needs urgent attention.`);
|
|
185
|
-
}
|
|
186
|
-
else if (overallScore >= 90) {
|
|
187
|
-
lines.push(`${targetLabel} is clean. Your AI assistant found nothing concerning. You're good to go.`);
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
lines.push(`${targetLabel} is in reasonable shape. A few minor things to be aware of, but nothing blocking.`);
|
|
191
|
-
}
|
|
192
|
-
lines.push('');
|
|
193
|
-
// Score in human terms
|
|
194
|
-
const gradeMeaning = grade === 'A'
|
|
195
|
-
? 'Excellent'
|
|
196
|
-
: grade === 'B'
|
|
197
|
-
? 'Good'
|
|
198
|
-
: grade === 'C'
|
|
199
|
-
? 'Fair — some issues to address'
|
|
200
|
-
: grade === 'D'
|
|
201
|
-
? 'Needs work'
|
|
202
|
-
: 'Urgent attention needed';
|
|
203
|
-
lines.push(`Security score: ${overallScore}/100 (${gradeMeaning})`);
|
|
204
|
-
lines.push(`Files checked: ${files.length}`);
|
|
205
|
-
lines.push('');
|
|
206
|
-
// What was found, in plain terms
|
|
207
|
-
if (criticalCount > 0 || highCount > 0 || mediumCount > 0) {
|
|
208
|
-
lines.push('What was found:');
|
|
209
|
-
if (criticalCount > 0) {
|
|
210
|
-
lines.push(` • ${criticalCount} serious issue${criticalCount !== 1 ? 's' : ''} — things like exposed passwords, unsafe commands, or code that could be exploited`);
|
|
211
|
-
}
|
|
212
|
-
if (highCount > 0) {
|
|
213
|
-
lines.push(` • ${highCount} notable issue${highCount !== 1 ? 's' : ''} — worth fixing, but not an emergency`);
|
|
214
|
-
}
|
|
215
|
-
if (mediumCount > 0) {
|
|
216
|
-
lines.push(` • ${mediumCount} minor thing${mediumCount !== 1 ? 's' : ''} — good to know about, low urgency`);
|
|
217
|
-
}
|
|
218
|
-
lines.push('');
|
|
219
|
-
}
|
|
220
|
-
// Plain English what to do
|
|
221
|
-
lines.push('What to do:');
|
|
222
|
-
if (criticalCount > 0) {
|
|
223
|
-
lines.push(` 1. Ask your developer to look at the ${criticalCount} serious issue${criticalCount !== 1 ? 's' : ''} listed in the full report.`);
|
|
224
|
-
lines.push(' 2. Do not share or deploy this code until those are resolved.');
|
|
225
|
-
if (highCount > 0) {
|
|
226
|
-
lines.push(` 3. The ${highCount} notable issue${highCount !== 1 ? 's' : ''} can be addressed in your next sprint.`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
else if (highCount > 0) {
|
|
230
|
-
lines.push(` 1. Ask your developer to review the ${highCount} notable issue${highCount !== 1 ? 's' : ''} when they get a chance.`);
|
|
231
|
-
lines.push(' 2. This code is safe to continue working on in the meantime.');
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
lines.push(' 1. Keep going — this code is in good shape.');
|
|
235
|
-
lines.push(' 2. Run this scan again before your next release.');
|
|
236
|
-
}
|
|
237
|
-
lines.push('');
|
|
238
|
-
lines.push('---');
|
|
239
|
-
lines.push('Generated by codehere — know what your AI agent did before your tech lead asks.');
|
|
240
|
-
return lines.join('\n');
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Write report to the requested path. If the path is unwritable (EROFS,
|
|
244
|
-
* EACCES, EPERM, ENOENT on a parent dir we can't create), fall back to
|
|
245
|
-
* stdout so the user still gets the report instead of a stack trace.
|
|
246
|
-
*
|
|
247
|
-
* Returns true if the file was written, false if we fell back to stdout.
|
|
248
|
-
*
|
|
249
|
-
* Provoked by rc.25 UAT: `codehere report --simple` from `/` crashed with
|
|
250
|
-
* `EROFS: read-only file system, open 'codehere-audit.md'`. A user running
|
|
251
|
-
* the command from any unwritable cwd should still get their report.
|
|
252
|
-
*/
|
|
253
|
-
function writeReportWithFallback(targetPath, content, label) {
|
|
254
|
-
try {
|
|
255
|
-
writeFileSync(targetPath, content);
|
|
256
|
-
console.log(success(`${label} written to ${targetPath}`));
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
catch (e) {
|
|
260
|
-
const code = e.code ?? '';
|
|
261
|
-
if (['EROFS', 'EACCES', 'EPERM', 'ENOENT'].includes(code)) {
|
|
262
|
-
console.error(error(`Could not write to ${targetPath} (${code}). Printing to stdout instead — pass --out <path> to a writable location.`));
|
|
263
|
-
process.stdout.write(content + '\n');
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
throw e;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
export async function executeReportCommand(target, options = {}) {
|
|
270
|
-
const cwd = process.cwd();
|
|
271
|
-
const resolved = target ? (isAbsolute(target) ? target : join(cwd, target)) : cwd;
|
|
272
|
-
// rc.18: explicit mutex on combinations Codex flagged as silently
|
|
273
|
-
// ambiguous. Two pairs:
|
|
274
|
-
// format {--json, --simple} — pick at most one output format
|
|
275
|
-
// dest {--stdout, --out} — pick at most one destination
|
|
276
|
-
// Previously the second flag silently lost; now we error so users know.
|
|
277
|
-
if (options.json && options.simple) {
|
|
278
|
-
console.error(error('--json and --simple cannot be used together. Pick one output format.'));
|
|
279
|
-
process.exit(2);
|
|
280
|
-
}
|
|
281
|
-
if (options.stdout && options.out) {
|
|
282
|
-
console.error(error('--stdout and --out cannot be used together. Pick one destination.'));
|
|
283
|
-
process.exit(2);
|
|
284
|
-
}
|
|
285
|
-
// Default routing rules (after mutex check):
|
|
286
|
-
// --json → stdout (pipeable to jq, every CLI convention).
|
|
287
|
-
// --simple / md → file (user expects a shareable artifact, not a wall of markdown).
|
|
288
|
-
// --out <path> → wins over the default (explicit beats default).
|
|
289
|
-
// --stdout → forces stdout for non-json formats.
|
|
290
|
-
// Rationale: rc.15 — `report --json` writing to disk silently broke pipelines
|
|
291
|
-
// (`codehere report --json | jq` produced empty stdout). Stdout for machine-
|
|
292
|
-
// readable formats matches `kubectl get -o json`, `docker inspect`, etc.
|
|
293
|
-
const defaultOut = options.json
|
|
294
|
-
? undefined // stdout
|
|
295
|
-
: 'codehere-audit.md';
|
|
296
|
-
const effectiveOut = options.stdout ? undefined : (options.out ?? defaultOut);
|
|
297
|
-
if (!existsSync(resolved)) {
|
|
298
|
-
console.error(error(`Not found: ${resolved}`));
|
|
299
|
-
process.exit(1);
|
|
300
|
-
}
|
|
301
|
-
// Collect and scan
|
|
302
|
-
const paths = collectCodeFiles(resolved);
|
|
303
|
-
if (paths.length === 0) {
|
|
304
|
-
console.log(info('No code files found to scan.'));
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
// Show progress for large codebases
|
|
308
|
-
if (!effectiveOut && paths.length > 20 && process.stderr.isTTY) {
|
|
309
|
-
process.stderr.write(colors.dim(` Scanning ${paths.length} files...`));
|
|
310
|
-
}
|
|
311
|
-
const results = paths.map((p) => scoreFile(relative(cwd, p), readFileSync(p, 'utf-8')));
|
|
312
|
-
// Clear progress line
|
|
313
|
-
if (!effectiveOut && paths.length > 20 && process.stderr.isTTY) {
|
|
314
|
-
process.stderr.write('\r\x1b[K');
|
|
315
|
-
}
|
|
316
|
-
const totalFindings = results.reduce((s, r) => s + r.findings.length, 0);
|
|
317
|
-
const criticalCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'critical').length, 0);
|
|
318
|
-
const highCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'high').length, 0);
|
|
319
|
-
const mediumCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'medium').length, 0);
|
|
320
|
-
const lowCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'low').length, 0);
|
|
321
|
-
const overallScore = results.length > 0 ? Math.round(results.reduce((s, r) => s + r.score, 0) / results.length) : 100;
|
|
322
|
-
const grade = overallScore >= 90 ? 'A' : overallScore >= 75 ? 'B' : overallScore >= 60 ? 'C' : overallScore >= 40 ? 'D' : 'F';
|
|
323
|
-
const recommendation = criticalCount > 0
|
|
324
|
-
? `${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} need${criticalCount === 1 ? 's' : ''} attention before this merges.`
|
|
325
|
-
: highCount > 0
|
|
326
|
-
? `Nothing critical, but ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} worth addressing.`
|
|
327
|
-
: overallScore >= 75
|
|
328
|
-
? 'Clean scan. Nothing blocking a merge.'
|
|
329
|
-
: 'A few things to look at — nothing critical.';
|
|
330
|
-
const report = {
|
|
331
|
-
files: results,
|
|
332
|
-
overallScore,
|
|
333
|
-
grade,
|
|
334
|
-
totalFindings,
|
|
335
|
-
criticalCount,
|
|
336
|
-
highCount,
|
|
337
|
-
mediumCount,
|
|
338
|
-
lowCount,
|
|
339
|
-
recommendation,
|
|
340
|
-
scannedAt: new Date().toISOString(),
|
|
341
|
-
};
|
|
342
|
-
if (paths.length > 20) {
|
|
343
|
-
process.stderr.write(' '.repeat(40) + '\r');
|
|
344
|
-
}
|
|
345
|
-
// JSON output
|
|
346
|
-
if (options.json) {
|
|
347
|
-
const output = JSON.stringify(report, null, 2);
|
|
348
|
-
if (effectiveOut) {
|
|
349
|
-
writeReportWithFallback(effectiveOut, output, 'Report');
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
console.log(output);
|
|
353
|
-
}
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
// Simple (non-technical) output
|
|
357
|
-
if (options.simple) {
|
|
358
|
-
const simple = generateSimpleReport(report, resolved);
|
|
359
|
-
if (effectiveOut) {
|
|
360
|
-
writeReportWithFallback(effectiveOut, simple, 'Plain-English report');
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
process.stdout.write(simple + '\n');
|
|
364
|
-
}
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
// Markdown output
|
|
368
|
-
const markdown = generateMarkdown(report, resolved, cwd);
|
|
369
|
-
if (effectiveOut) {
|
|
370
|
-
if (!writeReportWithFallback(effectiveOut, markdown, 'Report'))
|
|
371
|
-
return;
|
|
372
|
-
newline();
|
|
373
|
-
console.log(colors.dim(` ${paths.length} files · score ${overallScore}/100 · grade ${grade}`));
|
|
374
|
-
if (criticalCount > 0) {
|
|
375
|
-
console.log(colors.red(` ${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} — check the report`));
|
|
376
|
-
}
|
|
377
|
-
newline();
|
|
378
|
-
console.log(colors.dim(' Share it: paste it in your PR, add the badge to your README.'));
|
|
379
|
-
console.log(colors.dim(' Use --stdout to pipe the report instead of writing a file.'));
|
|
380
|
-
newline();
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
// --stdout: print to stdout (can be piped)
|
|
384
|
-
process.stdout.write(markdown);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
//# sourceMappingURL=report-command.js.map
|