@claude-flow/cli 3.56.7 → 3.56.9
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/.claude/helpers/helpers.manifest.json +3 -3
- package/.claude/helpers/hook-handler.cjs +109 -23
- package/README.md +9 -3
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/analyze.js +40 -29
- package/dist/src/commands/coverage-reader.d.ts +29 -0
- package/dist/src/commands/coverage-reader.js +247 -0
- package/dist/src/commands/hooks.js +9 -180
- package/dist/src/commands/tui.js +2 -2
- package/dist/src/init/helpers-generator.js +109 -23
- package/dist/src/parser.js +32 -22
- package/dist/src/production/monitoring.d.ts +1 -0
- package/dist/src/production/monitoring.js +19 -6
- package/dist/src/production/rate-limiter.js +5 -4
- package/dist/src/production/retry.js +3 -0
- package/dist/src/services/config-file-manager.js +3 -3
- package/dist/src/services/policy-ledger-anchor.js +20 -2
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
- package/node_modules/@claude-flow/codex/dist/index.js +1 -1
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
- package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +5 -5
- package/plugins/ruflo-console/dist/tui.mjs +171 -162
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
- package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
- package/plugins/ruflo-metaharness/hooks/screen.ts +10 -3
- package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Coverage reports are build output the project controls: refuse anything large enough to stall or exhaust the process. */
|
|
4
|
+
const MAX_REPORT_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
/** The file's text, or undefined when it is missing, not a regular file, unreadable, or over the size cap. */
|
|
6
|
+
function readReport(path) {
|
|
7
|
+
try {
|
|
8
|
+
const stat = statSync(path);
|
|
9
|
+
if (!stat.isFile() || stat.size > MAX_REPORT_BYTES)
|
|
10
|
+
return undefined;
|
|
11
|
+
return readFileSync(path, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read coverage data from disk. Checks these locations in order:
|
|
19
|
+
* 1. coverage/coverage-summary.json (Jest/Istanbul)
|
|
20
|
+
* 2. coverage/lcov.info (lcov format)
|
|
21
|
+
* 3. coverage/coverage-final.json (Istanbul counters)
|
|
22
|
+
* 4. .nyc_output/out.json (nyc counters or legacy summary)
|
|
23
|
+
*/
|
|
24
|
+
export function readCoverageFromDisk() {
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
const noData = {
|
|
27
|
+
found: false,
|
|
28
|
+
source: 'none',
|
|
29
|
+
entries: [],
|
|
30
|
+
summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
|
|
31
|
+
};
|
|
32
|
+
// 1. Try coverage-summary.json (Jest/Istanbul)
|
|
33
|
+
for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
|
|
34
|
+
const text = readReport(join(cwd, relPath));
|
|
35
|
+
if (text !== undefined) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = JSON.parse(text);
|
|
38
|
+
const report = parseCoverageSummaryJson(raw, relPath);
|
|
39
|
+
if (report.found)
|
|
40
|
+
return report;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// malformed, try next
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 2. Try lcov.info
|
|
48
|
+
for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
|
|
49
|
+
const raw = readReport(join(cwd, relPath));
|
|
50
|
+
if (raw !== undefined) {
|
|
51
|
+
try {
|
|
52
|
+
const report = parseLcovInfo(raw, relPath);
|
|
53
|
+
if (report.found)
|
|
54
|
+
return report;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// malformed, try next
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Istanbul's raw counter map (Jest, Vitest, nyc).
|
|
62
|
+
for (const relPath of ['coverage/coverage-final.json', 'coverage-final.json', '.nyc_output/out.json']) {
|
|
63
|
+
const text = readReport(join(cwd, relPath));
|
|
64
|
+
if (text === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(text);
|
|
68
|
+
const report = parseIstanbulCoverage(raw, relPath);
|
|
69
|
+
if (report.found)
|
|
70
|
+
return report;
|
|
71
|
+
const legacy = parseCoverageSummaryJson(raw, relPath);
|
|
72
|
+
if (legacy.found)
|
|
73
|
+
return legacy;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Missing or malformed, try the next supported report.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return noData;
|
|
80
|
+
}
|
|
81
|
+
function parseCoverageSummaryJson(data, source) {
|
|
82
|
+
const entries = [];
|
|
83
|
+
const metricsByFile = [];
|
|
84
|
+
for (const [filePath, metrics] of Object.entries(data)) {
|
|
85
|
+
if (filePath === 'total')
|
|
86
|
+
continue;
|
|
87
|
+
const m = metrics;
|
|
88
|
+
if (!m || typeof m !== 'object')
|
|
89
|
+
continue;
|
|
90
|
+
if (!['lines', 'branches', 'functions', 'statements'].some(key => m[key]))
|
|
91
|
+
continue;
|
|
92
|
+
metricsByFile.push(m);
|
|
93
|
+
const linePct = percentage(m.lines) ?? 0;
|
|
94
|
+
const branchPct = (percentage(m.branches) ?? 0);
|
|
95
|
+
const funcPct = (percentage(m.functions) ?? 0);
|
|
96
|
+
const stmtPct = (percentage(m.statements) ?? 0);
|
|
97
|
+
entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
|
|
98
|
+
}
|
|
99
|
+
// Only aggregate complete counts. A missing denominator is unknown, not empty.
|
|
100
|
+
const total = data['total'];
|
|
101
|
+
const aggregate = (key) => {
|
|
102
|
+
const supplied = percentage(total?.[key]);
|
|
103
|
+
if (supplied !== undefined)
|
|
104
|
+
return supplied;
|
|
105
|
+
const metrics = metricsByFile.map(file => file[key]);
|
|
106
|
+
if (metrics.length === 0 || !metrics.every(hasCounts))
|
|
107
|
+
return 0;
|
|
108
|
+
return ratio(metrics.reduce((sum, m) => sum + m.covered, 0), metrics.reduce((sum, m) => sum + m.total, 0));
|
|
109
|
+
};
|
|
110
|
+
// Sort by lowest line coverage
|
|
111
|
+
entries.sort((a, b) => a.lines - b.lines);
|
|
112
|
+
return {
|
|
113
|
+
found: entries.length > 0,
|
|
114
|
+
source,
|
|
115
|
+
entries,
|
|
116
|
+
summary: {
|
|
117
|
+
totalFiles: entries.length,
|
|
118
|
+
overallLineCoverage: aggregate('lines'),
|
|
119
|
+
overallBranchCoverage: aggregate('branches'),
|
|
120
|
+
overallFunctionCoverage: aggregate('functions'),
|
|
121
|
+
overallStatementCoverage: aggregate('statements'),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function parseLcovInfo(raw, source) {
|
|
126
|
+
const entries = [];
|
|
127
|
+
let currentFile = '';
|
|
128
|
+
let linesHit = 0, linesFound = 0;
|
|
129
|
+
let branchesHit = 0, branchesFound = 0;
|
|
130
|
+
let functionsHit = 0, functionsFound = 0;
|
|
131
|
+
let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0, totalFH = 0, totalFF = 0;
|
|
132
|
+
const flushRecord = () => {
|
|
133
|
+
if (currentFile) {
|
|
134
|
+
// A corrupt record cannot report more hits than lines, or a negative count.
|
|
135
|
+
linesHit = Math.min(linesHit, linesFound);
|
|
136
|
+
branchesHit = Math.min(branchesHit, branchesFound);
|
|
137
|
+
functionsHit = Math.min(functionsHit, functionsFound);
|
|
138
|
+
totalLH += linesHit;
|
|
139
|
+
totalLF += linesFound;
|
|
140
|
+
totalBH += branchesHit;
|
|
141
|
+
totalBF += branchesFound;
|
|
142
|
+
totalFH += functionsHit;
|
|
143
|
+
totalFF += functionsFound;
|
|
144
|
+
entries.push({
|
|
145
|
+
filePath: currentFile,
|
|
146
|
+
lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 100,
|
|
147
|
+
branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
|
|
148
|
+
functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
|
|
149
|
+
statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 100,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
for (const line of raw.split('\n')) {
|
|
154
|
+
const trimmed = line.trim();
|
|
155
|
+
if (trimmed.startsWith('SF:')) {
|
|
156
|
+
flushRecord();
|
|
157
|
+
currentFile = trimmed.slice(3);
|
|
158
|
+
linesHit = 0;
|
|
159
|
+
linesFound = 0;
|
|
160
|
+
branchesHit = 0;
|
|
161
|
+
branchesFound = 0;
|
|
162
|
+
functionsHit = 0;
|
|
163
|
+
functionsFound = 0;
|
|
164
|
+
}
|
|
165
|
+
else if (trimmed.startsWith('LH:')) {
|
|
166
|
+
linesHit = count(trimmed.slice(3));
|
|
167
|
+
}
|
|
168
|
+
else if (trimmed.startsWith('LF:')) {
|
|
169
|
+
linesFound = count(trimmed.slice(3));
|
|
170
|
+
}
|
|
171
|
+
else if (trimmed.startsWith('BRH:')) {
|
|
172
|
+
branchesHit = count(trimmed.slice(4));
|
|
173
|
+
}
|
|
174
|
+
else if (trimmed.startsWith('BRF:')) {
|
|
175
|
+
branchesFound = count(trimmed.slice(4));
|
|
176
|
+
}
|
|
177
|
+
else if (trimmed.startsWith('FNH:')) {
|
|
178
|
+
functionsHit = count(trimmed.slice(4));
|
|
179
|
+
}
|
|
180
|
+
else if (trimmed.startsWith('FNF:')) {
|
|
181
|
+
functionsFound = count(trimmed.slice(4));
|
|
182
|
+
}
|
|
183
|
+
else if (trimmed === 'end_of_record') {
|
|
184
|
+
flushRecord();
|
|
185
|
+
currentFile = '';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
flushRecord();
|
|
189
|
+
entries.sort((a, b) => a.lines - b.lines);
|
|
190
|
+
return {
|
|
191
|
+
found: entries.length > 0,
|
|
192
|
+
source,
|
|
193
|
+
entries,
|
|
194
|
+
summary: {
|
|
195
|
+
totalFiles: entries.length,
|
|
196
|
+
overallLineCoverage: ratio(totalLH, totalLF),
|
|
197
|
+
overallBranchCoverage: ratio(totalBH, totalBF),
|
|
198
|
+
overallFunctionCoverage: ratio(totalFH, totalFF),
|
|
199
|
+
overallStatementCoverage: ratio(totalLH, totalLF),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** A non-negative integer count; anything else in a report line is treated as 0. */
|
|
204
|
+
function count(text) {
|
|
205
|
+
const n = parseInt(text, 10);
|
|
206
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
207
|
+
}
|
|
208
|
+
function ratio(covered, total) {
|
|
209
|
+
return total > 0 ? covered / total * 100 : 100;
|
|
210
|
+
}
|
|
211
|
+
function hasCounts(metric) {
|
|
212
|
+
return typeof metric?.total === 'number' && Number.isFinite(metric.total) && metric.total >= 0
|
|
213
|
+
&& typeof metric.covered === 'number' && Number.isFinite(metric.covered)
|
|
214
|
+
&& metric.covered >= 0 && metric.covered <= metric.total;
|
|
215
|
+
}
|
|
216
|
+
function percentage(metric) {
|
|
217
|
+
if (typeof metric?.pct === 'number' && Number.isFinite(metric.pct)
|
|
218
|
+
&& metric.pct >= 0 && metric.pct <= 100)
|
|
219
|
+
return metric.pct;
|
|
220
|
+
return hasCounts(metric) ? ratio(metric.covered, metric.total) : undefined;
|
|
221
|
+
}
|
|
222
|
+
/** Istanbul line coverage uses the maximum statement hit count on each start line. */
|
|
223
|
+
function parseIstanbulCoverage(data, source) {
|
|
224
|
+
const summary = {};
|
|
225
|
+
for (const [filePath, value] of Object.entries(data)) {
|
|
226
|
+
const cov = value;
|
|
227
|
+
if (!cov || !cov.s || !cov.f || !cov.b || !cov.statementMap)
|
|
228
|
+
continue;
|
|
229
|
+
const lines = new Map();
|
|
230
|
+
for (const [id, hits] of Object.entries(cov.s)) {
|
|
231
|
+
const line = cov.statementMap[id]?.start?.line;
|
|
232
|
+
if (!Number.isInteger(line) || line < 1)
|
|
233
|
+
throw new Error('Invalid Istanbul statement location');
|
|
234
|
+
lines.set(line, Math.max(lines.get(line) ?? 0, hits));
|
|
235
|
+
}
|
|
236
|
+
const metric = (hits) => {
|
|
237
|
+
if (hits.some(n => !Number.isFinite(n) || n < 0))
|
|
238
|
+
throw new Error('Invalid Istanbul counter');
|
|
239
|
+
const covered = hits.filter(n => n > 0).length;
|
|
240
|
+
return { total: hits.length, covered, pct: ratio(covered, hits.length) };
|
|
241
|
+
};
|
|
242
|
+
summary[filePath] = { lines: metric([...lines.values()]), statements: metric(Object.values(cov.s)),
|
|
243
|
+
functions: metric(Object.values(cov.f)), branches: metric(Object.values(cov.b).flat()) };
|
|
244
|
+
}
|
|
245
|
+
return parseCoverageSummaryJson(summary, source);
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=coverage-reader.js.map
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 CLI Hooks Command
|
|
3
|
+
* Self-learning hooks system for intelligent workflow automation
|
|
4
|
+
*/
|
|
1
5
|
import { validateFeedbackPatterns } from '../memory/feedback-patterns.js';
|
|
6
|
+
import { readCoverageFromDisk } from './coverage-reader.js';
|
|
2
7
|
import { output } from '../output.js';
|
|
3
8
|
import { confirm } from '../prompt.js';
|
|
4
9
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
5
10
|
import { storeCommand } from './transfer-store.js';
|
|
6
|
-
import {
|
|
7
|
-
import { join } from 'node:path';
|
|
11
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
8
12
|
import { getSecurityStatus as sharedGetSecurityStatus, getSwarmStatus as sharedGetSwarmStatus, getGitUncommittedCount as sharedGetGitUncommittedCount, } from '../funnel/local-signals.js';
|
|
9
13
|
/**
|
|
10
14
|
* #1686 — `?? 0` only defaults null/undefined; NaN slips through and
|
|
@@ -16,181 +20,6 @@ function safeNum(value, fallback = 0) {
|
|
|
16
20
|
const n = typeof value === 'number' ? value : Number(value);
|
|
17
21
|
return Number.isFinite(n) ? n : fallback;
|
|
18
22
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Read coverage data from disk. Checks these locations in order:
|
|
21
|
-
* 1. coverage/coverage-summary.json (Jest/Istanbul)
|
|
22
|
-
* 2. coverage/lcov.info (lcov format)
|
|
23
|
-
* 3. .nyc_output/out.json (nyc)
|
|
24
|
-
*/
|
|
25
|
-
function readCoverageFromDisk() {
|
|
26
|
-
const cwd = process.cwd();
|
|
27
|
-
const noData = {
|
|
28
|
-
found: false,
|
|
29
|
-
source: 'none',
|
|
30
|
-
entries: [],
|
|
31
|
-
summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
|
|
32
|
-
};
|
|
33
|
-
// 1. Try coverage-summary.json (Jest/Istanbul)
|
|
34
|
-
for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
|
|
35
|
-
const summaryPath = join(cwd, relPath);
|
|
36
|
-
if (existsSync(summaryPath)) {
|
|
37
|
-
try {
|
|
38
|
-
const raw = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
39
|
-
return parseCoverageSummaryJson(raw, relPath);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// malformed, try next
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// 2. Try lcov.info
|
|
47
|
-
for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
|
|
48
|
-
const lcovPath = join(cwd, relPath);
|
|
49
|
-
if (existsSync(lcovPath)) {
|
|
50
|
-
try {
|
|
51
|
-
const raw = readFileSync(lcovPath, 'utf-8');
|
|
52
|
-
return parseLcovInfo(raw, relPath);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// malformed, try next
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// 3. Try .nyc_output/out.json
|
|
60
|
-
const nycPath = join(cwd, '.nyc_output', 'out.json');
|
|
61
|
-
if (existsSync(nycPath)) {
|
|
62
|
-
try {
|
|
63
|
-
const raw = JSON.parse(readFileSync(nycPath, 'utf-8'));
|
|
64
|
-
return parseCoverageSummaryJson(raw, '.nyc_output/out.json');
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// malformed
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return noData;
|
|
71
|
-
}
|
|
72
|
-
function parseCoverageSummaryJson(data, source) {
|
|
73
|
-
const entries = [];
|
|
74
|
-
let totalLines = 0, coveredLines = 0;
|
|
75
|
-
let totalBranches = 0, coveredBranches = 0;
|
|
76
|
-
let totalFunctions = 0, coveredFunctions = 0;
|
|
77
|
-
let totalStatements = 0, coveredStatements = 0;
|
|
78
|
-
for (const [filePath, metrics] of Object.entries(data)) {
|
|
79
|
-
if (filePath === 'total')
|
|
80
|
-
continue;
|
|
81
|
-
const m = metrics;
|
|
82
|
-
if (!m || typeof m !== 'object')
|
|
83
|
-
continue;
|
|
84
|
-
const linePct = m.lines?.pct ?? m.lines?.covered != null ? ((m.lines?.covered ?? 0) / Math.max(m.lines?.total ?? 1, 1)) * 100 : 0;
|
|
85
|
-
const branchPct = m.branches?.pct ?? (m.branches?.total ? ((m.branches?.covered ?? 0) / m.branches.total) * 100 : 100);
|
|
86
|
-
const funcPct = m.functions?.pct ?? (m.functions?.total ? ((m.functions?.covered ?? 0) / m.functions.total) * 100 : 100);
|
|
87
|
-
const stmtPct = m.statements?.pct ?? (m.statements?.total ? ((m.statements?.covered ?? 0) / m.statements.total) * 100 : 100);
|
|
88
|
-
entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
|
|
89
|
-
totalLines += m.lines?.total ?? 0;
|
|
90
|
-
coveredLines += m.lines?.covered ?? 0;
|
|
91
|
-
totalBranches += m.branches?.total ?? 0;
|
|
92
|
-
coveredBranches += m.branches?.covered ?? 0;
|
|
93
|
-
totalFunctions += m.functions?.total ?? 0;
|
|
94
|
-
coveredFunctions += m.functions?.covered ?? 0;
|
|
95
|
-
totalStatements += m.statements?.total ?? 0;
|
|
96
|
-
coveredStatements += m.statements?.covered ?? 0;
|
|
97
|
-
}
|
|
98
|
-
// Also read the total key if present
|
|
99
|
-
const total = data['total'];
|
|
100
|
-
const overallLine = total?.lines?.pct ?? (totalLines > 0 ? (coveredLines / totalLines) * 100 : 0);
|
|
101
|
-
const overallBranch = total?.branches?.pct ?? (totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0);
|
|
102
|
-
const overallFunction = total?.functions?.pct ?? (totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0);
|
|
103
|
-
const overallStatement = total?.statements?.pct ?? (totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0);
|
|
104
|
-
// Sort by lowest line coverage
|
|
105
|
-
entries.sort((a, b) => a.lines - b.lines);
|
|
106
|
-
return {
|
|
107
|
-
found: true,
|
|
108
|
-
source,
|
|
109
|
-
entries,
|
|
110
|
-
summary: {
|
|
111
|
-
totalFiles: entries.length,
|
|
112
|
-
overallLineCoverage: overallLine,
|
|
113
|
-
overallBranchCoverage: overallBranch,
|
|
114
|
-
overallFunctionCoverage: overallFunction,
|
|
115
|
-
overallStatementCoverage: overallStatement,
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function parseLcovInfo(raw, source) {
|
|
120
|
-
const entries = [];
|
|
121
|
-
let currentFile = '';
|
|
122
|
-
let linesHit = 0, linesFound = 0;
|
|
123
|
-
let branchesHit = 0, branchesFound = 0;
|
|
124
|
-
let functionsHit = 0, functionsFound = 0;
|
|
125
|
-
const flushRecord = () => {
|
|
126
|
-
if (currentFile) {
|
|
127
|
-
entries.push({
|
|
128
|
-
filePath: currentFile,
|
|
129
|
-
lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
|
|
130
|
-
branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
|
|
131
|
-
functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
|
|
132
|
-
statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
for (const line of raw.split('\n')) {
|
|
137
|
-
const trimmed = line.trim();
|
|
138
|
-
if (trimmed.startsWith('SF:')) {
|
|
139
|
-
currentFile = trimmed.slice(3);
|
|
140
|
-
linesHit = 0;
|
|
141
|
-
linesFound = 0;
|
|
142
|
-
branchesHit = 0;
|
|
143
|
-
branchesFound = 0;
|
|
144
|
-
functionsHit = 0;
|
|
145
|
-
functionsFound = 0;
|
|
146
|
-
}
|
|
147
|
-
else if (trimmed.startsWith('LH:')) {
|
|
148
|
-
linesHit = parseInt(trimmed.slice(3), 10) || 0;
|
|
149
|
-
}
|
|
150
|
-
else if (trimmed.startsWith('LF:')) {
|
|
151
|
-
linesFound = parseInt(trimmed.slice(3), 10) || 0;
|
|
152
|
-
}
|
|
153
|
-
else if (trimmed.startsWith('BRH:')) {
|
|
154
|
-
branchesHit = parseInt(trimmed.slice(4), 10) || 0;
|
|
155
|
-
}
|
|
156
|
-
else if (trimmed.startsWith('BRF:')) {
|
|
157
|
-
branchesFound = parseInt(trimmed.slice(4), 10) || 0;
|
|
158
|
-
}
|
|
159
|
-
else if (trimmed.startsWith('FNH:')) {
|
|
160
|
-
functionsHit = parseInt(trimmed.slice(4), 10) || 0;
|
|
161
|
-
}
|
|
162
|
-
else if (trimmed.startsWith('FNF:')) {
|
|
163
|
-
functionsFound = parseInt(trimmed.slice(4), 10) || 0;
|
|
164
|
-
}
|
|
165
|
-
else if (trimmed === 'end_of_record') {
|
|
166
|
-
flushRecord();
|
|
167
|
-
currentFile = '';
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
flushRecord();
|
|
171
|
-
entries.sort((a, b) => a.lines - b.lines);
|
|
172
|
-
let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0;
|
|
173
|
-
for (const e of entries) {
|
|
174
|
-
// Approximate from percentages (we lost exact counts after flush, but summaries are okay)
|
|
175
|
-
totalLH += e.lines;
|
|
176
|
-
totalLF += 100;
|
|
177
|
-
totalBH += e.branches;
|
|
178
|
-
totalBF += 100;
|
|
179
|
-
}
|
|
180
|
-
const n = entries.length || 1;
|
|
181
|
-
return {
|
|
182
|
-
found: true,
|
|
183
|
-
source,
|
|
184
|
-
entries,
|
|
185
|
-
summary: {
|
|
186
|
-
totalFiles: entries.length,
|
|
187
|
-
overallLineCoverage: totalLH / n,
|
|
188
|
-
overallBranchCoverage: totalBH / n,
|
|
189
|
-
overallFunctionCoverage: 0,
|
|
190
|
-
overallStatementCoverage: totalLH / n,
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
23
|
/**
|
|
195
24
|
* Classify a coverage gap by priority type based on coverage percentage and threshold
|
|
196
25
|
*/
|
|
@@ -2996,7 +2825,7 @@ const coverageRouteCommand = {
|
|
|
2996
2825
|
'c8: npx c8 npm test',
|
|
2997
2826
|
]);
|
|
2998
2827
|
output.writeln();
|
|
2999
|
-
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
|
|
2828
|
+
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
|
|
3000
2829
|
return { success: false, exitCode: 1 };
|
|
3001
2830
|
}
|
|
3002
2831
|
}
|
|
@@ -3187,7 +3016,7 @@ const coverageSuggestCommand = {
|
|
|
3187
3016
|
'c8: npx c8 npm test',
|
|
3188
3017
|
]);
|
|
3189
3018
|
output.writeln();
|
|
3190
|
-
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
|
|
3019
|
+
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
|
|
3191
3020
|
return { success: false, exitCode: 1 };
|
|
3192
3021
|
}
|
|
3193
3022
|
}
|
|
@@ -3407,7 +3236,7 @@ const coverageGapsCommand = {
|
|
|
3407
3236
|
'c8: npx c8 npm test',
|
|
3408
3237
|
]);
|
|
3409
3238
|
output.writeln();
|
|
3410
|
-
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
|
|
3239
|
+
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
|
|
3411
3240
|
return { success: false, exitCode: 1 };
|
|
3412
3241
|
}
|
|
3413
3242
|
}
|
package/dist/src/commands/tui.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* a one-line hint and exit 2, unless --plain asks for one frame as plain text and exit 0 (ADR-489 decision E).
|
|
10
10
|
*
|
|
11
11
|
* npx ruflo tui # interactive, in a terminal
|
|
12
|
-
* npx ruflo tui --view swarm # start on the Swarm page (keys 1-
|
|
12
|
+
* npx ruflo tui --view swarm # start on the Swarm page (keys 1-6 or o c s e m a switch pages)
|
|
13
13
|
* npx ruflo tui --plain | head # one plain frame, exit 0 (scripts, CI)
|
|
14
14
|
* npx ruflo tui | cat # not a terminal and no --plain: a hint on stderr, exit 2
|
|
15
15
|
*/
|
|
@@ -55,7 +55,7 @@ export const tuiCommand = {
|
|
|
55
55
|
],
|
|
56
56
|
examples: [
|
|
57
57
|
{ command: 'npx ruflo tui', description: 'Open the console in this terminal' },
|
|
58
|
-
{ command: 'npx ruflo tui --view swarm', description: 'Start on the Swarm page (keys 1-
|
|
58
|
+
{ command: 'npx ruflo tui --view swarm', description: 'Start on the Swarm page (keys 1-6 switch pages, tab cycles, q quits)' },
|
|
59
59
|
{ command: 'npx ruflo tui --view ai --model openai/gpt-4o-mini --max-spend-usd 0.1', description: 'The AI page on a cheap OpenRouter model, stopping at $0.10 reported (key in $OPENROUTER_API_KEY, never shown)' },
|
|
60
60
|
{ command: 'npx ruflo tui --plain | head -40', description: 'One plain frame, exit 0: for scripts and CI (no terminal needed)' },
|
|
61
61
|
],
|