ucn 3.1.4 → 3.1.6

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.

Potentially problematic release.


This version of ucn might be problematic. Click here for more details.

package/cli/index.js CHANGED
@@ -960,7 +960,8 @@ function runProjectCommand(rootDir, command, arg) {
960
960
 
961
961
  case 'graph': {
962
962
  requireArg(arg, 'Usage: ucn . graph <file>');
963
- const graphResult = index.graph(arg, { direction: 'both', maxDepth: flags.depth ?? 5 });
963
+ const graphDepth = flags.depth ?? 2; // Default to 2 for cleaner output
964
+ const graphResult = index.graph(arg, { direction: 'both', maxDepth: graphDepth });
964
965
  if (graphResult.nodes.length === 0) {
965
966
  console.log(`File not found: ${arg}`);
966
967
  } else {
@@ -970,7 +971,7 @@ function runProjectCommand(rootDir, command, arg) {
970
971
  nodes: r.nodes.map(n => ({ file: n.relativePath, depth: n.depth })),
971
972
  edges: r.edges.map(e => ({ from: path.relative(index.root, e.from), to: path.relative(index.root, e.to) }))
972
973
  }, null, 2),
973
- r => { printGraph(r, index.root); }
974
+ r => { printGraph(r, index.root, graphDepth); }
974
975
  );
975
976
  }
976
977
  break;
@@ -1801,12 +1802,15 @@ function printStats(stats) {
1801
1802
  }
1802
1803
  }
1803
1804
 
