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.
Files changed (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. 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
- ![codehere trust](${badge})
164
- \`\`\`
165
-
166
- Renders as: ![codehere trust](${badge})
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