ghagga 2.4.0 → 2.5.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 (54) hide show
  1. package/README.md +47 -7
  2. package/dist/commands/__tests__/review-tui.test.d.ts +11 -0
  3. package/dist/commands/__tests__/review-tui.test.d.ts.map +1 -0
  4. package/dist/commands/__tests__/review-tui.test.js +256 -0
  5. package/dist/commands/__tests__/review-tui.test.js.map +1 -0
  6. package/dist/commands/health.d.ts +16 -0
  7. package/dist/commands/health.d.ts.map +1 -0
  8. package/dist/commands/health.js +139 -0
  9. package/dist/commands/health.js.map +1 -0
  10. package/dist/commands/review-output.test.d.ts +10 -0
  11. package/dist/commands/review-output.test.d.ts.map +1 -0
  12. package/dist/commands/review-output.test.js +222 -0
  13. package/dist/commands/review-output.test.js.map +1 -0
  14. package/dist/commands/review.d.ts +8 -1
  15. package/dist/commands/review.d.ts.map +1 -1
  16. package/dist/commands/review.js +139 -22
  17. package/dist/commands/review.js.map +1 -1
  18. package/dist/commands/review.test.js +13 -9
  19. package/dist/commands/review.test.js.map +1 -1
  20. package/dist/index.js +44 -8
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/github-api.d.ts +69 -0
  23. package/dist/lib/github-api.d.ts.map +1 -0
  24. package/dist/lib/github-api.js +186 -0
  25. package/dist/lib/github-api.js.map +1 -0
  26. package/dist/lib/github-api.test.d.ts +2 -0
  27. package/dist/lib/github-api.test.d.ts.map +1 -0
  28. package/dist/lib/github-api.test.js +197 -0
  29. package/dist/lib/github-api.test.js.map +1 -0
  30. package/dist/ui/__tests__/tui-extensions.test.d.ts +9 -0
  31. package/dist/ui/__tests__/tui-extensions.test.d.ts.map +1 -0
  32. package/dist/ui/__tests__/tui-extensions.test.js +230 -0
  33. package/dist/ui/__tests__/tui-extensions.test.js.map +1 -0
  34. package/dist/ui/chalk.d.ts +17 -0
  35. package/dist/ui/chalk.d.ts.map +1 -0
  36. package/dist/ui/chalk.js +25 -0
  37. package/dist/ui/chalk.js.map +1 -0
  38. package/dist/ui/format.d.ts +21 -1
  39. package/dist/ui/format.d.ts.map +1 -1
  40. package/dist/ui/format.js +67 -1
  41. package/dist/ui/format.js.map +1 -1
  42. package/dist/ui/sarif.d.ts +6 -0
  43. package/dist/ui/sarif.d.ts.map +1 -0
  44. package/dist/ui/sarif.js +5 -0
  45. package/dist/ui/sarif.js.map +1 -0
  46. package/dist/ui/theme.d.ts +16 -0
  47. package/dist/ui/theme.d.ts.map +1 -1
  48. package/dist/ui/theme.js +16 -0
  49. package/dist/ui/theme.js.map +1 -1
  50. package/dist/ui/tui.d.ts +31 -0
  51. package/dist/ui/tui.d.ts.map +1 -1
  52. package/dist/ui/tui.js +91 -0
  53. package/dist/ui/tui.js.map +1 -1
  54. package/package.json +3 -2
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Minimal GitHub REST API client for issue management.
3
+ *
4
+ * Uses native fetch (Node 20+) — zero external dependencies.
5
+ * Auth via stored GitHub token from `ghagga login`.
6
+ * Design: AD8 — minimal fetch wrapper, 3 endpoints only.
7
+ */
8
+ // ─── Error ──────────────────────────────────────────────────────
9
+ export class GitHubApiError extends Error {
10
+ status;
11
+ body;
12
+ constructor(message, status, body) {
13
+ super(message);
14
+ this.status = status;
15
+ this.body = body;
16
+ this.name = 'GitHubApiError';
17
+ }
18
+ }
19
+ // ─── Shared Headers ─────────────────────────────────────────────
20
+ function apiHeaders(token) {
21
+ return {
22
+ Authorization: `Bearer ${token}`,
23
+ Accept: 'application/vnd.github+json',
24
+ 'X-GitHub-Api-Version': '2022-11-28',
25
+ 'Content-Type': 'application/json',
26
+ };
27
+ }
28
+ // ─── Error Handling ─────────────────────────────────────────────
29
+ async function handleErrorResponse(res) {
30
+ const body = await res.text();
31
+ const messages = {
32
+ 401: 'Authentication failed — token may be expired. Run `ghagga login` to re-authenticate.',
33
+ 403: 'Insufficient permissions — token lacks repo scope.',
34
+ 404: 'Repository or issue not found.',
35
+ 410: 'Issues are disabled for this repository.',
36
+ 422: `Validation error: ${body}`,
37
+ 429: 'GitHub API rate limit exceeded. Wait a few minutes and try again.',
38
+ };
39
+ const message = messages[res.status] ?? `GitHub API error (${res.status}): ${body}`;
40
+ throw new GitHubApiError(message, res.status, body);
41
+ }
42
+ // ─── API Functions ──────────────────────────────────────────────
43
+ /**
44
+ * Create a new GitHub issue.
45
+ * @throws GitHubApiError on auth failure, rate limit, or issues disabled.
46
+ */
47
+ export async function createIssue(opts) {
48
+ const url = `https://api.github.com/repos/${opts.owner}/${opts.repo}/issues`;
49
+ const res = await fetch(url, {
50
+ method: 'POST',
51
+ headers: apiHeaders(opts.token),
52
+ body: JSON.stringify({
53
+ title: opts.title,
54
+ body: opts.body,
55
+ labels: opts.labels,
56
+ }),
57
+ });
58
+ if (!res.ok)
59
+ await handleErrorResponse(res);
60
+ const data = (await res.json());
61
+ return { url: data.html_url, number: data.number };
62
+ }
63
+ /**
64
+ * Post a comment on an existing GitHub issue.
65
+ * @throws GitHubApiError on auth failure, rate limit, or issue not found.
66
+ */
67
+ export async function createComment(opts) {
68
+ const url = `https://api.github.com/repos/${opts.owner}/${opts.repo}/issues/${opts.issueNumber}/comments`;
69
+ const res = await fetch(url, {
70
+ method: 'POST',
71
+ headers: apiHeaders(opts.token),
72
+ body: JSON.stringify({ body: opts.body }),
73
+ });
74
+ if (!res.ok)
75
+ await handleErrorResponse(res);
76
+ const data = (await res.json());
77
+ return { url: data.html_url };
78
+ }
79
+ /**
80
+ * Ensure a label exists on the repo. Creates it if missing.
81
+ * Silently ignores 422 (already exists) and 403 (insufficient permissions).
82
+ */
83
+ export async function ensureLabel(opts) {
84
+ const url = `https://api.github.com/repos/${opts.owner}/${opts.repo}/labels`;
85
+ const res = await fetch(url, {
86
+ method: 'POST',
87
+ headers: apiHeaders(opts.token),
88
+ body: JSON.stringify({
89
+ name: opts.name,
90
+ color: opts.color,
91
+ description: opts.description,
92
+ }),
93
+ });
94
+ // 201 = created, 422 = already exists, 403 = insufficient permissions
95
+ // All are acceptable — only throw on unexpected errors
96
+ if (!res.ok && res.status !== 422 && res.status !== 403) {
97
+ await handleErrorResponse(res);
98
+ }
99
+ }
100
+ // ─── Remote URL Parsing ─────────────────────────────────────────
101
+ /**
102
+ * Parse owner/repo from a git remote URL.
103
+ * Supports HTTPS, SSH (git@), and ssh:// protocol formats.
104
+ * @throws if the remote is not a GitHub URL.
105
+ */
106
+ export function parseGitHubRemote(remoteUrl) {
107
+ const trimmed = remoteUrl.trim();
108
+ // HTTPS: https://github.com/owner/repo.git
109
+ const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/);
110
+ if (httpsMatch) {
111
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
112
+ }
113
+ // SSH: git@github.com:owner/repo.git
114
+ const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/\s]+?)(?:\.git)?$/);
115
+ if (sshMatch) {
116
+ return { owner: sshMatch[1], repo: sshMatch[2] };
117
+ }
118
+ // SSH protocol: ssh://git@github.com/owner/repo.git
119
+ const sshProtoMatch = trimmed.match(/^ssh:\/\/git@github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/);
120
+ if (sshProtoMatch) {
121
+ return { owner: sshProtoMatch[1], repo: sshProtoMatch[2] };
122
+ }
123
+ throw new Error(`Not a GitHub remote URL: "${trimmed}"`);
124
+ }
125
+ // ─── Issue Body Formatting ──────────────────────────────────────
126
+ /**
127
+ * Format a ReviewResult as a GitHub issue body with summary and collapsible details.
128
+ */
129
+ export function formatIssueBody(result, version) {
130
+ const timeSeconds = (result.metadata.executionTimeMs / 1000).toFixed(1);
131
+ // Count findings by severity
132
+ const counts = {};
133
+ for (const f of result.findings) {
134
+ counts[f.severity] = (counts[f.severity] ?? 0) + 1;
135
+ }
136
+ const countParts = [];
137
+ for (const sev of ['critical', 'high', 'medium', 'low', 'info']) {
138
+ if (counts[sev])
139
+ countParts.push(`${sev}: ${counts[sev]}`);
140
+ }
141
+ const lines = [];
142
+ // Summary section
143
+ lines.push('## Review Summary');
144
+ lines.push('');
145
+ lines.push(`| Field | Value |`);
146
+ lines.push(`|-------|-------|`);
147
+ lines.push(`| **Status** | ${result.status} |`);
148
+ lines.push(`| **Findings** | ${result.findings.length} total (${countParts.join(', ') || 'none'}) |`);
149
+ lines.push(`| **Mode** | ${result.metadata.mode} |`);
150
+ lines.push(`| **Model** | ${result.metadata.model} |`);
151
+ lines.push(`| **Execution time** | ${timeSeconds}s |`);
152
+ lines.push('');
153
+ // Collapsible full review
154
+ lines.push('<details>');
155
+ lines.push('<summary>Full Review Details</summary>');
156
+ lines.push('');
157
+ lines.push(formatFindingsMarkdown(result));
158
+ lines.push('');
159
+ lines.push('</details>');
160
+ lines.push('');
161
+ // Footer
162
+ lines.push(`---`);
163
+ lines.push(`*Generated by [GHAGGA](https://github.com/JNZader/ghagga) v${version}*`);
164
+ return lines.join('\n');
165
+ }
166
+ /**
167
+ * Format findings as markdown for the collapsible section.
168
+ */
169
+ function formatFindingsMarkdown(result) {
170
+ const lines = [];
171
+ lines.push(`**Summary:** ${result.summary}`);
172
+ lines.push('');
173
+ if (result.findings.length === 0) {
174
+ lines.push('No findings. Clean! 🎉');
175
+ return lines.join('\n');
176
+ }
177
+ for (const finding of result.findings) {
178
+ const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
179
+ lines.push(`- **[${finding.severity.toUpperCase()}]** \`${location}\` — ${finding.message}`);
180
+ if (finding.suggestion) {
181
+ lines.push(` - 💡 ${finding.suggestion}`);
182
+ }
183
+ }
184
+ return lines.join('\n');
185
+ }
186
+ //# sourceMappingURL=github-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-api.js","sourceRoot":"","sources":["../../src/lib/github-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=github-api.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-api.test.d.ts","sourceRoot":"","sources":["../../src/lib/github-api.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,197 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { createComment, createIssue, ensureLabel, formatIssueBody, GitHubApiError, parseGitHubRemote, } from './github-api.js';
3
+ // ─── Mocks ──────────────────────────────────────────────────────
4
+ const mockFetch = vi.fn();
5
+ vi.stubGlobal('fetch', mockFetch);
6
+ // ─── Helpers ────────────────────────────────────────────────────
7
+ function jsonResponse(status, body) {
8
+ return {
9
+ ok: status >= 200 && status < 300,
10
+ status,
11
+ text: () => Promise.resolve(JSON.stringify(body)),
12
+ json: () => Promise.resolve(body),
13
+ };
14
+ }
15
+ function errorResponse(status, message) {
16
+ return {
17
+ ok: false,
18
+ status,
19
+ text: () => Promise.resolve(message),
20
+ json: () => Promise.resolve({ message }),
21
+ };
22
+ }
23
+ /** Minimal ReviewResult for formatting tests. */
24
+ function makeResult(overrides = {}) {
25
+ return {
26
+ status: 'PASSED',
27
+ summary: 'All good',
28
+ findings: [
29
+ {
30
+ severity: 'medium',
31
+ category: 'style',
32
+ file: 'src/index.ts',
33
+ line: 10,
34
+ message: 'Use const',
35
+ source: 'ai',
36
+ },
37
+ {
38
+ severity: 'high',
39
+ category: 'security',
40
+ file: 'src/auth.ts',
41
+ message: 'Unsafe input',
42
+ source: 'semgrep',
43
+ },
44
+ ],
45
+ staticAnalysis: {
46
+ semgrep: { status: 'success', findings: [], executionTimeMs: 100 },
47
+ trivy: { status: 'skipped', findings: [], executionTimeMs: 0 },
48
+ cpd: { status: 'skipped', findings: [], executionTimeMs: 0 },
49
+ },
50
+ memoryContext: null,
51
+ metadata: {
52
+ mode: 'simple',
53
+ provider: 'github',
54
+ model: 'gpt-4o-mini',
55
+ tokensUsed: 500,
56
+ executionTimeMs: 3200,
57
+ toolsRun: ['semgrep'],
58
+ toolsSkipped: ['trivy', 'cpd'],
59
+ },
60
+ ...overrides,
61
+ };
62
+ }
63
+ // ─── Tests ──────────────────────────────────────────────────────
64
+ describe('parseGitHubRemote', () => {
65
+ beforeEach(() => vi.clearAllMocks());
66
+ it('parses HTTPS URL with .git suffix', () => {
67
+ expect(parseGitHubRemote('https://github.com/acme/widgets.git')).toEqual({
68
+ owner: 'acme',
69
+ repo: 'widgets',
70
+ });
71
+ });
72
+ it('parses SSH URL (git@host:owner/repo.git)', () => {
73
+ expect(parseGitHubRemote('git@github.com:acme/widgets.git')).toEqual({
74
+ owner: 'acme',
75
+ repo: 'widgets',
76
+ });
77
+ });
78
+ it('parses SSH protocol URL (ssh://git@github.com/owner/repo.git)', () => {
79
+ expect(parseGitHubRemote('ssh://git@github.com/acme/widgets.git')).toEqual({
80
+ owner: 'acme',
81
+ repo: 'widgets',
82
+ });
83
+ });
84
+ it('parses URL without .git suffix', () => {
85
+ expect(parseGitHubRemote('https://github.com/acme/widgets')).toEqual({
86
+ owner: 'acme',
87
+ repo: 'widgets',
88
+ });
89
+ });
90
+ it('throws on non-GitHub URL', () => {
91
+ expect(() => parseGitHubRemote('https://gitlab.com/acme/widgets.git')).toThrow('Not a GitHub remote URL');
92
+ });
93
+ });
94
+ describe('createIssue', () => {
95
+ beforeEach(() => vi.clearAllMocks());
96
+ const baseOpts = {
97
+ token: 'ghp_test123',
98
+ owner: 'acme',
99
+ repo: 'widgets',
100
+ title: 'Test Issue',
101
+ body: 'Issue body',
102
+ labels: ['ghagga-review'],
103
+ };
104
+ it('returns url and number on 201', async () => {
105
+ mockFetch.mockResolvedValueOnce(jsonResponse(201, { html_url: 'https://github.com/acme/widgets/issues/42', number: 42 }));
106
+ const result = await createIssue(baseOpts);
107
+ expect(result).toEqual({ url: 'https://github.com/acme/widgets/issues/42', number: 42 });
108
+ expect(mockFetch).toHaveBeenCalledWith('https://api.github.com/repos/acme/widgets/issues', expect.objectContaining({ method: 'POST' }));
109
+ });
110
+ it('throws GitHubApiError on 401', async () => {
111
+ mockFetch.mockResolvedValue(errorResponse(401, 'Bad credentials'));
112
+ await expect(createIssue(baseOpts)).rejects.toThrow(GitHubApiError);
113
+ await expect(createIssue(baseOpts)).rejects.toThrow(/Authentication failed/);
114
+ });
115
+ it('throws GitHubApiError on 410 (issues disabled)', async () => {
116
+ mockFetch.mockResolvedValue(errorResponse(410, 'Issues are disabled'));
117
+ await expect(createIssue(baseOpts)).rejects.toThrow(GitHubApiError);
118
+ await expect(createIssue(baseOpts)).rejects.toThrow(/Issues are disabled/);
119
+ });
120
+ it('throws GitHubApiError on 429 (rate limit)', async () => {
121
+ mockFetch.mockResolvedValue(errorResponse(429, 'Rate limit'));
122
+ await expect(createIssue(baseOpts)).rejects.toThrow(GitHubApiError);
123
+ await expect(createIssue(baseOpts)).rejects.toThrow(/rate limit/);
124
+ });
125
+ });
126
+ describe('createComment', () => {
127
+ beforeEach(() => vi.clearAllMocks());
128
+ const baseOpts = {
129
+ token: 'ghp_test123',
130
+ owner: 'acme',
131
+ repo: 'widgets',
132
+ issueNumber: 42,
133
+ body: 'Comment body',
134
+ };
135
+ it('returns url on 201', async () => {
136
+ mockFetch.mockResolvedValueOnce(jsonResponse(201, {
137
+ html_url: 'https://github.com/acme/widgets/issues/42#issuecomment-1',
138
+ }));
139
+ const result = await createComment(baseOpts);
140
+ expect(result).toEqual({
141
+ url: 'https://github.com/acme/widgets/issues/42#issuecomment-1',
142
+ });
143
+ expect(mockFetch).toHaveBeenCalledWith('https://api.github.com/repos/acme/widgets/issues/42/comments', expect.objectContaining({ method: 'POST' }));
144
+ });
145
+ it('throws GitHubApiError on 404 (issue not found)', async () => {
146
+ mockFetch.mockResolvedValue(errorResponse(404, 'Not Found'));
147
+ await expect(createComment(baseOpts)).rejects.toThrow(GitHubApiError);
148
+ await expect(createComment(baseOpts)).rejects.toThrow(/not found/);
149
+ });
150
+ });
151
+ describe('ensureLabel', () => {
152
+ beforeEach(() => vi.clearAllMocks());
153
+ const baseOpts = {
154
+ token: 'ghp_test123',
155
+ owner: 'acme',
156
+ repo: 'widgets',
157
+ name: 'ghagga-review',
158
+ color: '0ea5e9',
159
+ description: 'Automated review by GHAGGA',
160
+ };
161
+ it('succeeds on 201 (label created)', async () => {
162
+ mockFetch.mockResolvedValueOnce(jsonResponse(201, { name: 'ghagga-review' }));
163
+ await expect(ensureLabel(baseOpts)).resolves.toBeUndefined();
164
+ });
165
+ it('silently ignores 422 (label already exists)', async () => {
166
+ mockFetch.mockResolvedValueOnce(errorResponse(422, 'Validation Failed'));
167
+ await expect(ensureLabel(baseOpts)).resolves.toBeUndefined();
168
+ });
169
+ it('silently ignores 403 (insufficient permissions)', async () => {
170
+ mockFetch.mockResolvedValueOnce(errorResponse(403, 'Forbidden'));
171
+ await expect(ensureLabel(baseOpts)).resolves.toBeUndefined();
172
+ });
173
+ });
174
+ describe('formatIssueBody', () => {
175
+ it('contains <details> tag for collapsible section', () => {
176
+ const body = formatIssueBody(makeResult(), '2.5.0');
177
+ expect(body).toContain('<details>');
178
+ expect(body).toContain('</details>');
179
+ });
180
+ it('contains finding counts by severity', () => {
181
+ const body = formatIssueBody(makeResult(), '2.5.0');
182
+ expect(body).toContain('medium: 1');
183
+ expect(body).toContain('high: 1');
184
+ expect(body).toContain('2 total');
185
+ });
186
+ it('contains GHAGGA version in footer', () => {
187
+ const body = formatIssueBody(makeResult(), '2.5.0');
188
+ expect(body).toContain('v2.5.0');
189
+ expect(body).toContain('GHAGGA');
190
+ });
191
+ it('shows "none" when there are zero findings', () => {
192
+ const body = formatIssueBody(makeResult({ findings: [] }), '2.5.0');
193
+ expect(body).toContain('0 total');
194
+ expect(body).toContain('none');
195
+ });
196
+ });
197
+ //# sourceMappingURL=github-api.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-api.test.js","sourceRoot":"","sources":["../../src/lib/github-api.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * TUI extension tests — severity, box, divider, progress, setActiveSpinner.
3
+ *
4
+ * Tests the new TUI foundation methods in both plain and styled modes.
5
+ * Also tests colorSeverity() from the chalk adapter.
6
+ * Follows the same freshTui() + styledTui() patterns as tui.test.ts.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=tui-extensions.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui-extensions.test.d.ts","sourceRoot":"","sources":["../../../src/ui/__tests__/tui-extensions.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -0,0 +1,230 @@
1
+ /**
2
+ * TUI extension tests — severity, box, divider, progress, setActiveSpinner.
3
+ *
4
+ * Tests the new TUI foundation methods in both plain and styled modes.
5
+ * Also tests colorSeverity() from the chalk adapter.
6
+ * Follows the same freshTui() + styledTui() patterns as tui.test.ts.
7
+ */
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ // ─── Mock @clack/prompts ────────────────────────────────────────
10
+ const mockClackIntro = vi.hoisted(() => vi.fn());
11
+ const mockClackOutro = vi.hoisted(() => vi.fn());
12
+ const mockClackLog = vi.hoisted(() => ({
13
+ info: vi.fn(),
14
+ success: vi.fn(),
15
+ warn: vi.fn(),
16
+ error: vi.fn(),
17
+ step: vi.fn(),
18
+ message: vi.fn(),
19
+ }));
20
+ const mockClackSpinner = vi.hoisted(() => vi.fn());
21
+ vi.mock('@clack/prompts', () => ({
22
+ intro: mockClackIntro,
23
+ outro: mockClackOutro,
24
+ log: mockClackLog,
25
+ spinner: mockClackSpinner,
26
+ }));
27
+ // ─── Mock chalk ─────────────────────────────────────────────────
28
+ vi.mock('chalk', () => {
29
+ const mockChalk = {
30
+ red: (t) => `[RED]${t}[/RED]`,
31
+ hex: (_color) => (t) => `[HEX]${t}[/HEX]`,
32
+ yellow: (t) => `[YELLOW]${t}[/YELLOW]`,
33
+ blue: (t) => `[BLUE]${t}[/BLUE]`,
34
+ gray: (t) => `[GRAY]${t}[/GRAY]`,
35
+ };
36
+ return { default: mockChalk };
37
+ });
38
+ // ─── Tests ──────────────────────────────────────────────────────
39
+ describe('TUI extensions', () => {
40
+ let logSpy;
41
+ let errorSpy;
42
+ beforeEach(() => {
43
+ vi.clearAllMocks();
44
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
45
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
46
+ });
47
+ afterEach(() => {
48
+ logSpy.mockRestore();
49
+ errorSpy.mockRestore();
50
+ });
51
+ // Fresh module for each test to reset _plain and _activeSpinner state
52
+ async function freshTui() {
53
+ vi.resetModules();
54
+ return import('../tui.js');
55
+ }
56
+ // Styled mode: isTTY=true, no CI
57
+ async function styledTui() {
58
+ const originalTTY = process.stdout.isTTY;
59
+ const originalCI = process.env.CI;
60
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
61
+ delete process.env.CI;
62
+ const tui = await freshTui();
63
+ tui.init(); // should detect TTY, no CI → styled
64
+ // Restore immediately after init
65
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalTTY, configurable: true });
66
+ if (originalCI !== undefined)
67
+ process.env.CI = originalCI;
68
+ return tui;
69
+ }
70
+ // ─── severity() ─────────────────────────────────────────────
71
+ describe('severity() — plain mode', () => {
72
+ it('returns "[CRITICAL] msg" for critical level', async () => {
73
+ const tui = await freshTui();
74
+ tui.init({ plain: true });
75
+ expect(tui.severity('msg', 'critical')).toBe('[CRITICAL] msg');
76
+ });
77
+ it('returns "[HIGH] msg" for high level', async () => {
78
+ const tui = await freshTui();
79
+ tui.init({ plain: true });
80
+ expect(tui.severity('msg', 'high')).toBe('[HIGH] msg');
81
+ });
82
+ it('returns "[MEDIUM] msg" for medium level', async () => {
83
+ const tui = await freshTui();
84
+ tui.init({ plain: true });
85
+ expect(tui.severity('msg', 'medium')).toBe('[MEDIUM] msg');
86
+ });
87
+ it('returns "[LOW] msg" for low level', async () => {
88
+ const tui = await freshTui();
89
+ tui.init({ plain: true });
90
+ expect(tui.severity('msg', 'low')).toBe('[LOW] msg');
91
+ });
92
+ it('returns "[INFO] msg" for info level', async () => {
93
+ const tui = await freshTui();
94
+ tui.init({ plain: true });
95
+ expect(tui.severity('msg', 'info')).toBe('[INFO] msg');
96
+ });
97
+ it('returns empty string for empty text', async () => {
98
+ const tui = await freshTui();
99
+ tui.init({ plain: true });
100
+ expect(tui.severity('', 'critical')).toBe('');
101
+ });
102
+ });
103
+ describe('severity() — styled mode', () => {
104
+ it('returns a colored string (not plain [LEVEL] format)', async () => {
105
+ const tui = await styledTui();
106
+ const result = tui.severity('msg', 'critical');
107
+ // Should be a non-empty string
108
+ expect(result).toBeTruthy();
109
+ expect(typeof result).toBe('string');
110
+ // Must NOT be the plain format
111
+ expect(result).not.toBe('[CRITICAL] msg');
112
+ });
113
+ });
114
+ // ─── box() ──────────────────────────────────────────────────
115
+ describe('box() — plain mode', () => {
116
+ it('renders plain box with title and content lines', async () => {
117
+ const tui = await freshTui();
118
+ tui.init({ plain: true });
119
+ const result = tui.box('Title', ['line1', 'line2']);
120
+ expect(result).toBe('--- Title ---\nline1\nline2\n---');
121
+ });
122
+ it('renders plain box with empty content', async () => {
123
+ const tui = await freshTui();
124
+ tui.init({ plain: true });
125
+ const result = tui.box('Title', []);
126
+ expect(result).toBe('--- Title ---\n---');
127
+ });
128
+ });
129
+ describe('box() — styled mode', () => {
130
+ it('renders Unicode box-drawing characters', async () => {
131
+ const tui = await styledTui();
132
+ const result = tui.box('Title', ['line1', 'line2']);
133
+ expect(result).toContain('┌');
134
+ expect(result).toContain('┐');
135
+ expect(result).toContain('│');
136
+ expect(result).toContain('└');
137
+ expect(result).toContain('┘');
138
+ });
139
+ });
140
+ // ─── divider() ──────────────────────────────────────────────
141
+ describe('divider() — plain mode', () => {
142
+ it('renders "--- label ---" with a label', async () => {
143
+ const tui = await freshTui();
144
+ tui.init({ plain: true });
145
+ expect(tui.divider('label')).toBe('--- label ---');
146
+ });
147
+ it('renders "---" without a label', async () => {
148
+ const tui = await freshTui();
149
+ tui.init({ plain: true });
150
+ expect(tui.divider()).toBe('---');
151
+ });
152
+ });
153
+ describe('divider() — styled mode', () => {
154
+ it('renders ─ chars and the label text when given a label', async () => {
155
+ const tui = await styledTui();
156
+ const result = tui.divider('label');
157
+ expect(result).toContain('─');
158
+ expect(result).toContain('label');
159
+ });
160
+ it('renders 60 ─ chars without a label', async () => {
161
+ const tui = await styledTui();
162
+ const result = tui.divider();
163
+ expect(result).toBe('─'.repeat(60));
164
+ });
165
+ });
166
+ // ─── progress() ─────────────────────────────────────────────
167
+ describe('progress() — plain mode', () => {
168
+ it('calls console.log with "[current/total] label"', async () => {
169
+ const tui = await freshTui();
170
+ tui.init({ plain: true });
171
+ tui.progress(3, 7, 'Running...');
172
+ expect(logSpy).toHaveBeenCalledWith('[3/7] Running...');
173
+ });
174
+ });
175
+ describe('progress() — styled mode', () => {
176
+ it('updates active spinner message when spinner is set', async () => {
177
+ const mockMessage = vi.fn();
178
+ const mockSpinner = {
179
+ start: vi.fn(),
180
+ message: mockMessage,
181
+ stop: vi.fn(),
182
+ };
183
+ // Set up clack spinner mock so setActiveSpinner works
184
+ mockClackSpinner.mockReturnValue({
185
+ start: vi.fn(),
186
+ message: vi.fn(),
187
+ stop: vi.fn(),
188
+ });
189
+ const tui = await styledTui();
190
+ tui.setActiveSpinner(mockSpinner);
191
+ tui.progress(3, 7, 'Running...');
192
+ expect(mockMessage).toHaveBeenCalledWith('[3/7] Running...');
193
+ expect(logSpy).not.toHaveBeenCalled();
194
+ });
195
+ it('falls back to console.log when no active spinner', async () => {
196
+ mockClackSpinner.mockReturnValue({
197
+ start: vi.fn(),
198
+ message: vi.fn(),
199
+ stop: vi.fn(),
200
+ });
201
+ const tui = await styledTui();
202
+ // No setActiveSpinner call — _activeSpinner is null
203
+ tui.progress(3, 7, 'Running...');
204
+ expect(logSpy).toHaveBeenCalledWith('[3/7] Running...');
205
+ });
206
+ });
207
+ // ─── colorSeverity() from chalk.ts ──────────────────────────
208
+ describe('colorSeverity()', () => {
209
+ it('returns a non-empty string for each severity level', async () => {
210
+ vi.resetModules();
211
+ const { colorSeverity } = await import('../chalk.js');
212
+ const levels = ['critical', 'high', 'medium', 'low', 'info'];
213
+ for (const level of levels) {
214
+ const result = colorSeverity('test', level);
215
+ expect(result).toBeTruthy();
216
+ expect(typeof result).toBe('string');
217
+ }
218
+ });
219
+ it('produces distinct output for each severity level', async () => {
220
+ vi.resetModules();
221
+ const { colorSeverity } = await import('../chalk.js');
222
+ const levels = ['critical', 'high', 'medium', 'low', 'info'];
223
+ const results = levels.map((level) => colorSeverity('test', level));
224
+ // All 5 should be unique (distinct colors)
225
+ const unique = new Set(results);
226
+ expect(unique.size).toBe(5);
227
+ });
228
+ });
229
+ });
230
+ //# sourceMappingURL=tui-extensions.test.js.map