1804
- function printGraph(graph, root) {
1805
+ function printGraph(graph, root, maxDepth = 2) {
1805
1806
  const rootRelPath = path.relative(root, graph.root);
1806
1807
  console.log(`Dependency graph for ${rootRelPath}`);
1807
1808
  console.log('═'.repeat(60));
1808
1809
 
1809
1810
  const printed = new Set();
1811
+ const maxChildren = 8; // Limit children per node
1812
+ let truncatedNodes = 0;
1813
+ let depthLimited = false;
1810
1814
 
1811
1815
  function printNode(file, indent = 0) {
1812
1816
  const fileEntry = graph.nodes.find(n => n.file === file);
@@ -1819,15 +1823,43 @@ function printGraph(graph, root) {
1819
1823
  }
1820
1824
  printed.add(file);
1821
1825
 
1826
+ // Depth limiting
1827
+ if (indent > maxDepth) {
1828
+ depthLimited = true;
1829
+ console.log(`${prefix}${relPath} ...`);
1830
+ return;
1831
+ }
1832
+
1822
1833
  console.log(`${prefix}${relPath}`);
1823
1834
 
1824
1835
  const edges = graph.edges.filter(e => e.from === file);
1825
- for (const edge of edges) {
1836
+
1837
+ // Limit children
1838
+ const displayEdges = edges.slice(0, maxChildren);
1839
+ const hiddenCount = edges.length - displayEdges.length;
1840
+
1841
+ for (const edge of displayEdges) {
1826
1842
  printNode(edge.to, indent + 1);
1827
1843
  }
1844
+
1845
+ if (hiddenCount > 0) {
1846
+ truncatedNodes += hiddenCount;
1847
+ console.log(`${' '.repeat(indent)}└── ... and ${hiddenCount} more`);
1848
+ }
1828
1849
  }
1829
1850
 
1830
1851
  printNode(graph.root);
1852
+
1853
+ // Print helpful note about expanding
1854
+ if (depthLimited || truncatedNodes > 0) {
1855
+ console.log('\n' + '─'.repeat(60));
1856
+ if (depthLimited) {
1857
+ console.log(`Depth limited to ${maxDepth}. Use --depth=N for deeper graph.`);
1858
+ }
1859
+ if (truncatedNodes > 0) {
1860
+ console.log(`${truncatedNodes} nodes hidden. Graph has ${graph.nodes.length} total files.`);
1861
+ }
1862
+ }
1831
1863
  }
1832
1864
 
1833
1865
  function printSearchResults(results, term) {
package/core/discovery.js CHANGED
@@ -332,31 +332,51 @@ function findProjectRoot(startDir) {
332
332
 
333
333
  /**
334
334
  * Auto-detect the glob pattern for a project based on its type
335
+ * Checks both project root and immediate subdirectories for config files
335
336
  */
336
337
  function detectProjectPattern(projectRoot) {
337
338
  const extensions = [];
338
339
 
339
- if (fs.existsSync(path.join(projectRoot, 'package.json'))) {
340
- extensions.push('js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs');
341
- }
340
+ // Helper to check for config files in a directory
341
+ const checkDir = (dir) => {
342
+ if (fs.existsSync(path.join(dir, 'package.json'))) {
343
+ extensions.push('js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs');
344
+ }
342
345
 
343
- if (fs.existsSync(path.join(projectRoot, 'pyproject.toml')) ||
344
- fs.existsSync(path.join(projectRoot, 'setup.py')) ||
345
- fs.existsSync(path.join(projectRoot, 'requirements.txt'))) {
346
- extensions.push('py');
347
- }
346
+ if (fs.existsSync(path.join(dir, 'pyproject.toml')) ||
347
+ fs.existsSync(path.join(dir, 'setup.py')) ||
348
+ fs.existsSync(path.join(dir, 'requirements.txt'))) {
349
+ extensions.push('py');
350
+ }
348
351
 
349
- if (fs.existsSync(path.join(projectRoot, 'go.mod'))) {
350
- extensions.push('go');
351
- }
352
+ if (fs.existsSync(path.join(dir, 'go.mod'))) {
353
+ extensions.push('go');
354
+ }
352
355
 
353
- if (fs.existsSync(path.join(projectRoot, 'Cargo.toml'))) {
354
- extensions.push('rs');
355
- }
356
+ if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
357
+ extensions.push('rs');
358
+ }
356
359
 
357
- if (fs.existsSync(path.join(projectRoot, 'pom.xml')) ||
358
- fs.existsSync(path.join(projectRoot, 'build.gradle'))) {
359
- extensions.push('java', 'kt');
360
+ if (fs.existsSync(path.join(dir, 'pom.xml')) ||
361
+ fs.existsSync(path.join(dir, 'build.gradle'))) {
362
+ extensions.push('java', 'kt');
363
+ }
364
+ };
365
+
366
+ // Check project root
367
+ checkDir(projectRoot);
368
+
369
+ // Also check immediate subdirectories for multi-language projects (e.g., web/, frontend/, server/)
370
+ try {
371
+ const entries = fs.readdirSync(projectRoot, { withFileTypes: true });
372
+ for (const entry of entries) {
373
+ if (entry.isDirectory() && !entry.name.startsWith('.') &&
374
+ !EXCLUDED_DIRS.has(entry.name)) {
375
+ checkDir(path.join(projectRoot, entry.name));
376
+ }
377
+ }
378
+ } catch (e) {
379
+ // Ignore errors reading directory
360
380
  }
361
381
 
362
382
  if (extensions.length > 0) {
@@ -812,6 +812,50 @@ function findCallsInCode(code, parser) {
812
812
  return true;
813
813
  }
814
814
 
815
+ // Handle JSX component usage: <Component /> or <Component>...</Component>
816
+ // Only track PascalCase names (React components), not lowercase (HTML elements)
817
+ if (node.type === 'jsx_self_closing_element' || node.type === 'jsx_opening_element') {
818
+ // First named child is the element name
819
+ for (let i = 0; i < node.namedChildCount; i++) {
820
+ const child = node.namedChild(i);
821
+ if (child.type === 'identifier') {
822
+ const name = child.text;
823
+ // React components start with uppercase
824
+ if (name && /^[A-Z]/.test(name)) {
825
+ const enclosingFunction = getCurrentEnclosingFunction();
826
+ calls.push({
827
+ name: name,
828
+ line: node.startPosition.row + 1,
829
+ isMethod: false,
830
+ isJsxComponent: true,
831
+ enclosingFunction
832
+ });
833
+ }
834
+ break;
835
+ }
836
+ // Handle namespaced components: <Foo.Bar />
837
+ if (child.type === 'member_expression' || child.type === 'nested_identifier') {
838
+ const text = child.text;
839
+ // Get the last part after the dot
840
+ const parts = text.split('.');
841
+ const componentName = parts[parts.length - 1];
842
+ if (componentName && /^[A-Z]/.test(componentName)) {
843
+ const enclosingFunction = getCurrentEnclosingFunction();
844
+ calls.push({
845
+ name: componentName,
846
+ line: node.startPosition.row + 1,
847
+ isMethod: true,
848
+ receiver: parts.slice(0, -1).join('.'),
849
+ isJsxComponent: true,
850
+ enclosingFunction
851
+ });
852
+ }
853
+ break;
854
+ }
855
+ }
856
+ return true;
857
+ }
858
+
815
859
  return true;
816
860
  }, {
817
861
  onLeave: (node) => {
@@ -1338,6 +1382,10 @@ function findUsagesInCode(code, name, parser) {
1338
1382
  usageType = 'reference';
1339
1383
  }
1340
1384
  }
1385
+ // JSX component usage: <Component /> or <Component>...</Component>
1386
+ else if (parent.type === 'jsx_self_closing_element' || parent.type === 'jsx_opening_element') {
1387
+ usageType = 'call'; // Treat JSX component usage as a "call"
1388
+ }
1341
1389
  }
1342
1390
 
1343
1391
  usages.push({ line, column, usageType });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.1.4",
3
+ "version": "3.1.6",
4
4
  "description": "Code navigation built by AI, for AI. Reduces context usage when working with large codebases.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -4547,6 +4547,38 @@ func (s *ServiceB) helper() {}
4547
4547
  }
4548
4548
  });
4549
4549
 
4550
+ it('should detect JSX component usage as calls', () => {
4551
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-jsx-'));
4552
+ try {
4553
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), `{"name": "test"}`);
4554
+
4555
+ fs.writeFileSync(path.join(tmpDir, 'Page.tsx'), `
4556
+ function EnvironmentsPage() {
4557
+ return <div>Hello</div>;
4558
+ }
4559
+
4560
+ function App() {
4561
+ return <EnvironmentsPage />;
4562
+ }
4563
+
4564
+ export { App, EnvironmentsPage };
4565
+ `);
4566
+
4567
+ const index = new ProjectIndex(tmpDir);
4568
+ index.build('**/*.tsx', { quiet: true });
4569
+
4570
+ const usages = index.usages('EnvironmentsPage', { codeOnly: true });
4571
+ const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
4572
+
4573
+ // Should find JSX usage as a call
4574
+ assert.ok(calls.length >= 1, 'Should find at least 1 JSX component usage');
4575
+ assert.ok(calls.some(c => c.content && c.content.includes('<EnvironmentsPage')),
4576
+ 'Should detect <EnvironmentsPage /> as a call');
4577
+ } finally {
4578
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4579
+ }
4580
+ });
4581
+
4550
4582
  it('should detect Rust method calls in usages', () => {
4551
4583
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-rust-method-'));
4552
4584
  try {
@@ -1,477 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Public Repository Test Script for UCN
5
- *
6
- * Tests UCN against real public repositories to find edge cases and bugs.
7
- * Run with: node test/public-repos-test.js [--verbose] [--keep]
8
- */
9
-
10
- const { execSync, spawnSync } = require('child_process');
11
- const path = require('path');
12
- const fs = require('fs');
13
- const os = require('os');
14
-
15
- // Configuration
16
- const UCN_PATH = path.join(__dirname, '..', 'ucn.js');
17
- const TEMP_DIR = path.join(os.tmpdir(), 'ucn-test-repos');
18
- const TIMEOUT = 60000; // 60 seconds
19
-
20
- // Parse args
21
- const args = process.argv.slice(2);
22
- const verbose = args.includes('--verbose') || args.includes('-v');
23
- const keepRepos = args.includes('--keep');
24
-
25
- // Colors
26
- const c = {
27
- reset: '\x1b[0m',
28
- red: '\x1b[31m',
29
- green: '\x1b[32m',
30
- yellow: '\x1b[33m',
31
- blue: '\x1b[34m',
32
- cyan: '\x1b[36m',
33
- dim: '\x1b[2m',
34
- };
35
-
36
- // Results tracking
37
- const results = {
38
- passed: 0,
39
- failed: 0,
40
- bugs: [],
41
- };
42
-
43
- // Public repositories to test - small, well-known repos
44
- const repos = {
45
- javascript: [
46
- {
47
- name: 'preact-signals',
48
- url: 'https://github.com/preactjs/signals',
49
- dir: 'packages/core/src',
50
- symbols: ['signal', 'computed', 'effect', 'batch'],
51
- },
52
- ],
53
- typescript: [
54
- {
55
- name: 'zod',
56
- url: 'https://github.com/colinhacks/zod',
57
- dir: 'src',
58
- symbols: ['ZodType', 'string', 'parse', 'safeParse'],
59
- },
60
- ],
61
- python: [
62
- {
63
- name: 'httpx',
64
- url: 'https://github.com/encode/httpx',
65
- dir: 'httpx',
66
- symbols: ['Client', 'get', 'post', 'request'],
67
- },
68
- ],
69
- go: [
70
- {
71
- name: 'cobra',
72
- url: 'https://github.com/spf13/cobra',
73
- dir: '.',
74
- symbols: ['Command', 'Execute', 'AddCommand', 'Flags'],
75
- },
76
- ],
77
- rust: [
78
- {
79
- name: 'ripgrep',
80
- url: 'https://github.com/BurntSushi/ripgrep',
81
- dir: 'crates/core',
82
- symbols: ['Searcher', 'search', 'new', 'build'],
83
- },
84
- ],
85
- java: [
86
- {
87
- name: 'gson',
88
- url: 'https://github.com/google/gson',
89
- dir: 'gson/src/main/java/com/google/gson',
90
- symbols: ['Gson', 'toJson', 'fromJson', 'JsonElement'],
91
- },
92
- ],
93
- };
94
-
95
- // Commands to test
96
- const commands = [
97
- { name: 'toc', args: [] },
98
- { name: 'stats', args: [] },
99
- { name: 'find', args: ['$SYM'] },
100
- { name: 'usages', args: ['$SYM'] },
101
- { name: 'context', args: ['$SYM'] },
102
- { name: 'about', args: ['$SYM'] },
103
- { name: 'smart', args: ['$SYM'] },
104
- { name: 'impact', args: ['$SYM'] },
105
- { name: 'trace', args: ['$SYM', '--depth=2'] },
106
- { name: 'api', args: [] },
107
- { name: 'deadcode', args: [] },
108
- { name: 'find', args: ['$SYM', '--json'], id: 'find-json' },
109
- { name: 'toc', args: ['--json'], id: 'toc-json' },
110
- { name: 'search', args: ['TODO'] },
111
- { name: 'fn', args: ['$SYM'] },
112
- ];
113
-
114
- /**
115
- * Clone a repository
116
- */
117
- function cloneRepo(url, name) {
118
- const repoPath = path.join(TEMP_DIR, name);
119
-
120
- if (fs.existsSync(repoPath)) {
121
- if (verbose) console.log(`${c.dim} Using cached: ${name}${c.reset}`);
122
- return repoPath;
123
- }
124
-
125
- console.log(`${c.dim} Cloning: ${name}...${c.reset}`);
126
- try {
127
- execSync(`git clone --depth 1 ${url} ${repoPath}`, {
128
- stdio: verbose ? 'inherit' : 'pipe',
129
- timeout: 120000,
130
- });
131
- return repoPath;
132
- } catch (e) {
133
- console.log(`${c.yellow} ⚠ Failed to clone ${name}: ${e.message}${c.reset}`);
134
- return null;
135
- }
136
- }
137
-
138
- /**
139
- * Run UCN command
140
- */
141
- function runUcn(targetPath, command, args = []) {
142
- const fullArgs = ['node', UCN_PATH, targetPath, command, ...args, '--no-cache'];
143
- const cmdStr = fullArgs.join(' ');
144
-
145
- if (verbose) {
146
- console.log(`${c.dim} Running: ${cmdStr}${c.reset}`);
147
- }
148
-
149
- try {
150
- const result = spawnSync('node', [UCN_PATH, targetPath, command, ...args, '--no-cache'], {
151
- timeout: TIMEOUT,
152
- encoding: 'utf8',
153
- maxBuffer: 50 * 1024 * 1024, // 50MB buffer
154
- });
155
-
156
- if (result.error) {
157
- return { success: false, error: result.error.message, command: cmdStr };
158
- }
159
-
160
- if (result.status !== 0) {
161
- const stderr = result.stderr || '';
162
- // Check for expected "no results" type errors
163
- const isExpectedNoResult =
164
- stderr.includes('No matches found') ||
165
- stderr.includes('not found') ||
166
- stderr.includes('No usages found') ||
167
- stderr.includes('No tests found') ||
168
- stderr.includes('No deadcode') ||
169
- stderr.includes('No imports');
170
-
171
- return {
172
- success: isExpectedNoResult,
173
- output: result.stdout,
174
- error: stderr,
175
- command: cmdStr,
176
- exitCode: result.status,
177
- isExpectedNoResult,
178
- };
179
- }
180
-
181
- return { success: true, output: result.stdout, command: cmdStr };
182
- } catch (e) {
183
- return { success: false, error: e.message, command: cmdStr };
184
- }
185
- }
186
-
187
- /**
188
- * Check for error patterns in output
189
- */
190
- function checkForErrors(output) {
191
- if (!output) return null;
192
-
193
- const patterns = [
194
- { pattern: /TypeError: .+/g, name: 'TypeError' },
195
- { pattern: /ReferenceError: .+/g, name: 'ReferenceError' },
196
- { pattern: /SyntaxError: .+/g, name: 'SyntaxError' },
197
- { pattern: /Cannot read propert(y|ies) .+ of (undefined|null)/gi, name: 'Property access on null/undefined' },
198
- { pattern: /is not a function/gi, name: 'Not a function' },
199
- { pattern: /undefined is not/gi, name: 'Undefined error' },
200
- { pattern: /FATAL ERROR/gi, name: 'Fatal error' },
201
- { pattern: /Maximum call stack/gi, name: 'Stack overflow' },
202
- { pattern: /heap out of memory/gi, name: 'Memory error' },
203
- ];
204
-
205
- for (const { pattern, name } of patterns) {
206
- const match = output.match(pattern);
207
- if (match) {
208
- return { type: name, match: match[0] };
209
- }
210
- }
211
-
212
- return null;
213
- }
214
-
215
- /**
216
- * Test a repository
217
- */
218
- function testRepo(lang, repo) {
219
- console.log(`\n${c.blue}Testing ${lang}/${repo.name}${c.reset}`);
220
-
221
- const repoPath = cloneRepo(repo.url, repo.name);
222
- if (!repoPath) {
223
- results.failed++;
224
- results.bugs.push({
225
- language: lang,
226
- repo: repo.name,
227
- error: 'Failed to clone repository',
228
- });
229
- return;
230
- }
231
-
232
- const targetPath = path.join(repoPath, repo.dir);
233
- if (!fs.existsSync(targetPath)) {
234
- console.log(`${c.yellow} ⚠ Directory not found: ${repo.dir}${c.reset}`);
235
- return;
236
- }
237
-
238
- // Test each command
239
- for (const cmd of commands) {
240
- const cmdArgs = cmd.args.map(a => (a === '$SYM' ? repo.symbols[0] : a));
241
- const testId = cmd.id || `${cmd.name}(${cmdArgs.join(',')})`;
242
-
243
- const result = runUcn(targetPath, cmd.name, cmdArgs);
244
-
245
- // Check for crashes/errors
246
- const errorInOutput = checkForErrors(result.output);
247
- const errorInStderr = checkForErrors(result.error);
248
- const foundError = errorInOutput || errorInStderr;
249
-
250
- if (foundError) {
251
- results.failed++;
252
- results.bugs.push({
253
- language: lang,
254
- repo: repo.name,
255
- command: cmd.name,
256
- args: cmdArgs,
257
- error: `${foundError.type}: ${foundError.match}`,
258
- fullCommand: result.command,
259
- });
260
- console.log(` ${c.red}✗${c.reset} ${testId}: ${foundError.type}`);
261
- if (verbose) {
262
- console.log(` ${c.red}${foundError.match}${c.reset}`);
263
- }
264
- continue;
265
- }
266
-
267
- // Check for non-zero exit without expected "no results"
268
- if (!result.success && !result.isExpectedNoResult) {
269
- results.failed++;
270
- results.bugs.push({
271
- language: lang,
272
- repo: repo.name,
273
- command: cmd.name,
274
- args: cmdArgs,
275
- error: result.error || 'Command failed',
276
- fullCommand: result.command,
277
- exitCode: result.exitCode,
278
- });
279
- console.log(` ${c.red}✗${c.reset} ${testId}`);
280
- if (verbose) {
281
- console.log(` ${c.red}${result.error || 'Failed'}${c.reset}`);
282
- }
283
- continue;
284
- }
285
-
286
- // Check JSON validity for JSON commands
287
- if (cmd.args.includes('--json') && result.output) {
288
- try {
289
- JSON.parse(result.output);
290
- } catch (e) {
291
- results.failed++;
292
- results.bugs.push({
293
- language: lang,
294
- repo: repo.name,
295
- command: cmd.name,
296
- args: cmdArgs,
297
- error: `Invalid JSON: ${e.message}`,
298
- fullCommand: result.command,
299
- output: result.output.substring(0, 200),
300
- });
301
- console.log(` ${c.red}✗${c.reset} ${testId}: Invalid JSON`);
302
- continue;
303
- }
304
- }
305
-
306
- results.passed++;
307
- if (verbose) {
308
- console.log(` ${c.green}✓${c.reset} ${testId}`);
309
- }
310
- }
311
- }
312
-
313
- /**
314
- * Additional stress tests
315
- */
316
- function runStressTests() {
317
- console.log(`\n${c.blue}Running STRESS TESTS${c.reset}`);
318
-
319
- const jsFixture = path.join(__dirname, 'fixtures', 'javascript');
320
-
321
- const tests = [
322
- // Large depth values
323
- {
324
- name: 'Very large depth',
325
- run: () => runUcn(jsFixture, 'trace', ['processData', '--depth=100']),
326
- check: r => !checkForErrors(r.output) && !checkForErrors(r.error),
327
- },
328
- // Many concurrent operations simulation (sequential but rapid)
329
- {
330
- name: 'Rapid sequential commands',
331
- run: () => {
332
- for (let i = 0; i < 10; i++) {
333
- const r = runUcn(jsFixture, 'toc', []);
334
- if (!r.success) return r;
335
- }
336
- return { success: true };
337
- },
338
- check: r => r.success,
339
- },
340
- // Large symbol names in search
341
- {
342
- name: 'Pattern-like symbol',
343
- run: () => runUcn(jsFixture, 'find', ['.*']),
344
- check: r => !checkForErrors(r.output),
345
- },
346
- // Empty string searches
347
- {
348
- name: 'Empty search',
349
- run: () => runUcn(jsFixture, 'search', ['']),
350
- check: r => !checkForErrors(r.output),
351
- },
352
- // Newlines in search
353
- {
354
- name: 'Newline in search',
355
- run: () => runUcn(jsFixture, 'search', ['test\ntest']),
356
- check: r => !checkForErrors(r.output),
357
- },
358
- // Null bytes
359
- {
360
- name: 'Null byte in search',
361
- run: () => runUcn(jsFixture, 'search', ['test\x00test']),
362
- check: r => !checkForErrors(r.output),
363
- },
364
- ];
365
-
366
- for (const test of tests) {
367
- const result = test.run();
368
- const passed = test.check(result);
369
-
370
- if (passed) {
371
- results.passed++;
372
- if (verbose) {
373
- console.log(` ${c.green}✓${c.reset} ${test.name}`);
374
- }
375
- } else {
376
- results.failed++;
377
- results.bugs.push({
378
- language: 'stress',
379
- command: test.name,
380
- error: result.error || 'Test failed',
381
- fullCommand: result.command,
382
- });
383
- console.log(` ${c.red}✗${c.reset} ${test.name}`);
384
- }
385
- }
386
- }
387
-
388
- /**
389
- * Print summary
390
- */
391
- function printSummary() {
392
- console.log(`\n${c.cyan}${'='.repeat(60)}${c.reset}`);
393
- console.log(`${c.cyan}PUBLIC REPO TEST SUMMARY${c.reset}`);
394
- console.log(`${c.cyan}${'='.repeat(60)}${c.reset}`);
395
-
396
- console.log(`\n ${c.green}Passed:${c.reset} ${results.passed}`);
397
- console.log(` ${c.red}Failed:${c.reset} ${results.failed}`);
398
- console.log(` Total: ${results.passed + results.failed}`);
399
-
400
- if (results.bugs.length > 0) {
401
- console.log(`\n${c.red}BUGS FOUND (${results.bugs.length}):${c.reset}`);
402
- console.log(`${'-'.repeat(60)}`);
403
-
404
- // Group by language
405
- const byLang = {};
406
- for (const bug of results.bugs) {
407
- const key = bug.repo ? `${bug.language}/${bug.repo}` : bug.language;
408
- if (!byLang[key]) byLang[key] = [];
409
- byLang[key].push(bug);
410
- }
411
-
412
- for (const [key, bugs] of Object.entries(byLang)) {
413
- console.log(`\n${c.yellow}${key}:${c.reset}`);
414
- for (const bug of bugs) {
415
- console.log(` • ${bug.command}: ${bug.error}`);
416
- if (bug.fullCommand) {
417
- console.log(` ${c.dim}${bug.fullCommand}${c.reset}`);
418
- }
419
- }
420
- }
421
-
422
- // Save to file
423
- const bugsFile = path.join(__dirname, 'public-repos-bugs.json');
424
- fs.writeFileSync(bugsFile, JSON.stringify(results.bugs, null, 2));
425
- console.log(`\n${c.dim}Bug report saved to: ${bugsFile}${c.reset}`);
426
- } else {
427
- console.log(`\n${c.green}No bugs found!${c.reset}`);
428
- }
429
-
430
- console.log(`${c.cyan}${'='.repeat(60)}${c.reset}`);
431
- }
432
-
433
- /**
434
- * Cleanup
435
- */
436
- function cleanup() {
437
- if (!keepRepos && fs.existsSync(TEMP_DIR)) {
438
- console.log(`\n${c.dim}Cleaning up temp directory...${c.reset}`);
439
- fs.rmSync(TEMP_DIR, { recursive: true, force: true });
440
- }
441
- }
442
-
443
- /**
444
- * Main
445
- */
446
- async function main() {
447
- console.log(`${c.cyan}UCN Public Repository Test Suite${c.reset}`);
448
- console.log(`${c.dim}Testing against real-world repositories${c.reset}`);
449
-
450
- // Create temp directory
451
- if (!fs.existsSync(TEMP_DIR)) {
452
- fs.mkdirSync(TEMP_DIR, { recursive: true });
453
- }
454
-
455
- // Test each language
456
- for (const [lang, repoList] of Object.entries(repos)) {
457
- for (const repo of repoList) {
458
- testRepo(lang, repo);
459
- }
460
- }
461
-
462
- // Run stress tests
463
- runStressTests();
464
-
465
- // Print summary
466
- printSummary();
467
-
468
- // Cleanup
469
- cleanup();
470
-
471
- process.exit(results.failed > 0 ? 1 : 0);
472
- }
473
-
474
- main().catch(e => {
475
- console.error(`${c.red}Fatal error: ${e.message}${c.reset}`);
476
- process.exit(1);
477
- });