@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,245 @@
1
+ /**
2
+ * SunLint TUI Select Widget
3
+ * Arrow-key driven interactive selection for terminal.
4
+ * Zero external dependencies - uses Node.js built-in readline + raw mode.
5
+ *
6
+ * Rule C005: Single responsibility - terminal selection UI only
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const readline = require('readline');
12
+
13
+ // ─── ANSI helpers ────────────────────────────────────────────────────────────
14
+ const ansi = {
15
+ saveCursor: '\x1b7',
16
+ restoreCursor: '\x1b8',
17
+ clearToEnd: '\x1b[J',
18
+ clearLine: '\x1b[2K\r',
19
+ hideCursor: '\x1b[?25l',
20
+ showCursor: '\x1b[?25h',
21
+ reset: '\x1b[0m',
22
+ bold: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ cyan: '\x1b[36m',
25
+ blue: '\x1b[34m',
26
+ green: '\x1b[32m',
27
+ yellow: '\x1b[33m',
28
+ magenta: '\x1b[35m',
29
+ white: '\x1b[37m',
30
+ gray: '\x1b[90m',
31
+ bgCyan: '\x1b[46m',
32
+ bgBlue: '\x1b[44m',
33
+ };
34
+
35
+ function c(colorKey, text) {
36
+ return `${ansi[colorKey] || ''}${text}${ansi.reset}`;
37
+ }
38
+
39
+ function bold(text) { return `${ansi.bold}${text}${ansi.reset}`; }
40
+ function dim(text) { return `${ansi.dim}${text}${ansi.reset}`; }
41
+
42
+ // ─── Banner ──────────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * Print the SunLint init banner
46
+ */
47
+ function printBanner() {
48
+ const line = '─'.repeat(58);
49
+ process.stdout.write('\n');
50
+ process.stdout.write(c('cyan', ` ┌${line}┐\n`));
51
+ process.stdout.write(c('cyan', ` │`) + bold(' ☀️ SunLint Init') + c('gray', ' · Interactive Project Setup') + c('cyan', ' │\n'));
52
+ process.stdout.write(c('cyan', ` └${line}┘\n`));
53
+ process.stdout.write('\n');
54
+ }
55
+
56
+ // ─── Detection info ──────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Print the auto-detection result hint.
60
+ * @param {import('./project-detector').DetectionResult | null} detection
61
+ */
62
+ function printDetectionHint(detection) {
63
+ if (!detection) {
64
+ process.stdout.write(c('gray', ' 🔍 No framework detected — default preset: TypeScript\n\n'));
65
+ return;
66
+ }
67
+
68
+ const badge = detection.confidence === 'high'
69
+ ? c('green', '● high confidence')
70
+ : c('yellow', '◐ medium confidence');
71
+
72
+ process.stdout.write(
73
+ ` ✨ ${bold('Auto-detected:')} ${c('cyan', detection.framework)} ${badge}\n`
74
+ );
75
+ process.stdout.write(
76
+ c('gray', ` Signals: ${detection.signals.slice(0, 4).join(', ')}\n`)
77
+ );
78
+ process.stdout.write('\n');
79
+ }
80
+
81
+ // ─── Core TUI select ─────────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Show an interactive arrow-key selection list.
85
+ *
86
+ * @param {Object} params
87
+ * @param {string} params.question - Prompt label
88
+ * @param {string[]} params.options - List of option values
89
+ * @param {Object} [params.labels] - Optional display label map { value: 'Label' }
90
+ * @param {Object} [params.badges] - Optional badge map { value: '[badge text]' }
91
+ * @param {number} [params.defaultIndex] - Pre-selected index (0-based)
92
+ * @returns {Promise<string>} - The selected option value
93
+ */
94
+ function tuiSelect({ question, options, labels = {}, badges = {}, defaultIndex = 0 }) {
95
+ return new Promise((resolve, reject) => {
96
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
97
+ resolve(options[defaultIndex] || options[0]);
98
+ return;
99
+ }
100
+
101
+ let selected = Math.max(0, Math.min(defaultIndex, options.length - 1));
102
+
103
+ function renderLabel(value) {
104
+ return labels[value] || value;
105
+ }
106
+
107
+ function renderBadge(value) {
108
+ return badges[value] ? c('green', ` ${badges[value]}`) : '';
109
+ }
110
+
111
+ function render(firstRender = false) {
112
+ if (firstRender) {
113
+ // Save cursor position BEFORE first render
114
+ process.stdout.write(ansi.saveCursor);
115
+ } else {
116
+ // Restore to saved position, then clear everything below
117
+ process.stdout.write(ansi.restoreCursor + ansi.clearToEnd);
118
+ }
119
+
120
+ // Question line
121
+ process.stdout.write(
122
+ ` ${c('cyan', '?')} ${bold(question)}\n`
123
+ );
124
+
125
+ // Hint line
126
+ process.stdout.write(
127
+ ` ${dim('Use ↑↓ arrows · Enter to confirm · Esc to cancel')}\n\n`
128
+ );
129
+
130
+ // Option lines
131
+ for (let i = 0; i < options.length; i++) {
132
+ const value = options[i];
133
+ const label = renderLabel(value);
134
+ const badge = renderBadge(value);
135
+ const isActive = i === selected;
136
+
137
+ const pointer = isActive ? c('cyan', ' ❯ ') : ' ';
138
+ const dot = isActive ? c('cyan', '●') : c('gray', '○');
139
+ const text = isActive
140
+ ? bold(c('white', label))
141
+ : c('gray', label);
142
+
143
+ process.stdout.write(`${pointer}${dot} ${text}${badge}\n`);
144
+ }
145
+ }
146
+
147
+ // Hide cursor and switch to raw mode
148
+ process.stdout.write(ansi.hideCursor);
149
+ process.stdin.setRawMode(true);
150
+ process.stdin.resume();
151
+ process.stdin.setEncoding('utf8');
152
+
153
+ render(true);
154
+
155
+ function cleanup() {
156
+ process.stdin.setRawMode(false);
157
+ process.stdin.pause();
158
+ process.stdout.write(ansi.showCursor);
159
+ }
160
+
161
+ function onKeypress(key) {
162
+ if (key === '\u001b[A' || key === '\u001b[D') {
163
+ // Up / Left
164
+ selected = (selected - 1 + options.length) % options.length;
165
+ render();
166
+ } else if (key === '\u001b[B' || key === '\u001b[C') {
167
+ // Down / Right
168
+ selected = (selected + 1) % options.length;
169
+ render();
170
+ } else if (key === '\r' || key === '\n') {
171
+ // Enter
172
+ cleanup();
173
+ process.stdin.removeListener('data', onKeypress);
174
+
175
+ // Replace the whole rendered block with a single summary line
176
+ const value = options[selected];
177
+ const label = renderLabel(value);
178
+ const badge = renderBadge(value);
179
+ process.stdout.write(
180
+ ansi.restoreCursor + ansi.clearToEnd +
181
+ ` ${c('green', '✔')} ${bold(question)} ${c('cyan', label)}${badge}\n\n`
182
+ );
183
+
184
+ resolve(value);
185
+ } else if (key === '\u001b' || key === '\u0003') {
186
+ // Esc / Ctrl+C
187
+ cleanup();
188
+ process.stdin.removeListener('data', onKeypress);
189
+ process.stdout.write(ansi.restoreCursor + ansi.clearToEnd + '\n');
190
+ reject(new Error('Cancelled by user'));
191
+ }
192
+ }
193
+
194
+ process.stdin.on('data', onKeypress);
195
+ });
196
+ }
197
+
198
+ // ─── Confirm prompt ──────────────────────────────────────────────────────────
199
+
200
+ /**
201
+ * Simple yes/no confirmation.
202
+ * @param {string} question
203
+ * @param {boolean} [defaultYes=true]
204
+ * @returns {Promise<boolean>}
205
+ */
206
+ function tuiConfirm(question, defaultYes = true) {
207
+ return new Promise((resolve, reject) => {
208
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
209
+ resolve(defaultYes);
210
+ return;
211
+ }
212
+
213
+ const hint = defaultYes ? '(Y/n)' : '(y/N)';
214
+ process.stdout.write(` ${c('cyan', '?')} ${bold(question)} ${c('gray', hint)} `);
215
+
216
+ process.stdin.setRawMode(true);
217
+ process.stdin.resume();
218
+ process.stdin.setEncoding('utf8');
219
+
220
+ function onKey(key) {
221
+ process.stdin.setRawMode(false);
222
+ process.stdin.pause();
223
+ process.stdin.removeListener('data', onKey);
224
+
225
+ if (key === '\u0003') {
226
+ process.stdout.write('\n');
227
+ reject(new Error('Cancelled by user'));
228
+ return;
229
+ }
230
+
231
+ const input = key.toLowerCase().trim();
232
+ let answer;
233
+ if (input === 'y') answer = true;
234
+ else if (input === 'n') answer = false;
235
+ else answer = defaultYes;
236
+
237
+ process.stdout.write(`${answer ? c('green', 'Yes') : c('yellow', 'No')}\n`);
238
+ resolve(answer);
239
+ }
240
+
241
+ process.stdin.on('data', onKey);
242
+ });
243
+ }
244
+
245
+ module.exports = { printBanner, printDetectionHint, tuiSelect, tuiConfirm };
@@ -125,8 +125,8 @@ let L001PresentationLayer = (() => {
125
125
  }
126
126
  async customDetection(context) {
127
127
  const matches = [];
128
- let hasFolders = false;
129
- let hasFiles = false;
128
+ let _hasFolders = false;
129
+ let _hasFiles = false;
130
130
  let hasCode = false;
131
131
  // Check for controller folders
132
132
  const controllerFolders = [
@@ -147,7 +147,7 @@ let L001PresentationLayer = (() => {
147
147
  path: folder,
148
148
  matchedPattern: 'Controller folder',
149
149
  });
150
- hasFolders = true;
150
+ _hasFolders = true;
151
151
  }
152
152
  }
153
153
  // Check for controller files
@@ -162,7 +162,7 @@ let L001PresentationLayer = (() => {
162
162
  matchedPattern: 'Controller file',
163
163
  });
164
164
  }
