tlc-claude-code 1.2.27 → 1.2.29

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 (179) hide show
  1. package/README.md +9 -4
  2. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  3. package/dashboard/dist/components/ActivityFeed.js +42 -0
  4. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  5. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  6. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  7. package/dashboard/dist/components/BranchSelector.js +49 -0
  8. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  9. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  10. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  11. package/dashboard/dist/components/CommandPalette.js +118 -0
  12. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  13. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  14. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  15. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  17. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  18. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  19. package/dashboard/dist/components/DeviceFrame.js +52 -0
  20. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  21. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  22. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  23. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  25. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  26. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  27. package/dashboard/dist/components/FocusIndicator.js +47 -0
  28. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  29. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  30. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  31. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  33. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  34. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  35. package/dashboard/dist/components/LogSearch.js +43 -0
  36. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  37. package/dashboard/dist/components/LogSearch.test.js +100 -0
  38. package/dashboard/dist/components/LogStream.d.ts +21 -0
  39. package/dashboard/dist/components/LogStream.js +123 -0
  40. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  41. package/dashboard/dist/components/LogStream.test.js +159 -0
  42. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  43. package/dashboard/dist/components/PreviewPanel.js +73 -0
  44. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  45. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  46. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  47. package/dashboard/dist/components/ProjectCard.js +19 -0
  48. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  49. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  50. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  51. package/dashboard/dist/components/ProjectDetail.js +65 -0
  52. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  53. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  54. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  55. package/dashboard/dist/components/ProjectList.js +62 -0
  56. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  57. package/dashboard/dist/components/ProjectList.test.js +93 -0
  58. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  59. package/dashboard/dist/components/SettingsPanel.js +154 -0
  60. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  61. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  62. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  63. package/dashboard/dist/components/StatusBar.js +47 -0
  64. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  65. package/dashboard/dist/components/StatusBar.test.js +123 -0
  66. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  67. package/dashboard/dist/components/TaskBoard.js +102 -0
  68. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  69. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  70. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  71. package/dashboard/dist/components/TaskCard.js +29 -0
  72. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  73. package/dashboard/dist/components/TaskCard.test.js +109 -0
  74. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  75. package/dashboard/dist/components/TaskDetail.js +41 -0
  76. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  77. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  78. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  79. package/dashboard/dist/components/TaskFilter.js +138 -0
  80. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  81. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  82. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  83. package/dashboard/dist/components/TeamPanel.js +24 -0
  84. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  85. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  86. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  87. package/dashboard/dist/components/TeamPresence.js +31 -0
  88. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  89. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  90. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  91. package/dashboard/dist/components/layout/Header.js +11 -0
  92. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  93. package/dashboard/dist/components/layout/Header.test.js +35 -0
  94. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  95. package/dashboard/dist/components/layout/Shell.js +5 -0
  96. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  97. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  98. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  99. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  100. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  101. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  102. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  103. package/dashboard/dist/components/ui/Badge.js +13 -0
  104. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  105. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  106. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  107. package/dashboard/dist/components/ui/Button.js +14 -0
  108. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  109. package/dashboard/dist/components/ui/Button.test.js +81 -0
  110. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  111. package/dashboard/dist/components/ui/Card.js +20 -0
  112. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  113. package/dashboard/dist/components/ui/Card.test.js +82 -0
  114. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  115. package/dashboard/dist/components/ui/Input.js +8 -0
  116. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  117. package/dashboard/dist/components/ui/Input.test.js +68 -0
  118. package/dashboard/dist/styles/tokens.d.ts +150 -0
  119. package/dashboard/dist/styles/tokens.js +184 -0
  120. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  121. package/dashboard/dist/styles/tokens.test.js +95 -0
  122. package/dashboard/dist/test/setup.d.ts +1 -0
  123. package/dashboard/dist/test/setup.js +1 -0
  124. package/dashboard/package.json +3 -0
  125. package/package.json +15 -4
  126. package/scripts/capture-screenshots.js +170 -0
  127. package/scripts/docs-update.js +253 -0
  128. package/scripts/generate-screenshots.js +321 -0
  129. package/scripts/project-docs.js +377 -0
  130. package/scripts/vps-setup.sh +477 -0
  131. package/server/lib/adapters/base-adapter.js +114 -0
  132. package/server/lib/adapters/base-adapter.test.js +90 -0
  133. package/server/lib/adapters/claude-adapter.js +141 -0
  134. package/server/lib/adapters/claude-adapter.test.js +180 -0
  135. package/server/lib/adapters/deepseek-adapter.js +153 -0
  136. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  137. package/server/lib/adapters/openai-adapter.js +190 -0
  138. package/server/lib/adapters/openai-adapter.test.js +231 -0
  139. package/server/lib/budget-tracker.js +169 -0
  140. package/server/lib/budget-tracker.test.js +165 -0
  141. package/server/lib/claude-injector.js +85 -0
  142. package/server/lib/claude-injector.test.js +161 -0
  143. package/server/lib/consensus-engine.js +135 -0
  144. package/server/lib/consensus-engine.test.js +152 -0
  145. package/server/lib/context-builder.js +112 -0
  146. package/server/lib/context-builder.test.js +120 -0
  147. package/server/lib/file-collector.js +322 -0
  148. package/server/lib/file-collector.test.js +307 -0
  149. package/server/lib/memory-classifier.js +175 -0
  150. package/server/lib/memory-classifier.test.js +169 -0
  151. package/server/lib/memory-committer.js +138 -0
  152. package/server/lib/memory-committer.test.js +136 -0
  153. package/server/lib/memory-hooks.js +127 -0
  154. package/server/lib/memory-hooks.test.js +136 -0
  155. package/server/lib/memory-init.js +104 -0
  156. package/server/lib/memory-init.test.js +119 -0
  157. package/server/lib/memory-observer.js +149 -0
  158. package/server/lib/memory-observer.test.js +158 -0
  159. package/server/lib/memory-reader.js +243 -0
  160. package/server/lib/memory-reader.test.js +216 -0
  161. package/server/lib/memory-storage.js +120 -0
  162. package/server/lib/memory-storage.test.js +136 -0
  163. package/server/lib/memory-writer.js +176 -0
  164. package/server/lib/memory-writer.test.js +231 -0
  165. package/server/lib/overdrive-command.js +30 -6
  166. package/server/lib/overdrive-command.test.js +8 -1
  167. package/server/lib/pattern-detector.js +216 -0
  168. package/server/lib/pattern-detector.test.js +241 -0
  169. package/server/lib/relevance-scorer.js +175 -0
  170. package/server/lib/relevance-scorer.test.js +107 -0
  171. package/server/lib/review-command.js +238 -0
  172. package/server/lib/review-command.test.js +245 -0
  173. package/server/lib/review-orchestrator.js +273 -0
  174. package/server/lib/review-orchestrator.test.js +300 -0
  175. package/server/lib/review-reporter.js +288 -0
  176. package/server/lib/review-reporter.test.js +240 -0
  177. package/server/lib/session-summary.js +90 -0
  178. package/server/lib/session-summary.test.js +156 -0
  179. package/templates/docs-sync.yml +91 -0
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Review Command - /tlc:review implementation
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { ReviewOrchestrator } = require('./review-orchestrator.js');
8
+ const { generateReport } = require('./review-reporter.js');
9
+ const { ClaudeAdapter } = require('./adapters/claude-adapter.js');
10
+ const { OpenAIAdapter } = require('./adapters/openai-adapter.js');
11
+ const { DeepSeekAdapter } = require('./adapters/deepseek-adapter.js');
12
+ const { BudgetTracker } = require('./budget-tracker.js');
13
+
14
+ /**
15
+ * Parse command arguments
16
+ * @param {string} args - Command arguments
17
+ * @returns {Object} Parsed options
18
+ */
19
+ function parseArgs(args) {
20
+ const options = {
21
+ file: null,
22
+ dir: null,
23
+ format: 'md',
24
+ output: null,
25
+ models: ['claude', 'openai', 'deepseek'],
26
+ extensions: [],
27
+ consensusType: 'majority',
28
+ verbose: false,
29
+ };
30
+
31
+ if (!args) return options;
32
+
33
+ const parts = args.split(/\s+/);
34
+ let i = 0;
35
+
36
+ while (i < parts.length) {
37
+ const part = parts[i];
38
+
39
+ if (part === '--file' || part === '-f') {
40
+ options.file = parts[++i];
41
+ } else if (part === '--dir' || part === '-d') {
42
+ options.dir = parts[++i];
43
+ } else if (part === '--format') {
44
+ options.format = parts[++i];
45
+ } else if (part === '--output' || part === '-o') {
46
+ options.output = parts[++i];
47
+ } else if (part === '--models' || part === '-m') {
48
+ options.models = parts[++i].split(',').map(m => m.trim().toLowerCase());
49
+ } else if (part === '--ext' || part === '--extensions') {
50
+ options.extensions = parts[++i].split(',').map(e => e.trim());
51
+ } else if (part === '--consensus') {
52
+ options.consensusType = parts[++i];
53
+ } else if (part === '--verbose' || part === '-v') {
54
+ options.verbose = true;
55
+ } else if (!part.startsWith('-') && !options.file && !options.dir) {
56
+ // Positional argument - treat as file or dir
57
+ if (fs.existsSync(part)) {
58
+ const stat = fs.statSync(part);
59
+ if (stat.isDirectory()) {
60
+ options.dir = part;
61
+ } else {
62
+ options.file = part;
63
+ }
64
+ } else {
65
+ options.file = part; // Let it fail later with proper error
66
+ }
67
+ }
68
+
69
+ i++;
70
+ }
71
+
72
+ return options;
73
+ }
74
+
75
+ /**
76
+ * Create adapters based on model selection
77
+ * @param {string[]} models - Model names to use
78
+ * @param {Object} config - Configuration (budgets, etc.)
79
+ * @returns {Array} Adapter instances
80
+ */
81
+ function createAdapters(models, config = {}) {
82
+ const budgetTracker = config.budgetTracker || null;
83
+ const adapters = [];
84
+
85
+ for (const model of models) {
86
+ switch (model.toLowerCase()) {
87
+ case 'claude':
88
+ adapters.push(new ClaudeAdapter({
89
+ budgetTracker,
90
+ budget: config.claudeBudget,
91
+ trackCost: config.trackCost !== false,
92
+ }));
93
+ break;
94
+ case 'openai':
95
+ adapters.push(new OpenAIAdapter({
96
+ budgetTracker,
97
+ budget: config.openaiBudget,
98
+ }));
99
+ break;
100
+ case 'deepseek':
101
+ adapters.push(new DeepSeekAdapter({
102
+ budgetTracker,
103
+ budget: config.deepseekBudget,
104
+ }));
105
+ break;
106
+ default:
107
+ console.warn(`Unknown model: ${model}`);
108
+ }
109
+ }
110
+
111
+ return adapters;
112
+ }
113
+
114
+ /**
115
+ * Execute review command
116
+ * @param {string} args - Command arguments
117
+ * @param {Object} context - Execution context
118
+ * @returns {Promise<Object>} Command result
119
+ */
120
+ async function executeReview(args, context = {}) {
121
+ const options = parseArgs(args);
122
+
123
+ // Validate target
124
+ if (!options.file && !options.dir) {
125
+ return {
126
+ success: false,
127
+ error: 'No target specified. Use --file <path> or --dir <path>',
128
+ };
129
+ }
130
+
131
+ // Create adapters
132
+ const adapters = createAdapters(options.models, context);
133
+
134
+ if (adapters.length === 0) {
135
+ return {
136
+ success: false,
137
+ error: 'No valid models specified',
138
+ };
139
+ }
140
+
141
+ // Create orchestrator
142
+ const orchestrator = new ReviewOrchestrator(adapters, {
143
+ consensusType: options.consensusType,
144
+ requireMinimum: 1,
145
+ budgetAware: true,
146
+ });
147
+
148
+ // Show available models
149
+ const availableModels = orchestrator.getAvailableModels();
150
+ if (options.verbose) {
151
+ console.log(`Available models: ${availableModels.join(', ')}`);
152
+ }
153
+
154
+ // Run review
155
+ let results;
156
+ try {
157
+ if (options.file) {
158
+ results = await orchestrator.reviewFile(options.file);
159
+ // Wrap single file result in summary format
160
+ results = orchestrator.summarizeResults(
161
+ [results],
162
+ results.models || [],
163
+ results.costs || { byModel: {}, total: 0 }
164
+ );
165
+ } else {
166
+ results = await orchestrator.reviewDirectory(options.dir, {
167
+ extensions: options.extensions,
168
+ });
169
+ }
170
+ } catch (err) {
171
+ return {
172
+ success: false,
173
+ error: err.message,
174
+ };
175
+ }
176
+
177
+ // Generate report
178
+ const report = generateReport(results, options.format);
179
+
180
+ // Write to file or return
181
+ if (options.output) {
182
+ try {
183
+ fs.writeFileSync(options.output, report, 'utf-8');
184
+ return {
185
+ success: true,
186
+ message: `Report saved to ${options.output}`,
187
+ results,
188
+ outputPath: options.output,
189
+ };
190
+ } catch (err) {
191
+ return {
192
+ success: false,
193
+ error: `Failed to write report: ${err.message}`,
194
+ };
195
+ }
196
+ }
197
+
198
+ return {
199
+ success: true,
200
+ report,
201
+ results,
202
+ summary: {
203
+ files: results.files?.length || 0,
204
+ issues: results.totalIssues || 0,
205
+ cost: results.totalCost || 0,
206
+ models: results.models || [],
207
+ },
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Format summary for display
213
+ * @param {Object} summary - Summary object
214
+ * @returns {string} Formatted summary
215
+ */
216
+ function formatSummary(summary) {
217
+ const lines = [
218
+ '',
219
+ '═══════════════════════════════════════════════════════════════',
220
+ ' Review Summary ',
221
+ '═══════════════════════════════════════════════════════════════',
222
+ '',
223
+ ` Files reviewed: ${summary.files}`,
224
+ ` Issues found: ${summary.issues}`,
225
+ ` Models used: ${summary.models.join(', ')}`,
226
+ ` Total cost: $${summary.cost.toFixed(4)}`,
227
+ '',
228
+ '═══════════════════════════════════════════════════════════════',
229
+ ];
230
+ return lines.join('\n');
231
+ }
232
+
233
+ module.exports = {
234
+ executeReview,
235
+ parseArgs,
236
+ createAdapters,
237
+ formatSummary,
238
+ };
@@ -0,0 +1,245 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { executeReview, parseArgs, createAdapters, formatSummary } from './review-command.js';
6
+
7
+ describe('Review Command', () => {
8
+ let testDir;
9
+
10
+ beforeEach(() => {
11
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-review-cmd-'));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(testDir, { recursive: true, force: true });
16
+ });
17
+
18
+ describe('parseArgs', () => {
19
+ it('parses --file option', () => {
20
+ const options = parseArgs('--file src/index.js');
21
+ expect(options.file).toBe('src/index.js');
22
+ });
23
+
24
+ it('parses -f shorthand', () => {
25
+ const options = parseArgs('-f src/index.js');
26
+ expect(options.file).toBe('src/index.js');
27
+ });
28
+
29
+ it('parses --dir option', () => {
30
+ const options = parseArgs('--dir src');
31
+ expect(options.dir).toBe('src');
32
+ });
33
+
34
+ it('parses -d shorthand', () => {
35
+ const options = parseArgs('-d src');
36
+ expect(options.dir).toBe('src');
37
+ });
38
+
39
+ it('parses --format option', () => {
40
+ const options = parseArgs('--format json');
41
+ expect(options.format).toBe('json');
42
+ });
43
+
44
+ it('parses --output option', () => {
45
+ const options = parseArgs('--output report.md');
46
+ expect(options.output).toBe('report.md');
47
+ });
48
+
49
+ it('parses --models option', () => {
50
+ const options = parseArgs('--models claude,openai');
51
+ expect(options.models).toEqual(['claude', 'openai']);
52
+ });
53
+
54
+ it('parses --ext option', () => {
55
+ const options = parseArgs('--ext .js,.ts');
56
+ expect(options.extensions).toEqual(['.js', '.ts']);
57
+ });
58
+
59
+ it('parses --consensus option', () => {
60
+ const options = parseArgs('--consensus unanimous');
61
+ expect(options.consensusType).toBe('unanimous');
62
+ });
63
+
64
+ it('parses --verbose option', () => {
65
+ const options = parseArgs('--verbose');
66
+ expect(options.verbose).toBe(true);
67
+ });
68
+
69
+ it('parses positional file argument', () => {
70
+ const filePath = path.join(testDir, 'test.js');
71
+ fs.writeFileSync(filePath, 'code');
72
+
73
+ const options = parseArgs(filePath);
74
+ expect(options.file).toBe(filePath);
75
+ });
76
+
77
+ it('parses positional directory argument', () => {
78
+ const options = parseArgs(testDir);
79
+ expect(options.dir).toBe(testDir);
80
+ });
81
+
82
+ it('returns defaults for empty args', () => {
83
+ const options = parseArgs('');
84
+ expect(options.format).toBe('md');
85
+ expect(options.models).toEqual(['claude', 'openai', 'deepseek']);
86
+ });
87
+
88
+ it('handles multiple options', () => {
89
+ const options = parseArgs('--file test.js --format json --models claude -v');
90
+ expect(options.file).toBe('test.js');
91
+ expect(options.format).toBe('json');
92
+ expect(options.models).toEqual(['claude']);
93
+ expect(options.verbose).toBe(true);
94
+ });
95
+ });
96
+
97
+ describe('createAdapters', () => {
98
+ it('creates claude adapter', () => {
99
+ const adapters = createAdapters(['claude']);
100
+ expect(adapters).toHaveLength(1);
101
+ expect(adapters[0].name).toBe('claude');
102
+ });
103
+
104
+ it('creates openai adapter', () => {
105
+ const adapters = createAdapters(['openai']);
106
+ expect(adapters).toHaveLength(1);
107
+ expect(adapters[0].name).toBe('openai');
108
+ });
109
+
110
+ it('creates deepseek adapter', () => {
111
+ const adapters = createAdapters(['deepseek']);
112
+ expect(adapters).toHaveLength(1);
113
+ expect(adapters[0].name).toBe('deepseek');
114
+ });
115
+
116
+ it('creates multiple adapters', () => {
117
+ const adapters = createAdapters(['claude', 'openai', 'deepseek']);
118
+ expect(adapters).toHaveLength(3);
119
+ });
120
+
121
+ it('skips unknown models', () => {
122
+ const adapters = createAdapters(['claude', 'unknown', 'openai']);
123
+ expect(adapters).toHaveLength(2);
124
+ });
125
+
126
+ it('is case insensitive', () => {
127
+ const adapters = createAdapters(['CLAUDE', 'OpenAI']);
128
+ expect(adapters).toHaveLength(2);
129
+ });
130
+ });
131
+
132
+ describe('executeReview', () => {
133
+ it('returns error when no target specified', async () => {
134
+ const result = await executeReview('');
135
+ expect(result.success).toBe(false);
136
+ expect(result.error).toContain('No target specified');
137
+ });
138
+
139
+ it('reviews single file', async () => {
140
+ const filePath = path.join(testDir, 'test.js');
141
+ fs.writeFileSync(filePath, 'const x = 1;');
142
+
143
+ const result = await executeReview(`--file ${filePath} --models claude`);
144
+ expect(result.success).toBe(true);
145
+ expect(result.summary.files).toBe(1);
146
+ });
147
+
148
+ it('reviews directory', async () => {
149
+ fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
150
+ fs.writeFileSync(path.join(testDir, 'b.js'), 'code');
151
+
152
+ const result = await executeReview(`--dir ${testDir} --models claude`);
153
+ expect(result.success).toBe(true);
154
+ expect(result.summary.files).toBe(2);
155
+ });
156
+
157
+ it('generates markdown report by default', async () => {
158
+ const filePath = path.join(testDir, 'test.js');
159
+ fs.writeFileSync(filePath, 'const x = 1;');
160
+
161
+ const result = await executeReview(`--file ${filePath} --models claude`);
162
+ expect(result.report).toContain('# Code Review Report');
163
+ });
164
+
165
+ it('generates JSON report when requested', async () => {
166
+ const filePath = path.join(testDir, 'test.js');
167
+ fs.writeFileSync(filePath, 'const x = 1;');
168
+
169
+ const result = await executeReview(`--file ${filePath} --format json --models claude`);
170
+ expect(() => JSON.parse(result.report)).not.toThrow();
171
+ });
172
+
173
+ it('generates HTML report when requested', async () => {
174
+ const filePath = path.join(testDir, 'test.js');
175
+ fs.writeFileSync(filePath, 'const x = 1;');
176
+
177
+ const result = await executeReview(`--file ${filePath} --format html --models claude`);
178
+ expect(result.report).toContain('<!DOCTYPE html>');
179
+ });
180
+
181
+ it('saves report to file when --output specified', async () => {
182
+ const filePath = path.join(testDir, 'test.js');
183
+ const outputPath = path.join(testDir, 'report.md');
184
+ fs.writeFileSync(filePath, 'const x = 1;');
185
+
186
+ const result = await executeReview(`--file ${filePath} --output ${outputPath} --models claude`);
187
+
188
+ expect(result.success).toBe(true);
189
+ expect(fs.existsSync(outputPath)).toBe(true);
190
+ });
191
+
192
+ it('returns summary with cost', async () => {
193
+ const filePath = path.join(testDir, 'test.js');
194
+ fs.writeFileSync(filePath, 'const x = 1;');
195
+
196
+ const result = await executeReview(`--file ${filePath} --models claude`);
197
+ expect(result.summary).toHaveProperty('cost');
198
+ expect(result.summary).toHaveProperty('models');
199
+ });
200
+
201
+ it('filters by extension', async () => {
202
+ fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
203
+ fs.writeFileSync(path.join(testDir, 'b.ts'), 'code');
204
+
205
+ const result = await executeReview(`--dir ${testDir} --ext .js --models claude`);
206
+ expect(result.summary.files).toBe(1);
207
+ });
208
+
209
+ it('uses specified models only', async () => {
210
+ const filePath = path.join(testDir, 'test.js');
211
+ fs.writeFileSync(filePath, 'const x = 1;');
212
+
213
+ const result = await executeReview(`--file ${filePath} --models openai`);
214
+ expect(result.summary.models).toContain('openai');
215
+ expect(result.summary.models).not.toContain('claude');
216
+ });
217
+
218
+ it('returns error for invalid models', async () => {
219
+ const filePath = path.join(testDir, 'test.js');
220
+ fs.writeFileSync(filePath, 'code');
221
+
222
+ const result = await executeReview(`--file ${filePath} --models unknown`);
223
+ expect(result.success).toBe(false);
224
+ expect(result.error).toContain('No valid models');
225
+ });
226
+ });
227
+
228
+ describe('formatSummary', () => {
229
+ it('formats summary for display', () => {
230
+ const summary = {
231
+ files: 5,
232
+ issues: 10,
233
+ models: ['claude', 'openai'],
234
+ cost: 0.0234,
235
+ };
236
+
237
+ const formatted = formatSummary(summary);
238
+
239
+ expect(formatted).toContain('Files reviewed: 5');
240
+ expect(formatted).toContain('Issues found: 10');
241
+ expect(formatted).toContain('claude, openai');
242
+ expect(formatted).toContain('$0.0234');
243
+ });
244
+ });
245
+ });