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,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 ``;
|
|
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
|