@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
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.56.
|
|
3
|
+
"version": "3.56.9",
|
|
4
4
|
"files": {
|
|
5
5
|
"auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
|
|
6
|
-
"hook-handler.cjs": "
|
|
6
|
+
"hook-handler.cjs": "bc12b3cae92d5a7c8deef699fa00d0ef07b57625c18dbeac4387f15725b8e09f",
|
|
7
7
|
"intelligence.cjs": "f486be296eb2f0518f34b8f4453d9d0a988845e4aec6d18d8d47725aca6dd38b",
|
|
8
8
|
"statusline.cjs": "d72417ecb38634011a19f095a30e07b9929e6f97663520172ece9be7491b2eaa",
|
|
9
9
|
"router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
|
-
"signature": "
|
|
12
|
+
"signature": "1qMXitpeXl6mUlSGGx2dT68gaou7Uf1Z6YS+eT1K/3h+B//G2GqbgEmHhdfa5mKmcxM6eml2KlsUgMMW/WknAA==",
|
|
13
13
|
"algorithm": "ed25519"
|
|
14
14
|
}
|
|
@@ -682,7 +682,8 @@ const handlers = {
|
|
|
682
682
|
const dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
|
|
683
683
|
// Join literal quote fragments and normalize root paths without shell evaluation.
|
|
684
684
|
function hasRootDelete(command, depth = 0) {
|
|
685
|
-
|
|
685
|
+
// mask has one letter per character of word: 'u' unquoted, 'q' quoted or escaped (only unquoted braces expand).
|
|
686
|
+
let word = '', mask = '', quote = '', started = false, redirect = false
|
|
686
687
|
let ansiNul = false // a NUL inside $'...' ends that string, as in bash
|
|
687
688
|
let inRm = false, optionsEnded = false, recursive = false, force = false, root = false
|
|
688
689
|
const isRoot = (operand) => {
|
|
@@ -712,23 +713,92 @@ const handlers = {
|
|
|
712
713
|
if (c === 'c' && at + 1 < s.length) return [String.fromCharCode(s.charCodeAt(at + 1) & 31), at + 1]
|
|
713
714
|
return ['\\' + c, at]
|
|
714
715
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
716
|
+
// Past the rescan bound (or the brace budget), a string is judged by its tokens: an rm and a possible root operand
|
|
717
|
+
// anywhere in it deny. Quote characters are dropped first, so r"m" is still rm; a path token with braces may expand to /.
|
|
718
|
+
const looseRootDelete = (text) => {
|
|
719
|
+
const tokens = text.replace(/["'\\]/g, '').split(/[\s;&|()\x60$]+/)
|
|
720
|
+
return tokens.some(t => t === 'rm' || t.endsWith('/rm')) && tokens.some(t => isRoot(t) || (t.includes('/') && /[{}]/.test(t)))
|
|
721
|
+
}
|
|
722
|
+
// Bash brace expansion of one word ({a,b} lists, nested or glued to text), and nothing else: no {1..3} sequences, and
|
|
723
|
+
// nothing inside parameter braces after a dollar sign. Only unquoted braces and commas count. Iterative and bounded:
|
|
724
|
+
// past BRACE_WORDS words or BRACE_CHARS characters of work in this call it returns undefined.
|
|
725
|
+
const BRACE_WORDS = 4096
|
|
726
|
+
let braceChars = 1_000_000
|
|
727
|
+
// The leftmost expandable group: an unquoted {...} with an unquoted comma at its own level, outside any dollar-brace parameter. One pass.
|
|
728
|
+
const firstGroup = (text, marks) => {
|
|
729
|
+
const stack = []
|
|
730
|
+
let dollars = 0
|
|
731
|
+
let best
|
|
732
|
+
for (let k = 0; k < text.length; k++) {
|
|
733
|
+
if (marks[k] !== 'u') continue
|
|
734
|
+
const c = text[k]
|
|
735
|
+
if (c === '{') {
|
|
736
|
+
const dollar = k > 0 && text[k - 1] === '$' && marks[k - 1] === 'u'
|
|
737
|
+
if (dollar) dollars++
|
|
738
|
+
stack.push({ open: k, commas: [], dollar: dollar || dollars > 0 })
|
|
739
|
+
} else if (c === ',' && stack.length > 0) stack[stack.length - 1].commas.push(k)
|
|
740
|
+
else if (c === '}' && stack.length > 0) {
|
|
741
|
+
const top = stack.pop()
|
|
742
|
+
if (top.dollar && text[top.open - 1] === '$' && marks[top.open - 1] === 'u') dollars--
|
|
743
|
+
if (!top.dollar && top.commas.length > 0 && (best === undefined || top.open < best[0])) best = [top.open, top.commas, k]
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return best
|
|
747
|
+
}
|
|
748
|
+
const braceExpand = (text, marks) => {
|
|
749
|
+
const out = []
|
|
750
|
+
const todo = [[text, marks]]
|
|
751
|
+
while (todo.length > 0) {
|
|
752
|
+
const [t, m] = todo.pop()
|
|
753
|
+
if ((braceChars -= t.length) < 0) return undefined
|
|
754
|
+
const group = firstGroup(t, m)
|
|
755
|
+
if (group === undefined) {
|
|
756
|
+
out.push(t)
|
|
757
|
+
if (out.length > BRACE_WORDS) return undefined
|
|
758
|
+
continue
|
|
759
|
+
}
|
|
760
|
+
const [open, commas, close] = group
|
|
761
|
+
const cuts = [open, ...commas, close]
|
|
762
|
+
// Pushed in reverse, so the words come out in bash's left-to-right order.
|
|
763
|
+
for (let c = cuts.length - 2; c >= 0; c--) {
|
|
764
|
+
const from = cuts[c] + 1, to = cuts[c + 1]
|
|
765
|
+
todo.push([t.slice(0, open) + t.slice(from, to) + t.slice(close + 1), m.slice(0, open) + m.slice(from, to) + m.slice(close + 1)])
|
|
766
|
+
}
|
|
767
|
+
if (todo.length > BRACE_WORDS) return undefined
|
|
721
768
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
769
|
+
return out
|
|
770
|
+
}
|
|
771
|
+
const takeWord = (w) => {
|
|
772
|
+
if (!inRm) inRm = w === 'rm' || w.endsWith('/rm')
|
|
773
|
+
else if (!optionsEnded && w === '--') optionsEnded = true
|
|
774
|
+
else if (!optionsEnded && w.startsWith('-')) {
|
|
725
775
|
// GNU getopt accepts any unambiguous long-option prefix: --r, --recur, --forc.
|
|
726
|
-
recursive = recursive || (
|
|
727
|
-
force = force || (
|
|
728
|
-
} else root = root || isRoot(
|
|
729
|
-
word = ''; started = false
|
|
776
|
+
recursive = recursive || (w.length > 2 && '--recursive'.startsWith(w)) || (/^-[a-z]*$/.test(w) && w.includes('r'))
|
|
777
|
+
force = force || (w.length > 2 && '--force'.startsWith(w)) || (/^-[a-z]*$/.test(w) && w.includes('f'))
|
|
778
|
+
} else root = root || isRoot(w)
|
|
730
779
|
return inRm && recursive && force && root
|
|
731
780
|
}
|
|
781
|
+
const finishWord = () => {
|
|
782
|
+
if (!started) return false
|
|
783
|
+
const text = word, marks = mask
|
|
784
|
+
word = ''; mask = ''; started = false
|
|
785
|
+
// Literal shell strings (e.g. sh -c 'rm -rf /', or sh -c '{rm,-rf,/}') also carried the old guard.
|
|
786
|
+
// Bound rescanning to four levels; beyond that judge the string by its tokens.
|
|
787
|
+
if (/[\s;&|()\x60{]/.test(text)) {
|
|
788
|
+
if (depth < 4 ? hasRootDelete(text, depth + 1) : looseRootDelete(text)) return true
|
|
789
|
+
}
|
|
790
|
+
// An unquoted {a,b} becomes separate words, as bash expands it: rm -rf {/*,} is rm -rf /*.
|
|
791
|
+
const words = text.includes('{') ? braceExpand(text, marks) : [text]
|
|
792
|
+
if (words === undefined) {
|
|
793
|
+
// Too many alternatives to check: one of them may be the root (an rm in progress fails closed), or the
|
|
794
|
+
// list itself may spell the rm: judge its tokens.
|
|
795
|
+
if (inRm) root = true
|
|
796
|
+
if (looseRootDelete(text.replace(/[{},]/g, ' '))) return true
|
|
797
|
+
return takeWord(text)
|
|
798
|
+
}
|
|
799
|
+
for (const w of words) if (w !== '' && takeWord(w)) return true
|
|
800
|
+
return false
|
|
801
|
+
}
|
|
732
802
|
// A command substitution's body, from just after its opening backtick. Bash removes a
|
|
733
803
|
// backslash only before $, a backtick or a backslash (and " inside double quotes) and
|
|
734
804
|
// drops backslash-newline; any other backslash stays for the nested scan.
|
|
@@ -742,7 +812,7 @@ const handlers = {
|
|
|
742
812
|
}
|
|
743
813
|
body += command[j]
|
|
744
814
|
}
|
|
745
|
-
return [j, depth < 4 ? hasRootDelete(body, depth + 1) : body
|
|
815
|
+
return [j, depth < 4 ? hasRootDelete(body, depth + 1) : looseRootDelete(body)]
|
|
746
816
|
}
|
|
747
817
|
const finishCommand = () => {
|
|
748
818
|
const denied = inRm && recursive && force && root
|
|
@@ -759,8 +829,8 @@ const handlers = {
|
|
|
759
829
|
else if (char === '\\' && i + 1 < command.length) {
|
|
760
830
|
const [text, last] = ansiEscape(command, i + 1)
|
|
761
831
|
i = last
|
|
762
|
-
if (!ansiNul) { const nul = text.indexOf('\0');
|
|
763
|
-
} else if (!ansiNul) word += char
|
|
832
|
+
if (!ansiNul) { const nul = text.indexOf('\0'); const kept = nul < 0 ? text : text.slice(0, nul); word += kept; mask += 'q'.repeat(kept.length); if (nul >= 0) ansiNul = true }
|
|
833
|
+
} else if (!ansiNul) { word += char; mask += 'q' }
|
|
764
834
|
continue
|
|
765
835
|
}
|
|
766
836
|
if (char === quote) quote = ''
|
|
@@ -770,20 +840,20 @@ const handlers = {
|
|
|
770
840
|
// The substitution is gone from the word, as bash's empty output is (a quoted empty substitution glued to -rf leaves -rf).
|
|
771
841
|
// A # right after it is still inside the word: a placeholder keeps a rescan (sh -c "...")
|
|
772
842
|
// from reading it as the start of a comment.
|
|
773
|
-
if (command[end + 1] === '#') word += '\u0001'
|
|
843
|
+
if (command[end + 1] === '#') { word += '\u0001'; mask += 'q' }
|
|
774
844
|
i = end
|
|
775
845
|
}
|
|
776
846
|
else if (quote === '"' && char === '\\' && i + 1 < command.length &&
|
|
777
847
|
(command[i + 1] === '"' || command[i + 1] === '\\' || command[i + 1] === '$' ||
|
|
778
848
|
command.charCodeAt(i + 1) === 96 || command[i + 1] === '\n')) {
|
|
779
849
|
const next = command[++i]
|
|
780
|
-
if (next !== '\n') word += next
|
|
781
|
-
} else word += char
|
|
850
|
+
if (next !== '\n') { word += next; mask += 'q' }
|
|
851
|
+
} else { word += char; mask += 'q' }
|
|
782
852
|
continue
|
|
783
853
|
}
|
|
784
854
|
if (char === '\\' && i + 1 < command.length) {
|
|
785
855
|
const next = command[++i]
|
|
786
|
-
if (next !== '\n') { word += next; started = true }
|
|
856
|
+
if (next !== '\n') { word += next; mask += 'q'; started = true }
|
|
787
857
|
} else if (char === '\x60' && command.indexOf('\x60', i + 1) > i) {
|
|
788
858
|
// Command substitution: scan the body as its own command; the substitution
|
|
789
859
|
// stays inside the enclosing word, so the rm being parsed keeps its state,
|
|
@@ -806,13 +876,29 @@ const handlers = {
|
|
|
806
876
|
redirect = char === '<' || char === '>'
|
|
807
877
|
if (!redirectionAmpersand && (char === '\n' || ';|&()'.includes(char)) && finishCommand()) return true
|
|
808
878
|
} else {
|
|
809
|
-
word += char; started = true
|
|
879
|
+
word += char; mask += 'u'; started = true
|
|
810
880
|
}
|
|
811
881
|
}
|
|
812
882
|
return finishWord() || finishCommand()
|
|
813
883
|
}
|
|
884
|
+
|
|
885
|
+
// A fork bomb's definition as bash reads it, whatever the spacing or the name: f(){ f|f& }, function f { f|f& },
|
|
886
|
+
// function f(){ f|f& }. Defining one is the danger: the call may come anywhere later.
|
|
887
|
+
function hasForkBomb(command) {
|
|
888
|
+
const s = command.replace(/\s+/g, '')
|
|
889
|
+
for (let brace = s.indexOf('{'); brace >= 0; brace = s.indexOf('{', brace + 1)) {
|
|
890
|
+
const end = s.startsWith('()', brace - 2) ? brace - 2 : brace
|
|
891
|
+
let start = end
|
|
892
|
+
while (start > 0 && end - start < 72 && /[a-z0-9_:.-]/.test(s[start - 1])) start--
|
|
893
|
+
const run = s.slice(start, end)
|
|
894
|
+
for (const name of run.startsWith('function') && run.length > 8 ? [run, run.slice(8)] : [run]) {
|
|
895
|
+
if (name && s.startsWith(name + '|' + name + '&}', brace + 1)) return true
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return false
|
|
899
|
+
}
|
|
814
900
|
for (const d of dangerous) {
|
|
815
|
-
if (d === 'rm -rf /' ? hasRootDelete(cmd) : cmd.includes(d)) {
|
|
901
|
+
if (d === 'rm -rf /' ? hasRootDelete(cmd) : d === ':(){:|:&};:' ? hasForkBomb(cmd) : cmd.includes(d)) {
|
|
816
902
|
console.error(`[BLOCKED] Dangerous command detected: ${d}`);
|
|
817
903
|
// Claude Code PreToolUse: exit 2 blocks execution; exit 1 is non-blocking.
|
|
818
904
|
process.exit(2);
|
package/README.md
CHANGED
|
@@ -49,6 +49,10 @@ npx -y ruflo@latest mcp start
|
|
|
49
49
|
|
|
50
50
|
MCP exposes Ruflo tools to your client. The console and mods above are Claude Code integrations; hook support depends on the client.
|
|
51
51
|
|
|
52
|
+
<img src="docs/assets/readme/icons/terminal.svg" width="28" height="28" alt=""> **Prefer a plain terminal? Optional.** The console also runs on its own, with no Claude Code: `npx ruflo tui` (add `--plain` to print one frame for scripts). It shows Overview, Cost, Swarm, Events and Missions, plus an optional AI page. [Terminal UI guide](docs/USERGUIDE.md#terminal-ui-optional)
|
|
53
|
+
|
|
54
|
+
<p align="center"><img src="docs/assets/ruflo-tui-compact.svg" width="640" alt="Looping recording of the optional ruflo terminal UI cycling through Overview, Cost, Swarm, Events and Missions in a sample project"></p>
|
|
55
|
+
|
|
52
56
|
[Compare install paths](#quick-start) · [Open the console](#console-walkthrough) · [User guide](docs/USERGUIDE.md)
|
|
53
57
|
|
|
54
58
|
<div align="center">
|
|
@@ -116,9 +120,11 @@ User → Ruflo (CLI/MCP) → Router → Swarm → Agents → Memory → LLM prov
|
|
|
116
120
|
|
|
117
121
|
<p align="center"><img src="docs/assets/readme/console.svg" alt="Mission control: inspect agent work" width="100%"></p>
|
|
118
122
|
|
|
119
|
-
<p align="center"><
|
|
123
|
+
<p align="center"><a href="https://raw.githubusercontent.com/ruvnet/ruflo/main/docs/assets/ruflo-console-theme-compact.svg"><img src="docs/assets/ruflo-console-theme-compact.svg" width="100%" alt="Ruflo Orbital theme: animated boot checks, block logo, menu icons, rotating globe, swarm routing, six color palettes and responsive Theme Studio"></a></p>
|
|
124
|
+
|
|
125
|
+
[Open animated demo](https://raw.githubusercontent.com/ruvnet/ruflo/main/docs/assets/ruflo-console-theme-compact.svg) · [Theme Studio guide](plugins/ruflo-console/README.md) · [Workflow demo](docs/assets/ruflo-console-workflows.svg)
|
|
120
126
|
|
|
121
|
-
<img src="docs/assets/readme/console-icon-workflows.svg" width="28" height="28" alt=""> **Your agent cockpit.** Track workflows, agents, models, tokens and cost beside Claude Code. The animation shows
|
|
127
|
+
<img src="docs/assets/readme/console-icon-workflows.svg" width="28" height="28" alt=""> **Your agent cockpit.** Track workflows, agents, models, tokens and cost beside Claude Code. The 38 second animation shows **Orbital and Theme Studio** through actual renderer frames, including boot checks, six palettes, customization, saved themes, JSON import and resizing. This compact 3.84 MB SVG retains all 456 frames. Native host and AI provider execution are not demonstrated.
|
|
122
128
|
|
|
123
129
|
<img src="docs/assets/readme/console-icon-inspect.svg" width="28" height="28" alt=""> **Inspect any run.** Open agent logs and results, search, triage failures, replay and compare. Start with `npx ruflo init`, restart Claude Code, then `/ruflo`. [Full console tour](docs/assets/ruflo-console-walkthrough-wide.gif).
|
|
124
130
|
|
|
@@ -136,7 +142,7 @@ User → Ruflo (CLI/MCP) → Router → Swarm → Agents → Memory → LLM prov
|
|
|
136
142
|
4. **Verify the result.** Check that Missions contains the goal and Overview records the action. Creating a mission does not mean the feature has been implemented.
|
|
137
143
|
5. **Keep control.** **Take back control** pauses Claude's console tools. A later call should be refused until you restore control.
|
|
138
144
|
|
|
139
|
-
**What the recording demonstrates:** a real Claude Haiku session with `write + auto`, mission creation, page navigation, a refused swarm stop, and control being taken back. The steps above use `ask` so you approve creation yourself. The
|
|
145
|
+
**What the recording demonstrates:** a real Claude Haiku session with `write + auto`, mission creation, page navigation, a refused swarm stop, and control being taken back. The steps above use `ask` so you approve creation yourself. The linked workflow animation uses sample data, while the theme demo replays renderer frames. Neither is evidence of completed swarm work or memory recall.
|
|
140
146
|
|
|
141
147
|
[Implementation and recorded test findings](v3/docs/adr/ADR-444-claude-controls-the-console.md) · [Reproduction script](plugins/ruflo-console/scripts/e2e-control.sh)
|
|
142
148
|
|
package/catalog-manifest.json
CHANGED
|
@@ -97,11 +97,19 @@ const diffCommand = {
|
|
|
97
97
|
output.printInfo(`Analyzing diff: ${output.highlight(ref)}`);
|
|
98
98
|
try {
|
|
99
99
|
// Call MCP tool for diff analysis
|
|
100
|
+
// Shape returned by the analyze_diff MCP tool (ruvector/diff-classifier.ts).
|
|
101
|
+
// Typed from the classifier itself so the renderer cannot drift from it again.
|
|
100
102
|
const result = await callMCPTool('analyze_diff', {
|
|
101
103
|
ref,
|
|
102
|
-
|
|
104
|
+
// Always request file risks: the handler computes them anyway, and the
|
|
105
|
+
// high-risk file list below is derived from them.
|
|
106
|
+
includeFileRisks: true,
|
|
103
107
|
includeReviewers: showReviewers || showAll,
|
|
104
108
|
});
|
|
109
|
+
if (result.error) {
|
|
110
|
+
output.printError(`Diff analysis failed: ${result.message ?? 'unknown error'}`);
|
|
111
|
+
return { success: false, exitCode: 1 };
|
|
112
|
+
}
|
|
105
113
|
// JSON output
|
|
106
114
|
if (formatType === 'json') {
|
|
107
115
|
output.printJson(result);
|
|
@@ -110,13 +118,18 @@ const diffCommand = {
|
|
|
110
118
|
output.writeln();
|
|
111
119
|
// Summary box
|
|
112
120
|
const files = result.files || [];
|
|
113
|
-
const
|
|
114
|
-
const
|
|
121
|
+
const fileRisks = result.fileRisks || [];
|
|
122
|
+
const risk = result.risk || { overall: 'low', score: 0, breakdown: { low: 0, medium: 0, high: 0, critical: 0 } };
|
|
123
|
+
const classification = result.classification;
|
|
124
|
+
const category = classification?.primary ?? 'unknown';
|
|
125
|
+
const secondary = classification?.secondary?.length ? classification.secondary.join(', ') : '';
|
|
126
|
+
const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
|
|
127
|
+
const highRiskFiles = fileRisks.filter((r) => r.risk === 'high' || r.risk === 'critical').map((r) => r.file);
|
|
115
128
|
output.printBox([
|
|
116
129
|
`Ref: ${result.ref || 'HEAD'}`,
|
|
117
130
|
`Files: ${files.length}`,
|
|
118
131
|
`Risk: ${getRiskDisplay(risk.overall)} (${risk.score}/100)`,
|
|
119
|
-
`Type: ${
|
|
132
|
+
`Type: ${category}${secondary ? ` (${secondary})` : ''}`,
|
|
120
133
|
``,
|
|
121
134
|
result.summary || 'No summary available',
|
|
122
135
|
].join('\n'), 'Diff Analysis');
|
|
@@ -128,33 +141,28 @@ const diffCommand = {
|
|
|
128
141
|
output.printTable({
|
|
129
142
|
columns: [
|
|
130
143
|
{ key: 'metric', header: 'Metric', width: 25 },
|
|
131
|
-
{ key: 'value', header: 'Value', width:
|
|
144
|
+
{ key: 'value', header: 'Value', width: 42 },
|
|
132
145
|
],
|
|
133
146
|
data: [
|
|
134
147
|
{ metric: 'Overall Risk', value: getRiskDisplay(risk.overall) },
|
|
135
148
|
{ metric: 'Risk Score', value: `${risk.score}/100` },
|
|
136
|
-
{ metric: 'Files Changed', value:
|
|
137
|
-
{ metric: 'Total Lines Changed', value:
|
|
138
|
-
{ metric: '
|
|
149
|
+
{ metric: 'Files Changed', value: files.length },
|
|
150
|
+
{ metric: 'Total Lines Changed', value: totalChanges },
|
|
151
|
+
{ metric: 'Files by Risk', value: `low ${risk.breakdown.low} / medium ${risk.breakdown.medium} / high ${risk.breakdown.high} / critical ${risk.breakdown.critical}` },
|
|
139
152
|
],
|
|
140
153
|
});
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
output.writeln(output.bold(output.warning('Security Concerns')));
|
|
145
|
-
output.printList(risk.breakdown.securityConcerns.map(c => output.warning(c)));
|
|
146
|
-
}
|
|
147
|
-
// Breaking changes
|
|
148
|
-
if (risk.breakdown.breakingChanges.length > 0) {
|
|
154
|
+
// Risk factors reported by the classifier
|
|
155
|
+
const riskFactors = classification?.riskFactors ?? [];
|
|
156
|
+
if (riskFactors.length > 0) {
|
|
149
157
|
output.writeln();
|
|
150
|
-
output.writeln(output.bold(output.
|
|
151
|
-
output.printList(
|
|
158
|
+
output.writeln(output.bold(output.warning('Risk Factors')));
|
|
159
|
+
output.printList(riskFactors.map(c => output.warning(c)));
|
|
152
160
|
}
|
|
153
161
|
// High risk files
|
|
154
|
-
if (
|
|
162
|
+
if (highRiskFiles.length > 0) {
|
|
155
163
|
output.writeln();
|
|
156
164
|
output.writeln(output.bold('High Risk Files'));
|
|
157
|
-
output.printList(
|
|
165
|
+
output.printList(highRiskFiles.map(f => output.warning(f)));
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
// Classification
|
|
@@ -168,13 +176,16 @@ const diffCommand = {
|
|
|
168
176
|
{ key: 'value', header: 'Value', width: 40 },
|
|
169
177
|
],
|
|
170
178
|
data: [
|
|
171
|
-
{ field: 'Category', value:
|
|
172
|
-
{ field: '
|
|
173
|
-
{ field: 'Confidence', value: `${(classification
|
|
179
|
+
{ field: 'Category', value: category },
|
|
180
|
+
{ field: 'Secondary', value: secondary || '-' },
|
|
181
|
+
{ field: 'Confidence', value: `${((classification?.confidence ?? 0) * 100).toFixed(0)}%` },
|
|
182
|
+
{ field: 'Impact', value: classification?.impactLevel ?? '-' },
|
|
174
183
|
],
|
|
175
184
|
});
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
if (classification?.testingStrategy?.length) {
|
|
186
|
+
output.writeln();
|
|
187
|
+
output.writeln(output.dim(`Testing strategy: ${classification.testingStrategy.join(', ')}`));
|
|
188
|
+
}
|
|
178
189
|
}
|
|
179
190
|
// Reviewers
|
|
180
191
|
if (showReviewers || showAll) {
|
|
@@ -190,13 +201,13 @@ const diffCommand = {
|
|
|
190
201
|
}
|
|
191
202
|
}
|
|
192
203
|
// Verbose file-level details
|
|
193
|
-
if (verbose &&
|
|
204
|
+
if (verbose && fileRisks.length > 0) {
|
|
194
205
|
output.writeln();
|
|
195
206
|
output.writeln(output.bold('File-Level Analysis'));
|
|
196
207
|
output.writeln(output.dim('-'.repeat(50)));
|
|
197
208
|
output.printTable({
|
|
198
209
|
columns: [
|
|
199
|
-
{ key: '
|
|
210
|
+
{ key: 'file', header: 'File', width: 40 },
|
|
200
211
|
{ key: 'risk', header: 'Risk', width: 12, format: (v) => getRiskDisplay(String(v)) },
|
|
201
212
|
{ key: 'score', header: 'Score', width: 8, align: 'right' },
|
|
202
213
|
{ key: 'reasons', header: 'Reasons', width: 30, format: (v) => {
|
|
@@ -204,7 +215,7 @@ const diffCommand = {
|
|
|
204
215
|
return reasons.slice(0, 2).join('; ');
|
|
205
216
|
} },
|
|
206
217
|
],
|
|
207
|
-
data:
|
|
218
|
+
data: fileRisks.map((r) => ({ ...r })),
|
|
208
219
|
});
|
|
209
220
|
}
|
|
210
221
|
// Files changed table
|
|
@@ -219,7 +230,7 @@ const diffCommand = {
|
|
|
219
230
|
{ key: 'additions', header: '+', width: 8, align: 'right', format: (v) => output.success(`+${v}`) },
|
|
220
231
|
{ key: 'deletions', header: '-', width: 8, align: 'right', format: (v) => output.error(`-${v}`) },
|
|
221
232
|
],
|
|
222
|
-
data: files.slice(0, 20),
|
|
233
|
+
data: files.slice(0, 20).map((f) => ({ ...f })),
|
|
223
234
|
});
|
|
224
235
|
if (files.length > 20) {
|
|
225
236
|
output.writeln(output.dim(` ... and ${files.length - 20} more files`));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
interface CoverageFileEntry {
|
|
2
|
+
filePath: string;
|
|
3
|
+
lines: number;
|
|
4
|
+
branches: number;
|
|
5
|
+
functions: number;
|
|
6
|
+
statements: number;
|
|
7
|
+
}
|
|
8
|
+
interface CoverageData {
|
|
9
|
+
found: boolean;
|
|
10
|
+
source: string;
|
|
11
|
+
entries: CoverageFileEntry[];
|
|
12
|
+
summary: {
|
|
13
|
+
totalFiles: number;
|
|
14
|
+
overallLineCoverage: number;
|
|
15
|
+
overallBranchCoverage: number;
|
|
16
|
+
overallFunctionCoverage: number;
|
|
17
|
+
overallStatementCoverage: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read coverage data from disk. Checks these locations in order:
|
|
22
|
+
* 1. coverage/coverage-summary.json (Jest/Istanbul)
|
|
23
|
+
* 2. coverage/lcov.info (lcov format)
|
|
24
|
+
* 3. coverage/coverage-final.json (Istanbul counters)
|
|
25
|
+
* 4. .nyc_output/out.json (nyc counters or legacy summary)
|
|
26
|
+
*/
|
|
27
|
+
export declare function readCoverageFromDisk(): CoverageData;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=coverage-reader.d.ts.map
|