165
- hasFiles = true;
165
+ _hasFiles = true;
166
166
  }
167
167
  // Check for code patterns (sampling first 50 source files)
168
168
  const sourceFiles = context.files
@@ -201,17 +201,9 @@ let L001PresentationLayer = (() => {
201
201
  // Skip unreadable files
202
202
  }
203
203
  }
204
- // Calculate score based on presence
205
- let score = 0;
206
- if (hasFolders && hasFiles)
207
- score = 1;
208
- else if (hasFiles)
209
- score = 0.7;
210
- else if (hasCode)
211
- score = 0.6;
212
- else if (hasFolders)
213
- score = 0.4;
214
- return { matches, violations: [], score };
204
+ // Let BaseRule's language-aware scoring handle the score calculation
205
+ // customDetection only contributes additional matches
206
+ return { matches, violations: [] };
215
207
  }
216
208
  getMetadata(context, matches) {
217
209
  const controllerCount = matches.filter((m) => m.matchedPattern === 'Controller file').length;
@@ -105,8 +105,8 @@ let L002BusinessLayer = (() => {
105
105
  }
106
106
  async customDetection(context) {
107
107
  const matches = [];
108
- let hasFolders = false;
109
- let hasFiles = false;
108
+ let _hasFolders = false;
109
+ let _hasFiles = false;
110
110
  let hasCode = false;
111
111
  // Check for service folders
112
112
  const serviceFolders = [
@@ -128,7 +128,7 @@ let L002BusinessLayer = (() => {
128
128
  path: folder,
129
129
  matchedPattern: 'Service folder',
130
130
  });
131
- hasFolders = true;
131
+ _hasFolders = true;
132
132
  }
133
133
  }
134
134
  // Check for service files
@@ -144,7 +144,7 @@ let L002BusinessLayer = (() => {
144
144
  matchedPattern: 'Service file',
145
145
  });
146
146
  }
147
- hasFiles = true;
147
+ _hasFiles = true;
148
148
  }
