codehere 1.0.2 → 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -2
- package/README.md +89 -46
- package/bin/codehere.js +90 -0
- package/package.json +20 -64
- package/src/brain.js +186 -0
- package/src/commands.js +445 -0
- package/src/gemma.js +48 -0
- package/src/hooks.js +442 -0
- package/src/signals.js +186 -0
- package/dist/application/use-cases/check-claimed-tests.js +0 -81
- package/dist/application/use-cases/detect-test-weakening.js +0 -237
- package/dist/application/use-cases/rerun-tests.js +0 -329
- package/dist/index.js +0 -111
- package/dist/infrastructure/orchestration/artifacts-extractor.js +0 -66
- package/dist/presentation/cli/commands/check-command.js +0 -251
- package/dist/presentation/cli/commands/help-command.js +0 -77
- package/dist/ui.js +0 -53
- package/dist/utils/typo-guard.js +0 -133
- package/dist/utils/version.js +0 -40
package/dist/index.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* codehere CLI entry: `check` re-runs a project's tests and reports what actually passed.
|
|
4
|
-
* The earlier orchestrator, daemon, web UI and the other commands are on the legacy/2026-10 branch.
|
|
5
|
-
*/
|
|
6
|
-
import { Command } from 'commander';
|
|
7
|
-
import { join, dirname } from 'path';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
|
-
import { readFileSync } from 'fs';
|
|
10
|
-
// A crash must stop the process: `process.exitCode = 1` is only honoured once the event loop drains.
|
|
11
|
-
function safeVersion() {
|
|
12
|
-
try {
|
|
13
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8')).version ?? 'unknown';
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return 'unknown';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function crashReport(kind, err) {
|
|
21
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
22
|
-
return [
|
|
23
|
-
'',
|
|
24
|
-
`[codehere] ${kind} — this process is stopping.`,
|
|
25
|
-
` version : ${safeVersion()}`,
|
|
26
|
-
` node : ${process.version} on ${process.platform} ${process.arch}`,
|
|
27
|
-
'',
|
|
28
|
-
(e.stack ?? e.message)
|
|
29
|
-
.split('\n')
|
|
30
|
-
.map((l) => ` ${l}`)
|
|
31
|
-
.join('\n'),
|
|
32
|
-
'',
|
|
33
|
-
' Report at https://github.com/muhammadegaa/codehere/issues with the block above.',
|
|
34
|
-
'',
|
|
35
|
-
].join('\n');
|
|
36
|
-
}
|
|
37
|
-
process.on('uncaughtException', (err) => {
|
|
38
|
-
console.error(crashReport('uncaughtException', err));
|
|
39
|
-
process.exit(1);
|
|
40
|
-
});
|
|
41
|
-
process.on('unhandledRejection', (reason) => {
|
|
42
|
-
console.error(crashReport('unhandledRejection', reason));
|
|
43
|
-
process.exit(1);
|
|
44
|
-
});
|
|
45
|
-
import { executeCheckCommand } from './presentation/cli/commands/check-command.js';
|
|
46
|
-
import { executeHelpCommand } from './presentation/cli/commands/help-command.js';
|
|
47
|
-
import { error } from './ui.js';
|
|
48
|
-
import { getVersion } from './utils/version.js';
|
|
49
|
-
import { checkTypo, looksLikeUnknownCommand } from './utils/typo-guard.js';
|
|
50
|
-
const program = new Command();
|
|
51
|
-
program
|
|
52
|
-
.name('codehere')
|
|
53
|
-
.description('Re-run your project\'s tests yourself, so an AI\'s "all tests pass" is checked, not taken on trust.')
|
|
54
|
-
.version(getVersion())
|
|
55
|
-
.configureHelp({ helpWidth: 80, sortSubcommands: true });
|
|
56
|
-
// Bare `codehere` prints help. Anything else that is not a command goes through the typo guard.
|
|
57
|
-
program.allowExcessArguments(true).action(() => {
|
|
58
|
-
const input = program.args.join(' ').trim();
|
|
59
|
-
if (!input) {
|
|
60
|
-
program.outputHelp();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const known = program.commands.map((c) => c.name());
|
|
64
|
-
const suggestions = checkTypo(input, known);
|
|
65
|
-
const unknown = suggestions
|
|
66
|
-
? input.split(/\s+/)[0]
|
|
67
|
-
: (looksLikeUnknownCommand(input, known) ?? input.split(/\s+/)[0]);
|
|
68
|
-
console.error(`codehere: unknown command '${unknown}'.`);
|
|
69
|
-
console.error('');
|
|
70
|
-
if (suggestions) {
|
|
71
|
-
console.error(' Did you mean:');
|
|
72
|
-
for (const s of suggestions)
|
|
73
|
-
console.error(` codehere ${s}`);
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
console.error(' Run `codehere help` to see available commands.');
|
|
77
|
-
}
|
|
78
|
-
console.error('');
|
|
79
|
-
process.exit(1);
|
|
80
|
-
});
|
|
81
|
-
program
|
|
82
|
-
.command('check')
|
|
83
|
-
.description('Re-run your tests independently — proof of what the AI actually did, not what it claimed')
|
|
84
|
-
.argument('[target]', 'Repo or directory to check (default: current directory)')
|
|
85
|
-
.option('--json', 'Machine-readable output')
|
|
86
|
-
.option('--proof', 'Emit a paste-ready proof block for a PR description or review comment')
|
|
87
|
-
.option('--claim <text>', "The AI's own summary: test files it names that are not in the repo are flagged")
|
|
88
|
-
.option('--base <ref>', 'Look for weakened tests in everything since this branch left <ref> (e.g. origin/main)')
|
|
89
|
-
.action(async (target, options) => {
|
|
90
|
-
try {
|
|
91
|
-
process.exitCode = await executeCheckCommand(target, {
|
|
92
|
-
json: options?.json,
|
|
93
|
-
proof: options?.proof,
|
|
94
|
-
claim: options?.claim,
|
|
95
|
-
base: options?.base,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
console.error(error('Error:'), err instanceof Error ? err.message : String(err));
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
program
|
|
104
|
-
.command('help')
|
|
105
|
-
.description('Show help for commands')
|
|
106
|
-
.argument('[command]', 'Command name to get help for')
|
|
107
|
-
.action(async (commandName) => {
|
|
108
|
-
await executeHelpCommand(commandName);
|
|
109
|
-
});
|
|
110
|
-
program.parse();
|
|
111
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test summary parsing: "N passed", "M failed" and the runner-specific variants, out of collected stdout.
|
|
3
|
-
* Used by `codehere check` to read what a test run reported.
|
|
4
|
-
*/
|
|
5
|
-
/** Parse "N passed", "M failed", "K tests" out of accumulated stdout. Best-effort.
|
|
6
|
-
* Vitest + jest output both have TWO pass/fail lines ("Test Files 11 passed" then
|
|
7
|
-
* "Tests 47 passed"). We want the second/last. Take the last match in the buffer,
|
|
8
|
-
* which is also what appears in the final summary. */
|
|
9
|
-
function lastMatch(s, re) {
|
|
10
|
-
const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
|
|
11
|
-
let last = null;
|
|
12
|
-
let m;
|
|
13
|
-
while ((m = g.exec(s)) !== null)
|
|
14
|
-
last = m;
|
|
15
|
-
return last;
|
|
16
|
-
}
|
|
17
|
-
/** Parse a test pass/fail summary out of accumulated stdout. Best-effort and
|
|
18
|
-
* runner-agnostic so "trust the result" holds across whatever the repo uses,
|
|
19
|
-
* not just node runners. Exported for direct unit coverage. */
|
|
20
|
-
export function parseTestSummary(stdout) {
|
|
21
|
-
if (!stdout)
|
|
22
|
-
return undefined;
|
|
23
|
-
const s = stdout.slice(-8000); // only look at the tail
|
|
24
|
-
// Primary — "N passed" / "M failed": vitest, jest, pytest, mocha ("passing"),
|
|
25
|
-
// cargo ("5 passed; 0 failed"). Unchanged so existing matches never shift.
|
|
26
|
-
let passed = lastMatch(s, /(\d+)\s+pass(?:ed|ing)/i);
|
|
27
|
-
let failed = lastMatch(s, /(\d+)\s+fail(?:ed|ing)/i);
|
|
28
|
-
// Fallback A — reversed order "Passed: N" / "Failed: N" (dotnet test, some CI
|
|
29
|
-
// summaries). Only consulted when the primary matched nothing, so the common
|
|
30
|
-
// path is untouched.
|
|
31
|
-
if (!passed && !failed) {
|
|
32
|
-
passed = lastMatch(s, /pass(?:ed)?:\s*(\d+)/i);
|
|
33
|
-
failed = lastMatch(s, /fail(?:ed)?:\s*(\d+)/i);
|
|
34
|
-
}
|
|
35
|
-
// Fallback B — python unittest: "Ran 5 tests in 0.0s" then "OK" or
|
|
36
|
-
// "FAILED (failures=2, errors=1)". There is no "passed" word at all, so the
|
|
37
|
-
// earlier patterns never see it — this was a real drop the dogfood surfaced.
|
|
38
|
-
if (!passed && !failed) {
|
|
39
|
-
const ran = lastMatch(s, /Ran\s+(\d+)\s+tests?\b/i);
|
|
40
|
-
if (ran) {
|
|
41
|
-
const total = parseInt(ran[1], 10);
|
|
42
|
-
const f = lastMatch(s, /failures=(\d+)/i);
|
|
43
|
-
const e = lastMatch(s, /errors=(\d+)/i);
|
|
44
|
-
const failedCount = (f ? parseInt(f[1], 10) : 0) + (e ? parseInt(e[1], 10) : 0);
|
|
45
|
-
const unittest = {
|
|
46
|
-
passed: Math.max(0, total - failedCount),
|
|
47
|
-
failed: failedCount,
|
|
48
|
-
};
|
|
49
|
-
const d = lastMatch(s, /in\s+([\d.]+)s\b/i);
|
|
50
|
-
if (d)
|
|
51
|
-
unittest.durationMs = Math.round(parseFloat(d[1]) * 1000);
|
|
52
|
-
return unittest;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (!passed && !failed)
|
|
56
|
-
return undefined;
|
|
57
|
-
const result = {
|
|
58
|
-
passed: passed ? parseInt(passed[1], 10) : 0,
|
|
59
|
-
failed: failed ? parseInt(failed[1], 10) : 0,
|
|
60
|
-
};
|
|
61
|
-
const dur = lastMatch(s, /Duration\s+([\d.]+)s/i) ?? lastMatch(s, /Time:\s+([\d.]+)\s*s/i);
|
|
62
|
-
if (dur)
|
|
63
|
-
result.durationMs = Math.round(parseFloat(dur[1]) * 1000);
|
|
64
|
-
return result;
|
|
65
|
-
}
|
|
66
|
-
//# sourceMappingURL=artifacts-extractor.js.map
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI: check — the simplest proof. codehere independently RE-RUNS your project's
|
|
3
|
-
* tests and reports the real result, so you trust what the AI actually did, not
|
|
4
|
-
* what it claimed. Zero setup, no daemon, no API. Exit codes for CI: 0 a verified pass, 1 a finding (tests
|
|
5
|
-
* failed, no test ran, a test weakened, a claimed test file missing), 2 nothing could be verified.
|
|
6
|
-
*/
|
|
7
|
-
import { execFileSync } from 'node:child_process';
|
|
8
|
-
import { statSync } from 'node:fs';
|
|
9
|
-
import { resolve } from 'node:path';
|
|
10
|
-
import { colors, symbols } from '../../../ui.js';
|
|
11
|
-
import { rerunTests } from '../../../application/use-cases/rerun-tests.js';
|
|
12
|
-
import { scanTestWeakening, } from '../../../application/use-cases/detect-test-weakening.js';
|
|
13
|
-
import { findMissingClaimedTests } from '../../../application/use-cases/check-claimed-tests.js';
|
|
14
|
-
const WHY_TEXT = {
|
|
15
|
-
'trivial-assertion': 'assertion that checks nothing',
|
|
16
|
-
skipped: 'test skipped',
|
|
17
|
-
'commented-out': 'assertion commented out',
|
|
18
|
-
'unreachable-assertion': 'assertion after an early return',
|
|
19
|
-
};
|
|
20
|
-
// File names, diff lines and branch names come from whoever wrote the change, so they are treated as untrusted:
|
|
21
|
-
// control characters (terminal escapes among them) are replaced, and in the markdown receipt the text sits inside
|
|
22
|
-
// an inline code span, where no link, @mention or emphasis renders.
|
|
23
|
-
// Control characters, plus direction overrides, zero-width, soft-hyphen, line-separator and tag characters that can
|
|
24
|
-
// disguise a file name.
|
|
25
|
-
const plain = (s) => s.replace(/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff\u{e0000}-\u{e007f}]|\u034f/gu, '?');
|
|
26
|
-
const code = (s) => '`' + plain(s).replace(/`/g, "'") + '`';
|
|
27
|
-
/** Characters a branch name usually has; anything else is not repeated in the reproduce command. */
|
|
28
|
-
const SAFE_REF = /^[A-Za-z0-9._/@+-]+$/;
|
|
29
|
-
const weakenedLines = (w) => w.map((f) => `${plain(f.file)}:${f.line} · ${WHY_TEXT[f.why]} · ${plain(f.text)}`);
|
|
30
|
-
const weakenedMarkdown = (w) => w.map((f) => `${code(f.file)}:${f.line} · ${WHY_TEXT[f.why]} · ${code(f.text)}`);
|
|
31
|
-
export const RECEIPT_MARKER = '<!-- codehere-receipt -->';
|
|
32
|
-
/**
|
|
33
|
-
* One word per result, for CI. Green (`pass`) only when codehere re-ran the suite, it exited 0, no test
|
|
34
|
-
* was weakened, no named test file was missing and the weakened-test scan finished. Tampering is `fail` even if
|
|
35
|
-
* no suite could run; every other case where nothing was verified is `unverified`, never a pass.
|
|
36
|
-
*/
|
|
37
|
-
export function checkVerdict(r, weakened, missingClaimed, scanError) {
|
|
38
|
-
if (weakened.length || missingClaimed.length)
|
|
39
|
-
return 'fail';
|
|
40
|
-
if (r.kind === 'disabled')
|
|
41
|
-
return 'skipped';
|
|
42
|
-
if (r.kind === 'failed')
|
|
43
|
-
return 'fail';
|
|
44
|
-
if (scanError)
|
|
45
|
-
return 'unverified';
|
|
46
|
-
if (r.kind === 'passed' && r.ran && (r.exitCode ?? 1) === 0)
|
|
47
|
-
return 'pass';
|
|
48
|
-
return 'unverified';
|
|
49
|
-
}
|
|
50
|
-
/** The check command that gives the same verdict as this run: the options that changed it are repeated. */
|
|
51
|
-
function reproduceCommand({ base, claim }) {
|
|
52
|
-
let cmd = 'codehere check';
|
|
53
|
-
if (base)
|
|
54
|
-
cmd += SAFE_REF.test(base) ? ` --base ${base}` : ' --base <the same base>';
|
|
55
|
-
if (claim) {
|
|
56
|
-
const inlineable = claim.length <= 160 && !/[`'\n\r]/.test(claim);
|
|
57
|
-
cmd += inlineable ? ` --claim '${plain(claim)}'` : ' --claim <the same summary>';
|
|
58
|
-
}
|
|
59
|
-
return cmd;
|
|
60
|
-
}
|
|
61
|
-
/** Short commit SHA of `cwd`'s repo, or null when it isn't a git checkout. */
|
|
62
|
-
function gitShortSha(cwd) {
|
|
63
|
-
try {
|
|
64
|
-
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
65
|
-
.toString()
|
|
66
|
-
.trim();
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* A paste-ready "verified outcome receipt": the one thing a reviewer can trust
|
|
74
|
-
* without re-running anything — codehere independently re-ran the suite, here's the
|
|
75
|
-
* observed result, pinned to a commit + timestamp, WITH the exact command to
|
|
76
|
-
* reproduce it independently. Plain markdown so it drops into a PR description or
|
|
77
|
-
* review comment as-is.
|
|
78
|
-
*
|
|
79
|
-
* Why reproducibility is the trust mechanism (not a digest): a self-computed hash
|
|
80
|
-
* over self-reported facts proves nothing — a sender could edit the facts AND the
|
|
81
|
-
* hash. What a third party can actually trust is REPRODUCING it: check out the
|
|
82
|
-
* pinned commit, run `codehere check`, get the same exit code. So the receipt leads
|
|
83
|
-
* with the reproduce command and is honest about SCOPE (a test re-run, not a
|
|
84
|
-
* security/logic audit) — it never says more than it verified.
|
|
85
|
-
*
|
|
86
|
-
* ponytail: this is the standalone (no-daemon) receipt. Upgrade path to true
|
|
87
|
-
* tamper-EVIDENCE (authenticity, not just reproducibility): when a ~/.codehere audit
|
|
88
|
-
* chain covers this commit, fold in its verified hash (audit-chain.ts) so the receipt
|
|
89
|
-
* also carries a digest a third party can check against an append-only log.
|
|
90
|
-
*/
|
|
91
|
-
function buildProofBlock(cwd, r, weakened = [], missingClaimed = [], repro = {}, scan = { findings: [] }) {
|
|
92
|
-
const sha = gitShortSha(cwd);
|
|
93
|
-
const when = new Date().toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
94
|
-
const at = sha ? `commit \`${sha}\`` : 'the working tree';
|
|
95
|
-
const weakenedNote = weakened.length
|
|
96
|
-
? `> Tests changed in this diff, so a pass may check nothing: ${weakenedMarkdown(weakened).join(' · ')}\n`
|
|
97
|
-
: '';
|
|
98
|
-
const missingNote = missingClaimed.length
|
|
99
|
-
? `> The AI named test files that are not in the repo: ${missingClaimed.map(code).join(' · ')}\n`
|
|
100
|
-
: '';
|
|
101
|
-
const scanNote = scan.error
|
|
102
|
-
? `> Weakened-test scan could not run: ${scan.error}\n`
|
|
103
|
-
: scan.skipped
|
|
104
|
-
? `> Weakened-test scan skipped: ${scan.skipped}\n`
|
|
105
|
-
: '';
|
|
106
|
-
// A finding is reported even when the suite could not run.
|
|
107
|
-
const notes = weakenedNote + missingNote + scanNote;
|
|
108
|
-
if (!r.ran) {
|
|
109
|
-
if (r.kind === 'command-not-found') {
|
|
110
|
-
const fix = r.remedy ? ` · fix: \`${r.remedy}\`` : '';
|
|
111
|
-
return `> **codehere · verified outcome receipt** — couldn't run ${code(r.command || 'the tests')} (runner/deps not installed) · unverified${fix} · ${when}\n${notes}`;
|
|
112
|
-
}
|
|
113
|
-
return `> **codehere · verified outcome receipt** — no suite re-run (${code(r.skippedReason || 'none detected')}) · nothing verified · ${when}\n${notes}`;
|
|
114
|
-
}
|
|
115
|
-
if (r.kind === 'runner-missing') {
|
|
116
|
-
const fix = r.remedy ? ` · fix: \`${r.remedy}\`` : '';
|
|
117
|
-
return `> **codehere · verified outcome receipt** — couldn't run ${code(r.command ?? '')} on ${at} (runner not installed, exit ${r.exitCode}) · unverified${fix} · ${when}\n${notes}`;
|
|
118
|
-
}
|
|
119
|
-
const passed = (r.exitCode ?? 1) === 0 && !r.noTests;
|
|
120
|
-
const counts = typeof r.passed === 'number' || typeof r.failed === 'number'
|
|
121
|
-
? ` (${r.passed ?? '?'} passed · ${r.failed ?? 0} failed)`
|
|
122
|
-
: '';
|
|
123
|
-
const verdict = r.noTests
|
|
124
|
-
? '✕ no tests ran'
|
|
125
|
-
: !passed
|
|
126
|
-
? '✕ tests FAIL'
|
|
127
|
-
: weakened.length
|
|
128
|
-
? `✕ tests pass · ${weakened.length} weakened`
|
|
129
|
-
: missingClaimed.length
|
|
130
|
-
? `✕ tests pass · ${missingClaimed.length} claimed test file not found`
|
|
131
|
-
: scan.error
|
|
132
|
-
? '✕ tests pass · weakened-test scan could not run'
|
|
133
|
-
: '✓ tests pass';
|
|
134
|
-
// Reproduce line: the real, verifiable trust — anyone can re-run and confirm.
|
|
135
|
-
const reproduce = sha ? `> Reproduce independently: \`git checkout ${sha} && ${reproduceCommand(repro)}\`\n` : '';
|
|
136
|
-
return (`> **codehere · verified outcome receipt** · ${verdict}${counts}\n` +
|
|
137
|
-
`> codehere independently re-ran ${code(r.command ?? '')} on ${at} — exit ${r.exitCode}. Observed, not the AI's claim. · ${when}\n` +
|
|
138
|
-
notes +
|
|
139
|
-
reproduce +
|
|
140
|
-
`> Scope: the project's test suite — not a security or logic audit.\n`);
|
|
141
|
-
}
|
|
142
|
-
export async function executeCheckCommand(target, options = {}) {
|
|
143
|
-
const cwd = resolve(target || '.');
|
|
144
|
-
let isFolder = false;
|
|
145
|
-
try {
|
|
146
|
-
isFolder = statSync(cwd).isDirectory();
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
/* missing path: handled below */
|
|
150
|
-
}
|
|
151
|
-
if (!isFolder)
|
|
152
|
-
throw new Error(`No such folder: ${target}`);
|
|
153
|
-
const r = await rerunTests(cwd);
|
|
154
|
-
const scan = scanTestWeakening(cwd, options.base);
|
|
155
|
-
const weakened = scan.findings;
|
|
156
|
-
const missingClaimed = options.claim ? findMissingClaimedTests(cwd, options.claim) : [];
|
|
157
|
-
const verdict = checkVerdict(r, weakened, missingClaimed, scan.error);
|
|
158
|
-
const weakeningScan = scan.error ? `could not run: ${scan.error}` : scan.skipped ? `skipped: ${scan.skipped}` : 'ran';
|
|
159
|
-
const exitCode = verdict === 'pass' ? 0 : verdict === 'fail' ? 1 : 2;
|
|
160
|
-
const weakenedText = () => weakened.length
|
|
161
|
-
? weakenedLines(weakened)
|
|
162
|
-
.map((l) => colors.dim(` ${l}\n`))
|
|
163
|
-
.join('') + colors.dim(` › ${options.base ? `git diff ${plain(options.base)}...HEAD` : 'git diff HEAD'}\n`)
|
|
164
|
-
: '';
|
|
165
|
-
const scanText = () => scan.error
|
|
166
|
-
? colors.dim(` weakened-test scan could not run: ${scan.error}\n`)
|
|
167
|
-
: scan.skipped
|
|
168
|
-
? colors.dim(` weakened-test scan skipped: ${scan.skipped}\n`)
|
|
169
|
-
: '';
|
|
170
|
-
const missingLines = () => missingClaimed.length
|
|
171
|
-
? missingClaimed.map((f) => colors.dim(` ${plain(f)} · named by the AI, not in the repo\n`)).join('')
|
|
172
|
-
: '';
|
|
173
|
-
if (options.json) {
|
|
174
|
-
const receipt = RECEIPT_MARKER +
|
|
175
|
-
'\n' +
|
|
176
|
-
buildProofBlock(cwd, r, weakened, missingClaimed, { base: options.base, claim: options.claim }, scan);
|
|
177
|
-
process.stdout.write(JSON.stringify({ cwd, ...r, weakened, missingClaimed, weakeningScan, verdict, receipt }, null, 2) + '\n');
|
|
178
|
-
return exitCode;
|
|
179
|
-
}
|
|
180
|
-
if (options.proof) {
|
|
181
|
-
process.stdout.write(buildProofBlock(cwd, r, weakened, missingClaimed, { base: options.base, claim: options.claim }, scan));
|
|
182
|
-
return exitCode;
|
|
183
|
-
}
|
|
184
|
-
if (!r.ran) {
|
|
185
|
-
// The runner/deps aren't installed (e.g. `spawn pytest ENOENT`) — that's a
|
|
186
|
-
// fixable environment gap, not "no tests". Name the one command that fixes it.
|
|
187
|
-
if (r.kind === 'command-not-found') {
|
|
188
|
-
process.stdout.write('\n' +
|
|
189
|
-
colors.error(`${symbols.error} Couldn't run the tests`) +
|
|
190
|
-
colors.dim(` — the runner for '${r.command || 'your tests'}' isn't installed here.\n`) +
|
|
191
|
-
(r.remedy
|
|
192
|
-
? colors.dim(' Fix it, then re-check: ') + colors.success(`${r.remedy}\n\n`)
|
|
193
|
-
: colors.dim(' Install the test runner, then run check again.\n\n')));
|
|
194
|
-
process.stdout.write(weakenedText() + missingLines() + scanText());
|
|
195
|
-
return exitCode; // couldn't verify (2); a weakened test or a missing named test file still counts (1)
|
|
196
|
-
}
|
|
197
|
-
if (r.kind === 'timed-out' || r.kind === 'disabled') {
|
|
198
|
-
process.stdout.write(colors.error(`${symbols.error} Nothing verified`) +
|
|
199
|
-
colors.dim(` — ${r.kind === 'timed-out' ? `'${r.command}' ${r.skippedReason?.replace(/^test re-run /, '')}` : r.skippedReason}.\n`));
|
|
200
|
-
process.stdout.write(weakenedText() + missingLines() + scanText());
|
|
201
|
-
return exitCode;
|
|
202
|
-
}
|
|
203
|
-
process.stdout.write(colors.dim(`No test suite codehere can run here — ${r.skippedReason || 'none detected'}.\n`) +
|
|
204
|
-
colors.dim('codehere proves the AI by re-running your tests; add one (npm test / node --test / pytest / go test / cargo test) and run check again.\n'));
|
|
205
|
-
process.stdout.write(weakenedText() + missingLines() + scanText());
|
|
206
|
-
return exitCode; // nothing was verified (2); a weakened test or a missing named test file still counts (1)
|
|
207
|
-
}
|
|
208
|
-
// Honesty: runner-missing (exit 126/127) means the test command couldn't be RUN
|
|
209
|
-
// (deps not installed), not that the tests failed. Never report "FAIL" for that —
|
|
210
|
-
// name the one command that makes it runnable.
|
|
211
|
-
if (r.kind === 'runner-missing') {
|
|
212
|
-
process.stdout.write('\n' +
|
|
213
|
-
colors.error(`${symbols.error} Couldn't run the tests`) +
|
|
214
|
-
colors.dim(` — '${r.command}' exited ${r.exitCode}: the deps/runner aren't installed here.\n`) +
|
|
215
|
-
(r.remedy
|
|
216
|
-
? colors.dim(' Fix it, then re-check: ') + colors.success(`${r.remedy}\n\n`)
|
|
217
|
-
: colors.dim(' Install deps, then run check again.\n\n')));
|
|
218
|
-
process.stdout.write(weakenedText() + missingLines() + scanText());
|
|
219
|
-
return exitCode; // non-zero for CI (unverified), but NOT a false "tests failed"
|
|
220
|
-
}
|
|
221
|
-
const passed = (r.exitCode ?? 1) === 0 && !r.noTests;
|
|
222
|
-
const counts = typeof r.passed === 'number' || typeof r.failed === 'number'
|
|
223
|
-
? ` ${r.passed ?? '?'} passed · ${r.failed ?? 0} failed`
|
|
224
|
-
: '';
|
|
225
|
-
const headline = r.noTests
|
|
226
|
-
? colors.error(`${symbols.error} No tests ran`)
|
|
227
|
-
: !passed
|
|
228
|
-
? colors.error(`${symbols.error} Tests FAIL`)
|
|
229
|
-
: weakened.length
|
|
230
|
-
? colors.error(`${symbols.error} Tests pass · ${weakened.length} weakened in this change`)
|
|
231
|
-
: missingClaimed.length
|
|
232
|
-
? colors.error(`${symbols.error} Tests pass · ${missingClaimed.length} claimed test file not found`)
|
|
233
|
-
: scan.error
|
|
234
|
-
? colors.error(`${symbols.error} Tests pass · weakened-test scan could not run`)
|
|
235
|
-
: colors.success(`${symbols.success} Tests pass`);
|
|
236
|
-
process.stdout.write('\n' +
|
|
237
|
-
headline +
|
|
238
|
-
colors.dim(` — codehere re-ran them itself (${r.command}, exit ${r.exitCode})${counts}\n`) +
|
|
239
|
-
colors.dim("This is observed reality, not the agent's claim.\n") +
|
|
240
|
-
(r.noTests
|
|
241
|
-
? colors.dim(r.allSkipped
|
|
242
|
-
? ' The runner says every test was skipped, so nothing was tested.\n'
|
|
243
|
-
: ' The runner reported 0 tests, so nothing was tested. Check the test file names.\n')
|
|
244
|
-
: '') +
|
|
245
|
-
weakenedText() +
|
|
246
|
-
missingLines() +
|
|
247
|
-
scanText() +
|
|
248
|
-
'\n');
|
|
249
|
-
return exitCode;
|
|
250
|
-
}
|
|
251
|
-
//# sourceMappingURL=check-command.js.map
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Help Command
|
|
3
|
-
* Contextual help for the commands codehere ships: check and help.
|
|
4
|
-
*/
|
|
5
|
-
import { colors, newline } from '../../../ui.js';
|
|
6
|
-
const COMMAND_INFO = {
|
|
7
|
-
check: {
|
|
8
|
-
name: 'check',
|
|
9
|
-
description: 'Re-run your tests and report what actually passed',
|
|
10
|
-
usage: 'codehere check [target] [options]',
|
|
11
|
-
examples: [
|
|
12
|
-
'codehere check',
|
|
13
|
-
'codehere check --base origin/main',
|
|
14
|
-
'codehere check --claim "added tests, all pass"',
|
|
15
|
-
'codehere check --proof',
|
|
16
|
-
],
|
|
17
|
-
options: [
|
|
18
|
-
{ flag: '--json', description: 'Machine-readable output' },
|
|
19
|
-
{ flag: '--proof', description: 'Paste-ready proof block for a pull request' },
|
|
20
|
-
{ flag: '--claim <text>', description: "The AI's summary: test files it names that do not exist are flagged" },
|
|
21
|
-
{ flag: '--base <ref>', description: 'Look for weakened tests in everything since this branch left <ref>' },
|
|
22
|
-
],
|
|
23
|
-
},
|
|
24
|
-
help: {
|
|
25
|
-
name: 'help',
|
|
26
|
-
description: 'Show help for commands',
|
|
27
|
-
usage: 'codehere help [command]',
|
|
28
|
-
examples: ['codehere help', 'codehere help check'],
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
function displayCommandHelp(commandName) {
|
|
32
|
-
const command = COMMAND_INFO[commandName.toLowerCase()];
|
|
33
|
-
if (!command) {
|
|
34
|
-
newline();
|
|
35
|
-
console.error(colors.red(`Command '${commandName}' not found.`));
|
|
36
|
-
console.log(colors.dim('Run') + ' ' + colors.cyan('codehere --help') + ' ' + colors.dim('to see all commands.'));
|
|
37
|
-
newline();
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
newline();
|
|
41
|
-
console.log(colors.bold(command.name) + colors.dim(' · ' + command.description));
|
|
42
|
-
newline();
|
|
43
|
-
console.log(colors.bold('Usage'));
|
|
44
|
-
console.log(' ' + colors.cyan(command.usage));
|
|
45
|
-
newline();
|
|
46
|
-
console.log(colors.bold('Examples'));
|
|
47
|
-
command.examples.forEach((example) => {
|
|
48
|
-
console.log(colors.dim(' $ ') + colors.cyan(example));
|
|
49
|
-
});
|
|
50
|
-
newline();
|
|
51
|
-
if (command.options && command.options.length > 0) {
|
|
52
|
-
console.log(colors.bold('Options'));
|
|
53
|
-
command.options.forEach((opt) => {
|
|
54
|
-
console.log(' ' + colors.cyan(opt.flag.padEnd(24)) + colors.dim(opt.description));
|
|
55
|
-
});
|
|
56
|
-
newline();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function displayGeneralHelp() {
|
|
60
|
-
newline();
|
|
61
|
-
console.log(colors.bold('codehere') + colors.dim(' · re-run the tests an AI says passed'));
|
|
62
|
-
newline();
|
|
63
|
-
console.log(colors.bold('Check'));
|
|
64
|
-
console.log(' ' + colors.cyan('check'.padEnd(14)) + colors.dim('Re-run your tests and report what actually passed'));
|
|
65
|
-
newline();
|
|
66
|
-
console.log(colors.dim('Run ') + colors.cyan('codehere help check') + colors.dim(' for its options.'));
|
|
67
|
-
newline();
|
|
68
|
-
}
|
|
69
|
-
export async function executeHelpCommand(commandName) {
|
|
70
|
-
if (commandName) {
|
|
71
|
-
displayCommandHelp(commandName);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
displayGeneralHelp();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
//# sourceMappingURL=help-command.js.map
|
package/dist/ui.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
// Color roles (DESIGN.md §1): color carries meaning.
|
|
3
|
-
// verified = green (independently verified ONLY) caught = red (a contradiction caught ONLY)
|
|
4
|
-
// warn = amber (attention) evidence = dim (machine facts, metadata)
|
|
5
|
-
// identity = cyan (an id, sparingly) heading = bold (the one thing that matters)
|
|
6
|
-
export const colors = {
|
|
7
|
-
success: chalk.green,
|
|
8
|
-
error: chalk.red,
|
|
9
|
-
warning: chalk.yellow,
|
|
10
|
-
info: chalk.blue,
|
|
11
|
-
dim: chalk.dim,
|
|
12
|
-
bold: chalk.bold,
|
|
13
|
-
cyan: chalk.cyan,
|
|
14
|
-
magenta: chalk.magenta,
|
|
15
|
-
yellow: chalk.yellow,
|
|
16
|
-
red: chalk.red,
|
|
17
|
-
green: chalk.green,
|
|
18
|
-
gray: chalk.gray,
|
|
19
|
-
blue: chalk.blue,
|
|
20
|
-
verified: chalk.green,
|
|
21
|
-
caught: chalk.red,
|
|
22
|
-
warn: chalk.yellow,
|
|
23
|
-
evidence: chalk.dim,
|
|
24
|
-
identity: chalk.cyan,
|
|
25
|
-
heading: chalk.bold,
|
|
26
|
-
};
|
|
27
|
-
// Glyph language (DESIGN.md): the sanctioned geometric set plus ANSI color,
|
|
28
|
-
// never emoji-as-icon.
|
|
29
|
-
export const symbols = {
|
|
30
|
-
success: '✓',
|
|
31
|
-
error: '✕',
|
|
32
|
-
warning: '!',
|
|
33
|
-
info: '›',
|
|
34
|
-
processing: '›',
|
|
35
|
-
arrow: '›',
|
|
36
|
-
dot: '·',
|
|
37
|
-
check: '✓',
|
|
38
|
-
cross: '✕',
|
|
39
|
-
pipe: '│',
|
|
40
|
-
dash: '─',
|
|
41
|
-
plus: '+',
|
|
42
|
-
minus: '-',
|
|
43
|
-
bullet: '•',
|
|
44
|
-
};
|
|
45
|
-
export function error(message) {
|
|
46
|
-
return colors.red(symbols.error + ' ' + message);
|
|
47
|
-
}
|
|
48
|
-
export function newline(count = 1) {
|
|
49
|
-
for (let i = 0; i < count; i++) {
|
|
50
|
-
console.log();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
//# sourceMappingURL=ui.js.map
|