@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.
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/package.json +2 -1
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- 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
|
|
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
|
|
295
|
-
|
|
296
|
-
|
|
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
|
}
|
package/core/init-command.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
124
|
-
|
|
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
|
|
129
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
151
|
-
if (
|
|
152
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
183
|
-
console.log(chalk.yellow(`
|
|
184
|
-
|
|
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
|
/**
|