149
149
  // Check for code patterns
150
150
  const sourceFiles = context.files
@@ -179,17 +179,9 @@ let L002BusinessLayer = (() => {
179
179
  // Skip unreadable files
180
180
  }
181
181
  }
182
- // Calculate score
183
- let score = 0;
184
- if (hasFolders && hasFiles)
185
- score = 1;
186
- else if (hasFiles)
187
- score = 0.7;
188
- else if (hasCode)
189
- score = 0.6;
190
- else if (hasFolders)
191
- score = 0.4;
192
- return { matches, violations: [], score };
182
+ // Let BaseRule's language-aware scoring handle the score calculation
183
+ // customDetection only contributes additional matches
184
+ return { matches, violations: [] };
193
185
  }
194
186
  getMetadata(context, matches) {
195
187
  const serviceCount = matches.filter((m) => m.matchedPattern === 'Service file').length;
@@ -119,8 +119,8 @@ let L003DataLayer = (() => {
119
119
  }
120
120
  async customDetection(context) {
121
121
  const matches = [];
122
- let hasFolders = false;
123
- let hasFiles = false;
122
+ let _hasFolders = false;
123
+ let _hasFiles = false;
124
124
  let hasCode = false;
125
125
  // Check for repository folders
126
126
  const repoFolders = [
@@ -142,7 +142,7 @@ let L003DataLayer = (() => {
142
142
  path: folder,
143
143
  matchedPattern: 'Repository folder',
144
144
  });
145
- hasFolders = true;
145
+ _hasFolders = true;
146
146
  }
147
147
  }
148
148
  // Check for repository files
@@ -158,7 +158,7 @@ let L003DataLayer = (() => {
158
158
  matchedPattern: 'Repository file',
159
159
  });
160
160
  }
161
- hasFiles = true;
161
+ _hasFiles = true;
162
162
  }
