@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.
Files changed (199) hide show
  1. package/.agents/skills/find-bugs/SKILL.md +75 -0
  2. package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
  3. package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
  4. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  5. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  6. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  7. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  8. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  9. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  10. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  11. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  12. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  13. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  14. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  15. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  16. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  17. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  18. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  19. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  20. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  21. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  22. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  23. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  24. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  25. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  26. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  27. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  28. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  29. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  30. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  31. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  32. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  33. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  34. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  35. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  36. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  37. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  38. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  39. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  40. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  41. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  42. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  43. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  44. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  45. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  61. package/.claude/settings.json +57 -0
  62. package/.claude/settings.local.json +88 -0
  63. package/.claude/skills/agent-prompt/SKILL.md +54 -0
  64. package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
  65. package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
  66. package/.claude/skills/agent-prompt/references/context-design.md +124 -0
  67. package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
  68. package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
  69. package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
  70. package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
  71. package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
  72. package/.claude/skills/notseer/SKILL.md +131 -0
  73. package/.claude/skills/skill-writer/SKILL.md +140 -0
  74. package/.claude/skills/testing-guidelines/SKILL.md +132 -0
  75. package/.claude/skills/warden-skill/SKILL.md +250 -0
  76. package/.claude/skills/warden-skill/references/config-schema.md +133 -0
  77. package/.dex/config.toml +2 -0
  78. package/.github/workflows/ci.yml +33 -0
  79. package/.github/workflows/release.yml +54 -0
  80. package/.github/workflows/warden.yml +40 -0
  81. package/AGENTS.md +89 -0
  82. package/CONTRIBUTING.md +60 -0
  83. package/LICENSE +105 -0
  84. package/README.md +43 -0
  85. package/SPEC.md +263 -0
  86. package/action.yml +87 -0
  87. package/assets/favicon.png +0 -0
  88. package/assets/warden-icon-bw.svg +5 -0
  89. package/assets/warden-icon-purple.png +0 -0
  90. package/assets/warden-icon-purple.svg +5 -0
  91. package/docs/.claude/settings.local.json +11 -0
  92. package/docs/astro.config.mjs +43 -0
  93. package/docs/package.json +19 -0
  94. package/docs/pnpm-lock.yaml +4000 -0
  95. package/docs/public/favicon.svg +5 -0
  96. package/docs/src/components/Code.astro +141 -0
  97. package/docs/src/components/PackageManagerTabs.astro +183 -0
  98. package/docs/src/components/Terminal.astro +212 -0
  99. package/docs/src/layouts/Base.astro +380 -0
  100. package/docs/src/pages/cli.astro +167 -0
  101. package/docs/src/pages/config.astro +394 -0
  102. package/docs/src/pages/guide.astro +449 -0
  103. package/docs/src/pages/index.astro +490 -0
  104. package/docs/src/styles/global.css +551 -0
  105. package/docs/tsconfig.json +3 -0
  106. package/docs/vercel.json +5 -0
  107. package/eslint.config.js +33 -0
  108. package/package.json +73 -0
  109. package/src/action/index.ts +1 -0
  110. package/src/action/main.ts +868 -0
  111. package/src/cli/args.test.ts +477 -0
  112. package/src/cli/args.ts +415 -0
  113. package/src/cli/commands/add.ts +447 -0
  114. package/src/cli/commands/init.test.ts +136 -0
  115. package/src/cli/commands/init.ts +132 -0
  116. package/src/cli/commands/setup-app/browser.ts +38 -0
  117. package/src/cli/commands/setup-app/credentials.ts +45 -0
  118. package/src/cli/commands/setup-app/manifest.ts +48 -0
  119. package/src/cli/commands/setup-app/server.ts +172 -0
  120. package/src/cli/commands/setup-app.ts +156 -0
  121. package/src/cli/commands/sync.ts +114 -0
  122. package/src/cli/context.ts +131 -0
  123. package/src/cli/files.test.ts +155 -0
  124. package/src/cli/files.ts +89 -0
  125. package/src/cli/fix.test.ts +310 -0
  126. package/src/cli/fix.ts +387 -0
  127. package/src/cli/git.test.ts +119 -0
  128. package/src/cli/git.ts +318 -0
  129. package/src/cli/index.ts +14 -0
  130. package/src/cli/main.ts +672 -0
  131. package/src/cli/output/box.ts +235 -0
  132. package/src/cli/output/formatters.test.ts +187 -0
  133. package/src/cli/output/formatters.ts +269 -0
  134. package/src/cli/output/icons.ts +13 -0
  135. package/src/cli/output/index.ts +44 -0
  136. package/src/cli/output/ink-runner.tsx +337 -0
  137. package/src/cli/output/jsonl.test.ts +347 -0
  138. package/src/cli/output/jsonl.ts +126 -0
  139. package/src/cli/output/reporter.ts +435 -0
  140. package/src/cli/output/tasks.ts +374 -0
  141. package/src/cli/output/tty.test.ts +117 -0
  142. package/src/cli/output/tty.ts +60 -0
  143. package/src/cli/output/verbosity.test.ts +40 -0
  144. package/src/cli/output/verbosity.ts +31 -0
  145. package/src/cli/terminal.test.ts +148 -0
  146. package/src/cli/terminal.ts +301 -0
  147. package/src/config/index.ts +3 -0
  148. package/src/config/loader.test.ts +313 -0
  149. package/src/config/loader.ts +103 -0
  150. package/src/config/schema.ts +168 -0
  151. package/src/config/writer.test.ts +119 -0
  152. package/src/config/writer.ts +84 -0
  153. package/src/diff/classify.test.ts +162 -0
  154. package/src/diff/classify.ts +92 -0
  155. package/src/diff/coalesce.test.ts +208 -0
  156. package/src/diff/coalesce.ts +133 -0
  157. package/src/diff/context.test.ts +226 -0
  158. package/src/diff/context.ts +201 -0
  159. package/src/diff/index.ts +4 -0
  160. package/src/diff/parser.test.ts +212 -0
  161. package/src/diff/parser.ts +149 -0
  162. package/src/event/context.ts +132 -0
  163. package/src/event/index.ts +2 -0
  164. package/src/event/schedule-context.ts +101 -0
  165. package/src/examples/examples.integration.test.ts +66 -0
  166. package/src/examples/index.test.ts +101 -0
  167. package/src/examples/index.ts +122 -0
  168. package/src/examples/setup.ts +25 -0
  169. package/src/index.ts +115 -0
  170. package/src/output/dedup.test.ts +419 -0
  171. package/src/output/dedup.ts +607 -0
  172. package/src/output/github-checks.test.ts +300 -0
  173. package/src/output/github-checks.ts +476 -0
  174. package/src/output/github-issues.ts +329 -0
  175. package/src/output/index.ts +5 -0
  176. package/src/output/issue-renderer.ts +197 -0
  177. package/src/output/renderer.test.ts +727 -0
  178. package/src/output/renderer.ts +217 -0
  179. package/src/output/stale.test.ts +375 -0
  180. package/src/output/stale.ts +155 -0
  181. package/src/output/types.ts +34 -0
  182. package/src/sdk/index.ts +1 -0
  183. package/src/sdk/runner.test.ts +806 -0
  184. package/src/sdk/runner.ts +1232 -0
  185. package/src/skills/index.ts +36 -0
  186. package/src/skills/loader.test.ts +300 -0
  187. package/src/skills/loader.ts +423 -0
  188. package/src/skills/remote.test.ts +704 -0
  189. package/src/skills/remote.ts +604 -0
  190. package/src/triggers/matcher.test.ts +277 -0
  191. package/src/triggers/matcher.ts +152 -0
  192. package/src/types/index.ts +194 -0
  193. package/src/utils/async.ts +18 -0
  194. package/src/utils/index.test.ts +84 -0
  195. package/src/utils/index.ts +50 -0
  196. package/tsconfig.json +25 -0
  197. package/vitest.config.ts +8 -0
  198. package/vitest.integration.config.ts +11 -0
  199. 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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './schema.js';
2
+ export * from './loader.js';
3
+ export * from './writer.js';