@sentry/warden 0.0.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/.agents/skills/find-bugs/SKILL.md +75 -0
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.claude/settings.json +57 -0
- package/.claude/settings.local.json +88 -0
- package/.claude/skills/agent-prompt/SKILL.md +54 -0
- package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
- package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
- package/.claude/skills/agent-prompt/references/context-design.md +124 -0
- package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
- package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
- package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
- package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
- package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
- package/.claude/skills/notseer/SKILL.md +131 -0
- package/.claude/skills/skill-writer/SKILL.md +140 -0
- package/.claude/skills/testing-guidelines/SKILL.md +132 -0
- package/.claude/skills/warden-skill/SKILL.md +250 -0
- package/.claude/skills/warden-skill/references/config-schema.md +133 -0
- package/.dex/config.toml +2 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/release.yml +54 -0
- package/.github/workflows/warden.yml +40 -0
- package/AGENTS.md +89 -0
- package/CONTRIBUTING.md +60 -0
- package/LICENSE +105 -0
- package/README.md +43 -0
- package/SPEC.md +263 -0
- package/action.yml +87 -0
- package/assets/favicon.png +0 -0
- package/assets/warden-icon-bw.svg +5 -0
- package/assets/warden-icon-purple.png +0 -0
- package/assets/warden-icon-purple.svg +5 -0
- package/docs/.claude/settings.local.json +11 -0
- package/docs/astro.config.mjs +43 -0
- package/docs/package.json +19 -0
- package/docs/pnpm-lock.yaml +4000 -0
- package/docs/public/favicon.svg +5 -0
- package/docs/src/components/Code.astro +141 -0
- package/docs/src/components/PackageManagerTabs.astro +183 -0
- package/docs/src/components/Terminal.astro +212 -0
- package/docs/src/layouts/Base.astro +380 -0
- package/docs/src/pages/cli.astro +167 -0
- package/docs/src/pages/config.astro +394 -0
- package/docs/src/pages/guide.astro +449 -0
- package/docs/src/pages/index.astro +490 -0
- package/docs/src/styles/global.css +551 -0
- package/docs/tsconfig.json +3 -0
- package/docs/vercel.json +5 -0
- package/eslint.config.js +33 -0
- package/package.json +73 -0
- package/src/action/index.ts +1 -0
- package/src/action/main.ts +868 -0
- package/src/cli/args.test.ts +477 -0
- package/src/cli/args.ts +415 -0
- package/src/cli/commands/add.ts +447 -0
- package/src/cli/commands/init.test.ts +136 -0
- package/src/cli/commands/init.ts +132 -0
- package/src/cli/commands/setup-app/browser.ts +38 -0
- package/src/cli/commands/setup-app/credentials.ts +45 -0
- package/src/cli/commands/setup-app/manifest.ts +48 -0
- package/src/cli/commands/setup-app/server.ts +172 -0
- package/src/cli/commands/setup-app.ts +156 -0
- package/src/cli/commands/sync.ts +114 -0
- package/src/cli/context.ts +131 -0
- package/src/cli/files.test.ts +155 -0
- package/src/cli/files.ts +89 -0
- package/src/cli/fix.test.ts +310 -0
- package/src/cli/fix.ts +387 -0
- package/src/cli/git.test.ts +119 -0
- package/src/cli/git.ts +318 -0
- package/src/cli/index.ts +14 -0
- package/src/cli/main.ts +672 -0
- package/src/cli/output/box.ts +235 -0
- package/src/cli/output/formatters.test.ts +187 -0
- package/src/cli/output/formatters.ts +269 -0
- package/src/cli/output/icons.ts +13 -0
- package/src/cli/output/index.ts +44 -0
- package/src/cli/output/ink-runner.tsx +337 -0
- package/src/cli/output/jsonl.test.ts +347 -0
- package/src/cli/output/jsonl.ts +126 -0
- package/src/cli/output/reporter.ts +435 -0
- package/src/cli/output/tasks.ts +374 -0
- package/src/cli/output/tty.test.ts +117 -0
- package/src/cli/output/tty.ts +60 -0
- package/src/cli/output/verbosity.test.ts +40 -0
- package/src/cli/output/verbosity.ts +31 -0
- package/src/cli/terminal.test.ts +148 -0
- package/src/cli/terminal.ts +301 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.test.ts +313 -0
- package/src/config/loader.ts +103 -0
- package/src/config/schema.ts +168 -0
- package/src/config/writer.test.ts +119 -0
- package/src/config/writer.ts +84 -0
- package/src/diff/classify.test.ts +162 -0
- package/src/diff/classify.ts +92 -0
- package/src/diff/coalesce.test.ts +208 -0
- package/src/diff/coalesce.ts +133 -0
- package/src/diff/context.test.ts +226 -0
- package/src/diff/context.ts +201 -0
- package/src/diff/index.ts +4 -0
- package/src/diff/parser.test.ts +212 -0
- package/src/diff/parser.ts +149 -0
- package/src/event/context.ts +132 -0
- package/src/event/index.ts +2 -0
- package/src/event/schedule-context.ts +101 -0
- package/src/examples/examples.integration.test.ts +66 -0
- package/src/examples/index.test.ts +101 -0
- package/src/examples/index.ts +122 -0
- package/src/examples/setup.ts +25 -0
- package/src/index.ts +115 -0
- package/src/output/dedup.test.ts +419 -0
- package/src/output/dedup.ts +607 -0
- package/src/output/github-checks.test.ts +300 -0
- package/src/output/github-checks.ts +476 -0
- package/src/output/github-issues.ts +329 -0
- package/src/output/index.ts +5 -0
- package/src/output/issue-renderer.ts +197 -0
- package/src/output/renderer.test.ts +727 -0
- package/src/output/renderer.ts +217 -0
- package/src/output/stale.test.ts +375 -0
- package/src/output/stale.ts +155 -0
- package/src/output/types.ts +34 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/runner.test.ts +806 -0
- package/src/sdk/runner.ts +1232 -0
- package/src/skills/index.ts +36 -0
- package/src/skills/loader.test.ts +300 -0
- package/src/skills/loader.ts +423 -0
- package/src/skills/remote.test.ts +704 -0
- package/src/skills/remote.ts +604 -0
- package/src/triggers/matcher.test.ts +277 -0
- package/src/triggers/matcher.ts +152 -0
- package/src/types/index.ts +194 -0
- package/src/utils/async.ts +18 -0
- package/src/utils/index.test.ts +84 -0
- package/src/utils/index.ts +50 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +11 -0
- package/warden.toml +19 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { renderTerminalReport } from './terminal.js';
|
|
6
|
+
import type { SkillReport, Finding } from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
describe('renderTerminalReport', () => {
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tempDir = join(tmpdir(), `warden-terminal-test-${Date.now()}`);
|
|
13
|
+
mkdirSync(tempDir, { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function createFinding(overrides: Partial<Finding> = {}): Finding {
|
|
21
|
+
return {
|
|
22
|
+
id: 'test-1',
|
|
23
|
+
severity: 'medium',
|
|
24
|
+
title: 'Test Finding',
|
|
25
|
+
description: 'This is a test finding',
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createReport(overrides: Partial<SkillReport> = {}): SkillReport {
|
|
31
|
+
return {
|
|
32
|
+
skill: 'test-skill',
|
|
33
|
+
summary: 'Test summary',
|
|
34
|
+
findings: [],
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('file unavailable indication', () => {
|
|
40
|
+
it('shows file unavailable when source file cannot be read', () => {
|
|
41
|
+
const nonExistentPath = join(tempDir, 'nonexistent.ts');
|
|
42
|
+
const report = createReport({
|
|
43
|
+
findings: [
|
|
44
|
+
createFinding({
|
|
45
|
+
location: {
|
|
46
|
+
path: nonExistentPath,
|
|
47
|
+
startLine: 5,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const output = renderTerminalReport([report], {
|
|
54
|
+
isTTY: true,
|
|
55
|
+
supportsColor: false, // Disable color for easier assertions
|
|
56
|
+
columns: 80,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(output).toContain('5 │');
|
|
60
|
+
expect(output).toContain('(file unavailable)');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('shows code line when file exists and is readable', () => {
|
|
64
|
+
const filePath = join(tempDir, 'test.ts');
|
|
65
|
+
writeFileSync(
|
|
66
|
+
filePath,
|
|
67
|
+
'line 1\nline 2\nline 3\nline 4\nconst important = true;\nline 6'
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const report = createReport({
|
|
71
|
+
findings: [
|
|
72
|
+
createFinding({
|
|
73
|
+
location: {
|
|
74
|
+
path: filePath,
|
|
75
|
+
startLine: 5,
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const output = renderTerminalReport([report], {
|
|
82
|
+
isTTY: true,
|
|
83
|
+
supportsColor: false,
|
|
84
|
+
columns: 80,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(output).toContain('5 │');
|
|
88
|
+
expect(output).toContain('const important = true;');
|
|
89
|
+
expect(output).not.toContain('(file unavailable)');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('shows nothing when line number exceeds file length', () => {
|
|
93
|
+
const filePath = join(tempDir, 'short.ts');
|
|
94
|
+
writeFileSync(filePath, 'line 1\nline 2');
|
|
95
|
+
|
|
96
|
+
const report = createReport({
|
|
97
|
+
findings: [
|
|
98
|
+
createFinding({
|
|
99
|
+
location: {
|
|
100
|
+
path: filePath,
|
|
101
|
+
startLine: 100, // Way past end of file
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const output = renderTerminalReport([report], {
|
|
108
|
+
isTTY: true,
|
|
109
|
+
supportsColor: false,
|
|
110
|
+
columns: 80,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Should not show file unavailable or any code line for out-of-range
|
|
114
|
+
expect(output).not.toContain('100 │');
|
|
115
|
+
expect(output).not.toContain('(file unavailable)');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('basic rendering', () => {
|
|
120
|
+
it('renders report with no findings', () => {
|
|
121
|
+
const report = createReport();
|
|
122
|
+
|
|
123
|
+
const output = renderTerminalReport([report], {
|
|
124
|
+
isTTY: true,
|
|
125
|
+
supportsColor: false,
|
|
126
|
+
columns: 80,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(output).toContain('test-skill');
|
|
130
|
+
expect(output).toContain('No issues found');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('renders finding without location', () => {
|
|
134
|
+
const report = createReport({
|
|
135
|
+
findings: [createFinding()],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const output = renderTerminalReport([report], {
|
|
139
|
+
isTTY: true,
|
|
140
|
+
supportsColor: false,
|
|
141
|
+
columns: 80,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(output).toContain('Test Finding');
|
|
145
|
+
expect(output).toContain('This is a test finding');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import type { SkillReport, Finding, Severity, SeverityThreshold } from '../types/index.js';
|
|
4
|
+
import { filterFindingsBySeverity } from '../types/index.js';
|
|
5
|
+
import {
|
|
6
|
+
formatSeverityBadge,
|
|
7
|
+
formatSeverityPlain,
|
|
8
|
+
formatFindingCounts,
|
|
9
|
+
formatFindingCountsPlain,
|
|
10
|
+
formatDuration,
|
|
11
|
+
formatElapsed,
|
|
12
|
+
countBySeverity,
|
|
13
|
+
} from './output/index.js';
|
|
14
|
+
import { BoxRenderer } from './output/box.js';
|
|
15
|
+
import type { OutputMode } from './output/tty.js';
|
|
16
|
+
|
|
17
|
+
const SEVERITY_COLORS: Record<Severity, typeof chalk.red> = {
|
|
18
|
+
critical: chalk.red.bold,
|
|
19
|
+
high: chalk.red,
|
|
20
|
+
medium: chalk.yellow,
|
|
21
|
+
low: chalk.green,
|
|
22
|
+
info: chalk.blue,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type FileLineResult =
|
|
26
|
+
| { status: 'ok'; line: string }
|
|
27
|
+
| { status: 'file_unavailable' }
|
|
28
|
+
| { status: 'line_not_found' };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a specific line from a file.
|
|
32
|
+
* Returns a result indicating success, file unavailable, or line not found.
|
|
33
|
+
*/
|
|
34
|
+
function readFileLine(filePath: string, lineNumber: number): FileLineResult {
|
|
35
|
+
try {
|
|
36
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
const line = lines[lineNumber - 1];
|
|
39
|
+
if (lineNumber > 0 && lineNumber <= lines.length && line !== undefined) {
|
|
40
|
+
return { status: 'ok', line };
|
|
41
|
+
}
|
|
42
|
+
return { status: 'line_not_found' };
|
|
43
|
+
} catch {
|
|
44
|
+
return { status: 'file_unavailable' };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format a finding for TTY display.
|
|
50
|
+
*/
|
|
51
|
+
function formatFindingTTY(finding: Finding): string[] {
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
const badge = formatSeverityBadge(finding.severity);
|
|
54
|
+
const color = SEVERITY_COLORS[finding.severity];
|
|
55
|
+
|
|
56
|
+
// Title line with severity dot
|
|
57
|
+
const titleParts = [badge, color(finding.title)];
|
|
58
|
+
lines.push(titleParts.join(' '));
|
|
59
|
+
|
|
60
|
+
// Location with elapsed time
|
|
61
|
+
if (finding.location) {
|
|
62
|
+
const locParts = [chalk.dim(`${finding.location.path}:${finding.location.startLine}`)];
|
|
63
|
+
if (finding.elapsedMs !== undefined) {
|
|
64
|
+
locParts.push(chalk.dim(formatElapsed(finding.elapsedMs)));
|
|
65
|
+
}
|
|
66
|
+
lines.push(` ${locParts.join(' ')}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Code snippet
|
|
70
|
+
if (finding.location?.startLine) {
|
|
71
|
+
const result = readFileLine(finding.location.path, finding.location.startLine);
|
|
72
|
+
const lineNum = chalk.dim(`${finding.location.startLine} │`);
|
|
73
|
+
if (result.status === 'ok') {
|
|
74
|
+
lines.push(` ${lineNum} ${result.line.trimStart()}`);
|
|
75
|
+
} else if (result.status === 'file_unavailable') {
|
|
76
|
+
lines.push(` ${lineNum} ${chalk.dim.italic('(file unavailable)')}`);
|
|
77
|
+
}
|
|
78
|
+
// For 'line_not_found', we silently skip - the line may not exist in this version
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Blank line, then description
|
|
82
|
+
lines.push('');
|
|
83
|
+
lines.push(` ${chalk.dim(finding.description)}`);
|
|
84
|
+
|
|
85
|
+
// Suggested fix diff if available
|
|
86
|
+
if (finding.suggestedFix?.diff) {
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push(chalk.dim(' Suggested fix:'));
|
|
89
|
+
const diffLines = finding.suggestedFix.diff.split('\n').map((line) => {
|
|
90
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
91
|
+
return chalk.green(` ${line}`);
|
|
92
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
93
|
+
return chalk.red(` ${line}`);
|
|
94
|
+
} else if (line.startsWith('@@')) {
|
|
95
|
+
return chalk.cyan(` ${line}`);
|
|
96
|
+
}
|
|
97
|
+
return ` ${line}`;
|
|
98
|
+
});
|
|
99
|
+
lines.push(...diffLines);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format a finding for CI (non-TTY) display.
|
|
107
|
+
*/
|
|
108
|
+
function formatFindingCI(finding: Finding): string[] {
|
|
109
|
+
const lines: string[] = [];
|
|
110
|
+
const badge = formatSeverityPlain(finding.severity);
|
|
111
|
+
|
|
112
|
+
// Title line with location and elapsed time
|
|
113
|
+
const titleParts = [badge];
|
|
114
|
+
if (finding.location) {
|
|
115
|
+
titleParts.push(`${finding.location.path}:${finding.location.startLine}`);
|
|
116
|
+
}
|
|
117
|
+
titleParts.push('-', finding.title);
|
|
118
|
+
if (finding.elapsedMs !== undefined) {
|
|
119
|
+
titleParts.push(`(${formatElapsed(finding.elapsedMs)})`);
|
|
120
|
+
}
|
|
121
|
+
lines.push(titleParts.join(' '));
|
|
122
|
+
|
|
123
|
+
// Description
|
|
124
|
+
lines.push(` ${finding.description}`);
|
|
125
|
+
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render a skill report as a box (TTY mode).
|
|
131
|
+
*/
|
|
132
|
+
function renderSkillBoxTTY(report: SkillReport, mode: OutputMode): string[] {
|
|
133
|
+
const counts = countBySeverity(report.findings);
|
|
134
|
+
const durationStr = report.durationMs !== undefined ? formatDuration(report.durationMs) : undefined;
|
|
135
|
+
|
|
136
|
+
const box = new BoxRenderer({
|
|
137
|
+
title: report.skill,
|
|
138
|
+
badge: durationStr,
|
|
139
|
+
mode,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
box.header();
|
|
143
|
+
|
|
144
|
+
// Finding counts summary line
|
|
145
|
+
const countStr = formatFindingCounts(counts);
|
|
146
|
+
box.content(countStr);
|
|
147
|
+
|
|
148
|
+
if (report.findings.length === 0) {
|
|
149
|
+
box.blank();
|
|
150
|
+
box.content(chalk.green('No issues found.'));
|
|
151
|
+
} else {
|
|
152
|
+
// Render each finding
|
|
153
|
+
for (const [index, finding] of report.findings.entries()) {
|
|
154
|
+
box.divider();
|
|
155
|
+
box.blank();
|
|
156
|
+
const findingLines = formatFindingTTY(finding);
|
|
157
|
+
box.content(findingLines);
|
|
158
|
+
// Only add blank after finding if not the last one
|
|
159
|
+
if (index < report.findings.length - 1) {
|
|
160
|
+
box.blank();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
box.footer();
|
|
166
|
+
|
|
167
|
+
return box.render();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Render a skill report for CI (non-TTY) mode.
|
|
172
|
+
*/
|
|
173
|
+
function renderSkillCI(report: SkillReport): string[] {
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
const counts = countBySeverity(report.findings);
|
|
176
|
+
const durationStr = report.durationMs !== undefined ? formatDuration(report.durationMs) : '';
|
|
177
|
+
|
|
178
|
+
// Header
|
|
179
|
+
lines.push(`=== ${report.skill} (${durationStr}) ===`);
|
|
180
|
+
lines.push(`${formatFindingCountsPlain(counts)}`);
|
|
181
|
+
|
|
182
|
+
if (report.findings.length === 0) {
|
|
183
|
+
lines.push('No issues found.');
|
|
184
|
+
} else {
|
|
185
|
+
lines.push('---');
|
|
186
|
+
for (const finding of report.findings) {
|
|
187
|
+
const findingLines = formatFindingCI(finding);
|
|
188
|
+
lines.push(...findingLines);
|
|
189
|
+
lines.push('---');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Render skill reports for terminal output.
|
|
198
|
+
* @param reports - The skill reports to render
|
|
199
|
+
* @param mode - Output mode (TTY vs non-TTY)
|
|
200
|
+
*/
|
|
201
|
+
export function renderTerminalReport(reports: SkillReport[], mode?: OutputMode): string {
|
|
202
|
+
const lines: string[] = [];
|
|
203
|
+
|
|
204
|
+
// Default to TTY mode if not specified (for backwards compatibility)
|
|
205
|
+
const outputMode: OutputMode = mode ?? {
|
|
206
|
+
isTTY: true,
|
|
207
|
+
supportsColor: true,
|
|
208
|
+
columns: 80,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (outputMode.isTTY) {
|
|
212
|
+
// TTY mode: use boxes
|
|
213
|
+
for (const report of reports) {
|
|
214
|
+
lines.push(...renderSkillBoxTTY(report, outputMode));
|
|
215
|
+
lines.push('');
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
// CI mode: plain text
|
|
219
|
+
for (const report of reports) {
|
|
220
|
+
lines.push(...renderSkillCI(report));
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return lines.join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Aggregate usage stats from reports.
|
|
230
|
+
*/
|
|
231
|
+
function aggregateUsage(reports: SkillReport[]) {
|
|
232
|
+
const usages = reports.map((r) => r.usage).filter((u) => u !== undefined);
|
|
233
|
+
if (usages.length === 0) return undefined;
|
|
234
|
+
|
|
235
|
+
return usages.reduce((acc, u) => ({
|
|
236
|
+
inputTokens: acc.inputTokens + u.inputTokens,
|
|
237
|
+
outputTokens: acc.outputTokens + u.outputTokens,
|
|
238
|
+
cacheReadInputTokens: (acc.cacheReadInputTokens ?? 0) + (u.cacheReadInputTokens ?? 0),
|
|
239
|
+
cacheCreationInputTokens: (acc.cacheCreationInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0),
|
|
240
|
+
costUSD: acc.costUSD + u.costUSD,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Filter reports to only include findings at or above the given severity threshold.
|
|
246
|
+
* Returns new report objects with filtered findings; does not mutate the originals.
|
|
247
|
+
* If commentOn is 'off', returns reports with empty findings.
|
|
248
|
+
*/
|
|
249
|
+
export function filterReportsBySeverity(reports: SkillReport[], commentOn?: SeverityThreshold): SkillReport[] {
|
|
250
|
+
if (!commentOn) return reports;
|
|
251
|
+
return reports.map((report) => ({
|
|
252
|
+
...report,
|
|
253
|
+
findings: filterFindingsBySeverity(report.findings, commentOn),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Render skill reports as JSON.
|
|
259
|
+
*/
|
|
260
|
+
export function renderJsonReport(reports: SkillReport[]): string {
|
|
261
|
+
const totalUsage = aggregateUsage(reports);
|
|
262
|
+
|
|
263
|
+
const output = {
|
|
264
|
+
reports: reports.map((r) => ({
|
|
265
|
+
skill: r.skill,
|
|
266
|
+
summary: r.summary,
|
|
267
|
+
findings: r.findings,
|
|
268
|
+
metadata: r.metadata,
|
|
269
|
+
durationMs: r.durationMs,
|
|
270
|
+
usage: r.usage,
|
|
271
|
+
})),
|
|
272
|
+
summary: {
|
|
273
|
+
totalFindings: reports.reduce((sum, r) => sum + r.findings.length, 0),
|
|
274
|
+
bySeverity: {
|
|
275
|
+
critical: reports.reduce(
|
|
276
|
+
(sum, r) => sum + r.findings.filter((f) => f.severity === 'critical').length,
|
|
277
|
+
0
|
|
278
|
+
),
|
|
279
|
+
high: reports.reduce(
|
|
280
|
+
(sum, r) => sum + r.findings.filter((f) => f.severity === 'high').length,
|
|
281
|
+
0
|
|
282
|
+
),
|
|
283
|
+
medium: reports.reduce(
|
|
284
|
+
(sum, r) => sum + r.findings.filter((f) => f.severity === 'medium').length,
|
|
285
|
+
0
|
|
286
|
+
),
|
|
287
|
+
low: reports.reduce(
|
|
288
|
+
(sum, r) => sum + r.findings.filter((f) => f.severity === 'low').length,
|
|
289
|
+
0
|
|
290
|
+
),
|
|
291
|
+
info: reports.reduce(
|
|
292
|
+
(sum, r) => sum + r.findings.filter((f) => f.severity === 'info').length,
|
|
293
|
+
0
|
|
294
|
+
),
|
|
295
|
+
},
|
|
296
|
+
usage: totalUsage,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return JSON.stringify(output, null, 2);
|
|
301
|
+
}
|