163
163
  // Check for code patterns
164
164
  const sourceFiles = context.files
@@ -193,17 +193,9 @@ let L003DataLayer = (() => {
193
193
  // Skip unreadable files
194
194
  }
195
195
  }
196
- // Calculate score
197
- let score = 0;
198
- if (hasFolders && hasFiles)
199
- score = 1;
200
- else if (hasFiles)
201
- score = 0.7;
202
- else if (hasCode)
203
- score = 0.6;
204
- else if (hasFolders)
205
- score = 0.4;
206
- return { matches, violations: [], score };
196
+ // Let BaseRule's language-aware scoring handle the score calculation
197
+ // customDetection only contributes additional matches
198
+ return { matches, violations: [] };
207
199
  }
208
200
  getMetadata(context, matches) {
209
201
  const repoCount = matches.filter((m) => m.matchedPattern === 'Repository file').length;
@@ -91,8 +91,8 @@ let L004ModelLayer = (() => {
91
91
  }
92
92
  async customDetection(context) {
93
93
  const matches = [];
94
- let hasFolders = false;
95
- let hasFiles = false;
94
+ let _hasFolders = false;
95
+ let _hasFiles = false;
96
96
  let hasCode = false;
97
97
  // Check for model folders
98
98
  const modelFolders = [
@@ -114,7 +114,7 @@ let L004ModelLayer = (() => {
114
114
  path: folder,
115
115
  matchedPattern: 'Model folder',
116
116
  });
117
- hasFolders = true;
117
+ _hasFolders = true;
118
118
  }
119
119
  }
120
120
  // Check for model/entity files
@@ -132,7 +132,7 @@ let L004ModelLayer = (() => {
132
132
  matchedPattern: 'Model file',
133
133
  });
134
134
  }
135
- hasFiles = true;
135
+ _hasFiles = true;
136
136
  }
