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,288 +0,0 @@
1
- /**
2
- * CLI: `codehere thread` · v0.8 S4 · signed-payload share + import
3
- *
4
- * Subcommands:
5
- * export <session-id> [--out <file>] [--json]
6
- * Sign the session bundle and write it to <file> (or stdout when no --out).
7
- * import <file> [--trust] [--json]
8
- * Verify the signature. If the signer's key isn't trusted, refuse unless
9
- * --trust is set OR the user is on a TTY and confirms. On valid+trusted,
10
- * merge the session into ~/.codehere/sessions.jsonl so it appears in the
11
- * dashboard.
12
- * keys · print the local public key
13
- * trust <pubkey> [label] · add a public key to the trusted set
14
- * untrust <pubkey> · remove
15
- *
16
- * No network. Both export and import are local file operations.
17
- */
18
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
19
- import { homedir, hostname } from 'os';
20
- import { dirname, join, isAbsolute, resolve } from 'path';
21
- import { colors, symbols } from '../../../ui.js';
22
- import { signBundle, verifyBundle, shortFingerprint, } from '../../../application/use-cases/thread-export.js';
23
- import { loadOrCreateKeypair, localPublicKeyBase64, trustKey, untrustKey, trustedKeySet, loadTrustedKeys, isKeyTrusted, } from '../../../infrastructure/orchestration/thread-key-store.js';
24
- import { loadSessionById } from '../../../infrastructure/orchestration/session-persistence.js';
25
- import { getVersion } from '../../../utils/version.js';
26
- export function executeThreadCommand(sub, arg, extra, options = {}) {
27
- switch (sub) {
28
- case 'export':
29
- return exportThread(arg, options);
30
- case 'import':
31
- return importThread(arg, options);
32
- case 'keys':
33
- return showKeys(options.json ?? false);
34
- case 'trust':
35
- return trustCmd(arg, options.label, options.json ?? false);
36
- case 'untrust':
37
- return untrustCmd(arg, options.json ?? false);
38
- default:
39
- process.stderr.write(colors.error(`${symbols.error} Unknown subcommand: ${sub ?? '(none)'}\n`) +
40
- colors.dim('Try: codehere thread export <id> | import <file> | keys | trust <key> | untrust <key>\n'));
41
- return 2;
42
- }
43
- }
44
- /* ──────────────────────────────────────────────────────────────────────── */
45
- /* export */
46
- /* ──────────────────────────────────────────────────────────────────────── */
47
- function exportThread(sessionId, options) {
48
- if (!sessionId) {
49
- process.stderr.write(colors.error(`${symbols.error} Need a session id\n`));
50
- return 2;
51
- }
52
- const session = loadSessionById(sessionId);
53
- if (!session) {
54
- process.stderr.write(colors.error(`${symbols.error} No session found matching "${sessionId}"\n`));
55
- return 2;
56
- }
57
- const keys = loadOrCreateKeypair();
58
- const body = {
59
- schema: 1,
60
- kind: 'thread',
61
- sessionId: session.sessionId,
62
- session,
63
- exporter: {
64
- hostname: safeHostname(),
65
- codehereVersion: getVersion(),
66
- },
67
- exportedAt: new Date().toISOString(),
68
- };
69
- const bundle = signBundle(body, keys);
70
- const text = JSON.stringify(bundle, null, 2);
71
- if (options.out) {
72
- const outPath = isAbsolute(options.out) ? options.out : resolve(process.cwd(), options.out);
73
- mkdirSync(dirname(outPath), { recursive: true });
74
- writeFileSync(outPath, text, 'utf-8');
75
- if (options.json) {
76
- process.stdout.write(JSON.stringify({ ok: true, path: outPath, fingerprint: shortFingerprint(bundle.publicKey) }) + '\n');
77
- }
78
- else {
79
- process.stdout.write(colors.green(`${symbols.success} Exported`) +
80
- colors.dim(` ${outPath}\n`) +
81
- colors.dim(` signed by ${shortFingerprint(bundle.publicKey)}\n`) +
82
- colors.dim(` share via Slack/email/PR · recipient runs: `) +
83
- colors.bold(`codehere thread import ${outPath}\n`));
84
- }
85
- return 0;
86
- }
87
- // No --out · print to stdout (machine-pipe-friendly)
88
- process.stdout.write(text + '\n');
89
- return 0;
90
- }
91
- /* ──────────────────────────────────────────────────────────────────────── */
92
- /* import */
93
- /* ──────────────────────────────────────────────────────────────────────── */
94
- function importThread(filePath, options) {
95
- if (!filePath) {
96
- process.stderr.write(colors.error(`${symbols.error} Need a file path\n`));
97
- return 2;
98
- }
99
- const fp = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
100
- if (!existsSync(fp)) {
101
- process.stderr.write(colors.error(`${symbols.error} Not found: ${fp}\n`));
102
- return 2;
103
- }
104
- let raw;
105
- try {
106
- raw = JSON.parse(readFileSync(fp, 'utf-8'));
107
- }
108
- catch (err) {
109
- process.stderr.write(colors.error(`${symbols.error} Invalid JSON: ${err instanceof Error ? err.message : String(err)}\n`));
110
- return 2;
111
- }
112
- const verdict = verifyBundle(raw, trustedKeySet());
113
- if (verdict.status === 'malformed') {
114
- if (options.json) {
115
- process.stdout.write(JSON.stringify({ ok: false, error: 'malformed', detail: verdict.reason }) + '\n');
116
- }
117
- else {
118
- process.stderr.write(colors.error(`${symbols.error} Malformed bundle: ${verdict.reason}\n`));
119
- }
120
- return 2;
121
- }
122
- if (verdict.status === 'tampered') {
123
- if (options.json) {
124
- process.stdout.write(JSON.stringify({ ok: false, error: 'tampered', detail: verdict.reason }) + '\n');
125
- }
126
- else {
127
- process.stderr.write(colors.error(`${symbols.error} Signature INVALID · refusing to import\n`) +
128
- colors.dim(` ${verdict.reason}\n`));
129
- }
130
- return 1;
131
- }
132
- // status: 'valid' or 'unknown_key'
133
- const bundle = verdict.status === 'valid' ? verdict.bundle : verdict.bundle;
134
- const fp_key = shortFingerprint(bundle.publicKey);
135
- if (verdict.status === 'unknown_key') {
136
- if (!options.trust) {
137
- if (options.json) {
138
- process.stdout.write(JSON.stringify({
139
- ok: false,
140
- error: 'unknown_key',
141
- publicKey: bundle.publicKey,
142
- fingerprint: fp_key,
143
- hint: 'Re-run with --trust to add this key to your trust store and import.',
144
- }) + '\n');
145
- }
146
- else {
147
- process.stderr.write(colors.warning(`${symbols.warning} Signer's key is not in your trust store\n`) +
148
- colors.dim(` fingerprint: ${fp_key}\n`) +
149
- colors.dim(` The signature is cryptographically valid but you've never trusted this key.\n`) +
150
- colors.dim(` To trust + import: `) +
151
- colors.bold(`codehere thread import ${fp} --trust\n`) +
152
- colors.dim(` To inspect first: `) +
153
- colors.bold(`codehere thread trust ${bundle.publicKey} <label>`) +
154
- '\n');
155
- }
156
- return 2;
157
- }
158
- // --trust set · add key to store and continue
159
- trustKey(bundle.publicKey, options.label);
160
- }
161
- // Persist into ~/.codehere/sessions.jsonl as 3 records (spawned, completed, finalReport)
162
- const ses = bundle.body.session;
163
- const home = process.env.CODEHERE_HOME || homedir();
164
- const sessionsPath = join(home, '.codehere', 'sessions.jsonl');
165
- mkdirSync(dirname(sessionsPath), { recursive: true });
166
- const lines = [];
167
- lines.push(JSON.stringify({
168
- t: 'spawned',
169
- id: ses.sessionId,
170
- agent: ses.agent,
171
- prompt: ses.prompt,
172
- cwd: ses.cwd,
173
- startedAt: ses.startedAt,
174
- }));
175
- lines.push(JSON.stringify({
176
- t: 'completed',
177
- id: ses.sessionId,
178
- status: ses.status,
179
- durationMs: ses.durationMs,
180
- completedAt: ses.completedAt,
181
- steps: ses.steps,
182
- artifacts: ses.artifacts,
183
- }));
184
- if (ses.finalReport) {
185
- lines.push(JSON.stringify({
186
- t: 'finalReport',
187
- id: ses.sessionId,
188
- summary: ses.finalReport.summary,
189
- generatedAt: ses.finalReport.generatedAt,
190
- durationMs: ses.finalReport.durationMs,
191
- }));
192
- }
193
- appendFileSync(sessionsPath, lines.join('\n') + '\n', 'utf-8');
194
- if (options.json) {
195
- process.stdout.write(JSON.stringify({
196
- ok: true,
197
- sessionId: ses.sessionId,
198
- trusted: verdict.status === 'valid' ? true : 'just-added',
199
- fingerprint: fp_key,
200
- }) + '\n');
201
- }
202
- else {
203
- process.stdout.write(colors.green(`${symbols.success} Imported`) +
204
- colors.dim(` session ${ses.sessionId.slice(0, 8)} · signed by ${fp_key}\n`) +
205
- colors.dim(` view it: `) +
206
- colors.bold(`codehere start`) +
207
- colors.dim(` then click the task in the dashboard\n`));
208
- }
209
- return 0;
210
- }
211
- /* ──────────────────────────────────────────────────────────────────────── */
212
- /* keys / trust / untrust */
213
- /* ──────────────────────────────────────────────────────────────────────── */
214
- function showKeys(json) {
215
- loadOrCreateKeypair();
216
- const localB64 = localPublicKeyBase64() ?? '';
217
- const trusted = loadTrustedKeys();
218
- if (json) {
219
- process.stdout.write(JSON.stringify({
220
- local: { publicKey: localB64, fingerprint: shortFingerprint(localB64) },
221
- trusted: Object.entries(trusted.keys).map(([k, v]) => ({
222
- publicKey: k,
223
- fingerprint: shortFingerprint(k),
224
- label: v.label,
225
- addedAt: v.addedAt,
226
- })),
227
- }, null, 2) + '\n');
228
- return 0;
229
- }
230
- process.stdout.write('\n' + colors.bold('This machine') + '\n');
231
- process.stdout.write(' ' + colors.cyan(shortFingerprint(localB64)) + colors.dim(' signs your exports\n\n'));
232
- process.stdout.write(colors.bold('Trusted keys') + colors.dim(' (imports from these accepted silently)\n'));
233
- const entries = Object.entries(trusted.keys);
234
- if (entries.length === 0) {
235
- process.stdout.write(' ' + colors.dim('(empty)\n'));
236
- }
237
- else {
238
- for (const [k, v] of entries) {
239
- const label = v.label || '(no label)';
240
- process.stdout.write(' ' + colors.cyan(shortFingerprint(k)) + ' ' + colors.dim(label) + '\n');
241
- }
242
- }
243
- process.stdout.write('\n' + colors.dim('Add a key: ') + colors.bold('codehere thread trust <pubkey> <label>') + '\n');
244
- process.stdout.write(colors.dim('Remove a key: ') + colors.bold('codehere thread untrust <pubkey>') + '\n\n');
245
- return 0;
246
- }
247
- function trustCmd(pubkey, label, json) {
248
- if (!pubkey) {
249
- process.stderr.write(colors.error(`${symbols.error} Need a public key (base64)\n`));
250
- return 2;
251
- }
252
- trustKey(pubkey, label);
253
- if (json) {
254
- process.stdout.write(JSON.stringify({ ok: true, fingerprint: shortFingerprint(pubkey) }) + '\n');
255
- }
256
- else {
257
- process.stdout.write(colors.green(`${symbols.success} trusted ${shortFingerprint(pubkey)}\n`));
258
- }
259
- return 0;
260
- }
261
- function untrustCmd(pubkey, json) {
262
- if (!pubkey) {
263
- process.stderr.write(colors.error(`${symbols.error} Need a public key (base64)\n`));
264
- return 2;
265
- }
266
- const ok = untrustKey(pubkey);
267
- if (json) {
268
- process.stdout.write(JSON.stringify({ ok }) + '\n');
269
- }
270
- else if (ok) {
271
- process.stdout.write(colors.green(`${symbols.success} untrusted ${shortFingerprint(pubkey)}\n`));
272
- }
273
- else {
274
- process.stdout.write(colors.gray(`${symbols.dot} key not in trust store (or it's this machine's own key — can't remove)\n`));
275
- }
276
- return 0;
277
- }
278
- function safeHostname() {
279
- try {
280
- return hostname();
281
- }
282
- catch {
283
- return undefined;
284
- }
285
- }
286
- // Public alias for the connection-status check used in the daemon HTTP routes.
287
- export { isKeyTrusted };
288
- //# sourceMappingURL=thread-command.js.map
@@ -1,306 +0,0 @@
1
- /**
2
- * CLI Command: trust
3
- *
4
- * Layer 0 trust scorer — instant static analysis, zero API calls.
5
- * Runs on a file, directory, or piped git diff.
6
- *
7
- * Produces a trust score 0–100 with plain-English explanation:
8
- * 100 = no findings
9
- * 80+ = minor issues, safe to merge
10
- * 60-79 = review recommended
11
- * 40-59 = high-risk, human review required
12
- * < 40 = block until resolved
13
- *
14
- * Usage:
15
- * codehere trust src/utils.ts # score a file
16
- * codehere trust . # score all files in directory
17
- * git diff HEAD | codehere trust # score a diff
18
- * codehere trust --diff HEAD # equivalent using git directly
19
- * codehere trust --badge # output badge markdown
20
- */
21
- import { existsSync } from 'fs';
22
- import { join, relative, isAbsolute } from 'path';
23
- import { execFileSync } from 'child_process';
24
- import { scoreFile } from '../../../infrastructure/security/trust-scorer.js';
25
- import { collectCodeFiles } from '../../../infrastructure/security/file-collection.js';
26
- import { colors, sectionHeader, error, newline, info, randomCleanScan } from '../../../ui.js';
27
- import { loadProductContext, adjustRecommendation } from '../../../infrastructure/config/product-context.js';
28
- // ── File collection ──────────────────────────────────────────────────────────
29
- // See src/infrastructure/security/file-collection.ts for exclusion rules.
30
- /**
31
- * Read a file safely — handles iCloud-evicted files on macOS.
32
- * readFileSync blocks forever on iCloud-evicted files (dehydrated placeholders in ~/Documents).
33
- * We use cat with a timeout as a subprocess so the main thread never blocks.
34
- */
35
- function safeReadFile(filepath) {
36
- try {
37
- return execFileSync('cat', [filepath], { encoding: 'utf-8', timeout: 2000, maxBuffer: 5 * 1024 * 1024 });
38
- }
39
- catch {
40
- return null; // File unreadable (iCloud-evicted, permissions, too large) — skip silently
41
- }
42
- }
43
- // ── Git diff mode ────────────────────────────────────────────────────────────
44
- function parseDiffFiles(diff) {
45
- const files = [];
46
- const fileBlocks = diff.split(/^diff --git /m).filter(Boolean);
47
- for (const block of fileBlocks) {
48
- const filenameMatch = block.match(/^a\/(.+?) b\/(.+?)$/m);
49
- if (!filenameMatch)
50
- continue;
51
- const filepath = filenameMatch[2];
52
- // Skip internal backup/cache directories to avoid false findings from old code
53
- if (filepath.startsWith('.codehere/') || filepath.startsWith('.codehere-cache/'))
54
- continue;
55
- // Extract added lines only (lines starting with +, excluding +++)
56
- const addedLines = block
57
- .split('\n')
58
- .filter((l) => l.startsWith('+') && !l.startsWith('+++'))
59
- .map((l) => l.substring(1))
60
- .join('\n');
61
- if (addedLines.trim()) {
62
- files.push({ filepath, code: addedLines });
63
- }
64
- }
65
- return files;
66
- }
67
- // ── Report formatting ────────────────────────────────────────────────────────
68
- function gradeColor(grade) {
69
- switch (grade) {
70
- case 'A':
71
- return colors.green(grade);
72
- case 'B':
73
- return colors.cyan(grade);
74
- case 'C':
75
- return colors.yellow(grade);
76
- case 'D':
77
- return colors.red(grade);
78
- case 'F':
79
- return colors.red(colors.bold(grade));
80
- default:
81
- return grade;
82
- }
83
- }
84
- function scoreBar(score, width = 20) {
85
- const filled = Math.round((score / 100) * width);
86
- const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
87
- if (score >= 75)
88
- return colors.green(bar);
89
- if (score >= 60)
90
- return colors.yellow(bar);
91
- return colors.red(bar);
92
- }
93
- function badgeMarkdown(report) {
94
- const color = report.grade === 'A'
95
- ? 'brightgreen'
96
- : report.grade === 'B'
97
- ? 'green'
98
- : report.grade === 'C'
99
- ? 'yellow'
100
- : report.grade === 'D'
101
- ? 'orange'
102
- : 'red';
103
- return `![codehere trust](https://img.shields.io/badge/codehere%20trust-${report.overallScore}%25%20${report.grade}-${color}?logo=shield)`;
104
- }
105
- // ── Main command ─────────────────────────────────────────────────────────────
106
- export async function executeTrustCommand(target, options = {}) {
107
- const cwd = process.cwd();
108
- let filesToScan = [];
109
- // ── Input source ──────────────────────────────────────────────────────────
110
- if (options.diff !== undefined) {
111
- // Run git diff and scan added lines
112
- try {
113
- // THREAT-2 fix · ref comes from a user-supplied CLI flag. Using template-
114
- // string interpolation with execSync allows shell metacharacters in `ref`
115
- // (e.g. `--diff "main; rm -rf /"`) to escape the git invocation. Use the
116
- // execFileSync array form so the shell is never invoked.
117
- const ref = options.diff || 'HEAD';
118
- const diffOutput = execFileSync('git', ['diff', ref], { cwd, encoding: 'utf-8' });
119
- filesToScan = parseDiffFiles(diffOutput);
120
- if (filesToScan.length === 0) {
121
- console.log(info('No changed files to scan'));
122
- return;
123
- }
124
- }
125
- catch (e) {
126
- console.error(error('git diff failed: ' + String(e)));
127
- process.exit(1);
128
- }
129
- }
130
- else if (!process.stdin.isTTY && !target) {
131
- // Piped input (e.g., git diff HEAD | codehere trust)
132
- let diffInput = '';
133
- for await (const chunk of process.stdin) {
134
- diffInput += chunk;
135
- }
136
- if (diffInput.trim().startsWith('diff --git')) {
137
- filesToScan = parseDiffFiles(diffInput);
138
- }
139
- else {
140
- // Treat as raw code
141
- filesToScan = [{ filepath: 'stdin', code: diffInput }];
142
- }
143
- if (filesToScan.length === 0) {
144
- console.log(info('No content to scan'));
145
- return;
146
- }
147
- }
148
- else {
149
- // File or directory
150
- const resolved = target ? (isAbsolute(target) ? target : join(cwd, target)) : cwd;
151
- if (!existsSync(resolved)) {
152
- console.error(error(`Not found: ${resolved}`));
153
- process.exit(1);
154
- }
155
- const paths = collectCodeFiles(resolved);
156
- let skippedCount = 0;
157
- for (const p of paths) {
158
- const code = safeReadFile(p);
159
- if (code !== null) {
160
- filesToScan.push({ filepath: relative(cwd, p), code });
161
- }
162
- else {
163
- skippedCount++;
164
- }
165
- }
166
- if (skippedCount > 0) {
167
- console.log(colors.dim(` Skipped ${skippedCount} unreadable file${skippedCount > 1 ? 's' : ''}`));
168
- }
169
- if (filesToScan.length === 0 && skippedCount > 0) {
170
- console.log(info('No readable files to scan.'));
171
- console.log(colors.dim(' Check file permissions or availability and retry.'));
172
- return;
173
- }
174
- }
175
- // ── Scan ──────────────────────────────────────────────────────────────────
176
- const results = filesToScan.map((f) => scoreFile(f.filepath, f.code));
177
- const totalFindings = results.reduce((s, r) => s + r.findings.length, 0);
178
- const criticalCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'critical').length, 0);
179
- const highCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'high').length, 0);
180
- const mediumCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'medium').length, 0);
181
- const lowCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'low').length, 0);
182
- const overallScore = results.length > 0 ? Math.round(results.reduce((s, r) => s + r.score, 0) / results.length) : 100;
183
- const grade = overallScore >= 90 ? 'A' : overallScore >= 75 ? 'B' : overallScore >= 60 ? 'C' : overallScore >= 40 ? 'D' : 'F';
184
- const baseRecommendation = criticalCount > 0
185
- ? `${criticalCount} critical ${criticalCount === 1 ? 'issue' : 'issues'} — worth sorting before this merges.`
186
- : highCount > 0
187
- ? `Nothing critical, but ${highCount} high-severity ${highCount === 1 ? 'pattern' : 'patterns'} worth a look.`
188
- : mediumCount + lowCount > 0
189
- ? `${mediumCount + lowCount} low-priority ${mediumCount + lowCount === 1 ? 'finding' : 'findings'} — nothing blocking, but worth a pass.`
190
- : 'Clean scan. Nothing blocking a merge.';
191
- const productCtx = loadProductContext(process.cwd());
192
- const recommendation = adjustRecommendation(baseRecommendation, criticalCount, highCount, overallScore, productCtx);
193
- const report = {
194
- files: results,
195
- overallScore,
196
- grade,
197
- totalFindings,
198
- criticalCount,
199
- highCount,
200
- mediumCount,
201
- lowCount,
202
- recommendation,
203
- scannedAt: new Date().toISOString(),
204
- };
205
- // Track CI failure — will throw after display so the CLI action handler catches it
206
- const ciFailed = options.ci && (overallScore < 60 || criticalCount > 0);
207
- // ── Output ────────────────────────────────────────────────────────────────
208
- if (options.json) {
209
- console.log(JSON.stringify(report, null, 2));
210
- if (options.ci && (overallScore < 60 || criticalCount > 0))
211
- process.exit(1);
212
- return;
213
- }
214
- if (options.badge) {
215
- console.log(badgeMarkdown(report));
216
- return;
217
- }
218
- // Human-readable report
219
- newline();
220
- sectionHeader('codehere Trust Report');
221
- newline();
222
- // Score
223
- console.log(` Score ${scoreBar(overallScore)} ${colors.bold(String(overallScore))}/100 ${gradeColor(grade)}`);
224
- newline();
225
- // Findings summary (inline, compact)
226
- const parts = [
227
- criticalCount > 0 ? colors.red(`${criticalCount} critical`) : null,
228
- highCount > 0 ? colors.yellow(`${highCount} high`) : null,
229
- mediumCount > 0 ? colors.dim(`${mediumCount} medium`) : null,
230
- lowCount > 0 ? colors.dim(`${lowCount} low`) : null,
231
- ].filter(Boolean);
232
- if (parts.length === 0) {
233
- console.log(` ${colors.green('✓')} ${randomCleanScan()}`);
234
- }
235
- else {
236
- console.log(` ${parts.join(colors.dim(' · '))}`);
237
- }
238
- newline();
239
- // Per-file results (show worst first)
240
- if (results.length > 1) {
241
- const sorted = [...results].sort((a, b) => a.score - b.score);
242
- console.log(colors.dim(' FILES'));
243
- for (const r of sorted.slice(0, 10)) {
244
- const icon = r.score >= 75 ? colors.green('✓') : r.score >= 60 ? colors.yellow('!') : colors.red('✗');
245
- console.log(` ${icon} ${gradeColor(r.grade)} ${String(r.score).padStart(3)}/100 ${r.filepath}`);
246
- }
247
- if (sorted.length > 10) {
248
- console.log(colors.dim(` + ${sorted.length - 10} more files`));
249
- }
250
- newline();
251
- }
252
- // Findings detail
253
- if (options.verbose) {
254
- console.log(colors.dim(' FINDINGS'));
255
- for (const r of results) {
256
- if (r.findings.length === 0)
257
- continue;
258
- console.log(` ${colors.bold(r.filepath)}`);
259
- for (const f of r.findings) {
260
- const sev = f.severity === 'critical'
261
- ? colors.red('crit')
262
- : f.severity === 'high'
263
- ? colors.yellow('high')
264
- : colors.dim(f.severity.slice(0, 4));
265
- const ln = f.line ? colors.dim(`L${f.line}`) : '';
266
- console.log(` ${sev} ${ln.padEnd(6)} ${f.description}`);
267
- }
268
- }
269
- newline();
270
- }
271
- else if (totalFindings > 0) {
272
- console.log(colors.dim(' FINDINGS'));
273
- const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
274
- const allFindings = results.flatMap((r) => r.findings.map((f) => ({ ...f, filepath: r.filepath })));
275
- const sorted = allFindings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
276
- for (const f of sorted.slice(0, 5)) {
277
- const sev = f.severity === 'critical'
278
- ? colors.red('crit')
279
- : f.severity === 'high'
280
- ? colors.yellow('high')
281
- : colors.dim(f.severity.slice(0, 4));
282
- const ln = f.line ? `:${f.line}` : '';
283
- console.log(` ${sev} ${f.filepath}${ln}`);
284
- console.log(` ${colors.dim(f.description)}`);
285
- }
286
- if (sorted.length > 5) {
287
- console.log(colors.dim(` + ${sorted.length - 5} more findings (use --verbose to see all)`));
288
- }
289
- newline();
290
- }
291
- // Recommendation
292
- const recColor = criticalCount > 0 ? colors.red : highCount > 0 ? colors.yellow : colors.green;
293
- console.log(recColor(recommendation));
294
- newline();
295
- // Tips footer (quiet, separated)
296
- if (results.length > 0 && !options.ci) {
297
- console.log(colors.dim(' ─────'));
298
- console.log(colors.dim(' trust --badge Add a badge to your README'));
299
- console.log(colors.dim(' trust --ci Exit 1 on critical findings'));
300
- newline();
301
- }
302
- if (ciFailed) {
303
- throw new Error(`CI_FAIL:${criticalCount > 0 ? criticalCount + ' critical finding(s)' : 'score ' + overallScore + ' below threshold 60'}`);
304
- }
305
- }
306
- //# sourceMappingURL=trust-command.js.map