ucn 3.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.

Potentially problematic release.


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

Files changed (45) hide show
  1. package/.claude/skills/ucn/SKILL.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +135 -0
  4. package/cli/index.js +2437 -0
  5. package/core/discovery.js +513 -0
  6. package/core/imports.js +558 -0
  7. package/core/output.js +1274 -0
  8. package/core/parser.js +279 -0
  9. package/core/project.js +3261 -0
  10. package/index.js +52 -0
  11. package/languages/go.js +653 -0
  12. package/languages/index.js +267 -0
  13. package/languages/java.js +826 -0
  14. package/languages/javascript.js +1346 -0
  15. package/languages/python.js +667 -0
  16. package/languages/rust.js +950 -0
  17. package/languages/utils.js +457 -0
  18. package/package.json +42 -0
  19. package/test/fixtures/go/go.mod +3 -0
  20. package/test/fixtures/go/main.go +257 -0
  21. package/test/fixtures/go/service.go +187 -0
  22. package/test/fixtures/java/DataService.java +279 -0
  23. package/test/fixtures/java/Main.java +287 -0
  24. package/test/fixtures/java/Utils.java +199 -0
  25. package/test/fixtures/java/pom.xml +6 -0
  26. package/test/fixtures/javascript/main.js +109 -0
  27. package/test/fixtures/javascript/package.json +1 -0
  28. package/test/fixtures/javascript/service.js +88 -0
  29. package/test/fixtures/javascript/utils.js +67 -0
  30. package/test/fixtures/python/main.py +198 -0
  31. package/test/fixtures/python/pyproject.toml +3 -0
  32. package/test/fixtures/python/service.py +166 -0
  33. package/test/fixtures/python/utils.py +118 -0
  34. package/test/fixtures/rust/Cargo.toml +3 -0
  35. package/test/fixtures/rust/main.rs +253 -0
  36. package/test/fixtures/rust/service.rs +210 -0
  37. package/test/fixtures/rust/utils.rs +154 -0
  38. package/test/fixtures/typescript/main.ts +154 -0
  39. package/test/fixtures/typescript/package.json +1 -0
  40. package/test/fixtures/typescript/repository.ts +149 -0
  41. package/test/fixtures/typescript/types.ts +114 -0
  42. package/test/parser.test.js +3661 -0
  43. package/test/public-repos-test.js +477 -0
  44. package/test/systematic-test.js +619 -0
  45. package/ucn.js +8 -0
