@sun-asterisk/sunlint 1.3.48 → 1.3.50

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 (152) hide show
  1. package/core/file-targeting-service.js +148 -15
  2. package/core/init-command.js +118 -70
  3. package/core/project-detector.js +517 -0
  4. package/core/tui-select.js +245 -0
  5. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  6. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  7. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  8. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  9. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  10. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  11. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  12. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  13. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  14. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  15. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  16. package/package.json +2 -1
  17. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  18. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  19. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  20. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  21. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  22. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  23. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  24. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  25. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  26. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  27. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  28. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  29. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  30. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  31. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  32. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  33. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  34. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  35. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  36. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  37. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  38. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  39. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  40. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  41. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  42. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  43. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  44. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  45. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  46. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  47. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  48. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  49. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  50. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  51. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  52. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  53. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  54. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  55. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  56. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  57. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  58. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  59. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  60. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  61. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  62. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  63. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  64. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  65. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  66. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  67. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  68. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  69. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  70. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  71. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  72. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  73. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  74. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  75. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  76. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  77. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  78. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  79. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  80. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  81. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  82. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  83. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  84. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  85. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  86. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  87. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  88. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  89. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  90. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  91. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  92. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  93. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  94. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  95. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  96. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  97. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  98. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  99. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  100. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  101. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  102. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  103. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  104. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  105. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  106. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  107. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  108. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  109. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  110. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  111. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  112. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  113. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  114. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  115. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  116. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  117. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  118. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  119. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  120. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  121. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  122. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  123. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  124. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  125. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  126. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  127. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  128. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  129. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  130. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  131. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  132. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  133. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  134. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  135. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  136. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  137. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  138. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  139. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -41,7 +41,7 @@ class FileTargetingService {
41
41
  allFiles = await this.getGitChangedFiles(cliOptions);
42
42
  } else {
43
43
  // Smart project-level optimization
44
- const optimizedPaths = this.optimizeProjectPaths(inputPaths, cliOptions);
44
+ const optimizedPaths = this.optimizeProjectPaths(inputPaths, cliOptions, config);
45
45
 
46
46
  // Use enhanced targeting based on metadata
47
47
  if (metadata?.shouldBypassProjectDiscovery) {
@@ -247,7 +247,7 @@ class FileTargetingService {
247
247
  * Optimize project paths to focus on source and test directories
248
248
  * Prevents unnecessary scanning of entire project when smart targeting is possible
249
249
  */
250
- optimizeProjectPaths(inputPaths, cliOptions = {}) {
250
+ optimizeProjectPaths(inputPaths, cliOptions = {}, config = {}) {
251
251
  const optimizedPaths = [];
252
252
 
253
253
  for (const inputPath of inputPaths) {
@@ -268,7 +268,7 @@ class FileTargetingService {
268
268
  }
269
269
 
270
270
  // Only optimize if this appears to be a project root directory
271
- const projectOptimization = this.findProjectSourceDirs(inputPath, cliOptions);
271
+ const projectOptimization = this.findProjectSourceDirs(inputPath, cliOptions, config);
272
272
  if (projectOptimization.length > 0) {
273
273
  if (cliOptions.verbose) {
274
274
  console.log(chalk.blue(`🎯 Smart targeting: Found ${projectOptimization.length} source directories in ${path.basename(inputPath)}`));
@@ -286,26 +286,159 @@ class FileTargetingService {
286
286
  }
287
287
 
288
288
  /**
289
- * Find source directories in project to avoid scanning entire project
289
+ * Find source directories in project to avoid scanning entire project.
290
+ * When language info is available, dynamically discovers every top-level
291
+ * directory that actually contains matching language files instead of
292
+ * relying on a fixed list of directory names.
290
293
  */
291
- findProjectSourceDirs(projectPath, cliOptions = {}) {
292
- const sourceDirs = [];
294
+ findProjectSourceDirs(projectPath, cliOptions = {}, config = {}) {
295
+ const testDirNames = ['test', 'tests', '__tests__', 'spec', 'specs'];
296
+ const targetLanguages = this.getTargetLanguages(config, cliOptions);
297
+ const targetExtensions = this.getExtensionsForLanguages(targetLanguages);
298
+
299
+ if (targetExtensions.length > 0) {
300
+ // Dynamic discovery: find all top-level dirs that contain matching files
301
+ let discoveredDirs = this.discoverDirsWithLanguageFiles(projectPath, targetExtensions, cliOptions);
302
+
303
+ // Exclude test dirs unless --include-tests is set
304
+ if (!cliOptions.includeTests) {
305
+ discoveredDirs = discoveredDirs.filter(dir => !testDirNames.includes(path.basename(dir)));
306
+ }
307
+
308
+ return discoveredDirs;
309
+ }
310
+
311
+ // Fallback: use hardcoded candidate dirs when no language info is available
293
312
  const candidateDirs = ['src', 'lib', 'app', 'packages', 'components', 'hooks', 'utils', 'helpers', 'services', 'worker'];
294
- const testDirs = ['test', 'tests', '__tests__', 'spec', 'specs'];
295
-
296
- // Always include test directories if --include-tests flag is used
297
- const dirsToCheck = cliOptions.includeTests ? [...candidateDirs, ...testDirs] : candidateDirs;
298
-
313
+ const dirsToCheck = cliOptions.includeTests ? [...candidateDirs, ...testDirNames] : candidateDirs;
314
+ const sourceDirs = [];
315
+
299
316
  for (const dir of dirsToCheck) {
300
317
  const dirPath = path.join(projectPath, dir);
301
318
  if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
302
319
  sourceDirs.push(dirPath);
303
320
  }
304
321
  }
305
-
322
+
306
323
  return sourceDirs;
307
324
  }
308
325
 
326
+ /**
327
+ * Extract target languages from config or CLI options
328
+ * Rule C006: getTargetLanguages - verb-noun naming
329
+ */
330
+ getTargetLanguages(config, cliOptions) {
331
+ if (cliOptions.languages) {
332
+ return cliOptions.languages.split(',').map(l => l.trim());
333
+ }
334
+ if (Array.isArray(config.languages)) {
335
+ return config.languages;
336
+ }
337
+ if (config.languages && typeof config.languages === 'object') {
338
+ return Object.keys(config.languages);
339
+ }
340
+ return [];
341
+ }
342
+
343
+ /**
344
+ * Map language names to their corresponding file extensions
345
+ * Rule C006: getExtensionsForLanguages - verb-noun naming
346
+ */
347
+ getExtensionsForLanguages(languages) {
348
+ const extensionMap = {
349
+ typescript: ['.ts', '.tsx', '.mts', '.cts'],
350
+ javascript: ['.js', '.jsx', '.mjs', '.cjs'],
351
+ dart: ['.dart'],
352
+ kotlin: ['.kt', '.kts'],
353
+ java: ['.java'],
354
+ swift: ['.swift'],
355
+ };
356
+
357
+ const extensions = new Set();
358
+ for (const lang of languages) {
359
+ (extensionMap[lang] || []).forEach(ext => extensions.add(ext));
360
+ }
361
+ return [...extensions];
362
+ }
363
+
364
+ /**
365
+ * Return every immediate child directory of projectPath that contains
366
+ * at least one file with a target extension (checked recursively up to
367
+ * a limited depth so large platform dirs are not fully traversed).
368
+ * Rule C006: discoverDirsWithLanguageFiles - verb-noun naming
369
+ */
370
+ discoverDirsWithLanguageFiles(projectPath, targetExtensions, cliOptions = {}) {
371
+ const dirs = [];
372
+
373
+ try {
374
+ const entries = fs.readdirSync(projectPath);
375
+
376
+ for (const entry of entries) {
377
+ if (this.shouldSkipDirectory(entry)) continue;
378
+
379
+ const fullPath = path.join(projectPath, entry);
380
+ let stat;
381
+ try {
382
+ stat = fs.lstatSync(fullPath);
383
+ } catch {
384
+ continue;
385
+ }
386
+
387
+ if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
388
+
389
+ if (this.directoryContainsLanguageFiles(fullPath, targetExtensions)) {
390
+ if (cliOptions.verbose) {
391
+ console.log(chalk.blue(`🎯 Discovered language directory: ${entry}`));
392
+ }
393
+ dirs.push(fullPath);
394
+ }
395
+ }
396
+ } catch {
397
+ // silent fail — projectPath may be unreadable
398
+ }
399
+
400
+ return dirs;
401
+ }
402
+
403
+ /**
404
+ * Check recursively (up to depth) whether a directory contains at least
405
+ * one file whose extension is in targetExtensions.
406
+ * Rule C012: Query method - returns boolean, no side effects
407
+ */
408
+ directoryContainsLanguageFiles(dirPath, targetExtensions, depth = 5) {
409
+ if (depth <= 0) return false;
410
+
411
+ try {
412
+ const entries = fs.readdirSync(dirPath);
413
+
414
+ for (const entry of entries) {
415
+ if (this.shouldSkipDirectory(entry)) continue;
416
+
417
+ const fullPath = path.join(dirPath, entry);
418
+ let stat;
419
+ try {
420
+ stat = fs.lstatSync(fullPath);
421
+ } catch {
422
+ continue;
423
+ }
424
+
425
+ if (stat.isSymbolicLink()) continue;
426
+
427
+ if (stat.isFile()) {
428
+ if (targetExtensions.includes(path.extname(entry).toLowerCase())) return true;
429
+ } else if (stat.isDirectory()) {
430
+ if (this.directoryContainsLanguageFiles(fullPath, targetExtensions, depth - 1)) {
431
+ return true;
432
+ }
433
+ }
434
+ }
435
+ } catch {
436
+ // ignore unreadable subdirectory
437
+ }
438
+
439
+ return false;
440
+ }
441
+
309
442
  /**
310
443
  * Check if directory should be skipped
311
444
  * Enhanced with more comprehensive ignore patterns
@@ -771,7 +904,7 @@ class FileTargetingService {
771
904
  * Rule C012: Query method - returns boolean
772
905
  */
773
906
  isSourceFile(filePath, config) {
774
- const sourceExtensions = ['.ts', '.tsx', '.js', '.jsx', '.dart', '.kt', '.java', '.swift'];
907
+ const sourceExtensions = ['.ts', '.tsx', '.js', '.jsx', '.dart', '.kt', '.kts', '.java', '.swift'];
775
908
  const ext = path.extname(filePath);
776
909
  return sourceExtensions.includes(ext);
777
910
  }
@@ -793,10 +926,10 @@ class FileTargetingService {
793
926
  return ext === '.dart' && !normalizedPath.match(/\.(g|freezed|mocks)\.dart$/);
794
927
  case 'kotlin':
795
928
  return ['.kt', '.kts'].includes(ext);
929
+ case 'java':
930
+ return ext === '.java';
796
931
  case 'swift':
797
932
  return ext === '.swift';
798
- case 'python':
799
- return ext === '.py';
800
933
  default:
801
934
  return false;
802
935
  }
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * SunLint Init Command
3
3
  * Initializes a project with SunLint code quality skill and AGENTS.md
4
- * Following Rule C005: Single responsibility - only handle init logic
4
+ * Following Rule C005: Single responsibility - only handle init orchestration
5
+ * Following Rule C014: Dependency injection via module imports (detector, tui)
5
6
  */
6
7
 
7
8
  const fs = require('fs');
8
9
  const path = require('path');
9
10
  const chalk = require('chalk');
10
- const readline = require('readline');
11
+
12
+ const { detectProjectLanguage } = require('./project-detector');
13
+ const { printBanner, printDetectionHint, tuiSelect, tuiConfirm } = require('./tui-select');
11
14
 
12
15
  // Path to the bundled skill directory (relative to this file in the package)
13
16
  const SKILL_SOURCE_DIR = path.join(__dirname, '..', 'skill-assets', 'sunlint-code-quality');
@@ -62,78 +65,95 @@ function getAvailableLanguages() {
62
65
  return fs.readdirSync(rulesDir)
63
66
  .filter(item => {
64
67
  const fullPath = path.join(rulesDir, item);
65
- return fs.statSync(fullPath).isDirectory();
68
+ // Exclude framework-specific sub-dirs (e.g. php-laravel, ruby-rails, go-gin)
69
+ // They are installed automatically based on detection, not selectable as base languages.
70
+ return fs.statSync(fullPath).isDirectory() && !item.includes('-');
66
71
  });
67
72
  }
68
73
 
69
- /**
70
- * Interactive selection helper
71
- */
72
- async function promptSelection(question, options) {
73
- const rl = readline.createInterface({
74
- input: process.stdin,
75
- output: process.stdout
76
- });
77
-
78
- return new Promise((resolve) => {
79
- console.log(chalk.cyan(`\n? ${question}`));
80
- options.forEach((opt, i) => {
81
- console.log(` ${chalk.white(i + 1)}) ${opt.name || opt}`);
82
- });
83
-
84
- const ask = () => {
85
- rl.question(chalk.yellow(`\nSelect [1-${options.length}]: `), (answer) => {
86
- const choice = parseInt(answer.trim());
87
- if (choice >= 1 && choice <= options.length) {
88
- rl.close();
89
- const selected = options[choice - 1];
90
- resolve(selected.key || selected);
91
- } else {
92
- console.log(chalk.red(`Invalid selection. Please enter a number between 1 and ${options.length}.`));
93
- ask();
94
- }
95
- });
96
- };
97
- ask();
98
- });
99
- }
74
+ // (Legacy promptSelection removed — replaced by tuiSelect in initProject)
100
75
 
101
76
  /**
102
77
  * Initialize a project with SunLint skill and AGENTS.md
78
+ * Uses TUI arrow-key selection and auto-detects project language preset.
79
+ *
103
80
  * @param {string} targetDir - The target project directory
104
- * @param {object} options - Command options
81
+ * @param {object} options - CLI options {tool, language, force, nonInteractive}
105
82
  */
106
83
  async function initProject(targetDir, options = {}) {
107
84
  const resolvedTargetDir = path.resolve(targetDir);
108
85
 
86
+ // Validate target directory exists early
87
+ if (!fs.existsSync(resolvedTargetDir)) {
88
+ throw new Error(`Target directory does not exist: ${resolvedTargetDir}`);
89
+ }
90
+
109
91
  let tool = options.tool;
110
92
  let language = options.language;
111
93
 
112
- // Interactive step-by-step setup if options are missing and in TTY
113
- if (process.stdin.isTTY && process.stdout.isTTY && !options.nonInteractive) {
94
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY && !options.nonInteractive;
95
+
96
+ if (isInteractive) {
97
+ printBanner();
98
+
99
+ // ── Step 1: Auto-detect language preset ──────────────────────────────
100
+ const detection = options.language ? null : detectProjectLanguage(resolvedTargetDir);
101
+ printDetectionHint(detection);
102
+
103
+ // ── Step 2: Select AI tool ────────────────────────────────────────────
114
104
  if (!tool) {
115
- const toolOptions = Object.entries(AI_TOOL_CONFIG).map(([key, config]) => ({
116
- key,
117
- name: config.name
118
- }));
119
- tool = await promptSelection('Select Target AI Tool', toolOptions);
105
+ const toolKeys = Object.keys(AI_TOOL_CONFIG);
106
+ const toolLabels = Object.fromEntries(
107
+ Object.entries(AI_TOOL_CONFIG).map(([k, v]) => [k, v.name])
108
+ );
109
+ const toolDescs = Object.fromEntries(
110
+ Object.entries(AI_TOOL_CONFIG).map(([k, v]) => [k, v.skillPath + '/'])
111
+ );
112
+ const defaultToolIndex = toolKeys.indexOf(DEFAULT_TOOL);
113
+
114
+ tool = await tuiSelect({
115
+ question: 'Select Target AI Tool',
116
+ options: toolKeys,
117
+ labels: toolLabels,
118
+ badges: toolDescs,
119
+ defaultIndex: Math.max(0, defaultToolIndex),
120
+ });
120
121
  }
121
122
 
123
+ // ── Step 3: Select language ───────────────────────────────────────────
122
124
  if (!language) {
123
- const languageOptions = getAvailableLanguages();
124
- language = await promptSelection('Select Target Language', languageOptions);
125
+ const availableLanguages = getAvailableLanguages();
126
+ const detectedLang = detection ? detection.language : null;
127
+ const defaultLangIndex = detectedLang
128
+ ? Math.max(0, availableLanguages.indexOf(detectedLang))
129
+ : availableLanguages.indexOf('typescript') >= 0
130
+ ? availableLanguages.indexOf('typescript')
131
+ : 0;
132
+
133
+ const langBadges = {};
134
+ if (detectedLang && availableLanguages.includes(detectedLang)) {
135
+ const frameworkNote = detection ? `Recommended · ${detection.framework}` : 'Recommended';
136
+ langBadges[detectedLang] = `[${frameworkNote}]`;
137
+ }
138
+
139
+ language = await tuiSelect({
140
+ question: 'Select Programming Language',
141
+ options: availableLanguages,
142
+ badges: langBadges,
143
+ defaultIndex: defaultLangIndex,
144
+ });
125
145
  }
126
146
  }
127
147
 
128
- // Apply defaults if still missing (non-interactive or failed prompt)
129
- tool = tool || DEFAULT_TOOL;
148
+ // ── Apply defaults for non-interactive / partial flags ───────────────────
149
+ if (!language) {
150
+ const detection = detectProjectLanguage(resolvedTargetDir);
151
+ if (detection) language = detection.language;
152
+ }
153
+ tool = tool || DEFAULT_TOOL;
130
154
  language = language || 'typescript';
131
155
 
132
- // Update options for subsequent steps
133
- options.tool = tool;
134
- options.language = language;
135
-
136
- // Validate tool option
156
+ // ── Validate tool selection ───────────────────────────────────────────────
137
157
  if (!AI_TOOL_CONFIG[tool]) {
138
158
  console.error(chalk.red(`\n❌ Unknown tool: ${tool}`));
139
159
  console.log(chalk.yellow('\nAvailable tools:'));
@@ -141,31 +161,40 @@ async function initProject(targetDir, options = {}) {
141
161
  throw new Error(`Unknown tool: ${tool}. Use one of: ${Object.keys(AI_TOOL_CONFIG).join(', ')}`);
142
162
  }
143
163
 
144
- const toolConfig = AI_TOOL_CONFIG[tool];
145
-
146
- console.log(chalk.cyan('\n☀️ SunLint Init - Setting up code quality standards...\n'));
147
- console.log(chalk.blue(` 🤖 Target AI Tool: ${toolConfig.name}`));
148
- console.log(chalk.blue(` 📝 Target Language: ${language}`));
164
+ options.tool = tool;
165
+ options.language = language;
149
166
 
150
- // Validate target directory exists
151
- if (!fs.existsSync(resolvedTargetDir)) {
152
- throw new Error(`Target directory does not exist: ${resolvedTargetDir}`);
167
+ // Detect frameworkKey so framework-specific rules can be installed
168
+ if (options.frameworkKey === undefined) {
169
+ const fwDetection = detectProjectLanguage(resolvedTargetDir);
170
+ options.frameworkKey = (fwDetection && fwDetection.language === language)
171
+ ? (fwDetection.frameworkKey || null)
172
+ : null;
153
173
  }
154
174
 
155
- const skillTargetDir = path.join(resolvedTargetDir, toolConfig.skillPath, 'sunlint-code-quality');
175
+ const toolConfig = AI_TOOL_CONFIG[tool];
176
+ const skillTargetDir = path.join(resolvedTargetDir, toolConfig.skillPath, 'sunlint-code-quality');
156
177
  const agentsTargetPath = path.join(resolvedTargetDir, 'AGENTS.md');
157
178
 
158
- // Step 1: Copy skill folder
179
+ if (!isInteractive) {
180
+ // Non-interactive: still print a brief summary
181
+ console.log(chalk.cyan('\n☀️ SunLint Init - Setting up code quality standards...\n'));
182
+ console.log(chalk.blue(` 🤖 AI Tool: ${toolConfig.name}`));
183
+ console.log(chalk.blue(` 📝 Language: ${language}`));
184
+ }
185
+
186
+ // ── Step 4: Copy skill folder ─────────────────────────────────────────────
159
187
  await copySkillFolder(skillTargetDir, options, toolConfig);
160
188
 
161
- // Step 2: Append/Create AGENTS.md
189
+ // ── Step 5: Setup AGENTS.md ───────────────────────────────────────────────
162
190
  await setupAgentsMd(agentsTargetPath, options);
163
191
 
192
+ // ── Done ──────────────────────────────────────────────────────────────────
164
193
  console.log(chalk.green('\n✅ SunLint initialization complete!\n'));
165
194
  console.log(chalk.white(' Files created/updated:'));
166
195
  console.log(chalk.gray(` • ${path.relative(resolvedTargetDir, skillTargetDir)}/`));
167
196
  console.log(chalk.gray(` • AGENTS.md`));
168
- console.log(chalk.cyan(`\n📖 ${toolConfig.name} will now follow SunLint code quality standards for ${language}.\n`));
197
+ console.log(chalk.cyan(`\n📖 ${toolConfig.name} will now follow SunLint ${language} quality standards.\n`));
169
198
  }
170
199
 
171
200
  /**
@@ -175,17 +204,15 @@ async function initProject(targetDir, options = {}) {
175
204
  * @param {object} toolConfig - AI tool configuration
176
205
  */
177
206
  async function copySkillFolder(targetPath, options = {}, toolConfig = {}) {
178
- const force = options.force || false;
207
+ // Default to force overwrite — always keep skill folder up to date
208
+ const force = options.force !== false;
179
209
 
180
210
  // Check if skill folder already exists
181
211
  if (fs.existsSync(targetPath)) {
182
- if (!force) {
183
- console.log(chalk.yellow(` ⚠️ Skill folder already exists: ${targetPath}`));
184
- console.log(chalk.yellow(' Use --force to overwrite.'));
185
- return;
212
+ if (force) {
213
+ console.log(chalk.yellow(` 🔄 Updating existing skill folder...`));
214
+ fs.rmSync(targetPath, { recursive: true });
186
215
  }
187
- console.log(chalk.yellow(` 🔄 Overwriting existing skill folder...`));
188
- fs.rmSync(targetPath, { recursive: true });
189
216
  }
190
217
 
191
218
  // Create parent directories if they don't exist
@@ -231,6 +258,27 @@ async function copySkillFolder(targetPath, options = {}, toolConfig = {}) {
231
258
  copyDirectoryRecursive(fallbackDir, rulesTargetDir);
232
259
  }
233
260
  }
261
+
262
+ // 3. Copy framework-specific rules if a frameworkKey was detected
263
+ installFrameworkRules(targetPath, options);
264
+ }
265
+
266
+ /**
267
+ * Install framework-specific rules into an existing skill folder.
268
+ * Safe to call even if the rules subfolder already exists (overwrites).
269
+ * @param {string} targetPath - Path to the installed skill folder
270
+ * @param {object} options - Must contain options.frameworkKey if applicable
271
+ */
272
+ function installFrameworkRules(targetPath, options = {}) {
273
+ const frameworkKey = options.frameworkKey;
274
+ if (!frameworkKey) return;
275
+
276
+ const frameworkRulesDir = path.join(SKILL_SOURCE_DIR, 'rules', frameworkKey);
277
+ if (fs.existsSync(frameworkRulesDir)) {
278
+ const frameworkRulesTargetDir = path.join(targetPath, 'rules', frameworkKey);
279
+ console.log(chalk.blue(` 🏗️ Installing ${frameworkKey} framework rules...`));
280
+ copyDirectoryRecursive(frameworkRulesDir, frameworkRulesTargetDir);
281
+ }
234
282
  }
235
283
 
236
284
  /**