137
137
  // Check for code patterns
138
138
  const sourceFiles = context.files
@@ -168,17 +168,9 @@ let L004ModelLayer = (() => {
168
168
  // Skip unreadable files
169
169
  }
170
170
  }
171
- // Calculate score
172
- let score = 0;
173
- if (hasFolders && hasFiles)
174
- score = 1;
175
- else if (hasFiles)
176
- score = 0.7;
177
- else if (hasCode)
178
- score = 0.6;
179
- else if (hasFolders)
180
- score = 0.4;
181
- return { matches, violations: [], score };
171
+ // Let BaseRule's language-aware scoring handle the score calculation
172
+ // customDetection only contributes additional matches
173
+ return { matches, violations: [] };
182
174
  }
183
175
  getMetadata(context, matches) {
184
176
  const modelCount = matches.filter((m) => m.matchedPattern === 'Model file').length;
@@ -175,8 +175,28 @@ let L005LayerSeparation = (() => {
175
175
  .filter((f) => !f.isTest && !f.isConfig && f.language)
176
176
  .slice(0, 30);
177
177
  const layerIndicators = {
178
- presentation: [/@Controller\b/, /@Get\(/, /@Post\(/, /router\.(get|post)/],
179
- data: [/@Repository\b/, /DbContext/, /\.query\(/, /\.find\(/],
178
+ presentation: [
179
+ /@Controller\b/, // Java Spring
180
+ /@RestController\b/, // Java Spring
181
+ /@Get\(/, // NestJS
182
+ /@Post\(/, // NestJS
183
+ /router\.(get|post)/, // Express
184
+ /\[ApiController\]/, // C#
185
+ /@app\.route\s*\(/, // Python Flask
186
+ /Route::(get|post)\s*\(/, // PHP Laravel
187
+ /r\.HandleFunc\s*\(/, // Go
188
+ /gin\.Context/, // Go Gin
189
+ ],
190
+ data: [
191
+ /@Repository\b/, // Java Spring
192
+ /DbContext/, // C# EF
193
+ /\.query\s*\(/, // Generic
194
+ /DB::table\s*\(/, // PHP Laravel
195
+ /models\.Model/, // Python Django
196
+ /type\s+\w+Repository\s+struct/, // Go
197
+ /extends\s+Model\b/, // PHP Eloquent
198
+ /extends\s+(Jpa|Crud|Mongo)Repository\b/, // Java Spring Data
199
+ ],
180
200
  };
181
201
  for (const file of sourceFiles) {
182
202
  try {
@@ -118,11 +118,14 @@ let L006DependencyDirection = (() => {
118
118
  .filter((f) => !f.isTest && !f.isConfig && f.language)
119
119
  .slice(0, 50);
120
120
  const importPatterns = [
121
- /import\s+.*\s+from\s+['"]([^'"]+)['"]/g,
122
- /import\s+['"]([^'"]+)['"]/g,
123
- /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
124
- /from\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+import/g,
125
- /using\s+([a-zA-Z_.]+);/g,
121
+ /import\s+.*\s+from\s+['"]([^'"]+)['"]/g, // ES6 import
122
+ /import\s+['"]([^'"]+)['"]/g, // ES6 side-effect import
123
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, // CommonJS / Ruby require
124
+ /from\s+([a-zA-Z_][\w.]*)\s+import/g, // Python from...import
125
+ /using\s+([a-zA-Z_.]+);/g, // C# using
126
+ /import\s+([\w.]+);/g, // Java/Kotlin import
127
+ /use\s+([\w\\]+)/g, // PHP use
128
+ /import\s+"([^"]+)"/g, // Go import
126
129
  ];
127
130
  for (const file of sourceFiles) {
128
131
  const fromLayer = this.classifyFileLayer(file);
@@ -71,43 +71,81 @@ let M005NoDeepImports = (() => {
71
71
  // Not enough modules to check cross-module imports
72
72
  return { matches, violations, score: 0.5 };
73
73
  }
74
- // Check imports in source files
74
+ // Check imports in source files (all recognized languages)
75
75
  const sourceFiles = context.files
76
- .filter((f) => !f.isTest &&
77
- !f.isConfig &&
78
- (f.extension === '.ts' || f.extension === '.js' || f.extension === '.tsx'))
76
+ .filter((f) => !f.isTest && !f.isConfig && f.language != null)
79
77
  .slice(0, 50);
80
- const importPattern = /import\s+.*\s+from\s+['"]([^'"]+)['"]/g;
78
+ // Language-aware import patterns
79
+ const importPatterns = [
80
+ // TypeScript/JavaScript ES6 imports
81
+ {
82
+ pattern: /import\s+.*\s+from\s+['"]([^'"]+)['"]/g,
83
+ languages: new Set(['typescript', 'javascript']),
84
+ },
85
+ // TypeScript/JavaScript require
86
+ {
87
+ pattern: /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
88
+ languages: new Set(['typescript', 'javascript', 'ruby']),
89
+ },
90
+ // Java/Kotlin imports
91
+ { pattern: /import\s+([\w.]+)/g, languages: new Set(['java', 'kotlin']) },
92
+ // Python imports
93
+ { pattern: /from\s+([\w.]+)\s+import/g, languages: new Set(['python']) },
94
+ // PHP use statements
95
+ { pattern: /use\s+([\w\\]+)/g, languages: new Set(['php']) },
96
+ // Go imports
97
+ { pattern: /import\s+"([^"]+)"/g, languages: new Set(['go']) },
98
+ // C# using
99
+ { pattern: /using\s+([\w.]+);/g, languages: new Set(['csharp']) },
100
+ ];
81
101
  for (const file of sourceFiles) {
82
102
  try {
83
103
  const content = await context.helpers.readFile(file.absolutePath);
84
104
  const fileModulePath = this.getModulePath(file.relativePath, modulePaths);
85
- let match;
86
- while ((match = importPattern.exec(content)) !== null) {
87
- const importPath = match[1];
88
- // Skip external packages
89
- if (!importPath.startsWith('.') &&
90
- !importPath.startsWith('@app') &&
91
- !importPath.startsWith('@/')) {
92
- continue;
93
- }
94
- // Check if this is a cross-module import
95
- const targetModulePath = this.resolveImportToModule(importPath, file.relativePath, modulePaths);
96
- if (targetModulePath && targetModulePath !== fileModulePath) {
97
- // Cross-module import detected
98
- const isDeepImport = this.isDeepImport(importPath, targetModulePath);
99
- if (isDeepImport) {
100
- deepImportCount++;
101
- violations.push({
102
- type: 'deep_import',
103
- severity: enums_1.Severity.WARNING,
104
- path: file.relativePath,
105
- message: `Deep import into ${targetModulePath}: ${importPath}`,
106
- suggestion: `Import from ${targetModulePath}/index instead`,
107
- });
105
+ const fileLang = file.language || '';
106
+ // Select import patterns relevant to this file's language
107
+ const relevantPatterns = importPatterns.filter((ip) => ip.languages.has(fileLang));
108
+ for (const { pattern } of relevantPatterns) {
109
+ pattern.lastIndex = 0;
110
+ let match;
111
+ while ((match = pattern.exec(content)) !== null) {
112
+ const importPath = match[1];
113
+ // Skip external packages based on language
114
+ if (fileLang === 'typescript' || fileLang === 'javascript') {
115
+ if (!importPath.startsWith('.') &&
116
+ !importPath.startsWith('@app') &&
117
+ !importPath.startsWith('@/')) {
118
+ continue;
119
+ }
108
120
  }
109
121
  else {
110
- validImportCount++;
122
+ // For other languages, use folder-based module path resolution
123
+ // Skip if import path doesn't match any known module path segment
124
+ const matchesModule = modulePaths.some((mp) => {
125
+ const moduleName = mp.split('/').pop()?.toLowerCase() || '';
126
+ return importPath.toLowerCase().includes(moduleName);
127
+ });
128
+ if (!matchesModule)
129
+ continue;
130
+ }
131
+ // Check if this is a cross-module import
132
+ const targetModulePath = this.resolveImportToModule(importPath, file.relativePath, modulePaths);
133
+ if (targetModulePath && targetModulePath !== fileModulePath) {
134
+ // Cross-module import detected
135
+ const isDeepImport = this.isDeepImport(importPath, targetModulePath);
136
+ if (isDeepImport) {
137
+ deepImportCount++;
138
+ violations.push({
139
+ type: 'deep_import',
140
+ severity: enums_1.Severity.WARNING,
141
+ path: file.relativePath,
142
+ message: `Deep import into ${targetModulePath}: ${importPath}`,
143
+ suggestion: `Import from ${targetModulePath}/index instead`,
144
+ });
145
+ }
146
+ else {
147
+ validImportCount++;
148
+ }
111
149
  }
112
150
  }
113
151
  }
@@ -144,13 +144,17 @@ let PR001ViewLayer = (() => {
144
144
  }
145
145
  }
146
146
  // Check for view files
147
- const viewFiles = context.files.filter((f) => /View\.(ts|tsx|js|jsx|swift|kt|dart)$/i.test(f.fileName) ||
148
- /Screen\.(ts|tsx|js|jsx|swift|kt|dart)$/i.test(f.fileName) ||
149
- /Page\.(ts|tsx|js|jsx|swift|kt|dart)$/i.test(f.fileName) ||
147
+ const viewFiles = context.files.filter((f) => /View\.(ts|tsx|js|jsx|swift|kt|dart|java|php|py|go|rb|cs)$/i.test(f.fileName) ||
148
+ /Screen\.(ts|tsx|js|jsx|swift|kt|dart|java|php|py|go|rb|cs)$/i.test(f.fileName) ||
149
+ /Page\.(ts|tsx|js|jsx|swift|kt|dart|java|php|py|go|rb|cs)$/i.test(f.fileName) ||
150
150
  /Activity\.(kt|java)$/i.test(f.fileName) ||
151
151
  /Fragment\.(kt|java)$/i.test(f.fileName) ||
152
152
  /ViewController\.swift$/i.test(f.fileName) ||
153
- /\.vue$/i.test(f.fileName));
153
+ /\.vue$/i.test(f.fileName) ||
154
+ /\.blade\.php$/i.test(f.fileName) ||
155
+ /\.twig$/i.test(f.fileName) ||
156
+ /\.cshtml$/i.test(f.fileName) ||
157
+ /\.html\.erb$/i.test(f.fileName));
154
158
  if (viewFiles.length > 0) {
155
159
  for (const file of viewFiles.slice(0, 5)) {
156
160
  matches.push({
@@ -166,11 +170,14 @@ let PR001ViewLayer = (() => {
166
170
  .filter((f) => !f.isTest && !f.isConfig && f.language)
167
171
  .slice(0, 50);
168
172
  const codePatterns = [
169
- /@Composable\s+fun/,
170
- /struct\s+\w+View\s*:\s*View/,
171
- /class\s+\w+ViewController/,
172
- /extends\s+(StatefulWidget|StatelessWidget)/,
173
- /<template>/,
173
+ /@Composable\s+fun/, // Android Kotlin
174
+ /struct\s+\w+View\s*:\s*View/, // SwiftUI
175
+ /class\s+\w+ViewController/, // iOS UIKit
176
+ /extends\s+(StatefulWidget|StatelessWidget)/, // Flutter
177
+ /<template>/, // Vue
178
+ /class\s+\w+View\s*\(\s*APIView\s*\)/, // Python Django REST
179
+ /@app\.route\s*\(/, // Python Flask views
180
+ /class\s+\w+Controller\s*<\s*ApplicationController/, // Ruby Rails
174
181
  ];
175
182
  for (const file of sourceFiles) {
176
183
  try {