@@ -0,0 +1,513 @@
1
+ /**
2
+ * core/discovery.js - File discovery and glob pattern expansion
3
+ *
4
+ * Pure Node.js implementation (no external dependencies)
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ // Always ignore - unambiguous, never user code
11
+ const DEFAULT_IGNORES = [
12
+ // Package managers (unambiguous names)
13
+ 'node_modules',
14
+ 'bower_components',
15
+ '.bundle',
16
+
17
+ // Version control
18
+ '.git',
19
+ '.svn',
20
+ '.hg',
21
+
22
+ // Python
23
+ '__pycache__',
24
+ '.venv',
25
+ 'venv',
26
+ '.env',
27
+ '.tox',
28
+ '.eggs',
29
+ '*.egg-info',
30
+
31
+ // Build outputs
32
+ 'dist',
33
+ 'build',
34
+ 'out',
35
+ '.next',
36
+ '.nuxt',
37
+ '.output',
38
+ '.vercel',
39
+ '.netlify',
40
+
41
+ // Test/coverage
42
+ 'coverage',
43
+ '.nyc_output',
44
+ '.pytest_cache',
45
+ '.mypy_cache',
46
+
47
+ // Bundled/minified
48
+ '*.min.js',
49
+ '*.bundle.js',
50
+ '*.map',
51
+
52
+ // System
53
+ '.DS_Store',
54
+ '.ucn-cache'
55
+ ];
56
+
57
+ // Conditional ignores - only ignore when marker file exists in same directory
58
+ // Maps directory name -> array of marker files that indicate it's a vendor dir
59
+ const CONDITIONAL_IGNORES = {
60
+ 'vendor': ['go.mod', 'composer.json', 'Gemfile'], // Go, PHP, Ruby
61
+ 'Pods': ['Podfile'], // iOS CocoaPods
62
+ 'Carthage': ['Cartfile'], // iOS Carthage
63
+ 'deps': ['mix.exs', 'rebar.config'], // Elixir, Erlang
64
+ 'target': ['Cargo.toml', 'pom.xml', 'build.gradle'], // Rust, Maven, Gradle
65
+ 'env': ['requirements.txt', 'pyproject.toml'], // Python virtualenv
66
+ };
67
+
68
+ // Project root markers
69
+ const PROJECT_MARKERS = [
70
+ '.git',
71
+ '.ucn.js',
72
+ 'package.json',
73
+ 'pyproject.toml',
74
+ 'setup.py',
75
+ 'go.mod',
76
+ 'Cargo.toml',
77
+ 'pom.xml',
78
+ 'build.gradle',
79
+ 'Makefile'
80
+ ];
81
+
82
+ // Test file patterns by language
83
+ const TEST_PATTERNS = {
84
+ javascript: [
85
+ /\.test\.(js|jsx|ts|tsx|mjs|cjs)$/,
86
+ /\.spec\.(js|jsx|ts|tsx|mjs|cjs)$/,
87
+ /__tests__\//,
88
+ /\.test$/
89
+ ],
90
+ typescript: [
91
+ /\.test\.(ts|tsx)$/,
92
+ /\.spec\.(ts|tsx)$/,
93
+ /__tests__\//
94
+ ],
95
+ python: [
96
+ /^test_.*\.py$/,
97
+ /.*_test\.py$/,
98
+ /\/tests?\//
99
+ ],
100
+ go: [
101
+ /.*_test\.go$/
102
+ ],
103
+ java: [
104
+ /.*Test\.java$/,
105
+ /.*TestCase\.java$/,
106
+ /.*Tests\.java$/
107
+ ],
108
+ rust: [
109
+ /.*_test\.rs$/,
110
+ /\/tests\//,
111
+ /mod tests/
112
+ ]
113
+ };
114
+
115
+ function compareNames(a, b) {
116
+ const aLower = a.toLowerCase();
117
+ const bLower = b.toLowerCase();
118
+ if (aLower < bLower) return -1;
119
+ if (aLower > bLower) return 1;
120
+ if (a < b) return -1;
121
+ if (a > b) return 1;
122
+ return 0;
123
+ }
124
+
125
+ /**
126
+ * Expand a glob pattern to matching file paths
127
+ *
128
+ * @param {string} pattern - Glob pattern (e.g., "src/**\/*.py", "*.js")
129
+ * @param {object} options - Configuration options
130
+ * @param {string} options.root - Root directory (defaults to cwd)
131
+ * @param {string[]} options.ignores - Patterns to ignore
132
+ * @param {number} options.maxDepth - Maximum directory depth (default: 20)
133
+ * @param {number} options.maxFiles - Maximum files to return (default: 10000)
134
+ * @returns {string[]} - Array of absolute file paths
135
+ */
136
+ function expandGlob(pattern, options = {}) {
137
+ const root = path.resolve(options.root || process.cwd());
138
+ const ignores = options.ignores || DEFAULT_IGNORES;
139
+ const maxDepth = options.maxDepth || 20;
140
+ const maxFiles = options.maxFiles || 10000;
141
+ const followSymlinks = options.followSymlinks !== false; // default true
142
+
143
+ // Handle home directory expansion
144
+ if (pattern.startsWith('~/')) {
145
+ pattern = pattern.replace('~', require('os').homedir());
146
+ }
147
+
148
+ // Parse the pattern
149
+ const { baseDir, filePattern, recursive } = parseGlobPattern(pattern, root);
150
+
151
+ // Collect matching files
152
+ const files = [];
153
+ walkDir(baseDir, {
154
+ filePattern,
155
+ recursive,
156
+ ignores,
157
+ maxDepth,
158
+ followSymlinks,
159
+ onFile: (filePath) => {
160
+ if (files.length < maxFiles) {
161
+ files.push(filePath);
162
+ }
163
+ }
164
+ });
165
+
166
+ return files.sort(compareNames);
167
+ }
168
+
169
+ /**
170
+ * Parse a glob pattern into components
171
+ */
172
+ function parseGlobPattern(pattern, root) {
173
+ const recursive = pattern.includes('**');
174
+ const parts = pattern.split(/[/\\]/);
175
+
176
+ let dirParts = [];
177
+ let wildcardStart = -1;
178
+
179
+ for (let i = 0; i < parts.length; i++) {
180
+ if (parts[i].includes('*') || parts[i].includes('?')) {
181
+ wildcardStart = i;
182
+ break;
183
+ }
184
+ dirParts.push(parts[i]);
185
+ }
186
+
187
+ let baseDir;
188
+ if (dirParts.length === 0) {
189
+ baseDir = root;
190
+ } else if (path.isAbsolute(dirParts.join('/'))) {
191
+ baseDir = dirParts.join('/');
192
+ } else {
193
+ baseDir = path.join(root, ...dirParts);
194
+ }
195
+
196
+ let filePatternStr = wildcardStart >= 0
197
+ ? parts.slice(wildcardStart).join('/')
198
+ : '*';
199
+
200
+ filePatternStr = filePatternStr.replace(/^\*\*[/\\]?/, '');
201
+ const filePattern = globToRegex(filePatternStr || '*');
202
+
203
+ return { baseDir, filePattern, recursive };
204
+ }
205
+
206
+ /**
207
+ * Convert a glob pattern to a regular expression
208
+ */
209
+ function globToRegex(glob) {
210
+ let regex = glob.replace(/[.+^$[\]\\]/g, '\\$&');
211
+
212
+ // Handle brace expansion: {js,ts} -> (js|ts)
213
+ regex = regex.replace(/\{([^}]+)\}/g, (_, group) => {
214
+ const alternatives = group.split(',').map(s => s.trim());
215
+ return '(' + alternatives.join('|') + ')';
216
+ });
217
+
218
+ regex = regex.replace(/\*\*/g, '.*');
219
+ regex = regex.replace(/\*/g, '[^/]*');
220
+ regex = regex.replace(/\?/g, '.');
221
+
222
+ return new RegExp('^' + regex + '$');
223
+ }
224
+
225
+ /**
226
+ * Walk a directory tree, calling onFile for each matching file
227
+ */
228
+ function walkDir(dir, options, depth = 0, visited = new Set()) {
229
+ if (depth > options.maxDepth) return;
230
+ if (!fs.existsSync(dir)) return;
231
+
232
+ // Track visited directories to avoid circular symlinks
233
+ let realDir;
234
+ try {
235
+ realDir = fs.realpathSync(dir);
236
+ } catch (e) {
237
+ return; // broken symlink
238
+ }
239
+ if (visited.has(realDir)) return;
240
+ visited.add(realDir);
241
+
242
+ let entries;
243
+ try {
244
+ entries = fs.readdirSync(dir, { withFileTypes: true });
245
+ } catch (e) {
246
+ return;
247
+ }
248
+
249
+ entries.sort((a, b) => compareNames(a.name, b.name));
250
+
251
+ const followSymlinks = options.followSymlinks !== false; // default true
252
+
253
+ for (const entry of entries) {
254
+ const fullPath = path.join(dir, entry.name);
255
+
256
+ if (shouldIgnore(entry.name, options.ignores, dir)) continue;
257
+
258
+ let isDir = entry.isDirectory();
259
+ let isFile = entry.isFile();
260
+
261
+ // Follow symlinks if enabled
262
+ if (followSymlinks && entry.isSymbolicLink()) {
263
+ try {
264
+ const stat = fs.statSync(fullPath);
265
+ isDir = stat.isDirectory();
266
+ isFile = stat.isFile();
267
+ } catch (e) {
268
+ continue; // broken symlink
269
+ }
270
+ }
271
+
272
+ if (isDir) {
273
+ if (options.recursive) {
274
+ walkDir(fullPath, options, depth + 1, visited);
275
+ }
276
+ } else if (isFile) {
277
+ if (options.filePattern.test(entry.name)) {
278
+ options.onFile(fullPath);
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Check if a file/directory name should be ignored
286
+ * @param {string} name - File/directory name
287
+ * @param {string[]} ignores - Patterns to always ignore
288
+ * @param {string} [parentDir] - Parent directory path (for conditional checks)
289
+ */
290
+ function shouldIgnore(name, ignores, parentDir) {
291
+ // Check unconditional ignores
292
+ for (const pattern of ignores) {
293
+ if (pattern.includes('*')) {
294
+ const regex = globToRegex(pattern);
295
+ if (regex.test(name)) return true;
296
+ } else if (name === pattern) {
297
+ return true;
298
+ }
299
+ }
300
+
301
+ // Check conditional ignores (only if parentDir provided)
302
+ if (parentDir && CONDITIONAL_IGNORES[name]) {
303
+ const markers = CONDITIONAL_IGNORES[name];
304
+ for (const marker of markers) {
305
+ if (fs.existsSync(path.join(parentDir, marker))) {
306
+ return true; // Marker found, this is a real vendor dir
307
+ }
308
+ }
309
+ }
310
+
311
+ return false;
312
+ }
313
+
314
+ /**
315
+ * Find the project root directory by looking for marker files
316
+ */
317
+ function findProjectRoot(startDir) {
318
+ let dir = path.resolve(startDir);
319
+ const root = path.parse(dir).root;
320
+
321
+ while (dir !== root) {
322
+ for (const marker of PROJECT_MARKERS) {
323
+ if (fs.existsSync(path.join(dir, marker))) {
324
+ return dir;
325
+ }
326
+ }
327
+ dir = path.dirname(dir);
328
+ }
329
+
330
+ return path.resolve(startDir);
331
+ }
332
+
333
+ /**
334
+ * Auto-detect the glob pattern for a project based on its type
335
+ */
336
+ function detectProjectPattern(projectRoot) {
337
+ const extensions = [];
338
+
339
+ if (fs.existsSync(path.join(projectRoot, 'package.json'))) {
340
+ extensions.push('js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs');
341
+ }
342
+
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
+ }
348
+
349
+ if (fs.existsSync(path.join(projectRoot, 'go.mod'))) {
350
+ extensions.push('go');
351
+ }
352
+
353
+ if (fs.existsSync(path.join(projectRoot, 'Cargo.toml'))) {
354
+ extensions.push('rs');
355
+ }
356
+
357
+ if (fs.existsSync(path.join(projectRoot, 'pom.xml')) ||
358
+ fs.existsSync(path.join(projectRoot, 'build.gradle'))) {
359
+ extensions.push('java', 'kt');
360
+ }
361
+
362
+ if (extensions.length > 0) {
363
+ const unique = [...new Set(extensions)];
364
+ return `**/*.{${unique.join(',')}}`;
365
+ }
366
+
367
+ return '**/*.{js,jsx,ts,tsx,py,go,java,rs,rb,php,c,cpp,h,hpp}';
368
+ }
369
+
370
+ /**
371
+ * Get file statistics for a set of files
372
+ */
373
+ function getFileStats(files) {
374
+ const stats = {
375
+ totalFiles: files.length,
376
+ totalLines: 0,
377
+ byExtension: {}
378
+ };
379
+
380
+ for (const file of files) {
381
+ const ext = path.extname(file).toLowerCase() || '(none)';
382
+
383
+ if (!stats.byExtension[ext]) {
384
+ stats.byExtension[ext] = { count: 0, lines: 0 };
385
+ }
386
+
387
+ try {
388
+ const content = fs.readFileSync(file, 'utf-8');
389
+ const lines = content.split('\n').length;
390
+ stats.totalLines += lines;
391
+ stats.byExtension[ext].count++;
392
+ stats.byExtension[ext].lines += lines;
393
+ } catch (e) {
394
+ // Skip files that can't be read
395
+ }
396
+ }
397
+
398
+ return stats;
399
+ }
400
+
401
+ /**
402
+ * Check if a file is a test file based on its path and language
403
+ */
404
+ function isTestFile(filePath, language) {
405
+ const patterns = TEST_PATTERNS[language] || TEST_PATTERNS.javascript;
406
+ const normalizedPath = filePath.replace(/\\/g, '/');
407
+ const basename = path.basename(filePath);
408
+
409
+ for (const pattern of patterns) {
410
+ if (pattern.test(normalizedPath) || pattern.test(basename)) {
411
+ return true;
412
+ }
413
+ }
414
+ return false;
415
+ }
416
+
417
+ /**
418
+ * Find the test file for a given source file
419
+ */
420
+ function findTestFileFor(sourceFile, language) {
421
+ const dir = path.dirname(sourceFile);
422
+ const ext = path.extname(sourceFile);
423
+ const base = path.basename(sourceFile, ext);
424
+
425
+ const candidates = [];
426
+
427
+ switch (language) {
428
+ case 'javascript':
429
+ case 'typescript':
430
+ case 'tsx':
431
+ candidates.push(
432
+ `${base}.test${ext}`,
433
+ `${base}.spec${ext}`,
434
+ `${base}.test.ts`,
435
+ `${base}.test.js`,
436
+ `${base}.spec.ts`,
437
+ `${base}.spec.js`
438
+ );
439
+ break;
440
+ case 'python':
441
+ candidates.push(
442
+ `test_${base}.py`,
443
+ `${base}_test.py`
444
+ );
445
+ break;
446
+ case 'go':
447
+ candidates.push(`${base}_test.go`);
448
+ break;
449
+ case 'java':
450
+ candidates.push(
451
+ `${base}Test.java`,
452
+ `${base}Tests.java`,
453
+ `${base}TestCase.java`
454
+ );
455
+ break;
456
+ case 'rust':
457
+ candidates.push(`${base}_test.rs`);
458
+ break;
459
+ default:
460
+ candidates.push(
461
+ `${base}.test${ext}`,
462
+ `${base}.spec${ext}`
463
+ );
464
+ }
465
+
466
+ // Check in same directory
467
+ for (const candidate of candidates) {
468
+ const testPath = path.join(dir, candidate);
469
+ if (fs.existsSync(testPath)) {
470
+ return testPath;
471
+ }
472
+ }
473
+
474
+ // Check in __tests__ subdirectory
475
+ if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
476
+ const testsDir = path.join(dir, '__tests__');
477
+ for (const candidate of candidates) {
478
+ const testPath = path.join(testsDir, candidate);
479
+ if (fs.existsSync(testPath)) {
480
+ return testPath;
481
+ }
482
+ }
483
+ }
484
+
485
+ // Check in tests subdirectory
486
+ if (language === 'python' || language === 'rust') {
487
+ const testsDir = path.join(dir, 'tests');
488
+ for (const candidate of candidates) {
489
+ const testPath = path.join(testsDir, candidate);
490
+ if (fs.existsSync(testPath)) {
491
+ return testPath;
492
+ }
493
+ }
494
+ }
495
+
496
+ return null;
497
+ }
498
+
499
+ module.exports = {
500
+ expandGlob,
501
+ parseGlobPattern,
502
+ globToRegex,
503
+ walkDir,
504
+ shouldIgnore,
505
+ findProjectRoot,
506
+ detectProjectPattern,
507
+ getFileStats,
508
+ isTestFile,
509
+ findTestFileFor,
510
+ DEFAULT_IGNORES,
511
+ PROJECT_MARKERS,
512
+ TEST_PATTERNS
513
+ };