@sun-asterisk/sunlint 1.3.48 → 1.3.49

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 +1 -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
@@ -0,0 +1,517 @@
1
+ /**
2
+ * SunLint Project Detector
3
+ * Analyzes project directory structure to recommend a language/framework preset.
4
+ * Rule C005: Single responsibility - detect project type only
5
+ * Rule C015: Uses domain language (preset, framework, confidence)
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * @typedef {Object} DetectionResult
15
+ * @property {string} language - Matched language key (e.g. 'php', 'typescript')
16
+ * @property {string} framework - Detected framework name (e.g. 'Laravel', 'Next.js')
17
+ * @property {string|null} frameworkKey - Rules sub-folder key (e.g. 'php-laravel', 'ruby-rails', 'go-gin') or null
18
+ * @property {string} confidence - 'high' | 'medium' | 'low'
19
+ * @property {string} reason - Human-readable explanation
20
+ * @property {string[]} signals - File/dir signals that triggered the match
21
+ */
22
+
23
+ /**
24
+ * Ordered detection rules. First match wins.
25
+ * Each rule: { check(dir) → string[] | null, language, framework, confidence }
26
+ */
27
+ const DETECTION_RULES = [
28
+ // ── PHP ──────────────────────────────────────────────────────────────────
29
+ {
30
+ name: 'PHP/Laravel',
31
+ language: 'php',
32
+ framework: 'Laravel',
33
+ frameworkKey: 'php-laravel',
34
+ confidence: 'high',
35
+ check(dir) {
36
+ const signals = [];
37
+ if (fileExists(dir, 'artisan')) signals.push('artisan');
38
+ if (fileExists(dir, 'composer.json')) {
39
+ signals.push('composer.json');
40
+ const composerJson = readJsonSafe(path.join(dir, 'composer.json'));
41
+ if (composerJson && composerJson.require && composerJson.require['laravel/framework']) {
42
+ signals.push('require.laravel/framework');
43
+ }
44
+ }
45
+ if (dirExists(dir, 'vendor', 'laravel')) signals.push('vendor/laravel/');
46
+
47
+ const isLaravel = signals.includes('artisan') ||
48
+ signals.includes('require.laravel/framework') ||
49
+ signals.includes('vendor/laravel/');
50
+ return isLaravel && signals.length >= 2 ? signals : null;
51
+ }
52
+ },
53
+ {
54
+ name: 'PHP/Symfony',
55
+ language: 'php',
56
+ framework: 'Symfony',
57
+ confidence: 'high',
58
+ check(dir) {
59
+ const signals = [];
60
+ if (fileExists(dir, 'composer.json')) signals.push('composer.json');
61
+ if (fileExists(dir, 'symfony.lock')) signals.push('symfony.lock');
62
+ if (dirExists(dir, 'config', 'packages')) signals.push('config/packages/');
63
+ const composerJson = readJsonSafe(path.join(dir, 'composer.json'));
64
+ if (composerJson && composerJson.require && composerJson.require['symfony/framework-bundle']) {
65
+ signals.push('require.symfony/framework-bundle');
66
+ }
67
+ return signals.length >= 2 ? signals : null;
68
+ }
69
+ },
70
+ {
71
+ name: 'PHP',
72
+ language: 'php',
73
+ framework: 'PHP',
74
+ confidence: 'medium',
75
+ check(dir) {
76
+ const signals = [];
77
+ if (fileExists(dir, 'composer.json')) signals.push('composer.json');
78
+ if (fileExists(dir, 'composer.lock')) signals.push('composer.lock');
79
+ if (hasFilesWithExtension(dir, '.php', 2)) signals.push('*.php files');
80
+ return signals.length >= 1 ? signals : null;
81
+ }
82
+ },
83
+
84
+ // ── Dart / Flutter ───────────────────────────────────────────────────────
85
+ {
86
+ name: 'Dart/Flutter',
87
+ language: 'dart',
88
+ framework: 'Flutter',
89
+ confidence: 'high',
90
+ check(dir) {
91
+ const signals = [];
92
+ if (fileExists(dir, 'pubspec.yaml')) signals.push('pubspec.yaml');
93
+ const pubspec = readYamlLineSafe(path.join(dir, 'pubspec.yaml'), 'flutter');
94
+ if (pubspec) signals.push('pubspec.yaml → flutter dependency');
95
+ if (dirExists(dir, 'lib')) signals.push('lib/');
96
+ if (dirExists(dir, 'android') || dirExists(dir, 'ios')) signals.push('android/ or ios/');
97
+ return fileExists(dir, 'pubspec.yaml') ? signals : null;
98
+ }
99
+ },
100
+
101
+ // ── Kotlin ───────────────────────────────────────────────────────────────
102
+ {
103
+ name: 'Kotlin/Android',
104
+ language: 'kotlin',
105
+ framework: 'Android',
106
+ confidence: 'high',
107
+ check(dir) {
108
+ const signals = [];
109
+ if (fileExists(dir, 'build.gradle') || fileExists(dir, 'build.gradle.kts')) {
110
+ signals.push('build.gradle');
111
+ }
112
+ if (fileExists(dir, 'gradlew')) signals.push('gradlew');
113
+ if (dirExists(dir, 'app', 'src', 'main')) signals.push('app/src/main/');
114
+ if (hasFilesWithExtension(dir, '.kt', 2)) signals.push('*.kt files');
115
+ const hasAndroidManifest = fileExists(dir, 'app', 'src', 'main', 'AndroidManifest.xml');
116
+ if (hasAndroidManifest) signals.push('AndroidManifest.xml');
117
+ return (signals.includes('*.kt files') || hasAndroidManifest) ? signals : null;
118
+ }
119
+ },
120
+ {
121
+ name: 'Kotlin',
122
+ language: 'kotlin',
123
+ framework: 'Kotlin',
124
+ confidence: 'medium',
125
+ check(dir) {
126
+ const signals = [];
127
+ if (fileExists(dir, 'build.gradle.kts')) signals.push('build.gradle.kts');
128
+ if (hasFilesWithExtension(dir, '.kt', 1)) signals.push('*.kt files');
129
+ return signals.length >= 1 ? signals : null;
130
+ }
131
+ },
132
+
133
+ // ── Java ─────────────────────────────────────────────────────────────────
134
+ {
135
+ name: 'Java/Spring Boot',
136
+ language: 'java',
137
+ framework: 'Spring Boot',
138
+ confidence: 'high',
139
+ check(dir) {
140
+ const signals = [];
141
+ if (fileExists(dir, 'pom.xml')) signals.push('pom.xml');
142
+ if (fileExists(dir, 'build.gradle') || fileExists(dir, 'gradlew')) signals.push('build.gradle');
143
+ const pom = readFileSafeu(path.join(dir, 'pom.xml'));
144
+ if (pom && pom.includes('spring-boot')) signals.push('pom.xml → spring-boot');
145
+ if (dirExists(dir, 'src', 'main', 'java')) signals.push('src/main/java/');
146
+ return (signals.includes('pom.xml → spring-boot') || signals.includes('src/main/java/')) ? signals : null;
147
+ }
148
+ },
149
+ {
150
+ name: 'Java',
151
+ language: 'java',
152
+ framework: 'Java',
153
+ confidence: 'medium',
154
+ check(dir) {
155
+ const signals = [];
156
+ if (fileExists(dir, 'pom.xml')) signals.push('pom.xml');
157
+ if (fileExists(dir, 'build.gradle')) signals.push('build.gradle');
158
+ if (hasFilesWithExtension(dir, '.java', 2)) signals.push('*.java files');
159
+ return signals.length >= 1 ? signals : null;
160
+ }
161
+ },
162
+
163
+ // ── C# / .NET ────────────────────────────────────────────────────────────
164
+ {
165
+ name: 'C#/.NET',
166
+ language: 'csharp',
167
+ framework: '.NET',
168
+ confidence: 'high',
169
+ check(dir) {
170
+ const signals = [];
171
+ if (hasFilesWithExtension(dir, '.csproj', 1)) signals.push('*.csproj');
172
+ if (hasFilesWithExtension(dir, '.sln', 1)) signals.push('*.sln');
173
+ if (fileExists(dir, 'global.json')) signals.push('global.json');
174
+ if (hasFilesWithExtension(dir, '.cs', 2)) signals.push('*.cs files');
175
+ return signals.length >= 1 ? signals : null;
176
+ }
177
+ },
178
+
179
+ // ── Swift / iOS ──────────────────────────────────────────────────────────
180
+ {
181
+ name: 'Swift/iOS',
182
+ language: 'swift',
183
+ framework: 'iOS/macOS',
184
+ confidence: 'high',
185
+ check(dir) {
186
+ const signals = [];
187
+ if (hasFilesWithExtension(dir, '.xcodeproj', 1)) signals.push('*.xcodeproj');
188
+ if (hasFilesWithExtension(dir, '.xcworkspace', 1)) signals.push('*.xcworkspace');
189
+ if (fileExists(dir, 'Podfile')) signals.push('Podfile');
190
+ if (fileExists(dir, 'Package.swift')) signals.push('Package.swift');
191
+ if (hasFilesWithExtension(dir, '.swift', 2)) signals.push('*.swift files');
192
+ return signals.length >= 1 ? signals : null;
193
+ }
194
+ },
195
+
196
+ // ── Go ───────────────────────────────────────────────────────────────────
197
+ {
198
+ name: 'Go/Gin',
199
+ language: 'go',
200
+ framework: 'Gin',
201
+ frameworkKey: 'go-gin',
202
+ confidence: 'high',
203
+ check(dir) {
204
+ const signals = [];
205
+ if (fileExists(dir, 'go.mod')) {
206
+ signals.push('go.mod');
207
+ const goMod = readFileSafe(path.join(dir, 'go.mod'));
208
+ if (goMod && goMod.includes('github.com/gin-gonic/gin')) {
209
+ signals.push('go.mod → gin-gonic/gin');
210
+ }
211
+ }
212
+ if (fileExists(dir, 'go.sum')) signals.push('go.sum');
213
+ return signals.includes('go.mod → gin-gonic/gin') ? signals : null;
214
+ }
215
+ },
216
+ {
217
+ name: 'Go',
218
+ language: 'go',
219
+ framework: 'Go',
220
+ frameworkKey: null,
221
+ confidence: 'high',
222
+ check(dir) {
223
+ const signals = [];
224
+ if (fileExists(dir, 'go.mod')) signals.push('go.mod');
225
+ if (fileExists(dir, 'go.sum')) signals.push('go.sum');
226
+ if (hasFilesWithExtension(dir, '.go', 2)) signals.push('*.go files');
227
+ return signals.length >= 1 ? signals : null;
228
+ }
229
+ },
230
+
231
+ // ── Ruby ─────────────────────────────────────────────────────────────────
232
+ {
233
+ name: 'Ruby/Rails',
234
+ language: 'ruby',
235
+ framework: 'Rails',
236
+ frameworkKey: 'ruby-rails',
237
+ confidence: 'high',
238
+ check(dir) {
239
+ const signals = [];
240
+ if (fileExists(dir, 'Gemfile')) signals.push('Gemfile');
241
+ if (fileExists(dir, 'config', 'application.rb')) signals.push('config/application.rb');
242
+ if (fileExists(dir, 'config', 'routes.rb')) signals.push('config/routes.rb');
243
+ if (fileExists(dir, 'Rakefile')) signals.push('Rakefile');
244
+ const gemfile = readFileSafe(path.join(dir, 'Gemfile'));
245
+ if (gemfile && gemfile.includes("gem 'rails'")) signals.push('Gemfile → rails gem');
246
+ return (signals.includes('config/routes.rb') || signals.includes('Gemfile → rails gem')) ? signals : null;
247
+ }
248
+ },
249
+ {
250
+ name: 'Ruby',
251
+ language: 'ruby',
252
+ framework: 'Ruby',
253
+ confidence: 'medium',
254
+ check(dir) {
255
+ const signals = [];
256
+ if (fileExists(dir, 'Gemfile')) signals.push('Gemfile');
257
+ if (fileExists(dir, 'Gemfile.lock')) signals.push('Gemfile.lock');
258
+ if (hasFilesWithExtension(dir, '.rb', 2)) signals.push('*.rb files');
259
+ return signals.length >= 1 ? signals : null;
260
+ }
261
+ },
262
+
263
+ // ── Python ───────────────────────────────────────────────────────────────
264
+ {
265
+ name: 'Python/Django',
266
+ language: 'python',
267
+ framework: 'Django',
268
+ confidence: 'high',
269
+ check(dir) {
270
+ const signals = [];
271
+ if (fileExists(dir, 'manage.py')) signals.push('manage.py');
272
+ if (fileExists(dir, 'requirements.txt')) signals.push('requirements.txt');
273
+ const req = readFileSafe(path.join(dir, 'requirements.txt'));
274
+ if (req && req.toLowerCase().includes('django')) signals.push('requirements.txt → django');
275
+ return signals.includes('manage.py') ? signals : null;
276
+ }
277
+ },
278
+ {
279
+ name: 'Python/FastAPI',
280
+ language: 'python',
281
+ framework: 'FastAPI',
282
+ confidence: 'high',
283
+ check(dir) {
284
+ const signals = [];
285
+ if (fileExists(dir, 'requirements.txt')) {
286
+ signals.push('requirements.txt');
287
+ const req = readFileSafe(path.join(dir, 'requirements.txt'));
288
+ if (req && req.toLowerCase().includes('fastapi')) signals.push('requirements.txt → fastapi');
289
+ }
290
+ if (fileExists(dir, 'pyproject.toml')) {
291
+ signals.push('pyproject.toml');
292
+ const pyp = readFileSafe(path.join(dir, 'pyproject.toml'));
293
+ if (pyp && pyp.toLowerCase().includes('fastapi')) signals.push('pyproject.toml → fastapi');
294
+ }
295
+ return signals.some(s => s.includes('fastapi')) ? signals : null;
296
+ }
297
+ },
298
+ {
299
+ name: 'Python',
300
+ language: 'python',
301
+ framework: 'Python',
302
+ confidence: 'medium',
303
+ check(dir) {
304
+ const signals = [];
305
+ if (fileExists(dir, 'requirements.txt')) signals.push('requirements.txt');
306
+ if (fileExists(dir, 'pyproject.toml')) signals.push('pyproject.toml');
307
+ if (fileExists(dir, 'setup.py')) signals.push('setup.py');
308
+ if (fileExists(dir, 'Pipfile')) signals.push('Pipfile');
309
+ if (hasFilesWithExtension(dir, '.py', 2)) signals.push('*.py files');
310
+ return signals.length >= 1 ? signals : null;
311
+ }
312
+ },
313
+
314
+ // ── TypeScript / JavaScript (Next.js, Nuxt, Angular, Vue, React, etc.) ──
315
+ {
316
+ name: 'TypeScript/Next.js',
317
+ language: 'typescript',
318
+ framework: 'Next.js',
319
+ confidence: 'high',
320
+ check(dir) {
321
+ const signals = [];
322
+ if (fileExists(dir, 'package.json')) signals.push('package.json');
323
+ if (fileExists(dir, 'next.config.js') || fileExists(dir, 'next.config.ts') || fileExists(dir, 'next.config.mjs')) {
324
+ signals.push('next.config.*');
325
+ }
326
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
327
+ if (pkg && pkg.dependencies && pkg.dependencies['next']) signals.push('package.json → next');
328
+ return signals.includes('next.config.*') || signals.includes('package.json → next') ? signals : null;
329
+ }
330
+ },
331
+ {
332
+ name: 'TypeScript/Nuxt',
333
+ language: 'typescript',
334
+ framework: 'Nuxt',
335
+ confidence: 'high',
336
+ check(dir) {
337
+ const signals = [];
338
+ if (fileExists(dir, 'package.json')) signals.push('package.json');
339
+ if (fileExists(dir, 'nuxt.config.js') || fileExists(dir, 'nuxt.config.ts')) signals.push('nuxt.config.*');
340
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
341
+ if (pkg && pkg.dependencies && (pkg.dependencies['nuxt'] || pkg.dependencies['nuxt3'])) signals.push('package.json → nuxt');
342
+ return signals.includes('nuxt.config.*') || signals.includes('package.json → nuxt') ? signals : null;
343
+ }
344
+ },
345
+ {
346
+ name: 'TypeScript/Angular',
347
+ language: 'typescript',
348
+ framework: 'Angular',
349
+ confidence: 'high',
350
+ check(dir) {
351
+ const signals = [];
352
+ if (fileExists(dir, 'angular.json')) signals.push('angular.json');
353
+ if (fileExists(dir, 'package.json')) {
354
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
355
+ if (pkg && pkg.dependencies && pkg.dependencies['@angular/core']) signals.push('package.json → @angular/core');
356
+ }
357
+ return signals.length >= 1 ? signals : null;
358
+ }
359
+ },
360
+ {
361
+ name: 'TypeScript/NestJS',
362
+ language: 'typescript',
363
+ framework: 'NestJS',
364
+ confidence: 'high',
365
+ check(dir) {
366
+ const signals = [];
367
+ if (fileExists(dir, 'nest-cli.json')) signals.push('nest-cli.json');
368
+ if (fileExists(dir, 'package.json')) {
369
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
370
+ if (pkg && pkg.dependencies && pkg.dependencies['@nestjs/core']) signals.push('package.json → @nestjs/core');
371
+ }
372
+ return signals.length >= 1 ? signals : null;
373
+ }
374
+ },
375
+ {
376
+ name: 'TypeScript/React',
377
+ language: 'typescript',
378
+ framework: 'React',
379
+ confidence: 'medium',
380
+ check(dir) {
381
+ const signals = [];
382
+ if (fileExists(dir, 'package.json')) {
383
+ signals.push('package.json');
384
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
385
+ if (pkg && pkg.dependencies && pkg.dependencies['react']) signals.push('package.json → react');
386
+ if (pkg && pkg.dependencies && pkg.dependencies['typescript']) signals.push('package.json → typescript');
387
+ }
388
+ if (fileExists(dir, 'tsconfig.json')) signals.push('tsconfig.json');
389
+ return signals.includes('package.json → react') ? signals : null;
390
+ }
391
+ },
392
+ {
393
+ name: 'TypeScript',
394
+ language: 'typescript',
395
+ framework: 'TypeScript',
396
+ confidence: 'medium',
397
+ check(dir) {
398
+ const signals = [];
399
+ if (fileExists(dir, 'tsconfig.json')) signals.push('tsconfig.json');
400
+ if (fileExists(dir, 'package.json')) signals.push('package.json');
401
+ if (hasFilesWithExtension(dir, '.ts', 2)) signals.push('*.ts files');
402
+ return signals.length >= 1 ? signals : null;
403
+ }
404
+ },
405
+ ];
406
+
407
+ // ─── Helpers ────────────────────────────────────────────────────────────────
408
+
409
+ function fileExists(dir, ...parts) {
410
+ try {
411
+ return fs.existsSync(path.join(dir, ...parts));
412
+ } catch {
413
+ return false;
414
+ }
415
+ }
416
+
417
+ function dirExists(dir, ...parts) {
418
+ try {
419
+ const p = path.join(dir, ...parts);
420
+ return fs.existsSync(p) && fs.statSync(p).isDirectory();
421
+ } catch {
422
+ return false;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Check if at least `minCount` files with the given extension exist (shallow scan + 1-level deep)
428
+ */
429
+ function hasFilesWithExtension(dir, ext, minCount = 1) {
430
+ try {
431
+ let count = 0;
432
+ const topItems = fs.readdirSync(dir);
433
+ for (const item of topItems) {
434
+ if (item.endsWith(ext)) count++;
435
+ if (count >= minCount) return true;
436
+ }
437
+ // One level deeper
438
+ for (const item of topItems) {
439
+ const subDir = path.join(dir, item);
440
+ try {
441
+ if (fs.statSync(subDir).isDirectory() && !item.startsWith('.') && item !== 'node_modules' && item !== 'vendor') {
442
+ const subItems = fs.readdirSync(subDir);
443
+ for (const sub of subItems) {
444
+ if (sub.endsWith(ext)) count++;
445
+ if (count >= minCount) return true;
446
+ }
447
+ }
448
+ } catch { /* skip */ }
449
+ }
450
+ return false;
451
+ } catch {
452
+ return false;
453
+ }
454
+ }
455
+
456
+ function readFileSafe(filePath) {
457
+ try {
458
+ return fs.readFileSync(filePath, 'utf8');
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+
464
+ function readJsonSafe(filePath) {
465
+ try {
466
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
467
+ } catch {
468
+ return null;
469
+ }
470
+ }
471
+
472
+ /** Check if a YAML file contains a top-level key (simple line-based check) */
473
+ function readYamlLineSafe(filePath, key) {
474
+ const content = readFileSafe(filePath);
475
+ if (!content) return null;
476
+ return content.split('\n').some(line => line.startsWith(key + ':')) ? key : null;
477
+ }
478
+
479
+ // ─── Public API ─────────────────────────────────────────────────────────────
480
+
481
+ /**
482
+ * Analyze a project directory and return the best-matched language preset.
483
+ * Returns null if no match found (caller should fall back to 'typescript').
484
+ *
485
+ * @param {string} projectDir - Absolute path to project root
486
+ * @returns {DetectionResult | null}
487
+ */
488
+ function detectProjectLanguage(projectDir) {
489
+ if (!fs.existsSync(projectDir)) return null;
490
+
491
+ for (const rule of DETECTION_RULES) {
492
+ try {
493
+ const signals = rule.check(projectDir);
494
+ if (signals && signals.length > 0) {
495
+ return {
496
+ language: rule.language,
497
+ framework: rule.framework,
498
+ frameworkKey: rule.frameworkKey || null,
499
+ confidence: rule.confidence,
500
+ reason: buildReason(rule, signals),
501
+ signals,
502
+ };
503
+ }
504
+ } catch {
505
+ // Skip broken checks silently
506
+ }
507
+ }
508
+
509
+ return null;
510
+ }
511
+
512
+ function buildReason(rule, signals) {
513
+ const sigStr = signals.slice(0, 3).join(', ');
514
+ return `${rule.framework} detected (${sigStr})`;
515
+ }
516
+
517
+ module.exports = { detectProjectLanguage };